1 Commits

Author SHA1 Message Date
Thomas Girod
775a3282dc rename UV to UE 2025-12-19 23:12:02 +01:00
45 changed files with 824 additions and 1174 deletions

View File

@@ -35,7 +35,7 @@ TODO : rewrite the pagination used in this template an Alpine one
{% csrf_token %} {% csrf_token %}
{{ form }} {{ form }}
<p><input type="submit" value="{% trans %}Show{% endtrans %}" /></p> <p><input type="submit" value="{% trans %}Show{% endtrans %}" /></p>
<p><input type="submit" value="{% trans %}Download as csv{% endtrans %}" formaction="{{ url('club:sellings_csv', club_id=object.id) }}"/></p> <p><input type="submit" value="{% trans %}Download as cvs{% endtrans %}" formaction="{{ url('club:sellings_csv', club_id=object.id) }}"/></p>
</form> </form>
<p> <p>
{% trans %}Quantity: {% endtrans %}{{ total_quantity }} {% trans %}units{% endtrans %}<br/> {% trans %}Quantity: {% endtrans %}{{ total_quantity }} {% trans %}units{% endtrans %}<br/>

View File

@@ -203,7 +203,7 @@
<ul> <ul>
<li> <li>
<i class="fa-solid fa-graduation-cap fa-xl"></i> <i class="fa-solid fa-graduation-cap fa-xl"></i>
<a href="{{ url("pedagogy:guide") }}">{% trans %}UV Guide{% endtrans %}</a> <a href="{{ url("pedagogy:guide") }}">{% trans %}UE Guide{% endtrans %}</a>
</li> </li>
<li> <li>
<i class="fa-solid fa-calendar-days fa-xl"></i> <i class="fa-solid fa-calendar-days fa-xl"></i>

View File

@@ -44,7 +44,7 @@ from core.utils import resize_image
from counter.models import Counter, Product, ProductType, ReturnableProduct, StudentCard from counter.models import Counter, Product, ProductType, ReturnableProduct, StudentCard
from election.models import Candidature, Election, ElectionList, Role from election.models import Candidature, Election, ElectionList, Role
from forum.models import Forum from forum.models import Forum
from pedagogy.models import UV from pedagogy.models import UE
from sas.models import Album, PeoplePictureRelation, Picture from sas.models import Album, PeoplePictureRelation, Picture
from subscription.models import Subscription from subscription.models import Subscription
@@ -661,20 +661,20 @@ class Command(BaseCommand):
# Create some data for pedagogy # Create some data for pedagogy
UV( UE(
code="PA00", code="PA00",
author=User.objects.get(id=0), author=User.objects.get(id=0),
credit_type=settings.SITH_PEDAGOGY_UV_TYPE[3][0], credit_type=settings.SITH_PEDAGOGY_UE_TYPE[3][0],
manager="Laurent HEYBERGER", manager="Laurent HEYBERGER",
semester=settings.SITH_PEDAGOGY_UV_SEMESTER[3][0], semester=settings.SITH_PEDAGOGY_UE_SEMESTER[3][0],
language=settings.SITH_PEDAGOGY_UV_LANGUAGE[0][0], language=settings.SITH_PEDAGOGY_UE_LANGUAGE[0][0],
department=settings.SITH_PROFILE_DEPARTMENTS[-2][0], department=settings.SITH_PROFILE_DEPARTMENTS[-2][0],
credits=5, credits=5,
title="Participation dans une association étudiante", title="Participation dans une association étudiante",
objectives="* Permettre aux étudiants de réaliser, pendant un semestre, un projet culturel ou associatif et de le valoriser.", objectives="* Permettre aux étudiants de réaliser, pendant un semestre, un projet culturel ou associatif et de le valoriser.",
program="""* Semestre précédent proposition d'un projet et d'un cahier des charges program="""* Semestre précédent proposition d'un projet et d'un cahier des charges
* Evaluation par un jury de six membres * Evaluation par un jury de six membres
* Si accord réalisation dans le cadre de l'UV * Si accord réalisation dans le cadre de l'UE
* Compte-rendu de l'expérience * Compte-rendu de l'expérience
* Présentation""", * Présentation""",
skills="""* Gérer un projet associatif ou une action éducative en autonomie: skills="""* Gérer un projet associatif ou une action éducative en autonomie:
@@ -790,16 +790,16 @@ class Command(BaseCommand):
subscribers = Group.objects.create(name="Cotisants") subscribers = Group.objects.create(name="Cotisants")
subscribers.permissions.add( subscribers.permissions.add(
*list(perms.filter(codename__in=["add_news", "add_uvcomment"])) *list(perms.filter(codename__in=["add_news", "add_uecomment"]))
) )
old_subscribers = Group.objects.create(name="Anciens cotisants") old_subscribers = Group.objects.create(name="Anciens cotisants")
old_subscribers.permissions.add( old_subscribers.permissions.add(
*list( *list(
perms.filter( perms.filter(
codename__in=[ codename__in=[
"view_uv", "view_ue",
"view_uvcomment", "view_uecomment",
"add_uvcommentreport", "add_uecommentreport",
"view_user", "view_user",
"view_picture", "view_picture",
"view_album", "view_album",
@@ -875,7 +875,7 @@ class Command(BaseCommand):
pedagogy_admin.permissions.add( pedagogy_admin.permissions.add(
*list( *list(
perms.filter(content_type__app_label="pedagogy") perms.filter(content_type__app_label="pedagogy")
.exclude(codename__in=["change_uvcomment"]) .exclude(codename__in=["change_uecomment"])
.values_list("pk", flat=True) .values_list("pk", flat=True)
) )
) )

View File

@@ -23,7 +23,7 @@ from counter.models import (
Selling, Selling,
) )
from forum.models import Forum, ForumMessage, ForumTopic from forum.models import Forum, ForumMessage, ForumTopic
from pedagogy.models import UV from pedagogy.models import UE
from subscription.models import Subscription from subscription.models import Subscription
@@ -74,7 +74,7 @@ class Command(BaseCommand):
random.sample(old_subscribers, k=min(80, len(old_subscribers))), random.sample(old_subscribers, k=min(80, len(old_subscribers))),
) )
self.stdout.write("Creating uvs...") self.stdout.write("Creating uvs...")
self.create_uvs() self.create_ues()
self.stdout.write("Creating products...") self.stdout.write("Creating products...")
self.create_products() self.create_products()
self.stdout.write("Creating sales and refills...") self.stdout.write("Creating sales and refills...")
@@ -192,7 +192,7 @@ class Command(BaseCommand):
memberships = Membership.objects.bulk_create(memberships) memberships = Membership.objects.bulk_create(memberships)
Membership._add_club_groups(memberships) Membership._add_club_groups(memberships)
def create_uvs(self): def create_ues(self):
root = User.objects.get(username="root") root = User.objects.get(username="root")
categories = ["CS", "TM", "OM", "QC", "EC"] categories = ["CS", "TM", "OM", "QC", "EC"]
branches = ["TC", "GMC", "GI", "EDIM", "E", "IMSI", "HUMA"] branches = ["TC", "GMC", "GI", "EDIM", "E", "IMSI", "HUMA"]
@@ -207,7 +207,7 @@ class Command(BaseCommand):
+ str(random.randint(10, 90)) + str(random.randint(10, 90))
) )
uvs.append( uvs.append(
UV( UE(
code=code, code=code,
author=root, author=root,
manager=random.choice(teachers), manager=random.choice(teachers),
@@ -229,7 +229,7 @@ class Command(BaseCommand):
hours_TE=random.randint(15, 40), hours_TE=random.randint(15, 40),
) )
) )
UV.objects.bulk_create(uvs, ignore_conflicts=True) UE.objects.bulk_create(uvs, ignore_conflicts=True)
def create_products(self): def create_products(self):
categories = [ categories = [

View File

@@ -184,18 +184,18 @@
</div> </div>
{% endif %} {% endif %}
{% if user.has_perm("pedagogy.add_uv") or user.has_perm("pedagogy.delete_uvcomment") %} {% if user.has_perm("pedagogy.add_ue") or user.has_perm("pedagogy.delete_uecomment") %}
<div> <div>
<h4>{% trans %}Pedagogy{% endtrans %}</h4> <h4>{% trans %}Pedagogy{% endtrans %}</h4>
<ul> <ul>
{% if user.has_perm("pedagogy.add_uv") %} {% if user.has_perm("pedagogy.add_ue") %}
<li> <li>
<a href="{{ url("pedagogy:uv_create") }}"> <a href="{{ url("pedagogy:ue_create") }}">
{% trans %}Create UV{% endtrans %} {% trans %}Create UE{% endtrans %}
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% if user.has_perm("pedagogy.delete_uvcomment") %} {% if user.has_perm("pedagogy.delete_uecomment") %}
<li> <li>
<a href="{{ url("pedagogy:moderation") }}"> <a href="{{ url("pedagogy:moderation") }}">
{% trans %}Moderate comments{% endtrans %} {% trans %}Moderate comments{% endtrans %}

View File

@@ -5,7 +5,6 @@ from datetime import date, datetime, timezone
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django import forms from django import forms
from django.core.validators import MaxValueValidator
from django.db.models import Exists, OuterRef, Q from django.db.models import Exists, OuterRef, Q
from django.forms import BaseModelFormSet from django.forms import BaseModelFormSet
from django.utils.timezone import now from django.utils.timezone import now
@@ -35,7 +34,6 @@ from counter.models import (
Eticket, Eticket,
InvoiceCall, InvoiceCall,
Product, Product,
ProductFormula,
Refilling, Refilling,
ReturnableProduct, ReturnableProduct,
ScheduledProductAction, ScheduledProductAction,
@@ -318,6 +316,7 @@ class ProductForm(forms.ModelForm):
} }
counters = forms.ModelMultipleChoiceField( counters = forms.ModelMultipleChoiceField(
help_text=None,
label=_("Counters"), label=_("Counters"),
required=False, required=False,
widget=AutoCompleteSelectMultipleCounter, widget=AutoCompleteSelectMultipleCounter,
@@ -328,31 +327,10 @@ class ProductForm(forms.ModelForm):
super().__init__(*args, instance=instance, **kwargs) super().__init__(*args, instance=instance, **kwargs)
if self.instance.id: if self.instance.id:
self.fields["counters"].initial = self.instance.counters.all() self.fields["counters"].initial = self.instance.counters.all()
if hasattr(self.instance, "formula"):
self.formula_init(self.instance.formula)
self.action_formset = ScheduledProductActionFormSet( self.action_formset = ScheduledProductActionFormSet(
*args, product=self.instance, **kwargs *args, product=self.instance, **kwargs
) )
def formula_init(self, formula: ProductFormula):
"""Part of the form initialisation specific to formula products."""
self.fields["selling_price"].help_text = _(
"This product is a formula. "
"Its price cannot be greater than the price "
"of the products constituting it, which is %(price)s"
) % {"price": formula.max_selling_price}
self.fields["special_selling_price"].help_text = _(
"This product is a formula. "
"Its special price cannot be greater than the price "
"of the products constituting it, which is %(price)s"
) % {"price": formula.max_special_selling_price}
for key, price in (
("selling_price", formula.max_selling_price),
("special_selling_price", formula.max_special_selling_price),
):
self.fields[key].widget.attrs["max"] = price
self.fields[key].validators.append(MaxValueValidator(price))
def is_valid(self): def is_valid(self):
return super().is_valid() and self.action_formset.is_valid() return super().is_valid() and self.action_formset.is_valid()
@@ -371,47 +349,13 @@ class ProductForm(forms.ModelForm):
return product return product
class ProductFormulaForm(forms.ModelForm):
class Meta:
model = ProductFormula
fields = ["products", "result"]
widgets = {
"products": AutoCompleteSelectMultipleProduct,
"result": AutoCompleteSelectProduct,
}
def clean(self):
cleaned_data = super().clean()
if cleaned_data["result"] in cleaned_data["products"]:
self.add_error(
None,
_(
"The same product cannot be at the same time "
"the result and a part of the formula."
),
)
prices = [p.selling_price for p in cleaned_data["products"]]
special_prices = [p.special_selling_price for p in cleaned_data["products"]]
selling_price = cleaned_data["result"].selling_price
special_selling_price = cleaned_data["result"].special_selling_price
if selling_price > sum(prices) or special_selling_price > sum(special_prices):
self.add_error(
"result",
_(
"The result cannot be more expensive "
"than the total of the other products."
),
)
return cleaned_data
class ReturnableProductForm(forms.ModelForm): class ReturnableProductForm(forms.ModelForm):
class Meta: class Meta:
model = ReturnableProduct model = ReturnableProduct
fields = ["product", "returned_product", "max_return"] fields = ["product", "returned_product", "max_return"]
widgets = { widgets = {
"product": AutoCompleteSelectProduct, "product": AutoCompleteSelectProduct(),
"returned_product": AutoCompleteSelectProduct, "returned_product": AutoCompleteSelectProduct(),
} }
def save(self, commit: bool = True) -> ReturnableProduct: # noqa FBT def save(self, commit: bool = True) -> ReturnableProduct: # noqa FBT

View File

@@ -1,43 +0,0 @@
# Generated by Django 5.2.8 on 2025-11-26 11:34
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("counter", "0035_remove_selling_is_validated_and_more")]
operations = [
migrations.CreateModel(
name="ProductFormula",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"products",
models.ManyToManyField(
help_text="The products that constitute this formula.",
related_name="formulas",
to="counter.product",
verbose_name="products",
),
),
(
"result",
models.OneToOneField(
help_text="The formula product.",
on_delete=django.db.models.deletion.CASCADE,
to="counter.product",
verbose_name="result product",
),
),
],
),
]

View File

@@ -455,37 +455,6 @@ class Product(models.Model):
return self.selling_price - self.purchase_price return self.selling_price - self.purchase_price
class ProductFormula(models.Model):
products = models.ManyToManyField(
Product,
related_name="formulas",
verbose_name=_("products"),
help_text=_("The products that constitute this formula."),
)
result = models.OneToOneField(
Product,
related_name="formula",
on_delete=models.CASCADE,
verbose_name=_("result product"),
help_text=_("The product got with the formula."),
)
def __str__(self):
return self.result.name
@cached_property
def max_selling_price(self) -> float:
# iterating over all products is less efficient than doing
# a simple aggregation, but this method is likely to be used in
# coordination with `max_special_selling_price`,
# and Django caches the result of the `all` queryset.
return sum(p.selling_price for p in self.products.all())
@cached_property
def max_special_selling_price(self) -> float:
return sum(p.special_selling_price for p in self.products.all())
class CounterQuerySet(models.QuerySet): class CounterQuerySet(models.QuerySet):
def annotate_has_barman(self, user: User) -> Self: def annotate_has_barman(self, user: User) -> Self:
"""Annotate the queryset with the `user_is_barman` field. """Annotate the queryset with the `user_is_barman` field.

View File

@@ -1,10 +1,6 @@
import { AlertMessage } from "#core:utils/alert-message"; import { AlertMessage } from "#core:utils/alert-message";
import { BasketItem } from "#counter:counter/basket"; import { BasketItem } from "#counter:counter/basket";
import type { import type { CounterConfig, ErrorMessage } from "#counter:counter/types";
CounterConfig,
ErrorMessage,
ProductFormula,
} from "#counter:counter/types";
import type { CounterProductSelect } from "./components/counter-product-select-index.ts"; import type { CounterProductSelect } from "./components/counter-product-select-index.ts";
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
@@ -51,43 +47,15 @@ document.addEventListener("alpine:init", () => {
this.basket[id] = item; this.basket[id] = item;
this.checkFormulas();
if (this.sumBasket() > this.customerBalance) { if (this.sumBasket() > this.customerBalance) {
item.quantity = oldQty; item.quantity = oldQty;
if (item.quantity === 0) { if (item.quantity === 0) {
delete this.basket[id]; delete this.basket[id];
} }
this.alertMessage.display(gettext("Not enough money"), { success: false }); return gettext("Not enough money");
} }
},
checkFormulas() { return "";
const products = new Set(
Object.keys(this.basket).map((i: string) => Number.parseInt(i)),
);
const formula: ProductFormula = config.formulas.find((f: ProductFormula) => {
return f.products.every((p: number) => products.has(p));
});
if (formula === undefined) {
return;
}
for (const product of formula.products) {
const key = product.toString();
this.basket[key].quantity -= 1;
if (this.basket[key].quantity <= 0) {
this.removeFromBasket(key);
}
}
this.alertMessage.display(
interpolate(
gettext("Formula %(formula)s applied"),
{ formula: config.products[formula.result.toString()].name },
true,
),
{ success: true },
);
this.addToBasket(formula.result.toString(), 1);
}, },
getBasketSize() { getBasketSize() {
@@ -102,7 +70,14 @@ document.addEventListener("alpine:init", () => {
(acc: number, cur: BasketItem) => acc + cur.sum(), (acc: number, cur: BasketItem) => acc + cur.sum(),
0, 0,
) as number; ) as number;
return Math.round(total * 100) / 100; return total;
},
addToBasketWithMessage(id: string, quantity: number) {
const message = this.addToBasket(id, quantity);
if (message.length > 0) {
this.alertMessage.display(message, { success: false });
}
}, },
onRefillingSuccess(event: CustomEvent) { onRefillingSuccess(event: CustomEvent) {
@@ -141,7 +116,7 @@ document.addEventListener("alpine:init", () => {
this.finish(); this.finish();
} }
} else { } else {
this.addToBasket(code, quantity); this.addToBasketWithMessage(code, quantity);
} }
this.codeField.widget.clear(); this.codeField.widget.clear();
this.codeField.widget.focus(); this.codeField.widget.focus();

View File

@@ -7,16 +7,10 @@ export interface InitialFormData {
errors?: string[]; errors?: string[];
} }
export interface ProductFormula {
result: number;
products: number[];
}
export interface CounterConfig { export interface CounterConfig {
customerBalance: number; customerBalance: number;
customerId: number; customerId: number;
products: Record<string, Product>; products: Record<string, Product>;
formulas: ProductFormula[];
formInitial: InitialFormData[]; formInitial: InitialFormData[];
cancelUrl: string; cancelUrl: string;
} }

View File

@@ -10,12 +10,12 @@
float: right; float: right;
} }
.basket-message-container { .basket-error-container {
position: relative; position: relative;
display: block display: block
} }
.basket-message { .basket-error {
z-index: 10; // to get on top of tomselect z-index: 10; // to get on top of tomselect
text-align: center; text-align: center;
position: absolute; position: absolute;

View File

@@ -32,11 +32,13 @@
<div id="bar-ui" x-data="counter({ <div id="bar-ui" x-data="counter({
customerBalance: {{ customer.amount }}, customerBalance: {{ customer.amount }},
products: products, products: products,
formulas: formulas,
customerId: {{ customer.pk }}, customerId: {{ customer.pk }},
formInitial: formInitial, formInitial: formInitial,
cancelUrl: '{{ cancel_url }}', cancelUrl: '{{ cancel_url }}',
})"> })">
<noscript>
<p class="important">Javascript is required for the counter UI.</p>
</noscript>
<div id="user_info"> <div id="user_info">
<h5>{% trans %}Customer{% endtrans %}</h5> <h5>{% trans %}Customer{% endtrans %}</h5>
@@ -86,12 +88,11 @@
<form x-cloak method="post" action="" x-ref="basketForm"> <form x-cloak method="post" action="" x-ref="basketForm">
<div class="basket-message-container"> <div class="basket-error-container">
<div <div
x-cloak x-cloak
class="alert basket-message" class="alert alert-red basket-error"
:class="alertMessage.success ? 'alert-green' : 'alert-red'" x-show="alertMessage.show"
x-show="alertMessage.open"
x-transition.duration.500ms x-transition.duration.500ms
x-text="alertMessage.content" x-text="alertMessage.content"
></div> ></div>
@@ -110,9 +111,9 @@
</div> </div>
</template> </template>
<button @click.prevent="addToBasket(item.product.id, -1)">-</button> <button @click.prevent="addToBasketWithMessage(item.product.id, -1)">-</button>
<span class="quantity" x-text="item.quantity"></span> <span class="quantity" x-text="item.quantity"></span>
<button @click.prevent="addToBasket(item.product.id, 1)">+</button> <button @click.prevent="addToBasketWithMessage(item.product.id, 1)">+</button>
<span x-text="item.product.name"></span> : <span x-text="item.product.name"></span> :
<span x-text="item.sum().toLocaleString(undefined, { minimumFractionDigits: 2 })">€</span> <span x-text="item.sum().toLocaleString(undefined, { minimumFractionDigits: 2 })">€</span>
@@ -212,7 +213,7 @@
<h5 class="margin-bottom">{{ category }}</h5> <h5 class="margin-bottom">{{ category }}</h5>
<div class="row gap-2x"> <div class="row gap-2x">
{% for product in categories[category] -%} {% for product in categories[category] -%}
<button class="card shadow" @click="addToBasket('{{ product.id }}', 1)"> <button class="card shadow" @click="addToBasketWithMessage('{{ product.id }}', 1)">
<img <img
class="card-image" class="card-image"
alt="image de {{ product.name }}" alt="image de {{ product.name }}"
@@ -251,18 +252,6 @@
}, },
{%- endfor -%} {%- endfor -%}
}; };
const formulas = [
{%- for formula in formulas -%}
{
result: {{ formula.result_id }},
products: [
{%- for product in formula.products.all() -%}
{{ product.id }},
{%- endfor -%}
]
},
{%- endfor -%}
];
const formInitial = [ const formInitial = [
{%- for f in form -%} {%- for f in form -%}
{%- if f.cleaned_data -%} {%- if f.cleaned_data -%}

View File

@@ -1,35 +0,0 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}Product formulas{% endtrans %}
{% endblock %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static("core/components/card.scss") }}">
<link rel="stylesheet" href="{{ static("counter/css/admin.scss") }}">
{% endblock %}
{% block content %}
<main>
<h3 class="margin-bottom">{% trans %}Product formulas{% endtrans %}</h3>
<p>
<a href="{{ url('counter:product_formula_create') }}" class="btn btn-blue">
{% trans %}New formula{% endtrans %}
<i class="fa fa-plus"></i>
</a>
</p>
<ul class="product-group">
{%- for formula in object_list -%}
<li>
<a href="{{ url('counter:product_formula_edit', formula_id=formula.id) }}">
{{ formula.result.name }}
</a>
<a href="{{ url('counter:product_formula_delete', formula_id=formula.id) }}">
<i class="fa fa-trash delete-action"></i>
</a>
</li>
{%- endfor -%}
</ul>
</div>
</main>
{% endblock %}

View File

@@ -89,7 +89,7 @@
:disabled="csvLoading" :disabled="csvLoading"
:aria-busy="csvLoading" :aria-busy="csvLoading"
> >
{% trans %}Download as csv{% endtrans %} <i class="fa fa-file-arrow-down"></i> {% trans %}Download as cvs{% endtrans %} <i class="fa fa-file-arrow-down"></i>
</button> </button>
</div> </div>

View File

@@ -1,59 +0,0 @@
from django.test import TestCase
from counter.baker_recipes import product_recipe
from counter.forms import ProductFormulaForm
class TestFormulaForm(TestCase):
@classmethod
def setUpTestData(cls):
cls.products = product_recipe.make(
selling_price=iter([1.5, 1, 1]),
special_selling_price=iter([1.4, 0.9, 0.9]),
_quantity=3,
_bulk_create=True,
)
def test_ok(self):
form = ProductFormulaForm(
data={
"result": self.products[0].id,
"products": [self.products[1].id, self.products[2].id],
}
)
assert form.is_valid()
formula = form.save()
assert formula.result == self.products[0]
assert set(formula.products.all()) == set(self.products[1:])
def test_price_invalid(self):
self.products[0].selling_price = 2.1
self.products[0].save()
form = ProductFormulaForm(
data={
"result": self.products[0].id,
"products": [self.products[1].id, self.products[2].id],
}
)
assert not form.is_valid()
assert form.errors == {
"result": [
"Le résultat ne peut pas être plus cher "
"que le total des autres produits."
]
}
def test_product_both_in_result_and_products(self):
form = ProductFormulaForm(
data={
"result": self.products[0].id,
"products": [self.products[0].id, self.products[1].id],
}
)
assert not form.is_valid()
assert form.errors == {
"__all__": [
"Un même produit ne peut pas être à la fois "
"le résultat et un élément de la formule."
]
}

View File

@@ -15,9 +15,8 @@ from pytest_django.asserts import assertNumQueries, assertRedirects
from club.models import Club from club.models import Club
from core.baker_recipes import board_user, subscriber_user from core.baker_recipes import board_user, subscriber_user
from core.models import Group, User from core.models import Group, User
from counter.baker_recipes import product_recipe
from counter.forms import ProductForm from counter.forms import ProductForm
from counter.models import Product, ProductFormula, ProductType from counter.models import Product, ProductType
@pytest.mark.django_db @pytest.mark.django_db
@@ -94,9 +93,6 @@ class TestCreateProduct(TestCase):
def setUpTestData(cls): def setUpTestData(cls):
cls.product_type = baker.make(ProductType) cls.product_type = baker.make(ProductType)
cls.club = baker.make(Club) cls.club = baker.make(Club)
cls.counter_admin = baker.make(
User, groups=[Group.objects.get(id=settings.SITH_GROUP_COUNTER_ADMIN_ID)]
)
cls.data = { cls.data = {
"name": "foo", "name": "foo",
"description": "bar", "description": "bar",
@@ -120,36 +116,13 @@ class TestCreateProduct(TestCase):
assert instance.name == "foo" assert instance.name == "foo"
assert instance.selling_price == 1.0 assert instance.selling_price == 1.0
def test_form_with_product_from_formula(self):
"""Test when the edited product is a result of a formula."""
self.client.force_login(self.counter_admin)
products = product_recipe.make(
selling_price=iter([1.5, 1, 1]),
special_selling_price=iter([1.4, 0.9, 0.9]),
_quantity=3,
_bulk_create=True,
)
baker.make(ProductFormula, result=products[0], products=products[1:])
data = self.data | {"selling_price": 1.7, "special_selling_price": 1.5}
form = ProductForm(data=data, instance=products[0])
assert form.is_valid()
# it shouldn't be possible to give a price higher than the formula's products
data = self.data | {"selling_price": 2.1, "special_selling_price": 1.9}
form = ProductForm(data=data, instance=products[0])
assert not form.is_valid()
assert form.errors == {
"selling_price": [
"Assurez-vous que cette valeur est inférieure ou égale à 2.00."
],
"special_selling_price": [
"Assurez-vous que cette valeur est inférieure ou égale à 1.80."
],
}
def test_view(self): def test_view(self):
self.client.force_login(self.counter_admin) self.client.force_login(
baker.make(
User,
groups=[Group.objects.get(id=settings.SITH_GROUP_COUNTER_ADMIN_ID)],
)
)
url = reverse("counter:new_product") url = reverse("counter:new_product")
response = self.client.get(url) response = self.client.get(url)
assert response.status_code == 200 assert response.status_code == 200

View File

@@ -25,10 +25,6 @@ from counter.views.admin import (
CounterStatView, CounterStatView,
ProductCreateView, ProductCreateView,
ProductEditView, ProductEditView,
ProductFormulaCreateView,
ProductFormulaDeleteView,
ProductFormulaEditView,
ProductFormulaListView,
ProductListView, ProductListView,
ProductTypeCreateView, ProductTypeCreateView,
ProductTypeEditView, ProductTypeEditView,
@@ -120,24 +116,6 @@ urlpatterns = [
ProductEditView.as_view(), ProductEditView.as_view(),
name="product_edit", name="product_edit",
), ),
path(
"admin/formula/", ProductFormulaListView.as_view(), name="product_formula_list"
),
path(
"admin/formula/new/",
ProductFormulaCreateView.as_view(),
name="product_formula_create",
),
path(
"admin/formula/<int:formula_id>/edit",
ProductFormulaEditView.as_view(),
name="product_formula_edit",
),
path(
"admin/formula/<int:formula_id>/delete",
ProductFormulaDeleteView.as_view(),
name="product_formula_delete",
),
path( path(
"admin/product-type/list/", "admin/product-type/list/",
ProductTypeListView.as_view(), ProductTypeListView.as_view(),

View File

@@ -34,13 +34,11 @@ from counter.forms import (
CloseCustomerAccountForm, CloseCustomerAccountForm,
CounterEditForm, CounterEditForm,
ProductForm, ProductForm,
ProductFormulaForm,
ReturnableProductForm, ReturnableProductForm,
) )
from counter.models import ( from counter.models import (
Counter, Counter,
Product, Product,
ProductFormula,
ProductType, ProductType,
Refilling, Refilling,
ReturnableProduct, ReturnableProduct,
@@ -164,49 +162,6 @@ class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
current_tab = "products" current_tab = "products"
class ProductFormulaListView(CounterAdminTabsMixin, PermissionRequiredMixin, ListView):
model = ProductFormula
queryset = ProductFormula.objects.select_related("result")
template_name = "counter/formula_list.jinja"
current_tab = "formulas"
permission_required = "counter.view_productformula"
class ProductFormulaCreateView(
CounterAdminTabsMixin, PermissionRequiredMixin, CreateView
):
model = ProductFormula
form_class = ProductFormulaForm
pk_url_kwarg = "formula_id"
template_name = "core/create.jinja"
current_tab = "formulas"
success_url = reverse_lazy("counter:product_formula_list")
permission_required = "counter.add_productformula"
class ProductFormulaEditView(
CounterAdminTabsMixin, PermissionRequiredMixin, UpdateView
):
model = ProductFormula
form_class = ProductFormulaForm
pk_url_kwarg = "formula_id"
template_name = "core/edit.jinja"
current_tab = "formulas"
success_url = reverse_lazy("counter:product_formula_list")
permission_required = "counter.change_productformula"
class ProductFormulaDeleteView(
CounterAdminTabsMixin, PermissionRequiredMixin, DeleteView
):
model = ProductFormula
pk_url_kwarg = "formula_id"
template_name = "core/delete_confirm.jinja"
current_tab = "formulas"
success_url = reverse_lazy("counter:product_formula_list")
permission_required = "counter.delete_productformula"
class ReturnableProductListView( class ReturnableProductListView(
CounterAdminTabsMixin, PermissionRequiredMixin, ListView CounterAdminTabsMixin, PermissionRequiredMixin, ListView
): ):

View File

@@ -12,7 +12,6 @@
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #
from collections import defaultdict
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import transaction from django.db import transaction
@@ -32,7 +31,6 @@ from counter.forms import BasketForm, RefillForm
from counter.models import ( from counter.models import (
Counter, Counter,
Customer, Customer,
ProductFormula,
ReturnableProduct, ReturnableProduct,
Selling, Selling,
) )
@@ -208,13 +206,12 @@ class CounterClick(
"""Add customer to the context.""" """Add customer to the context."""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["products"] = self.products kwargs["products"] = self.products
kwargs["formulas"] = ProductFormula.objects.filter( kwargs["categories"] = {}
result__in=self.products
).prefetch_related("products")
kwargs["categories"] = defaultdict(list)
for product in kwargs["products"]: for product in kwargs["products"]:
if product.product_type: if product.product_type:
kwargs["categories"][product.product_type].append(product) kwargs["categories"].setdefault(product.product_type, []).append(
product
)
kwargs["customer"] = self.customer kwargs["customer"] = self.customer
kwargs["cancel_url"] = self.get_success_url() kwargs["cancel_url"] = self.get_success_url()

View File

@@ -100,11 +100,6 @@ class CounterAdminTabsMixin(TabedViewMixin):
"slug": "products", "slug": "products",
"name": _("Products"), "name": _("Products"),
}, },
{
"url": reverse_lazy("counter:product_formula_list"),
"slug": "formulas",
"name": _("Formulas"),
},
{ {
"url": reverse_lazy("counter:product_type_list"), "url": reverse_lazy("counter:product_type_list"),
"slug": "product_types", "slug": "product_types",

View File

@@ -177,7 +177,7 @@ from django.conf import settings
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("pedagogy", "0003_alter_uv_language"), ("pedagogy", "0003_alter_ue_language"),
] ]
operations = [ operations = [
@@ -215,11 +215,12 @@ On modifie donc le modèle :
```python ```python
from django.db import models from django.db import models
from core.models import User from core.models import User
from pedagogy.models import UV from pedagogy.models import UE
class UserUe(models.Model): class UserUe(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
ue = models.ForeignKey(UV, on_delete=models.CASCADE) ue = models.ForeignKey(UE, on_delete=models.CASCADE)
``` ```
On refait la commande `makemigrations` et on obtient : On refait la commande `makemigrations` et on obtient :
@@ -237,7 +238,7 @@ class Migration(migrations.Migration):
model_name="userue", model_name="userue",
name="ue", name="ue",
field=models.ForeignKey( field=models.ForeignKey(
on_delete=models.deletion.CASCADE, to="pedagogy.uv" on_delete=models.deletion.CASCADE, to="pedagogy.ue"
), ),
), ),
] ]
@@ -280,7 +281,7 @@ python ./manage.py squasmigrations <app> <migration de début (incluse)> <migrat
Par exemple, dans notre cas, ça donnera : Par exemple, dans notre cas, ça donnera :
```bash ```bash
python ./manage.py squasmigrations pedagogy 0004 0005 python ./manage.py squashmigrations pedagogy 0004 0005
``` ```
La commande vous donnera ceci : La commande vous donnera ceci :
@@ -292,7 +293,7 @@ class Migration(migrations.Migration):
replaces = [("pedagogy", "0004_userue"), ("pedagogy", "0005_alter_userue_ue")] replaces = [("pedagogy", "0004_userue"), ("pedagogy", "0005_alter_userue_ue")]
dependencies = [ dependencies = [
("pedagogy", "0003_alter_uv_language"), ("pedagogy", "0003_alter_ue_language"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
@@ -312,7 +313,7 @@ class Migration(migrations.Migration):
( (
"ue", "ue",
models.ForeignKey( models.ForeignKey(
on_delete=models.deletion.CASCADE, to="pedagogy.uv" on_delete=models.deletion.CASCADE, to="pedagogy.ue"
), ),
), ),
( (

View File

@@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-12-17 00:03+0100\n" "POT-Creation-Date: 2025-12-19 23:10+0100\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"
@@ -218,7 +218,7 @@ msgid "You can not make loops in clubs"
msgstr "Vous ne pouvez pas faire de boucles dans les clubs" msgstr "Vous ne pouvez pas faire de boucles dans les clubs"
#: club/models.py core/models.py counter/models.py eboutic/models.py #: club/models.py core/models.py counter/models.py eboutic/models.py
#: election/models.py sas/models.py trombi/models.py #: election/models.py pedagogy/models.py sas/models.py trombi/models.py
msgid "user" msgid "user"
msgstr "utilisateur" msgstr "utilisateur"
@@ -388,7 +388,7 @@ msgstr "Montrer"
#: club/templates/club/club_sellings.jinja #: club/templates/club/club_sellings.jinja
#: counter/templates/counter/product_list.jinja #: counter/templates/counter/product_list.jinja
msgid "Download as csv" msgid "Download as cvs"
msgstr "Télécharger en CSV" msgstr "Télécharger en CSV"
#: club/templates/club/club_sellings.jinja #: club/templates/club/club_sellings.jinja
@@ -470,14 +470,15 @@ msgstr "Méthode de paiement"
#: core/templates/core/file_detail.jinja #: core/templates/core/file_detail.jinja
#: core/templates/core/file_moderation.jinja #: core/templates/core/file_moderation.jinja
#: core/templates/core/group_detail.jinja core/templates/core/group_list.jinja #: core/templates/core/group_detail.jinja core/templates/core/group_list.jinja
#: core/templates/core/macros.jinja core/templates/core/page/prop.jinja #: core/templates/core/page/prop.jinja
#: core/templates/core/user_account_detail.jinja #: core/templates/core/user_account_detail.jinja
#: core/templates/core/user_clubs.jinja core/templates/core/user_edit.jinja #: core/templates/core/user_clubs.jinja core/templates/core/user_edit.jinja
#: core/templates/core/user_godfathers.jinja
#: counter/templates/counter/fragments/create_student_card.jinja #: counter/templates/counter/fragments/create_student_card.jinja
#: counter/templates/counter/last_ops.jinja #: counter/templates/counter/last_ops.jinja
#: election/templates/election/election_detail.jinja #: election/templates/election/election_detail.jinja
#: forum/templates/forum/macros.jinja pedagogy/templates/pedagogy/guide.jinja #: forum/templates/forum/macros.jinja pedagogy/templates/pedagogy/guide.jinja
#: pedagogy/templates/pedagogy/uv_detail.jinja sas/templates/sas/album.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja sas/templates/sas/album.jinja
#: sas/templates/sas/moderation.jinja sas/templates/sas/picture.jinja #: sas/templates/sas/moderation.jinja sas/templates/sas/picture.jinja
#: trombi/templates/trombi/detail.jinja #: trombi/templates/trombi/detail.jinja
#: trombi/templates/trombi/edit_profile.jinja #: trombi/templates/trombi/edit_profile.jinja
@@ -672,7 +673,7 @@ msgstr "Outils"
#: counter/templates/counter/counter_list.jinja #: counter/templates/counter/counter_list.jinja
#: election/templates/election/election_detail.jinja #: election/templates/election/election_detail.jinja
#: forum/templates/forum/macros.jinja pedagogy/templates/pedagogy/guide.jinja #: forum/templates/forum/macros.jinja pedagogy/templates/pedagogy/guide.jinja
#: pedagogy/templates/pedagogy/uv_detail.jinja sas/templates/sas/album.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja sas/templates/sas/album.jinja
#: trombi/templates/trombi/detail.jinja #: trombi/templates/trombi/detail.jinja
#: trombi/templates/trombi/edit_profile.jinja #: trombi/templates/trombi/edit_profile.jinja
msgid "Edit" msgid "Edit"
@@ -1076,9 +1077,9 @@ msgstr "Liens"
msgid "Our services" msgid "Our services"
msgstr "Nos services" msgstr "Nos services"
#: com/templates/com/news_list.jinja #: com/templates/com/news_list.jinja pedagogy/templates/pedagogy/guide.jinja
msgid "UV Guide" msgid "UE Guide"
msgstr "Guide des UVs" msgstr "Guide des UEs"
#: com/templates/com/news_list.jinja #: com/templates/com/news_list.jinja
msgid "Timetable" msgid "Timetable"
@@ -1214,7 +1215,7 @@ msgstr "Descendre"
#: com/templates/com/weekmail_preview.jinja #: com/templates/com/weekmail_preview.jinja
#: core/templates/core/user_account_detail.jinja #: core/templates/core/user_account_detail.jinja
#: pedagogy/templates/pedagogy/uv_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
#: trombi/templates/trombi/comment_moderation.jinja #: trombi/templates/trombi/comment_moderation.jinja
#: trombi/templates/trombi/export.jinja #: trombi/templates/trombi/export.jinja
msgid "Back" msgid "Back"
@@ -2062,7 +2063,7 @@ msgstr "Éditer le groupe"
#: core/templates/core/group_edit.jinja core/templates/core/user_edit.jinja #: core/templates/core/group_edit.jinja core/templates/core/user_edit.jinja
#: core/templates/core/user_group.jinja #: core/templates/core/user_group.jinja
#: pedagogy/templates/pedagogy/uv_edit.jinja #: pedagogy/templates/pedagogy/ue_edit.jinja
msgid "Update" msgid "Update"
msgstr "Mettre à jour" msgstr "Mettre à jour"
@@ -2787,8 +2788,8 @@ msgid "Subscription stats"
msgstr "Statistiques de cotisation" msgstr "Statistiques de cotisation"
#: core/templates/core/user_tools.jinja pedagogy/templates/pedagogy/guide.jinja #: core/templates/core/user_tools.jinja pedagogy/templates/pedagogy/guide.jinja
msgid "Create UV" msgid "Create UE"
msgstr "Créer UV" msgstr "Créer UE"
#: core/templates/core/user_tools.jinja pedagogy/templates/pedagogy/guide.jinja #: core/templates/core/user_tools.jinja pedagogy/templates/pedagogy/guide.jinja
#: trombi/templates/trombi/detail.jinja #: trombi/templates/trombi/detail.jinja
@@ -2880,10 +2881,6 @@ msgstr "Fillot / Fillote"
msgid "Select user" msgid "Select user"
msgstr "Choisir un utilisateur" msgstr "Choisir un utilisateur"
#: core/views/forms.py
msgid "This user does not exist"
msgstr "Cet utilisateur n'existe pas"
#: core/views/forms.py #: core/views/forms.py
msgid "You cannot be related to yourself" msgid "You cannot be related to yourself"
msgstr "Vous ne pouvez pas être relié à vous-même" msgstr "Vous ne pouvez pas être relié à vous-même"
@@ -2960,38 +2957,6 @@ msgstr ""
"Décrivez le produit. Si c'est un click pour un évènement, donnez quelques " "Décrivez le produit. Si c'est un click pour un évènement, donnez quelques "
"détails dessus, comme la date (en incluant l'année)." "détails dessus, comme la date (en incluant l'année)."
#: counter/forms.py
#, python-format
msgid ""
"This product is a formula. Its price cannot be greater than the price of the "
"products constituting it, which is %(price)s"
msgstr ""
"Ce produit est une formule. Son prix ne peut pas être supérieur au prix des "
"produits qui la constituent, soit %(price)s €."
#: counter/forms.py
#, python-format
msgid ""
"This product is a formula. Its special price cannot be greater than the "
"price of the products constituting it, which is %(price)s"
msgstr ""
"Ce produit est une formule. Son prix spécial ne peut pas être supérieur au "
"prix des produits qui la constituent, soit %(price)s €."
#: counter/forms.py
msgid ""
"The same product cannot be at the same time the result and a part of the "
"formula."
msgstr ""
"Un même produit ne peut pas être à la fois le résultat et un élément de la "
"formule."
#: counter/forms.py
msgid ""
"The result cannot be more expensive than the total of the other products."
msgstr ""
"Le résultat ne peut pas être plus cher que le total des autres produits."
#: counter/forms.py #: counter/forms.py
msgid "Refound this account" msgid "Refound this account"
msgstr "Rembourser ce compte" msgstr "Rembourser ce compte"
@@ -3152,18 +3117,6 @@ msgstr "produit"
msgid "products" msgid "products"
msgstr "produits" msgstr "produits"
#: counter/models.py
msgid "The products that constitute this formula."
msgstr "Les produits qui constituent cette formule."
#: counter/models.py
msgid "result product"
msgstr "produit résultat"
#: counter/models.py
msgid "The product got with the formula."
msgstr "Le produit obtenu par la formule."
#: counter/models.py #: counter/models.py
msgid "counter type" msgid "counter type"
msgstr "type de comptoir" msgstr "type de comptoir"
@@ -3431,7 +3384,7 @@ msgstr "Coffre vidé"
#: counter/templates/counter/cash_summary_list.jinja counter/views/cash.py #: counter/templates/counter/cash_summary_list.jinja counter/views/cash.py
#: pedagogy/templates/pedagogy/moderation.jinja #: pedagogy/templates/pedagogy/moderation.jinja
#: pedagogy/templates/pedagogy/uv_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
#: trombi/templates/trombi/comment.jinja #: trombi/templates/trombi/comment.jinja
#: trombi/templates/trombi/user_tools.jinja #: trombi/templates/trombi/user_tools.jinja
msgid "Comment" msgid "Comment"
@@ -3584,14 +3537,6 @@ msgstr "Nouveau eticket"
msgid "There is no eticket in this website." msgid "There is no eticket in this website."
msgstr "Il n'y a pas de eticket sur ce site web." msgstr "Il n'y a pas de eticket sur ce site web."
#: counter/templates/counter/formula_list.jinja
msgid "Product formulas"
msgstr "Formules de produits"
#: counter/templates/counter/formula_list.jinja
msgid "New formula"
msgstr "Nouvelle formule"
#: counter/templates/counter/fragments/create_student_card.jinja #: counter/templates/counter/fragments/create_student_card.jinja
msgid "No student card registered." msgid "No student card registered."
msgstr "Aucune carte étudiante enregistrée." msgstr "Aucune carte étudiante enregistrée."
@@ -3931,10 +3876,6 @@ msgstr "Dernières opérations"
msgid "Counter administration" msgid "Counter administration"
msgstr "Administration des comptoirs" msgstr "Administration des comptoirs"
#: counter/views/mixins.py
msgid "Formulas"
msgstr "Formules"
#: counter/views/mixins.py #: counter/views/mixins.py
msgid "Product types" msgid "Product types"
msgstr "Types de produit" msgstr "Types de produit"
@@ -4461,8 +4402,8 @@ msgid "Do not vote"
msgstr "Ne pas voter" msgstr "Ne pas voter"
#: pedagogy/forms.py #: pedagogy/forms.py
msgid "This user has already commented on this UV" msgid "This user has already commented on this UE"
msgstr "Cet utilisateur a déjà commenté cette UV" msgstr "Cet utilisateur a déjà commenté cette UE"
#: pedagogy/forms.py #: pedagogy/forms.py
msgid "Accepted reports" msgid "Accepted reports"
@@ -4474,10 +4415,10 @@ msgstr "Signalements refusés"
#: pedagogy/models.py #: pedagogy/models.py
msgid "" msgid ""
"The code of an UV must only contains uppercase characters without accent and " "The code of an UE must only contains uppercase characters without accent and "
"numbers" "numbers"
msgstr "" msgstr ""
"Le code d'une UV doit seulement contenir des caractères majuscule sans " "Le code d'une UE doit seulement contenir des caractères majuscule sans "
"accents et nombres" "accents et nombres"
#: pedagogy/models.py #: pedagogy/models.py
@@ -4485,8 +4426,8 @@ msgid "credit type"
msgstr "type de crédit" msgstr "type de crédit"
#: pedagogy/models.py #: pedagogy/models.py
msgid "uv manager" msgid "ue manager"
msgstr "gestionnaire d'uv" msgstr "gestionnaire d'ue"
#: pedagogy/models.py #: pedagogy/models.py
msgid "language" msgid "language"
@@ -4537,7 +4478,7 @@ msgid "hours TE"
msgstr "heures TE" msgstr "heures TE"
#: pedagogy/models.py #: pedagogy/models.py
msgid "uv" msgid "ue"
msgstr "UE" msgstr "UE"
#: pedagogy/models.py #: pedagogy/models.py
@@ -4576,10 +4517,6 @@ msgstr "signaler"
msgid "reporter" msgid "reporter"
msgstr "signalant" msgstr "signalant"
#: pedagogy/templates/pedagogy/guide.jinja
msgid "UE Guide"
msgstr "Guide des UEs"
#: pedagogy/templates/pedagogy/guide.jinja #: pedagogy/templates/pedagogy/guide.jinja
msgid "A guide of courses available at UTBM." msgid "A guide of courses available at UTBM."
msgstr "Un guide de tous les cours disponibles à l'UTBM." msgstr "Un guide de tous les cours disponibles à l'UTBM."
@@ -4596,7 +4533,7 @@ msgstr "%(credit_type)s"
#: pedagogy/templates/pedagogy/guide.jinja #: pedagogy/templates/pedagogy/guide.jinja
#: pedagogy/templates/pedagogy/moderation.jinja #: pedagogy/templates/pedagogy/moderation.jinja
msgid "UV" msgid "UE"
msgstr "UE" msgstr "UE"
#: pedagogy/templates/pedagogy/guide.jinja #: pedagogy/templates/pedagogy/guide.jinja
@@ -4608,16 +4545,16 @@ msgid "Credit type"
msgstr "Type de crédit" msgstr "Type de crédit"
#: pedagogy/templates/pedagogy/guide.jinja #: pedagogy/templates/pedagogy/guide.jinja
msgid "closed uv" msgid "closed ue"
msgstr "uv fermée" msgstr "ue fermée"
#: pedagogy/templates/pedagogy/macros.jinja #: pedagogy/templates/pedagogy/macros.jinja
msgid " not rated " msgid " not rated "
msgstr "non noté" msgstr "non noté"
#: pedagogy/templates/pedagogy/moderation.jinja #: pedagogy/templates/pedagogy/moderation.jinja
msgid "UV comment moderation" msgid "UE comment moderation"
msgstr "Modération des commentaires d'UV" msgstr "Modération des commentaires d'UE"
#: pedagogy/templates/pedagogy/moderation.jinja #: pedagogy/templates/pedagogy/moderation.jinja
#: rootplace/templates/rootplace/userban.jinja sas/models.py #: rootplace/templates/rootplace/userban.jinja sas/models.py
@@ -4632,96 +4569,96 @@ msgstr "Supprimer commentaire"
msgid "Delete report" msgid "Delete report"
msgstr "Supprimer signalement" msgstr "Supprimer signalement"
#: pedagogy/templates/pedagogy/uv_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "UV Details" msgid "UE Details"
msgstr "Détails d'UV" msgstr "Détails d'UE"
#: pedagogy/templates/pedagogy/uv_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "CM: " msgid "CM: "
msgstr "CM : " msgstr "CM : "
#: pedagogy/templates/pedagogy/uv_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "TD: " msgid "TD: "
msgstr "TD : " msgstr "TD : "
#: pedagogy/templates/pedagogy/uv_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "TP: " msgid "TP: "
msgstr "TP : " msgstr "TP : "
#: pedagogy/templates/pedagogy/uv_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "TE: " msgid "TE: "
msgstr "TE : " msgstr "TE : "
#: pedagogy/templates/pedagogy/uv_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "THE: " msgid "THE: "
msgstr "THE : " msgstr "THE : "
#: pedagogy/templates/pedagogy/uv_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Global grade" msgid "Global grade"
msgstr "Note globale" msgstr "Note globale"
#: pedagogy/templates/pedagogy/uv_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Utility" msgid "Utility"
msgstr "Utilité" msgstr "Utilité"
#: pedagogy/templates/pedagogy/uv_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Interest" msgid "Interest"
msgstr "Intérêt" msgstr "Intérêt"
#: pedagogy/templates/pedagogy/uv_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Teaching" msgid "Teaching"
msgstr "Enseignement" msgstr "Enseignement"
#: pedagogy/templates/pedagogy/uv_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Work load" msgid "Work load"
msgstr "Charge de travail" msgstr "Charge de travail"
#: pedagogy/templates/pedagogy/uv_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Objectives" msgid "Objectives"
msgstr "Objectifs" msgstr "Objectifs"
#: pedagogy/templates/pedagogy/uv_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Program" msgid "Program"
msgstr "Programme" msgstr "Programme"
#: pedagogy/templates/pedagogy/uv_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Earned skills" msgid "Earned skills"
msgstr "Compétences acquises" msgstr "Compétences acquises"
#: pedagogy/templates/pedagogy/uv_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Key concepts" msgid "Key concepts"
msgstr "Concepts clefs" msgstr "Concepts clefs"
#: pedagogy/templates/pedagogy/uv_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "UE manager: " msgid "UE manager: "
msgstr "Gestionnaire d'UE : " msgstr "Gestionnaire d'UE : "
#: pedagogy/templates/pedagogy/uv_detail.jinja pedagogy/tests/tests.py #: pedagogy/templates/pedagogy/ue_detail.jinja pedagogy/tests/tests.py
msgid "" msgid ""
"You already posted a comment on this UV. If you want to comment again, " "You already posted a comment on this UE. If you want to comment again, "
"please modify or delete your previous comment." "please modify or delete your previous comment."
msgstr "" msgstr ""
"Vous avez déjà commenté cette UV. Si vous voulez de nouveau commenter, " "Vous avez déjà commenté cette UE. Si vous voulez de nouveau commenter, "
"veuillez modifier ou supprimer votre commentaire précédent." "veuillez modifier ou supprimer votre commentaire précédent."
#: pedagogy/templates/pedagogy/uv_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Leave comment" msgid "Leave comment"
msgstr "Laisser un commentaire" msgstr "Laisser un commentaire"
#: pedagogy/templates/pedagogy/uv_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
#: trombi/templates/trombi/export.jinja #: trombi/templates/trombi/export.jinja
msgid "Comments" msgid "Comments"
msgstr "Commentaires" msgstr "Commentaires"
#: pedagogy/templates/pedagogy/uv_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "This comment has been reported" msgid "This comment has been reported"
msgstr "Ce commentaire a été signalé" msgstr "Ce commentaire a été signalé"
#: pedagogy/templates/pedagogy/uv_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Report this comment" msgid "Report this comment"
msgstr "Signaler ce commentaire" msgstr "Signaler ce commentaire"
#: pedagogy/templates/pedagogy/uv_edit.jinja #: pedagogy/templates/pedagogy/ue_edit.jinja
msgid "Edit UE" msgid "Edit UE"
msgstr "Éditer l'UE" msgstr "Éditer l'UE"

View File

@@ -7,7 +7,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-26 15:45+0100\n" "POT-Creation-Date: 2025-08-23 15:30+0200\n"
"PO-Revision-Date: 2024-09-17 11:54+0200\n" "PO-Revision-Date: 2024-09-17 11:54+0200\n"
"Last-Translator: Sli <antoine@bartuccio.fr>\n" "Last-Translator: Sli <antoine@bartuccio.fr>\n"
"Language-Team: AE info <ae.info@utbm.fr>\n" "Language-Team: AE info <ae.info@utbm.fr>\n"
@@ -206,10 +206,6 @@ msgstr "capture.%s"
msgid "Not enough money" msgid "Not enough money"
msgstr "Pas assez d'argent" msgstr "Pas assez d'argent"
#: counter/static/bundled/counter/counter-click-index.ts
msgid "Formula %(formula)s applied"
msgstr "Formule %(formula)s appliquée"
#: counter/static/bundled/counter/counter-click-index.ts #: counter/static/bundled/counter/counter-click-index.ts
msgid "You can't send an empty basket." msgid "You can't send an empty basket."
msgstr "Vous ne pouvez pas envoyer un panier vide." msgstr "Vous ne pouvez pas envoyer un panier vide."
@@ -266,9 +262,3 @@ msgstr "Il n'a pas été possible de modérer l'image"
#: sas/static/bundled/sas/viewer-index.ts #: sas/static/bundled/sas/viewer-index.ts
msgid "Couldn't delete picture" msgid "Couldn't delete picture"
msgstr "Il n'a pas été possible de supprimer l'image" msgstr "Il n'a pas été possible de supprimer l'image"
#: timetable/static/bundled/timetable/generator-index.ts
msgid ""
"Wrong timetable format. Make sure you copied if from your student folder."
msgstr ""
"Mauvais format d'emploi du temps. Assurez-vous que vous l'avez copié depuis votre dossier étudiants."

View File

@@ -23,35 +23,35 @@
from django.contrib import admin from django.contrib import admin
from haystack.admin import SearchModelAdmin from haystack.admin import SearchModelAdmin
from pedagogy.models import UV, UVComment, UVCommentReport from pedagogy.models import UE, UEComment, UECommentReport
@admin.register(UV) @admin.register(UE)
class UVAdmin(admin.ModelAdmin): class UEAdmin(admin.ModelAdmin):
list_display = ("code", "title", "credit_type", "credits", "department") list_display = ("code", "title", "credit_type", "credits", "department")
search_fields = ("code", "title", "department") search_fields = ("code", "title", "department")
autocomplete_fields = ("author",) autocomplete_fields = ("author",)
@admin.register(UVComment) @admin.register(UEComment)
class UVCommentAdmin(admin.ModelAdmin): class UECommentAdmin(admin.ModelAdmin):
list_display = ("author", "uv", "grade_global", "publish_date") list_display = ("author", "ue", "grade_global", "publish_date")
search_fields = ( search_fields = (
"author__username", "author__username",
"author__first_name", "author__first_name",
"author__last_name", "author__last_name",
"uv__code", "ue__code",
) )
autocomplete_fields = ("author",) autocomplete_fields = ("author",)
@admin.register(UVCommentReport) @admin.register(UECommentReport)
class UVCommentReportAdmin(SearchModelAdmin): class UECommentReportAdmin(SearchModelAdmin):
list_display = ("reporter", "uv") list_display = ("reporter", "ue")
search_fields = ( search_fields = (
"reporter__username", "reporter__username",
"reporter__first_name", "reporter__first_name",
"reporter__last_name", "reporter__last_name",
"comment__uv__code", "comment__ue__code",
) )
autocomplete_fields = ("reporter",) autocomplete_fields = ("reporter",)

View File

@@ -10,23 +10,23 @@ from ninja_extra.pagination import PageNumberPaginationExtra, PaginatedResponseS
from api.auth import ApiKeyAuth from api.auth import ApiKeyAuth
from api.permissions import HasPerm from api.permissions import HasPerm
from pedagogy.models import UV from pedagogy.models import UE
from pedagogy.schemas import SimpleUvSchema, UvFilterSchema, UvSchema from pedagogy.schemas import SimpleUeSchema, UeFilterSchema, UeSchema
from pedagogy.utbm_api import UtbmApiClient from pedagogy.utbm_api import UtbmApiClient
@api_controller("/uv") @api_controller("/ue")
class UvController(ControllerBase): class UeController(ControllerBase):
@route.get( @route.get(
"/{code}", "/{code}",
auth=[ApiKeyAuth(), SessionAuth()], auth=[ApiKeyAuth(), SessionAuth()],
permissions=[ permissions=[
# this route will almost always be called in the context # this route will almost always be called in the context
# of a UV creation/edition # of a UE creation/edition
HasPerm(["pedagogy.add_uv", "pedagogy.change_uv"], op=operator.or_) HasPerm(["pedagogy.add_ue", "pedagogy.change_ue"], op=operator.or_)
], ],
url_name="fetch_uv_from_utbm", url_name="fetch_ue_from_utbm",
response=UvSchema, response=UeSchema,
) )
def fetch_from_utbm_api( def fetch_from_utbm_api(
self, self,
@@ -34,20 +34,20 @@ class UvController(ControllerBase):
lang: Query[str] = "fr", lang: Query[str] = "fr",
year: Query[Annotated[int, Ge(2010)] | None] = None, year: Query[Annotated[int, Ge(2010)] | None] = None,
): ):
"""Fetch UV data from the UTBM API and returns it after some parsing.""" """Fetch UE data from the UTBM API and returns it after some parsing."""
with UtbmApiClient() as client: with UtbmApiClient() as client:
res = client.find_uv(lang, code, year) res = client.find_ue(lang, code, year)
if res is None: if res is None:
raise NotFound raise NotFound
return res return res
@route.get( @route.get(
"", "",
response=PaginatedResponseSchema[SimpleUvSchema], response=PaginatedResponseSchema[SimpleUeSchema],
url_name="fetch_uvs", url_name="fetch_ues",
auth=[ApiKeyAuth(), SessionAuth()], auth=[ApiKeyAuth(), SessionAuth()],
permissions=[HasPerm("pedagogy.view_uv")], permissions=[HasPerm("pedagogy.view_ue")],
) )
@paginate(PageNumberPaginationExtra, page_size=100) @paginate(PageNumberPaginationExtra, page_size=100)
def fetch_uv_list(self, search: Query[UvFilterSchema]): def fetch_ue_list(self, search: Query[UeFilterSchema]):
return search.filter(UV.objects.order_by("code").values()) return search.filter(UE.objects.order_by("code").values())

View File

@@ -26,14 +26,14 @@ from django.utils.translation import gettext_lazy as _
from core.models import User from core.models import User
from core.views.widgets.markdown import MarkdownInput from core.views.widgets.markdown import MarkdownInput
from pedagogy.models import UV, UVComment, UVCommentReport from pedagogy.models import UE, UEComment, UECommentReport
class UVForm(forms.ModelForm): class UEForm(forms.ModelForm):
"""Form handeling creation and edit of an UV.""" """Form handeling creation and edit of an UE."""
class Meta: class Meta:
model = UV model = UE
fields = ( fields = (
"code", "code",
"author", "author",
@@ -82,14 +82,14 @@ class StarList(forms.NumberInput):
return context return context
class UVCommentForm(forms.ModelForm): class UECommentForm(forms.ModelForm):
"""Form handeling creation and edit of an UVComment.""" """Form handeling creation and edit of an UEComment."""
class Meta: class Meta:
model = UVComment model = UEComment
fields = ( fields = (
"author", "author",
"uv", "ue",
"grade_global", "grade_global",
"grade_utility", "grade_utility",
"grade_interest", "grade_interest",
@@ -100,7 +100,7 @@ class UVCommentForm(forms.ModelForm):
widgets = { widgets = {
"comment": MarkdownInput, "comment": MarkdownInput,
"author": forms.HiddenInput, "author": forms.HiddenInput,
"uv": forms.HiddenInput, "ue": forms.HiddenInput,
"grade_global": StarList(5), "grade_global": StarList(5),
"grade_utility": StarList(5), "grade_utility": StarList(5),
"grade_interest": StarList(5), "grade_interest": StarList(5),
@@ -108,35 +108,35 @@ class UVCommentForm(forms.ModelForm):
"grade_work_load": StarList(5), "grade_work_load": StarList(5),
} }
def __init__(self, author_id, uv_id, is_creation, *args, **kwargs): def __init__(self, author_id, ue_id, is_creation, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields["author"].queryset = User.objects.filter(id=author_id).all() self.fields["author"].queryset = User.objects.filter(id=author_id).all()
self.fields["author"].initial = author_id self.fields["author"].initial = author_id
self.fields["uv"].queryset = UV.objects.filter(id=uv_id).all() self.fields["ue"].queryset = UE.objects.filter(id=ue_id).all()
self.fields["uv"].initial = uv_id self.fields["ue"].initial = ue_id
self.is_creation = is_creation self.is_creation = is_creation
def clean(self): def clean(self):
self.cleaned_data = super().clean() self.cleaned_data = super().clean()
uv = self.cleaned_data.get("uv") ue = self.cleaned_data.get("ue")
author = self.cleaned_data.get("author") author = self.cleaned_data.get("author")
if self.is_creation and uv and author and uv.has_user_already_commented(author): if self.is_creation and ue and author and ue.has_user_already_commented(author):
self.add_error( self.add_error(
None, None,
forms.ValidationError( forms.ValidationError(
_("This user has already commented on this UV"), code="invalid" _("This user has already commented on this UE"), code="invalid"
), ),
) )
return self.cleaned_data return self.cleaned_data
class UVCommentReportForm(forms.ModelForm): class UECommentReportForm(forms.ModelForm):
"""Form handeling creation and edit of an UVReport.""" """Form handeling creation and edit of an UEReport."""
class Meta: class Meta:
model = UVCommentReport model = UECommentReport
fields = ("comment", "reporter", "reason") fields = ("comment", "reporter", "reason")
widgets = { widgets = {
"comment": forms.HiddenInput, "comment": forms.HiddenInput,
@@ -148,22 +148,22 @@ class UVCommentReportForm(forms.ModelForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields["reporter"].queryset = User.objects.filter(id=reporter_id).all() self.fields["reporter"].queryset = User.objects.filter(id=reporter_id).all()
self.fields["reporter"].initial = reporter_id self.fields["reporter"].initial = reporter_id
self.fields["comment"].queryset = UVComment.objects.filter(id=comment_id).all() self.fields["comment"].queryset = UEComment.objects.filter(id=comment_id).all()
self.fields["comment"].initial = comment_id self.fields["comment"].initial = comment_id
class UVCommentModerationForm(forms.Form): class UECommentModerationForm(forms.Form):
"""Form handeling bulk comment deletion.""" """Form handeling bulk comment deletion."""
accepted_reports = forms.ModelMultipleChoiceField( accepted_reports = forms.ModelMultipleChoiceField(
UVCommentReport.objects.all(), UECommentReport.objects.all(),
label=_("Accepted reports"), label=_("Accepted reports"),
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
required=False, required=False,
) )
denied_reports = forms.ModelMultipleChoiceField( denied_reports = forms.ModelMultipleChoiceField(
UVCommentReport.objects.all(), UECommentReport.objects.all(),
label=_("Denied reports"), label=_("Denied reports"),
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
required=False, required=False,

View File

@@ -2,36 +2,36 @@ from django.conf import settings
from django.core.management import BaseCommand from django.core.management import BaseCommand
from core.models import User from core.models import User
from pedagogy.models import UV from pedagogy.models import UE
from pedagogy.schemas import UvSchema from pedagogy.schemas import UeSchema
from pedagogy.utbm_api import UtbmApiClient from pedagogy.utbm_api import UtbmApiClient
class Command(BaseCommand): class Command(BaseCommand):
help = "Update the UV guide" help = "Update the UE guide"
def handle(self, *args, **options): def handle(self, *args, **options):
seen_uvs: set[int] = set() seen_ues: set[int] = set()
root_user = User.objects.get(pk=settings.SITH_ROOT_USER_ID) root_user = User.objects.get(pk=settings.SITH_ROOT_USER_ID)
with UtbmApiClient() as client: with UtbmApiClient() as client:
self.stdout.write( self.stdout.write(
"Fetching UVs from the UTBM API.\n" "Fetching UEs from the UTBM API.\n"
"This may take a few minutes to complete." "This may take a few minutes to complete."
) )
for uv in client.fetch_uvs(): for ue in client.fetch_ues():
db_uv = UV.objects.filter(code=uv.code).first() db_ue = UE.objects.filter(code=ue.code).first()
if db_uv is None: if db_ue is None:
db_uv = UV(code=uv.code, author=root_user) db_ue = UE(code=ue.code, author=root_user)
fields = list(UvSchema.model_fields.keys()) fields = list(UeSchema.model_fields.keys())
fields.remove("id") fields.remove("id")
fields.remove("code") fields.remove("code")
for field in fields: for field in fields:
setattr(db_uv, field, getattr(uv, field)) setattr(db_ue, field, getattr(ue, field))
db_uv.save() db_ue.save()
# if it's a creation, django will set the id when saving, # if it's a creation, django will set the id when saving,
# so at this point, a db_uv will always have an id # so at this point, a db_ue will always have an id
seen_uvs.add(db_uv.id) seen_ues.add(db_ue.id)
# UVs that are in database but have not been returned by the API # UEs that are in database but have not been returned by the API
# are considered as closed UEs # are considered as closed UEs
UV.objects.exclude(id__in=seen_uvs).update(semester="CLOSED") UE.objects.exclude(id__in=seen_ues).update(semester="CLOSED")
self.stdout.write(self.style.SUCCESS("UV guide updated successfully")) self.stdout.write(self.style.SUCCESS("UE guide updated successfully"))

View File

@@ -0,0 +1,140 @@
# Generated by Django 4.2.20 on 2025-04-08 10:12
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("pedagogy", "0003_alter_uv_language"),
]
operations = [
migrations.RenameModel(old_name="UV", new_name="UE"),
migrations.RenameModel(old_name="UVComment", new_name="UEComment"),
migrations.RenameModel(old_name="UVCommentReport", new_name="UECommentReport"),
migrations.RenameModel(old_name="UVResult", new_name="UEResult"),
migrations.RenameField(model_name="ueresult", old_name="uv", new_name="ue"),
migrations.RenameField(model_name="uecomment", old_name="uv", new_name="ue"),
migrations.AlterField(
model_name="ue",
name="credits",
field=models.PositiveIntegerField(verbose_name="credits"),
),
migrations.AlterField(
model_name="ue",
name="hours_CM",
field=models.PositiveIntegerField(default=0, verbose_name="hours CM"),
),
migrations.AlterField(
model_name="ue",
name="hours_TD",
field=models.PositiveIntegerField(default=0, verbose_name="hours TD"),
),
migrations.AlterField(
model_name="ue",
name="hours_TE",
field=models.PositiveIntegerField(default=0, verbose_name="hours TE"),
),
migrations.AlterField(
model_name="ue",
name="hours_THE",
field=models.PositiveIntegerField(default=0, verbose_name="hours THE"),
),
migrations.AlterField(
model_name="ue",
name="hours_TP",
field=models.PositiveIntegerField(default=0, verbose_name="hours TP"),
),
migrations.AlterField(
model_name="ue",
name="manager",
field=models.CharField(max_length=300, verbose_name="ue manager"),
),
migrations.AlterField(
model_name="ueresult",
name="ue",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="results",
to="pedagogy.ue",
verbose_name="ue",
),
),
migrations.AlterField(
model_name="ue",
name="author",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="ue_created",
to=settings.AUTH_USER_MODEL,
verbose_name="author",
),
),
migrations.AlterField(
model_name="ue",
name="code",
field=models.CharField(
max_length=10,
unique=True,
validators=[
django.core.validators.RegexValidator(
message=(
"The code of an UE must only contains "
"uppercase characters without accent and numbers"
),
regex="([A-Z0-9]+)",
)
],
verbose_name="code",
),
),
migrations.AlterField(
model_name="uecomment",
name="author",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="ue_comments",
to=settings.AUTH_USER_MODEL,
verbose_name="author",
),
),
migrations.AlterField(
model_name="uecomment",
name="comment",
field=models.TextField(blank=True, default="", verbose_name="comment"),
),
migrations.AlterField(
model_name="uecomment",
name="ue",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="comments",
to="pedagogy.ue",
verbose_name="ue",
),
),
migrations.AlterField(
model_name="uecommentreport",
name="reporter",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="reported_ue_comment",
to=settings.AUTH_USER_MODEL,
verbose_name="reporter",
),
),
migrations.AlterField(
model_name="ueresult",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="ue_results",
to=settings.AUTH_USER_MODEL,
verbose_name="user",
),
),
]

View File

@@ -36,8 +36,8 @@ from core.models import User
# Create your models here. # Create your models here.
class UV(models.Model): class UE(models.Model):
"""Contains infos about an UV (course).""" """Contains infos about an UE (course)."""
code = models.CharField( code = models.CharField(
_("code"), _("code"),
@@ -47,7 +47,7 @@ class UV(models.Model):
validators.RegexValidator( validators.RegexValidator(
regex="([A-Z0-9]+)", regex="([A-Z0-9]+)",
message=_( message=_(
"The code of an UV must only contains " "The code of an UE must only contains "
"uppercase characters without accent and numbers" "uppercase characters without accent and numbers"
), ),
) )
@@ -55,7 +55,7 @@ class UV(models.Model):
) )
author = models.ForeignKey( author = models.ForeignKey(
User, User,
related_name="uv_created", related_name="ue_created",
verbose_name=_("author"), verbose_name=_("author"),
null=False, null=False,
blank=False, blank=False,
@@ -64,29 +64,23 @@ class UV(models.Model):
credit_type = models.CharField( credit_type = models.CharField(
_("credit type"), _("credit type"),
max_length=10, max_length=10,
choices=settings.SITH_PEDAGOGY_UV_TYPE, choices=settings.SITH_PEDAGOGY_UE_TYPE,
default=settings.SITH_PEDAGOGY_UV_TYPE[0][0], default=settings.SITH_PEDAGOGY_UE_TYPE[0][0],
) )
manager = models.CharField(_("uv manager"), max_length=300) manager = models.CharField(_("ue manager"), max_length=300)
semester = models.CharField( semester = models.CharField(
_("semester"), _("semester"),
max_length=20, max_length=20,
choices=settings.SITH_PEDAGOGY_UV_SEMESTER, choices=settings.SITH_PEDAGOGY_UE_SEMESTER,
default=settings.SITH_PEDAGOGY_UV_SEMESTER[0][0], default=settings.SITH_PEDAGOGY_UE_SEMESTER[0][0],
) )
language = models.CharField( language = models.CharField(
_("language"), _("language"),
max_length=10, max_length=10,
choices=settings.SITH_PEDAGOGY_UV_LANGUAGE, choices=settings.SITH_PEDAGOGY_UE_LANGUAGE,
default=settings.SITH_PEDAGOGY_UV_LANGUAGE[0][0], default=settings.SITH_PEDAGOGY_UE_LANGUAGE[0][0],
) )
credits = models.IntegerField( credits = models.PositiveIntegerField(_("credits"))
_("credits"),
validators=[validators.MinValueValidator(0)],
blank=False,
null=False,
)
# Double star type not implemented yet
department = models.CharField( department = models.CharField(
_("departmenmt"), _("departmenmt"),
@@ -95,9 +89,9 @@ class UV(models.Model):
default=settings.SITH_PROFILE_DEPARTMENTS[-1][0], default=settings.SITH_PROFILE_DEPARTMENTS[-1][0],
) )
# All texts about the UV # All texts about the UE
title = models.CharField(_("title"), max_length=300) title = models.CharField(_("title"), max_length=300)
manager = models.CharField(_("uv manager"), max_length=300) manager = models.CharField(_("ue manager"), max_length=300)
objectives = models.TextField(_("objectives")) objectives = models.TextField(_("objectives"))
program = models.TextField(_("program")) program = models.TextField(_("program"))
skills = models.TextField(_("skills")) skills = models.TextField(_("skills"))
@@ -105,47 +99,17 @@ class UV(models.Model):
# Hours types CM, TD, TP, THE and TE # Hours types CM, TD, TP, THE and TE
# Kind of dirty but I have nothing else in mind for now # Kind of dirty but I have nothing else in mind for now
hours_CM = models.IntegerField( hours_CM = models.PositiveIntegerField(_("hours CM"), default=0)
_("hours CM"), hours_TD = models.PositiveIntegerField(_("hours TD"), default=0)
validators=[validators.MinValueValidator(0)], hours_TP = models.PositiveIntegerField(_("hours TP"), default=0)
blank=False, hours_THE = models.PositiveIntegerField(_("hours THE"), default=0)
null=False, hours_TE = models.PositiveIntegerField(_("hours TE"), default=0)
default=0,
)
hours_TD = models.IntegerField(
_("hours TD"),
validators=[validators.MinValueValidator(0)],
blank=False,
null=False,
default=0,
)
hours_TP = models.IntegerField(
_("hours TP"),
validators=[validators.MinValueValidator(0)],
blank=False,
null=False,
default=0,
)
hours_THE = models.IntegerField(
_("hours THE"),
validators=[validators.MinValueValidator(0)],
blank=False,
null=False,
default=0,
)
hours_TE = models.IntegerField(
_("hours TE"),
validators=[validators.MinValueValidator(0)],
blank=False,
null=False,
default=0,
)
def __str__(self): def __str__(self):
return self.code return self.code
def get_absolute_url(self): def get_absolute_url(self):
return reverse("pedagogy:uv_detail", kwargs={"uv_id": self.id}) return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.id})
def __grade_average_generic(self, field): def __grade_average_generic(self, field):
comments = self.comments.filter(**{field + "__gte": 0}) comments = self.comments.filter(**{field + "__gte": 0})
@@ -160,7 +124,7 @@ class UV(models.Model):
This function checks that no other comment has been posted by a specified user. This function checks that no other comment has been posted by a specified user.
Returns: Returns:
True if the user has already posted a comment on this UV, else False. True if the user has already posted a comment on this UE, else False.
""" """
return self.comments.filter(author=user).exists() return self.comments.filter(author=user).exists()
@@ -185,78 +149,66 @@ class UV(models.Model):
return self.__grade_average_generic("grade_work_load") return self.__grade_average_generic("grade_work_load")
class UVCommentQuerySet(models.QuerySet): class UECommentQuerySet(models.QuerySet):
def viewable_by(self, user: User) -> Self: def viewable_by(self, user: User) -> Self:
if user.has_perms(["pedagogy.view_uvcomment", "pedagogy.view_uvcommentreport"]): if user.has_perms(["pedagogy.view_uecomment", "pedagogy.view_uecommentreport"]):
# the user can view uv comment reports, # the user can view ue comment reports,
# so he can view non-moderated comments # so he can view non-moderated comments
return self return self
if user.has_perm("pedagogy.view_uvcomment"): if user.has_perm("pedagogy.view_uecomment"):
return self.filter(reports=None) return self.filter(reports=None)
return self.filter(author=user) return self.filter(author=user)
def annotate_is_reported(self) -> Self: def annotate_is_reported(self) -> Self:
return self.annotate( return self.annotate(
is_reported=Exists(UVCommentReport.objects.filter(comment=OuterRef("pk"))) is_reported=Exists(UECommentReport.objects.filter(comment=OuterRef("pk")))
) )
class UVComment(models.Model): class UEComment(models.Model):
"""A comment about an UV.""" """A comment about an UE."""
author = models.ForeignKey( author = models.ForeignKey(
User, User,
related_name="uv_comments", related_name="ue_comments",
verbose_name=_("author"), verbose_name=_("author"),
null=False,
blank=False,
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
uv = models.ForeignKey( ue = models.ForeignKey(
UV, related_name="comments", verbose_name=_("uv"), on_delete=models.CASCADE UE, related_name="comments", verbose_name=_("ue"), on_delete=models.CASCADE
) )
comment = models.TextField(_("comment"), blank=True) comment = models.TextField(_("comment"), blank=True, default="")
grade_global = models.IntegerField( grade_global = models.IntegerField(
_("global grade"), _("global grade"),
validators=[validators.MinValueValidator(-1), validators.MaxValueValidator(4)], validators=[validators.MinValueValidator(-1), validators.MaxValueValidator(4)],
blank=False,
null=False,
default=-1, default=-1,
) )
grade_utility = models.IntegerField( grade_utility = models.IntegerField(
_("utility grade"), _("utility grade"),
validators=[validators.MinValueValidator(-1), validators.MaxValueValidator(4)], validators=[validators.MinValueValidator(-1), validators.MaxValueValidator(4)],
blank=False,
null=False,
default=-1, default=-1,
) )
grade_interest = models.IntegerField( grade_interest = models.IntegerField(
_("interest grade"), _("interest grade"),
validators=[validators.MinValueValidator(-1), validators.MaxValueValidator(4)], validators=[validators.MinValueValidator(-1), validators.MaxValueValidator(4)],
blank=False,
null=False,
default=-1, default=-1,
) )
grade_teaching = models.IntegerField( grade_teaching = models.IntegerField(
_("teaching grade"), _("teaching grade"),
validators=[validators.MinValueValidator(-1), validators.MaxValueValidator(4)], validators=[validators.MinValueValidator(-1), validators.MaxValueValidator(4)],
blank=False,
null=False,
default=-1, default=-1,
) )
grade_work_load = models.IntegerField( grade_work_load = models.IntegerField(
_("work load grade"), _("work load grade"),
validators=[validators.MinValueValidator(-1), validators.MaxValueValidator(4)], validators=[validators.MinValueValidator(-1), validators.MaxValueValidator(4)],
blank=False,
null=False,
default=-1, default=-1,
) )
publish_date = models.DateTimeField(_("publish date"), blank=True) publish_date = models.DateTimeField(_("publish date"), blank=True)
objects = UVCommentQuerySet.as_manager() objects = UECommentQuerySet.as_manager()
def __str__(self): def __str__(self):
return f"{self.uv} - {self.author}" return f"{self.ue} - {self.author}"
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.publish_date is None: if self.publish_date is None:
@@ -268,30 +220,32 @@ class UVComment(models.Model):
# to use this model. # to use this model.
# However, it seems that the implementation finally didn't happen. # However, it seems that the implementation finally didn't happen.
# It should be discussed, when possible, of what to do with that : # It should be discussed, when possible, of what to do with that :
# - go on and finally implement the UV results features ? # - go on and finally implement the UE results features ?
# - or fuck go back and remove this model ? # - or fuck go back and remove this model ?
class UVResult(models.Model): class UEResult(models.Model):
"""Results got to an UV. """Results got to an UE.
Views will be implemented after the first release Views will be implemented after the first release
Will list every UV done by an user Will list every UE done by an user
Linked to user Linked to user and ue
uv Contains a grade settings.SITH_PEDAGOGY_UE_RESULT_GRADE
Contains a grade settings.SITH_PEDAGOGY_UV_RESULT_GRADE
a semester (P/A)20xx. a semester (P/A)20xx.
""" """
uv = models.ForeignKey( ue = models.ForeignKey(
UV, related_name="results", verbose_name=_("uv"), on_delete=models.CASCADE UE, related_name="results", verbose_name=_("ue"), on_delete=models.CASCADE
) )
user = models.ForeignKey( user = models.ForeignKey(
User, related_name="uv_results", verbose_name=("user"), on_delete=models.CASCADE User,
related_name="ue_results",
verbose_name=_("user"),
on_delete=models.CASCADE,
) )
grade = models.CharField( grade = models.CharField(
_("grade"), _("grade"),
max_length=10, max_length=10,
choices=settings.SITH_PEDAGOGY_UV_RESULT_GRADE, choices=settings.SITH_PEDAGOGY_UE_RESULT_GRADE,
default=settings.SITH_PEDAGOGY_UV_RESULT_GRADE[0][0], default=settings.SITH_PEDAGOGY_UE_RESULT_GRADE[0][0],
) )
semester = models.CharField( semester = models.CharField(
_("semester"), _("semester"),
@@ -300,21 +254,21 @@ class UVResult(models.Model):
) )
def __str__(self): def __str__(self):
return f"{self.user.username} ; {self.uv.code} ; {self.grade}" return f"{self.user.username} ; {self.ue.code} ; {self.grade}"
class UVCommentReport(models.Model): class UECommentReport(models.Model):
"""Report an inapropriate comment.""" """Report an inapropriate comment."""
comment = models.ForeignKey( comment = models.ForeignKey(
UVComment, UEComment,
related_name="reports", related_name="reports",
verbose_name=_("report"), verbose_name=_("report"),
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
reporter = models.ForeignKey( reporter = models.ForeignKey(
User, User,
related_name="reported_uv_comment", related_name="reported_ue_comment",
verbose_name=_("reporter"), verbose_name=_("reporter"),
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
@@ -324,5 +278,5 @@ class UVCommentReport(models.Model):
return f"{self.reporter.username} : {self.reason}" return f"{self.reporter.username} : {self.reason}"
@cached_property @cached_property
def uv(self): def ue(self):
return self.comment.uv return self.comment.ue

View File

@@ -7,11 +7,11 @@ from ninja import FilterLookup, FilterSchema, ModelSchema, Schema
from pydantic import AliasPath, ConfigDict, Field, TypeAdapter from pydantic import AliasPath, ConfigDict, Field, TypeAdapter
from pydantic.alias_generators import to_camel from pydantic.alias_generators import to_camel
from pedagogy.models import UV from pedagogy.models import UE
class UtbmShortUvSchema(Schema): class UtbmShortUeSchema(Schema):
"""Short representation of an UV in the UTBM API. """Short representation of an UE in the UTBM API.
Notes: Notes:
This schema holds only the fields we actually need. This schema holds only the fields we actually need.
@@ -35,8 +35,8 @@ class WorkloadSchema(Schema):
nbh: int nbh: int
class SemesterUvState(Schema): class SemesterUeState(Schema):
"""The state of the UV during either autumn or spring semester""" """The state of the UE during either autumn or spring semester"""
model_config = ConfigDict(alias_generator=to_camel) model_config = ConfigDict(alias_generator=to_camel)
@@ -44,11 +44,11 @@ class SemesterUvState(Schema):
ouvert: bool ouvert: bool
ShortUvList = TypeAdapter(list[UtbmShortUvSchema]) ShortUeList = TypeAdapter(list[UtbmShortUeSchema])
class UtbmFullUvSchema(Schema): class UtbmFullUeSchema(Schema):
"""Long representation of an UV in the UTBM API.""" """Long representation of an UE in the UTBM API."""
model_config = ConfigDict(alias_generator=to_camel) model_config = ConfigDict(alias_generator=to_camel)
@@ -71,11 +71,11 @@ class UtbmFullUvSchema(Schema):
) )
class SimpleUvSchema(ModelSchema): class SimpleUeSchema(ModelSchema):
"""Our minimal representation of an UV.""" """Our minimal representation of an UE."""
class Meta: class Meta:
model = UV model = UE
fields = [ fields = [
"id", "id",
"title", "title",
@@ -86,11 +86,11 @@ class SimpleUvSchema(ModelSchema):
] ]
class UvSchema(ModelSchema): class UeSchema(ModelSchema):
"""Our complete representation of an UV""" """Our complete representation of an UE"""
class Meta: class Meta:
model = UV model = UE
fields = [ fields = [
"id", "id",
"title", "title",
@@ -113,7 +113,7 @@ class UvSchema(ModelSchema):
] ]
class UvFilterSchema(FilterSchema): class UeFilterSchema(FilterSchema):
search: Annotated[str | None, FilterLookup("code__icontains")] = None search: Annotated[str | None, FilterLookup("code__icontains")] = None
semester: set[Literal["AUTUMN", "SPRING"]] | None = None semester: set[Literal["AUTUMN", "SPRING"]] | None = None
credit_type: Annotated[ credit_type: Annotated[
@@ -132,12 +132,12 @@ class UvFilterSchema(FilterSchema):
return Q() return Q()
if len(value) < 3 or (len(value) < 5 and any(c.isdigit() for c in value)): if len(value) < 3 or (len(value) < 5 and any(c.isdigit() for c in value)):
# Likely to be an UV code # Likely to be an UE code
return Q(code__istartswith=value) return Q(code__istartswith=value)
qs = list( qs = list(
SearchQuerySet() SearchQuerySet()
.models(UV) .models(UE)
.autocomplete(auto=html.escape(value)) .autocomplete(auto=html.escape(value))
.values_list("pk", flat=True) .values_list("pk", flat=True)
) )
@@ -147,7 +147,7 @@ class UvFilterSchema(FilterSchema):
def filter_semester(self, value: set[str] | None) -> Q: def filter_semester(self, value: set[str] | None) -> Q:
"""Special filter for the semester. """Special filter for the semester.
If either "SPRING" or "AUTUMN" is given, UV that are available If either "SPRING" or "AUTUMN" is given, UE that are available
during "AUTUMN_AND_SPRING" will be filtered. during "AUTUMN_AND_SPRING" will be filtered.
""" """
if not value: if not value:

View File

@@ -25,28 +25,28 @@ from django.db import models
from haystack import indexes, signals from haystack import indexes, signals
from core.search_indexes import BigCharFieldIndex from core.search_indexes import BigCharFieldIndex
from pedagogy.models import UV from pedagogy.models import UE
class IndexSignalProcessor(signals.BaseSignalProcessor): class IndexSignalProcessor(signals.BaseSignalProcessor):
"""Auto update index on CRUD operations.""" """Auto update index on CRUD operations."""
def setup(self): def setup(self):
# Listen only to the ``UV`` model. # Listen only to the ``UE`` model.
models.signals.post_save.connect(self.handle_save, sender=UV) models.signals.post_save.connect(self.handle_save, sender=UE)
models.signals.post_delete.connect(self.handle_delete, sender=UV) models.signals.post_delete.connect(self.handle_delete, sender=UE)
def teardown(self): def teardown(self):
# Disconnect only to the ``UV`` model. # Disconnect only to the ``UE`` model.
models.signals.post_save.disconnect(self.handle_save, sender=UV) models.signals.post_save.disconnect(self.handle_save, sender=UE)
models.signals.post_delete.disconnect(self.handle_delete, sender=UV) models.signals.post_delete.disconnect(self.handle_delete, sender=UE)
class UVIndex(indexes.SearchIndex, indexes.Indexable): class UEIndex(indexes.SearchIndex, indexes.Indexable):
"""Indexer class for UVs.""" """Indexer class for UEs."""
text = BigCharFieldIndex(document=True, use_template=True) text = BigCharFieldIndex(document=True, use_template=True)
auto = indexes.EdgeNgramField(use_template=True) auto = indexes.EdgeNgramField(use_template=True)
def get_model(self): def get_model(self):
return UV return UE

View File

@@ -1,12 +1,12 @@
import { History, getCurrentUrlParams, updateQueryString } from "#core:utils/history"; import { History, getCurrentUrlParams, updateQueryString } from "#core:utils/history";
import { uvFetchUvList } from "#openapi"; import { ueFetchUeList } from "#openapi";
const pageDefault = 1; const pageDefault = 1;
const pageSizeDefault = 100; const pageSizeDefault = 100;
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.data("uv_search", () => ({ Alpine.data("ue_search", () => ({
uvs: { ues: {
count: 0, count: 0,
next: null, next: null,
previous: null, previous: null,
@@ -103,16 +103,12 @@ document.addEventListener("alpine:init", () => {
args[param] = value; args[param] = value;
} }
} }
this.uvs = ( this.ues = (await ueFetchUeList({ query: args })).data;
await uvFetchUvList({
query: args,
})
).data;
this.loading = false; this.loading = false;
}, },
maxPage() { maxPage() {
return Math.ceil(this.uvs.count / this.page_size); return Math.ceil(this.ues.count / this.page_size);
}, },
})); }));
}); });

View File

@@ -50,7 +50,7 @@ $large-devices: 992px;
} }
} }
#uv-list { #ue-list {
font-size: 1.1em; font-size: 1.1em;
overflow-wrap: break-word; overflow-wrap: break-word;
@@ -164,10 +164,10 @@ $large-devices: 992px;
} }
} }
#uv_detail { #ue_detail {
color: #062f38; color: #062f38;
.uv-quick-info-container { .ue-quick-info-container {
display: grid; display: grid;
grid-template-columns: 20% 20% 20% 20% auto; grid-template-columns: 20% 20% 20% 20% auto;
grid-template-rows: auto auto; grid-template-rows: auto auto;
@@ -254,20 +254,20 @@ $large-devices: 992px;
} }
} }
.uv-details-container { .ue-details-container {
display: grid; display: grid;
grid-template-columns: 150px 100px auto; grid-template-columns: 150px 100px auto;
grid-template-rows: 156px 1fr; grid-template-rows: 156px 1fr;
grid-template-areas: grid-template-areas:
"grade grade-stars uv-infos" "grade grade-stars ue-infos"
". . uv-infos"; ". . ue-infos";
@media screen and (max-width: $large-devices) { @media screen and (max-width: $large-devices) {
grid-template-columns: 50% 50%; grid-template-columns: 50% 50%;
grid-template-rows: auto auto; grid-template-rows: auto auto;
grid-template-areas: grid-template-areas:
"grade grade-stars" "grade grade-stars"
"uv-infos uv-infos"; "ue-infos ue-infos";
} }
} }
@@ -290,8 +290,8 @@ $large-devices: 992px;
font-weight: bold; font-weight: bold;
} }
.uv-infos { .ue-infos {
grid-area: uv-infos; grid-area: ue-infos;
padding-left: 10px; padding-left: 10px;
} }

View File

@@ -23,10 +23,10 @@
{% endblock head %} {% endblock head %}
{% block content %} {% block content %}
{% if user.has_perm("pedagogy.add_uv") %} {% if user.has_perm("pedagogy.add_ue") %}
<div class="action-bar"> <div class="action-bar">
<p> <p>
<a href="{{ url('pedagogy:uv_create') }}">{% trans %}Create UV{% endtrans %}</a> <a href="{{ url('pedagogy:ue_create') }}">{% trans %}Create UE{% endtrans %}</a>
</p> </p>
<p> <p>
<a href="{{ url('pedagogy:moderation') }}">{% trans %}Moderate comments{% endtrans %}</a> <a href="{{ url('pedagogy:moderation') }}">{% trans %}Moderate comments{% endtrans %}</a>
@@ -34,7 +34,7 @@
</div> </div>
<br/> <br/>
{% endif %} {% endif %}
<div class="pedagogy" x-data="uv_search" x-cloak> <div class="pedagogy" x-data="ue_search" x-cloak>
<form id="search_form"> <form id="search_form">
<div class="search-form-container"> <div class="search-form-container">
<div class="search-bar"> <div class="search-bar">
@@ -89,43 +89,43 @@
</div> </div>
</div> </div>
</form> </form>
<table id="uv-list"> <table id="ue-list">
<thead> <thead>
<tr> <tr>
<td>{% trans %}UV{% endtrans %}</td> <td>{% trans %}UE{% endtrans %}</td>
<td>{% trans %}Title{% endtrans %}</td> <td>{% trans %}Title{% endtrans %}</td>
<td>{% trans %}Department{% endtrans %}</td> <td>{% trans %}Department{% endtrans %}</td>
<td>{% trans %}Credit type{% endtrans %}</td> <td>{% trans %}Credit type{% endtrans %}</td>
<td><i class="fa fa-leaf"></i></td> <td><i class="fa fa-leaf"></i></td>
<td><i class="fa-regular fa-sun"></i></td> <td><i class="fa-regular fa-sun"></i></td>
{%- if user.has_perm("pedagogy.change_uv") -%} {%- if user.has_perm("pedagogy.change_ue") -%}
<td>{% trans %}Edit{% endtrans %}</td> <td>{% trans %}Edit{% endtrans %}</td>
{%- endif -%} {%- endif -%}
{%- if user.has_perm("pedagogy.delete_uv") -%} {%- if user.has_perm("pedagogy.delete_ue") -%}
<td>{% trans %}Delete{% endtrans %}</td> <td>{% trans %}Delete{% endtrans %}</td>
{% endif %} {% endif %}
</tr> </tr>
</thead> </thead>
<tbody :aria-busy="loading"> <tbody :aria-busy="loading">
<template x-for="uv in uvs.results" :key="uv.id"> <template x-for="ue in ues.results" :key="ue.id">
<tr <tr
@click="window.location.href = `/pedagogy/uv/${uv.id}`" @click="window.location.href = `/pedagogy/ue/${ue.id}`"
class="clickable" class="clickable"
:class="{closed: uv.semester === 'CLOSED'}" :class="{closed: ue.semester === 'CLOSED'}"
> >
<td><a :href="`/pedagogy/uv/${uv.id}`" x-text="uv.code"></a></td> <td><a :href="`/pedagogy/ue/${ue.id}`" x-text="ue.code"></a></td>
<td class="title" <td class="title"
x-text="uv.title + (uv.semester === 'CLOSED' ? ' ({% trans %}closed uv{% endtrans %})' : '')" x-text="ue.title + (ue.semester === 'CLOSED' ? ' ({% trans %}closed ue{% endtrans %})' : '')"
></td> ></td>
<td x-text="uv.department"></td> <td x-text="ue.department"></td>
<td x-text="uv.credit_type"></td> <td x-text="ue.credit_type"></td>
<td><i :class="uv.semester.includes('AUTUMN') && 'fa fa-leaf'"></i></td> <td><i :class="ue.semester.includes('AUTUMN') && 'fa fa-leaf'"></i></td>
<td><i :class="uv.semester.includes('SPRING') && 'fa-regular fa-sun'"></i></td> <td><i :class="ue.semester.includes('SPRING') && 'fa-regular fa-sun'"></i></td>
{%- if user.has_perm("pedagogy.change_uv") -%} {%- if user.has_perm("pedagogy.change_ue") -%}
<td><a :href="`/pedagogy/uv/${uv.id}/edit`">{% trans %}Edit{% endtrans %}</a></td> <td><a :href="`/pedagogy/ue/${ue.id}/edit`">{% trans %}Edit{% endtrans %}</a></td>
{%- endif -%} {%- endif -%}
{%- if user.has_perm("pedagogy.delete_uv") -%} {%- if user.has_perm("pedagogy.delete_ue") -%}
<td><a :href="`/pedagogy/uv/${uv.id}/delete`">{% trans %}Delete{% endtrans %}</a></td> <td><a :href="`/pedagogy/ue/${ue.id}/delete`">{% trans %}Delete{% endtrans %}</a></td>
{%- endif -%} {%- endif -%}
</tr> </tr>
</template> </template>

View File

@@ -1,7 +1,7 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block title %} {% block title %}
{% trans %}UV comment moderation{% endtrans %} {% trans %}UE comment moderation{% endtrans %}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
@@ -9,7 +9,7 @@
<table> <table>
<thead> <thead>
<tr> <tr>
<td>{% trans %}UV{% endtrans %}</td> <td>{% trans %}UE{% endtrans %}</td>
<td>{% trans %}Comment{% endtrans %}</td> <td>{% trans %}Comment{% endtrans %}</td>
<td>{% trans %}Reason{% endtrans %}</td> <td>{% trans %}Reason{% endtrans %}</td>
<td>{% trans %}Action{% endtrans %}</td> <td>{% trans %}Action{% endtrans %}</td>
@@ -22,7 +22,7 @@
<form action="{{ url('pedagogy:moderation') }}" method="post" enctype="multipart/form-data"> <form action="{{ url('pedagogy:moderation') }}" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<tr> <tr>
<td><a href="{{ url('pedagogy:uv_detail', uv_id=report.comment.uv.id) }}#{{ report.comment.uv.id }}">{{ report.comment.uv }}</a></td> <td><a href="{{ url('pedagogy:ue_detail', ue_id=report.comment.ue_id) }}#{{ report.comment.ue_id }}">{{ report.comment.ue }}</a></td>
<td>{{ report.comment.comment|markdown }}</td> <td>{{ report.comment.comment|markdown }}</td>
<td>{{ report.reason|markdown }}</td> <td>{{ report.reason|markdown }}</td>
<td> <td>

View File

@@ -7,12 +7,12 @@
{% endblock %} {% endblock %}
{% block title %} {% block title %}
{% trans %}UV Details{% endtrans %} {% trans %}UE Details{% endtrans %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="pedagogy"> <div class="pedagogy">
<div id="uv_detail"> <div id="ue_detail">
<button onclick='(function(){ <button onclick='(function(){
// If comes from the guide page, go back with history // If comes from the guide page, go back with history
if (document.referrer.replace(/\?(.+)/gm,"").endsWith(`{{ url("pedagogy:guide") }}`)){ if (document.referrer.replace(/\?(.+)/gm,"").endsWith(`{{ url("pedagogy:guide") }}`)){
@@ -25,7 +25,7 @@
<h1>{{ object.code }} - {{ object.title }}</h1> <h1>{{ object.code }} - {{ object.title }}</h1>
<br> <br>
<div class="uv-quick-info-container"> <div class="ue-quick-info-container">
<div class="hours-cm"> <div class="hours-cm">
<b>{% trans %}CM: {% endtrans %}</b>{{ object.hours_CM }} <b>{% trans %}CM: {% endtrans %}</b>{{ object.hours_CM }}
</div> </div>
@@ -55,7 +55,7 @@
<br> <br>
<div class="uv-details-container"> <div class="ue-details-container">
<div class="grade"> <div class="grade">
<p>{% trans %}Global grade{% endtrans %}</p> <p>{% trans %}Global grade{% endtrans %}</p>
<p>{% trans %}Utility{% endtrans %}</p> <p>{% trans %}Utility{% endtrans %}</p>
@@ -70,7 +70,7 @@
<p>{{ display_star(object.grade_teaching_average) }}</p> <p>{{ display_star(object.grade_teaching_average) }}</p>
<p>{{ display_star(object.grade_work_load_average) }}</p> <p>{{ display_star(object.grade_work_load_average) }}</p>
</div> </div>
<div class="uv-infos"> <div class="ue-infos">
<p><b>{% trans %}Objectives{% endtrans %}</b></p> <p><b>{% trans %}Objectives{% endtrans %}</b></p>
<p>{{ object.objectives|markdown }}</p> <p>{{ object.objectives|markdown }}</p>
<p><b>{% trans %}Program{% endtrans %}</b></p> <p><b>{% trans %}Program{% endtrans %}</b></p>
@@ -86,21 +86,21 @@
<br> <br>
{% if object.has_user_already_commented(user) %} {% if object.has_user_already_commented(user) %}
<div id="leave_comment_not_allowed"> <div id="leave_comment_not_allowed">
<p>{% trans %}You already posted a comment on this UV. 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_uvcomment") %} {% elif user.has_perm("pedagogy.add_uecomment") %}
<details class="accordion" id="leave_comment"> <details class="accordion" id="leave_comment">
<summary>{% trans %}Leave comment{% endtrans %}</summary> <summary>{% trans %}Leave comment{% endtrans %}</summary>
<div class="accordion-content"> <div class="accordion-content">
<form action="{{ url('pedagogy:uv_detail', uv_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.author.errors }} {{ form.author.errors }}
{{ form.uv.errors }} {{ form.ue.errors }}
{{ form.author }} {{ form.author }}
{{ form.uv }} {{ form.ue }}
<div class="input-stars"> <div class="input-stars">
<label for="{{ form.grade_global.id_for_label }}">{{ form.grade_global.label }} :</label> <label for="{{ form.grade_global.id_for_label }}">{{ form.grade_global.label }} :</label>
@@ -170,7 +170,7 @@
<div class="comment"> <div class="comment">
<div class="anchor"> <div class="anchor">
<a href="{{ url('pedagogy:uv_detail', uv_id=uv.id) }}#{{ comment.id }}"><i class="fa fa-paragraph"></i></a> <a href="{{ url('pedagogy:ue_detail', ue_id=ue.id) }}#{{ comment.id }}"><i class="fa fa-paragraph"></i></a>
</div> </div>
{{ comment.comment|markdown }} {{ comment.comment|markdown }}
</div> </div>

View File

@@ -9,19 +9,19 @@ from model_bakery.recipe import Recipe
from core.baker_recipes import subscriber_user from core.baker_recipes import subscriber_user
from core.models import Group, User from core.models import Group, User
from pedagogy.models import UV from pedagogy.models import UE
class TestUVSearch(TestCase): class TestUESearch(TestCase):
"""Test UV guide rights for view and API.""" """Test UE guide rights for view and API."""
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
cls.root = User.objects.get(username="root") cls.root = User.objects.get(username="root")
cls.url = reverse("api:fetch_uvs") cls.url = reverse("api:fetch_ues")
uv_recipe = Recipe(UV, author=cls.root) ue_recipe = Recipe(UE, author=cls.root)
uvs = [ ues = [
uv_recipe.prepare( ue_recipe.prepare(
code="AP4A", code="AP4A",
credit_type="CS", credit_type="CS",
semester="AUTUMN", semester="AUTUMN",
@@ -32,7 +32,7 @@ class TestUVSearch(TestCase):
"Concepts fondamentaux et mise en pratique avec le langage C++" "Concepts fondamentaux et mise en pratique avec le langage C++"
), ),
), ),
uv_recipe.prepare( ue_recipe.prepare(
code="MT01", code="MT01",
credit_type="CS", credit_type="CS",
semester="AUTUMN", semester="AUTUMN",
@@ -40,10 +40,10 @@ class TestUVSearch(TestCase):
manager="ben", manager="ben",
title="Intégration1. Algèbre linéaire - Fonctions de deux variables", title="Intégration1. Algèbre linéaire - Fonctions de deux variables",
), ),
uv_recipe.prepare( ue_recipe.prepare(
code="PHYS11", credit_type="CS", semester="AUTUMN", department="TC" code="PHYS11", credit_type="CS", semester="AUTUMN", department="TC"
), ),
uv_recipe.prepare( ue_recipe.prepare(
code="TNEV", code="TNEV",
credit_type="TM", credit_type="TM",
semester="SPRING", semester="SPRING",
@@ -51,10 +51,10 @@ class TestUVSearch(TestCase):
manager="moss", manager="moss",
title="tnetennba", title="tnetennba",
), ),
uv_recipe.prepare( ue_recipe.prepare(
code="MT10", credit_type="TM", semester="AUTUMN", department="IMSI" code="MT10", credit_type="TM", semester="AUTUMN", department="IMSI"
), ),
uv_recipe.prepare( ue_recipe.prepare(
code="DA50", code="DA50",
credit_type="TM", credit_type="TM",
semester="AUTUMN_AND_SPRING", semester="AUTUMN_AND_SPRING",
@@ -62,7 +62,7 @@ class TestUVSearch(TestCase):
manager="francky", manager="francky",
), ),
] ]
UV.objects.bulk_create(uvs) UE.objects.bulk_create(ues)
call_command("update_index") call_command("update_index")
def test_permissions(self): def test_permissions(self):
@@ -93,7 +93,7 @@ class TestUVSearch(TestCase):
"""Test that the return data format is correct""" """Test that the return data format is correct"""
self.client.force_login(self.root) self.client.force_login(self.root)
res = self.client.get(self.url + "?search=PA00") res = self.client.get(self.url + "?search=PA00")
uv = UV.objects.get(code="PA00") ue = UE.objects.get(code="PA00")
assert res.status_code == 200 assert res.status_code == 200
assert json.loads(res.content) == { assert json.loads(res.content) == {
"count": 1, "count": 1,
@@ -101,12 +101,12 @@ class TestUVSearch(TestCase):
"previous": None, "previous": None,
"results": [ "results": [
{ {
"id": uv.id, "id": ue.id,
"title": uv.title, "title": ue.title,
"code": uv.code, "code": ue.code,
"credit_type": uv.credit_type, "credit_type": ue.credit_type,
"semester": uv.semester, "semester": ue.semester,
"department": uv.department, "department": ue.department,
} }
], ],
} }
@@ -114,7 +114,7 @@ class TestUVSearch(TestCase):
def test_search_by_text(self): def test_search_by_text(self):
self.client.force_login(self.root) self.client.force_login(self.root)
for query, expected in ( for query, expected in (
# UV code search case insensitive # UE code search case insensitive
("m", {"MT01", "MT10"}), ("m", {"MT01", "MT10"}),
("M", {"MT01", "MT10"}), ("M", {"MT01", "MT10"}),
("mt", {"MT01", "MT10"}), ("mt", {"MT01", "MT10"}),
@@ -126,24 +126,24 @@ class TestUVSearch(TestCase):
): ):
res = self.client.get(self.url + f"?search={query}") res = self.client.get(self.url + f"?search={query}")
assert res.status_code == 200 assert res.status_code == 200
assert {uv["code"] for uv in json.loads(res.content)["results"]} == expected assert {ue["code"] for ue in json.loads(res.content)["results"]} == expected
def test_search_by_credit_type(self): def test_search_by_credit_type(self):
self.client.force_login(self.root) self.client.force_login(self.root)
res = self.client.get(self.url + "?credit_type=CS") res = self.client.get(self.url + "?credit_type=CS")
assert res.status_code == 200 assert res.status_code == 200
codes = [uv["code"] for uv in json.loads(res.content)["results"]] codes = [ue["code"] for ue in json.loads(res.content)["results"]]
assert codes == ["AP4A", "MT01", "PHYS11"] assert codes == ["AP4A", "MT01", "PHYS11"]
res = self.client.get(self.url + "?credit_type=CS&credit_type=OM") res = self.client.get(self.url + "?credit_type=CS&credit_type=OM")
assert res.status_code == 200 assert res.status_code == 200
codes = {uv["code"] for uv in json.loads(res.content)["results"]} codes = {ue["code"] for ue in json.loads(res.content)["results"]}
assert codes == {"AP4A", "MT01", "PHYS11", "PA00"} assert codes == {"AP4A", "MT01", "PHYS11", "PA00"}
def test_search_by_semester(self): def test_search_by_semester(self):
self.client.force_login(self.root) self.client.force_login(self.root)
res = self.client.get(self.url + "?semester=SPRING") res = self.client.get(self.url + "?semester=SPRING")
assert res.status_code == 200 assert res.status_code == 200
codes = {uv["code"] for uv in json.loads(res.content)["results"]} codes = {ue["code"] for ue in json.loads(res.content)["results"]}
assert codes == {"DA50", "TNEV", "PA00"} assert codes == {"DA50", "TNEV", "PA00"}
def test_search_multiple_filters(self): def test_search_multiple_filters(self):
@@ -152,7 +152,7 @@ class TestUVSearch(TestCase):
self.url + "?semester=AUTUMN&credit_type=CS&department=TC" self.url + "?semester=AUTUMN&credit_type=CS&department=TC"
) )
assert res.status_code == 200 assert res.status_code == 200
codes = {uv["code"] for uv in json.loads(res.content)["results"]} codes = {ue["code"] for ue in json.loads(res.content)["results"]}
assert codes == {"MT01", "PHYS11"} assert codes == {"MT01", "PHYS11"}
def test_search_fails(self): def test_search_fails(self):
@@ -163,15 +163,15 @@ class TestUVSearch(TestCase):
def test_search_pa00_fail(self): def test_search_pa00_fail(self):
self.client.force_login(self.root) self.client.force_login(self.root)
# Search with UV code # Search with UE code
response = self.client.get(reverse("pedagogy:guide"), {"search": "IFC"}) response = self.client.get(reverse("pedagogy:guide"), {"search": "IFC"})
self.assertNotContains(response, text="PA00") self.assertNotContains(response, text="PA00")
# Search with first letter of UV code # Search with first letter of UE code
response = self.client.get(reverse("pedagogy:guide"), {"search": "I"}) response = self.client.get(reverse("pedagogy:guide"), {"search": "I"})
self.assertNotContains(response, text="PA00") self.assertNotContains(response, text="PA00")
# Search with UV manager # Search with UE manager
response = self.client.get(reverse("pedagogy:guide"), {"search": "GILLES"}) response = self.client.get(reverse("pedagogy:guide"), {"search": "GILLES"})
self.assertNotContains(response, text="PA00") self.assertNotContains(response, text="PA00")

View File

@@ -33,14 +33,14 @@ from pytest_django.asserts import assertRedirects
from core.baker_recipes import old_subscriber_user, subscriber_user from core.baker_recipes import old_subscriber_user, subscriber_user
from core.models import Notification, User from core.models import Notification, User
from pedagogy.models import UV, UVComment, UVCommentReport from pedagogy.models import UE, UEComment, UECommentReport
def create_uv_template(user_id, code="IFC1", exclude_list=None): def create_ue_template(user_id, code="IFC1", exclude_list=None):
"""Factory to help UV creation/update in post requests.""" """Factory to help UE creation/update in post requests."""
if exclude_list is None: if exclude_list is None:
exclude_list = [] exclude_list = []
uv = { ue = {
"code": code, "code": code,
"author": user_id, "author": user_id,
"credit_type": "TM", "credit_type": "TM",
@@ -74,15 +74,15 @@ def create_uv_template(user_id, code="IFC1", exclude_list=None):
* Chaînes de caractères""", * Chaînes de caractères""",
} }
for excluded in exclude_list: for excluded in exclude_list:
uv.pop(excluded) ue.pop(excluded)
return uv return ue
# UV class tests # UE class tests
class TestUVCreation(TestCase): class TestUECreation(TestCase):
"""Test uv creation.""" """Test ue creation."""
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@@ -90,62 +90,62 @@ class TestUVCreation(TestCase):
cls.tutu = User.objects.get(username="tutu") cls.tutu = User.objects.get(username="tutu")
cls.sli = User.objects.get(username="sli") cls.sli = User.objects.get(username="sli")
cls.guy = User.objects.get(username="guy") cls.guy = User.objects.get(username="guy")
cls.create_uv_url = reverse("pedagogy:uv_create") cls.create_ue_url = reverse("pedagogy:ue_create")
def test_create_uv_admin_success(self): def test_create_ue_admin_success(self):
self.client.force_login(self.bibou) self.client.force_login(self.bibou)
response = self.client.post( response = self.client.post(
self.create_uv_url, create_uv_template(self.bibou.id) self.create_ue_url, create_ue_template(self.bibou.id)
) )
assert response.status_code == 302 assert response.status_code == 302
assert UV.objects.filter(code="IFC1").exists() assert UE.objects.filter(code="IFC1").exists()
def test_create_uv_pedagogy_admin_success(self): def test_create_ue_pedagogy_admin_success(self):
self.client.force_login(self.tutu) self.client.force_login(self.tutu)
response = self.client.post( response = self.client.post(
self.create_uv_url, create_uv_template(self.tutu.id) self.create_ue_url, create_ue_template(self.tutu.id)
) )
assert response.status_code == 302 assert response.status_code == 302
assert UV.objects.filter(code="IFC1").exists() assert UE.objects.filter(code="IFC1").exists()
def test_create_uv_unauthorized_fail(self): def test_create_ue_unauthorized_fail(self):
# Test with anonymous user # Test with anonymous user
response = self.client.post(self.create_uv_url, create_uv_template(0)) response = self.client.post(self.create_ue_url, create_ue_template(0))
assertRedirects( assertRedirects(
response, reverse("core:login", query={"next": self.create_uv_url}) response, reverse("core:login", query={"next": self.create_ue_url})
) )
# Test with subscribed user # Test with subscribed user
self.client.force_login(self.sli) self.client.force_login(self.sli)
response = self.client.post(self.create_uv_url, create_uv_template(self.sli.id)) response = self.client.post(self.create_ue_url, create_ue_template(self.sli.id))
assert response.status_code == 403 assert response.status_code == 403
# Test with non subscribed user # Test with non subscribed user
self.client.force_login(self.guy) self.client.force_login(self.guy)
response = self.client.post(self.create_uv_url, create_uv_template(self.guy.id)) response = self.client.post(self.create_ue_url, create_ue_template(self.guy.id))
assert response.status_code == 403 assert response.status_code == 403
# Check that the UV has never been created # Check that the UE has never been created
assert not UV.objects.filter(code="IFC1").exists() assert not UE.objects.filter(code="IFC1").exists()
def test_create_uv_bad_request_fail(self): def test_create_ue_bad_request_fail(self):
self.client.force_login(self.tutu) self.client.force_login(self.tutu)
# Test with wrong user id (if someone cheats on the hidden input) # Test with wrong user id (if someone cheats on the hidden input)
response = self.client.post( response = self.client.post(
self.create_uv_url, create_uv_template(self.bibou.id) self.create_ue_url, create_ue_template(self.bibou.id)
) )
assert response.status_code == 200 assert response.status_code == 200
# Remove a required field # Remove a required field
response = self.client.post( response = self.client.post(
self.create_uv_url, self.create_ue_url,
create_uv_template(self.tutu.id, exclude_list=["title"]), create_ue_template(self.tutu.id, exclude_list=["title"]),
) )
assert response.status_code == 200 assert response.status_code == 200
# Check that the UV hase never been created # Check that the UE has never been created
assert not UV.objects.filter(code="IFC1").exists() assert not UE.objects.filter(code="IFC1").exists()
@pytest.mark.django_db @pytest.mark.django_db
@@ -171,8 +171,8 @@ def test_guide_anonymous_permission_denied(client: Client):
assert res.status_code == 302 assert res.status_code == 302
class TestUVDelete(TestCase): class TestUEDelete(TestCase):
"""Test UV deletion rights.""" """Test UE deletion rights."""
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@@ -180,37 +180,37 @@ class TestUVDelete(TestCase):
cls.tutu = User.objects.get(username="tutu") cls.tutu = User.objects.get(username="tutu")
cls.sli = User.objects.get(username="sli") cls.sli = User.objects.get(username="sli")
cls.guy = User.objects.get(username="guy") cls.guy = User.objects.get(username="guy")
cls.uv = UV.objects.get(code="PA00") cls.ue = UE.objects.get(code="PA00")
cls.delete_uv_url = reverse("pedagogy:uv_delete", kwargs={"uv_id": cls.uv.id}) cls.delete_ue_url = reverse("pedagogy:ue_delete", kwargs={"ue_id": cls.ue.id})
def test_uv_delete_root_success(self): def test_ue_delete_root_success(self):
self.client.force_login(self.bibou) self.client.force_login(self.bibou)
self.client.post(self.delete_uv_url) self.client.post(self.delete_ue_url)
assert not UV.objects.filter(pk=self.uv.pk).exists() assert not UE.objects.filter(pk=self.ue.pk).exists()
def test_uv_delete_pedagogy_admin_success(self): def test_ue_delete_pedagogy_admin_success(self):
self.client.force_login(self.tutu) self.client.force_login(self.tutu)
self.client.post(self.delete_uv_url) self.client.post(self.delete_ue_url)
assert not UV.objects.filter(pk=self.uv.pk).exists() assert not UE.objects.filter(pk=self.ue.pk).exists()
def test_uv_delete_pedagogy_unauthorized_fail(self): def test_ue_delete_pedagogy_unauthorized_fail(self):
# Anonymous user # Anonymous user
response = self.client.post(self.delete_uv_url) response = self.client.post(self.delete_ue_url)
assertRedirects( assertRedirects(
response, reverse("core:login", query={"next": self.delete_uv_url}) response, reverse("core:login", query={"next": self.delete_ue_url})
) )
assert UV.objects.filter(pk=self.uv.pk).exists() assert UE.objects.filter(pk=self.ue.pk).exists()
for user in baker.make(User), subscriber_user.make(): for user in baker.make(User), subscriber_user.make():
with self.subTest(): with self.subTest():
self.client.force_login(user) self.client.force_login(user)
response = self.client.post(self.delete_uv_url) response = self.client.post(self.delete_ue_url)
assert response.status_code == 403 assert response.status_code == 403
assert UV.objects.filter(pk=self.uv.pk).exists() assert UE.objects.filter(pk=self.ue.pk).exists()
class TestUVUpdate(TestCase): class TestUEUpdate(TestCase):
"""Test UV update rights.""" """Test UE update rights."""
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@@ -218,79 +218,79 @@ class TestUVUpdate(TestCase):
cls.tutu = User.objects.get(username="tutu") cls.tutu = User.objects.get(username="tutu")
cls.sli = User.objects.get(username="sli") cls.sli = User.objects.get(username="sli")
cls.guy = User.objects.get(username="guy") cls.guy = User.objects.get(username="guy")
cls.uv = UV.objects.get(code="PA00") cls.ue = UE.objects.get(code="PA00")
cls.update_uv_url = reverse("pedagogy:uv_update", kwargs={"uv_id": cls.uv.id}) cls.update_ue_url = reverse("pedagogy:ue_update", kwargs={"ue_id": cls.ue.id})
def test_uv_update_root_success(self): def test_ue_update_root_success(self):
self.client.force_login(self.bibou) self.client.force_login(self.bibou)
self.client.post( self.client.post(
self.update_uv_url, create_uv_template(self.bibou.id, code="PA00") self.update_ue_url, create_ue_template(self.bibou.id, code="PA00")
) )
self.uv.refresh_from_db() self.ue.refresh_from_db()
assert self.uv.credit_type == "TM" assert self.ue.credit_type == "TM"
def test_uv_update_pedagogy_admin_success(self): def test_ue_update_pedagogy_admin_success(self):
self.client.force_login(self.tutu) self.client.force_login(self.tutu)
self.client.post( self.client.post(
self.update_uv_url, create_uv_template(self.bibou.id, code="PA00") self.update_ue_url, create_ue_template(self.bibou.id, code="PA00")
) )
self.uv.refresh_from_db() self.ue.refresh_from_db()
assert self.uv.credit_type == "TM" assert self.ue.credit_type == "TM"
def test_uv_update_original_author_does_not_change(self): def test_ue_update_original_author_does_not_change(self):
self.client.force_login(self.tutu) self.client.force_login(self.tutu)
response = self.client.post( response = self.client.post(
self.update_uv_url, self.update_ue_url,
create_uv_template(self.tutu.id, code="PA00"), create_ue_template(self.tutu.id, code="PA00"),
) )
assert response.status_code == 200 assert response.status_code == 200
self.uv.refresh_from_db() self.ue.refresh_from_db()
assert self.uv.author == self.bibou assert self.ue.author == self.bibou
def test_uv_update_pedagogy_unauthorized_fail(self): def test_ue_update_pedagogy_unauthorized_fail(self):
# Anonymous user # Anonymous user
response = self.client.post( response = self.client.post(
self.update_uv_url, create_uv_template(self.bibou.id, code="PA00") self.update_ue_url, create_ue_template(self.bibou.id, code="PA00")
) )
assertRedirects( assertRedirects(
response, reverse("core:login", query={"next": self.update_uv_url}) response, reverse("core:login", query={"next": self.update_ue_url})
) )
# Not subscribed user # Not subscribed user
self.client.force_login(self.guy) self.client.force_login(self.guy)
response = self.client.post( response = self.client.post(
self.update_uv_url, create_uv_template(self.bibou.id, code="PA00") self.update_ue_url, create_ue_template(self.bibou.id, code="PA00")
) )
assert response.status_code == 403 assert response.status_code == 403
# Simply subscribed user # Simply subscribed user
self.client.force_login(self.sli) self.client.force_login(self.sli)
response = self.client.post( response = self.client.post(
self.update_uv_url, create_uv_template(self.bibou.id, code="PA00") self.update_ue_url, create_ue_template(self.bibou.id, code="PA00")
) )
assert response.status_code == 403 assert response.status_code == 403
# Check that the UV has not changed # Check that the UE has not changed
self.uv.refresh_from_db() self.ue.refresh_from_db()
assert self.uv.credit_type == "OM" assert self.ue.credit_type == "OM"
# UVComment class tests # UEComment class tests
def create_uv_comment_template(user_id, uv_code="PA00", exclude_list=None): def create_ue_comment_template(user_id, ue_code="PA00", exclude_list=None):
"""Factory to help UVComment creation/update in post requests.""" """Factory to help UEComment creation/update in post requests."""
if exclude_list is None: if exclude_list is None:
exclude_list = [] exclude_list = []
comment = { comment = {
"author": user_id, "author": user_id,
"uv": UV.objects.get(code=uv_code).id, "ue": UE.objects.get(code=ue_code).id,
"grade_global": 4, "grade_global": 4,
"grade_utility": 4, "grade_utility": 4,
"grade_interest": 4, "grade_interest": 4,
"grade_teaching": -1, "grade_teaching": -1,
"grade_work_load": 2, "grade_work_load": 2,
"comment": "Superbe UV qui fait vivre la vie associative de l'école", "comment": "Superbe UE qui fait vivre la vie associative de l'école",
} }
for excluded in exclude_list: for excluded in exclude_list:
comment.pop(excluded) comment.pop(excluded)
@@ -298,7 +298,7 @@ def create_uv_comment_template(user_id, uv_code="PA00", exclude_list=None):
class TestUVCommentCreationAndDisplay(TestCase): class TestUVCommentCreationAndDisplay(TestCase):
"""Test UVComment creation and its display. """Test UEComment creation and its display.
Display and creation are the same view. Display and creation are the same view.
""" """
@@ -309,124 +309,124 @@ class TestUVCommentCreationAndDisplay(TestCase):
cls.tutu = User.objects.get(username="tutu") cls.tutu = User.objects.get(username="tutu")
cls.sli = User.objects.get(username="sli") cls.sli = User.objects.get(username="sli")
cls.guy = User.objects.get(username="guy") cls.guy = User.objects.get(username="guy")
cls.uv = UV.objects.get(code="PA00") cls.ue = UE.objects.get(code="PA00")
cls.uv_url = reverse("pedagogy:uv_detail", kwargs={"uv_id": cls.uv.id}) cls.ue_url = reverse("pedagogy:ue_detail", kwargs={"ue_id": cls.ue.id})
def test_create_uv_comment_admin_success(self): def test_create_ue_comment_admin_success(self):
self.client.force_login(self.bibou) self.client.force_login(self.bibou)
response = self.client.post( response = self.client.post(
self.uv_url, create_uv_comment_template(self.bibou.id) self.ue_url, create_ue_comment_template(self.bibou.id)
) )
assertRedirects(response, self.uv_url) assertRedirects(response, self.ue_url)
response = self.client.get(self.uv_url) response = self.client.get(self.ue_url)
self.assertContains(response, text="Superbe UV") self.assertContains(response, text="Superbe UE")
def test_create_uv_comment_pedagogy_admin_success(self): def test_create_ue_comment_pedagogy_admin_success(self):
self.client.force_login(self.tutu) self.client.force_login(self.tutu)
response = self.client.post( response = self.client.post(
self.uv_url, create_uv_comment_template(self.tutu.id) self.ue_url, create_ue_comment_template(self.tutu.id)
) )
self.assertRedirects(response, self.uv_url) self.assertRedirects(response, self.ue_url)
response = self.client.get(self.uv_url) response = self.client.get(self.ue_url)
self.assertContains(response, text="Superbe UV") self.assertContains(response, text="Superbe UE")
def test_create_uv_comment_subscriber_success(self): def test_create_ue_comment_subscriber_success(self):
self.client.force_login(self.sli) self.client.force_login(self.sli)
response = self.client.post( response = self.client.post(
self.uv_url, create_uv_comment_template(self.sli.id) self.ue_url, create_ue_comment_template(self.sli.id)
) )
self.assertRedirects(response, self.uv_url) self.assertRedirects(response, self.ue_url)
response = self.client.get(self.uv_url) response = self.client.get(self.ue_url)
self.assertContains(response, text="Superbe UV") self.assertContains(response, text="Superbe UE")
def test_create_uv_comment_unauthorized_fail(self): def test_create_ue_comment_unauthorized_fail(self):
nb_comments = self.uv.comments.count() nb_comments = self.ue.comments.count()
# Test with anonymous user # Test with anonymous user
response = self.client.post(self.uv_url, create_uv_comment_template(0)) response = self.client.post(self.ue_url, create_ue_comment_template(0))
assertRedirects(response, reverse("core:login", query={"next": self.uv_url})) assertRedirects(response, reverse("core:login", query={"next": self.ue_url}))
# Test with non subscribed user # Test with non subscribed user
self.client.force_login(self.guy) self.client.force_login(self.guy)
response = self.client.post( response = self.client.post(
self.uv_url, create_uv_comment_template(self.guy.id) self.ue_url, create_ue_comment_template(self.guy.id)
) )
assert response.status_code == 403 assert response.status_code == 403
# Check that no comment has been created # Check that no comment has been created
assert self.uv.comments.count() == nb_comments assert self.ue.comments.count() == nb_comments
def test_create_uv_comment_bad_form_fail(self): def test_create_ue_comment_bad_form_fail(self):
nb_comments = self.uv.comments.count() nb_comments = self.ue.comments.count()
self.client.force_login(self.bibou) self.client.force_login(self.bibou)
response = self.client.post( response = self.client.post(
self.uv_url, self.ue_url,
create_uv_comment_template(self.bibou.id, exclude_list=["grade_global"]), create_ue_comment_template(self.bibou.id, exclude_list=["grade_global"]),
) )
assert response.status_code == 200 assert response.status_code == 200
assert self.uv.comments.count() == nb_comments assert self.ue.comments.count() == nb_comments
def test_create_uv_comment_twice_fail(self): 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 proprely
assert not self.uv.has_user_already_commented(self.bibou) assert not self.ue.has_user_already_commented(self.bibou)
# Create a first comment # Create a first comment
self.client.force_login(self.bibou) self.client.force_login(self.bibou)
self.client.post(self.uv_url, create_uv_comment_template(self.bibou.id)) self.client.post(self.ue_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 proprely
assert self.uv.has_user_already_commented(self.bibou) assert self.ue.has_user_already_commented(self.bibou)
# Create the second comment # Create the second comment
comment = create_uv_comment_template(self.bibou.id) comment = create_ue_comment_template(self.bibou.id)
comment["comment"] = "Twice" comment["comment"] = "Twice"
response = self.client.post(self.uv_url, comment) response = self.client.post(self.ue_url, comment)
assert response.status_code == 200 assert response.status_code == 200
assert UVComment.objects.filter(comment__contains="Superbe UV").exists() assert UEComment.objects.filter(comment__contains="Superbe UE").exists()
assert not UVComment.objects.filter(comment__contains="Twice").exists() assert not UEComment.objects.filter(comment__contains="Twice").exists()
self.assertContains( self.assertContains(
response, response,
_( _(
"You already posted a comment on this UV. " "You already posted a comment on this UE. "
"If you want to comment again, " "If you want to comment again, "
"please modify or delete your previous comment." "please modify or delete your previous comment."
), ),
) )
# Ensure that there is no crash when no uv or no author is given # Ensure that there is no crash when no ue or no author is given
self.client.post( self.client.post(
self.uv_url, create_uv_comment_template(self.bibou.id, exclude_list=["uv"]) self.ue_url, create_ue_comment_template(self.bibou.id, exclude_list=["ue"])
) )
assert response.status_code == 200 assert response.status_code == 200
self.client.post( self.client.post(
self.uv_url, self.ue_url,
create_uv_comment_template(self.bibou.id, exclude_list=["author"]), create_ue_comment_template(self.bibou.id, exclude_list=["author"]),
) )
assert response.status_code == 200 assert response.status_code == 200
class TestUVCommentDelete(TestCase): class TestUVCommentDelete(TestCase):
"""Test UVComment deletion rights.""" """Test UEComment deletion rights."""
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
cls.comment = baker.make(UVComment) cls.comment = baker.make(UEComment)
def test_uv_comment_delete_success(self): def test_ue_comment_delete_success(self):
url = reverse("pedagogy:comment_delete", kwargs={"comment_id": self.comment.id}) url = reverse("pedagogy:comment_delete", kwargs={"comment_id": self.comment.id})
for user in ( for user in (
baker.make(User, is_superuser=True), baker.make(User, is_superuser=True),
baker.make( baker.make(
User, user_permissions=[Permission.objects.get(codename="view_uv")] User, user_permissions=[Permission.objects.get(codename="view_ue")]
), ),
self.comment.author, self.comment.author,
): ):
with self.subTest(): with self.subTest():
self.client.force_login(user) self.client.force_login(user)
self.client.post(url) self.client.post(url)
assert not UVComment.objects.filter(id=self.comment.id).exists() assert not UEComment.objects.filter(id=self.comment.id).exists()
def test_uv_comment_delete_unauthorized_fail(self): def test_ue_comment_delete_unauthorized_fail(self):
url = reverse("pedagogy:comment_delete", kwargs={"comment_id": self.comment.id}) url = reverse("pedagogy:comment_delete", kwargs={"comment_id": self.comment.id})
# Anonymous user # Anonymous user
@@ -441,11 +441,11 @@ class TestUVCommentDelete(TestCase):
assert response.status_code == 403 assert response.status_code == 403
# Check that the comment still exists # Check that the comment still exists
assert UVComment.objects.filter(id=self.comment.id).exists() assert UEComment.objects.filter(id=self.comment.id).exists()
class TestUVCommentUpdate(TestCase): class TestUVCommentUpdate(TestCase):
"""Test UVComment update rights.""" """Test UEComment update rights."""
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@@ -457,17 +457,17 @@ class TestUVCommentUpdate(TestCase):
def setUp(self): def setUp(self):
# Prepare a comment # Prepare a comment
comment_kwargs = create_uv_comment_template(self.krophil.id) comment_kwargs = create_ue_comment_template(self.krophil.id)
comment_kwargs["author"] = self.krophil comment_kwargs["author"] = self.krophil
comment_kwargs["uv"] = UV.objects.get(id=comment_kwargs["uv"]) comment_kwargs["ue"] = UE.objects.get(id=comment_kwargs["ue"])
self.comment = UVComment(**comment_kwargs) self.comment = UEComment(**comment_kwargs)
self.comment.save() self.comment.save()
# Prepare edit of this comment for post requests # Prepare edit of this comment for post requests
self.comment_edit = create_uv_comment_template(self.krophil.id) self.comment_edit = create_ue_comment_template(self.krophil.id)
self.comment_edit["comment"] = "Edited" self.comment_edit["comment"] = "Edited"
def test_uv_comment_update_root_success(self): def test_ue_comment_update_root_success(self):
self.client.force_login(self.bibou) self.client.force_login(self.bibou)
response = self.client.post( response = self.client.post(
reverse("pedagogy:comment_update", kwargs={"comment_id": self.comment.id}), reverse("pedagogy:comment_update", kwargs={"comment_id": self.comment.id}),
@@ -477,7 +477,7 @@ class TestUVCommentUpdate(TestCase):
self.comment.refresh_from_db() self.comment.refresh_from_db()
self.assertEqual(self.comment.comment, self.comment_edit["comment"]) self.assertEqual(self.comment.comment, self.comment_edit["comment"])
def test_uv_comment_update_author_success(self): def test_ue_comment_update_author_success(self):
self.client.force_login(self.krophil) self.client.force_login(self.krophil)
response = self.client.post( response = self.client.post(
reverse("pedagogy:comment_update", kwargs={"comment_id": self.comment.id}), reverse("pedagogy:comment_update", kwargs={"comment_id": self.comment.id}),
@@ -487,7 +487,7 @@ class TestUVCommentUpdate(TestCase):
self.comment.refresh_from_db() self.comment.refresh_from_db()
self.assertEqual(self.comment.comment, self.comment_edit["comment"]) self.assertEqual(self.comment.comment, self.comment_edit["comment"])
def test_uv_comment_update_unauthorized_fail(self): def test_ue_comment_update_unauthorized_fail(self):
url = reverse("pedagogy:comment_update", kwargs={"comment_id": self.comment.id}) url = reverse("pedagogy:comment_update", kwargs={"comment_id": self.comment.id})
# Anonymous user # Anonymous user
response = self.client.post(url, self.comment_edit) response = self.client.post(url, self.comment_edit)
@@ -506,7 +506,7 @@ class TestUVCommentUpdate(TestCase):
self.comment.refresh_from_db() self.comment.refresh_from_db()
self.assertNotEqual(self.comment.comment, self.comment_edit["comment"]) self.assertNotEqual(self.comment.comment, self.comment_edit["comment"])
def test_uv_comment_update_original_author_does_not_change(self): def test_ue_comment_update_original_author_does_not_change(self):
self.client.force_login(self.bibou) self.client.force_login(self.bibou)
self.comment_edit["author"] = User.objects.get(username="root").id self.comment_edit["author"] = User.objects.get(username="root").id
@@ -531,31 +531,31 @@ class TestUVModerationForm(TestCase):
def setUp(self): def setUp(self):
# Prepare a comment # Prepare a comment
comment_kwargs = create_uv_comment_template(self.krophil.id) comment_kwargs = create_ue_comment_template(self.krophil.id)
comment_kwargs["author"] = self.krophil comment_kwargs["author"] = self.krophil
comment_kwargs["uv"] = UV.objects.get(id=comment_kwargs["uv"]) comment_kwargs["ue"] = UE.objects.get(id=comment_kwargs["ue"])
self.comment_1 = UVComment(**comment_kwargs) self.comment_1 = UEComment(**comment_kwargs)
self.comment_1.save() self.comment_1.save()
# Prepare another comment # Prepare another comment
comment_kwargs = create_uv_comment_template(self.krophil.id) comment_kwargs = create_ue_comment_template(self.krophil.id)
comment_kwargs["author"] = self.krophil comment_kwargs["author"] = self.krophil
comment_kwargs["uv"] = UV.objects.get(id=comment_kwargs["uv"]) comment_kwargs["ue"] = UE.objects.get(id=comment_kwargs["ue"])
self.comment_2 = UVComment(**comment_kwargs) self.comment_2 = UEComment(**comment_kwargs)
self.comment_2.save() self.comment_2.save()
# Prepare a comment report for comment 1 # Prepare a comment report for comment 1
self.report_1 = UVCommentReport( self.report_1 = UECommentReport(
comment=self.comment_1, reporter=self.krophil, reason="C'est moche" comment=self.comment_1, reporter=self.krophil, reason="C'est moche"
) )
self.report_1.save() self.report_1.save()
self.report_1_bis = UVCommentReport( self.report_1_bis = UECommentReport(
comment=self.comment_1, reporter=self.krophil, reason="C'est moche 2" comment=self.comment_1, reporter=self.krophil, reason="C'est moche 2"
) )
self.report_1_bis.save() self.report_1_bis.save()
# Prepare a comment report for comment 2 # Prepare a comment report for comment 2
self.report_2 = UVCommentReport( self.report_2 = UECommentReport(
comment=self.comment_2, reporter=self.krophil, reason="C'est moche" comment=self.comment_2, reporter=self.krophil, reason="C'est moche"
) )
self.report_2.save() self.report_2.save()
@@ -593,11 +593,11 @@ class TestUVModerationForm(TestCase):
assert response.status_code == 302 assert response.status_code == 302
# Test that nothing has changed # Test that nothing has changed
assert UVCommentReport.objects.filter(id=self.report_1.id).exists() assert UECommentReport.objects.filter(id=self.report_1.id).exists()
assert UVComment.objects.filter(id=self.comment_1.id).exists() assert UEComment.objects.filter(id=self.comment_1.id).exists()
assert UVCommentReport.objects.filter(id=self.report_1_bis.id).exists() assert UECommentReport.objects.filter(id=self.report_1_bis.id).exists()
assert UVCommentReport.objects.filter(id=self.report_2.id).exists() assert UECommentReport.objects.filter(id=self.report_2.id).exists()
assert UVComment.objects.filter(id=self.comment_2.id).exists() assert UEComment.objects.filter(id=self.comment_2.id).exists()
def test_delete_comment(self): def test_delete_comment(self):
self.client.force_login(self.bibou) self.client.force_login(self.bibou)
@@ -607,14 +607,14 @@ class TestUVModerationForm(TestCase):
assert response.status_code == 302 assert response.status_code == 302
# Test that the comment and it's associated report has been deleted # Test that the comment and it's associated report has been deleted
assert not UVCommentReport.objects.filter(id=self.report_1.id).exists() assert not UECommentReport.objects.filter(id=self.report_1.id).exists()
assert not UVComment.objects.filter(id=self.comment_1.id).exists() assert not UEComment.objects.filter(id=self.comment_1.id).exists()
# Test that the bis report has been deleted # Test that the bis report has been deleted
assert not UVCommentReport.objects.filter(id=self.report_1_bis.id).exists() assert not UECommentReport.objects.filter(id=self.report_1_bis.id).exists()
# Test that the other comment and report still exists # Test that the other comment and report still exists
assert UVCommentReport.objects.filter(id=self.report_2.id).exists() assert UECommentReport.objects.filter(id=self.report_2.id).exists()
assert UVComment.objects.filter(id=self.comment_2.id).exists() assert UEComment.objects.filter(id=self.comment_2.id).exists()
def test_delete_comment_bulk(self): def test_delete_comment_bulk(self):
self.client.force_login(self.bibou) self.client.force_login(self.bibou)
@@ -625,12 +625,12 @@ class TestUVModerationForm(TestCase):
assert response.status_code == 302 assert response.status_code == 302
# Test that comments and their associated reports has been deleted # Test that comments and their associated reports has been deleted
assert not UVCommentReport.objects.filter(id=self.report_1.id).exists() assert not UECommentReport.objects.filter(id=self.report_1.id).exists()
assert not UVComment.objects.filter(id=self.comment_1.id).exists() assert not UEComment.objects.filter(id=self.comment_1.id).exists()
assert not UVCommentReport.objects.filter(id=self.report_2.id).exists() assert not UECommentReport.objects.filter(id=self.report_2.id).exists()
assert not UVComment.objects.filter(id=self.comment_2.id).exists() assert not UEComment.objects.filter(id=self.comment_2.id).exists()
# Test that the bis report has been deleted # Test that the bis report has been deleted
assert not UVCommentReport.objects.filter(id=self.report_1_bis.id).exists() assert not UECommentReport.objects.filter(id=self.report_1_bis.id).exists()
def test_delete_comment_with_bis(self): def test_delete_comment_with_bis(self):
# Test case if two reports targets the same comment and are both deleted # Test case if two reports targets the same comment and are both deleted
@@ -642,10 +642,10 @@ class TestUVModerationForm(TestCase):
assert response.status_code == 302 assert response.status_code == 302
# Test that the comment and it's associated report has been deleted # Test that the comment and it's associated report has been deleted
assert not UVCommentReport.objects.filter(id=self.report_1.id).exists() assert not UECommentReport.objects.filter(id=self.report_1.id).exists()
assert not UVComment.objects.filter(id=self.comment_1.id).exists() assert not UEComment.objects.filter(id=self.comment_1.id).exists()
# Test that the bis report has been deleted # Test that the bis report has been deleted
assert not UVCommentReport.objects.filter(id=self.report_1_bis.id).exists() assert not UECommentReport.objects.filter(id=self.report_1_bis.id).exists()
def test_delete_report(self): def test_delete_report(self):
self.client.force_login(self.bibou) self.client.force_login(self.bibou)
@@ -655,14 +655,14 @@ class TestUVModerationForm(TestCase):
assert response.status_code == 302 assert response.status_code == 302
# Test that the report has been deleted and that the comment still exists # Test that the report has been deleted and that the comment still exists
assert not UVCommentReport.objects.filter(id=self.report_1.id).exists() assert not UECommentReport.objects.filter(id=self.report_1.id).exists()
assert UVComment.objects.filter(id=self.comment_1.id).exists() assert UEComment.objects.filter(id=self.comment_1.id).exists()
# Test that the bis report is still there # Test that the bis report is still there
assert UVCommentReport.objects.filter(id=self.report_1_bis.id).exists() assert UECommentReport.objects.filter(id=self.report_1_bis.id).exists()
# Test that the other comment and report still exists # Test that the other comment and report still exists
assert UVCommentReport.objects.filter(id=self.report_2.id).exists() assert UECommentReport.objects.filter(id=self.report_2.id).exists()
assert UVComment.objects.filter(id=self.comment_2.id).exists() assert UEComment.objects.filter(id=self.comment_2.id).exists()
def test_delete_report_bulk(self): def test_delete_report_bulk(self):
self.client.force_login(self.bibou) self.client.force_login(self.bibou)
@@ -679,12 +679,12 @@ class TestUVModerationForm(TestCase):
assert response.status_code == 302 assert response.status_code == 302
# Test that every reports has been deleted # Test that every reports has been deleted
assert not UVCommentReport.objects.filter(id=self.report_1.id).exists() assert not UECommentReport.objects.filter(id=self.report_1.id).exists()
assert not UVCommentReport.objects.filter(id=self.report_1_bis.id).exists() assert not UECommentReport.objects.filter(id=self.report_1_bis.id).exists()
assert not UVCommentReport.objects.filter(id=self.report_2.id).exists() assert not UECommentReport.objects.filter(id=self.report_2.id).exists()
# Test that comments still exists # Test that comments still exists
assert UVComment.objects.filter(id=self.comment_1.id).exists() assert UEComment.objects.filter(id=self.comment_1.id).exists()
assert UVComment.objects.filter(id=self.comment_2.id).exists() assert UEComment.objects.filter(id=self.comment_2.id).exists()
def test_delete_mixed(self): def test_delete_mixed(self):
self.client.force_login(self.bibou) self.client.force_login(self.bibou)
@@ -698,15 +698,15 @@ class TestUVModerationForm(TestCase):
assert response.status_code == 302 assert response.status_code == 302
# Test that report 2 and his comment has been deleted # Test that report 2 and his comment has been deleted
assert not UVCommentReport.objects.filter(id=self.report_2.id).exists() assert not UECommentReport.objects.filter(id=self.report_2.id).exists()
assert not UVComment.objects.filter(id=self.comment_2.id).exists() assert not UEComment.objects.filter(id=self.comment_2.id).exists()
# Test that report 1 has been deleted and it's comment still exists # Test that report 1 has been deleted and it's comment still exists
assert not UVCommentReport.objects.filter(id=self.report_1.id).exists() assert not UECommentReport.objects.filter(id=self.report_1.id).exists()
assert UVComment.objects.filter(id=self.comment_1.id).exists() assert UEComment.objects.filter(id=self.comment_1.id).exists()
# Test that report 1 bis is still there # Test that report 1 bis is still there
assert UVCommentReport.objects.filter(id=self.report_1_bis.id).exists() assert UECommentReport.objects.filter(id=self.report_1_bis.id).exists()
def test_delete_mixed_with_bis(self): def test_delete_mixed_with_bis(self):
self.client.force_login(self.bibou) self.client.force_login(self.bibou)
@@ -720,16 +720,16 @@ class TestUVModerationForm(TestCase):
assert response.status_code == 302 assert response.status_code == 302
# Test that report 1 and 1 bis has been deleted # Test that report 1 and 1 bis has been deleted
assert not UVCommentReport.objects.filter( assert not UECommentReport.objects.filter(
id__in=[self.report_1.id, self.report_1_bis.id] id__in=[self.report_1.id, self.report_1_bis.id]
).exists() ).exists()
# Test that comment 1 has been deleted # Test that comment 1 has been deleted
assert not UVComment.objects.filter(id=self.comment_1.id).exists() assert not UEComment.objects.filter(id=self.comment_1.id).exists()
# Test that report and comment 2 still exists # Test that report and comment 2 still exists
assert UVCommentReport.objects.filter(id=self.report_2.id).exists() assert UECommentReport.objects.filter(id=self.report_2.id).exists()
assert UVComment.objects.filter(id=self.comment_2.id).exists() assert UEComment.objects.filter(id=self.comment_2.id).exists()
class TestUVCommentReportCreate(TestCase): class TestUVCommentReportCreate(TestCase):
@@ -743,10 +743,10 @@ class TestUVCommentReportCreate(TestCase):
self.tutu = User.objects.get(username="tutu") self.tutu = User.objects.get(username="tutu")
# Prepare a comment # Prepare a comment
comment_kwargs = create_uv_comment_template(self.krophil.id) comment_kwargs = create_ue_comment_template(self.krophil.id)
comment_kwargs["author"] = self.krophil comment_kwargs["author"] = self.krophil
comment_kwargs["uv"] = UV.objects.get(id=comment_kwargs["uv"]) comment_kwargs["ue"] = UE.objects.get(id=comment_kwargs["ue"])
self.comment = UVComment(**comment_kwargs) self.comment = UEComment(**comment_kwargs)
self.comment.save() self.comment.save()
def create_report_test(self, username: str, *, success: bool): def create_report_test(self, username: str, *, success: bool):
@@ -763,7 +763,7 @@ class TestUVCommentReportCreate(TestCase):
assert response.status_code == 302 assert response.status_code == 302
else: else:
assert response.status_code == 403 assert response.status_code == 403
self.assertEqual(UVCommentReport.objects.all().exists(), success) self.assertEqual(UECommentReport.objects.all().exists(), success)
def test_create_report_root_success(self): def test_create_report_root_success(self):
self.create_report_test("root", success=True) self.create_report_test("root", success=True)
@@ -783,7 +783,7 @@ class TestUVCommentReportCreate(TestCase):
url, {"comment": self.comment.id, "reporter": 0, "reason": "C'est moche"} url, {"comment": self.comment.id, "reporter": 0, "reason": "C'est moche"}
) )
assertRedirects(response, reverse("core:login", query={"next": url})) assertRedirects(response, reverse("core:login", query={"next": url}))
assert not UVCommentReport.objects.all().exists() assert not UECommentReport.objects.all().exists()
def test_notifications(self): def test_notifications(self):
assert not self.tutu.notifications.filter(type="PEDAGOGY_MODERATION").exists() assert not self.tutu.notifications.filter(type="PEDAGOGY_MODERATION").exists()

View File

@@ -24,40 +24,40 @@
from django.urls import path from django.urls import path
from pedagogy.views import ( from pedagogy.views import (
UVCommentDeleteView, UECommentDeleteView,
UVCommentReportCreateView, UECommentReportCreateView,
UVCommentUpdateView, UECommentUpdateView,
UVCreateView, UECreateView,
UVDeleteView, UEDeleteView,
UVDetailFormView, UEDetailFormView,
UVGuideView, UEGuideView,
UVModerationFormView, UEModerationFormView,
UVUpdateView, UEUpdateView,
) )
urlpatterns = [ urlpatterns = [
# Urls displaying the actual application for visitors # Urls displaying the actual application for visitors
path("", UVGuideView.as_view(), name="guide"), path("", UEGuideView.as_view(), name="guide"),
path("uv/<int:uv_id>/", UVDetailFormView.as_view(), name="uv_detail"), path("ue/<int:ue_id>/", UEDetailFormView.as_view(), name="ue_detail"),
path( path(
"comment/<int:comment_id>/edit/", "comment/<int:comment_id>/edit/",
UVCommentUpdateView.as_view(), UECommentUpdateView.as_view(),
name="comment_update", name="comment_update",
), ),
path( path(
"comment/<int:comment_id>/delete/", "comment/<int:comment_id>/delete/",
UVCommentDeleteView.as_view(), UECommentDeleteView.as_view(),
name="comment_delete", name="comment_delete",
), ),
path( path(
"comment/<int:comment_id>/report/", "comment/<int:comment_id>/report/",
UVCommentReportCreateView.as_view(), UECommentReportCreateView.as_view(),
name="comment_report", name="comment_report",
), ),
# Moderation # Moderation
path("moderation/", UVModerationFormView.as_view(), name="moderation"), path("moderation/", UEModerationFormView.as_view(), name="moderation"),
# Administration : Create Update Delete Edit # Administration : Create Update Delete Edit
path("uv/create/", UVCreateView.as_view(), name="uv_create"), path("ue/create/", UECreateView.as_view(), name="ue_create"),
path("uv/<int:uv_id>/delete/", UVDeleteView.as_view(), name="uv_delete"), path("ue/<int:ue_id>/delete/", UEDeleteView.as_view(), name="ue_delete"),
path("uv/<int:uv_id>/edit/", UVUpdateView.as_view(), name="uv_update"), path("ue/<int:ue_id>/edit/", UEUpdateView.as_view(), name="ue_update"),
] ]

View File

@@ -1,4 +1,4 @@
"""Set of functions to interact with the UTBM UV api.""" """Set of functions to interact with the UTBM UE api."""
from typing import Iterator from typing import Iterator
@@ -6,14 +6,14 @@ import requests
from django.conf import settings from django.conf import settings
from django.utils.functional import cached_property from django.utils.functional import cached_property
from pedagogy.schemas import ShortUvList, UtbmFullUvSchema, UtbmShortUvSchema, UvSchema from pedagogy.schemas import ShortUeList, UeSchema, UtbmFullUeSchema, UtbmShortUeSchema
class UtbmApiClient(requests.Session): class UtbmApiClient(requests.Session):
"""A wrapper around `requests.Session` to perform requests to the UTBM UV API.""" """A wrapper around `requests.Session` to perform requests to the UTBM UE API."""
BASE_URL = settings.SITH_PEDAGOGY_UTBM_API BASE_URL = settings.SITH_PEDAGOGY_UTBM_API
_cache = {"short_uvs": {}} _cache = {"short_ues": {}}
@cached_property @cached_property
def current_year(self) -> int: def current_year(self) -> int:
@@ -22,83 +22,83 @@ class UtbmApiClient(requests.Session):
response = self.get(url) response = self.get(url)
return response.json()[-1]["annee"] return response.json()[-1]["annee"]
def fetch_short_uvs( def fetch_short_ues(
self, lang: str = "fr", year: int | None = None self, lang: str = "fr", year: int | None = None
) -> list[UtbmShortUvSchema]: ) -> list[UtbmShortUeSchema]:
"""Get the list of UVs in their short format from the UTBM API""" """Get the list of UEs in their short format from the UTBM API"""
if year is None: if year is None:
year = self.current_year year = self.current_year
if lang not in self._cache["short_uvs"]: if lang not in self._cache["short_ues"]:
self._cache["short_uvs"][lang] = {} self._cache["short_ues"][lang] = {}
if year not in self._cache["short_uvs"][lang]: if year not in self._cache["short_ues"][lang]:
url = f"{self.BASE_URL}/uvs/{lang}/{year}" url = f"{self.BASE_URL}/uvs/{lang}/{year}"
response = self.get(url) response = self.get(url)
uvs = ShortUvList.validate_json(response.content) ues = ShortUeList.validate_json(response.content)
self._cache["short_uvs"][lang][year] = uvs self._cache["short_ues"][lang][year] = ues
return self._cache["short_uvs"][lang][year] return self._cache["short_ues"][lang][year]
def fetch_uvs( def fetch_ues(
self, lang: str = "fr", year: int | None = None self, lang: str = "fr", year: int | None = None
) -> Iterator[UvSchema]: ) -> Iterator[UeSchema]:
"""Fetch all UVs from the UTBM API, parsed in a format that we can use. """Fetch all UEs from the UTBM API, parsed in a format that we can use.
Warning: Warning:
We need infos from the full uv schema, and the UTBM UV API We need infos from the full ue schema, and the UTBM UE API
has no route to get all of them at once. has no route to get all of them at once.
We must do one request per UV (for a total of around 730 UVs), We must do one request per UE (for a total of around 730 UEs),
which takes a lot of time. which takes a lot of time.
Hopefully, there seems to be no rate-limit, so an error Hopefully, there seems to be no rate-limit, so an error
in the middle of the process isn't likely to occur. in the middle of the process isn't likely to occur.
""" """
if year is None: if year is None:
year = self.current_year year = self.current_year
shorts_uvs = self.fetch_short_uvs(lang, year) shorts_ues = self.fetch_short_ues(lang, year)
# When UVs are common to multiple branches (like most HUMA) # When UEs are common to multiple branches (like most HUMA)
# the UTBM API duplicates them for every branch. # the UTBM API duplicates them for every branch.
# We have no way in our db to link a UV to multiple formations, # We have no way in our db to link a UE to multiple formations,
# so we just create a single UV, which formation is the one # so we just create a single UE, which formation is the one
# of the first UV found in the list. # of the first UE found in the list.
# For example, if we have CC01 (TC), CC01 (IMSI) and CC01 (EDIM), # For example, if we have CC01 (TC), CC01 (IMSI) and CC01 (EDIM),
# we will only keep CC01 (TC). # we will only keep CC01 (TC).
unique_short_uvs = {} unique_short_ues = {}
for uv in shorts_uvs: for ue in shorts_ues:
if uv.code not in unique_short_uvs: if ue.code not in unique_short_ues:
unique_short_uvs[uv.code] = uv unique_short_ues[ue.code] = ue
for uv in unique_short_uvs.values(): for ue in unique_short_ues.values():
uv_url = f"{self.BASE_URL}/uv/{lang}/{year}/{uv.code}/{uv.code_formation}" ue_url = f"{self.BASE_URL}/uv/{lang}/{year}/{ue.code}/{ue.code_formation}"
response = requests.get(uv_url) response = requests.get(ue_url)
full_uv = UtbmFullUvSchema.model_validate_json(response.content) full_ue = UtbmFullUeSchema.model_validate_json(response.content)
yield make_clean_uv(uv, full_uv) yield make_clean_ue(ue, full_ue)
def find_uv(self, lang: str, code: str, year: int | None = None) -> UvSchema | None: def find_uu(self, lang: str, code: str, year: int | None = None) -> UeSchema | None:
"""Find an UV from the UTBM API.""" """Find an UE from the UTBM API."""
# query the UV list # query the UE list
if not year: if not year:
year = self.current_year year = self.current_year
# the UTBM API has no way to fetch a single short uv, # the UTBM API has no way to fetch a single short ue,
# and short uvs contain infos that we need and are not # and short ues contain infos that we need and are not
# in the full uv schema, so we must fetch everything. # in the full ue schema, so we must fetch everything.
short_uvs = self.fetch_short_uvs(lang, year) short_ues = self.fetch_short_ues(lang, year)
short_uv = next((uv for uv in short_uvs if uv.code == code), None) short_ue = next((ue for ue in short_ues if ue.code == code), None)
if short_uv is None: if short_ue is None:
return None return None
# get detailed information about the UV # get detailed information about the UE
uv_url = f"{self.BASE_URL}/uv/{lang}/{year}/{code}/{short_uv.code_formation}" ue_url = f"{self.BASE_URL}/uv/{lang}/{year}/{code}/{short_ue.code_formation}"
response = requests.get(uv_url) response = requests.get(ue_url)
full_uv = UtbmFullUvSchema.model_validate_json(response.content) full_ue = UtbmFullUeSchema.model_validate_json(response.content)
return make_clean_uv(short_uv, full_uv) return make_clean_ue(short_ue, full_ue)
def make_clean_uv(short_uv: UtbmShortUvSchema, full_uv: UtbmFullUvSchema) -> UvSchema: def make_clean_ue(short_ue: UtbmShortUeSchema, full_ue: UtbmFullUeSchema) -> UeSchema:
"""Cleans the data up so that it corresponds to our data representation. """Cleans the data up so that it corresponds to our data representation.
Some of the needed information are in the short uv schema, some Some of the needed information are in the short ue schema, some
other in the full uv schema. other in the full ue schema.
Thus we combine those information to obtain a data schema suitable Thus we combine those information to obtain a data schema suitable
for our needs. for our needs.
""" """
if full_uv.departement == "Pôle Humanités": if full_ue.departement == "Pôle Humanités":
department = "HUMA" department = "HUMA"
else: else:
department = { department = {
@@ -112,9 +112,9 @@ def make_clean_uv(short_uv: UtbmShortUvSchema, full_uv: UtbmFullUvSchema) -> UvS
"ED": "EDIM", "ED": "EDIM",
"AI": "GI", "AI": "GI",
"AM": "MC", "AM": "MC",
}.get(short_uv.code_formation, "NA") }.get(short_ue.code_formation, "NA")
match short_uv.ouvert_printemps, short_uv.ouvert_automne: match short_ue.ouvert_printemps, short_ue.ouvert_automne:
case True, True: case True, True:
semester = "AUTUMN_AND_SPRING" semester = "AUTUMN_AND_SPRING"
case True, False: case True, False:
@@ -124,22 +124,22 @@ def make_clean_uv(short_uv: UtbmShortUvSchema, full_uv: UtbmFullUvSchema) -> UvS
case _: case _:
semester = "CLOSED" semester = "CLOSED"
return UvSchema( return UeSchema(
title=full_uv.libelle or "", title=full_ue.libelle or "",
code=full_uv.code, code=full_ue.code,
credit_type=short_uv.code_categorie or "FREE", credit_type=short_ue.code_categorie or "FREE",
semester=semester, semester=semester,
language=short_uv.code_langue.upper(), language=short_ue.code_langue.upper(),
credits=full_uv.credits_ects, credits=full_ue.credits_ects,
department=department, department=department,
hours_THE=next((i.nbh for i in full_uv.activites if i.code == "THE"), 0) // 60, hours_THE=next((i.nbh for i in full_ue.activites if i.code == "THE"), 0) // 60,
hours_TD=next((i.nbh for i in full_uv.activites if i.code == "TD"), 0) // 60, hours_TD=next((i.nbh for i in full_ue.activites if i.code == "TD"), 0) // 60,
hours_TP=next((i.nbh for i in full_uv.activites if i.code == "TP"), 0) // 60, hours_TP=next((i.nbh for i in full_ue.activites if i.code == "TP"), 0) // 60,
hours_TE=next((i.nbh for i in full_uv.activites if i.code == "TE"), 0) // 60, hours_TE=next((i.nbh for i in full_ue.activites if i.code == "TE"), 0) // 60,
hours_CM=next((i.nbh for i in full_uv.activites if i.code == "CM"), 0) // 60, hours_CM=next((i.nbh for i in full_ue.activites if i.code == "CM"), 0) // 60,
manager=full_uv.respo_automne or full_uv.respo_printemps or "", manager=full_ue.respo_automne or full_ue.respo_printemps or "",
objectives=full_uv.objectifs or "", objectives=full_ue.objectifs or "",
program=full_uv.programme or "", program=full_ue.programme or "",
skills=full_uv.acquisition_competences or "", skills=full_ue.acquisition_competences or "",
key_concepts=full_uv.acquisition_notions or "", key_concepts=full_ue.acquisition_notions or "",
) )

View File

@@ -38,39 +38,39 @@ 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 import DetailFormView
from pedagogy.forms import ( from pedagogy.forms import (
UVCommentForm, UECommentForm,
UVCommentModerationForm, UECommentModerationForm,
UVCommentReportForm, UECommentReportForm,
UVForm, UEForm,
) )
from pedagogy.models import UV, UVComment, UVCommentReport from pedagogy.models import UE, UEComment, UECommentReport
class UVDetailFormView(PermissionRequiredMixin, DetailFormView): class UEDetailFormView(PermissionRequiredMixin, DetailFormView):
"""Display every comment of an UV and detailed infos about it. """Display every comment of an UE and detailed infos about it.
Allow to comment the UV. Allow to comment the UE.
""" """
model = UV model = UE
pk_url_kwarg = "uv_id" pk_url_kwarg = "ue_id"
template_name = "pedagogy/uv_detail.jinja" template_name = "pedagogy/ue_detail.jinja"
form_class = UVCommentForm form_class = UECommentForm
permission_required = "pedagogy.view_uv" permission_required = "pedagogy.view_ue"
def has_permission(self): def has_permission(self):
if self.request.method == "POST" and not self.request.user.has_perm( if self.request.method == "POST" and not self.request.user.has_perm(
"pedagogy.add_uvcomment" "pedagogy.add_uecomment"
): ):
# if it's a POST request, the user is trying to add a new UVComment # if it's a POST request, the user is trying to add a new UEComment
# thus he also needs the "add_uvcomment" permission # 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["uv_id"] = self.object.id kwargs["ue_id"] = self.object.id
kwargs["is_creation"] = True kwargs["is_creation"] = True
return kwargs return kwargs
@@ -89,68 +89,68 @@ class UVDetailFormView(PermissionRequiredMixin, DetailFormView):
} }
def get_success_url(self): def get_success_url(self):
# once the new uv comment has been saved # once the new ue comment has been saved
# redirect to the same page we are currently # redirect to the same page we are currently
return self.request.path return self.request.path
class UVCommentUpdateView(PermissionOrAuthorRequiredMixin, UpdateView): class UECommentUpdateView(PermissionOrAuthorRequiredMixin, UpdateView):
"""Allow edit of a given comment.""" """Allow edit of a given comment."""
model = UVComment model = UEComment
form_class = UVCommentForm form_class = UECommentForm
pk_url_kwarg = "comment_id" pk_url_kwarg = "comment_id"
template_name = "core/edit.jinja" template_name = "core/edit.jinja"
permission_required = "pedagogy.change_uvcomment" permission_required = "pedagogy.change_uecomment"
author_field = "author" author_field = "author"
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs["author_id"] = self.object.author_id kwargs["author_id"] = self.object.author_id
kwargs["uv_id"] = self.object.uv_id kwargs["ue_id"] = self.object.ue_id
kwargs["is_creation"] = False kwargs["is_creation"] = False
return kwargs return kwargs
def get_success_url(self): def get_success_url(self):
return reverse("pedagogy:uv_detail", kwargs={"uv_id": self.object.uv_id}) return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.object.ue_id})
class UVCommentDeleteView(PermissionOrAuthorRequiredMixin, DeleteView): class UECommentDeleteView(PermissionOrAuthorRequiredMixin, DeleteView):
"""Allow delete of a given comment.""" """Allow to delete a given comment."""
model = UVComment model = UEComment
pk_url_kwarg = "comment_id" pk_url_kwarg = "comment_id"
template_name = "core/delete_confirm.jinja" template_name = "core/delete_confirm.jinja"
permission_required = "pedagogy.delete_uvcomment" permission_required = "pedagogy.delete_uecomment"
author_field = "author" author_field = "author"
def get_success_url(self): def get_success_url(self):
return reverse("pedagogy:uv_detail", kwargs={"uv_id": self.object.uv_id}) return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.object.ue_id})
class UVGuideView(PermissionRequiredMixin, TemplateView): class UEGuideView(PermissionRequiredMixin, TemplateView):
"""UV guide main page.""" """UE guide main page."""
template_name = "pedagogy/guide.jinja" template_name = "pedagogy/guide.jinja"
permission_required = "pedagogy.view_uv" permission_required = "pedagogy.view_ue"
class UVCommentReportCreateView(PermissionRequiredMixin, CreateView): class UECommentReportCreateView(PermissionRequiredMixin, CreateView):
"""Create a new report for an inapropriate comment.""" """Create a new report for an inappropriate comment."""
model = UVCommentReport model = UECommentReport
form_class = UVCommentReportForm form_class = UECommentReportForm
template_name = "core/edit.jinja" template_name = "core/edit.jinja"
permission_required = "pedagogy.add_uvcommentreport" permission_required = "pedagogy.add_uecommentreport"
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.uv_comment = get_object_or_404(UVComment, 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)
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.uv_comment.id kwargs["comment_id"] = self.ue_comment.id
return kwargs return kwargs
def form_valid(self, form): def form_valid(self, form):
@@ -172,35 +172,35 @@ class UVCommentReportCreateView(PermissionRequiredMixin, CreateView):
return resp return resp
def get_success_url(self): def get_success_url(self):
return reverse("pedagogy:uv_detail", kwargs={"uv_id": self.uv_comment.uv_id}) return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.ue_comment.ue_id})
class UVModerationFormView(PermissionRequiredMixin, FormView): class UEModerationFormView(PermissionRequiredMixin, FormView):
"""Moderation interface (Privileged).""" """Moderation interface (Privileged)."""
form_class = UVCommentModerationForm form_class = UECommentModerationForm
template_name = "pedagogy/moderation.jinja" template_name = "pedagogy/moderation.jinja"
permission_required = "pedagogy.delete_uvcomment" permission_required = "pedagogy.delete_uecomment"
success_url = reverse_lazy("pedagogy:moderation") success_url = reverse_lazy("pedagogy:moderation")
def form_valid(self, form): def form_valid(self, form):
form_clean = form.clean() form_clean = form.clean()
accepted = form_clean.get("accepted_reports", []) accepted = form_clean.get("accepted_reports", [])
if len(accepted) > 0: # delete the reported comments if len(accepted) > 0: # delete the reported comments
UVComment.objects.filter(reports__in=accepted).delete() UEComment.objects.filter(reports__in=accepted).delete()
denied = form_clean.get("denied_reports", []) denied = form_clean.get("denied_reports", [])
if len(denied) > 0: # delete the comments themselves if len(denied) > 0: # delete the comments themselves
UVCommentReport.objects.filter(id__in={d.id for d in denied}).delete() UECommentReport.objects.filter(id__in={d.id for d in denied}).delete()
return super().form_valid(form) return super().form_valid(form)
class UVCreateView(PermissionRequiredMixin, CreateView): class UECreateView(PermissionRequiredMixin, CreateView):
"""Add a new UV (Privileged).""" """Add a new UE (Privileged)."""
model = UV model = UE
form_class = UVForm form_class = UEForm
template_name = "pedagogy/uv_edit.jinja" template_name = "pedagogy/ue_edit.jinja"
permission_required = "pedagogy.add_uv" permission_required = "pedagogy.add_ue"
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
@@ -208,24 +208,24 @@ class UVCreateView(PermissionRequiredMixin, CreateView):
return kwargs return kwargs
class UVDeleteView(PermissionRequiredMixin, DeleteView): class UEDeleteView(PermissionRequiredMixin, DeleteView):
"""Allow to delete an UV (Privileged).""" """Allow to delete an UE (Privileged)."""
model = UV model = UE
pk_url_kwarg = "uv_id" pk_url_kwarg = "ue_id"
template_name = "core/delete_confirm.jinja" template_name = "core/delete_confirm.jinja"
permission_required = "pedagogy.delete_uv" permission_required = "pedagogy.delete_ue"
success_url = reverse_lazy("pedagogy:guide") success_url = reverse_lazy("pedagogy:guide")
class UVUpdateView(PermissionRequiredMixin, UpdateView): class UEUpdateView(PermissionRequiredMixin, UpdateView):
"""Allow to edit an UV (Privilegied).""" """Allow to edit an UE (Privilegied)."""
model = UV model = UE
form_class = UVForm form_class = UEForm
pk_url_kwarg = "uv_id" pk_url_kwarg = "ue_id"
template_name = "pedagogy/uv_edit.jinja" template_name = "pedagogy/ue_edit.jinja"
permission_required = "pedagogy.change_uv" permission_required = "pedagogy.change_ue"
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()

View File

@@ -439,7 +439,7 @@ SITH_SUBSCRIPTION_LOCATIONS = [
SITH_COUNTER_BARS = [(1, "MDE"), (2, "Foyer"), (35, "La Gommette")] SITH_COUNTER_BARS = [(1, "MDE"), (2, "Foyer"), (35, "La Gommette")]
SITH_PEDAGOGY_UV_TYPE = [ SITH_PEDAGOGY_UE_TYPE = [
("FREE", _("Free")), ("FREE", _("Free")),
("CS", _("CS")), ("CS", _("CS")),
("TM", _("TM")), ("TM", _("TM")),
@@ -451,21 +451,21 @@ SITH_PEDAGOGY_UV_TYPE = [
("EXT", _("EXT")), ("EXT", _("EXT")),
] ]
SITH_PEDAGOGY_UV_SEMESTER = [ SITH_PEDAGOGY_UE_SEMESTER = [
("CLOSED", _("Closed")), ("CLOSED", _("Closed")),
("AUTUMN", _("Autumn")), ("AUTUMN", _("Autumn")),
("SPRING", _("Spring")), ("SPRING", _("Spring")),
("AUTUMN_AND_SPRING", _("Autumn and spring")), ("AUTUMN_AND_SPRING", _("Autumn and spring")),
] ]
SITH_PEDAGOGY_UV_LANGUAGE = [ SITH_PEDAGOGY_UE_LANGUAGE = [
("FR", _("French")), ("FR", _("French")),
("EN", _("English")), ("EN", _("English")),
("DE", _("German")), ("DE", _("German")),
("SP", _("Spanish")), ("SP", _("Spanish")),
] ]
SITH_PEDAGOGY_UV_RESULT_GRADE = [ SITH_PEDAGOGY_UE_RESULT_GRADE = [
("A", _("A")), ("A", _("A")),
("B", _("B")), ("B", _("B")),
("C", _("C")), ("C", _("C")),