Zusammenführen von doppelten Personen
In der Produktionsdatenbank gibt es z.T. doppelte Einträge von Personen, weil manche manuell und dann später durch Login angelegt wurden. Diese Aufgabe ist noch aus dem Juni-Paket (#30 (closed)) übrig.
Ich habe ein ähnliches Problem schonmal bei kritisch-lesen mit einer Django-Admin-Funktion für Tags gelöst. Ich würde jetzt nicht wahllos Funktionen in Django-Admin verschieben, aber vielleicht reicht diese Variante erstmal.
Code-Dump
# admin.py
class MergeTagsMixin:
@admin.action(description=_("Merge tags"), permissions=["change", "delete"])
def merge_tags(self, request: django.http.HttpRequest, queryset: QuerySet[taggit.models.Tag]):
class MergeTagsForm(forms.Form):
merge_into = forms.ModelChoiceField(queryset=queryset, label=_("Merge into"))
is_confirmed = request.POST.get("post", None) == "yes"
form = MergeTagsForm(request.POST if is_confirmed else None)
if is_confirmed and form.is_valid():
merge_into = form.cleaned_data["merge_into"]
tags = queryset.exclude(pk=merge_into.pk)
self.perform_tag_merge(merge_into, tags)
return django.http.HttpResponseRedirect(
request.META.get("HTTP_REFERRER", request.get_full_path())
)
request.current_app = self.admin_site.name
return TemplateResponse(
request,
"taggit/admin/merge_tags.html",
dict(
self.admin_site.each_context(request),
title=_("Merge tags"),
subtitle=None,
queryset=queryset,
action_checkbox_name=helpers.ACTION_CHECKBOX_NAME,
form=MergeTagsForm(),
opts=self.opts,
app_label=self.opts.app_label,
action=request.POST["action"],
),
)
def perform_tag_merge(self, merge_into: taggit.models.Tag, tags: QuerySet[taggit.models.Tag]):
for tag in tags:
items = tag.taggit_taggeditem_items.all()
for item in items:
item.tag = merge_into
try:
item.save()
except django.db.utils.IntegrityError:
# the tag we're merging into has already been assigned to
# this item, so we can safely delete the current assignment
item.delete()
tag.delete()
@admin.register(taggit.models.Tag)
class TagAdmin(BaseAdmin, MergeTagsMixin, taggit.admin.TagAdmin):
inlines = []
actions = ["merge_tags"]
{% extends "admin/base_site.html" %}
{% load i18n l10n admin_urls static %}
{% block extrahead %}
{{ block.super }}
{{ media }}
<script src="{% static 'admin/js/cancel.js' %}" async></script>
{% endblock %}
{# we add the delete-confirmation class because the button styles for the form depend on it #}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} delete-confirmation merge-tags{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
› <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
› <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
› {% translate 'Merge tags into one' %}
</div>
{% endblock %}
{% block content %}
<p>{% translate "The following tags will be merged together:" %}</p>
<ul>
{% for tag in queryset %}
<li>{{ tag.name }}
({% blocktranslate count counter=tag.taggit_taggeditem_items.count trimmed %}
used one time
{% plural %}
used {{ counter }} times
{% endblocktranslate %})
</li>
{% endfor %}
</ul>
<p>{% translate "All items tagged with the tags above will be tagged with the selected tag afterwards." %}</p>
<form method="post">
{% csrf_token %}
<div>
<div>{{ form.as_p }}</div>
{% for obj in queryset %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}">
{% endfor %}
<input type="hidden" name="action" value="{{ action }}">
<input type="hidden" name="post" value="yes">
<input type="submit" value="{% translate 'Yes, merge tags' %}">
<a href="#" class="button cancel-link">{% translate "No, take me back" %}</a>
</div>
</form>
{% endblock %}
Falls wir das Problem lieber im Frontend lösen wollen wäre es gut, wenn die Logik für die Zusammenführung nicht vom Frontend übernommen werden muss. Dafür fände ich einen Endpunkt in folgender Form passend:
# POST /persons/:id/merge-aliases
{ "aliasPersonIds": [2, 4] }