Merge pull request #1460 from ae-utbm/pedagogy

Fix star width and rewrite star widget with nice hovering
This commit is contained in:
2026-08-23 18:00:45 +02:00
committed by GitHub
7 changed files with 144 additions and 73 deletions
+5 -1
View File
@@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-06-10 20:18+0200\n" "POT-Creation-Date: 2026-08-21 14:10+0200\n"
"PO-Revision-Date: 2016-07-18\n" "PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n" "Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n" "Language-Team: AE info <ae.info@utbm.fr>\n"
@@ -4969,6 +4969,10 @@ msgstr "Ne pas voter"
msgid "This user has already commented on this UE" msgid "This user has already commented on this UE"
msgstr "Cet utilisateur a déjà commenté cette UE" msgstr "Cet utilisateur a déjà commenté cette UE"
#: pedagogy/forms.py
msgid "UE comment can't be empty."
msgstr "Un commentaire d'UE ne peut pas être vide."
#: pedagogy/forms.py #: pedagogy/forms.py
msgid "Accepted reports" msgid "Accepted reports"
msgstr "Signalements acceptés" msgstr "Signalements acceptés"
+29 -10
View File
@@ -22,6 +22,8 @@
# #
from django import forms from django import forms
from django.contrib.staticfiles.storage import staticfiles_storage
from django.forms.fields import ValidationError
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from core.models import User from core.models import User
@@ -30,7 +32,7 @@ from pedagogy.models import UE, UEComment, UECommentReport
class UEForm(forms.ModelForm): class UEForm(forms.ModelForm):
"""Form handeling creation and edit of an UE.""" """Form handling creation and edit of an UE."""
class Meta: class Meta:
model = UE model = UE
@@ -68,22 +70,28 @@ class UEForm(forms.ModelForm):
self.fields["author"].initial = author_id self.fields["author"].initial = author_id
class StarList(forms.NumberInput): class StarList(forms.RadioSelect):
template_name = "pedagogy/starlist.jinja" template_name = "pedagogy/starlist.jinja"
def __init__(self, nubmer_of_stars=0): def __init__(self, number_of_stars=0, attrs=None):
super().__init__(None) super().__init__(
self.number_of_stars = nubmer_of_stars attrs=attrs,
choices=(
(choice, _("Do not vote") if choice == -1 else choice)
for choice in range(-1, number_of_stars)
),
)
def get_context(self, name, value, attrs): def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs) context = super().get_context(name, value, attrs)
context["number_of_stars"] = range(0, self.number_of_stars) context["statics"] = {
context["translations"] = {"do_not_vote": _("Do not vote")} "css": staticfiles_storage.url("pedagogy/css/starlist.scss"),
}
return context return context
class UECommentForm(forms.ModelForm): class UECommentForm(forms.ModelForm):
"""Form handeling creation and edit of an UEComment.""" """Form handling creation and edit of an UEComment."""
class Meta: class Meta:
model = UEComment model = UEComment
@@ -129,11 +137,22 @@ class UECommentForm(forms.ModelForm):
), ),
) )
# Ensure that at least one value is exists
if (
all(
grade == -1
for key, grade in self.cleaned_data.items()
if key.startswith("grade_")
)
and not self.cleaned_data["comment"]
):
raise ValidationError(message=_("UE comment can't be empty."))
return self.cleaned_data return self.cleaned_data
class UECommentReportForm(forms.ModelForm): class UECommentReportForm(forms.ModelForm):
"""Form handeling creation and edit of an UEReport.""" """Form handling creation and edit of an UEReport."""
class Meta: class Meta:
model = UECommentReport model = UECommentReport
@@ -153,7 +172,7 @@ class UECommentReportForm(forms.ModelForm):
class UECommentModerationForm(forms.Form): class UECommentModerationForm(forms.Form):
"""Form handeling bulk comment deletion.""" """Form handling bulk comment deletion."""
accepted_reports = forms.ModelMultipleChoiceField( accepted_reports = forms.ModelMultipleChoiceField(
UECommentReport.objects.all(), UECommentReport.objects.all(),
+2 -5
View File
@@ -1,3 +1,4 @@
@import "core/static/core/devices";
@import "core/static/core/colors"; @import "core/static/core/colors";
@@ -7,10 +8,6 @@ $pedagogy-hover-blue: #0e97ce;
$pedagogy-light-blue: #caf0ff; $pedagogy-light-blue: #caf0ff;
$pedagogy-white-text: #f0f0f0; $pedagogy-white-text: #f0f0f0;
$small-devices: 576px;
$medium-devices: 768px;
$large-devices: 992px;
.pedagogy { .pedagogy {
&.star-not-checked { &.star-not-checked {
color: #f7f7f7; color: #f7f7f7;
@@ -256,7 +253,7 @@ $large-devices: 992px;
.ue-details-container { .ue-details-container {
display: grid; display: grid;
grid-template-columns: 150px 100px auto; grid-template-columns: 150px 130px auto;
grid-template-rows: 156px 1fr; grid-template-rows: 156px 1fr;
grid-template-areas: grid-template-areas:
"grade grade-stars ue-infos" "grade grade-stars ue-infos"
@@ -0,0 +1,38 @@
fieldset.star {
border: none;
label {
display: inline;
cursor: pointer;
}
.checked {
color: orange;
}
.unchecked {
color: gray;
}
.unchecked.hovered {
color: #FFD700;
}
.removed {
color: red;
}
input[type="radio"] {
display: none;
}
label:first-child {
margin-right: 10px;
}
&:has(input:required:invalid) {
border: 1px solid;
border-color: #c00000;
}
}
+46 -52
View File
@@ -1,58 +1,52 @@
<div> <link-once rel="stylesheet" type="text/css" href="{{ statics.css }}" defer></link-once>
<style> <fieldset
.checked { class="star"
color : orange; x-data="{ 'value': {{ widget.value.0 }}, 'hover': -1 }"
} @mouseover.outside="hover = -1"
.unchecked { {% include "django/forms/widgets/attrs.html" %}
color : gray; >
}
.star input[type="radio"] {
display : none;
}
.star {
display: inline;
}
</style>
{# Do not vote button #} {# Do not vote button #}
<label class="star"> <label
<input type="radio" name="{{ widget.name }}" value="-1" onclick=' @mouseover="hover = -1"
var stars = document.getElementsByClassName("{{ widget.name }}"); for="{{ widget.optgroups.0.1.0.attrs.id }}"
for (var i = 0; i < stars.length; i++){ >
var attrs = stars[i].getAttribute("class"); <input
attrs = attrs.replace("unchecked", ""); type="radio"
attrs = attrs.replace("checked", ""); name="{{ widget.name }}"
stars[i].setAttribute("class", attrs + " unchecked"); value="{{ widget.optgroups.0.1.0.value }}"
} @click="value = -1"
' checked> {% with widget=widget.optgroups.0.1.0 %}
<span class="fa fa-times-circle"> {{ translations.do_not_vote }}</span> {% include "django/forms/widgets/attrs.html" %}
{% endwith %}
>
<i class="fa fa-times-circle"></i>
{{ widget.optgroups.0.1.0.label }}
</label> </label>
{# Star widget #} {# Star widget #}
{% for i in number_of_stars %} {% for opt in widget.optgroups|slice:"1:" %}
<label class="star"> <label
<input type="radio" name="{{ widget.name }}" value="{{ forloop.counter0 }}" onclick=' @mouseover="hover = {{ opt.1.0.value }}"
var stars = document.getElementsByClassName("{{ widget.name }}"); for="{{ opt.1.0.attrs.id }}"
>
for (var i = 0; i < stars.length; i++){ <input
var attrs = stars[i].getAttribute("class"); type="radio"
attrs = attrs.replace("unchecked", ""); name="{{ widget.name }}"
attrs = attrs.replace("checked", ""); value="{{ opt.1.0.value }}"
if (i > {{ forloop.counter0 }}){ @click="value = {{ opt.1.0.value }}"
stars[i].setAttribute("class", attrs + " unchecked"); {% with widget=opt.1.0 %}
} else { {% include "django/forms/widgets/attrs.html" %}
stars[i].setAttribute("class", attrs + " checked"); {% endwith %}
} >
} <i
'> class="{{ widget.name }} fa fa-star"
<i class="{{ widget.name }} fa fa-star unchecked"></i> :class="{
'checked': value >= {{ opt.1.0.value }},
'unchecked': value < {{ opt.1.0.value }},
'hovered': hover >= {{ opt.1.0.value }},
'removed': hover >= 0 && value >= {{ opt.1.0.value }} && hover < {{ opt.1.0.value }},
}"
></i>
</label> </label>
{% endfor %} {% endfor %}
</fieldset>
{# Restaure previous (-1 is default) #}
<script type="text/javascript">
document.querySelector("input[name='{{ widget.name }}'][value='{{ widget.value }}']").click()
</script>
</div>
+2 -1
View File
@@ -89,13 +89,14 @@
<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> <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> </div>
{% elif user.has_perm("pedagogy.add_uecomment") %} {% elif user.has_perm("pedagogy.add_uecomment") %}
<details class="accordion" id="leave_comment"> <details class="accordion" id="leave_comment" {% if form.errors %}open{%endif%}>
<summary>{% trans %}Leave comment{% endtrans %}</summary> <summary>{% trans %}Leave comment{% endtrans %}</summary>
<div class="accordion-content"> <div class="accordion-content">
<form action="{{ url('pedagogy:ue_detail', ue_id=object.id) }}" method="post" enctype="multipart/form-data"> <form action="{{ url('pedagogy:ue_detail', ue_id=object.id) }}" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<div class="leave-comment-grid-container"> <div class="leave-comment-grid-container">
<div class="form-stars"> <div class="form-stars">
{{ form.non_field_errors() }}
{{ form.author.errors }} {{ form.author.errors }}
{{ form.ue.errors }} {{ form.ue.errors }}
+18
View File
@@ -339,6 +339,24 @@ class TestUVCommentCreationAndDisplay(TestCase):
response = self.client.get(self.ue_url) response = self.client.get(self.ue_url)
self.assertContains(response, text="Superbe UE") self.assertContains(response, text="Superbe UE")
def test_create_ue_empty_comment_fail(self):
self.client.force_login(self.tutu)
response = self.client.post(
self.ue_url,
{
"author": self.tutu.id,
"ue": UE.objects.get(code="PA00").id,
"grade_global": -1,
"grade_utility": -1,
"grade_interest": -1,
"grade_teaching": -1,
"grade_work_load": -1,
"comment": "",
},
)
assert response.status_code == 200
self.assertInHTML("Un commentaire d'UE ne peut pas être vide.", response.text)
def test_create_ue_comment_unauthorized_fail(self): def test_create_ue_comment_unauthorized_fail(self):
nb_comments = self.ue.comments.count() nb_comments = self.ue.comments.count()
# Test with anonymous user # Test with anonymous user