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
20 from django
.contrib
.auth
import get_user_model
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
.publisher
.models
import PublishedPodcast
31 from mygpo
.api
.httpresponse
import JsonResponse
32 from mygpo
.celery
import celery
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 i
= celery
.control
.inspect()
61 scheduled
= i
.scheduled()
63 num_celery_tasks
= None
65 num_celery_tasks
= sum(len(node
) for node
in list(scheduled
.values()))
67 feed_queue_status
= self
._get
_feed
_queue
_status
()
69 return self
.render_to_response({
74 'django_version': django_version
,
75 'num_celery_tasks': num_celery_tasks
,
76 'feed_queue_status': feed_queue_status
,
79 def _get_feed_queue_status(self
):
80 now
= datetime
.utcnow()
81 next_podcast
= Podcast
.objects
.order_by_next_update().first()
83 delta
= (next_podcast
.next_update
- now
)
84 delta_mins
= delta
.total_seconds() / 60
88 class MergeSelect(AdminView
):
89 template_name
= 'admin/merge-select.html'
91 def get(self
, request
):
92 num
= int(request
.GET
.get('podcasts', 2))
95 return self
.render_to_response({
100 class MergeBase(AdminView
):
102 def _get_podcasts(self
, request
):
105 podcast_url
= request
.POST
.get('feed%d' % n
, None)
106 if podcast_url
is None:
112 p
= Podcast
.objects
.get(urls__url
=podcast_url
)
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_id_e
[1].url
, e_id_e
[1].title
), e_id_e
[0])
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 list(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_e1
: (features
.get(e_id_e1
[0], e_id_e1
[0]), e_id_e1
[0])
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': list(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(list(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
.objects
.count_fast(),
278 'episodes': Episode
.objects
.count_fast(),
279 'users': UserProxy
.objects
.count_fast(),
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 ActivateUserView(AdminView
):
298 """ Lets admins manually activate users """
300 template_name
= 'admin/activate-user.html'
302 def get(self
, request
):
303 return self
.render_to_response({})
305 def post(self
, request
):
307 username
= request
.POST
.get('username')
308 email
= request
.POST
.get('email')
310 if not (username
or email
):
311 messages
.error(request
,
312 _('Provide either username or email address'))
313 return HttpResponseRedirect(reverse('admin-activate-user'))
316 user
= UserProxy
.objects
.by_username_or_email(username
, email
)
317 except UserProxy
.DoesNotExist
:
318 messages
.error(request
, _('No user found'))
319 return HttpResponseRedirect(reverse('admin-activate-user'))
322 messages
.success(request
,
323 _('User {username} ({email}) activated'.format(
324 username
=user
.username
, email
=user
.email
)))
325 return HttpResponseRedirect(reverse('admin-activate-user'))
329 class MakePublisherInput(AdminView
):
330 """ Get all information necessary for making someone publisher """
332 template_name
= 'admin/make-publisher-input.html'
335 class MakePublisher(AdminView
):
336 """ Assign publisher permissions """
338 template_name
= 'admin/make-publisher-result.html'
340 def post(self
, request
):
341 User
= get_user_model()
342 username
= request
.POST
.get('username')
343 user
= User
.objects
.get(username
=username
)
345 messages
.error(request
, 'User "{username}" not found'.format(username
=username
))
346 return HttpResponseRedirect(reverse('admin-make-publisher-input'))
348 feeds
= request
.POST
.get('feeds')
349 feeds
= feeds
.split()
354 podcast
= Podcast
.objects
.get(urls__url
=feed
)
355 except Podcast
.DoesNotExist
:
356 messages
.warning(request
, 'Podcast with URL {feed} not found'.format(feed
=feed
))
359 podcasts
.add(podcast
)
361 created
, existed
= self
.set_publisher(request
, user
, podcasts
)
363 if (created
+ existed
) > 0:
364 self
.send_mail(request
, user
, podcasts
)
365 return HttpResponseRedirect(reverse('admin-make-publisher-result'))
367 def set_publisher(self
, request
, user
, podcasts
):
368 created
, existed
= PublishedPodcast
.objects
.publish_podcasts(user
,
370 messages
.success(request
,
371 'Set publisher permissions for {created} podcasts; '
372 '{existed} already existed'.format(created
=created
,
374 return created
, existed
376 def send_mail(self
, request
, user
, podcasts
):
377 site
= RequestSite(request
)
378 msg
= render_to_string('admin/make-publisher-mail.txt', {
380 'podcasts': podcasts
,
381 'support_url': settings
.SUPPORT_URL
,
384 context_instance
=RequestContext(request
))
385 subj
= get_email_subject(site
, _('Publisher Permissions'))
387 user
.email_user(subj
, msg
)
388 messages
.success(request
, 'Sent email to user "{username}"'.format(username
=user
.username
))
391 class MakePublisherResult(AdminView
):
392 template_name
= 'make-publisher-result.html'
395 def get_email_subject(site
, txt
):
396 return '[{domain}] {txt}'.format(domain
=site
.domain
, txt
=txt
)