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
, \
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
35 logger
= logging
.getLogger(__name__
)
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
)
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
)
64 episodes
= episode_list(podcast
, request
.user
, limit
=num_episodes
)
67 max_listeners
= max([e
.listeners
for e
in episodes
] + [0])
73 episodes
= episodes
[1:]
76 group
= PodcastGroup
.get(podcast
.group
)
77 rel_podcasts
= filter(lambda x
: x
!= podcast
, group
.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
97 subscribed_devices
= []
98 subscribe_targets
= []
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', {
109 'has_tagged': has_tagged
,
111 'has_history': has_history
,
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
,
119 'episodes': episodes
,
120 'max_listeners': max_listeners
,
121 'can_flattr': can_flattr
,
122 'is_publisher': is_publisher
,
123 'page_list': page_list
,
128 def get_tags(podcast
, user
):
130 for t
in tags_for_podcast(podcast
):
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(), []):
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
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
)
178 annotate_episode
= partial(_annotate_episode
, listeners
, episode_actions
)
179 return map(annotate_episode
, episodes
)
185 def history(request
, podcast
):
186 """ shows the subscription history of the user """
189 state
= podcast_state_for_user_podcast(user
, podcast
)
190 history
= list(state
.actions
)
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', {
203 def all_episodes(request
, podcast
, page_size
=20):
205 # Make sure page request is an int. If not, deliver first page.
207 page
= int(request
.GET
.get('page', '1'))
213 episodes
= episode_list(podcast
, user
, (page
-1) * 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', {
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
)
243 def add_tag(request
, podcast
):
244 podcast_state
= podcast_state_for_user_podcast(request
.user
, podcast
)
246 tag_str
= request
.GET
.get('tag', '')
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
))
261 def remove_tag(request
, podcast
):
262 podcast_state
= podcast_state_for_user_podcast(request
.user
, podcast
)
264 tag_str
= request
.GET
.get('tag', '')
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
))
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
:
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', {
312 @allowed_methods(['POST'])
313 def subscribe_all(request
, podcast
):
314 """ subscribe all of the user's devices to the podcast """
317 devs
= podcast
.subscribe_targets(user
)
319 devs
= [dev
[0] if isinstance(dev
, list) else dev
for dev
in devs
]
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
))
331 def unsubscribe(request
, podcast
, device_uid
):
333 return_to
= request
.GET
.get('return_to', None)
336 raise Http404('Wrong URL')
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
)
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
)
356 @allowed_methods(['POST'])
357 def unsubscribe_all(request
, podcast
):
358 """ unsubscribe all of the user's devices from the podcast """
361 state
= podcast_state_for_user_podcast(user
, podcast
)
363 dev_ids
= state
.get_subscribed_device_ids()
364 devs
= user
.get_devices(dev_ids
)
366 devs
= [dev
[0] if isinstance(dev
, list) else dev
for dev
in devs
]
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
))
378 def subscribe_url(request
):
379 url
= request
.GET
.get('url', None)
382 raise Http404('http://my.gpodder.org/subscribe?url=http://www.example.com/podcast.xml')
384 url
= normalize_feed_url(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'))
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
))
404 def flattr_podcast(request
, podcast
):
405 """ Flattrs a podcast, records an event and redirects to the podcast """
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()
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"))
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
432 def slug_id_decorator(f
):
434 def _decorator(request
, slug_id
, *args
, **kwargs
):
435 podcast
= podcast_for_slug_id(slug_id
)
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
)
449 def oldid_decorator(f
):
451 def _decorator(request
, pid
, *args
, **kwargs
):
454 except (TypeError, ValueError):
457 podcast
= podcast_for_oldid(pid
)
462 # redirect to Id or slug URL
463 return HttpResponseRedirect(get_podcast_link_target(podcast
))
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
)