Compare commits

..
Author SHA1 Message Date
klmp200 01c546fd0a Alternative queryset 2026-09-09 20:44:14 +02:00
klmp200 a17f9241f4 Fix ue comment view permission 2026-09-09 14:35:44 +02:00
klmp200 d47e1f394b Use HTMX in UE details 2026-09-09 13:24:54 +02:00
20 changed files with 494 additions and 342 deletions
+6 -11
View File
@@ -15,6 +15,9 @@
{% 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 %}
@@ -23,12 +26,6 @@
{%- 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 %}
@@ -36,17 +33,15 @@
<h3>{% trans %}Filters{% endtrans %}</h3> <h3>{% trans %}Filters{% endtrans %}</h3>
<form <form
id="club-list-filters" id="club-list-filters"
method="GET" hx-get="{{ url("club:club_list") }}"
hx-action="{{ url("club:club_list") }}"
hx-target="#content" hx-target="#content"
hx-swap="innerHTML" hx-swap="outerHtml"
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 class="btn btn-blue margin-bottom"> <button type="submit" 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>
+1
View File
@@ -70,6 +70,7 @@ htmx.registerExtension("aria-busy", {
}); });
htmx.config.transitions = true; htmx.config.transitions = true;
Object.assign(window, { htmx }); Object.assign(window, { htmx });
/** /**
-2
View File
@@ -14,8 +14,6 @@
{%- endblock %}" {%- endblock %}"
> >
<meta property="og:site_name" content="Association des Étudiants de l'UTBM" /> <meta property="og:site_name" content="Association des Étudiants de l'UTBM" />
<meta property="og:locale" content="fr_FR" />
<meta property="og:locale:alternate" content="en_GB" />
{% block metatags %} {% block metatags %}
<meta property="og:url" content="{{ request.build_absolute_uri() }}" /> <meta property="og:url" content="{{ request.build_absolute_uri() }}" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
+31 -29
View File
@@ -4,11 +4,15 @@
{% trans %}Delete confirmation{% endtrans %} {% trans %}Delete confirmation{% endtrans %}
{% endblock %} {% endblock %}
{# Don't display tabs and errors #} {% if is_fragment %}
{% block tabs %}
{% endblock %} {# Don't display tabs and errors #}
{% block errors %} {% block tabs %}
{% endblock %} {% endblock %}
{% block errors %}
{% endblock %}
{% endif %}
{% block file %} {% block file %}
<h2>{% trans %}Delete confirmation{% endtrans %}</h2> <h2>{% trans %}Delete confirmation{% endtrans %}</h2>
@@ -19,32 +23,30 @@
{% set action = current %} {% set action = current %}
{% endif %} {% endif %}
<p>{% trans obj=object %}Are you sure you want to delete "{{ obj }}"?{% endtrans %}</p> <form action="{{ action }}" method="post">
<form
method="post"
{% if is_fragment %}
hx-action="{{ action }}"
hx-target="#content"
hx-swap="innerHTML"
{% else %}
action="{{ action }}"
{% endif %}
>
{% csrf_token %} {% csrf_token %}
<input type="submit" value="{% trans %}Confirm{% endtrans %}" />
</form> <p>{% trans obj=object %}Are you sure you want to delete "{{ obj }}"?{% endtrans %}</p>
<form <button
method="get" {% if is_fragment %}
{% if is_fragment %} hx-post="{{ action }}"
hx-action="{{ previous }}" hx-target="#content"
hx-target="#content" hx-swap="outerHtml"
hx-swap="innerHTML" {% endif %}
{% else %} >{% trans %}Confirm{% endtrans %}</button>
action="javascript:history.back();"
{% endif %} <button
> {% if is_fragment %}
<input type="submit" name="cancel" value="{% trans %}Cancel{% endtrans %}" /> hx-get="{{ previous }}"
hx-target="#content"
hx-swap="outerHtml"
{% else %}
action="window.history.back()"
{% endif %}
>{% trans %}Cancel{% endtrans %}</button>
</form> </form>
{% endblock %} {% endblock %}
+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="innerHTML" hx-swap="outerHtml"
>{% 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="innerHTML" hx-swap="outerHtml"
>{% trans %}Delete{% endtrans %}</button></p> >{% trans %}Delete{% endtrans %}</button></p>
</div> </div>
{% endfor %} {% endfor %}
+22 -6
View File
@@ -21,7 +21,7 @@
# Place - Suite 330, Boston, MA 02111-1307, USA. # Place - Suite 330, Boston, MA 02111-1307, USA.
# #
# #
from django.urls import path, register_converter from django.urls import path, re_path, register_converter
from django.views.generic import RedirectView from django.views.generic import RedirectView
from com.views import NewsListView from com.views import NewsListView
@@ -193,11 +193,27 @@ urlpatterns = [
name="user_gift_delete", name="user_gift_delete",
), ),
# File views # File views
path("file/", FileListView.as_view(), name="file_list"), re_path(r"^file/$", FileListView.as_view(), name="file_list"),
path("file/<int:file_id>/", FileView.as_view(), name="file_detail"), re_path(
path("file/<int:file_id>/edit/", FileEditView.as_view(), name="file_edit"), r"^file/(?P<file_id>[0-9]+)/$",
path("file/<int:file_id>/prop/", FileEditPropView.as_view(), name="file_prop"), FileView.as_view(),
path("file/<int:file_id>/delete/", FileDeleteView.as_view(), name="file_delete"), name="file_detail",
),
re_path(
r"^file/(?P<file_id>[0-9]+)/edit/$",
FileEditView.as_view(),
name="file_edit",
),
re_path(
r"^file/(?P<file_id>[0-9]+)/prop/$",
FileEditPropView.as_view(),
name="file_prop",
),
re_path(
r"^file/(?P<file_id>[0-9]+)/delete/$",
FileDeleteView.as_view(),
name="file_delete",
),
path("file/moderation/", FileModerationView.as_view(), name="file_moderation"), path("file/moderation/", FileModerationView.as_view(), name="file_moderation"),
path( path(
"file/<int:file_id>/moderate/", FileModerateView.as_view(), name="file_moderate" "file/<int:file_id>/moderate/", FileModerateView.as_view(), name="file_moderate"
+9 -2
View File
@@ -356,8 +356,15 @@ class FileDeleteView(AllowFragment, CanEditPropMixin, DeleteView):
if "next" in self.request.GET: if "next" in self.request.GET:
return self.request.GET["next"] return self.request.GET["next"]
if self.object.parent is None: if self.object.parent is None:
return reverse("core:file_list") return reverse(
return reverse("core:file_detail", kwargs={"file_id": self.object.parent.id}) "core:file_list",
)
return reverse(
"core:file_detail",
kwargs={
"file_id": self.object.parent.id,
},
)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
+2 -12
View File
@@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-09 07:21+0200\n" "POT-Creation-Date: 2026-09-08 23:36+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"
@@ -5852,15 +5852,9 @@ msgstr "les groupes pouvant créer des cotisations"
msgid "Timetable generator" msgid "Timetable generator"
msgstr "Générateur d'emploi du temps" msgstr "Générateur d'emploi du temps"
#: timetable/templates/timetable/generator.jinja
msgid "Get a nice schedule from the raw text in your student file"
msgstr ""
"Générez un joli emploi du temps à partir du texte brut de votre dossier "
"étudiant"
#: timetable/templates/timetable/generator.jinja #: timetable/templates/timetable/generator.jinja
msgid "" msgid ""
"Wrong timetable format. Make sure you copied it from your student folder." "Wrong timetable format. Make sure you copied if from your student folder."
msgstr "" msgstr ""
"Mauvais format d'emploi du temps. Assurez-vous que vous l'avez copié depuis " "Mauvais format d'emploi du temps. Assurez-vous que vous l'avez copié depuis "
"votre dossier étudiant." "votre dossier étudiant."
@@ -5869,10 +5863,6 @@ msgstr ""
msgid "Incorrect row" msgid "Incorrect row"
msgstr "Ligne incorrecte" msgstr "Ligne incorrecte"
#: timetable/templates/timetable/generator.jinja
msgid "Paste your schedule (without the header)"
msgstr "Colle ton emploi du temps (sans l'entête)"
#: timetable/templates/timetable/generator.jinja #: timetable/templates/timetable/generator.jinja
msgid "Generate" msgid "Generate"
msgstr "Générer" msgstr "Générer"
+18 -3
View File
@@ -26,6 +26,7 @@ from django.conf import settings
from django.core import validators from django.core import validators
from django.db import models from django.db import models
from django.db.models import Exists, OuterRef from django.db.models import Exists, OuterRef
from django.db.models.query_utils import Q
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
@@ -125,7 +126,11 @@ 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.
""" """
return self.comments.filter(author=user).exists() self._has_user_commented = getattr(self, "_has_user_commented", {})
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):
@@ -155,7 +160,17 @@ class UECommentQuerySet(models.QuerySet):
# so he can view non-moderated comments # so he can view non-moderated comments
return self return self
if user.has_perm("pedagogy.view_uecomment"): if user.has_perm("pedagogy.view_uecomment"):
return self.filter(reports=None) return self.filter(
Q(reports=None)
| Q(author=user)
| Q(
Exists(
UECommentReport.objects.filter(
comment=OuterRef("pk"), reporter=user
)
)
)
)
return self.filter(author=user) return self.filter(author=user)
def annotate_is_reported(self) -> Self: def annotate_is_reported(self) -> Self:
@@ -257,7 +272,7 @@ class UEResult(models.Model):
class UECommentReport(models.Model): class UECommentReport(models.Model):
"""Report an inapropriate comment.""" """Report an inappropriate comment."""
comment = models.ForeignKey( comment = models.ForeignKey(
UEComment, UEComment,
+12 -7
View File
@@ -69,6 +69,7 @@ $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;
@@ -128,14 +129,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;
@@ -168,10 +169,6 @@ $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 {
@@ -333,8 +330,9 @@ $pedagogy-white-text: #f0f0f0;
padding-left: 10px; padding-left: 10px;
} }
.actions { .action {
float: right; float: right;
margin-top: 0;
} }
} }
@@ -402,6 +400,7 @@ $pedagogy-white-text: #f0f0f0;
a { a {
color: $pedagogy-white-text; color: $pedagogy-white-text;
cursor: pointer;
} }
a:hover { a:hover {
@@ -444,3 +443,9 @@ 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;
}
@@ -0,0 +1,31 @@
<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>
@@ -0,0 +1,71 @@
<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>
@@ -0,0 +1,91 @@
{% 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>
@@ -0,0 +1,10 @@
{% 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 %}
@@ -0,0 +1,27 @@
<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>
+76 -205
View File
@@ -1,220 +1,91 @@
{% extends "core/base.jinja" %}
{% from "core/macros.jinja" import user_profile_link %}
{% from "pedagogy/macros.jinja" import display_star %} {% from "pedagogy/macros.jinja" import display_star %}
{% block additional_css %} {% if is_fragment %}
<link rel="stylesheet" href="{{ static('pedagogy/css/pedagogy.scss') }}">
{% endblock %}
{% block title %} <hx-partial id="ue-grade">
{% trans %}UE Details{% endtrans %} {% include "pedagogy/fragments/ue_details/grade.jinja" %}
{% endblock %} </hx-partial>
{% block content %} <hx-partial id="comment-form">
<div class="pedagogy"> {% include "pedagogy/fragments/ue_details/form.jinja" %}
<div id="ue_detail"> </hx-partial>
<button onclick='(function(){
// If comes from the guide page, go back with history
if (document.referrer.replace(/\?(.+)/gm,"").endsWith(`{{ url("pedagogy:guide") }}`)){
window.history.back();
return;
}
// Simply goes to the guide page
window.location.href = `{{ url("pedagogy:guide") }}`;
})()' hidden>{% trans %}Back{% endtrans %}</button>
<h1>{{ object.code }} - {{ object.title }}</h1> <hx-partial id="comments" hx-swap="innerMorph">
<br> {% include "pedagogy/fragments/ue_details/comments.jinja" %}
<div class="ue-quick-info-container"> </hx-partial>
<div class="hours-cm">
<b>{% trans %}CM: {% endtrans %}</b>{{ object.hours_CM }}
</div>
<div class="hours-td">
<b>{% trans %}TD: {% endtrans %}</b>{{ object.hours_TD }}
</div>
<div class="hours-tp">
<b>{% trans %}TP: {% endtrans %}</b>{{ object.hours_TP }}
</div>
<div class="hours-te">
<b>{% trans %}TE: {% endtrans %}</b>{{ object.hours_TE }}
</div>
<div class="hours-the">
<b>{% trans %}THE: {% endtrans %}</b>{{ object.hours_THE }}
</div>
<div class="department"> {% else %}
{{ object.department }} {% extends "core/base.jinja" %}
</div>
<div class="credit-type">
{{ object.credit_type }}
</div>
<div class="semester">
{{ object.get_semester_display() }}
</div>
</div>
<br> {% block additional_css %}
<link rel="stylesheet" href="{{ static('pedagogy/css/pedagogy.scss') }}">
{% endblock %}
<div class="ue-details-container"> {% block title %}
<div class="grade"> {% trans %}UE Details{% endtrans %}
<p>{% trans %}Global grade{% endtrans %}</p> {% endblock %}
<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>
<br> {% block content %}
{% 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 }}
{{ form.author }} <div class="pedagogy">
{{ form.ue }} <div id="ue_detail">
<button onclick='(function(){
// If comes from the guide page, go back with history
if (document.referrer.replace(/\?(.+)/gm,"").endsWith(`{{ url("pedagogy:guide") }}`)){
window.history.back();
return;
}
// Simply goes to the guide page
window.location.href = `{{ url("pedagogy:guide") }}`;
})()' hidden>{% trans %}Back{% endtrans %}</button>
<div class="input-stars"> <h1>{{ object.code }} - {{ object.title }}</h1>
<label for="{{ form.grade_global.id_for_label }}">{{ form.grade_global.label }} :</label> <br>
{{ form.grade_global.errors }} <div class="ue-quick-info-container">
{{ form.grade_global }} <div class="hours-cm">
</div> <b>{% trans %}CM: {% endtrans %}</b>{{ object.hours_CM }}
<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>
<p><input type="submit" value="{% trans %}Comment{% endtrans %}" /></p>
</form>
</div> </div>
</details> <div class="hours-td">
{% endif %} <b>{% trans %}TD: {% endtrans %}</b>{{ object.hours_TD }}
<br> </div>
<div class="hours-tp">
{% if comments %} <b>{% trans %}TP: {% endtrans %}</b>{{ object.hours_TP }}
<h2>{% trans %}Comments{% endtrans %}</h2> </div>
{% for comment in comments %} <div class="hours-te">
<div id="{{ comment.id }}" class="comment-container"> <b>{% trans %}TE: {% endtrans %}</b>{{ object.hours_TE }}
</div>
<div class="grade-block"> <div class="hours-the">
<div class="grade-type"> <b>{% trans %}THE: {% endtrans %}</b>{{ object.hours_THE }}
<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> </div>
{% endfor %}
{% endif %}
<div class="department">
{{ object.department }}
</div>
<div class="credit-type">
{{ object.credit_type }}
</div>
<div class="semester">
{{ object.get_semester_display() }}
</div>
</div>
<br>
<div id="ue-grade">
{% include "pedagogy/fragments/ue_details/grade.jinja" %}
</div>
<br>
<div id="comment-form">
{% include "pedagogy/fragments/ue_details/form.jinja" %}
</div>
<div id="comments">
{% include "pedagogy/fragments/ue_details/comments.jinja" %}
</div>
</div>
</div> </div>
</div> {% endblock %}
{% endblock %} {% endif %}
+8 -2
View File
@@ -24,12 +24,13 @@
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,
UEDetailFormView, UEDetailView,
UEGuideView, UEGuideView,
UEModerationFormView, UEModerationFormView,
UEUpdateView, UEUpdateView,
@@ -38,7 +39,12 @@ 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>/", UEDetailFormView.as_view(), name="ue_detail"), path("ue/<int:ue_id>/", UEDetailView.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(),
+73 -36
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 Sofware Foundation, Inc., 59 Temple # this program; if not, write to the Free Software Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA. # Place - Suite 330, Boston, MA 02111-1307, USA.
# #
# #
@@ -26,9 +26,11 @@ 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,
@@ -36,7 +38,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 import DetailFormView from core.views.mixins import AllowFragment, FragmentMixin, UseFragmentsMixin
from pedagogy.forms import ( from pedagogy.forms import (
UECommentForm, UECommentForm,
UECommentModerationForm, UECommentModerationForm,
@@ -46,37 +48,56 @@ from pedagogy.forms import (
from pedagogy.models import UE, UEComment, UECommentReport from pedagogy.models import UE, UEComment, UECommentReport
class UEDetailFormView(PermissionRequiredMixin, DetailFormView): class UECommentCreateView(PermissionRequiredMixin, FragmentMixin, CreateView):
"""Display every comment of an UE and detailed infos about it. model = UEComment
template_name = "pedagogy/fragments/ue_comment_form.jinja"
Allow to comment the UE.
"""
model = UE
pk_url_kwarg = "ue_id"
template_name = "pedagogy/ue_detail.jinja"
form_class = UECommentForm form_class = UECommentForm
permission_required = "pedagogy.view_ue" permission_required = "pedagogy.add_uecomment"
object = None # Avoid initialisation bug with FragmentMixin
@cached_property
def ue(self):
return get_object_or_404(UE, id=self.kwargs.get("ue_id"))
def has_permission(self): def has_permission(self):
if self.request.method == "POST" and not self.request.user.has_perm( if self.ue.has_user_already_commented(self.request.user):
"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.object.id kwargs["ue_id"] = self.ue.id
kwargs["is_creation"] = True kwargs["is_creation"] = True
return kwargs return kwargs
def form_valid(self, form): def get_context_data(self, **kwargs):
form.save() return super().get_context_data(**kwargs) | {
return super().form_valid(form) "action": reverse("pedagogy:comment_create", kwargs={"ue_id": self.ue.id}),
"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) | {
@@ -85,22 +106,17 @@ class UEDetailFormView(PermissionRequiredMixin, DetailFormView):
.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 = "core/edit.jinja" template_name = "pedagogy/fragments/ue_comment_form.jinja"
permission_required = "pedagogy.change_uecomment" permission_required = "pedagogy.change_uecomment"
author_field = "author" author_field = "author"
@@ -111,11 +127,18 @@ class UECommentUpdateView(PermissionOrAuthorRequiredMixin, UpdateView):
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, DeleteView): class UECommentDeleteView(PermissionOrAuthorRequiredMixin, AllowFragment, DeleteView):
"""Allow to delete a given comment.""" """Allow to delete a given comment."""
model = UEComment model = UEComment
@@ -124,8 +147,13 @@ class UECommentDeleteView(PermissionOrAuthorRequiredMixin, DeleteView):
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):
@@ -135,24 +163,34 @@ class UEGuideView(PermissionRequiredMixin, TemplateView):
permission_required = "pedagogy.view_ue" permission_required = "pedagogy.view_ue"
class UECommentReportCreateView(PermissionRequiredMixin, CreateView): class UECommentReportCreateView(PermissionRequiredMixin, AllowFragment, 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 = "core/edit.jinja" template_name = "pedagogy/fragments/comment_report.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
@@ -168,11 +206,10 @@ class UECommentReportCreateView(PermissionRequiredMixin, CreateView):
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 reverse("pedagogy:ue_detail", kwargs={"ue_id": self.ue_comment.ue_id}) return self.ue_detail_url
class UEModerationFormView(PermissionRequiredMixin, FormView): class UEModerationFormView(PermissionRequiredMixin, FormView):
+3 -24
View File
@@ -12,35 +12,16 @@
{% trans %}Timetable generator{% endtrans %} {% trans %}Timetable generator{% endtrans %}
{% endblock %} {% endblock %}
{% block description -%}
{% trans trimmed %}
Get a nice schedule from the raw text in your student file
{% endtrans %}
{%- endblock %}
{% block metatags %}
<meta property="og:url" content="{{ request.build_absolute_uri() }}" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Générateur d'emploi du temps" />
<meta property="og:image" content="{{ request.build_absolute_uri(static("core/img/logo_no_text.png")) }}" />
<meta
property="og:description"
content="Générateur d'emploi du temps de l'UTBM.
Copiez le texte de l'emploi du temps de votre dossier étudiant
et récupérez une image plus jolie."
/>
{% endblock %}
{% block content %} {% block content %}
<div x-data="timetableGenerator"> <div x-data="timetableGenerator">
<form @submit.prevent="generate()"> <form @submit.prevent="generate()">
<h1>{% trans %}Timetable generator{% endtrans %}</h1> <h1>Générateur d'emploi du temps</h1>
<template x-if="error !== null" x-cloak> <template x-if="error !== null" x-cloak>
<div class="alert alert-red"> <div class="alert alert-red">
<div class="alert-main"> <div class="alert-main">
<p> <p>
{% trans trimmed %} {% trans trimmed %}
Wrong timetable format. Make sure you copied it from your student folder. Wrong timetable format. Make sure you copied if from your student folder.
{% endtrans %} {% endtrans %}
</p> </p>
<template x-if="!!error.incorrectRow"> <template x-if="!!error.incorrectRow">
@@ -53,9 +34,7 @@
</div> </div>
</template> </template>
<div class="form-group"> <div class="form-group">
<label for="timetable-input"> <label for="timetable-input">Colle ton emploi du temps (sans l'entête)</label>
{% trans %}Paste your schedule (without the header){% endtrans %}
</label>
<textarea id="timetable-input" cols="30" rows="15" x-model="content"></textarea> <textarea id="timetable-input" cols="30" rows="15" x-model="content"></textarea>
</div> </div>
<input type="submit" class="btn btn-blue" value="{% trans %}Generate{% endtrans %}"> <input type="submit" class="btn btn-blue" value="{% trans %}Generate{% endtrans %}">