[Tasks] Update Celery configuration / setup
[mygpo.git] / mygpo / administration / views.py
blobf5480552368b69c2d3d1fbb60f6b9165003cd14a
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.template.loader import render_to_string
14 from django.template import RequestContext
15 from django.utils.translation import ugettext as _
16 from django.contrib.sites.models import RequestSite
17 from django.views.generic import TemplateView
18 from django.utils.decorators import method_decorator
19 from django.conf import settings
21 from mygpo.podcasts.models import Podcast, Episode
22 from mygpo.administration.auth import require_staff
23 from mygpo.administration.group import PodcastGrouper
24 from mygpo.maintenance.merge import PodcastMerger, IncorrectMergeException
25 from mygpo.users.models import User
26 from mygpo.administration.clients import UserAgentStats, ClientStats
27 from mygpo.administration.tasks import merge_podcasts
28 from mygpo.utils import get_git_head
29 from mygpo.api.httpresponse import JsonResponse
30 from mygpo.celery import celery
31 from mygpo.db.couchdb import get_userdata_database
32 from mygpo.db.couchdb.user import activate_user, add_published_objs
35 class InvalidPodcast(Exception):
36 """ raised when we try to merge a podcast that doesn't exist """
38 class AdminView(TemplateView):
40 @method_decorator(require_staff)
41 def dispatch(self, *args, **kwargs):
42 return super(AdminView, self).dispatch(*args, **kwargs)
45 class Overview(AdminView):
46 template_name = 'admin/overview.html'
49 class HostInfo(AdminView):
50 """ shows host information for diagnosis """
52 template_name = 'admin/hostinfo.html'
54 def get(self, request):
55 commit, msg = get_git_head()
56 base_dir = settings.BASE_DIR
57 hostname = socket.gethostname()
58 django_version = django.VERSION
60 main_db = get_userdata_database()
62 db_tasks = main_db.server.active_tasks()
64 i = celery.control.inspect()
65 scheduled = i.scheduled()
66 if not scheduled:
67 num_celery_tasks = None
68 else:
69 num_celery_tasks = sum(len(node) for node in scheduled.values())
71 feed_queue_status = self._get_feed_queue_status()
73 return self.render_to_response({
74 'git_commit': commit,
75 'git_msg': msg,
76 'base_dir': base_dir,
77 'hostname': hostname,
78 'django_version': django_version,
79 'main_db': main_db.uri,
80 'db_tasks': db_tasks,
81 'num_celery_tasks': num_celery_tasks,
82 'feed_queue_status': feed_queue_status,
85 def _get_feed_queue_status(self):
86 now = datetime.utcnow()
87 next_podcast = Podcast.objects.order_by_next_update().first()
89 delta = (next_podcast.next_update - now)
90 delta_mins = delta.total_seconds() / 60
91 return delta_mins
94 class MergeSelect(AdminView):
95 template_name = 'admin/merge-select.html'
97 def get(self, request):
98 num = int(request.GET.get('podcasts', 2))
99 urls = [''] * num
101 return self.render_to_response({
102 'urls': urls,
106 class MergeBase(AdminView):
108 def _get_podcasts(self, request):
109 podcasts = []
110 for n in count():
111 podcast_url = request.POST.get('feed%d' % n, None)
112 if podcast_url is None:
113 break
115 if not podcast_url:
116 continue
118 p = Podcast.objects.get(urls__url=podcast_url)
119 podcasts.append(p)
121 return podcasts
124 class MergeVerify(MergeBase):
126 template_name = 'admin/merge-grouping.html'
128 def post(self, request):
130 try:
131 podcasts = self._get_podcasts(request)
133 grouper = PodcastGrouper(podcasts)
135 get_features = lambda (e_id, e): ((e.url, e.title), e_id)
137 num_groups = grouper.group(get_features)
140 except InvalidPodcast as ip:
141 messages.error(request,
142 _('No podcast with URL {url}').format(url=str(ip)))
144 podcasts = []
145 num_groups = []
147 return self.render_to_response({
148 'podcasts': podcasts,
149 'groups': num_groups,
153 class MergeProcess(MergeBase):
155 RE_EPISODE = re.compile(r'episode_([0-9a-fA-F]{32})')
157 def post(self, request):
159 try:
160 podcasts = self._get_podcasts(request)
162 except InvalidPodcast as ip:
163 messages.error(request,
164 _('No podcast with URL {url}').format(url=str(ip)))
166 grouper = PodcastGrouper(podcasts)
168 features = {}
169 for key, feature in request.POST.items():
170 m = self.RE_EPISODE.match(key)
171 if m:
172 episode_id = m.group(1)
173 features[episode_id] = feature
175 get_features = lambda (e_id, e): (features.get(e_id, e_id), e_id)
177 num_groups = grouper.group(get_features)
179 if 'renew' in request.POST:
180 return render(request, 'admin/merge-grouping.html', {
181 'podcasts': podcasts,
182 'groups': num_groups,
186 elif 'merge' in request.POST:
188 podcast_ids = [p.get_id() for p in podcasts]
189 num_groups = list(num_groups)
191 res = merge_podcasts.delay(podcast_ids, num_groups)
193 return HttpResponseRedirect(reverse('admin-merge-status',
194 args=[res.task_id]))
197 class MergeStatus(AdminView):
198 """ Displays the status of the merge operation """
200 template_name = 'admin/task-status.html'
202 def get(self, request, task_id):
203 result = merge_podcasts.AsyncResult(task_id)
205 if not result.ready():
206 return self.render_to_response({
207 'ready': False,
210 # clear cache to make merge result visible
211 # TODO: what to do with multiple frontends?
212 cache.clear()
214 try:
215 actions, podcast = result.get()
217 except IncorrectMergeException as ime:
218 messages.error(request, str(ime))
219 return HttpResponseRedirect(reverse('admin-merge'))
221 return self.render_to_response({
222 'ready': True,
223 'actions': actions.items(),
224 'podcast': podcast,
229 class UserAgentStatsView(AdminView):
230 template_name = 'admin/useragents.html'
232 def get(self, request):
234 uas = UserAgentStats()
235 useragents = uas.get_entries()
237 return self.render_to_response({
238 'useragents': useragents.most_common(),
239 'max_users': uas.max_users,
240 'total': uas.total_users,
244 class ClientStatsView(AdminView):
245 template_name = 'admin/clients.html'
247 def get(self, request):
249 cs = ClientStats()
250 clients = cs.get_entries()
252 return self.render_to_response({
253 'clients': clients.most_common(),
254 'max_users': cs.max_users,
255 'total': cs.total_users,
259 class ClientStatsJsonView(AdminView):
260 def get(self, request):
262 cs = ClientStats()
263 clients = cs.get_entries()
265 return JsonResponse(map(self.to_dict, clients.most_common()))
267 def to_dict(self, res):
268 obj, count = res
270 if not isinstance(obj, tuple):
271 return obj, count
273 return obj._asdict(), count
276 class StatsView(AdminView):
277 """ shows general stats as HTML page """
279 template_name = 'admin/stats.html'
281 def _get_stats(self):
282 return {
283 'podcasts': Podcast.objects.count_fast(),
284 'episodes': Episode.objects.count_fast(),
285 'users': User.count(),
288 def get(self, request):
289 stats = self._get_stats()
290 return self.render_to_response({
291 'stats': stats,
295 class StatsJsonView(StatsView):
296 """ provides general stats as JSON """
298 def get(self, request):
299 stats = self._get_stats()
300 return JsonResponse(stats)
303 class ActivateUserView(AdminView):
304 """ Lets admins manually activate users """
306 template_name = 'admin/activate-user.html'
308 def get(self, request):
309 return self.render_to_response({})
311 def post(self, request):
313 username = request.POST.get('username')
314 email = request.POST.get('email')
316 if not (username or email):
317 messages.error(request,
318 _('Provide either username or email address'))
319 return HttpResponseRedirect(reverse('admin-activate-user'))
321 user = None
323 if username:
324 user = User.get_user(username, is_active=None)
326 if email and not user:
327 user = User.get_user_by_email(email, is_active=None)
329 if not user:
330 messages.error(request, _('No user found'))
331 return HttpResponseRedirect(reverse('admin-activate-user'))
333 activate_user(user)
334 messages.success(request,
335 _('User {username} ({email}) activated'.format(
336 username=user.username, email=user.email)))
337 return HttpResponseRedirect(reverse('admin-activate-user'))
341 class MakePublisherInput(AdminView):
342 """ Get all information necessary for making someone publisher """
344 template_name = 'admin/make-publisher-input.html'
347 class MakePublisher(AdminView):
348 """ Assign publisher permissions """
350 template_name = 'admin/make-publisher-result.html'
352 def post(self, request):
353 username = request.POST.get('username')
354 user = User.get_user(username)
355 if user is None:
356 messages.error(request, 'User "{username}" not found'.format(username=username))
357 return HttpResponseRedirect(reverse('admin-make-publisher-input'))
359 feeds = request.POST.get('feeds')
360 feeds = feeds.split()
361 podcasts = set()
363 for feed in feeds:
364 try:
365 podcast = Podcast.objects.get(urls__url=feed)
366 except Podcast.DoesNotExist:
367 messages.warning(request, 'Podcast with URL {feed} not found'.format(feed=feed))
368 continue
370 podcasts.add(podcast)
372 self.set_publisher(request, user, podcasts)
373 self.send_mail(request, user, podcasts)
374 return HttpResponseRedirect(reverse('admin-make-publisher-result'))
376 def set_publisher(self, request, user, podcasts):
377 podcast_ids = set(p.get_id() for p in podcasts)
378 add_published_objs(user, podcast_ids)
379 messages.success(request, 'Set publisher permissions for {count} podcasts'.format(count=len(podcast_ids)))
381 def send_mail(self, request, user, podcasts):
382 site = RequestSite(request)
383 msg = render_to_string('admin/make-publisher-mail.txt', {
384 'user': user,
385 'podcasts': podcasts,
386 'support_url': settings.SUPPORT_URL,
387 'site': site,
389 context_instance=RequestContext(request))
390 subj = get_email_subject(site, _('Publisher Permissions'))
392 user.email_user(subj, msg)
393 messages.success(request, 'Sent email to user "{username}"'.format(username=user.username))
396 class MakePublisherResult(AdminView):
397 template_name = 'make-publisher-result.html'
400 def get_email_subject(site, txt):
401 return '[{domain}] {txt}'.format(domain=site.domain, txt=txt)