Skip to content

Commit 66f355d

Browse files
authored
Merge branch 'main' into readme-dj-ver-guidance
2 parents c12cbfb + 009d66c commit 66f355d

File tree

4 files changed

+258
-4
lines changed

4 files changed

+258
-4
lines changed

django_mongodb_backend/forms/fields/embedded_model.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,17 @@ def decompress(self, value):
2020

2121

2222
class EmbeddedModelBoundField(forms.BoundField):
23+
def __init__(self, form, field, name, prefix_override=None):
24+
super().__init__(form, field, name)
25+
# prefix_override overrides the prefix in self.field.form_kwargs so
26+
# that nested embedded model form elements have the correct name.
27+
self.prefix_override = prefix_override
28+
2329
def __str__(self):
2430
"""Render the model form as the representation for this field."""
2531
form = self.field.model_form_cls(instance=self.value(), **self.field.form_kwargs)
32+
if self.prefix_override:
33+
form.prefix = self.prefix_override
2634
return mark_safe(f"{form.as_div()}") # noqa: S308
2735

2836

@@ -53,10 +61,21 @@ def compress(self, data_dict):
5361
return self.model_form._meta.model(**values)
5462

5563
def get_bound_field(self, form, field_name):
56-
return EmbeddedModelBoundField(form, self, field_name)
64+
# Nested embedded model form fields need a double prefix.
65+
prefix_override = f"{form.prefix}-{self.model_form.prefix}" if form.prefix else None
66+
return EmbeddedModelBoundField(form, self, field_name, prefix_override)
5767

5868
def bound_data(self, data, initial):
5969
if self.disabled:
6070
return initial
6171
# Transform the bound data into a model instance.
6272
return self.compress(data)
73+
74+
def prepare_value(self, value):
75+
# When rendering a form with errors, nested EmbeddedModelField data
76+
# won't be compressed if MultiValueField.clean() raises ValidationError
77+
# error before compress() is called. The data must be compressed here
78+
# so that EmbeddedModelBoundField.value() returns a model instance
79+
# (rather than a list) for initializing the form in
80+
# EmbeddedModelBoundField.__str__().
81+
return self.compress(value) if isinstance(value, list) else value

tests/model_forms_/forms.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
from django import forms
22

3-
from .models import Author
3+
from .models import Author, Book
44

55

66
class AuthorForm(forms.ModelForm):
77
class Meta:
88
fields = "__all__"
99
model = Author
10+
11+
12+
class BookForm(forms.ModelForm):
13+
class Meta:
14+
fields = "__all__"
15+
model = Book

tests/model_forms_/models.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,13 @@ class Author(models.Model):
1616
age = models.IntegerField()
1717
address = EmbeddedModelField(Address)
1818
billing_address = EmbeddedModelField(Address, blank=True, null=True)
19+
20+
21+
class Publisher(EmbeddedModel):
22+
name = models.CharField(max_length=50)
23+
address = EmbeddedModelField(Address)
24+
25+
26+
class Book(models.Model):
27+
title = models.CharField(max_length=50)
28+
publisher = EmbeddedModelField(Publisher)

tests/model_forms_/test_embedded_model.py

Lines changed: 221 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from django.test import TestCase
22

3-
from .forms import AuthorForm
4-
from .models import Address, Author
3+
from .forms import AuthorForm, BookForm
4+
from .models import Address, Author, Book, Publisher
55

66

77
class ModelFormTests(TestCase):
@@ -128,3 +128,222 @@ def test_rendering(self):
128128
<input type="number" name="address-zip_code" required id="id_address-zip_code">
129129
</div>""",
130130
)
131+
132+
133+
class NestedFormTests(TestCase):
134+
def test_update(self):
135+
book = Book.objects.create(
136+
title="Learning MongoDB",
137+
publisher=Publisher(
138+
name="Random House", address=Address(city="NYC", state="NY", zip_code="10001")
139+
),
140+
)
141+
data = {
142+
"title": "Learning MongoDB!",
143+
"publisher-name": "Random House!",
144+
"publisher-address-po_box": "",
145+
"publisher-address-city": "New York City",
146+
"publisher-address-state": "NY",
147+
"publisher-address-zip_code": "10001",
148+
}
149+
form = BookForm(data, instance=book)
150+
self.assertTrue(form.is_valid())
151+
form.save()
152+
book.refresh_from_db()
153+
self.assertEqual(book.title, "Learning MongoDB!")
154+
self.assertEqual(book.publisher.name, "Random House!")
155+
self.assertEqual(book.publisher.address.city, "New York City")
156+
157+
def test_some_missing_data(self):
158+
"""A required field (zip_code) is missing."""
159+
book = Book.objects.create(
160+
title="Learning MongoDB",
161+
publisher=Publisher(
162+
name="Random House", address=Address(city="NYC", state="NY", zip_code="10001")
163+
),
164+
)
165+
data = {
166+
"title": "Learning MongoDB!",
167+
"publisher-name": "Random House!",
168+
"publisher-address-po_box": "",
169+
"publisher-address-city": "New York City",
170+
"publisher-address-state": "NY",
171+
"publisher-address-zip_code": "",
172+
}
173+
form = BookForm(data, instance=book)
174+
self.assertFalse(form.is_valid())
175+
self.assertEqual(form.errors["publisher"], ["Enter all required values."])
176+
self.assertHTMLEqual(
177+
str(form),
178+
"""
179+
<div>
180+
<label for="id_title">Title:</label>
181+
<input type="text" name="title" value="Learning MongoDB!" maxlength="50"
182+
required id="id_title">
183+
</div>
184+
<div>
185+
<fieldset>
186+
<legend>Publisher:</legend>
187+
<ul class="errorlist">
188+
<li>Enter all required values.</li>
189+
</ul>
190+
<div>
191+
<label for="id_publisher-name">Name:</label>
192+
<input type="text" name="publisher-name" value="Random House!" maxlength="50"
193+
required id="id_publisher-name">
194+
</div>
195+
<div>
196+
<fieldset>
197+
<legend>Address:</legend>
198+
<div>
199+
<label for="id_publisher-address-po_box">PO Box:</label>
200+
<input type="text" name="publisher-address-po_box" maxlength="50"
201+
id="id_publisher-address-po_box">
202+
</div>
203+
<div>
204+
<label for="id_publisher-address-city">City:</label>
205+
<input type="text" name="publisher-address-city" value="New York City"
206+
maxlength="20" required id="id_publisher-address-city">
207+
</div>
208+
<div>
209+
<label for="id_publisher-address-state">State:</label>
210+
<input type="text" name="publisher-address-state" value="NY"
211+
maxlength="2" required id="id_publisher-address-state">
212+
</div>
213+
<div>
214+
<label for="id_publisher-address-zip_code">Zip code:</label>
215+
<input type="number" name="publisher-address-zip_code"
216+
required id="id_publisher-address-zip_code">
217+
</div>
218+
</fieldset>
219+
</div>
220+
</fieldset>
221+
</div>""",
222+
)
223+
224+
def test_invalid_field_data(self):
225+
"""A field's data (state) is too long."""
226+
book = Book.objects.create(
227+
title="Learning MongoDB",
228+
publisher=Publisher(
229+
name="Random House", address=Address(city="NYC", state="NY", zip_code="10001")
230+
),
231+
)
232+
data = {
233+
"title": "Learning MongoDB!",
234+
"publisher-name": "Random House!",
235+
"publisher-address-po_box": "",
236+
"publisher-address-city": "New York City",
237+
"publisher-address-state": "TOO LONG",
238+
"publisher-address-zip_code": "10001",
239+
}
240+
form = BookForm(data, instance=book)
241+
self.assertFalse(form.is_valid())
242+
self.assertEqual(
243+
form.errors["publisher"],
244+
["Ensure this value has at most 2 characters (it has 8)."],
245+
)
246+
self.assertHTMLEqual(
247+
str(form),
248+
"""
249+
<div>
250+
<label for="id_title">Title:</label>
251+
<input type="text" name="title" value="Learning MongoDB!"
252+
maxlength="50" required id="id_title">
253+
</div>
254+
<div>
255+
<fieldset>
256+
<legend>Publisher:</legend>
257+
<ul class="errorlist">
258+
<li>Ensure this value has at most 2 characters (it has 8).</li>
259+
</ul>
260+
<div>
261+
<label for="id_publisher-name">Name:</label>
262+
<input type="text" name="publisher-name" value="Random House!"
263+
maxlength="50" required id="id_publisher-name">
264+
</div>
265+
<div>
266+
<fieldset>
267+
<legend>Address:</legend>
268+
<div>
269+
<label for="id_publisher-address-po_box">PO Box:</label>
270+
<input type="text" name="publisher-address-po_box"
271+
maxlength="50" id="id_publisher-address-po_box">
272+
</div>
273+
<div>
274+
<label for="id_publisher-address-city">City:</label>
275+
<input type="text" name="publisher-address-city" value="New York City"
276+
maxlength="20" required id="id_publisher-address-city">
277+
</div>
278+
<div>
279+
<label for="id_publisher-address-state">State:</label>
280+
<input type="text" name="publisher-address-state" value="TOO LONG"
281+
maxlength="2" required id="id_publisher-address-state">
282+
</div>
283+
<div>
284+
<label for="id_publisher-address-zip_code">Zip code:</label>
285+
<input type="number" name="publisher-address-zip_code" value="10001"
286+
required id="id_publisher-address-zip_code">
287+
</div>
288+
</fieldset>
289+
</div>
290+
</fieldset>
291+
</div>""",
292+
)
293+
294+
def test_all_missing_data(self):
295+
"""An embedded model with all data missing triggers a required error."""
296+
book = Book.objects.create(
297+
title="Learning MongoDB",
298+
publisher=Publisher(
299+
name="Random House", address=Address(city="NYC", state="NY", zip_code="10001")
300+
),
301+
)
302+
data = {
303+
"title": "Learning MongoDB!",
304+
"publisher-name": "Random House!",
305+
"publisher-address-po_box": "",
306+
"publisher-address-city": "",
307+
"publisher-address-state": "",
308+
"publisher-address-zip_code": "",
309+
}
310+
form = BookForm(data, instance=book)
311+
self.assertFalse(form.is_valid())
312+
self.assertEqual(form.errors["publisher"], ["This field is required."])
313+
314+
def test_rendering(self):
315+
form = BookForm()
316+
self.assertHTMLEqual(
317+
str(form.fields["publisher"].get_bound_field(form, "publisher")),
318+
"""
319+
<div>
320+
<label for="id_publisher-name">Name:</label>
321+
<input type="text" name="publisher-name" maxlength="50"
322+
required id="id_publisher-name">
323+
</div>
324+
<div>
325+
<fieldset>
326+
<legend>Address:</legend>
327+
<div>
328+
<label for="id_publisher-address-po_box">PO Box:</label>
329+
<input type="text" name="publisher-address-po_box" maxlength="50"
330+
id="id_publisher-address-po_box">
331+
</div>
332+
<div>
333+
<label for="id_publisher-address-city">City:</label>
334+
<input type="text" name="publisher-address-city" maxlength="20"
335+
required id="id_publisher-address-city">
336+
</div>
337+
<div>
338+
<label for="id_publisher-address-state">State:</label>
339+
<input type="text" name="publisher-address-state" maxlength="2"
340+
required id="id_publisher-address-state">
341+
</div>
342+
<div>
343+
<label for="id_publisher-address-zip_code">Zip code:</label>
344+
<input type="number" name="publisher-address-zip_code"
345+
required id="id_publisher-address-zip_code">
346+
</div>
347+
</fieldset>
348+
</div>""",
349+
)

0 commit comments

Comments
 (0)