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
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
56 if request
.user
.is_authenticated():
57 return dashboard(request
)
59 return welcome(request
)
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)
71 lang
= process_lang_params(request
, '/toplist/')
72 except utils
.UpdatedException
, updated
:
76 entries
= ToplistEntry
.objects
.all()[:toplist_entries
]
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
,
86 'episode_count': episodes
,
88 'hours_listened': hours_listened
,
90 'sponsored_podcast': sponsored_podcast
,
91 }, context_instance
=RequestContext(request
))
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', {
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
):
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
)
137 im
= Image
.open(filepath
)
138 if im
.mode
not in ('RGB', 'RGBA'):
139 im
= im
.convert('RGB')
141 raise Http404('Cannot open cover file')
144 resized
= im
.resize((size
, size
), Image
.ANTIALIAS
)
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))
156 resized
= Image
.composite(resized
, background
, resized
)
158 io
= StringIO
.StringIO()
159 resized
.save(io
, 'JPEG', optimize
=True, progression
=True, quality
=80)
162 fp
= open(target
, 'wb')
166 return HttpResponseRedirect('/media/logo/%s/%s.jpg' % (str(size
), filename
))
168 raise Http404('Cover art not available')
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
,
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
):
185 subscriptions
= Subscription
.objects
.filter(user
=request
.user
)
188 for s
in subscriptions
:
190 l
[s
.podcast
]['devices'].append(s
.device
)
192 e
= Episode
.objects
.filter(podcast
=s
.podcast
, timestamp__isnull
=False).order_by('-timestamp')
193 episode
= e
[0] if e
.count() > 0 else None
195 l
[s
.podcast
] = {'podcast': s
.podcast
, 'episode': episode
, 'devices': devices
}
200 def history(request
, len=15, device_id
=None):
202 devices
= Device
.objects
.filter(id=device_id
)
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]
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
))
226 def podcast_subscribe(request
, pid
):
227 podcast
= get_object_or_404(Podcast
, pk
=pid
)
230 if request
.method
== 'POST':
231 form
= SyncForm(request
.POST
)
234 target
= form
.get_target()
236 if isinstance(target
, SyncGroup
):
237 device
= target
.devices()[0]
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
)
254 form
.set_targets(targets
, _('Choose a device:'))
256 return render_to_response('subscribe.html', {
257 'error_message': error_message
,
259 'can_subscribe': len(targets
) > 0,
261 }, context_instance
=RequestContext(request
))
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
)
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
)
284 def toplist(request
, num
=100, lang
=None):
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
]
294 media_types
= dict([(t
, t
in set_types
) for t
in CONTENT_TYPES
])
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', {
306 'max_subscribers': max_subscribers
,
309 'all_languages': all_langs
,
310 'types': media_types
,
311 }, context_instance
=RequestContext(request
))
315 def episode_toplist(request
, num
=100):
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
]
325 media_types
= dict([(t
, t
in set_types
) for t
in CONTENT_TYPES
])
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', {
338 'max_listeners': max_listeners
,
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
)
363 def suggestions(request
):
367 if 'rate' in request
.GET
:
368 Rating
.objects
.create(target
='suggestions', user
=request
.user
, rating
=request
.GET
['rate'], timestamp
=datetime
.now())
371 if 'blacklist' in request
.GET
:
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
384 entries
= SuggestionEntry
.objects
.for_user(request
.user
)
385 current_site
= Site
.objects
.get_current()
386 return render_to_response('suggestions.html', {
390 }, context_instance
=RequestContext(request
))
395 def podcast_subscribe_url(request
):
396 url
= request
.GET
.get('url')
399 raise Http404('http://my.gpodder.org/subscribe?url=http://www.example.com/podcast.xml')
401 url
= sanitize_url(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
)
412 def resend_activation(request
):
415 if request
.method
== 'GET':
416 form
= ResendActivationForm()
417 return render_to_response('registration/resend_activation.html', {
419 }, context_instance
=RequestContext(request
))
421 site
= Site
.objects
.get_current()
422 form
= ResendActivationForm(request
.POST
)
425 if not form
.is_valid():
426 raise ValueError(_('Invalid Username entered'))
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
)
435 raise ValueError(_('You have deleted your account, but you can regster again.'))
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
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', {
454 }, context_instance
=RequestContext(request
))
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
))
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
,
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
,
488 }, context_instance
=RequestContext(request
))
489 response
['Content-Disposition'] = 'attachment; filename=%s-subscriptions.opml' % username
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'
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
))
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
] = []
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
))