[PodcastLists] migration to Django ORM
[mygpo.git] / mygpo / directory / views.py
blobd7e0dd25450988ac13848b8c074424480db0cbae
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.core.paginator import Paginator, EmptyPage, PageNotAnInteger
10 from django.shortcuts import render
11 from django.db.models import Count
12 from django.contrib.sites.models import RequestSite
13 from django.views.decorators.cache import cache_control
14 from django.views.decorators.vary import vary_on_cookie
15 from django.views.generic import ListView
16 from django.utils.decorators import method_decorator
17 from django.views.generic.base import View, TemplateView
18 from django.contrib.auth.decorators import login_required
19 from django.contrib import messages
20 from django.utils.translation import ugettext as _
21 from django.contrib.auth import get_user_model
23 from feedservice.parse.models import ParserException
24 from feedservice.parse import FetchFeedException
26 from mygpo.core.proxy import proxy_object
27 from mygpo.podcasts.models import Podcast, Episode
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.podcastlists.models import PodcastList
34 from mygpo.data.feeddownloader import PodcastUpdater, NoEpisodesException
35 from mygpo.data.tasks import update_podcasts
36 from mygpo.db.couchdb.directory import category_for_tag
39 class ToplistView(TemplateView):
40 """ Generic Top List view """
42 @method_decorator(vary_on_cookie)
43 @method_decorator(cache_control(private=True))
44 def dispatch(self, *args, **kwargs):
45 """ Only used for applying decorators """
46 return super(ToplistView, self).dispatch(*args, **kwargs)
48 def all_languages(self):
49 """ Returns all 2-letter language codes that are used by podcasts.
51 It filters obviously invalid strings, but does not check if any
52 of these codes is contained in ISO 639. """
54 query = Podcast.objects.exclude(language__isnull=True)
55 query = query.distinct('language').values('language')
57 langs = [o['language'] for o in query]
58 langs = sorted(sanitize_language_codes(langs))
60 return get_language_names(langs)
62 def language(self):
63 """ Currently selected language """
64 return process_lang_params(self.request)
66 def site(self):
67 """ Current site for constructing absolute links """
68 return RequestSite(self.request)
71 class PodcastToplistView(ToplistView):
72 """ Most subscribed podcasts """
74 template_name = 'toplist.html'
76 def get_context_data(self, num=100):
77 context = super(PodcastToplistView, self).get_context_data()
79 entries = Podcast.objects.all()\
80 .prefetch_related('slugs')\
81 .toplist(self.language())[:num]
82 context['entries'] = entries
84 context['max_subscribers'] = max([0] + [p.subscriber_count() for p in entries])
86 return context
89 class EpisodeToplistView(ToplistView):
90 """ Most listened-to episodes """
92 template_name = 'episode_toplist.html'
94 def get_context_data(self, num=100):
95 context = super(EpisodeToplistView, self).get_context_data()
97 entries = Episode.objects.all()\
98 .select_related('podcast')\
99 .prefetch_related('slugs', 'podcast__slugs')\
100 .toplist(self.language())[:num]
101 context['entries'] = entries
103 # Determine maximum listener amount (or 0 if no entries exist)
104 context['max_listeners'] = max([0]+[e.listeners for e in entries])
106 return context
109 class Carousel(View):
110 """ A carousel demo """
112 @method_decorator(cache_control(private=True))
113 @method_decorator(vary_on_cookie)
114 def get(self, request):
116 return render(request, 'carousel.html', {
117 # evaluated lazyly, cached by template
118 'topics': Topics(),
122 class Directory(View):
123 """ The main directory page """
125 @method_decorator(cache_control(private=True))
126 @method_decorator(vary_on_cookie)
127 def get(self, request):
129 return render(request, 'directory.html', {
131 # evaluated lazyly, cached by template
132 'topics': Topics(),
133 'podcastlists': self.get_random_list(),
134 'random_podcast': Podcast.objects.all().random().first(),
138 def get_random_list(self, podcasts_per_list=5):
139 random_list = PodcastList.objects.order_by('?').first()
140 yield random_list
143 @cache_control(private=True)
144 @vary_on_cookie
145 def category(request, category, page_size=20):
146 category = category_for_tag(category)
147 if not category:
148 return HttpResponseNotFound()
150 # Make sure page request is an int. If not, deliver first page.
151 try:
152 page = int(request.GET.get('page', '1'))
153 except ValueError:
154 page = 1
156 entries = category.get_podcasts( (page-1) * page_size, page*page_size )
157 podcasts = filter(None, entries)
158 num_pages = int(ceil(len(category.podcasts) / page_size))
160 page_list = get_page_list(1, num_pages, page, 15)
162 return render(request, 'category.html', {
163 'entries': podcasts,
164 'category': category.label,
165 'page_list': page_list,
170 RESULTS_PER_PAGE=20
172 @cache_control(private=True)
173 @vary_on_cookie
174 def search(request, template='search.html', args={}):
176 if 'q' in request.GET:
177 q = request.GET.get('q', '').encode('utf-8')
179 try:
180 page = int(request.GET.get('page', 1))
181 except ValueError:
182 page = 1
184 start = RESULTS_PER_PAGE*(page-1)
185 results = search_podcasts(q)
186 total = len(results)
187 num_pages = int(ceil(total / RESULTS_PER_PAGE))
188 results = results[start:start+RESULTS_PER_PAGE]
190 page_list = get_page_list(1, num_pages, page, 15)
192 else:
193 results = []
194 q = None
195 page_list = []
197 max_subscribers = max([p.subscribers for p in results] + [0])
199 current_site = RequestSite(request)
201 return render(request, template, dict(
202 q= q,
203 results= results,
204 page_list= page_list,
205 max_subscribers= max_subscribers,
206 domain= current_site.domain,
207 **args
212 @cache_control(private=True)
213 @vary_on_cookie
214 def podcast_lists(request, page_size=20):
216 lists = PodcastList.objects.all()\
217 .annotate(num_votes=Count('votes'))\
218 .order_by('-num_votes')
220 paginator = Paginator(lists, page_size)
222 page = request.GET.get('page')
223 try:
224 lists = paginator.page(page)
225 except PageNotAnInteger:
226 lists = paginator.page(1)
227 except EmptyPage:
228 lists = paginator.page(paginator.num_pages)
230 num_pages = int(ceil(PodcastList.objects.count() / float(page_size)))
231 page_list = get_page_list(1, num_pages, page, 15)
233 return render(request, 'podcast_lists.html', {
234 'lists': lists,
235 'page_list': page_list,
240 class MissingPodcast(View):
241 """ Check if a podcast is missing """
243 @method_decorator(login_required)
244 def get(self, request):
246 site = RequestSite(request)
248 # check if we're doing a query
249 url = request.GET.get('q', None)
251 if not url:
252 podcast = None
253 can_add = False
255 else:
256 try:
257 podcast = Podcast.objects.get(urls__url=url)
258 can_add = False
260 except Podcast.DoesNotExist:
261 # check if we could add a podcast for the given URL
262 podcast = False
263 updater = PodcastUpdater()
265 try:
266 can_add = updater.verify_podcast_url(url)
268 except (ParserException, FetchFeedException,
269 NoEpisodesException) as ex:
270 can_add = False
271 messages.error(request, unicode(ex))
273 return render(request, 'missing.html', {
274 'site': site,
275 'q': url,
276 'podcast': podcast,
277 'can_add': can_add,
281 class AddPodcast(View):
282 """ Add a missing podcast"""
284 @method_decorator(login_required)
285 @method_decorator(cache_control(private=True))
286 @method_decorator(vary_on_cookie)
287 def post(self, request):
289 url = request.POST.get('url', None)
291 if not url:
292 raise Http404
294 res = update_podcasts.delay([url])
296 return HttpResponseRedirect(reverse('add-podcast-status',
297 args=[res.task_id]))
300 class AddPodcastStatus(TemplateView):
301 """ Status of adding a podcast """
303 template_name = 'directory/add-podcast-status.html'
305 def get(self, request, task_id):
306 result = update_podcasts.AsyncResult(task_id)
308 if not result.ready():
309 return self.render_to_response({
310 'ready': False,
313 try:
314 podcasts = result.get()
315 messages.success(request, _('%d podcasts added' % len(podcasts)))
317 except (ParserException, FetchFeedException,
318 NoEpisodesException) as ex:
319 messages.error(request, str(ex))
320 podcast = None
322 return self.render_to_response({
323 'ready': True,
324 'podcasts': podcasts,
328 class PodcastListView(ListView):
329 """ A generic podcast list view """
331 paginate_by = 15
332 context_object_name = 'podcasts'
334 @method_decorator(vary_on_cookie)
335 @method_decorator(cache_control(private=True))
336 def dispatch(self, *args, **kwargs):
337 """ Only used for applying decorators """
338 return super(PodcastListView, self).dispatch(*args, **kwargs)
340 @property
341 def _page(self):
342 """ The current page
344 There seems to be no other pre-defined method for getting the current
345 page, see
346 https://docs.djangoproject.com/en/dev/ref/class-based-views/mixins-multiple-object/#multipleobjectmixin
348 return self.get_context_data()['page_obj']
350 def page_list(self, page_size=15):
351 """ Return a list of pages, eg [1, 2, 3, '...', 6, 7, 8] """
352 page = self._page
353 return get_page_list(1,
354 page.paginator.num_pages,
355 page.number,
356 page.paginator.per_page,
359 def max_subscribers(self):
360 """ Maximum subscribers of the podcasts on this page """
361 page = self._page
362 podcasts = page.object_list
363 return max([p.subscriber_count() for p in podcasts] + [0])
366 class FlattrPodcastList(PodcastListView):
367 """ Lists podcasts that have Flattr payment URLs """
369 template_name = 'flattr-podcasts.html'
371 def get_queryset(self):
372 return Podcast.objects.all().flattr()
374 def get_context_data(self, num=100):
375 context = super(FlattrPodcastList, self).get_context_data()
376 context['flattr_auth'] = (self.request.user.is_authenticated()
377 # and bool(self.request.user.get_wksetting(FLATTR_TOKEN))
379 return context
382 class LicensePodcastList(PodcastListView):
383 """ Lists podcasts with a given license """
385 template_name = 'directory/license-podcasts.html'
387 def get_queryset(self):
388 return Podcast.objects.all().license(self.license_url)
390 @property
391 def license_url(self):
392 return self.kwargs['license_url']
395 class LicenseList(TemplateView):
396 """ Lists all podcast licenses """
398 template_name = 'directory/licenses.html'
400 def licenses(self):
401 """ Returns all podcast licenses """
402 query = Podcast.objects.exclude(license__isnull=True)
403 values = query.values("license").annotate(Count("id")).order_by()
405 counter = Counter({l['license']: l['id__count'] for l in values})
406 return counter.most_common()