Coverage for sm/forms.py: 98%

49 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-24 19:06 +0000

1from django import forms 

2 

3# from django.forms import TextInput # If we want/need to override 

4from django.forms.models import ModelChoiceField, ModelMultipleChoiceField 

5from taggit.forms import TagWidget 

6 

7from .mixins import filter_queryset_for_user 

8from django.utils.translation import gettext as _ 

9 

10 

11class SMForm(forms.ModelForm): 

12 """ 

13 Base model form. Accepts a ``user`` kwarg so FK choice fields are scoped to 

14 the user's accessible groups (prevents a user from referencing another 

15 tenant's related objects through the web UI). 

16 """ 

17 

18 class Meta: 

19 fields = "__all__" 

20 

21 def __init__(self, *args, user=None, **kwargs): 

22 self.user = user 

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

24 self.scope_querysets() 

25 

26 def scope_querysets(self): 

27 for field in self.fields.values(): 

28 if isinstance(field, (ModelChoiceField, ModelMultipleChoiceField)): 

29 model = getattr(field.queryset, "model", None) 

30 if model is not None and hasattr(model, "group"): 

31 field.queryset = filter_queryset_for_user(field.queryset, self.user) 

32 

33 

34class UniquePerGroupMixin: 

35 """ 

36 Enforces name uniqueness per scope at the form level. The scope is the 

37 instance's group (pre-assigned by ``MultiTenantMixin.get_form``) or the 

38 single global scope when the group is null. The DB UniqueConstraint 

39 ``(name, group)`` treats NULL rows as always distinct, so this check is 

40 needed to prevent duplicate *global* items. 

41 """ 

42 

43 #: Model fields that must be unique together within a scope. 

44 unique_fields: tuple = ("name",) 

45 

46 def clean(self): 

47 cleaned_data = super().clean() 

48 values = [cleaned_data.get(f) for f in self.unique_fields] 

49 if any(v is None or v == "" for v in values): 

50 return cleaned_data 

51 model = self.Meta.model 

52 lookup = {f: cleaned_data.get(f) for f in self.unique_fields} 

53 group = getattr(self.instance, "group_id", None) 

54 if group: 

55 lookup["group"] = group 

56 qs = model.objects.filter(**lookup) 

57 if self.instance and self.instance.pk: 

58 qs = qs.exclude(pk=self.instance.pk) 

59 if qs.exists(): 

60 self.add_error( 

61 "name", 

62 _("An item with this name already exists in this scope."), 

63 ) 

64 return cleaned_data 

65 

66 

67class BulkActionForm(forms.Form): 

68 """ 

69 Generic bulk-action form used by the shared list templates. Subclasses can 

70 add extra fields (e.g. a status dropdown for models with a status FK). 

71 """ 

72 

73 delete = forms.BooleanField(required=False, label=_("Delete selected items")) 

74 

75 

76class SMFormDisabled(SMForm): 

77 """ 

78 Form for the detail view, disables all user input 

79 """ 

80 

81 def __init__(self, *args, **kwargs): 

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

83 instance = getattr(self, "instance", None) 

84 if instance and instance.pk: 

85 for field in self.fields: 

86 if self.fields[field].widget.__class__ is not TagWidget: 

87 # self.fields[field].widget = TextInput() 

88 pass # No logic for this atm 

89 self.fields[field].widget.attrs["disabled"] = True 

90 

91 class Meta(SMForm.Meta): 

92 pass