Use Django's new UUIDField
[mygpo.git] / mygpo / administration / views.py
blob6248ad296a6acc6b211be0ed55778856ed153666
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
20 from django.contrib.auth import get_user_model
22 from mygpo.podcasts.models import Podcast, Episode
23 from mygpo.administration.auth import require_staff
24 from mygpo.administration.group import PodcastGrouper
25 from mygpo.maintenance.merge import PodcastMerger, IncorrectMergeException
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.users.models import UserProxy
30 from mygpo.publisher.models import PublishedPodcast
31 from mygpo.api.httpresponse import JsonResponse
32 from mygpo.celery import celery
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 i = celery.control.inspect()
61 scheduled = i.scheduled()
62 if not scheduled:
63 num_celery_tasks = None
64 else:
65 num_celery_tasks = sum(len(node) for node in scheduled.values())
67 feed_queue_status = self._get_feed_queue_status()
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': num_celery_tasks,
76 'feed_queue_status': feed_queue_status,
79 def _get_feed_queue_status(self):
80 now = datetime.utcnow()
81 next_podcast = Podcast.objects.order_by_next_update().first()
83 delta = (next_podcast.next_update - now)
84 delta_mins = delta.total_seconds() / 60
85 return delta_mins
88 class MergeSelect(AdminView):
89 template_name = 'admin/merge-select.html'
91 def get(self, request):
92 num = int(request.GET.get('podcasts', 2))
93 urls = [''] * num
95 return self.render_to_response({
96 'urls': urls,
100 class MergeBase(AdminView):
102 def _get_podcasts(self, request):
103 podcasts = []
104 for n in count():
105 podcast_url = request.POST.get('feed%d' % n, None)
106 if podcast_url is None:
107 break
109 if not podcast_url:
110 continue
112 p = Podcast.objects.get(urls__url=podcast_url)
113 podcasts.append(p)
115 return podcasts
118 class MergeVerify(MergeBase):
120 template_name = 'admin/merge-grouping.html'
122 def post(self, request):
124 try:
125 podcasts = self._get_podcasts(request)
127 grouper = PodcastGrouper(podcasts)
129 get_features = lambda (e_id, e): ((e.url, e.title), e_id)
131 num_groups = grouper.group(get_features)
134 except InvalidPodcast as ip:
135 messages.error(request,
136 _('No podcast with URL {url}').format(url=str(ip)))
138 podcasts = []
139 num_groups = []
141 return self.render_to_response({
142 'podcasts': podcasts,
143 'groups': num_groups,
147 class MergeProcess(MergeBase):
149 RE_EPISODE = re.compile(r'episode_([0-9a-fA-F]{32})')
151 def post(self, request):
153 try:
154 podcasts = self._get_podcasts(request)
156 except InvalidPodcast as ip:
157 messages.error(request,
158 _('No podcast with URL {url}').format(url=str(ip)))
160 grouper = PodcastGrouper(podcasts)
162 features = {}
163 for key, feature in request.POST.items():
164 m = self.RE_EPISODE.match(key)
165 if m:
166 episode_id = m.group(1)
167 features[episode_id] = feature
169 get_features = lambda (e_id, e): (features.get(e_id, e_id), e_id)
171 num_groups = grouper.group(get_features)
173 if 'renew' in request.POST:
174 return render(request, 'admin/merge-grouping.html', {
175 'podcasts': podcasts,
176 'groups': num_groups,
180 elif 'merge' in request.POST:
182 podcast_ids = [p.get_id() for p in podcasts]
183 num_groups = list(num_groups)
185 res = merge_podcasts.delay(podcast_ids, num_groups)
187 return HttpResponseRedirect(reverse('admin-merge-status',
188 args=[res.task_id]))
191 class MergeStatus(AdminView):
192 """ Displays the status of the merge operation """
194 template_name = 'admin/task-status.html'
196 def get(self, request, task_id):
197 result = merge_podcasts.AsyncResult(task_id)
199 if not result.ready():
200 return self.render_to_response({
201 'ready': False,
204 # clear cache to make merge result visible
205 # TODO: what to do with multiple frontends?
206 cache.clear()
208 try:
209 actions, podcast = result.get()
211 except IncorrectMergeException as ime:
212 messages.error(request, str(ime))
213 return HttpResponseRedirect(reverse('admin-merge'))
215 return self.render_to_response({
216 'ready': True,
217 'actions': actions.items(),
218 'podcast': podcast,
223 class UserAgentStatsView(AdminView):
224 template_name = 'admin/useragents.html'
226 def get(self, request):
228 uas = UserAgentStats()
229 useragents = uas.get_entries()
231 return self.render_to_response({
232 'useragents': useragents.most_common(),
233 'max_users': uas.max_users,
234 'total': uas.total_users,
238 class ClientStatsView(AdminView):
239 template_name = 'admin/clients.html'
241 def get(self, request):
243 cs = ClientStats()
244 clients = cs.get_entries()
246 return self.render_to_response({
247 'clients': clients.most_common(),
248 'max_users': cs.max_users,
249 'total': cs.total_users,
253 class ClientStatsJsonView(AdminView):
254 def get(self, request):
256 cs = ClientStats()
257 clients = cs.get_entries()
259 return JsonResponse(map(self.to_dict, clients.most_common()))
261 def to_dict(self, res):
262 obj, count = res
264 if not isinstance(obj, tuple):
265 return obj, count
267 return obj._asdict(), count
270 class StatsView(AdminView):
271 """ shows general stats as HTML page """
273 template_name = 'admin/stats.html'
275 def _get_stats(self):
276 return {
277 'podcasts': Podcast.objects.count_fast(),
278 'episodes': Episode.objects.count_fast(),
279 'users': UserProxy.objects.count_fast(),
282 def get(self, request):
283 stats = self._get_stats()
284 return self.render_to_response({
285 'stats': stats,
289 class StatsJsonView(StatsView):
290 """ provides general stats as JSON """
292 def get(self, request):
293 stats = self._get_stats()
294 return JsonResponse(stats)
297 class ActivateUserView(AdminView):
298 """ Lets admins manually activate users """
300 template_name = 'admin/activate-user.html'
302 def get(self, request):
303 return self.render_to_response({})
305 def post(self, request):
307 username = request.POST.get('username')
308 email = request.POST.get('email')
310 if not (username or email):
311 messages.error(request,
312 _('Provide either username or email address'))
313 return HttpResponseRedirect(reverse('admin-activate-user'))
315 try:
316 user = UserProxy.objects.by_username_or_email(username, email)
317 except UserProxy.DoesNotExist:
318 messages.error(request, _('No user found'))
319 return HttpResponseRedirect(reverse('admin-activate-user'))
321 user.activate()
322 messages.success(request,
323 _('User {username} ({email}) activated'.format(
324 username=user.username, email=user.email)))
325 return HttpResponseRedirect(reverse('admin-activate-user'))
329 class MakePublisherInput(AdminView):
330 """ Get all information necessary for making someone publisher """
332 template_name = 'admin/make-publisher-input.html'
335 class MakePublisher(AdminView):
336 """ Assign publisher permissions """
338 template_name = 'admin/make-publisher-result.html'
340 def post(self, request):
341 User = get_user_model()
342 username = request.POST.get('username')
343 user = User.objects.get(username=username)
344 if user is None:
345 messages.error(request, 'User "{username}" not found'.format(username=username))
346 return HttpResponseRedirect(reverse('admin-make-publisher-input'))
348 feeds = request.POST.get('feeds')
349 feeds = feeds.split()
350 podcasts = set()
352 for feed in feeds:
353 try:
354 podcast = Podcast.objects.get(urls__url=feed)
355 except Podcast.DoesNotExist:
356 messages.warning(request, 'Podcast with URL {feed} not found'.format(feed=feed))
357 continue
359 podcasts.add(podcast)
361 created, existed = self.set_publisher(request, user, podcasts)
363 if (created + existed) > 0:
364 self.send_mail(request, user, podcasts)
365 return HttpResponseRedirect(reverse('admin-make-publisher-result'))
367 def set_publisher(self, request, user, podcasts):
368 created, existed = PublishedPodcast.objects.publish_podcasts(user,
369 podcasts)
370 messages.success(request,
371 'Set publisher permissions for {created} podcasts; '
372 '{existed} already existed'.format(created=created,
373 existed=existed))
374 return created, existed
376 def send_mail(self, request, user, podcasts):
377 site = RequestSite(request)
378 msg = render_to_string('admin/make-publisher-mail.txt', {
379 'user': user,
380 'podcasts': podcasts,
381 'support_url': settings.SUPPORT_URL,
382 'site': site,
384 context_instance=RequestContext(request))
385 subj = get_email_subject(site, _('Publisher Permissions'))
387 user.email_user(subj, msg)
388 messages.success(request, 'Sent email to user "{username}"'.format(username=user.username))
391 class MakePublisherResult(AdminView):
392 template_name = 'make-publisher-result.html'
395 def get_email_subject(site, txt):
396 return '[{domain}] {txt}'.format(domain=site.domain, txt=txt)