[Web] Fix podcast list
[mygpo.git] / mygpo / web / views / podcast.py
bloba73108a38ad048fa12ae688ef488128739e74b48
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
15 from mygpo.core.models import PodcastGroup, 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.episode import episodes_for_podcast
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, subscribe, unsubscribe
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(request, podcast):
45 """ Shows a podcast detail page """
47 podcast = check_restrictions(podcast)
49 current_site = RequestSite(request)
50 num_episodes = 20
51 episodes = episode_list(podcast, request.user, limit=num_episodes)
52 user = request.user
54 max_listeners = max([e.listeners for e in episodes] + [0])
56 episode = None
58 if episodes:
59 episode = episodes[0]
60 episodes = episodes[1:]
62 if podcast.group:
63 group = PodcastGroup.get(podcast.group)
64 rel_podcasts = filter(lambda x: x != podcast, group.podcasts)
65 else:
66 rel_podcasts = []
68 tags, has_tagged = get_tags(podcast, user)
70 if user.is_authenticated():
71 state = podcast_state_for_user_podcast(user, podcast)
72 subscribed_devices = state.get_subscribed_device_ids()
73 subscribed_devices = user.get_devices(subscribed_devices)
75 subscribe_targets = podcast.subscribe_targets(user)
77 has_history = bool(state.actions)
78 is_public = state.settings.get('public_subscription', True)
79 can_flattr = request.user.get_wksetting(FLATTR_TOKEN) and podcast.flattr_url
81 else:
82 has_history = False
83 is_public = False
84 subscribed_devices = []
85 subscribe_targets = []
86 can_flattr = False
88 is_publisher = check_publisher_permission(user, podcast)
90 episodes_total = podcast.episode_count or 0
91 num_pages = episodes_total / num_episodes
92 page_list = get_page_list(1, num_pages, 1, 15)
94 return render(request, 'podcast.html', {
95 'tags': tags,
96 'has_tagged': has_tagged,
97 'url': current_site,
98 'has_history': has_history,
99 'podcast': podcast,
100 'is_public': is_public,
101 'devices': subscribed_devices,
102 'related_podcasts': rel_podcasts,
103 'can_subscribe': len(subscribe_targets) > 0,
104 'subscribe_targets': subscribe_targets,
105 'episode': episode,
106 'episodes': episodes,
107 'max_listeners': max_listeners,
108 'can_flattr': can_flattr,
109 'is_publisher': is_publisher,
110 'page_list': page_list,
111 'current_page': 1,
115 def get_tags(podcast, user):
116 tags = {}
117 for t in tags_for_podcast(podcast):
118 tag_str = t.lower()
119 tags[tag_str] = False
121 if not user.is_anonymous():
122 users_tags = tags_for_user(user, podcast.get_id())
123 for t in users_tags.get(podcast.get_id(), []):
124 tag_str = t.lower()
125 tags[tag_str] = True
127 tag_list = [{'tag': key, 'is_own': value} for key, value in tags.iteritems()]
128 tag_list.sort(key=lambda x: x['tag'])
130 if len(tag_list) > MAX_TAGS_ON_PAGE:
131 tag_list = filter(lambda x: x['is_own'], tag_list)
132 tag_list.append({'tag': '...', 'is_own': False})
134 has_own = any(t['is_own'] for t in tag_list)
136 return tag_list, has_own
139 def episode_list(podcast, user, offset=0, limit=None):
141 Returns a list of episodes, with their action-attribute set to the latest
142 action. The attribute is unsert if there is no episode-action for
143 the episode.
146 listeners = dict(episode_listener_counts(podcast))
147 episodes = podcast.episode_set.all()
148 episodes = episodes.prefetch_related('slugs')[offset:limit]
150 if user.is_authenticated():
152 # prepare pre-populated data for HistoryEntry.fetch_data
153 podcasts_dict = {podcast.get_id(): podcast}
154 episodes_dict = dict( (episode.id, episode) for episode in episodes)
156 actions = get_podcasts_episode_states(podcast, user._id)
157 actions = map(HistoryEntry.from_action_dict, actions)
159 HistoryEntry.fetch_data(user, actions,
160 podcasts=podcasts_dict, episodes=episodes_dict)
162 episode_actions = dict( (action.episode_id, action) for action in actions)
163 else:
164 episode_actions = {}
166 annotate_episode = partial(_annotate_episode, listeners, episode_actions)
167 return map(annotate_episode, episodes)
171 @never_cache
172 @login_required
173 def history(request, podcast):
174 """ shows the subscription history of the user """
176 user = request.user
177 state = podcast_state_for_user_podcast(user, podcast)
178 history = list(state.actions)
180 def _set_objects(h):
181 dev = user.get_device(h.device)
182 return proxy_object(h, device=dev)
183 history = map(_set_objects, history)
185 return render(request, 'podcast-history.html', {
186 'history': history,
187 'podcast': podcast,
191 def all_episodes(request, podcast, page_size=20):
193 # Make sure page request is an int. If not, deliver first page.
194 try:
195 page = int(request.GET.get('page', '1'))
196 except ValueError:
197 page = 1
199 user = request.user
201 episodes = episode_list(podcast, user, (page-1) * page_size,
202 page_size)
203 episodes_total = podcast.episode_count or 0
204 num_pages = episodes_total / page_size
205 page_list = get_page_list(1, num_pages, page, 15)
207 max_listeners = max([e.listeners for e in episodes] + [0])
209 is_publisher = check_publisher_permission(user, podcast)
211 return render(request, 'episodes.html', {
212 'podcast': podcast,
213 'episodes': episodes,
214 'max_listeners': max_listeners,
215 'page_list': page_list,
216 'current_page': page,
217 'is_publisher': is_publisher,
222 def _annotate_episode(listeners, episode_actions, episode):
223 episode.listener_count = listeners.pop(episode.get_id(), None)
224 episode.action = episode_actions.pop(episode.get_id(), None)
225 return episode
229 @never_cache
230 @login_required
231 def add_tag(request, podcast):
232 podcast_state = podcast_state_for_user_podcast(request.user, podcast)
234 tag_str = request.GET.get('tag', '')
235 if not tag_str:
236 return HttpResponseBadRequest()
238 tags = tag_str.split(',')
239 add_podcast_tags(podcast_state, tags)
241 if request.GET.get('next', '') == 'mytags':
242 return HttpResponseRedirect('/tags/')
244 return HttpResponseRedirect(get_podcast_link_target(podcast))
247 @never_cache
248 @login_required
249 def remove_tag(request, podcast):
250 podcast_state = podcast_state_for_user_podcast(request.user, podcast)
252 tag_str = request.GET.get('tag', '')
253 if not tag_str:
254 return HttpResponseBadRequest()
256 remove_podcast_tags(podcast_state, tag_str)
258 if request.GET.get('next', '') == 'mytags':
259 return HttpResponseRedirect('/tags/')
261 return HttpResponseRedirect(get_podcast_link_target(podcast))
264 @never_cache
265 @login_required
266 @allowed_methods(['GET', 'POST'])
267 def subscribe(request, podcast):
269 if request.method == 'POST':
271 # multiple UIDs from the /podcast/<slug>/subscribe
272 device_uids = [k for (k,v) in request.POST.items() if k==v]
274 # single UID from /podcast/<slug>
275 if 'targets' in request.POST:
276 devices = request.POST.get('targets')
277 devices = devices.split(',')
278 device_uids.extend(devices)
280 for uid in device_uids:
281 try:
282 device = request.user.get_device_by_uid(uid)
283 subscribe(podcast, request.user, device)
285 except (SubscriptionException, DeviceDoesNotExist, ValueError) as e:
286 messages.error(request, str(e))
288 return HttpResponseRedirect(get_podcast_link_target(podcast))
290 targets = podcast.subscribe_targets(request.user)
292 return render(request, 'subscribe.html', {
293 'targets': targets,
294 'podcast': podcast,
298 @never_cache
299 @login_required
300 @allowed_methods(['POST'])
301 def subscribe_all(request, podcast):
302 """ subscribe all of the user's devices to the podcast """
303 user = request.user
305 devs = podcast.subscribe_targets(user)
306 # ungroup groups
307 devs = [dev[0] if isinstance(dev, list) else dev for dev in devs]
309 try:
310 subscribe(podcast, user, devs)
311 except (SubscriptionException, DeviceDoesNotExist, ValueError) as e:
312 messages.error(request, str(e))
314 return HttpResponseRedirect(get_podcast_link_target(podcast))
317 @never_cache
318 @login_required
319 def unsubscribe(request, podcast, device_uid):
321 return_to = request.GET.get('return_to', None)
323 if not return_to:
324 raise Http404('Wrong URL')
326 try:
327 device = request.user.get_device_by_uid(device_uid)
329 except DeviceDoesNotExist as e:
330 messages.error(request, str(e))
331 return HttpResponseRedirect(return_to)
333 try:
334 unsubscribe(podcast, request.user, device)
335 except SubscriptionException as e:
336 logger.exception('Web: %(username)s: could not unsubscribe from podcast %(podcast_url)s on device %(device_id)s' %
337 {'username': request.user.username, 'podcast_url': podcast.url, 'device_id': device.id})
339 return HttpResponseRedirect(return_to)
342 @never_cache
343 @login_required
344 @allowed_methods(['POST'])
345 def unsubscribe_all(request, podcast):
346 """ unsubscribe all of the user's devices from the podcast """
348 user = request.user
349 state = podcast_state_for_user_podcast(user, podcast)
351 dev_ids = state.get_subscribed_device_ids()
352 devs = user.get_devices(dev_ids)
353 # ungroup groups
354 devs = [dev[0] if isinstance(dev, list) else dev for dev in devs]
356 try:
357 unsubscribe(podcast, user, devs)
358 except (SubscriptionException, DeviceDoesNotExist, ValueError) as e:
359 messages.error(request, str(e))
361 return HttpResponseRedirect(get_podcast_link_target(podcast))
364 @never_cache
365 @login_required
366 def subscribe_url(request):
367 url = request.GET.get('url', None)
369 if not url:
370 raise Http404('http://my.gpodder.org/subscribe?url=http://www.example.com/podcast.xml')
372 url = normalize_feed_url(url)
374 if not url:
375 raise Http404('Please specify a valid url')
377 podcast = Podcasts.objects.get_or_create_for_url(url)
379 return HttpResponseRedirect(get_podcast_link_target(podcast, 'subscribe'))
382 @never_cache
383 @allowed_methods(['POST'])
384 def set_public(request, podcast, public):
385 state = podcast_state_for_user_podcast(request.user, podcast)
386 set_podcast_privacy_settings(state, public)
387 return HttpResponseRedirect(get_podcast_link_target(podcast))
390 @never_cache
391 @login_required
392 def flattr_podcast(request, podcast):
393 """ Flattrs a podcast, records an event and redirects to the podcast """
395 user = request.user
396 site = RequestSite(request)
398 # do flattring via the tasks queue, but wait for the result
399 task = flattr_thing.delay(user, podcast.get_id(), site.domain,
400 request.is_secure(), 'Podcast')
401 success, msg = task.get()
403 if success:
404 action = SubscriptionAction()
405 action.action = 'flattr'
406 state = podcast_state_for_user_podcast(request.user, podcast)
407 add_subscription_action(state, action)
408 messages.success(request, _("Flattr\'d"))
410 else:
411 messages.error(request, msg)
413 return HttpResponseRedirect(get_podcast_link_target(podcast))
416 # To make all view accessible via either IDs or Slugs
417 # a decorator queries the podcast and passes the Id on to the
418 # regular views
420 def slug_decorator(f):
421 @wraps(f)
422 def _decorator(request, slug, *args, **kwargs):
424 podcast = Podcast.objects.filter(slugs__slug=slug)
425 podcast = podcast.prefetch_related('slugs', 'urls').get()
427 # redirect when a non-cannonical slug is used
428 if slug != podcast.slug:
429 return HttpResponseRedirect(get_podcast_link_target(podcast))
431 return f(request, podcast, *args, **kwargs)
433 return _decorator
436 def id_decorator(f):
437 @wraps(f)
438 def _decorator(request, podcast_id, *args, **kwargs):
440 try:
441 podcast = Podcast.objects.get(id=podcast_id)
442 return f(request, podcast, *args, **kwargs)
443 # TODO: redirect to Slug URL?
445 except Podcast.DoesNotExist:
446 podcast = get_object_or_404(Podcast, merged_uuids__uuid=podcast_id)
447 return HttpResponseRedirect(get_podcast_link_target(podcast))
449 return _decorator
452 show_slug = slug_decorator(show)
453 subscribe_slug = slug_decorator(subscribe)
454 subscribe_all_slug = slug_decorator(subscribe_all)
455 unsubscribe_slug = slug_decorator(unsubscribe)
456 unsubscribe_all_slug = slug_decorator(unsubscribe_all)
457 add_tag_slug = slug_decorator(add_tag)
458 remove_tag_slug = slug_decorator(remove_tag)
459 set_public_slug = slug_decorator(set_public)
460 all_episodes_slug = slug_decorator(all_episodes)
461 flattr_podcast_slug = slug_decorator(flattr_podcast)
462 history_podcast_slug = slug_decorator(history)
465 show_id = id_decorator(show)
466 subscribe_id = id_decorator(subscribe)
467 subscribe_all_id = id_decorator(subscribe_all)
468 unsubscribe_id = id_decorator(unsubscribe)
469 unsubscribe_all_id = id_decorator(unsubscribe_all)
470 add_tag_id = id_decorator(add_tag)
471 remove_tag_id = id_decorator(remove_tag)
472 set_public_id = id_decorator(set_public)
473 all_episodes_id = id_decorator(all_episodes)
474 flattr_podcast_id = id_decorator(flattr_podcast)
475 history_podcast_id = id_decorator(history)