avoid update conflict when saving a Device
[mygpo.git] / mygpo / migrate.py
blob34fb705ede7f8bf48e004d5e91c4d9e6e5be0b55
1 from datetime import datetime
2 from couchdbkit import Server, Document
4 from mygpo.core.models import Podcast, PodcastGroup, Episode, SubscriberData
5 from mygpo.users.models import Rating, EpisodeAction, User, Device, SubscriptionAction, EpisodeUserState, PodcastUserState
6 from mygpo.log import log
7 from mygpo import utils
8 from mygpo.decorators import repeat_on_conflict
10 """
11 This module contains methods for converting objects from the old
12 ORM-based backend to the CouchDB-based backend
13 """
16 def save_podcast_signal(sender, instance=False, **kwargs):
17 """
18 Signal-handler for creating/updating a CouchDB-based podcast when
19 an ORM-based podcast has been saved
20 """
21 if not instance:
22 return
24 try:
25 newp = Podcast.for_oldid(instance.id)
26 if newp:
27 update_podcast(oldp=instance, newp=newp)
28 else:
29 create_podcast(instance)
31 except Exception, e:
32 log('error while updating CouchDB-Podcast: %s' % repr(e))
35 def delete_podcast_signal(sender, instance=False, **kwargs):
36 """
37 Signal-handler for deleting a CouchDB-based podcast when an ORM-based
38 podcast is deleted
39 """
40 if not instance:
41 return
43 try:
44 newp = Podcast.for_oldid(instance.id)
45 if newp:
46 newp.delete()
48 except Exception, e:
49 log('error while deleting CouchDB-Podcast: %s' % repr(e))
53 def save_device_signal(sender, instance=False, **kwargs):
55 if not instance:
56 return
58 dev = get_or_migrate_device(instance)
59 d = update_device(instance, dev)
60 user = get_or_migrate_user(instance.user)
61 user.set_device(d)
62 user.save()
64 podcast_states = PodcastUserState.for_user(instance.user)
65 for state in podcast_states:
67 @repeat_on_conflict(['state'])
68 def update_state(state):
69 if not state.ref_url:
70 podcast = Podcast.get(state.podcast)
71 if not podcast or not podcast.url:
72 return
73 state.ref_url = podcast.url
75 state.set_device_state(dev)
76 state.save()
78 update_state(state=state)
81 def delete_device_signal(sender, instance=False, **kwargs):
82 if not instance:
83 return
85 user = get_or_migrate_user(instance.user)
86 dev = get_or_migrate_device(instance)
87 user.remove_device(dev)
88 user.save()
92 def save_episode_signal(sender, instance=False, **kwargs):
93 """
94 Signal-handler for creating/updating a CouchDB-based episode when
95 an ORM-based episode has been saved
96 """
97 if not instance:
98 return
100 try:
101 newe = Episode.for_oldid(instance.id)
102 newp = Podcast.get(newe.podcast)
104 if newe:
105 update_episode(instance, newe, newp)
106 else:
107 create_episode(instance)
109 except Exception, e:
110 log('error while updating CouchDB Episode: %s' % repr(e))
114 @repeat_on_conflict(['newp'], reload_f=lambda x: Podcast.get(x.get_id()))
115 def update_podcast(oldp, newp):
117 Updates newp based on oldp and returns True if an update was necessary
119 updated = False
121 # Update related podcasts
122 from mygpo.data.models import RelatedPodcast
123 if newp._id:
124 rel_podcast = set([r.rel_podcast for r in RelatedPodcast.objects.filter(ref_podcast=oldp)])
125 rel = list(podcasts_to_ids(rel_podcast))
126 if newp.related_podcasts != rel:
127 newp.related_podcasts = rel
128 updated = True
130 # Update Group-assignment
131 if oldp.group:
132 group = get_group(oldp.group)
133 if not newp in list(group.podcasts):
134 newp = group.add_podcast(newp)
135 updated = True
137 # Update subscriber-data
138 from mygpo.data.models import HistoricPodcastData
139 sub = HistoricPodcastData.objects.filter(podcast=oldp).order_by('date')
140 if sub.count() and len(newp.subscribers) != sub.count():
141 transf = lambda s: SubscriberData(
142 timestamp = datetime(s.date.year, s.date.month, s.date.day),
143 subscriber_count = s.subscriber_count)
144 check = lambda s: s.date.weekday() == 6
146 newp.subscribers = newp.subscribers + map(transf, filter(check, sub))
147 newp.subscribers = utils.set_cmp(newp.subscribers, lambda x: x.timestamp)
148 newp.subscribers = list(sorted(set(newp.subscribers), key=lambda s: s.timestamp))
149 updated = True
151 PROPERTIES = ('language', 'content_types', 'title',
152 'description', 'link', 'last_update', 'logo_url',
153 'author', 'group_member_name')
155 for p in PROPERTIES:
156 if getattr(newp, p, None) != getattr(oldp, p, None):
157 setattr(newp, p, getattr(oldp, p, None))
158 updated = True
160 if not oldp.url in newp.urls:
161 newp.urls.append(oldp.url)
162 updated = True
164 if updated:
165 newp.save()
167 return updated
170 def create_podcast(oldp, sparse=False):
172 Creates a (CouchDB) Podcast document from a (ORM) Podcast object
174 p = Podcast()
175 p.oldid = oldp.id
176 p.save()
177 if not sparse:
178 update_podcast(oldp=oldp, newp=p)
180 return p
183 def get_group(oldg):
184 group = PodcastGroup.for_oldid(oldg.id)
185 if not group:
186 group = create_podcastgroup(oldg)
188 return group
191 def create_podcastgroup(oldg):
193 Creates a (CouchDB) PodcastGroup document from a
194 (ORM) PodcastGroup object
196 g = PodcastGroup()
197 g.oldid = oldg.id
198 update_podcastgroup(oldg, g)
199 g.save()
200 return g
204 @repeat_on_conflict(['newg'])
205 def update_podcastgroup(oldg, newg):
207 if newg.title != oldg.title:
208 newg.title = oldg.title
209 newg.save()
210 return True
212 return False
215 def get_blacklist(blacklist):
217 Returns a list of Ids of all blacklisted podcasts
219 blacklisted = [b.podcast for b in blacklist]
220 blacklist_ids = []
221 for p in blacklisted:
222 newp = Podcast.for_oldid(p.id)
223 if not newp:
224 newp = create_podcast(p)
226 blacklist_ids.append(newp._id)
227 return blacklist_ids
230 def get_ratings(ratings):
232 Returns a list of Rating-objects, based on the relational Ratings
234 conv = lambda r: Rating(rating=r.rating, timestamp=r.timestamp)
235 return map(conv, ratings)
238 def podcasts_to_ids(podcasts):
239 for p in podcasts:
240 podcast = Podcast.for_oldid(p.id)
241 if not podcast:
242 podcast = create_podcast(p, sparse=True)
243 yield podcast.get_id()
246 def get_or_migrate_podcast(oldp):
247 return Podcast.for_oldid(oldp.id) or create_podcast(oldp)
250 def create_episode_action(action):
251 a = EpisodeAction()
252 a.action = action.action
253 a.timestamp = action.timestamp
254 a.device_oldid = action.device.id if action.device else None
255 a.started = action.started
256 a.playmark = action.playmark
257 return a
259 def create_episode(olde, sparse=False):
260 podcast = get_or_migrate_podcast(olde.podcast)
261 e = Episode()
262 e.oldid = olde.id
263 e.urls.append(olde.url)
264 e.podcast = podcast.get_id()
266 if not sparse:
267 update_episode(olde, e, podcast)
269 e.save()
271 return e
274 def get_or_migrate_episode(olde):
275 return Episode.for_oldid(olde.id) or create_episode(olde)
278 def update_episode(olde, newe, podcast):
279 updated = False
281 if not olde.url in newe.urls:
282 newe.urls.append(olde.url)
283 updated = False
285 PROPERTIES = ('title', 'description', 'link',
286 'author', 'duration', 'filesize', 'language',
287 'last_update')
289 for p in PROPERTIES:
290 if getattr(newe, p, None) != getattr(olde, p, None):
291 setattr(newe, p, getattr(olde, p, None))
292 updated = True
294 if newe.outdated != olde.outdated:
295 newe.outdated = bool(olde.outdated)
296 updated = True
298 if newe.released != olde.timestamp:
299 newe.released = olde.timestamp
300 updated = True
302 if olde.mimetype and not olde.mimetype in newe.mimetypes:
303 newe.mimetypes.append(olde.mimetype)
304 updated = True
306 @repeat_on_conflict(['newe'])
307 def save(newe):
308 newe.save()
310 if updated:
311 save(newe=newe)
313 return updated
316 def get_or_migrate_user(user):
317 u = User.for_oldid(user.id)
318 if u:
319 return u
321 u = User()
322 u.oldid = user.id
323 u.username = user.username
324 u.save()
325 return u
328 def get_or_migrate_device(device, user=None):
329 return Device.for_oldid(device.id) or create_device(device)
332 def create_device(oldd, sparse=False):
333 user = get_or_migrate_user(oldd.user)
335 d = Device()
336 d.oldid = oldd.id
338 if not sparse:
339 update_device(oldd, d)
341 user.devices.append(d)
342 user.save()
343 return d
346 def update_device(oldd, newd):
347 newd.uid = oldd.uid
348 newd.name = oldd.name
349 newd.type = oldd.type
350 newd.deleted = bool(oldd.deleted)
351 return newd
354 def migrate_subscription_action(old_action):
355 action = SubscriptionAction()
356 action.timestamp = old_action.timestamp
357 action.action = 'subscribe' if old_action.action == 1 else 'unsubscribe'
358 action.device = get_or_migrate_device(old_action.device).id
359 return action
362 def get_episode_user_state(user, episode, podcast):
363 e_state = EpisodeUserState.for_user_episode(user.id, episode._id)
365 if e_state is None:
367 p_state = PodcastUserState.for_user_podcast(user, podcast)
368 e_state = p_state.episodes.get(episode._id, None)
370 if e_state is None:
371 e_state = EpisodeUserState()
372 e_state.episode = episode._id
373 else:
374 @repeat_on_conflict(['p_state'])
375 def remove_episode_status(p_state):
376 del p_state.episodes[episode._id]
377 p_state.save()
379 remove_episode_status(p_state=p_state)
382 if not e_state.podcast_ref_url:
383 e_state.podcast_ref_url = podcast.url
385 if not e_state.ref_url:
386 e_state.ref_url = episode.url
388 e_state.podcast = podcast.get_id()
389 e_state.user_oldid = user.id
390 e_state.save()
392 return e_state
395 def get_devices(user):
396 from mygpo.api.models import Device
397 return [get_or_migrate_device(dev) for dev in Device.objects.filter(user=user)]