FossilRepo

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

Keyboard Shortcuts

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