FossilRepo

fossilrepo / organization / forms.py
Source Blame History 230 lines
c588255… ragelink 1 import contextlib
c588255… ragelink 2
4ce269c… ragelink 3 from django import forms
c588255… ragelink 4 from django.contrib.auth.models import Permission, User
c588255… ragelink 5 from django.contrib.auth.password_validation import validate_password
c588255… ragelink 6 from django.core.exceptions import ValidationError
4ce269c… ragelink 7
c588255… ragelink 8 from .models import Organization, OrgRole, Team
4ce269c… ragelink 9
4ce269c… ragelink 10 tw = "w-full rounded-md border-gray-300 shadow-sm focus:border-brand focus:ring-brand sm:text-sm"
4ce269c… ragelink 11
4ce269c… ragelink 12
4ce269c… ragelink 13 class OrganizationSettingsForm(forms.ModelForm):
4ce269c… ragelink 14 class Meta:
4ce269c… ragelink 15 model = Organization
4ce269c… ragelink 16 fields = ["name", "description", "website"]
4ce269c… ragelink 17 widgets = {
4ce269c… ragelink 18 "name": forms.TextInput(attrs={"class": tw, "placeholder": "Organization name"}),
4ce269c… ragelink 19 "description": forms.Textarea(attrs={"class": tw, "rows": 3, "placeholder": "Description"}),
4ce269c… ragelink 20 "website": forms.URLInput(attrs={"class": tw, "placeholder": "https://example.com"}),
4ce269c… ragelink 21 }
4ce269c… ragelink 22
4ce269c… ragelink 23
4ce269c… ragelink 24 class MemberAddForm(forms.Form):
4ce269c… ragelink 25 user = forms.ModelChoiceField(
4ce269c… ragelink 26 queryset=User.objects.none(),
4ce269c… ragelink 27 widget=forms.Select(attrs={"class": tw}),
4ce269c… ragelink 28 label="User",
4ce269c… ragelink 29 )
4ce269c… ragelink 30
4ce269c… ragelink 31 def __init__(self, *args, org=None, **kwargs):
4ce269c… ragelink 32 super().__init__(*args, **kwargs)
4ce269c… ragelink 33 if org:
4ce269c… ragelink 34 existing_member_ids = org.members.filter(deleted_at__isnull=True).values_list("member_id", flat=True)
4ce269c… ragelink 35 self.fields["user"].queryset = User.objects.filter(is_active=True).exclude(id__in=existing_member_ids)
4ce269c… ragelink 36
4ce269c… ragelink 37
4ce269c… ragelink 38 class TeamForm(forms.ModelForm):
4ce269c… ragelink 39 class Meta:
4ce269c… ragelink 40 model = Team
4ce269c… ragelink 41 fields = ["name", "description"]
4ce269c… ragelink 42 widgets = {
4ce269c… ragelink 43 "name": forms.TextInput(attrs={"class": tw, "placeholder": "Team name"}),
4ce269c… ragelink 44 "description": forms.Textarea(attrs={"class": tw, "rows": 3, "placeholder": "Description"}),
4ce269c… ragelink 45 }
4ce269c… ragelink 46
4ce269c… ragelink 47
4ce269c… ragelink 48 class TeamMemberAddForm(forms.Form):
4ce269c… ragelink 49 user = forms.ModelChoiceField(
4ce269c… ragelink 50 queryset=User.objects.none(),
4ce269c… ragelink 51 widget=forms.Select(attrs={"class": tw}),
4ce269c… ragelink 52 label="User",
4ce269c… ragelink 53 )
4ce269c… ragelink 54
4ce269c… ragelink 55 def __init__(self, *args, team=None, **kwargs):
4ce269c… ragelink 56 super().__init__(*args, **kwargs)
4ce269c… ragelink 57 if team:
4ce269c… ragelink 58 existing_member_ids = team.members.values_list("id", flat=True)
4ce269c… ragelink 59 self.fields["user"].queryset = User.objects.filter(is_active=True).exclude(id__in=existing_member_ids)
c588255… ragelink 60
c588255… ragelink 61
c588255… ragelink 62 class UserCreateForm(forms.ModelForm):
c588255… ragelink 63 password1 = forms.CharField(
c588255… ragelink 64 label="Password",
c588255… ragelink 65 widget=forms.PasswordInput(attrs={"class": tw, "placeholder": "Password"}),
c588255… ragelink 66 strip=False,
c588255… ragelink 67 )
c588255… ragelink 68 password2 = forms.CharField(
c588255… ragelink 69 label="Confirm Password",
c588255… ragelink 70 widget=forms.PasswordInput(attrs={"class": tw, "placeholder": "Confirm password"}),
c588255… ragelink 71 strip=False,
c588255… ragelink 72 )
c588255… ragelink 73 role = forms.ModelChoiceField(
c588255… ragelink 74 queryset=OrgRole.objects.filter(deleted_at__isnull=True),
c588255… ragelink 75 required=False,
c588255… ragelink 76 empty_label="No role",
c588255… ragelink 77 widget=forms.Select(attrs={"class": tw}),
c588255… ragelink 78 )
c588255… ragelink 79
c588255… ragelink 80 class Meta:
c588255… ragelink 81 model = User
c588255… ragelink 82 fields = ["username", "email", "first_name", "last_name"]
c588255… ragelink 83 widgets = {
c588255… ragelink 84 "username": forms.TextInput(attrs={"class": tw, "placeholder": "Username"}),
c588255… ragelink 85 "email": forms.EmailInput(attrs={"class": tw, "placeholder": "[email protected]"}),
c588255… ragelink 86 "first_name": forms.TextInput(attrs={"class": tw, "placeholder": "First name"}),
c588255… ragelink 87 "last_name": forms.TextInput(attrs={"class": tw, "placeholder": "Last name"}),
c588255… ragelink 88 }
c588255… ragelink 89
c588255… ragelink 90 def clean_password1(self):
c588255… ragelink 91 password = self.cleaned_data.get("password1")
c588255… ragelink 92 try:
c588255… ragelink 93 validate_password(password)
c588255… ragelink 94 except ValidationError as e:
c588255… ragelink 95 raise ValidationError(e.messages) from None
c588255… ragelink 96 return password
c588255… ragelink 97
c588255… ragelink 98 def clean(self):
c588255… ragelink 99 cleaned_data = super().clean()
c588255… ragelink 100 p1 = cleaned_data.get("password1")
c588255… ragelink 101 p2 = cleaned_data.get("password2")
c588255… ragelink 102 if p1 and p2 and p1 != p2:
c588255… ragelink 103 self.add_error("password2", "Passwords do not match.")
c588255… ragelink 104 return cleaned_data
c588255… ragelink 105
c588255… ragelink 106 def save(self, commit=True):
c588255… ragelink 107 user = super().save(commit=False)
c588255… ragelink 108 user.set_password(self.cleaned_data["password1"])
c588255… ragelink 109 if commit:
c588255… ragelink 110 user.save()
c588255… ragelink 111 return user
c588255… ragelink 112
c588255… ragelink 113
c588255… ragelink 114 class UserEditForm(forms.ModelForm):
c588255… ragelink 115 role = forms.ModelChoiceField(
c588255… ragelink 116 queryset=OrgRole.objects.filter(deleted_at__isnull=True),
c588255… ragelink 117 required=False,
c588255… ragelink 118 empty_label="No role",
c588255… ragelink 119 widget=forms.Select(attrs={"class": tw}),
c588255… ragelink 120 )
c588255… ragelink 121
c588255… ragelink 122 class Meta:
c588255… ragelink 123 model = User
c588255… ragelink 124 fields = ["email", "first_name", "last_name", "is_active", "is_staff"]
c588255… ragelink 125 widgets = {
c588255… ragelink 126 "email": forms.EmailInput(attrs={"class": tw, "placeholder": "[email protected]"}),
c588255… ragelink 127 "first_name": forms.TextInput(attrs={"class": tw, "placeholder": "First name"}),
c588255… ragelink 128 "last_name": forms.TextInput(attrs={"class": tw, "placeholder": "Last name"}),
c588255… ragelink 129 "is_active": forms.CheckboxInput(attrs={"class": "rounded border-gray-300 text-brand focus:ring-brand"}),
c588255… ragelink 130 "is_staff": forms.CheckboxInput(attrs={"class": "rounded border-gray-300 text-brand focus:ring-brand"}),
c588255… ragelink 131 }
c588255… ragelink 132
c588255… ragelink 133 def __init__(self, *args, editing_self=False, **kwargs):
c588255… ragelink 134 super().__init__(*args, **kwargs)
c588255… ragelink 135 if editing_self:
c588255… ragelink 136 # Prevent self-lockout: cannot toggle own is_active
c588255… ragelink 137 self.fields["is_active"].disabled = True
c588255… ragelink 138 self.fields["is_active"].help_text = "You cannot deactivate your own account."
c588255… ragelink 139
c588255… ragelink 140
c588255… ragelink 141 class UserPasswordForm(forms.Form):
c588255… ragelink 142 new_password1 = forms.CharField(
c588255… ragelink 143 label="New Password",
c588255… ragelink 144 widget=forms.PasswordInput(attrs={"class": tw, "placeholder": "New password"}),
c588255… ragelink 145 strip=False,
c588255… ragelink 146 )
c588255… ragelink 147 new_password2 = forms.CharField(
c588255… ragelink 148 label="Confirm New Password",
c588255… ragelink 149 widget=forms.PasswordInput(attrs={"class": tw, "placeholder": "Confirm new password"}),
c588255… ragelink 150 strip=False,
c588255… ragelink 151 )
c588255… ragelink 152
c588255… ragelink 153 def clean_new_password1(self):
c588255… ragelink 154 password = self.cleaned_data.get("new_password1")
c588255… ragelink 155 try:
c588255… ragelink 156 validate_password(password)
c588255… ragelink 157 except ValidationError as e:
c588255… ragelink 158 raise ValidationError(e.messages) from None
c588255… ragelink 159 return password
c588255… ragelink 160
c588255… ragelink 161 def clean(self):
c588255… ragelink 162 cleaned_data = super().clean()
c588255… ragelink 163 p1 = cleaned_data.get("new_password1")
c588255… ragelink 164 p2 = cleaned_data.get("new_password2")
c588255… ragelink 165 if p1 and p2 and p1 != p2:
c588255… ragelink 166 self.add_error("new_password2", "Passwords do not match.")
c588255… ragelink 167 return cleaned_data
c588255… ragelink 168
c588255… ragelink 169
c588255… ragelink 170 # App labels whose permissions appear in the role permission picker
c588255… ragelink 171 ROLE_APP_LABELS = ["organization", "projects", "pages", "fossil"]
c588255… ragelink 172
c588255… ragelink 173 ROLE_APP_DISPLAY = {
c588255… ragelink 174 "organization": "Organization",
c588255… ragelink 175 "projects": "Projects",
c588255… ragelink 176 "pages": "Pages",
c588255… ragelink 177 "fossil": "Fossil",
c588255… ragelink 178 }
c588255… ragelink 179
c588255… ragelink 180
c588255… ragelink 181 class OrgRoleForm(forms.ModelForm):
c588255… ragelink 182 permissions = forms.ModelMultipleChoiceField(
c588255… ragelink 183 queryset=Permission.objects.none(),
c588255… ragelink 184 required=False,
c588255… ragelink 185 widget=forms.CheckboxSelectMultiple,
c588255… ragelink 186 )
c588255… ragelink 187
c588255… ragelink 188 class Meta:
c588255… ragelink 189 model = OrgRole
c588255… ragelink 190 fields = ["name", "description", "is_default"]
c588255… ragelink 191 widgets = {
c588255… ragelink 192 "name": forms.TextInput(attrs={"class": tw, "placeholder": "Role name"}),
c588255… ragelink 193 "description": forms.Textarea(attrs={"class": tw, "rows": 3, "placeholder": "Description"}),
c588255… ragelink 194 "is_default": forms.CheckboxInput(attrs={"class": "rounded border-gray-300 text-brand focus:ring-brand"}),
c588255… ragelink 195 }
c588255… ragelink 196
c588255… ragelink 197 def __init__(self, *args, **kwargs):
c588255… ragelink 198 super().__init__(*args, **kwargs)
c588255… ragelink 199 self.fields["permissions"].queryset = (
c588255… ragelink 200 Permission.objects.filter(content_type__app_label__in=ROLE_APP_LABELS)
c588255… ragelink 201 .select_related("content_type")
c588255… ragelink 202 .order_by("content_type__app_label", "codename")
c588255… ragelink 203 )
c588255… ragelink 204
c588255… ragelink 205 if self.instance.pk:
c588255… ragelink 206 self.fields["permissions"].initial = self.instance.permissions.values_list("pk", flat=True)
c588255… ragelink 207
c588255… ragelink 208 def grouped_permissions(self):
c588255… ragelink 209 """Return permissions grouped by app label for the template."""
c588255… ragelink 210 grouped = {}
c588255… ragelink 211 selected_ids = set()
c588255… ragelink 212 if self.instance.pk:
c588255… ragelink 213 selected_ids = set(self.instance.permissions.values_list("pk", flat=True))
c588255… ragelink 214 elif self.data:
c588255… ragelink 215 # Handle POST data (validation errors, re-render)
c588255… ragelink 216 with contextlib.suppress(ValueError, TypeError):
c588255… ragelink 217 selected_ids = set(int(v) for v in self.data.getlist("permissions"))
c588255… ragelink 218
c588255… ragelink 219 for perm in self.fields["permissions"].queryset:
c588255… ragelink 220 app = perm.content_type.app_label
c588255… ragelink 221 label = ROLE_APP_DISPLAY.get(app, app.title())
c588255… ragelink 222 grouped.setdefault(label, [])
c588255… ragelink 223 grouped[label].append({"perm": perm, "checked": perm.pk in selected_ids})
c588255… ragelink 224 return grouped
c588255… ragelink 225
c588255… ragelink 226 def save(self, commit=True):
c588255… ragelink 227 role = super().save(commit=commit)
c588255… ragelink 228 if commit:
c588255… ragelink 229 role.permissions.set(self.cleaned_data["permissions"])
c588255… ragelink 230 return role

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button