avoid conflict when confirming pubsub sbuscription
[mygpo.git] / mygpo / web / views / podcast.py
blobf07d396e64f8028daab22e0697671ad49c32cac2
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
13 from mygpo.core.models import PodcastGroup, SubscriptionException
14 from mygpo.core.proxy import proxy_object
15 from mygpo.core.tasks import flattr_thing
16 from mygpo.utils import normalize_feed_url
17 from mygpo.users.settings import PUBLIC_SUB_PODCAST, FLATTR_TOKEN
18 from mygpo.publisher.utils import check_publisher_permission
19 from mygpo.users.models import HistoryEntry, DeviceDoesNotExist, SubscriptionAction
20 from mygpo.web.forms import SyncForm
21 from mygpo.decorators import allowed_methods, repeat_on_conflict
22 from mygpo.web.utils import get_podcast_link_target, get_page_list, \
23 check_restrictions
24 from mygpo.db.couchdb.episode import episodes_for_podcast
25 from mygpo.db.couchdb.podcast import podcast_for_slug, podcast_for_slug_id, \
26 podcast_for_oldid, podcast_for_url
27 from mygpo.db.couchdb.podcast_state import podcast_state_for_user_podcast, \
28 add_subscription_action, add_podcast_tags, remove_podcast_tags, \
29 set_podcast_privacy_settings
30 from mygpo.db.couchdb.episode_state import get_podcasts_episode_states, \
31 episode_listener_counts
32 from mygpo.db.couchdb.directory import tags_for_user, tags_for_podcast
34 import logging
35 logger = logging.getLogger(__name__)
38 MAX_TAGS_ON_PAGE=50
41 @vary_on_cookie
42 @cache_control(private=True)
43 @allowed_methods(['GET'])
44 def show_slug(request, slug):
45 podcast = podcast_for_slug(slug)
47 if slug != podcast.slug:
48 target = reverse('podcast_slug', args=[podcast.slug])
49 return HttpResponseRedirect(target)
51 return show(request, podcast.oldid)
54 @vary_on_cookie
55 @cache_control(private=True)
56 @allowed_methods(['GET'])
57 def show(request, podcast):
58 """ Shows a podcast detail page """
60 check_restrictions(podcast)
62 current_site = RequestSite(request)
63 num_episodes = 20
64 episodes = episode_list(podcast, request.user, limit=num_episodes)
65 user = request.user
67 max_listeners = max([e.listeners for e in episodes] + [0])
69 episode = None
71 if episodes:
72 episode = episodes[0]
73 episodes = episodes[1:]
75 if podcast.group:
76 group = PodcastGroup.get(podcast.group)
77 rel_podcasts = filter(lambda x: x != podcast, group.podcasts)
78 else:
79 rel_podcasts = []
81 tags, has_tagged = get_tags(podcast, user)
83 if user.is_authenticated():
84 state = podcast_state_for_user_podcast(user, podcast)
85 subscribed_devices = state.get_subscribed_device_ids()
86 subscribed_devices = user.get_devices(subscribed_devices)
88 subscribe_targets = podcast.subscribe_targets(user)
90 has_history = bool(state.actions)
91 is_public = state.settings.get('public_subscription', True)
92 can_flattr = request.user.get_wksetting(FLATTR_TOKEN) and podcast.flattr_url
94 else:
95 has_history = False
96 is_public = False
97 subscribed_devices = []
98 subscribe_targets = []
99 can_flattr = False
101 is_publisher = check_publisher_permission(user, podcast)
103 episodes_total = podcast.episode_count or 0
104 num_pages = episodes_total / num_episodes
105 page_list = get_page_list(1, num_pages, 1, 15)
107 return render(request, 'podcast.html', {
108 'tags': tags,
109 'has_tagged': has_tagged,
110 'url': current_site,
111 'has_history': has_history,
112 'podcast': podcast,
113 'is_public': is_public,
114 'devices': subscribed_devices,
115 'related_podcasts': rel_podcasts,
116 'can_subscribe': len(subscribe_targets) > 0,
117 'subscribe_targets': subscribe_targets,
118 'episode': episode,
119 'episodes': episodes,
120 'max_listeners': max_listeners,
121 'can_flattr': can_flattr,
122 'is_publisher': is_publisher,
123 'page_list': page_list,
124 'current_page': 1,
128 def get_tags(podcast, user):
129 tags = {}
130 for t in tags_for_podcast(podcast):
131 tag_str = t.lower()
132 tags[tag_str] = False
134 if not user.is_anonymous():
135 users_tags = tags_for_user(user, podcast.get_id())
136 for t in users_tags.get(podcast.get_id(), []):
137 tag_str = t.lower()
138 tags[tag_str] = True
140 tag_list = [{'tag': key, 'is_own': value} for key, value in tags.iteritems()]
141 tag_list.sort(key=lambda x: x['tag'])
143 if len(tag_list) > MAX_TAGS_ON_PAGE:
144 tag_list = filter(lambda x: x['is_own'], tag_list)
145 tag_list.append({'tag': '...', 'is_own': False})
147 has_own = any(t['is_own'] for t in tag_list)
149 return tag_list, has_own
152 def episode_list(podcast, user, offset=0, limit=None):
154 Returns a list of episodes, with their action-attribute set to the latest
155 action. The attribute is unsert if there is no episode-action for
156 the episode.
159 listeners = dict(episode_listener_counts(podcast))
160 episodes = episodes_for_podcast(podcast, descending=True, skip=offset, limit=limit)
162 if user.is_authenticated():
164 # prepare pre-populated data for HistoryEntry.fetch_data
165 podcasts_dict = dict( (p_id, podcast) for p_id in podcast.get_ids())
166 episodes_dict = dict( (episode._id, episode) for episode in episodes)
168 actions = get_podcasts_episode_states(podcast, user._id)
169 actions = map(HistoryEntry.from_action_dict, actions)
171 HistoryEntry.fetch_data(user, actions,
172 podcasts=podcasts_dict, episodes=episodes_dict)
174 episode_actions = dict( (action.episode_id, action) for action in actions)
175 else:
176 episode_actions = {}
178 annotate_episode = partial(_annotate_episode, listeners, episode_actions)
179 return map(annotate_episode, episodes)
183 @never_cache
184 @login_required
185 def history(request, podcast):
186 """ shows the subscription history of the user """
188 user = request.user
189 state = podcast_state_for_user_podcast(user, podcast)
190 history = list(state.actions)
192 def _set_objects(h):
193 dev = user.get_device(h.device)
194 return proxy_object(h, device=dev)
195 history = map(_set_objects, history)
197 return render(request, 'podcast-history.html', {
198 'history': history,
199 'podcast': podcast,
203 def all_episodes(request, podcast, page_size=20):
205 # Make sure page request is an int. If not, deliver first page.
206 try:
207 page = int(request.GET.get('page', '1'))
208 except ValueError:
209 page = 1
211 user = request.user
213 episodes = episode_list(podcast, user, (page-1) * page_size,
214 page_size)
215 episodes_total = podcast.episode_count or 0
216 num_pages = episodes_total / page_size
217 page_list = get_page_list(1, num_pages, page, 15)
219 max_listeners = max([e.listeners for e in episodes] + [0])
221 is_publisher = check_publisher_permission(user, podcast)
223 return render(request, 'episodes.html', {
224 'podcast': podcast,
225 'episodes': episodes,
226 'max_listeners': max_listeners,
227 'page_list': page_list,
228 'current_page': page,
229 'is_publisher': is_publisher,
234 def _annotate_episode(listeners, episode_actions, episode):
235 listener_count = listeners.pop(episode._id, None)
236 action = episode_actions.pop(episode._id, None)
237 return proxy_object(episode, listeners=listener_count, action=action)
241 @never_cache
242 @login_required
243 def add_tag(request, podcast):
244 podcast_state = podcast_state_for_user_podcast(request.user, podcast)
246 tag_str = request.GET.get('tag', '')
247 if not tag_str:
248 return HttpResponseBadRequest()
250 tags = tag_str.split(',')
251 add_podcast_tags(podcast_state, tags)
253 if request.GET.get('next', '') == 'mytags':
254 return HttpResponseRedirect('/tags/')
256 return HttpResponseRedirect(get_podcast_link_target(podcast))
259 @never_cache
260 @login_required
261 def remove_tag(request, podcast):
262 podcast_state = podcast_state_for_user_podcast(request.user, podcast)
264 tag_str = request.GET.get('tag', '')
265 if not tag_str:
266 return HttpResponseBadRequest()
268 remove_podcast_tags(podcast_state, tag_str)
270 if request.GET.get('next', '') == 'mytags':
271 return HttpResponseRedirect('/tags/')
273 return HttpResponseRedirect(get_podcast_link_target(podcast))
276 @never_cache
277 @login_required
278 @allowed_methods(['GET', 'POST'])
279 def subscribe(request, podcast):
281 if request.method == 'POST':
283 # multiple UIDs from the /podcast/<slug>/subscribe
284 device_uids = [k for (k,v) in request.POST.items() if k==v]
286 # single UID from /podcast/<slug>
287 if 'targets' in request.POST:
288 devices = request.POST.get('targets')
289 devices = devices.split(',')
290 device_uids.extend(devices)
292 for uid in device_uids:
293 try:
294 device = request.user.get_device_by_uid(uid)
295 podcast.subscribe(request.user, device)
297 except (SubscriptionException, DeviceDoesNotExist, ValueError) as e:
298 messages.error(request, str(e))
300 return HttpResponseRedirect(get_podcast_link_target(podcast))
302 targets = podcast.subscribe_targets(request.user)
304 return render(request, 'subscribe.html', {
305 'targets': targets,
306 'podcast': podcast,
310 @never_cache
311 @login_required
312 @allowed_methods(['POST'])
313 def subscribe_all(request, podcast):
314 """ subscribe all of the user's devices to the podcast """
315 user = request.user
317 devs = podcast.subscribe_targets(user)
318 # ungroup groups
319 devs = [dev[0] if isinstance(dev, list) else dev for dev in devs]
321 try:
322 podcast.subscribe(user, devs)
323 except (SubscriptionException, DeviceDoesNotExist, ValueError) as e:
324 messages.error(request, str(e))
326 return HttpResponseRedirect(get_podcast_link_target(podcast))
329 @never_cache
330 @login_required
331 def unsubscribe(request, podcast, device_uid):
333 return_to = request.GET.get('return_to', None)
335 if not return_to:
336 raise Http404('Wrong URL')
338 try:
339 device = request.user.get_device_by_uid(device_uid)
341 except DeviceDoesNotExist as e:
342 messages.error(request, str(e))
343 return HttpResponseRedirect(return_to)
345 try:
346 podcast.unsubscribe(request.user, device)
347 except SubscriptionException as e:
348 logger.exception('Web: %(username)s: could not unsubscribe from podcast %(podcast_url)s on device %(device_id)s' %
349 {'username': request.user.username, 'podcast_url': podcast.url, 'device_id': device.id})
351 return HttpResponseRedirect(return_to)
354 @never_cache
355 @login_required
356 @allowed_methods(['POST'])
357 def unsubscribe_all(request, podcast):
358 """ unsubscribe all of the user's devices from the podcast """
360 user = request.user
361 state = podcast_state_for_user_podcast(user, podcast)
363 dev_ids = state.get_subscribed_device_ids()
364 devs = user.get_devices(dev_ids)
365 # ungroup groups
366 devs = [dev[0] if isinstance(dev, list) else dev for dev in devs]
368 try:
369 podcast.unsubscribe(user, devs)
370 except (SubscriptionException, DeviceDoesNotExist, ValueError) as e:
371 messages.error(request, str(e))
373 return HttpResponseRedirect(get_podcast_link_target(podcast))
376 @never_cache
377 @login_required
378 def subscribe_url(request):
379 url = request.GET.get('url', None)
381 if not url:
382 raise Http404('http://my.gpodder.org/subscribe?url=http://www.example.com/podcast.xml')
384 url = normalize_feed_url(url)
386 if not url:
387 raise Http404('Please specify a valid url')
389 podcast = podcast_for_url(url, create=True)
391 return HttpResponseRedirect(get_podcast_link_target(podcast, 'subscribe'))
394 @never_cache
395 @allowed_methods(['POST'])
396 def set_public(request, podcast, public):
397 state = podcast_state_for_user_podcast(request.user, podcast)
398 set_podcast_privacy_settings(state, public)
399 return HttpResponseRedirect(get_podcast_link_target(podcast))
402 @never_cache
403 @login_required
404 def flattr_podcast(request, podcast):
405 """ Flattrs a podcast, records an event and redirects to the podcast """
407 user = request.user
408 site = RequestSite(request)
410 # do flattring via the tasks queue, but wait for the result
411 task = flattr_thing.delay(user, podcast.get_id(), site.domain,
412 request.is_secure(), 'Podcast')
413 success, msg = task.get()
415 if success:
416 action = SubscriptionAction()
417 action.action = 'flattr'
418 state = podcast_state_for_user_podcast(request.user, podcast)
419 add_subscription_action(state, action)
420 messages.success(request, _("Flattr\'d"))
422 else:
423 messages.error(request, msg)
425 return HttpResponseRedirect(get_podcast_link_target(podcast))
428 # To make all view accessible via either CouchDB-ID or Slugs
429 # a decorator queries the podcast and passes the Id on to the
430 # regular views
432 def slug_id_decorator(f):
433 @wraps(f)
434 def _decorator(request, slug_id, *args, **kwargs):
435 podcast = podcast_for_slug_id(slug_id)
437 if podcast is None:
438 raise Http404
440 # redirect when Id or a merged (non-cannonical) slug is used
441 if podcast.slug and slug_id != podcast.slug:
442 return HttpResponseRedirect(get_podcast_link_target(podcast))
444 return f(request, podcast, *args, **kwargs)
446 return _decorator
449 def oldid_decorator(f):
450 @wraps(f)
451 def _decorator(request, pid, *args, **kwargs):
452 try:
453 pid = int(pid)
454 except (TypeError, ValueError):
455 raise Http404
457 podcast = podcast_for_oldid(pid)
459 if not podcast:
460 raise Http404
462 # redirect to Id or slug URL
463 return HttpResponseRedirect(get_podcast_link_target(podcast))
465 return _decorator
468 show_slug_id = slug_id_decorator(show)
469 subscribe_slug_id = slug_id_decorator(subscribe)
470 subscribe_all_slug_id= slug_id_decorator(subscribe_all)
471 unsubscribe_slug_id = slug_id_decorator(unsubscribe)
472 unsubscribe_all_slug_id= slug_id_decorator(unsubscribe_all)
473 add_tag_slug_id = slug_id_decorator(add_tag)
474 remove_tag_slug_id = slug_id_decorator(remove_tag)
475 set_public_slug_id = slug_id_decorator(set_public)
476 all_episodes_slug_id= slug_id_decorator(all_episodes)
477 flattr_podcast_slug_id=slug_id_decorator(flattr_podcast)
478 history_podcast_slug_id= slug_id_decorator(history)
481 show_oldid = oldid_decorator(show)
482 subscribe_oldid = oldid_decorator(subscribe)
483 subscribe_all_oldid = oldid_decorator(subscribe_all)
484 unsubscribe_oldid = oldid_decorator(unsubscribe)
485 unsubscribe_all_oldid= oldid_decorator(unsubscribe_all)
486 add_tag_oldid = oldid_decorator(add_tag)
487 remove_tag_oldid = oldid_decorator(remove_tag)
488 set_public_oldid = oldid_decorator(set_public)
489 all_episodes_oldid = oldid_decorator(all_episodes)
490 flattr_podcast_oldid= oldid_decorator(flattr_podcast)
491 history_podcast_oldid= oldid_decorator(history)