[Clients] handle Unicode decode error when uploading OPML
[mygpo.git] / mygpo / web / views / device.py
blobed1184dd9b6f499cf91acb009cd74510d04fb288
2 # This file is part of my.gpodder.org.
4 # my.gpodder.org is free software: you can redistribute it and/or modify it
5 # under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or (at your
7 # option) any later version.
9 # my.gpodder.org is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
12 # License for more details.
14 # You should have received a copy of the GNU Affero General Public License
15 # along with my.gpodder.org. If not, see <http://www.gnu.org/licenses/>.
18 import uuid
19 from functools import wraps
20 from xml.parsers.expat import ExpatError
22 from django.db import transaction, IntegrityError
23 from django.shortcuts import render
24 from django.core.urlresolvers import reverse
25 from django.core.exceptions import ValidationError
26 from django.http import HttpResponseRedirect, HttpResponseBadRequest, \
27 HttpResponseNotFound
28 from django.contrib import messages
29 from mygpo.web.forms import DeviceForm, SyncForm
30 from mygpo.web.utils import symbian_opml_changes
31 from django.utils.translation import ugettext as _
32 from django.contrib.auth.decorators import login_required
33 from django.views.decorators.vary import vary_on_cookie
34 from django.views.decorators.cache import never_cache, cache_control
36 from mygpo.api import simple
37 from mygpo.decorators import allowed_methods
38 from mygpo.users.models import Client, UserProxy
39 from mygpo.subscriptions.models import Subscription
40 from mygpo.users.tasks import sync_user
43 @vary_on_cookie
44 @cache_control(private=True)
45 @login_required
46 def overview(request):
48 user = UserProxy.objects.from_user(request.user)
49 device_groups = user.get_grouped_devices()
50 deleted_devices = Client.objects.filter(user=request.user, deleted=True)
52 # create a "default" device
53 device = Client()
54 device_form = DeviceForm({
55 'name': device.name,
56 'type': device.type,
57 'uid': device.uid
60 return render(request, 'devicelist.html', {
61 'device_groups': list(device_groups),
62 'deleted_devices': list(deleted_devices),
63 'device_form': device_form,
68 def device_decorator(f):
69 @login_required
70 @vary_on_cookie
71 @cache_control(private=True)
72 @wraps(f)
73 def _decorator(request, uid, *args, **kwargs):
75 try:
76 device = Client.objects.get(user=request.user, uid=uid)
78 except Client.DoesNotExist as e:
79 return HttpResponseNotFound(str(e))
81 return f(request, device, *args, **kwargs)
83 return _decorator
87 @login_required
88 @device_decorator
89 def show(request, device):
91 subscriptions = list(device.get_subscribed_podcasts())
92 synced_with = device.synced_with()
94 sync_targets = list(device.get_sync_targets())
95 sync_form = SyncForm()
96 sync_form.set_targets(sync_targets,
97 _('Synchronize with the following devices'))
99 return render(request, 'device.html', {
100 'device': device,
101 'sync_form': sync_form,
102 'subscriptions': subscriptions,
103 'synced_with': synced_with,
104 'has_sync_targets': len(sync_targets) > 0,
108 @login_required
109 @never_cache
110 @allowed_methods(['POST'])
111 def create(request):
112 device_form = DeviceForm(request.POST)
114 if not device_form.is_valid():
115 messages.error(request, _('Please fill out all fields.'))
116 return HttpResponseRedirect(reverse('devices'))
118 try:
119 device = Client()
120 device.user = request.user
121 device.id = uuid.uuid1()
122 device.name = device_form.cleaned_data['name']
123 device.type = device_form.cleaned_data['type']
124 device.uid = device_form.cleaned_data['uid'].replace(' ', '-')
125 device.full_clean()
126 device.save()
127 messages.success(request, _('Device saved'))
129 except ValidationError as e:
130 messages.error(request, '; '.join(e.messages))
131 return HttpResponseRedirect(reverse('devices'))
133 except IntegrityError:
134 messages.error(request, _("You can't use the same Device "
135 "ID for two devices."))
136 return HttpResponseRedirect(reverse('devices'))
138 return HttpResponseRedirect(reverse('device-edit', args=[device.uid]))
142 @device_decorator
143 @login_required
144 @allowed_methods(['POST'])
145 def update(request, device):
146 device_form = DeviceForm(request.POST)
148 uid = device.uid
150 if device_form.is_valid():
152 try:
153 device.name = device_form.cleaned_data['name']
154 device.type = device_form.cleaned_data['type']
155 device.uid = device_form.cleaned_data['uid'].replace(' ', '-')
156 device.full_clean()
157 device.save()
158 messages.success(request, _('Device updated'))
159 uid = device.uid # accept the new UID after rest has succeeded
161 except ValidationError as e:
162 messages.error(request, _(str(e)))
164 except IntegrityError:
165 messages.error(request, _("You can't use the same Device "
166 "ID for two devices."))
168 return HttpResponseRedirect(reverse('device-edit', args=[uid]))
171 @device_decorator
172 @login_required
173 @allowed_methods(['GET'])
174 def edit(request, device):
176 device_form = DeviceForm({
177 'name': device.name,
178 'type': device.type,
179 'uid': device.uid
182 synced_with = device.synced_with()
184 sync_targets = list(device.get_sync_targets())
185 sync_form = SyncForm()
186 sync_form.set_targets(sync_targets,
187 _('Synchronize with the following devices'))
189 return render(request, 'device-edit.html', {
190 'device': device,
191 'device_form': device_form,
192 'sync_form': sync_form,
193 'synced_with': synced_with,
194 'has_sync_targets': len(sync_targets) > 0,
198 @device_decorator
199 @login_required
200 def upload_opml(request, device):
202 if not 'opml' in request.FILES:
203 return HttpResponseRedirect(reverse('device-edit', args=[device.uid]))
205 try:
206 opml = request.FILES['opml'].read().decode('utf-8')
207 subscriptions = simple.parse_subscription(opml, 'opml')
208 simple.set_subscriptions(subscriptions, request.user, device.uid, None)
210 except (ExpatError, UnicodeDecodeError) as ex:
211 msg = _('Could not upload subscriptions: {err}').format(err=str(ex))
212 messages.error(request, msg)
213 return HttpResponseRedirect(reverse('device-edit', args=[device.uid]))
215 return HttpResponseRedirect(reverse('device', args=[device.uid]))
218 @device_decorator
219 @login_required
220 def opml(request, device):
221 response = simple.format_podcast_list(simple.get_subscriptions(request.user, device.uid), 'opml', request.user.username)
222 response['Content-Disposition'] = 'attachment; filename=%s.opml' % device.uid
223 return response
226 @device_decorator
227 @login_required
228 def symbian_opml(request, device):
229 subscriptions = simple.get_subscriptions(request.user, device.uid)
230 subscriptions = map(symbian_opml_changes, subscriptions)
232 response = simple.format_podcast_list(subscriptions, 'opml', request.user.username)
233 response['Content-Disposition'] = 'attachment; filename=%s.opml' % device.uid
234 return response
237 @device_decorator
238 @login_required
239 @allowed_methods(['POST'])
240 @transaction.atomic
241 def delete(request, device):
242 """ Mars a client as deleted, but does not permanently delete it """
244 # remoe the device from the sync group
245 device.stop_sync()
247 # mark the subscriptions as deleted
248 Subscription.objects.filter(user=request.user, client=device)\
249 .update(deleted=True)
251 # mark the client as deleted
252 device.deleted = True
253 device.save()
255 return HttpResponseRedirect(reverse('devices'))
258 @login_required
259 @device_decorator
260 def delete_permanently(request, device):
261 device.delete()
262 return HttpResponseRedirect(reverse('devices'))
264 @device_decorator
265 @login_required
266 @transaction.atomic
267 def undelete(request, device):
268 """ Marks the client as not deleted anymore """
270 # mark the subscriptions as not deleted anymore
271 Subscription.objects.filter(user=request.user, client=device)\
272 .update(deleted=False)
274 # mark the client as not deleted anymore
275 device.deleted = False
276 device.save()
278 return HttpResponseRedirect(reverse('device', args=[device.uid]))
281 @device_decorator
282 @login_required
283 @allowed_methods(['POST'])
284 def sync(request, device):
286 form = SyncForm(request.POST)
287 if not form.is_valid():
288 return HttpResponseBadRequest('invalid')
290 try:
291 target_uid = form.get_target()
292 sync_target = request.user.client_set.get(uid=target_uid)
293 device.sync_with(sync_target)
295 except Client.DoesNotExist as e:
296 messages.error(request, str(e))
298 sync_user.delay(request.user)
300 return HttpResponseRedirect(reverse('device', args=[device.uid]))
303 @device_decorator
304 @login_required
305 def resync(request, device):
306 """ Manually triggers a re-sync of a client """
307 sync_user.delay(request.user)
308 messages.success(request,
309 _('Your subscription will be updated in a moment.'))
310 return HttpResponseRedirect(reverse('device', args=[device.uid]))
313 @device_decorator
314 @login_required
315 @allowed_methods(['GET'])
316 def unsync(request, device):
317 try:
318 device.stop_sync()
320 except ValueError as e:
321 messages.error(request, 'Could not unsync the device: {err}'.format(
322 err=str(e)))
324 return HttpResponseRedirect(reverse('device', args=[device.uid]))