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 _
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
)
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
):
56 returns all public subscriptions to this podcast
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
)
73 def subscription_count(self
):
74 return self
.subscriptions().count()
76 def subscriber_count(self
):
78 Returns the number of public subscriptions to this podcast
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
):
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
98 devices
= Device
.objects
.filter(user
=user
, deleted
=False)
100 subscriptions
= [x
.podcast
for x
in d
.get_subscriptions()]
101 if self
in subscriptions
: continue
104 if not d
.sync_group
in targets
: targets
.append(d
.sync_group
)
111 def group_with(self
, other
, grouptitle
, myname
, othername
):
112 if self
.group
== other
.group
and self
.group
!= None:
115 if self
.group
!= None:
116 if other
.group
== None:
117 self
.group
.add(other
, othername
)
120 raise ValueError('the podcasts are already in different groups')
122 if other
.group
== None:
123 g
= PodcastGroup
.objects
.create(title
=grouptitle
)
125 g
.add(other
, othername
)
131 if self
.group
== None:
132 raise ValueError('the podcast currently isn\'t in any group')
138 podcasts
= Podcast
.objects
.filter(group
=g
)
139 if podcasts
.count() == 1:
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
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:
166 podcast
.group_member_name
= membername
170 return Podcast
.objects
.filter(group
=self
)
172 def __unicode__(self
):
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()
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
209 return self
.podcast_group
.podcasts()[0]
211 def __unicode__(self
):
212 return '%s (%s)' % (self
.podcast
, self
.subscriptions
)
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
)
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
)
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
)
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
282 m
= re
.search('\D*(\d+)\D+', self
.title
)
287 s
= s
.replace(self
.podcast
.title
, '')
288 s
= s
.replace(self
.number(), '')
289 s
= re
.search('\W*(.+)', s
).group(1)
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
)
302 unique_together
= ('podcast', 'url')
304 class SyncGroup(models
.Model
):
306 Devices that should be synced with each other need to be grouped
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
)
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:
329 device
.sync_group
= self
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
):
349 return Subscription
.objects
.filter(device
=self
)
352 for s
in self
.get_sync_actions():
354 SubscriptionAction
.objects
.create(device
=self
, podcast
=s
.podcast
, action
=s
.action
)
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
) )
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:
380 devices
= self
.sync_group
.devices().exclude(pk
=self
.id)
382 sync_actions
= self
.latest_actions()
385 a
= d
.latest_actions()
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
412 actions
[p
] = self
.latest_action(p
)
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:
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
):
441 if self
.sync_group
== other
.sync_group
and self
.sync_group
!= None:
444 if self
.sync_group
!= None:
445 if other
.sync_group
== None:
446 self
.sync_group
.add(other
)
449 raise ValueError('the devices are in different sync groups')
452 if other
.sync_group
== None:
453 g
= SyncGroup
.objects
.create(user
=self
.user
)
458 oter
.sync_group
.add(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')
472 self
.sync_group
= None
475 devices
= Device
.objects
.filter(sync_group
=g
)
476 if devices
.count() == 1:
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
))
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
)
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
)
519 return SubscriptionMeta(user
=self
.user
, podcast
=self
.podcast
)
523 #this method has to be overwritten, if not it tries to delete a view
528 db_table
= 'current_subscription'
529 #not available in Django 1.0 (Debian stable)
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
)
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
)
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)
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
)