Skip to content

Commit f10e071

Browse files
committed
Fix podcast merging
1 parent cebbc01 commit f10e071

5 files changed

Lines changed: 68 additions & 35 deletions

File tree

mygpo/administration/group.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,18 @@ def __get_episodes(self):
2929

3030

3131
def group(self, get_features):
32+
""" Groups the episodes by features extracted using ``get_features``
33+
34+
get_features is a callable that expects an episode as parameter, and
35+
returns a value representing the extracted feature(s).
36+
"""
3237

3338
episodes = self.__get_episodes()
3439

3540
episode_groups = defaultdict(list)
3641

37-
episode_features = map(get_features, episodes.items())
38-
39-
for features, episode_id in episode_features:
40-
episode = episodes[episode_id]
42+
for episode in episodes.values():
43+
features = get_features(episode)
4144
episode_groups[features].append(episode)
4245

4346
groups = sorted(episode_groups.values(), key=_SORT_KEY)

mygpo/administration/templates/admin/merge-grouping.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ <h1>{% trans "Merge Podcasts and Episodes" %}</h1>
4747
<td>
4848
{% for episode in episodes %}
4949
{% if episode.podcast.get_id == podcast.get_id %}
50-
<input type="text" name="episode_{{ gepisode.id }}" value="{{ n }}" size="2"/>
50+
<input type="text" name="episode_{{ episode.get_id }}" value="{{ n }}" size="2"/>
5151
{% episode_link episode podcast %}<br />
5252
{% endif %}
5353
{% endfor %}

mygpo/administration/views.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,10 @@ def post(self, request):
140140

141141
grouper = PodcastGrouper(podcasts)
142142

143-
get_features = lambda id_e: ((id_e[1].url, id_e[1].title), id_e[0])
143+
get_features = lambda episode: (episode.url, episode.title)
144144

145145
num_groups = grouper.group(get_features)
146146

147-
148147
except InvalidPodcast as ip:
149148
messages.error(request,
150149
_('No podcast with URL {url}').format(url=str(ip)))
@@ -178,10 +177,10 @@ def post(self, request):
178177
for key, feature in request.POST.items():
179178
m = self.RE_EPISODE.match(key)
180179
if m:
181-
episode_id = m.group(1)
180+
episode_id = uuid.UUID(m.group(1))
182181
features[episode_id] = feature
183182

184-
get_features = lambda id_e: (features.get(id_e[0], id_e[0]), id_e[0])
183+
get_features = lambda episode: features[episode.id]
185184

186185
num_groups = grouper.group(get_features)
187186
queue_id = request.POST.get('queue_id', '')

mygpo/maintenance/merge.py

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from mygpo.history.models import HistoryEntry, EpisodeHistoryEntry
1313
from mygpo.publisher.models import PublishedPodcast
1414
from mygpo.subscriptions.models import Subscription
15+
from . import models
1516

1617
import logging
1718
logger = logging.getLogger(__name__)
@@ -68,7 +69,7 @@ def merge_episodes(self):
6869

6970
# based on https://djangosnippets.org/snippets/2283/
7071
@transaction.atomic
71-
def merge_model_objects(primary_object, alias_objects=[], keep_old=False):
72+
def merge_model_objects(primary_object, alias_objects, keep_old=False):
7273
"""
7374
Use this function to merge model objects (i.e. Users, Organizations, Polls,
7475
etc.) and migrate all of the related fields from the alias objects to the
@@ -78,10 +79,8 @@ def merge_model_objects(primary_object, alias_objects=[], keep_old=False):
7879
from django.contrib.auth.models import User
7980
primary_user = User.objects.get(email='good_email@example.com')
8081
duplicate_user = User.objects.get(email='good_email+duplicate@example.com')
81-
merge_model_objects(primary_user, duplicate_user)
82+
merge_model_objects(primary_user, [duplicate_user])
8283
"""
83-
if not isinstance(alias_objects, list):
84-
alias_objects = [alias_objects]
8584

8685
# check that all aliases are the same class as primary one and that
8786
# they are subclass of model
@@ -105,11 +104,6 @@ def merge_model_objects(primary_object, alias_objects=[], keep_old=False):
105104
for field_name, field in fields:
106105
generic_fields.append(field)
107106

108-
blank_local_fields = set(
109-
[field.attname for field
110-
in primary_object._meta.local_fields
111-
if getattr(primary_object, field.attname) in [None, '']])
112-
113107
# Loop through all alias objects and migrate their data to
114108
# the primary object.
115109
for alias_object in alias_objects:
@@ -123,8 +117,9 @@ def merge_model_objects(primary_object, alias_objects=[], keep_old=False):
123117
related_objects = getattr(alias_object, alias_varname)
124118
for obj in related_objects.all():
125119
setattr(obj, obj_varname, primary_object)
126-
reassigned(obj, primary_object)
127-
obj.save()
120+
deleted = reassigned(obj, primary_object)
121+
if not deleted:
122+
obj.save()
128123

129124
# Migrate all many to many references from alias object to
130125
# primary object.
@@ -143,8 +138,9 @@ def merge_model_objects(primary_object, alias_objects=[], keep_old=False):
143138
obj_varname).all()
144139
for obj in related_many_objects.all():
145140
getattr(obj, obj_varname).remove(alias_object)
146-
reassigned(obj, primary_object)
147-
getattr(obj, obj_varname).add(primary_object)
141+
deleted = reassigned(obj, primary_object)
142+
if not deleted:
143+
getattr(obj, obj_varname).add(primary_object)
148144

149145
# Migrate all generic foreign key references from alias
150146
# object to primary object.
@@ -156,7 +152,10 @@ def merge_model_objects(primary_object, alias_objects=[], keep_old=False):
156152
related = field.model.objects.filter(**filter_kwargs)
157153
for generic_related_object in related:
158154
setattr(generic_related_object, field.name, primary_object)
159-
reassigned(generic_related_object, primary_object)
155+
deleted = reassigned(generic_related_object, primary_object)
156+
if deleted:
157+
continue
158+
160159
try:
161160
# execute save in a savepoint, so we can resume in the
162161
# transaction
@@ -166,20 +165,10 @@ def merge_model_objects(primary_object, alias_objects=[], keep_old=False):
166165
if ie.__cause__.pgcode == PG_UNIQUE_VIOLATION:
167166
merge(generic_related_object, primary_object)
168167

169-
# Try to fill all missing values in primary object by
170-
# values of duplicates
171-
filled_up = set()
172-
for field_name in blank_local_fields:
173-
val = getattr(alias_object, field_name)
174-
if val not in [None, '']:
175-
setattr(primary_object, field_name, val)
176-
filled_up.add(field_name)
177-
blank_local_fields -= filled_up
178-
179168
if not keep_old:
180169
before_delete(alias_object, primary_object)
181170
alias_object.delete()
182-
primary_object.save()
171+
183172
return primary_object
184173

185174

@@ -199,6 +188,17 @@ def _get_all_related_many_to_many_objects(obj):
199188

200189

201190
def reassigned(obj, new):
191+
""" handles changes necessary when reassigning `obj` to `new`
192+
193+
Some objects have a dependent object (eg URL has a Podcast or Episode.
194+
During merging, the object might be assigned from to a new Episode.
195+
The re-assignment requires the "scope" field to be set to the value
196+
of the new episode. In some cases it might require the existing object to
197+
be deleted, to preserve uniqueness.
198+
199+
Returns whether the object was deleted.
200+
"""
201+
202202
if isinstance(obj, URL):
203203
# a URL has its parent's scope
204204
obj.scope = new.scope
@@ -207,11 +207,27 @@ def reassigned(obj, new):
207207
max_order = max([-1] + [u.order for u in existing_urls])
208208
obj.order = max_order+1
209209

210+
elif isinstance(obj, Slug):
211+
# a Slug has its parent's scope
212+
obj.scope = new.scope
213+
214+
existing_slugs = new.slugs.all()
215+
max_order = max([-1] + [s.order for s in existing_slugs])
216+
obj.order = max_order+1
217+
210218
elif isinstance(obj, Episode):
211219
# obj is an Episode, new is a podcast
212220
for url in obj.urls.all():
213221
url.scope = new.as_scope
214-
url.save()
222+
try:
223+
with transaction.atomic():
224+
url.save()
225+
except IntegrityError as ie:
226+
if 'podcasts_url_url_scope_key' in str(ie):
227+
url.delete()
228+
return True
229+
else:
230+
raise
215231

216232
elif isinstance(obj, Subscription):
217233
pass
@@ -222,10 +238,17 @@ def reassigned(obj, new):
222238
elif isinstance(obj, HistoryEntry):
223239
pass
224240

241+
elif isinstance(obj, models.MergeQueueEntry):
242+
obj.delete()
243+
return True
244+
225245
else:
226246
raise TypeError('unknown type for reassigning: {objtype}'.format(
227247
objtype=type(obj)))
228248

249+
# Object was not deleted
250+
return False
251+
229252

230253
def before_delete(old, new):
231254

mygpo/maintenance/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
class MergeQueue(UUIDModel):
88
""" A Group of podcasts that could be merged """
99

10+
@property
11+
def podcasts(self):
12+
""" Returns the podcasts of the queue, sorted by subscribers """
13+
podcasts = [entry.podcast for entry in self.entries.all()]
14+
podcasts = sorted(podcasts,
15+
key=lambda p: p.subscribers, reverse=True)
16+
return podcasts
17+
1018

1119
class MergeQueueEntry(UUIDModel):
1220
""" An entry in a MergeQueue """

0 commit comments

Comments
 (0)