[Models] move SubscriptionException to mygpo.users
[mygpo.git] / mygpo / core / models.py
blob6345a98f03f55e8772f9f0c6f58628b549e113c4
1 from __future__ import division
3 import re
4 from random import random
5 from datetime import timedelta
7 from couchdbkit.ext.django.schema import *
8 from restkit.errors import Unauthorized
10 from mygpo.decorators import repeat_on_conflict
11 from mygpo import utils
12 from mygpo.core.proxy import DocumentABCMeta
13 from mygpo.core.slugs import SlugMixin
14 from mygpo.core.oldid import OldIdMixin
16 # make sure this code is executed at startup
17 from mygpo.core.signals import *
20 # default podcast update interval in hours
21 DEFAULT_UPDATE_INTERVAL = 7 * 24
23 # minium podcast update interval in hours
24 MIN_UPDATE_INTERVAL = 5
26 # every podcast should be updated at least once a month
27 MAX_UPDATE_INTERVAL = 24 * 30
30 class MergedIdException(Exception):
31 """ raised when an object is accessed through one of its merged_ids """
33 def __init__(self, obj, current_id):
34 self.obj = obj
35 self.current_id = current_id
38 class Episode(Document, SlugMixin, OldIdMixin):
39 """
40 Represents an Episode. Can only be part of a Podcast
41 """
43 __metaclass__ = DocumentABCMeta
45 title = StringProperty()
46 guid = StringProperty()
47 description = StringProperty(default="")
48 subtitle = StringProperty()
49 content = StringProperty(default="")
50 link = StringProperty()
51 released = DateTimeProperty()
52 author = StringProperty()
53 duration = IntegerProperty()
54 filesize = IntegerProperty()
55 language = StringProperty()
56 last_update = DateTimeProperty()
57 outdated = BooleanProperty(default=False)
58 mimetypes = StringListProperty()
59 merged_ids = StringListProperty()
60 urls = StringListProperty()
61 podcast = StringProperty(required=True)
62 listeners = IntegerProperty()
63 content_types = StringListProperty()
64 flattr_url = StringProperty()
65 created_timestamp = IntegerProperty()
66 license = StringProperty()
70 @property
71 def url(self):
72 return self.urls[0]
74 def __repr__(self):
75 return 'Episode %s' % self._id
79 def get_short_title(self, common_title):
80 if not self.title or not common_title:
81 return None
83 title = self.title.replace(common_title, '').strip()
84 title = re.sub(r'^[\W\d]+', '', title)
85 return title
88 def get_episode_number(self, common_title):
89 if not self.title or not common_title:
90 return None
92 title = self.title.replace(common_title, '').strip()
93 match = re.search(r'^\W*(\d+)', title)
94 if not match:
95 return None
97 return int(match.group(1))
100 def get_ids(self):
101 return set([self._id] + self.merged_ids)
104 @property
105 def needs_update(self):
106 """ Indicates if the object requires an updated from its feed """
107 return not self.title and not self.outdated
109 def __eq__(self, other):
110 if other is None:
111 return False
112 return self._id == other._id
115 def __hash__(self):
116 return hash(self._id)
119 def __unicode__(self):
120 return u'<{cls} {title} ({id})>'.format(cls=self.__class__.__name__,
121 title=self.title, id=self._id)
125 class SubscriberData(DocumentSchema):
126 timestamp = DateTimeProperty()
127 subscriber_count = IntegerProperty()
129 def __eq__(self, other):
130 if not isinstance(other, SubscriberData):
131 return False
133 return (self.timestamp == other.timestamp) and \
134 (self.subscriber_count == other.subscriber_count)
136 def __hash__(self):
137 return hash(frozenset([self.timestamp, self.subscriber_count]))
140 class PodcastSubscriberData(Document):
141 podcast = StringProperty()
142 subscribers = SchemaListProperty(SubscriberData)
145 def __repr__(self):
146 return 'PodcastSubscriberData for Podcast %s (%s)' % (self.podcast, self._id)
149 class Podcast(Document, SlugMixin, OldIdMixin):
151 __metaclass__ = DocumentABCMeta
153 id = StringProperty()
154 title = StringProperty()
155 urls = StringListProperty()
156 description = StringProperty()
157 subtitle = StringProperty()
158 link = StringProperty()
159 last_update = DateTimeProperty()
160 logo_url = StringProperty()
161 author = StringProperty()
162 merged_ids = StringListProperty()
163 group = StringProperty()
164 group_member_name = StringProperty()
165 related_podcasts = StringListProperty()
166 subscribers = SchemaListProperty(SubscriberData)
167 language = StringProperty()
168 content_types = StringListProperty()
169 tags = DictProperty()
170 restrictions = StringListProperty()
171 common_episode_title = StringProperty()
172 new_location = StringProperty()
173 latest_episode_timestamp = DateTimeProperty()
174 episode_count = IntegerProperty()
175 random_key = FloatProperty(default=random)
176 flattr_url = StringProperty()
177 outdated = BooleanProperty(default=False)
178 created_timestamp = IntegerProperty()
179 hub = StringProperty()
180 license = StringProperty()
182 # avg time between podcast updates (eg new episodes) in hours
183 update_interval = IntegerProperty(default=DEFAULT_UPDATE_INTERVAL)
186 def get_podcast_by_id(self, id, current_id=False):
187 if current_id and id != self.get_id():
188 raise MergedIdException(self, self.get_id())
190 return self
193 get_podcast_by_oldid = get_podcast_by_id
194 get_podcast_by_url = get_podcast_by_id
197 def get_id(self):
198 return self.id or self._id
200 def get_ids(self):
201 return set([self.get_id()] + self.merged_ids)
203 @property
204 def display_title(self):
205 return self.title or self.url
207 @property
208 def url(self):
209 return self.urls[0]
212 def get_podcast(self):
213 return self
216 def subscriber_change(self):
217 prev = self.prev_subscriber_count()
218 if prev <= 0:
219 return 0
221 return self.subscriber_count() / prev
224 def subscriber_count(self):
225 if not self.subscribers:
226 return 0
227 return self.subscribers[-1].subscriber_count
230 def prev_subscriber_count(self):
231 if len(self.subscribers) < 2:
232 return 0
233 return self.subscribers[-2].subscriber_count
236 @property
237 def needs_update(self):
238 """ Indicates if the object requires an updated from its feed """
239 return not self.title and not self.outdated
241 @property
242 def next_update(self):
243 return self.last_update + timedelta(hours=self.update_interval)
245 def __hash__(self):
246 return hash(self.get_id())
249 def __repr__(self):
250 if not self._id:
251 return super(Podcast, self).__repr__()
252 elif self.oldid:
253 return '%s %s (%s)' % (self.__class__.__name__, self.get_id(), self.oldid)
254 else:
255 return '%s %s' % (self.__class__.__name__, self.get_id())
258 def save(self):
259 group = getattr(self, 'group', None)
260 if group: # we are part of a PodcastGroup
261 group = PodcastGroup.get(group)
262 podcasts = list(group.podcasts)
264 if not self in podcasts:
265 # the podcast has not been added to the group correctly
266 group.add_podcast(self)
268 else:
269 i = podcasts.index(self)
270 podcasts[i] = self
271 group.podcasts = podcasts
272 group.save()
274 i = podcasts.index(self)
275 podcasts[i] = self
276 group.podcasts = podcasts
277 group.save()
279 else:
280 super(Podcast, self).save()
283 def delete(self):
284 group = getattr(self, 'group', None)
285 if group:
286 group = PodcastGroup.get(group)
287 podcasts = list(group.podcasts)
289 if self in podcasts:
290 i = podcasts.index(self)
291 del podcasts[i]
292 group.podcasts = podcasts
293 group.save()
295 else:
296 super(Podcast, self).delete()
299 def __eq__(self, other):
300 if not self.get_id():
301 return self == other
303 if other is None:
304 return False
306 return self.get_id() == other.get_id()
310 class PodcastGroup(Document, SlugMixin, OldIdMixin):
311 title = StringProperty()
312 podcasts = SchemaListProperty(Podcast)
314 def get_id(self):
315 return self._id
318 def get_podcast_by_id(self, id, current_id=False):
319 for podcast in self.podcasts:
320 if podcast.get_id() == id:
321 return podcast
323 if id in podcast.merged_ids:
324 if current_id:
325 raise MergedIdException(podcast, podcast.get_id())
327 return podcast
330 def get_podcast_by_oldid(self, oldid):
331 for podcast in list(self.podcasts):
332 if podcast.oldid == oldid or oldid in podcast.merged_oldids:
333 return podcast
336 def get_podcast_by_url(self, url):
337 for podcast in self.podcasts:
338 if url in list(podcast.urls):
339 return podcast
342 def subscriber_change(self):
343 prev = self.prev_subscriber_count()
344 if not prev:
345 return 0
347 return self.subscriber_count() / prev
350 def subscriber_count(self):
351 return sum([p.subscriber_count() for p in self.podcasts])
354 def prev_subscriber_count(self):
355 return sum([p.prev_subscriber_count() for p in self.podcasts])
357 @property
358 def display_title(self):
359 return self.title
361 @property
362 def license(self):
363 return utils.first(p.license for p in self.podcasts)
366 @property
367 def needs_update(self):
368 """ Indicates if the object requires an updated from its feed """
369 # A PodcastGroup has been manually created and therefore never
370 # requires an update
371 return False
373 def get_podcast(self):
374 # return podcast with most subscribers (bug 1390)
375 return sorted(self.podcasts, key=Podcast.subscriber_count,
376 reverse=True)[0]
379 @property
380 def logo_url(self):
381 return utils.first(p.logo_url for p in self.podcasts)
383 @logo_url.setter
384 def logo_url(self, value):
385 self.podcasts[0].logo_url = value
388 def add_podcast(self, podcast, member_name):
390 if not self._id:
391 raise ValueError('group has to have an _id first')
393 if not podcast._id:
394 raise ValueError('podcast needs to have an _id first')
396 if not podcast.id:
397 podcast.id = podcast._id
399 podcast.delete()
400 podcast.group = self._id
401 podcast.group_member_name = member_name
402 self.podcasts = sorted(self.podcasts + [podcast],
403 key=Podcast.subscriber_count, reverse=True)
404 self.save()
407 def __repr__(self):
408 if not self._id:
409 return super(PodcastGroup, self).__repr__()
410 elif self.oldid:
411 return '%s %s (%s)' % (self.__class__.__name__, self._id[:10], self.oldid)
412 else:
413 return '%s %s' % (self.__class__.__name__, self._id[:10])