From 3a02a015d2f924116e3973b54afd2c3d3ec85897 Mon Sep 17 00:00:00 2001 From: Akhilesh Muthusamy Date: Thu, 6 Feb 2025 21:54:11 +0000 Subject: [PATCH 1/7] Fix issue #261: Handle unique constraint validation error --- modeltranslation/admin.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/modeltranslation/admin.py b/modeltranslation/admin.py index 905328aa..e15669f9 100644 --- a/modeltranslation/admin.py +++ b/modeltranslation/admin.py @@ -239,9 +239,25 @@ def _get_form_or_formset( exclude.extend(self.form._meta.exclude) # If exclude is an empty list we pass None to be consistent with the # default on modelform_factory - exclude = self.replace_orig_field(exclude) or None - exclude = self._exclude_original_fields(exclude) - kwargs.update({"exclude": exclude}) + updated_exclude = self.replace_orig_field(exclude) or None + # In order for BaseModelAdmin to show unique-contraint validation error, + # add the original translation fields to kwargs, and do not exclude the + # original translation fields. + if kwargs["fields"]: + original_fields = [ + field for field in self.trans_opts.fields.keys() if field not in exclude + ] + kwargs["fields"].extend(original_fields) + kwargs.update( + { + "exclude": list(set(exclude + updated_exclude)) + if updated_exclude + else updated_exclude + } + ) + else: + updated_exclude = self._exclude_original_fields(updated_exclude) + kwargs.update({"exclude": updated_exclude}) return kwargs From 5ab41ada1831d9502dd6e2233d91b60a0387978c Mon Sep 17 00:00:00 2001 From: Akhilesh Muthusamy Date: Fri, 7 Feb 2025 22:02:20 +0000 Subject: [PATCH 2/7] Add logic to populate original field --- modeltranslation/admin.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/modeltranslation/admin.py b/modeltranslation/admin.py index e15669f9..5c06746e 100644 --- a/modeltranslation/admin.py +++ b/modeltranslation/admin.py @@ -1,6 +1,7 @@ from __future__ import annotations from copy import deepcopy +from itertools import chain from typing import Any, TypeVar, TYPE_CHECKING from collections.abc import Iterable, Sequence @@ -243,16 +244,36 @@ def _get_form_or_formset( # In order for BaseModelAdmin to show unique-contraint validation error, # add the original translation fields to kwargs, and do not exclude the # original translation fields. - if kwargs["fields"]: + + if isinstance(self.trans_opts.fields, dict): + trans_ops_fields = self.trans_opts.fields.keys() + else: + trans_ops_fields = self.trans_opts.fields + + # Copy default language field value into the original field. + if request.POST: + mutable_query_dict = request.POST.copy() + for field in trans_ops_fields: + default_lang_field = field + "_" + mt_settings.DEFAULT_LANGUAGE + if default_lang_field in mutable_query_dict.keys(): + if field not in mutable_query_dict.keys(): + mutable_query_dict.appendlist( + field, mutable_query_dict.get(default_lang_field) + ) + request.POST = mutable_query_dict + + if kwargs.get("fields"): original_fields = [ - field for field in self.trans_opts.fields.keys() if field not in exclude + field for field in trans_ops_fields if field not in exclude ] kwargs["fields"].extend(original_fields) kwargs.update( { - "exclude": list(set(exclude + updated_exclude)) - if updated_exclude - else updated_exclude + "exclude": ( + list(set(chain(exclude, updated_exclude))) + if updated_exclude + else updated_exclude + ) } ) else: From ebd5ade7411fad84bb8abcce08590be43fad2256 Mon Sep 17 00:00:00 2001 From: Akhilesh Muthusamy Date: Sun, 9 Feb 2025 01:15:40 -0500 Subject: [PATCH 3/7] Update unit test for unique constraint error --- modeltranslation/admin.py | 19 +++++-------------- modeltranslation/tests/test_admin_views.py | 22 +++++++++++++++------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/modeltranslation/admin.py b/modeltranslation/admin.py index fef53bfe..da122e40 100644 --- a/modeltranslation/admin.py +++ b/modeltranslation/admin.py @@ -241,19 +241,10 @@ def _get_form_or_formset( # If exclude is an empty list we pass None to be consistent with the # default on modelform_factory updated_exclude = self.replace_orig_field(exclude) or None - # In order for BaseModelAdmin to show unique-contraint validation error, - # add the original translation fields to kwargs, and do not exclude the - # original translation fields. - - if isinstance(self.trans_opts.fields, dict): - trans_ops_fields = self.trans_opts.fields.keys() - else: - trans_ops_fields = self.trans_opts.fields - # Copy default language field value into the original field. if request.POST: mutable_query_dict = request.POST.copy() - for field in trans_ops_fields: + for field in self.trans_opts.fields: default_lang_field = field + "_" + mt_settings.DEFAULT_LANGUAGE if default_lang_field in mutable_query_dict.keys(): if field not in mutable_query_dict.keys(): @@ -261,11 +252,11 @@ def _get_form_or_formset( field, mutable_query_dict.get(default_lang_field) ) request.POST = mutable_query_dict - + # In order for BaseModelAdmin to show unique-constraint validation error, + # add the original translation fields to kwargs, and do not exclude the + # original translation fields. if kwargs.get("fields"): - original_fields = [ - field for field in trans_ops_fields if field not in exclude - ] + original_fields = [field for field in self.trans_opts.fields if field not in exclude] kwargs["fields"].extend(original_fields) kwargs.update( { diff --git a/modeltranslation/tests/test_admin_views.py b/modeltranslation/tests/test_admin_views.py index 701ef364..b9629cf2 100644 --- a/modeltranslation/tests/test_admin_views.py +++ b/modeltranslation/tests/test_admin_views.py @@ -1,17 +1,25 @@ +"""Tests for admin view.""" + from django.test import Client from django.urls import reverse -import pytest + from modeltranslation.tests.models import ModelWithConstraint -from django.db import IntegrityError def test_create_duplicate(admin_client: Client): + """Unique constraint error should be handled by TranslationAdmin.""" ModelWithConstraint.objects.create(title="1", sub_title="One") url = reverse("admin:tests_modelwithconstraint_add") - with pytest.raises(IntegrityError): - response = admin_client.post( - url, {"title": "1", "sub_title_en": "One", "sub_title_de": "Ein"} - ) + response = admin_client.post( + url, + { + "title": "1", + "sub_title_en": "One", + "sub_title_de": "Ein", + }, + ) - assert response.status_code == 302 + error_msg = "Model with constraint with this Title and Sub title already exists." + assert error_msg in response.context["errors"][0] + assert response.status_code == 200 From 924d4351748c25e1290b9ccf6c1a4e137242e837 Mon Sep 17 00:00:00 2001 From: Serhii Tereshchenko Date: Sun, 9 Feb 2025 15:32:08 +0200 Subject: [PATCH 4/7] chore: Fix type errors; rename variable --- modeltranslation/admin.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/modeltranslation/admin.py b/modeltranslation/admin.py index da122e40..cdd3e190 100644 --- a/modeltranslation/admin.py +++ b/modeltranslation/admin.py @@ -243,15 +243,14 @@ def _get_form_or_formset( updated_exclude = self.replace_orig_field(exclude) or None # Copy default language field value into the original field. if request.POST: - mutable_query_dict = request.POST.copy() + data = request.POST.copy() for field in self.trans_opts.fields: default_lang_field = field + "_" + mt_settings.DEFAULT_LANGUAGE - if default_lang_field in mutable_query_dict.keys(): - if field not in mutable_query_dict.keys(): - mutable_query_dict.appendlist( - field, mutable_query_dict.get(default_lang_field) - ) - request.POST = mutable_query_dict + if default_lang_field in data.keys(): + value = data.get(default_lang_field) + if field not in data.keys() and value: + data.appendlist(field, value) + request.POST = data # type: ignore[assignment] # In order for BaseModelAdmin to show unique-constraint validation error, # add the original translation fields to kwargs, and do not exclude the # original translation fields. From d1426d1ad4193438d388941077badd783a0c9edb Mon Sep 17 00:00:00 2001 From: Serhii Tereshchenko Date: Sun, 9 Feb 2025 15:37:31 +0200 Subject: [PATCH 5/7] chore: Cleanup --- modeltranslation/admin.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/modeltranslation/admin.py b/modeltranslation/admin.py index cdd3e190..8f422ba7 100644 --- a/modeltranslation/admin.py +++ b/modeltranslation/admin.py @@ -1,7 +1,6 @@ from __future__ import annotations from copy import deepcopy -from itertools import chain from typing import Any, TypeVar, TYPE_CHECKING from collections.abc import Iterable, Sequence @@ -257,15 +256,7 @@ def _get_form_or_formset( if kwargs.get("fields"): original_fields = [field for field in self.trans_opts.fields if field not in exclude] kwargs["fields"].extend(original_fields) - kwargs.update( - { - "exclude": ( - list(set(chain(exclude, updated_exclude))) - if updated_exclude - else updated_exclude - ) - } - ) + kwargs.update({"exclude": (updated_exclude and list({*exclude, *updated_exclude}))}) else: updated_exclude = self._exclude_original_fields(updated_exclude) kwargs.update({"exclude": updated_exclude}) From e207669de03a683c2b3d2898baa78e14ae8d48b3 Mon Sep 17 00:00:00 2001 From: Serhii Tereshchenko Date: Sun, 9 Feb 2025 15:47:56 +0200 Subject: [PATCH 6/7] fix: Use build_localized_fieldname --- modeltranslation/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modeltranslation/admin.py b/modeltranslation/admin.py index 8f422ba7..4fb2a557 100644 --- a/modeltranslation/admin.py +++ b/modeltranslation/admin.py @@ -244,7 +244,7 @@ def _get_form_or_formset( if request.POST: data = request.POST.copy() for field in self.trans_opts.fields: - default_lang_field = field + "_" + mt_settings.DEFAULT_LANGUAGE + default_lang_field = build_localized_fieldname(field, mt_settings.DEFAULT_LANGUAGE) if default_lang_field in data.keys(): value = data.get(default_lang_field) if field not in data.keys() and value: From 9133314c48b57ac86db5c12a65d37dfe897a8b1c Mon Sep 17 00:00:00 2001 From: Serhii Tereshchenko Date: Sun, 9 Feb 2025 15:48:09 +0200 Subject: [PATCH 7/7] chore: Cleanup --- modeltranslation/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modeltranslation/admin.py b/modeltranslation/admin.py index 4fb2a557..138d7695 100644 --- a/modeltranslation/admin.py +++ b/modeltranslation/admin.py @@ -245,7 +245,7 @@ def _get_form_or_formset( data = request.POST.copy() for field in self.trans_opts.fields: default_lang_field = build_localized_fieldname(field, mt_settings.DEFAULT_LANGUAGE) - if default_lang_field in data.keys(): + if default_lang_field in data: value = data.get(default_lang_field) if field not in data.keys() and value: data.appendlist(field, value)