2 from functools
import wraps
3 from xml
.parsers
.expat
import ExpatError
5 from django
.db
import transaction
, IntegrityError
6 from django
.shortcuts
import render
7 from django
.urls
import reverse
8 from django
.core
.exceptions
import ValidationError
9 from django
.http
import (
11 HttpResponseBadRequest
,
14 from django
.contrib
import messages
15 from mygpo
.web
.forms
import DeviceForm
, SyncForm
16 from mygpo
.web
.utils
import symbian_opml_changes
17 from django
.utils
.translation
import gettext
as _
18 from django
.contrib
.auth
.decorators
import login_required
19 from django
.views
.decorators
.vary
import vary_on_cookie
20 from django
.views
.decorators
.cache
import never_cache
, cache_control
22 from mygpo
.api
import simple
23 from mygpo
.decorators
import allowed_methods
24 from mygpo
.users
.models
import Client
, UserProxy
25 from mygpo
.subscriptions
.models
import Subscription
26 from mygpo
.users
.tasks
import sync_user
30 @cache_control(private
=True)
32 def overview(request
):
34 user
= UserProxy
.objects
.from_user(request
.user
)
35 device_groups
= user
.get_grouped_devices()
36 deleted_devices
= Client
.objects
.filter(user
=request
.user
, deleted
=True)
38 # create a "default" device
40 device_form
= DeviceForm(
41 {"name": device
.name
, "type": device
.type, "uid": device
.uid
}
48 "device_groups": list(device_groups
),
49 "deleted_devices": list(deleted_devices
),
50 "device_form": device_form
,
55 def device_decorator(f
):
58 @cache_control(private
=True)
60 def _decorator(request
, uid
, *args
, **kwargs
):
63 device
= Client
.objects
.get(user
=request
.user
, uid
=uid
)
65 except Client
.DoesNotExist
as e
:
66 return HttpResponseNotFound(str(e
))
68 return f(request
, device
, *args
, **kwargs
)
75 def show(request
, device
):
77 subscriptions
= list(device
.get_subscribed_podcasts().prefetch_related("slugs"))
78 synced_with
= device
.synced_with()
80 sync_targets
= list(device
.get_sync_targets())
81 sync_form
= SyncForm()
82 sync_form
.set_targets(sync_targets
, _("Synchronize with the following devices"))
89 "sync_form": sync_form
,
90 "subscriptions": subscriptions
,
91 "synced_with": synced_with
,
92 "has_sync_targets": len(sync_targets
) > 0,
99 @allowed_methods(["POST"])
101 device_form
= DeviceForm(request
.POST
)
103 if not device_form
.is_valid():
104 messages
.error(request
, _("Please fill out all fields."))
105 return HttpResponseRedirect(reverse("devices"))
109 device
.user
= request
.user
110 device
.id = uuid
.uuid1()
111 device
.name
= device_form
.cleaned_data
["name"]
112 device
.type = device_form
.cleaned_data
["type"]
113 device
.uid
= device_form
.cleaned_data
["uid"].replace(" ", "-")
116 messages
.success(request
, _("Device saved"))
118 except ValidationError
as e
:
119 messages
.error(request
, "; ".join(e
.messages
))
120 return HttpResponseRedirect(reverse("devices"))
122 except IntegrityError
:
124 request
, _("You can't use the same Device " "ID for two devices.")
126 return HttpResponseRedirect(reverse("devices"))
128 return HttpResponseRedirect(reverse("device-edit", args
=[device
.uid
]))
133 @allowed_methods(["POST"])
134 def update(request
, device
):
135 device_form
= DeviceForm(request
.POST
)
139 if device_form
.is_valid():
142 device
.name
= device_form
.cleaned_data
["name"]
143 device
.type = device_form
.cleaned_data
["type"]
144 device
.uid
= device_form
.cleaned_data
["uid"].replace(" ", "-")
147 messages
.success(request
, _("Device updated"))
148 uid
= device
.uid
# accept the new UID after rest has succeeded
150 except ValidationError
as e
:
151 messages
.error(request
, _(str(e
)))
153 except IntegrityError
:
155 request
, _("You can't use the same Device " "ID for two devices.")
158 return HttpResponseRedirect(reverse("device-edit", args
=[uid
]))
163 @allowed_methods(["GET"])
164 def edit(request
, device
):
166 device_form
= DeviceForm(
167 {"name": device
.name
, "type": device
.type, "uid": device
.uid
}
170 synced_with
= device
.synced_with()
172 sync_targets
= list(device
.get_sync_targets())
173 sync_form
= SyncForm()
174 sync_form
.set_targets(sync_targets
, _("Synchronize with the following devices"))
181 "device_form": device_form
,
182 "sync_form": sync_form
,
183 "synced_with": synced_with
,
184 "has_sync_targets": len(sync_targets
) > 0,
191 def upload_opml(request
, device
):
193 if not "opml" in request
.FILES
:
194 return HttpResponseRedirect(reverse("device-edit", args
=[device
.uid
]))
197 opml
= request
.FILES
["opml"].read().decode("utf-8")
198 subscriptions
= simple
.parse_subscription(opml
, "opml")
199 simple
.set_subscriptions(subscriptions
, request
.user
, device
.uid
, None)
201 except (ValueError, ExpatError
, UnicodeDecodeError) as ex
:
202 msg
= _("Could not upload subscriptions: {err}").format(err
=str(ex
))
203 messages
.error(request
, msg
)
204 return HttpResponseRedirect(reverse("device-edit", args
=[device
.uid
]))
206 return HttpResponseRedirect(reverse("device", args
=[device
.uid
]))
211 def opml(request
, device
):
212 response
= simple
.format_podcast_list(
213 simple
.get_subscriptions(request
.user
, device
.uid
),
215 request
.user
.username
,
217 response
["Content-Disposition"] = "attachment; filename=%s.opml" % device
.uid
223 def symbian_opml(request
, device
):
224 subscriptions
= simple
.get_subscriptions(request
.user
, device
.uid
)
225 subscriptions
= map(symbian_opml_changes
, subscriptions
)
227 response
= simple
.format_podcast_list(subscriptions
, "opml", request
.user
.username
)
228 response
["Content-Disposition"] = "attachment; filename=%s.opml" % device
.uid
234 @allowed_methods(["POST"])
236 def delete(request
, device
):
237 """Mars a client as deleted, but does not permanently delete it"""
239 # remoe the device from the sync group
242 # mark the subscriptions as deleted
243 Subscription
.objects
.filter(user
=request
.user
, client
=device
).update(deleted
=True)
245 # mark the client as deleted
246 device
.deleted
= True
249 return HttpResponseRedirect(reverse("devices"))
254 def delete_permanently(request
, device
):
256 return HttpResponseRedirect(reverse("devices"))
262 def undelete(request
, device
):
263 """Marks the client as not deleted anymore"""
265 # mark the subscriptions as not deleted anymore
266 Subscription
.objects
.filter(user
=request
.user
, client
=device
).update(deleted
=False)
268 # mark the client as not deleted anymore
269 device
.deleted
= False
272 return HttpResponseRedirect(reverse("device", args
=[device
.uid
]))
277 @allowed_methods(["POST"])
278 def sync(request
, device
):
280 form
= SyncForm(request
.POST
)
281 if not form
.is_valid():
282 return HttpResponseBadRequest("invalid")
285 target_uid
= form
.get_target()
286 sync_target
= request
.user
.client_set
.get(uid
=target_uid
)
287 device
.sync_with(sync_target
)
289 except Client
.DoesNotExist
as e
:
290 messages
.error(request
, str(e
))
292 sync_user
.delay(request
.user
.pk
)
294 return HttpResponseRedirect(reverse("device", args
=[device
.uid
]))
299 def resync(request
, device
):
300 """Manually triggers a re-sync of a client"""
301 sync_user
.delay(request
.user
.pk
)
302 messages
.success(request
, _("Your subscription will be updated in a moment."))
303 return HttpResponseRedirect(reverse("device", args
=[device
.uid
]))
308 @allowed_methods(["GET"])
309 def unsync(request
, device
):
313 except ValueError as e
:
314 messages
.error(request
, "Could not unsync the device: {err}".format(err
=str(e
)))
316 return HttpResponseRedirect(reverse("device", args
=[device
.uid
]))