add admin task for unifying the slugs of episodes
[mygpo.git] / mygpo / admin / views.py
blob083b582c733d77ac0665ed1446d6af2cb91370cd
1 import re
2 import socket
3 from itertools import count
4 from collections import Counter
6 import django
7 from django.shortcuts import render
8 from django.contrib import messages
9 from django.core.urlresolvers import reverse
10 from django.core.cache import cache
11 from django.http import HttpResponseRedirect
12 from django.utils.translation import ugettext as _
13 from django.views.generic import TemplateView
14 from django.utils.decorators import method_decorator
15 from django.conf import settings
17 from mygpo.admin.auth import require_staff
18 from mygpo.admin.group import PodcastGrouper
19 from mygpo.maintenance.merge import PodcastMerger, IncorrectMergeException
20 from mygpo.users.models import User
21 from mygpo.admin.clients import UserAgentStats, ClientStats
22 from mygpo.admin.tasks import merge_podcasts, unify_slugs
23 from mygpo.utils import get_git_head
24 from mygpo.api.httpresponse import JsonResponse
25 from mygpo.cel import celery
26 from mygpo.db.couchdb import get_main_database
27 from mygpo.db.couchdb.user import activate_user
28 from mygpo.db.couchdb.episode import episode_count, filetype_stats
29 from mygpo.db.couchdb.podcast import podcast_count, podcast_for_url
32 class InvalidPodcast(Exception):
33 """ raised when we try to merge a podcast that doesn't exist """
35 class AdminView(TemplateView):
37 @method_decorator(require_staff)
38 def dispatch(self, *args, **kwargs):
39 return super(AdminView, self).dispatch(*args, **kwargs)
42 class Overview(AdminView):
43 template_name = 'admin/overview.html'
46 class HostInfo(AdminView):
47 """ shows host information for diagnosis """
49 template_name = 'admin/hostinfo.html'
51 def get(self, request):
52 commit, msg = get_git_head()
53 base_dir = settings.BASE_DIR
54 hostname = socket.gethostname()
55 django_version = django.VERSION
57 main_db = get_main_database()
59 db_tasks = main_db.server.active_tasks()
61 i = celery.control.inspect()
62 num_celery_tasks = len(i.scheduled() or [])
64 return self.render_to_response({
65 'git_commit': commit,
66 'git_msg': msg,
67 'base_dir': base_dir,
68 'hostname': hostname,
69 'django_version': django_version,
70 'main_db': main_db.uri,
71 'db_tasks': db_tasks,
72 'num_celery_tasks': num_celery_tasks,
77 class MergeSelect(AdminView):
78 template_name = 'admin/merge-select.html'
80 def get(self, request):
81 num = int(request.GET.get('podcasts', 2))
82 urls = [''] * num
84 return self.render_to_response({
85 'urls': urls,
89 class MergeBase(AdminView):
91 def _get_podcasts(self, request):
92 podcasts = []
93 for n in count():
94 podcast_url = request.POST.get('feed%d' % n, None)
95 if podcast_url is None:
96 break
98 if not podcast_url:
99 continue
101 podcast = podcast_for_url(podcast_url)
103 if not podcast:
104 raise InvalidPodcast(podcast_url)
106 podcasts.append(podcast_for_url(podcast_url))
108 return podcasts
111 class MergeVerify(MergeBase):
113 template_name = 'admin/merge-grouping.html'
115 def post(self, request):
117 try:
118 podcasts = self._get_podcasts(request)
120 except InvalidPodcast as ip:
121 messages.error(request,
122 _('No podcast with URL {url}').format(url=str(ip)))
124 grouper = PodcastGrouper(podcasts)
126 get_features = lambda (e_id, e): ((e.url, e.title), e_id)
128 num_groups = grouper.group(get_features)
130 return self.render_to_response({
131 'podcasts': podcasts,
132 'groups': num_groups,
136 class MergeProcess(MergeBase):
138 RE_EPISODE = re.compile(r'episode_([0-9a-fA-F]{32})')
140 def post(self, request):
142 try:
143 podcasts = self._get_podcasts(request)
145 except InvalidPodcast as ip:
146 messages.error(request,
147 _('No podcast with URL {url}').format(url=str(ip)))
149 grouper = PodcastGrouper(podcasts)
151 features = {}
152 for key, feature in request.POST.items():
153 m = self.RE_EPISODE.match(key)
154 if m:
155 episode_id = m.group(1)
156 features[episode_id] = feature
158 get_features = lambda (e_id, e): (features[e_id], e_id)
160 num_groups = grouper.group(get_features)
162 if 'renew' in request.POST:
163 return render(request, 'admin/merge-grouping.html', {
164 'podcasts': podcasts,
165 'groups': num_groups,
169 elif 'merge' in request.POST:
171 podcast_ids = [p.get_id() for p in podcasts]
172 num_groups = list(num_groups)
174 res = merge_podcasts.delay(podcast_ids, num_groups)
176 return HttpResponseRedirect(reverse('admin-merge-status',
177 args=[res.task_id]))
180 class MergeStatus(AdminView):
181 """ Displays the status of the merge operation """
183 template_name = 'admin/task-status.html'
185 def get(self, request, task_id):
186 result = merge_podcasts.AsyncResult(task_id)
188 if not result.ready():
189 return self.render_to_response({
190 'ready': False,
193 # clear cache to make merge result visible
194 # TODO: what to do with multiple frontends?
195 cache.clear()
197 try:
198 actions, podcast = result.get()
200 except IncorrectMergeException as ime:
201 messages.error(request, str(ime))
202 return HttpResponseRedirect(reverse('admin-merge'))
204 return self.render_to_response({
205 'ready': True,
206 'actions': actions.items(),
207 'podcast': podcast,
212 class UserAgentStatsView(AdminView):
213 template_name = 'admin/useragents.html'
215 def get(self, request):
217 uas = UserAgentStats()
218 useragents = uas.get_entries()
220 return self.render_to_response({
221 'useragents': useragents.most_common(),
222 'max_users': uas.max_users,
223 'total': uas.total_users,
227 class ClientStatsView(AdminView):
228 template_name = 'admin/clients.html'
230 def get(self, request):
232 cs = ClientStats()
233 clients = cs.get_entries()
235 return self.render_to_response({
236 'clients': clients.most_common(),
237 'max_users': cs.max_users,
238 'total': cs.total_users,
242 class ClientStatsJsonView(AdminView):
243 def get(self, request):
245 cs = ClientStats()
246 clients = cs.get_entries()
248 return JsonResponse(map(self.to_dict, clients.most_common()))
250 def to_dict(self, res):
251 obj, count = res
253 if not isinstance(obj, tuple):
254 return obj, count
256 return obj._asdict(), count
259 class StatsView(AdminView):
260 """ shows general stats as HTML page """
262 template_name = 'admin/stats.html'
264 def _get_stats(self):
265 return {
266 'podcasts': podcast_count(),
267 'episodes': episode_count(),
268 'users': User.count(),
271 def get(self, request):
272 stats = self._get_stats()
273 return self.render_to_response({
274 'stats': stats,
278 class StatsJsonView(StatsView):
279 """ provides general stats as JSON """
281 def get(self, request):
282 stats = self._get_stats()
283 return JsonResponse(stats)
286 class FiletypeStatsView(AdminView):
288 template_name = 'admin/filetypes.html'
290 def get(self, request):
291 stats = filetype_stats()
293 if len(stats):
294 max_num = stats.most_common(1)[0][1]
295 else:
296 max_num = 0
298 return self.render_to_response({
299 'max_num': max_num,
300 'stats': stats.most_common(),
304 class ActivateUserView(AdminView):
305 """ Lets admins manually activate users """
307 template_name = 'admin/activate-user.html'
309 def get(self, request):
310 return self.render_to_response({})
312 def post(self, request):
314 username = request.POST.get('username')
315 email = request.POST.get('email')
317 if not (username or email):
318 messages.error(request,
319 _('Provide either username or email address'))
320 return HttpResponseRedirect(reverse('admin-activate-user'))
322 user = None
324 if username:
325 user = User.get_user(username, is_active=None)
327 if email and not user:
328 user = User.get_user_by_email(email, is_active=None)
330 if not user:
331 messages.error(request, _('No user found'))
332 return HttpResponseRedirect(reverse('admin-activate-user'))
334 activate_user(user)
335 messages.success(request,
336 _('User {username} ({email}) activated'.format(
337 username=user.username, email=user.email)))
338 return HttpResponseRedirect(reverse('admin-activate-user'))
342 class UnifyDuplicateSlugsSelect(AdminView):
343 """ select a podcast for which to unify slugs """
344 template_name = 'admin/unify-slugs-select.html'
347 class UnifyDuplicateSlugs(AdminView):
348 """ start slug-unification task """
350 def post(self, request):
351 podcast_url = request.POST.get('feed')
352 podcast = podcast_for_url(podcast_url)
354 if not podcast:
355 messages.error(request, _('Podcast with URL "%s" does not exist' %
356 (podcast_url,)))
357 return HttpResponseRedirect(reverse('admin-unify-slugs-select'))
359 res = unify_slugs.delay(podcast)
360 return HttpResponseRedirect(reverse('admin-unify-slugs-status',
361 args=[res.task_id]))
364 class UnifySlugsStatus(AdminView):
365 """ Displays the status of the unify-slugs operation """
367 template_name = 'admin/task-status.html'
369 def get(self, request, task_id):
370 result = merge_podcasts.AsyncResult(task_id)
372 if not result.ready():
373 return self.render_to_response({
374 'ready': False,
377 # clear cache to make merge result visible
378 # TODO: what to do with multiple frontends?
379 cache.clear()
381 actions, podcast = result.get()
383 return self.render_to_response({
384 'ready': True,
385 'actions': actions.items(),
386 'podcast': podcast,