1212from mygpo .history .models import HistoryEntry , EpisodeHistoryEntry
1313from mygpo .publisher .models import PublishedPodcast
1414from mygpo .subscriptions .models import Subscription
15+ from . import models
1516
1617import logging
1718logger = 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
201190def 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
230253def before_delete (old , new ):
231254
0 commit comments