[Migration] refactor client syncing
[mygpo.git] / mygpo / users / sync.py
blob5bf6ad16ea0eac73303908b68b09f53f500313e8
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()
44 def get_device_sync_group(self, device):
45 """ Returns the sync-group Id of the device """
47 for n, group in enumerate(self.sync_groups):
48 if device.id in group:
49 return n
51 def get_devices_in_group(self, sg):
52 """ Returns the devices in the group with the given Id """
54 ids = self.sync_groups[sg]
55 return map(self.get_device, ids)
58 def sync_group(self, device):
59 """ Sync the group of the device """
61 group_index = self.get_device_sync_group(device)
63 if group_index is None:
64 return
66 group_state = self.get_group_state(group_index)
68 for device in self.get_devices_in_group(group_index):
69 sync_actions = self.get_sync_actions(device, group_state)
70 self.apply_sync_actions(device, sync_actions)
73 def apply_sync_actions(self, device, sync_actions):
74 """ Applies the sync-actions to the device """
76 from mygpo.db.couchdb.podcast_state import subscribe, unsubscribe
77 from mygpo.users.models import SubscriptionException
78 add, rem = sync_actions
80 podcasts = Podcast.objects.filter(id__in=(add+rem))
81 podcasts = {podcast.id: podcast for podcast in podcasts}
83 for podcast_id in add:
84 podcast = podcasts.get(podcast_id, None)
85 if podcast is None:
86 continue
87 try:
88 subscribe(podcast, self, device)
89 except SubscriptionException as e:
90 logger.warn('Web: %(username)s: cannot sync device: %(error)s' %
91 dict(username=self.username, error=repr(e)))
93 for podcast_id in rem:
94 podcast = podcasts.get(podcast_id, None)
95 if not podcast:
96 continue
98 try:
99 unsubscribe(podcast, self, device)
100 except SubscriptionException as e:
101 logger.warn('Web: %(username)s: cannot sync device: %(error)s' %
102 dict(username=self.username, error=repr(e)))
105 def get_group_state(self, group_index):
106 """ Returns the group's subscription state
108 The state is represented by the latest actions for each podcast """
110 device_ids = self.sync_groups[group_index]
111 devices = self.get_devices(device_ids)
113 state = {}
115 for d in devices:
116 actions = dict(d.get_latest_changes())
117 for podcast_id, action in actions.items():
118 if not podcast_id in state or \
119 action.timestamp > state[podcast_id].timestamp:
120 state[podcast_id] = action
122 return state
125 def get_sync_actions(self, device, group_state):
126 """ Get the actions required to bring the device to the group's state
128 After applying the actions the device reflects the group's state """
130 sg = self.get_device_sync_group(device)
131 if sg is None:
132 return [], []
134 # Filter those that describe actual changes to the current state
135 add, rem = [], []
136 current_state = dict(device.get_latest_changes())
138 for podcast_id, action in group_state.items():
140 # Sync-Actions must be newer than current state
141 if podcast_id in current_state and \
142 action.timestamp <= current_state[podcast_id].timestamp:
143 continue
145 # subscribe only what hasn't been subscribed before
146 if action.action == 'subscribe' and \
147 (podcast_id not in current_state or \
148 current_state[podcast_id].action == 'unsubscribe'):
149 add.append(podcast_id)
151 # unsubscribe only what has been subscribed before
152 elif action.action == 'unsubscribe' and \
153 podcast_id in current_state and \
154 current_state[podcast_id].action == 'subscribe':
155 rem.append(podcast_id)
157 return add, rem