add "My Tags" view at /tags/
[mygpo.git] / mygpo / web / views / __init__.py
blobfcc93bdb9a8f1b85dd7c8d4825b191e34c145344
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.http import HttpResponseRedirect, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, Http404, HttpResponseForbidden
19 from django.contrib.auth import authenticate, login, logout
20 from django.contrib.auth.models import User
21 from django.template import RequestContext
22 from mygpo.api.models import Podcast, Episode, Device, EpisodeAction, SubscriptionAction, ToplistEntry, EpisodeToplistEntry, Subscription, SuggestionEntry, SyncGroup, SUBSCRIBE_ACTION, UNSUBSCRIBE_ACTION, SubscriptionMeta, UserProfile
23 from mygpo.data.models import Listener, SuggestionBlacklist, PodcastTag
24 from mygpo.data.mimetype import CONTENT_TYPES
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, render_to_response
34 from django.db import IntegrityError
35 from django.db.models import Sum
36 from datetime import datetime
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.constants import PODCAST_LOGO_SIZE, PODCAST_LOGO_BIG_SIZE
45 from mygpo.web import utils
46 from mygpo.api import simple
47 from mygpo.api import backend
48 import re
49 import os
50 import Image
51 import ImageDraw
52 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 @manual_gc
200 def history(request, len=15, device_id=None):
201 if device_id:
202 devices = Device.objects.filter(id=device_id)
203 else:
204 devices = Device.objects.filter(user=request.user)
206 history = SubscriptionAction.objects.filter(device__in=devices).order_by('-timestamp')[:len]
207 episodehistory = EpisodeAction.objects.filter(device__in=devices).order_by('-timestamp')[:len]
209 generalhistory = []
211 for row in history:
212 generalhistory.append(row)
213 for row in episodehistory:
214 generalhistory.append(row)
216 generalhistory.sort(key=lambda x: x.timestamp,reverse=True)
218 return render_to_response('history.html', {
219 'generalhistory': generalhistory,
220 'singledevice': devices[0] if device_id else None
221 }, context_instance=RequestContext(request))
224 @manual_gc
225 @login_required
226 def podcast_subscribe(request, pid):
227 podcast = get_object_or_404(Podcast, pk=pid)
228 error_message = None
230 if request.method == 'POST':
231 form = SyncForm(request.POST)
233 try:
234 target = form.get_target()
236 if isinstance(target, SyncGroup):
237 device = target.devices()[0]
238 else:
239 device = target
241 try:
242 SubscriptionAction.objects.create(podcast=podcast, device=device, action=SUBSCRIBE_ACTION)
243 except IntegrityError, e:
244 log('error while subscribing to podcast (device %s, podcast %s)' % (device.id, podcast.id))
246 return HttpResponseRedirect('/podcast/%s' % podcast.id)
248 except ValueError, e:
249 error_message = _('Could not subscribe to the podcast: %s' % e)
251 targets = podcast.subscribe_targets(request.user)
253 form = SyncForm()
254 form.set_targets(targets, _('Choose a device:'))
256 return render_to_response('subscribe.html', {
257 'error_message': error_message,
258 'podcast': podcast,
259 'can_subscribe': len(targets) > 0,
260 'form': form
261 }, context_instance=RequestContext(request))
264 @manual_gc
265 @login_required
266 def podcast_unsubscribe(request, pid, device_id):
268 return_to = request.GET.get('return_to')
270 if return_to == None:
271 raise Http404('Wrong URL')
273 podcast = get_object_or_404(Podcast, pk=pid)
274 device = Device.objects.get(pk=device_id)
275 try:
276 SubscriptionAction.objects.create(podcast=podcast, device=device, action=UNSUBSCRIBE_ACTION, timestamp=datetime.now())
277 except IntegrityError, e:
278 log('error while unsubscribing from podcast (device %s, podcast %s)' % (device.id, podcast.id))
280 return HttpResponseRedirect(return_to)
283 @manual_gc
284 def toplist(request, num=100, lang=None):
286 try:
287 lang = process_lang_params(request, '/toplist/')
288 except utils.UpdatedException, updated:
289 return HttpResponseRedirect('/toplist/?lang=%s' % ','.join(updated.data))
291 type_str = request.GET.get('types', '')
292 set_types = [t for t in type_str.split(',') if t]
293 if set_types:
294 media_types = dict([(t, t in set_types) for t in CONTENT_TYPES])
295 else:
296 media_types = dict([(t, True) for t in CONTENT_TYPES])
298 entries = backend.get_toplist(num, lang, set_types)
300 max_subscribers = max([e.subscriptions for e in entries]) if entries else 0
301 current_site = Site.objects.get_current()
302 all_langs = utils.get_language_names(utils.get_podcast_languages())
304 return render_to_response('toplist.html', {
305 'entries': entries,
306 'max_subscribers': max_subscribers,
307 'url': current_site,
308 'languages': lang,
309 'all_languages': all_langs,
310 'types': media_types,
311 }, context_instance=RequestContext(request))
314 @manual_gc
315 def episode_toplist(request, num=100):
317 try:
318 lang = process_lang_params(request, '/toplist/episodes')
319 except utils.UpdatedException, updated:
320 return HttpResponseRedirect('/toplist/episodes?lang=%s' % ','.join(updated.data))
322 type_str = request.GET.get('types', '')
323 set_types = [t for t in type_str.split(',') if t]
324 if set_types:
325 media_types = dict([(t, t in set_types) for t in CONTENT_TYPES])
326 else:
327 media_types = dict([(t, True) for t in CONTENT_TYPES])
329 entries = backend.get_episode_toplist(num, lang, set_types)
331 current_site = Site.objects.get_current()
333 # Determine maximum listener amount (or 0 if no entries exist)
334 max_listeners = max([0]+[e.listeners for e in entries])
335 all_langs = utils.get_language_names(utils.get_podcast_languages())
336 return render_to_response('episode_toplist.html', {
337 'entries': entries,
338 'max_listeners': max_listeners,
339 'url': current_site,
340 'languages': lang,
341 'all_languages': all_langs,
342 'types': media_types,
343 }, context_instance=RequestContext(request))
346 def process_lang_params(request, url):
347 if 'lang' in request.GET:
348 lang = list(set([x for x in request.GET.get('lang').split(',') if x]))
350 if request.method == 'POST':
351 if request.POST.get('lang'):
352 lang = list(set(lang + [request.POST.get('lang')]))
353 raise utils.UpdatedException(lang)
355 if not 'lang' in request.GET:
356 lang = utils.get_accepted_lang(request)
358 return utils.sanitize_language_codes(lang)
361 @manual_gc
362 @login_required
363 def suggestions(request):
365 rated = False
367 if 'rate' in request.GET:
368 Rating.objects.create(target='suggestions', user=request.user, rating=request.GET['rate'], timestamp=datetime.now())
369 rated = True
371 if 'blacklist' in request.GET:
372 try:
373 blacklisted_podcast = Podcast.objects.get(id=request.GET['blacklist'])
374 SuggestionBlacklist.objects.create(user=request.user, podcast=blacklisted_podcast)
376 p, _created = UserProfile.objects.get_or_create(user=request.user)
377 p.suggestion_up_to_date = False
378 p.save()
380 except Exception, e:
381 print e
384 entries = SuggestionEntry.objects.for_user(request.user)
385 current_site = Site.objects.get_current()
386 return render_to_response('suggestions.html', {
387 'entries': entries,
388 'rated' : rated,
389 'url': current_site
390 }, context_instance=RequestContext(request))
393 @manual_gc
394 @login_required
395 def podcast_subscribe_url(request):
396 url = request.GET.get('url')
398 if url == None:
399 raise Http404('http://my.gpodder.org/subscribe?url=http://www.example.com/podcast.xml')
401 url = sanitize_url(url)
403 if url == '':
404 raise Http404('Please specify a valid url')
406 podcast, created = Podcast.objects.get_or_create(url=url)
408 return HttpResponseRedirect('/podcast/%d/subscribe' % podcast.pk)
411 @manual_gc
412 def resend_activation(request):
413 error_message = ''
415 if request.method == 'GET':
416 form = ResendActivationForm()
417 return render_to_response('registration/resend_activation.html', {
418 'form': form,
419 }, context_instance=RequestContext(request))
421 site = Site.objects.get_current()
422 form = ResendActivationForm(request.POST)
424 try:
425 if not form.is_valid():
426 raise ValueError(_('Invalid Username entered'))
428 try:
429 user = get_user(form.cleaned_data['username'], form.cleaned_data['email'])
430 except User.DoesNotExist:
431 raise ValueError(_('User does not exist.'))
433 p, c = UserProfile.objects.get_or_create(user=user)
434 if p.deleted:
435 raise ValueError(_('You have deleted your account, but you can regster again.'))
437 try:
438 profile = RegistrationProfile.objects.get(user=user)
439 except RegistrationProfile.DoesNotExist:
440 profile = RegistrationProfile.objects.create_profile(user)
442 if profile.activation_key == RegistrationProfile.ACTIVATED:
443 user.is_active = True
444 user.save()
445 raise ValueError(_('Your account already has been activated. Go ahead and log in.'))
447 elif profile.activation_key_expired():
448 raise ValueError(_('Your activation key has expired. Please try another username, or retry with the same one tomorrow.'))
450 except ValueError, e:
451 return render_to_response('registration/resend_activation.html', {
452 'form': form,
453 'error_message' : e
454 }, context_instance=RequestContext(request))
457 try:
458 profile.send_activation_email(site)
460 except AttributeError:
461 #old versions of django-registration send registration mails from RegistrationManager
462 RegistrationProfile.objects.send_activation_email(profile, site)
464 return render_to_response('registration/resent_activation.html', context_instance=RequestContext(request))
467 @manual_gc
468 @requires_token(object='subscriptions', action='r', denied_template='user_subscriptions_denied.html')
469 def user_subscriptions(request, username):
470 user = get_object_or_404(User, username=username)
471 public_subscriptions = backend.get_public_subscriptions(user)
472 token = SecurityToken.objects.get(object='subscriptions', action='r', user__username=username)
474 return render_to_response('user_subscriptions.html', {
475 'subscriptions': public_subscriptions,
476 'other_user': user,
477 'token': token,
478 }, context_instance=RequestContext(request))
480 @requires_token(object='subscriptions', action='r')
481 def user_subscriptions_opml(request, username):
482 user = get_object_or_404(User, username=username)
483 public_subscriptions = backend.get_public_subscriptions(user)
485 response = render_to_response('user_subscriptions.opml', {
486 'subscriptions': public_subscriptions,
487 'other_user': user
488 }, context_instance=RequestContext(request))
489 response['Content-Disposition'] = 'attachment; filename=%s-subscriptions.opml' % username
490 return response
493 @manual_gc
494 @login_required
495 def all_subscriptions_download(request):
496 podcasts = backend.get_all_subscriptions(request.user)
497 response = simple.format_subscriptions(podcasts, 'opml', request.user.username)
498 response['Content-Disposition'] = 'attachment; filename=all-subscriptions.opml'
499 return response
502 def gpodder_example_podcasts(request):
503 sponsored_podcast = utils.get_sponsored_podcast()
504 return render_to_response('gpodder_examples.opml', {
505 'sponsored_podcast': sponsored_podcast
506 }, context_instance=RequestContext(request))
508 @login_required
509 def mytags(request):
510 tags_podcast = {}
511 tags_tag = {}
512 for tag in PodcastTag.objects.filter(user=request.user):
513 if not tag.podcast in tags_podcast:
514 tags_podcast[tag.podcast] = []
516 if not tag.tag in tags_tag:
517 tags_tag[tag.tag] = []
519 tag.is_own = True
520 tags_podcast[tag.podcast].append(tag)
521 tags_tag[tag.tag].append(tag)
523 return render_to_response('mytags.html', {
524 'tags_podcast': tags_podcast,
525 'tags_tag': tags_tag,
526 }, context_instance=RequestContext(request))