[Categories] use categories from Django ORM
[mygpo.git] / mygpo / directory / views.py
blob2edbba90379fb7260ef2af562e03ff8423c83eb6
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.categories.models import Category
34 from mygpo.podcastlists.models import PodcastList
35 from mygpo.data.feeddownloader import PodcastUpdater, NoEpisodesException
36 from mygpo.data.tasks import update_podcasts
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 try:
147 category = Category.objects.get(tags__tag=category)
148 except Category.DoesNotExist:
149 return HttpResponseNotFound()
151 podcasts = category.entries.all()\
152 .prefetch_related('podcast', 'podcast__slugs')
154 paginator = Paginator(podcasts, page_size)
156 page = request.GET.get('page')
157 try:
158 podcasts = paginator.page(page)
159 except PageNotAnInteger:
160 # If page is not an integer, deliver first page.
161 podcasts = paginator.page(1)
162 except EmptyPage:
163 # If page is out of range (e.g. 9999), deliver last page of results.
164 podcasts = paginator.page(paginator.num_pages)
166 page_list = get_page_list(1, paginator.num_pages, podcasts.number, 15)
168 return render(request, 'category.html', {
169 'entries': podcasts,
170 'category': category.title,
171 'page_list': page_list,
176 RESULTS_PER_PAGE=20
178 @cache_control(private=True)
179 @vary_on_cookie
180 def search(request, template='search.html', args={}):
182 if 'q' in request.GET:
183 q = request.GET.get('q', '').encode('utf-8')
185 try:
186 page = int(request.GET.get('page', 1))
187 except ValueError:
188 page = 1
190 start = RESULTS_PER_PAGE*(page-1)
191 results = search_podcasts(q)
192 total = len(results)
193 num_pages = int(ceil(total / RESULTS_PER_PAGE))
194 results = results[start:start+RESULTS_PER_PAGE]
196 page_list = get_page_list(1, num_pages, page, 15)
198 else:
199 results = []
200 q = None
201 page_list = []
203 max_subscribers = max([p.subscribers for p in results] + [0])
205 current_site = RequestSite(request)
207 return render(request, template, dict(
208 q= q,
209 results= results,
210 page_list= page_list,
211 max_subscribers= max_subscribers,
212 domain= current_site.domain,
213 **args
218 @cache_control(private=True)
219 @vary_on_cookie
220 def podcast_lists(request, page_size=20):
222 lists = PodcastList.objects.all()\
223 .annotate(num_votes=Count('votes'))\
224 .order_by('-num_votes')
226 paginator = Paginator(lists, page_size)
228 page = request.GET.get('page')
229 try:
230 lists = paginator.page(page)
231 except PageNotAnInteger:
232 lists = paginator.page(1)
233 page = 1
234 except EmptyPage:
235 lists = paginator.page(paginator.num_pages)
236 page = paginator.num_pages
238 num_pages = int(ceil(PodcastList.objects.count() / float(page_size)))
239 page_list = get_page_list(1, num_pages, page, 15)
241 return render(request, 'podcast_lists.html', {
242 'lists': lists,
243 'page_list': page_list,
248 class MissingPodcast(View):
249 """ Check if a podcast is missing """
251 @method_decorator(login_required)
252 def get(self, request):
254 site = RequestSite(request)
256 # check if we're doing a query
257 url = request.GET.get('q', None)
259 if not url:
260 podcast = None
261 can_add = False
263 else:
264 try:
265 podcast = Podcast.objects.get(urls__url=url)
266 can_add = False
268 except Podcast.DoesNotExist:
269 # check if we could add a podcast for the given URL
270 podcast = False
271 updater = PodcastUpdater()
273 try:
274 can_add = updater.verify_podcast_url(url)
276 except (ParserException, FetchFeedException,
277 NoEpisodesException) as ex:
278 can_add = False
279 messages.error(request, unicode(ex))
281 return render(request, 'missing.html', {
282 'site': site,
283 'q': url,
284 'podcast': podcast,
285 'can_add': can_add,
289 class AddPodcast(View):
290 """ Add a missing podcast"""
292 @method_decorator(login_required)
293 @method_decorator(cache_control(private=True))
294 @method_decorator(vary_on_cookie)
295 def post(self, request):
297 url = request.POST.get('url', None)
299 if not url:
300 raise Http404
302 res = update_podcasts.delay([url])
304 return HttpResponseRedirect(reverse('add-podcast-status',
305 args=[res.task_id]))
308 class AddPodcastStatus(TemplateView):
309 """ Status of adding a podcast """
311 template_name = 'directory/add-podcast-status.html'
313 def get(self, request, task_id):
314 result = update_podcasts.AsyncResult(task_id)
316 if not result.ready():
317 return self.render_to_response({
318 'ready': False,
321 try:
322 podcasts = result.get()
323 messages.success(request, _('%d podcasts added' % len(podcasts)))
325 except (ParserException, FetchFeedException,
326 NoEpisodesException) as ex:
327 messages.error(request, str(ex))
328 podcast = None
330 return self.render_to_response({
331 'ready': True,
332 'podcasts': podcasts,
336 class PodcastListView(ListView):
337 """ A generic podcast list view """
339 paginate_by = 15
340 context_object_name = 'podcasts'
342 @method_decorator(vary_on_cookie)
343 @method_decorator(cache_control(private=True))
344 def dispatch(self, *args, **kwargs):
345 """ Only used for applying decorators """
346 return super(PodcastListView, self).dispatch(*args, **kwargs)
348 @property
349 def _page(self):
350 """ The current page
352 There seems to be no other pre-defined method for getting the current
353 page, see
354 https://docs.djangoproject.com/en/dev/ref/class-based-views/mixins-multiple-object/#multipleobjectmixin
356 return self.get_context_data()['page_obj']
358 def page_list(self, page_size=15):
359 """ Return a list of pages, eg [1, 2, 3, '...', 6, 7, 8] """
360 page = self._page
361 return get_page_list(1,
362 page.paginator.num_pages,
363 page.number,
364 page.paginator.per_page,
367 def max_subscribers(self):
368 """ Maximum subscribers of the podcasts on this page """
369 page = self._page
370 podcasts = page.object_list
371 return max([p.subscriber_count() for p in podcasts] + [0])
374 class FlattrPodcastList(PodcastListView):
375 """ Lists podcasts that have Flattr payment URLs """
377 template_name = 'flattr-podcasts.html'
379 def get_queryset(self):
380 return Podcast.objects.all().flattr()
382 def get_context_data(self, num=100):
383 context = super(FlattrPodcastList, self).get_context_data()
384 context['flattr_auth'] = (self.request.user.is_authenticated()
385 # and bool(self.request.user.get_wksetting(FLATTR_TOKEN))
387 return context
390 class LicensePodcastList(PodcastListView):
391 """ Lists podcasts with a given license """
393 template_name = 'directory/license-podcasts.html'
395 def get_queryset(self):
396 return Podcast.objects.all().license(self.license_url)
398 @property
399 def license_url(self):
400 return self.kwargs['license_url']
403 class LicenseList(TemplateView):
404 """ Lists all podcast licenses """
406 template_name = 'directory/licenses.html'
408 def licenses(self):
409 """ Returns all podcast licenses """
410 query = Podcast.objects.exclude(license__isnull=True)
411 values = query.values("license").annotate(Count("id")).order_by()
413 counter = Counter({l['license']: l['id__count'] for l in values})
414 return counter.most_common()