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
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
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
,
60 'episode_count': episodes
,
62 }, context_instance
=RequestContext(request
))
65 def cover_art(request
, size
, filename
):
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
)
81 im
= Image
.open(filename
)
82 if im
.mode
not in ('RGB', 'RGBA'):
83 im
= im
.convert('RGB')
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), \
96 resized
= Image
.composite(resized
, background
, resized
)
98 io
= StringIO
.StringIO()
99 resized
.save(io
, 'JPEG', optimize
=True, progression
=True, quality
=80)
102 fp
= open(target
, 'wb')
106 return HttpResponse(s
, mimetype
='image/jpeg')
108 raise Http404('Cover art not available')
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
,
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
):
124 subscriptions
= Subscription
.objects
.filter(user
=request
.user
)
127 for s
in subscriptions
:
129 l
[s
.podcast
]['devices'].append(s
.device
)
131 e
= Episode
.objects
.filter(podcast
=s
.podcast
, timestamp__isnull
=False).order_by('-timestamp')
132 episode
= e
[0] if e
.count() > 0 else None
134 l
[s
.podcast
] = {'podcast': s
.podcast
, 'episode': episode
, 'devices': devices
}
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
)
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
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']
161 subscriptionmeta
.save()
163 except IntegrityError
, ie
:
164 error_message
= _('You can\'t use the same UID for two devices.')
166 privacy_form
= PrivacyForm({
167 'public': subscriptionmeta
.public
173 timeline_data
= listener_data(podcast
)
175 return render_to_response('podcast.html', {
177 'timeline_data': timeline_data
,
179 'privacy_form': privacy_form
,
180 'devices': subscribed_devices
,
181 'can_subscribe': len(subscribe_targets
) > 0,
182 'episodes': episodes
,
183 'max_listeners': max_listeners
,
185 }, context_instance
=RequestContext(request
))
187 current_site
= Site
.objects
.get_current()
188 return render_to_response('podcast.html', {
191 'episodes': episodes
,
192 'max_listeners': max_listeners
,
193 }, context_instance
=RequestContext(request
))
195 def listener_data(podcast
):
198 episodes
= EpisodeAction
.objects
.filter(episode__podcast
=podcast
, timestamp__gte
=d
).order_by('timestamp').values('timestamp')
199 if len(episodes
) == 0:
202 start
= episodes
[0]['timestamp']
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
212 'listeners': listeners
,
217 def history(request
, len=15, device_id
=None):
219 devices
= Device
.objects
.filter(id=device_id
)
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]
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
))
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', {
246 }, context_instance
=RequestContext(request
))
250 def podcast_subscribe(request
, pid
):
251 podcast
= get_object_or_404(Podcast
, pk
=pid
)
254 if request
.method
== 'POST':
255 form
= SyncForm(request
.POST
)
258 target
= form
.get_target()
260 if isinstance(target
, SyncGroup
):
261 device
= target
.devices()[0]
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
)
278 form
.set_targets(targets
, _('Choose a device:'))
280 return render_to_response('subscribe.html', {
281 'error_message': error_message
,
283 'can_subscribe': len(targets
) > 0,
285 }, context_instance
=RequestContext(request
))
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
)
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
310 episodes
= Episode
.objects
.filter(podcast
=podcast
).order_by('-timestamp')
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]
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', {
329 'max_subscribers': max_subscribers
,
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', {
343 'max_listeners': max_listeners
,
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')
358 def suggestions(request
):
362 if 'rate' in request
.GET
:
363 Rating
.objects
.create(target
='suggestions', user
=request
.user
, rating
=request
.GET
['rate'], timestamp
=datetime
.now())
366 entries
= SuggestionEntry
.forUser(request
.user
)
367 current_site
= Site
.objects
.get_current()
368 return render_to_response('suggestions.html', {
372 }, context_instance
=RequestContext(request
))
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
)
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']
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.')
404 device_form
= DeviceForm({
410 return render_to_response('device.html', {
412 'device_form': device_form
,
413 'sync_form': sync_form
,
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
))
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
431 current_site
= Site
.objects
.get_current()
432 subscriptionlist
= create_subscriptionlist(request
)
433 return render_to_response('subscriptions.html', {
434 'subscriptionlist': subscriptionlist
,
436 'deletedevice_success': True,
437 'device_name': device
.name
438 }, context_instance
=RequestContext(request
))
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')
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
)
463 def device_unsync(request
, device_id
):
464 if request
.method
!= 'GET':
465 return HttpResponseNotAllowed(['GET'])
467 dev
= Device
.objects
.get(pk
=device_id
)
471 except ValueError, e
:
472 return device(request
, device_id
, e
)
474 return HttpResponseRedirect('/device/%s' % device_id
)
477 def podcast_subscribe_url(request
):
478 url
= request
.GET
.get('url')
481 raise Http404('http://my.gpodder.org/subscribe?url=http://www.example.com/podcast.xml')
483 url
= sanitize_url(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
):
496 if request
.method
== 'GET':
497 form
= ResendActivationForm()
498 return render_to_response('registration/resend_activation.html', {
502 site
= Site
.objects
.get_current()
503 form
= ResendActivationForm(request
.POST
)
506 if not form
.is_valid():
507 raise ValueError(_('Invalid Username entered'))
510 user
= get_user(form
.cleaned_data
['username'], form
.cleaned_data
['email'])
511 except User
.DoesNotExist
:
512 raise ValueError(_('User does not exist.'))
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', {
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
,
555 }, context_instance
=RequestContext(request
))
558 return render_to_response('user_subscriptions_denied.html', {
560 }, context_instance
=RequestContext(request
))