Coverage for apis_core/relations/forms.py: 25%
100 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-16 07:42 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-16 07:42 +0000
1from crispy_forms.helper import FormHelper
2from crispy_forms.layout import Submit
3from dal import autocomplete
4from django import forms
5from django.contrib.contenttypes.models import ContentType
6from django.core.exceptions import ValidationError
7from django.shortcuts import get_object_or_404
8from django.urls import reverse
9from django.utils.http import urlencode
11from apis_core.generic.forms import GenericModelForm
14class CustomSelect2ListChoiceField(autocomplete.Select2ListChoiceField):
15 """
16 We use a custom Select2ListChoiceField in our Relation form,
17 because we don't want the form value to be validated. The field
18 uses the `choices` setting as a basis for validating, but our
19 choices span over multiple querysets, so its easier to simply not
20 validate (some validation happens in the `clean` method anyway).
21 """
23 def validate(self, value):
24 if "_" not in value:
25 raise ValidationError("please choose a correct value")
28class RelationForm(GenericModelForm):
29 """
30 This form overrides generic form for relation editing.
31 Relations have generic relations to subj and obj, but we
32 hide those ForeignKey form fields and instead show
33 autocomplete choices fields.
34 """
36 class Meta:
37 fields = "__all__"
38 widgets = {
39 "subj_content_type": forms.HiddenInput(),
40 "subj_object_id": forms.HiddenInput(),
41 "obj_content_type": forms.HiddenInput(),
42 "obj_object_id": forms.HiddenInput(),
43 }
45 def __entities_autocomplete_with_params(self, content_types=[ContentType]) -> str:
46 """helper method to generate the entities autocomplete url with contentype parameters"""
47 url = reverse("apis_core:apis_entities:autocomplete")
48 params = [f"entities={ct.app_label}.{ct.model}" for ct in content_types]
49 return url + "?" + "&".join(params)
51 def __subj_autocomplete_url(self, subj_content_type=None) -> str:
52 """generate the autocomplete url for the subj field. by default use the
53 subject types configured in the relation. If there is a subj_content_type
54 passed in the `initial` dict of the form, use that contenttype instead of
55 the configured ones"""
56 subj_content_types = [
57 ContentType.objects.get_for_model(model)
58 for model in self.Meta.model.subj_list()
59 ]
60 if subj_content_type:
61 subj_content_types = [ContentType.objects.get(pk=subj_content_type)]
62 return self.__entities_autocomplete_with_params(subj_content_types)
64 def __obj_autocomplete_url(self, obj_content_type=None) -> str:
65 """generate the autocomplete url for the obj field. by default use the
66 object types configured in the relation. If there is a obj_content_type
67 passed in the `initial` dict of the form, use that contenttype instead of
68 the configured ones"""
69 obj_content_types = [
70 ContentType.objects.get_for_model(model)
71 for model in self.Meta.model.obj_list()
72 ]
73 if obj_content_type:
74 obj_content_types = [ContentType.objects.get(pk=obj_content_type)]
75 return self.__entities_autocomplete_with_params(obj_content_types)
77 def __init__(self, *args, **kwargs):
78 """
79 Initialize the form and add the `subj` and `obj` fields using the
80 generic apis_entities autocomplete with the correct parameters.
81 """
83 super().__init__(*args, **kwargs)
84 subj_content_type = kwargs["initial"].get("subj_content_type", None)
85 subj_object_id = kwargs["initial"].get("subj_object_id", None)
86 obj_content_type = kwargs["initial"].get("obj_content_type", None)
87 obj_object_id = kwargs["initial"].get("obj_object_id", None)
89 self.subj_instance, self.obj_instance = None, None
90 if instance := kwargs.get("instance"):
91 self.subj_instance = instance.subj
92 self.obj_instance = instance.obj
93 else:
94 if subj_content_type and subj_object_id:
95 model = get_object_or_404(ContentType, pk=subj_content_type)
96 self.subj_instance = get_object_or_404(
97 model.model_class(), pk=subj_object_id
98 )
99 if obj_content_type and obj_object_id:
100 model = get_object_or_404(ContentType, pk=obj_content_type)
101 self.obj_instance = get_object_or_404(
102 model.model_class(), pk=obj_object_id
103 )
105 self.fields["subj_object_id"].required = False
106 self.fields["subj_content_type"].required = False
107 if not subj_object_id:
108 self.fields["subj"] = CustomSelect2ListChoiceField()
109 self.fields["subj"].widget = autocomplete.ListSelect2(
110 url=self.__subj_autocomplete_url(subj_content_type)
111 )
112 if self.subj_instance:
113 content_type = ContentType.objects.get_for_model(self.subj_instance)
114 select_identifier = f"{content_type.id}_{self.subj_instance.id}"
115 self.fields["subj"].initial = select_identifier
116 self.fields["subj"].choices = [(select_identifier, self.subj_instance)]
118 self.fields["obj_object_id"].required = False
119 self.fields["obj_content_type"].required = False
120 if not obj_object_id:
121 self.fields["obj"] = CustomSelect2ListChoiceField()
122 self.fields["obj"].widget = autocomplete.ListSelect2(
123 url=self.__obj_autocomplete_url(obj_content_type)
124 )
125 if self.obj_instance:
126 content_type = ContentType.objects.get_for_model(self.obj_instance)
127 select_identifier = f"{content_type.id}_{self.obj_instance.id}"
128 self.fields["obj"].initial = select_identifier
129 self.fields["obj"].choices = [(select_identifier, self.obj_instance)]
131 self.helper = FormHelper(self)
132 model_ct = ContentType.objects.get_for_model(self.Meta.model)
133 self.helper.form_id = f"relation_{model_ct.model}_form"
134 self.helper.add_input(Submit("submit", "Submit"))
136 def clean(self) -> dict:
137 """
138 We check if there are `subj` or `obj` fields in the form data
139 and if so, we use the data to create objects for the real fields of
140 the Relation
141 """
142 cleaned_data = super().clean()
143 if "subj" in cleaned_data:
144 subj_content_type, subj_object_id = cleaned_data["subj"].split("_")
145 cleaned_data["subj_content_type"] = ContentType.objects.get(
146 pk=subj_content_type
147 )
148 cleaned_data["subj_object_id"] = subj_object_id
149 del cleaned_data["subj"]
150 if "obj" in cleaned_data:
151 obj_content_type, obj_object_id = cleaned_data["obj"].split("_")
152 cleaned_data["obj_content_type"] = ContentType.objects.get(
153 pk=obj_content_type
154 )
155 cleaned_data["obj_object_id"] = obj_object_id
156 del cleaned_data["obj"]
157 return cleaned_data
160class RelationFormHX(RelationForm):
161 """
162 A Relation form that sets a hx-post attribute to the
163 form to make the htmx POST request use another route.
164 We also pass a `reverse` boolean, wich gets passed on
165 to the POST endpoint using url parameters. The POST
166 endpoint then calculates the success_url based on the
167 `reverse` state.
168 """
170 def __init__(self, *args, **kwargs):
171 self.is_reverse = kwargs.pop("reverse", False)
172 super().__init__(*args, **kwargs)
173 urlparams = kwargs["initial"]
174 urlparams["reverse"] = self.is_reverse
175 urlparams = {k: v for k, v in urlparams.items() if v}
176 relation_content_type = ContentType.objects.get_for_model(self.Meta.model)
177 hx_post = (
178 reverse(
179 "apis_core:relations:create_relation_form", args=[relation_content_type]
180 )
181 + "?"
182 + urlencode(urlparams)
183 )
184 self.helper.attrs = {
185 "hx-post": hx_post,
186 "hx-swap": "outerHTML",
187 "hx-target": f"#{self.helper.form_id}",
188 }
190 @property
191 def relation_name(self) -> str:
192 if self.is_reverse:
193 return self._meta.model.reverse_name
194 return self._meta.model.name