2 import uuid
, collections
3 from datetime
import datetime
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):
30 class Suggestions(Document
, RatingMixin
):
31 user
= StringProperty(required
=True)
32 user_oldid
= IntegerProperty()
33 podcasts
= StringListProperty()
34 blacklist
= StringListProperty()
37 def for_user(cls
, user
):
38 r
= cls
.view('users/suggestions_by_user', key
=user
._id
, \
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
)
55 return filter(lambda x
: x
and x
.title
, Podcast
.get_multi(ids
))
60 return super(Suggestions
, self
).__repr
__()
62 return '%d Suggestions for %s (%s)' % \
63 (len(self
.podcasts
), self
.user
, self
._id
)
66 class EpisodeAction(DocumentSchema
):
68 One specific action to an episode. Must
69 always be part of a EpisodeUserState
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
):
83 vals
= ('action', 'timestamp', 'device', 'started', 'playmark',
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
100 def filter(user_id
, since
=None, until
={}, podcast_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
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')
131 if None not in (until
, podcast_id
): # filter in database
132 startkey
[3] = device_id
133 endkey
[3] = device_id
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',
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
):
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')
177 return '%s-Action on %s at %s (in %s)' % \
178 (self
.action
, self
.device
, self
.timestamp
, self
._id
)
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()
197 def for_episode(cls
, episode_id
):
199 r
= db
.view('users/chapters_by_episode',
200 startkey
= [episode_id
, None],
201 endkey
= [episode_id
, {}],
207 chapter
= Chapter
.wrap(res
['value'])
208 yield (user
, chapter
)
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)
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)
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
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)
259 state
.ref_url
= episode_url
260 state
.podcast_ref_url
= podcast_url
264 podcast
= Podcast
.for_url(podcast_url
, create
=True)
265 episode
= Episode
.for_podcast_id_url(podcast
.get_id(), episode_url
,
268 return episode
.get_user_state(user
)
273 r
= cls
.view('users/episode_states_by_user_episode',
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'])
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
)
316 def get_history_entries(self
):
317 return imap(EpisodeAction
.to_history_entry
, self
.actions
)
321 return 'Episode-State %s (in %s)' % \
322 (self
.episode
, self
._id
)
324 def __eq__(self
, other
):
325 if not isinstance(other
, EpisodeUserState
):
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
351 return hash(self
.action
) + hash(self
.timestamp
) + hash(self
.device
)
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()
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)
382 p
= PodcastUserState()
383 p
.podcast
= podcast
.get_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
)
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'],
404 def for_device(cls
, device_id
):
405 r
= PodcastUserState
.view('users/podcast_states_by_device',
406 startkey
=[device_id
, None], endkey
=[device_id
, {}],
411 def remove_device(self
, device
):
413 Removes all actions from the podcast state that refer to the
416 self
.actions
= filter(lambda a
: a
.device
!= device
.id, self
.actions
)
421 r
= PodcastUserState
.view('users/podcast_states_by_user',
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
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
472 then
= before
[-1] if before
else None
476 if now
.action
!= 'unsubscribe':
478 elif then
.action
!= now
.action
:
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
, {}],
489 return (res
['key'][2] for res
in r
)
493 return self
.settings
.get('public_subscription', True)
496 def __eq__(self
, other
):
500 return self
.podcast
== other
.podcast
and \
501 self
.user
== other
.user
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
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
)
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
))
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()))
560 return hash(frozenset([self
.uid
, self
.name
, self
.type, self
.deleted
]))
563 def __eq__(self
, other
):
564 return self
.id == other
.id
568 return '<{cls} {id}>'.format(cls
=self
.__class
__.__name
__, id=self
.id)
574 def __unicode__(self
):
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
)
604 def create_new_token(self
, token_name
, length
=32):
605 setattr(self
, token_name
, token_generator(length
))
609 def active_devices(self
):
610 not_deleted
= lambda d
: not d
.deleted
611 return filter(not_deleted
, self
.devices
)
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
:
628 def get_device_by_uid(self
, uid
):
629 for device
in self
.devices
:
630 if device
.uid
== uid
:
634 def get_device_by_oldid(self
, oldid
):
635 for device
in self
.devices
:
636 if device
.oldid
== oldid
:
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
)
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(
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
664 index
= ids
.index(device
.id)
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
:
676 index
= ids
.index(device
.id)
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],
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
)
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
:
733 if public
is not None and state
.is_public() != public
:
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
743 if device_id
is None:
744 podcast_states
= PodcastUserState
.for_user(self
)
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
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:
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:
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
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
,
806 podcast_dict
= dict((p
.get_id(), p
) for p
in podcasts
)
808 # contains the un-yielded episodes, newest first
811 for podcast
in podcasts
:
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
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
):
854 _update_state(state
=state
)
859 def __eq__(self
, other
):
863 # ensure that other isn't AnonymousUser
864 return other
.is_authenticated() and self
._id
== other
._id
868 return 'User %s' % self
._id
871 class History(object):
873 def __init__(self
, user
, device
):
876 self
._db
= EpisodeUserState
.get_db()
879 self
._view
= 'users/device_history'
880 self
._startkey
= [self
.user
._id
, device
.id, None]
881 self
._endkey
= [self
.user
._id
, device
.id, {}]
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
897 res
= self
._db
.view(self
._view
,
899 startkey
= self
._endkey
,
900 endkey
= self
._startkey
,
907 state_doc
= action
['doc']
908 index
= int(action
['value'])
910 if state_doc
['doc_type'] == 'EpisodeUserState':
911 state
= EpisodeUserState
.wrap(state_doc
)
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 """
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
936 entry
.device_id
= action
.device
938 entry
.started
= action
.started
940 entry
.position
= action
.playmark
942 entry
.total
= action
.total
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
958 return getattr(self
, 'position', None)
962 def fetch_data(cls
, user
, entries
,
963 podcasts
=None, episodes
=None):
964 """ Efficiently loads additional data for a number of entries """
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
)
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
)
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)
993 device
= devices
.get(getattr(entry
, 'device_id', None), None)
994 entry
.device
= device