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

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 _ 

8 

9from apis_core.core.fields import ApisListSelect2 

10from apis_core.generic.forms import GenericFilterSetForm, GenericModelForm 

11from apis_core.generic.forms.fields import ModelImportChoiceField 

12 

13 

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 """ 

27 

28 class Media: 

29 js = ["js/relation_dialog.js"] 

30 

31 class Meta: 

32 fields = "__all__" 

33 

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 """ 

39 

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) 

43 

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 

50 

51 super().__init__(*args, **kwargs) 

52 

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 

72 

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 

92 

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")) 

98 

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 } 

105 

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 

123 

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 

130 

131 

132class RelationFilterSetForm(GenericFilterSetForm): 

133 """ 

134 FilterSet form for relations based on GenericFilterSetForm. 

135 Excludes `relation_ptr` from the columns selector 

136 """ 

137 

138 columns_exclude = ["relation_ptr"]