fix grouping of episodes before merging
[mygpo.git] / mygpo / admin / views.py
bloba2abab8f37aba6259193a6ca878cd08e511e94d5
1 import re
2 import socket
3 from itertools import count, chain
4 from collections import Counter
5 from datetime import datetime
7 import django
8 from django.shortcuts import render
9 from django.contrib import messages
10 from django.core.urlresolvers import reverse
11 from django.core.cache import cache
12 from django.http import HttpResponseRedirect
13 from django.utils.translation import ugettext as _
14 from django.views.generic import TemplateView
15 from django.utils.decorators import method_decorator
16 from django.conf import settings
18 from mygpo.admin.auth import require_staff
19 from mygpo.admin.group import PodcastGrouper
20 from mygpo.maintenance.merge import PodcastMerger, IncorrectMergeException
21 from mygpo.users.models import User
22 from mygpo.admin.clients import UserAgentStats, ClientStats
23 from mygpo.admin.tasks import merge_podcasts, unify_slugs
24 from mygpo.utils import get_git_head
25 from mygpo.api.httpresponse import JsonResponse
26 from mygpo.cel import celery
27 from mygpo.db.couchdb import get_main_database
28 from mygpo.db.couchdb.user import activate_user
29 from mygpo.db.couchdb.episode import episode_count, filetype_stats
30 from mygpo.db.couchdb.podcast import podcast_count, podcast_for_url, \
31 podcast_duplicates_for_url, podcasts_by_next_update
34 class InvalidPodcast(Exception):
35 """ raised when we try to merge a podcast that doesn't exist """
37 class AdminView(TemplateView):
39 @method_decorator(require_staff)
40 def dispatch(self, *args, **kwargs):
41 return super(AdminView, self).dispatch(*args, **kwargs)
44 class Overview(AdminView):
45 template_name = 'admin/overview.html'
48 class HostInfo(AdminView):
49 """ shows host information for diagnosis """
51 template_name = 'admin/hostinfo.html'
53 def get(self, request):
54 commit, msg = get_git_head()
55 base_dir = settings.BASE_DIR
56 hostname = socket.gethostname()
57 django_version = django.VERSION
59 main_db = get_main_database()
61 db_tasks = main_db.server.active_tasks()
63 i = celery.control.inspect()
64 scheduled = i.scheduled()
65 if not scheduled:
66 num_celery_tasks = None
67 else:
68 num_celery_tasks = sum(len(node) for node in scheduled.values())
70 feed_queue_status = self._get_feed_queue_status()
72 return self.render_to_response({
73 'git_commit': commit,
74 'git_msg': msg,
75 'base_dir': base_dir,
76 'hostname': hostname,
77 'django_version': django_version,
78 'main_db': main_db.uri,
79 'db_tasks': db_tasks,
80 'num_celery_tasks': num_celery_tasks,
81 'feed_queue_status': feed_queue_status,
84 def _get_feed_queue_status(self):
85 now = datetime.utcnow()
86 next_podcast = podcasts_by_next_update(limit=1)[0]
88 delta = (next_podcast.next_update - now)
89 delta_mins = delta.total_seconds() / 60
90 return delta_mins
93 class MergeSelect(AdminView):
94 template_name = 'admin/merge-select.html'
96 def get(self, request):
97 num = int(request.GET.get('podcasts', 2))
98 urls = [''] * num
100 return self.render_to_response({
101 'urls': urls,
105 class MergeBase(AdminView):
107 def _get_podcasts(self, request):
108 podcasts = []
109 for n in count():
110 podcast_url = request.POST.get('feed%d' % n, None)
111 if podcast_url is None:
112 break
114 if not podcast_url:
115 continue
117 ps = podcast_duplicates_for_url(podcast_url)
119 if not ps:
120 raise InvalidPodcast(podcast_url)
122 for p in ps:
123 if p not in podcasts:
124 podcasts.append(p)
126 return podcasts
129 class MergeVerify(MergeBase):
131 template_name = 'admin/merge-grouping.html'
133 def post(self, request):
135 try:
136 podcasts = self._get_podcasts(request)
138 grouper = PodcastGrouper(podcasts)
140 get_features = lambda (e_id, e): ((e.url, e.title), e_id)
142 num_groups = grouper.group(get_features)
145 except InvalidPodcast as ip:
146 messages.error(request,
147 _('No podcast with URL {url}').format(url=str(ip)))
149 podcasts = []
150 num_groups = []
152 return self.render_to_response({
153 'podcasts': podcasts,
154 'groups': num_groups,
158 class MergeProcess(MergeBase):
160 RE_EPISODE = re.compile(r'episode_([0-9a-fA-F]{32})')
162 def post(self, request):
164 try:
165 podcasts = self._get_podcasts(request)
167 except InvalidPodcast as ip:
168 messages.error(request,
169 _('No podcast with URL {url}').format(url=str(ip)))
171 grouper = PodcastGrouper(podcasts)
173 features = {}
174 for key, feature in request.POST.items():
175 m = self.RE_EPISODE.match(key)
176 if m:
177 episode_id = m.group(1)
178 features[episode_id] = feature
180 get_features = lambda (e_id, e): (features.get(e_id, e_id), e_id)
182 num_groups = grouper.group(get_features)
184 if 'renew' in request.POST:
185 return render(request, 'admin/merge-grouping.html', {
186 'podcasts': podcasts,
187 'groups': num_groups,
191 elif 'merge' in request.POST:
193 podcast_ids = [p.get_id() for p in podcasts]
194 num_groups = list(num_groups)
196 res = merge_podcasts.delay(podcast_ids, num_groups)
198 return HttpResponseRedirect(reverse('admin-merge-status',
199 args=[res.task_id]))
202 class MergeStatus(AdminView):
203 """ Displays the status of the merge operation """
205 template_name = 'admin/task-status.html'
207 def get(self, request, task_id):
208 result = merge_podcasts.AsyncResult(task_id)
210 if not result.ready():
211 return self.render_to_response({
212 'ready': False,
215 # clear cache to make merge result visible
216 # TODO: what to do with multiple frontends?
217 cache.clear()
219 try:
220 actions, podcast = result.get()
222 except IncorrectMergeException as ime:
223 messages.error(request, str(ime))
224 return HttpResponseRedirect(reverse('admin-merge'))
226 return self.render_to_response({
227 'ready': True,
228 'actions': actions.items(),
229 'podcast': podcast,
234 class UserAgentStatsView(AdminView):
235 template_name = 'admin/useragents.html'
237 def get(self, request):
239 uas = UserAgentStats()
240 useragents = uas.get_entries()
242 return self.render_to_response({
243 'useragents': useragents.most_common(),
244 'max_users': uas.max_users,
245 'total': uas.total_users,
249 class ClientStatsView(AdminView):
250 template_name = 'admin/clients.html'
252 def get(self, request):
254 cs = ClientStats()
255 clients = cs.get_entries()
257 return self.render_to_response({
258 'clients': clients.most_common(),
259 'max_users': cs.max_users,
260 'total': cs.total_users,
264 class ClientStatsJsonView(AdminView):
265 def get(self, request):
267 cs = ClientStats()
268 clients = cs.get_entries()
270 return JsonResponse(map(self.to_dict, clients.most_common()))
272 def to_dict(self, res):
273 obj, count = res
275 if not isinstance(obj, tuple):
276 return obj, count
278 return obj._asdict(), count
281 class StatsView(AdminView):
282 """ shows general stats as HTML page """
284 template_name = 'admin/stats.html'
286 def _get_stats(self):
287 return {
288 'podcasts': podcast_count(),
289 'episodes': episode_count(),
290 'users': User.count(),
293 def get(self, request):
294 stats = self._get_stats()
295 return self.render_to_response({
296 'stats': stats,
300 class StatsJsonView(StatsView):
301 """ provides general stats as JSON """
303 def get(self, request):
304 stats = self._get_stats()
305 return JsonResponse(stats)
308 class FiletypeStatsView(AdminView):
310 template_name = 'admin/filetypes.html'
312 def get(self, request):
313 stats = filetype_stats()
315 if len(stats):
316 max_num = stats.most_common(1)[0][1]
317 else:
318 max_num = 0
320 return self.render_to_response({
321 'max_num': max_num,
322 'stats': stats.most_common(),
326 class ActivateUserView(AdminView):
327 """ Lets admins manually activate users """
329 template_name = 'admin/activate-user.html'
331 def get(self, request):
332 return self.render_to_response({})
334 def post(self, request):
336 username = request.POST.get('username')
337 email = request.POST.get('email')
339 if not (username or email):
340 messages.error(request,
341 _('Provide either username or email address'))
342 return HttpResponseRedirect(reverse('admin-activate-user'))
344 user = None
346 if username:
347 user = User.get_user(username, is_active=None)
349 if email and not user:
350 user = User.get_user_by_email(email, is_active=None)
352 if not user:
353 messages.error(request, _('No user found'))
354 return HttpResponseRedirect(reverse('admin-activate-user'))
356 activate_user(user)
357 messages.success(request,
358 _('User {username} ({email}) activated'.format(
359 username=user.username, email=user.email)))
360 return HttpResponseRedirect(reverse('admin-activate-user'))
364 class UnifyDuplicateSlugsSelect(AdminView):
365 """ select a podcast for which to unify slugs """
366 template_name = 'admin/unify-slugs-select.html'
369 class UnifyDuplicateSlugs(AdminView):
370 """ start slug-unification task """
372 def post(self, request):
373 podcast_url = request.POST.get('feed')
374 podcast = podcast_for_url(podcast_url)
376 if not podcast:
377 messages.error(request, _('Podcast with URL "%s" does not exist' %
378 (podcast_url,)))
379 return HttpResponseRedirect(reverse('admin-unify-slugs-select'))
381 res = unify_slugs.delay(podcast)
382 return HttpResponseRedirect(reverse('admin-unify-slugs-status',
383 args=[res.task_id]))
386 class UnifySlugsStatus(AdminView):
387 """ Displays the status of the unify-slugs operation """
389 template_name = 'admin/task-status.html'
391 def get(self, request, task_id):
392 result = merge_podcasts.AsyncResult(task_id)
394 if not result.ready():
395 return self.render_to_response({
396 'ready': False,
399 # clear cache to make merge result visible
400 # TODO: what to do with multiple frontends?
401 cache.clear()
403 actions, podcast = result.get()
405 return self.render_to_response({
406 'ready': True,
407 'actions': actions.items(),
408 'podcast': podcast,