3 from itertools
import count
, chain
4 from collections
import Counter
5 from datetime
import datetime
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({
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
}
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
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))
113 return self
.render_to_response({
118 class MergeBase(AdminView
):
120 def _get_podcasts(self
, request
):
123 podcast_url
= request
.POST
.get('feed%d' % n
, None)
124 if podcast_url
is None:
130 p
= Podcast
.objects
.get(urls__url
=podcast_url
)
136 class MergeVerify(MergeBase
):
138 template_name
= 'admin/merge-grouping.html'
140 def post(self
, request
):
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
)))
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
):
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
)
181 for key
, feature
in request
.POST
.items():
182 m
= self
.RE_EPISODE
.match(key
)
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',
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({
222 # clear cache to make merge result visible
223 # TODO: what to do with multiple frontends?
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({
235 'actions': actions
.items(),
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
):
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
):
275 clients
= cs
.get_entries()
277 return JsonResponse(map(self
.to_dict
, clients
.most_common()))
279 def to_dict(self
, res
):
282 if not isinstance(obj
, tuple):
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
):
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({
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'))
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'))
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'))
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'))
371 messages
.success(request
, 'User {username} is already activated')
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')
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()
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
))
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
,
426 messages
.success(request
,
427 'Set publisher permissions for {created} podcasts; '
428 '{existed} already existed'.format(created
=created
,
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', {
436 'podcasts': podcasts
,
437 'support_url': settings
.SUPPORT_URL
,
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
)