def get_changed_object(self, obj) -> ChangedObject: if isinstance(obj, ModelInstanceWrapper): obj = obj._obj model = obj.__class__ pk = obj.pk if pk is None: return ChangedObject(changeset=self, model_class=model) self.fill_changes_cache() objects = tuple( obj for obj in ((submodel, self.changed_objects.get(submodel, {}).get(pk, None)) for submodel in get_submodels(model)) if obj[1] is not None) if len(objects) > 1: raise model.MultipleObjectsReturned if objects: return objects[0][1] if is_created_pk(pk): raise model.DoesNotExist return ChangedObject(changeset=self, model_class=model, existing_object_pk=pk)
def get_missing_dependencies(self, force_query=False, max_one=False): result = set() if not self.deleted: return result for field in self.model_class._meta.get_fields(): if not field.many_to_one: continue if field.name not in self.updated_fields: continue related_model = field.related_model if related_model._meta.app_label != 'mapdata': continue pk = self.updated_fields[field.name] if force_query: # query here to avoid a race condition related_content_type = ContentType.objects.get_for_model( related_model) qs = self.changeset.changed_objects_set.filter( content_type=related_content_type) if is_created_pk(pk): if not qs.filter(pk=int(pk[1:]), deleted=False).exists(): result.add(field.name) else: if qs.filter(existing_object_pk=pk, deleted=True).exists(): result.add(field.name) else: if is_created_pk(pk): if pk not in self.changeset.created_objects.get( related_model, ()): result.add(field.name) else: if pk in self.changeset.deleted_existing.get( related_model, ()): result.add(field.name) if result and max_one: return result return result
def save_instance(self, instance): old_updated_fields = self.updated_fields self.updated_fields = {} if instance.pk is None and self.model_class == LocationRedirect and not is_created_pk( instance.target_id): obj = LocationRedirect.objects.filter( pk__in=self.changeset.deleted_existing.get( LocationRedirect, ()), slug=instance.slug, target_id=instance.target_id).first() if obj is not None: self.changeset.get_changed_object(obj).restore() return for field in self.model_class._meta.get_fields(): if not isinstance(field, Field) or field.primary_key: continue elif not field.is_relation: value = getattr(instance, field.attname) if isinstance(field, I18nField): for lang, subvalue in value.items(): self.updated_fields['%s__i18n__%s' % (field.name, lang)] = subvalue elif isinstance(field, (CharField, TextField)): self.updated_fields[ field. name] = None if field.null and not value else field.get_prep_value( value) else: self.updated_fields[field.name] = field.get_prep_value( value) elif field.many_to_one or field.one_to_one: try: value = getattr(instance, field.get_cache_name()) except AttributeError: value = getattr(instance, field.attname) else: value = None if value is None else value.pk self.updated_fields[field.name] = value self.clean_updated_fields() for name, value in self.updated_fields.items(): if old_updated_fields.get(name, None) != value: self.changeset._object_changed = True break self.save() if instance.pk is None and self.pk is not None: instance.pk = self.obj_pk
def get_missing_dependencies(self, force_query=False, max_one=False): result = set() if not self.deleted: return result for field in self.model_class._meta.get_fields(): if not field.many_to_one: continue if field.name not in self.updated_fields: continue related_model = field.related_model if related_model._meta.app_label != 'mapdata': continue pk = self.updated_fields[field.name] if force_query: # query here to avoid a race condition related_content_type = ContentType.objects.get_for_model(related_model) qs = self.changeset.changed_objects_set.filter(content_type=related_content_type) if is_created_pk(pk): if not qs.filter(pk=int(pk[1:]), deleted=False).exists(): result.add(field.name) else: if qs.filter(existing_object_pk=pk, deleted=True).exists(): result.add(field.name) else: if is_created_pk(pk): if pk not in self.changeset.created_objects.get(related_model, ()): result.add(field.name) else: if pk in self.changeset.deleted_existing.get(related_model, ()): result.add(field.name) if result and max_one: return result return result
def apply_to_instance(self, instance: ModelInstanceWrapper, created_pks=None): for name, value in self.updated_fields.items(): if '__i18n__' in name: name, i18n, lang = name.split('__') field = instance._meta.get_field(name) if not value: getattr(instance, field.attname).pop(lang, None) else: getattr(instance, field.attname)[lang] = value continue field = instance._meta.get_field(name) if not field.is_relation: setattr(instance, field.name, field.to_python(value)) elif field.many_to_one or field.one_to_one: if is_created_pk(value): if created_pks is None: try: obj = self.changeset.get_created_object( field.related_model, value, allow_deleted=True) except field.related_model.DoesNotExist: pass else: setattr(instance, field.get_cache_name(), obj) else: try: delattr(instance, field.get_cache_name()) except AttributeError: pass try: value = created_pks[field.related_model][value] except KeyError: raise ApplyToInstanceError else: try: delattr(instance, field.get_cache_name()) except AttributeError: pass setattr(instance, field.attname, value) else: raise NotImplementedError
def save_instance(self, instance): old_updated_fields = self.updated_fields self.updated_fields = {} if instance.pk is None and self.model_class == LocationRedirect and not is_created_pk(instance.target_id): obj = LocationRedirect.objects.filter(pk__in=self.changeset.deleted_existing.get(LocationRedirect, ()), slug=instance.slug, target_id=instance.target_id).first() if obj is not None: self.changeset.get_changed_object(obj).restore() return for field in self.model_class._meta.get_fields(): if not isinstance(field, Field) or field.primary_key: continue elif not field.is_relation: value = getattr(instance, field.attname) if isinstance(field, I18nField): for lang, subvalue in value.items(): self.updated_fields['%s__i18n__%s' % (field.name, lang)] = subvalue elif isinstance(field, (CharField, TextField)): self.updated_fields[field.name] = None if field.null and not value else field.get_prep_value(value) else: self.updated_fields[field.name] = field.get_prep_value(value) elif field.many_to_one or field.one_to_one: try: value = getattr(instance, field.get_cache_name()) except AttributeError: value = getattr(instance, field.attname) else: value = None if value is None else value.pk self.updated_fields[field.name] = value self.clean_updated_fields() for name, value in self.updated_fields.items(): if old_updated_fields.get(name, None) != value: self.changeset._object_changed = True break self.save() if instance.pk is None and self.pk is not None: instance.pk = self.obj_pk
def get_changed_object(self, obj) -> ChangedObject: if isinstance(obj, ModelInstanceWrapper): obj = obj._obj model = obj.__class__ pk = obj.pk if pk is None: return ChangedObject(changeset=self, model_class=model) self.fill_changes_cache() objects = tuple(obj for obj in ((submodel, self.changed_objects.get(submodel, {}).get(pk, None)) for submodel in get_submodels(model)) if obj[1] is not None) if len(objects) > 1: raise model.MultipleObjectsReturned if objects: return objects[0][1] if is_created_pk(pk): raise model.DoesNotExist return ChangedObject(changeset=self, model_class=model, existing_object_pk=pk)
def apply_to_instance(self, instance: ModelInstanceWrapper, created_pks=None): for name, value in self.updated_fields.items(): if '__i18n__' in name: name, i18n, lang = name.split('__') field = instance._meta.get_field(name) if not value: getattr(instance, field.attname).pop(lang, None) else: getattr(instance, field.attname)[lang] = value continue field = instance._meta.get_field(name) if not field.is_relation: setattr(instance, field.name, field.to_python(value)) elif field.many_to_one or field.one_to_one: if is_created_pk(value): if created_pks is None: try: obj = self.changeset.get_created_object(field.related_model, value, allow_deleted=True) except field.related_model.DoesNotExist: pass else: setattr(instance, field.get_cache_name(), obj) else: try: delattr(instance, field.get_cache_name()) except AttributeError: pass try: value = created_pks[field.related_model][value] except KeyError: raise ApplyToInstanceError else: try: delattr(instance, field.get_cache_name()) except AttributeError: pass setattr(instance, field.attname, value) else: raise NotImplementedError
def get_objects(self, many=True, changed_objects=None, prefetch_related=()): if changed_objects is None: if self.changed_objects is None: raise TypeError changed_objects = self.iter_changed_objects() # collect pks of relevant objects object_pks = {} for change in changed_objects: change.add_relevant_object_pks(object_pks, many=many) # create dummy objects for deleted ones objects = {} for model, pks in object_pks.items(): objects[model] = {pk: model(pk=pk) for pk in pks} slug_submodels = tuple(model for model in object_pks.keys() if model is not LocationSlug and issubclass(model, LocationSlug)) if slug_submodels: object_pks[LocationSlug] = reduce(operator.or_, (object_pks[model] for model in slug_submodels)) for model in slug_submodels: object_pks.pop(model) # retrieve relevant objects for model, pks in object_pks.items(): if not pks: continue created_pks = set(pk for pk in pks if is_created_pk(pk)) existing_pks = pks - created_pks model_objects = {} if existing_pks: qs = model.objects if model is LocationSlug: qs = qs.select_related_target() qs = qs.filter(pk__in=existing_pks) for prefetch in prefetch_related: try: model._meta.get_field(prefetch) except FieldDoesNotExist: pass else: qs = qs.prefetch_related(prefetch) for obj in qs: if model == LocationSlug: obj = obj.get_child() model_objects[obj.pk] = obj if created_pks: for pk in created_pks: model_objects[pk] = self.get_created_object(model, pk, allow_deleted=True)._obj objects[model] = model_objects # add LocationSlug objects as their correct model for pk, obj in objects.get(LocationSlug, {}).items(): objects.setdefault(obj.__class__, {})[pk] = obj for pk, obj in objects.get(LocationRedirect, {}).items(): try: target = obj.target.get_child(obj.target) except FieldDoesNotExist: # todo: fix this continue # todo: why is it sometimes wrapped and sometimes not? objects.setdefault(LocationSlug, {})[target.pk] = getattr(target, '_obj', target) objects.setdefault(target.__class__, {})[target.pk] = getattr(target, '_obj', target) return objects
def _clean_changes(self): if self.direct_editing: return with self.lock_to_edit() as changeset: last_map_update_pk = MapUpdate.last_update()[0] if changeset.last_cleaned_with_id == last_map_update_pk: return changed_objects = changeset.changed_objects_set.all() # delete changed objects that refer in some way to deleted objects and clean up m2m changes object_pks = {} for changed_object in changed_objects: changed_object.add_relevant_object_pks(object_pks) to_save = set() deleted_object_pks = {} for model, pks in object_pks.items(): pks = set(pk for pk in pks if not is_created_pk(pk)) deleted_object_pks[model] = pks - set(model.objects.filter(pk__in=pks).values_list('pk', flat=True)) repeat = True while repeat: repeat = False for changed_object in changed_objects: if changed_object.handle_deleted_object_pks(deleted_object_pks): to_save.add(changed_object) if changed_object.pk is None: repeat = True # remove deleted objects changed_objects = [obj for obj in changed_objects if obj.pk is not None] # clean updated fields objects = changeset.get_objects(many=False, changed_objects=changed_objects, prefetch_related=('groups', )) for changed_object in changed_objects: if changed_object.clean_updated_fields(objects): to_save.add(changed_object) # clean m2m for changed_object in changed_objects: if changed_object.clean_m2m(objects): to_save.add(changed_object) # remove duplicate slugs slugs = set() for changed_object in changed_objects: if issubclass(changed_object.model_class, LocationSlug): slug = changed_object.updated_fields.get('slug', None) if slug is not None: slugs.add(slug) qs = LocationSlug.objects.filter(slug__in=slugs) if slugs: qs = qs.filter(reduce(operator.or_, (Q(slug__startswith=slug+'__') for slug in slugs))) existing_slugs = dict(qs.values_list('slug', 'redirect__target_id')) slug_length = LocationSlug._meta.get_field('slug').max_length for changed_object in changed_objects: if issubclass(changed_object.model_class, LocationSlug): slug = changed_object.updated_fields.get('slug', None) if slug is None: continue if slug in existing_slugs: redirect_to = existing_slugs[slug] if issubclass(changed_object.model_class, LocationRedirect) and redirect_to is not None: to_save.discard(changed_object) changed_object.delete() continue new_slug = slug i = 0 while new_slug in existing_slugs: suffix = '__'+str(i) new_slug = slug[:slug_length-len(suffix)]+suffix i += 1 slug = new_slug changed_object.updated_fields['slug'] = new_slug to_save.add(changed_object) existing_slugs[slug] = (None if not issubclass(changed_object.model_class, LocationRedirect) else changed_object.updated_fields['target']) for changed_object in to_save: changed_object.save(standalone=True) changeset.last_cleaned_with_id = last_map_update_pk changeset.save()
def changeset_detail(request, pk): changeset = request.changeset active = True if str(pk) != str(request.changeset.pk): active = False qs = ChangeSet.qs_for_request(request).select_related('last_update', 'last_state_update', 'last_change', 'author') changeset = get_object_or_404(qs, pk=pk) if not changeset.can_see(request): raise Http404 can_edit = changeset.can_edit(request) can_delete = changeset.can_delete(request) if request.method == 'POST': restore = request.POST.get('restore') if restore and restore.isdigit(): with changeset.lock_to_edit(request) as changeset: if changeset.can_edit(request): try: changed_object = changeset.changed_objects_set.get(pk=restore) except Exception: pass else: try: changed_object.restore() messages.success(request, _('Object has been successfully restored.')) except PermissionError: messages.error(request, _('You cannot restore this object, because it depends on ' 'a deleted object or it would violate a unique contraint.')) else: messages.error(request, _('You can not edit changes on this change set.')) return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk})) elif request.POST.get('activate') == '1': with changeset.lock_to_edit(request) as changeset: if changeset.can_activate(request): changeset.activate(request) messages.success(request, _('You activated this change set.')) else: messages.error(request, _('You can not activate this change set.')) return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk})) elif request.POST.get('propose') == '1': if not request.user.is_authenticated: messages.info(request, _('You need to log in to propose changes.')) return redirect(reverse('editor.login')+'?r='+request.path) with changeset.lock_to_edit(request) as changeset: if not changeset.title or not changeset.description: messages.warning(request, _('You need to add a title an a description to propose this change set.')) return redirect(reverse('editor.changesets.edit', kwargs={'pk': changeset.pk})) if changeset.can_propose(request): changeset.propose(request.user) messages.success(request, _('You proposed your changes.')) else: messages.error(request, _('You cannot propose this change set.')) return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk})) elif request.POST.get('unpropose') == '1': with changeset.lock_to_edit(request) as changeset: if changeset.can_unpropose(request): changeset.unpropose(request.user) messages.success(request, _('You unproposed your changes.')) else: messages.error(request, _('You cannot unpropose this change set.')) return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk})) elif request.POST.get('review') == '1': with changeset.lock_to_edit(request) as changeset: if changeset.can_start_review(request): changeset.start_review(request.user) messages.success(request, _('You are now reviewing these changes.')) else: messages.error(request, _('You cannot review these changes.')) return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk})) elif request.POST.get('reject') == '1': with changeset.lock_to_edit(request) as changeset: if not changeset.can_end_review(request): messages.error(request, _('You cannot reject these changes.')) return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk})) if request.POST.get('reject_confirm') == '1': form = RejectForm(data=request.POST) if form.is_valid(): changeset.reject(request.user, form.cleaned_data['comment'], form.cleaned_data['final']) messages.success(request, _('You rejected these changes.')) return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk})) else: form = RejectForm() return render(request, 'editor/changeset_reject.html', { 'changeset': changeset, 'form': form, }) elif request.POST.get('unreject') == '1': with changeset.lock_to_edit(request) as changeset: if not changeset.can_unreject(request): messages.error(request, _('You cannot unreject these changes.')) return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk})) changeset.unreject(request.user) messages.success(request, _('You unrejected these changes.')) return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk})) elif request.POST.get('apply') == '1': with changeset.lock_to_edit(request) as changeset: if not changeset.can_end_review(request): messages.error(request, _('You cannot accept and apply these changes.')) return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk})) if request.POST.get('apply_confirm') == '1': changeset.apply(request.user) messages.success(request, _('You accepted and applied these changes.')) return redirect(reverse('editor.changesets.detail', kwargs={'pk': changeset.pk})) return render(request, 'editor/changeset_apply.html', {}) elif request.POST.get('delete') == '1': with changeset.lock_to_edit(request) as changeset: if not changeset.can_delete(request): messages.error(request, _('You cannot delete this change set.')) if request.POST.get('delete_confirm') == '1': changeset.delete() messages.success(request, _('You deleted this change set.')) if request.user.is_authenticated: return redirect(reverse('editor.users.detail', kwargs={'pk': request.user.pk})) else: return redirect(reverse('editor.index')) return render(request, 'editor/delete.html', { 'model_title': ChangeSet._meta.verbose_name, 'obj_title': changeset.title, }) changeset.fill_changes_cache() ctx = { 'changeset': changeset, 'can_edit': can_edit, 'can_delete': can_delete, 'can_propose': changeset.can_propose(request), 'can_unpropose': changeset.can_unpropose(request), 'can_start_review': changeset.can_start_review(request), 'can_end_review': changeset.can_end_review(request), 'can_unreject': changeset.can_unreject(request), 'active': active, } cache_key = '%s:%s:%s:view_data' % (changeset.cache_key_by_changes, changeset.last_update_id, int(can_edit)) changed_objects_data = cache.get(cache_key) if changed_objects_data: ctx['changed_objects'] = changed_objects_data return render(request, 'editor/changeset.html', ctx) objects = changeset.get_objects() changed_objects_data = [] added_redirects = {} removed_redirects = {} for changed_object in changeset.changed_objects.get(LocationRedirect, {}).values(): if changed_object.is_created == changed_object.deleted: continue obj = objects[LocationRedirect][changed_object.obj_pk] redirect_list = (removed_redirects if changed_object.deleted else added_redirects) redirect_list.setdefault(obj.target_id, []).append(obj.slug) redirect_changed_objects = [] for pk in set(added_redirects.keys()) | set(removed_redirects.keys()): obj = objects[LocationSlug][pk] model = obj.__class__ try: changeset.changed_objects[model][pk] except KeyError: redirect_changed_objects.append((model, {pk: changeset.get_changed_object(obj)})) for model, changed_objects in chain(changeset.changed_objects.items(), redirect_changed_objects): if model == LocationRedirect: continue for pk, changed_object in changed_objects.items(): obj = objects[model][pk] obj_desc = format_lazy(_('{model} #{id}'), model=obj.__class__._meta.verbose_name, id=pk) if is_created_pk(pk): obj_still_exists = pk in changeset.created_objects.get(obj.__class__, ()) else: obj_still_exists = pk not in changeset.deleted_existing.get(obj.__class__, ()) edit_url = None if obj_still_exists and can_edit and not isinstance(obj, LocationRedirect): reverse_kwargs = {'pk': obj.pk} if hasattr(obj, 'space_id'): reverse_kwargs['space'] = obj.space_id elif hasattr(obj, 'level_id'): reverse_kwargs['level'] = obj.level_id try: edit_url = reverse('editor.' + obj.__class__._meta.default_related_name + '.edit', kwargs=reverse_kwargs) except NoReverseMatch: pass changes = [] missing_dependencies = changed_object.get_missing_dependencies() unique_collisions = changed_object.get_unique_collisions() changed_object_data = { 'model': obj.__class__, 'model_title': obj.__class__._meta.verbose_name, 'pk': changed_object.pk, 'desc': obj_desc, 'title': obj.title if getattr(obj, 'titles', None) else None, 'changes': changes, 'edit_url': edit_url, 'deleted': changed_object.deleted, 'missing_dependencies': missing_dependencies, 'unique_collisions': unique_collisions, 'order': (changed_object.deleted and changed_object.is_created, not changed_object.is_created), } changed_objects_data.append(changed_object_data) form_fields = changeset.wrap_model(type(obj)).EditorForm._meta.fields if changed_object.is_created: changes.append({ 'icon': 'plus', 'class': 'success', 'empty': True, 'title': _('created'), }) update_changes = [] for name, value in changed_object.updated_fields.items(): change_data = { 'icon': 'option-vertical', 'class': 'muted', } if name == 'geometry': change_data.update({ 'icon': 'map-marker', 'class': 'info', 'empty': True, 'title': _('created geometry') if changed_object.is_created else _('edited geometry'), 'order': (8,), }) elif name == 'data': change_data.update({ 'icon': 'signal', 'class': 'info', 'empty': True, 'title': _('created scan data') if changed_object.is_created else _('edited scan data'), 'order': (9,), }) else: if '__i18n__' in name: orig_name, i18n, lang = name.split('__') lang_info = get_language_info(lang) field = model._meta.get_field(orig_name) field_title = format_lazy(_('{field_name} ({lang})'), field_name=field.verbose_name, lang=lang_info['name_translated']) field_value = str(value) if field_value: getattr(obj, field.attname)[lang] = field_value else: getattr(obj, field.attname).pop(lang, None) change_data.update({ 'order': (4, tuple(code for code, title in settings.LANGUAGES).index(lang)), }) else: field = model._meta.get_field(name) field_title = field.verbose_name field_value = field.to_python(value) if field.related_model is not None: if issubclass(field.related_model, User): field_value = objects[field.related_model][field_value].username else: field_value = objects[field.related_model][field_value].title change_data.update({ 'missing_dependency': field.name in missing_dependencies, }) if name in unique_collisions: change_data.update({ 'unique_collision': field.name in unique_collisions, }) order = 5 if name == 'slug': order = 1 if name not in form_fields: order = 0 change_data.update({ 'order': (order, form_fields.index(name) if order else 1), }) if field_value == '' or field_value is None: change_data.update({ 'empty': True, 'title': format_lazy(_('remove {field_title}'), field_title=field_title), }) else: change_data.update({ 'title': field_title, 'value': field_value, }) update_changes.append(change_data) changes.extend(sorted(update_changes, key=itemgetter('order'))) for m2m_mode in ('m2m_added', 'm2m_removed'): m2m_list = getattr(changed_object, m2m_mode).items() for name, values in sorted(m2m_list, key=lambda nv: form_fields.index(nv[0])): field = model._meta.get_field(name) for value in values: changes.append({ 'icon': 'chevron-right' if m2m_mode == 'm2m_added' else 'chevron-left', 'class': 'info', 'title': field.verbose_name, 'value': objects[field.related_model][value].title, }) if isinstance(obj, LocationSlug): for slug in added_redirects.get(obj.pk, ()): changes.append({ 'icon': 'chevron-right', 'class': 'info', 'title': _('Redirect slugs'), 'value': slug, }) for slug in removed_redirects.get(obj.pk, ()): changes.append({ 'icon': 'chevron-left', 'class': 'info', 'title': _('Redirect slugs'), 'value': slug, }) if changed_object.deleted: changes.append({ 'icon': 'minus', 'class': 'danger', 'empty': True, 'title': _('deleted'), 'order': (9,), }) changed_objects_data = sorted(changed_objects_data, key=itemgetter('order')) cache.set(cache_key, changed_objects_data, 300) ctx['changed_objects'] = changed_objects_data return render(request, 'editor/changeset.html', ctx)
def get_objects(self, many=True, changed_objects=None, prefetch_related=()): if changed_objects is None: if self.changed_objects is None: raise TypeError changed_objects = self.iter_changed_objects() # collect pks of relevant objects object_pks = {} for change in changed_objects: change.add_relevant_object_pks(object_pks, many=many) # create dummy objects for deleted ones objects = {} for model, pks in object_pks.items(): objects[model] = {pk: model(pk=pk) for pk in pks} slug_submodels = tuple( model for model in object_pks.keys() if model is not LocationSlug and issubclass(model, LocationSlug)) if slug_submodels: object_pks[LocationSlug] = reduce(operator.or_, (object_pks[model] for model in slug_submodels)) for model in slug_submodels: object_pks.pop(model) # retrieve relevant objects for model, pks in object_pks.items(): if not pks: continue created_pks = set(pk for pk in pks if is_created_pk(pk)) existing_pks = pks - created_pks model_objects = {} if existing_pks: qs = model.objects if model is LocationSlug: qs = qs.select_related_target() qs = qs.filter(pk__in=existing_pks) for prefetch in prefetch_related: try: model._meta.get_field(prefetch) except FieldDoesNotExist: pass else: qs = qs.prefetch_related(prefetch) for obj in qs: if model == LocationSlug: obj = obj.get_child() model_objects[obj.pk] = obj if created_pks: for pk in created_pks: model_objects[pk] = self.get_created_object( model, pk, allow_deleted=True)._obj objects[model] = model_objects # add LocationSlug objects as their correct model for pk, obj in objects.get(LocationSlug, {}).items(): objects.setdefault(obj.__class__, {})[pk] = obj for pk, obj in objects.get(LocationRedirect, {}).items(): try: target = obj.target.get_child(obj.target) except FieldDoesNotExist: # todo: fix this continue # todo: why is it sometimes wrapped and sometimes not? objects.setdefault(LocationSlug, {})[target.pk] = getattr( target, '_obj', target) objects.setdefault(target.__class__, {})[target.pk] = getattr( target, '_obj', target) return objects
def _clean_changes(self): if self.direct_editing: return with self.lock_to_edit() as changeset: last_map_update_pk = MapUpdate.last_update()[0] if changeset.last_cleaned_with_id == last_map_update_pk: return changed_objects = changeset.changed_objects_set.all() # delete changed objects that refer in some way to deleted objects and clean up m2m changes object_pks = {} for changed_object in changed_objects: changed_object.add_relevant_object_pks(object_pks) to_save = set() deleted_object_pks = {} for model, pks in object_pks.items(): pks = set(pk for pk in pks if not is_created_pk(pk)) deleted_object_pks[model] = pks - set( model.objects.filter(pk__in=pks).values_list('pk', flat=True)) repeat = True while repeat: repeat = False for changed_object in changed_objects: if changed_object.handle_deleted_object_pks( deleted_object_pks): to_save.add(changed_object) if changed_object.pk is None: repeat = True # remove deleted objects changed_objects = [ obj for obj in changed_objects if obj.pk is not None ] # clean updated fields objects = changeset.get_objects(many=False, changed_objects=changed_objects, prefetch_related=('groups', )) for changed_object in changed_objects: if changed_object.clean_updated_fields(objects): to_save.add(changed_object) # clean m2m for changed_object in changed_objects: if changed_object.clean_m2m(objects): to_save.add(changed_object) # remove duplicate slugs slugs = set() for changed_object in changed_objects: if issubclass(changed_object.model_class, LocationSlug): slug = changed_object.updated_fields.get('slug', None) if slug is not None: slugs.add(slug) qs = LocationSlug.objects.filter(slug__in=slugs) if slugs: qs = qs.filter( reduce(operator.or_, (Q(slug__startswith=slug + '__') for slug in slugs))) existing_slugs = dict(qs.values_list('slug', 'redirect__target_id')) slug_length = LocationSlug._meta.get_field('slug').max_length for changed_object in changed_objects: if issubclass(changed_object.model_class, LocationSlug): slug = changed_object.updated_fields.get('slug', None) if slug is None: continue if slug in existing_slugs: redirect_to = existing_slugs[slug] if issubclass( changed_object.model_class, LocationRedirect) and redirect_to is not None: to_save.discard(changed_object) changed_object.delete() continue new_slug = slug i = 0 while new_slug in existing_slugs: suffix = '__' + str(i) new_slug = slug[:slug_length - len(suffix)] + suffix i += 1 slug = new_slug changed_object.updated_fields['slug'] = new_slug to_save.add(changed_object) existing_slugs[slug] = ( None if not issubclass(changed_object.model_class, LocationRedirect) else changed_object.updated_fields['target']) for changed_object in to_save: changed_object.save(standalone=True) changeset.last_cleaned_with_id = last_map_update_pk changeset.save()