Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

This page tries to contain all use facing changes made on DocHub.

# Unreleased

Moderation:
* moderators can now edit any document
* moderators can now approve new moderators
* admins can now add and remove moderators
* add a moderation tree view showing who promoted whom
* add a public moderation transparency log

# 2026.4.0

* Add a institutional disclaimer and contribution call on the login page
Expand Down
2 changes: 1 addition & 1 deletion catalog/templates/catalog/course.html
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ <h4 class="alert-heading">
mais il se pourrait que certains documents soient mal rangés.
<br>
Si tu ne trouves plus des documents des années précédentes,
<a href="/catalog/f/archives/">regarde dans les archives</a> !
<a href="{% url 'catalog:finder' slugs='archives' %}">regarde dans les archives</a> !
</div>
<div>
<h3>Il n’y a encore rien dans ce cours…</h3>
Expand Down
2 changes: 1 addition & 1 deletion catalog/templates/catalog/finder.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ <h4 class="alert-heading">
</h4>
Le programme de cours de l'ULB a parfois beaucoup changé au cours des années.
Si tu ne trouves plus des documents des années précédentes,
<a href="/catalog/f/archives/">regarde dans les archives</a> !
<a href="{% url 'catalog:finder' slugs='archives' %}">regarde dans les archives</a> !
</div>
{% endif %}
<div class="d-flex gap-2">
Expand Down
9 changes: 0 additions & 9 deletions documents/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,15 +154,6 @@ def add_to_queue(self) -> None:
def get_absolute_url(self) -> str:
return reverse("document_show", args=(self.id,))

def write_perm(self, user, moderated_courses) -> bool:
if user.id == self.user_id:
return True

if self.course_id in moderated_courses:
return True

return False

def tag_from_name(self) -> None:
tags = logic.tags_from_name(self.name)
self.tags.add(*tags)
Expand Down
8 changes: 8 additions & 0 deletions documents/templates/documents/document_edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ <h5 class="card-header">Modération</h5>
<input type="input" class="form-control" id="added" value="{{ doc.user }}" disabled>
</div>
-->
{% if request.user != doc.user and request.user|has_moderation_perm_on:doc %}
<p class="text-muted mt-3 mb-2">
⚠️ Tu modifies ici un document qui ne t'appartient pas. En tant que modérateur·trice,
tu peux corriger, masquer ou mettre en avant des documents pour garder DocHub propre et utile,
et ces actions sont enregistrées publiquement dans le
<a href="{% url 'public_logs' %}">journal de modération</a>.
</p>
{% endif %}
<div class="mt-3">
<input type="submit" class="btn btn-success" name="update" value="Mettre à jour"/>
{% if doc.hidden %}
Expand Down
24 changes: 13 additions & 11 deletions documents/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ def document_edit(request, pk):
reverse("catalog:course_show", args=[doc.course.slug])
)

old_name = doc.name
old_description = doc.description
old_tags = list(doc.tags.all())

form = DocumentForm(request.POST, instance=doc)

if form.is_valid():
Expand All @@ -133,12 +137,12 @@ def document_edit(request, pk):
user=request.user,
content_object=doc,
values={
"name": (doc.name, form.cleaned_data["name"]),
"name": (old_name, form.cleaned_data["name"]),
"description": (
doc.description,
old_description,
form.cleaned_data["description"],
),
"tags": (doc.tags.all(), form.cleaned_data["tags"]),
"tags": (old_tags, form.cleaned_data["tags"]),
},
)

Expand Down Expand Up @@ -169,7 +173,6 @@ def document_reupload(request, pk):

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

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

document.reprocess(force=True)

# TODO Log new version upload
# action.send(
# request.user,
# verb="a uploadé une nouvelle version de",
# action_object=document,
# target=document.course,
# )
if request.user != document.user:
ModerationLog.track(
user=request.user,
content_object=document,
values={"reupload": ("", file.name)},
)

return HttpResponseRedirect(
reverse("catalog:course_show", args=(document.course.slug,))
Expand Down
4 changes: 4 additions & 0 deletions moderation/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ def action_text(self):
return "a accepté la demande de"
elif self.target_field == "action_rejeter":
return "a refusé la demande de"
elif self.target_field == "reupload":
return "a re-uploadé"
return f"a modifié '{self.target_field}' sur"

@property
Expand All @@ -92,6 +94,8 @@ def action_color(self):
return "success"
elif self.target_field == "action_rejeter":
return "warning"
elif self.target_field == "reupload":
return "info"
return "secondary"

@property
Expand Down
55 changes: 55 additions & 0 deletions moderation/templates/moderation/about.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{% extends "base.html" %}

{% block title %}À propos de la modération{% endblock %}

{% block content %}
<div class="container-xl mt-4" style="max-width: 800px;">
<h1 id="moderation" class="mb-4">La modération sur DocHub</h1>

<h2 id="role">Que peuvent faire les modérateur·trices ?</h2>
<p>
Les modérateur·trices sont des étudiant·es bénévoles qui aident à garder DocHub propre et bien organisé.
Elles et ils peuvent :
</p>
<ul class="mb-4">
<li>Modifier le titre, la description et les tags de n'importe quel document</li>
<li>Cacher ou rendre visible un document (par exemple si le contenu n'est pas approprié)</li>
<li>Re-uploader une nouvelle version d'un document</li>
<li>Marquer un document comme "Staff pick"</li>
</ul>

<h2 id="transparence">Transparence</h2>
<p>
Toutes les actions de modération sont enregistrées publiquement.
N'importe quel utilisateur connecté peut consulter le journal de modération
pour voir qui a fait quoi et quand.
</p>
<p class="mb-4"><a href="{% url 'public_logs' %}">Consulter le journal de modération</a></p>

{% if request.user.is_moderator or request.user.is_staff %}
<h2 id="ajouter-moderateur">Ajouter un·e modérateur·trice</h2>
<p>
En tant que modérateur·trice, tu peux promouvoir un·e étudiant·e en lui accordant les droits de modération depuis la liste des modérateur·trices.
Attention, en ajoutant quelqu'un·e comme modérateur·trice, <b>tu en es responsable</b>, n'accorde pas ce droit à la légère.
</p>
<p class="mb-4"><a href="{% url 'manage_moderators' %}">Ajouter des modérateur·trices</a></p>
{% else %}
<h2 id="devenir-moderateur">Comment devenir modérateur·trice ?</h2>
<p>
Tu es délégué·e d'année, délégué·e cours de ton cercle, membre d'un bureau étudiant·e
ou simplement un·e étudiant·e motivé·e ? Tu peux demander les droits de modération !
</p>
<p>
Remplis le formulaire de demande, et un·e modérateur·trice examinera ta candidature.
Si elle est acceptée, tu recevras immédiatement les droits.
</p>
<p class="mb-4"><a href="{% url 'representative_request' %}">Demander les droits de modération</a></p>
{% endif %}

<h2 id="arbre">L'arbre de modération</h2>
<p>
Tu peux voir qui a promu qui dans l'arbre de modération.
</p>
<p><a href="{% url 'moderation_tree' %}">Voir l'arbre de modération</a></p>
</div>
{% endblock content %}
94 changes: 12 additions & 82 deletions moderation/templates/moderation/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,90 +7,20 @@
<h1 class="d-flex align-items-center gap-2">
Modération
</h1>
<a class="btn btn-outline-primary btn-sm d-inline-flex align-items-center gap-1"
href="{% url 'moderators_list' %}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-people-fill" viewBox="0 0 16 16">
<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"/>
</svg>
<span>Gérer les modérateurs</span>
</a>
<div class="d-flex gap-2 flex-wrap">
<a class="btn btn-outline-primary btn-sm d-inline-flex align-items-center gap-1"
href="{% url 'manage_moderators' %}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-people-fill" viewBox="0 0 16 16">
<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"/>
</svg>
<span>Gérer les modérateurs</span>
</a>
<a class="btn btn-outline-secondary btn-sm" href="{% url 'moderation_tree' %}">Arbre</a>
<a class="btn btn-outline-secondary btn-sm" href="{% url 'public_logs' %}">Journal</a>
<a class="btn btn-outline-secondary btn-sm" href="{% url 'moderation_about' %}">À propos</a>
</div>
</header>
{% endblock header %}

{% block content %}
<div class="container-xl mt-4">

{% if request.GET.error == 'reason' %}
<div class="alert alert-danger alert-dismissible fade show shadow-sm" role="alert">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-exclamation-triangle-fill flex-shrink-0 me-2" viewBox="0 0 16 16">
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
</svg>
Veuillez fournir une raison d'au moins 10 caractères pour justifier le refus.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
<div class="d-flex align-items-center justify-content-between mb-4">
<h2 class="h4 mb-0">Demandes en attente ({{ pending_requests.count }})</h2>
</div>

<div class="row">
{% for req in pending_requests %}
<div class="col-md-6 col-lg-4 mb-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-body d-flex flex-column">

<div class="d-flex align-items-center gap-3 mb-3">
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center shadow-sm" style="width: 45px; height: 45px; font-size: 1.1rem; font-weight: bold;">
{{ req.user.initials|upper }}
</div>
<div>
<h5 class="card-title h6 mb-0">{{ req.user.fullname }}</h5>
<small class="text-muted">{{ req.user.netid }}</small>
</div>
</div>

<div class="mb-3">
<span class="badge bg-light text-dark border border-secondary">{{ req.get_faculty_display }}</span>
<span class="badge bg-light text-dark border border-secondary">{{ req.get_role_display }}</span>
</div>

{% if req.comment %}
<div class="bg-light p-3 rounded mb-3 flex-grow-1">
<p class="card-text small fst-italic mb-0 text-muted">
"{{ req.comment }}"
</p>
</div>
{% else %}
<div class="flex-grow-1"></div>
{% endif %}

<p class="text-muted small mb-3">
📅 Reçue le {{ req.created|date:"d/m/Y à H:i" }}
</p>

<div class="pt-3 border-top mt-auto">
<form method="post" action="{% url 'process_representative_request' req.id %}" class="w-100">
{% csrf_token %}
<input type="text" name="rejection_reason" class="form-control form-control-sm mb-2" placeholder="Message de refus (10 caractères min.)" minlength="10">

<div class="d-flex gap-2">
<button type="submit" name="action" value="accept" class="btn btn-success btn-sm flex-grow-1 fw-bold">Accepter</button>
<button type="submit" name="action" value="reject" class="btn btn-outline-danger btn-sm flex-grow-1" data-turbo-confirm="Es-tu sûr de vouloir refuser cette demande ?">Refuser</button>
</div>
</form>
</div>

</div>
</div>
</div>
{% empty %}
<div class="col-12">
<div class="alert alert-light border border-secondary text-center py-5 text-muted shadow-sm">
<h5 class="mb-2">Tout est calme ! ☕</h5>
<p class="mb-0">Il n'y a aucune demande d'accès modérateur en attente pour le moment.</p>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock content %}
Loading
Loading