[Tests] Test slug functionality, fix Slug ordering
[mygpo.git] / mygpo / maintenance / merge.py
blob5533363056d940c12c0694609d45a0eb42574660
1 from itertools import chain, imap as map
2 import logging
3 from functools import partial
5 import restkit
7 from django.db import IntegrityError
9 from mygpo.podcasts.models import MergedUUID
10 from mygpo import utils
11 from mygpo.decorators import repeat_on_conflict
12 from mygpo.db.couchdb.podcast_state import all_podcast_states, \
13 delete_podcast_state, update_podcast_state_podcast, merge_podcast_states
14 from mygpo.db.couchdb.episode_state import all_episode_states, \
15 update_episode_state_object, add_episode_actions, delete_episode_state, \
16 merge_episode_states
18 import logging
19 logger = logging.getLogger(__name__)
22 class IncorrectMergeException(Exception):
23 pass
26 class PodcastMerger(object):
27 """ Merges podcasts and their related objects """
29 def __init__(self, podcasts, actions, groups):
30 """ Prepares to merge podcasts[1:] into podcasts[0] """
32 for n, podcast1 in enumerate(podcasts):
33 for m, podcast2 in enumerate(podcasts):
34 if podcast1 == podcast2 and n != m:
35 raise IncorrectMergeException(
36 "can't merge podcast %s into itself %s" % (podcast1.get_id(), podcast2.get_id()))
38 self.podcasts = podcasts
39 self.actions = actions
40 self.groups = groups
42 def merge(self):
43 """ Carries out the actual merging """
45 logger.info('Start merging of podcasts: %r', self.podcasts)
47 podcast1 = self.podcasts.pop(0)
48 logger.info('Merge target: %r', podcast1)
50 self.merge_episodes()
52 for podcast2 in self.podcasts:
53 logger.info('Merging %r into target', podcast2)
54 self.merge_states(podcast1, podcast2)
55 self.reassign_episodes(podcast1, podcast2)
56 self._merge_objs(podcast1=podcast1, podcast2=podcast2)
57 logger.info('Deleting %r', podcast2)
58 podcast2.delete()
59 self.actions['merge-podcast'] += 1
61 return podcast1
63 def merge_episodes(self):
64 """ Merges the episodes according to the groups """
66 for n, episodes in self.groups:
67 if not episodes:
68 continue
70 episode = episodes.pop(0)
71 for ep in episodes:
72 em = EpisodeMerger(episode, ep, self.actions)
73 em.merge()
75 def _merge_objs(self, podcast1, podcast2):
76 reassign_merged_uuids(podcast1, podcast2)
77 reassign_slugs(podcast1, podcast2)
78 reassign_urls(podcast1, podcast2)
79 podcast1.content_types = ','.join(podcast1.content_types.split(',') +
80 podcast2.content_types.split(','))
81 podcast1.save()
84 def reassign_episodes(self, podcast1, podcast2):
86 logger.info('Re-assigning episodes of %r into %r', podcast2, podcast1)
88 # re-assign episodes to new podcast
89 # if necessary, they will be merged later anyway
90 for e in podcast2.episode_set.all():
91 self.actions['reassign-episode'] += 1
93 for s in all_episode_states(e):
94 self.actions['reassign-episode-state'] += 1
96 update_episode_state_object(s, podcast1.get_id())
98 # TODO: change scopes?
99 e.podcast = podcast1
100 e.save()
102 def merge_states(self, podcast1, podcast2):
103 """Merges the Podcast states that are associated with the two Podcasts.
105 This should be done after two podcasts are merged
108 key = lambda x: x.user
109 states1 = sorted(all_podcast_states(podcast1), key=key)
110 states2 = sorted(all_podcast_states(podcast2), key=key)
112 logger.info('Merging %d podcast states of %r into %r', len(states2),
113 podcast2, podcast1)
115 for state, state2 in utils.iterate_together([states1, states2], key):
117 if state == state2:
118 continue
120 if state is None:
121 self.actions['move-podcast-state'] += 1
122 update_podcast_state_podcast(state2, podcast1.get_id(),
123 podcast1.url)
125 elif state2 is None:
126 continue
128 else:
129 psm = PodcastStateMerger(state, state2, self.actions)
130 psm.merge()
133 class EpisodeMerger(object):
134 """ Merges two episodes """
136 def __init__(self, episode1, episode2, actions):
137 """ episode2 will be merged into episode1 """
139 if episode1 == episode2:
140 raise IncorrectMergeException("can't merge episode into itself")
142 self.episode1 = episode1
143 self.episode2 = episode2
144 self.actions = actions
146 def merge(self):
147 logger.info('Merging episode %r into %r', self.episode2, self.episode1)
148 self._merge_objs(episode1=self.episode1, episode2=self.episode2)
149 self.merge_states(self.episode1, self.episode2)
150 logger.info('Deleting %r', self.episode2)
151 self.episode2.delete()
152 self.actions['merge-episode'] += 1
154 def _merge_objs(self, episode1, episode2):
155 reassign_urls(episode1, episode2)
156 reassign_merged_uuids(episode1, episode2)
157 reassign_slugs(episode1, episode2)
160 def merge_states(self, episode, episode2):
161 key = lambda x: x.user
162 states1 = sorted(all_episode_states(self.episode1), key=key)
163 states2 = sorted(all_episode_states(self.episode2), key=key)
165 logger.info('Merging %d episode states of %r into %r', len(states2),
166 episode2, episode)
168 for state, state2 in utils.iterate_together([states1, states2], key):
169 if state == state2:
170 continue
172 if state is None:
173 self.actions['move-episode-state'] += 1
174 update_episode_state_object(state2,
175 self.episode1.podcast.get_id(),
176 self.episode1.get_id())
178 elif state2 is None:
179 continue
181 else:
182 esm = EpisodeStateMerger(state, state2, self.actions)
183 esm.merge()
186 class PodcastStateMerger(object):
187 """Merges the two given podcast states"""
189 def __init__(self, state, state2, actions):
191 if state._id == state2._id:
192 raise IncorrectMergeException(
193 "can't merge podcast state into itself")
195 if state.user != state2.user:
196 raise IncorrectMergeException(
197 "states don't belong to the same user")
199 self.state = state
200 self.state2 = state2
201 self.actions = actions
203 def merge(self):
204 merge_podcast_states(self.state, self.state2)
205 self._add_actions(state=self.state, actions=self.state2.actions)
206 delete_podcast_state(self.state2)
207 self.actions['merged-podcast-state'] += 1
210 def _add_actions(self, state, actions):
211 try:
212 add_episode_actions(state, actions)
213 except restkit.Unauthorized:
214 # the merge could result in an invalid list of
215 # subscribe/unsubscribe actions -- we ignore it and
216 # just use the actions from state
217 return
220 class EpisodeStateMerger(object):
221 """ Merges state2 in state """
223 def __init__(self, state, state2, actions):
225 if state._id == state2._id:
226 raise IncorrectMergeException(
227 "can't merge episode state into itself")
229 if state.user != state2.user:
230 raise IncorrectMergeException(
231 "states don't belong to the same user")
233 self.state = state
234 self.state2 = state2
235 self.actions = actions
237 def merge(self):
238 merge_episode_states(self.state, self.state2)
239 delete_episode_state(self.state2)
240 self.actions['merge-episode-state'] += 1
243 def reassign_urls(obj1, obj2):
244 # Reassign all URLs of obj2 to obj1
245 max_order = max([0] + [u.order for u in obj1.urls.all()])
247 for n, url in enumerate(obj2.urls.all(), max_order+1):
248 url.content_object = obj1
249 url.order = n
250 url.scope = obj1.scope
251 try:
252 url.save()
253 except IntegrityError as ie:
254 logger.warn('Moving URL failed: %s. Deleting.', str(ie))
255 url.delete()
257 def reassign_merged_uuids(obj1, obj2):
258 # Reassign all IDs of obj2 to obj1
259 MergedUUID.objects.create(uuid=obj2.id, content_object=obj1)
260 for m in obj2.merged_uuids.all():
261 m.content_object = obj1
262 m.save()
264 def reassign_slugs(obj1, obj2):
265 # Reassign all Slugs of obj2 to obj1
266 max_order = max([0] + [s.order for s in obj1.slugs.all()])
267 for n, slug in enumerate(obj2.slugs.all(), max_order+1):
268 slug.content_object = obj1
269 slug.order = n
270 slug.scope = obj1.scope
271 try:
272 slug.save()
273 except IntegrityError as ie:
274 logger.warn('Moving Slug failed: %s. Deleting', str(ie))
275 slug.delete()