Merge branch 'master' into pg-search
[mygpo.git] / mygpo / directory / views.py
blobf7118fefbc9853cb14a50ac203c091e8ced12360
4 from math import ceil
5 from collections import Counter
7 from django.http import HttpResponseNotFound, Http404, HttpResponseRedirect
8 from django.urls 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.requests 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 import View
18 from django.views.generic import TemplateView
19 from django.contrib.auth.decorators import login_required
20 from django.contrib import messages
21 from django.utils.translation import ugettext as _
22 from django.contrib.auth import get_user_model
24 from mygpo.podcasts.models import Podcast, Episode
25 from mygpo.directory.search import search_podcasts
26 from mygpo.web.utils import process_lang_params, get_language_names, \
27 get_page_list, get_podcast_link_target, sanitize_language_codes
28 from mygpo.directory.tags import Topics
29 from mygpo.users.settings import FLATTR_TOKEN
30 from mygpo.categories.models import Category
31 from mygpo.podcastlists.models import PodcastList
32 from mygpo.data.feeddownloader import (verify_podcast_url, NoEpisodesException,
33 UpdatePodcastException)
34 from mygpo.data.tasks import update_podcasts
37 class ToplistView(TemplateView):
38 """ Generic Top List view """
40 @method_decorator(vary_on_cookie)
41 @method_decorator(cache_control(private=True))
42 def dispatch(self, *args, **kwargs):
43 """ Only used for applying decorators """
44 return super(ToplistView, self).dispatch(*args, **kwargs)
46 def all_languages(self):
47 """ Returns all 2-letter language codes that are used by podcasts.
49 It filters obviously invalid strings, but does not check if any
50 of these codes is contained in ISO 639. """
52 query = Podcast.objects.exclude(language__isnull=True)
53 query = query.distinct('language').values('language')
55 langs = [o['language'] for o in query]
56 langs = sorted(sanitize_language_codes(langs))
58 return get_language_names(langs)
60 def language(self):
61 """ Currently selected language """
62 return process_lang_params(self.request)
64 def site(self):
65 """ Current site for constructing absolute links """
66 return RequestSite(self.request)
69 class PodcastToplistView(ToplistView):
70 """ Most subscribed podcasts """
72 template_name = 'toplist.html'
74 def get_context_data(self, num=100):
75 context = super(PodcastToplistView, self).get_context_data()
77 entries = Podcast.objects.all()\
78 .prefetch_related('slugs')\
79 .toplist(self.language())[:num]
80 context['entries'] = entries
82 context['max_subscribers'] = max([0] + [p.subscriber_count() for p in entries])
84 return context
87 class EpisodeToplistView(ToplistView):
88 """ Most listened-to episodes """
90 template_name = 'episode_toplist.html'
92 def get_context_data(self, num=100):
93 context = super(EpisodeToplistView, self).get_context_data()
95 entries = Episode.objects.all()\
96 .select_related('podcast')\
97 .prefetch_related('slugs', 'podcast__slugs')\
98 .toplist(self.language())[:num]
99 context['entries'] = entries
101 # Determine maximum listener amount (or 0 if no entries exist)
102 listeners = [e.listeners for e in entries if e.listeners is not None]
103 max_listeners = max(listeners, default=0)
104 context['max_listeners'] = max_listeners
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(),
135 'podcast_ad': Podcast.objects.get_advertised_podcast(),
139 def get_random_list(self, podcasts_per_list=5):
140 random_list = PodcastList.objects.order_by('?').first()
141 yield random_list
144 @cache_control(private=True)
145 @vary_on_cookie
146 def category(request, category, page_size=20):
147 try:
148 category = Category.objects.get(tags__tag=category)
149 except Category.DoesNotExist:
150 return HttpResponseNotFound()
152 podcasts = category.entries.all()\
153 .prefetch_related('podcast', 'podcast__slugs')
155 paginator = Paginator(podcasts, page_size)
157 page = request.GET.get('page')
158 try:
159 podcasts = paginator.page(page)
160 except PageNotAnInteger:
161 # If page is not an integer, deliver first page.
162 podcasts = paginator.page(1)
163 except EmptyPage:
164 # If page is out of range (e.g. 9999), deliver last page of results.
165 podcasts = paginator.page(paginator.num_pages)
167 page_list = get_page_list(1, paginator.num_pages, podcasts.number, 15)
169 return render(request, 'category.html', {
170 'entries': podcasts,
171 'category': category.title,
172 'page_list': page_list,
177 RESULTS_PER_PAGE=20
179 @cache_control(private=True)
180 @vary_on_cookie
181 def search(request, template='search.html', args={}):
183 if 'q' in request.GET:
184 q = request.GET.get('q', '')
186 try:
187 page = int(request.GET.get('page', 1))
188 except ValueError:
189 page = 1
191 start = RESULTS_PER_PAGE*(page-1)
192 results = search_podcasts(q)
193 total = len(results)
194 num_pages = int(ceil(total / RESULTS_PER_PAGE))
195 results = results[start:start+RESULTS_PER_PAGE]
197 page_list = get_page_list(1, num_pages, page, 15)
199 else:
200 results = []
201 q = None
202 page_list = []
204 max_subscribers = max([p.subscribers for p in results] + [0])
206 current_site = RequestSite(request)
208 return render(request, template, dict(
209 q= q,
210 results= results,
211 page_list= page_list,
212 max_subscribers= max_subscribers,
213 domain= current_site.domain,
214 **args
219 @cache_control(private=True)
220 @vary_on_cookie
221 def podcast_lists(request, page_size=20):
223 lists = PodcastList.objects.all()\
224 .annotate(num_votes=Count('votes'))\
225 .order_by('-num_votes')
227 paginator = Paginator(lists, page_size)
229 page = request.GET.get('page')
230 try:
231 lists = paginator.page(page)
232 except PageNotAnInteger:
233 lists = paginator.page(1)
234 page = 1
235 except EmptyPage:
236 lists = paginator.page(paginator.num_pages)
237 page = paginator.num_pages
239 num_pages = int(ceil(PodcastList.objects.count() / float(page_size)))
240 page_list = get_page_list(1, num_pages, int(page), 15)
242 return render(request, 'podcast_lists.html', {
243 'lists': lists,
244 'page_list': page_list,
249 class MissingPodcast(View):
250 """ Check if a podcast is missing """
252 @method_decorator(login_required)
253 def get(self, request):
255 site = RequestSite(request)
257 # check if we're doing a query
258 url = request.GET.get('q', None)
260 if not url:
261 podcast = None
262 can_add = False
264 else:
265 try:
266 podcast = Podcast.objects.get(urls__url=url)
267 can_add = False
269 except Podcast.DoesNotExist:
270 # check if we could add a podcast for the given URL
271 podcast = False
272 try:
273 can_add = verify_podcast_url(url)
275 except (UpdatePodcastException, NoEpisodesException) as ex:
276 can_add = False
277 messages.error(request, str(ex))
279 return render(request, 'missing.html', {
280 'site': site,
281 'q': url,
282 'podcast': podcast,
283 'can_add': can_add,
287 class AddPodcast(View):
288 """ Add a missing podcast"""
290 @method_decorator(login_required)
291 @method_decorator(cache_control(private=True))
292 @method_decorator(vary_on_cookie)
293 def post(self, request):
295 url = request.POST.get('url', None)
297 if not url:
298 raise Http404
300 res = update_podcasts.delay([url])
302 return HttpResponseRedirect(reverse('add-podcast-status',
303 args=[res.task_id]))
306 class AddPodcastStatus(TemplateView):
307 """ Status of adding a podcast """
309 template_name = 'directory/add-podcast-status.html'
311 def get(self, request, task_id):
312 result = update_podcasts.AsyncResult(task_id)
314 if not result.ready():
315 return self.render_to_response({
316 'ready': False,
319 try:
320 podcasts = result.get()
321 messages.success(request, _('%d podcasts added' % len(podcasts)))
323 except (UpdatePodcastException, NoEpisodesException) as ex:
324 messages.error(request, str(ex))
325 podcast = None
327 return self.render_to_response({
328 'ready': True,
329 'podcasts': podcasts,
333 class PodcastListView(ListView):
334 """ A generic podcast list view """
336 paginate_by = 15
337 context_object_name = 'podcasts'
339 @method_decorator(vary_on_cookie)
340 @method_decorator(cache_control(private=True))
341 def dispatch(self, *args, **kwargs):
342 """ Only used for applying decorators """
343 return super(PodcastListView, self).dispatch(*args, **kwargs)
345 @property
346 def _page(self):
347 """ The current page
349 There seems to be no other pre-defined method for getting the current
350 page, see
351 https://docs.djangoproject.com/en/dev/ref/class-based-views/mixins-multiple-object/#multipleobjectmixin
353 return self.get_context_data()['page_obj']
355 def page_list(self, page_size=15):
356 """ Return a list of pages, eg [1, 2, 3, '...', 6, 7, 8] """
357 page = self._page
358 return get_page_list(1,
359 page.paginator.num_pages,
360 page.number,
361 page.paginator.per_page,
364 def max_subscribers(self):
365 """ Maximum subscribers of the podcasts on this page """
366 page = self._page
367 podcasts = page.object_list
368 return max([p.subscriber_count() for p in podcasts] + [0])
371 class FlattrPodcastList(PodcastListView):
372 """ Lists podcasts that have Flattr payment URLs """
374 template_name = 'flattr-podcasts.html'
376 def get_queryset(self):
377 return Podcast.objects.all().flattr()
379 def get_context_data(self, num=100):
380 context = super(FlattrPodcastList, self).get_context_data()
381 context['flattr_auth'] = (self.request.user.is_authenticated
382 # and bool(self.request.user.get_wksetting(FLATTR_TOKEN))
384 return context
387 class LicensePodcastList(PodcastListView):
388 """ Lists podcasts with a given license """
390 template_name = 'directory/license-podcasts.html'
392 def get_queryset(self):
393 return Podcast.objects.all().license(self.license_url)
395 @property
396 def license_url(self):
397 return self.kwargs['license_url']
400 class LicenseList(TemplateView):
401 """ Lists all podcast licenses """
403 template_name = 'directory/licenses.html'
405 def licenses(self):
406 """ Returns all podcast licenses """
407 query = Podcast.objects.exclude(license__isnull=True)
408 values = query.values("license").annotate(Count("id")).order_by()
410 counter = Counter({l['license']: l['id__count'] for l in values})
411 return counter.most_common()