3 from itertools
import count
, chain
4 from collections
import Counter
7 from django
.shortcuts
import render
8 from django
.contrib
import messages
9 from django
.core
.urlresolvers
import reverse
10 from django
.core
.cache
import cache
11 from django
.http
import HttpResponseRedirect
12 from django
.utils
.translation
import ugettext
as _
13 from django
.views
.generic
import TemplateView
14 from django
.utils
.decorators
import method_decorator
15 from django
.conf
import settings
17 from mygpo
.admin
.auth
import require_staff
18 from mygpo
.admin
.group
import PodcastGrouper
19 from mygpo
.maintenance
.merge
import PodcastMerger
, IncorrectMergeException
20 from mygpo
.users
.models
import User
21 from mygpo
.admin
.clients
import UserAgentStats
, ClientStats
22 from mygpo
.admin
.tasks
import merge_podcasts
, unify_slugs
23 from mygpo
.utils
import get_git_head
24 from mygpo
.api
.httpresponse
import JsonResponse
25 from mygpo
.cel
import celery
26 from mygpo
.db
.couchdb
import get_main_database
27 from mygpo
.db
.couchdb
.user
import activate_user
28 from mygpo
.db
.couchdb
.episode
import episode_count
, filetype_stats
29 from mygpo
.db
.couchdb
.podcast
import podcast_count
, podcast_for_url
, \
30 podcast_duplicates_for_url
33 class InvalidPodcast(Exception):
34 """ raised when we try to merge a podcast that doesn't exist """
36 class AdminView(TemplateView
):
38 @method_decorator(require_staff
)
39 def dispatch(self
, *args
, **kwargs
):
40 return super(AdminView
, self
).dispatch(*args
, **kwargs
)
43 class Overview(AdminView
):
44 template_name
= 'admin/overview.html'
47 class HostInfo(AdminView
):
48 """ shows host information for diagnosis """
50 template_name
= 'admin/hostinfo.html'
52 def get(self
, request
):
53 commit
, msg
= get_git_head()
54 base_dir
= settings
.BASE_DIR
55 hostname
= socket
.gethostname()
56 django_version
= django
.VERSION
58 main_db
= get_main_database()
60 db_tasks
= main_db
.server
.active_tasks()
62 i
= celery
.control
.inspect()
63 scheduled
= i
.scheduled()
65 num_celery_tasks
= None
67 num_celery_tasks
= sum(len(node
) for node
in scheduled
.values())
69 return self
.render_to_response({
74 'django_version': django_version
,
75 'main_db': main_db
.uri
,
77 'num_celery_tasks': num_celery_tasks
,
82 class MergeSelect(AdminView
):
83 template_name
= 'admin/merge-select.html'
85 def get(self
, request
):
86 num
= int(request
.GET
.get('podcasts', 2))
89 return self
.render_to_response({
94 class MergeBase(AdminView
):
96 def _get_podcasts(self
, request
):
99 podcast_url
= request
.POST
.get('feed%d' % n
, None)
100 if podcast_url
is None:
106 ps
= podcast_duplicates_for_url(podcast_url
)
109 raise InvalidPodcast(podcast_url
)
112 if p
not in podcasts
:
118 class MergeVerify(MergeBase
):
120 template_name
= 'admin/merge-grouping.html'
122 def post(self
, request
):
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
)))
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
):
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
)
163 for key
, feature
in request
.POST
.items():
164 m
= self
.RE_EPISODE
.match(key
)
166 episode_id
= m
.group(1)
167 features
[episode_id
] = feature
169 get_features
= lambda (e_id
, e
): (features
[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',
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({
204 # clear cache to make merge result visible
205 # TODO: what to do with multiple frontends?
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({
217 'actions': actions
.items(),
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
):
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
):
257 clients
= cs
.get_entries()
259 return JsonResponse(map(self
.to_dict
, clients
.most_common()))
261 def to_dict(self
, res
):
264 if not isinstance(obj
, tuple):
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
):
277 'podcasts': podcast_count(),
278 'episodes': episode_count(),
279 'users': User
.count(),
282 def get(self
, request
):
283 stats
= self
._get
_stats
()
284 return self
.render_to_response({
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 FiletypeStatsView(AdminView
):
299 template_name
= 'admin/filetypes.html'
301 def get(self
, request
):
302 stats
= filetype_stats()
305 max_num
= stats
.most_common(1)[0][1]
309 return self
.render_to_response({
311 'stats': stats
.most_common(),
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'))
336 user
= User
.get_user(username
, is_active
=None)
338 if email
and not user
:
339 user
= User
.get_user_by_email(email
, is_active
=None)
342 messages
.error(request
, _('No user found'))
343 return HttpResponseRedirect(reverse('admin-activate-user'))
346 messages
.success(request
,
347 _('User {username} ({email}) activated'.format(
348 username
=user
.username
, email
=user
.email
)))
349 return HttpResponseRedirect(reverse('admin-activate-user'))
353 class UnifyDuplicateSlugsSelect(AdminView
):
354 """ select a podcast for which to unify slugs """
355 template_name
= 'admin/unify-slugs-select.html'
358 class UnifyDuplicateSlugs(AdminView
):
359 """ start slug-unification task """
361 def post(self
, request
):
362 podcast_url
= request
.POST
.get('feed')
363 podcast
= podcast_for_url(podcast_url
)
366 messages
.error(request
, _('Podcast with URL "%s" does not exist' %
368 return HttpResponseRedirect(reverse('admin-unify-slugs-select'))
370 res
= unify_slugs
.delay(podcast
)
371 return HttpResponseRedirect(reverse('admin-unify-slugs-status',
375 class UnifySlugsStatus(AdminView
):
376 """ Displays the status of the unify-slugs operation """
378 template_name
= 'admin/task-status.html'
380 def get(self
, request
, task_id
):
381 result
= merge_podcasts
.AsyncResult(task_id
)
383 if not result
.ready():
384 return self
.render_to_response({
388 # clear cache to make merge result visible
389 # TODO: what to do with multiple frontends?
392 actions
, podcast
= result
.get()
394 return self
.render_to_response({
396 'actions': actions
.items(),