let podcasts, episodes link to their publisher pages
[mygpo.git] / mygpo / web / views / podcast.py
blob6f34e0e8421d3eec4bd675009914c77217a96f35
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 from mygpo.db.couchdb.episode import episodes_for_podcast
24 from mygpo.db.couchdb.podcast import podcast_for_slug, podcast_for_slug_id, \
25 podcast_for_oldid, podcast_for_url
26 from mygpo.db.couchdb.podcast_state import podcast_state_for_user_podcast, \
27 add_subscription_action
28 from mygpo.db.couchdb.episode_state import get_podcasts_episode_states, \
29 episode_listener_counts
30 from mygpo.db.couchdb.directory import tags_for_user, tags_for_podcast
32 import logging
33 logger = logging.getLogger(__name__)
36 MAX_TAGS_ON_PAGE=50
39 @repeat_on_conflict(['state'])
40 def update_podcast_settings(state, is_public):
41 state.settings[PUBLIC_SUB_PODCAST.name] = is_public
42 state.save()
45 @vary_on_cookie
46 @cache_control(private=True)
47 @allowed_methods(['GET'])
48 def show_slug(request, slug):
49 podcast = podcast_for_slug(slug)
51 if slug != podcast.slug:
52 target = reverse('podcast_slug', args=[podcast.slug])
53 return HttpResponseRedirect(target)
55 return show(request, podcast.oldid)
58 @vary_on_cookie
59 @cache_control(private=True)
60 @allowed_methods(['GET'])
61 def show(request, podcast):
62 """ Shows a podcast detail page """
64 current_site = RequestSite(request)
65 episodes = episode_list(podcast, request.user, limit=20)
66 user = request.user
68 max_listeners = max([e.listeners for e in episodes] + [0])
70 episode = None
72 if episodes:
73 episode = episodes[0]
74 episodes = episodes[1:]
76 if podcast.group:
77 group = PodcastGroup.get(podcast.group)
78 rel_podcasts = filter(lambda x: x != podcast, group.podcasts)
79 else:
80 rel_podcasts = []
82 tags = get_tags(podcast, user)
84 if user.is_authenticated():
85 state = podcast_state_for_user_podcast(user, podcast)
86 subscribed_devices = state.get_subscribed_device_ids()
87 subscribed_devices = [user.get_device(x) for x in subscribed_devices]
89 subscribe_targets = podcast.subscribe_targets(user)
91 history = list(state.actions)
92 is_public = state.settings.get('public_subscription', True)
93 can_flattr = request.user.get_wksetting(FLATTR_TOKEN) and podcast.flattr_url
95 else:
96 history = []
97 is_public = False
98 subscribed_devices = []
99 subscribe_targets = []
100 can_flattr = False
102 is_publisher = check_publisher_permission(user, podcast)
104 def _set_objects(h):
105 dev = user.get_device(h.device)
106 return proxy_object(h, device=dev)
107 history = map(_set_objects, history)
109 return render(request, 'podcast.html', {
110 'tags': tags,
111 'url': current_site,
112 'history': history,
113 'podcast': podcast,
114 'is_public': is_public,
115 'devices': subscribed_devices,
116 'related_podcasts': rel_podcasts,
117 'can_subscribe': len(subscribe_targets) > 0,
118 'subscribe_targets': subscribe_targets,
119 'episode': episode,
120 'episodes': episodes,
121 'max_listeners': max_listeners,
122 'can_flattr': can_flattr,
123 'is_publisher': is_publisher,
127 def get_tags(podcast, user):
128 tags = {}
129 for t in tags_for_podcast(podcast):
130 tag_str = t.lower()
131 tags[tag_str] = False
133 if not user.is_anonymous():
134 users_tags = tags_for_user(user, podcast.get_id())
135 for t in users_tags.get(podcast.get_id(), []):
136 tag_str = t.lower()
137 tags[tag_str] = True
139 tag_list = [{'tag': key, 'is_own': value} for key, value in tags.iteritems()]
140 tag_list.sort(key=lambda x: x['tag'])
142 if len(tag_list) > MAX_TAGS_ON_PAGE:
143 tag_list = filter(lambda x: x['is_own'], tag_list)
144 tag_list.append({'tag': '...', 'is_own': False})
146 return tag_list
149 def episode_list(podcast, user, offset=0, limit=None):
151 Returns a list of episodes, with their action-attribute set to the latest
152 action. The attribute is unsert if there is no episode-action for
153 the episode.
156 listeners = dict(episode_listener_counts(podcast))
157 episodes = episodes_for_podcast(podcast, descending=True, skip=offset, limit=limit)
159 if user.is_authenticated():
161 # prepare pre-populated data for HistoryEntry.fetch_data
162 podcasts_dict = dict( (p_id, podcast) for p_id in podcast.get_ids())
163 episodes_dict = dict( (episode._id, episode) for episode in episodes)
165 actions = get_podcasts_episode_states(podcast, user._id)
166 actions = map(HistoryEntry.from_action_dict, actions)
168 HistoryEntry.fetch_data(user, actions,
169 podcasts=podcasts_dict, episodes=episodes_dict)
171 episode_actions = dict( (action.episode_id, action) for action in actions)
172 else:
173 episode_actions = {}
175 annotate_episode = partial(_annotate_episode, listeners, episode_actions)
176 return map(annotate_episode, episodes)
180 def all_episodes(request, podcast, page_size=20):
182 # Make sure page request is an int. If not, deliver first page.
183 try:
184 page = int(request.GET.get('page', '1'))
185 except ValueError:
186 page = 1
188 user = request.user
190 episodes = episode_list(podcast, user, (page-1) * page_size,
191 page_size)
192 episodes_total = podcast.episode_count or 0
193 num_pages = episodes_total / page_size
194 page_list = get_page_list(1, num_pages, page, 15)
196 max_listeners = max([e.listeners for e in episodes] + [0])
198 is_publisher = check_publisher_permission(user, podcast)
200 return render(request, 'episodes.html', {
201 'podcast': podcast,
202 'episodes': episodes,
203 'max_listeners': max_listeners,
204 'page_list': page_list,
205 'current_page': page,
206 'is_publisher': is_publisher,
211 def _annotate_episode(listeners, episode_actions, episode):
212 listener_count = listeners.pop(episode._id, None)
213 action = episode_actions.pop(episode._id, None)
214 return proxy_object(episode, listeners=listener_count, action=action)
218 @never_cache
219 @login_required
220 def add_tag(request, podcast):
221 podcast_state = podcast_state_for_user_podcast(request.user, podcast)
223 tag_str = request.GET.get('tag', '')
224 if not tag_str:
225 return HttpResponseBadRequest()
227 tags = tag_str.split(',')
229 @repeat_on_conflict(['state'])
230 def update(state):
231 state.add_tags(tags)
232 state.save()
234 update(state=podcast_state)
236 if request.GET.get('next', '') == 'mytags':
237 return HttpResponseRedirect('/tags/')
239 return HttpResponseRedirect(get_podcast_link_target(podcast))
242 @never_cache
243 @login_required
244 def remove_tag(request, podcast):
245 podcast_state = podcast_state_for_user_podcast(request.user, podcast)
247 tag_str = request.GET.get('tag', '')
248 if not tag_str:
249 return HttpResponseBadRequest()
251 @repeat_on_conflict(['state'])
252 def update(state):
253 tags = list(state.tags)
254 if tag_str in tags:
255 state.tags.remove(tag_str)
256 state.save()
258 update(state=podcast_state)
260 if request.GET.get('next', '') == 'mytags':
261 return HttpResponseRedirect('/tags/')
263 return HttpResponseRedirect(get_podcast_link_target(podcast))
266 @never_cache
267 @login_required
268 @allowed_methods(['GET', 'POST'])
269 def subscribe(request, podcast):
271 if request.method == 'POST':
273 # multiple UIDs from the /podcast/<slug>/subscribe
274 device_uids = [k for (k,v) in request.POST.items() if k==v]
276 # single UID from /podcast/<slug>
277 if 'targets' in request.POST:
278 device_uids.append(request.POST.get('targets'))
280 for uid in device_uids:
281 try:
282 device = request.user.get_device_by_uid(uid)
283 podcast.subscribe(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 def unsubscribe(request, podcast, device_uid):
302 return_to = request.GET.get('return_to', None)
304 if not return_to:
305 raise Http404('Wrong URL')
307 try:
308 device = request.user.get_device_by_uid(device_uid)
310 except DeviceDoesNotExist as e:
311 messages.error(request, str(e))
312 return HttpResponseRedirect(return_to)
314 try:
315 podcast.unsubscribe(request.user, device)
316 except SubscriptionException as e:
317 logger.exception('Web: %(username)s: could not unsubscribe from podcast %(podcast_url)s on device %(device_id)s' %
318 {'username': request.user.username, 'podcast_url': podcast.url, 'device_id': device.id})
320 return HttpResponseRedirect(return_to)
323 @never_cache
324 @login_required
325 def subscribe_url(request):
326 url = request.GET.get('url', None)
328 if not url:
329 raise Http404('http://my.gpodder.org/subscribe?url=http://www.example.com/podcast.xml')
331 url = normalize_feed_url(url)
333 if not url:
334 raise Http404('Please specify a valid url')
336 podcast = podcast_for_url(url, create=True)
338 return HttpResponseRedirect(get_podcast_link_target(podcast, 'subscribe'))
341 @never_cache
342 @allowed_methods(['POST'])
343 def set_public(request, podcast, public):
344 state = podcast_state_for_user_podcast(request.user, podcast)
345 update_podcast_settings(state=state, is_public=public)
346 return HttpResponseRedirect(get_podcast_link_target(podcast))
349 @never_cache
350 @login_required
351 def flattr_podcast(request, podcast):
352 """ Flattrs a podcast, records an event and redirects to the podcast """
354 user = request.user
355 site = RequestSite(request)
357 # do flattring via the tasks queue, but wait for the result
358 task = flattr_thing.delay(user, podcast.get_id(), site.domain,
359 request.is_secure(), 'Podcast')
360 success, msg = task.get()
362 if success:
363 action = SubscriptionAction()
364 action.action = 'flattr'
365 state = podcast_state_for_user_podcast(request.user, podcast)
366 add_subscription_action(state, action)
367 messages.success(request, _("Flattr\'d"))
369 else:
370 messages.error(request, msg)
372 return HttpResponseRedirect(get_podcast_link_target(podcast))
375 # To make all view accessible via either CouchDB-ID or Slugs
376 # a decorator queries the podcast and passes the Id on to the
377 # regular views
379 def slug_id_decorator(f):
380 @wraps(f)
381 def _decorator(request, slug_id, *args, **kwargs):
382 podcast = podcast_for_slug_id(slug_id)
384 if podcast is None:
385 raise Http404
387 # redirect when Id or a merged (non-cannonical) slug is used
388 if podcast.slug and slug_id != podcast.slug:
389 return HttpResponseRedirect(get_podcast_link_target(podcast))
391 return f(request, podcast, *args, **kwargs)
393 return _decorator
396 def oldid_decorator(f):
397 @wraps(f)
398 def _decorator(request, pid, *args, **kwargs):
399 try:
400 pid = int(pid)
401 except (TypeError, ValueError):
402 raise Http404
404 podcast = podcast_for_oldid(pid)
406 if not podcast:
407 raise Http404
409 # redirect to Id or slug URL
410 return HttpResponseRedirect(get_podcast_link_target(podcast))
412 return _decorator
415 show_slug_id = slug_id_decorator(show)
416 subscribe_slug_id = slug_id_decorator(subscribe)
417 unsubscribe_slug_id = slug_id_decorator(unsubscribe)
418 add_tag_slug_id = slug_id_decorator(add_tag)
419 remove_tag_slug_id = slug_id_decorator(remove_tag)
420 set_public_slug_id = slug_id_decorator(set_public)
421 all_episodes_slug_id= slug_id_decorator(all_episodes)
422 flattr_podcast_slug_id=slug_id_decorator(flattr_podcast)
425 show_oldid = oldid_decorator(show)
426 subscribe_oldid = oldid_decorator(subscribe)
427 unsubscribe_oldid = oldid_decorator(unsubscribe)
428 add_tag_oldid = oldid_decorator(add_tag)
429 remove_tag_oldid = oldid_decorator(remove_tag)
430 set_public_oldid = oldid_decorator(set_public)
431 all_episodes_oldid = oldid_decorator(all_episodes)
432 flattr_podcast_oldid= oldid_decorator(flattr_podcast)