get Flattr / licensed podcasts from PostgreSQL
[mygpo.git] / mygpo / directory / views.py
blob3b90ffe240ae106002c8bcd22701d7de1f847efd
1 from __future__ import division
3 from itertools import imap as map
4 from math import ceil
5 from collections import Counter
7 from django.http import HttpResponseNotFound, Http404, HttpResponseRedirect
8 from django.core.urlresolvers import reverse
9 from django.shortcuts import render
10 from django.db.models import Count
11 from django.contrib.sites.models import RequestSite
12 from django.views.decorators.cache import cache_control
13 from django.views.decorators.vary import vary_on_cookie
14 from django.views.generic import ListView
15 from django.utils.decorators import method_decorator
16 from django.views.generic.base import View, TemplateView
17 from django.contrib.auth.decorators import login_required
18 from django.contrib import messages
19 from django.utils.translation import ugettext as _
21 from feedservice.parse.models import ParserException
22 from feedservice.parse import FetchFeedException
24 from mygpo.core.proxy import proxy_object
25 from mygpo.podcasts.models import Podcast
26 from mygpo.directory.toplist import PodcastToplist, EpisodeToplist, \
27 TrendingPodcasts
28 from mygpo.directory.search import search_podcasts
29 from mygpo.web.utils import process_lang_params, get_language_names, \
30 get_page_list, get_podcast_link_target, sanitize_language_codes
31 from mygpo.directory.tags import Topics
32 from mygpo.users.settings import FLATTR_TOKEN
33 from mygpo.data.feeddownloader import PodcastUpdater, NoEpisodesException
34 from mygpo.data.tasks import update_podcasts
35 from mygpo.db.couchdb.user import get_user_by_id
36 from mygpo.db.couchdb.podcast import podcasts_by_id, \
37 podcasts_to_dict, podcast_for_url
38 from mygpo.db.couchdb.directory import category_for_tag
39 from mygpo.db.couchdb.podcastlist import random_podcastlists, \
40 podcastlist_count, podcastlists_by_rating
43 class ToplistView(TemplateView):
44 """ Generic Top List view """
46 @method_decorator(vary_on_cookie)
47 @method_decorator(cache_control(private=True))
48 def dispatch(self, *args, **kwargs):
49 """ Only used for applying decorators """
50 return super(ToplistView, self).dispatch(*args, **kwargs)
52 def all_languages(self):
53 """ Returns all 2-letter language codes that are used by podcasts.
55 It filters obviously invalid strings, but does not check if any
56 of these codes is contained in ISO 639. """
58 query = Podcast.objects.exclude(language__isnull=True)
59 query = query.distinct('language').values('language')
61 langs = [o['language'] for o in query]
62 langs = sorted(sanitize_language_codes(langs))
64 return get_language_names(langs)
66 def language(self):
67 """ Currently selected language """
68 return process_lang_params(self.request)
70 def site(self):
71 """ Current site for constructing absolute links """
72 return RequestSite(self.request)
75 class PodcastToplistView(ToplistView):
76 """ Most subscribed podcasts """
78 template_name = 'toplist.html'
80 def get_context_data(self, num=100):
81 context = super(PodcastToplistView, self).get_context_data()
83 toplist = PodcastToplist(self.language())
84 entries = toplist[:num]
85 context['entries'] = entries
87 context['max_subscribers'] = max([0] + [p.subscriber_count() for (oldp, p) in entries])
89 return context
92 class EpisodeToplistView(ToplistView):
93 """ Most listened-to episodes """
95 template_name = 'episode_toplist.html'
97 def get_context_data(self, num=100):
98 context = super(EpisodeToplistView, self).get_context_data()
100 toplist = EpisodeToplist(language=self.language())
101 entries = list(map(proxy_object, toplist[:num]))
103 # load podcast objects
104 podcast_ids = [e.podcast for e in entries]
105 podcasts = podcasts_to_dict(podcast_ids, True)
106 for entry in entries:
107 entry.podcast = podcasts.get(entry.podcast, None)
109 context['entries'] = entries
111 # Determine maximum listener amount (or 0 if no entries exist)
112 context['max_listeners'] = max([0]+[e.listeners for e in entries])
114 return context
117 class Carousel(View):
118 """ A carousel demo """
120 @method_decorator(cache_control(private=True))
121 @method_decorator(vary_on_cookie)
122 def get(self, request):
124 return render(request, 'carousel.html', {
125 # evaluated lazyly, cached by template
126 'topics': Topics(),
130 class Directory(View):
131 """ The main directory page """
133 @method_decorator(cache_control(private=True))
134 @method_decorator(vary_on_cookie)
135 def get(self, request):
137 return render(request, 'directory.html', {
139 # evaluated lazyly, cached by template
140 'topics': Topics(),
141 'trending_podcasts': TrendingPodcasts(''),
142 'podcastlists': self.get_random_list(),
143 'random_podcast': Podcast.objects.random().first(),
147 def get_random_list(self, podcasts_per_list=5):
148 random_list = next(random_podcastlists(), None)
149 list_owner = None
150 if random_list:
151 random_list = proxy_object(random_list)
152 random_list.more_podcasts = max(0, len(random_list.podcasts) - podcasts_per_list)
153 random_list.podcasts = podcasts_by_id(random_list.podcasts[:podcasts_per_list])
154 random_list.user = get_user_by_id(random_list.user)
156 yield random_list
159 @cache_control(private=True)
160 @vary_on_cookie
161 def category(request, category, page_size=20):
162 category = category_for_tag(category)
163 if not category:
164 return HttpResponseNotFound()
166 # Make sure page request is an int. If not, deliver first page.
167 try:
168 page = int(request.GET.get('page', '1'))
169 except ValueError:
170 page = 1
172 entries = category.get_podcasts( (page-1) * page_size, page*page_size )
173 podcasts = filter(None, entries)
174 num_pages = int(ceil(len(category.podcasts) / page_size))
176 page_list = get_page_list(1, num_pages, page, 15)
178 return render(request, 'category.html', {
179 'entries': podcasts,
180 'category': category.label,
181 'page_list': page_list,
186 RESULTS_PER_PAGE=20
188 @cache_control(private=True)
189 @vary_on_cookie
190 def search(request, template='search.html', args={}):
192 if 'q' in request.GET:
193 q = request.GET.get('q', '').encode('utf-8')
195 try:
196 page = int(request.GET.get('page', 1))
197 except ValueError:
198 page = 1
200 results, total = search_podcasts(q=q, skip=RESULTS_PER_PAGE*(page-1))
201 num_pages = int(ceil(total / RESULTS_PER_PAGE))
203 page_list = get_page_list(1, num_pages, page, 15)
205 else:
206 results = []
207 q = None
208 page_list = []
210 max_subscribers = max([p.subscriber_count() for p in results] + [0])
211 current_site = RequestSite(request)
213 return render(request, template, dict(
214 q= q,
215 results= results,
216 page_list= page_list,
217 max_subscribers= max_subscribers,
218 domain= current_site.domain,
219 **args
224 @cache_control(private=True)
225 @vary_on_cookie
226 def podcast_lists(request, page_size=20):
228 # Make sure page request is an int. If not, deliver first page.
229 try:
230 page = int(request.GET.get('page', '1'))
231 except ValueError:
232 page = 1
234 lists = podcastlists_by_rating(skip=(page-1) * page_size, limit=page_size)
237 def _prepare_list(l):
238 user = get_user_by_id(l.user)
239 l = proxy_object(l)
240 l.username = user.username if user else ''
241 return l
243 lists = map(_prepare_list, lists)
245 num_pages = int(ceil(podcastlist_count() / float(page_size)))
247 page_list = get_page_list(1, num_pages, page, 15)
249 return render(request, 'podcast_lists.html', {
250 'lists': lists,
251 'page_list': page_list,
256 class MissingPodcast(View):
257 """ Check if a podcast is missing """
259 @method_decorator(login_required)
260 def get(self, request):
262 site = RequestSite(request)
264 # check if we're doing a query
265 url = request.GET.get('q', None)
267 if not url:
268 podcast = None
269 can_add = False
271 else:
272 podcast = podcast_for_url(url)
274 # if the podcast does already exist, there's nothing more to do
275 if podcast:
276 can_add = False
278 # check if we could add a podcast for the given URL
279 else:
280 podcast = False
281 updater = PodcastUpdater()
283 try:
284 can_add = updater.verify_podcast_url(url)
286 except (ParserException, FetchFeedException,
287 NoEpisodesException) as ex:
288 can_add = False
289 messages.error(request, unicode(ex))
291 return render(request, 'missing.html', {
292 'site': site,
293 'q': url,
294 'podcast': podcast,
295 'can_add': can_add,
299 class AddPodcast(View):
300 """ Add a missing podcast"""
302 @method_decorator(login_required)
303 @method_decorator(cache_control(private=True))
304 @method_decorator(vary_on_cookie)
305 def post(self, request):
307 url = request.POST.get('url', None)
309 if not url:
310 raise Http404
312 res = update_podcasts.delay([url])
314 return HttpResponseRedirect(reverse('add-podcast-status',
315 args=[res.task_id]))
318 class AddPodcastStatus(TemplateView):
319 """ Status of adding a podcast """
321 template_name = 'directory/add-podcast-status.html'
323 def get(self, request, task_id):
324 result = update_podcasts.AsyncResult(task_id)
326 if not result.ready():
327 return self.render_to_response({
328 'ready': False,
331 try:
332 podcasts = result.get()
333 messages.success(request, _('%d podcasts added' % len(podcasts)))
335 except (ParserException, FetchFeedException,
336 NoEpisodesException) as ex:
337 messages.error(request, str(ex))
338 podcast = None
340 return self.render_to_response({
341 'ready': True,
342 'podcasts': podcasts,
346 class PodcastListView(ListView):
347 """ A generic podcast list view """
349 paginate_by = 15
350 context_object_name = 'podcasts'
352 @method_decorator(vary_on_cookie)
353 @method_decorator(cache_control(private=True))
354 def dispatch(self, *args, **kwargs):
355 """ Only used for applying decorators """
356 return super(PodcastListView, self).dispatch(*args, **kwargs)
358 @property
359 def _page(self):
360 """ The current page
362 There seems to be no other pre-defined method for getting the current
363 page, see
364 https://docs.djangoproject.com/en/dev/ref/class-based-views/mixins-multiple-object/#multipleobjectmixin
366 return self.get_context_data()['page_obj']
368 def page_list(self, page_size=15):
369 """ Return a list of pages, eg [1, 2, 3, '...', 6, 7, 8] """
370 page = self._page
371 return get_page_list(1,
372 page.paginator.num_pages,
373 page.number,
374 page.paginator.per_page,
377 def max_subscribers(self):
378 """ Maximum subscribers of the podcasts on this page """
379 page = self._page
380 podcasts = page.object_list
381 return max([p.subscriber_count() for p in podcasts] + [0])
384 class FlattrPodcastList(PodcastListView):
385 """ Lists podcasts that have Flattr payment URLs """
387 template_name = 'flattr-podcasts.html'
389 def get_queryset(self):
390 return Podcast.objects.flattr()
392 def get_context_data(self, num=100):
393 context = super(FlattrPodcastList, self).get_context_data()
394 context['flattr_auth'] = (self.request.user.is_authenticated()
395 # and bool(self.request.user.get_wksetting(FLATTR_TOKEN))
397 return context
400 class LicensePodcastList(PodcastListView):
401 """ Lists podcasts with a given license """
403 template_name = 'directory/license-podcasts.html'
405 def get_queryset(self):
406 return Podcast.objects.license(self.license_url)
408 @property
409 def license_url(self):
410 return self.kwargs['license_url']
413 class LicenseList(TemplateView):
414 """ Lists all podcast licenses """
416 template_name = 'directory/licenses.html'
418 def licenses(self):
419 """ Returns all podcast licenses """
420 query = Podcast.objects.exclude(license__isnull=True)
421 values = query.values("license").annotate(Count("id")).order_by()
423 counter = Counter({l['license']: l['id__count'] for l in values})
424 return counter.most_common()