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
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
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 datetime
import datetime
, date
, timedelta
36 from django
.contrib
.sites
.models
import Site
37 from django
.conf
import settings
38 from registration
.models
import RegistrationProfile
40 from mygpo
.api
.sanitizing
import sanitize_url
41 from mygpo
.web
.users
import get_user
42 from mygpo
.log
import log
43 from mygpo
.utils
import daterange
44 from mygpo
.constants
import PODCAST_LOGO_SIZE
, PODCAST_LOGO_BIG_SIZE
45 from mygpo
.web
import utils
46 from mygpo
.api
import simple
54 if request
.user
.is_authenticated():
55 return dashboard(request
)
57 return welcome(request
)
61 current_site
= Site
.objects
.get_current()
62 podcasts
= Podcast
.objects
.count()
63 users
= User
.objects
.count()
64 episodes
= Episode
.objects
.count()
65 return render_to_response('home.html', {
66 'podcast_count': podcasts
,
68 'episode_count': episodes
,
70 }, context_instance
=RequestContext(request
))
74 def dashboard(request
, episode_count
=10):
75 site
= Site
.objects
.get_current()
76 devices
= Device
.objects
.filter(user
=request
.user
, deleted
=False)
77 subscribed_podcasts
= set([s
.podcast
for s
in Subscription
.objects
.filter(user
=request
.user
)])
78 newest_episodes
= Episode
.objects
.filter(podcast__in
=subscribed_podcasts
).order_by('-timestamp')[:episode_count
]
80 return render_to_response('dashboard.html', {
83 'subscribed_podcasts': subscribed_podcasts
,
84 'newest_episodes': newest_episodes
,
85 }, context_instance
=RequestContext(request
))
88 def cover_art(request
, size
, filename
):
90 if size
not in (PODCAST_LOGO_SIZE
, PODCAST_LOGO_BIG_SIZE
):
91 raise Http404('Wrong size')
93 # XXX: Is there a "cleaner" way to get the root directory of the installation?
94 root
= os
.path
.join(os
.path
.dirname(__file__
), '..', '..', '..')
95 target
= os
.path
.join(root
, 'htdocs', 'media', 'logo', str(size
), filename
+'.jpg')
96 filepath
= os
.path
.join(root
, 'htdocs', 'media', 'logo', filename
)
98 if os
.path
.exists(target
):
99 return HttpResponseRedirect('/media/logo/%s/%s.jpg' % (str(size
), filename
))
101 if os
.path
.exists(filepath
):
102 target_dir
= os
.path
.dirname(target
)
103 if not os
.path
.isdir(target_dir
):
104 os
.makedirs(target_dir
)
107 im
= Image
.open(filepath
)
108 if im
.mode
not in ('RGB', 'RGBA'):
109 im
= im
.convert('RGB')
111 raise Http404('Cannot open cover file')
114 resized
= im
.resize((size
, size
), Image
.ANTIALIAS
)
116 # raised when trying to read an interlaced PNG; we use the original instead
117 return HttpResponseRedirect('/media/logo/%s' % filename
)
119 # If it's a RGBA image, composite it onto a white background for JPEG
120 if resized
.mode
== 'RGBA':
121 background
= Image
.new('RGB', resized
.size
)
122 draw
= ImageDraw
.Draw(background
)
123 draw
.rectangle((-1, -1, resized
.size
[0]+1, resized
.size
[1]+1), \
124 fill
=(255, 255, 255))
126 resized
= Image
.composite(resized
, background
, resized
)
128 io
= StringIO
.StringIO()
129 resized
.save(io
, 'JPEG', optimize
=True, progression
=True, quality
=80)
132 fp
= open(target
, 'wb')
136 return HttpResponseRedirect('/media/logo/%s/%s.jpg' % (str(size
), filename
))
138 raise Http404('Cover art not available')
141 def subscriptions(request
):
142 current_site
= Site
.objects
.get_current()
143 subscriptionlist
= create_subscriptionlist(request
)
144 return render_to_response('subscriptions.html', {
145 'subscriptionlist': subscriptionlist
,
147 }, context_instance
=RequestContext(request
))
149 def create_subscriptionlist(request
):
150 #sync all devices first
151 for d
in Device
.objects
.filter(user
=request
.user
):
154 subscriptions
= Subscription
.objects
.filter(user
=request
.user
)
157 for s
in subscriptions
:
159 l
[s
.podcast
]['devices'].append(s
.device
)
161 e
= Episode
.objects
.filter(podcast
=s
.podcast
, timestamp__isnull
=False).order_by('-timestamp')
162 episode
= e
[0] if e
.count() > 0 else None
164 l
[s
.podcast
] = {'podcast': s
.podcast
, 'episode': episode
, 'devices': devices
}
168 def podcast(request
, pid
):
169 podcast
= get_object_or_404(Podcast
, pk
=pid
)
170 episodes
= episode_list(podcast
, request
.user
)
171 max_listeners
= max([x
.listeners
for x
in episodes
]) if len(episodes
) else 0
173 if request
.user
.is_authenticated():
174 devices
= Device
.objects
.filter(user
=request
.user
)
175 history
= SubscriptionAction
.objects
.filter(podcast
=podcast
,device__in
=devices
).order_by('-timestamp')
176 subscribed_devices
= [s
.device
for s
in Subscription
.objects
.filter(podcast
=podcast
,user
=request
.user
)]
177 subscribe_targets
= podcast
.subscribe_targets(request
.user
)
181 qs
= Subscription
.objects
.filter(podcast
=podcast
, user
=request
.user
)
182 if qs
.count()>0 and request
.user
.get_profile().public_profile
:
183 # subscription meta is valid for all subscriptions, so we get one - doesn't matter which
185 subscriptionmeta
= subscription
.get_meta()
186 if request
.method
== 'POST':
187 privacy_form
= PrivacyForm(request
.POST
)
188 if privacy_form
.is_valid():
189 subscriptionmeta
.public
= privacy_form
.cleaned_data
['public']
191 subscriptionmeta
.save()
193 except IntegrityError
, ie
:
194 error_message
= _('You can\'t use the same UID for two devices.')
196 privacy_form
= PrivacyForm({
197 'public': subscriptionmeta
.public
203 timeline_data
= listener_data(podcast
)
205 return render_to_response('podcast.html', {
207 'timeline_data': timeline_data
,
209 'privacy_form': privacy_form
,
210 'devices': subscribed_devices
,
211 'can_subscribe': len(subscribe_targets
) > 0,
212 'episodes': episodes
,
213 'max_listeners': max_listeners
,
215 }, context_instance
=RequestContext(request
))
217 current_site
= Site
.objects
.get_current()
218 return render_to_response('podcast.html', {
221 'episodes': episodes
,
222 'max_listeners': max_listeners
,
223 }, context_instance
=RequestContext(request
))
225 def listener_data(podcast
):
228 episodes
= EpisodeAction
.objects
.filter(episode__podcast
=podcast
, timestamp__gte
=d
).order_by('timestamp').values('timestamp')
229 if len(episodes
) == 0:
232 start
= episodes
[0]['timestamp']
235 for d
in daterange(start
):
236 next
= d
+ timedelta(days
=1)
237 listeners
= EpisodeAction
.objects
.filter(episode__podcast
=podcast
, timestamp__gte
=d
, timestamp__lt
=next
).values('user_id').distinct().count()
238 e
= Episode
.objects
.filter(podcast
=podcast
, timestamp__gte
=d
, timestamp__lt
=next
)
239 episode
= e
[0] if e
.count() > 0 else None
242 'listeners': listeners
,
247 def history(request
, len=15, device_id
=None):
249 devices
= Device
.objects
.filter(id=device_id
)
251 devices
= Device
.objects
.filter(user
=request
.user
)
253 history
= SubscriptionAction
.objects
.filter(device__in
=devices
).order_by('-timestamp')[:len]
254 episodehistory
= EpisodeAction
.objects
.filter(device__in
=devices
).order_by('-timestamp')[:len]
259 generalhistory
.append(row
)
260 for row
in episodehistory
:
261 generalhistory
.append(row
)
263 generalhistory
.sort(key
=lambda x
: x
.timestamp
,reverse
=True)
265 return render_to_response('history.html', {
266 'generalhistory': generalhistory
,
267 'singledevice': devices
[0] if device_id
else None
268 }, context_instance
=RequestContext(request
))
272 def podcast_subscribe(request
, pid
):
273 podcast
= get_object_or_404(Podcast
, pk
=pid
)
276 if request
.method
== 'POST':
277 form
= SyncForm(request
.POST
)
280 target
= form
.get_target()
282 if isinstance(target
, SyncGroup
):
283 device
= target
.devices()[0]
288 SubscriptionAction
.objects
.create(podcast
=podcast
, device
=device
, action
=SUBSCRIBE_ACTION
)
289 except IntegrityError
, e
:
290 log('error while subscribing to podcast (device %s, podcast %s)' % (device
.id, podcast
.id))
292 return HttpResponseRedirect('/podcast/%s' % podcast
.id)
294 except ValueError, e
:
295 error_message
= _('Could not subscribe to the podcast: %s' % e
)
297 targets
= podcast
.subscribe_targets(request
.user
)
300 form
.set_targets(targets
, _('Choose a device:'))
302 return render_to_response('subscribe.html', {
303 'error_message': error_message
,
305 'can_subscribe': len(targets
) > 0,
307 }, context_instance
=RequestContext(request
))
310 def podcast_unsubscribe(request
, pid
, device_id
):
312 return_to
= request
.GET
.get('return_to')
314 if return_to
== None:
315 raise Http404('Wrong URL')
317 podcast
= get_object_or_404(Podcast
, pk
=pid
)
318 device
= Device
.objects
.get(pk
=device_id
)
320 SubscriptionAction
.objects
.create(podcast
=podcast
, device
=device
, action
=UNSUBSCRIBE_ACTION
, timestamp
=datetime
.now())
321 except IntegrityError
, e
:
322 log('error while unsubscribing from podcast (device %s, podcast %s)' % (device
.id, podcast
.id))
324 return HttpResponseRedirect(return_to
)
326 def episode_list(podcast
, user
):
328 Returns a list of episodes, with their action-attribute set to the latest
329 action. The attribute is unsert if there is no episode-action for
332 episodes
= Episode
.objects
.filter(podcast
=podcast
).order_by('-timestamp')
334 listeners
= Listener
.objects
.filter(episode
=e
).values('user').distinct()
335 e
.listeners
= listeners
.count()
337 if user
.is_authenticated():
338 actions
= EpisodeAction
.objects
.filter(episode
=e
, user
=user
).order_by('-timestamp')
339 if actions
.count() > 0:
340 e
.action
= actions
[0]
345 def toplist(request
, num
=100, lang
=None):
348 lang
= process_lang_params(request
, '/toplist/')
349 except utils
.UpdatedException
, updated
:
350 return HttpResponseRedirect('/toplist/?lang=%s' % ','.join(updated
.data
))
353 entries
= ToplistEntry
.objects
.order_by('-subscriptions')[:num
]
356 regex
= '^(' + '|'.join(lang
) + ')'
357 entries
= ToplistEntry
.objects
.filter(podcast__language__regex
=regex
).order_by('-subscriptions')[:num
]
359 max_subscribers
= max([e
.subscriptions
for e
in entries
]) if entries
else 0
360 current_site
= Site
.objects
.get_current()
361 all_langs
= utils
.get_language_names(utils
.get_podcast_languages())
362 return render_to_response('toplist.html', {
364 'max_subscribers': max_subscribers
,
367 'all_languages': all_langs
,
368 }, context_instance
=RequestContext(request
))
371 def episode_toplist(request
, num
=100):
374 lang
= process_lang_params(request
, '/toplist/episodes')
375 except utils
.UpdatedException
, updated
:
376 return HttpResponseRedirect('/toplist/episodes?lang=%s' % ','.join(updated
.data
))
379 entries
= EpisodeToplistEntry
.objects
.order_by('-listeners')[:num
]
382 regex
= '^(' + '|'.join(lang
) + ')'
383 entries
= EpisodeToplistEntry
.objects
.filter(episode__podcast__language__regex
=regex
).order_by('-listeners')[:num
]
385 current_site
= Site
.objects
.get_current()
387 # Determine maximum listener amount (or 0 if no entries exist)
388 max_listeners
= max([0]+[e
.listeners
for e
in entries
])
389 all_langs
= utils
.get_language_names(utils
.get_podcast_languages())
390 return render_to_response('episode_toplist.html', {
392 'max_listeners': max_listeners
,
395 'all_languages': all_langs
,
396 }, context_instance
=RequestContext(request
))
399 def process_lang_params(request
, url
):
400 if 'lang' in request
.GET
:
401 lang
= list(set([x
for x
in request
.GET
.get('lang').split(',') if x
]))
403 if request
.method
== 'POST':
404 if request
.POST
.get('lang'):
405 lang
= list(set(lang
+ [request
.POST
.get('lang')]))
406 raise utils
.UpdatedException(lang
)
408 if not 'lang' in request
.GET
:
409 lang
= utils
.get_accepted_lang(request
)
413 def toplist_opml(request
, count
):
414 entries
= ToplistEntry
.objects
.all().order_by('-subscriptions')[:count
]
415 exporter
= Exporter(_('my.gpodder.org - Top %s') % count
)
417 opml
= exporter
.generate([e
.podcast
for e
in entries
])
419 return HttpResponse(opml
, mimetype
='text/xml')
423 def suggestions(request
):
427 if 'rate' in request
.GET
:
428 Rating
.objects
.create(target
='suggestions', user
=request
.user
, rating
=request
.GET
['rate'], timestamp
=datetime
.now())
431 entries
= SuggestionEntry
.forUser(request
.user
)
432 current_site
= Site
.objects
.get_current()
433 return render_to_response('suggestions.html', {
437 }, context_instance
=RequestContext(request
))
441 def podcast_subscribe_url(request
):
442 url
= request
.GET
.get('url')
445 raise Http404('http://my.gpodder.org/subscribe?url=http://www.example.com/podcast.xml')
447 url
= sanitize_url(url
)
450 raise Http404('Please specify a valid url')
452 podcast
, created
= Podcast
.objects
.get_or_create(url
=url
)
454 return HttpResponseRedirect('/podcast/%d/subscribe' % podcast
.pk
)
457 def resend_activation(request
):
460 if request
.method
== 'GET':
461 form
= ResendActivationForm()
462 return render_to_response('registration/resend_activation.html', {
464 }, context_instance
=RequestContext(request
))
466 site
= Site
.objects
.get_current()
467 form
= ResendActivationForm(request
.POST
)
470 if not form
.is_valid():
471 raise ValueError(_('Invalid Username entered'))
474 user
= get_user(form
.cleaned_data
['username'], form
.cleaned_data
['email'])
475 except User
.DoesNotExist
:
476 raise ValueError(_('User does not exist.'))
479 profile
= RegistrationProfile
.objects
.get(user
=user
)
480 except RegistrationProfile
.DoesNotExist
:
481 profile
= RegistrationProfile
.objects
.create_profile(user
)
483 if profile
.activation_key
== RegistrationProfile
.ACTIVATED
:
484 raise ValueError(_('Your account already has been activated. Go ahead and log in.'))
486 elif profile
.activation_key_expired():
487 raise ValueError(_('Your activation key has expired. Please try another username, or retry with the same one tomorrow.'))
489 except ValueError, e
:
490 return render_to_response('registration/resend_activation.html', {
493 }, context_instance
=RequestContext(request
))
497 profile
.send_activation_email(site
)
499 except AttributeError:
500 #old versions of django-registration send registration mails from RegistrationManager
501 RegistrationProfile
.objects
.send_activation_email(profile
, site
)
503 return render_to_response('registration/resent_activation.html', context_instance
=RequestContext(request
))
506 @requires_token(object='subscriptions', action
='r', denied_template
='user_subscriptions_denied.html')
507 def user_subscriptions(request
, username
):
508 user
= get_object_or_404(User
, username
=username
)
509 subscriptions
= [s
for s
in Subscription
.objects
.filter(user
=user
)]
510 public_subscriptions
= set([s
.podcast
for s
in subscriptions
if s
.get_meta().public
])
511 return render_to_response('user_subscriptions.html', {
512 'subscriptions': public_subscriptions
,
514 }, context_instance
=RequestContext(request
))