refactoring
[mygpo.git] / mygpo / web / views / __init__.py
blobbd326ef9d99cb0a8c69088f5f0a5a4b52dd2b67e
2 # This file is part of my.gpodder.org.
4 # my.gpodder.org is free software: you can redistribute it and/or modify it
5 # under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or (at your
7 # option) any later version.
9 # my.gpodder.org is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
12 # License for more details.
14 # You should have received a copy of the GNU Affero General Public License
15 # along with my.gpodder.org. If not, see <http://www.gnu.org/licenses/>.
18 from django.shortcuts import render_to_response
19 from django.http import HttpResponseRedirect, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, Http404, HttpResponseForbidden
20 from django.contrib.auth import authenticate, login, logout
21 from django.contrib.auth.models import User
22 from django.template import RequestContext
23 from mygpo.api.models import Podcast, Episode, Device, EpisodeAction, SubscriptionAction, ToplistEntry, EpisodeToplistEntry, Subscription, SuggestionEntry, SyncGroup, SUBSCRIBE_ACTION, UNSUBSCRIBE_ACTION, SubscriptionMeta, UserProfile
24 from mygpo.data.models import Listener, SuggestionBlacklist
25 from mygpo.web.models import Rating, SecurityToken
26 from mygpo.web.forms import UserAccountForm, DeviceForm, SyncForm, PrivacyForm, ResendActivationForm
27 from django.forms import ValidationError
28 from mygpo.api.opml import Exporter
29 from django.utils.translation import ugettext as _
30 from mygpo.api.basic_auth import require_valid_user
31 from mygpo.decorators import requires_token, manual_gc
32 from django.contrib.auth.decorators import login_required
33 from django.shortcuts import get_object_or_404
34 from django.db import IntegrityError
35 from django.db.models import Sum
36 from datetime import datetime, date, timedelta
37 from django.contrib.sites.models import Site
38 from django.conf import settings
39 from registration.models import RegistrationProfile
40 from sets import Set
41 from mygpo.api.sanitizing import sanitize_url
42 from mygpo.web.users import get_user
43 from mygpo.log import log
44 from mygpo.utils import daterange
45 from mygpo.constants import PODCAST_LOGO_SIZE, PODCAST_LOGO_BIG_SIZE
46 from mygpo.web import utils
47 from mygpo.api import simple
48 from mygpo.api import backend
49 import re
50 import os
51 import Image
52 import ImageDraw
53 import StringIO
55 def home(request):
56 if request.user.is_authenticated():
57 return dashboard(request)
58 else:
59 return welcome(request)
62 @manual_gc
63 def welcome(request, toplist_entries=10):
64 current_site = Site.objects.get_current()
65 podcasts = Podcast.objects.count()
66 users = User.objects.filter(is_active=True).count()
67 episodes = Episode.objects.count()
68 hours_listened = Listener.objects.all().aggregate(hours=Sum('episode__duration'))['hours'] / (60 * 60)
70 try:
71 lang = process_lang_params(request, '/toplist/')
72 except utils.UpdatedException, updated:
73 lang = []
75 if len(lang) == 0:
76 entries = ToplistEntry.objects.all()[:toplist_entries]
77 else:
78 entries = backend.get_toplist(toplist_entries, lang)
80 toplist = [e.get_podcast() for e in entries]
81 sponsored_podcast = utils.get_sponsored_podcast()
83 return render_to_response('home.html', {
84 'podcast_count': podcasts,
85 'user_count': users,
86 'episode_count': episodes,
87 'url': current_site,
88 'hours_listened': hours_listened,
89 'toplist': toplist,
90 'sponsored_podcast': sponsored_podcast,
91 }, context_instance=RequestContext(request))
94 @manual_gc
95 @login_required
96 def dashboard(request, episode_count=10):
97 site = Site.objects.get_current()
98 devices = Device.objects.filter(user=request.user, deleted=False)
99 subscribed_podcasts = set([s.podcast for s in Subscription.objects.filter(user=request.user)])
100 newest_episodes = Episode.objects.filter(podcast__in=subscribed_podcasts).order_by('-timestamp')[:episode_count]
102 lang = utils.get_accepted_lang(request)
103 lang = utils.sanitize_language_codes(lang)
105 random_podcasts = backend.get_random_picks(lang)[:5]
106 sponsored_podcast = utils.get_sponsored_podcast()
108 return render_to_response('dashboard.html', {
109 'site': site,
110 'devices': devices,
111 'subscribed_podcasts': subscribed_podcasts,
112 'newest_episodes': newest_episodes,
113 'random_podcasts': random_podcasts,
114 'sponsored_podcast': sponsored_podcast,
115 }, context_instance=RequestContext(request))
118 def cover_art(request, size, filename):
119 size = int(size)
120 if size not in (PODCAST_LOGO_SIZE, PODCAST_LOGO_BIG_SIZE):
121 raise Http404('Wrong size')
123 # XXX: Is there a "cleaner" way to get the root directory of the installation?
124 root = os.path.join(os.path.dirname(__file__), '..', '..', '..')
125 target = os.path.join(root, 'htdocs', 'media', 'logo', str(size), filename+'.jpg')
126 filepath = os.path.join(root, 'htdocs', 'media', 'logo', filename)
128 if os.path.exists(target):
129 return HttpResponseRedirect('/media/logo/%s/%s.jpg' % (str(size), filename))
131 if os.path.exists(filepath):
132 target_dir = os.path.dirname(target)
133 if not os.path.isdir(target_dir):
134 os.makedirs(target_dir)
136 try:
137 im = Image.open(filepath)
138 if im.mode not in ('RGB', 'RGBA'):
139 im = im.convert('RGB')
140 except:
141 raise Http404('Cannot open cover file')
143 try:
144 resized = im.resize((size, size), Image.ANTIALIAS)
145 except IOError:
146 # raised when trying to read an interlaced PNG; we use the original instead
147 return HttpResponseRedirect('/media/logo/%s' % filename)
149 # If it's a RGBA image, composite it onto a white background for JPEG
150 if resized.mode == 'RGBA':
151 background = Image.new('RGB', resized.size)
152 draw = ImageDraw.Draw(background)
153 draw.rectangle((-1, -1, resized.size[0]+1, resized.size[1]+1), \
154 fill=(255, 255, 255))
155 del draw
156 resized = Image.composite(resized, background, resized)
158 io = StringIO.StringIO()
159 resized.save(io, 'JPEG', optimize=True, progression=True, quality=80)
160 s = io.getvalue()
162 fp = open(target, 'wb')
163 fp.write(s)
164 fp.close()
166 return HttpResponseRedirect('/media/logo/%s/%s.jpg' % (str(size), filename))
167 else:
168 raise Http404('Cover art not available')
170 @manual_gc
171 @login_required
172 def subscriptions(request):
173 current_site = Site.objects.get_current()
174 subscriptionlist = create_subscriptionlist(request)
175 return render_to_response('subscriptions.html', {
176 'subscriptionlist': subscriptionlist,
177 'url': current_site
178 }, context_instance=RequestContext(request))
180 def create_subscriptionlist(request):
181 #sync all devices first
182 for d in Device.objects.filter(user=request.user):
183 d.sync()
185 subscriptions = Subscription.objects.filter(user=request.user)
187 l = {}
188 for s in subscriptions:
189 if s.podcast in l:
190 l[s.podcast]['devices'].append(s.device)
191 else:
192 e = Episode.objects.filter(podcast=s.podcast, timestamp__isnull=False).order_by('-timestamp')
193 episode = e[0] if e.count() > 0 else None
194 devices = [s.device]
195 l[s.podcast] = {'podcast': s.podcast, 'episode': episode, 'devices': devices}
197 return l.values()
199 def podcast(request, pid):
200 podcast = get_object_or_404(Podcast, pk=pid)
201 episodes = episode_list(podcast, request.user)
202 max_listeners = max([x.listeners for x in episodes]) if len(episodes) else 0
203 related_podcasts = [x for x in podcast.group.podcasts() if x != podcast] if podcast.group else []
205 if request.user.is_authenticated():
206 devices = Device.objects.filter(user=request.user)
207 history = SubscriptionAction.objects.filter(podcast=podcast,device__in=devices).order_by('-timestamp')
208 subscribed_devices = [s.device for s in Subscription.objects.filter(podcast=podcast,user=request.user)]
209 subscribe_targets = podcast.subscribe_targets(request.user)
210 success = False
213 qs = Subscription.objects.filter(podcast=podcast, user=request.user)
214 if qs.count()>0 and request.user.get_profile().public_profile:
215 # subscription meta is valid for all subscriptions, so we get one - doesn't matter which
216 subscription = qs[0]
217 subscriptionmeta = subscription.get_meta()
218 if request.method == 'POST':
219 privacy_form = PrivacyForm(request.POST)
220 if privacy_form.is_valid():
221 subscriptionmeta.public = privacy_form.cleaned_data['public']
222 try:
223 subscriptionmeta.save()
224 success = True
225 except IntegrityError, ie:
226 error_message = _('You can\'t use the same Device ID for two devices.')
227 else:
228 privacy_form = PrivacyForm({
229 'public': subscriptionmeta.public
232 else:
233 privacy_form = None
235 timeline_data = listener_data(podcast)
237 return render_to_response('podcast.html', {
238 'history': history,
239 'timeline_data': timeline_data,
240 'podcast': podcast,
241 'privacy_form': privacy_form,
242 'devices': subscribed_devices,
243 'related_podcasts': related_podcasts,
244 'can_subscribe': len(subscribe_targets) > 0,
245 'episodes': episodes,
246 'max_listeners': max_listeners,
247 'success': success
248 }, context_instance=RequestContext(request))
249 else:
250 current_site = Site.objects.get_current()
251 return render_to_response('podcast.html', {
252 'podcast': podcast,
253 'related_podcasts': related_podcasts,
254 'url': current_site,
255 'episodes': episodes,
256 'max_listeners': max_listeners,
257 }, context_instance=RequestContext(request))
259 def listener_data(podcast):
260 d = date(2010, 1, 1)
261 day = timedelta(1)
262 episodes = EpisodeAction.objects.filter(episode__podcast=podcast, timestamp__gte=d).order_by('timestamp').values('timestamp')
263 if len(episodes) == 0:
264 return []
266 start = episodes[0]['timestamp']
268 days = []
269 for d in daterange(start):
270 next = d + timedelta(days=1)
271 listeners = EpisodeAction.objects.filter(episode__podcast=podcast, timestamp__gte=d, timestamp__lt=next).values('user_id').distinct().count()
272 e = Episode.objects.filter(podcast=podcast, timestamp__gte=d, timestamp__lt=next)
273 episode = e[0] if e.count() > 0 else None
274 days.append({
275 'date': d,
276 'listeners': listeners,
277 'episode': episode})
279 return days
281 @manual_gc
282 def history(request, len=15, device_id=None):
283 if device_id:
284 devices = Device.objects.filter(id=device_id)
285 else:
286 devices = Device.objects.filter(user=request.user)
288 history = SubscriptionAction.objects.filter(device__in=devices).order_by('-timestamp')[:len]
289 episodehistory = EpisodeAction.objects.filter(device__in=devices).order_by('-timestamp')[:len]
291 generalhistory = []
293 for row in history:
294 generalhistory.append(row)
295 for row in episodehistory:
296 generalhistory.append(row)
298 generalhistory.sort(key=lambda x: x.timestamp,reverse=True)
300 return render_to_response('history.html', {
301 'generalhistory': generalhistory,
302 'singledevice': devices[0] if device_id else None
303 }, context_instance=RequestContext(request))
306 @manual_gc
307 @login_required
308 def podcast_subscribe(request, pid):
309 podcast = get_object_or_404(Podcast, pk=pid)
310 error_message = None
312 if request.method == 'POST':
313 form = SyncForm(request.POST)
315 try:
316 target = form.get_target()
318 if isinstance(target, SyncGroup):
319 device = target.devices()[0]
320 else:
321 device = target
323 try:
324 SubscriptionAction.objects.create(podcast=podcast, device=device, action=SUBSCRIBE_ACTION)
325 except IntegrityError, e:
326 log('error while subscribing to podcast (device %s, podcast %s)' % (device.id, podcast.id))
328 return HttpResponseRedirect('/podcast/%s' % podcast.id)
330 except ValueError, e:
331 error_message = _('Could not subscribe to the podcast: %s' % e)
333 targets = podcast.subscribe_targets(request.user)
335 form = SyncForm()
336 form.set_targets(targets, _('Choose a device:'))
338 return render_to_response('subscribe.html', {
339 'error_message': error_message,
340 'podcast': podcast,
341 'can_subscribe': len(targets) > 0,
342 'form': form
343 }, context_instance=RequestContext(request))
346 @manual_gc
347 @login_required
348 def podcast_unsubscribe(request, pid, device_id):
350 return_to = request.GET.get('return_to')
352 if return_to == None:
353 raise Http404('Wrong URL')
355 podcast = get_object_or_404(Podcast, pk=pid)
356 device = Device.objects.get(pk=device_id)
357 try:
358 SubscriptionAction.objects.create(podcast=podcast, device=device, action=UNSUBSCRIBE_ACTION, timestamp=datetime.now())
359 except IntegrityError, e:
360 log('error while unsubscribing from podcast (device %s, podcast %s)' % (device.id, podcast.id))
362 return HttpResponseRedirect(return_to)
364 def episode_list(podcast, user):
366 Returns a list of episodes, with their action-attribute set to the latest
367 action. The attribute is unsert if there is no episode-action for
368 the episode.
370 episodes = Episode.objects.filter(podcast=podcast).order_by('-timestamp')
371 for e in episodes:
372 listeners = Listener.objects.filter(episode=e).values('user').distinct()
373 e.listeners = listeners.count()
375 if user.is_authenticated():
376 actions = EpisodeAction.objects.filter(episode=e, user=user).order_by('-timestamp')
377 if actions.count() > 0:
378 e.action = actions[0]
380 return episodes
383 @manual_gc
384 def toplist(request, num=100, lang=None):
386 try:
387 lang = process_lang_params(request, '/toplist/')
388 except utils.UpdatedException, updated:
389 return HttpResponseRedirect('/toplist/?lang=%s' % ','.join(updated.data))
391 if len(lang) == 0:
392 entries = ToplistEntry.objects.all()[:num]
394 else:
395 entries = backend.get_toplist(num, lang)
397 max_subscribers = max([e.subscriptions for e in entries]) if entries else 0
398 current_site = Site.objects.get_current()
399 all_langs = utils.get_language_names(utils.get_podcast_languages())
400 return render_to_response('toplist.html', {
401 'entries': entries,
402 'max_subscribers': max_subscribers,
403 'url': current_site,
404 'languages': lang,
405 'all_languages': all_langs,
406 }, context_instance=RequestContext(request))
409 @manual_gc
410 def episode_toplist(request, num=100):
412 try:
413 lang = process_lang_params(request, '/toplist/episodes')
414 except utils.UpdatedException, updated:
415 return HttpResponseRedirect('/toplist/episodes?lang=%s' % ','.join(updated.data))
417 if len(lang) == 0:
418 entries = EpisodeToplistEntry.objects.all()[:num]
420 else:
421 regex = '^(' + '|'.join(lang) + ')'
422 entries = EpisodeToplistEntry.objects.filter(episode__podcast__language__regex=regex)[:num]
424 current_site = Site.objects.get_current()
426 # Determine maximum listener amount (or 0 if no entries exist)
427 max_listeners = max([0]+[e.listeners for e in entries])
428 all_langs = utils.get_language_names(utils.get_podcast_languages())
429 return render_to_response('episode_toplist.html', {
430 'entries': entries,
431 'max_listeners': max_listeners,
432 'url': current_site,
433 'languages': lang,
434 'all_languages': all_langs,
435 }, context_instance=RequestContext(request))
438 def process_lang_params(request, url):
439 if 'lang' in request.GET:
440 lang = list(set([x for x in request.GET.get('lang').split(',') if x]))
442 if request.method == 'POST':
443 if request.POST.get('lang'):
444 lang = list(set(lang + [request.POST.get('lang')]))
445 raise utils.UpdatedException(lang)
447 if not 'lang' in request.GET:
448 lang = utils.get_accepted_lang(request)
450 return utils.sanitize_language_codes(lang)
453 @manual_gc
454 @login_required
455 def suggestions(request):
457 rated = False
459 if 'rate' in request.GET:
460 Rating.objects.create(target='suggestions', user=request.user, rating=request.GET['rate'], timestamp=datetime.now())
461 rated = True
463 if 'blacklist' in request.GET:
464 try:
465 blacklisted_podcast = Podcast.objects.get(id=request.GET['blacklist'])
466 SuggestionBlacklist.objects.create(user=request.user, podcast=blacklisted_podcast)
468 p, _created = UserProfile.objects.get_or_create(user=request.user)
469 p.suggestion_up_to_date = False
470 p.save()
472 except Exception, e:
473 print e
476 entries = SuggestionEntry.objects.for_user(request.user)
477 current_site = Site.objects.get_current()
478 return render_to_response('suggestions.html', {
479 'entries': entries,
480 'rated' : rated,
481 'url': current_site
482 }, context_instance=RequestContext(request))
485 @manual_gc
486 @login_required
487 def podcast_subscribe_url(request):
488 url = request.GET.get('url')
490 if url == None:
491 raise Http404('http://my.gpodder.org/subscribe?url=http://www.example.com/podcast.xml')
493 url = sanitize_url(url)
495 if url == '':
496 raise Http404('Please specify a valid url')
498 podcast, created = Podcast.objects.get_or_create(url=url)
500 return HttpResponseRedirect('/podcast/%d/subscribe' % podcast.pk)
503 @manual_gc
504 def resend_activation(request):
505 error_message = ''
507 if request.method == 'GET':
508 form = ResendActivationForm()
509 return render_to_response('registration/resend_activation.html', {
510 'form': form,
511 }, context_instance=RequestContext(request))
513 site = Site.objects.get_current()
514 form = ResendActivationForm(request.POST)
516 try:
517 if not form.is_valid():
518 raise ValueError(_('Invalid Username entered'))
520 try:
521 user = get_user(form.cleaned_data['username'], form.cleaned_data['email'])
522 except User.DoesNotExist:
523 raise ValueError(_('User does not exist.'))
525 p, c = UserProfile.objects.get_or_create(user=user)
526 if p.deleted:
527 raise ValueError(_('You have deleted your account, but you can regster again.'))
529 try:
530 profile = RegistrationProfile.objects.get(user=user)
531 except RegistrationProfile.DoesNotExist:
532 profile = RegistrationProfile.objects.create_profile(user)
534 if profile.activation_key == RegistrationProfile.ACTIVATED:
535 user.is_active = True
536 user.save()
537 raise ValueError(_('Your account already has been activated. Go ahead and log in.'))
539 elif profile.activation_key_expired():
540 raise ValueError(_('Your activation key has expired. Please try another username, or retry with the same one tomorrow.'))
542 except ValueError, e:
543 return render_to_response('registration/resend_activation.html', {
544 'form': form,
545 'error_message' : e
546 }, context_instance=RequestContext(request))
549 try:
550 profile.send_activation_email(site)
552 except AttributeError:
553 #old versions of django-registration send registration mails from RegistrationManager
554 RegistrationProfile.objects.send_activation_email(profile, site)
556 return render_to_response('registration/resent_activation.html', context_instance=RequestContext(request))
559 @manual_gc
560 @requires_token(object='subscriptions', action='r', denied_template='user_subscriptions_denied.html')
561 def user_subscriptions(request, username):
562 user = get_object_or_404(User, username=username)
563 public_subscriptions = backend.get_public_subscriptions(user)
564 token = SecurityToken.objects.get(object='subscriptions', action='r', user__username=username)
566 return render_to_response('user_subscriptions.html', {
567 'subscriptions': public_subscriptions,
568 'other_user': user,
569 'token': token,
570 }, context_instance=RequestContext(request))
572 @requires_token(object='subscriptions', action='r')
573 def user_subscriptions_opml(request, username):
574 user = get_object_or_404(User, username=username)
575 public_subscriptions = backend.get_public_subscriptions(user)
577 response = render_to_response('user_subscriptions.opml', {
578 'subscriptions': public_subscriptions,
579 'other_user': user
580 }, context_instance=RequestContext(request))
581 response['Content-Disposition'] = 'attachment; filename=%s-subscriptions.opml' % username
582 return response
585 @manual_gc
586 @login_required
587 def all_subscriptions_download(request):
588 podcasts = backend.get_all_subscriptions(request.user)
589 response = simple.format_subscriptions(podcasts, 'opml', request.user.username)
590 response['Content-Disposition'] = 'attachment; filename=all-subscriptions.opml'
591 return response
594 def gpodder_example_podcasts(request):
595 sponsored_podcast = utils.get_sponsored_podcast()
596 return render_to_response('gpodder_examples.opml', {
597 'sponsored_podcast': sponsored_podcast
598 }, context_instance=RequestContext(request))