remove User.for_oldid, Device.for_oldid
[mygpo.git] / mygpo / users / models.py
blob317de5067d8cb177d327fc8e7407bf599336f9c6
1 import re
2 import uuid, collections
3 from datetime import datetime
4 import dateutil.parser
5 from itertools import imap
6 from operator import itemgetter
8 from couchdbkit import ResourceNotFound
9 from couchdbkit.ext.django.schema import *
11 from django_couchdb_utils.registration.models import User as BaseUser
13 from mygpo.core.proxy import proxy_object, DocumentABCMeta
14 from mygpo.core.models import Podcast, Episode
15 from mygpo.utils import linearize, get_to_dict, iterate_together
16 from mygpo.decorators import repeat_on_conflict
17 from mygpo.users.ratings import RatingMixin
18 from mygpo.users.sync import SyncedDevicesMixin
19 from mygpo.log import log
22 RE_DEVICE_UID = re.compile(r'^[\w.-]+$')
25 class DeviceUIDException(Exception):
26 pass
30 class Suggestions(Document, RatingMixin):
31 user = StringProperty(required=True)
32 user_oldid = IntegerProperty()
33 podcasts = StringListProperty()
34 blacklist = StringListProperty()
36 @classmethod
37 def for_user(cls, user):
38 r = cls.view('users/suggestions_by_user', key=user._id, \
39 include_docs=True)
40 if r:
41 return r.first()
42 else:
43 s = Suggestions()
44 s.user = user._id
45 return s
48 def get_podcasts(self, count=None):
49 user = User.get(self.user)
50 subscriptions = user.get_subscribed_podcast_ids()
52 ids = filter(lambda x: not x in self.blacklist + subscriptions, self.podcasts)
53 if count:
54 ids = ids[:count]
55 return filter(lambda x: x and x.title, Podcast.get_multi(ids))
58 def __repr__(self):
59 if not self._id:
60 return super(Suggestions, self).__repr__()
61 else:
62 return '%d Suggestions for %s (%s)' % \
63 (len(self.podcasts), self.user, self._id)
66 class EpisodeAction(DocumentSchema):
67 """
68 One specific action to an episode. Must
69 always be part of a EpisodeUserState
70 """
72 action = StringProperty(required=True)
73 timestamp = DateTimeProperty(required=True, default=datetime.utcnow)
74 device_oldid = IntegerProperty(required=False)
75 device = StringProperty()
76 started = IntegerProperty()
77 playmark = IntegerProperty()
78 total = IntegerProperty()
80 def __eq__(self, other):
81 if not isinstance(other, EpisodeAction):
82 return False
83 vals = ('action', 'timestamp', 'device', 'started', 'playmark',
84 'total')
85 return all([getattr(self, v, None) == getattr(other, v, None) for v in vals])
88 def to_history_entry(self):
89 entry = HistoryEntry()
90 entry.action = self.action
91 entry.timestamp = self.timestamp
92 entry.device_id = self.device
93 entry.started = self.started
94 entry.position = self.playmark
95 entry.total = self.total
96 return entry
99 @staticmethod
100 def filter(user_id, since=None, until={}, podcast_id=None,
101 device_id=None):
102 """ Returns Episode Actions for the given criteria"""
104 since_str = since.strftime('%Y-%m-%dT%H:%M:%S') if since else None
105 until_str = until.strftime('%Y-%m-%dT%H:%M:%S') if until else {}
107 # further parts of the key are filled in below
108 startkey = [user_id, since_str, None, None]
109 endkey = [user_id, until_str, {}, {}]
111 # additional filter that are carried out by the
112 # application, not by the database
113 add_filters = []
115 if isinstance(podcast_id, basestring):
116 if until is not None: # filter in database
117 startkey[2] = podcast_id
118 endkey[2] = podcast_id
120 add_filters.append( lambda x: x.podcast_id == podcast_id )
122 elif isinstance(podcast_id, list):
123 add_filters.append( lambda x: x.podcast_id in podcast_id )
125 elif podcast_id is not None:
126 raise ValueError('podcast_id can be either None, basestring '
127 'or a list of basestrings')
130 if device_id:
131 if None not in (until, podcast_id): # filter in database
132 startkey[3] = device_id
133 endkey[3] = device_id
134 else:
135 dev_filter = lambda x: getattr(x, 'device_id', None) == device_id
136 add_filters.append(dev_filter)
139 db = EpisodeUserState.get_db()
140 res = db.view('users/episode_actions',
141 startkey = startkey,
142 endkey = endkey,
143 include_docs = True,
146 for r in res:
147 state = EpisodeUserState.wrap(r['doc'])
148 index = int(r['value'])
149 action = HistoryEntry.from_action_dict(state, index)
150 if all( f(action) for f in add_filters):
151 yield action
154 def validate_time_values(self):
155 """ Validates allowed combinations of time-values """
157 PLAY_ACTION_KEYS = ('playmark', 'started', 'total')
159 # Key found, but must not be supplied (no play action!)
160 if self.action != 'play':
161 for key in PLAY_ACTION_KEYS:
162 if getattr(self, key, None) is not None:
163 raise ValueError('%s only allowed in play actions' % key)
165 # Sanity check: If started or total are given, require playmark
166 if ((self.started is not None) or (self.total is not None)) and \
167 self.playmark is None:
168 raise ValueError('started and total require position')
170 # Sanity check: total and playmark can only appear together
171 if ((self.total is not None) or (self.started is not None)) and \
172 ((self.total is None) or (self.started is None)):
173 raise ValueError('total and started can only appear together')
176 def __repr__(self):
177 return '%s-Action on %s at %s (in %s)' % \
178 (self.action, self.device, self.timestamp, self._id)
181 def __hash__(self):
182 return hash(frozenset([self.action, self.timestamp, self.device,
183 self.started, self.playmark, self.total]))
186 class Chapter(Document):
187 """ A user-entered episode chapter """
189 device = StringProperty()
190 created = DateTimeProperty()
191 start = IntegerProperty(required=True)
192 end = IntegerProperty(required=True)
193 label = StringProperty()
194 advertisement = BooleanProperty()
196 @classmethod
197 def for_episode(cls, episode_id):
198 db = cls.get_db()
199 r = db.view('users/chapters_by_episode',
200 startkey = [episode_id, None],
201 endkey = [episode_id, {}],
202 wrap_doc = False,
205 for res in r:
206 user = res['key'][1]
207 chapter = Chapter.wrap(res['value'])
208 yield (user, chapter)
211 def __repr__(self):
212 return '<%s %s (%d-%d)>' % (self.__class__.__name__, self.label,
213 self.start, self.end)
216 class EpisodeUserState(Document):
218 Contains everything a user has done with an Episode
221 episode = StringProperty(required=True)
222 actions = SchemaListProperty(EpisodeAction)
223 settings = DictProperty()
224 user_oldid = IntegerProperty()
225 user = StringProperty(required=True)
226 ref_url = StringProperty(required=True)
227 podcast_ref_url = StringProperty(required=True)
228 merged_ids = StringListProperty()
229 chapters = SchemaListProperty(Chapter)
230 podcast = StringProperty(required=True)
233 @classmethod
234 def for_user_episode(cls, user, episode):
235 r = cls.view('users/episode_states_by_user_episode',
236 key=[user.id, episode._id], include_docs=True)
238 if r:
239 return r.first()
241 else:
242 podcast = Podcast.get(episode.podcast)
244 state = EpisodeUserState()
245 state.episode = episode._id
246 state.podcast = episode.podcast
247 state.user = user._id
248 state.ref_url = episode.url
249 state.podcast_ref_url = podcast.url
251 return state
253 @classmethod
254 def for_ref_urls(cls, user, podcast_url, episode_url):
255 res = cls.view('users/episode_states_by_ref_urls',
256 key = [user.id, podcast_url, episode_url], limit=1, include_docs=True)
257 if res:
258 state = res.first()
259 state.ref_url = episode_url
260 state.podcast_ref_url = podcast_url
261 return state
263 else:
264 podcast = Podcast.for_url(podcast_url, create=True)
265 episode = Episode.for_podcast_id_url(podcast.get_id(), episode_url,
266 create=True)
268 return episode.get_user_state(user)
271 @classmethod
272 def count(cls):
273 r = cls.view('users/episode_states_by_user_episode',
274 limit=0)
275 return r.total_rows
278 def add_actions(self, actions):
279 map(EpisodeAction.validate_time_values, actions)
280 self.actions = list(self.actions) + actions
281 self.actions = list(set(self.actions))
282 self.actions = sorted(self.actions, key=lambda x: x.timestamp)
285 def is_favorite(self):
286 return self.settings.get('is_favorite', False)
289 def set_favorite(self, set_to=True):
290 self.settings['is_favorite'] = set_to
293 def update_chapters(self, add=[], rem=[]):
294 """ Updates the Chapter list
296 * add contains the chapters to be added
298 * rem contains tuples of (start, end) times. Chapters that match
299 both endpoints will be removed
302 @repeat_on_conflict(['state'])
303 def update(state):
304 for chapter in add:
305 self.chapters = self.chapters + [chapter]
307 for start, end in rem:
308 keep = lambda c: c.start != start or c.end != end
309 self.chapters = filter(keep, self.chapters)
311 self.save()
313 update(state=self)
316 def get_history_entries(self):
317 return imap(EpisodeAction.to_history_entry, self.actions)
320 def __repr__(self):
321 return 'Episode-State %s (in %s)' % \
322 (self.episode, self._id)
324 def __eq__(self, other):
325 if not isinstance(other, EpisodeUserState):
326 return False
328 return (self.episode == other.episode and
329 self.user_oldid == other.user_oldid)
333 class SubscriptionAction(Document):
334 action = StringProperty()
335 timestamp = DateTimeProperty(default=datetime.utcnow)
336 device = StringProperty()
339 __metaclass__ = DocumentABCMeta
342 def __cmp__(self, other):
343 return cmp(self.timestamp, other.timestamp)
345 def __eq__(self, other):
346 return self.action == other.action and \
347 self.timestamp == other.timestamp and \
348 self.device == other.device
350 def __hash__(self):
351 return hash(self.action) + hash(self.timestamp) + hash(self.device)
353 def __repr__(self):
354 return '<SubscriptionAction %s on %s at %s>' % (
355 self.action, self.device, self.timestamp)
358 class PodcastUserState(Document):
360 Contains everything that a user has done
361 with a specific podcast and all its episodes
364 podcast = StringProperty(required=True)
365 user_oldid = IntegerProperty()
366 user = StringProperty(required=True)
367 settings = DictProperty()
368 actions = SchemaListProperty(SubscriptionAction)
369 tags = StringListProperty()
370 ref_url = StringProperty(required=True)
371 disabled_devices = StringListProperty()
372 merged_ids = StringListProperty()
375 @classmethod
376 def for_user_podcast(cls, user, podcast):
377 r = PodcastUserState.view('users/podcast_states_by_podcast', \
378 key=[podcast.get_id(), user._id], limit=1, include_docs=True)
379 if r:
380 return r.first()
381 else:
382 p = PodcastUserState()
383 p.podcast = podcast.get_id()
384 p.user = user._id
385 p.ref_url = podcast.url
386 p.settings['public_subscription'] = user.settings.get('public_subscriptions', True)
388 p.set_device_state(user.devices)
390 return p
393 @classmethod
394 def for_user(cls, user):
395 r = PodcastUserState.view('users/podcast_states_by_user',
396 startkey = [user._id, None],
397 endkey = [user._id, 'ZZZZ'],
398 include_docs = True,
400 return list(r)
403 @classmethod
404 def for_device(cls, device_id):
405 r = PodcastUserState.view('users/podcast_states_by_device',
406 startkey=[device_id, None], endkey=[device_id, {}],
407 include_docs=True)
408 return list(r)
411 def remove_device(self, device):
413 Removes all actions from the podcast state that refer to the
414 given device
416 self.actions = filter(lambda a: a.device != device.id, self.actions)
419 @classmethod
420 def count(cls):
421 r = PodcastUserState.view('users/podcast_states_by_user',
422 limit=0)
423 return r.total_rows
426 def subscribe(self, device):
427 action = SubscriptionAction()
428 action.action = 'subscribe'
429 action.device = device.id
430 self.add_actions([action])
433 def unsubscribe(self, device):
434 action = SubscriptionAction()
435 action.action = 'unsubscribe'
436 action.device = device.id
437 self.add_actions([action])
440 def add_actions(self, actions):
441 self.actions = list(set(self.actions + actions))
442 self.actions = sorted(self.actions)
445 def add_tags(self, tags):
446 self.tags = list(set(self.tags + tags))
449 def set_device_state(self, devices):
450 disabled_devices = [device.id for device in devices if device.deleted]
451 self.disabled_devices = disabled_devices
454 def get_change_between(self, device_id, since, until):
456 Returns the change of the subscription status for the given device
457 between the two timestamps.
459 The change is given as either 'subscribe' (the podcast has been
460 subscribed), 'unsubscribed' (the podcast has been unsubscribed) or
461 None (no change)
464 device_actions = filter(lambda x: x.device == device_id, self.actions)
465 before = filter(lambda x: x.timestamp <= since, device_actions)
466 after = filter(lambda x: x.timestamp <= until, device_actions)
468 # nothing happened, so there can be no change
469 if not after:
470 return None
472 then = before[-1] if before else None
473 now = after[-1]
475 if then is None:
476 if now.action != 'unsubscribe':
477 return now.action
478 elif then.action != now.action:
479 return now.action
480 return None
483 def get_subscribed_device_ids(self):
484 r = PodcastUserState.view('users/subscriptions_by_podcast',
485 startkey = [self.podcast, self.user, None],
486 endkey = [self.podcast, self.user, {}],
487 reduce = False,
489 return (res['key'][2] for res in r)
492 def is_public(self):
493 return self.settings.get('public_subscription', True)
496 def __eq__(self, other):
497 if other is None:
498 return False
500 return self.podcast == other.podcast and \
501 self.user == other.user
503 def __repr__(self):
504 return 'Podcast %s for User %s (%s)' % \
505 (self.podcast, self.user, self._id)
508 class Device(Document):
509 id = StringProperty(default=lambda: uuid.uuid4().hex)
510 oldid = IntegerProperty(required=False)
511 uid = StringProperty(required=True)
512 name = StringProperty(required=True, default='New Device')
513 type = StringProperty(required=True, default='other')
514 settings = DictProperty()
515 deleted = BooleanProperty(default=False)
516 user_agent = StringProperty()
519 def get_subscription_changes(self, since, until):
521 Returns the subscription changes for the device as two lists.
522 The first lists contains the Ids of the podcasts that have been
523 subscribed to, the second list of those that have been unsubscribed
524 from.
527 add, rem = [], []
528 podcast_states = PodcastUserState.for_device(self.id)
529 for p_state in podcast_states:
530 change = p_state.get_change_between(self.id, since, until)
531 if change == 'subscribe':
532 add.append( p_state.podcast )
533 elif change == 'unsubscribe':
534 rem.append( p_state.podcast )
536 return add, rem
539 def get_latest_changes(self):
540 podcast_states = PodcastUserState.for_device(self.id)
541 for p_state in podcast_states:
542 actions = filter(lambda x: x.device == self.id, reversed(p_state.actions))
543 if actions:
544 yield (p_state.podcast, actions[0])
547 def get_subscribed_podcast_ids(self):
548 r = self.view('users/subscribed_podcasts_by_device',
549 startkey = [self.id, None],
550 endkey = [self.id, {}]
552 return [res['key'][1] for res in r]
555 def get_subscribed_podcasts(self):
556 return set(Podcast.get_multi(self.get_subscribed_podcast_ids()))
559 def __hash__(self):
560 return hash(frozenset([self.uid, self.name, self.type, self.deleted]))
563 def __eq__(self, other):
564 return self.id == other.id
567 def __repr__(self):
568 return '<{cls} {id}>'.format(cls=self.__class__.__name__, id=self.id)
571 def __str__(self):
572 return self.name
574 def __unicode__(self):
575 return self.name
578 def token_generator(length=32):
579 import random, string
580 return "".join(random.sample(string.letters+string.digits, length))
583 class User(BaseUser, SyncedDevicesMixin):
584 oldid = IntegerProperty()
585 settings = DictProperty()
586 devices = SchemaListProperty(Device)
587 published_objects = StringListProperty()
588 deleted = BooleanProperty(default=False)
589 suggestions_up_to_date = BooleanProperty(default=False)
591 # token for accessing subscriptions of this use
592 subscriptions_token = StringProperty(default=token_generator)
594 # token for accessing the favorite-episodes feed of this user
595 favorite_feeds_token = StringProperty(default=token_generator)
597 # token for automatically updating feeds published by this user
598 publisher_update_token = StringProperty(default=token_generator)
600 class Meta:
601 app_label = 'users'
604 def create_new_token(self, token_name, length=32):
605 setattr(self, token_name, token_generator(length))
608 @property
609 def active_devices(self):
610 not_deleted = lambda d: not d.deleted
611 return filter(not_deleted, self.devices)
614 @property
615 def inactive_devices(self):
616 deleted = lambda d: d.deleted
617 return filter(deleted, self.devices)
620 def get_device(self, id):
621 for device in self.devices:
622 if device.id == id:
623 return device
625 return None
628 def get_device_by_uid(self, uid):
629 for device in self.devices:
630 if device.uid == uid:
631 return device
634 def get_device_by_oldid(self, oldid):
635 for device in self.devices:
636 if device.oldid == oldid:
637 return device
640 def update_device(self, device):
641 """ Sets the device and saves the user """
643 @repeat_on_conflict(['user'])
644 def _update(user, device):
645 user.set_device(device)
646 user.save()
648 _update(user=self, device=device)
651 def set_device(self, device):
653 if not RE_DEVICE_UID.match(device.uid):
654 raise DeviceUIDException("'{uid} is not a valid device ID".format(
655 uid=device.uid))
657 devices = list(self.devices)
658 ids = [x.id for x in devices]
659 if not device.id in ids:
660 devices.append(device)
661 self.devices = devices
662 return
664 index = ids.index(device.id)
665 devices.pop(index)
666 devices.insert(index, device)
667 self.devices = devices
670 def remove_device(self, device):
671 devices = list(self.devices)
672 ids = [x.id for x in devices]
673 if not device.id in ids:
674 return
676 index = ids.index(device.id)
677 devices.pop(index)
678 self.devices = devices
680 if self.is_synced(device):
681 self.unsync_device(device)
685 def get_subscriptions(self, public=None):
687 Returns a list of (podcast-id, device-id) tuples for all
688 of the users subscriptions
691 r = PodcastUserState.view('users/subscribed_podcasts_by_user',
692 startkey = [self._id, public, None, None],
693 endkey = [self._id+'ZZZ', None, None, None],
694 reduce = False,
696 return [res['key'][1:] for res in r]
699 def get_subscriptions_by_device(self, public=None):
700 get_dev = itemgetter(2)
701 groups = collections.defaultdict(list)
702 subscriptions = self.get_subscriptions(public=public)
703 subscriptions = sorted(subscriptions, key=get_dev)
705 for public, podcast_id, device_id in subscriptions:
706 groups[device_id].append(podcast_id)
708 return groups
711 def get_subscribed_podcast_ids(self, public=None):
713 Returns the Ids of all subscribed podcasts
715 return list(set(x[1] for x in self.get_subscriptions(public=public)))
718 def get_subscribed_podcasts(self, public=None):
719 return set(Podcast.get_multi(self.get_subscribed_podcast_ids(public=public)))
722 def get_subscription_history(self, device_id=None, reverse=False, public=None):
723 """ Returns chronologically ordered subscription history entries
725 Setting device_id restricts the actions to a certain device
728 def action_iter(state):
729 for action in sorted(state.actions, reverse=reverse):
730 if device_id is not None and device_id != action.device:
731 continue
733 if public is not None and state.is_public() != public:
734 continue
736 entry = HistoryEntry()
737 entry.timestamp = action.timestamp
738 entry.action = action.action
739 entry.podcast_id = state.podcast
740 entry.device_id = action.device
741 yield entry
743 if device_id is None:
744 podcast_states = PodcastUserState.for_user(self)
745 else:
746 podcast_states = PodcastUserState.for_device(device_id)
748 # create an action_iter for each PodcastUserState
749 subscription_action_lists = [action_iter(x) for x in podcast_states]
751 action_cmp_key = lambda x: x.timestamp
753 # Linearize their subscription-actions
754 return linearize(action_cmp_key, subscription_action_lists, reverse)
757 def get_global_subscription_history(self, public=None):
758 """ Actions that added/removed podcasts from the subscription list
760 Returns an iterator of all subscription actions that either
761 * added subscribed a podcast that hasn't been subscribed directly
762 before the action (but could have been subscribed) earlier
763 * removed a subscription of the podcast is not longer subscribed
764 after the action
767 subscriptions = collections.defaultdict(int)
769 for entry in self.get_subscription_history(public=public):
770 if entry.action == 'subscribe':
771 subscriptions[entry.podcast_id] += 1
773 # a new subscription has been added
774 if subscriptions[entry.podcast_id] == 1:
775 yield entry
777 elif entry.action == 'unsubscribe':
778 subscriptions[entry.podcast_id] -= 1
780 # the last subscription has been removed
781 if subscriptions[entry.podcast_id] == 0:
782 yield entry
786 def get_newest_episodes(self, max_date, max_per_podcast=5):
787 """ Returns the newest episodes of all subscribed podcasts
789 Only max_per_podcast episodes per podcast are loaded. Episodes with
790 release dates above max_date are discarded.
792 This method returns a generator that produces the newest episodes.
794 The number of required DB queries is equal to the number of (distinct)
795 podcasts of all consumed episodes (max: number of subscribed podcasts),
796 plus a constant number of initial queries (when the first episode is
797 consumed). """
799 cmp_key = lambda episode: episode.released or datetime(2000, 01, 01)
801 podcasts = list(self.get_subscribed_podcasts())
802 podcasts = filter(lambda p: p.latest_episode_timestamp, podcasts)
803 podcasts = sorted(podcasts, key=lambda p: p.latest_episode_timestamp,
804 reverse=True)
806 podcast_dict = dict((p.get_id(), p) for p in podcasts)
808 # contains the un-yielded episodes, newest first
809 episodes = []
811 for podcast in podcasts:
813 yielded_episodes = 0
815 for episode in episodes:
816 # determine for which episodes there won't be a new episodes
817 # that is newer; those can be yielded
818 if episode.released > podcast.latest_episode_timestamp:
819 p = podcast_dict.get(episode.podcast, None)
820 yield proxy_object(episode, podcast=p)
821 yielded_episodes += 1
822 else:
823 break
825 # remove the episodes that have been yielded before
826 episodes = episodes[yielded_episodes:]
828 # fetch and merge episodes for the next podcast
829 new_episodes = list(podcast.get_episodes(since=1, until=max_date,
830 descending=True, limit=max_per_podcast))
831 episodes = sorted(episodes+new_episodes, key=cmp_key, reverse=True)
834 # yield the remaining episodes
835 for episode in episodes:
836 podcast = podcast_dict.get(episode.podcast, None)
837 yield proxy_object(episode, podcast=podcast)
841 def save(self, *args, **kwargs):
842 super(User, self).save(*args, **kwargs)
844 podcast_states = PodcastUserState.for_user(self)
845 for state in podcast_states:
846 @repeat_on_conflict(['state'])
847 def _update_state(state):
848 old_devs = set(state.disabled_devices)
849 state.set_device_state(self.devices)
851 if old_devs != set(state.disabled_devices):
852 state.save()
854 _update_state(state=state)
859 def __eq__(self, other):
860 if not other:
861 return False
863 # ensure that other isn't AnonymousUser
864 return other.is_authenticated() and self._id == other._id
867 def __repr__(self):
868 return 'User %s' % self._id
871 class History(object):
873 def __init__(self, user, device):
874 self.user = user
875 self.device = device
876 self._db = EpisodeUserState.get_db()
878 if device:
879 self._view = 'users/device_history'
880 self._startkey = [self.user._id, device.id, None]
881 self._endkey = [self.user._id, device.id, {}]
882 else:
883 self._view = 'users/history'
884 self._startkey = [self.user._id, None]
885 self._endkey = [self.user._id, {}]
888 def __getitem__(self, key):
890 if isinstance(key, slice):
891 start = key.start or 0
892 length = key.stop - start
893 else:
894 start = key
895 length = 1
897 res = self._db.view(self._view,
898 descending = True,
899 startkey = self._endkey,
900 endkey = self._startkey,
901 limit = length,
902 skip = start,
903 include_docs = True,
906 for action in res:
907 state_doc = action['doc']
908 index = int(action['value'])
910 if state_doc['doc_type'] == 'EpisodeUserState':
911 state = EpisodeUserState.wrap(state_doc)
912 else:
913 state = PodcastUserState.wrap(state_doc)
915 yield HistoryEntry.from_action_dict(state, index)
919 class HistoryEntry(object):
920 """ A class that can represent subscription and episode actions """
923 @classmethod
924 def from_action_dict(cls, state, index):
926 entry = HistoryEntry()
927 action = state.actions[index]
929 if isinstance(state, EpisodeUserState):
930 entry.type = 'Episode'
931 entry.podcast_url = state.podcast_ref_url
932 entry.episode_url = state.ref_url
933 entry.podcast_id = state.podcast
934 entry.episode_id = state.episode
935 if action.device:
936 entry.device_id = action.device
937 if action.started:
938 entry.started = action.started
939 if action.playmark:
940 entry.position = action.playmark
941 if action.total:
942 entry.total = action.total
944 else:
945 entry.type = 'Subscription'
946 entry.podcast_url = state.ref_url
947 entry.podcast_id = state.podcast
950 entry.action = action.action
951 entry.timestamp = action.timestamp
953 return entry
956 @property
957 def playmark(self):
958 return getattr(self, 'position', None)
961 @classmethod
962 def fetch_data(cls, user, entries,
963 podcasts=None, episodes=None):
964 """ Efficiently loads additional data for a number of entries """
966 if podcasts is None:
967 # load podcast data
968 podcast_ids = [getattr(x, 'podcast_id', None) for x in entries]
969 podcast_ids = filter(None, podcast_ids)
970 podcasts = get_to_dict(Podcast, podcast_ids, get_id=Podcast.get_id)
972 if episodes is None:
973 # load episode data
974 episode_ids = [getattr(x, 'episode_id', None) for x in entries]
975 episode_ids = filter(None, episode_ids)
976 episodes = get_to_dict(Episode, episode_ids)
978 # load device data
979 # does not need pre-populated data because no db-access is required
980 device_ids = [getattr(x, 'device_id', None) for x in entries]
981 device_ids = filter(None, device_ids)
982 devices = dict([ (id, user.get_device(id)) for id in device_ids])
985 for entry in entries:
986 podcast_id = getattr(entry, 'podcast_id', None)
987 entry.podcast = podcasts.get(podcast_id, None)
989 episode_id = getattr(entry, 'episode_id', None)
990 entry.episode = episodes.get(episode_id, None)
991 entry.user = user
993 device = devices.get(getattr(entry, 'device_id', None), None)
994 entry.device = device
997 return entries