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
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
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
}
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
)
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
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']
223 subscriptionmeta
.save()
225 except IntegrityError
, ie
:
226 error_message
= _('You can\'t use the same Device ID for two devices.')
228 privacy_form
= PrivacyForm({
229 'public': subscriptionmeta
.public
235 timeline_data
= listener_data(podcast
)
237 return render_to_response('podcast.html', {
239 'timeline_data': timeline_data
,
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
,
248 }, context_instance
=RequestContext(request
))
250 current_site
= Site
.objects
.get_current()
251 return render_to_response('podcast.html', {
253 'related_podcasts': related_podcasts
,
255 'episodes': episodes
,
256 'max_listeners': max_listeners
,
257 }, context_instance
=RequestContext(request
))
259 def listener_data(podcast
):
262 episodes
= EpisodeAction
.objects
.filter(episode__podcast
=podcast
, timestamp__gte
=d
).order_by('timestamp').values('timestamp')
263 if len(episodes
) == 0:
266 start
= episodes
[0]['timestamp']
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
276 'listeners': listeners
,
282 def history(request
, len=15, device_id
=None):
284 devices
= Device
.objects
.filter(id=device_id
)
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]
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
))
308 def podcast_subscribe(request
, pid
):
309 podcast
= get_object_or_404(Podcast
, pk
=pid
)
312 if request
.method
== 'POST':
313 form
= SyncForm(request
.POST
)
316 target
= form
.get_target()
318 if isinstance(target
, SyncGroup
):
319 device
= target
.devices()[0]
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
)
336 form
.set_targets(targets
, _('Choose a device:'))
338 return render_to_response('subscribe.html', {
339 'error_message': error_message
,
341 'can_subscribe': len(targets
) > 0,
343 }, context_instance
=RequestContext(request
))
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
)
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
370 episodes
= Episode
.objects
.filter(podcast
=podcast
).order_by('-timestamp')
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]
384 def toplist(request
, num
=100, lang
=None):
387 lang
= process_lang_params(request
, '/toplist/')
388 except utils
.UpdatedException
, updated
:
389 return HttpResponseRedirect('/toplist/?lang=%s' % ','.join(updated
.data
))
392 entries
= ToplistEntry
.objects
.all()[:num
]
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', {
402 'max_subscribers': max_subscribers
,
405 'all_languages': all_langs
,
406 }, context_instance
=RequestContext(request
))
410 def episode_toplist(request
, num
=100):
413 lang
= process_lang_params(request
, '/toplist/episodes')
414 except utils
.UpdatedException
, updated
:
415 return HttpResponseRedirect('/toplist/episodes?lang=%s' % ','.join(updated
.data
))
418 entries
= EpisodeToplistEntry
.objects
.all()[:num
]
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', {
431 'max_listeners': max_listeners
,
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
)
455 def suggestions(request
):
459 if 'rate' in request
.GET
:
460 Rating
.objects
.create(target
='suggestions', user
=request
.user
, rating
=request
.GET
['rate'], timestamp
=datetime
.now())
463 if 'blacklist' in request
.GET
:
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
476 entries
= SuggestionEntry
.objects
.for_user(request
.user
)
477 current_site
= Site
.objects
.get_current()
478 return render_to_response('suggestions.html', {
482 }, context_instance
=RequestContext(request
))
487 def podcast_subscribe_url(request
):
488 url
= request
.GET
.get('url')
491 raise Http404('http://my.gpodder.org/subscribe?url=http://www.example.com/podcast.xml')
493 url
= sanitize_url(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
)
504 def resend_activation(request
):
507 if request
.method
== 'GET':
508 form
= ResendActivationForm()
509 return render_to_response('registration/resend_activation.html', {
511 }, context_instance
=RequestContext(request
))
513 site
= Site
.objects
.get_current()
514 form
= ResendActivationForm(request
.POST
)
517 if not form
.is_valid():
518 raise ValueError(_('Invalid Username entered'))
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
)
527 raise ValueError(_('You have deleted your account, but you can regster again.'))
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
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', {
546 }, context_instance
=RequestContext(request
))
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
))
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
,
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
,
580 }, context_instance
=RequestContext(request
))
581 response
['Content-Disposition'] = 'attachment; filename=%s-subscriptions.opml' % username
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'
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
))