properly mark deleted users (bug 786)
[mygpo.git] / mygpo / api / models / __init__.py
blob9514f44ae311613338af93d4951cb4d9e38e1712
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)
35 def __unicode__(self):
36 return '%s (%s, %s)' % (self.user.username, self.public_profile, self.generated_id)
38 class Meta:
39 db_table = 'user'
41 class Podcast(models.Model):
42 url = models.URLField(unique=True, verify_exists=False)
43 title = models.CharField(max_length=100, blank=True)
44 description = models.TextField(blank=True, null=True)
45 link = models.URLField(blank=True, null=True, verify_exists=False)
46 last_update = models.DateTimeField(null=True,blank=True)
47 logo_url = models.CharField(max_length=1000,null=True,blank=True)
48 author = models.CharField(max_length=100, null=True, blank=True)
49 language = models.CharField(max_length=10, null=True, blank=True)
50 group = models.ForeignKey('PodcastGroup', null=True)
51 group_member_name = models.CharField(max_length=20, default=None, null=True, blank=False)
53 def subscriptions(self):
54 return Subscription.objects.filter(podcast=self)
56 def subscription_count(self):
57 return self.subscriptions().count()
59 def subscriber_count(self):
60 return self.subscriptions().values('user').distinct().count()
62 def listener_count(self):
63 from mygpo.data.models import Listener
64 return Listener.objects.filter(podcast=self).values('user').distinct().count()
66 def logo_shortname(self):
67 return hashlib.sha1(self.logo_url).hexdigest()
69 def subscribe_targets(self, user):
70 """
71 returns all Devices and SyncGroups on which this podcast can be subsrbied. This excludes all
72 devices/syncgroups on which the podcast is already subscribed
73 """
74 targets = []
76 devices = Device.objects.filter(user=user, deleted=False)
77 for d in devices:
78 subscriptions = [x.podcast for x in d.get_subscriptions()]
79 if self in subscriptions: continue
81 if d.sync_group:
82 if not d.sync_group in targets: targets.append(d.sync_group)
83 else:
84 targets.append(d)
86 return targets
89 def group_with(self, other, grouptitle, myname, othername):
90 if self.group == other.group and self.group != None:
91 return
93 if self.group != None:
94 if other.group == None:
95 self.group.add(other, myname)
97 else:
98 raise ValueError('the podcasts are already in different groups')
99 else:
100 if other.group == None:
101 g = PodcastGroup.objects.create(title=grouptitle)
102 g.add(self, myname)
103 g.add(other, othername)
105 else:
106 oter.group.add(self)
108 def ungroup(self):
109 if self.group == None:
110 raise ValueError('the podcast currently isn\'t in any group')
112 g = self.group
113 self.group = None
114 self.save()
116 podcasts = Podcast.objects.filter(group=g)
117 if podcasts.count() == 1:
118 p = podcasts[0]
119 p.group = None
120 p.save()
123 def __unicode__(self):
124 return self.title if self.title != '' else self.url
126 class Meta:
127 db_table = 'podcast'
130 class PodcastGroup(models.Model):
131 title = models.CharField(max_length=100, blank=False)
133 def add(self, podcast, membername):
134 if podcast.group == self:
135 podcast.group_member_name = membername
137 elif podcast.group != None:
138 podcast.ungroup()
140 podcast.group = self
141 podcast.group_member_name = membername
142 podcast.save()
144 def podcasts(self):
145 return Podcast.objects.filter(group=self)
147 def __unicode__(self):
148 return self.title
150 class Meta:
151 db_table = 'podcast_groups'
154 class ToplistEntry(models.Model):
155 podcast = models.ForeignKey(Podcast, null=True)
156 podcast_group = models.ForeignKey(PodcastGroup, null=True)
157 oldplace = models.IntegerField(db_column='old_place')
158 subscriptions = models.IntegerField(db_column='subscription_count')
160 def get_item(self):
161 if self.podcast:
162 return self.podcast
163 else:
164 return self.podcast_group
166 def get_podcast(self):
168 Returns a podcast which is representative for this toplist-entry
169 If the entry is a non-grouped podcast, it is returned
170 If the entry is a podcast group, one of its podcasts is returned
172 if self.podcast:
173 return self.podcast
174 else:
175 return self.podcast_group.podcasts()[0]
177 def __unicode__(self):
178 return '%s (%s)' % (self.podcast, self.subscriptions)
180 class Meta:
181 db_table = 'toplist'
183 class EpisodeToplistEntry(models.Model):
184 episode = models.ForeignKey('Episode')
185 listeners = models.PositiveIntegerField()
187 def __unicode__(self):
188 return '%s (%s)' % (self.episode, self.listeners)
190 class Meta:
191 db_table = 'episode_toplist'
193 class SuggestionEntry(models.Model):
194 podcast = models.ForeignKey(Podcast)
195 user = models.ForeignKey(User)
196 priority = models.IntegerField()
198 @staticmethod
199 def forUser(user):
200 subscriptions = [x.podcast for x in Subscription.objects.filter(user=user)]
201 suggestions = SuggestionEntry.objects.filter(user=user).order_by('-priority')
202 return [s for s in suggestions if s.podcast not in subscriptions]
204 def __unicode__(self):
205 return '%s (%s)' % (self.podcast, self.priority)
207 class Meta:
208 db_table = 'suggestion'
211 class Episode(models.Model):
212 podcast = models.ForeignKey(Podcast)
213 url = models.URLField(verify_exists=False)
214 title = models.CharField(max_length=100, blank=True)
215 description = models.TextField(null=True, blank=True)
216 link = models.URLField(null=True, blank=True, verify_exists=False)
217 timestamp = models.DateTimeField(null=True, blank=True)
218 author = models.CharField(max_length=100, null=True, blank=True)
219 duration = models.PositiveIntegerField(null=True, blank=True)
220 filesize = models.PositiveIntegerField(null=True, blank=True)
221 language = models.CharField(max_length=10, null=True, blank=True)
222 last_update = models.DateTimeField(auto_now=True)
223 outdated = models.BooleanField(default=False) #set to true after episode hasn't been found in feed
225 def number(self):
226 m = re.search('\D*(\d+)\D+', self.title)
227 return m.group(1)
229 def shortname(self):
230 s = self.title
231 s = s.replace(self.podcast.title, '')
232 s = s.replace(self.number(), '')
233 s = re.search('\W*(.+)', s).group(1)
234 s = s.strip()
235 return s
237 def listener_count(self):
238 from mygpo.data.models import Listener
239 return Listener.objects.filter(episode=self).values('user').distinct().count()
241 def __unicode__(self):
242 return '%s (%s)' % (self.shortname(), self.podcast)
244 class Meta:
245 db_table = 'episode'
246 unique_together = ('podcast', 'url')
248 class SyncGroup(models.Model):
250 Devices that should be synced with each other need to be grouped
251 in a SyncGroup.
253 SyncGroups are automatically created by calling
254 device.sync_with(other_device), but can also be created manually.
256 device.sync() synchronizes the device for which the method is called
257 with the other devices in its SyncGroup.
259 user = models.ForeignKey(User)
261 def __unicode__(self):
262 devices = [d.name for d in Device.objects.filter(sync_group=self)]
263 return ', '.join(devices)
265 def devices(self):
266 return Device.objects.filter(sync_group=self)
268 def add(self, device):
269 if device.sync_group == self: return
270 if device.sync_group != None:
271 device.unsync()
273 device.sync_group = self
274 device.save()
276 class Meta:
277 db_table = 'sync_group'
280 class Device(models.Model):
281 user = models.ForeignKey(User)
282 uid = models.SlugField(max_length=50)
283 name = models.CharField(max_length=100, blank=True)
284 type = models.CharField(max_length=10, choices=DEVICE_TYPES)
285 sync_group = models.ForeignKey(SyncGroup, blank=True, null=True)
286 deleted = models.BooleanField(default=False)
288 def __unicode__(self):
289 return self.name if self.name else _('Unnamed Device (%s)' % self.uid)
291 def get_subscriptions(self):
292 self.sync()
293 return Subscription.objects.filter(device=self)
295 def sync(self):
296 for s in self.get_sync_actions():
297 try:
298 SubscriptionAction.objects.create(device=self, podcast=s.podcast, action=s.action)
299 except Exception, e:
300 log('Error adding subscription action: %s (device %s, podcast %s, action %s)' % (str(e), repr(self), repr(s.podcast), repr(s.action)))
302 def sync_targets(self):
304 returns all Devices and SyncGroups that can be used as a parameter for self.sync_with()
306 sync_targets = list(Device.objects.filter(user=self.user, sync_group=None, deleted=False).exclude(pk=self.id))
308 sync_groups = SyncGroup.objects.filter(user=self.user)
309 if self.sync_group != None: sync_groups = sync_groups.exclude(pk=self.sync_group.id)
311 sync_targets.extend( list(sync_groups) )
312 return sync_targets
315 def get_sync_actions(self):
317 returns the SyncGroupSubscriptionActions correspond to the
318 SubscriptionActions that need to be saved for the current device
319 to synchronize it with its SyncGroup
321 if self.sync_group == None:
322 return []
324 devices = self.sync_group.devices().exclude(pk=self.id)
326 sync_actions = self.latest_actions()
328 for d in devices:
329 a = d.latest_actions()
330 for s in a.keys():
331 if not sync_actions.has_key(s):
332 if a[s].action == SUBSCRIBE_ACTION:
333 sync_actions[s] = a[s]
334 elif a[s].newer_than(sync_actions[s]) and (sync_actions[s].action != a[s].action):
335 sync_actions[s] = a[s]
337 #remove actions that did not change
338 current_state = self.latest_actions()
339 for podcast in current_state.keys():
340 if sync_actions[podcast] == current_state[podcast]:
341 del sync_actions[podcast]
343 return sync_actions.values()
345 def latest_actions(self):
347 returns the latest action for each podcast
348 that has an action on this device
350 #all podcasts that have an action on this device
351 podcasts = [sa.podcast for sa in SubscriptionAction.objects.filter(device=self)]
352 podcasts = list(set(podcasts)) #remove duplicates
354 actions = {}
355 for p in podcasts:
356 actions[p] = self.latest_action(p)
358 return actions
360 def latest_action(self, podcast):
362 returns the latest action for the given podcast on this device
364 actions = SubscriptionAction.objects.filter(podcast=podcast,device=self).order_by('-timestamp', '-id')
365 if actions.count() == 0:
366 return None
367 else:
368 return actions[0]
370 def sync_with(self, other):
372 set the device to be synchronized with other, which can either be a Device or a SyncGroup.
373 this method places them in the same SyncGroup. get_sync_actions() can
374 then return the SyncGroupSubscriptionActions for brining the device
375 in sync with its group
377 if self.user != other.user:
378 raise ValueError('the devices belong to different users')
380 if isinstance(other, SyncGroup):
381 other.add(self)
382 self.save()
383 return
385 if self.sync_group == other.sync_group and self.sync_group != None:
386 return
388 if self.sync_group != None:
389 if other.sync_group == None:
390 self.sync_group.add(other)
392 else:
393 raise ValueError('the devices are in different sync groups')
395 else:
396 if other.sync_group == None:
397 g = SyncGroup.objects.create(user=self.user)
398 g.add(self)
399 g.add(other)
401 else:
402 oter.sync_group.add(self)
404 def unsync(self):
406 stops synchronizing the device
407 this method removes the device from its SyncGroup. If only one
408 device remains in the SyncGroup, it is removed so the device can
409 be used in other groups.
411 if self.sync_group == None:
412 raise ValueError('the device is not synced')
414 g = self.sync_group
415 print g
416 self.sync_group = None
417 self.save()
419 devices = Device.objects.filter(sync_group=g)
420 if devices.count() == 1:
421 d = devices[0]
422 d.sync_group = None
423 d.save()
424 g.delete()
426 class Meta:
427 db_table = 'device'
429 class EpisodeAction(models.Model):
430 user = models.ForeignKey(User)
431 episode = models.ForeignKey(Episode)
432 device = models.ForeignKey(Device,null=True)
433 action = models.CharField(max_length=10, choices=EPISODE_ACTION_TYPES)
434 timestamp = models.DateTimeField(default=datetime.now)
435 playmark = models.IntegerField(null=True, blank=True)
437 def __unicode__(self):
438 return '%s %s %s' % (self.user, self.action, self.episode)
440 def playmark_time(self):
441 return datetime.fromtimestamp(float(self.playmark))
443 class Meta:
444 db_table = 'episode_log'
447 class Subscription(models.Model):
448 device = models.ForeignKey(Device, primary_key=True)
449 podcast = models.ForeignKey(Podcast)
450 user = models.ForeignKey(User)
451 subscribed_since = models.DateTimeField()
453 def __unicode__(self):
454 return '%s - %s on %s' % (self.device.user, self.podcast, self.device)
456 def get_meta(self):
457 #this is different than get_or_create because it does not necessarily create a new meta-object
458 qs = SubscriptionMeta.objects.filter(user=self.user, podcast=self.podcast)
460 if qs.count() == 0:
461 return SubscriptionMeta(user=self.user, podcast=self.podcast)
462 else:
463 return qs[0]
465 #this method has to be overwritten, if not it tries to delete a view
466 def delete(self):
467 pass
469 class Meta:
470 db_table = 'current_subscription'
471 #not available in Django 1.0 (Debian stable)
472 managed = False
475 class SubscriptionMeta(models.Model):
476 user = models.ForeignKey(User)
477 podcast = models.ForeignKey(Podcast)
478 public = models.BooleanField(default=True)
480 def __unicode__(self):
481 return '%s - %s - %s' % (self.user, self.podcast, self.public)
483 class Meta:
484 db_table = 'subscription'
485 unique_together = ('user', 'podcast')
488 class SubscriptionAction(models.Model):
489 device = models.ForeignKey(Device)
490 podcast = models.ForeignKey(Podcast)
491 action = models.IntegerField(choices=SUBSCRIPTION_ACTION_TYPES)
492 timestamp = models.DateTimeField(blank=True, default=datetime.now)
494 def action_string(self):
495 return 'subscribe' if self.action == SUBSCRIBE_ACTION else 'unsubscribe'
497 def newer_than(self, action):
498 return self.timestamp > action.timestamp
500 def __unicode__(self):
501 return '%s %s %s %s' % (self.device.user, self.device, self.action_string(), self.podcast)
503 class Meta:
504 db_table = 'subscription_log'
505 unique_together = ('device', 'podcast', 'timestamp')
508 class URLSanitizingRule(models.Model):
509 use_podcast = models.BooleanField()
510 use_episode = models.BooleanField()
511 search = models.CharField(max_length=100)
512 search_precompiled = None
513 replace = models.CharField(max_length=100, null=False, blank=True)
514 priority = models.PositiveIntegerField()
515 description = models.TextField(null=False, blank=True)
517 class Meta:
518 db_table = 'sanitizing_rules'
520 def __unicode__(self):
521 return '%s -> %s' % (self.search, self.replace)