remove unnecessary imports
[mygpo.git] / mygpo / api / models / __init__.py
blob3066a01d82b8f29148b8910fb570676d6e1e9d65
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.db import models
19 from django.contrib.auth.models import User
20 from datetime import datetime
21 from django.utils.translation import ugettext as _
22 from mygpo.api.fields import SeparatedValuesField, JSONField
23 import hashlib
24 import re
26 from mygpo.api.constants import EPISODE_ACTION_TYPES, DEVICE_TYPES, SUBSCRIBE_ACTION, SUBSCRIPTION_ACTION_TYPES
27 from mygpo.log import log
29 class UserProfile(models.Model):
30 user = models.ForeignKey(User, unique=True, db_column='user_ptr_id')
32 public_profile = models.BooleanField(default=True)
33 generated_id = models.BooleanField(default=False)
34 deleted = models.BooleanField(default=False)
35 suggestion_up_to_date = models.BooleanField(default=False)
36 settings = JSONField(default={})
38 def __unicode__(self):
39 return '%s (%s, %s)' % (self.user.username, self.public_profile, self.generated_id)
41 def save(self, *args, **kwargs):
42 self.public_profile = self.settings.get('public_profile', True)
43 super(UserProfile, self).save(*args, **kwargs)
45 class Meta:
46 db_table = 'user'
48 class Podcast(models.Model):
49 url = models.URLField(unique=True, verify_exists=False)
50 title = models.CharField(max_length=100, blank=True)
51 description = models.TextField(blank=True, null=True)
52 link = models.URLField(blank=True, null=True, verify_exists=False)
53 last_update = models.DateTimeField(null=True,blank=True)
54 logo_url = models.CharField(max_length=1000,null=True,blank=True)
55 author = models.CharField(max_length=100, null=True, blank=True)
56 language = models.CharField(max_length=10, null=True, blank=True)
57 group = models.ForeignKey('PodcastGroup', null=True)
58 group_member_name = models.CharField(max_length=20, default=None, null=True, blank=False)
59 content_types = SeparatedValuesField(null=True, blank=True)
61 def subscriptions(self):
62 """
63 returns all public subscriptions to this podcast
64 """
65 return Subscription.objects.public_subscriptions([self])
68 def subscription_count(self):
69 return self.subscriptions().count()
71 def subscriber_count(self):
72 """
73 Returns the number of public subscriptions to this podcast
74 """
75 subscriptions = self.subscriptions()
76 return subscriptions.values('user').distinct().count()
79 def listener_count(self):
80 from mygpo.data.models import Listener
81 return Listener.objects.filter(podcast=self).values('user').distinct().count()
83 def listener_count_timespan(self, start, end):
84 return EpisodeAction.objects.filter(episode__podcast=self,
85 timestamp__range=(start, end),
86 action='play').values('user_id').distinct().count()
88 def logo_shortname(self):
89 return hashlib.sha1(self.logo_url).hexdigest()
91 def subscribe_targets(self, user):
92 """
93 returns all Devices and SyncGroups on which this podcast can be subsrbied. This excludes all
94 devices/syncgroups on which the podcast is already subscribed
95 """
96 targets = []
98 devices = Device.objects.filter(user=user, deleted=False)
99 for d in devices:
100 subscriptions = [x.podcast for x in d.get_subscriptions()]
101 if self in subscriptions: continue
103 if d.sync_group:
104 if not d.sync_group in targets: targets.append(d.sync_group)
105 else:
106 targets.append(d)
108 return targets
111 def group_with(self, other, grouptitle, myname, othername):
112 if self.group == other.group and self.group != None:
113 return
115 if self.group != None:
116 if other.group == None:
117 self.group.add(other, othername)
119 else:
120 raise ValueError('the podcasts are already in different groups')
121 else:
122 if other.group == None:
123 g = PodcastGroup.objects.create(title=grouptitle)
124 g.add(self, myname)
125 g.add(other, othername)
127 else:
128 oter.group.add(self)
130 def ungroup(self):
131 if self.group == None:
132 raise ValueError('the podcast currently isn\'t in any group')
134 g = self.group
135 self.group = None
136 self.save()
138 podcasts = Podcast.objects.filter(group=g)
139 if podcasts.count() == 1:
140 p = podcasts[0]
141 p.group = None
142 p.save()
144 def get_similar(self):
145 from mygpo.data.models import RelatedPodcast
146 return [r.rel_podcast for r in RelatedPodcast.objects.filter(ref_podcast=self)]
148 def get_episodes(self):
149 return Episode.objects.filter(podcast=self)
151 def __unicode__(self):
152 return self.title if self.title != '' else self.url
154 class Meta:
155 db_table = 'podcast'
158 class PodcastGroup(models.Model):
159 title = models.CharField(max_length=100, blank=False)
161 def add(self, podcast, membername):
162 if podcast.group == self:
163 podcast.group_member_name = membername
165 elif podcast.group != None:
166 podcast.ungroup()
168 podcast.group = self
169 podcast.group_member_name = membername
170 podcast.save()
172 def podcasts(self):
173 return Podcast.objects.filter(group=self)
175 def subscriptions(self):
177 returns the public subscriptions to podcasts in the group
179 return Subscription.objects.public_subscriptions(self.podcasts())
181 def subscription_count(self):
182 return self.subscriptions().count()
184 def subscriber_count(self):
186 Returns the number of public subscriptions to podcasts of this group
188 subscriptions = self.subscriptions()
189 return subscriptions.values('user').distinct().count()
192 def __unicode__(self):
193 return self.title
195 class Meta:
196 db_table = 'podcast_groups'
199 class ToplistEntryManager(models.Manager):
201 def get_query_set(self):
202 return super(ToplistEntryManager, self).get_query_set().order_by('-subscriptions')
205 class ToplistEntry(models.Model):
206 podcast = models.ForeignKey(Podcast, null=True)
207 podcast_group = models.ForeignKey(PodcastGroup, null=True)
208 oldplace = models.IntegerField(db_column='old_place')
209 subscriptions = models.IntegerField(db_column='subscription_count')
211 objects = ToplistEntryManager()
214 def get_item(self):
215 if self.podcast:
216 return self.podcast
217 else:
218 return self.podcast_group
220 def get_podcast(self):
222 Returns a podcast which is representative for this toplist-entry
223 If the entry is a non-grouped podcast, it is returned
224 If the entry is a podcast group, one of its podcasts is returned
226 if self.podcast:
227 return self.podcast
228 else:
229 return self.podcast_group.podcasts()[0]
231 def __unicode__(self):
232 return '%s (%s)' % (self.podcast, self.subscriptions)
234 class Meta:
235 db_table = 'toplist'
238 class EpisodeToplistEntryManager(models.Manager):
240 def get_query_set(self):
241 return super(EpisodeToplistEntryManager, self).get_query_set().order_by('-listeners')
244 class EpisodeToplistEntry(models.Model):
245 episode = models.ForeignKey('Episode')
246 listeners = models.PositiveIntegerField()
248 objects = EpisodeToplistEntryManager()
250 def __unicode__(self):
251 return '%s (%s)' % (self.episode, self.listeners)
253 class Meta:
254 db_table = 'episode_toplist'
257 class SuggestionEntryManager(models.Manager):
259 def for_user(self, user):
260 from mygpo.data.models import SuggestionBlacklist
262 suggestions = SuggestionEntry.objects.filter(user=user).order_by('-priority')
264 subscriptions = [x.podcast for x in Subscription.objects.filter(user=user)]
265 suggestions = filter(lambda x: x.podcast not in subscriptions, suggestions)
267 blacklist = [x.podcast for x in SuggestionBlacklist.objects.filter(user=user)]
268 suggestions = filter(lambda x: x.podcast not in blacklist, suggestions)
270 return suggestions
273 class SuggestionEntry(models.Model):
274 podcast = models.ForeignKey(Podcast)
275 user = models.ForeignKey(User)
276 priority = models.IntegerField()
278 objects = SuggestionEntryManager()
280 def __unicode__(self):
281 return '%s (%s)' % (self.podcast, self.priority)
283 class Meta:
284 db_table = 'suggestion'
287 class Episode(models.Model):
288 podcast = models.ForeignKey(Podcast)
289 url = models.URLField(verify_exists=False)
290 title = models.CharField(max_length=100, blank=True)
291 description = models.TextField(null=True, blank=True)
292 link = models.URLField(null=True, blank=True, verify_exists=False)
293 timestamp = models.DateTimeField(null=True, blank=True)
294 author = models.CharField(max_length=100, null=True, blank=True)
295 duration = models.PositiveIntegerField(null=True, blank=True)
296 filesize = models.PositiveIntegerField(null=True, blank=True)
297 language = models.CharField(max_length=10, null=True, blank=True)
298 last_update = models.DateTimeField(auto_now=True)
299 outdated = models.BooleanField(default=False) #set to true after episode hasn't been found in feed
300 mimetype = models.CharField(max_length=30, blank=True, null=True)
302 def number(self):
303 m = re.search('\D*(\d+)\D+', self.title)
304 return m.group(1) if m else ''
306 def shortname(self):
307 s = self.title
308 s = s.replace(self.podcast.title, '')
309 s = s.replace(self.number(), '')
310 m = re.search('\W*(.+)', s)
311 s = m.group(1) if m else s
312 s = s.strip()
313 return s
315 def listener_count(self):
316 from mygpo.data.models import Listener
317 return Listener.objects.filter(episode=self).values('user').distinct().count()
319 def listener_count_timespan(self, start, end):
320 return EpisodeAction.objects.filter(episode=self,
321 timestamp__range=(start, end),
322 action='play').values('user_id').distinct().count()
324 def __unicode__(self):
325 return '%s (%s)' % (self.shortname(), self.podcast)
327 class Meta:
328 db_table = 'episode'
329 unique_together = ('podcast', 'url')
331 class SyncGroup(models.Model):
333 Devices that should be synced with each other need to be grouped
334 in a SyncGroup.
336 SyncGroups are automatically created by calling
337 device.sync_with(other_device), but can also be created manually.
339 device.sync() synchronizes the device for which the method is called
340 with the other devices in its SyncGroup.
342 user = models.ForeignKey(User)
344 def __unicode__(self):
345 devices = [d.name for d in Device.objects.filter(sync_group=self)]
346 return ', '.join(devices)
348 def devices(self):
349 return Device.objects.filter(sync_group=self)
351 def add(self, device):
352 if device.sync_group == self: return
353 if device.sync_group != None:
354 device.unsync()
356 device.sync_group = self
357 device.save()
359 class Meta:
360 db_table = 'sync_group'
363 class Device(models.Model):
364 user = models.ForeignKey(User)
365 uid = models.SlugField(max_length=50)
366 name = models.CharField(max_length=100, blank=True)
367 type = models.CharField(max_length=10, choices=DEVICE_TYPES)
368 sync_group = models.ForeignKey(SyncGroup, blank=True, null=True)
369 deleted = models.BooleanField(default=False)
370 settings = JSONField(default={})
372 def __unicode__(self):
373 return self.name if self.name else _('Unnamed Device (%s)' % self.uid)
375 def get_subscriptions(self):
376 self.sync()
377 return Subscription.objects.filter(device=self)
379 def sync(self):
380 for s in self.get_sync_actions():
381 try:
382 SubscriptionAction.objects.create(device=self, podcast=s.podcast, action=s.action)
383 except Exception, e:
384 log('Error adding subscription action: %s (device %s, podcast %s, action %s)' % (str(e), repr(self), repr(s.podcast), repr(s.action)))
386 def sync_targets(self):
388 returns all Devices and SyncGroups that can be used as a parameter for self.sync_with()
390 sync_targets = list(Device.objects.filter(user=self.user, sync_group=None, deleted=False).exclude(pk=self.id))
392 sync_groups = SyncGroup.objects.filter(user=self.user)
393 if self.sync_group != None: sync_groups = sync_groups.exclude(pk=self.sync_group.id)
395 sync_targets.extend( list(sync_groups) )
396 return sync_targets
399 def get_sync_actions(self):
401 returns the SyncGroupSubscriptionActions correspond to the
402 SubscriptionActions that need to be saved for the current device
403 to synchronize it with its SyncGroup
405 if self.sync_group == None:
406 return []
408 devices = self.sync_group.devices().exclude(pk=self.id)
410 sync_actions = self.latest_actions()
412 for d in devices:
413 a = d.latest_actions()
414 for s in a.keys():
415 if not sync_actions.has_key(s):
416 if a[s].action == SUBSCRIBE_ACTION:
417 sync_actions[s] = a[s]
418 elif a[s].newer_than(sync_actions[s]) and (sync_actions[s].action != a[s].action):
419 sync_actions[s] = a[s]
421 #remove actions that did not change
422 current_state = self.latest_actions()
423 for podcast in current_state.keys():
424 if podcast in current_state and podcast in sync_actions and sync_actions[podcast] == current_state[podcast]:
425 del sync_actions[podcast]
427 return sync_actions.values()
429 def latest_actions(self):
431 returns the latest action for each podcast
432 that has an action on this device
434 #all podcasts that have an action on this device
435 podcasts = [sa.podcast for sa in SubscriptionAction.objects.filter(device=self)]
436 podcasts = list(set(podcasts)) #remove duplicates
438 actions = {}
439 for p in podcasts:
440 actions[p] = self.latest_action(p)
442 return actions
444 def latest_action(self, podcast):
446 returns the latest action for the given podcast on this device
448 actions = SubscriptionAction.objects.filter(podcast=podcast,device=self).order_by('-timestamp', '-id')
449 if actions.count() == 0:
450 return None
451 else:
452 return actions[0]
454 def sync_with(self, other):
456 set the device to be synchronized with other, which can either be a Device or a SyncGroup.
457 this method places them in the same SyncGroup. get_sync_actions() can
458 then return the SyncGroupSubscriptionActions for brining the device
459 in sync with its group
461 if self.user != other.user:
462 raise ValueError('the devices belong to different users')
464 if isinstance(other, SyncGroup):
465 other.add(self)
466 self.save()
467 return
469 if self.sync_group == other.sync_group and self.sync_group != None:
470 return
472 if self.sync_group != None:
473 if other.sync_group == None:
474 self.sync_group.add(other)
476 else:
477 raise ValueError('the devices are in different sync groups')
479 else:
480 if other.sync_group == None:
481 g = SyncGroup.objects.create(user=self.user)
482 g.add(self)
483 g.add(other)
485 else:
486 oter.sync_group.add(self)
488 def unsync(self):
490 stops synchronizing the device
491 this method removes the device from its SyncGroup. If only one
492 device remains in the SyncGroup, it is removed so the device can
493 be used in other groups.
495 if self.sync_group == None:
496 raise ValueError('the device is not synced')
498 g = self.sync_group
499 self.sync_group = None
500 self.save()
502 devices = Device.objects.filter(sync_group=g)
503 if devices.count() == 1:
504 d = devices[0]
505 d.sync_group = None
506 d.save()
507 g.delete()
509 class Meta:
510 db_table = 'device'
512 class EpisodeAction(models.Model):
513 user = models.ForeignKey(User)
514 episode = models.ForeignKey(Episode)
515 device = models.ForeignKey(Device,null=True)
516 action = models.CharField(max_length=10, choices=EPISODE_ACTION_TYPES)
517 timestamp = models.DateTimeField(default=datetime.now)
518 started = models.IntegerField(null=True, blank=True)
519 playmark = models.IntegerField(null=True, blank=True)
520 total = models.IntegerField(null=True, blank=True)
522 def __unicode__(self):
523 return '%s %s %s' % (self.user, self.action, self.episode)
525 def playmark_time(self):
526 return datetime.fromtimestamp(float(self.playmark))
528 def started_time(self):
529 return datetime.fromtimestamp(float(self.started))
531 class Meta:
532 db_table = 'episode_log'
535 class SubscriptionManager(models.Manager):
537 def public_subscriptions(self, podcasts=None):
539 Returns either all public subscriptions or those for the given podcasts
542 subscriptions = self.filter(podcast__in=podcasts) if podcasts else self.all()
544 # remove users with private profiles
545 subscriptions = subscriptions.exclude(user__userprofile__public_profile=False)
547 # remove inactive (eg deleted) users
548 subscriptions = subscriptions.exclude(user__is_active=False)
550 if podcasts:
551 # remove uers that have marked their subscription to this podcast as private
552 private_users = SubscriptionMeta.objects.filter(podcast__in=podcasts, public=False).values('user')
553 subscriptions = subscriptions.exclude(user__in=private_users)
555 return subscriptions
558 class Subscription(models.Model):
559 device = models.ForeignKey(Device, primary_key=True)
560 podcast = models.ForeignKey(Podcast)
561 user = models.ForeignKey(User)
562 subscribed_since = models.DateTimeField()
564 objects = SubscriptionManager()
566 def __unicode__(self):
567 return '%s - %s on %s' % (self.device.user, self.podcast, self.device)
569 def get_meta(self):
570 #this is different than get_or_create because it does not necessarily create a new meta-object
571 qs = SubscriptionMeta.objects.filter(user=self.user, podcast=self.podcast)
573 if qs.count() == 0:
574 return SubscriptionMeta(user=self.user, podcast=self.podcast)
575 else:
576 return qs[0]
578 #this method has to be overwritten, if not it tries to delete a view
579 def delete(self):
580 pass
582 class Meta:
583 db_table = 'current_subscription'
584 #not available in Django 1.0 (Debian stable)
585 managed = False
588 class SubscriptionMeta(models.Model):
589 user = models.ForeignKey(User)
590 podcast = models.ForeignKey(Podcast)
591 public = models.BooleanField(default=True)
592 settings = JSONField(default={})
594 def __unicode__(self):
595 return '%s - %s - %s' % (self.user, self.podcast, self.public)
597 def save(self, *args, **kwargs):
598 self.public = self.settings.get('public_subscription', True)
599 super(SubscriptionMeta, self).save(*args, **kwargs)
602 class Meta:
603 db_table = 'subscription'
604 unique_together = ('user', 'podcast')
607 class EpisodeSettings(models.Model):
608 user = models.ForeignKey(User)
609 episode = models.ForeignKey(Episode)
610 settings = JSONField(default={})
612 def save(self, *args, **kwargs):
613 super(EpisodeSettings, self).save(*args, **kwargs)
615 from mygpo.api.models.users import EpisodeFavorite
616 fav = self.settings.get('is_favorite', False)
617 if fav:
618 EpisodeFavorite.objects.get_or_create(user=self.user, episode=self.episode)
619 else:
620 EpisodeFavorite.objects.filter(user=self.user, episode=self.episode).delete()
623 class Meta:
624 db_table = 'episode_settings'
625 unique_together = ('user', 'episode')
628 class SubscriptionAction(models.Model):
629 device = models.ForeignKey(Device)
630 podcast = models.ForeignKey(Podcast)
631 action = models.IntegerField(choices=SUBSCRIPTION_ACTION_TYPES)
632 timestamp = models.DateTimeField(blank=True, default=datetime.now)
634 def action_string(self):
635 return 'subscribe' if self.action == SUBSCRIBE_ACTION else 'unsubscribe'
637 def newer_than(self, action):
638 return self.timestamp > action.timestamp
640 def __unicode__(self):
641 return '%s %s %s %s' % (self.device.user, self.device, self.action_string(), self.podcast)
643 class Meta:
644 db_table = 'subscription_log'
645 unique_together = ('device', 'podcast', 'timestamp')
648 class URLSanitizingRule(models.Model):
649 use_podcast = models.BooleanField()
650 use_episode = models.BooleanField()
651 search = models.CharField(max_length=100)
652 search_precompiled = None
653 replace = models.CharField(max_length=100, null=False, blank=True)
654 priority = models.PositiveIntegerField()
655 description = models.TextField(null=False, blank=True)
657 class Meta:
658 db_table = 'sanitizing_rules'
660 def __unicode__(self):
661 return '%s -> %s' % (self.search, self.replace)
664 from mygpo.search.signals import update_podcast_entry, update_podcast_group_entry, remove_podcast_entry, remove_podcast_group_entry
665 from django.db.models.signals import post_save, pre_delete
667 post_save.connect(update_podcast_entry, sender=Podcast)
668 pre_delete.connect(remove_podcast_entry, sender=Podcast)
670 post_save.connect(update_podcast_group_entry, sender=PodcastGroup)
671 pre_delete.connect(remove_podcast_group_entry, sender=PodcastGroup)