Django-magic to prevent cross-site request forgery for POST requests
[mygpo.git] / mygpo / web / views / __init__.py
blobb4768b0ca42f12bbffe0d4b2a7873f5fed7227d9
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
24 from mygpo.data.models import Listener
25 from mygpo.web.models import Rating
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
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 datetime import datetime, date, timedelta
36 from django.contrib.sites.models import Site
37 from django.conf import settings
38 from registration.models import RegistrationProfile
39 from sets import Set
40 from mygpo.api.sanitizing import sanitize_url
41 from mygpo.web.users import get_user
42 from mygpo.log import log
43 from mygpo.utils import daterange
44 from mygpo.constants import PODCAST_LOGO_SIZE, PODCAST_LOGO_BIG_SIZE
45 from mygpo.web import utils
46 from mygpo.api import simple
47 import re
48 import os
49 import Image
50 import ImageDraw
51 import StringIO
53 def home(request):
54 if request.user.is_authenticated():
55 return dashboard(request)
56 else:
57 return welcome(request)
60 def welcome(request):
61 current_site = Site.objects.get_current()
62 podcasts = Podcast.objects.count()
63 users = User.objects.count()
64 episodes = Episode.objects.count()
65 return render_to_response('home.html', {
66 'podcast_count': podcasts,
67 'user_count': users,
68 'episode_count': episodes,
69 'url': current_site,
70 }, context_instance=RequestContext(request))
73 @login_required
74 def dashboard(request, episode_count=10):
75 site = Site.objects.get_current()
76 devices = Device.objects.filter(user=request.user, deleted=False)
77 subscribed_podcasts = set([s.podcast for s in Subscription.objects.filter(user=request.user)])
78 newest_episodes = Episode.objects.filter(podcast__in=subscribed_podcasts).order_by('-timestamp')[:episode_count]
80 return render_to_response('dashboard.html', {
81 'site': site,
82 'devices': devices,
83 'subscribed_podcasts': subscribed_podcasts,
84 'newest_episodes': newest_episodes,
85 }, context_instance=RequestContext(request))
88 def cover_art(request, size, filename):
89 size = int(size)
90 if size not in (PODCAST_LOGO_SIZE, PODCAST_LOGO_BIG_SIZE):
91 raise Http404('Wrong size')
93 # XXX: Is there a "cleaner" way to get the root directory of the installation?
94 root = os.path.join(os.path.dirname(__file__), '..', '..', '..')
95 target = os.path.join(root, 'htdocs', 'media', 'logo', str(size), filename+'.jpg')
96 filepath = os.path.join(root, 'htdocs', 'media', 'logo', filename)
98 if os.path.exists(target):
99 return HttpResponseRedirect('/media/logo/%s/%s.jpg' % (str(size), filename))
101 if os.path.exists(filepath):
102 target_dir = os.path.dirname(target)
103 if not os.path.isdir(target_dir):
104 os.makedirs(target_dir)
106 try:
107 im = Image.open(filepath)
108 if im.mode not in ('RGB', 'RGBA'):
109 im = im.convert('RGB')
110 except:
111 raise Http404('Cannot open cover file')
113 try:
114 resized = im.resize((size, size), Image.ANTIALIAS)
115 except IOError:
116 # raised when trying to read an interlaced PNG; we use the original instead
117 return HttpResponseRedirect('/media/logo/%s' % filename)
119 # If it's a RGBA image, composite it onto a white background for JPEG
120 if resized.mode == 'RGBA':
121 background = Image.new('RGB', resized.size)
122 draw = ImageDraw.Draw(background)
123 draw.rectangle((-1, -1, resized.size[0]+1, resized.size[1]+1), \
124 fill=(255, 255, 255))
125 del draw
126 resized = Image.composite(resized, background, resized)
128 io = StringIO.StringIO()
129 resized.save(io, 'JPEG', optimize=True, progression=True, quality=80)
130 s = io.getvalue()
132 fp = open(target, 'wb')
133 fp.write(s)
134 fp.close()
136 return HttpResponseRedirect('/media/logo/%s/%s.jpg' % (str(size), filename))
137 else:
138 raise Http404('Cover art not available')
140 @login_required
141 def subscriptions(request):
142 current_site = Site.objects.get_current()
143 subscriptionlist = create_subscriptionlist(request)
144 return render_to_response('subscriptions.html', {
145 'subscriptionlist': subscriptionlist,
146 'url': current_site
147 }, context_instance=RequestContext(request))
149 def create_subscriptionlist(request):
150 #sync all devices first
151 for d in Device.objects.filter(user=request.user):
152 d.sync()
154 subscriptions = Subscription.objects.filter(user=request.user)
156 l = {}
157 for s in subscriptions:
158 if s.podcast in l:
159 l[s.podcast]['devices'].append(s.device)
160 else:
161 e = Episode.objects.filter(podcast=s.podcast, timestamp__isnull=False).order_by('-timestamp')
162 episode = e[0] if e.count() > 0 else None
163 devices = [s.device]
164 l[s.podcast] = {'podcast': s.podcast, 'episode': episode, 'devices': devices}
166 return l.values()
168 def podcast(request, pid):
169 podcast = get_object_or_404(Podcast, pk=pid)
170 episodes = episode_list(podcast, request.user)
171 max_listeners = max([x.listeners for x in episodes]) if len(episodes) else 0
173 if request.user.is_authenticated():
174 devices = Device.objects.filter(user=request.user)
175 history = SubscriptionAction.objects.filter(podcast=podcast,device__in=devices).order_by('-timestamp')
176 subscribed_devices = [s.device for s in Subscription.objects.filter(podcast=podcast,user=request.user)]
177 subscribe_targets = podcast.subscribe_targets(request.user)
178 success = False
181 qs = Subscription.objects.filter(podcast=podcast, user=request.user)
182 if qs.count()>0 and request.user.get_profile().public_profile:
183 # subscription meta is valid for all subscriptions, so we get one - doesn't matter which
184 subscription = qs[0]
185 subscriptionmeta = subscription.get_meta()
186 if request.method == 'POST':
187 privacy_form = PrivacyForm(request.POST)
188 if privacy_form.is_valid():
189 subscriptionmeta.public = privacy_form.cleaned_data['public']
190 try:
191 subscriptionmeta.save()
192 success = True
193 except IntegrityError, ie:
194 error_message = _('You can\'t use the same UID for two devices.')
195 else:
196 privacy_form = PrivacyForm({
197 'public': subscriptionmeta.public
200 else:
201 privacy_form = None
203 timeline_data = listener_data(podcast)
205 return render_to_response('podcast.html', {
206 'history': history,
207 'timeline_data': timeline_data,
208 'podcast': podcast,
209 'privacy_form': privacy_form,
210 'devices': subscribed_devices,
211 'can_subscribe': len(subscribe_targets) > 0,
212 'episodes': episodes,
213 'max_listeners': max_listeners,
214 'success': success
215 }, context_instance=RequestContext(request))
216 else:
217 current_site = Site.objects.get_current()
218 return render_to_response('podcast.html', {
219 'podcast': podcast,
220 'url': current_site,
221 'episodes': episodes,
222 'max_listeners': max_listeners,
223 }, context_instance=RequestContext(request))
225 def listener_data(podcast):
226 d = date(2010, 1, 1)
227 day = timedelta(1)
228 episodes = EpisodeAction.objects.filter(episode__podcast=podcast, timestamp__gte=d).order_by('timestamp').values('timestamp')
229 if len(episodes) == 0:
230 return []
232 start = episodes[0]['timestamp']
234 days = []
235 for d in daterange(start):
236 next = d + timedelta(days=1)
237 listeners = EpisodeAction.objects.filter(episode__podcast=podcast, timestamp__gte=d, timestamp__lt=next).values('user_id').distinct().count()
238 e = Episode.objects.filter(podcast=podcast, timestamp__gte=d, timestamp__lt=next)
239 episode = e[0] if e.count() > 0 else None
240 days.append({
241 'date': d,
242 'listeners': listeners,
243 'episode': episode})
245 return days
247 def history(request, len=15, device_id=None):
248 if device_id:
249 devices = Device.objects.filter(id=device_id)
250 else:
251 devices = Device.objects.filter(user=request.user)
253 history = SubscriptionAction.objects.filter(device__in=devices).order_by('-timestamp')[:len]
254 episodehistory = EpisodeAction.objects.filter(device__in=devices).order_by('-timestamp')[:len]
256 generalhistory = []
258 for row in history:
259 generalhistory.append(row)
260 for row in episodehistory:
261 generalhistory.append(row)
263 generalhistory.sort(key=lambda x: x.timestamp,reverse=True)
265 return render_to_response('history.html', {
266 'generalhistory': generalhistory,
267 'singledevice': devices[0] if device_id else None
268 }, context_instance=RequestContext(request))
271 @login_required
272 def podcast_subscribe(request, pid):
273 podcast = get_object_or_404(Podcast, pk=pid)
274 error_message = None
276 if request.method == 'POST':
277 form = SyncForm(request.POST)
279 try:
280 target = form.get_target()
282 if isinstance(target, SyncGroup):
283 device = target.devices()[0]
284 else:
285 device = target
287 try:
288 SubscriptionAction.objects.create(podcast=podcast, device=device, action=SUBSCRIBE_ACTION)
289 except IntegrityError, e:
290 log('error while subscribing to podcast (device %s, podcast %s)' % (device.id, podcast.id))
292 return HttpResponseRedirect('/podcast/%s' % podcast.id)
294 except ValueError, e:
295 error_message = _('Could not subscribe to the podcast: %s' % e)
297 targets = podcast.subscribe_targets(request.user)
299 form = SyncForm()
300 form.set_targets(targets, _('Choose a device:'))
302 return render_to_response('subscribe.html', {
303 'error_message': error_message,
304 'podcast': podcast,
305 'can_subscribe': len(targets) > 0,
306 'form': form
307 }, context_instance=RequestContext(request))
309 @login_required
310 def podcast_unsubscribe(request, pid, device_id):
312 return_to = request.GET.get('return_to')
314 if return_to == None:
315 raise Http404('Wrong URL')
317 podcast = get_object_or_404(Podcast, pk=pid)
318 device = Device.objects.get(pk=device_id)
319 try:
320 SubscriptionAction.objects.create(podcast=podcast, device=device, action=UNSUBSCRIBE_ACTION, timestamp=datetime.now())
321 except IntegrityError, e:
322 log('error while unsubscribing from podcast (device %s, podcast %s)' % (device.id, podcast.id))
324 return HttpResponseRedirect(return_to)
326 def episode_list(podcast, user):
328 Returns a list of episodes, with their action-attribute set to the latest
329 action. The attribute is unsert if there is no episode-action for
330 the episode.
332 episodes = Episode.objects.filter(podcast=podcast).order_by('-timestamp')
333 for e in episodes:
334 listeners = Listener.objects.filter(episode=e).values('user').distinct()
335 e.listeners = listeners.count()
337 if user.is_authenticated():
338 actions = EpisodeAction.objects.filter(episode=e, user=user).order_by('-timestamp')
339 if actions.count() > 0:
340 e.action = actions[0]
342 return episodes
345 def toplist(request, num=100, lang=None):
347 try:
348 lang = process_lang_params(request, '/toplist/')
349 except utils.UpdatedException, updated:
350 return HttpResponseRedirect('/toplist/?lang=%s' % ','.join(updated.data))
352 if len(lang) == 0:
353 entries = ToplistEntry.objects.order_by('-subscriptions')[:num]
355 else:
356 regex = '^(' + '|'.join(lang) + ')'
357 entries = ToplistEntry.objects.filter(podcast__language__regex=regex).order_by('-subscriptions')[:num]
359 max_subscribers = max([e.subscriptions for e in entries]) if entries else 0
360 current_site = Site.objects.get_current()
361 all_langs = utils.get_language_names(utils.get_podcast_languages())
362 return render_to_response('toplist.html', {
363 'entries': entries,
364 'max_subscribers': max_subscribers,
365 'url': current_site,
366 'languages': lang,
367 'all_languages': all_langs,
368 }, context_instance=RequestContext(request))
371 def episode_toplist(request, num=100):
373 try:
374 lang = process_lang_params(request, '/toplist/episodes')
375 except utils.UpdatedException, updated:
376 return HttpResponseRedirect('/toplist/episodes?lang=%s' % ','.join(updated.data))
378 if len(lang) == 0:
379 entries = EpisodeToplistEntry.objects.order_by('-listeners')[:num]
381 else:
382 regex = '^(' + '|'.join(lang) + ')'
383 entries = EpisodeToplistEntry.objects.filter(episode__podcast__language__regex=regex).order_by('-listeners')[:num]
385 current_site = Site.objects.get_current()
387 # Determine maximum listener amount (or 0 if no entries exist)
388 max_listeners = max([0]+[e.listeners for e in entries])
389 all_langs = utils.get_language_names(utils.get_podcast_languages())
390 return render_to_response('episode_toplist.html', {
391 'entries': entries,
392 'max_listeners': max_listeners,
393 'url': current_site,
394 'languages': lang,
395 'all_languages': all_langs,
396 }, context_instance=RequestContext(request))
399 def process_lang_params(request, url):
400 if 'lang' in request.GET:
401 lang = list(set([x for x in request.GET.get('lang').split(',') if x]))
403 if request.method == 'POST':
404 if request.POST.get('lang'):
405 lang = list(set(lang + [request.POST.get('lang')]))
406 raise utils.UpdatedException(lang)
408 if not 'lang' in request.GET:
409 lang = utils.get_accepted_lang(request)
411 return lang
413 def toplist_opml(request, count):
414 entries = ToplistEntry.objects.all().order_by('-subscriptions')[:count]
415 exporter = Exporter(_('my.gpodder.org - Top %s') % count)
417 opml = exporter.generate([e.podcast for e in entries])
419 return HttpResponse(opml, mimetype='text/xml')
422 @login_required
423 def suggestions(request):
425 rated = False
427 if 'rate' in request.GET:
428 Rating.objects.create(target='suggestions', user=request.user, rating=request.GET['rate'], timestamp=datetime.now())
429 rated = True
431 entries = SuggestionEntry.forUser(request.user)
432 current_site = Site.objects.get_current()
433 return render_to_response('suggestions.html', {
434 'entries': entries,
435 'rated' : rated,
436 'url': current_site
437 }, context_instance=RequestContext(request))
440 @login_required
441 def podcast_subscribe_url(request):
442 url = request.GET.get('url')
444 if url == None:
445 raise Http404('http://my.gpodder.org/subscribe?url=http://www.example.com/podcast.xml')
447 url = sanitize_url(url)
449 if url == '':
450 raise Http404('Please specify a valid url')
452 podcast, created = Podcast.objects.get_or_create(url=url)
454 return HttpResponseRedirect('/podcast/%d/subscribe' % podcast.pk)
457 def resend_activation(request):
458 error_message = ''
460 if request.method == 'GET':
461 form = ResendActivationForm()
462 return render_to_response('registration/resend_activation.html', {
463 'form': form,
464 }, context_instance=RequestContext(request))
466 site = Site.objects.get_current()
467 form = ResendActivationForm(request.POST)
469 try:
470 if not form.is_valid():
471 raise ValueError(_('Invalid Username entered'))
473 try:
474 user = get_user(form.cleaned_data['username'], form.cleaned_data['email'])
475 except User.DoesNotExist:
476 raise ValueError(_('User does not exist.'))
478 try:
479 profile = RegistrationProfile.objects.get(user=user)
480 except RegistrationProfile.DoesNotExist:
481 profile = RegistrationProfile.objects.create_profile(user)
483 if profile.activation_key == RegistrationProfile.ACTIVATED:
484 raise ValueError(_('Your account already has been activated. Go ahead and log in.'))
486 elif profile.activation_key_expired():
487 raise ValueError(_('Your activation key has expired. Please try another username, or retry with the same one tomorrow.'))
489 except ValueError, e:
490 return render_to_response('registration/resend_activation.html', {
491 'form': form,
492 'error_message' : e
493 }, context_instance=RequestContext(request))
496 try:
497 profile.send_activation_email(site)
499 except AttributeError:
500 #old versions of django-registration send registration mails from RegistrationManager
501 RegistrationProfile.objects.send_activation_email(profile, site)
503 return render_to_response('registration/resent_activation.html', context_instance=RequestContext(request))
506 @requires_token(object='subscriptions', action='r', denied_template='user_subscriptions_denied.html')
507 def user_subscriptions(request, username):
508 user = get_object_or_404(User, username=username)
509 subscriptions = [s for s in Subscription.objects.filter(user=user)]
510 public_subscriptions = set([s.podcast for s in subscriptions if s.get_meta().public])
511 return render_to_response('user_subscriptions.html', {
512 'subscriptions': public_subscriptions,
513 'other_user': user
514 }, context_instance=RequestContext(request))