Merge pull request #1462 from ae-utbm/pedagogy

Use HTMX in UE details
This commit is contained in:
2026-09-13 23:01:48 +02:00
committed by GitHub
13 changed files with 574 additions and 287 deletions
+1
View File
@@ -70,6 +70,7 @@ htmx.registerExtension("aria-busy", {
});
htmx.config.transitions = true;
Object.assign(window, { htmx });
/**
+13 -3
View File
@@ -26,6 +26,7 @@ from django.conf import settings
from django.core import validators
from django.db import models
from django.db.models import Exists, OuterRef
from django.db.models.query_utils import Q
from django.urls import reverse
from django.utils import timezone
from django.utils.functional import cached_property
@@ -123,8 +124,10 @@ class UE(models.Model):
This function checks that no other comment has been posted by a specified user.
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 or is anonymous, else False.
"""
if user.is_anonymous:
return True
return self.comments.filter(author=user).exists()
@cached_property
@@ -155,7 +158,14 @@ class UECommentQuerySet(models.QuerySet):
# so he can view non-moderated comments
return self
if user.has_perm("pedagogy.view_uecomment"):
return self.filter(reports=None)
return self.filter(
Q(author=user)
| ~Exists(
UECommentReport.objects.filter(
~Q(reporter=user), comment=OuterRef("pk")
)
)
).distinct()
return self.filter(author=user)
def annotate_is_reported(self) -> Self:
@@ -257,7 +267,7 @@ class UEResult(models.Model):
class UECommentReport(models.Model):
"""Report an inapropriate comment."""
"""Report an inappropriate comment."""
comment = models.ForeignKey(
UEComment,
+12 -7
View File
@@ -69,6 +69,7 @@ $pedagogy-white-text: #f0f0f0;
input[type="checkbox"] {
display: none;
}
margin-top: 10px;
margin-bottom: 0;
color: white;
@@ -128,14 +129,14 @@ $pedagogy-white-text: #f0f0f0;
grid-area: hours-the;
}
#leave_comment_not_allowed {
.leave-comment-not-allowed {
p {
text-align: center;
color: red;
}
}
#leave_comment {
.leave-comment {
.leave-comment-grid-container {
display: grid;
grid-template-columns: 270px auto;
@@ -168,10 +169,6 @@ $pedagogy-white-text: #f0f0f0;
.input-stars {
margin-top: 20px;
}
input[type="submit"] {
float: right;
}
}
.ue-details-container {
@@ -333,8 +330,9 @@ $pedagogy-white-text: #f0f0f0;
padding-left: 10px;
}
.actions {
.action {
float: right;
margin-top: 0;
}
}
@@ -402,6 +400,7 @@ $pedagogy-white-text: #f0f0f0;
a {
color: $pedagogy-white-text;
cursor: pointer;
}
a:hover {
@@ -444,3 +443,9 @@ details.accordion>.accordion-content {
border-color: $pedagogy-orange;
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 row gap">
<button
class="btn btn-grey"
hx-get="{{ ue_detail_url }}"
hx-target="closest form"
hx-swap="outerHTML"
>
{% trans %}Cancel{% endtrans %}
</button>
<input class="btn btn-blue" 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>
+31 -160
View File
@@ -1,16 +1,32 @@
{% extends "core/base.jinja" %}
{% from "core/macros.jinja" import user_profile_link %}
{% from "pedagogy/macros.jinja" import display_star %}
{% block additional_css %}
{% 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" %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static('pedagogy/css/pedagogy.scss') }}">
{% endblock %}
{% endblock %}
{% block title %}
{% block title %}
{% trans %}UE Details{% endtrans %}
{% endblock %}
{% endblock %}
{% block content %}
{% block content %}
<div class="pedagogy">
<div id="ue_detail">
<button onclick='(function(){
@@ -55,166 +71,21 @@
<br>
<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 id="ue-grade">
{% include "pedagogy/fragments/ue_details/grade.jinja" %}
</div>
<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 }}
{{ 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 id="comment-form">
{% include "pedagogy/fragments/ue_details/form.jinja" %}
</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>
<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 id="comments">
{% include "pedagogy/fragments/ue_details/comments.jinja" %}
</div>
</div>
{% endfor %}
{% endif %}
</div>
</div>
{% endblock %}
{% endblock %}
{% endif %}
+168 -36
View File
@@ -25,9 +25,9 @@ from typing import Callable
import pytest
from django.conf import settings
from django.contrib.auth.models import Permission
from django.db.models import Max
from django.test import Client, TestCase
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from model_bakery import baker
from pytest_django.asserts import assertRedirects
@@ -278,13 +278,17 @@ class TestUEUpdate(TestCase):
# UEComment class tests
def create_ue_comment_template(user_id, ue_code="PA00", exclude_list=None):
def create_ue_comment_template(
user_id: int,
ue: int | str = "PA00",
exclude_list: list[str] | None = None,
):
"""Factory to help UEComment creation/update in post requests."""
if exclude_list is None:
exclude_list = []
comment = {
"author": user_id,
"ue": UE.objects.get(code=ue_code).id,
"ue": UE.objects.get(code=ue).id if isinstance(ue, str) else ue,
"grade_global": 4,
"grade_utility": 4,
"grade_interest": 4,
@@ -297,12 +301,111 @@ def create_ue_comment_template(user_id, ue_code="PA00", exclude_list=None):
return comment
class TestUVCommentCreationAndDisplay(TestCase):
"""Test UEComment creation and its display.
class TestUVCommentDisplay(TestCase):
@classmethod
def setUpTestData(cls):
cls.admin = baker.make(User, is_superuser=True)
cls.ue = baker.make(UE)
cls.ue_url = reverse("pedagogy:ue_detail", kwargs={"ue_id": cls.ue.id})
cls.pedagogy_admin = baker.make(
User,
user_permissions=[
Permission.objects.get(codename="view_ue"),
Permission.objects.get(codename="view_uecomment"),
Permission.objects.get(codename="view_uecommentreport"),
],
)
cls.subscriber = subscriber_user.make()
comments = baker.make(UEComment, ue=cls.ue, _quantity=10)
cls.unreported, cls.reported = comments[:5], comments[5:]
baker.make(
UECommentReport,
comment=iter(cls.reported),
_quantity=len(cls.reported),
_bulk_create=True,
)
Display and creation are the same view.
"""
def test_access_succses(self):
self.client.force_login(self.admin)
assert self.client.get(self.ue_url).status_code == 200
self.client.force_login(self.pedagogy_admin)
assert self.client.get(self.ue_url).status_code == 200
self.client.force_login(self.subscriber)
assert self.client.get(self.ue_url).status_code == 200
def test_access_fail(self):
# Anonymous user
assertRedirects(
self.client.get(self.ue_url),
reverse("core:login", query={"next": self.ue_url}),
)
# Unauthorized user
self.client.force_login(baker.make(User))
assert self.client.get(self.ue_url).status_code == 403
def test_access_not_found(self):
self.client.force_login(self.admin)
res = self.client.get(
reverse(
"pedagogy:ue_detail",
kwargs={"ue_id": UE.objects.aggregate(id=Max("id"))["id"] + 1},
)
)
assert res.status_code == 404
def test_comments_normal_user(self):
# Normal user only see
# * Unreported comments
# * Comments that he wrote but were reported
# * Comments that he himself reported
self.client.force_login(self.subscriber)
comments = self.client.get(self.ue_url).context_data.get("comments", [])
assert len(comments) == len(self.unreported)
assert set(comments) == set(self.unreported)
# Make user comment
user_comment = baker.make(UEComment, ue=self.ue, author=self.subscriber)
comments = self.client.get(self.ue_url).context_data.get("comments", [])
assert len(comments) == len(self.unreported) + 1
assert set(comments) == {*self.unreported, user_comment}
# Report user comment
baker.make(UECommentReport, comment=user_comment)
comments = self.client.get(self.ue_url).context_data.get("comments", [])
assert len(comments) == len(self.unreported) + 1
assert set(comments) == {*self.unreported, user_comment}
# Report someone's else comment
comment_reported_by_user = baker.make(UEComment, ue=self.ue)
baker.make(
UECommentReport, comment=comment_reported_by_user, reporter=self.subscriber
)
comments = self.client.get(self.ue_url).context_data.get("comments", [])
assert len(comments) == len(self.unreported) + 2
assert set(comments) == {
*self.unreported,
user_comment,
comment_reported_by_user,
}
def test_comments_pedagogy_admin(self):
# Pedagogy admin sees everything
self.client.force_login(self.pedagogy_admin)
comments = self.client.get(self.ue_url).context_data.get("comments", [])
assert len(comments) == 10
def test_comments_admin(self):
# Admin sees everything
self.client.force_login(self.admin)
comments = self.client.get(self.ue_url).context_data.get("comments", [])
assert len(comments) == 10
class TestUECommentCreation(TestCase):
@classmethod
def setUpTestData(cls):
cls.bibou = User.objects.get(username="root")
@@ -311,11 +414,14 @@ class TestUVCommentCreationAndDisplay(TestCase):
cls.guy = User.objects.get(username="guy")
cls.ue = UE.objects.get(code="PA00")
cls.ue_url = reverse("pedagogy:ue_detail", kwargs={"ue_id": cls.ue.id})
cls.comment_create_url = reverse(
"pedagogy:comment_create", kwargs={"ue_id": cls.ue.id}
)
def test_create_ue_comment_admin_success(self):
self.client.force_login(self.bibou)
response = self.client.post(
self.ue_url, create_ue_comment_template(self.bibou.id)
self.comment_create_url, create_ue_comment_template(self.bibou.id)
)
assertRedirects(response, self.ue_url)
response = self.client.get(self.ue_url)
@@ -324,7 +430,7 @@ class TestUVCommentCreationAndDisplay(TestCase):
def test_create_ue_comment_pedagogy_admin_success(self):
self.client.force_login(self.tutu)
response = self.client.post(
self.ue_url, create_ue_comment_template(self.tutu.id)
self.comment_create_url, create_ue_comment_template(self.tutu.id)
)
self.assertRedirects(response, self.ue_url)
response = self.client.get(self.ue_url)
@@ -333,7 +439,7 @@ class TestUVCommentCreationAndDisplay(TestCase):
def test_create_ue_comment_subscriber_success(self):
self.client.force_login(self.sli)
response = self.client.post(
self.ue_url, create_ue_comment_template(self.sli.id)
self.comment_create_url, create_ue_comment_template(self.sli.id)
)
self.assertRedirects(response, self.ue_url)
response = self.client.get(self.ue_url)
@@ -342,7 +448,7 @@ class TestUVCommentCreationAndDisplay(TestCase):
def test_create_ue_empty_comment_fail(self):
self.client.force_login(self.tutu)
response = self.client.post(
self.ue_url,
self.comment_create_url,
{
"author": self.tutu.id,
"ue": UE.objects.get(code="PA00").id,
@@ -360,24 +466,40 @@ class TestUVCommentCreationAndDisplay(TestCase):
def test_create_ue_comment_unauthorized_fail(self):
nb_comments = self.ue.comments.count()
# Test with anonymous user
response = self.client.post(self.ue_url, create_ue_comment_template(0))
assertRedirects(response, reverse("core:login", query={"next": self.ue_url}))
response = self.client.post(
self.comment_create_url, create_ue_comment_template(0)
)
assertRedirects(
response, reverse("core:login", query={"next": self.comment_create_url})
)
# Test with non subscribed user
self.client.force_login(self.guy)
response = self.client.post(
self.ue_url, create_ue_comment_template(self.guy.id)
self.comment_create_url, create_ue_comment_template(self.guy.id)
)
assert response.status_code == 403
# Check that no comment has been created
assert self.ue.comments.count() == nb_comments
def test_create_ue_comment_ue_not_exist_fails(self):
self.client.force_login(self.bibou)
not_existing_id = UE.objects.aggregate(id=Max("id"))["id"] + 1
response = self.client.post(
reverse("pedagogy:comment_create", kwargs={"ue_id": not_existing_id}),
create_ue_comment_template(
self.bibou.id,
ue=not_existing_id,
),
)
assert response.status_code == 404
def test_create_ue_comment_bad_form_fail(self):
nb_comments = self.ue.comments.count()
self.client.force_login(self.bibou)
response = self.client.post(
self.ue_url,
self.comment_create_url,
create_ue_comment_template(self.bibou.id, exclude_list=["grade_global"]),
)
@@ -385,45 +507,55 @@ class TestUVCommentCreationAndDisplay(TestCase):
assert self.ue.comments.count() == nb_comments
def test_create_ue_comment_twice_fail(self):
# Checks that the has_user_already_commented method works proprely
# Checks that the has_user_already_commented method works properly
assert not self.ue.has_user_already_commented(self.bibou)
# Create a first comment
self.client.force_login(self.bibou)
self.client.post(self.ue_url, create_ue_comment_template(self.bibou.id))
self.client.post(
self.comment_create_url, create_ue_comment_template(self.bibou.id)
)
# Checks that the has_user_already_commented method works proprely
# Checks that the has_user_already_commented method works properly
assert self.ue.has_user_already_commented(self.bibou)
# Create the second comment
comment = create_ue_comment_template(self.bibou.id)
comment["comment"] = "Twice"
response = self.client.post(self.ue_url, comment)
assert response.status_code == 200
response = self.client.post(self.comment_create_url, comment)
assert response.status_code == 403
assert UEComment.objects.filter(comment__contains="Superbe UE").exists()
assert not UEComment.objects.filter(comment__contains="Twice").exists()
self.assertContains(
response,
_(
"You already posted a comment on this UE. "
"If you want to comment again, "
"please modify or delete your previous comment."
),
)
def test_create_ue_comment_wrong_args(self):
self.client.force_login(self.bibou)
# Ensure that there is no crash when no ue or no author is given
self.client.post(
self.ue_url, create_ue_comment_template(self.bibou.id, exclude_list=["ue"])
response = self.client.post(
self.comment_create_url,
create_ue_comment_template(self.bibou.id, exclude_list=["ue"]),
)
assert response.status_code == 200
self.client.post(
self.ue_url,
assert not self.ue.has_user_already_commented(self.bibou)
response = self.client.post(
self.comment_create_url,
create_ue_comment_template(self.bibou.id, exclude_list=["author"]),
)
assert response.status_code == 200
assert not self.ue.has_user_already_commented(self.bibou)
# Ensure that we can't push the wrong UE id
other_ue = baker.make(UE)
response = self.client.post(
self.comment_create_url,
create_ue_comment_template(self.bibou.id, ue=other_ue.id),
)
assert response.status_code == 200
assert not self.ue.has_user_already_commented(self.bibou)
class TestUVCommentDelete(TestCase):
class TestUECommentDelete(TestCase):
"""Test UEComment deletion rights."""
@classmethod
@@ -462,7 +594,7 @@ class TestUVCommentDelete(TestCase):
assert UEComment.objects.filter(id=self.comment.id).exists()
class TestUVCommentUpdate(TestCase):
class TestUECommentUpdate(TestCase):
"""Test UEComment update rights."""
@classmethod
@@ -536,7 +668,7 @@ class TestUVCommentUpdate(TestCase):
self.assertEqual(self.comment.author, self.krophil)
class TestUVModerationForm(TestCase):
class TestUEModerationForm(TestCase):
"""Assert access rights and if the form works well."""
@classmethod
@@ -750,7 +882,7 @@ class TestUVModerationForm(TestCase):
assert UEComment.objects.filter(id=self.comment_2.id).exists()
class TestUVCommentReportCreate(TestCase):
class TestUECommentReportCreate(TestCase):
"""Test report creation view.
Assert access rights and if you can create with it.
+8 -2
View File
@@ -24,12 +24,13 @@
from django.urls import path
from pedagogy.views import (
UECommentCreateView,
UECommentDeleteView,
UECommentReportCreateView,
UECommentUpdateView,
UECreateView,
UEDeleteView,
UEDetailFormView,
UEDetailView,
UEGuideView,
UEModerationFormView,
UEUpdateView,
@@ -38,7 +39,12 @@ from pedagogy.views import (
urlpatterns = [
# Urls displaying the actual application for visitors
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(
"comment/<int:comment_id>/edit/",
UECommentUpdateView.as_view(),
+65 -33
View File
@@ -16,7 +16,7 @@
# details.
#
# 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.
#
#
@@ -26,9 +26,11 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Exists, OuterRef
from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy
from django.utils.functional import cached_property
from django.views.generic import (
CreateView,
DeleteView,
DetailView,
FormView,
TemplateView,
UpdateView,
@@ -36,7 +38,7 @@ from django.views.generic import (
from core.auth.mixins import PermissionOrAuthorRequiredMixin
from core.models import Notification, User
from core.views import DetailFormView
from core.views.mixins import AllowFragment, FragmentMixin, UseFragmentsMixin
from pedagogy.forms import (
UECommentForm,
UECommentModerationForm,
@@ -46,37 +48,56 @@ from pedagogy.forms import (
from pedagogy.models import UE, UEComment, UECommentReport
class UEDetailFormView(PermissionRequiredMixin, DetailFormView):
"""Display every comment of an UE and detailed infos about it.
Allow to comment the UE.
"""
model = UE
pk_url_kwarg = "ue_id"
template_name = "pedagogy/ue_detail.jinja"
class UECommentCreateView(PermissionRequiredMixin, FragmentMixin, CreateView):
model = UEComment
template_name = "pedagogy/fragments/ue_comment_form.jinja"
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):
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
if self.ue.has_user_already_commented(self.request.user):
return False
return super().has_permission()
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["author_id"] = self.request.user.id
kwargs["ue_id"] = self.object.id
kwargs["ue_id"] = self.ue.id
kwargs["is_creation"] = True
return kwargs
def form_valid(self, form):
form.save()
return super().form_valid(form)
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"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):
return super().get_context_data(**kwargs) | {
@@ -88,19 +109,14 @@ class UEDetailFormView(PermissionRequiredMixin, DetailFormView):
)
}
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, UpdateView):
class UECommentUpdateView(PermissionOrAuthorRequiredMixin, AllowFragment, UpdateView):
"""Allow edit of a given comment."""
model = UEComment
form_class = UECommentForm
pk_url_kwarg = "comment_id"
template_name = "core/edit.jinja"
template_name = "pedagogy/fragments/ue_comment_form.jinja"
permission_required = "pedagogy.change_uecomment"
author_field = "author"
@@ -111,11 +127,18 @@ class UECommentUpdateView(PermissionOrAuthorRequiredMixin, UpdateView):
kwargs["is_creation"] = False
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):
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."""
model = UEComment
@@ -135,24 +158,34 @@ class UEGuideView(PermissionRequiredMixin, TemplateView):
permission_required = "pedagogy.view_ue"
class UECommentReportCreateView(PermissionRequiredMixin, CreateView):
class UECommentReportCreateView(PermissionRequiredMixin, AllowFragment, CreateView):
"""Create a new report for an inappropriate comment."""
model = UECommentReport
form_class = UECommentReportForm
template_name = "core/edit.jinja"
template_name = "pedagogy/fragments/comment_report.jinja"
permission_required = "pedagogy.add_uecommentreport"
def dispatch(self, request, *args, **kwargs):
self.ue_comment = get_object_or_404(UEComment, pk=kwargs["comment_id"])
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):
kwargs = super().get_form_kwargs()
kwargs["reporter_id"] = self.request.user.id
kwargs["comment_id"] = self.ue_comment.id
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):
resp = super().form_valid(form)
# Send a message to moderation admins
@@ -168,11 +201,10 @@ class UECommentReportCreateView(PermissionRequiredMixin, CreateView):
url=reverse("pedagogy:moderation"),
type="PEDAGOGY_MODERATION",
)
return resp
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):