simplify SearchEntry instantiation
[mygpo.git] / mygpo / api / models / __init__.py
blob9f40f98e5978eceda60bf95eebed684811baf591
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, UserManager
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
25 import json
27 from mygpo.api.constants import EPISODE_ACTION_TYPES, DEVICE_TYPES, SUBSCRIBE_ACTION, UNSUBSCRIBE_ACTION, SUBSCRIPTION_ACTION_TYPES
28 from mygpo.log import log
30 class UserProfile(models.Model):
31 user = models.ForeignKey(User, unique=True, db_column='user_ptr_id')
33 public_profile = models.BooleanField(default=True)
34 generated_id = models.BooleanField(default=False)
35 deleted = models.BooleanField(default=False)
36 suggestion_up_to_date = models.BooleanField(default=False)
37 settings = JSONField(default={})
39 def __unicode__(self):
40 return '%s (%s, %s)' % (self.user.username, self.public_profile, self.generated_id)
42 def save(self, *args, **kwargs):
43 self.public_profile = self.settings.get('public_profile', True)
44 super(UserProfile, self).save(*args, **kwargs)
46 class Meta:
47 db_table = 'user'
49 class Podcast(models.Model):
50 url = models.URLField(unique=True, verify_exists=False)
51 title = models.CharField(max_length=100, blank=True)
52 description = models.TextField(blank=True, null=True)
53 link = models.URLField(blank=True, null=True, verify_exists=False)
54 last_update = models.DateTimeField(null=True,blank=True)
55 logo_url = models.CharField(max_length=1000,null=True,blank=True)
56 author = models.CharField(max_length=100, null=True, blank=True)
57 language = models.CharField(max_length=10, null=True, blank=True)
58 group = models.ForeignKey('PodcastGroup', null=True)
59 group_member_name = models.CharField(max_length=20, default=None, null=True, blank=False)
60 content_types = SeparatedValuesField(null=True, blank=True)
62 def subscriptions(self):
63 """
64 returns all public subscriptions to this podcast
65 """
66 return Subscription.objects.public_subscriptions([self])
69 def subscription_count(self):
70 return self.subscriptions().count()
72 def subscriber_count(self):
73 """
74 Returns the number of public subscriptions to this podcast
75 """
76 subscriptions = self.subscriptions()
77 return subscriptions.values('user').distinct().count()
80 def listener_count(self):
81 from mygpo.data.models import Listener
82 return Listener.objects.filter(podcast=self).values('user').distinct().count()
84 def logo_shortname(self):
85 return hashlib.sha1(self.logo_url).hexdigest()
87 def subscribe_targets(self, user):
88 """
89 returns all Devices and SyncGroups on which this podcast can be subsrbied. This excludes all
90 devices/syncgroups on which the podcast is already subscribed
91 """
92 targets = []
94 devices = Device.objects.filter(user=user, deleted=False)
95 for d in devices:
96 subscriptions = [x.podcast for x in d.get_subscriptions()]
97 if self in subscriptions: continue
99 if d.sync_group:
100 if not d.sync_group in targets: targets.append(d.sync_group)
101 else:
102 targets.append(d)
104 return targets
107 def group_with(self, other, grouptitle, myname, othername):
108 if self.group == other.group and self.group != None:
109 return
111 if self.group != None:
112 if other.group == None:
113 self.group.add(other, othername)
115 else:
116 raise ValueError('the podcasts are already in different groups')
117 else:
118 if other.group == None:
119 g = PodcastGroup.objects.create(title=grouptitle)
120 g.add(self, myname)
121 g.add(other, othername)
123 else:
124 oter.group.add(self)
126 def ungroup(self):
127 if self.group == None:
128 raise ValueError('the podcast currently isn\'t in any group')
130 g = self.group
131 self.group = None
132 self.save()
134 podcasts = Podcast.objects.filter(group=g)
135 if podcasts.count() == 1:
136 p = podcasts[0]
137 p.group = None
138 p.save()
140 def get_similar(self):
141 from mygpo.data.models import RelatedPodcast
142 return [r.rel_podcast for r in RelatedPodcast.objects.filter(ref_podcast=self)]
144 def __unicode__(self):
145 return self.title if self.title != '' else self.url
147 class Meta:
148 db_table = 'podcast'
151 class PodcastGroup(models.Model):
152 title = models.CharField(max_length=100, blank=False)
154 def add(self, podcast, membername):
155 if podcast.group == self:
156 podcast.group_member_name = membername
158 elif podcast.group != None:
159 podcast.ungroup()
161 podcast.group = self
162 podcast.group_member_name = membername
163 podcast.save()
165 def podcasts(self):
166 return Podcast.objects.filter(group=self)
168 def subscriptions(self):
170 returns the public subscriptions to podcasts in the group
172 return Subscription.objects.public_subscriptions(self.podcasts())
174 def subscription_count(self):
175 return self.subscriptions().count()
177 def subscriber_count(self):
179 Returns the number of public subscriptions to podcasts of this group
181 subscriptions = self.subscriptions()
182 return subscriptions.values('user').distinct().count()
185 def __unicode__(self):
186 return self.title
188 class Meta:
189 db_table = 'podcast_groups'
192 class ToplistEntryManager(models.Manager):
194 def get_query_set(self):
195 return super(ToplistEntryManager, self).get_query_set().order_by('-subscriptions')
198 class ToplistEntry(models.Model):
199 podcast = models.ForeignKey(Podcast, null=True)
200 podcast_group = models.ForeignKey(PodcastGroup, null=True)
201 oldplace = models.IntegerField(db_column='old_place')
202 subscriptions = models.IntegerField(db_column='subscription_count')
204 objects = ToplistEntryManager()
207 def get_item(self):
208 if self.podcast:
209 return self.podcast
210 else:
211 return self.podcast_group
213 def get_podcast(self):
215 Returns a podcast which is representative for this toplist-entry
216 If the entry is a non-grouped podcast, it is returned
217 If the entry is a podcast group, one of its podcasts is returned
219 if self.podcast:
220 return self.podcast
221 else:
222 return self.podcast_group.podcasts()[0]
224 def __unicode__(self):
225 return '%s (%s)' % (self.podcast, self.subscriptions)
227 class Meta:
228 db_table = 'toplist'
231 class EpisodeToplistEntryManager(models.Manager):
233 def get_query_set(self):
234 return super(EpisodeToplistEntryManager, self).get_query_set().order_by('-listeners')
237 class EpisodeToplistEntry(models.Model):
238 episode = models.ForeignKey('Episode')
239 listeners = models.PositiveIntegerField()
241 objects = EpisodeToplistEntryManager()
243 def __unicode__(self):
244 return '%s (%s)' % (self.episode, self.listeners)
246 class Meta:
247 db_table = 'episode_toplist'
250 class SuggestionEntryManager(models.Manager):
252 def for_user(self, user):
253 from mygpo.data.models import SuggestionBlacklist
255 suggestions = SuggestionEntry.objects.filter(user=user).order_by('-priority')
257 subscriptions = [x.podcast for x in Subscription.objects.filter(user=user)]
258 suggestions = filter(lambda x: x.podcast not in subscriptions, suggestions)
260 blacklist = [x.podcast for x in SuggestionBlacklist.objects.filter(user=user)]
261 suggestions = filter(lambda x: x.podcast not in blacklist, suggestions)
263 return suggestions
266 class SuggestionEntry(models.Model):
267 podcast = models.ForeignKey(Podcast)
268 user = models.ForeignKey(User)
269 priority = models.IntegerField()
271 objects = SuggestionEntryManager()
273 def __unicode__(self):
274 return '%s (%s)' % (self.podcast, self.priority)
276 class Meta:
277 db_table = 'suggestion'
280 class Episode(models.Model):
281 podcast = models.ForeignKey(Podcast)
282 url = models.URLField(verify_exists=False)
283 title = models.CharField(max_length=100, blank=True)
284 description = models.TextField(null=True, blank=True)
285 link = models.URLField(null=True, blank=True, verify_exists=False)
286 timestamp = models.DateTimeField(null=True, blank=True)
287 author = models.CharField(max_length=100, null=True, blank=True)
288 duration = models.PositiveIntegerField(null=True, blank=True)
289 filesize = models.PositiveIntegerField(null=True, blank=True)
290 language = models.CharField(max_length=10, null=True, blank=True)
291 last_update = models.DateTimeField(auto_now=True)
292 outdated = models.BooleanField(default=False) #set to true after episode hasn't been found in feed
293 mimetype = models.CharField(max_length=30, blank=True, null=True)
295 def number(self):
296 m = re.search('\D*(\d+)\D+', self.title)
297 return m.group(1)
299 def shortname(self):
300 s = self.title
301 s = s.replace(self.podcast.title, '')
302 s = s.replace(self.number(), '')
303 s = re.search('\W*(.+)', s).group(1)
304 s = s.strip()
305 return s
307 def listener_count(self):
308 from mygpo.data.models import Listener
309 return Listener.objects.filter(episode=self).values('user').distinct().count()
311 def __unicode__(self):
312 return '%s (%s)' % (self.shortname(), self.podcast)
314 class Meta:
315 db_table = 'episode'
316 unique_together = ('podcast', 'url')
318 class SyncGroup(models.Model):
320 Devices that should be synced with each other need to be grouped
321 in a SyncGroup.
323 SyncGroups are automatically created by calling
324 device.sync_with(other_device), but can also be created manually.
326 device.sync() synchronizes the device for which the method is called
327 with the other devices in its SyncGroup.
329 user = models.ForeignKey(User)
331 def __unicode__(self):
332 devices = [d.name for d in Device.objects.filter(sync_group=self)]
333 return ', '.join(devices)
335 def devices(self):
336 return Device.objects.filter(sync_group=self)
338 def add(self, device):
339 if device.sync_group == self: return
340 if device.sync_group != None:
341 device.unsync()
343 device.sync_group = self
344 device.save()
346 class Meta:
347 db_table = 'sync_group'
350 class Device(models.Model):
351 user = models.ForeignKey(User)
352 uid = models.SlugField(max_length=50)
353 name = models.CharField(max_length=100, blank=True)
354 type = models.CharField(max_length=10, choices=DEVICE_TYPES)
355 sync_group = models.ForeignKey(SyncGroup, blank=True, null=True)
356 deleted = models.BooleanField(default=False)
357 settings = JSONField(default={})
359 def __unicode__(self):
360 return self.name if self.name else _('Unnamed Device (%s)' % self.uid)
362 def get_subscriptions(self):
363 self.sync()
364 return Subscription.objects.filter(device=self)
366 def sync(self):
367 for s in self.get_sync_actions():
368 try:
369 SubscriptionAction.objects.create(device=self, podcast=s.podcast, action=s.action)
370 except Exception, e:
371 log('Error adding subscription action: %s (device %s, podcast %s, action %s)' % (str(e), repr(self), repr(s.podcast), repr(s.action)))
373 def sync_targets(self):
375 returns all Devices and SyncGroups that can be used as a parameter for self.sync_with()
377 sync_targets = list(Device.objects.filter(user=self.user, sync_group=None, deleted=False).exclude(pk=self.id))
379 sync_groups = SyncGroup.objects.filter(user=self.user)
380 if self.sync_group != None: sync_groups = sync_groups.exclude(pk=self.sync_group.id)
382 sync_targets.extend( list(sync_groups) )
383 return sync_targets
386 def get_sync_actions(self):
388 returns the SyncGroupSubscriptionActions correspond to the
389 SubscriptionActions that need to be saved for the current device
390 to synchronize it with its SyncGroup
392 if self.sync_group == None:
393 return []
395 devices = self.sync_group.devices().exclude(pk=self.id)
397 sync_actions = self.latest_actions()
399 for d in devices:
400 a = d.latest_actions()
401 for s in a.keys():
402 if not sync_actions.has_key(s):
403 if a[s].action == SUBSCRIBE_ACTION:
404 sync_actions[s] = a[s]
405 elif a[s].newer_than(sync_actions[s]) and (sync_actions[s].action != a[s].action):
406 sync_actions[s] = a[s]
408 #remove actions that did not change
409 current_state = self.latest_actions()
410 for podcast in current_state.keys():
411 if podcast in current_state and podcast in sync_actions and sync_actions[podcast] == current_state[podcast]:
412 del sync_actions[podcast]
414 return sync_actions.values()
416 def latest_actions(self):
418 returns the latest action for each podcast
419 that has an action on this device
421 #all podcasts that have an action on this device
422 podcasts = [sa.podcast for sa in SubscriptionAction.objects.filter(device=self)]
423 podcasts = list(set(podcasts)) #remove duplicates
425 actions = {}
426 for p in podcasts:
427 actions[p] = self.latest_action(p)
429 return actions
431 def latest_action(self, podcast):
433 returns the latest action for the given podcast on this device
435 actions = SubscriptionAction.objects.filter(podcast=podcast,device=self).order_by('-timestamp', '-id')
436 if actions.count() == 0:
437 return None
438 else:
439 return actions[0]
441 def sync_with(self, other):
443 set the device to be synchronized with other, which can either be a Device or a SyncGroup.
444 this method places them in the same SyncGroup. get_sync_actions() can
445 then return the SyncGroupSubscriptionActions for brining the device
446 in sync with its group
448 if self.user != other.user:
449 raise ValueError('the devices belong to different users')
451 if isinstance(other, SyncGroup):
452 other.add(self)
453 self.save()
454 return
456 if self.sync_group == other.sync_group and self.sync_group != None:
457 return
459 if self.sync_group != None:
460 if other.sync_group == None:
461 self.sync_group.add(other)
463 else:
464 raise ValueError('the devices are in different sync groups')
466 else:
467 if other.sync_group == None:
468 g = SyncGroup.objects.create(user=self.user)
469 g.add(self)
470 g.add(other)
472 else:
473 oter.sync_group.add(self)
475 def unsync(self):
477 stops synchronizing the device
478 this method removes the device from its SyncGroup. If only one
479 device remains in the SyncGroup, it is removed so the device can
480 be used in other groups.
482 if self.sync_group == None:
483 raise ValueError('the device is not synced')
485 g = self.sync_group
486 self.sync_group = None
487 self.save()
489 devices = Device.objects.filter(sync_group=g)
490 if devices.count() == 1:
491 d = devices[0]
492 d.sync_group = None
493 d.save()
494 g.delete()
496 class Meta:
497 db_table = 'device'
499 class EpisodeAction(models.Model):
500 user = models.ForeignKey(User)
501 episode = models.ForeignKey(Episode)
502 device = models.ForeignKey(Device,null=True)
503 action = models.CharField(max_length=10, choices=EPISODE_ACTION_TYPES)
504 timestamp = models.DateTimeField(default=datetime.now)
505 started = models.IntegerField(null=True, blank=True)
506 playmark = models.IntegerField(null=True, blank=True)
507 total = models.IntegerField(null=True, blank=True)
509 def __unicode__(self):
510 return '%s %s %s' % (self.user, self.action, self.episode)
512 def playmark_time(self):
513 return datetime.fromtimestamp(float(self.playmark))
515 def started_time(self):
516 return datetime.fromtimestamp(float(self.started))
518 class Meta:
519 db_table = 'episode_log'
522 class SubscriptionManager(models.Manager):
524 def public_subscriptions(self, podcasts=None):
526 Returns either all public subscriptions or those for the given podcasts
529 subscriptions = self.filter(podcast__in=podcasts) if podcasts else self.all()
531 # remove users with private profiles
532 subscriptions = subscriptions.exclude(user__userprofile__public_profile=False)
534 # remove inactive (eg deleted) users
535 subscriptions = subscriptions.exclude(user__is_active=False)
537 if podcasts:
538 # remove uers that have marked their subscription to this podcast as private
539 private_users = SubscriptionMeta.objects.filter(podcast__in=podcasts, public=False).values('user')
540 subscriptions = subscriptions.exclude(user__in=private_users)
542 return subscriptions
545 class Subscription(models.Model):
546 device = models.ForeignKey(Device, primary_key=True)
547 podcast = models.ForeignKey(Podcast)
548 user = models.ForeignKey(User)
549 subscribed_since = models.DateTimeField()
551 objects = SubscriptionManager()
553 def __unicode__(self):
554 return '%s - %s on %s' % (self.device.user, self.podcast, self.device)
556 def get_meta(self):
557 #this is different than get_or_create because it does not necessarily create a new meta-object
558 qs = SubscriptionMeta.objects.filter(user=self.user, podcast=self.podcast)
560 if qs.count() == 0:
561 return SubscriptionMeta(user=self.user, podcast=self.podcast)
562 else:
563 return qs[0]
565 #this method has to be overwritten, if not it tries to delete a view
566 def delete(self):
567 pass
569 class Meta:
570 db_table = 'current_subscription'
571 #not available in Django 1.0 (Debian stable)
572 managed = False
575 class SubscriptionMeta(models.Model):
576 user = models.ForeignKey(User)
577 podcast = models.ForeignKey(Podcast)
578 public = models.BooleanField(default=True)
579 settings = JSONField(default={})
581 def __unicode__(self):
582 return '%s - %s - %s' % (self.user, self.podcast, self.public)
584 def save(self, *args, **kwargs):
585 self.public = self.settings.get('public_subscription', True)
586 super(SubscriptionMeta, self).save(*args, **kwargs)
589 class Meta:
590 db_table = 'subscription'
591 unique_together = ('user', 'podcast')
594 class EpisodeSettings(models.Model):
595 user = models.ForeignKey(User)
596 episode = models.ForeignKey(Episode)
597 settings = JSONField(default={})
599 def save(self, *args, **kwargs):
600 super(EpisodeSettings, self).save(*args, **kwargs)
602 from mygpo.api.models.users import EpisodeFavorite
603 fav = self.settings.get('is_favorite', False)
604 if fav:
605 EpisodeFavorite.objects.get_or_create(user=self.user, episode=self.episode)
606 else:
607 EpisodeFavorite.objects.filter(user=self.user, episode=self.episode).delete()
610 class Meta:
611 db_table = 'episode_settings'
612 unique_together = ('user', 'episode')
615 class SubscriptionAction(models.Model):
616 device = models.ForeignKey(Device)
617 podcast = models.ForeignKey(Podcast)
618 action = models.IntegerField(choices=SUBSCRIPTION_ACTION_TYPES)
619 timestamp = models.DateTimeField(blank=True, default=datetime.now)
621 def action_string(self):
622 return 'subscribe' if self.action == SUBSCRIBE_ACTION else 'unsubscribe'
624 def newer_than(self, action):
625 return self.timestamp > action.timestamp
627 def __unicode__(self):
628 return '%s %s %s %s' % (self.device.user, self.device, self.action_string(), self.podcast)
630 class Meta:
631 db_table = 'subscription_log'
632 unique_together = ('device', 'podcast', 'timestamp')
635 class URLSanitizingRule(models.Model):
636 use_podcast = models.BooleanField()
637 use_episode = models.BooleanField()
638 search = models.CharField(max_length=100)
639 search_precompiled = None
640 replace = models.CharField(max_length=100, null=False, blank=True)
641 priority = models.PositiveIntegerField()
642 description = models.TextField(null=False, blank=True)
644 class Meta:
645 db_table = 'sanitizing_rules'
647 def __unicode__(self):
648 return '%s -> %s' % (self.search, self.replace)
651 from mygpo.search.signals import update_podcast_entry, update_podcast_group_entry, remove_podcast_entry, remove_podcast_group_entry
652 from django.db.models.signals import post_save, pre_delete
654 post_save.connect(update_podcast_entry, sender=Podcast)
655 pre_delete.connect(remove_podcast_entry, sender=Podcast)
657 post_save.connect(update_podcast_group_entry, sender=PodcastGroup)
658 pre_delete.connect(remove_podcast_group_entry, sender=PodcastGroup)