1 from collections
import namedtuple
3 from couchdbkit
.ext
.django
.schema
import *
5 from mygpo
.core
.models
import Podcast
, SubscriptionException
8 logger
= logging
.getLogger(__name__
)
11 GroupedDevices
= namedtuple('GroupedDevices', 'is_synced devices')
15 class SyncedDevicesMixin(DocumentSchema
):
16 """ Contains the device-syncing functionality of a user """
18 sync_groups
= ListProperty()
21 def get_grouped_devices(self
):
22 """ Returns groups of synced devices and a unsynced group """
24 indexed_devices
= dict( (dev
.id, dev
) for dev
in self
.active_devices
)
26 for group
in self
.sync_groups
:
28 devices
= [indexed_devices
.pop(device_id
, None) for device_id
in group
]
29 devices
= filter(None, devices
)
41 indexed_devices
.values()
45 def sync_devices(self
, device1
, device2
):
46 """ Puts two devices in a common sync group"""
48 devices
= set([device1
, device2
])
49 if not devices
.issubset(set(self
.devices
)):
50 raise ValueError('the devices do not belong to the user')
52 sg1
= self
.get_device_sync_group(device1
)
53 sg2
= self
.get_device_sync_group(device2
)
55 if sg1
is not None and sg2
is not None:
57 self
.sync_groups
[sg1
].extend(self
.sync_groups
[sg2
])
58 self
.sync_groups
.pop(sg2
)
60 elif sg1
is None and sg2
is None:
61 self
.sync_groups
.append([device1
.id, device2
.id])
64 self
.sync_groups
[sg1
].append(device2
.id)
67 self
.sync_groups
[sg2
].append(device1
.id)
70 def unsync_device(self
, device
):
71 """ Removts the device from its sync-group
73 Raises a ValueError if the device is not synced """
75 sg
= self
.get_device_sync_group(device
)
78 raise ValueError('the device is not synced')
80 group
= self
.sync_groups
[sg
]
83 self
.sync_groups
.pop(sg
)
86 group
.remove(device
.id)
89 def get_device_sync_group(self
, device
):
90 """ Returns the sync-group Id of the device """
92 for n
, group
in enumerate(self
.sync_groups
):
93 if device
.id in group
:
97 def is_synced(self
, device
):
98 return self
.get_device_sync_group(device
) is not None
101 def get_synced(self
, device
):
102 """ Returns the devices that are synced with the given one """
104 sg
= self
.get_device_sync_group(device
)
109 devices
= self
.get_devices_in_group(sg
)
110 devices
.remove(device
)
115 def get_sync_targets(self
, device
):
116 """ Returns the devices and groups with which the device can be synced
118 Groups are represented as lists of devices """
120 sg
= self
.get_device_sync_group(device
)
122 for n
, group
in enumerate(self
.get_grouped_devices()):
125 # the device's group can't be a sync-target
128 elif group
.is_synced
:
132 # every unsynced device is a sync-target
133 for dev
in group
.devices
:
134 if not dev
== device
:
138 def get_devices_in_group(self
, sg
):
139 """ Returns the devices in the group with the given Id """
141 ids
= self
.sync_groups
[sg
]
142 return map(self
.get_device
, ids
)
145 def sync_group(self
, device
):
146 """ Sync the group of the device """
148 group_index
= self
.get_device_sync_group(device
)
150 if group_index
is None:
153 group_state
= self
.get_group_state(group_index
)
155 for device
in self
.get_devices_in_group(group_index
):
156 sync_actions
= self
.get_sync_actions(device
, group_state
)
157 self
.apply_sync_actions(device
, sync_actions
)
160 def apply_sync_actions(self
, device
, sync_actions
):
161 """ Applies the sync-actions to the device """
163 from mygpo
.db
.couchdb
.podcast_state
import subscribe
, unsubscribe
164 add
, rem
= sync_actions
166 podcasts
= Podcast
.objects
.filter(id__in
=(add
+rem
))
167 podcasts
= {podcast
.id: podcast
for podcast
in podcasts
}
169 for podcast_id
in add
:
170 podcast
= podcasts
.get(podcast_id
, None)
174 subscribe(podcast
, self
, device
)
175 except SubscriptionException
as e
:
176 logger
.warn('Web: %(username)s: cannot sync device: %(error)s' %
177 dict(username
=self
.username
, error
=repr(e
)))
179 for podcast_id
in rem
:
180 podcast
= podcasts
.get(podcast_id
, None)
185 unsubscribe(podcast
, self
, device
)
186 except SubscriptionException
as e
:
187 logger
.warn('Web: %(username)s: cannot sync device: %(error)s' %
188 dict(username
=self
.username
, error
=repr(e
)))
191 def get_group_state(self
, group_index
):
192 """ Returns the group's subscription state
194 The state is represented by the latest actions for each podcast """
196 device_ids
= self
.sync_groups
[group_index
]
197 devices
= self
.get_devices(device_ids
)
202 actions
= dict(d
.get_latest_changes())
203 for podcast_id
, action
in actions
.items():
204 if not podcast_id
in state
or \
205 action
.timestamp
> state
[podcast_id
].timestamp
:
206 state
[podcast_id
] = action
211 def get_sync_actions(self
, device
, group_state
):
212 """ Get the actions required to bring the device to the group's state
214 After applying the actions the device reflects the group's state """
216 sg
= self
.get_device_sync_group(device
)
220 # Filter those that describe actual changes to the current state
222 current_state
= dict(device
.get_latest_changes())
224 for podcast_id
, action
in group_state
.items():
226 # Sync-Actions must be newer than current state
227 if podcast_id
in current_state
and \
228 action
.timestamp
<= current_state
[podcast_id
].timestamp
:
231 # subscribe only what hasn't been subscribed before
232 if action
.action
== 'subscribe' and \
233 (podcast_id
not in current_state
or \
234 current_state
[podcast_id
].action
== 'unsubscribe'):
235 add
.append(podcast_id
)
237 # unsubscribe only what has been subscribed before
238 elif action
.action
== 'unsubscribe' and \
239 podcast_id
in current_state
and \
240 current_state
[podcast_id
].action
== 'subscribe':
241 rem
.append(podcast_id
)