Coverage for apis_core/relations/forms/__init__.py: 32%
65 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-09-03 06:15 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-09-03 06:15 +0000
1from crispy_forms.helper import FormHelper
2from crispy_forms.layout import Submit
3from django import forms
4from django.contrib.contenttypes.models import ContentType
5from django.urls import reverse
6from django.utils.http import urlencode
7from django.utils.translation import gettext_lazy as _
9from apis_core.core.fields import ApisListSelect2
10from apis_core.generic.forms import GenericFilterSetForm, GenericModelForm
11from apis_core.generic.forms.fields import ModelImportChoiceField
14class RelationForm(GenericModelForm):
15 """
16 This form overrides generic form for relation editing.
17 Relations have generic relations to subj and obj, but we
18 hide those ForeignKey form fields and instead show
19 autocomplete choices fields.
20 In addition, one can pass a hx_post_route argument to the
21 form to make the form set the `hx-post` attribute to the
22 given value.
23 We also pass a `reverse` boolean, wich gets passed on
24 to the htmx POST endpoint using url parameters (the endpoint
25 can then select the success_url based on the `reverse` state).
26 """
28 class Media:
29 js = ["js/relation_dialog.js"]
31 class Meta:
32 fields = "__all__"
34 def __init__(self, *args, **kwargs):
35 """
36 Initialize the form and replace `subj_object_id` and
37 `obj_object_id` fields with a autocomplete widget.
38 """
40 self.params = kwargs.pop("params", {})
41 subj_model = getattr(self.Meta.model, "subj_model", None)
42 obj_model = getattr(self.Meta.model, "obj_model", None)
44 if subj_model:
45 subj_ct = ContentType.objects.get_for_model(subj_model)
46 kwargs["initial"]["subj_content_type"] = subj_ct
47 if obj_model:
48 obj_ct = ContentType.objects.get_for_model(obj_model)
49 kwargs["initial"]["obj_content_type"] = obj_ct
51 super().__init__(*args, **kwargs)
53 if subj_model:
54 self.fields["subj_content_type"].required = False
55 self.fields["subj_content_type"].widget = forms.HiddenInput()
56 # if we already now the id of the subject, hide the subject input
57 if kwargs["initial"].get("subj_object_id"):
58 self.fields["subj_object_id"].widget = forms.HiddenInput()
59 # otherwise create an autocomplete to select the subject
60 else:
61 self.fields["subj_object_id"] = ModelImportChoiceField(
62 queryset=subj_model.objects.all(), label=_("Subject")
63 )
64 self.fields["subj_object_id"].widget = ApisListSelect2(
65 attrs={"data-html": True},
66 url=reverse("apis_core:generic:autocomplete", args=[subj_ct])
67 + "?create=True",
68 )
69 self.fields["subj_object_id"].widget.choices = self.fields[
70 "subj_object_id"
71 ].choices
73 if obj_model:
74 self.fields["obj_content_type"].required = False
75 self.fields["obj_content_type"].widget = forms.HiddenInput()
76 # if we already now the id of the object, hide the object input
77 if kwargs["initial"].get("obj_object_id"):
78 self.fields["obj_object_id"].widget = forms.HiddenInput()
79 # otherwise create an autocomplete to select the object
80 else:
81 self.fields["obj_object_id"] = ModelImportChoiceField(
82 queryset=obj_model.objects.all(), label=_("Object")
83 )
84 self.fields["obj_object_id"].widget = ApisListSelect2(
85 attrs={"data-html": True},
86 url=reverse("apis_core:generic:autocomplete", args=[obj_ct])
87 + "?create=True",
88 )
89 self.fields["obj_object_id"].widget.choices = self.fields[
90 "obj_object_id"
91 ].choices
93 self.order_fields(self.field_order)
94 self.helper = FormHelper(self)
95 model_ct = ContentType.objects.get_for_model(self.Meta.model)
96 self.helper.form_id = f"relation_{model_ct.model}_form"
97 self.helper.add_input(Submit("submit", "Submit"))
99 if hx_post_route := self.params.get("hx_post_route", False):
100 self.helper.attrs = {
101 "hx-post": hx_post_route + "?" + urlencode(self.params, doseq=True),
102 "hx-swap": "outerHTML",
103 "hx-target": f"#{self.helper.form_id}",
104 }
106 def clean(self) -> dict:
107 """
108 subj_object_id and obj_object_id can either come
109 from initial values or from the autocomplete.
110 In the former case, the values are simply the primary keys,
111 which is what we want.
112 In the latter case, the values are object instances and we
113 want to set them to the primary keys instead.
114 """
115 cleaned_data = super().clean()
116 subj_object_id = cleaned_data.get("subj_object_id")
117 if subj_object_id and not isinstance(subj_object_id, int):
118 cleaned_data["subj_object_id"] = subj_object_id.id
119 obj_object_id = cleaned_data.get("obj_object_id")
120 if obj_object_id and not isinstance(obj_object_id, int):
121 cleaned_data["obj_object_id"] = obj_object_id.id
122 return cleaned_data
124 @property
125 def relation_name(self) -> str:
126 """A helper method to access the correct name of the relation"""
127 if self.params["reverse"]:
128 return self._meta.model.reverse_name
129 return self._meta.model.name
132class RelationFilterSetForm(GenericFilterSetForm):
133 """
134 FilterSet form for relations based on GenericFilterSetForm.
135 Excludes `relation_ptr` from the columns selector
136 """
138 columns_exclude = ["relation_ptr"]