[Migration] remove CouchDB fulltext index
[mygpo.git] / mygpo / directory / views.py
blob846634db1f098d24e5357238020901b7f9a33139
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.shortcuts import render
10 from django.db.models import Count
11 from django.contrib.sites.models import RequestSite
12 from django.views.decorators.cache import cache_control
13 from django.views.decorators.vary import vary_on_cookie
14 from django.views.generic import ListView
15 from django.utils.decorators import method_decorator
16 from django.views.generic.base import View, TemplateView
17 from django.contrib.auth.decorators import login_required
18 from django.contrib import messages
19 from django.utils.translation import ugettext as _
21 from feedservice.parse.models import ParserException
22 from feedservice.parse import FetchFeedException
24 from mygpo.core.proxy import proxy_object
25 from mygpo.podcasts.models import Podcast
26 from mygpo.search.index import search_podcasts
27 from mygpo.web.utils import process_lang_params, get_language_names, \
28 get_page_list, get_podcast_link_target, sanitize_language_codes
29 from mygpo.directory.tags import Topics
30 from mygpo.users.settings import FLATTR_TOKEN
31 from mygpo.data.feeddownloader import PodcastUpdater, NoEpisodesException
32 from mygpo.data.tasks import update_podcasts
33 from mygpo.db.couchdb.user import get_user_by_id
34 from mygpo.db.couchdb.directory import category_for_tag
35 from mygpo.db.couchdb.podcastlist import random_podcastlists, \
36 podcastlist_count, podcastlists_by_rating
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().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().toplist(self.language())[:num]
97 # load podcast objects
98 podcast_ids = [e.podcast for e in entries]
99 podcasts = Podcast.objects.get(id__in=podcast_ids)
100 podcasts = {podcast.id: podcast for podcast in podcasts}
101 for entry in entries:
102 entry.podcast = podcasts.get(entry.podcast, None)
104 context['entries'] = entries
106 # Determine maximum listener amount (or 0 if no entries exist)
107 context['max_listeners'] = max([0]+[e.listeners for e in entries])
109 return context
112 class Carousel(View):
113 """ A carousel demo """
115 @method_decorator(cache_control(private=True))
116 @method_decorator(vary_on_cookie)
117 def get(self, request):
119 return render(request, 'carousel.html', {
120 # evaluated lazyly, cached by template
121 'topics': Topics(),
125 class Directory(View):
126 """ The main directory page """
128 @method_decorator(cache_control(private=True))
129 @method_decorator(vary_on_cookie)
130 def get(self, request):
132 return render(request, 'directory.html', {
134 # evaluated lazyly, cached by template
135 'topics': Topics(),
136 'podcastlists': self.get_random_list(),
137 'random_podcast': Podcast.objects.random().first(),
141 def get_random_list(self, podcasts_per_list=5):
142 random_list = next(random_podcastlists(), None)
143 list_owner = None
144 if random_list:
145 random_list = proxy_object(random_list)
146 random_list.more_podcasts = max(0, len(random_list.podcasts) - podcasts_per_list)
147 random_list.podcasts = Podcast.objects.filter(id__in=random_list.podcasts[:podcasts_per_list])
148 random_list.user = get_user_by_id(random_list.user)
150 yield random_list
153 @cache_control(private=True)
154 @vary_on_cookie
155 def category(request, category, page_size=20):
156 category = category_for_tag(category)
157 if not category:
158 return HttpResponseNotFound()
160 # Make sure page request is an int. If not, deliver first page.
161 try:
162 page = int(request.GET.get('page', '1'))
163 except ValueError:
164 page = 1
166 entries = category.get_podcasts( (page-1) * page_size, page*page_size )
167 podcasts = filter(None, entries)
168 num_pages = int(ceil(len(category.podcasts) / page_size))
170 page_list = get_page_list(1, num_pages, page, 15)
172 return render(request, 'category.html', {
173 'entries': podcasts,
174 'category': category.label,
175 'page_list': page_list,
180 RESULTS_PER_PAGE=20
182 @cache_control(private=True)
183 @vary_on_cookie
184 def search(request, template='search.html', args={}):
186 if 'q' in request.GET:
187 q = request.GET.get('q', '').encode('utf-8')
189 try:
190 page = int(request.GET.get('page', 1))
191 except ValueError:
192 page = 1
194 start = RESULTS_PER_PAGE*(page-1)
195 results = search_podcasts(q)
196 num_pages = int(ceil(results.total / RESULTS_PER_PAGE))
197 results = results[start:start+RESULTS_PER_PAGE]
199 page_list = get_page_list(1, num_pages, page, 15)
201 else:
202 results = []
203 q = None
204 page_list = []
206 max_subscribers = max([p.subscribers for p in results] + [0])
208 current_site = RequestSite(request)
210 return render(request, template, dict(
211 q= q,
212 results= results,
213 page_list= page_list,
214 max_subscribers= max_subscribers,
215 domain= current_site.domain,
216 **args
221 @cache_control(private=True)
222 @vary_on_cookie
223 def podcast_lists(request, page_size=20):
225 # Make sure page request is an int. If not, deliver first page.
226 try:
227 page = int(request.GET.get('page', '1'))
228 except ValueError:
229 page = 1
231 lists = podcastlists_by_rating(skip=(page-1) * page_size, limit=page_size)
234 def _prepare_list(l):
235 user = get_user_by_id(l.user)
236 l = proxy_object(l)
237 l.username = user.username if user else ''
238 return l
240 lists = map(_prepare_list, lists)
242 num_pages = int(ceil(podcastlist_count() / float(page_size)))
244 page_list = get_page_list(1, num_pages, page, 15)
246 return render(request, 'podcast_lists.html', {
247 'lists': lists,
248 'page_list': page_list,
253 class MissingPodcast(View):
254 """ Check if a podcast is missing """
256 @method_decorator(login_required)
257 def get(self, request):
259 site = RequestSite(request)
261 # check if we're doing a query
262 url = request.GET.get('q', None)
264 if not url:
265 podcast = None
266 can_add = False
268 else:
269 try:
270 podcast = Podcast.objects.get(urls__url=url)
271 can_add = False
273 except Podcast.DoesNotExist:
274 # check if we could add a podcast for the given URL
275 podcast = False
276 updater = PodcastUpdater()
278 try:
279 can_add = updater.verify_podcast_url(url)
281 except (ParserException, FetchFeedException,
282 NoEpisodesException) as ex:
283 can_add = False
284 messages.error(request, unicode(ex))
286 return render(request, 'missing.html', {
287 'site': site,
288 'q': url,
289 'podcast': podcast,
290 'can_add': can_add,
294 class AddPodcast(View):
295 """ Add a missing podcast"""
297 @method_decorator(login_required)
298 @method_decorator(cache_control(private=True))
299 @method_decorator(vary_on_cookie)
300 def post(self, request):
302 url = request.POST.get('url', None)
304 if not url:
305 raise Http404
307 res = update_podcasts.delay([url])
309 return HttpResponseRedirect(reverse('add-podcast-status',
310 args=[res.task_id]))
313 class AddPodcastStatus(TemplateView):
314 """ Status of adding a podcast """
316 template_name = 'directory/add-podcast-status.html'
318 def get(self, request, task_id):
319 result = update_podcasts.AsyncResult(task_id)
321 if not result.ready():
322 return self.render_to_response({
323 'ready': False,
326 try:
327 podcasts = result.get()
328 messages.success(request, _('%d podcasts added' % len(podcasts)))
330 except (ParserException, FetchFeedException,
331 NoEpisodesException) as ex:
332 messages.error(request, str(ex))
333 podcast = None
335 return self.render_to_response({
336 'ready': True,
337 'podcasts': podcasts,
341 class PodcastListView(ListView):
342 """ A generic podcast list view """
344 paginate_by = 15
345 context_object_name = 'podcasts'
347 @method_decorator(vary_on_cookie)
348 @method_decorator(cache_control(private=True))
349 def dispatch(self, *args, **kwargs):
350 """ Only used for applying decorators """
351 return super(PodcastListView, self).dispatch(*args, **kwargs)
353 @property
354 def _page(self):
355 """ The current page
357 There seems to be no other pre-defined method for getting the current
358 page, see
359 https://docs.djangoproject.com/en/dev/ref/class-based-views/mixins-multiple-object/#multipleobjectmixin
361 return self.get_context_data()['page_obj']
363 def page_list(self, page_size=15):
364 """ Return a list of pages, eg [1, 2, 3, '...', 6, 7, 8] """
365 page = self._page
366 return get_page_list(1,
367 page.paginator.num_pages,
368 page.number,
369 page.paginator.per_page,
372 def max_subscribers(self):
373 """ Maximum subscribers of the podcasts on this page """
374 page = self._page
375 podcasts = page.object_list
376 return max([p.subscriber_count() for p in podcasts] + [0])
379 class FlattrPodcastList(PodcastListView):
380 """ Lists podcasts that have Flattr payment URLs """
382 template_name = 'flattr-podcasts.html'
384 def get_queryset(self):
385 return Podcast.objects.flattr()
387 def get_context_data(self, num=100):
388 context = super(FlattrPodcastList, self).get_context_data()
389 context['flattr_auth'] = (self.request.user.is_authenticated()
390 # and bool(self.request.user.get_wksetting(FLATTR_TOKEN))
392 return context
395 class LicensePodcastList(PodcastListView):
396 """ Lists podcasts with a given license """
398 template_name = 'directory/license-podcasts.html'
400 def get_queryset(self):
401 return Podcast.objects.license(self.license_url)
403 @property
404 def license_url(self):
405 return self.kwargs['license_url']
408 class LicenseList(TemplateView):
409 """ Lists all podcast licenses """
411 template_name = 'directory/licenses.html'
413 def licenses(self):
414 """ Returns all podcast licenses """
415 query = Podcast.objects.exclude(license__isnull=True)
416 values = query.values("license").annotate(Count("id")).order_by()
418 counter = Counter({l['license']: l['id__count'] for l in values})
419 return counter.most_common()