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
.contrib
.auth
import get_user_model
18 from django
.views
.generic
import TemplateView
19 from django
.utils
.decorators
import method_decorator
20 from django
.conf
import settings
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
.api
.httpresponse
import JsonResponse
31 from mygpo
.celery
import celery
32 from mygpo
.db
.couchdb
import get_userdata_database
33 from mygpo
.db
.couchdb
.user
import activate_user
, add_published_objs
36 class InvalidPodcast(Exception):
37 """ raised when we try to merge a podcast that doesn't exist """
39 class AdminView(TemplateView
):
41 @method_decorator(require_staff
)
42 def dispatch(self
, *args
, **kwargs
):
43 return super(AdminView
, self
).dispatch(*args
, **kwargs
)
46 class Overview(AdminView
):
47 template_name
= 'admin/overview.html'
50 class HostInfo(AdminView
):
51 """ shows host information for diagnosis """
53 template_name
= 'admin/hostinfo.html'
55 def get(self
, request
):
56 commit
, msg
= get_git_head()
57 base_dir
= settings
.BASE_DIR
58 hostname
= socket
.gethostname()
59 django_version
= django
.VERSION
61 main_db
= get_userdata_database()
63 db_tasks
= main_db
.server
.active_tasks()
65 i
= celery
.control
.inspect()
66 scheduled
= i
.scheduled()
68 num_celery_tasks
= None
70 num_celery_tasks
= sum(len(node
) for node
in scheduled
.values())
72 feed_queue_status
= self
._get
_feed
_queue
_status
()
74 return self
.render_to_response({
79 'django_version': django_version
,
80 'main_db': main_db
.uri
,
82 'num_celery_tasks': num_celery_tasks
,
83 'feed_queue_status': feed_queue_status
,
86 def _get_feed_queue_status(self
):
87 now
= datetime
.utcnow()
88 next_podcast
= Podcast
.objects
.order_by_next_update().first()
90 delta
= (next_podcast
.next_update
- now
)
91 delta_mins
= delta
.total_seconds() / 60
95 class MergeSelect(AdminView
):
96 template_name
= 'admin/merge-select.html'
98 def get(self
, request
):
99 num
= int(request
.GET
.get('podcasts', 2))
102 return self
.render_to_response({
107 class MergeBase(AdminView
):
109 def _get_podcasts(self
, request
):
112 podcast_url
= request
.POST
.get('feed%d' % n
, None)
113 if podcast_url
is None:
119 p
= Podcast
.objects
.get(urls__url
=podcast_url
)
125 class MergeVerify(MergeBase
):
127 template_name
= 'admin/merge-grouping.html'
129 def post(self
, request
):
132 podcasts
= self
._get
_podcasts
(request
)
134 grouper
= PodcastGrouper(podcasts
)
136 get_features
= lambda (e_id
, e
): ((e
.url
, e
.title
), e_id
)
138 num_groups
= grouper
.group(get_features
)
141 except InvalidPodcast
as ip
:
142 messages
.error(request
,
143 _('No podcast with URL {url}').format(url
=str(ip
)))
148 return self
.render_to_response({
149 'podcasts': podcasts
,
150 'groups': num_groups
,
154 class MergeProcess(MergeBase
):
156 RE_EPISODE
= re
.compile(r
'episode_([0-9a-fA-F]{32})')
158 def post(self
, request
):
161 podcasts
= self
._get
_podcasts
(request
)
163 except InvalidPodcast
as ip
:
164 messages
.error(request
,
165 _('No podcast with URL {url}').format(url
=str(ip
)))
167 grouper
= PodcastGrouper(podcasts
)
170 for key
, feature
in request
.POST
.items():
171 m
= self
.RE_EPISODE
.match(key
)
173 episode_id
= m
.group(1)
174 features
[episode_id
] = feature
176 get_features
= lambda (e_id
, e
): (features
.get(e_id
, e_id
), e_id
)
178 num_groups
= grouper
.group(get_features
)
180 if 'renew' in request
.POST
:
181 return render(request
, 'admin/merge-grouping.html', {
182 'podcasts': podcasts
,
183 'groups': num_groups
,
187 elif 'merge' in request
.POST
:
189 podcast_ids
= [p
.get_id() for p
in podcasts
]
190 num_groups
= list(num_groups
)
192 res
= merge_podcasts
.delay(podcast_ids
, num_groups
)
194 return HttpResponseRedirect(reverse('admin-merge-status',
198 class MergeStatus(AdminView
):
199 """ Displays the status of the merge operation """
201 template_name
= 'admin/task-status.html'
203 def get(self
, request
, task_id
):
204 result
= merge_podcasts
.AsyncResult(task_id
)
206 if not result
.ready():
207 return self
.render_to_response({
211 # clear cache to make merge result visible
212 # TODO: what to do with multiple frontends?
216 actions
, podcast
= result
.get()
218 except IncorrectMergeException
as ime
:
219 messages
.error(request
, str(ime
))
220 return HttpResponseRedirect(reverse('admin-merge'))
222 return self
.render_to_response({
224 'actions': actions
.items(),
230 class UserAgentStatsView(AdminView
):
231 template_name
= 'admin/useragents.html'
233 def get(self
, request
):
235 uas
= UserAgentStats()
236 useragents
= uas
.get_entries()
238 return self
.render_to_response({
239 'useragents': useragents
.most_common(),
240 'max_users': uas
.max_users
,
241 'total': uas
.total_users
,
245 class ClientStatsView(AdminView
):
246 template_name
= 'admin/clients.html'
248 def get(self
, request
):
251 clients
= cs
.get_entries()
253 return self
.render_to_response({
254 'clients': clients
.most_common(),
255 'max_users': cs
.max_users
,
256 'total': cs
.total_users
,
260 class ClientStatsJsonView(AdminView
):
261 def get(self
, request
):
264 clients
= cs
.get_entries()
266 return JsonResponse(map(self
.to_dict
, clients
.most_common()))
268 def to_dict(self
, res
):
271 if not isinstance(obj
, tuple):
274 return obj
._asdict
(), count
277 class StatsView(AdminView
):
278 """ shows general stats as HTML page """
280 template_name
= 'admin/stats.html'
282 def _get_stats(self
):
284 'podcasts': Podcast
.objects
.count_fast(),
285 'episodes': Episode
.objects
.count_fast(),
286 'users': UserProxy
.objects
.count_fast(),
289 def get(self
, request
):
290 stats
= self
._get
_stats
()
291 return self
.render_to_response({
296 class StatsJsonView(StatsView
):
297 """ provides general stats as JSON """
299 def get(self
, request
):
300 stats
= self
._get
_stats
()
301 return JsonResponse(stats
)
304 class ActivateUserView(AdminView
):
305 """ Lets admins manually activate users """
307 template_name
= 'admin/activate-user.html'
309 def get(self
, request
):
310 return self
.render_to_response({})
312 def post(self
, request
):
314 User
= get_user_model()
315 username
= request
.POST
.get('username')
316 email
= request
.POST
.get('email')
318 if not (username
or email
):
319 messages
.error(request
,
320 _('Provide either username or email address'))
321 return HttpResponseRedirect(reverse('admin-activate-user'))
326 user
= User
.objects
.get(username
=username
)
328 if email
and not user
:
329 user
= User
.objects
.get(email
=email
)
332 messages
.error(request
, _('No user found'))
333 return HttpResponseRedirect(reverse('admin-activate-user'))
336 messages
.success(request
,
337 _('User {username} ({email}) activated'.format(
338 username
=user
.username
, email
=user
.email
)))
339 return HttpResponseRedirect(reverse('admin-activate-user'))
343 class MakePublisherInput(AdminView
):
344 """ Get all information necessary for making someone publisher """
346 template_name
= 'admin/make-publisher-input.html'
349 class MakePublisher(AdminView
):
350 """ Assign publisher permissions """
352 template_name
= 'admin/make-publisher-result.html'
354 def post(self
, request
):
355 username
= request
.POST
.get('username')
356 user
= User
.objects
.get(username
=username
)
358 messages
.error(request
, 'User "{username}" not found'.format(username
=username
))
359 return HttpResponseRedirect(reverse('admin-make-publisher-input'))
361 feeds
= request
.POST
.get('feeds')
362 feeds
= feeds
.split()
367 podcast
= Podcast
.objects
.get(urls__url
=feed
)
368 except Podcast
.DoesNotExist
:
369 messages
.warning(request
, 'Podcast with URL {feed} not found'.format(feed
=feed
))
372 podcasts
.add(podcast
)
374 self
.set_publisher(request
, user
, podcasts
)
375 self
.send_mail(request
, user
, podcasts
)
376 return HttpResponseRedirect(reverse('admin-make-publisher-result'))
378 def set_publisher(self
, request
, user
, podcasts
):
379 podcast_ids
= set(p
.get_id() for p
in podcasts
)
380 add_published_objs(user
, podcast_ids
)
381 messages
.success(request
, 'Set publisher permissions for {count} podcasts'.format(count
=len(podcast_ids
)))
383 def send_mail(self
, request
, user
, podcasts
):
384 site
= RequestSite(request
)
385 msg
= render_to_string('admin/make-publisher-mail.txt', {
387 'podcasts': podcasts
,
388 'support_url': settings
.SUPPORT_URL
,
391 context_instance
=RequestContext(request
))
392 subj
= get_email_subject(site
, _('Publisher Permissions'))
394 user
.email_user(subj
, msg
)
395 messages
.success(request
, 'Sent email to user "{username}"'.format(username
=user
.username
))
398 class MakePublisherResult(AdminView
):
399 template_name
= 'make-publisher-result.html'
402 def get_email_subject(site
, txt
):
403 return '[{domain}] {txt}'.format(domain
=site
.domain
, txt
=txt
)