remove unnecessary print output
[mygpo.git] / mygpo / api / models / __init__.py
blob19a6b1f014a29bf7d95c4e714d793b68f4ecaf0e
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
23 import hashlib
24 import re
26 from mygpo.api.constants import EPISODE_ACTION_TYPES, DEVICE_TYPES, SUBSCRIBE_ACTION, UNSUBSCRIBE_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)
37 def __unicode__(self):
38 return '%s (%s, %s)' % (self.user.username, self.public_profile, self.generated_id)
40 class Meta:
41 db_table = 'user'
43 class Podcast(models.Model):
44 url = models.URLField(unique=True, verify_exists=False)
45 title = models.CharField(max_length=100, blank=True)
46 description = models.TextField(blank=True, null=True)
47 link = models.URLField(blank=True, null=True, verify_exists=False)
48 last_update = models.DateTimeField(null=True,blank=True)
49 logo_url = models.CharField(max_length=1000,null=True,blank=True)
50 author = models.CharField(max_length=100, null=True, blank=True)
51 language = models.CharField(max_length=10, null=True, blank=True)
52 group = models.ForeignKey('PodcastGroup', null=True)
53 group_member_name = models.CharField(max_length=20, default=None, null=True, blank=False)
54 content_types = SeparatedValuesField(null=True, blank=True)
56 def subscriptions(self):
57 """
58 returns all public subscriptions to this podcast
59 """
60 subscriptions = Subscription.objects.filter(podcast=self)
62 # remove users with private profiles
63 subscriptions = subscriptions.exclude(user__userprofile__public_profile=False)
65 # remove inactive (eg deleted) users
66 subscriptions = subscriptions.exclude(user__is_active=False)
68 # remove uers that have marked their subscription to this podcast as private
69 private_users = SubscriptionMeta.objects.filter(podcast=self, public=False).values('user')
70 subscriptions = subscriptions.exclude(user__in=private_users)
72 return subscriptions
75 def subscription_count(self):
76 return self.subscriptions().count()
78 def subscriber_count(self):
79 """
80 Returns the number of public subscriptions to this podcast
81 """
82 subscriptions = self.subscriptions()
83 return subscriptions.values('user').distinct().count()
86 def listener_count(self):
87 from mygpo.data.models import Listener
88 return Listener.objects.filter(podcast=self).values('user').distinct().count()
90 def logo_shortname(self):
91 return hashlib.sha1(self.logo_url).hexdigest()
93 def subscribe_targets(self, user):
94 """
95 returns all Devices and SyncGroups on which this podcast can be subsrbied. This excludes all
96 devices/syncgroups on which the podcast is already subscribed
97 """
98 targets = []
100 devices = Device.objects.filter(user=user, deleted=False)
101 for d in devices:
102 subscriptions = [x.podcast for x in d.get_subscriptions()]
103 if self in subscriptions: continue
105 if d.sync_group:
106 if not d.sync_group in targets: targets.append(d.sync_group)
107 else:
108 targets.append(d)
110 return targets
113 def group_with(self, other, grouptitle, myname, othername):
114 if self.group == other.group and self.group != None:
115 return
117 if self.group != None:
118 if other.group == None:
119 self.group.add(other, othername)
121 else:
122 raise ValueError('the podcasts are already in different groups')
123 else:
124 if other.group == None:
125 g = PodcastGroup.objects.create(title=grouptitle)
126 g.add(self, myname)
127 g.add(other, othername)
129 else:
130 oter.group.add(self)
132 def ungroup(self):
133 if self.group == None:
134 raise ValueError('the podcast currently isn\'t in any group')
136 g = self.group
137 self.group = None
138 self.save()
140 podcasts = Podcast.objects.filter(group=g)
141 if podcasts.count() == 1:
142 p = podcasts[0]
143 p.group = None
144 p.save()
146 def get_similar(self):
147 from mygpo.data.models import RelatedPodcast
148 return [r.rel_podcast for r in RelatedPodcast.objects.filter(ref_podcast=self)]
150 def __unicode__(self):
151 return self.title if self.title != '' else self.url
153 class Meta:
154 db_table = 'podcast'
157 class PodcastGroup(models.Model):
158 title = models.CharField(max_length=100, blank=False)
160 def add(self, podcast, membername):
161 if podcast.group == self:
162 podcast.group_member_name = membername
164 elif podcast.group != None:
165 podcast.ungroup()
167 podcast.group = self
168 podcast.group_member_name = membername
169 podcast.save()
171 def podcasts(self):
172 return Podcast.objects.filter(group=self)
174 def __unicode__(self):
175 return self.title
177 class Meta:
178 db_table = 'podcast_groups'
181 class ToplistEntryManager(models.Manager):
183 def get_query_set(self):
184 return super(ToplistEntryManager, self).get_query_set().order_by('-subscriptions')
187 class ToplistEntry(models.Model):
188 podcast = models.ForeignKey(Podcast, null=True)
189 podcast_group = models.ForeignKey(PodcastGroup, null=True)
190 oldplace = models.IntegerField(db_column='old_place')
191 subscriptions = models.IntegerField(db_column='subscription_count')
193 objects = ToplistEntryManager()
196 def get_item(self):
197 if self.podcast:
198 return self.podcast
199 else:
200 return self.podcast_group
202 def get_podcast(self):
204 Returns a podcast which is representative for this toplist-entry
205 If the entry is a non-grouped podcast, it is returned
206 If the entry is a podcast group, one of its podcasts is returned
208 if self.podcast:
209 return self.podcast
210 else:
211 return self.podcast_group.podcasts()[0]
213 def __unicode__(self):
214 return '%s (%s)' % (self.podcast, self.subscriptions)
216 class Meta:
217 db_table = 'toplist'
220 class EpisodeToplistEntryManager(models.Manager):
222 def get_query_set(self):
223 return super(EpisodeToplistEntryManager, self).get_query_set().order_by('-listeners')
226 class EpisodeToplistEntry(models.Model):
227 episode = models.ForeignKey('Episode')
228 listeners = models.PositiveIntegerField()
230 objects = EpisodeToplistEntryManager()
232 def __unicode__(self):
233 return '%s (%s)' % (self.episode, self.listeners)
235 class Meta:
236 db_table = 'episode_toplist'
239 class SuggestionEntryManager(models.Manager):
241 def for_user(self, user):
242 from mygpo.data.models import SuggestionBlacklist
244 suggestions = SuggestionEntry.objects.filter(user=user).order_by('-priority')
246 subscriptions = [x.podcast for x in Subscription.objects.filter(user=user)]
247 suggestions = filter(lambda x: x.podcast not in subscriptions, suggestions)
249 blacklist = [x.podcast for x in SuggestionBlacklist.objects.filter(user=user)]
250 suggestions = filter(lambda x: x.podcast not in blacklist, suggestions)
252 return suggestions
255 class SuggestionEntry(models.Model):
256 podcast = models.ForeignKey(Podcast)
257 user = models.ForeignKey(User)
258 priority = models.IntegerField()
260 objects = SuggestionEntryManager()
262 def __unicode__(self):
263 return '%s (%s)' % (self.podcast, self.priority)
265 class Meta:
266 db_table = 'suggestion'
269 class Episode(models.Model):
270 podcast = models.ForeignKey(Podcast)
271 url = models.URLField(verify_exists=False)
272 title = models.CharField(max_length=100, blank=True)
273 description = models.TextField(null=True, blank=True)
274 link = models.URLField(null=True, blank=True, verify_exists=False)
275 timestamp = models.DateTimeField(null=True, blank=True)
276 author = models.CharField(max_length=100, null=True, blank=True)
277 duration = models.PositiveIntegerField(null=True, blank=True)
278 filesize = models.PositiveIntegerField(null=True, blank=True)
279 language = models.CharField(max_length=10, null=True, blank=True)
280 last_update = models.DateTimeField(auto_now=True)
281 outdated = models.BooleanField(default=False) #set to true after episode hasn't been found in feed
282 mimetype = models.CharField(max_length=30, blank=True, null=True)
284 def number(self):
285 m = re.search('\D*(\d+)\D+', self.title)
286 return m.group(1)
288 def shortname(self):
289 s = self.title
290 s = s.replace(self.podcast.title, '')
291 s = s.replace(self.number(), '')
292 s = re.search('\W*(.+)', s).group(1)
293 s = s.strip()
294 return s
296 def listener_count(self):
297 from mygpo.data.models import Listener
298 return Listener.objects.filter(episode=self).values('user').distinct().count()
300 def __unicode__(self):
301 return '%s (%s)' % (self.shortname(), self.podcast)
303 class Meta:
304 db_table = 'episode'
305 unique_together = ('podcast', 'url')
307 class SyncGroup(models.Model):
309 Devices that should be synced with each other need to be grouped
310 in a SyncGroup.
312 SyncGroups are automatically created by calling
313 device.sync_with(other_device), but can also be created manually.
315 device.sync() synchronizes the device for which the method is called
316 with the other devices in its SyncGroup.
318 user = models.ForeignKey(User)
320 def __unicode__(self):
321 devices = [d.name for d in Device.objects.filter(sync_group=self)]
322 return ', '.join(devices)
324 def devices(self):
325 return Device.objects.filter(sync_group=self)
327 def add(self, device):
328 if device.sync_group == self: return
329 if device.sync_group != None:
330 device.unsync()
332 device.sync_group = self
333 device.save()
335 class Meta:
336 db_table = 'sync_group'
339 class Device(models.Model):
340 user = models.ForeignKey(User)
341 uid = models.SlugField(max_length=50)
342 name = models.CharField(max_length=100, blank=True)
343 type = models.CharField(max_length=10, choices=DEVICE_TYPES)
344 sync_group = models.ForeignKey(SyncGroup, blank=True, null=True)
345 deleted = models.BooleanField(default=False)
347 def __unicode__(self):
348 return self.name if self.name else _('Unnamed Device (%s)' % self.uid)
350 def get_subscriptions(self):
351 self.sync()
352 return Subscription.objects.filter(device=self)
354 def sync(self):
355 for s in self.get_sync_actions():
356 try:
357 SubscriptionAction.objects.create(device=self, podcast=s.podcast, action=s.action)
358 except Exception, e:
359 log('Error adding subscription action: %s (device %s, podcast %s, action %s)' % (str(e), repr(self), repr(s.podcast), repr(s.action)))
361 def sync_targets(self):
363 returns all Devices and SyncGroups that can be used as a parameter for self.sync_with()
365 sync_targets = list(Device.objects.filter(user=self.user, sync_group=None, deleted=False).exclude(pk=self.id))
367 sync_groups = SyncGroup.objects.filter(user=self.user)
368 if self.sync_group != None: sync_groups = sync_groups.exclude(pk=self.sync_group.id)
370 sync_targets.extend( list(sync_groups) )
371 return sync_targets
374 def get_sync_actions(self):
376 returns the SyncGroupSubscriptionActions correspond to the
377 SubscriptionActions that need to be saved for the current device
378 to synchronize it with its SyncGroup
380 if self.sync_group == None:
381 return []
383 devices = self.sync_group.devices().exclude(pk=self.id)
385 sync_actions = self.latest_actions()
387 for d in devices:
388 a = d.latest_actions()
389 for s in a.keys():
390 if not sync_actions.has_key(s):
391 if a[s].action == SUBSCRIBE_ACTION:
392 sync_actions[s] = a[s]
393 elif a[s].newer_than(sync_actions[s]) and (sync_actions[s].action != a[s].action):
394 sync_actions[s] = a[s]
396 #remove actions that did not change
397 current_state = self.latest_actions()
398 for podcast in current_state.keys():
399 if podcast in current_state and podcast in sync_actions and sync_actions[podcast] == current_state[podcast]:
400 del sync_actions[podcast]
402 return sync_actions.values()
404 def latest_actions(self):
406 returns the latest action for each podcast
407 that has an action on this device
409 #all podcasts that have an action on this device
410 podcasts = [sa.podcast for sa in SubscriptionAction.objects.filter(device=self)]
411 podcasts = list(set(podcasts)) #remove duplicates
413 actions = {}
414 for p in podcasts:
415 actions[p] = self.latest_action(p)
417 return actions
419 def latest_action(self, podcast):
421 returns the latest action for the given podcast on this device
423 actions = SubscriptionAction.objects.filter(podcast=podcast,device=self).order_by('-timestamp', '-id')
424 if actions.count() == 0:
425 return None
426 else:
427 return actions[0]
429 def sync_with(self, other):
431 set the device to be synchronized with other, which can either be a Device or a SyncGroup.
432 this method places them in the same SyncGroup. get_sync_actions() can
433 then return the SyncGroupSubscriptionActions for brining the device
434 in sync with its group
436 if self.user != other.user:
437 raise ValueError('the devices belong to different users')
439 if isinstance(other, SyncGroup):
440 other.add(self)
441 self.save()
442 return
444 if self.sync_group == other.sync_group and self.sync_group != None:
445 return
447 if self.sync_group != None:
448 if other.sync_group == None:
449 self.sync_group.add(other)
451 else:
452 raise ValueError('the devices are in different sync groups')
454 else:
455 if other.sync_group == None:
456 g = SyncGroup.objects.create(user=self.user)
457 g.add(self)
458 g.add(other)
460 else:
461 oter.sync_group.add(self)
463 def unsync(self):
465 stops synchronizing the device
466 this method removes the device from its SyncGroup. If only one
467 device remains in the SyncGroup, it is removed so the device can
468 be used in other groups.
470 if self.sync_group == None:
471 raise ValueError('the device is not synced')
473 g = self.sync_group
474 self.sync_group = None
475 self.save()
477 devices = Device.objects.filter(sync_group=g)
478 if devices.count() == 1:
479 d = devices[0]
480 d.sync_group = None
481 d.save()
482 g.delete()
484 class Meta:
485 db_table = 'device'
487 class EpisodeAction(models.Model):
488 user = models.ForeignKey(User)
489 episode = models.ForeignKey(Episode)
490 device = models.ForeignKey(Device,null=True)
491 action = models.CharField(max_length=10, choices=EPISODE_ACTION_TYPES)
492 timestamp = models.DateTimeField(default=datetime.now)
493 started = models.IntegerField(null=True, blank=True)
494 playmark = models.IntegerField(null=True, blank=True)
495 total = models.IntegerField(null=True, blank=True)
497 def __unicode__(self):
498 return '%s %s %s' % (self.user, self.action, self.episode)
500 def playmark_time(self):
501 return datetime.fromtimestamp(float(self.playmark))
503 class Meta:
504 db_table = 'episode_log'
507 class Subscription(models.Model):
508 device = models.ForeignKey(Device, primary_key=True)
509 podcast = models.ForeignKey(Podcast)
510 user = models.ForeignKey(User)
511 subscribed_since = models.DateTimeField()
513 def __unicode__(self):
514 return '%s - %s on %s' % (self.device.user, self.podcast, self.device)
516 def get_meta(self):
517 #this is different than get_or_create because it does not necessarily create a new meta-object
518 qs = SubscriptionMeta.objects.filter(user=self.user, podcast=self.podcast)
520 if qs.count() == 0:
521 return SubscriptionMeta(user=self.user, podcast=self.podcast)
522 else:
523 return qs[0]
525 #this method has to be overwritten, if not it tries to delete a view
526 def delete(self):
527 pass
529 class Meta:
530 db_table = 'current_subscription'
531 #not available in Django 1.0 (Debian stable)
532 managed = False
535 class SubscriptionMeta(models.Model):
536 user = models.ForeignKey(User)
537 podcast = models.ForeignKey(Podcast)
538 public = models.BooleanField(default=True)
540 def __unicode__(self):
541 return '%s - %s - %s' % (self.user, self.podcast, self.public)
543 class Meta:
544 db_table = 'subscription'
545 unique_together = ('user', 'podcast')
548 class SubscriptionAction(models.Model):
549 device = models.ForeignKey(Device)
550 podcast = models.ForeignKey(Podcast)
551 action = models.IntegerField(choices=SUBSCRIPTION_ACTION_TYPES)
552 timestamp = models.DateTimeField(blank=True, default=datetime.now)
554 def action_string(self):
555 return 'subscribe' if self.action == SUBSCRIBE_ACTION else 'unsubscribe'
557 def newer_than(self, action):
558 return self.timestamp > action.timestamp
560 def __unicode__(self):
561 return '%s %s %s %s' % (self.device.user, self.device, self.action_string(), self.podcast)
563 class Meta:
564 db_table = 'subscription_log'
565 unique_together = ('device', 'podcast', 'timestamp')
568 class URLSanitizingRule(models.Model):
569 use_podcast = models.BooleanField()
570 use_episode = models.BooleanField()
571 search = models.CharField(max_length=100)
572 search_precompiled = None
573 replace = models.CharField(max_length=100, null=False, blank=True)
574 priority = models.PositiveIntegerField()
575 description = models.TextField(null=False, blank=True)
577 class Meta:
578 db_table = 'sanitizing_rules'
580 def __unicode__(self):
581 return '%s -> %s' % (self.search, self.replace)
584 from mygpo.search.signals import update_podcast_entry, update_podcast_group_entry
585 from django.db.models.signals import post_save, pre_delete
587 post_save.connect(update_podcast_entry, sender=Podcast)
588 pre_delete.connect(update_podcast_entry, sender=Podcast)
590 post_save.connect(update_podcast_group_entry, sender=PodcastGroup)
591 pre_delete.connect(update_podcast_group_entry, sender=PodcastGroup)