[Migration] remove set_device()
[mygpo.git] / mygpo / users / sync.py
blob108cca9cdc668ebeafbc48fe3cf0995933bb0c17
1 from collections import namedtuple
3 from couchdbkit.ext.django.schema import *
5 from mygpo.podcasts.models import Podcast
7 import logging
8 logger = logging.getLogger(__name__)
11 GroupedDevices = namedtuple('GroupedDevices', 'is_synced devices')
14 def get_grouped_devices(user):
15 """ Returns groups of synced devices and a unsynced group """
17 from mygpo.users.models import Client
18 clients = Client.objects.filter(user=user, deleted=False)\
19 .order_by('-sync_group')
21 last_group = object()
22 group = None
24 for client in clients:
25 # check if we have just found a new group
26 if last_group != client.sync_group:
27 if group != None:
28 yield group
30 group = GroupedDevices(client.sync_group is not None, [])
32 last_group = client.sync_group
33 group.devices.append(client)
35 # yield remaining group
36 yield group
39 class SyncedDevicesMixin(DocumentSchema):
40 """ Contains the device-syncing functionality of a user """
42 sync_groups = ListProperty()
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 from mygpo.db.couchdb.podcast_state import subscribe, unsubscribe
165 from mygpo.users.models import SubscriptionException
166 add, rem = sync_actions
168 podcasts = Podcast.objects.filter(id__in=(add+rem))
169 podcasts = {podcast.id: podcast for podcast in podcasts}
171 for podcast_id in add:
172 podcast = podcasts.get(podcast_id, None)
173 if podcast is None:
174 continue
175 try:
176 subscribe(podcast, self, device)
177 except SubscriptionException as e:
178 logger.warn('Web: %(username)s: cannot sync device: %(error)s' %
179 dict(username=self.username, error=repr(e)))
181 for podcast_id in rem:
182 podcast = podcasts.get(podcast_id, None)
183 if not podcast:
184 continue
186 try:
187 unsubscribe(podcast, self, device)
188 except SubscriptionException as e:
189 logger.warn('Web: %(username)s: cannot sync device: %(error)s' %
190 dict(username=self.username, error=repr(e)))
193 def get_group_state(self, group_index):
194 """ Returns the group's subscription state
196 The state is represented by the latest actions for each podcast """
198 device_ids = self.sync_groups[group_index]
199 devices = self.get_devices(device_ids)
201 state = {}
203 for d in devices:
204 actions = dict(d.get_latest_changes())
205 for podcast_id, action in actions.items():
206 if not podcast_id in state or \
207 action.timestamp > state[podcast_id].timestamp:
208 state[podcast_id] = action
210 return state
213 def get_sync_actions(self, device, group_state):
214 """ Get the actions required to bring the device to the group's state
216 After applying the actions the device reflects the group's state """
218 sg = self.get_device_sync_group(device)
219 if sg is None:
220 return [], []
222 # Filter those that describe actual changes to the current state
223 add, rem = [], []
224 current_state = dict(device.get_latest_changes())
226 for podcast_id, action in group_state.items():
228 # Sync-Actions must be newer than current state
229 if podcast_id in current_state and \
230 action.timestamp <= current_state[podcast_id].timestamp:
231 continue
233 # subscribe only what hasn't been subscribed before
234 if action.action == 'subscribe' and \
235 (podcast_id not in current_state or \
236 current_state[podcast_id].action == 'unsubscribe'):
237 add.append(podcast_id)
239 # unsubscribe only what has been subscribed before
240 elif action.action == 'unsubscribe' and \
241 podcast_id in current_state and \
242 current_state[podcast_id].action == 'subscribe':
243 rem.append(podcast_id)
245 return add, rem