From 5f21c4dfb1a428ff076006029f6474c569c9fa1a Mon Sep 17 00:00:00 2001 From: Birger Schacht Date: Wed, 4 Sep 2024 14:53:32 +0200 Subject: [PATCH] feat(generic): introduce a generic merge view This view shows the values before and after merge instead of simply merging. --- apis_core/generic/abc.py | 4 ++ apis_core/generic/forms/__init__.py | 7 +++ .../templates/generic/generic_merge.html | 42 +++++++++++++++++ apis_core/generic/urls.py | 5 ++ apis_core/generic/views.py | 47 ++++++++++++++++++- 5 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 apis_core/generic/templates/generic/generic_merge.html diff --git a/apis_core/generic/abc.py b/apis_core/generic/abc.py index 1997181e0..2f24f4a3c 100644 --- a/apis_core/generic/abc.py +++ b/apis_core/generic/abc.py @@ -28,6 +28,10 @@ def get_delete_url(self): ct = ContentType.objects.get_for_model(self) return reverse("apis_core:generic:delete", args=[ct, self.id]) + def get_merge_url(self, other_id): + ct = ContentType.objects.get_for_model(self) + return reverse("apis_core:generic:merge", args=[ct, self.id, other_id]) + def get_create_success_url(self): return self.get_absolute_url() diff --git a/apis_core/generic/forms/__init__.py b/apis_core/generic/forms/__init__.py index 49ee37588..7b788a9e0 100644 --- a/apis_core/generic/forms/__init__.py +++ b/apis_core/generic/forms/__init__.py @@ -86,3 +86,10 @@ def __init__(self, *args, **kwargs): url, attrs={"data-html": True} ) self.fields[field].widget.choices = self.fields[field].choices + + +class GenericMergeForm(forms.Form): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.add_input(Submit("submit", "Merge")) diff --git a/apis_core/generic/templates/generic/generic_merge.html b/apis_core/generic/templates/generic/generic_merge.html new file mode 100644 index 000000000..d5e663262 --- /dev/null +++ b/apis_core/generic/templates/generic/generic_merge.html @@ -0,0 +1,42 @@ +{% extends basetemplate|default:"base.html" %} +{% load apisgeneric %} +{% load apis_history_templatetags %} +{% load crispy_forms_tags %} +{% load static %} + +{% block styles %} + {{ block.super }} + +{% endblock styles %} + +{% block content %} +
+

+ Merging values of {{ other }} into {{ object }} +

+ + + + + + + + + + + {% for change in changes %} + + + + + + {% endfor %} + +
Old valueNew value
{{ change.field }}{{ change|get_diff_old }}{{ change|get_diff_new }}
+ {% if form %} + {% crispy form form.helper %} + {% endif %} +
+{% endblock %} diff --git a/apis_core/generic/urls.py b/apis_core/generic/urls.py index b74a2d3d3..c73a27a8f 100644 --- a/apis_core/generic/urls.py +++ b/apis_core/generic/urls.py @@ -49,6 +49,11 @@ def to_url(self, value): path("create", views.Create.as_view(), name="create"), path("delete/", views.Delete.as_view(), name="delete"), path("update/", views.Update.as_view(), name="update"), + path( + "merge//", + views.MergeWith.as_view(), + name="merge", + ), path("autocomplete", views.Autocomplete.as_view(), name="autocomplete"), path("import", views.Import.as_view(), name="import"), path( diff --git a/apis_core/generic/views.py b/apis_core/generic/views.py index 8d8cb695e..c127ca5a2 100644 --- a/apis_core/generic/views.py +++ b/apis_core/generic/views.py @@ -1,8 +1,12 @@ +from collections import namedtuple + from dal import autocomplete from django import forms, http from django.conf import settings +from django.contrib import messages from django.contrib.auth.mixins import PermissionRequiredMixin from django.forms import modelform_factory +from django.shortcuts import get_object_or_404 from django.template.exceptions import TemplateDoesNotExist from django.template.loader import select_template from django.urls import reverse, reverse_lazy @@ -19,7 +23,7 @@ from apis_core.utils.helpers import create_object_from_uri from .filtersets import GenericFilterSet -from .forms import GenericImportForm, GenericModelForm +from .forms import GenericImportForm, GenericMergeForm, GenericModelForm from .helpers import ( first_member_match, generate_search_filter, @@ -331,3 +335,44 @@ def form_valid(self, form): def get_success_url(self): return self.object.get_absolute_url() + + +class MergeWith(GenericModelMixin, PermissionRequiredMixin, FormView): + """ + Generic merge view. + """ + + permission_action_required = "change" + form_class = GenericMergeForm + template_name = "generic/generic_merge.html" + + def setup(self, *args, **kwargs): + super().setup(*args, **kwargs) + self.object = get_object_or_404(self.model, pk=self.kwargs["pk"]) + self.other = get_object_or_404(self.model, pk=self.kwargs["otherpk"]) + + def get_context_data(self, **kwargs): + """ + The context consists of the two objects that are merged as well + as a list of changes. Those changes are presented in the view as + a table with diffs + """ + Change = namedtuple("Change", "field old new") + ctx = super().get_context_data(**kwargs) + ctx["changes"] = [] + for field in self.object._meta.fields: + newval = self.object.get_field_value_after_merge(self.other, field) + ctx["changes"].append( + Change(field.verbose_name, getattr(self.object, field.name), newval) + ) + ctx["object"] = self.object + ctx["other"] = self.other + return ctx + + def form_valid(self, form): + self.object.merge_with([self.other]) + messages.info(self.request, f"Merged values of {self.other} into {self.object}") + return super().form_valid(form) + + def get_success_url(self): + return self.object.get_absolute_url()