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
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
)
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
):
64 returns all public subscriptions to this podcast
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
)
81 def subscription_count(self
):
82 return self
.subscriptions().count()
84 def subscriber_count(self
):
86 Returns the number of public subscriptions to this podcast
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
106 devices
= Device
.objects
.filter(user
=user
, deleted
=False)
108 subscriptions
= [x
.podcast
for x
in d
.get_subscriptions()]
109 if self
in subscriptions
: continue
112 if not d
.sync_group
in targets
: targets
.append(d
.sync_group
)
119 def group_with(self
, other
, grouptitle
, myname
, othername
):
120 if self
.group
== other
.group
and self
.group
!= None:
123 if self
.group
!= None:
124 if other
.group
== None:
125 self
.group
.add(other
, othername
)
128 raise ValueError('the podcasts are already in different groups')
130 if other
.group
== None:
131 g
= PodcastGroup
.objects
.create(title
=grouptitle
)
133 g
.add(other
, othername
)
139 if self
.group
== None:
140 raise ValueError('the podcast currently isn\'t in any group')
146 podcasts
= Podcast
.objects
.filter(group
=g
)
147 if podcasts
.count() == 1:
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
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:
174 podcast
.group_member_name
= membername
178 return Podcast
.objects
.filter(group
=self
)
180 def __unicode__(self
):
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()
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
217 return self
.podcast_group
.podcasts()[0]
219 def __unicode__(self
):
220 return '%s (%s)' % (self
.podcast
, self
.subscriptions
)
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
)
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
)
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
)
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)
291 m
= re
.search('\D*(\d+)\D+', self
.title
)
296 s
= s
.replace(self
.podcast
.title
, '')
297 s
= s
.replace(self
.number(), '')
298 s
= re
.search('\W*(.+)', s
).group(1)
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
)
311 unique_together
= ('podcast', 'url')
313 class SyncGroup(models
.Model
):
315 Devices that should be synced with each other need to be grouped
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
)
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:
338 device
.sync_group
= self
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
):
359 return Subscription
.objects
.filter(device
=self
)
362 for s
in self
.get_sync_actions():
364 SubscriptionAction
.objects
.create(device
=self
, podcast
=s
.podcast
, action
=s
.action
)
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
) )
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:
390 devices
= self
.sync_group
.devices().exclude(pk
=self
.id)
392 sync_actions
= self
.latest_actions()
395 a
= d
.latest_actions()
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
422 actions
[p
] = self
.latest_action(p
)
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:
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
):
451 if self
.sync_group
== other
.sync_group
and self
.sync_group
!= None:
454 if self
.sync_group
!= None:
455 if other
.sync_group
== None:
456 self
.sync_group
.add(other
)
459 raise ValueError('the devices are in different sync groups')
462 if other
.sync_group
== None:
463 g
= SyncGroup
.objects
.create(user
=self
.user
)
468 oter
.sync_group
.add(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')
481 self
.sync_group
= None
484 devices
= Device
.objects
.filter(sync_group
=g
)
485 if devices
.count() == 1:
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
))
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
)
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
)
531 return SubscriptionMeta(user
=self
.user
, podcast
=self
.podcast
)
535 #this method has to be overwritten, if not it tries to delete a view
540 db_table
= 'current_subscription'
541 #not available in Django 1.0 (Debian stable)
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
)
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
={})
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
)
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)
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
)