Skip to content

Commit 5a3e6ae

Browse files
committed
wip forms
1 parent 9acb7a1 commit 5a3e6ae

File tree

8 files changed

+589
-2
lines changed

8 files changed

+589
-2
lines changed

django_mongodb_backend/fields/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
from .duration import register_duration_field
44
from .embedded_model import EmbeddedModelField
55
from .json import register_json_field
6+
from .multiple_embedded_model import MultipleEmbeddedModelField
67
from .objectid import ObjectIdField
78

89
__all__ = [
910
"register_fields",
1011
"ArrayField",
1112
"EmbeddedModelField",
13+
"MultipleEmbeddedModelField",
1214
"ObjectIdAutoField",
1315
"ObjectIdField",
1416
]
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from ..forms import MultipleEmbeddedModelFormField
2+
from . import EmbeddedModelField
3+
from .array import ArrayField
4+
5+
6+
class MultipleEmbeddedModelField(ArrayField):
7+
def __init__(self, model, **kwargs):
8+
super().__init__(EmbeddedModelField(model), **kwargs)
9+
10+
def deconstruct(self):
11+
name, path, args, kwargs = super().deconstruct()
12+
if (
13+
path
14+
== "django_mongodb_backend.fields.multiple_embedded_model.MultipleEmbeddedModelField"
15+
):
16+
path = "django_mongodb_backend.fields.MultipleEmbeddedModelField"
17+
kwargs.update(
18+
{
19+
"model": self.base_field.embedded_model,
20+
"size": self.size,
21+
}
22+
)
23+
del kwargs["base_field"]
24+
return name, path, args, kwargs
25+
26+
def formfield(self, **kwargs):
27+
return super().formfield(
28+
**{
29+
"form_class": MultipleEmbeddedModelFormField,
30+
"model": self.base_field.embedded_model,
31+
"max_length": self.size,
32+
"prefix": self.name,
33+
**kwargs,
34+
}
35+
)

django_mongodb_backend/forms/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .fields import (
22
EmbeddedModelField,
3+
MultipleEmbeddedModelFormField,
34
ObjectIdField,
45
SimpleArrayField,
56
SplitArrayField,
@@ -8,6 +9,7 @@
89

910
__all__ = [
1011
"EmbeddedModelField",
12+
"MultipleEmbeddedModelFormField",
1113
"SimpleArrayField",
1214
"SplitArrayField",
1315
"SplitArrayWidget",

django_mongodb_backend/forms/fields/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from .array import SimpleArrayField, SplitArrayField, SplitArrayWidget
22
from .embedded_model import EmbeddedModelField
3+
from .multiple_embedded_model import MultipleEmbeddedModelFormField
34
from .objectid import ObjectIdField
45

56
__all__ = [
67
"EmbeddedModelField",
8+
"MultipleEmbeddedModelFormField",
79
"SimpleArrayField",
810
"SplitArrayField",
911
"SplitArrayWidget",
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from django import forms
2+
from django.core.exceptions import ValidationError
3+
from django.db.models import Model
4+
from django.forms import formset_factory, model_to_dict
5+
from django.forms.models import modelform_factory
6+
from django.utils.html import format_html, format_html_join
7+
from django.utils.translation import gettext_lazy as _
8+
9+
10+
class MultipleEmbeddedModelFormField(forms.Field):
11+
default_error_messages = {"incomplete": _("Enter all required values.")}
12+
13+
def __init__(self, model, prefix, max_length=None, *args, **kwargs):
14+
kwargs.pop("base_field")
15+
self.model = model
16+
self.prefix = prefix
17+
self.model_form_cls = modelform_factory(model, fields="__all__")
18+
self.formset = formset_factory(
19+
form=self.model_form_cls, can_delete=True, max_num=max_length
20+
)
21+
kwargs["widget"] = MultipleEmbeddedModelWidget(self.model_form_cls.__name__)
22+
super().__init__(*args, **kwargs)
23+
24+
def clean(self, value):
25+
if not value:
26+
return []
27+
formset = self.formset(value, prefix=self.prefix)
28+
if not formset.is_valid():
29+
raise ValidationError(formset.errors + formset.non_form_errors())
30+
cleaned_data = []
31+
for data in formset.cleaned_data:
32+
if data.get("DELETE", True):
33+
continue
34+
data.pop("DELETE")
35+
cleaned_data.append(self.model_form_cls._meta.model(**data))
36+
return cleaned_data
37+
38+
def has_changed(self, initial, data):
39+
formset_initial = []
40+
for initial_data in initial or []:
41+
formset_initial.append(forms.model_to_dict(initial_data))
42+
formset = self.formset(data, initial=formset_initial, prefix=self.prefix)
43+
return formset.has_changed()
44+
45+
def get_bound_field(self, form, field_name):
46+
return MultipleEmbeddedModelBoundField(form, self, field_name)
47+
48+
49+
class MultipleEmbeddedModelBoundField(forms.BoundField):
50+
def __init__(self, form, field, name):
51+
super().__init__(form, field, name)
52+
data = self.data if form.is_bound else None
53+
formset_initial = []
54+
if self.initial is not None:
55+
for initial in self.initial:
56+
if isinstance(initial, Model):
57+
formset_initial.append(model_to_dict(initial))
58+
self.formset = field.formset(data, initial=formset_initial, prefix=self.html_name)
59+
60+
def __getitem__(self, idx):
61+
if not isinstance(idx, (int | slice)):
62+
raise TypeError
63+
return self.formset[idx]
64+
65+
def __iter__(self):
66+
yield from self.formset
67+
68+
def __str__(self):
69+
table = format_html_join(
70+
"\n", "<tbody>{}</tbody>", ((form.as_table(),) for form in self.formset)
71+
)
72+
table = format_html("\n<table>" "\n{}" "\n</table>", table)
73+
return format_html("{}\n{}", table, self.formset.management_form)
74+
75+
def __len__(self):
76+
return len(self.formset)
77+
78+
79+
class MultipleEmbeddedModelWidget(forms.Widget):
80+
def __init__(self, field_id, attrs=None):
81+
self.field_id = field_id
82+
super().__init__(attrs)
83+
84+
def render(self, name, value, attrs=None, renderer=None):
85+
raise NotImplementedError("This widget is not meant to be rendered.")
86+
87+
def id_for_label(self, id_):
88+
return f"{id_}-0-{self.field_id}"
89+
90+
def value_from_datadict(self, data, files, name):
91+
return {key: data[key] for key in data if key.startswith(name)}
92+
93+
def value_omitted_from_data(self, data, files, name):
94+
return any(key.startswith(name) for key in data)

tests/model_forms_/forms.py

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

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

55

66
class AuthorForm(forms.ModelForm):
@@ -13,3 +13,9 @@ class BookForm(forms.ModelForm):
1313
class Meta:
1414
fields = "__all__"
1515
model = Book
16+
17+
18+
class MovieForm(forms.ModelForm):
19+
class Meta:
20+
fields = "__all__"
21+
model = Movie

tests/model_forms_/models.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from django.db import models
22

3-
from django_mongodb_backend.fields import EmbeddedModelField
3+
from django_mongodb_backend.fields import EmbeddedModelField, MultipleEmbeddedModelField
44
from django_mongodb_backend.models import EmbeddedModel
55

66

@@ -26,3 +26,19 @@ class Publisher(EmbeddedModel):
2626
class Book(models.Model):
2727
title = models.CharField(max_length=50)
2828
publisher = EmbeddedModelField(Publisher)
29+
30+
31+
class Review(EmbeddedModel):
32+
title = models.CharField(max_length=255)
33+
rating = models.IntegerField()
34+
35+
def __str__(self):
36+
return self.title
37+
38+
39+
class Movie(models.Model):
40+
title = models.CharField(max_length=255)
41+
reviews = MultipleEmbeddedModelField(Review, null=True)
42+
43+
def __str__(self):
44+
return self.title

0 commit comments

Comments
 (0)