[Search] don't encode query string
[mygpo.git] / mygpo / directory / views.py
blobcaca44a1aa2216dec2d4918cdecb5e94ec411ace
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 mygpo.podcasts.models import Podcast, Episode
24 from mygpo.directory.search import search_podcasts
25 from mygpo.web.utils import process_lang_params, get_language_names, \
26 get_page_list, get_podcast_link_target, sanitize_language_codes
27 from mygpo.directory.tags import Topics
28 from mygpo.users.settings import FLATTR_TOKEN
29 from mygpo.categories.models import Category
30 from mygpo.podcastlists.models import PodcastList
31 from mygpo.data.feeddownloader import (verify_podcast_url, NoEpisodesException,
32 UpdatePodcastException)
33 from mygpo.data.tasks import update_podcasts
36 class ToplistView(TemplateView):
37 """ Generic Top List view """
39 @method_decorator(vary_on_cookie)
40 @method_decorator(cache_control(private=True))
41 def dispatch(self, *args, **kwargs):
42 """ Only used for applying decorators """
43 return super(ToplistView, self).dispatch(*args, **kwargs)
45 def all_languages(self):
46 """ Returns all 2-letter language codes that are used by podcasts.
48 It filters obviously invalid strings, but does not check if any
49 of these codes is contained in ISO 639. """
51 query = Podcast.objects.exclude(language__isnull=True)
52 query = query.distinct('language').values('language')
54 langs = [o['language'] for o in query]
55 langs = sorted(sanitize_language_codes(langs))
57 return get_language_names(langs)
59 def language(self):
60 """ Currently selected language """
61 return process_lang_params(self.request)
63 def site(self):
64 """ Current site for constructing absolute links """
65 return RequestSite(self.request)
68 class PodcastToplistView(ToplistView):
69 """ Most subscribed podcasts """
71 template_name = 'toplist.html'
73 def get_context_data(self, num=100):
74 context = super(PodcastToplistView, self).get_context_data()
76 entries = Podcast.objects.all()\
77 .prefetch_related('slugs')\
78 .toplist(self.language())[:num]
79 context['entries'] = entries
81 context['max_subscribers'] = max([0] + [p.subscriber_count() for p in entries])
83 return context
86 class EpisodeToplistView(ToplistView):
87 """ Most listened-to episodes """
89 template_name = 'episode_toplist.html'
91 def get_context_data(self, num=100):
92 context = super(EpisodeToplistView, self).get_context_data()
94 entries = Episode.objects.all()\
95 .select_related('podcast')\
96 .prefetch_related('slugs', 'podcast__slugs')\
97 .toplist(self.language())[:num]
98 context['entries'] = entries
100 # Determine maximum listener amount (or 0 if no entries exist)
101 context['max_listeners'] = max([0]+[e.listeners for e in entries])
103 return context
106 class Carousel(View):
107 """ A carousel demo """
109 @method_decorator(cache_control(private=True))
110 @method_decorator(vary_on_cookie)
111 def get(self, request):
113 return render(request, 'carousel.html', {
114 # evaluated lazyly, cached by template
115 'topics': Topics(),
119 class Directory(View):
120 """ The main directory page """
122 @method_decorator(cache_control(private=True))
123 @method_decorator(vary_on_cookie)
124 def get(self, request):
126 return render(request, 'directory.html', {
128 # evaluated lazyly, cached by template
129 'topics': Topics(),
130 'podcastlists': self.get_random_list(),
131 'random_podcast': Podcast.objects.all().random().first(),
135 def get_random_list(self, podcasts_per_list=5):
136 random_list = PodcastList.objects.order_by('?').first()
137 yield random_list
140 @cache_control(private=True)
141 @vary_on_cookie
142 def category(request, category, page_size=20):
143 try:
144 category = Category.objects.get(tags__tag=category)
145 except Category.DoesNotExist:
146 return HttpResponseNotFound()
148 podcasts = category.entries.all()\
149 .prefetch_related('podcast', 'podcast__slugs')
151 paginator = Paginator(podcasts, page_size)
153 page = request.GET.get('page')
154 try:
155 podcasts = paginator.page(page)
156 except PageNotAnInteger:
157 # If page is not an integer, deliver first page.
158 podcasts = paginator.page(1)
159 except EmptyPage:
160 # If page is out of range (e.g. 9999), deliver last page of results.
161 podcasts = paginator.page(paginator.num_pages)
163 page_list = get_page_list(1, paginator.num_pages, podcasts.number, 15)
165 return render(request, 'category.html', {
166 'entries': podcasts,
167 'category': category.title,
168 'page_list': page_list,
173 RESULTS_PER_PAGE=20
175 @cache_control(private=True)
176 @vary_on_cookie
177 def search(request, template='search.html', args={}):
179 if 'q' in request.GET:
180 q = request.GET.get('q', '')
182 try:
183 page = int(request.GET.get('page', 1))
184 except ValueError:
185 page = 1
187 start = RESULTS_PER_PAGE*(page-1)
188 results = search_podcasts(q)
189 total = len(results)
190 num_pages = int(ceil(total / RESULTS_PER_PAGE))
191 results = results[start:start+RESULTS_PER_PAGE]
193 page_list = get_page_list(1, num_pages, page, 15)
195 else:
196 results = []
197 q = None
198 page_list = []
200 max_subscribers = max([p.subscribers for p in results] + [0])
202 current_site = RequestSite(request)
204 return render(request, template, dict(
205 q= q,
206 results= results,
207 page_list= page_list,
208 max_subscribers= max_subscribers,
209 domain= current_site.domain,
210 **args
215 @cache_control(private=True)
216 @vary_on_cookie
217 def podcast_lists(request, page_size=20):
219 lists = PodcastList.objects.all()\
220 .annotate(num_votes=Count('votes'))\
221 .order_by('-num_votes')
223 paginator = Paginator(lists, page_size)
225 page = request.GET.get('page')
226 try:
227 lists = paginator.page(page)
228 except PageNotAnInteger:
229 lists = paginator.page(1)
230 page = 1
231 except EmptyPage:
232 lists = paginator.page(paginator.num_pages)
233 page = paginator.num_pages
235 num_pages = int(ceil(PodcastList.objects.count() / float(page_size)))
236 page_list = get_page_list(1, num_pages, int(page), 15)
238 return render(request, 'podcast_lists.html', {
239 'lists': lists,
240 'page_list': page_list,
245 class MissingPodcast(View):
246 """ Check if a podcast is missing """
248 @method_decorator(login_required)
249 def get(self, request):
251 site = RequestSite(request)
253 # check if we're doing a query
254 url = request.GET.get('q', None)
256 if not url:
257 podcast = None
258 can_add = False
260 else:
261 try:
262 podcast = Podcast.objects.get(urls__url=url)
263 can_add = False
265 except Podcast.DoesNotExist:
266 # check if we could add a podcast for the given URL
267 podcast = False
268 try:
269 can_add = verify_podcast_url(url)
271 except (UpdatePodcastException, NoEpisodesException) as ex:
272 can_add = False
273 messages.error(request, str(ex))
275 return render(request, 'missing.html', {
276 'site': site,
277 'q': url,
278 'podcast': podcast,
279 'can_add': can_add,
283 class AddPodcast(View):
284 """ Add a missing podcast"""
286 @method_decorator(login_required)
287 @method_decorator(cache_control(private=True))
288 @method_decorator(vary_on_cookie)
289 def post(self, request):
291 url = request.POST.get('url', None)
293 if not url:
294 raise Http404
296 res = update_podcasts.delay([url])
298 return HttpResponseRedirect(reverse('add-podcast-status',
299 args=[res.task_id]))
302 class AddPodcastStatus(TemplateView):
303 """ Status of adding a podcast """
305 template_name = 'directory/add-podcast-status.html'
307 def get(self, request, task_id):
308 result = update_podcasts.AsyncResult(task_id)
310 if not result.ready():
311 return self.render_to_response({
312 'ready': False,
315 try:
316 podcasts = result.get()
317 messages.success(request, _('%d podcasts added' % len(podcasts)))
319 except (UpdatePodcastException, NoEpisodesException) as ex:
320 messages.error(request, str(ex))
321 podcast = None
323 return self.render_to_response({
324 'ready': True,
325 'podcasts': podcasts,
329 class PodcastListView(ListView):
330 """ A generic podcast list view """
332 paginate_by = 15
333 context_object_name = 'podcasts'
335 @method_decorator(vary_on_cookie)
336 @method_decorator(cache_control(private=True))
337 def dispatch(self, *args, **kwargs):
338 """ Only used for applying decorators """
339 return super(PodcastListView, self).dispatch(*args, **kwargs)
341 @property
342 def _page(self):
343 """ The current page
345 There seems to be no other pre-defined method for getting the current
346 page, see
347 https://docs.djangoproject.com/en/dev/ref/class-based-views/mixins-multiple-object/#multipleobjectmixin
349 return self.get_context_data()['page_obj']
351 def page_list(self, page_size=15):
352 """ Return a list of pages, eg [1, 2, 3, '...', 6, 7, 8] """
353 page = self._page
354 return get_page_list(1,
355 page.paginator.num_pages,
356 page.number,
357 page.paginator.per_page,
360 def max_subscribers(self):
361 """ Maximum subscribers of the podcasts on this page """
362 page = self._page
363 podcasts = page.object_list
364 return max([p.subscriber_count() for p in podcasts] + [0])
367 class FlattrPodcastList(PodcastListView):
368 """ Lists podcasts that have Flattr payment URLs """
370 template_name = 'flattr-podcasts.html'
372 def get_queryset(self):
373 return Podcast.objects.all().flattr()
375 def get_context_data(self, num=100):
376 context = super(FlattrPodcastList, self).get_context_data()
377 context['flattr_auth'] = (self.request.user.is_authenticated()
378 # and bool(self.request.user.get_wksetting(FLATTR_TOKEN))
380 return context
383 class LicensePodcastList(PodcastListView):
384 """ Lists podcasts with a given license """
386 template_name = 'directory/license-podcasts.html'
388 def get_queryset(self):
389 return Podcast.objects.all().license(self.license_url)
391 @property
392 def license_url(self):
393 return self.kwargs['license_url']
396 class LicenseList(TemplateView):
397 """ Lists all podcast licenses """
399 template_name = 'directory/licenses.html'
401 def licenses(self):
402 """ Returns all podcast licenses """
403 query = Podcast.objects.exclude(license__isnull=True)
404 values = query.values("license").annotate(Count("id")).order_by()
406 counter = Counter({l['license']: l['id__count'] for l in values})
407 return counter.most_common()