Source code for apis_core.relations.forms
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
from dal import autocomplete
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.http import urlencode
from apis_core.generic.forms import GenericModelForm
[docs]
class CustomSelect2ListChoiceField(autocomplete.Select2ListChoiceField):
"""
We use a custom Select2ListChoiceField in our Relation form,
because we don't want the form value to be validated. The field
uses the `choices` setting as a basis for validating, but our
choices span over multiple querysets, so its easier to simply not
validate (some validation happens in the `clean` method anyway).
"""
[docs]
def validate(self, value):
if "_" not in value:
raise ValidationError("please choose a correct value")
[docs]
class RelationForm(GenericModelForm):
"""
This form overrides generic form for relation editing.
Relations have generic relations to subj and obj, but we
hide those ForeignKey form fields and instead show
autocomplete choices fields.
"""
[docs]
class Meta:
fields = "__all__"
widgets = {
"subj_content_type": forms.HiddenInput(),
"subj_object_id": forms.HiddenInput(),
"obj_content_type": forms.HiddenInput(),
"obj_object_id": forms.HiddenInput(),
}
def __entities_autocomplete_with_params(self, content_types=[ContentType]) -> str:
"""helper method to generate the entities autocomplete url with contentype parameters"""
url = reverse("apis_core:apis_entities:autocomplete")
params = [f"entities={ct.app_label}.{ct.model}" for ct in content_types]
return url + "?" + "&".join(params)
def __subj_autocomplete_url(self, subj_content_type=None) -> str:
"""generate the autocomplete url for the subj field. by default use the
subject types configured in the relation. If there is a subj_content_type
passed in the `initial` dict of the form, use that contenttype instead of
the configured ones"""
subj_content_types = [
ContentType.objects.get_for_model(model)
for model in self.Meta.model.subj_list()
]
if subj_content_type:
subj_content_types = [ContentType.objects.get(pk=subj_content_type)]
return self.__entities_autocomplete_with_params(subj_content_types)
def __obj_autocomplete_url(self, obj_content_type=None) -> str:
"""generate the autocomplete url for the obj field. by default use the
object types configured in the relation. If there is a obj_content_type
passed in the `initial` dict of the form, use that contenttype instead of
the configured ones"""
obj_content_types = [
ContentType.objects.get_for_model(model)
for model in self.Meta.model.obj_list()
]
if obj_content_type:
obj_content_types = [ContentType.objects.get(pk=obj_content_type)]
return self.__entities_autocomplete_with_params(obj_content_types)
def __init__(self, *args, **kwargs):
"""
Initialize the form and add the `subj` and `obj` fields using the
generic apis_entities autocomplete with the correct parameters.
"""
super().__init__(*args, **kwargs)
subj_content_type = kwargs["initial"].get("subj_content_type", None)
subj_object_id = kwargs["initial"].get("subj_object_id", None)
obj_content_type = kwargs["initial"].get("obj_content_type", None)
obj_object_id = kwargs["initial"].get("obj_object_id", None)
self.subj_instance, self.obj_instance = None, None
if instance := kwargs.get("instance"):
self.subj_instance = instance.subj
self.obj_instance = instance.obj
else:
if subj_content_type and subj_object_id:
model = get_object_or_404(ContentType, pk=subj_content_type)
self.subj_instance = get_object_or_404(
model.model_class(), pk=subj_object_id
)
if obj_content_type and obj_object_id:
model = get_object_or_404(ContentType, pk=obj_content_type)
self.obj_instance = get_object_or_404(
model.model_class(), pk=obj_object_id
)
self.fields["subj_object_id"].required = False
self.fields["subj_content_type"].required = False
if not subj_object_id:
self.fields["subj"] = CustomSelect2ListChoiceField()
self.fields["subj"].widget = autocomplete.ListSelect2(
url=self.__subj_autocomplete_url(subj_content_type)
)
if self.subj_instance:
content_type = ContentType.objects.get_for_model(self.subj_instance)
select_identifier = f"{content_type.id}_{self.subj_instance.id}"
self.fields["subj"].initial = select_identifier
self.fields["subj"].choices = [(select_identifier, self.subj_instance)]
self.fields["obj_object_id"].required = False
self.fields["obj_content_type"].required = False
if not obj_object_id:
self.fields["obj"] = CustomSelect2ListChoiceField()
self.fields["obj"].widget = autocomplete.ListSelect2(
url=self.__obj_autocomplete_url(obj_content_type)
)
if self.obj_instance:
content_type = ContentType.objects.get_for_model(self.obj_instance)
select_identifier = f"{content_type.id}_{self.obj_instance.id}"
self.fields["obj"].initial = select_identifier
self.fields["obj"].choices = [(select_identifier, self.obj_instance)]
self.helper = FormHelper(self)
model_ct = ContentType.objects.get_for_model(self.Meta.model)
self.helper.form_id = f"relation_{model_ct.model}_form"
self.helper.add_input(Submit("submit", "Submit"))
[docs]
def clean(self) -> dict:
"""
We check if there are `subj` or `obj` fields in the form data
and if so, we use the data to create objects for the real fields of
the Relation
"""
cleaned_data = super().clean()
if "subj" in cleaned_data:
subj_content_type, subj_object_id = cleaned_data["subj"].split("_")
cleaned_data["subj_content_type"] = ContentType.objects.get(
pk=subj_content_type
)
cleaned_data["subj_object_id"] = subj_object_id
del cleaned_data["subj"]
if "obj" in cleaned_data:
obj_content_type, obj_object_id = cleaned_data["obj"].split("_")
cleaned_data["obj_content_type"] = ContentType.objects.get(
pk=obj_content_type
)
cleaned_data["obj_object_id"] = obj_object_id
del cleaned_data["obj"]
return cleaned_data
[docs]
class RelationFormHX(RelationForm):
"""
A Relation form that sets a hx-post attribute to the
form to make the htmx POST request use another route.
We also pass a `reverse` boolean, wich gets passed on
to the POST endpoint using url parameters. The POST
endpoint then calculates the success_url based on the
`reverse` state.
"""
def __init__(self, *args, **kwargs):
self.is_reverse = kwargs.pop("reverse", False)
super().__init__(*args, **kwargs)
urlparams = kwargs["initial"]
urlparams["reverse"] = self.is_reverse
urlparams = {k: v for k, v in urlparams.items() if v}
relation_content_type = ContentType.objects.get_for_model(self.Meta.model)
hx_post = (
reverse(
"apis_core:relations:create_relation_form", args=[relation_content_type]
)
+ "?"
+ urlencode(urlparams)
)
self.helper.attrs = {
"hx-post": hx_post,
"hx-swap": "outerHTML",
"hx-target": f"#{self.helper.form_id}",
}
@property
def relation_name(self) -> str:
if self.is_reverse:
return self._meta.model.reverse_name
return self._meta.model.name