FossilRepo

fossilrepo / organization / views.py
Source Blame History 550 lines
4ce269c… ragelink 1 from django.contrib import messages
4ce269c… ragelink 2 from django.contrib.auth.decorators import login_required
4ce269c… ragelink 3 from django.contrib.auth.models import User
c588255… ragelink 4 from django.core.paginator import Paginator
c588255… ragelink 5 from django.db import models
4ce269c… ragelink 6 from django.http import HttpResponse
4ce269c… ragelink 7 from django.shortcuts import get_object_or_404, redirect, render
4ce269c… ragelink 8
c588255… ragelink 9 from core.pagination import PER_PAGE_OPTIONS, get_per_page
4ce269c… ragelink 10 from core.permissions import P
4ce269c… ragelink 11
c588255… ragelink 12 from .forms import (
c588255… ragelink 13 MemberAddForm,
c588255… ragelink 14 OrganizationSettingsForm,
c588255… ragelink 15 OrgRoleForm,
c588255… ragelink 16 TeamForm,
c588255… ragelink 17 TeamMemberAddForm,
c588255… ragelink 18 UserCreateForm,
c588255… ragelink 19 UserEditForm,
c588255… ragelink 20 UserPasswordForm,
c588255… ragelink 21 )
c588255… ragelink 22 from .models import Organization, OrganizationMember, OrgRole, Team
4ce269c… ragelink 23
4ce269c… ragelink 24
4ce269c… ragelink 25 def get_org():
4ce269c… ragelink 26 return Organization.objects.first()
4ce269c… ragelink 27
4ce269c… ragelink 28
4ce269c… ragelink 29 # --- Organization Settings ---
4ce269c… ragelink 30
4ce269c… ragelink 31
4ce269c… ragelink 32 @login_required
4ce269c… ragelink 33 def org_settings(request):
4ce269c… ragelink 34 P.ORGANIZATION_VIEW.check(request.user)
4ce269c… ragelink 35 org = get_org()
4ce269c… ragelink 36 return render(request, "organization/settings.html", {"org": org})
4ce269c… ragelink 37
4ce269c… ragelink 38
4ce269c… ragelink 39 @login_required
4ce269c… ragelink 40 def org_settings_edit(request):
4ce269c… ragelink 41 P.ORGANIZATION_CHANGE.check(request.user)
4ce269c… ragelink 42 org = get_org()
4ce269c… ragelink 43
4ce269c… ragelink 44 if request.method == "POST":
4ce269c… ragelink 45 form = OrganizationSettingsForm(request.POST, instance=org)
4ce269c… ragelink 46 if form.is_valid():
4ce269c… ragelink 47 org = form.save(commit=False)
4ce269c… ragelink 48 org.updated_by = request.user
4ce269c… ragelink 49 org.save()
4ce269c… ragelink 50 messages.success(request, "Organization settings updated.")
4ce269c… ragelink 51 return redirect("organization:settings")
4ce269c… ragelink 52 else:
4ce269c… ragelink 53 form = OrganizationSettingsForm(instance=org)
4ce269c… ragelink 54
4ce269c… ragelink 55 return render(request, "organization/settings_form.html", {"form": form, "org": org})
4ce269c… ragelink 56
4ce269c… ragelink 57
4ce269c… ragelink 58 # --- Members ---
4ce269c… ragelink 59
4ce269c… ragelink 60
4ce269c… ragelink 61 @login_required
4ce269c… ragelink 62 def member_list(request):
4ce269c… ragelink 63 P.ORGANIZATION_MEMBER_VIEW.check(request.user)
4ce269c… ragelink 64 org = get_org()
c588255… ragelink 65 members = OrganizationMember.objects.filter(organization=org).select_related("member", "role")
4ce269c… ragelink 66
4ce269c… ragelink 67 search = request.GET.get("search", "").strip()
4ce269c… ragelink 68 if search:
4ce269c… ragelink 69 members = members.filter(member__username__icontains=search)
4ce269c… ragelink 70
c588255… ragelink 71 per_page = get_per_page(request)
c588255… ragelink 72 paginator = Paginator(members, per_page)
c588255… ragelink 73 page_obj = paginator.get_page(request.GET.get("page", 1))
c588255… ragelink 74
c588255… ragelink 75 ctx = {
c588255… ragelink 76 "members": page_obj,
c588255… ragelink 77 "page_obj": page_obj,
c588255… ragelink 78 "org": org,
c588255… ragelink 79 "search": search,
c588255… ragelink 80 "per_page": per_page,
c588255… ragelink 81 "per_page_options": PER_PAGE_OPTIONS,
c588255… ragelink 82 }
c588255… ragelink 83
4ce269c… ragelink 84 if request.headers.get("HX-Request"):
c588255… ragelink 85 return render(request, "organization/partials/member_table.html", ctx)
4ce269c… ragelink 86
c588255… ragelink 87 return render(request, "organization/member_list.html", ctx)
4ce269c… ragelink 88
4ce269c… ragelink 89
4ce269c… ragelink 90 @login_required
4ce269c… ragelink 91 def member_add(request):
4ce269c… ragelink 92 P.ORGANIZATION_MEMBER_ADD.check(request.user)
4ce269c… ragelink 93 org = get_org()
4ce269c… ragelink 94
4ce269c… ragelink 95 if request.method == "POST":
4ce269c… ragelink 96 form = MemberAddForm(request.POST, org=org)
4ce269c… ragelink 97 if form.is_valid():
4ce269c… ragelink 98 user = form.cleaned_data["user"]
4ce269c… ragelink 99 OrganizationMember.objects.create(member=user, organization=org, created_by=request.user)
4ce269c… ragelink 100 messages.success(request, f'Member "{user.username}" added.')
4ce269c… ragelink 101 return redirect("organization:members")
4ce269c… ragelink 102 else:
4ce269c… ragelink 103 form = MemberAddForm(org=org)
4ce269c… ragelink 104
4ce269c… ragelink 105 return render(request, "organization/member_add.html", {"form": form, "org": org})
4ce269c… ragelink 106
4ce269c… ragelink 107
4ce269c… ragelink 108 @login_required
4ce269c… ragelink 109 def member_remove(request, username):
4ce269c… ragelink 110 P.ORGANIZATION_MEMBER_DELETE.check(request.user)
4ce269c… ragelink 111 org = get_org()
4ce269c… ragelink 112 membership = get_object_or_404(OrganizationMember, member__username=username, organization=org, deleted_at__isnull=True)
4ce269c… ragelink 113
4ce269c… ragelink 114 if request.method == "POST":
4ce269c… ragelink 115 membership.soft_delete(user=request.user)
4ce269c… ragelink 116 messages.success(request, f'Member "{username}" removed.')
4ce269c… ragelink 117
4ce269c… ragelink 118 if request.headers.get("HX-Request"):
4ce269c… ragelink 119 return HttpResponse(status=200, headers={"HX-Redirect": "/settings/members/"})
4ce269c… ragelink 120
4ce269c… ragelink 121 return redirect("organization:members")
4ce269c… ragelink 122
4ce269c… ragelink 123 return render(request, "organization/member_confirm_remove.html", {"membership": membership, "org": org})
4ce269c… ragelink 124
4ce269c… ragelink 125
4ce269c… ragelink 126 # --- Teams ---
4ce269c… ragelink 127
4ce269c… ragelink 128
4ce269c… ragelink 129 @login_required
4ce269c… ragelink 130 def team_list(request):
4ce269c… ragelink 131 P.TEAM_VIEW.check(request.user)
4ce269c… ragelink 132 org = get_org()
4ce269c… ragelink 133 teams = Team.objects.filter(organization=org)
4ce269c… ragelink 134
4ce269c… ragelink 135 search = request.GET.get("search", "").strip()
4ce269c… ragelink 136 if search:
4ce269c… ragelink 137 teams = teams.filter(name__icontains=search)
4ce269c… ragelink 138
c588255… ragelink 139 per_page = get_per_page(request)
c588255… ragelink 140 paginator = Paginator(teams, per_page)
c588255… ragelink 141 page_obj = paginator.get_page(request.GET.get("page", 1))
c588255… ragelink 142
c588255… ragelink 143 ctx = {"teams": page_obj, "page_obj": page_obj, "search": search, "per_page": per_page, "per_page_options": PER_PAGE_OPTIONS}
c588255… ragelink 144
4ce269c… ragelink 145 if request.headers.get("HX-Request"):
c588255… ragelink 146 return render(request, "organization/partials/team_table.html", ctx)
4ce269c… ragelink 147
c588255… ragelink 148 return render(request, "organization/team_list.html", ctx)
4ce269c… ragelink 149
4ce269c… ragelink 150
4ce269c… ragelink 151 @login_required
4ce269c… ragelink 152 def team_create(request):
4ce269c… ragelink 153 P.TEAM_ADD.check(request.user)
4ce269c… ragelink 154 org = get_org()
4ce269c… ragelink 155
4ce269c… ragelink 156 if request.method == "POST":
4ce269c… ragelink 157 form = TeamForm(request.POST)
4ce269c… ragelink 158 if form.is_valid():
4ce269c… ragelink 159 team = form.save(commit=False)
4ce269c… ragelink 160 team.organization = org
4ce269c… ragelink 161 team.created_by = request.user
4ce269c… ragelink 162 team.save()
4ce269c… ragelink 163 messages.success(request, f'Team "{team.name}" created.')
4ce269c… ragelink 164 return redirect("organization:team_detail", slug=team.slug)
4ce269c… ragelink 165 else:
4ce269c… ragelink 166 form = TeamForm()
4ce269c… ragelink 167
4ce269c… ragelink 168 return render(request, "organization/team_form.html", {"form": form, "title": "New Team"})
4ce269c… ragelink 169
4ce269c… ragelink 170
4ce269c… ragelink 171 @login_required
4ce269c… ragelink 172 def team_detail(request, slug):
4ce269c… ragelink 173 P.TEAM_VIEW.check(request.user)
4ce269c… ragelink 174 team = get_object_or_404(Team, slug=slug, deleted_at__isnull=True)
4ce269c… ragelink 175 team_members = team.members.filter(is_active=True)
4ce269c… ragelink 176 return render(request, "organization/team_detail.html", {"team": team, "team_members": team_members})
4ce269c… ragelink 177
4ce269c… ragelink 178
4ce269c… ragelink 179 @login_required
4ce269c… ragelink 180 def team_update(request, slug):
4ce269c… ragelink 181 P.TEAM_CHANGE.check(request.user)
4ce269c… ragelink 182 team = get_object_or_404(Team, slug=slug, deleted_at__isnull=True)
4ce269c… ragelink 183
4ce269c… ragelink 184 if request.method == "POST":
4ce269c… ragelink 185 form = TeamForm(request.POST, instance=team)
4ce269c… ragelink 186 if form.is_valid():
4ce269c… ragelink 187 team = form.save(commit=False)
4ce269c… ragelink 188 team.updated_by = request.user
4ce269c… ragelink 189 team.save()
4ce269c… ragelink 190 messages.success(request, f'Team "{team.name}" updated.')
4ce269c… ragelink 191 return redirect("organization:team_detail", slug=team.slug)
4ce269c… ragelink 192 else:
4ce269c… ragelink 193 form = TeamForm(instance=team)
4ce269c… ragelink 194
4ce269c… ragelink 195 return render(request, "organization/team_form.html", {"form": form, "team": team, "title": "Edit Team"})
4ce269c… ragelink 196
4ce269c… ragelink 197
4ce269c… ragelink 198 @login_required
4ce269c… ragelink 199 def team_delete(request, slug):
4ce269c… ragelink 200 P.TEAM_DELETE.check(request.user)
4ce269c… ragelink 201 team = get_object_or_404(Team, slug=slug, deleted_at__isnull=True)
4ce269c… ragelink 202
4ce269c… ragelink 203 if request.method == "POST":
4ce269c… ragelink 204 team.soft_delete(user=request.user)
4ce269c… ragelink 205 messages.success(request, f'Team "{team.name}" deleted.')
4ce269c… ragelink 206
4ce269c… ragelink 207 if request.headers.get("HX-Request"):
4ce269c… ragelink 208 return HttpResponse(status=200, headers={"HX-Redirect": "/settings/teams/"})
4ce269c… ragelink 209
4ce269c… ragelink 210 return redirect("organization:team_list")
4ce269c… ragelink 211
4ce269c… ragelink 212 return render(request, "organization/team_confirm_delete.html", {"team": team})
4ce269c… ragelink 213
4ce269c… ragelink 214
4ce269c… ragelink 215 @login_required
4ce269c… ragelink 216 def team_member_add(request, slug):
4ce269c… ragelink 217 P.TEAM_CHANGE.check(request.user)
4ce269c… ragelink 218 team = get_object_or_404(Team, slug=slug, deleted_at__isnull=True)
4ce269c… ragelink 219
4ce269c… ragelink 220 if request.method == "POST":
4ce269c… ragelink 221 form = TeamMemberAddForm(request.POST, team=team)
4ce269c… ragelink 222 if form.is_valid():
4ce269c… ragelink 223 user = form.cleaned_data["user"]
4ce269c… ragelink 224 team.members.add(user)
4ce269c… ragelink 225 messages.success(request, f'"{user.username}" added to {team.name}.')
4ce269c… ragelink 226 return redirect("organization:team_detail", slug=team.slug)
4ce269c… ragelink 227 else:
4ce269c… ragelink 228 form = TeamMemberAddForm(team=team)
4ce269c… ragelink 229
4ce269c… ragelink 230 return render(request, "organization/team_member_add.html", {"form": form, "team": team})
4ce269c… ragelink 231
4ce269c… ragelink 232
4ce269c… ragelink 233 @login_required
4ce269c… ragelink 234 def team_member_remove(request, slug, username):
4ce269c… ragelink 235 P.TEAM_CHANGE.check(request.user)
4ce269c… ragelink 236 team = get_object_or_404(Team, slug=slug, deleted_at__isnull=True)
4ce269c… ragelink 237 user = get_object_or_404(User, username=username)
4ce269c… ragelink 238
4ce269c… ragelink 239 if request.method == "POST":
4ce269c… ragelink 240 team.members.remove(user)
4ce269c… ragelink 241 messages.success(request, f'"{username}" removed from {team.name}.')
4ce269c… ragelink 242
4ce269c… ragelink 243 if request.headers.get("HX-Request"):
4ce269c… ragelink 244 return HttpResponse(status=200, headers={"HX-Redirect": f"/settings/teams/{team.slug}/"})
4ce269c… ragelink 245
4ce269c… ragelink 246 return redirect("organization:team_detail", slug=team.slug)
4ce269c… ragelink 247
4ce269c… ragelink 248 return render(request, "organization/team_member_confirm_remove.html", {"team": team, "member_user": user})
c588255… ragelink 249
c588255… ragelink 250
c588255… ragelink 251 # --- User Management ---
c588255… ragelink 252
c588255… ragelink 253
c588255… ragelink 254 def _check_user_management_permission(request):
c588255… ragelink 255 """User management requires superuser or ORGANIZATION_CHANGE permission."""
c588255… ragelink 256 if request.user.is_superuser:
c588255… ragelink 257 return True
c588255… ragelink 258 return P.ORGANIZATION_CHANGE.check(request.user)
c588255… ragelink 259
c588255… ragelink 260
c588255… ragelink 261 @login_required
c588255… ragelink 262 def user_create(request):
c588255… ragelink 263 _check_user_management_permission(request)
c588255… ragelink 264 org = get_org()
c588255… ragelink 265
c588255… ragelink 266 if request.method == "POST":
c588255… ragelink 267 form = UserCreateForm(request.POST)
c588255… ragelink 268 if form.is_valid():
c588255… ragelink 269 user = form.save()
c588255… ragelink 270 role = form.cleaned_data.get("role")
c588255… ragelink 271 OrganizationMember.objects.create(member=user, organization=org, role=role, created_by=request.user)
c588255… ragelink 272 if role:
c588255… ragelink 273 role.apply_to_user(user)
c588255… ragelink 274 messages.success(request, f'User "{user.username}" created and added as member.')
c588255… ragelink 275 return redirect("organization:members")
c588255… ragelink 276 else:
c588255… ragelink 277 form = UserCreateForm()
c588255… ragelink 278
c588255… ragelink 279 return render(request, "organization/user_form.html", {"form": form, "title": "New User"})
c588255… ragelink 280
c588255… ragelink 281
c588255… ragelink 282 @login_required
c588255… ragelink 283 def user_detail(request, username):
c588255… ragelink 284 P.ORGANIZATION_MEMBER_VIEW.check(request.user)
c588255… ragelink 285 org = get_org()
c588255… ragelink 286 target_user = get_object_or_404(User, username=username)
c588255… ragelink 287 membership = (
c588255… ragelink 288 OrganizationMember.objects.filter(member=target_user, organization=org, deleted_at__isnull=True).select_related("role").first()
c588255… ragelink 289 )
c588255… ragelink 290 user_teams = Team.objects.filter(members=target_user, organization=org, deleted_at__isnull=True)
c588255… ragelink 291
c588255… ragelink 292 from fossil.user_keys import UserSSHKey
c588255… ragelink 293
c588255… ragelink 294 ssh_keys = UserSSHKey.objects.filter(user=target_user)
c588255… ragelink 295
c588255… ragelink 296 can_manage = request.user.is_superuser or P.ORGANIZATION_CHANGE.check(request.user, raise_error=False)
c588255… ragelink 297
c588255… ragelink 298 return render(
c588255… ragelink 299 request,
c588255… ragelink 300 "organization/user_detail.html",
c588255… ragelink 301 {
c588255… ragelink 302 "target_user": target_user,
c588255… ragelink 303 "membership": membership,
c588255… ragelink 304 "user_teams": user_teams,
c588255… ragelink 305 "ssh_keys": ssh_keys,
c588255… ragelink 306 "can_manage": can_manage,
c588255… ragelink 307 "org": org,
c588255… ragelink 308 },
c588255… ragelink 309 )
c588255… ragelink 310
c588255… ragelink 311
c588255… ragelink 312 @login_required
c588255… ragelink 313 def user_edit(request, username):
c588255… ragelink 314 _check_user_management_permission(request)
c588255… ragelink 315 org = get_org()
c588255… ragelink 316 target_user = get_object_or_404(User, username=username)
c588255… ragelink 317 editing_self = request.user.pk == target_user.pk
c588255… ragelink 318 membership = (
c588255… ragelink 319 OrganizationMember.objects.filter(member=target_user, organization=org, deleted_at__isnull=True).select_related("role").first()
c588255… ragelink 320 )
c588255… ragelink 321
c588255… ragelink 322 if request.method == "POST":
c588255… ragelink 323 form = UserEditForm(request.POST, instance=target_user, editing_self=editing_self)
c588255… ragelink 324 if form.is_valid():
c588255… ragelink 325 form.save()
c588255… ragelink 326 role = form.cleaned_data.get("role")
c588255… ragelink 327 if membership:
c588255… ragelink 328 membership.role = role
c588255… ragelink 329 membership.updated_by = request.user
c588255… ragelink 330 membership.save()
c588255… ragelink 331 if role:
c588255… ragelink 332 role.apply_to_user(target_user)
c588255… ragelink 333 else:
c588255… ragelink 334 OrgRole.remove_role_groups(target_user)
c588255… ragelink 335 messages.success(request, f'User "{target_user.username}" updated.')
c588255… ragelink 336 return redirect("organization:members")
c588255… ragelink 337 else:
c588255… ragelink 338 initial = {}
c588255… ragelink 339 if membership and membership.role:
c588255… ragelink 340 initial["role"] = membership.role.pk
c588255… ragelink 341 form = UserEditForm(instance=target_user, editing_self=editing_self, initial=initial)
c588255… ragelink 342
c588255… ragelink 343 return render(
c588255… ragelink 344 request,
c588255… ragelink 345 "organization/user_form.html",
c588255… ragelink 346 {"form": form, "title": f"Edit {target_user.username}", "edit_user": target_user},
c588255… ragelink 347 )
c588255… ragelink 348
c588255… ragelink 349
c588255… ragelink 350 @login_required
c588255… ragelink 351 def user_password(request, username):
c588255… ragelink 352 target_user = get_object_or_404(User, username=username)
c588255… ragelink 353 editing_own = request.user.pk == target_user.pk
c588255… ragelink 354
c588255… ragelink 355 # Allow changing own password, or require admin/org-change for others
c588255… ragelink 356 if not editing_own:
c588255… ragelink 357 _check_user_management_permission(request)
c588255… ragelink 358
c588255… ragelink 359 if request.method == "POST":
c588255… ragelink 360 form = UserPasswordForm(request.POST)
c588255… ragelink 361 if form.is_valid():
c588255… ragelink 362 target_user.set_password(form.cleaned_data["new_password1"])
c588255… ragelink 363 target_user.save()
c588255… ragelink 364 messages.success(request, f'Password changed for "{target_user.username}".')
c588255… ragelink 365 return redirect("organization:user_detail", username=target_user.username)
c588255… ragelink 366 else:
c588255… ragelink 367 form = UserPasswordForm()
c588255… ragelink 368
c588255… ragelink 369 return render(request, "organization/user_password.html", {"form": form, "target_user": target_user})
c588255… ragelink 370
c588255… ragelink 371
c588255… ragelink 372 # --- Roles ---
c588255… ragelink 373
c588255… ragelink 374
c588255… ragelink 375 @login_required
c588255… ragelink 376 def role_list(request):
c588255… ragelink 377 P.ORGANIZATION_VIEW.check(request.user)
c588255… ragelink 378 roles = OrgRole.objects.annotate(
c588255… ragelink 379 member_count=models.Count("members", filter=models.Q(members__deleted_at__isnull=True)),
c588255… ragelink 380 permission_count=models.Count("permissions"),
c588255… ragelink 381 )
c588255… ragelink 382 return render(request, "organization/role_list.html", {"roles": roles})
c588255… ragelink 383
c588255… ragelink 384
c588255… ragelink 385 @login_required
c588255… ragelink 386 def role_detail(request, slug):
c588255… ragelink 387 P.ORGANIZATION_VIEW.check(request.user)
c588255… ragelink 388 role = get_object_or_404(OrgRole, slug=slug, deleted_at__isnull=True)
c588255… ragelink 389 role_permissions = role.permissions.select_related("content_type").order_by("content_type__app_label", "codename")
c588255… ragelink 390
c588255… ragelink 391 # Group permissions by app label
c588255… ragelink 392 grouped = {}
c588255… ragelink 393 app_labels = {
c588255… ragelink 394 "organization": "Organization",
c588255… ragelink 395 "projects": "Projects",
c588255… ragelink 396 "pages": "Pages",
c588255… ragelink 397 "fossil": "Fossil",
c588255… ragelink 398 }
c588255… ragelink 399 for perm in role_permissions:
c588255… ragelink 400 app = perm.content_type.app_label
c588255… ragelink 401 label = app_labels.get(app, app.title())
c588255… ragelink 402 grouped.setdefault(label, []).append(perm)
c588255… ragelink 403
c588255… ragelink 404 role_members = OrganizationMember.objects.filter(role=role, deleted_at__isnull=True).select_related("member")
c588255… ragelink 405
c588255… ragelink 406 return render(
c588255… ragelink 407 request,
c588255… ragelink 408 "organization/role_detail.html",
c588255… ragelink 409 {"role": role, "grouped_permissions": grouped, "role_members": role_members},
c588255… ragelink 410 )
c588255… ragelink 411
c588255… ragelink 412
c588255… ragelink 413 @login_required
c588255… ragelink 414 def role_create(request):
c588255… ragelink 415 P.ORGANIZATION_CHANGE.check(request.user)
c588255… ragelink 416
c588255… ragelink 417 if request.method == "POST":
c588255… ragelink 418 form = OrgRoleForm(request.POST)
c588255… ragelink 419 if form.is_valid():
c588255… ragelink 420 role = form.save(commit=False)
c588255… ragelink 421 role.created_by = request.user
c588255… ragelink 422 role.save()
c588255… ragelink 423 form.save_m2m()
c588255… ragelink 424 role.permissions.set(form.cleaned_data["permissions"])
c588255… ragelink 425 messages.success(request, f'Role "{role.name}" created.')
c588255… ragelink 426 return redirect("organization:role_detail", slug=role.slug)
c588255… ragelink 427 else:
c588255… ragelink 428 form = OrgRoleForm()
c588255… ragelink 429
c588255… ragelink 430 return render(request, "organization/role_form.html", {"form": form, "title": "New Role"})
c588255… ragelink 431
c588255… ragelink 432
c588255… ragelink 433 @login_required
c588255… ragelink 434 def role_edit(request, slug):
c588255… ragelink 435 P.ORGANIZATION_CHANGE.check(request.user)
c588255… ragelink 436 role = get_object_or_404(OrgRole, slug=slug, deleted_at__isnull=True)
c588255… ragelink 437
c588255… ragelink 438 if request.method == "POST":
c588255… ragelink 439 form = OrgRoleForm(request.POST, instance=role)
c588255… ragelink 440 if form.is_valid():
c588255… ragelink 441 role = form.save(commit=False)
c588255… ragelink 442 role.updated_by = request.user
c588255… ragelink 443 role.save()
c588255… ragelink 444 role.permissions.set(form.cleaned_data["permissions"])
c588255… ragelink 445 messages.success(request, f'Role "{role.name}" updated.')
c588255… ragelink 446 return redirect("organization:role_detail", slug=role.slug)
c588255… ragelink 447 else:
c588255… ragelink 448 form = OrgRoleForm(instance=role)
c588255… ragelink 449
c588255… ragelink 450 return render(request, "organization/role_form.html", {"form": form, "role": role, "title": f"Edit {role.name}"})
c588255… ragelink 451
c588255… ragelink 452
c588255… ragelink 453 @login_required
c588255… ragelink 454 def role_delete(request, slug):
c588255… ragelink 455 P.ORGANIZATION_CHANGE.check(request.user)
c588255… ragelink 456 role = get_object_or_404(OrgRole, slug=slug, deleted_at__isnull=True)
c588255… ragelink 457 active_members = OrganizationMember.objects.filter(role=role, deleted_at__isnull=True)
c588255… ragelink 458
c588255… ragelink 459 if request.method == "POST":
c588255… ragelink 460 if active_members.exists():
c588255… ragelink 461 messages.error(
c588255… ragelink 462 request, f'Cannot delete role "{role.name}" -- it has {active_members.count()} active member(s). Reassign them first.'
c588255… ragelink 463 )
c588255… ragelink 464 return redirect("organization:role_detail", slug=role.slug)
c588255… ragelink 465
c588255… ragelink 466 role.soft_delete(user=request.user)
c588255… ragelink 467 messages.success(request, f'Role "{role.name}" deleted.')
c588255… ragelink 468
c588255… ragelink 469 if request.headers.get("HX-Request"):
c588255… ragelink 470 return HttpResponse(status=200, headers={"HX-Redirect": "/settings/roles/"})
c588255… ragelink 471
c588255… ragelink 472 return redirect("organization:role_list")
c588255… ragelink 473
c588255… ragelink 474 return render(
c588255… ragelink 475 request,
c588255… ragelink 476 "organization/role_confirm_delete.html",
c588255… ragelink 477 {"role": role, "active_members": active_members},
c588255… ragelink 478 )
c588255… ragelink 479
c588255… ragelink 480
c588255… ragelink 481 @login_required
c588255… ragelink 482 def audit_log(request):
c588255… ragelink 483 """Unified audit log across all tracked models. Requires superuser or org admin."""
c588255… ragelink 484 from core.pagination import manual_paginate
c588255… ragelink 485
c588255… ragelink 486 if not request.user.is_superuser:
c588255… ragelink 487 P.ORGANIZATION_CHANGE.check(request.user)
c588255… ragelink 488
c588255… ragelink 489 from fossil.models import FossilRepository
c588255… ragelink 490 from projects.models import Project
c588255… ragelink 491
c588255… ragelink 492 trackable_models = [
c588255… ragelink 493 ("Project", Project),
c588255… ragelink 494 ("Organization", Organization),
c588255… ragelink 495 ("Team", Team),
c588255… ragelink 496 ("FossilRepository", FossilRepository),
c588255… ragelink 497 ]
c588255… ragelink 498
c588255… ragelink 499 entries = []
c588255… ragelink 500 model_filter = request.GET.get("model", "").strip()
c588255… ragelink 501
c588255… ragelink 502 for label, model in trackable_models:
c588255… ragelink 503 if model_filter and label.lower() != model_filter.lower():
c588255… ragelink 504 continue
c588255… ragelink 505 history_model = model.history.model
c588255… ragelink 506 qs = history_model.objects.all().select_related("history_user").order_by("-history_date")[:500]
c588255… ragelink 507 for h in qs:
c588255… ragelink 508 entries.append(
c588255… ragelink 509 {
c588255… ragelink 510 "date": h.history_date,
c588255… ragelink 511 "user": h.history_user,
c588255… ragelink 512 "action": h.get_history_type_display(),
c588255… ragelink 513 "model": label,
c588255… ragelink 514 "object_repr": str(h),
c588255… ragelink 515 "object_id": h.pk,
c588255… ragelink 516 }
c588255… ragelink 517 )
c588255… ragelink 518
c588255… ragelink 519 entries.sort(key=lambda x: x["date"], reverse=True)
c588255… ragelink 520
c588255… ragelink 521 per_page = get_per_page(request)
c588255… ragelink 522 entries, pagination = manual_paginate(entries, request, per_page=per_page)
c588255… ragelink 523
c588255… ragelink 524 available_models = [label for label, _ in trackable_models]
c588255… ragelink 525
c588255… ragelink 526 return render(
c588255… ragelink 527 request,
c588255… ragelink 528 "organization/audit_log.html",
c588255… ragelink 529 {
c588255… ragelink 530 "entries": entries,
c588255… ragelink 531 "model_filter": model_filter,
c588255… ragelink 532 "available_models": available_models,
c588255… ragelink 533 "pagination": pagination,
c588255… ragelink 534 "per_page": per_page,
c588255… ragelink 535 "per_page_options": PER_PAGE_OPTIONS,
c588255… ragelink 536 },
c588255… ragelink 537 )
c588255… ragelink 538
c588255… ragelink 539
c588255… ragelink 540 @login_required
c588255… ragelink 541 def role_initialize(request):
c588255… ragelink 542 P.ORGANIZATION_CHANGE.check(request.user)
c588255… ragelink 543
c588255… ragelink 544 if request.method == "POST":
c588255… ragelink 545 from django.core.management import call_command
c588255… ragelink 546
c588255… ragelink 547 call_command("seed_roles")
c588255… ragelink 548 messages.success(request, "Roles initialized successfully.")
c588255… ragelink 549
c588255… ragelink 550 return redirect("organization:role_list")

Keyboard Shortcuts

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