Merge branch 'master' into py3
[mygpo.git] / mygpo / directory / views.py
blob95912255c59f551c8bad71234dae589115651ce6
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.podcasts.models import Podcast, Episode
27 from mygpo.directory.search import search_podcasts
28 from mygpo.web.utils import process_lang_params, get_language_names, \
29 get_page_list, get_podcast_link_target, sanitize_language_codes
30 from mygpo.directory.tags import Topics
31 from mygpo.users.settings import FLATTR_TOKEN
32 from mygpo.categories.models import Category
33 from mygpo.podcastlists.models import PodcastList
34 from mygpo.data.feeddownloader import PodcastUpdater, NoEpisodesException
35 from mygpo.data.tasks import update_podcasts
38 class ToplistView(TemplateView):
39 """ Generic Top List view """
41 @method_decorator(vary_on_cookie)
42 @method_decorator(cache_control(private=True))
43 def dispatch(self, *args, **kwargs):
44 """ Only used for applying decorators """
45 return super(ToplistView, self).dispatch(*args, **kwargs)
47 def all_languages(self):
48 """ Returns all 2-letter language codes that are used by podcasts.
50 It filters obviously invalid strings, but does not check if any
51 of these codes is contained in ISO 639. """
53 query = Podcast.objects.exclude(language__isnull=True)
54 query = query.distinct('language').values('language')
56 langs = [o['language'] for o in query]
57 langs = sorted(sanitize_language_codes(langs))
59 return get_language_names(langs)
61 def language(self):
62 """ Currently selected language """
63 return process_lang_params(self.request)
65 def site(self):
66 """ Current site for constructing absolute links """
67 return RequestSite(self.request)
70 class PodcastToplistView(ToplistView):
71 """ Most subscribed podcasts """
73 template_name = 'toplist.html'
75 def get_context_data(self, num=100):
76 context = super(PodcastToplistView, self).get_context_data()
78 entries = Podcast.objects.all()\
79 .prefetch_related('slugs')\
80 .toplist(self.language())[:num]
81 context['entries'] = entries
83 context['max_subscribers'] = max([0] + [p.subscriber_count() for p in entries])
85 return context
88 class EpisodeToplistView(ToplistView):
89 """ Most listened-to episodes """
91 template_name = 'episode_toplist.html'
93 def get_context_data(self, num=100):
94 context = super(EpisodeToplistView, self).get_context_data()
96 entries = Episode.objects.all()\
97 .select_related('podcast')\
98 .prefetch_related('slugs', 'podcast__slugs')\
99 .toplist(self.language())[:num]
100 context['entries'] = entries
102 # Determine maximum listener amount (or 0 if no entries exist)
103 context['max_listeners'] = max([0]+[e.listeners for e in entries])
105 return context
108 class Carousel(View):
109 """ A carousel demo """
111 @method_decorator(cache_control(private=True))
112 @method_decorator(vary_on_cookie)
113 def get(self, request):
115 return render(request, 'carousel.html', {
116 # evaluated lazyly, cached by template
117 'topics': Topics(),
121 class Directory(View):
122 """ The main directory page """
124 @method_decorator(cache_control(private=True))
125 @method_decorator(vary_on_cookie)
126 def get(self, request):
128 return render(request, 'directory.html', {
130 # evaluated lazyly, cached by template
131 'topics': Topics(),
132 'podcastlists': self.get_random_list(),
133 'random_podcast': Podcast.objects.all().random().first(),
137 def get_random_list(self, podcasts_per_list=5):
138 random_list = PodcastList.objects.order_by('?').first()
139 yield random_list
142 @cache_control(private=True)
143 @vary_on_cookie
144 def category(request, category, page_size=20):
145 try:
146 category = Category.objects.get(tags__tag=category)
147 except Category.DoesNotExist:
148 return HttpResponseNotFound()
150 podcasts = category.entries.all()\
151 .prefetch_related('podcast', 'podcast__slugs')
153 paginator = Paginator(podcasts, page_size)
155 page = request.GET.get('page')
156 try:
157 podcasts = paginator.page(page)
158 except PageNotAnInteger:
159 # If page is not an integer, deliver first page.
160 podcasts = paginator.page(1)
161 except EmptyPage:
162 # If page is out of range (e.g. 9999), deliver last page of results.
163 podcasts = paginator.page(paginator.num_pages)
165 page_list = get_page_list(1, paginator.num_pages, podcasts.number, 15)
167 return render(request, 'category.html', {
168 'entries': podcasts,
169 'category': category.title,
170 'page_list': page_list,
175 RESULTS_PER_PAGE=20
177 @cache_control(private=True)
178 @vary_on_cookie
179 def search(request, template='search.html', args={}):
181 if 'q' in request.GET:
182 q = request.GET.get('q', '').encode('utf-8')
184 try:
185 page = int(request.GET.get('page', 1))
186 except ValueError:
187 page = 1
189 start = RESULTS_PER_PAGE*(page-1)
190 results = search_podcasts(q)
191 total = len(results)
192 num_pages = int(ceil(total / RESULTS_PER_PAGE))
193 results = results[start:start+RESULTS_PER_PAGE]
195 page_list = get_page_list(1, num_pages, page, 15)
197 else:
198 results = []
199 q = None
200 page_list = []
202 max_subscribers = max([p.subscribers for p in results] + [0])
204 current_site = RequestSite(request)
206 return render(request, template, dict(
207 q= q,
208 results= results,
209 page_list= page_list,
210 max_subscribers= max_subscribers,
211 domain= current_site.domain,
212 **args
217 @cache_control(private=True)
218 @vary_on_cookie
219 def podcast_lists(request, page_size=20):
221 lists = PodcastList.objects.all()\
222 .annotate(num_votes=Count('votes'))\
223 .order_by('-num_votes')
225 paginator = Paginator(lists, page_size)
227 page = request.GET.get('page')
228 try:
229 lists = paginator.page(page)
230 except PageNotAnInteger:
231 lists = paginator.page(1)
232 page = 1
233 except EmptyPage:
234 lists = paginator.page(paginator.num_pages)
235 page = paginator.num_pages
237 num_pages = int(ceil(PodcastList.objects.count() / float(page_size)))
238 page_list = get_page_list(1, num_pages, int(page), 15)
240 return render(request, 'podcast_lists.html', {
241 'lists': lists,
242 'page_list': page_list,
247 class MissingPodcast(View):
248 """ Check if a podcast is missing """
250 @method_decorator(login_required)
251 def get(self, request):
253 site = RequestSite(request)
255 # check if we're doing a query
256 url = request.GET.get('q', None)
258 if not url:
259 podcast = None
260 can_add = False
262 else:
263 try:
264 podcast = Podcast.objects.get(urls__url=url)
265 can_add = False
267 except Podcast.DoesNotExist:
268 # check if we could add a podcast for the given URL
269 podcast = False
270 updater = PodcastUpdater()
272 try:
273 can_add = updater.verify_podcast_url(url)
275 except (ParserException, FetchFeedException,
276 NoEpisodesException) as ex:
277 can_add = False
278 messages.error(request, str(ex))
280 return render(request, 'missing.html', {
281 'site': site,
282 'q': url,
283 'podcast': podcast,
284 'can_add': can_add,
288 class AddPodcast(View):
289 """ Add a missing podcast"""
291 @method_decorator(login_required)
292 @method_decorator(cache_control(private=True))
293 @method_decorator(vary_on_cookie)
294 def post(self, request):
296 url = request.POST.get('url', None)
298 if not url:
299 raise Http404
301 res = update_podcasts.delay([url])
303 return HttpResponseRedirect(reverse('add-podcast-status',
304 args=[res.task_id]))
307 class AddPodcastStatus(TemplateView):
308 """ Status of adding a podcast """
310 template_name = 'directory/add-podcast-status.html'
312 def get(self, request, task_id):
313 result = update_podcasts.AsyncResult(task_id)
315 if not result.ready():
316 return self.render_to_response({
317 'ready': False,
320 try:
321 podcasts = result.get()
322 messages.success(request, _('%d podcasts added' % len(podcasts)))
324 except (ParserException, FetchFeedException,
325 NoEpisodesException) as ex:
326 messages.error(request, str(ex))
327 podcast = None
329 return self.render_to_response({
330 'ready': True,
331 'podcasts': podcasts,
335 class PodcastListView(ListView):
336 """ A generic podcast list view """
338 paginate_by = 15
339 context_object_name = 'podcasts'
341 @method_decorator(vary_on_cookie)
342 @method_decorator(cache_control(private=True))
343 def dispatch(self, *args, **kwargs):
344 """ Only used for applying decorators """
345 return super(PodcastListView, self).dispatch(*args, **kwargs)
347 @property
348 def _page(self):
349 """ The current page
351 There seems to be no other pre-defined method for getting the current
352 page, see
353 https://docs.djangoproject.com/en/dev/ref/class-based-views/mixins-multiple-object/#multipleobjectmixin
355 return self.get_context_data()['page_obj']
357 def page_list(self, page_size=15):
358 """ Return a list of pages, eg [1, 2, 3, '...', 6, 7, 8] """
359 page = self._page
360 return get_page_list(1,
361 page.paginator.num_pages,
362 page.number,
363 page.paginator.per_page,
366 def max_subscribers(self):
367 """ Maximum subscribers of the podcasts on this page """
368 page = self._page
369 podcasts = page.object_list
370 return max([p.subscriber_count() for p in podcasts] + [0])
373 class FlattrPodcastList(PodcastListView):
374 """ Lists podcasts that have Flattr payment URLs """
376 template_name = 'flattr-podcasts.html'
378 def get_queryset(self):
379 return Podcast.objects.all().flattr()
381 def get_context_data(self, num=100):
382 context = super(FlattrPodcastList, self).get_context_data()
383 context['flattr_auth'] = (self.request.user.is_authenticated()
384 # and bool(self.request.user.get_wksetting(FLATTR_TOKEN))
386 return context
389 class LicensePodcastList(PodcastListView):
390 """ Lists podcasts with a given license """
392 template_name = 'directory/license-podcasts.html'
394 def get_queryset(self):
395 return Podcast.objects.all().license(self.license_url)
397 @property
398 def license_url(self):
399 return self.kwargs['license_url']
402 class LicenseList(TemplateView):
403 """ Lists all podcast licenses """
405 template_name = 'directory/licenses.html'
407 def licenses(self):
408 """ Returns all podcast licenses """
409 query = Podcast.objects.exclude(license__isnull=True)
410 values = query.values("license").annotate(Count("id")).order_by()
412 counter = Counter({l['license']: l['id__count'] for l in values})
413 return counter.most_common()