using precompiled rules for sanitizing-maintenance
[mygpo.git] / mygpo / api / models.py
blob4c4d3bc62d805998eaee17e69a5d0adb7ee2934f
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
24 EPISODE_ACTION_TYPES = (
25 ('download', 'downloaded'),
26 ('play', 'played'),
27 ('delete', 'deleted'),
28 ('new', 'marked new')
31 DEVICE_TYPES = (
32 ('desktop', 'Desktop'),
33 ('laptop', 'Laptop'),
34 ('mobile', 'Mobile'),
35 ('server', 'Server'),
36 ('other', 'Other')
40 SUBSCRIBE_ACTION = 1
41 UNSUBSCRIBE_ACTION = -1
43 SUBSCRIPTION_ACTION_TYPES = (
44 (SUBSCRIBE_ACTION, 'subscribed'),
45 (UNSUBSCRIBE_ACTION, 'unsubscribed')
48 class UserProfile(models.Model):
49 user = models.ForeignKey(User, unique=True, db_column='user_ptr_id')
51 public_profile = models.BooleanField(default=True)
52 generated_id = models.BooleanField(default=False)
54 def __unicode__(self):
55 return '%s (%s, %s)' % (self.user.username, self.public_profile, self.generated_id)
57 class Meta:
58 db_table = 'user'
60 class Podcast(models.Model):
61 url = models.URLField(unique=True)
62 title = models.CharField(max_length=100, blank=True)
63 description = models.TextField(blank=True, null=True)
64 link = models.URLField(blank=True, null=True)
65 last_update = models.DateTimeField(null=True,blank=True)
66 logo_url = models.CharField(max_length=1000,null=True,blank=True)
68 def subscriptions(self):
69 return Subscription.objects.filter(podcast=self)
71 def subscription_count(self):
72 return self.subscriptions().count()
74 def logo_shortname(self):
75 return hashlib.sha1(self.logo_url).hexdigest()
77 def subscribe_targets(self, user):
78 """
79 returns all Devices and SyncGroups on which this podcast can be subsrbied. This excludes all
80 devices/syncgroups on which the podcast is already subscribed
81 """
82 targets = []
84 devices = Device.objects.filter(user=user)
85 for d in devices:
86 subscriptions = [x.podcast for x in d.get_subscriptions()]
87 if self in subscriptions: continue
89 if d.sync_group:
90 if not d.sync_group in targets: targets.append(d.sync_group)
91 else:
92 targets.append(d)
94 return targets
97 def __unicode__(self):
98 return self.title if self.title != '' else self.url
100 class Meta:
101 db_table = 'podcast'
104 class ToplistEntry(models.Model):
105 podcast = models.ForeignKey(Podcast)
106 oldplace = models.IntegerField(db_column='old_place')
107 subscriptions = models.IntegerField(db_column='subscription_count')
109 def __unicode__(self):
110 return '%s (%s)' % (self.podcast, self.subscriptions)
112 class Meta:
113 db_table = 'toplist'
115 class SuggestionEntry(models.Model):
116 podcast = models.ForeignKey(Podcast)
117 user = models.ForeignKey(User)
118 priority = models.IntegerField()
120 @staticmethod
121 def forUser(user):
122 subscriptions = [x.podcast for x in Subscription.objects.filter(user=user)]
123 suggestions = SuggestionEntry.objects.filter(user=user).order_by('-priority')
124 return [s for s in suggestions if s.podcast not in subscriptions]
126 def __unicode__(self):
127 return '%s (%s)' % (self.podcast, self.priority)
129 class Meta:
130 db_table = 'suggestion'
133 class Episode(models.Model):
134 podcast = models.ForeignKey(Podcast)
135 url = models.URLField(unique=True)
136 title = models.CharField(max_length=100, blank=True)
137 description = models.TextField(null=True, blank=True)
138 link = models.URLField(null=True, blank=True)
139 timestamp = models.DateTimeField(null=True, blank=True)
141 def __unicode__(self):
142 return '%s (%s)' % (self.title, self.podcast)
144 class Meta:
145 db_table = 'episode'
147 class SyncGroup(models.Model):
149 Devices that should be synced with each other need to be grouped
150 in a SyncGroup.
152 SyncGroups are automatically created by calling
153 device.sync_with(other_device), but can also be created manually.
155 device.sync() synchronizes the device for which the method is called
156 with the other devices in its SyncGroup.
158 user = models.ForeignKey(User)
160 def __unicode__(self):
161 devices = [d.name for d in Device.objects.filter(sync_group=self)]
162 return ', '.join(devices)
164 def devices(self):
165 return Device.objects.filter(sync_group=self)
167 def add(self, device):
168 if device.sync_group == self: return
169 if device.sync_group != None:
170 device.unsync()
172 device.sync_group = self
173 device.save()
175 class Meta:
176 db_table = 'sync_group'
179 class Device(models.Model):
180 user = models.ForeignKey(User)
181 uid = models.SlugField(max_length=50)
182 name = models.CharField(max_length=100, blank=True)
183 type = models.CharField(max_length=10, choices=DEVICE_TYPES)
184 sync_group = models.ForeignKey(SyncGroup, blank=True, null=True)
186 def __unicode__(self):
187 return self.name if self.name else _('Unnamed Device (%s)' % self.uid)
189 def get_subscriptions(self):
190 self.sync()
191 return Subscription.objects.filter(device=self)
193 def sync(self):
194 for s in self.get_sync_actions():
195 SubscriptionAction.objects.create(device=self, podcast=s.podcast, timestamp=s.timestamp, action=s.action)
197 def sync_targets(self):
199 returns all Devices and SyncGroups that can be used as a parameter for self.sync_with()
201 sync_targets = list(Device.objects.filter(user=self.user, sync_group=None).exclude(pk=self.id))
203 sync_groups = SyncGroup.objects.filter(user=self.user)
204 if self.sync_group != None: sync_groups = sync_groups.exclude(pk=self.sync_group.id)
206 sync_targets.extend( list(sync_groups) )
207 return sync_targets
210 def get_sync_actions(self):
212 returns the SyncGroupSubscriptionActions correspond to the
213 SubscriptionActions that need to be saved for the current device
214 to synchronize it with its SyncGroup
216 if self.sync_group == None:
217 return []
219 devices = self.sync_group.devices().exclude(pk=self.id)
221 sync_actions = self.latest_actions()
223 for d in devices:
224 a = d.latest_actions()
225 for s in a.keys():
226 if not sync_actions.has_key(s):
227 if a[s].action == SUBSCRIBE_ACTION:
228 sync_actions[s] = a[s]
229 elif a[s].newer_than(sync_actions[s]) and (sync_actions[s].action != a[s].action):
230 sync_actions[s] = a[s]
232 #remove actions that did not change
233 current_state = self.latest_actions()
234 for podcast in current_state.keys():
235 if sync_actions[podcast] == current_state[podcast]:
236 del sync_actions[podcast]
238 return sync_actions.values()
240 def latest_actions(self):
242 returns the latest action for each podcast
243 that has an action on this device
245 #all podcasts that have an action on this device
246 podcasts = [sa.podcast for sa in SubscriptionAction.objects.filter(device=self)]
247 podcasts = list(set(podcasts)) #remove duplicates
249 actions = {}
250 for p in podcasts:
251 actions[p] = self.latest_action(p)
253 return actions
255 def latest_action(self, podcast):
257 returns the latest action for the given podcast on this device
259 actions = SubscriptionAction.objects.filter(podcast=podcast,device=self).order_by('-timestamp', '-id')
260 if len(actions) == 0:
261 return None
262 else:
263 return actions[0]
265 def sync_with(self, other):
267 set the device to be synchronized with other, which can either be a Device or a SyncGroup.
268 this method places them in the same SyncGroup. get_sync_actions() can
269 then return the SyncGroupSubscriptionActions for brining the device
270 in sync with its group
272 if self.user != other.user:
273 raise ValueError('the devices belong to different users')
275 if isinstance(other, SyncGroup):
276 other.add(self)
277 self.save()
278 return
280 if self.sync_group == other.sync_group and self.sync_group != None:
281 return
283 if self.sync_group != None:
284 if other.sync_group == None:
285 self.sync_group.add(other)
287 else:
288 raise ValueError('the devices are in different sync groups')
290 else:
291 if other.sync_group == None:
292 g = SyncGroup.objects.create(user=self.user)
293 g.add(self)
294 g.add(other)
296 else:
297 oter.sync_group.add(self)
299 def unsync(self):
301 stops synchronizing the device
302 this method removes the device from its SyncGroup. If only one
303 device remains in the SyncGroup, it is removed so the device can
304 be used in other groups.
306 if self.sync_group == None:
307 raise ValueError('the device is not synced')
309 g = self.sync_group
310 print g
311 self.sync_group = None
312 self.save()
314 devices = Device.objects.filter(sync_group=g)
315 if devices.count() == 1:
316 d = devices[0]
317 d.sync_group = None
318 d.save()
319 g.delete()
321 class Meta:
322 db_table = 'device'
324 class EpisodeAction(models.Model):
325 user = models.ForeignKey(User)
326 episode = models.ForeignKey(Episode)
327 device = models.ForeignKey(Device,null=True)
328 action = models.CharField(max_length=10, choices=EPISODE_ACTION_TYPES)
329 timestamp = models.DateTimeField(default=datetime.now)
330 playmark = models.IntegerField(null=True, blank=True)
332 def __unicode__(self):
333 return '%s %s %s' % (self.user, self.action, self.episode)
335 class Meta:
336 db_table = 'episode_log'
337 unique_together = ('user', 'episode', 'timestamp')
340 class Subscription(models.Model):
341 device = models.ForeignKey(Device, primary_key=True)
342 podcast = models.ForeignKey(Podcast)
343 user = models.ForeignKey(User)
344 subscribed_since = models.DateTimeField()
346 def __unicode__(self):
347 return '%s - %s on %s' % (self.device.user, self.podcast, self.device)
349 def get_meta(self):
350 #this is different than get_or_create because it does not necessarily create a new meta-object
351 try:
352 return SubscriptionMeta.objects.get(user=self.user, podcast=self.podcast)
353 except SubscriptionMeta.DoesNotExist:
354 return SubscriptionMeta(user=self.user, podcast=self.podcast)
356 class Meta:
357 db_table = 'current_subscription'
358 #not available in Django 1.0 (Debian stable)
359 managed = False
362 class SubscriptionMeta(models.Model):
363 user = models.ForeignKey(User)
364 podcast = models.ForeignKey(Podcast)
365 public = models.BooleanField(default=True)
367 def __unicode__(self):
368 return '%s - %s - %s' % (self.user, self.podcast, self.public)
370 class Meta:
371 db_table = 'subscription'
372 unique_together = ('user', 'podcast')
375 class SubscriptionAction(models.Model):
376 device = models.ForeignKey(Device)
377 podcast = models.ForeignKey(Podcast)
378 action = models.IntegerField(choices=SUBSCRIPTION_ACTION_TYPES)
379 timestamp = models.DateTimeField(blank=True, default=datetime.now)
381 def action_string(self):
382 return 'subscribe' if self.action == SUBSCRIBE_ACTION else 'unsubscribe'
384 def newer_than(self, action):
385 if (self.timestamp == action.timestamp): return self.id > action.id
386 return self.timestamp > action.timestamp
388 def __unicode__(self):
389 return '%s %s %s %s' % (self.device.user, self.device, self.action_string(), self.podcast)
391 class Meta:
392 db_table = 'subscription_log'
393 unique_together = ('device', 'podcast', 'timestamp')
395 class Rating(models.Model):
396 target = models.CharField(max_length=15)
397 user = models.ForeignKey(User)
398 rating = models.IntegerField()
399 timestamp = models.DateTimeField(default=datetime.now)
401 class Meta:
402 db_table = 'ratings'
404 def __unicode__(self):
405 return '%s rates %s as %s on %s' % (self.user, self.target, self.rating, self.timestamp)
407 class URLSanitizingRule(models.Model):
408 use_podcast = models.BooleanField()
409 use_episode = models.BooleanField()
410 search = models.CharField(max_length=100)
411 search_precompiled = None
412 replace = models.CharField(max_length=100, null=False, blank=True)
413 priority = models.PositiveIntegerField()
414 description = models.TextField(null=False, blank=True)
416 class Meta:
417 db_table = 'sanitizing_rules'
419 def __unicode__(self):
420 return '%s -> %s' % (self.search, self.replace)