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