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
.web
.models
import Rating
, SecurityToken
25 from mygpo
.web
.forms
import UserAccountForm
, DeviceForm
, SyncForm
, PrivacyForm
, ResendActivationForm
26 from django
.forms
import ValidationError
27 from mygpo
.api
.opml
import Exporter
28 from django
.utils
.translation
import ugettext
as _
29 from mygpo
.api
.basic_auth
import require_valid_user
30 from django
.contrib
.auth
.decorators
import login_required
31 from django
.shortcuts
import get_object_or_404
32 from django
.db
import IntegrityError
33 from datetime
import datetime
, date
, timedelta
34 from django
.contrib
.sites
.models
import Site
35 from django
.conf
import settings
36 from registration
.models
import RegistrationProfile
38 from mygpo
.api
.sanitizing
import sanitize_url
39 from mygpo
.web
.users
import get_user
40 from mygpo
.log
import log
41 from mygpo
.utils
import daterange
47 current_site
= Site
.objects
.get_current()
48 if request
.user
.is_authenticated():
49 subscriptionlist
= create_subscriptionlist(request
)
51 return render_to_response('home-user.html', {
52 'subscriptionlist': subscriptionlist
,
54 }, context_instance
=RequestContext(request
))
57 podcasts
= Podcast
.objects
.count()
58 return render_to_response('home.html', {
59 'podcast_count': podcasts
,
63 def create_subscriptionlist(request
):
64 #sync all devices first
65 for d
in Device
.objects
.filter(user
=request
.user
):
68 subscriptions
= Subscription
.objects
.filter(user
=request
.user
)
71 for s
in subscriptions
:
73 l
[s
.podcast
]['devices'].append(s
.device
)
75 e
= Episode
.objects
.filter(podcast
=s
.podcast
, timestamp__isnull
=False).order_by('-timestamp')
76 episode
= e
[0] if e
.count() > 0 else None
78 l
[s
.podcast
] = {'podcast': s
.podcast
, 'episode': episode
, 'devices': devices
}
82 def podcast(request
, pid
):
83 podcast
= get_object_or_404(Podcast
, pk
=pid
)
85 if request
.user
.is_authenticated():
86 devices
= Device
.objects
.filter(user
=request
.user
)
87 history
= SubscriptionAction
.objects
.filter(podcast
=podcast
,device__in
=devices
).order_by('-timestamp')
88 subscribed_devices
= [s
.device
for s
in Subscription
.objects
.filter(podcast
=podcast
,user
=request
.user
)]
89 subscribe_targets
= podcast
.subscribe_targets(request
.user
)
90 episodes
= episode_list(podcast
, request
.user
)
91 max_listeners
= max([x
.listeners
for x
in episodes
]) if len(episodes
) else 0
95 qs
= Subscription
.objects
.filter(podcast
=podcast
, user
=request
.user
)
96 if qs
.count()>0 and request
.user
.get_profile().public_profile
:
97 # subscription meta is valid for all subscriptions, so we get one - doesn't matter which
99 subscriptionmeta
= subscription
.get_meta()
100 if request
.method
== 'POST':
101 privacy_form
= PrivacyForm(request
.POST
)
102 if privacy_form
.is_valid():
103 subscriptionmeta
.public
= privacy_form
.cleaned_data
['public']
105 subscriptionmeta
.save()
107 except IntegrityError
, ie
:
108 error_message
= _('You can\'t use the same UID for two devices.')
110 privacy_form
= PrivacyForm({
111 'public': subscriptionmeta
.public
117 timeline_data
= listener_data(podcast
)
119 return render_to_response('podcast.html', {
121 'timeline_data': timeline_data
,
123 'privacy_form': privacy_form
,
124 'devices': subscribed_devices
,
125 'can_subscribe': len(subscribe_targets
) > 0,
126 'episodes': episodes
,
127 'max_listeners': max_listeners
,
129 }, context_instance
=RequestContext(request
))
131 current_site
= Site
.objects
.get_current()
132 return render_to_response('podcast.html', {
135 }, context_instance
=RequestContext(request
))
137 def listener_data(podcast
):
140 episodes
= EpisodeAction
.objects
.filter(episode__podcast
=podcast
, timestamp__gte
=d
).order_by('timestamp').values('timestamp')
141 if len(episodes
) == 0:
144 start
= episodes
[0]['timestamp']
147 for d
in daterange(start
):
148 next
= d
+ timedelta(days
=1)
149 listeners
= EpisodeAction
.objects
.filter(episode__podcast
=podcast
, timestamp__gte
=d
, timestamp__lt
=next
).values('user_id').distinct().count()
150 e
= Episode
.objects
.filter(podcast
=podcast
, timestamp__gte
=d
, timestamp__lt
=next
)
151 episode
= e
[0] if e
.count() > 0 else None
154 'listeners': listeners
,
159 def history(request
, len=15, device_id
=None):
161 devices
= Device
.objects
.filter(id=device_id
)
163 devices
= Device
.objects
.filter(user
=request
.user
)
165 history
= SubscriptionAction
.objects
.filter(device__in
=devices
).order_by('-timestamp')[:len]
166 episodehistory
= EpisodeAction
.objects
.filter(device__in
=devices
).order_by('-timestamp')[:len]
171 generalhistory
.append(row
)
172 for row
in episodehistory
:
173 generalhistory
.append(row
)
175 generalhistory
.sort(key
=lambda x
: x
.timestamp
,reverse
=True)
177 return render_to_response('history.html', {
178 'generalhistory': generalhistory
,
179 'singledevice': devices
[0] if device_id
else None
180 }, context_instance
=RequestContext(request
))
182 def devices(request
):
183 devices
= Device
.objects
.filter(user
=request
.user
,deleted
=False).order_by('sync_group')
184 return render_to_response('devicelist.html', {
186 }, context_instance
=RequestContext(request
))
189 def podcast_subscribe(request
, pid
):
190 podcast
= get_object_or_404(Podcast
, pk
=pid
)
193 if request
.method
== 'POST':
194 form
= SyncForm(request
.POST
)
197 target
= form
.get_target()
199 if isinstance(target
, SyncGroup
):
200 device
= target
.devices()[0]
205 SubscriptionAction
.objects
.create(podcast
=podcast
, device
=device
, action
=SUBSCRIBE_ACTION
)
206 except IntegrityError
, e
:
207 log('error while subscribing to podcast (device %s, podcast %s)' % (device
.id, podcast
.id))
209 return HttpResponseRedirect('/podcast/%s' % podcast
.id)
211 except ValueError, e
:
212 error_message
= _('Could not subscribe to the podcast: %s' % e
)
214 targets
= podcast
.subscribe_targets(request
.user
)
217 form
.set_targets(targets
, _('With which client do you want to subscribe?'))
219 return render_to_response('subscribe.html', {
220 'error_message': error_message
,
222 'can_subscribe': len(targets
) > 0,
224 }, context_instance
=RequestContext(request
))
227 def podcast_unsubscribe(request
, pid
, device_id
):
229 return_to
= request
.GET
.get('return_to')
231 if return_to
== None:
232 raise Http404('Wrong URL')
234 podcast
= get_object_or_404(Podcast
, pk
=pid
)
235 device
= Device
.objects
.get(pk
=device_id
)
237 SubscriptionAction
.objects
.create(podcast
=podcast
, device
=device
, action
=UNSUBSCRIBE_ACTION
, timestamp
=datetime
.now())
238 except IntegrityError
, e
:
239 log('error while unsubscribing from podcast (device %s, podcast %s)' % (device
.id, podcast
.id))
241 return HttpResponseRedirect(return_to
)
243 def episode_list(podcast
, user
):
245 Returns a list of episodes, with their action-attribute set to the latest
246 action. The attribute is unsert if there is no episode-action for
249 episodes
= Episode
.objects
.filter(podcast
=podcast
).order_by('-timestamp')
251 listeners
= EpisodeAction
.objects
.filter(episode
=e
, action
='play').values('user').distinct()
252 e
.listeners
= listeners
.count()
254 if user
.is_authenticated():
255 actions
= EpisodeAction
.objects
.filter(episode
=e
, user
=user
).order_by('-timestamp')
256 if actions
.count() > 0:
257 e
.action
= actions
[0]
263 def account(request
):
266 site
= Site
.objects
.get_current()
267 token
, c
= SecurityToken
.objects
.get_or_create(user
=request
.user
, object='subscriptions', action
='r',
268 defaults
= {'token': "".join(random
.sample(string
.letters
+string
.digits
, 32))})
271 if request
.method
== 'GET':
273 if 'public_subscriptions' in request
.GET
:
277 elif 'private_subscriptions' in request
.GET
:
278 token
.token
= "".join(random
.sample(string
.letters
+string
.digits
, 32))
281 form
= UserAccountForm({
282 'email': request
.user
.email
,
283 'public': request
.user
.get_profile().public_profile
286 return render_to_response('account.html', {
288 'token': token
.token
,
290 }, context_instance
=RequestContext(request
))
293 form
= UserAccountForm(request
.POST
)
295 if not form
.is_valid():
296 raise ValueError('Invalid data entered.')
298 if form
.cleaned_data
['password_current']:
299 if not request
.user
.check_password(form
.cleaned_data
['password_current']):
300 raise ValueError('Current password is incorrect')
302 request
.user
.set_password(form
.cleaned_data
['password1'])
304 request
.user
.email
= form
.cleaned_data
['email']
306 request
.user
.get_profile().public_profile
= form
.cleaned_data
['public']
307 request
.user
.get_profile().save()
311 except ValueError, e
:
315 except ValidationError
, e
:
319 return render_to_response('account.html', {
321 'token': token
.token
,
324 'error_message': error_message
325 }, context_instance
=RequestContext(request
))
328 def toplist(request
, len=100):
329 entries
= ToplistEntry
.objects
.all().order_by('-subscriptions')[:len]
330 max_subscribers
= max([e
.subscriptions
for e
in entries
])
331 current_site
= Site
.objects
.get_current()
332 return render_to_response('toplist.html', {
334 'max_subscribers': max_subscribers
,
336 }, context_instance
=RequestContext(request
))
339 def episode_toplist(request
, len=100):
340 entries
= EpisodeToplistEntry
.objects
.all().order_by('-listeners')[:len]
341 current_site
= Site
.objects
.get_current()
342 max_listeners
= max([e
.listeners
for e
in entries
])
343 return render_to_response('episode_toplist.html', {
345 'max_listeners': max_listeners
,
347 }, context_instance
=RequestContext(request
))
350 def toplist_opml(request
, count
):
351 entries
= ToplistEntry
.objects
.all().order_by('-subscriptions')[:count
]
352 exporter
= Exporter(_('my.gpodder.org - Top %s') % count
)
354 opml
= exporter
.generate([e
.podcast
for e
in entries
])
356 return HttpResponse(opml
, mimetype
='text/xml')
360 def suggestions(request
):
364 if 'rate' in request
.GET
:
365 Rating
.objects
.create(target
='suggestions', user
=request
.user
, rating
=request
.GET
['rate'], timestamp
=datetime
.now())
368 entries
= SuggestionEntry
.forUser(request
.user
)
369 current_site
= Site
.objects
.get_current()
370 return render_to_response('suggestions.html', {
374 }, context_instance
=RequestContext(request
))
378 def device(request
, device_id
):
379 device
= Device
.objects
.get(pk
=device_id
)
381 if device
.user
!= request
.user
:
382 return HttpResponseForbidden(_('You are not allowed to access this device'))
384 subscriptions
= device
.get_subscriptions()
385 synced_with
= list(device
.sync_group
.devices()) if device
.sync_group
else []
386 if device
in synced_with
: synced_with
.remove(device
)
389 sync_form
= SyncForm()
390 sync_form
.set_targets(device
.sync_targets(), _('Synchronize with the following devices'))
392 if request
.method
== 'POST':
393 device_form
= DeviceForm(request
.POST
)
395 if device_form
.is_valid():
396 device
.name
= device_form
.cleaned_data
['name']
397 device
.type = device_form
.cleaned_data
['type']
398 device
.uid
= device_form
.cleaned_data
['uid']
402 except IntegrityError
, ie
:
403 device
= Device
.objects
.get(pk
=device_id
)
404 error_message
= _('You can\'t use the same UID for two devices.')
407 device_form
= DeviceForm({
413 return render_to_response('device.html', {
415 'device_form': device_form
,
416 'sync_form': sync_form
,
418 'error_message': error_message
,
419 'subscriptions': subscriptions
,
420 'synced_with': synced_with
,
421 'has_sync_targets': len(device
.sync_targets()) > 0
422 }, context_instance
=RequestContext(request
))
426 def device_delete(request
, device_id
):
427 if request
.method
!= 'POST':
428 return HttpResponseNotAllowed(['POST'])
430 device
= Device
.objects
.get(pk
=device_id
)
431 device
.deleted
= True
434 current_site
= Site
.objects
.get_current()
435 subscriptionlist
= create_subscriptionlist(request
)
436 return render_to_response('home-user.html', {
437 'subscriptionlist': subscriptionlist
,
439 'deletedevice_success': True,
440 'device_name': device
.name
441 }, context_instance
=RequestContext(request
))
445 def device_sync(request
, device_id
):
447 if request
.method
!= 'POST':
448 return HttpResponseNotAllowed(['POST'])
450 form
= SyncForm(request
.POST
)
451 if not form
.is_valid():
452 return HttpResponseBadRequest('invalid')
455 target
= form
.get_target()
457 device
= Device
.objects
.get(pk
=device_id
)
458 device
.sync_with(target
)
460 except ValueError, e
:
461 log('error while syncing device %s: %s' % (device_id
, e
))
463 return HttpResponseRedirect('/device/%s' % device_id
)
466 def device_unsync(request
, device_id
):
467 if request
.method
!= 'GET':
468 return HttpResponseNotAllowed(['GET'])
470 device
= Device
.objects
.get(pk
=device_id
)
473 return HttpResponseRedirect('/device/%s' % device_id
)
476 def podcast_subscribe_url(request
):
477 url
= request
.GET
.get('url')
480 raise Http404('http://my.gpodder.org/subscribe?url=http://www.example.com/podcast.xml')
482 url
= sanitize_url(url
)
485 raise Http404('Please specify a valid url')
487 podcast
, created
= Podcast
.objects
.get_or_create(url
=url
)
489 return HttpResponseRedirect('/podcast/%d/subscribe' % podcast
.pk
)
492 def delete_account(request
):
494 if request
.method
== 'GET':
495 return render_to_response('delete_account.html')
497 request
.user
.is_active
= False
500 return render_to_response('delete_account.html', {
505 current_site
= Site
.objects
.get_current()
506 return render_to_response('authors.html', {
508 }, context_instance
=RequestContext(request
))
511 def resend_activation(request
):
514 if request
.method
== 'GET':
515 form
= ResendActivationForm()
516 return render_to_response('registration/resend_activation.html', {
520 site
= Site
.objects
.get_current()
521 form
= ResendActivationForm(request
.POST
)
524 if not form
.is_valid():
525 raise ValueError(_('Invalid Username entered'))
528 user
= get_user(form
.cleaned_data
['username'], form
.cleaned_data
['email'])
529 except User
.DoesNotExist
:
530 raise ValueError(_('User does not exist.'))
532 profile
= RegistrationProfile
.objects
.get(user
=user
)
534 if profile
.activation_key
== RegistrationProfile
.ACTIVATED
:
535 raise ValueError(_('Your account already has been activated. Go ahead and log in.'))
537 elif profile
.activation_key_expired():
538 raise ValueError(_('Your activation key has expired. Please try another username, or retry with the same one tomorrow.'))
540 except ValueError, e
:
541 return render_to_response('registration/resend_activation.html', {
548 profile
.send_activation_email(site
)
550 except AttributeError:
551 #old versions of django-registration send registration mails from RegistrationManager
552 RegistrationProfile
.objects
.send_activation_email(profile
, site
)
554 return render_to_response('registration/resent_activation.html')
557 def user_subscriptions(request
, username
):
558 user
= get_object_or_404(User
, username
=username
)
560 token
, c
= SecurityToken
.objects
.get_or_create(user
=user
, object='subscriptions', action
='r',
561 defaults
= {'token': "".join(random
.sample(string
.letters
+string
.digits
, 32))})
563 u_token
= request
.GET
.get('token', '')
564 if token
.token
== '' or token
.token
== u_token
:
565 subscriptions
= set([s
.podcast
for s
in Subscription
.objects
.filter(user
=user
)])
566 return render_to_response('user_subscriptions.html', {
567 'subscriptions': subscriptions
,
571 return render_to_response('user_subscriptions_denied.html', {