Add admin function to resend activation email
[mygpo.git] / mygpo / administration / views.py
blob2a19d7b377b860cf8f55edcc71308bd941d15824
1 import re
2 import socket
3 from itertools import count, chain
4 from collections import Counter
5 from datetime import datetime
7 import redis
9 import django
10 from django.db.models import Avg
11 from django.shortcuts import render
12 from django.contrib import messages
13 from django.urls import reverse
14 from django.core.cache import cache
15 from django.http import HttpResponseRedirect
16 from django.template.loader import render_to_string
17 from django.template import RequestContext
18 from django.utils.translation import ugettext as _
19 from django.contrib.sites.requests import RequestSite
20 from django.views.generic import TemplateView
21 from django.utils.decorators import method_decorator
22 from django.conf import settings
23 from django.contrib.auth import get_user_model
25 from mygpo.podcasts.models import Podcast, Episode
26 from mygpo.administration.auth import require_staff
27 from mygpo.administration.group import PodcastGrouper
28 from mygpo.maintenance.merge import PodcastMerger, IncorrectMergeException
29 from mygpo.administration.clients import UserAgentStats, ClientStats
30 from mygpo.users.views.registration import send_activation_email
31 from mygpo.administration.tasks import merge_podcasts
32 from mygpo.utils import get_git_head
33 from mygpo.data.models import PodcastUpdateResult
34 from mygpo.users.models import UserProxy
35 from mygpo.publisher.models import PublishedPodcast
36 from mygpo.api.httpresponse import JsonResponse
37 from mygpo.celery import celery
40 class InvalidPodcast(Exception):
41 """ raised when we try to merge a podcast that doesn't exist """
43 class AdminView(TemplateView):
45 @method_decorator(require_staff)
46 def dispatch(self, *args, **kwargs):
47 return super(AdminView, self).dispatch(*args, **kwargs)
50 class Overview(AdminView):
51 template_name = 'admin/overview.html'
54 class HostInfo(AdminView):
55 """ shows host information for diagnosis """
57 template_name = 'admin/hostinfo.html'
59 def get(self, request):
60 commit, msg = get_git_head()
61 base_dir = settings.BASE_DIR
62 hostname = socket.gethostname()
63 django_version = django.VERSION
65 feed_queue_status = self._get_feed_queue_status()
66 num_index_outdated = self._get_num_outdated_search_index()
67 avg_podcast_update_duration = self._get_avg_podcast_update_duration()
69 return self.render_to_response({
70 'git_commit': commit,
71 'git_msg': msg,
72 'base_dir': base_dir,
73 'hostname': hostname,
74 'django_version': django_version,
75 'num_celery_tasks': self._get_waiting_celery_tasks(),
76 'avg_podcast_update_duration': avg_podcast_update_duration,
77 'feed_queue_status': feed_queue_status,
78 'num_index_outdated': num_index_outdated,
81 def _get_waiting_celery_tasks(self):
82 con = celery.broker_connection()
84 args = {'host': con.hostname}
85 if con.port:
86 args['port'] = con.port
88 r = redis.StrictRedis(**args)
89 return r.llen('celery')
91 def _get_avg_podcast_update_duration(self):
92 queryset = PodcastUpdateResult.objects.filter(successful=True)
93 return queryset.aggregate(avg_duration=Avg('duration'))['avg_duration']
95 def _get_feed_queue_status(self):
96 now = datetime.utcnow()
97 next_podcast = Podcast.objects.all().order_by_next_update().first()
99 delta = (next_podcast.next_update - now)
100 delta_mins = delta.total_seconds() / 60
101 return delta_mins
103 def _get_num_outdated_search_index(self):
104 return Podcast.objects.filter(search_index_uptodate=False).count()
106 class MergeSelect(AdminView):
107 template_name = 'admin/merge-select.html'
109 def get(self, request):
110 num = int(request.GET.get('podcasts', 2))
111 urls = [''] * num
113 return self.render_to_response({
114 'urls': urls,
118 class MergeBase(AdminView):
120 def _get_podcasts(self, request):
121 podcasts = []
122 for n in count():
123 podcast_url = request.POST.get('feed%d' % n, None)
124 if podcast_url is None:
125 break
127 if not podcast_url:
128 continue
130 p = Podcast.objects.get(urls__url=podcast_url)
131 podcasts.append(p)
133 return podcasts
136 class MergeVerify(MergeBase):
138 template_name = 'admin/merge-grouping.html'
140 def post(self, request):
142 try:
143 podcasts = self._get_podcasts(request)
145 grouper = PodcastGrouper(podcasts)
147 get_features = lambda id_e: ((id_e[1].url, id_e[1].title), id_e[0])
149 num_groups = grouper.group(get_features)
152 except InvalidPodcast as ip:
153 messages.error(request,
154 _('No podcast with URL {url}').format(url=str(ip)))
156 podcasts = []
157 num_groups = []
159 return self.render_to_response({
160 'podcasts': podcasts,
161 'groups': num_groups,
165 class MergeProcess(MergeBase):
167 RE_EPISODE = re.compile(r'episode_([0-9a-fA-F]{32})')
169 def post(self, request):
171 try:
172 podcasts = self._get_podcasts(request)
174 except InvalidPodcast as ip:
175 messages.error(request,
176 _('No podcast with URL {url}').format(url=str(ip)))
178 grouper = PodcastGrouper(podcasts)
180 features = {}
181 for key, feature in request.POST.items():
182 m = self.RE_EPISODE.match(key)
183 if m:
184 episode_id = m.group(1)
185 features[episode_id] = feature
187 get_features = lambda id_e: (features.get(id_e[0], id_e[0]), id_e[0])
189 num_groups = grouper.group(get_features)
191 if 'renew' in request.POST:
192 return render(request, 'admin/merge-grouping.html', {
193 'podcasts': podcasts,
194 'groups': num_groups,
198 elif 'merge' in request.POST:
200 podcast_ids = [p.get_id() for p in podcasts]
201 num_groups = list(num_groups)
203 res = merge_podcasts.delay(podcast_ids, num_groups)
205 return HttpResponseRedirect(reverse('admin-merge-status',
206 args=[res.task_id]))
209 class MergeStatus(AdminView):
210 """ Displays the status of the merge operation """
212 template_name = 'admin/task-status.html'
214 def get(self, request, task_id):
215 result = merge_podcasts.AsyncResult(task_id)
217 if not result.ready():
218 return self.render_to_response({
219 'ready': False,
222 # clear cache to make merge result visible
223 # TODO: what to do with multiple frontends?
224 cache.clear()
226 try:
227 actions, podcast = result.get()
229 except IncorrectMergeException as ime:
230 messages.error(request, str(ime))
231 return HttpResponseRedirect(reverse('admin-merge'))
233 return self.render_to_response({
234 'ready': True,
235 'actions': actions.items(),
236 'podcast': podcast,
241 class UserAgentStatsView(AdminView):
242 template_name = 'admin/useragents.html'
244 def get(self, request):
246 uas = UserAgentStats()
247 useragents = uas.get_entries()
249 return self.render_to_response({
250 'useragents': useragents.most_common(),
251 'max_users': uas.max_users,
252 'total': uas.total_users,
256 class ClientStatsView(AdminView):
257 template_name = 'admin/clients.html'
259 def get(self, request):
261 cs = ClientStats()
262 clients = cs.get_entries()
264 return self.render_to_response({
265 'clients': clients.most_common(),
266 'max_users': cs.max_users,
267 'total': cs.total_users,
271 class ClientStatsJsonView(AdminView):
272 def get(self, request):
274 cs = ClientStats()
275 clients = cs.get_entries()
277 return JsonResponse(map(self.to_dict, clients.most_common()))
279 def to_dict(self, res):
280 obj, count = res
282 if not isinstance(obj, tuple):
283 return obj, count
285 return obj._asdict(), count
288 class StatsView(AdminView):
289 """ shows general stats as HTML page """
291 template_name = 'admin/stats.html'
293 def _get_stats(self):
294 return {
295 'podcasts': Podcast.objects.count_fast(),
296 'episodes': Episode.objects.count_fast(),
297 'users': UserProxy.objects.count_fast(),
300 def get(self, request):
301 stats = self._get_stats()
302 return self.render_to_response({
303 'stats': stats,
307 class StatsJsonView(StatsView):
308 """ provides general stats as JSON """
310 def get(self, request):
311 stats = self._get_stats()
312 return JsonResponse(stats)
315 class ActivateUserView(AdminView):
316 """ Lets admins manually activate users """
318 template_name = 'admin/activate-user.html'
320 def get(self, request):
321 return self.render_to_response({})
323 def post(self, request):
325 username = request.POST.get('username')
326 email = request.POST.get('email')
328 if not (username or email):
329 messages.error(request,
330 _('Provide either username or email address'))
331 return HttpResponseRedirect(reverse('admin-activate-user'))
333 try:
334 user = UserProxy.objects.all().by_username_or_email(username, email)
335 except UserProxy.DoesNotExist:
336 messages.error(request, _('No user found'))
337 return HttpResponseRedirect(reverse('admin-activate-user'))
339 user.activate()
340 messages.success(request,
341 _('User {username} ({email}) activated'.format(
342 username=user.username, email=user.email)))
343 return HttpResponseRedirect(reverse('admin-activate-user'))
346 class ResendActivationEmail(AdminView):
347 """ Resends the users activation email """
349 template_name = 'admin/resend-acivation.html'
351 def get(self, request):
352 return self.render_to_response({})
354 def post(self, request):
356 username = request.POST.get('username')
357 email = request.POST.get('email')
359 if not (username or email):
360 messages.error(request,
361 _('Provide either username or email address'))
362 return HttpResponseRedirect(reverse('admin-resend-activation'))
364 try:
365 user = UserProxy.objects.all().by_username_or_email(username, email)
366 except UserProxy.DoesNotExist:
367 messages.error(request, _('No user found'))
368 return HttpResponseRedirect(reverse('admin-resend-activation'))
370 if user.is_active:
371 messages.success(request, 'User {username} is already activated')
373 else:
374 send_activation_email(user, request)
375 messages.success(request,
376 _('Email for {username} ({email}) resent'.format(
377 username=user.username, email=user.email)))
379 return HttpResponseRedirect(reverse('admin-resend-activation'))
383 class MakePublisherInput(AdminView):
384 """ Get all information necessary for making someone publisher """
386 template_name = 'admin/make-publisher-input.html'
389 class MakePublisher(AdminView):
390 """ Assign publisher permissions """
392 template_name = 'admin/make-publisher-result.html'
394 def post(self, request):
395 User = get_user_model()
396 username = request.POST.get('username')
398 try:
399 user = User.objects.get(username__iexact=username)
400 except User.DoesNotExist:
401 messages.error(request, 'User "{username}" not found'.format(username=username))
402 return HttpResponseRedirect(reverse('admin-make-publisher-input'))
404 feeds = request.POST.get('feeds')
405 feeds = feeds.split()
406 podcasts = set()
408 for feed in feeds:
409 try:
410 podcast = Podcast.objects.get(urls__url=feed)
411 except Podcast.DoesNotExist:
412 messages.warning(request, 'Podcast with URL {feed} not found'.format(feed=feed))
413 continue
415 podcasts.add(podcast)
417 created, existed = self.set_publisher(request, user, podcasts)
419 if (created + existed) > 0:
420 self.send_mail(request, user, podcasts)
421 return HttpResponseRedirect(reverse('admin-make-publisher-result'))
423 def set_publisher(self, request, user, podcasts):
424 created, existed = PublishedPodcast.objects.publish_podcasts(user,
425 podcasts)
426 messages.success(request,
427 'Set publisher permissions for {created} podcasts; '
428 '{existed} already existed'.format(created=created,
429 existed=existed))
430 return created, existed
432 def send_mail(self, request, user, podcasts):
433 site = RequestSite(request)
434 msg = render_to_string('admin/make-publisher-mail.txt', {
435 'user': user,
436 'podcasts': podcasts,
437 'support_url': settings.SUPPORT_URL,
438 'site': site,
440 request=request)
441 subj = get_email_subject(site, _('Publisher Permissions'))
443 user.email_user(subj, msg)
444 messages.success(request, 'Sent email to user "{username}"'.format(username=user.username))
447 class MakePublisherResult(AdminView):
448 template_name = 'make-publisher-result.html'
451 def get_email_subject(site, txt):
452 return '[{domain}] {txt}'.format(domain=site.domain, txt=txt)