fix exception when iterating grouped devices
[mygpo.git] / mygpo / users / sync.py
blobf1ddf6093b24050ab8a2c5a0a21be2abc9583a93
1 from collections import namedtuple
3 from couchdbkit.ext.django.schema import *
5 from mygpo.core.models import Podcast, SubscriptionException
6 from mygpo.log import log
7 from mygpo.db.couchdb.podcast import podcasts_to_dict
10 GroupedDevices = namedtuple('GroupedDevices', 'is_synced devices')
14 class SyncedDevicesMixin(DocumentSchema):
15 """ Contains the device-syncing functionality of a user """
17 sync_groups = ListProperty()
20 def get_grouped_devices(self):
21 """ Returns groups of synced devices and a unsynced group """
23 indexed_devices = dict( (dev.id, dev) for dev in self.active_devices )
25 for group in self.sync_groups:
27 devices = [indexed_devices.pop(device_id, None) for device_id in group]
28 devices = filter(None, devices)
29 if not devices:
30 continue
32 yield GroupedDevices(
33 True,
34 devices
37 # un-synced devices
38 yield GroupedDevices(
39 False,
40 indexed_devices.values()
44 def sync_devices(self, device1, device2):
45 """ Puts two devices in a common sync group"""
47 devices = set([device1, device2])
48 if not devices.issubset(set(self.devices)):
49 raise ValueError('the devices do not belong to the user')
51 sg1 = self.get_device_sync_group(device1)
52 sg2 = self.get_device_sync_group(device2)
54 if sg1 is not None and sg2 is not None:
55 # merge sync_groups
56 self.sync_groups[sg1].extend(self.sync_groups[sg2])
57 self.sync_groups.pop(sg2)
59 elif sg1 is None and sg2 is None:
60 self.sync_groups.append([device1.id, device2.id])
62 elif sg1 is not None:
63 self.sync_groups[sg1].append(device2.id)
65 elif sg2 is not None:
66 self.sync_groups[sg2].append(device1.id)
69 def unsync_device(self, device):
70 """ Removts the device from its sync-group
72 Raises a ValueError if the device is not synced """
74 sg = self.get_device_sync_group(device)
76 if sg is None:
77 raise ValueError('the device is not synced')
79 group = self.sync_groups[sg]
81 if len(group) <= 2:
82 self.sync_groups.pop(sg)
84 else:
85 group.remove(device.id)
88 def get_device_sync_group(self, device):
89 """ Returns the sync-group Id of the device """
91 for n, group in enumerate(self.sync_groups):
92 if device.id in group:
93 return n
96 def is_synced(self, device):
97 return self.get_device_sync_group(device) is not None
100 def get_synced(self, device):
101 """ Returns the devices that are synced with the given one """
103 sg = self.get_device_sync_group(device)
105 if sg is None:
106 return []
108 devices = self.get_devices_in_group(sg)
109 devices.remove(device)
110 return devices
114 def get_sync_targets(self, device):
115 """ Returns the devices and groups with which the device can be synced
117 Groups are represented as lists of devices """
119 sg = self.get_device_sync_group(device)
121 for n, group in enumerate(self.get_grouped_devices()):
123 if sg == n:
124 # the device's group can't be a sync-target
125 continue
127 elif group.is_synced:
128 yield group.devices
130 else:
131 # every unsynced device is a sync-target
132 for dev in group.devices:
133 if not dev == device:
134 yield dev
137 def get_devices_in_group(self, sg):
138 """ Returns the devices in the group with the given Id """
140 ids = self.sync_groups[sg]
141 return map(self.get_device, ids)
144 def sync_group(self, device):
145 """ Sync the group of the device """
147 group_index = self.get_device_sync_group(device)
149 if group_index is None:
150 return
152 group_state = self.get_group_state(group_index)
154 for device in self.get_devices_in_group(group_index):
155 sync_actions = self.get_sync_actions(device, group_state)
156 self.apply_sync_actions(device, sync_actions)
159 def apply_sync_actions(self, device, sync_actions):
160 """ Applies the sync-actions to the device """
162 add, rem = sync_actions
164 podcasts = podcasts_to_dict(add + rem)
166 for podcast_id in add:
167 podcast = podcasts.get(podcast_id, None)
168 if podcast is None:
169 continue
170 try:
171 podcast.subscribe(self, device)
172 except SubscriptionException as e:
173 log('Web: %(username)s: cannot sync device: %(error)s' %
174 dict(username=self.username, error=repr(e)))
176 for podcast_id in rem:
177 podcast = podcasts.get(podcast_id, None)
178 if not podcast:
179 continue
181 try:
182 podcast.unsubscribe(self, device)
183 except SubscriptionException as e:
184 log('Web: %(username)s: cannot sync device: %(error)s' %
185 dict(username=self.username, error=repr(e)))
188 def get_group_state(self, group_index):
189 """ Returns the group's subscription state
191 The state is represented by the latest actions for each podcast """
193 device_ids = self.sync_groups[group_index]
194 devices = [self.get_device(device_id) for device_id in device_ids]
196 state = {}
198 for d in devices:
199 actions = dict(d.get_latest_changes())
200 for podcast_id, action in actions.items():
201 if not podcast_id in state or \
202 action.timestamp > state[podcast_id].timestamp:
203 state[podcast_id] = action
205 return state
208 def get_sync_actions(self, device, group_state):
209 """ Get the actions required to bring the device to the group's state
211 After applying the actions the device reflects the group's state """
213 sg = self.get_device_sync_group(device)
214 if sg is None:
215 return [], []
217 # Filter those that describe actual changes to the current state
218 add, rem = [], []
219 current_state = dict(device.get_latest_changes())
221 for podcast_id, action in group_state.items():
223 # Sync-Actions must be newer than current state
224 if podcast_id in current_state and \
225 action.timestamp <= current_state[podcast_id].timestamp:
226 continue
228 # subscribe only what hasn't been subscribed before
229 if action.action == 'subscribe' and \
230 (podcast_id not in current_state or \
231 current_state[podcast_id].action == 'unsubscribe'):
232 add.append(podcast_id)
234 # unsubscribe only what has been subscribed before
235 elif action.action == 'unsubscribe' and \
236 podcast_id in current_state and \
237 current_state[podcast_id].action == 'subscribe':
238 rem.append(podcast_id)
240 return add, rem