[Models] move SubscriptionException to mygpo.users
[mygpo.git] / mygpo / web / views / podcast.py
blobcb35941691a817f314d0a5608dad921e2ff18732
1 from functools import wraps, partial
3 from django.core.urlresolvers import reverse
4 from django.http import HttpResponseBadRequest, HttpResponseRedirect, Http404
5 from django.shortcuts import render
6 from django.contrib.auth.decorators import login_required
7 from django.contrib.sites.models import RequestSite
8 from django.utils.translation import ugettext as _
9 from django.contrib import messages
10 from django.views.decorators.vary import vary_on_cookie
11 from django.views.decorators.cache import never_cache, cache_control
12 from django.shortcuts import get_object_or_404
14 from mygpo.podcasts.models import Podcast, PodcastGroup
15 from mygpo.users.models import SubscriptionException
16 from mygpo.core.proxy import proxy_object
17 from mygpo.core.tasks import flattr_thing
18 from mygpo.utils import normalize_feed_url
19 from mygpo.users.settings import PUBLIC_SUB_PODCAST, FLATTR_TOKEN
20 from mygpo.publisher.utils import check_publisher_permission
21 from mygpo.users.models import HistoryEntry, DeviceDoesNotExist, SubscriptionAction
22 from mygpo.web.forms import SyncForm
23 from mygpo.decorators import allowed_methods, repeat_on_conflict
24 from mygpo.web.utils import get_podcast_link_target, get_page_list, \
25 check_restrictions
26 from mygpo.db.couchdb.podcast_state import podcast_state_for_user_podcast, \
27 add_subscription_action, add_podcast_tags, remove_podcast_tags, \
28 set_podcast_privacy_settings, subscribe, unsubscribe
29 from mygpo.db.couchdb.episode_state import get_podcasts_episode_states, \
30 episode_listener_counts
31 from mygpo.db.couchdb.directory import tags_for_user, tags_for_podcast
33 import logging
34 logger = logging.getLogger(__name__)
37 MAX_TAGS_ON_PAGE=50
40 @vary_on_cookie
41 @cache_control(private=True)
42 @allowed_methods(['GET'])
43 def show(request, podcast):
44 """ Shows a podcast detail page """
46 podcast = check_restrictions(podcast)
48 current_site = RequestSite(request)
49 num_episodes = 20
50 episodes = episode_list(podcast, request.user, limit=num_episodes)
51 user = request.user
53 max_listeners = max([e.listeners for e in episodes] + [0])
55 episode = None
57 if episodes:
58 episode = episodes[0]
59 episodes = episodes[1:]
61 if podcast.group:
62 group = podcast.group
63 rel_podcasts = filter(lambda x: x != podcast, group.podcasts)
64 else:
65 rel_podcasts = []
67 tags, has_tagged = get_tags(podcast, user)
69 if user.is_authenticated():
70 state = podcast_state_for_user_podcast(user, podcast)
71 subscribed_devices = state.get_subscribed_device_ids()
72 subscribed_devices = user.get_devices(subscribed_devices)
74 subscribe_targets = podcast.subscribe_targets(user)
76 has_history = bool(state.actions)
77 is_public = state.settings.get('public_subscription', True)
78 can_flattr = request.user.get_wksetting(FLATTR_TOKEN) and podcast.flattr_url
80 else:
81 has_history = False
82 is_public = False
83 subscribed_devices = []
84 subscribe_targets = []
85 can_flattr = False
87 is_publisher = check_publisher_permission(user, podcast)
89 episodes_total = podcast.episode_count or 0
90 num_pages = episodes_total / num_episodes
91 page_list = get_page_list(1, num_pages, 1, 15)
93 return render(request, 'podcast.html', {
94 'tags': tags,
95 'has_tagged': has_tagged,
96 'url': current_site,
97 'has_history': has_history,
98 'podcast': podcast,
99 'is_public': is_public,
100 'devices': subscribed_devices,
101 'related_podcasts': rel_podcasts,
102 'can_subscribe': len(subscribe_targets) > 0,
103 'subscribe_targets': subscribe_targets,
104 'episode': episode,
105 'episodes': episodes,
106 'max_listeners': max_listeners,
107 'can_flattr': can_flattr,
108 'is_publisher': is_publisher,
109 'page_list': page_list,
110 'current_page': 1,
114 def get_tags(podcast, user):
115 tags = {}
116 for t in tags_for_podcast(podcast):
117 tag_str = t.lower()
118 tags[tag_str] = False
120 if not user.is_anonymous():
121 users_tags = tags_for_user(user, podcast.get_id())
122 for t in users_tags.get(podcast.get_id(), []):
123 tag_str = t.lower()
124 tags[tag_str] = True
126 tag_list = [{'tag': key, 'is_own': value} for key, value in tags.iteritems()]
127 tag_list.sort(key=lambda x: x['tag'])
129 if len(tag_list) > MAX_TAGS_ON_PAGE:
130 tag_list = filter(lambda x: x['is_own'], tag_list)
131 tag_list.append({'tag': '...', 'is_own': False})
133 has_own = any(t['is_own'] for t in tag_list)
135 return tag_list, has_own
138 def episode_list(podcast, user, offset=0, limit=None):
140 Returns a list of episodes, with their action-attribute set to the latest
141 action. The attribute is unsert if there is no episode-action for
142 the episode.
145 listeners = dict(episode_listener_counts(podcast))
146 episodes = podcast.episode_set.all()
147 episodes = episodes.prefetch_related('slugs')[offset:limit]
149 if user.is_authenticated():
151 # prepare pre-populated data for HistoryEntry.fetch_data
152 podcasts_dict = {podcast.get_id(): podcast}
153 episodes_dict = dict( (episode.id, episode) for episode in episodes)
155 actions = get_podcasts_episode_states(podcast, user._id)
156 actions = map(HistoryEntry.from_action_dict, actions)
158 HistoryEntry.fetch_data(user, actions,
159 podcasts=podcasts_dict, episodes=episodes_dict)
161 episode_actions = dict( (action.episode_id, action) for action in actions)
162 else:
163 episode_actions = {}
165 annotate_episode = partial(_annotate_episode, listeners, episode_actions)
166 return map(annotate_episode, episodes)
170 @never_cache
171 @login_required
172 def history(request, podcast):
173 """ shows the subscription history of the user """
175 user = request.user
176 state = podcast_state_for_user_podcast(user, podcast)
177 history = list(state.actions)
179 def _set_objects(h):
180 dev = user.get_device(h.device)
181 return proxy_object(h, device=dev)
182 history = map(_set_objects, history)
184 return render(request, 'podcast-history.html', {
185 'history': history,
186 'podcast': podcast,
190 def all_episodes(request, podcast, page_size=20):
192 # Make sure page request is an int. If not, deliver first page.
193 try:
194 page = int(request.GET.get('page', '1'))
195 except ValueError:
196 page = 1
198 user = request.user
200 episodes = episode_list(podcast, user, (page-1) * page_size,
201 page_size)
202 episodes_total = podcast.episode_count or 0
203 num_pages = episodes_total / page_size
204 page_list = get_page_list(1, num_pages, page, 15)
206 max_listeners = max([e.listeners for e in episodes] + [0])
208 is_publisher = check_publisher_permission(user, podcast)
210 return render(request, 'episodes.html', {
211 'podcast': podcast,
212 'episodes': episodes,
213 'max_listeners': max_listeners,
214 'page_list': page_list,
215 'current_page': page,
216 'is_publisher': is_publisher,
221 def _annotate_episode(listeners, episode_actions, episode):
222 episode.listener_count = listeners.pop(episode.get_id(), None)
223 episode.action = episode_actions.pop(episode.get_id(), None)
224 return episode
228 @never_cache
229 @login_required
230 def add_tag(request, podcast):
231 podcast_state = podcast_state_for_user_podcast(request.user, podcast)
233 tag_str = request.GET.get('tag', '')
234 if not tag_str:
235 return HttpResponseBadRequest()
237 tags = tag_str.split(',')
238 add_podcast_tags(podcast_state, tags)
240 if request.GET.get('next', '') == 'mytags':
241 return HttpResponseRedirect('/tags/')
243 return HttpResponseRedirect(get_podcast_link_target(podcast))
246 @never_cache
247 @login_required
248 def remove_tag(request, podcast):
249 podcast_state = podcast_state_for_user_podcast(request.user, podcast)
251 tag_str = request.GET.get('tag', '')
252 if not tag_str:
253 return HttpResponseBadRequest()
255 remove_podcast_tags(podcast_state, tag_str)
257 if request.GET.get('next', '') == 'mytags':
258 return HttpResponseRedirect('/tags/')
260 return HttpResponseRedirect(get_podcast_link_target(podcast))
263 @never_cache
264 @login_required
265 @allowed_methods(['GET', 'POST'])
266 def subscribe(request, podcast):
268 if request.method == 'POST':
270 # multiple UIDs from the /podcast/<slug>/subscribe
271 device_uids = [k for (k,v) in request.POST.items() if k==v]
273 # single UID from /podcast/<slug>
274 if 'targets' in request.POST:
275 devices = request.POST.get('targets')
276 devices = devices.split(',')
277 device_uids.extend(devices)
279 for uid in device_uids:
280 try:
281 device = request.user.get_device_by_uid(uid)
282 subscribe(podcast, request.user, device)
284 except (SubscriptionException, DeviceDoesNotExist, ValueError) as e:
285 messages.error(request, str(e))
287 return HttpResponseRedirect(get_podcast_link_target(podcast))
289 targets = podcast.subscribe_targets(request.user)
291 return render(request, 'subscribe.html', {
292 'targets': targets,
293 'podcast': podcast,
297 @never_cache
298 @login_required
299 @allowed_methods(['POST'])
300 def subscribe_all(request, podcast):
301 """ subscribe all of the user's devices to the podcast """
302 user = request.user
304 devs = podcast.subscribe_targets(user)
305 # ungroup groups
306 devs = [dev[0] if isinstance(dev, list) else dev for dev in devs]
308 try:
309 subscribe(podcast, user, devs)
310 except (SubscriptionException, DeviceDoesNotExist, ValueError) as e:
311 messages.error(request, str(e))
313 return HttpResponseRedirect(get_podcast_link_target(podcast))
316 @never_cache
317 @login_required
318 def unsubscribe(request, podcast, device_uid):
320 return_to = request.GET.get('return_to', None)
322 if not return_to:
323 raise Http404('Wrong URL')
325 try:
326 device = request.user.get_device_by_uid(device_uid)
328 except DeviceDoesNotExist as e:
329 messages.error(request, str(e))
330 return HttpResponseRedirect(return_to)
332 try:
333 unsubscribe(podcast, request.user, device)
334 except SubscriptionException as e:
335 logger.exception('Web: %(username)s: could not unsubscribe from podcast %(podcast_url)s on device %(device_id)s' %
336 {'username': request.user.username, 'podcast_url': podcast.url, 'device_id': device.id})
338 return HttpResponseRedirect(return_to)
341 @never_cache
342 @login_required
343 @allowed_methods(['POST'])
344 def unsubscribe_all(request, podcast):
345 """ unsubscribe all of the user's devices from the podcast """
347 user = request.user
348 state = podcast_state_for_user_podcast(user, podcast)
350 dev_ids = state.get_subscribed_device_ids()
351 devs = user.get_devices(dev_ids)
352 # ungroup groups
353 devs = [dev[0] if isinstance(dev, list) else dev for dev in devs]
355 try:
356 unsubscribe(podcast, user, devs)
357 except (SubscriptionException, DeviceDoesNotExist, ValueError) as e:
358 messages.error(request, str(e))
360 return HttpResponseRedirect(get_podcast_link_target(podcast))
363 @never_cache
364 @login_required
365 def subscribe_url(request):
366 url = request.GET.get('url', None)
368 if not url:
369 raise Http404('http://my.gpodder.org/subscribe?url=http://www.example.com/podcast.xml')
371 url = normalize_feed_url(url)
373 if not url:
374 raise Http404('Please specify a valid url')
376 podcast = Podcasts.objects.get_or_create_for_url(url)
378 return HttpResponseRedirect(get_podcast_link_target(podcast, 'subscribe'))
381 @never_cache
382 @allowed_methods(['POST'])
383 def set_public(request, podcast, public):
384 state = podcast_state_for_user_podcast(request.user, podcast)
385 set_podcast_privacy_settings(state, public)
386 return HttpResponseRedirect(get_podcast_link_target(podcast))
389 @never_cache
390 @login_required
391 def flattr_podcast(request, podcast):
392 """ Flattrs a podcast, records an event and redirects to the podcast """
394 user = request.user
395 site = RequestSite(request)
397 # do flattring via the tasks queue, but wait for the result
398 task = flattr_thing.delay(user, podcast.get_id(), site.domain,
399 request.is_secure(), 'Podcast')
400 success, msg = task.get()
402 if success:
403 action = SubscriptionAction()
404 action.action = 'flattr'
405 state = podcast_state_for_user_podcast(request.user, podcast)
406 add_subscription_action(state, action)
407 messages.success(request, _("Flattr\'d"))
409 else:
410 messages.error(request, msg)
412 return HttpResponseRedirect(get_podcast_link_target(podcast))
415 # To make all view accessible via either IDs or Slugs
416 # a decorator queries the podcast and passes the Id on to the
417 # regular views
419 def slug_decorator(f):
420 @wraps(f)
421 def _decorator(request, slug, *args, **kwargs):
423 podcast = Podcast.objects.filter(slugs__slug=slug)
424 podcast = podcast.prefetch_related('slugs', 'urls').get()
426 # redirect when a non-cannonical slug is used
427 if slug != podcast.slug:
428 return HttpResponseRedirect(get_podcast_link_target(podcast))
430 return f(request, podcast, *args, **kwargs)
432 return _decorator
435 def id_decorator(f):
436 @wraps(f)
437 def _decorator(request, podcast_id, *args, **kwargs):
439 try:
440 podcast = Podcast.objects.get(id=podcast_id)
441 return f(request, podcast, *args, **kwargs)
442 # TODO: redirect to Slug URL?
444 except Podcast.DoesNotExist:
445 podcast = get_object_or_404(Podcast, merged_uuids__uuid=podcast_id)
446 return HttpResponseRedirect(get_podcast_link_target(podcast))
448 return _decorator
451 show_slug = slug_decorator(show)
452 subscribe_slug = slug_decorator(subscribe)
453 subscribe_all_slug = slug_decorator(subscribe_all)
454 unsubscribe_slug = slug_decorator(unsubscribe)
455 unsubscribe_all_slug = slug_decorator(unsubscribe_all)
456 add_tag_slug = slug_decorator(add_tag)
457 remove_tag_slug = slug_decorator(remove_tag)
458 set_public_slug = slug_decorator(set_public)
459 all_episodes_slug = slug_decorator(all_episodes)
460 flattr_podcast_slug = slug_decorator(flattr_podcast)
461 history_podcast_slug = slug_decorator(history)
464 show_id = id_decorator(show)
465 subscribe_id = id_decorator(subscribe)
466 subscribe_all_id = id_decorator(subscribe_all)
467 unsubscribe_id = id_decorator(unsubscribe)
468 unsubscribe_all_id = id_decorator(unsubscribe_all)
469 add_tag_id = id_decorator(add_tag)
470 remove_tag_id = id_decorator(remove_tag)
471 set_public_id = id_decorator(set_public)
472 all_episodes_id = id_decorator(all_episodes)
473 flattr_podcast_id = id_decorator(flattr_podcast)
474 history_podcast_id = id_decorator(history)