1 from __future__
import division
3 from itertools
import imap
as map
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
.podcastlists
.models
import PodcastList
34 from mygpo
.data
.feeddownloader
import PodcastUpdater
, NoEpisodesException
35 from mygpo
.data
.tasks
import update_podcasts
36 from mygpo
.db
.couchdb
.directory
import category_for_tag
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
)
63 """ Currently selected language """
64 return process_lang_params(self
.request
)
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
])
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
])
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
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
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()
143 @cache_control(private
=True)
145 def category(request
, category
, page_size
=20):
146 category
= category_for_tag(category
)
148 return HttpResponseNotFound()
150 # Make sure page request is an int. If not, deliver first page.
152 page
= int(request
.GET
.get('page', '1'))
156 entries
= category
.get_podcasts( (page
-1) * page_size
, page
*page_size
)
157 podcasts
= filter(None, entries
)
158 num_pages
= int(ceil(len(category
.podcasts
) / page_size
))
160 page_list
= get_page_list(1, num_pages
, page
, 15)
162 return render(request
, 'category.html', {
164 'category': category
.label
,
165 'page_list': page_list
,
172 @cache_control(private
=True)
174 def search(request
, template
='search.html', args
={}):
176 if 'q' in request
.GET
:
177 q
= request
.GET
.get('q', '').encode('utf-8')
180 page
= int(request
.GET
.get('page', 1))
184 start
= RESULTS_PER_PAGE
*(page
-1)
185 results
= search_podcasts(q
)
187 num_pages
= int(ceil(total
/ RESULTS_PER_PAGE
))
188 results
= results
[start
:start
+RESULTS_PER_PAGE
]
190 page_list
= get_page_list(1, num_pages
, page
, 15)
197 max_subscribers
= max([p
.subscribers
for p
in results
] + [0])
199 current_site
= RequestSite(request
)
201 return render(request
, template
, dict(
204 page_list
= page_list
,
205 max_subscribers
= max_subscribers
,
206 domain
= current_site
.domain
,
212 @cache_control(private
=True)
214 def podcast_lists(request
, page_size
=20):
216 lists
= PodcastList
.objects
.all()\
217 .annotate(num_votes
=Count('votes'))\
218 .order_by('-num_votes')
220 paginator
= Paginator(lists
, page_size
)
222 page
= request
.GET
.get('page')
224 lists
= paginator
.page(page
)
225 except PageNotAnInteger
:
226 lists
= paginator
.page(1)
228 lists
= paginator
.page(paginator
.num_pages
)
230 num_pages
= int(ceil(PodcastList
.objects
.count() / float(page_size
)))
231 page_list
= get_page_list(1, num_pages
, page
, 15)
233 return render(request
, 'podcast_lists.html', {
235 'page_list': page_list
,
240 class MissingPodcast(View
):
241 """ Check if a podcast is missing """
243 @method_decorator(login_required
)
244 def get(self
, request
):
246 site
= RequestSite(request
)
248 # check if we're doing a query
249 url
= request
.GET
.get('q', None)
257 podcast
= Podcast
.objects
.get(urls__url
=url
)
260 except Podcast
.DoesNotExist
:
261 # check if we could add a podcast for the given URL
263 updater
= PodcastUpdater()
266 can_add
= updater
.verify_podcast_url(url
)
268 except (ParserException
, FetchFeedException
,
269 NoEpisodesException
) as ex
:
271 messages
.error(request
, unicode(ex
))
273 return render(request
, 'missing.html', {
281 class AddPodcast(View
):
282 """ Add a missing podcast"""
284 @method_decorator(login_required
)
285 @method_decorator(cache_control(private
=True))
286 @method_decorator(vary_on_cookie
)
287 def post(self
, request
):
289 url
= request
.POST
.get('url', None)
294 res
= update_podcasts
.delay([url
])
296 return HttpResponseRedirect(reverse('add-podcast-status',
300 class AddPodcastStatus(TemplateView
):
301 """ Status of adding a podcast """
303 template_name
= 'directory/add-podcast-status.html'
305 def get(self
, request
, task_id
):
306 result
= update_podcasts
.AsyncResult(task_id
)
308 if not result
.ready():
309 return self
.render_to_response({
314 podcasts
= result
.get()
315 messages
.success(request
, _('%d podcasts added' % len(podcasts
)))
317 except (ParserException
, FetchFeedException
,
318 NoEpisodesException
) as ex
:
319 messages
.error(request
, str(ex
))
322 return self
.render_to_response({
324 'podcasts': podcasts
,
328 class PodcastListView(ListView
):
329 """ A generic podcast list view """
332 context_object_name
= 'podcasts'
334 @method_decorator(vary_on_cookie
)
335 @method_decorator(cache_control(private
=True))
336 def dispatch(self
, *args
, **kwargs
):
337 """ Only used for applying decorators """
338 return super(PodcastListView
, self
).dispatch(*args
, **kwargs
)
344 There seems to be no other pre-defined method for getting the current
346 https://docs.djangoproject.com/en/dev/ref/class-based-views/mixins-multiple-object/#multipleobjectmixin
348 return self
.get_context_data()['page_obj']
350 def page_list(self
, page_size
=15):
351 """ Return a list of pages, eg [1, 2, 3, '...', 6, 7, 8] """
353 return get_page_list(1,
354 page
.paginator
.num_pages
,
356 page
.paginator
.per_page
,
359 def max_subscribers(self
):
360 """ Maximum subscribers of the podcasts on this page """
362 podcasts
= page
.object_list
363 return max([p
.subscriber_count() for p
in podcasts
] + [0])
366 class FlattrPodcastList(PodcastListView
):
367 """ Lists podcasts that have Flattr payment URLs """
369 template_name
= 'flattr-podcasts.html'
371 def get_queryset(self
):
372 return Podcast
.objects
.all().flattr()
374 def get_context_data(self
, num
=100):
375 context
= super(FlattrPodcastList
, self
).get_context_data()
376 context
['flattr_auth'] = (self
.request
.user
.is_authenticated()
377 # and bool(self.request.user.get_wksetting(FLATTR_TOKEN))
382 class LicensePodcastList(PodcastListView
):
383 """ Lists podcasts with a given license """
385 template_name
= 'directory/license-podcasts.html'
387 def get_queryset(self
):
388 return Podcast
.objects
.all().license(self
.license_url
)
391 def license_url(self
):
392 return self
.kwargs
['license_url']
395 class LicenseList(TemplateView
):
396 """ Lists all podcast licenses """
398 template_name
= 'directory/licenses.html'
401 """ Returns all podcast licenses """
402 query
= Podcast
.objects
.exclude(license__isnull
=True)
403 values
= query
.values("license").annotate(Count("id")).order_by()
405 counter
= Counter({l
['license']: l
['id__count'] for l
in values
})
406 return counter
.most_common()