diff --git a/django_mongodb_backend/forms/fields/embedded_model.py b/django_mongodb_backend/forms/fields/embedded_model.py index b86e85e78..bbfa9c02c 100644 --- a/django_mongodb_backend/forms/fields/embedded_model.py +++ b/django_mongodb_backend/forms/fields/embedded_model.py @@ -20,9 +20,17 @@ def decompress(self, value): class EmbeddedModelBoundField(forms.BoundField): + def __init__(self, form, field, name, prefix_override=None): + super().__init__(form, field, name) + # prefix_override overrides the prefix in self.field.form_kwargs so + # that nested embedded model form elements have the correct name. + self.prefix_override = prefix_override + def __str__(self): """Render the model form as the representation for this field.""" form = self.field.model_form_cls(instance=self.value(), **self.field.form_kwargs) + if self.prefix_override: + form.prefix = self.prefix_override return mark_safe(f"{form.as_div()}") # noqa: S308 @@ -53,10 +61,21 @@ def compress(self, data_dict): return self.model_form._meta.model(**values) def get_bound_field(self, form, field_name): - return EmbeddedModelBoundField(form, self, field_name) + # Nested embedded model form fields need a double prefix. + prefix_override = f"{form.prefix}-{self.model_form.prefix}" if form.prefix else None + return EmbeddedModelBoundField(form, self, field_name, prefix_override) def bound_data(self, data, initial): if self.disabled: return initial # Transform the bound data into a model instance. return self.compress(data) + + def prepare_value(self, value): + # When rendering a form with errors, nested EmbeddedModelField data + # won't be compressed if MultiValueField.clean() raises ValidationError + # error before compress() is called. The data must be compressed here + # so that EmbeddedModelBoundField.value() returns a model instance + # (rather than a list) for initializing the form in + # EmbeddedModelBoundField.__str__(). + return self.compress(value) if isinstance(value, list) else value diff --git a/tests/model_forms_/forms.py b/tests/model_forms_/forms.py index 7bfed3fbb..1ac7b92a9 100644 --- a/tests/model_forms_/forms.py +++ b/tests/model_forms_/forms.py @@ -1,9 +1,15 @@ from django import forms -from .models import Author +from .models import Author, Book class AuthorForm(forms.ModelForm): class Meta: fields = "__all__" model = Author + + +class BookForm(forms.ModelForm): + class Meta: + fields = "__all__" + model = Book diff --git a/tests/model_forms_/models.py b/tests/model_forms_/models.py index df3bd580f..4e7cd0d6c 100644 --- a/tests/model_forms_/models.py +++ b/tests/model_forms_/models.py @@ -16,3 +16,13 @@ class Author(models.Model): age = models.IntegerField() address = EmbeddedModelField(Address) billing_address = EmbeddedModelField(Address, blank=True, null=True) + + +class Publisher(EmbeddedModel): + name = models.CharField(max_length=50) + address = EmbeddedModelField(Address) + + +class Book(models.Model): + title = models.CharField(max_length=50) + publisher = EmbeddedModelField(Publisher) diff --git a/tests/model_forms_/test_embedded_model.py b/tests/model_forms_/test_embedded_model.py index 240f8c6d8..4447b59f2 100644 --- a/tests/model_forms_/test_embedded_model.py +++ b/tests/model_forms_/test_embedded_model.py @@ -1,7 +1,7 @@ from django.test import TestCase -from .forms import AuthorForm -from .models import Address, Author +from .forms import AuthorForm, BookForm +from .models import Address, Author, Book, Publisher class ModelFormTests(TestCase): @@ -128,3 +128,222 @@ def test_rendering(self): """, ) + + +class NestedFormTests(TestCase): + def test_update(self): + book = Book.objects.create( + title="Learning MongoDB", + publisher=Publisher( + name="Random House", address=Address(city="NYC", state="NY", zip_code="10001") + ), + ) + data = { + "title": "Learning MongoDB!", + "publisher-name": "Random House!", + "publisher-address-po_box": "", + "publisher-address-city": "New York City", + "publisher-address-state": "NY", + "publisher-address-zip_code": "10001", + } + form = BookForm(data, instance=book) + self.assertTrue(form.is_valid()) + form.save() + book.refresh_from_db() + self.assertEqual(book.title, "Learning MongoDB!") + self.assertEqual(book.publisher.name, "Random House!") + self.assertEqual(book.publisher.address.city, "New York City") + + def test_some_missing_data(self): + """A required field (zip_code) is missing.""" + book = Book.objects.create( + title="Learning MongoDB", + publisher=Publisher( + name="Random House", address=Address(city="NYC", state="NY", zip_code="10001") + ), + ) + data = { + "title": "Learning MongoDB!", + "publisher-name": "Random House!", + "publisher-address-po_box": "", + "publisher-address-city": "New York City", + "publisher-address-state": "NY", + "publisher-address-zip_code": "", + } + form = BookForm(data, instance=book) + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors["publisher"], ["Enter all required values."]) + self.assertHTMLEqual( + str(form), + """ +
+ + +
+
+
+ Publisher: + +
+ + +
+
+
+ Address: +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
""", + ) + + def test_invalid_field_data(self): + """A field's data (state) is too long.""" + book = Book.objects.create( + title="Learning MongoDB", + publisher=Publisher( + name="Random House", address=Address(city="NYC", state="NY", zip_code="10001") + ), + ) + data = { + "title": "Learning MongoDB!", + "publisher-name": "Random House!", + "publisher-address-po_box": "", + "publisher-address-city": "New York City", + "publisher-address-state": "TOO LONG", + "publisher-address-zip_code": "10001", + } + form = BookForm(data, instance=book) + self.assertFalse(form.is_valid()) + self.assertEqual( + form.errors["publisher"], + ["Ensure this value has at most 2 characters (it has 8)."], + ) + self.assertHTMLEqual( + str(form), + """ +
+ + +
+
+
+ Publisher: + +
+ + +
+
+
+ Address: +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
""", + ) + + def test_all_missing_data(self): + """An embedded model with all data missing triggers a required error.""" + book = Book.objects.create( + title="Learning MongoDB", + publisher=Publisher( + name="Random House", address=Address(city="NYC", state="NY", zip_code="10001") + ), + ) + data = { + "title": "Learning MongoDB!", + "publisher-name": "Random House!", + "publisher-address-po_box": "", + "publisher-address-city": "", + "publisher-address-state": "", + "publisher-address-zip_code": "", + } + form = BookForm(data, instance=book) + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors["publisher"], ["This field is required."]) + + def test_rendering(self): + form = BookForm() + self.assertHTMLEqual( + str(form.fields["publisher"].get_bound_field(form, "publisher")), + """ +
+ + +
+
+
+ Address: +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
""", + )