Compare commits

..
3 Commits
Author SHA1 Message Date
klmp200 35a55ff95a Fix animation bug on club list
Also, due to some weird jinja bug, css block is applied even if supposedly
in a conditional statement
2026-09-08 23:17:35 +02:00
klmp200 00cae33a33 Fix moderation view 2026-09-07 17:36:53 +02:00
klmp200 4bfcb6a267 Fix club-list pagination 2026-09-07 17:36:50 +02:00
23 changed files with 288 additions and 548 deletions
+2 -2
View File
@@ -50,8 +50,8 @@ class ClubAdmin(admin.ModelAdmin):
@admin.register(ClubRole) @admin.register(ClubRole)
class ClubRoleAdmin(admin.ModelAdmin): class ClubRoleAdmin(admin.ModelAdmin):
list_display = ("name", "club", "is_board", "is_presidency") list_display = ("name", "club", "is_board", "is_presidency")
search_fields = ("name", "club__name") search_fields = ("name",)
autocomplete_fields = ("club", "linked_groups") autocomplete_fields = ("club",)
list_select_related = ("club",) list_select_related = ("club",)
list_filter = ( list_filter = (
"is_board", "is_board",
-7
View File
@@ -479,13 +479,6 @@ class ClubRoleCreateForm(forms.ModelForm):
class ClubRoleBaseFormSet(forms.BaseInlineFormSet): class ClubRoleBaseFormSet(forms.BaseInlineFormSet):
ordering_widget = forms.HiddenInput() ordering_widget = forms.HiddenInput()
def __init__(self, *args, queryset=None, **kwargs):
if queryset is None:
queryset = self.model._default_manager
super().__init__(
*args, queryset=queryset.prefetch_related("linked_groups"), **kwargs
)
ClubRoleFormSet = forms.inlineformset_factory( ClubRoleFormSet = forms.inlineformset_factory(
Club, Club,
@@ -1,26 +0,0 @@
# Generated by Django 5.2.17 on 2026-09-01 14:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("club", "0017_linktype_clublink"),
("core", "0050_alter_sithfile_moderator"),
]
operations = [
migrations.AddField(
model_name="clubrole",
name="linked_groups",
field=models.ManyToManyField(
help_text=(
"Groups that are automatically given or removed "
"to user receiving or losing this club role"
),
related_name="club_roles",
to="core.group",
verbose_name="Linked groups",
),
),
]
+11 -32
View File
@@ -23,8 +23,6 @@
# #
from __future__ import annotations from __future__ import annotations
import operator
from functools import reduce
from typing import Iterable, Self from typing import Iterable, Self
from django.conf import settings from django.conf import settings
@@ -284,15 +282,6 @@ class ClubRole(OrderedModel):
"If the role is inactive, people joining the club won't be able to get it." "If the role is inactive, people joining the club won't be able to get it."
), ),
) )
linked_groups = models.ManyToManyField(
Group,
verbose_name=_("Linked groups"),
help_text=_(
"Groups that are automatically given or removed "
"to user receiving or losing this club role"
),
related_name="linked_roles",
)
order_with_respect_to = "club" order_with_respect_to = "club"
@@ -545,7 +534,7 @@ class Membership(models.Model):
def _remove_club_groups( def _remove_club_groups(
memberships: Iterable[Membership], memberships: Iterable[Membership],
) -> tuple[int, dict[str, int]]: ) -> tuple[int, dict[str, int]]:
"""Remove users of those memberships from the club and club role groups. """Remove users of those memberships from the club groups.
For example, if a user is in the Troll club board, For example, if a user is in the Troll club board,
he is in the board group and the members group of the Troll. he is in the board group and the members group of the Troll.
@@ -564,19 +553,15 @@ class Membership(models.Model):
clubs = {m.club_id for m in memberships} clubs = {m.club_id for m in memberships}
users = {m.user_id for m in memberships} users = {m.user_id for m in memberships}
groups = Group.objects.filter(Q(club__in=clubs) | Q(club_board__in=clubs)) groups = Group.objects.filter(Q(club__in=clubs) | Q(club_board__in=clubs))
role_groups = [
Q(user_id=m.user_id, group__linked_roles=m.role_id) for m in memberships
]
return User.groups.through.objects.filter( return User.groups.through.objects.filter(
(Q(group__in=groups) & Q(user__in=users)) Q(group__in=groups) & Q(user__in=users)
| reduce(operator.or_, role_groups)
).delete() ).delete()
@staticmethod @staticmethod
def _add_club_groups( def _add_club_groups(
memberships: Iterable[Membership], memberships: Iterable[Membership],
) -> list[User.groups.through]: ) -> list[User.groups.through]:
"""Add users of those memberships to the club and club role groups. """Add users of those memberships to the club groups.
For example, if a user just joined the Troll club board, For example, if a user just joined the Troll club board,
he will be added in both the members group and the board group he will be added in both the members group and the board group
@@ -597,40 +582,34 @@ class Membership(models.Model):
memberships = [m for m in memberships if m.end_date is None] memberships = [m for m in memberships if m.end_date is None]
if not memberships: if not memberships:
return [] return []
nb_prefetched = sum(
1 for m in memberships if not hasattr(m, "club") or not hasattr(m, "role") if sum(1 for m in memberships if not hasattr(m, "club")) > 1:
)
if nb_prefetched > 1:
# if more than one membership hasn't its `club` attribute set # if more than one membership hasn't its `club` attribute set
# it's less expensive to reload the whole query with # it's less expensive to reload the whole query with
# a select_related than perform a distinct query # a select_related than perform a distinct query
# to fetch each club. # to fetch each club.
ids = {m.id for m in memberships} ids = {m.id for m in memberships}
memberships = list( memberships = list(
Membership.objects.filter(id__in=ids) Membership.objects.filter(id__in=ids).select_related("club")
.select_related("club", "role")
.prefetch_related("role__linked_groups")
) )
groups = [] club_groups = []
for membership in memberships: for membership in memberships:
groups.append( club_groups.append(
User.groups.through( User.groups.through(
user_id=membership.user_id, user_id=membership.user_id,
group_id=membership.club.members_group_id, group_id=membership.club.members_group_id,
) )
) )
if membership.role.is_board: if membership.role.is_board:
groups.append( club_groups.append(
User.groups.through( User.groups.through(
user_id=membership.user_id, user_id=membership.user_id,
group_id=membership.club.board_group_id, group_id=membership.club.board_group_id,
) )
) )
groups.extend( return User.groups.through.objects.bulk_create(
User.groups.through(user_id=membership.user_id, group_id=g.id) club_groups, ignore_conflicts=True
for g in membership.role.linked_groups.all()
) )
return User.groups.through.objects.bulk_create(groups, ignore_conflicts=True)
class Mailing(models.Model): class Mailing(models.Model):
+11 -6
View File
@@ -15,9 +15,6 @@
{% endblock %} {% endblock %}
{% else %} {% else %}
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static("club/list.scss") }}">
{% endblock %}
{% block description -%} {% block description -%}
{% trans %}The list of all clubs existing at UTBM.{% endtrans %} {% trans %}The list of all clubs existing at UTBM.{% endtrans %}
{%- endblock %} {%- endblock %}
@@ -26,6 +23,12 @@
{%- endblock %} {%- endblock %}
{% endif %} {% endif %}
{% block additional_css %}
{% if not is_fragment %}
<link rel="stylesheet" href="{{ static("club/list.scss") }}">
{% endif %}
{% endblock %}
{% from "core/macros.jinja" import paginate_htmx %} {% from "core/macros.jinja" import paginate_htmx %}
{% block content %} {% block content %}
@@ -33,15 +36,17 @@
<h3>{% trans %}Filters{% endtrans %}</h3> <h3>{% trans %}Filters{% endtrans %}</h3>
<form <form
id="club-list-filters" id="club-list-filters"
hx-get="{{ url("club:club_list") }}" method="GET"
hx-action="{{ url("club:club_list") }}"
hx-target="#content" hx-target="#content"
hx-swap="outerHtml" hx-swap="innerHTML"
hx-push-url="true" hx-push-url="true"
hx-disable="find input, find button"
> >
<div class="row gap-4x"> <div class="row gap-4x">
{{ form }} {{ form }}
</div> </div>
<button type="submit" class="btn btn-blue margin-bottom"> <button class="btn btn-blue margin-bottom">
<i class="fa fa-magnifying-glass"></i>{% trans %}Search{% endtrans %} <i class="fa fa-magnifying-glass"></i>{% trans %}Search{% endtrans %}
</button> </button>
</form> </form>
-14
View File
@@ -49,20 +49,6 @@
{{ subform.is_active.help_text }} {{ subform.is_active.help_text }}
</span> </span>
</div> </div>
{% set groups = subform.instance.linked_groups.all()|list %}
{% if groups %}
<div>
<p>
<strong>{% trans %}Linked groups : {% endtrans %}</strong>
{{ groups|map(attribute="name")|join(", ") }}
</p>
<p class="helptext">
{% trans trimmed %}
Users receiving this role will also be assigned to those groups
{% endtrans %}
</p>
</div>
{% endif %}
</div> </div>
</details> </details>
</div> </div>
+1 -24
View File
@@ -17,7 +17,7 @@ from club.forms import ClubAddMemberForm, JoinClubForm
from club.models import Club, ClubRole, Membership from club.models import Club, ClubRole, Membership
from club.tests.base import TestClub from club.tests.base import TestClub
from core.baker_recipes import subscriber_user from core.baker_recipes import subscriber_user
from core.models import AnonymousUser, Group, User from core.models import AnonymousUser, User
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable from collections.abc import Callable
@@ -503,29 +503,6 @@ class TestMembership(TestClub):
assert self.subscriber.groups.contains(self.club.members_group) assert self.subscriber.groups.contains(self.club.members_group)
assert self.subscriber.groups.contains(self.club.board_group) assert self.subscriber.groups.contains(self.club.board_group)
def test_add_to_club_role_group(self):
groups = baker.make(Group, _quantity=4)
self.subscriber.groups.set(groups[:1])
self.board_role.linked_groups.set(groups[1:3])
baker.make(
Membership, club=self.club, user=self.subscriber, role=self.board_role
)
assert set(self.subscriber.groups.all()) == {
*groups[:3],
self.club.board_group,
self.club.members_group,
}
def test_remove_from_club_role_group(self):
groups = baker.make(Group, _quantity=3)
baker.make(
Membership, club=self.club, user=self.subscriber, role=self.board_role
)
self.subscriber.groups.set(groups[:1])
self.board_role.linked_groups.set(groups[1:])
self.subscriber.memberships.update(end_date=localdate())
assert set(self.subscriber.groups.all()) == {groups[0]}
def test_change_position_in_club(self): def test_change_position_in_club(self):
"""Test that when moving from board to members, club group change""" """Test that when moving from board to members, club group change"""
membership = baker.make( membership = baker.make(
-1
View File
@@ -63,7 +63,6 @@ document.body.addEventListener(
); );
htmx.config.transitions = true; htmx.config.transitions = true;
Object.assign(window, { htmx }); Object.assign(window, { htmx });
/** /**
@@ -31,7 +31,7 @@
{% if is_fragment %} {% if is_fragment %}
hx-post="{{ action }}" hx-post="{{ action }}"
hx-target="#content" hx-target="#content"
hx-swap="outerHtml" hx-swap="innerHTML"
{% endif %} {% endif %}
>{% trans %}Confirm{% endtrans %}</button> >{% trans %}Confirm{% endtrans %}</button>
@@ -39,7 +39,7 @@
{% if is_fragment %} {% if is_fragment %}
hx-get="{{ previous }}" hx-get="{{ previous }}"
hx-target="#content" hx-target="#content"
hx-swap="outerHtml" hx-swap="innerHTML"
{% else %} {% else %}
action="window.history.back()" action="window.history.back()"
{% endif %} {% endif %}
+2 -2
View File
@@ -39,13 +39,13 @@
<p><button <p><button
hx-get="{{ url('core:file_moderate', file_id=f.id) }}" hx-get="{{ url('core:file_moderate', file_id=f.id) }}"
hx-target="#content" hx-target="#content"
hx-swap="outerHtml" hx-swap="innerHTML"
>{% trans %}Moderate{% endtrans %}</button> - >{% trans %}Moderate{% endtrans %}</button> -
{% set current_page = url('core:file_moderation') + "?page=" + page_obj.number | string %} {% set current_page = url('core:file_moderation') + "?page=" + page_obj.number | string %}
<button <button
hx-get="{{ url('core:file_delete', file_id=f.id) }}?next={{ current_page | urlencode }}&previous={{ current_page | urlencode }}" hx-get="{{ url('core:file_delete', file_id=f.id) }}?next={{ current_page | urlencode }}&previous={{ current_page | urlencode }}"
hx-target="#file-{{ loop.index }}" hx-target="#file-{{ loop.index }}"
hx-swap="outerHtml" hx-swap="innerHTML"
>{% trans %}Delete{% endtrans %}</button></p> >{% trans %}Delete{% endtrans %}</button></p>
</div> </div>
{% endfor %} {% endfor %}
-20
View File
@@ -260,18 +260,6 @@ msgstr ""
"Si ce rôle est inactif, il ne pourra pas être attribué aux gens qui " "Si ce rôle est inactif, il ne pourra pas être attribué aux gens qui "
"rejoignent le club." "rejoignent le club."
#: club/models.py
msgid "Linked groups"
msgstr "Groupes liés"
#: club/models.py
msgid ""
"Groups that are automatically given or removed to user receiving or losing "
"this club role"
msgstr ""
"Les groupes qui sont automatiquement donnés ou retirés quand l'utilisateur "
"reçoit ou perd ce rôle de club"
#: club/models.py election/models.py #: club/models.py election/models.py
msgid "club role" msgid "club role"
msgstr "rôle de club" msgstr "rôle de club"
@@ -497,14 +485,6 @@ msgstr "Du"
msgid "To" msgid "To"
msgstr "Au" msgstr "Au"
#: club/templates/club/club_roles.jinja
msgid "Linked groups : "
msgstr "Groupes liés : "
#: club/templates/club/club_roles.jinja
msgid "Users receiving this role will also be assigned to those groups"
msgstr "Les utilisateurs recevant ce rôle seront aussi assignés à ces groupes"
#: club/templates/club/club_roles.jinja #: club/templates/club/club_roles.jinja
msgid "" msgid ""
"Roles give rights on the club. Higher roles grant more rights, and the " "Roles give rights on the club. Higher roles grant more rights, and the "
+1 -5
View File
@@ -125,11 +125,7 @@ class UE(models.Model):
Returns: Returns:
True if the user has already posted a comment on this UE, else False. True if the user has already posted a comment on this UE, else False.
""" """
self._has_user_commented = getattr(self, "_has_user_commented", {}) return self.comments.filter(author=user).exists()
self._has_user_commented[user] = self._has_user_commented.get(
user, self.comments.filter(author=user).exists()
)
return self._has_user_commented[user]
@cached_property @cached_property
def grade_global_average(self): def grade_global_average(self):
+7 -12
View File
@@ -69,7 +69,6 @@ $pedagogy-white-text: #f0f0f0;
input[type="checkbox"] { input[type="checkbox"] {
display: none; display: none;
} }
margin-top: 10px; margin-top: 10px;
margin-bottom: 0; margin-bottom: 0;
color: white; color: white;
@@ -129,14 +128,14 @@ $pedagogy-white-text: #f0f0f0;
grid-area: hours-the; grid-area: hours-the;
} }
.leave-comment-not-allowed { #leave_comment_not_allowed {
p { p {
text-align: center; text-align: center;
color: red; color: red;
} }
} }
.leave-comment { #leave_comment {
.leave-comment-grid-container { .leave-comment-grid-container {
display: grid; display: grid;
grid-template-columns: 270px auto; grid-template-columns: 270px auto;
@@ -169,6 +168,10 @@ $pedagogy-white-text: #f0f0f0;
.input-stars { .input-stars {
margin-top: 20px; margin-top: 20px;
} }
input[type="submit"] {
float: right;
}
} }
.ue-details-container { .ue-details-container {
@@ -330,9 +333,8 @@ $pedagogy-white-text: #f0f0f0;
padding-left: 10px; padding-left: 10px;
} }
.action { .actions {
float: right; float: right;
margin-top: 0;
} }
} }
@@ -400,7 +402,6 @@ $pedagogy-white-text: #f0f0f0;
a { a {
color: $pedagogy-white-text; color: $pedagogy-white-text;
cursor: pointer;
} }
a:hover { a:hover {
@@ -443,9 +444,3 @@ details.accordion>.accordion-content {
border-color: $pedagogy-orange; border-color: $pedagogy-orange;
border-right: none; border-right: none;
} }
.buttons {
display: flex;
justify-content: flex-end;
align-items: center;
}
@@ -1,31 +0,0 @@
<form
class="report-form"
hx-post="{{ request.get_full_path() }}"
hx-target="this"
hx-swap="outerHTML"
hx-disable="button input[type='submit']"
hx-trigger="submit"
>
{% csrf_token %}
{{ form.non_field_errors() }}
{{ form.reason.errors }}
{{ form.reason }}
{# Hidden fields #}
{{ form.reporter }}
{{ form.comment }}
<div class="buttons">
<button
class="btn btn-red left"
hx-get="{{ ue_detail_url }}"
hx-target="closest form"
hx-swap="outerHTML"
>
{% trans %}Cancel{% endtrans %}
</button>
<input class="btn btn-green" type="submit" value="{% trans %}Report{% endtrans %}" />
</div>
</form>
@@ -1,71 +0,0 @@
<div class="leave-comment">
{% if form.is_creation %}
<details class="accordion" id="leave_comment" {% if form.errors %}open{% endif %}>
<summary>{% trans %}Leave comment{% endtrans %}</summary>
<div class="accordion-content">
{% endif %}
<form
hx-post="{{ action }}"
hx-target="closest .leave-comment"
hx-swap="outerHTML"
hx-disabled-elt="find input[type='submit']"
>
{% csrf_token %}
<div class="leave-comment-grid-container">
<div class="form-stars">
{{ form.non_field_errors() }}
{{ form.author.errors }}
{{ form.ue.errors }}
{{ form.author }}
{{ form.ue }}
<div class="input-stars">
<label for="{{ form.grade_global.id_for_label }}">{{ form.grade_global.label }} :</label>
{{ form.grade_global.errors }}
{{ form.grade_global }}
</div>
<div class="input-stars">
<label for="{{ form.grade_utility.id_for_label }}">{{ form.grade_utility.label }} :</label>
{{ form.grade_utility.errors }}
{{ form.grade_utility }}
</div>
<div class="input-stars">
<label for="{{ form.grade_interest.id_for_label }}">{{ form.grade_interest.label }} :</label>
{{ form.grade_interest.errors }}
{{ form.grade_interest }}
</div>
<div class="input-stars">
<label for="{{ form.grade_teaching.id_for_label }}">{{ form.grade_teaching.label }} :</label>
{{ form.grade_teaching.errors }}
{{ form.grade_teaching }}
</div>
<div class="input-stars">
<label for="{{ form.grade_work_load.id_for_label }}">{{ form.grade_work_load.label }} :</label>
{{ form.grade_work_load.errors }}
{{ form.grade_work_load }}
</div>
</div>
<div class="form-comment">
<label for="{{ form.comment.id_for_label }}">{{ form.comment.label }} :</label>
{{ form.comment.errors }}
{{ form.comment }}
</div>
</div>
<div class="buttons">
<input type="submit" value="{% trans %}Comment{% endtrans %}" />
</div>
</form>
{% if form.is_creation %}
</div>
</details>
{% endif %}
<br>
</div>
@@ -1,91 +0,0 @@
{% from "pedagogy/macros.jinja" import display_star %}
{% from "core/macros.jinja" import user_profile_link %}
{% if comments %}
<h2>{% trans %}Comments{% endtrans %}</h2>
<br>
{% endif %}
<section>
{% for comment in comments %}
<div id="comment-{{ comment.id }}" class="comment-container">
<div class="grade-block">
<div class="grade-type">
<p>{% trans %}Global grade{% endtrans %}</p>
<p>{% trans %}Utility{% endtrans %}</p>
<p>{% trans %}Interest{% endtrans %}</p>
<p>{% trans %}Teaching{% endtrans %}</p>
<p>{% trans %}Work load{% endtrans %}</p>
</div>
<div class="grade-stars">
<p>{{ display_star(comment.grade_global) }}</p>
<p>{{ display_star(comment.grade_utility) }}</p>
<p>{{ display_star(comment.grade_interest) }}</p>
<p>{{ display_star(comment.grade_teaching) }}</p>
<p>{{ display_star(comment.grade_work_load) }}</p>
</div>
<div class="grade-extension"></div>
</div>
<div class="comment">
<div class="anchor">
<a href="{{ url('pedagogy:ue_detail', ue_id=ue.id) }}#comment-{{ comment.id }}"><i class="fa fa-paragraph"></i></a>
</div>
{{ comment.comment|markdown }}
</div>
<div class="info">
{% if comment.is_reported %}
<p class="status-reported">
{% trans %}This comment has been reported{% endtrans %}
</p>
{% endif %}
{% if comment.author_id == user.id or user.has_perm("pedagogy.change_comment") %}
<button
class="btn btn-orange action"
hx-get="{{ url('pedagogy:comment_update', comment_id=comment.id) }}"
hx-swap="outerHTML"
hx-target="#comment-{{ comment.id }}"
>
<i class="fa fa-pencil"></i> {% trans %}Edit{% endtrans %}
</button>
{% endif %}
{% if comment.author_id == user.id or user.has_perm("pedagogy.delete_comment") %}
<form class="action"
hx-post="{{ url('pedagogy:comment_delete', comment_id=comment.id) }}"
hx-confirm='{% trans obj=object %}Are you sure you want to delete "{{ obj }}"?{% endtrans %}'
hx-swap="outerHTML"
hx-target="#comment-{{ comment.id }}"
>
{% csrf_token %}
<button class="btn btn-red action">
<i class="fa fa-trash-can"></i> {% trans %}Delete{% endtrans %}
</button>
</form>
{% endif %}
</div>
<div class="comment-end-bar">
<div class="report">
<p>
<a
hx-get="{{ url('pedagogy:comment_report', comment_id=comment.id) }}"
hx-swap="outerHTML"
hx-target="#comment-{{ comment.id }}"
>
{% trans %}Report this comment{% endtrans %}
</a>
</p>
</div>
<div class="date"><p>{{ comment.publish_date.strftime('%d/%m/%Y') }}</p></div>
<div class="author"><p>{{ user_profile_link(comment.author) }}</p></div>
</div>
</div>
{% endfor %}
</section>
@@ -1,10 +0,0 @@
{% if object.has_user_already_commented(user) %}
<div class="leave-comment-not-allowed">
<p>{% trans %}You already posted a comment on this UE. If you want to comment again, please modify or delete your previous comment.{% endtrans %}</p>
</div>
<br>
{% endif %}
{% if not object.has_user_already_commented(user) and user.has_perm("pedagogy.add_uecomment") %}
{{ add_comment_form }}
{% endif %}
@@ -1,27 +0,0 @@
<div class="ue-details-container">
<div class="grade">
<p>{% trans %}Global grade{% endtrans %}</p>
<p>{% trans %}Utility{% endtrans %}</p>
<p>{% trans %}Interest{% endtrans %}</p>
<p>{% trans %}Teaching{% endtrans %}</p>
<p>{% trans %}Work load{% endtrans %}</p>
</div>
<div class="grade-stars">
<p>{{ display_star(object.grade_global_average) }}</p>
<p>{{ display_star(object.grade_utility_average) }}</p>
<p>{{ display_star(object.grade_interest_average) }}</p>
<p>{{ display_star(object.grade_teaching_average) }}</p>
<p>{{ display_star(object.grade_work_load_average) }}</p>
</div>
<div class="ue-infos">
<p><b>{% trans %}Objectives{% endtrans %}</b></p>
<p>{{ object.objectives|markdown }}</p>
<p><b>{% trans %}Program{% endtrans %}</b></p>
<p>{{ object.program|markdown }}</p>
<p><b>{% trans %}Earned skills{% endtrans %}</b></p>
<p>{{ object.skills|markdown }}</p>
<p><b>{% trans %}Key concepts{% endtrans %}</b></p>
<p>{{ object.key_concepts|markdown }}</p>
<p><b>{% trans %}UE manager: {% endtrans %}</b>{{ object.manager }}</p>
</div>
</div>
+154 -25
View File
@@ -1,21 +1,6 @@
{% from "pedagogy/macros.jinja" import display_star %}
{% if is_fragment %}
<hx-partial id="ue-grade">
{% include "pedagogy/fragments/ue_details/grade.jinja" %}
</hx-partial>
<hx-partial id="comment-form">
{% include "pedagogy/fragments/ue_details/form.jinja" %}
</hx-partial>
<hx-partial id="comments" hx-swap="innerMorph">
{% include "pedagogy/fragments/ue_details/comments.jinja" %}
</hx-partial>
{% else %}
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from "core/macros.jinja" import user_profile_link %}
{% from "pedagogy/macros.jinja" import display_star %}
{% block additional_css %} {% block additional_css %}
<link rel="stylesheet" href="{{ static('pedagogy/css/pedagogy.scss') }}"> <link rel="stylesheet" href="{{ static('pedagogy/css/pedagogy.scss') }}">
@@ -26,7 +11,6 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="pedagogy"> <div class="pedagogy">
<div id="ue_detail"> <div id="ue_detail">
<button onclick='(function(){ <button onclick='(function(){
@@ -71,21 +55,166 @@
<br> <br>
<div id="ue-grade"> <div class="ue-details-container">
{% include "pedagogy/fragments/ue_details/grade.jinja" %} <div class="grade">
<p>{% trans %}Global grade{% endtrans %}</p>
<p>{% trans %}Utility{% endtrans %}</p>
<p>{% trans %}Interest{% endtrans %}</p>
<p>{% trans %}Teaching{% endtrans %}</p>
<p>{% trans %}Work load{% endtrans %}</p>
</div>
<div class="grade-stars">
<p>{{ display_star(object.grade_global_average) }}</p>
<p>{{ display_star(object.grade_utility_average) }}</p>
<p>{{ display_star(object.grade_interest_average) }}</p>
<p>{{ display_star(object.grade_teaching_average) }}</p>
<p>{{ display_star(object.grade_work_load_average) }}</p>
</div>
<div class="ue-infos">
<p><b>{% trans %}Objectives{% endtrans %}</b></p>
<p>{{ object.objectives|markdown }}</p>
<p><b>{% trans %}Program{% endtrans %}</b></p>
<p>{{ object.program|markdown }}</p>
<p><b>{% trans %}Earned skills{% endtrans %}</b></p>
<p>{{ object.skills|markdown }}</p>
<p><b>{% trans %}Key concepts{% endtrans %}</b></p>
<p>{{ object.key_concepts|markdown }}</p>
<p><b>{% trans %}UE manager: {% endtrans %}</b>{{ object.manager }}</p>
</div>
</div> </div>
<br> <br>
{% if object.has_user_already_commented(user) %}
<div id="leave_comment_not_allowed">
<p>{% trans %}You already posted a comment on this UE. If you want to comment again, please modify or delete your previous comment.{% endtrans %}</p>
</div>
{% elif user.has_perm("pedagogy.add_uecomment") %}
<details class="accordion" id="leave_comment" {% if form.errors %}open{%endif%}>
<summary>{% trans %}Leave comment{% endtrans %}</summary>
<div class="accordion-content">
<form action="{{ url('pedagogy:ue_detail', ue_id=object.id) }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="leave-comment-grid-container">
<div class="form-stars">
{{ form.non_field_errors() }}
{{ form.author.errors }}
{{ form.ue.errors }}
<div id="comment-form"> {{ form.author }}
{% include "pedagogy/fragments/ue_details/form.jinja" %} {{ form.ue }}
<div class="input-stars">
<label for="{{ form.grade_global.id_for_label }}">{{ form.grade_global.label }} :</label>
{{ form.grade_global.errors }}
{{ form.grade_global }}
</div> </div>
<div id="comments"> <div class="input-stars">
{% include "pedagogy/fragments/ue_details/comments.jinja" %} <label for="{{ form.grade_utility.id_for_label }}">{{ form.grade_utility.label }} :</label>
{{ form.grade_utility.errors }}
{{ form.grade_utility }}
</div> </div>
<div class="input-stars">
<label for="{{ form.grade_interest.id_for_label }}">{{ form.grade_interest.label }} :</label>
{{ form.grade_interest.errors }}
{{ form.grade_interest }}
</div>
<div class="input-stars">
<label for="{{ form.grade_teaching.id_for_label }}">{{ form.grade_teaching.label }} :</label>
{{ form.grade_teaching.errors }}
{{ form.grade_teaching }}
</div>
<div class="input-stars">
<label for="{{ form.grade_work_load.id_for_label }}">{{ form.grade_work_load.label }} :</label>
{{ form.grade_work_load.errors }}
{{ form.grade_work_load }}
</div>
</div>
<div class="form-comment">
<label for="{{ form.comment.id_for_label }}">{{ form.comment.label }} :</label>
{{ form.comment.errors }}
{{ form.comment }}
</div>
</div>
<p><input type="submit" value="{% trans %}Comment{% endtrans %}" /></p>
</form>
</div>
</details>
{% endif %}
<br>
{% if comments %}
<h2>{% trans %}Comments{% endtrans %}</h2>
{% for comment in comments %}
<div id="{{ comment.id }}" class="comment-container">
<div class="grade-block">
<div class="grade-type">
<p>{% trans %}Global grade{% endtrans %}</p>
<p>{% trans %}Utility{% endtrans %}</p>
<p>{% trans %}Interest{% endtrans %}</p>
<p>{% trans %}Teaching{% endtrans %}</p>
<p>{% trans %}Work load{% endtrans %}</p>
</div>
<div class="grade-stars">
<p>{{ display_star(comment.grade_global) }}</p>
<p>{{ display_star(comment.grade_utility) }}</p>
<p>{{ display_star(comment.grade_interest) }}</p>
<p>{{ display_star(comment.grade_teaching) }}</p>
<p>{{ display_star(comment.grade_work_load) }}</p>
</div>
<div class="grade-extension"></div>
</div>
<div class="comment">
<div class="anchor">
<a href="{{ url('pedagogy:ue_detail', ue_id=ue.id) }}#{{ comment.id }}"><i class="fa fa-paragraph"></i></a>
</div>
{{ comment.comment|markdown }}
</div>
<div class="info">
{% if comment.is_reported %}
<p class="status-reported">
{% trans %}This comment has been reported{% endtrans %}
</p>
{% endif %}
{% if comment.author_id == user.id or user.has_perm("pedagogy.change_comment") %}
<p class="actions">
<a href="{{ url('pedagogy:comment_update', comment_id=comment.id) }}">
{% trans %}Edit{% endtrans %}
</a>
{% endif %}
{% if comment.author_id == user.id or user.has_perm("pedagogy.delete_comment") %}
<a href="{{ url('pedagogy:comment_delete', comment_id=comment.id) }}">
{% trans %}Delete{% endtrans %}
</a>
</p>
{% endif %}
</div>
<div class="comment-end-bar">
<div class="report">
<p>
<a href="{{ url('pedagogy:comment_report', comment_id=comment.id) }}">
{% trans %}Report this comment{% endtrans %}
</a>
</p>
</div>
<div class="date"><p>{{ comment.publish_date.strftime('%d/%m/%Y') }}</p></div>
<div class="author"><p>{{ user_profile_link(comment.author) }}</p></div>
</div>
</div>
{% endfor %}
{% endif %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% endif %}
+2 -8
View File
@@ -24,13 +24,12 @@
from django.urls import path from django.urls import path
from pedagogy.views import ( from pedagogy.views import (
UECommentCreateView,
UECommentDeleteView, UECommentDeleteView,
UECommentReportCreateView, UECommentReportCreateView,
UECommentUpdateView, UECommentUpdateView,
UECreateView, UECreateView,
UEDeleteView, UEDeleteView,
UEDetailView, UEDetailFormView,
UEGuideView, UEGuideView,
UEModerationFormView, UEModerationFormView,
UEUpdateView, UEUpdateView,
@@ -39,12 +38,7 @@ from pedagogy.views import (
urlpatterns = [ urlpatterns = [
# Urls displaying the actual application for visitors # Urls displaying the actual application for visitors
path("", UEGuideView.as_view(), name="guide"), path("", UEGuideView.as_view(), name="guide"),
path("ue/<int:ue_id>/", UEDetailView.as_view(), name="ue_detail"), path("ue/<int:ue_id>/", UEDetailFormView.as_view(), name="ue_detail"),
path(
"ue/<int:ue_id>/comment",
UECommentCreateView.as_view(),
name="comment_create",
),
path( path(
"comment/<int:comment_id>/edit/", "comment/<int:comment_id>/edit/",
UECommentUpdateView.as_view(), UECommentUpdateView.as_view(),
+36 -73
View File
@@ -16,7 +16,7 @@
# details. # details.
# #
# You should have received a copy of the GNU General Public License along with # You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 59 Temple # this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA. # Place - Suite 330, Boston, MA 02111-1307, USA.
# #
# #
@@ -26,11 +26,9 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Exists, OuterRef from django.db.models import Exists, OuterRef
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.functional import cached_property
from django.views.generic import ( from django.views.generic import (
CreateView, CreateView,
DeleteView, DeleteView,
DetailView,
FormView, FormView,
TemplateView, TemplateView,
UpdateView, UpdateView,
@@ -38,7 +36,7 @@ from django.views.generic import (
from core.auth.mixins import PermissionOrAuthorRequiredMixin from core.auth.mixins import PermissionOrAuthorRequiredMixin
from core.models import Notification, User from core.models import Notification, User
from core.views.mixins import AllowFragment, FragmentMixin, UseFragmentsMixin from core.views import DetailFormView
from pedagogy.forms import ( from pedagogy.forms import (
UECommentForm, UECommentForm,
UECommentModerationForm, UECommentModerationForm,
@@ -48,56 +46,37 @@ from pedagogy.forms import (
from pedagogy.models import UE, UEComment, UECommentReport from pedagogy.models import UE, UEComment, UECommentReport
class UECommentCreateView(PermissionRequiredMixin, FragmentMixin, CreateView): class UEDetailFormView(PermissionRequiredMixin, DetailFormView):
model = UEComment """Display every comment of an UE and detailed infos about it.
template_name = "pedagogy/fragments/ue_comment_form.jinja"
form_class = UECommentForm
permission_required = "pedagogy.add_uecomment"
object = None # Avoid initialisation bug with FragmentMixin
@cached_property Allow to comment the UE.
def ue(self): """
return get_object_or_404(UE, id=self.kwargs.get("ue_id"))
model = UE
pk_url_kwarg = "ue_id"
template_name = "pedagogy/ue_detail.jinja"
form_class = UECommentForm
permission_required = "pedagogy.view_ue"
def has_permission(self): def has_permission(self):
if self.ue.has_user_already_commented(self.request.user): if self.request.method == "POST" and not self.request.user.has_perm(
"pedagogy.add_uecomment"
):
# if it's a POST request, the user is trying to add a new UEComment
# thus he also needs the "add_uecomment" permission
return False return False
return super().has_permission() return super().has_permission()
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs["author_id"] = self.request.user.id kwargs["author_id"] = self.request.user.id
kwargs["ue_id"] = self.ue.id kwargs["ue_id"] = self.object.id
kwargs["is_creation"] = True kwargs["is_creation"] = True
return kwargs return kwargs
def get_context_data(self, **kwargs): def form_valid(self, form):
return super().get_context_data(**kwargs) | { form.save()
"action": reverse("pedagogy:comment_create", kwargs={"ue_id": self.ue.id}), return super().form_valid(form)
"object": self.ue,
}
def get_success_url(self):
return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.ue.id})
class UEDetailView(
PermissionRequiredMixin, UseFragmentsMixin, AllowFragment, DetailView
):
"""Display every comment of an UE and detailed infos about it."""
model = UE
pk_url_kwarg = "ue_id"
template_name = "pedagogy/ue_detail.jinja"
permission_required = "pedagogy.view_ue"
fragments = {
"add_comment_form": UECommentCreateView,
}
def get_fragment_data(self):
return {
"add_comment_form": {"ue_id": self.object.id},
}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | { return super().get_context_data(**kwargs) | {
@@ -106,17 +85,22 @@ class UEDetailView(
.annotate_is_reported() .annotate_is_reported()
.select_related("author") .select_related("author")
.order_by("-publish_date") .order_by("-publish_date")
), )
} }
def get_success_url(self):
# once the new ue comment has been saved
# redirect to the same page we are currently
return self.request.path
class UECommentUpdateView(PermissionOrAuthorRequiredMixin, AllowFragment, UpdateView):
class UECommentUpdateView(PermissionOrAuthorRequiredMixin, UpdateView):
"""Allow edit of a given comment.""" """Allow edit of a given comment."""
model = UEComment model = UEComment
form_class = UECommentForm form_class = UECommentForm
pk_url_kwarg = "comment_id" pk_url_kwarg = "comment_id"
template_name = "pedagogy/fragments/ue_comment_form.jinja" template_name = "core/edit.jinja"
permission_required = "pedagogy.change_uecomment" permission_required = "pedagogy.change_uecomment"
author_field = "author" author_field = "author"
@@ -127,18 +111,11 @@ class UECommentUpdateView(PermissionOrAuthorRequiredMixin, AllowFragment, Update
kwargs["is_creation"] = False kwargs["is_creation"] = False
return kwargs return kwargs
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"action": reverse(
"pedagogy:comment_update", kwargs={"comment_id": self.object.id}
)
}
def get_success_url(self): def get_success_url(self):
return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.object.ue.id}) return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.object.ue_id})
class UECommentDeleteView(PermissionOrAuthorRequiredMixin, AllowFragment, DeleteView): class UECommentDeleteView(PermissionOrAuthorRequiredMixin, DeleteView):
"""Allow to delete a given comment.""" """Allow to delete a given comment."""
model = UEComment model = UEComment
@@ -147,13 +124,8 @@ class UECommentDeleteView(PermissionOrAuthorRequiredMixin, AllowFragment, Delete
permission_required = "pedagogy.delete_uecomment" permission_required = "pedagogy.delete_uecomment"
author_field = "author" author_field = "author"
def form_valid(self, form):
response = super().form_valid(form)
response.headers["HX-Trigger"] = "CommentUpdate"
return response
def get_success_url(self): def get_success_url(self):
return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.object.ue.id}) return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.object.ue_id})
class UEGuideView(PermissionRequiredMixin, TemplateView): class UEGuideView(PermissionRequiredMixin, TemplateView):
@@ -163,34 +135,24 @@ class UEGuideView(PermissionRequiredMixin, TemplateView):
permission_required = "pedagogy.view_ue" permission_required = "pedagogy.view_ue"
class UECommentReportCreateView(PermissionRequiredMixin, AllowFragment, CreateView): class UECommentReportCreateView(PermissionRequiredMixin, CreateView):
"""Create a new report for an inappropriate comment.""" """Create a new report for an inappropriate comment."""
model = UECommentReport model = UECommentReport
form_class = UECommentReportForm form_class = UECommentReportForm
template_name = "pedagogy/fragments/comment_report.jinja" template_name = "core/edit.jinja"
permission_required = "pedagogy.add_uecommentreport" permission_required = "pedagogy.add_uecommentreport"
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.ue_comment = get_object_or_404(UEComment, pk=kwargs["comment_id"]) self.ue_comment = get_object_or_404(UEComment, pk=kwargs["comment_id"])
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
@cached_property
def ue_detail_url(self):
return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.ue_comment.ue.id})
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs["reporter_id"] = self.request.user.id kwargs["reporter_id"] = self.request.user.id
kwargs["comment_id"] = self.ue_comment.id kwargs["comment_id"] = self.ue_comment.id
return kwargs return kwargs
def get_context_data(self, **kwargs):
return super().get_context_data() | {
"comment_id": self.ue_comment.id,
"ue_detail_url": self.ue_detail_url,
}
def form_valid(self, form): def form_valid(self, form):
resp = super().form_valid(form) resp = super().form_valid(form)
# Send a message to moderation admins # Send a message to moderation admins
@@ -206,10 +168,11 @@ class UECommentReportCreateView(PermissionRequiredMixin, AllowFragment, CreateVi
url=reverse("pedagogy:moderation"), url=reverse("pedagogy:moderation"),
type="PEDAGOGY_MODERATION", type="PEDAGOGY_MODERATION",
) )
return resp return resp
def get_success_url(self): def get_success_url(self):
return self.ue_detail_url return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.ue_comment.ue_id})
class UEModerationFormView(PermissionRequiredMixin, FormView): class UEModerationFormView(PermissionRequiredMixin, FormView):
+3 -3
View File
@@ -92,11 +92,11 @@ docs = [
default-groups = ["dev", "tests", "docs"] default-groups = ["dev", "tests", "docs"]
[tool.xapian] [tool.xapian]
version = "2.1.0" version = "2.0.0"
# Those hashes are here to protect against supply chains attacks # Those hashes are here to protect against supply chains attacks
# See `https://ae-utbm.github.io/sith/howto/xapian/` for more information # See `https://ae-utbm.github.io/sith/howto/xapian/` for more information
core-sha256 = "8e1259586d342e3d12b5e1f772e9185a10f2ba16e541566b5c3c239f71b8aacc" core-sha256 = "6cea3f49952a47224439a40bdb3608f928d121ad8721b9921cc42802d548ecf8"
bindings-sha256 = "f52ec189f13b4fa66ea625a6eb94bb32dd651b9ec806be6a911dda54cbe3875c" bindings-sha256 = "9a544b69c31355a92edbcd4102cf0f1ec4407fd0a4645f4870fb52300b736910"
[tool.ruff] [tool.ruff]
output-format = "concise" # makes ruff error logs easier to read output-format = "concise" # makes ruff error logs easier to read