refactoring
[mygpo.git] / mygpo / api / models / __init__.py
blobe1973858df40bb16a21ea690f2a2e1d9b26cf8ee
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 import hashlib
23 import re
25 from mygpo.api.constants import EPISODE_ACTION_TYPES, DEVICE_TYPES, SUBSCRIBE_ACTION, UNSUBSCRIBE_ACTION, SUBSCRIPTION_ACTION_TYPES
26 from mygpo.log import log
28 class UserProfile(models.Model):
29 user = models.ForeignKey(User, unique=True, db_column='user_ptr_id')
31 public_profile = models.BooleanField(default=True)
32 generated_id = models.BooleanField(default=False)
33 deleted = models.BooleanField(default=False)
34 suggestion_up_to_date = models.BooleanField(default=False)
36 def __unicode__(self):
37 return '%s (%s, %s)' % (self.user.username, self.public_profile, self.generated_id)
39 class Meta:
40 db_table = 'user'
42 class Podcast(models.Model):
43 url = models.URLField(unique=True, verify_exists=False)
44 title = models.CharField(max_length=100, blank=True)
45 description = models.TextField(blank=True, null=True)
46 link = models.URLField(blank=True, null=True, verify_exists=False)
47 last_update = models.DateTimeField(null=True,blank=True)
48 logo_url = models.CharField(max_length=1000,null=True,blank=True)
49 author = models.CharField(max_length=100, null=True, blank=True)
50 language = models.CharField(max_length=10, null=True, blank=True)
51 group = models.ForeignKey('PodcastGroup', null=True)
52 group_member_name = models.CharField(max_length=20, default=None, null=True, blank=False)
54 def subscriptions(self):
55 """
56 returns all public subscriptions to this podcast
57 """
58 subscriptions = Subscription.objects.filter(podcast=self)
60 # remove users with private profiles
61 subscriptions = subscriptions.exclude(user__userprofile__public_profile=False)
63 # remove inactive (eg deleted) users
64 subscriptions = subscriptions.exclude(user__is_active=False)
66 # remove uers that have marked their subscription to this podcast as private
67 private_users = SubscriptionMeta.objects.filter(podcast=self, public=False).values('user')
68 subscriptions = subscriptions.exclude(user__in=private_users)
70 return subscriptions
73 def subscription_count(self):
74 return self.subscriptions().count()
76 def subscriber_count(self):
77 """
78 Returns the number of public subscriptions to this podcast
79 """
80 subscriptions = self.subscriptions()
81 return subscriptions.values('user').distinct().count()
84 def listener_count(self):
85 from mygpo.data.models import Listener
86 return Listener.objects.filter(podcast=self).values('user').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 __unicode__(self):
149 return self.title if self.title != '' else self.url
151 class Meta:
152 db_table = 'podcast'
155 class PodcastGroup(models.Model):
156 title = models.CharField(max_length=100, blank=False)
158 def add(self, podcast, membername):
159 if podcast.group == self:
160 podcast.group_member_name = membername
162 elif podcast.group != None:
163 podcast.ungroup()
165 podcast.group = self
166 podcast.group_member_name = membername
167 podcast.save()
169 def podcasts(self):
170 return Podcast.objects.filter(group=self)
172 def __unicode__(self):
173 return self.title
175 class Meta:
176 db_table = 'podcast_groups'
179 class ToplistEntryManager(models.Manager):
181 def get_query_set(self):
182 return super(ToplistEntryManager, self).get_query_set().order_by('-subscriptions')
185 class ToplistEntry(models.Model):
186 podcast = models.ForeignKey(Podcast, null=True)
187 podcast_group = models.ForeignKey(PodcastGroup, null=True)
188 oldplace = models.IntegerField(db_column='old_place')
189 subscriptions = models.IntegerField(db_column='subscription_count')
191 objects = ToplistEntryManager()
194 def get_item(self):
195 if self.podcast:
196 return self.podcast
197 else:
198 return self.podcast_group
200 def get_podcast(self):
202 Returns a podcast which is representative for this toplist-entry
203 If the entry is a non-grouped podcast, it is returned
204 If the entry is a podcast group, one of its podcasts is returned
206 if self.podcast:
207 return self.podcast
208 else:
209 return self.podcast_group.podcasts()[0]
211 def __unicode__(self):
212 return '%s (%s)' % (self.podcast, self.subscriptions)
214 class Meta:
215 db_table = 'toplist'
218 class EpisodeToplistEntryManager(models.Manager):
220 def get_query_set(self):
221 return super(EpisodeToplistEntryManager, self).get_query_set().order_by('-listeners')
224 class EpisodeToplistEntry(models.Model):
225 episode = models.ForeignKey('Episode')
226 listeners = models.PositiveIntegerField()
228 objects = EpisodeToplistEntryManager()
230 def __unicode__(self):
231 return '%s (%s)' % (self.episode, self.listeners)
233 class Meta:
234 db_table = 'episode_toplist'
237 class SuggestionEntryManager(models.Manager):
239 def for_user(self, user):
240 from mygpo.data.models import SuggestionBlacklist
242 suggestions = SuggestionEntry.objects.filter(user=user).order_by('-priority')
244 subscriptions = [x.podcast for x in Subscription.objects.filter(user=user)]
245 suggestions = filter(lambda x: x.podcast not in subscriptions, suggestions)
247 blacklist = [x.podcast for x in SuggestionBlacklist.objects.filter(user=user)]
248 suggestions = filter(lambda x: x.podcast not in blacklist, suggestions)
250 return suggestions
253 class SuggestionEntry(models.Model):
254 podcast = models.ForeignKey(Podcast)
255 user = models.ForeignKey(User)
256 priority = models.IntegerField()
258 objects = SuggestionEntryManager()
260 def __unicode__(self):
261 return '%s (%s)' % (self.podcast, self.priority)
263 class Meta:
264 db_table = 'suggestion'
267 class Episode(models.Model):
268 podcast = models.ForeignKey(Podcast)
269 url = models.URLField(verify_exists=False)
270 title = models.CharField(max_length=100, blank=True)
271 description = models.TextField(null=True, blank=True)
272 link = models.URLField(null=True, blank=True, verify_exists=False)
273 timestamp = models.DateTimeField(null=True, blank=True)
274 author = models.CharField(max_length=100, null=True, blank=True)
275 duration = models.PositiveIntegerField(null=True, blank=True)
276 filesize = models.PositiveIntegerField(null=True, blank=True)
277 language = models.CharField(max_length=10, null=True, blank=True)
278 last_update = models.DateTimeField(auto_now=True)
279 outdated = models.BooleanField(default=False) #set to true after episode hasn't been found in feed
281 def number(self):
282 m = re.search('\D*(\d+)\D+', self.title)
283 return m.group(1)
285 def shortname(self):
286 s = self.title
287 s = s.replace(self.podcast.title, '')
288 s = s.replace(self.number(), '')
289 s = re.search('\W*(.+)', s).group(1)
290 s = s.strip()
291 return s
293 def listener_count(self):
294 from mygpo.data.models import Listener
295 return Listener.objects.filter(episode=self).values('user').distinct().count()
297 def __unicode__(self):
298 return '%s (%s)' % (self.shortname(), self.podcast)
300 class Meta:
301 db_table = 'episode'
302 unique_together = ('podcast', 'url')
304 class SyncGroup(models.Model):
306 Devices that should be synced with each other need to be grouped
307 in a SyncGroup.
309 SyncGroups are automatically created by calling
310 device.sync_with(other_device), but can also be created manually.
312 device.sync() synchronizes the device for which the method is called
313 with the other devices in its SyncGroup.
315 user = models.ForeignKey(User)
317 def __unicode__(self):
318 devices = [d.name for d in Device.objects.filter(sync_group=self)]
319 return ', '.join(devices)
321 def devices(self):
322 return Device.objects.filter(sync_group=self)
324 def add(self, device):
325 if device.sync_group == self: return
326 if device.sync_group != None:
327 device.unsync()
329 device.sync_group = self
330 device.save()
332 class Meta:
333 db_table = 'sync_group'
336 class Device(models.Model):
337 user = models.ForeignKey(User)
338 uid = models.SlugField(max_length=50)
339 name = models.CharField(max_length=100, blank=True)
340 type = models.CharField(max_length=10, choices=DEVICE_TYPES)
341 sync_group = models.ForeignKey(SyncGroup, blank=True, null=True)
342 deleted = models.BooleanField(default=False)
344 def __unicode__(self):
345 return self.name if self.name else _('Unnamed Device (%s)' % self.uid)
347 def get_subscriptions(self):
348 self.sync()
349 return Subscription.objects.filter(device=self)
351 def sync(self):
352 for s in self.get_sync_actions():
353 try:
354 SubscriptionAction.objects.create(device=self, podcast=s.podcast, action=s.action)
355 except Exception, e:
356 log('Error adding subscription action: %s (device %s, podcast %s, action %s)' % (str(e), repr(self), repr(s.podcast), repr(s.action)))
358 def sync_targets(self):
360 returns all Devices and SyncGroups that can be used as a parameter for self.sync_with()
362 sync_targets = list(Device.objects.filter(user=self.user, sync_group=None, deleted=False).exclude(pk=self.id))
364 sync_groups = SyncGroup.objects.filter(user=self.user)
365 if self.sync_group != None: sync_groups = sync_groups.exclude(pk=self.sync_group.id)
367 sync_targets.extend( list(sync_groups) )
368 return sync_targets
371 def get_sync_actions(self):
373 returns the SyncGroupSubscriptionActions correspond to the
374 SubscriptionActions that need to be saved for the current device
375 to synchronize it with its SyncGroup
377 if self.sync_group == None:
378 return []
380 devices = self.sync_group.devices().exclude(pk=self.id)
382 sync_actions = self.latest_actions()
384 for d in devices:
385 a = d.latest_actions()
386 for s in a.keys():
387 if not sync_actions.has_key(s):
388 if a[s].action == SUBSCRIBE_ACTION:
389 sync_actions[s] = a[s]
390 elif a[s].newer_than(sync_actions[s]) and (sync_actions[s].action != a[s].action):
391 sync_actions[s] = a[s]
393 #remove actions that did not change
394 current_state = self.latest_actions()
395 for podcast in current_state.keys():
396 if podcast in current_state and sync_actions[podcast] == current_state[podcast]:
397 del sync_actions[podcast]
399 return sync_actions.values()
401 def latest_actions(self):
403 returns the latest action for each podcast
404 that has an action on this device
406 #all podcasts that have an action on this device
407 podcasts = [sa.podcast for sa in SubscriptionAction.objects.filter(device=self)]
408 podcasts = list(set(podcasts)) #remove duplicates
410 actions = {}
411 for p in podcasts:
412 actions[p] = self.latest_action(p)
414 return actions
416 def latest_action(self, podcast):
418 returns the latest action for the given podcast on this device
420 actions = SubscriptionAction.objects.filter(podcast=podcast,device=self).order_by('-timestamp', '-id')
421 if actions.count() == 0:
422 return None
423 else:
424 return actions[0]
426 def sync_with(self, other):
428 set the device to be synchronized with other, which can either be a Device or a SyncGroup.
429 this method places them in the same SyncGroup. get_sync_actions() can
430 then return the SyncGroupSubscriptionActions for brining the device
431 in sync with its group
433 if self.user != other.user:
434 raise ValueError('the devices belong to different users')
436 if isinstance(other, SyncGroup):
437 other.add(self)
438 self.save()
439 return
441 if self.sync_group == other.sync_group and self.sync_group != None:
442 return
444 if self.sync_group != None:
445 if other.sync_group == None:
446 self.sync_group.add(other)
448 else:
449 raise ValueError('the devices are in different sync groups')
451 else:
452 if other.sync_group == None:
453 g = SyncGroup.objects.create(user=self.user)
454 g.add(self)
455 g.add(other)
457 else:
458 oter.sync_group.add(self)
460 def unsync(self):
462 stops synchronizing the device
463 this method removes the device from its SyncGroup. If only one
464 device remains in the SyncGroup, it is removed so the device can
465 be used in other groups.
467 if self.sync_group == None:
468 raise ValueError('the device is not synced')
470 g = self.sync_group
471 print g
472 self.sync_group = None
473 self.save()
475 devices = Device.objects.filter(sync_group=g)
476 if devices.count() == 1:
477 d = devices[0]
478 d.sync_group = None
479 d.save()
480 g.delete()
482 class Meta:
483 db_table = 'device'
485 class EpisodeAction(models.Model):
486 user = models.ForeignKey(User)
487 episode = models.ForeignKey(Episode)
488 device = models.ForeignKey(Device,null=True)
489 action = models.CharField(max_length=10, choices=EPISODE_ACTION_TYPES)
490 timestamp = models.DateTimeField(default=datetime.now)
491 started = models.IntegerField(null=True, blank=True)
492 playmark = models.IntegerField(null=True, blank=True)
493 total = models.IntegerField(null=True, blank=True)
495 def __unicode__(self):
496 return '%s %s %s' % (self.user, self.action, self.episode)
498 def playmark_time(self):
499 return datetime.fromtimestamp(float(self.playmark))
501 class Meta:
502 db_table = 'episode_log'
505 class Subscription(models.Model):
506 device = models.ForeignKey(Device, primary_key=True)
507 podcast = models.ForeignKey(Podcast)
508 user = models.ForeignKey(User)
509 subscribed_since = models.DateTimeField()
511 def __unicode__(self):
512 return '%s - %s on %s' % (self.device.user, self.podcast, self.device)
514 def get_meta(self):
515 #this is different than get_or_create because it does not necessarily create a new meta-object
516 qs = SubscriptionMeta.objects.filter(user=self.user, podcast=self.podcast)
518 if qs.count() == 0:
519 return SubscriptionMeta(user=self.user, podcast=self.podcast)
520 else:
521 return qs[0]
523 #this method has to be overwritten, if not it tries to delete a view
524 def delete(self):
525 pass
527 class Meta:
528 db_table = 'current_subscription'
529 #not available in Django 1.0 (Debian stable)
530 managed = False
533 class SubscriptionMeta(models.Model):
534 user = models.ForeignKey(User)
535 podcast = models.ForeignKey(Podcast)
536 public = models.BooleanField(default=True)
538 def __unicode__(self):
539 return '%s - %s - %s' % (self.user, self.podcast, self.public)
541 class Meta:
542 db_table = 'subscription'
543 unique_together = ('user', 'podcast')
546 class SubscriptionAction(models.Model):
547 device = models.ForeignKey(Device)
548 podcast = models.ForeignKey(Podcast)
549 action = models.IntegerField(choices=SUBSCRIPTION_ACTION_TYPES)
550 timestamp = models.DateTimeField(blank=True, default=datetime.now)
552 def action_string(self):
553 return 'subscribe' if self.action == SUBSCRIBE_ACTION else 'unsubscribe'
555 def newer_than(self, action):
556 return self.timestamp > action.timestamp
558 def __unicode__(self):
559 return '%s %s %s %s' % (self.device.user, self.device, self.action_string(), self.podcast)
561 class Meta:
562 db_table = 'subscription_log'
563 unique_together = ('device', 'podcast', 'timestamp')
566 class URLSanitizingRule(models.Model):
567 use_podcast = models.BooleanField()
568 use_episode = models.BooleanField()
569 search = models.CharField(max_length=100)
570 search_precompiled = None
571 replace = models.CharField(max_length=100, null=False, blank=True)
572 priority = models.PositiveIntegerField()
573 description = models.TextField(null=False, blank=True)
575 class Meta:
576 db_table = 'sanitizing_rules'
578 def __unicode__(self):
579 return '%s -> %s' % (self.search, self.replace)
582 from mygpo.search.signals import update_podcast_entry, update_podcast_group_entry
583 from django.db.models.signals import post_save, pre_delete
585 post_save.connect(update_podcast_entry, sender=Podcast)
586 pre_delete.connect(update_podcast_entry, sender=Podcast)
588 post_save.connect(update_podcast_group_entry, sender=PodcastGroup)
589 pre_delete.connect(update_podcast_group_entry, sender=PodcastGroup)