move all User and EpisodetUserState db queries into separate module
[mygpo.git] / mygpo / web / views / podcast.py
blobb3ba10228e22a8e9fbb404287d4e63f4817084d5
1 from datetime import date, timedelta, datetime
2 from functools import wraps, partial
4 from django.core.urlresolvers import reverse
5 from django.http import HttpResponseBadRequest, HttpResponseRedirect, Http404
6 from django.db import IntegrityError
7 from django.shortcuts import render
8 from django.contrib.auth.decorators import login_required
9 from django.contrib.sites.models import RequestSite
10 from django.utils.translation import ugettext as _
11 from django.contrib import messages
12 from django.views.decorators.vary import vary_on_cookie
13 from django.views.decorators.cache import never_cache, cache_control
15 from mygpo.core.models import Podcast, PodcastGroup, SubscriptionException
16 from mygpo.core.proxy import proxy_object
17 from mygpo.api.sanitizing import sanitize_url
18 from mygpo.users.models import HistoryEntry, DeviceDoesNotExist
19 from mygpo.web.forms import PrivacyForm, SyncForm
20 from mygpo.directory.tags import Tag
21 from mygpo.decorators import allowed_methods, repeat_on_conflict
22 from mygpo.utils import daterange
23 from mygpo.web.utils import get_podcast_link_target
24 from mygpo.log import log
25 from mygpo.db.couchdb.episode import episodes_for_podcast
26 from mygpo.db.couchdb.podcast import podcast_for_slug, podcast_for_slug_id, \
27 podcast_for_oldid, podcast_for_url
28 from mygpo.db.couchdb.podcast_state import podcast_state_for_user_podcast
29 from mygpo.db.couchdb.episode_state import get_podcasts_episode_states
32 MAX_TAGS_ON_PAGE=50
35 @repeat_on_conflict(['state'])
36 def update_podcast_settings(state, is_public):
37 state.settings['public_subscription'] = is_public
38 state.save()
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):
59 episodes = episode_list(podcast, request.user, limit=20)
61 max_listeners = max([e.listeners for e in episodes] + [0])
63 episode = None
65 if episodes:
66 episode = episodes[0]
67 episodes = episodes[1:]
69 if podcast.group:
70 group = PodcastGroup.get(podcast.group)
71 rel_podcasts = filter(lambda x: x != podcast, group.podcasts)
72 else:
73 rel_podcasts = []
75 tags = get_tags(podcast, request.user)
77 if request.user.is_authenticated():
79 request.user.sync_all()
81 state = podcast_state_for_user_podcast(request.user, podcast)
82 subscribed_devices = state.get_subscribed_device_ids()
83 subscribed_devices = [request.user.get_device(x) for x in subscribed_devices]
85 subscribe_targets = podcast.subscribe_targets(request.user)
87 history = list(state.actions)
88 def _set_objects(h):
89 dev = request.user.get_device(h.device)
90 return proxy_object(h, device=dev)
91 history = map(_set_objects, history)
93 is_public = state.settings.get('public_subscription', True)
95 subscribe_form = SyncForm()
96 subscribe_form.set_targets(subscribe_targets, '')
98 return render(request, 'podcast.html', {
99 'tags': tags,
100 'history': history,
101 'podcast': podcast,
102 'is_public': is_public,
103 'devices': subscribed_devices,
104 'related_podcasts': rel_podcasts,
105 'can_subscribe': len(subscribe_targets) > 0,
106 'subscribe_form': subscribe_form,
107 'episode': episode,
108 'episodes': episodes,
109 'max_listeners': max_listeners,
111 else:
112 current_site = RequestSite(request)
113 return render(request, 'podcast.html', {
114 'podcast': podcast,
115 'related_podcasts': rel_podcasts,
116 'tags': tags,
117 'url': current_site,
118 'episode': episode,
119 'episodes': episodes,
120 'max_listeners': max_listeners,
124 def get_tags(podcast, user):
125 tags = {}
126 for t in Tag.for_podcast(podcast):
127 tag_str = t.lower()
128 tags[tag_str] = False
130 if not user.is_anonymous():
131 users_tags = Tag.for_user(user, podcast.get_id())
132 for t in users_tags.get(podcast.get_id(), []):
133 tag_str = t.lower()
134 tags[tag_str] = True
136 tag_list = [{'tag': key, 'is_own': value} for key, value in tags.iteritems()]
137 tag_list.sort(key=lambda x: x['tag'])
139 if len(tag_list) > MAX_TAGS_ON_PAGE:
140 tag_list = filter(lambda x: x['is_own'], tag_list)
141 tag_list.append({'tag': '...', 'is_own': False})
143 return tag_list
146 def episode_list(podcast, user, limit=None):
148 Returns a list of episodes, with their action-attribute set to the latest
149 action. The attribute is unsert if there is no episode-action for
150 the episode.
153 listeners = dict(podcast.episode_listener_counts())
154 episodes = episodes_for_podcast(podcast, descending=True, limit=limit)
156 if user.is_authenticated():
158 # prepare pre-populated data for HistoryEntry.fetch_data
159 podcasts_dict = dict( (p_id, podcast) for p_id in podcast.get_ids())
160 episodes_dict = dict( (episode._id, episode) for episode in episodes)
162 actions = get_podcasts_episode_states(podcast, user._id)
163 actions = map(HistoryEntry.from_action_dict, actions)
165 HistoryEntry.fetch_data(user, actions,
166 podcasts=podcasts_dict, episodes=episodes_dict)
168 episode_actions = dict( (action.episode_id, action) for action in actions)
169 else:
170 episode_actions = {}
172 annotate_episode = partial(_annotate_episode, listeners, episode_actions)
173 return map(annotate_episode, episodes)
177 def all_episodes(request, podcast):
179 episodes = episode_list(podcast, request.user)
181 max_listeners = max([e.listeners for e in episodes] + [0])
183 if request.user.is_authenticated():
185 request.user.sync_all()
187 return render(request, 'episodes.html', {
188 'podcast': podcast,
189 'episodes': episodes,
190 'max_listeners': max_listeners,
195 def _annotate_episode(listeners, episode_actions, episode):
196 listener_count = listeners.pop(episode._id, None)
197 action = episode_actions.pop(episode._id, None)
198 return proxy_object(episode, listeners=listener_count, action=action)
202 @never_cache
203 @login_required
204 def add_tag(request, podcast):
205 podcast_state = podcast_state_for_user_podcast(request.user, podcast)
207 tag_str = request.GET.get('tag', '')
208 if not tag_str:
209 return HttpResponseBadRequest()
211 tags = tag_str.split(',')
213 @repeat_on_conflict(['state'])
214 def update(state):
215 state.add_tags(tags)
216 state.save()
218 update(state=podcast_state)
220 if request.GET.get('next', '') == 'mytags':
221 return HttpResponseRedirect('/tags/')
223 return HttpResponseRedirect(get_podcast_link_target(podcast))
226 @never_cache
227 @login_required
228 def remove_tag(request, podcast):
229 podcast_state = podcast_state_for_user_podcast(request.user, podcast)
231 tag_str = request.GET.get('tag', '')
232 if not tag_str:
233 return HttpResponseBadRequest()
235 @repeat_on_conflict(['state'])
236 def update(state):
237 tags = list(state.tags)
238 if tag_str in tags:
239 state.tags.remove(tag_str)
240 state.save()
242 update(state=podcast_state)
244 if request.GET.get('next', '') == 'mytags':
245 return HttpResponseRedirect('/tags/')
247 return HttpResponseRedirect(get_podcast_link_target(podcast))
250 @never_cache
251 @login_required
252 @allowed_methods(['GET', 'POST'])
253 def subscribe(request, podcast):
255 if request.method == 'POST':
256 form = SyncForm(request.POST)
258 try:
259 device = request.user.get_device_by_uid(form.get_target())
260 podcast.subscribe(request.user, device)
262 except (SubscriptionException, DeviceDoesNotExist) as e:
263 messages.error(request, str(e))
265 return HttpResponseRedirect(get_podcast_link_target(podcast))
268 request.user.sync_all()
270 targets = podcast.subscribe_targets(request.user)
272 form = SyncForm()
273 form.set_targets(targets, _('Choose a device:'))
275 return render(request, 'subscribe.html', {
276 'podcast': podcast,
277 'can_subscribe': len(targets) > 0,
278 'form': form
282 @never_cache
283 @login_required
284 def unsubscribe(request, podcast, device_uid):
286 return_to = request.GET.get('return_to', None)
288 if not return_to:
289 raise Http404('Wrong URL')
291 try:
292 device = request.user.get_device_by_uid(device_uid)
294 except DeviceDoesNotExist as e:
295 messages.error(request, str(e))
297 try:
298 podcast.unsubscribe(request.user, device)
299 except SubscriptionException as e:
300 log('Web: %(username)s: could not unsubscribe from podcast %(podcast_url)s on device %(device_id)s: %(exception)s' %
301 {'username': request.user.username, 'podcast_url': podcast.url, 'device_id': device.id, 'exception': e})
303 return HttpResponseRedirect(return_to)
306 @never_cache
307 @login_required
308 def subscribe_url(request):
309 url = request.GET.get('url', None)
311 if not url:
312 raise Http404('http://my.gpodder.org/subscribe?url=http://www.example.com/podcast.xml')
314 url = sanitize_url(url)
316 if url == '':
317 raise Http404('Please specify a valid url')
319 podcast = podcast_for_url(url, create=True)
321 return HttpResponseRedirect(get_podcast_link_target(podcast, 'subscribe'))
324 @never_cache
325 @allowed_methods(['POST'])
326 def set_public(request, podcast, public):
327 state = podcast_state_for_user_podcast(request.user, podcast)
328 update_podcast_settings(state=state, is_public=public)
329 return HttpResponseRedirect(get_podcast_link_target(podcast))
332 # To make all view accessible via either CouchDB-ID or Slugs
333 # a decorator queries the podcast and passes the Id on to the
334 # regular views
336 def slug_id_decorator(f):
337 @wraps(f)
338 def _decorator(request, slug_id, *args, **kwargs):
339 podcast = podcast_for_slug_id(slug_id)
341 if podcast is None:
342 raise Http404
344 return f(request, podcast, *args, **kwargs)
346 return _decorator
349 def oldid_decorator(f):
350 @wraps(f)
351 def _decorator(request, pid, *args, **kwargs):
352 try:
353 pid = int(pid)
354 except (TypeError, ValueError):
355 raise Http404
357 podcast = podcast_for_oldid(pid)
359 if not podcast:
360 raise Http404
362 return f(request, podcast, *args, **kwargs)
364 return _decorator
367 show_slug_id = slug_id_decorator(show)
368 subscribe_slug_id = slug_id_decorator(subscribe)
369 unsubscribe_slug_id = slug_id_decorator(unsubscribe)
370 add_tag_slug_id = slug_id_decorator(add_tag)
371 remove_tag_slug_id = slug_id_decorator(remove_tag)
372 set_public_slug_id = slug_id_decorator(set_public)
373 all_episodes_slug_id= slug_id_decorator(all_episodes)
376 show_oldid = oldid_decorator(show)
377 subscribe_oldid = oldid_decorator(subscribe)
378 unsubscribe_oldid = oldid_decorator(unsubscribe)
379 add_tag_oldid = oldid_decorator(add_tag)
380 remove_tag_oldid = oldid_decorator(remove_tag)
381 set_public_oldid = oldid_decorator(set_public)
382 all_episodes_oldid = oldid_decorator(all_episodes)