fix imports of mygpo.couch
[mygpo.git] / mygpo / core / models.py
blob98f1a54474e4ecc0b3353061e0cfc68d58f035d6
1 from __future__ import division
3 import hashlib
4 import os.path
5 import re
6 from datetime import datetime
7 from dateutil import parser
8 from random import randint, random
10 from couchdbkit.ext.django.schema import *
11 from restkit.errors import Unauthorized
13 from django.conf import settings
14 from django.core.urlresolvers import reverse
16 from mygpo.decorators import repeat_on_conflict
17 from mygpo import utils
18 from mygpo.couch import get_main_database
19 from mygpo.core.proxy import DocumentABCMeta
20 from mygpo.core.slugs import SlugMixin
21 from mygpo.core.oldid import OldIdMixin
22 from mygpo.web.logo import CoverArt
25 class SubscriptionException(Exception):
26 pass
29 class MergedIdException(Exception):
30 """ raised when an object is accessed through one of its merged_ids """
32 def __init__(self, obj, current_id):
33 self.obj = obj
34 self.current_id = current_id
37 class Episode(Document, SlugMixin, OldIdMixin):
38 """
39 Represents an Episode. Can only be part of a Podcast
40 """
42 __metaclass__ = DocumentABCMeta
44 title = StringProperty()
45 description = StringProperty()
46 link = StringProperty()
47 released = DateTimeProperty()
48 author = StringProperty()
49 duration = IntegerProperty()
50 filesize = IntegerProperty()
51 language = StringProperty()
52 last_update = DateTimeProperty()
53 outdated = BooleanProperty(default=False)
54 mimetypes = StringListProperty()
55 merged_ids = StringListProperty()
56 urls = StringListProperty()
57 podcast = StringProperty(required=True)
58 listeners = IntegerProperty()
59 content_types = StringListProperty()
62 @classmethod
63 def get(cls, id, current_id=False):
64 r = cls.view('episodes/by_id',
65 key=id,
66 include_docs=True,
69 if not r:
70 return None
72 obj = r.one()
73 if current_id and obj._id != id:
74 raise MergedIdException(obj, obj._id)
76 return obj
79 @classmethod
80 def get_multi(cls, ids):
81 return cls.view('episodes/by_id',
82 include_docs=True,
83 keys=ids
87 @classmethod
88 def for_oldid(self, oldid):
89 oldid = int(oldid)
90 r = Episode.view('episodes/by_oldid', key=oldid, limit=1, include_docs=True)
91 return r.one() if r else None
94 @classmethod
95 def for_slug(cls, podcast_id, slug):
96 r = cls.view('episodes/by_slug',
97 key = [podcast_id, slug],
98 include_docs = True
100 return r.first() if r else None
103 @classmethod
104 def for_podcast_url(cls, podcast_url, episode_url, create=False):
105 podcast = Podcast.for_url(podcast_url, create=create)
107 if not podcast: # podcast does not exist and should not be created
108 return None
110 return cls.for_podcast_id_url(podcast.get_id(), episode_url, create)
113 @classmethod
114 def for_podcast_id_url(cls, podcast_id, episode_url, create=False):
115 r = cls.view('episodes/by_podcast_url',
116 key = [podcast_id, episode_url],
117 include_docs = True,
118 reduce = False,
121 if r:
122 return r.first()
124 if create:
125 episode = Episode()
126 episode.podcast = podcast_id
127 episode.urls = [episode_url]
128 episode.save()
129 return episode
131 return None
134 @classmethod
135 def for_slug_id(cls, p_slug_id, e_slug_id):
136 """ Returns the Episode for Podcast Slug/Id and Episode Slug/Id """
138 # The Episode-Id is unique, so take that
139 if utils.is_couchdb_id(e_slug_id):
140 return cls.get(e_slug_id)
142 # If we search using a slug, we need the Podcast's Id
143 if utils.is_couchdb_id(p_slug_id):
144 p_id = p_slug_id
145 else:
146 podcast = Podcast.for_slug_id(p_slug_id)
148 if podcast is None:
149 return None
151 p_id = podcast.get_id()
153 return cls.for_slug(p_id, e_slug_id)
156 def get_user_state(self, user):
157 from mygpo.users.models import EpisodeUserState
158 return EpisodeUserState.for_user_episode(user, self)
161 def get_all_states(self):
162 from mygpo.users.models import EpisodeUserState
163 r = EpisodeUserState.view('episode_states/by_podcast_episode',
164 startkey = [self.podcast, self._id, None],
165 endkey = [self.podcast, self._id, {}],
166 include_docs=True)
167 return iter(r)
170 @property
171 def url(self):
172 return self.urls[0]
174 def __repr__(self):
175 return 'Episode %s' % self._id
178 def listener_count(self, start=None, end={}):
179 """ returns the number of users that have listened to this episode """
181 from mygpo.users.models import EpisodeUserState
182 r = EpisodeUserState.view('listeners/by_episode',
183 startkey = [self._id, start],
184 endkey = [self._id, end],
185 reduce = True,
186 group = True,
187 group_level = 2
189 return r.first()['value'] if r else 0
192 def listener_count_timespan(self, start=None, end={}):
193 """ returns (date, listener-count) tuples for all days w/ listeners """
195 if isinstance(start, datetime):
196 start = start.isoformat()
198 if isinstance(end, datetime):
199 end = end.isoformat()
201 from mygpo.users.models import EpisodeUserState
202 r = EpisodeUserState.view('listeners/by_episode',
203 startkey = [self._id, start],
204 endkey = [self._id, end],
205 reduce = True,
206 group = True,
207 group_level = 3,
210 for res in r:
211 date = parser.parse(res['key'][1]).date()
212 listeners = res['value']
213 yield (date, listeners)
216 def get_short_title(self, common_title):
217 if not self.title or not common_title:
218 return None
220 title = self.title.replace(common_title, '').strip()
221 title = re.sub(r'^[\W\d]+', '', title)
222 return title
225 def get_episode_number(self, common_title):
226 if not self.title or not common_title:
227 return None
229 title = self.title.replace(common_title, '').strip()
230 match = re.search(r'^\W*(\d+)', title)
231 if not match:
232 return None
234 return int(match.group(1))
237 def get_ids(self):
238 return set([self._id] + self.merged_ids)
241 @classmethod
242 def count(cls):
243 r = cls.view('episodes/by_podcast',
244 reduce = True,
245 stale = 'update_after',
247 return r.one()['value'] if r else 0
250 @classmethod
251 def all(cls):
252 return utils.multi_request_view(cls, 'episodes/by_podcast',
253 reduce = False,
254 include_docs = True,
255 stale = 'update_after',
258 def __eq__(self, other):
259 if other == None:
260 return False
261 return self._id == other._id
264 def __hash__(self):
265 return hash(self._id)
268 def __str__(self):
269 return '<{cls} {title} ({id})>'.format(cls=self.__class__.__name__,
270 title=self.title, id=self._id)
272 __repr__ = __str__
275 class SubscriberData(DocumentSchema):
276 timestamp = DateTimeProperty()
277 subscriber_count = IntegerProperty()
279 def __eq__(self, other):
280 if not isinstance(other, SubscriberData):
281 return False
283 return (self.timestamp == other.timestamp) and \
284 (self.subscriber_count == other.subscriber_count)
286 def __hash__(self):
287 return hash(frozenset([self.timestamp, self.subscriber_count]))
290 class PodcastSubscriberData(Document):
291 podcast = StringProperty()
292 subscribers = SchemaListProperty(SubscriberData)
294 @classmethod
295 def for_podcast(cls, id):
296 r = cls.view('podcasts/subscriber_data', key=id, include_docs=True)
297 if r:
298 return r.first()
300 data = PodcastSubscriberData()
301 data.podcast = id
302 return data
304 def __repr__(self):
305 return 'PodcastSubscriberData for Podcast %s (%s)' % (self.podcast, self._id)
308 class Podcast(Document, SlugMixin, OldIdMixin):
310 __metaclass__ = DocumentABCMeta
312 id = StringProperty()
313 title = StringProperty()
314 urls = StringListProperty()
315 description = StringProperty()
316 link = StringProperty()
317 last_update = DateTimeProperty()
318 logo_url = StringProperty()
319 author = StringProperty()
320 merged_ids = StringListProperty()
321 group = StringProperty()
322 group_member_name = StringProperty()
323 related_podcasts = StringListProperty()
324 subscribers = SchemaListProperty(SubscriberData)
325 language = StringProperty()
326 content_types = StringListProperty()
327 tags = DictProperty()
328 restrictions = StringListProperty()
329 common_episode_title = StringProperty()
330 new_location = StringProperty()
331 latest_episode_timestamp = DateTimeProperty()
332 episode_count = IntegerProperty()
333 random_key = FloatProperty(default=random)
336 @classmethod
337 def get(cls, id, current_id=False):
338 r = cls.view('podcasts/by_id',
339 key=id,
340 classes=[Podcast, PodcastGroup],
341 include_docs=True,
344 if not r:
345 return None
347 podcast_group = r.first()
348 return podcast_group.get_podcast_by_id(id, current_id)
351 @classmethod
352 def for_slug(cls, slug):
353 r = cls.view('podcasts/by_slug',
354 startkey = [slug, None],
355 endkey = [slug, {}],
356 include_docs = True,
357 wrap_doc = False,
360 if not r:
361 return None
363 res = r.first()
364 doc = res['doc']
365 if doc['doc_type'] == 'Podcast':
366 return Podcast.wrap(doc)
367 else:
368 pid = res['key'][1]
369 pg = PodcastGroup.wrap(doc)
370 return pg.get_podcast_by_id(pid)
373 @classmethod
374 def for_slug_id(cls, slug_id):
375 """ Returns the Podcast for either an CouchDB-ID for a Slug """
377 if utils.is_couchdb_id(slug_id):
378 return cls.get(slug_id)
379 else:
380 return cls.for_slug(slug_id)
383 @classmethod
384 def get_multi(cls, ids):
385 r = cls.view('podcasts/by_id',
386 keys = ids,
387 include_docs = True,
388 wrap_doc = False
391 for res in r:
392 if res['doc']['doc_type'] == 'Podcast':
393 yield Podcast.wrap(res['doc'])
394 else:
395 pg = PodcastGroup.wrap(res['doc'])
396 id = res['key']
397 yield pg.get_podcast_by_id(id)
400 @classmethod
401 def for_oldid(cls, oldid):
402 oldid = int(oldid)
403 r = cls.view('podcasts/by_oldid',
404 key=long(oldid),
405 classes=[Podcast, PodcastGroup],
406 include_docs=True
409 if not r:
410 return None
412 podcast_group = r.first()
413 return podcast_group.get_podcast_by_oldid(oldid)
416 @classmethod
417 def for_url(cls, url, create=False):
418 r = cls.view('podcasts/by_url',
419 key=url,
420 classes=[Podcast, PodcastGroup],
421 include_docs=True
424 if r:
425 podcast_group = r.first()
426 return podcast_group.get_podcast_by_url(url)
428 if create:
429 podcast = cls()
430 podcast.urls = [url]
431 podcast.save()
432 return podcast
434 return None
437 @classmethod
438 def random(cls, language='', chunk_size=5):
439 """ Returns an iterator of random podcasts
441 optionaly a language code can be specified. If given the podcasts will
442 be restricted to this language. chunk_size determines how many podcasts
443 will be fetched at once """
445 while True:
446 rnd = random()
447 res = cls.view('podcasts/random',
448 startkey = [language, rnd],
449 include_docs = True,
450 limit = chunk_size,
451 stale = 'ok',
452 wrap_doc = False,
455 if not res:
456 break
458 for r in res:
459 obj = r['doc']
460 if obj['doc_type'] == 'Podcast':
461 yield Podcast.wrap(obj)
463 elif obj['doc_type'] == 'PodcastGroup':
464 yield PodcastGroup.wrap(obj)
467 @classmethod
468 def by_last_update(cls):
469 res = cls.view('podcasts/by_last_update',
470 include_docs = True,
471 stale = 'update_after',
472 wrap_doc = False,
475 for r in res:
476 obj = r['doc']
477 if obj['doc_type'] == 'Podcast':
478 yield Podcast.wrap(obj)
480 else:
481 pid = r[u'key'][1]
482 pg = PodcastGroup.wrap(obj)
483 podcast = pg.get_podcast_by_id(pid)
484 yield podcast
487 @classmethod
488 def for_language(cls, language, **kwargs):
490 res = cls.view('podcasts/by_language',
491 startkey = [language, None],
492 endkey = [language, {}],
493 include_docs = True,
494 reduce = False,
495 stale = 'update_after',
496 wrap_doc = False,
497 **kwargs
500 for r in res:
501 obj = r['doc']
502 if obj['doc_type'] == 'Podcast':
503 yield Podcast.wrap(obj)
505 else:
506 pid = r[u'key'][1]
507 pg = PodcastGroup.wrap(obj)
508 podcast = pg.get_podcast_by_id(pid)
509 yield podcast
512 @classmethod
513 def count(cls):
514 # TODO: fix number calculation
515 r = cls.view('podcasts/by_id',
516 limit = 0,
517 stale = 'update_after',
519 return r.total_rows
522 def get_podcast_by_id(self, id, current_id=False):
523 if current_id and id != self.get_id():
524 raise MergedIdException(self, self.get_id())
526 return self
529 get_podcast_by_oldid = get_podcast_by_id
530 get_podcast_by_url = get_podcast_by_id
533 def get_id(self):
534 return self.id or self._id
536 def get_ids(self):
537 return set([self.get_id()] + self.merged_ids)
539 @property
540 def display_title(self):
541 return self.title or self.url
544 def group_with(self, other, grouptitle, myname, othername):
546 if self.group and (self.group == other.group):
547 # they are already grouped
548 return
550 group1 = PodcastGroup.get(self.group) if self.group else None
551 group2 = PodcastGroup.get(other.group) if other.group else None
553 if group1 and group2:
554 raise ValueError('both podcasts already are in different groups')
556 elif not (group1 or group2):
557 group = PodcastGroup(title=grouptitle)
558 group.save()
559 group.add_podcast(self, myname)
560 group.add_podcast(other, othername)
561 return group
563 elif group1:
564 group1.add_podcast(other, othername)
565 return group1
567 else:
568 group2.add_podcast(self, myname)
569 return group2
573 def get_episodes(self, since=None, until={}, **kwargs):
575 if kwargs.get('descending', False):
576 since, until = until, since
578 if isinstance(since, datetime):
579 since = since.isoformat()
581 if isinstance(until, datetime):
582 until = until.isoformat()
584 res = Episode.view('episodes/by_podcast',
585 startkey = [self.get_id(), since],
586 endkey = [self.get_id(), until],
587 include_docs = True,
588 reduce = False,
589 **kwargs
592 return iter(res)
595 def get_episode_count(self, since=None, until={}, **kwargs):
597 # use stored episode count for better performance
598 if getattr(self, 'episode_count', None) is not None:
599 return self.episode_count
601 if kwargs.get('descending', False):
602 since, until = until, since
604 if isinstance(since, datetime):
605 since = since.isoformat()
607 if isinstance(until, datetime):
608 until = until.isoformat()
610 res = Episode.view('episodes/by_podcast',
611 startkey = [self.get_id(), since],
612 endkey = [self.get_id(), until],
613 reduce = True,
614 group_level = 1,
615 **kwargs
618 return res.one()['value']
621 def get_common_episode_title(self, num_episodes=100):
623 if self.common_episode_title:
624 return self.common_episode_title
626 episodes = self.get_episodes(descending=True, limit=num_episodes)
628 # We take all non-empty titles
629 titles = filter(None, (e.title for e in episodes))
630 # get the longest common substring
631 common_title = utils.longest_substr(titles)
633 # but consider only the part up to the first number. Otherwise we risk
634 # removing part of the number (eg if a feed contains episodes 100-199)
635 common_title = re.search(r'^\D*', common_title).group(0)
637 if len(common_title.strip()) < 2:
638 return None
640 return common_title
643 def get_latest_episode(self):
644 # since = 1 ==> has a timestamp
645 episodes = self.get_episodes(since=1, descending=True, limit=1)
646 return next(episodes, None)
649 def get_episode_before(self, episode):
650 if not episode.released:
651 return None
653 prevs = self.get_episodes(until=episode.released, descending=True,
654 limit=1)
656 try:
657 return next(prevs)
658 except StopIteration:
659 return None
662 def get_episode_after(self, episode):
663 if not episode.released:
664 return None
666 nexts = self.get_episodes(since=episode.released, limit=1)
668 try:
669 return next(nexts)
670 except StopIteration:
671 return None
674 def get_episode_for_slug(self, slug):
675 return Episode.for_slug(self.get_id(), slug)
678 @property
679 def url(self):
680 return self.urls[0]
683 def get_podcast(self):
684 return self
687 def get_logo_url(self, size):
688 if self.logo_url:
689 filename = hashlib.sha1(self.logo_url).hexdigest()
690 else:
691 filename = 'podcast-%d.png' % (hash(self.title) % 5, )
693 prefix = CoverArt.get_prefix(filename)
695 return reverse('logo', args=[size, prefix, filename])
698 def subscriber_change(self):
699 prev = self.prev_subscriber_count()
700 if prev <= 0:
701 return 0
703 return self.subscriber_count() / prev
706 def subscriber_count(self):
707 if not self.subscribers:
708 return 0
709 return self.subscribers[-1].subscriber_count
712 def prev_subscriber_count(self):
713 if len(self.subscribers) < 2:
714 return 0
715 return self.subscribers[-2].subscriber_count
718 def get_user_state(self, user):
719 from mygpo.users.models import PodcastUserState
720 return PodcastUserState.for_user_podcast(user, self)
723 def get_all_states(self):
724 from mygpo.users.models import PodcastUserState
725 return PodcastUserState.view('podcast_states/by_podcast',
726 startkey = [self.get_id(), None],
727 endkey = [self.get_id(), {}],
728 include_docs=True)
730 def get_all_subscriber_data(self):
731 subdata = PodcastSubscriberData.for_podcast(self.get_id())
732 return sorted(self.subscribers + subdata.subscribers,
733 key=lambda s: s.timestamp)
736 @repeat_on_conflict()
737 def subscribe(self, user, device):
738 state = self.get_user_state(user)
739 state.subscribe(device)
740 try:
741 state.save()
742 except Unauthorized as ex:
743 raise SubscriptionException(ex)
746 @repeat_on_conflict()
747 def unsubscribe(self, user, device):
748 state = self.get_user_state(user)
749 state.unsubscribe(device)
750 try:
751 state.save()
752 except Unauthorized as ex:
753 raise SubscriptionException(ex)
756 def subscribe_targets(self, user):
758 returns all Devices and SyncGroups on which this podcast can be subsrbied. This excludes all
759 devices/syncgroups on which the podcast is already subscribed
761 targets = []
763 subscriptions_by_devices = user.get_subscriptions_by_device()
765 for group in user.get_grouped_devices():
767 if group.is_synced:
769 dev = group.devices[0]
771 if not self.get_id() in subscriptions_by_devices[dev.id]:
772 targets.append(group.devices)
774 else:
775 for device in group.devices:
776 if not self.get_id() in subscriptions_by_devices[device.id]:
777 targets.append(device)
779 return targets
782 def listener_count(self):
783 """ returns the number of users that have listened to this podcast """
785 from mygpo.users.models import EpisodeUserState
786 r = EpisodeUserState.view('listeners/by_podcast',
787 startkey = [self.get_id(), None],
788 endkey = [self.get_id(), {}],
789 group = True,
790 group_level = 1,
791 reduce = True,
793 return r.first()['value'] if r else 0
796 def listener_count_timespan(self, start=None, end={}):
797 """ returns (date, listener-count) tuples for all days w/ listeners """
799 if isinstance(start, datetime):
800 start = start.isoformat()
802 if isinstance(end, datetime):
803 end = end.isoformat()
805 from mygpo.users.models import EpisodeUserState
806 r = EpisodeUserState.view('listeners/by_podcast',
807 startkey = [self.get_id(), start],
808 endkey = [self.get_id(), end],
809 group = True,
810 group_level = 2,
811 reduce = True,
814 for res in r:
815 date = parser.parse(res['key'][1]).date()
816 listeners = res['value']
817 yield (date, listeners)
820 def episode_listener_counts(self):
821 """ (Episode-Id, listener-count) tuples for episodes w/ listeners """
823 from mygpo.users.models import EpisodeUserState
824 r = EpisodeUserState.view('listeners/by_podcast_episode',
825 startkey = [self.get_id(), None, None],
826 endkey = [self.get_id(), {}, {}],
827 group = True,
828 group_level = 2,
829 reduce = True,
832 for res in r:
833 episode = res['key'][1]
834 listeners = res['value']
835 yield (episode, listeners)
838 def get_episode_states(self, user_id):
839 """ Returns the latest episode actions for the podcast's episodes """
841 from mygpo.users.models import EpisodeUserState
842 db = get_main_database()
844 res = db.view('episode_states/by_user_podcast',
845 startkey = [user_id, self.get_id(), None],
846 endkey = [user_id, self.get_id(), {}],
849 for r in res:
850 action = r['value']
851 yield action
854 def __hash__(self):
855 return hash(self.get_id())
858 def __repr__(self):
859 if not self._id:
860 return super(Podcast, self).__repr__()
861 elif self.oldid:
862 return '%s %s (%s)' % (self.__class__.__name__, self.get_id(), self.oldid)
863 else:
864 return '%s %s' % (self.__class__.__name__, self.get_id())
867 def save(self):
868 group = getattr(self, 'group', None)
869 if group: #we are part of a PodcastGroup
870 group = PodcastGroup.get(group)
871 podcasts = list(group.podcasts)
873 if not self in podcasts:
874 # the podcast has not been added to the group correctly
875 group.add_podcast(self)
877 else:
878 i = podcasts.index(self)
879 podcasts[i] = self
880 group.podcasts = podcasts
881 group.save()
883 i = podcasts.index(self)
884 podcasts[i] = self
885 group.podcasts = podcasts
886 group.save()
888 else:
889 super(Podcast, self).save()
892 def delete(self):
893 group = getattr(self, 'group', None)
894 if group:
895 group = PodcastGroup.get(group)
896 podcasts = list(group.podcasts)
898 if self in podcasts:
899 i = podcasts.index(self)
900 del podcasts[i]
901 group.podcasts = podcasts
902 group.save()
904 else:
905 super(Podcast, self).delete()
907 @classmethod
908 def all_podcasts_groups(cls):
909 return cls.view('podcasts/podcasts_groups', include_docs=True,
910 classes=[Podcast, PodcastGroup]).iterator()
913 def __eq__(self, other):
914 if not self.get_id():
915 return self == other
917 if other == None:
918 return False
920 return self.get_id() == other.get_id()
923 @classmethod
924 def all_podcasts(cls):
925 res = utils.multi_request_view(cls, 'podcasts/by_id',
926 wrap = False,
927 include_docs = True,
928 stale = 'update_after',
931 for r in res:
932 obj = r['doc']
933 if obj['doc_type'] == 'Podcast':
934 yield Podcast.wrap(obj)
935 else:
936 pid = r[u'key']
937 pg = PodcastGroup.wrap(obj)
938 podcast = pg.get_podcast_by_id(pid)
939 yield podcast
943 class PodcastGroup(Document, SlugMixin, OldIdMixin):
944 title = StringProperty()
945 podcasts = SchemaListProperty(Podcast)
947 def get_id(self):
948 return self._id
950 @classmethod
951 def for_oldid(cls, oldid):
952 oldid = int(oldid)
953 r = cls.view('podcasts/groups_by_oldid', \
954 key=oldid, limit=1, include_docs=True)
955 return r.first() if r else None
958 @classmethod
959 def for_slug_id(cls, slug_id):
960 """ Returns the Podcast for either an CouchDB-ID for a Slug """
962 if utils.is_couchdb_id(slug_id):
963 return cls.get(slug_id)
964 else:
965 #TODO: implement
966 return cls.for_slug(slug_id)
969 def get_podcast_by_id(self, id, current_id=False):
970 for podcast in self.podcasts:
971 if podcast.get_id() == id:
972 return podcast
974 if id in podcast.merged_ids:
975 if current_id:
976 raise MergedIdException(podcast, podcast.get_id())
978 return podcast
981 def get_podcast_by_oldid(self, oldid):
982 for podcast in list(self.podcasts):
983 if podcast.oldid == oldid:
984 return podcast
987 def get_podcast_by_url(self, url):
988 for podcast in self.podcasts:
989 if url in list(podcast.urls):
990 return podcast
993 def subscriber_change(self):
994 prev = self.prev_subscriber_count()
995 if not prev:
996 return 0
998 return self.subscriber_count() / prev
1001 def subscriber_count(self):
1002 return sum([p.subscriber_count() for p in self.podcasts])
1005 def prev_subscriber_count(self):
1006 return sum([p.prev_subscriber_count() for p in self.podcasts])
1008 @property
1009 def display_title(self):
1010 return self.title
1013 def get_podcast(self):
1014 # return podcast with most subscribers (bug 1390)
1015 return sorted(self.podcasts, key=Podcast.subscriber_count,
1016 reverse=True)[0]
1019 @property
1020 def logo_url(self):
1021 return utils.first(p.logo_url for p in self.podcasts)
1024 def get_logo_url(self, size):
1025 if self.logo_url:
1026 filename = hashlib.sha1(self.logo_url).hexdigest()
1027 else:
1028 filename = 'podcast-%d.png' % (hash(self.title) % 5, )
1030 prefix = CoverArt.get_prefix(filename)
1032 return reverse('logo', args=[size, prefix, filename])
1035 def add_podcast(self, podcast, member_name):
1037 if not self._id:
1038 raise ValueError('group has to have an _id first')
1040 if not podcast._id:
1041 raise ValueError('podcast needs to have an _id first')
1043 if not podcast.id:
1044 podcast.id = podcast._id
1046 podcast.delete()
1047 podcast.group = self._id
1048 podcast.group_member_name = member_name
1049 self.podcasts = sorted(self.podcasts + [podcast],
1050 key=Podcast.subscriber_count, reverse=True)
1051 self.save()
1054 def __repr__(self):
1055 if not self._id:
1056 return super(PodcastGroup, self).__repr__()
1057 elif self.oldid:
1058 return '%s %s (%s)' % (self.__class__.__name__, self._id[:10], self.oldid)
1059 else:
1060 return '%s %s' % (self.__class__.__name__, self._id[:10])
1063 class SanitizingRuleStub(object):
1064 pass
1066 class SanitizingRule(Document):
1067 slug = StringProperty()
1068 applies_to = StringListProperty()
1069 search = StringProperty()
1070 replace = StringProperty()
1071 priority = IntegerProperty()
1072 description = StringProperty()
1075 @classmethod
1076 def for_obj_type(cls, obj_type):
1077 r = cls.view('sanitizing_rules/by_target', include_docs=True,
1078 startkey=[obj_type, None], endkey=[obj_type, {}])
1080 for rule in r:
1081 obj = SanitizingRuleStub()
1082 obj.slug = rule.slug
1083 obj.applies_to = list(rule.applies_to)
1084 obj.search = rule.search
1085 obj.replace = rule.replace
1086 obj.priority = rule.priority
1087 obj.description = rule.description
1088 yield obj
1091 @classmethod
1092 def for_slug(cls, slug):
1093 r = cls.view('sanitizing_rules/by_slug', include_docs=True,
1094 key=slug)
1095 return r.one() if r else None
1098 def __repr__(self):
1099 return 'SanitizingRule %s' % self._id