refactor retrieval of devices
[mygpo.git] / mygpo / users / sync.py
blob402ef005af5ddbb13788acd70c54c71ce00441c9
1 from collections import namedtuple
3 from couchdbkit.ext.django.schema import *
5 from mygpo.core.models import Podcast, SubscriptionException
6 from mygpo.db.couchdb.podcast import podcasts_to_dict
8 import logging
9 logger = logging.getLogger(__name__)
12 GroupedDevices = namedtuple('GroupedDevices', 'is_synced devices')
16 class SyncedDevicesMixin(DocumentSchema):
17 """ Contains the device-syncing functionality of a user """
19 sync_groups = ListProperty()
22 def get_grouped_devices(self):
23 """ Returns groups of synced devices and a unsynced group """
25 indexed_devices = dict( (dev.id, dev) for dev in self.active_devices )
27 for group in self.sync_groups:
29 devices = [indexed_devices.pop(device_id, None) for device_id in group]
30 devices = filter(None, devices)
31 if not devices:
32 continue
34 yield GroupedDevices(
35 True,
36 devices
39 # un-synced devices
40 yield GroupedDevices(
41 False,
42 indexed_devices.values()
46 def sync_devices(self, device1, device2):
47 """ Puts two devices in a common sync group"""
49 devices = set([device1, device2])
50 if not devices.issubset(set(self.devices)):
51 raise ValueError('the devices do not belong to the user')
53 sg1 = self.get_device_sync_group(device1)
54 sg2 = self.get_device_sync_group(device2)
56 if sg1 is not None and sg2 is not None:
57 # merge sync_groups
58 self.sync_groups[sg1].extend(self.sync_groups[sg2])
59 self.sync_groups.pop(sg2)
61 elif sg1 is None and sg2 is None:
62 self.sync_groups.append([device1.id, device2.id])
64 elif sg1 is not None:
65 self.sync_groups[sg1].append(device2.id)
67 elif sg2 is not None:
68 self.sync_groups[sg2].append(device1.id)
71 def unsync_device(self, device):
72 """ Removts the device from its sync-group
74 Raises a ValueError if the device is not synced """
76 sg = self.get_device_sync_group(device)
78 if sg is None:
79 raise ValueError('the device is not synced')
81 group = self.sync_groups[sg]
83 if len(group) <= 2:
84 self.sync_groups.pop(sg)
86 else:
87 group.remove(device.id)
90 def get_device_sync_group(self, device):
91 """ Returns the sync-group Id of the device """
93 for n, group in enumerate(self.sync_groups):
94 if device.id in group:
95 return n
98 def is_synced(self, device):
99 return self.get_device_sync_group(device) is not None
102 def get_synced(self, device):
103 """ Returns the devices that are synced with the given one """
105 sg = self.get_device_sync_group(device)
107 if sg is None:
108 return []
110 devices = self.get_devices_in_group(sg)
111 devices.remove(device)
112 return devices
116 def get_sync_targets(self, device):
117 """ Returns the devices and groups with which the device can be synced
119 Groups are represented as lists of devices """
121 sg = self.get_device_sync_group(device)
123 for n, group in enumerate(self.get_grouped_devices()):
125 if sg == n:
126 # the device's group can't be a sync-target
127 continue
129 elif group.is_synced:
130 yield group.devices
132 else:
133 # every unsynced device is a sync-target
134 for dev in group.devices:
135 if not dev == device:
136 yield dev
139 def get_devices_in_group(self, sg):
140 """ Returns the devices in the group with the given Id """
142 ids = self.sync_groups[sg]
143 return map(self.get_device, ids)
146 def sync_group(self, device):
147 """ Sync the group of the device """
149 group_index = self.get_device_sync_group(device)
151 if group_index is None:
152 return
154 group_state = self.get_group_state(group_index)
156 for device in self.get_devices_in_group(group_index):
157 sync_actions = self.get_sync_actions(device, group_state)
158 self.apply_sync_actions(device, sync_actions)
161 def apply_sync_actions(self, device, sync_actions):
162 """ Applies the sync-actions to the device """
164 add, rem = sync_actions
166 podcasts = podcasts_to_dict(add + rem)
168 for podcast_id in add:
169 podcast = podcasts.get(podcast_id, None)
170 if podcast is None:
171 continue
172 try:
173 podcast.subscribe(self, device)
174 except SubscriptionException as e:
175 logger.warn('Web: %(username)s: cannot sync device: %(error)s' %
176 dict(username=self.username, error=repr(e)))
178 for podcast_id in rem:
179 podcast = podcasts.get(podcast_id, None)
180 if not podcast:
181 continue
183 try:
184 podcast.unsubscribe(self, device)
185 except SubscriptionException as e:
186 logger.warn('Web: %(username)s: cannot sync device: %(error)s' %
187 dict(username=self.username, error=repr(e)))
190 def get_group_state(self, group_index):
191 """ Returns the group's subscription state
193 The state is represented by the latest actions for each podcast """
195 device_ids = self.sync_groups[group_index]
196 devices = self.get_devices(device_ids)
198 state = {}
200 for d in devices:
201 actions = dict(d.get_latest_changes())
202 for podcast_id, action in actions.items():
203 if not podcast_id in state or \
204 action.timestamp > state[podcast_id].timestamp:
205 state[podcast_id] = action
207 return state
210 def get_sync_actions(self, device, group_state):
211 """ Get the actions required to bring the device to the group's state
213 After applying the actions the device reflects the group's state """
215 sg = self.get_device_sync_group(device)
216 if sg is None:
217 return [], []
219 # Filter those that describe actual changes to the current state
220 add, rem = [], []
221 current_state = dict(device.get_latest_changes())
223 for podcast_id, action in group_state.items():
225 # Sync-Actions must be newer than current state
226 if podcast_id in current_state and \
227 action.timestamp <= current_state[podcast_id].timestamp:
228 continue
230 # subscribe only what hasn't been subscribed before
231 if action.action == 'subscribe' and \
232 (podcast_id not in current_state or \
233 current_state[podcast_id].action == 'unsubscribe'):
234 add.append(podcast_id)
236 # unsubscribe only what has been subscribed before
237 elif action.action == 'unsubscribe' and \
238 podcast_id in current_state and \
239 current_state[podcast_id].action == 'subscribe':
240 rem.append(podcast_id)
242 return add, rem