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):
29 class DeviceDoesNotExist(Exception):
33 class DeviceDeletedException(DeviceDoesNotExist
):
37 class Suggestions(Document
, RatingMixin
):
38 user
= StringProperty(required
=True)
39 user_oldid
= IntegerProperty()
40 podcasts
= StringListProperty()
41 blacklist
= StringListProperty()
44 def for_user(cls
, user
):
45 r
= cls
.view('suggestions/by_user', key
=user
._id
, \
55 def get_podcasts(self
, count
=None):
56 user
= User
.get(self
.user
)
57 subscriptions
= user
.get_subscribed_podcast_ids()
59 ids
= filter(lambda x
: not x
in self
.blacklist
+ subscriptions
, self
.podcasts
)
62 return filter(lambda x
: x
and x
.title
, Podcast
.get_multi(ids
))
67 return super(Suggestions
, self
).__repr
__()
69 return '%d Suggestions for %s (%s)' % \
70 (len(self
.podcasts
), self
.user
, self
._id
)
73 class EpisodeAction(DocumentSchema
):
75 One specific action to an episode. Must
76 always be part of a EpisodeUserState
79 action
= StringProperty(required
=True)
80 timestamp
= DateTimeProperty(required
=True, default
=datetime
.utcnow
)
81 device_oldid
= IntegerProperty(required
=False)
82 device
= StringProperty()
83 started
= IntegerProperty()
84 playmark
= IntegerProperty()
85 total
= IntegerProperty()
87 def __eq__(self
, other
):
88 if not isinstance(other
, EpisodeAction
):
90 vals
= ('action', 'timestamp', 'device', 'started', 'playmark',
92 return all([getattr(self
, v
, None) == getattr(other
, v
, None) for v
in vals
])
95 def to_history_entry(self
):
96 entry
= HistoryEntry()
97 entry
.action
= self
.action
98 entry
.timestamp
= self
.timestamp
99 entry
.device_id
= self
.device
100 entry
.started
= self
.started
101 entry
.position
= self
.playmark
102 entry
.total
= self
.total
107 def filter(user_id
, since
=None, until
={}, podcast_id
=None,
109 """ Returns Episode Actions for the given criteria"""
111 since_str
= since
.strftime('%Y-%m-%dT%H:%M:%S') if since
else None
112 until_str
= until
.strftime('%Y-%m-%dT%H:%M:%S') if until
else {}
115 if not podcast_id
and not device_id
:
116 view
= 'episode_actions/by_user'
117 startkey
= [user_id
, since_str
]
118 endkey
= [user_id
, until_str
]
120 elif podcast_id
and not device_id
:
121 view
= 'episode_actions/by_podcast'
122 startkey
= [user_id
, podcast_id
, since_str
]
123 endkey
= [user_id
, podcast_id
, until_str
]
125 elif device_id
and not podcast_id
:
126 view
= 'episode_actions/by_device'
127 startkey
= [user_id
, device_id
, since_str
]
128 endkey
= [user_id
, device_id
, until_str
]
131 view
= 'episode_actions/by_podcast_device'
132 startkey
= [user_id
, podcast_id
, device_id
, since_str
]
133 endkey
= [user_id
, podcast_id
, device_id
, until_str
]
135 db
= EpisodeUserState
.get_db()
146 def validate_time_values(self
):
147 """ Validates allowed combinations of time-values """
149 PLAY_ACTION_KEYS
= ('playmark', 'started', 'total')
151 # Key found, but must not be supplied (no play action!)
152 if self
.action
!= 'play':
153 for key
in PLAY_ACTION_KEYS
:
154 if getattr(self
, key
, None) is not None:
155 raise ValueError('%s only allowed in play actions' % key
)
157 # Sanity check: If started or total are given, require playmark
158 if ((self
.started
is not None) or (self
.total
is not None)) and \
159 self
.playmark
is None:
160 raise ValueError('started and total require position')
162 # Sanity check: total and playmark can only appear together
163 if ((self
.total
is not None) or (self
.started
is not None)) and \
164 ((self
.total
is None) or (self
.started
is None)):
165 raise ValueError('total and started can only appear together')
169 return '%s-Action on %s at %s (in %s)' % \
170 (self
.action
, self
.device
, self
.timestamp
, self
._id
)
174 return hash(frozenset([self
.action
, self
.timestamp
, self
.device
,
175 self
.started
, self
.playmark
, self
.total
]))
178 class Chapter(Document
):
179 """ A user-entered episode chapter """
181 device
= StringProperty()
182 created
= DateTimeProperty()
183 start
= IntegerProperty(required
=True)
184 end
= IntegerProperty(required
=True)
185 label
= StringProperty()
186 advertisement
= BooleanProperty()
189 def for_episode(cls
, episode_id
):
191 r
= db
.view('chapters/by_episode',
192 startkey
= [episode_id
, None],
193 endkey
= [episode_id
, {}],
199 chapter
= Chapter
.wrap(res
['value'])
200 yield (user
, chapter
)
204 return '<%s %s (%d-%d)>' % (self
.__class
__.__name
__, self
.label
,
205 self
.start
, self
.end
)
208 class EpisodeUserState(Document
):
210 Contains everything a user has done with an Episode
213 episode
= StringProperty(required
=True)
214 actions
= SchemaListProperty(EpisodeAction
)
215 settings
= DictProperty()
216 user_oldid
= IntegerProperty()
217 user
= StringProperty(required
=True)
218 ref_url
= StringProperty(required
=True)
219 podcast_ref_url
= StringProperty(required
=True)
220 merged_ids
= StringListProperty()
221 chapters
= SchemaListProperty(Chapter
)
222 podcast
= StringProperty(required
=True)
226 def for_user_episode(cls
, user
, episode
):
227 r
= cls
.view('episode_states/by_user_episode',
228 key
= [user
._id
, episode
._id
],
237 podcast
= Podcast
.get(episode
.podcast
)
239 state
= EpisodeUserState()
240 state
.episode
= episode
._id
241 state
.podcast
= episode
.podcast
242 state
.user
= user
._id
243 state
.ref_url
= episode
.url
244 state
.podcast_ref_url
= podcast
.url
249 def for_ref_urls(cls
, user
, podcast_url
, episode_url
):
250 res
= cls
.view('episode_states/by_ref_urls',
251 key
= [user
._id
, podcast_url
, episode_url
], limit
=1, include_docs
=True)
254 state
.ref_url
= episode_url
255 state
.podcast_ref_url
= podcast_url
259 episode
= Episode
.for_podcast_url(podcast_url
, episode_url
, create
=True)
260 return episode
.get_user_state(user
)
265 r
= cls
.view('episode_states/by_user_episode',
267 stale
= 'update_after',
272 def add_actions(self
, actions
):
273 map(EpisodeAction
.validate_time_values
, actions
)
274 self
.actions
= list(self
.actions
) + actions
275 self
.actions
= list(set(self
.actions
))
276 self
.actions
= sorted(self
.actions
, key
=lambda x
: x
.timestamp
)
279 def is_favorite(self
):
280 return self
.settings
.get('is_favorite', False)
283 def set_favorite(self
, set_to
=True):
284 self
.settings
['is_favorite'] = set_to
287 def update_chapters(self
, add
=[], rem
=[]):
288 """ Updates the Chapter list
290 * add contains the chapters to be added
292 * rem contains tuples of (start, end) times. Chapters that match
293 both endpoints will be removed
296 @repeat_on_conflict(['state'])
299 self
.chapters
= self
.chapters
+ [chapter
]
301 for start
, end
in rem
:
302 keep
= lambda c
: c
.start
!= start
or c
.end
!= end
303 self
.chapters
= filter(keep
, self
.chapters
)
310 def get_history_entries(self
):
311 return imap(EpisodeAction
.to_history_entry
, self
.actions
)
315 return 'Episode-State %s (in %s)' % \
316 (self
.episode
, self
._id
)
318 def __eq__(self
, other
):
319 if not isinstance(other
, EpisodeUserState
):
322 return (self
.episode
== other
.episode
and
323 self
.user
== other
.user
)
327 class SubscriptionAction(Document
):
328 action
= StringProperty()
329 timestamp
= DateTimeProperty(default
=datetime
.utcnow
)
330 device
= StringProperty()
333 __metaclass__
= DocumentABCMeta
336 def __cmp__(self
, other
):
337 return cmp(self
.timestamp
, other
.timestamp
)
339 def __eq__(self
, other
):
340 return self
.action
== other
.action
and \
341 self
.timestamp
== other
.timestamp
and \
342 self
.device
== other
.device
345 return hash(self
.action
) + hash(self
.timestamp
) + hash(self
.device
)
348 return '<SubscriptionAction %s on %s at %s>' % (
349 self
.action
, self
.device
, self
.timestamp
)
352 class PodcastUserState(Document
):
354 Contains everything that a user has done
355 with a specific podcast and all its episodes
358 podcast
= StringProperty(required
=True)
359 user_oldid
= IntegerProperty()
360 user
= StringProperty(required
=True)
361 settings
= DictProperty()
362 actions
= SchemaListProperty(SubscriptionAction
)
363 tags
= StringListProperty()
364 ref_url
= StringProperty(required
=True)
365 disabled_devices
= StringListProperty()
366 merged_ids
= StringListProperty()
370 def for_user_podcast(cls
, user
, podcast
):
371 r
= PodcastUserState
.view('podcast_states/by_podcast', \
372 key
=[podcast
.get_id(), user
._id
], limit
=1, include_docs
=True)
376 p
= PodcastUserState()
377 p
.podcast
= podcast
.get_id()
379 p
.ref_url
= podcast
.url
380 p
.settings
['public_subscription'] = user
.settings
.get('public_subscriptions', True)
382 p
.set_device_state(user
.devices
)
388 def for_user(cls
, user
):
389 r
= PodcastUserState
.view('podcast_states/by_user',
390 startkey
= [user
._id
, None],
391 endkey
= [user
._id
, 'ZZZZ'],
398 def for_device(cls
, device_id
):
399 r
= PodcastUserState
.view('podcast_states/by_device',
400 startkey
=[device_id
, None], endkey
=[device_id
, {}],
405 def remove_device(self
, device
):
407 Removes all actions from the podcast state that refer to the
410 self
.actions
= filter(lambda a
: a
.device
!= device
.id, self
.actions
)
415 r
= PodcastUserState
.view('podcast_states/by_user',
417 stale
= 'update_after',
422 def subscribe(self
, device
):
423 action
= SubscriptionAction()
424 action
.action
= 'subscribe'
425 action
.device
= device
.id
426 self
.add_actions([action
])
429 def unsubscribe(self
, device
):
430 action
= SubscriptionAction()
431 action
.action
= 'unsubscribe'
432 action
.device
= device
.id
433 self
.add_actions([action
])
436 def add_actions(self
, actions
):
437 self
.actions
= list(set(self
.actions
+ actions
))
438 self
.actions
= sorted(self
.actions
)
441 def add_tags(self
, tags
):
442 self
.tags
= list(set(self
.tags
+ tags
))
445 def set_device_state(self
, devices
):
446 disabled_devices
= [device
.id for device
in devices
if device
.deleted
]
447 self
.disabled_devices
= disabled_devices
450 def get_change_between(self
, device_id
, since
, until
):
452 Returns the change of the subscription status for the given device
453 between the two timestamps.
455 The change is given as either 'subscribe' (the podcast has been
456 subscribed), 'unsubscribed' (the podcast has been unsubscribed) or
460 device_actions
= filter(lambda x
: x
.device
== device_id
, self
.actions
)
461 before
= filter(lambda x
: x
.timestamp
<= since
, device_actions
)
462 after
= filter(lambda x
: x
.timestamp
<= until
, device_actions
)
464 # nothing happened, so there can be no change
468 then
= before
[-1] if before
else None
472 if now
.action
!= 'unsubscribe':
474 elif then
.action
!= now
.action
:
479 def get_subscribed_device_ids(self
):
480 """ device Ids on which the user subscribed to the podcast """
483 for action
in self
.actions
:
484 if action
.action
== "subscribe":
485 if not action
.device
in self
.disabled_devices
:
486 devices
.add(action
.device
)
488 if action
.device
in devices
:
489 devices
.remove(action
.device
)
496 return self
.settings
.get('public_subscription', True)
499 def __eq__(self
, other
):
503 return self
.podcast
== other
.podcast
and \
504 self
.user
== other
.user
507 return 'Podcast %s for User %s (%s)' % \
508 (self
.podcast
, self
.user
, self
._id
)
511 class Device(Document
):
512 id = StringProperty(default
=lambda: uuid
.uuid4().hex)
513 oldid
= IntegerProperty(required
=False)
514 uid
= StringProperty(required
=True)
515 name
= StringProperty(required
=True, default
='New Device')
516 type = StringProperty(required
=True, default
='other')
517 settings
= DictProperty()
518 deleted
= BooleanProperty(default
=False)
519 user_agent
= StringProperty()
522 def get_subscription_changes(self
, since
, until
):
524 Returns the subscription changes for the device as two lists.
525 The first lists contains the Ids of the podcasts that have been
526 subscribed to, the second list of those that have been unsubscribed
531 podcast_states
= PodcastUserState
.for_device(self
.id)
532 for p_state
in podcast_states
:
533 change
= p_state
.get_change_between(self
.id, since
, until
)
534 if change
== 'subscribe':
535 add
.append( p_state
.ref_url
)
536 elif change
== 'unsubscribe':
537 rem
.append( p_state
.ref_url
)
542 def get_latest_changes(self
):
543 podcast_states
= PodcastUserState
.for_device(self
.id)
544 for p_state
in podcast_states
:
545 actions
= filter(lambda x
: x
.device
== self
.id, reversed(p_state
.actions
))
547 yield (p_state
.podcast
, actions
[0])
550 def get_subscribed_podcast_ids(self
):
551 r
= self
.view('subscriptions/by_device',
552 startkey
= [self
.id, None],
553 endkey
= [self
.id, {}]
555 return [res
['key'][1] for res
in r
]
558 def get_subscribed_podcasts(self
):
559 return set(Podcast
.get_multi(self
.get_subscribed_podcast_ids()))
563 return hash(frozenset([self
.uid
, self
.name
, self
.type, self
.deleted
]))
566 def __eq__(self
, other
):
567 return self
.id == other
.id
571 return '<{cls} {id}>'.format(cls
=self
.__class
__.__name
__, id=self
.id)
577 def __unicode__(self
):
581 def token_generator(length
=32):
582 import random
, string
583 return "".join(random
.sample(string
.letters
+string
.digits
, length
))
586 class User(BaseUser
, SyncedDevicesMixin
):
587 oldid
= IntegerProperty()
588 settings
= DictProperty()
589 devices
= SchemaListProperty(Device
)
590 published_objects
= StringListProperty()
591 deleted
= BooleanProperty(default
=False)
592 suggestions_up_to_date
= BooleanProperty(default
=False)
594 # token for accessing subscriptions of this use
595 subscriptions_token
= StringProperty(default
=token_generator
)
597 # token for accessing the favorite-episodes feed of this user
598 favorite_feeds_token
= StringProperty(default
=token_generator
)
600 # token for automatically updating feeds published by this user
601 publisher_update_token
= StringProperty(default
=token_generator
)
607 def create_new_token(self
, token_name
, length
=32):
608 setattr(self
, token_name
, token_generator(length
))
612 def active_devices(self
):
613 not_deleted
= lambda d
: not d
.deleted
614 return filter(not_deleted
, self
.devices
)
618 def inactive_devices(self
):
619 deleted
= lambda d
: d
.deleted
620 return filter(deleted
, self
.devices
)
623 def get_device(self
, id):
625 if not hasattr(self
, '__device_by_id'):
626 self
.__devices
_by
_id
= dict( (d
.id, d
) for d
in self
.devices
)
628 return self
.__devices
_by
_id
.get(id, None)
631 def get_device_by_uid(self
, uid
, only_active
=True):
633 if not hasattr(self
, '__devices_by_uio'):
634 self
.__devices
_by
_uid
= dict( (d
.uid
, d
) for d
in self
.devices
)
637 device
= self
.__devices
_by
_uid
[uid
]
639 if only_active
and device
.deleted
:
640 raise DeviceDeletedException(
641 'Device with UID %s is deleted' % uid
)
645 except KeyError as e
:
646 raise DeviceDoesNotExist('There is no device with UID %s' % uid
)
649 def update_device(self
, device
):
650 """ Sets the device and saves the user """
652 @repeat_on_conflict(['user'])
653 def _update(user
, device
):
654 user
.set_device(device
)
657 _update(user
=self
, device
=device
)
660 def set_device(self
, device
):
662 if not RE_DEVICE_UID
.match(device
.uid
):
663 raise DeviceUIDException("'{uid} is not a valid device ID".format(
666 devices
= list(self
.devices
)
667 ids
= [x
.id for x
in devices
]
668 if not device
.id in ids
:
669 devices
.append(device
)
670 self
.devices
= devices
673 index
= ids
.index(device
.id)
675 devices
.insert(index
, device
)
676 self
.devices
= devices
679 def remove_device(self
, device
):
680 devices
= list(self
.devices
)
681 ids
= [x
.id for x
in devices
]
682 if not device
.id in ids
:
685 index
= ids
.index(device
.id)
687 self
.devices
= devices
689 if self
.is_synced(device
):
690 self
.unsync_device(device
)
694 def get_subscriptions(self
, public
=None):
696 Returns a list of (podcast-id, device-id) tuples for all
697 of the users subscriptions
700 r
= PodcastUserState
.view('subscriptions/by_user',
701 startkey
= [self
._id
, public
, None, None],
702 endkey
= [self
._id
+'ZZZ', None, None, None],
705 return [res
['key'][1:] for res
in r
]
708 def get_subscriptions_by_device(self
, public
=None):
709 get_dev
= itemgetter(2)
710 groups
= collections
.defaultdict(list)
711 subscriptions
= self
.get_subscriptions(public
=public
)
712 subscriptions
= sorted(subscriptions
, key
=get_dev
)
714 for public
, podcast_id
, device_id
in subscriptions
:
715 groups
[device_id
].append(podcast_id
)
720 def get_subscribed_podcast_ids(self
, public
=None):
722 Returns the Ids of all subscribed podcasts
724 return list(set(x
[1] for x
in self
.get_subscriptions(public
=public
)))
727 def get_subscribed_podcasts(self
, public
=None):
728 return set(Podcast
.get_multi(self
.get_subscribed_podcast_ids(public
=public
)))
731 def get_subscription_history(self
, device_id
=None, reverse
=False, public
=None):
732 """ Returns chronologically ordered subscription history entries
734 Setting device_id restricts the actions to a certain device
737 def action_iter(state
):
738 for action
in sorted(state
.actions
, reverse
=reverse
):
739 if device_id
is not None and device_id
!= action
.device
:
742 if public
is not None and state
.is_public() != public
:
745 entry
= HistoryEntry()
746 entry
.timestamp
= action
.timestamp
747 entry
.action
= action
.action
748 entry
.podcast_id
= state
.podcast
749 entry
.device_id
= action
.device
752 if device_id
is None:
753 podcast_states
= PodcastUserState
.for_user(self
)
755 podcast_states
= PodcastUserState
.for_device(device_id
)
757 # create an action_iter for each PodcastUserState
758 subscription_action_lists
= [action_iter(x
) for x
in podcast_states
]
760 action_cmp_key
= lambda x
: x
.timestamp
762 # Linearize their subscription-actions
763 return linearize(action_cmp_key
, subscription_action_lists
, reverse
)
766 def get_global_subscription_history(self
, public
=None):
767 """ Actions that added/removed podcasts from the subscription list
769 Returns an iterator of all subscription actions that either
770 * added subscribed a podcast that hasn't been subscribed directly
771 before the action (but could have been subscribed) earlier
772 * removed a subscription of the podcast is not longer subscribed
776 subscriptions
= collections
.defaultdict(int)
778 for entry
in self
.get_subscription_history(public
=public
):
779 if entry
.action
== 'subscribe':
780 subscriptions
[entry
.podcast_id
] += 1
782 # a new subscription has been added
783 if subscriptions
[entry
.podcast_id
] == 1:
786 elif entry
.action
== 'unsubscribe':
787 subscriptions
[entry
.podcast_id
] -= 1
789 # the last subscription has been removed
790 if subscriptions
[entry
.podcast_id
] == 0:
795 def get_newest_episodes(self
, max_date
, max_per_podcast
=5):
796 """ Returns the newest episodes of all subscribed podcasts
798 Only max_per_podcast episodes per podcast are loaded. Episodes with
799 release dates above max_date are discarded.
801 This method returns a generator that produces the newest episodes.
803 The number of required DB queries is equal to the number of (distinct)
804 podcasts of all consumed episodes (max: number of subscribed podcasts),
805 plus a constant number of initial queries (when the first episode is
808 cmp_key
= lambda episode
: episode
.released
or datetime(2000, 01, 01)
810 podcasts
= list(self
.get_subscribed_podcasts())
811 podcasts
= filter(lambda p
: p
.latest_episode_timestamp
, podcasts
)
812 podcasts
= sorted(podcasts
, key
=lambda p
: p
.latest_episode_timestamp
,
815 podcast_dict
= dict((p
.get_id(), p
) for p
in podcasts
)
817 # contains the un-yielded episodes, newest first
820 for podcast
in podcasts
:
824 for episode
in episodes
:
825 # determine for which episodes there won't be a new episodes
826 # that is newer; those can be yielded
827 if episode
.released
> podcast
.latest_episode_timestamp
:
828 p
= podcast_dict
.get(episode
.podcast
, None)
829 yield proxy_object(episode
, podcast
=p
)
830 yielded_episodes
+= 1
834 # remove the episodes that have been yielded before
835 episodes
= episodes
[yielded_episodes
:]
837 # fetch and merge episodes for the next podcast
838 new_episodes
= list(podcast
.get_episodes(since
=1, until
=max_date
,
839 descending
=True, limit
=max_per_podcast
))
840 episodes
= sorted(episodes
+new_episodes
, key
=cmp_key
, reverse
=True)
843 # yield the remaining episodes
844 for episode
in episodes
:
845 podcast
= podcast_dict
.get(episode
.podcast
, None)
846 yield proxy_object(episode
, podcast
=podcast
)
850 def save(self
, *args
, **kwargs
):
851 super(User
, self
).save(*args
, **kwargs
)
853 podcast_states
= PodcastUserState
.for_user(self
)
854 for state
in podcast_states
:
855 @repeat_on_conflict(['state'])
856 def _update_state(state
):
857 old_devs
= set(state
.disabled_devices
)
858 state
.set_device_state(self
.devices
)
860 if old_devs
!= set(state
.disabled_devices
):
863 _update_state(state
=state
)
868 def __eq__(self
, other
):
872 # ensure that other isn't AnonymousUser
873 return other
.is_authenticated() and self
._id
== other
._id
877 return 'User %s' % self
._id
880 class History(object):
882 def __init__(self
, user
, device
):
885 self
._db
= EpisodeUserState
.get_db()
888 self
._view
= 'history/by_device'
889 self
._startkey
= [self
.user
._id
, device
.id, None]
890 self
._endkey
= [self
.user
._id
, device
.id, {}]
892 self
._view
= 'history/by_user'
893 self
._startkey
= [self
.user
._id
, None]
894 self
._endkey
= [self
.user
._id
, {}]
897 def __getitem__(self
, key
):
899 if isinstance(key
, slice):
900 start
= key
.start
or 0
901 length
= key
.stop
- start
906 res
= self
._db
.view(self
._view
,
908 startkey
= self
._endkey
,
909 endkey
= self
._startkey
,
915 action
= action
['value']
916 yield HistoryEntry
.from_action_dict(action
)
920 class HistoryEntry(object):
921 """ A class that can represent subscription and episode actions """
925 def from_action_dict(cls
, action
):
927 entry
= HistoryEntry()
929 if 'timestamp' in action
:
930 ts
= action
.pop('timestamp')
931 entry
.timestamp
= dateutil
.parser
.parse(ts
)
933 for key
, value
in action
.items():
934 setattr(entry
, key
, value
)
941 return getattr(self
, 'position', None)
945 def fetch_data(cls
, user
, entries
,
946 podcasts
=None, episodes
=None):
947 """ Efficiently loads additional data for a number of entries """
951 podcast_ids
= [getattr(x
, 'podcast_id', None) for x
in entries
]
952 podcast_ids
= filter(None, podcast_ids
)
953 podcasts
= get_to_dict(Podcast
, podcast_ids
, get_id
=Podcast
.get_id
)
957 episode_ids
= [getattr(x
, 'episode_id', None) for x
in entries
]
958 episode_ids
= filter(None, episode_ids
)
959 episodes
= get_to_dict(Episode
, episode_ids
)
962 # does not need pre-populated data because no db-access is required
963 device_ids
= [getattr(x
, 'device_id', None) for x
in entries
]
964 device_ids
= filter(None, device_ids
)
965 devices
= dict([ (id, user
.get_device(id)) for id in device_ids
])
968 for entry
in entries
:
969 podcast_id
= getattr(entry
, 'podcast_id', None)
970 entry
.podcast
= podcasts
.get(podcast_id
, None)
972 episode_id
= getattr(entry
, 'episode_id', None)
973 entry
.episode
= episodes
.get(episode_id
, None)
976 device
= devices
.get(getattr(entry
, 'device_id', None), None)
977 entry
.device
= device