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
.utils
.translation
import ugettext
as _
14 from django
.views
.generic
import TemplateView
15 from django
.utils
.decorators
import method_decorator
16 from django
.conf
import settings
18 from mygpo
.admin
.auth
import require_staff
19 from mygpo
.admin
.group
import PodcastGrouper
20 from mygpo
.maintenance
.merge
import PodcastMerger
, IncorrectMergeException
21 from mygpo
.users
.models
import User
22 from mygpo
.admin
.clients
import UserAgentStats
, ClientStats
23 from mygpo
.admin
.tasks
import merge_podcasts
, unify_slugs
24 from mygpo
.utils
import get_git_head
25 from mygpo
.api
.httpresponse
import JsonResponse
26 from mygpo
.cel
import celery
27 from mygpo
.db
.couchdb
import get_main_database
28 from mygpo
.db
.couchdb
.user
import activate_user
29 from mygpo
.db
.couchdb
.episode
import episode_count
, filetype_stats
30 from mygpo
.db
.couchdb
.podcast
import podcast_count
, podcast_for_url
, \
31 podcast_duplicates_for_url
, podcasts_by_next_update
34 class InvalidPodcast(Exception):
35 """ raised when we try to merge a podcast that doesn't exist """
37 class AdminView(TemplateView
):
39 @method_decorator(require_staff
)
40 def dispatch(self
, *args
, **kwargs
):
41 return super(AdminView
, self
).dispatch(*args
, **kwargs
)
44 class Overview(AdminView
):
45 template_name
= 'admin/overview.html'
48 class HostInfo(AdminView
):
49 """ shows host information for diagnosis """
51 template_name
= 'admin/hostinfo.html'
53 def get(self
, request
):
54 commit
, msg
= get_git_head()
55 base_dir
= settings
.BASE_DIR
56 hostname
= socket
.gethostname()
57 django_version
= django
.VERSION
59 main_db
= get_main_database()
61 db_tasks
= main_db
.server
.active_tasks()
63 i
= celery
.control
.inspect()
64 scheduled
= i
.scheduled()
66 num_celery_tasks
= None
68 num_celery_tasks
= sum(len(node
) for node
in scheduled
.values())
70 feed_queue_status
= self
._get
_feed
_queue
_status
()
72 return self
.render_to_response({
77 'django_version': django_version
,
78 'main_db': main_db
.uri
,
80 'num_celery_tasks': num_celery_tasks
,
81 'feed_queue_status': feed_queue_status
,
84 def _get_feed_queue_status(self
):
85 now
= datetime
.utcnow()
86 next_podcast
= podcasts_by_next_update(limit
=1)[0]
88 delta
= (next_podcast
.next_update
- now
)
89 delta_mins
= delta
.total_seconds() / 60
93 class MergeSelect(AdminView
):
94 template_name
= 'admin/merge-select.html'
96 def get(self
, request
):
97 num
= int(request
.GET
.get('podcasts', 2))
100 return self
.render_to_response({
105 class MergeBase(AdminView
):
107 def _get_podcasts(self
, request
):
110 podcast_url
= request
.POST
.get('feed%d' % n
, None)
111 if podcast_url
is None:
117 ps
= podcast_duplicates_for_url(podcast_url
)
120 raise InvalidPodcast(podcast_url
)
123 if p
not in podcasts
:
129 class MergeVerify(MergeBase
):
131 template_name
= 'admin/merge-grouping.html'
133 def post(self
, request
):
136 podcasts
= self
._get
_podcasts
(request
)
138 grouper
= PodcastGrouper(podcasts
)
140 get_features
= lambda (e_id
, e
): ((e
.url
, e
.title
), e_id
)
142 num_groups
= grouper
.group(get_features
)
145 except InvalidPodcast
as ip
:
146 messages
.error(request
,
147 _('No podcast with URL {url}').format(url
=str(ip
)))
152 return self
.render_to_response({
153 'podcasts': podcasts
,
154 'groups': num_groups
,
158 class MergeProcess(MergeBase
):
160 RE_EPISODE
= re
.compile(r
'episode_([0-9a-fA-F]{32})')
162 def post(self
, request
):
165 podcasts
= self
._get
_podcasts
(request
)
167 except InvalidPodcast
as ip
:
168 messages
.error(request
,
169 _('No podcast with URL {url}').format(url
=str(ip
)))
171 grouper
= PodcastGrouper(podcasts
)
174 for key
, feature
in request
.POST
.items():
175 m
= self
.RE_EPISODE
.match(key
)
177 episode_id
= m
.group(1)
178 features
[episode_id
] = feature
180 get_features
= lambda (e_id
, e
): (features
.get(e_id
, e_id
), e_id
)
182 num_groups
= grouper
.group(get_features
)
184 if 'renew' in request
.POST
:
185 return render(request
, 'admin/merge-grouping.html', {
186 'podcasts': podcasts
,
187 'groups': num_groups
,
191 elif 'merge' in request
.POST
:
193 podcast_ids
= [p
.get_id() for p
in podcasts
]
194 num_groups
= list(num_groups
)
196 res
= merge_podcasts
.delay(podcast_ids
, num_groups
)
198 return HttpResponseRedirect(reverse('admin-merge-status',
202 class MergeStatus(AdminView
):
203 """ Displays the status of the merge operation """
205 template_name
= 'admin/task-status.html'
207 def get(self
, request
, task_id
):
208 result
= merge_podcasts
.AsyncResult(task_id
)
210 if not result
.ready():
211 return self
.render_to_response({
215 # clear cache to make merge result visible
216 # TODO: what to do with multiple frontends?
220 actions
, podcast
= result
.get()
222 except IncorrectMergeException
as ime
:
223 messages
.error(request
, str(ime
))
224 return HttpResponseRedirect(reverse('admin-merge'))
226 return self
.render_to_response({
228 'actions': actions
.items(),
234 class UserAgentStatsView(AdminView
):
235 template_name
= 'admin/useragents.html'
237 def get(self
, request
):
239 uas
= UserAgentStats()
240 useragents
= uas
.get_entries()
242 return self
.render_to_response({
243 'useragents': useragents
.most_common(),
244 'max_users': uas
.max_users
,
245 'total': uas
.total_users
,
249 class ClientStatsView(AdminView
):
250 template_name
= 'admin/clients.html'
252 def get(self
, request
):
255 clients
= cs
.get_entries()
257 return self
.render_to_response({
258 'clients': clients
.most_common(),
259 'max_users': cs
.max_users
,
260 'total': cs
.total_users
,
264 class ClientStatsJsonView(AdminView
):
265 def get(self
, request
):
268 clients
= cs
.get_entries()
270 return JsonResponse(map(self
.to_dict
, clients
.most_common()))
272 def to_dict(self
, res
):
275 if not isinstance(obj
, tuple):
278 return obj
._asdict
(), count
281 class StatsView(AdminView
):
282 """ shows general stats as HTML page """
284 template_name
= 'admin/stats.html'
286 def _get_stats(self
):
288 'podcasts': podcast_count(),
289 'episodes': episode_count(),
290 'users': User
.count(),
293 def get(self
, request
):
294 stats
= self
._get
_stats
()
295 return self
.render_to_response({
300 class StatsJsonView(StatsView
):
301 """ provides general stats as JSON """
303 def get(self
, request
):
304 stats
= self
._get
_stats
()
305 return JsonResponse(stats
)
308 class FiletypeStatsView(AdminView
):
310 template_name
= 'admin/filetypes.html'
312 def get(self
, request
):
313 stats
= filetype_stats()
316 max_num
= stats
.most_common(1)[0][1]
320 return self
.render_to_response({
322 'stats': stats
.most_common(),
326 class ActivateUserView(AdminView
):
327 """ Lets admins manually activate users """
329 template_name
= 'admin/activate-user.html'
331 def get(self
, request
):
332 return self
.render_to_response({})
334 def post(self
, request
):
336 username
= request
.POST
.get('username')
337 email
= request
.POST
.get('email')
339 if not (username
or email
):
340 messages
.error(request
,
341 _('Provide either username or email address'))
342 return HttpResponseRedirect(reverse('admin-activate-user'))
347 user
= User
.get_user(username
, is_active
=None)
349 if email
and not user
:
350 user
= User
.get_user_by_email(email
, is_active
=None)
353 messages
.error(request
, _('No user found'))
354 return HttpResponseRedirect(reverse('admin-activate-user'))
357 messages
.success(request
,
358 _('User {username} ({email}) activated'.format(
359 username
=user
.username
, email
=user
.email
)))
360 return HttpResponseRedirect(reverse('admin-activate-user'))
364 class UnifyDuplicateSlugsSelect(AdminView
):
365 """ select a podcast for which to unify slugs """
366 template_name
= 'admin/unify-slugs-select.html'
369 class UnifyDuplicateSlugs(AdminView
):
370 """ start slug-unification task """
372 def post(self
, request
):
373 podcast_url
= request
.POST
.get('feed')
374 podcast
= podcast_for_url(podcast_url
)
377 messages
.error(request
, _('Podcast with URL "%s" does not exist' %
379 return HttpResponseRedirect(reverse('admin-unify-slugs-select'))
381 res
= unify_slugs
.delay(podcast
)
382 return HttpResponseRedirect(reverse('admin-unify-slugs-status',
386 class UnifySlugsStatus(AdminView
):
387 """ Displays the status of the unify-slugs operation """
389 template_name
= 'admin/task-status.html'
391 def get(self
, request
, task_id
):
392 result
= merge_podcasts
.AsyncResult(task_id
)
394 if not result
.ready():
395 return self
.render_to_response({
399 # clear cache to make merge result visible
400 # TODO: what to do with multiple frontends?
403 actions
, podcast
= result
.get()
405 return self
.render_to_response({
407 'actions': actions
.items(),