Skip to content

Commit 35ad047

Browse files
C4ptainCrunchclaude
andcommitted
Add moderation system: tree view, public logs, representative requests, and permission wiring
- Drop course-scoped moderation — moderators can edit any document - Add promoted_by FK on User to track who granted moderator rights - Add moderation tree view (lobste.rs-style) showing the chain of trust - Add public about page explaining the moderation system - Fix document edit logging bug (old values were captured after form binding) - Replace all hardcoded href="/" paths in templates with {% url %} tags - Add isort skip for .venv to prevent corrupting installed packages - Add comprehensive moderation test suite (18 tests) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 07ee27e commit 35ad047

24 files changed

Lines changed: 607 additions & 103 deletions

catalog/templates/catalog/course.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ <h4 class="alert-heading">
246246
mais il se pourrait que certains documents soient mal rangés.
247247
<br>
248248
Si tu ne trouves plus des documents des années précédentes,
249-
<a href="/catalog/f/archives/">regarde dans les archives</a> !
249+
<a href="{% url 'catalog:finder' slugs='archives' %}">regarde dans les archives</a> !
250250
</div>
251251
<div>
252252
<h3>Il n’y a encore rien dans ce cours…</h3>

catalog/templates/catalog/finder.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ <h4 class="alert-heading">
3333
</h4>
3434
Le programme de cours de l'ULB a parfois beaucoup changé au cours des années.
3535
Si tu ne trouves plus des documents des années précédentes,
36-
<a href="/catalog/f/archives/">regarde dans les archives</a> !
36+
<a href="{% url 'catalog:finder' slugs='archives' %}">regarde dans les archives</a> !
3737
</div>
3838
{% endif %}
3939
<div class="d-flex gap-2">

documents/models.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -154,15 +154,6 @@ def add_to_queue(self) -> None:
154154
def get_absolute_url(self) -> str:
155155
return reverse("document_show", args=(self.id,))
156156

157-
def write_perm(self, user, moderated_courses) -> bool:
158-
if user.id == self.user_id:
159-
return True
160-
161-
if self.course_id in moderated_courses:
162-
return True
163-
164-
return False
165-
166157
def tag_from_name(self) -> None:
167158
tags = logic.tags_from_name(self.name)
168159
self.tags.add(*tags)

documents/views.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ def document_edit(request, pk):
125125
reverse("catalog:course_show", args=[doc.course.slug])
126126
)
127127

128+
old_name = doc.name
129+
old_description = doc.description
130+
old_tags = list(doc.tags.all())
131+
128132
form = DocumentForm(request.POST, instance=doc)
129133

130134
if form.is_valid():
@@ -133,12 +137,12 @@ def document_edit(request, pk):
133137
user=request.user,
134138
content_object=doc,
135139
values={
136-
"name": (doc.name, form.cleaned_data["name"]),
140+
"name": (old_name, form.cleaned_data["name"]),
137141
"description": (
138-
doc.description,
142+
old_description,
139143
form.cleaned_data["description"],
140144
),
141-
"tags": (doc.tags.all(), form.cleaned_data["tags"]),
145+
"tags": (old_tags, form.cleaned_data["tags"]),
142146
},
143147
)
144148

@@ -169,7 +173,6 @@ def document_reupload(request, pk):
169173

170174
if not request.user.write_perm(obj=document):
171175
return HttpResponse("You may not edit this document.", status=403)
172-
# FIXME: log moderation action
173176

174177
if document.state != Document.DocumentState.DONE:
175178
return HttpResponse(
@@ -196,13 +199,12 @@ def document_reupload(request, pk):
196199

197200
document.reprocess(force=True)
198201

199-
# TODO Log new version upload
200-
# action.send(
201-
# request.user,
202-
# verb="a uploadé une nouvelle version de",
203-
# action_object=document,
204-
# target=document.course,
205-
# )
202+
if request.user != document.user:
203+
ModerationLog.track(
204+
user=request.user,
205+
content_object=document,
206+
values={"reupload": ("", file.name)},
207+
)
206208

207209
return HttpResponseRedirect(
208210
reverse("catalog:course_show", args=(document.course.slug,))

moderation/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ def action_text(self):
8181
return "a accepté la demande de"
8282
elif self.target_field == "action_rejeter":
8383
return "a refusé la demande de"
84+
elif self.target_field == "reupload":
85+
return "a re-uploadé"
8486
return f"a modifié '{self.target_field}' sur"
8587

8688
@property
@@ -92,6 +94,8 @@ def action_color(self):
9294
return "success"
9395
elif self.target_field == "action_rejeter":
9496
return "warning"
97+
elif self.target_field == "reupload":
98+
return "info"
9599
return "secondary"
96100

97101
@property
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{% extends "base.html" %}
2+
3+
{% block title %}À propos de la modération{% endblock %}
4+
5+
{% block content %}
6+
<div class="container-xl mt-4" style="max-width: 800px;">
7+
<h1 class="h3 mb-4">La modération sur DocHub</h1>
8+
9+
<div class="card shadow-sm border-0 mb-4">
10+
<div class="card-body">
11+
<h2 class="h5 card-title">Que peuvent faire les modérateurs ?</h2>
12+
<p>
13+
Les modérateurs sont des étudiants bénévoles qui aident à garder DocHub propre et bien organisé.
14+
Ils peuvent :
15+
</p>
16+
<ul>
17+
<li>Modifier le titre, la description et les tags de n'importe quel document</li>
18+
<li>Cacher ou rendre visible un document (par exemple si le contenu n'est pas approprié)</li>
19+
<li>Re-uploader une nouvelle version d'un document</li>
20+
<li>Marquer un document comme "Staff pick"</li>
21+
</ul>
22+
<p class="mb-0 text-muted">
23+
Les modérateurs n'ont <strong>pas</strong> accès au panneau d'administration de Django, qui est réservé aux administrateurs système.
24+
</p>
25+
</div>
26+
</div>
27+
28+
<div class="card shadow-sm border-0 mb-4">
29+
<div class="card-body">
30+
<h2 class="h5 card-title">Transparence totale</h2>
31+
<p>
32+
Toutes les actions de modération sont enregistrées publiquement.
33+
N'importe quel utilisateur connecté peut consulter le journal de modération
34+
pour voir qui a fait quoi et quand.
35+
</p>
36+
<a href="{% url 'public_logs' %}" class="btn btn-outline-primary btn-sm">
37+
Consulter le journal de modération
38+
</a>
39+
</div>
40+
</div>
41+
42+
<div class="card shadow-sm border-0 mb-4">
43+
<div class="card-body">
44+
<h2 class="h5 card-title">Comment devenir modérateur ?</h2>
45+
<p>
46+
Tu es délégué d'année, délégué cours de ton cercle, membre d'un bureau étudiant
47+
ou simplement un étudiant motivé ? Tu peux demander les droits de modération !
48+
</p>
49+
<p>
50+
Remplis le formulaire de demande, et un modérateur existant examinera ta candidature.
51+
Si elle est acceptée, tu recevras immédiatement les droits.
52+
</p>
53+
<a href="{% url 'representative_request' %}" class="btn btn-outline-success btn-sm">
54+
Demander les droits de modération
55+
</a>
56+
</div>
57+
</div>
58+
59+
<div class="card shadow-sm border-0 mb-4">
60+
<div class="card-body">
61+
<h2 class="h5 card-title">L'arbre de modération</h2>
62+
<p>
63+
Tu peux voir qui a promu qui dans l'arbre de modération.
64+
Chaque branche montre la chaîne de confiance entre modérateurs.
65+
</p>
66+
<a href="{% url 'moderation_tree' %}" class="btn btn-outline-secondary btn-sm">
67+
Voir l'arbre de modération
68+
</a>
69+
</div>
70+
</div>
71+
</div>
72+
{% endblock content %}

moderation/templates/moderation/home.html

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@
77
<h1 class="d-flex align-items-center gap-2">
88
Modération
99
</h1>
10-
<a class="btn btn-outline-primary btn-sm d-inline-flex align-items-center gap-1"
11-
href="{% url 'moderators_list' %}">
12-
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-people-fill" viewBox="0 0 16 16">
13-
<path d="M7 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1H7Zm4-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm-5.784 6A2.238 2.238 0 0 1 5 13c0-1.355.68-2.75 1.936-3.72A6.325 6.325 0 0 0 5 9c-4 0-5 3-5 4s1 1 1 1h4.216ZM4.5 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z"/>
14-
</svg>
15-
<span>Gérer les modérateurs</span>
16-
</a>
10+
<div class="d-flex gap-2 flex-wrap">
11+
<a class="btn btn-outline-primary btn-sm d-inline-flex align-items-center gap-1"
12+
href="{% url 'moderators_list' %}">
13+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-people-fill" viewBox="0 0 16 16">
14+
<path d="M7 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1H7Zm4-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm-5.784 6A2.238 2.238 0 0 1 5 13c0-1.355.68-2.75 1.936-3.72A6.325 6.325 0 0 0 5 9c-4 0-5 3-5 4s1 1 1 1h4.216ZM4.5 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z"/>
15+
</svg>
16+
<span>Gérer les modérateurs</span>
17+
</a>
18+
<a class="btn btn-outline-secondary btn-sm" href="{% url 'moderation_tree' %}">Arbre</a>
19+
<a class="btn btn-outline-secondary btn-sm" href="{% url 'public_logs' %}">Journal</a>
20+
<a class="btn btn-outline-secondary btn-sm" href="{% url 'moderation_about' %}">À propos</a>
21+
</div>
1722
</header>
1823
{% endblock header %}
1924

moderation/templates/moderation/representative_request_received.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ <h1>Demande de droits de modération</h1>
1616
</div>
1717
</div>
1818

19-
<a href="/" class="btn btn-primary">Retour</a>
19+
<a href="{% url 'index' %}" class="btn btn-primary">Retour</a>
2020
{% endblock content %}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
{% extends "base.html" %}
2+
3+
{% block title %}Arbre de modération{% endblock %}
4+
5+
{% block content %}
6+
<div class="container-xl mt-4">
7+
<div class="d-flex align-items-center justify-content-between mb-4">
8+
<h1 class="h3 mb-0">Arbre de modération</h1>
9+
<a href="{% url 'moderation_about' %}" class="btn btn-outline-secondary btn-sm">Retour</a>
10+
</div>
11+
12+
<p class="text-muted mb-4">
13+
Cette page montre qui a promu qui en tant que modérateur.
14+
Chaque branche représente une chaîne de confiance.
15+
</p>
16+
17+
<style>
18+
.mod-tree ul {
19+
list-style: none;
20+
padding-left: 1.5rem;
21+
margin: 0;
22+
position: relative;
23+
}
24+
.mod-tree ul::before {
25+
content: '';
26+
position: absolute;
27+
left: 0.4rem;
28+
top: 0;
29+
bottom: 0.75rem;
30+
border-left: 1px solid #ccc;
31+
}
32+
.mod-tree li {
33+
position: relative;
34+
padding: 0.15rem 0;
35+
}
36+
.mod-tree li::before {
37+
content: '';
38+
position: absolute;
39+
left: -1.1rem;
40+
top: 0.75rem;
41+
width: 0.7rem;
42+
border-top: 1px solid #ccc;
43+
}
44+
.mod-tree .tree-root {
45+
list-style: none;
46+
padding: 0;
47+
margin: 0;
48+
}
49+
.mod-tree .tree-root::before {
50+
display: none;
51+
}
52+
.mod-tree .tree-root > li::before {
53+
display: none;
54+
}
55+
.mod-tree .doc-count {
56+
color: #888;
57+
font-size: 0.9em;
58+
}
59+
</style>
60+
61+
{% if tree %}
62+
<div class="mod-tree">
63+
<ul class="tree-root">
64+
{% for root in tree %}
65+
{% include "moderation/tree_node.html" with node=root %}
66+
{% endfor %}
67+
</ul>
68+
</div>
69+
{% else %}
70+
<div class="alert alert-light border text-center py-5 text-muted">
71+
<p class="mb-0">Aucune promotion enregistrée.</p>
72+
</div>
73+
{% endif %}
74+
</div>
75+
{% endblock content %}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<li>
2+
<strong>{{ node.user.netid }}</strong>
3+
<span class="text-muted">{{ node.user.name }}</span>
4+
<span class="doc-count">({{ node.user.document_count }})</span>
5+
{% if node.user.is_staff %}
6+
<span class="badge bg-danger ms-1" style="font-size:0.7em;">Admin</span>
7+
{% endif %}
8+
{% if node.children %}
9+
<ul>
10+
{% for child in node.children %}
11+
{% include "moderation/tree_node.html" with node=child %}
12+
{% endfor %}
13+
</ul>
14+
{% endif %}
15+
</li>

0 commit comments

Comments
 (0)