apply new navigation to publisher pages
[mygpo.git] / mygpo / web / views / __init__.py
blob5f1882612b6413c43225b5763faae714ab7ba872
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, 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 django.contrib.auth.decorators import login_required
32 from django.shortcuts import get_object_or_404
33 from django.db import IntegrityError
34 from datetime import datetime, date, timedelta
35 from django.contrib.sites.models import Site
36 from django.conf import settings
37 from registration.models import RegistrationProfile
38 from sets import Set
39 from mygpo.api.sanitizing import sanitize_url
40 from mygpo.web.users import get_user
41 from mygpo.log import log
42 from mygpo.utils import daterange
43 from mygpo.constants import PODCAST_LOGO_SIZE, PODCAST_LOGO_BIG_SIZE
44 import re
45 import random
46 import string
47 import os
48 import Image
49 import ImageDraw
50 import StringIO
52 def home(request):
53 current_site = Site.objects.get_current()
54 podcasts = Podcast.objects.count()
55 users = User.objects.count()
56 episodes = Episode.objects.count()
57 return render_to_response('home.html', {
58 'podcast_count': podcasts,
59 'user_count': users,
60 'episode_count': episodes,
61 'url': current_site,
62 }, context_instance=RequestContext(request))
65 def cover_art(request, size, filename):
66 size = int(size)
67 if size not in (PODCAST_LOGO_SIZE, PODCAST_LOGO_BIG_SIZE):
68 raise http404('Wrong size')
70 # XXX: Is there a "cleaner" way to get the root directory of the installation?
71 root = os.path.join(os.path.dirname(__file__), '..', '..', '..')
72 target = os.path.join(root, 'htdocs', 'media', 'logo', str(size), filename+'.jpg')
73 filename = os.path.join(root, 'htdocs', 'media', 'logo', filename)
75 if os.path.exists(filename):
76 target_dir = os.path.dirname(target)
77 if not os.path.isdir(target_dir):
78 os.makedirs(target_dir)
80 try:
81 im = Image.open(filename)
82 if im.mode not in ('RGB', 'RGBA'):
83 im = im.convert('RGB')
84 except:
85 raise http404('Cannot open cover file')
87 resized = im.resize((size, size), Image.ANTIALIAS)
89 # If it's a RGBA image, composite it onto a white background for JPEG
90 if resized.mode == 'RGBA':
91 background = Image.new('RGB', resized.size)
92 draw = ImageDraw.Draw(background)
93 draw.rectangle((-1, -1, resized.size[0]+1, resized.size[1]+1), \
94 fill=(255, 255, 255))
95 del draw
96 resized = Image.composite(resized, background, resized)
98 io = StringIO.StringIO()
99 resized.save(io, 'JPEG', optimize=True, progression=True, quality=80)
100 s = io.getvalue()
102 fp = open(target, 'wb')
103 fp.write(s)
104 fp.close()
106 return HttpResponse(s, mimetype='image/jpeg')
107 else:
108 raise Http404('Cover art not available')
110 @login_required
111 def subscriptions(request):
112 current_site = Site.objects.get_current()
113 subscriptionlist = create_subscriptionlist(request)
114 return render_to_response('subscriptions.html', {
115 'subscriptionlist': subscriptionlist,
116 'url': current_site
117 }, context_instance=RequestContext(request))
119 def create_subscriptionlist(request):
120 #sync all devices first
121 for d in Device.objects.filter(user=request.user):
122 d.sync()
124 subscriptions = Subscription.objects.filter(user=request.user)
126 l = {}
127 for s in subscriptions:
128 if s.podcast in l:
129 l[s.podcast]['devices'].append(s.device)
130 else:
131 e = Episode.objects.filter(podcast=s.podcast, timestamp__isnull=False).order_by('-timestamp')
132 episode = e[0] if e.count() > 0 else None
133 devices = [s.device]
134 l[s.podcast] = {'podcast': s.podcast, 'episode': episode, 'devices': devices}
136 return l.values()
138 def podcast(request, pid):
139 podcast = get_object_or_404(Podcast, pk=pid)
140 episodes = episode_list(podcast, request.user)
141 max_listeners = max([x.listeners for x in episodes]) if len(episodes) else 0
143 if request.user.is_authenticated():
144 devices = Device.objects.filter(user=request.user)
145 history = SubscriptionAction.objects.filter(podcast=podcast,device__in=devices).order_by('-timestamp')
146 subscribed_devices = [s.device for s in Subscription.objects.filter(podcast=podcast,user=request.user)]
147 subscribe_targets = podcast.subscribe_targets(request.user)
148 success = False
151 qs = Subscription.objects.filter(podcast=podcast, user=request.user)
152 if qs.count()>0 and request.user.get_profile().public_profile:
153 # subscription meta is valid for all subscriptions, so we get one - doesn't matter which
154 subscription = qs[0]
155 subscriptionmeta = subscription.get_meta()
156 if request.method == 'POST':
157 privacy_form = PrivacyForm(request.POST)
158 if privacy_form.is_valid():
159 subscriptionmeta.public = privacy_form.cleaned_data['public']
160 try:
161 subscriptionmeta.save()
162 success = True
163 except IntegrityError, ie:
164 error_message = _('You can\'t use the same UID for two devices.')
165 else:
166 privacy_form = PrivacyForm({
167 'public': subscriptionmeta.public
170 else:
171 privacy_form = None
173 timeline_data = listener_data(podcast)
175 return render_to_response('podcast.html', {
176 'history': history,
177 'timeline_data': timeline_data,
178 'podcast': podcast,
179 'privacy_form': privacy_form,
180 'devices': subscribed_devices,
181 'can_subscribe': len(subscribe_targets) > 0,
182 'episodes': episodes,
183 'max_listeners': max_listeners,
184 'success': success
185 }, context_instance=RequestContext(request))
186 else:
187 current_site = Site.objects.get_current()
188 return render_to_response('podcast.html', {
189 'podcast': podcast,
190 'url': current_site,
191 'episodes': episodes,
192 'max_listeners': max_listeners,
193 }, context_instance=RequestContext(request))
195 def listener_data(podcast):
196 d = date(2010, 1, 1)
197 day = timedelta(1)
198 episodes = EpisodeAction.objects.filter(episode__podcast=podcast, timestamp__gte=d).order_by('timestamp').values('timestamp')
199 if len(episodes) == 0:
200 return []
202 start = episodes[0]['timestamp']
204 days = []
205 for d in daterange(start):
206 next = d + timedelta(days=1)
207 listeners = EpisodeAction.objects.filter(episode__podcast=podcast, timestamp__gte=d, timestamp__lt=next).values('user_id').distinct().count()
208 e = Episode.objects.filter(podcast=podcast, timestamp__gte=d, timestamp__lt=next)
209 episode = e[0] if e.count() > 0 else None
210 days.append({
211 'date': d,
212 'listeners': listeners,
213 'episode': episode})
215 return days
217 def history(request, len=15, device_id=None):
218 if device_id:
219 devices = Device.objects.filter(id=device_id)
220 else:
221 devices = Device.objects.filter(user=request.user)
223 history = SubscriptionAction.objects.filter(device__in=devices).order_by('-timestamp')[:len]
224 episodehistory = EpisodeAction.objects.filter(device__in=devices).order_by('-timestamp')[:len]
226 generalhistory = []
228 for row in history:
229 generalhistory.append(row)
230 for row in episodehistory:
231 generalhistory.append(row)
233 generalhistory.sort(key=lambda x: x.timestamp,reverse=True)
235 return render_to_response('history.html', {
236 'generalhistory': generalhistory,
237 'singledevice': devices[0] if device_id else None
238 }, context_instance=RequestContext(request))
241 @login_required
242 def devices(request):
243 devices = Device.objects.filter(user=request.user,deleted=False).order_by('sync_group')
244 return render_to_response('devicelist.html', {
245 'devices': devices,
246 }, context_instance=RequestContext(request))
249 @login_required
250 def podcast_subscribe(request, pid):
251 podcast = get_object_or_404(Podcast, pk=pid)
252 error_message = None
254 if request.method == 'POST':
255 form = SyncForm(request.POST)
257 try:
258 target = form.get_target()
260 if isinstance(target, SyncGroup):
261 device = target.devices()[0]
262 else:
263 device = target
265 try:
266 SubscriptionAction.objects.create(podcast=podcast, device=device, action=SUBSCRIBE_ACTION)
267 except IntegrityError, e:
268 log('error while subscribing to podcast (device %s, podcast %s)' % (device.id, podcast.id))
270 return HttpResponseRedirect('/podcast/%s' % podcast.id)
272 except ValueError, e:
273 error_message = _('Could not subscribe to the podcast: %s' % e)
275 targets = podcast.subscribe_targets(request.user)
277 form = SyncForm()
278 form.set_targets(targets, _('Choose a device:'))
280 return render_to_response('subscribe.html', {
281 'error_message': error_message,
282 'podcast': podcast,
283 'can_subscribe': len(targets) > 0,
284 'form': form
285 }, context_instance=RequestContext(request))
287 @login_required
288 def podcast_unsubscribe(request, pid, device_id):
290 return_to = request.GET.get('return_to')
292 if return_to == None:
293 raise Http404('Wrong URL')
295 podcast = get_object_or_404(Podcast, pk=pid)
296 device = Device.objects.get(pk=device_id)
297 try:
298 SubscriptionAction.objects.create(podcast=podcast, device=device, action=UNSUBSCRIBE_ACTION, timestamp=datetime.now())
299 except IntegrityError, e:
300 log('error while unsubscribing from podcast (device %s, podcast %s)' % (device.id, podcast.id))
302 return HttpResponseRedirect(return_to)
304 def episode_list(podcast, user):
306 Returns a list of episodes, with their action-attribute set to the latest
307 action. The attribute is unsert if there is no episode-action for
308 the episode.
310 episodes = Episode.objects.filter(podcast=podcast).order_by('-timestamp')
311 for e in episodes:
312 listeners = Listener.objects.filter(episode=e).values('user').distinct()
313 e.listeners = listeners.count()
315 if user.is_authenticated():
316 actions = EpisodeAction.objects.filter(episode=e, user=user).order_by('-timestamp')
317 if actions.count() > 0:
318 e.action = actions[0]
320 return episodes
323 def toplist(request, len=100):
324 entries = ToplistEntry.objects.all().order_by('-subscriptions')[:len]
325 max_subscribers = max([e.subscriptions for e in entries])
326 current_site = Site.objects.get_current()
327 return render_to_response('toplist.html', {
328 'entries': entries,
329 'max_subscribers': max_subscribers,
330 'url': current_site
331 }, context_instance=RequestContext(request))
334 def episode_toplist(request, len=100):
335 entries = EpisodeToplistEntry.objects.all().order_by('-listeners')[:len]
336 current_site = Site.objects.get_current()
338 # Determine maximum listener amount (or 0 if no entries exist)
339 max_listeners = max([0]+[e.listeners for e in entries])
341 return render_to_response('episode_toplist.html', {
342 'entries': entries,
343 'max_listeners': max_listeners,
344 'url': current_site
345 }, context_instance=RequestContext(request))
348 def toplist_opml(request, count):
349 entries = ToplistEntry.objects.all().order_by('-subscriptions')[:count]
350 exporter = Exporter(_('my.gpodder.org - Top %s') % count)
352 opml = exporter.generate([e.podcast for e in entries])
354 return HttpResponse(opml, mimetype='text/xml')
357 @login_required
358 def suggestions(request):
360 rated = False
362 if 'rate' in request.GET:
363 Rating.objects.create(target='suggestions', user=request.user, rating=request.GET['rate'], timestamp=datetime.now())
364 rated = True
366 entries = SuggestionEntry.forUser(request.user)
367 current_site = Site.objects.get_current()
368 return render_to_response('suggestions.html', {
369 'entries': entries,
370 'rated' : rated,
371 'url': current_site
372 }, context_instance=RequestContext(request))
375 @login_required
376 def device(request, device_id, error_message=None):
377 device = Device.objects.get(pk=device_id)
379 if device.user != request.user:
380 return HttpResponseForbidden(_('You are not allowed to access this device'))
382 subscriptions = device.get_subscriptions()
383 synced_with = list(device.sync_group.devices()) if device.sync_group else []
384 if device in synced_with: synced_with.remove(device)
385 success = False
386 sync_form = SyncForm()
387 sync_form.set_targets(device.sync_targets(), _('Synchronize with the following devices'))
389 if request.method == 'POST':
390 device_form = DeviceForm(request.POST)
392 if device_form.is_valid():
393 device.name = device_form.cleaned_data['name']
394 device.type = device_form.cleaned_data['type']
395 device.uid = device_form.cleaned_data['uid']
396 try:
397 device.save()
398 success = True
399 except IntegrityError, ie:
400 device = Device.objects.get(pk=device_id)
401 error_message = _('You can\'t use the same UID for two devices.')
403 else:
404 device_form = DeviceForm({
405 'name': device.name,
406 'type': device.type,
407 'uid' : device.uid
410 return render_to_response('device.html', {
411 'device': device,
412 'device_form': device_form,
413 'sync_form': sync_form,
414 'success': success,
415 'error_message': error_message,
416 'subscriptions': subscriptions,
417 'synced_with': synced_with,
418 'has_sync_targets': len(device.sync_targets()) > 0
419 }, context_instance=RequestContext(request))
422 @login_required
423 def device_delete(request, device_id):
424 if request.method != 'POST':
425 return HttpResponseNotAllowed(['POST'])
427 device = Device.objects.get(pk=device_id)
428 device.deleted = True
429 device.save()
431 current_site = Site.objects.get_current()
432 subscriptionlist = create_subscriptionlist(request)
433 return render_to_response('subscriptions.html', {
434 'subscriptionlist': subscriptionlist,
435 'url': current_site,
436 'deletedevice_success': True,
437 'device_name': device.name
438 }, context_instance=RequestContext(request))
441 @login_required
442 def device_sync(request, device_id):
444 if request.method != 'POST':
445 return HttpResponseNotAllowed(['POST'])
447 form = SyncForm(request.POST)
448 if not form.is_valid():
449 return HttpResponseBadRequest('invalid')
451 try:
452 target = form.get_target()
454 device = Device.objects.get(pk=device_id)
455 device.sync_with(target)
457 except ValueError, e:
458 log('error while syncing device %s: %s' % (device_id, e))
460 return HttpResponseRedirect('/device/%s' % device_id)
462 @login_required
463 def device_unsync(request, device_id):
464 if request.method != 'GET':
465 return HttpResponseNotAllowed(['GET'])
467 dev = Device.objects.get(pk=device_id)
469 try:
470 dev.unsync()
471 except ValueError, e:
472 return device(request, device_id, e)
474 return HttpResponseRedirect('/device/%s' % device_id)
476 @login_required
477 def podcast_subscribe_url(request):
478 url = request.GET.get('url')
480 if url == None:
481 raise Http404('http://my.gpodder.org/subscribe?url=http://www.example.com/podcast.xml')
483 url = sanitize_url(url)
485 if url == '':
486 raise Http404('Please specify a valid url')
488 podcast, created = Podcast.objects.get_or_create(url=url)
490 return HttpResponseRedirect('/podcast/%d/subscribe' % podcast.pk)
493 def resend_activation(request):
494 error_message = ''
496 if request.method == 'GET':
497 form = ResendActivationForm()
498 return render_to_response('registration/resend_activation.html', {
499 'form': form,
502 site = Site.objects.get_current()
503 form = ResendActivationForm(request.POST)
505 try:
506 if not form.is_valid():
507 raise ValueError(_('Invalid Username entered'))
509 try:
510 user = get_user(form.cleaned_data['username'], form.cleaned_data['email'])
511 except User.DoesNotExist:
512 raise ValueError(_('User does not exist.'))
514 try:
515 profile = RegistrationProfile.objects.get(user=user)
516 except RegistrationProfile.DoesNotExist:
517 profile = RegistrationProfile.objects.create_profile(user)
519 if profile.activation_key == RegistrationProfile.ACTIVATED:
520 raise ValueError(_('Your account already has been activated. Go ahead and log in.'))
522 elif profile.activation_key_expired():
523 raise ValueError(_('Your activation key has expired. Please try another username, or retry with the same one tomorrow.'))
525 except ValueError, e:
526 return render_to_response('registration/resend_activation.html', {
527 'form': form,
528 'error_message' : e
532 try:
533 profile.send_activation_email(site)
535 except AttributeError:
536 #old versions of django-registration send registration mails from RegistrationManager
537 RegistrationProfile.objects.send_activation_email(profile, site)
539 return render_to_response('registration/resent_activation.html')
542 def user_subscriptions(request, username):
543 user = get_object_or_404(User, username=username)
545 token, c = SecurityToken.objects.get_or_create(user=user, object='subscriptions', action='r',
546 defaults = {'token': "".join(random.sample(string.letters+string.digits, 32))})
548 u_token = request.GET.get('token', '')
549 if token.token == '' or token.token == u_token:
550 subscriptions = [s for s in Subscription.objects.filter(user=user)]
551 public_subscriptions = set([s.podcast for s in subscriptions if s.get_meta().public])
552 return render_to_response('user_subscriptions.html', {
553 'subscriptions': public_subscriptions,
554 'other_user': user
555 }, context_instance=RequestContext(request))
557 else:
558 return render_to_response('user_subscriptions_denied.html', {
559 'other_user': user
560 }, context_instance=RequestContext(request))