redirect when using non-canonical URL for podcasts, episodes
[mygpo.git] / mygpo / web / views / podcast.py
blob03df9b1ca93940969d48aec3fea18bc103769476
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.api.sanitizing import sanitize_url
16 from mygpo.users.models import HistoryEntry, DeviceDoesNotExist
17 from mygpo.web.forms import SyncForm
18 from mygpo.decorators import allowed_methods, repeat_on_conflict
19 from mygpo.web.utils import get_podcast_link_target
20 from mygpo.log import log
21 from mygpo.db.couchdb.episode import episodes_for_podcast
22 from mygpo.db.couchdb.podcast import podcast_for_slug, podcast_for_slug_id, \
23 podcast_for_oldid, podcast_for_url
24 from mygpo.db.couchdb.podcast_state import podcast_state_for_user_podcast
25 from mygpo.db.couchdb.episode_state import get_podcasts_episode_states, \
26 episode_listener_counts
27 from mygpo.db.couchdb.directory import tags_for_user, tags_for_podcast
30 MAX_TAGS_ON_PAGE=50
33 @repeat_on_conflict(['state'])
34 def update_podcast_settings(state, is_public):
35 state.settings['public_subscription'] = is_public
36 state.save()
39 @vary_on_cookie
40 @cache_control(private=True)
41 @allowed_methods(['GET'])
42 def show_slug(request, slug):
43 podcast = podcast_for_slug(slug)
45 if slug != podcast.slug:
46 target = reverse('podcast_slug', args=[podcast.slug])
47 return HttpResponseRedirect(target)
49 return show(request, podcast.oldid)
52 @vary_on_cookie
53 @cache_control(private=True)
54 @allowed_methods(['GET'])
55 def show(request, podcast):
57 episodes = episode_list(podcast, request.user, limit=20)
59 max_listeners = max([e.listeners for e in episodes] + [0])
61 episode = None
63 if episodes:
64 episode = episodes[0]
65 episodes = episodes[1:]
67 if podcast.group:
68 group = PodcastGroup.get(podcast.group)
69 rel_podcasts = filter(lambda x: x != podcast, group.podcasts)
70 else:
71 rel_podcasts = []
73 tags = get_tags(podcast, request.user)
75 if request.user.is_authenticated():
76 state = podcast_state_for_user_podcast(request.user, podcast)
77 subscribed_devices = state.get_subscribed_device_ids()
78 subscribed_devices = [request.user.get_device(x) for x in subscribed_devices]
80 subscribe_targets = podcast.subscribe_targets(request.user)
82 history = list(state.actions)
83 def _set_objects(h):
84 dev = request.user.get_device(h.device)
85 return proxy_object(h, device=dev)
86 history = map(_set_objects, history)
88 is_public = state.settings.get('public_subscription', True)
90 return render(request, 'podcast.html', {
91 'tags': tags,
92 'history': history,
93 'podcast': podcast,
94 'is_public': is_public,
95 'devices': subscribed_devices,
96 'related_podcasts': rel_podcasts,
97 'can_subscribe': len(subscribe_targets) > 0,
98 'subscribe_targets': subscribe_targets,
99 'episode': episode,
100 'episodes': episodes,
101 'max_listeners': max_listeners,
103 else:
104 current_site = RequestSite(request)
105 return render(request, 'podcast.html', {
106 'podcast': podcast,
107 'related_podcasts': rel_podcasts,
108 'tags': tags,
109 'url': current_site,
110 'episode': episode,
111 'episodes': episodes,
112 'max_listeners': max_listeners,
116 def get_tags(podcast, user):
117 tags = {}
118 for t in tags_for_podcast(podcast):
119 tag_str = t.lower()
120 tags[tag_str] = False
122 if not user.is_anonymous():
123 users_tags = tags_for_user(user, podcast.get_id())
124 for t in users_tags.get(podcast.get_id(), []):
125 tag_str = t.lower()
126 tags[tag_str] = True
128 tag_list = [{'tag': key, 'is_own': value} for key, value in tags.iteritems()]
129 tag_list.sort(key=lambda x: x['tag'])
131 if len(tag_list) > MAX_TAGS_ON_PAGE:
132 tag_list = filter(lambda x: x['is_own'], tag_list)
133 tag_list.append({'tag': '...', 'is_own': False})
135 return tag_list
138 def episode_list(podcast, user, 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 = episodes_for_podcast(podcast, descending=True, limit=limit)
148 if user.is_authenticated():
150 # prepare pre-populated data for HistoryEntry.fetch_data
151 podcasts_dict = dict( (p_id, podcast) for p_id in podcast.get_ids())
152 episodes_dict = dict( (episode._id, episode) for episode in episodes)
154 actions = get_podcasts_episode_states(podcast, user._id)
155 actions = map(HistoryEntry.from_action_dict, actions)
157 HistoryEntry.fetch_data(user, actions,
158 podcasts=podcasts_dict, episodes=episodes_dict)
160 episode_actions = dict( (action.episode_id, action) for action in actions)
161 else:
162 episode_actions = {}
164 annotate_episode = partial(_annotate_episode, listeners, episode_actions)
165 return map(annotate_episode, episodes)
169 def all_episodes(request, podcast):
171 episodes = episode_list(podcast, request.user)
173 max_listeners = max([e.listeners for e in episodes] + [0])
175 return render(request, 'episodes.html', {
176 'podcast': podcast,
177 'episodes': episodes,
178 'max_listeners': max_listeners,
183 def _annotate_episode(listeners, episode_actions, episode):
184 listener_count = listeners.pop(episode._id, None)
185 action = episode_actions.pop(episode._id, None)
186 return proxy_object(episode, listeners=listener_count, action=action)
190 @never_cache
191 @login_required
192 def add_tag(request, podcast):
193 podcast_state = podcast_state_for_user_podcast(request.user, podcast)
195 tag_str = request.GET.get('tag', '')
196 if not tag_str:
197 return HttpResponseBadRequest()
199 tags = tag_str.split(',')
201 @repeat_on_conflict(['state'])
202 def update(state):
203 state.add_tags(tags)
204 state.save()
206 update(state=podcast_state)
208 if request.GET.get('next', '') == 'mytags':
209 return HttpResponseRedirect('/tags/')
211 return HttpResponseRedirect(get_podcast_link_target(podcast))
214 @never_cache
215 @login_required
216 def remove_tag(request, podcast):
217 podcast_state = podcast_state_for_user_podcast(request.user, podcast)
219 tag_str = request.GET.get('tag', '')
220 if not tag_str:
221 return HttpResponseBadRequest()
223 @repeat_on_conflict(['state'])
224 def update(state):
225 tags = list(state.tags)
226 if tag_str in tags:
227 state.tags.remove(tag_str)
228 state.save()
230 update(state=podcast_state)
232 if request.GET.get('next', '') == 'mytags':
233 return HttpResponseRedirect('/tags/')
235 return HttpResponseRedirect(get_podcast_link_target(podcast))
238 @never_cache
239 @login_required
240 @allowed_methods(['GET', 'POST'])
241 def subscribe(request, podcast):
243 if request.method == 'POST':
245 # multiple UIDs from the /podcast/<slug>/subscribe
246 device_uids = [k for (k,v) in request.POST.items() if k==v]
248 # single UID from /podcast/<slug>
249 if 'targets' in request.POST:
250 device_uids.append(request.POST.get('targets'))
252 for uid in device_uids:
253 try:
254 device = request.user.get_device_by_uid(uid)
255 podcast.subscribe(request.user, device)
257 except (SubscriptionException, DeviceDoesNotExist, ValueError) as e:
258 messages.error(request, str(e))
260 return HttpResponseRedirect(get_podcast_link_target(podcast))
262 targets = podcast.subscribe_targets(request.user)
264 return render(request, 'subscribe.html', {
265 'targets': targets,
266 'podcast': podcast,
270 @never_cache
271 @login_required
272 def unsubscribe(request, podcast, device_uid):
274 return_to = request.GET.get('return_to', None)
276 if not return_to:
277 raise Http404('Wrong URL')
279 try:
280 device = request.user.get_device_by_uid(device_uid)
282 except DeviceDoesNotExist as e:
283 messages.error(request, str(e))
285 try:
286 podcast.unsubscribe(request.user, device)
287 except SubscriptionException as e:
288 log('Web: %(username)s: could not unsubscribe from podcast %(podcast_url)s on device %(device_id)s: %(exception)s' %
289 {'username': request.user.username, 'podcast_url': podcast.url, 'device_id': device.id, 'exception': e})
291 return HttpResponseRedirect(return_to)
294 @never_cache
295 @login_required
296 def subscribe_url(request):
297 url = request.GET.get('url', None)
299 if not url:
300 raise Http404('http://my.gpodder.org/subscribe?url=http://www.example.com/podcast.xml')
302 url = sanitize_url(url)
304 if url == '':
305 raise Http404('Please specify a valid url')
307 podcast = podcast_for_url(url, create=True)
309 return HttpResponseRedirect(get_podcast_link_target(podcast, 'subscribe'))
312 @never_cache
313 @allowed_methods(['POST'])
314 def set_public(request, podcast, public):
315 state = podcast_state_for_user_podcast(request.user, podcast)
316 update_podcast_settings(state=state, is_public=public)
317 return HttpResponseRedirect(get_podcast_link_target(podcast))
320 # To make all view accessible via either CouchDB-ID or Slugs
321 # a decorator queries the podcast and passes the Id on to the
322 # regular views
324 def slug_id_decorator(f):
325 @wraps(f)
326 def _decorator(request, slug_id, *args, **kwargs):
327 podcast = podcast_for_slug_id(slug_id)
329 if podcast is None:
330 raise Http404
332 # redirect when Id or a merged (non-cannonical) slug is used
333 if podcast.slug and slug_id != podcast.slug:
334 return HttpResponseRedirect(get_podcast_link_target(podcast))
336 return f(request, podcast, *args, **kwargs)
338 return _decorator
341 def oldid_decorator(f):
342 @wraps(f)
343 def _decorator(request, pid, *args, **kwargs):
344 try:
345 pid = int(pid)
346 except (TypeError, ValueError):
347 raise Http404
349 podcast = podcast_for_oldid(pid)
351 if not podcast:
352 raise Http404
354 # redirect to Id or slug URL
355 return HttpResponseRedirect(get_podcast_link_target(podcast))
357 return _decorator
360 show_slug_id = slug_id_decorator(show)
361 subscribe_slug_id = slug_id_decorator(subscribe)
362 unsubscribe_slug_id = slug_id_decorator(unsubscribe)
363 add_tag_slug_id = slug_id_decorator(add_tag)
364 remove_tag_slug_id = slug_id_decorator(remove_tag)
365 set_public_slug_id = slug_id_decorator(set_public)
366 all_episodes_slug_id= slug_id_decorator(all_episodes)
369 show_oldid = oldid_decorator(show)
370 subscribe_oldid = oldid_decorator(subscribe)
371 unsubscribe_oldid = oldid_decorator(unsubscribe)
372 add_tag_oldid = oldid_decorator(add_tag)
373 remove_tag_oldid = oldid_decorator(remove_tag)
374 set_public_oldid = oldid_decorator(set_public)
375 all_episodes_oldid = oldid_decorator(all_episodes)