remove SearchEntry objects for deleted podcast/podcast groups
[mygpo.git] / mygpo / api / models / __init__.py
blobbca16355832a8a706e47134d04af455c1978ab7a
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 = 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 subscriptions = Subscription.objects.filter(podcast=self)
68 # remove users with private profiles
69 subscriptions = subscriptions.exclude(user__userprofile__public_profile=False)
71 # remove inactive (eg deleted) users
72 subscriptions = subscriptions.exclude(user__is_active=False)
74 # remove uers that have marked their subscription to this podcast as private
75 private_users = SubscriptionMeta.objects.filter(podcast=self, public=False).values('user')
76 subscriptions = subscriptions.exclude(user__in=private_users)
78 return subscriptions
81 def subscription_count(self):
82 return self.subscriptions().count()
84 def subscriber_count(self):
85 """
86 Returns the number of public subscriptions to this podcast
87 """
88 subscriptions = self.subscriptions()
89 return subscriptions.values('user').distinct().count()
92 def listener_count(self):
93 from mygpo.data.models import Listener
94 return Listener.objects.filter(podcast=self).values('user').distinct().count()
96 def logo_shortname(self):
97 return hashlib.sha1(self.logo_url).hexdigest()
99 def subscribe_targets(self, user):
101 returns all Devices and SyncGroups on which this podcast can be subsrbied. This excludes all
102 devices/syncgroups on which the podcast is already subscribed
104 targets = []
106 devices = Device.objects.filter(user=user, deleted=False)
107 for d in devices:
108 subscriptions = [x.podcast for x in d.get_subscriptions()]
109 if self in subscriptions: continue
111 if d.sync_group:
112 if not d.sync_group in targets: targets.append(d.sync_group)
113 else:
114 targets.append(d)
116 return targets
119 def group_with(self, other, grouptitle, myname, othername):
120 if self.group == other.group and self.group != None:
121 return
123 if self.group != None:
124 if other.group == None:
125 self.group.add(other, othername)
127 else:
128 raise ValueError('the podcasts are already in different groups')
129 else:
130 if other.group == None:
131 g = PodcastGroup.objects.create(title=grouptitle)
132 g.add(self, myname)
133 g.add(other, othername)
135 else:
136 oter.group.add(self)
138 def ungroup(self):
139 if self.group == None:
140 raise ValueError('the podcast currently isn\'t in any group')
142 g = self.group
143 self.group = None
144 self.save()
146 podcasts = Podcast.objects.filter(group=g)
147 if podcasts.count() == 1:
148 p = podcasts[0]
149 p.group = None
150 p.save()
152 def get_similar(self):
153 from mygpo.data.models import RelatedPodcast
154 return [r.rel_podcast for r in RelatedPodcast.objects.filter(ref_podcast=self)]
156 def __unicode__(self):
157 return self.title if self.title != '' else self.url
159 class Meta:
160 db_table = 'podcast'
163 class PodcastGroup(models.Model):
164 title = models.CharField(max_length=100, blank=False)
166 def add(self, podcast, membername):
167 if podcast.group == self:
168 podcast.group_member_name = membername
170 elif podcast.group != None:
171 podcast.ungroup()
173 podcast.group = self
174 podcast.group_member_name = membername
175 podcast.save()
177 def podcasts(self):
178 return Podcast.objects.filter(group=self)
180 def __unicode__(self):
181 return self.title
183 class Meta:
184 db_table = 'podcast_groups'
187 class ToplistEntryManager(models.Manager):
189 def get_query_set(self):
190 return super(ToplistEntryManager, self).get_query_set().order_by('-subscriptions')
193 class ToplistEntry(models.Model):
194 podcast = models.ForeignKey(Podcast, null=True)
195 podcast_group = models.ForeignKey(PodcastGroup, null=True)
196 oldplace = models.IntegerField(db_column='old_place')
197 subscriptions = models.IntegerField(db_column='subscription_count')
199 objects = ToplistEntryManager()
202 def get_item(self):
203 if self.podcast:
204 return self.podcast
205 else:
206 return self.podcast_group
208 def get_podcast(self):
210 Returns a podcast which is representative for this toplist-entry
211 If the entry is a non-grouped podcast, it is returned
212 If the entry is a podcast group, one of its podcasts is returned
214 if self.podcast:
215 return self.podcast
216 else:
217 return self.podcast_group.podcasts()[0]
219 def __unicode__(self):
220 return '%s (%s)' % (self.podcast, self.subscriptions)
222 class Meta:
223 db_table = 'toplist'
226 class EpisodeToplistEntryManager(models.Manager):
228 def get_query_set(self):
229 return super(EpisodeToplistEntryManager, self).get_query_set().order_by('-listeners')
232 class EpisodeToplistEntry(models.Model):
233 episode = models.ForeignKey('Episode')
234 listeners = models.PositiveIntegerField()
236 objects = EpisodeToplistEntryManager()
238 def __unicode__(self):
239 return '%s (%s)' % (self.episode, self.listeners)
241 class Meta:
242 db_table = 'episode_toplist'
245 class SuggestionEntryManager(models.Manager):
247 def for_user(self, user):
248 from mygpo.data.models import SuggestionBlacklist
250 suggestions = SuggestionEntry.objects.filter(user=user).order_by('-priority')
252 subscriptions = [x.podcast for x in Subscription.objects.filter(user=user)]
253 suggestions = filter(lambda x: x.podcast not in subscriptions, suggestions)
255 blacklist = [x.podcast for x in SuggestionBlacklist.objects.filter(user=user)]
256 suggestions = filter(lambda x: x.podcast not in blacklist, suggestions)
258 return suggestions
261 class SuggestionEntry(models.Model):
262 podcast = models.ForeignKey(Podcast)
263 user = models.ForeignKey(User)
264 priority = models.IntegerField()
266 objects = SuggestionEntryManager()
268 def __unicode__(self):
269 return '%s (%s)' % (self.podcast, self.priority)
271 class Meta:
272 db_table = 'suggestion'
275 class Episode(models.Model):
276 podcast = models.ForeignKey(Podcast)
277 url = models.URLField(verify_exists=False)
278 title = models.CharField(max_length=100, blank=True)
279 description = models.TextField(null=True, blank=True)
280 link = models.URLField(null=True, blank=True, verify_exists=False)
281 timestamp = models.DateTimeField(null=True, blank=True)
282 author = models.CharField(max_length=100, null=True, blank=True)
283 duration = models.PositiveIntegerField(null=True, blank=True)
284 filesize = models.PositiveIntegerField(null=True, blank=True)
285 language = models.CharField(max_length=10, null=True, blank=True)
286 last_update = models.DateTimeField(auto_now=True)
287 outdated = models.BooleanField(default=False) #set to true after episode hasn't been found in feed
288 mimetype = models.CharField(max_length=30, blank=True, null=True)
290 def number(self):
291 m = re.search('\D*(\d+)\D+', self.title)
292 return m.group(1)
294 def shortname(self):
295 s = self.title
296 s = s.replace(self.podcast.title, '')
297 s = s.replace(self.number(), '')
298 s = re.search('\W*(.+)', s).group(1)
299 s = s.strip()
300 return s
302 def listener_count(self):
303 from mygpo.data.models import Listener
304 return Listener.objects.filter(episode=self).values('user').distinct().count()
306 def __unicode__(self):
307 return '%s (%s)' % (self.shortname(), self.podcast)
309 class Meta:
310 db_table = 'episode'
311 unique_together = ('podcast', 'url')
313 class SyncGroup(models.Model):
315 Devices that should be synced with each other need to be grouped
316 in a SyncGroup.
318 SyncGroups are automatically created by calling
319 device.sync_with(other_device), but can also be created manually.
321 device.sync() synchronizes the device for which the method is called
322 with the other devices in its SyncGroup.
324 user = models.ForeignKey(User)
326 def __unicode__(self):
327 devices = [d.name for d in Device.objects.filter(sync_group=self)]
328 return ', '.join(devices)
330 def devices(self):
331 return Device.objects.filter(sync_group=self)
333 def add(self, device):
334 if device.sync_group == self: return
335 if device.sync_group != None:
336 device.unsync()
338 device.sync_group = self
339 device.save()
341 class Meta:
342 db_table = 'sync_group'
345 class Device(models.Model):
346 user = models.ForeignKey(User)
347 uid = models.SlugField(max_length=50)
348 name = models.CharField(max_length=100, blank=True)
349 type = models.CharField(max_length=10, choices=DEVICE_TYPES)
350 sync_group = models.ForeignKey(SyncGroup, blank=True, null=True)
351 deleted = models.BooleanField(default=False)
352 settings = JSONField(default={})
354 def __unicode__(self):
355 return self.name if self.name else _('Unnamed Device (%s)' % self.uid)
357 def get_subscriptions(self):
358 self.sync()
359 return Subscription.objects.filter(device=self)
361 def sync(self):
362 for s in self.get_sync_actions():
363 try:
364 SubscriptionAction.objects.create(device=self, podcast=s.podcast, action=s.action)
365 except Exception, e:
366 log('Error adding subscription action: %s (device %s, podcast %s, action %s)' % (str(e), repr(self), repr(s.podcast), repr(s.action)))
368 def sync_targets(self):
370 returns all Devices and SyncGroups that can be used as a parameter for self.sync_with()
372 sync_targets = list(Device.objects.filter(user=self.user, sync_group=None, deleted=False).exclude(pk=self.id))
374 sync_groups = SyncGroup.objects.filter(user=self.user)
375 if self.sync_group != None: sync_groups = sync_groups.exclude(pk=self.sync_group.id)
377 sync_targets.extend( list(sync_groups) )
378 return sync_targets
381 def get_sync_actions(self):
383 returns the SyncGroupSubscriptionActions correspond to the
384 SubscriptionActions that need to be saved for the current device
385 to synchronize it with its SyncGroup
387 if self.sync_group == None:
388 return []
390 devices = self.sync_group.devices().exclude(pk=self.id)
392 sync_actions = self.latest_actions()
394 for d in devices:
395 a = d.latest_actions()
396 for s in a.keys():
397 if not sync_actions.has_key(s):
398 if a[s].action == SUBSCRIBE_ACTION:
399 sync_actions[s] = a[s]
400 elif a[s].newer_than(sync_actions[s]) and (sync_actions[s].action != a[s].action):
401 sync_actions[s] = a[s]
403 #remove actions that did not change
404 current_state = self.latest_actions()
405 for podcast in current_state.keys():
406 if podcast in current_state and podcast in sync_actions and sync_actions[podcast] == current_state[podcast]:
407 del sync_actions[podcast]
409 return sync_actions.values()
411 def latest_actions(self):
413 returns the latest action for each podcast
414 that has an action on this device
416 #all podcasts that have an action on this device
417 podcasts = [sa.podcast for sa in SubscriptionAction.objects.filter(device=self)]
418 podcasts = list(set(podcasts)) #remove duplicates
420 actions = {}
421 for p in podcasts:
422 actions[p] = self.latest_action(p)
424 return actions
426 def latest_action(self, podcast):
428 returns the latest action for the given podcast on this device
430 actions = SubscriptionAction.objects.filter(podcast=podcast,device=self).order_by('-timestamp', '-id')
431 if actions.count() == 0:
432 return None
433 else:
434 return actions[0]
436 def sync_with(self, other):
438 set the device to be synchronized with other, which can either be a Device or a SyncGroup.
439 this method places them in the same SyncGroup. get_sync_actions() can
440 then return the SyncGroupSubscriptionActions for brining the device
441 in sync with its group
443 if self.user != other.user:
444 raise ValueError('the devices belong to different users')
446 if isinstance(other, SyncGroup):
447 other.add(self)
448 self.save()
449 return
451 if self.sync_group == other.sync_group and self.sync_group != None:
452 return
454 if self.sync_group != None:
455 if other.sync_group == None:
456 self.sync_group.add(other)
458 else:
459 raise ValueError('the devices are in different sync groups')
461 else:
462 if other.sync_group == None:
463 g = SyncGroup.objects.create(user=self.user)
464 g.add(self)
465 g.add(other)
467 else:
468 oter.sync_group.add(self)
470 def unsync(self):
472 stops synchronizing the device
473 this method removes the device from its SyncGroup. If only one
474 device remains in the SyncGroup, it is removed so the device can
475 be used in other groups.
477 if self.sync_group == None:
478 raise ValueError('the device is not synced')
480 g = self.sync_group
481 self.sync_group = None
482 self.save()
484 devices = Device.objects.filter(sync_group=g)
485 if devices.count() == 1:
486 d = devices[0]
487 d.sync_group = None
488 d.save()
489 g.delete()
491 class Meta:
492 db_table = 'device'
494 class EpisodeAction(models.Model):
495 user = models.ForeignKey(User)
496 episode = models.ForeignKey(Episode)
497 device = models.ForeignKey(Device,null=True)
498 action = models.CharField(max_length=10, choices=EPISODE_ACTION_TYPES)
499 timestamp = models.DateTimeField(default=datetime.now)
500 started = models.IntegerField(null=True, blank=True)
501 playmark = models.IntegerField(null=True, blank=True)
502 total = models.IntegerField(null=True, blank=True)
504 def __unicode__(self):
505 return '%s %s %s' % (self.user, self.action, self.episode)
507 def playmark_time(self):
508 return datetime.fromtimestamp(float(self.playmark))
510 def started_time(self):
511 return datetime.fromtimestamp(float(self.started))
513 class Meta:
514 db_table = 'episode_log'
517 class Subscription(models.Model):
518 device = models.ForeignKey(Device, primary_key=True)
519 podcast = models.ForeignKey(Podcast)
520 user = models.ForeignKey(User)
521 subscribed_since = models.DateTimeField()
523 def __unicode__(self):
524 return '%s - %s on %s' % (self.device.user, self.podcast, self.device)
526 def get_meta(self):
527 #this is different than get_or_create because it does not necessarily create a new meta-object
528 qs = SubscriptionMeta.objects.filter(user=self.user, podcast=self.podcast)
530 if qs.count() == 0:
531 return SubscriptionMeta(user=self.user, podcast=self.podcast)
532 else:
533 return qs[0]
535 #this method has to be overwritten, if not it tries to delete a view
536 def delete(self):
537 pass
539 class Meta:
540 db_table = 'current_subscription'
541 #not available in Django 1.0 (Debian stable)
542 managed = False
545 class SubscriptionMeta(models.Model):
546 user = models.ForeignKey(User)
547 podcast = models.ForeignKey(Podcast)
548 public = models.BooleanField(default=True)
549 settings = JSONField(default={})
551 def __unicode__(self):
552 return '%s - %s - %s' % (self.user, self.podcast, self.public)
554 def save(self, *args, **kwargs):
555 self.public = self.settings.get('public_subscription', True)
556 super(SubscriptionMeta, self).save(*args, **kwargs)
559 class Meta:
560 db_table = 'subscription'
561 unique_together = ('user', 'podcast')
564 class EpisodeSettings(models.Model):
565 user = models.ForeignKey(User)
566 episode = models.ForeignKey(Episode)
567 settings = JSONField(default={})
569 class Meta:
570 db_table = 'episode_settings'
571 unique_together = ('user', 'episode')
574 class SubscriptionAction(models.Model):
575 device = models.ForeignKey(Device)
576 podcast = models.ForeignKey(Podcast)
577 action = models.IntegerField(choices=SUBSCRIPTION_ACTION_TYPES)
578 timestamp = models.DateTimeField(blank=True, default=datetime.now)
580 def action_string(self):
581 return 'subscribe' if self.action == SUBSCRIBE_ACTION else 'unsubscribe'
583 def newer_than(self, action):
584 return self.timestamp > action.timestamp
586 def __unicode__(self):
587 return '%s %s %s %s' % (self.device.user, self.device, self.action_string(), self.podcast)
589 class Meta:
590 db_table = 'subscription_log'
591 unique_together = ('device', 'podcast', 'timestamp')
594 class URLSanitizingRule(models.Model):
595 use_podcast = models.BooleanField()
596 use_episode = models.BooleanField()
597 search = models.CharField(max_length=100)
598 search_precompiled = None
599 replace = models.CharField(max_length=100, null=False, blank=True)
600 priority = models.PositiveIntegerField()
601 description = models.TextField(null=False, blank=True)
603 class Meta:
604 db_table = 'sanitizing_rules'
606 def __unicode__(self):
607 return '%s -> %s' % (self.search, self.replace)
610 from mygpo.search.signals import update_podcast_entry, update_podcast_group_entry, remove_podcast_entry, remove_podcast_group_entry
611 from django.db.models.signals import post_save, pre_delete
613 post_save.connect(update_podcast_entry, sender=Podcast)
614 pre_delete.connect(remove_podcast_entry, sender=Podcast)
616 post_save.connect(update_podcast_group_entry, sender=PodcastGroup)
617 pre_delete.connect(remove_podcast_group_entry, sender=PodcastGroup)