3 from itertools
import count
, chain
4 from collections
import Counter
5 from datetime
import datetime
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
, unify_slugs
28 from mygpo
.utils
import get_git_head
29 from mygpo
.api
.httpresponse
import JsonResponse
30 from mygpo
.cel
import celery
31 from mygpo
.db
.couchdb
import get_main_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_main_database()
62 db_tasks
= main_db
.server
.active_tasks()
64 i
= celery
.control
.inspect()
65 scheduled
= i
.scheduled()
67 num_celery_tasks
= None
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({
78 'django_version': django_version
,
79 'main_db': main_db
.uri
,
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
94 class MergeSelect(AdminView
):
95 template_name
= 'admin/merge-select.html'
97 def get(self
, request
):
98 num
= int(request
.GET
.get('podcasts', 2))
101 return self
.render_to_response({
106 class MergeBase(AdminView
):
108 def _get_podcasts(self
, request
):
111 podcast_url
= request
.POST
.get('feed%d' % n
, None)
112 if podcast_url
is None:
118 p
= Podcast
.objects
.get(urls__url
=podcast_url
)
124 class MergeVerify(MergeBase
):
126 template_name
= 'admin/merge-grouping.html'
128 def post(self
, request
):
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
)))
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
):
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
)
169 for key
, feature
in request
.POST
.items():
170 m
= self
.RE_EPISODE
.match(key
)
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',
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({
210 # clear cache to make merge result visible
211 # TODO: what to do with multiple frontends?
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({
223 'actions': actions
.items(),
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
):
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
):
263 clients
= cs
.get_entries()
265 return JsonResponse(map(self
.to_dict
, clients
.most_common()))
267 def to_dict(self
, res
):
270 if not isinstance(obj
, tuple):
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
):
283 'podcasts': Podcast
.objects
.count(),
284 'episodes': Episode
.objects
.count(),
285 'users': User
.count(),
288 def get(self
, request
):
289 stats
= self
._get
_stats
()
290 return self
.render_to_response({
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'))
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)
330 messages
.error(request
, _('No user found'))
331 return HttpResponseRedirect(reverse('admin-activate-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 UnifyDuplicateSlugsSelect(AdminView
):
342 """ select a podcast for which to unify slugs """
343 template_name
= 'admin/unify-slugs-select.html'
346 class UnifyDuplicateSlugs(AdminView
):
347 """ start slug-unification task """
349 def post(self
, request
):
350 podcast_url
= request
.POST
.get('feed')
353 podcast
= Podcast
.objects
.get(urls__url
=podcast_url
)
354 except Podcast
.DoesNotExist
:
355 messages
.error(request
, _('Podcast with URL "%s" does not exist' %
357 return HttpResponseRedirect(reverse('admin-unify-slugs-select'))
359 res
= unify_slugs
.delay(podcast
)
360 return HttpResponseRedirect(reverse('admin-unify-slugs-status',
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({
377 # clear cache to make merge result visible
378 # TODO: what to do with multiple frontends?
381 actions
, podcast
= result
.get()
383 return self
.render_to_response({
385 'actions': actions
.items(),
390 class MakePublisherInput(AdminView
):
391 """ Get all information necessary for making someone publisher """
393 template_name
= 'admin/make-publisher-input.html'
396 class MakePublisher(AdminView
):
397 """ Assign publisher permissions """
399 template_name
= 'admin/make-publisher-result.html'
401 def post(self
, request
):
402 username
= request
.POST
.get('username')
403 user
= User
.get_user(username
)
405 messages
.error(request
, 'User "{username}" not found'.format(username
=username
))
406 return HttpResponseRedirect(reverse('admin-make-publisher-input'))
408 feeds
= request
.POST
.get('feeds')
409 feeds
= feeds
.split()
414 podcast
= Podcast
.objects
.get(urls__url
=feed
)
415 except Podcast
.DoesNotExist
:
416 messages
.warning(request
, 'Podcast with URL {feed} not found'.format(feed
=feed
))
419 podcasts
.add(podcast
)
421 self
.set_publisher(request
, user
, podcasts
)
422 self
.send_mail(request
, user
, podcasts
)
423 return HttpResponseRedirect(reverse('admin-make-publisher-result'))
425 def set_publisher(self
, request
, user
, podcasts
):
426 podcast_ids
= set(p
.get_id() for p
in podcasts
)
427 add_published_objs(user
, podcast_ids
)
428 messages
.success(request
, 'Set publisher permissions for {count} podcasts'.format(count
=len(podcast_ids
)))
430 def send_mail(self
, request
, user
, podcasts
):
431 site
= RequestSite(request
)
432 msg
= render_to_string('admin/make-publisher-mail.txt', {
434 'podcasts': podcasts
,
435 'support_url': settings
.SUPPORT_URL
,
438 context_instance
=RequestContext(request
))
439 subj
= get_email_subject(site
, _('Publisher Permissions'))
441 user
.email_user(subj
, msg
)
442 messages
.success(request
, 'Sent email to user "{username}"'.format(username
=user
.username
))
445 class MakePublisherResult(AdminView
):
446 template_name
= 'make-publisher-result.html'
449 def get_email_subject(site
, txt
):
450 return '[{domain}] {txt}'.format(domain
=site
.domain
, txt
=txt
)