[Subscriptions] fix OPML upload
[mygpo.git] / mygpo / web / views / device.py
blobdc3e4500cf04513bf4e2f88c7f72edd9e1ecd339
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 opml = request.FILES['opml'].read().decode('utf-8')
207 try:
208 subscriptions = simple.parse_subscription(opml, 'opml')
209 simple.set_subscriptions(subscriptions, request.user, device.uid, None)
211 except ExpatError as ee:
212 msg = _('Could not upload subscriptions: {err}').format(err=str(ee))
213 messages.error(request, msg)
214 return HttpResponseRedirect(reverse('device-edit', args=[device.uid]))
216 return HttpResponseRedirect(reverse('device', args=[device.uid]))
219 @device_decorator
220 @login_required
221 def opml(request, device):
222 response = simple.format_podcast_list(simple.get_subscriptions(request.user, device.uid), 'opml', request.user.username)
223 response['Content-Disposition'] = 'attachment; filename=%s.opml' % device.uid
224 return response
227 @device_decorator
228 @login_required
229 def symbian_opml(request, device):
230 subscriptions = simple.get_subscriptions(request.user, device.uid)
231 subscriptions = map(symbian_opml_changes, subscriptions)
233 response = simple.format_podcast_list(subscriptions, 'opml', request.user.username)
234 response['Content-Disposition'] = 'attachment; filename=%s.opml' % device.uid
235 return response
238 @device_decorator
239 @login_required
240 @allowed_methods(['POST'])
241 @transaction.atomic
242 def delete(request, device):
243 """ Mars a client as deleted, but does not permanently delete it """
245 # remoe the device from the sync group
246 device.stop_sync()
248 # mark the subscriptions as deleted
249 Subscription.objects.filter(user=request.user, client=device)\
250 .update(deleted=True)
252 # mark the client as deleted
253 device.deleted = True
254 device.save()
256 return HttpResponseRedirect(reverse('devices'))
259 @login_required
260 @device_decorator
261 def delete_permanently(request, device):
262 device.delete()
263 return HttpResponseRedirect(reverse('devices'))
265 @device_decorator
266 @login_required
267 @transaction.atomic
268 def undelete(request, device):
269 """ Marks the client as not deleted anymore """
271 # mark the subscriptions as not deleted anymore
272 Subscription.objects.filter(user=request.user, client=device)\
273 .update(deleted=False)
275 # mark the client as not deleted anymore
276 device.deleted = False
277 device.save()
279 return HttpResponseRedirect(reverse('device', args=[device.uid]))
282 @device_decorator
283 @login_required
284 @allowed_methods(['POST'])
285 def sync(request, device):
287 form = SyncForm(request.POST)
288 if not form.is_valid():
289 return HttpResponseBadRequest('invalid')
291 try:
292 target_uid = form.get_target()
293 sync_target = request.user.client_set.get(uid=target_uid)
294 device.sync_with(sync_target)
296 except Client.DoesNotExist as e:
297 messages.error(request, str(e))
299 sync_user.delay(request.user)
301 return HttpResponseRedirect(reverse('device', args=[device.uid]))
304 @device_decorator
305 @login_required
306 def resync(request, device):
307 """ Manually triggers a re-sync of a client """
308 sync_user.delay(request.user)
309 messages.success(request,
310 _('Your subscription will be updated in a moment.'))
311 return HttpResponseRedirect(reverse('device', args=[device.uid]))
314 @device_decorator
315 @login_required
316 @allowed_methods(['GET'])
317 def unsync(request, device):
318 try:
319 device.stop_sync()
321 except ValueError as e:
322 messages.error(request, 'Could not unsync the device: {err}'.format(
323 err=str(e)))
325 return HttpResponseRedirect(reverse('device', args=[device.uid]))