Merge pull request #1488 from ae-utbm/cgu-approve

Force CGU/ToS approval on account creation
This commit is contained in:
thomas girod authored and GitHub committed 2026-10-01 09:43:02 +02:00
commit e39a73ae32
13 files changed
+403 -185

No files matched your search

+49 -33
View File
@@ -20,6 +20,7 @@
# Place - Suite 330, Boston, MA 02111-1307, USA. # Place - Suite 330, Boston, MA 02111-1307, USA.
# #
# #
import itertools
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from io import StringIO from io import StringIO
from pathlib import Path from pathlib import Path
@@ -120,11 +121,6 @@ class Command(BaseCommand):
) )
self.profiles_root = SithFile.objects.create(name="profiles", owner=root) self.profiles_root = SithFile.objects.create(name="profiles", owner=root)
home_root = SithFile.objects.create(name="users", owner=root) home_root = SithFile.objects.create(name="users", owner=root)
# Page needed for club creation
p = Page(name=settings.SITH_CLUB_ROOT_PAGE)
p.save(force_lock=True)
club_root = SithFile.objects.create(name="clubs", owner=root) club_root = SithFile.objects.create(name="clubs", owner=root)
sas = SithFile.objects.create( sas = SithFile.objects.create(
name="SAS", owner=root, id=settings.SITH_SAS_ROOT_DIR_ID name="SAS", owner=root, id=settings.SITH_SAS_ROOT_DIR_ID
@@ -256,6 +252,7 @@ class Command(BaseCommand):
date_of_birth="1942-06-12", date_of_birth="1942-06-12",
password="plop", password="plop",
) )
User.objects.all().update(cgu_approved_at=now())
User.groups.through.objects.bulk_create( User.groups.through.objects.bulk_create(
[ [
User.groups.through(group=groups.counter_admin, user=counter), User.groups.through(group=groups.counter_admin, user=counter),
@@ -279,34 +276,7 @@ class Command(BaseCommand):
] ]
) )
# Adding syntax help page self._create_pages(groups)
syntax_page = Page(name="Aide_sur_la_syntaxe")
syntax_page.save(force_lock=True)
PageRev.objects.create(
page=syntax_page,
title="Aide sur la syntaxe",
author=skia,
content=(self.ROOT_PATH / "core" / "fixtures" / "SYNTAX.md").read_text(),
)
services_page = Page(name="Services")
services_page.save(force_lock=True)
PageRev.objects.create(
page=services_page,
title="Services",
author=skia,
content="- [Eboutic](/eboutic)\n- Matmat\n- SAS\n- Weekmail\n- Forum",
)
index_page = Page(name="Index")
index_page.save(force_lock=True)
PageRev.objects.create(
page=index_page,
title="Wiki index",
author=root,
content="Welcome to the wiki page!",
)
groups.public.viewable_page.set([syntax_page, services_page, index_page])
self._create_subscription(root) self._create_subscription(root)
self._create_subscription(skia) self._create_subscription(skia)
@@ -577,6 +547,48 @@ class Command(BaseCommand):
] ]
) )
def _create_pages(self, groups: PopulatedGroups):
pages = Page.objects.bulk_create(
[Page(name=s, _full_name=s) for s in settings.SITH_CGU_PAGE.split("/")]
)
for parent, son in itertools.pairwise(pages):
son.parent = parent
son.save(force_lock=True)
cgu_page = pages[-1]
syntax_page = Page(name="Aide_sur_la_syntaxe")
syntax_page.save(force_lock=True)
services_page = Page(name="Services")
services_page.save(force_lock=True)
index_page = Page(name="Index")
index_page.save(force_lock=True)
page_revs = [
PageRev(page=cgu_page, title="Règlement informatique", content=""),
PageRev(
page=syntax_page,
title="Aide sur la syntaxe",
content=(
self.ROOT_PATH / "core" / "fixtures" / "SYNTAX.md"
).read_text(),
),
PageRev(
page=services_page,
title="Services",
content="- [Eboutic](/eboutic)\n- Matmat\n- SAS\n- Weekmail\n- Forum",
),
PageRev(
page=index_page, title="Wiki index", content="Welcome to the wiki page!"
),
]
for rev in page_revs:
rev.author_id = settings.SITH_ROOT_USER_ID
rev.revision = 1
PageRev.objects.bulk_create(page_revs)
groups.public.viewable_page.set(
[syntax_page, services_page, index_page, cgu_page]
)
def _create_products(self, groups: PopulatedGroups, clubs: PopulatedClubs): def _create_products(self, groups: PopulatedGroups, clubs: PopulatedClubs):
beers_type, cotis_type, refill_type, verre_type = ( beers_type, cotis_type, refill_type, verre_type = (
ProductType.objects.bulk_create( ProductType.objects.bulk_create(
@@ -737,6 +749,10 @@ class Command(BaseCommand):
s.save() s.save()
def _create_clubs(self) -> PopulatedClubs: def _create_clubs(self) -> PopulatedClubs:
# Page needed for club creation
p = Page(name=settings.SITH_CLUB_ROOT_PAGE)
p.save(force_lock=True)
ae = Club.objects.create( ae = Club.objects.create(
id=1, name="AE", address="6 Boulevard Anatole France, 90000 Belfort" id=1, name="AE", address="6 Boulevard Anatole France, 90000 Belfort"
) )
@@ -0,0 +1,15 @@
# Generated by Django 5.2.17 on 2026-09-25 12:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("core", "0050_alter_sithfile_moderator")]
operations = [
migrations.AddField(
model_name="user",
name="cgu_approved_at",
field=models.DateTimeField(null=True, verbose_name="ToS approved at"),
),
]
+12 -4
View File
@@ -44,6 +44,7 @@ from django.core.files.base import ContentFile
from django.core.mail import send_mail from django.core.mail import send_mail
from django.db import models, transaction from django.db import models, transaction
from django.db.models import Exists, F, OuterRef, Q from django.db.models import Exists, F, OuterRef, Q
from django.db.models.aggregates import Max
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
@@ -291,6 +292,7 @@ class User(AbstractUser):
), ),
blank=True, blank=True,
) )
cgu_approved_at = models.DateTimeField(_("ToS approved at"), null=True, blank=False)
godfathers = models.ManyToManyField("User", related_name="godchildren", blank=True) godfathers = models.ManyToManyField("User", related_name="godchildren", blank=True)
objects = CustomUserManager() objects = CustomUserManager()
@@ -418,6 +420,14 @@ class User(AbstractUser):
) )
return age return age
@cached_property
def approved_current_cgu(self) -> bool:
qs = PageRev.objects.filter(page___full_name=settings.SITH_CGU_PAGE)
return (
self.cgu_approved_at is not None
and self.cgu_approved_at > qs.aggregate(date=Max("date"))["date"]
)
def make_home(self): def make_home(self):
if self.home is None: if self.home is None:
home_root = SithFile.objects.filter(parent=None, name="users").first() home_root = SithFile.objects.filter(parent=None, name="users").first()
@@ -1233,9 +1243,8 @@ class Page(models.Model):
raise NotLocked("The page is not locked and thus can not be saved") raise NotLocked("The page is not locked and thus can not be saved")
self.full_clean() self.full_clean()
if not self.id: if not self.id:
super().save( # Save a first time to correctly set _full_name
*args, **kwargs super().save(*args, **kwargs)
) # Save a first time to correctly set _full_name
# This reset the _full_name just before saving to maintain a coherent field quicker for queries than the # This reset the _full_name just before saving to maintain a coherent field quicker for queries than the
# recursive method # recursive method
# It also update all the children to maintain correct names # It also update all the children to maintain correct names
@@ -1254,7 +1263,6 @@ class Page(models.Model):
return Page.objects.filter(_full_name=name).first() return Page.objects.filter(_full_name=name).first()
def clean(self): def clean(self):
"""Cleans up only the name for the moment, but this can be used to make any treatment before saving the object."""
if "/" in self.name: if "/" in self.name:
self.name = self.name.split("/")[-1] self.name = self.name.split("/")[-1]
if ( if (
+10 -45
View File
@@ -37,8 +37,7 @@ body {
margin: 0; margin: 0;
} }
>div, form {
>form {
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -49,67 +48,33 @@ body {
max-width: 500px; max-width: 500px;
margin-top: 20px; margin-top: 20px;
>p, input[type="submit"] {
>div {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%; width: 100%;
margin: 0;
>label {
width: 100%;
@media (min-width: 500px) {
width: 300px;
}
}
}
>input,
>p>input,
>div>input {
box-sizing: border-box;
width: 100%;
max-width: 500px;
@media (min-width: 500px) {
max-width: 300px; max-width: 300px;
} margin-top: 1em;
} }
>.errorlist { .errorlist {
color: red; color: red;
text-align: center; text-align: center;
margin: 10px 0 0 0; margin: 10px 0 0 0;
list-style-type: none; list-style-type: none;
} }
>.required>.helptext { div, fieldset {
text-align: center;
font-style: italic;
}
>.required:last-of-type {
box-sizing: border-box;
max-width: 300px; max-width: 300px;
flex-direction: row; }
flex-wrap: wrap; .captcha {
justify-content: space-between; box-sizing: border-box;
>label { fieldset {
width: 100%; margin-bottom: unset
} }
>img { >img {
width: 70px; width: 70px;
object-fit: contain; object-fit: contain;
} }
>input {
width: 200px;
}
} }
} }
} }
+39
View File
@@ -0,0 +1,39 @@
{% extends "core/base.jinja" %}
{%- block additional_css -%}
<link rel="stylesheet" href="{{ static('user/login.scss') }}">
{%- endblock -%}
{% block title %}
{% trans %}Login{% endtrans %}
{% endblock %}
{% block info_boxes %}
{% endblock %}
{% block nav %}
{% endblock %}
{% block content %}
<h1 class="title">{% trans %}Terms of Service{% endtrans %}</h1>
<form method="post" id="login-form">
{% csrf_token %}
<div class="alert alert-yellow">
{% trans trimmed %}
To continue using our services,
please read and approve the AE website's terms of service
{% endtrans %}
</div>
<div class="form-group">
{{ form.cgu_approved_at.errors }}
{{ form.cgu_approved_at }}
{{ form.cgu_approved_at.label_tag() }}
</div>
<input type="hidden" name="next" value="{{ next }}">
<input type="submit" class="btn btn-blue">
</form>
{% endblock %}
+8 -19
View File
@@ -35,28 +35,17 @@
{% csrf_token %} {% csrf_token %}
<div> {{ form }}
<label for="{{ form.username.name }}">{{ form.username.label }}</label>
{{ form.username }}
{{ form.username.errors }}
</div>
<div>
<label for="{{ form.password.name }}">{{ form.password.label }}</label>
{{ form.password }}
{{ form.password.errors }}
</div>
<input type="hidden" name="next" value="{{ next }}"> <input type="hidden" name="next" value="{{ next }}">
<input type="submit" value="{% trans %}Login{% endtrans %}"> <input type="submit" class="btn btn-blue" value="{% trans %}Login{% endtrans %}">
<div>
{# Assumes you setup the password_reset view in your URLconf #} <a href="{{ url("core:password_reset") }}">{% trans %}Lost password?{% endtrans %}</a>
<p> </div>
<a href="{{ url('core:password_reset') }}">{% trans %}Lost password?{% endtrans %}</a> <div>
&nbsp;&nbsp; <a href="{{ url("core:register") }}">{% trans %}Create account{% endtrans %}</a>
<a href="{{ url('core:register') }}">{% trans %}Create account{% endtrans %}</a> </div>
</p>
</form> </form>
{% endblock %} {% endblock %}
+12 -2
View File
@@ -18,7 +18,17 @@
<form action="{{ url('core:register') }}" method="post"> <form action="{{ url('core:register') }}" method="post">
{% csrf_token %} {% csrf_token %}
{% render_honeypot_field %} {% render_honeypot_field %}
{{ form.as_p() }} {% for field in form %}
<input type="submit" value="{% trans %}Register{% endtrans %}" /> {% if field.name not in ["cgu_approved_at", "captcha"] %}
<div>{{ field.as_field_group() }}</div>
{% endif %}
{% endfor %}
<div class="captcha">{{ form.captcha.as_field_group() }}</div>
<div class="form-group">
{{ form.cgu_approved_at.errors }}
{{ form.cgu_approved_at }}
{{ form.cgu_approved_at.label_tag() }}
</div>
<input type="submit" class="btn btn-blue" value="{% trans %}Register{% endtrans %}" />
</form> </form>
{% endblock %} {% endblock %}
+38 -2
View File
@@ -27,6 +27,7 @@ from django.core.exceptions import ValidationError
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.test import Client, RequestFactory, TestCase from django.test import Client, RequestFactory, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import now
from django.views.generic import View from django.views.generic import View
from django.views.generic.base import ContextMixin from django.views.generic.base import ContextMixin
from model_bakery import baker from model_bakery import baker
@@ -55,6 +56,7 @@ class TestUserRegistration:
"password2": "plop", "password2": "plop",
"captcha_0": "dummy-value", "captcha_0": "dummy-value",
"captcha_1": "PASSED", "captcha_1": "PASSED",
"cgu_approved_at": now(),
} }
@pytest.fixture() @pytest.fixture()
@@ -92,6 +94,10 @@ class TestUserRegistration:
({"first_name": ""}, "Ce champ est obligatoire."), ({"first_name": ""}, "Ce champ est obligatoire."),
({"last_name": ""}, "Ce champ est obligatoire."), ({"last_name": ""}, "Ce champ est obligatoire."),
({"captcha_1": "WRONG_CAPTCHA"}, "CAPTCHA invalide"), ({"captcha_1": "WRONG_CAPTCHA"}, "CAPTCHA invalide"),
(
{"cgu_approved_at": False},
"Vous devez approuver les conditions générales d'utilisation",
),
], ],
) )
def test_register_user_form_fail( def test_register_user_form_fail(
@@ -150,7 +156,7 @@ class TestUserRegistration:
class TestUserLogin: class TestUserLogin:
@pytest.fixture() @pytest.fixture()
def user(self) -> User: def user(self) -> User:
return baker.make(User, password=make_password("plop")) return baker.make(User, password=make_password("plop"), cgu_approved_at=now())
@pytest.mark.parametrize( @pytest.mark.parametrize(
"identifier_getter", "identifier_getter",
@@ -191,10 +197,40 @@ class TestUserLogin:
reverse("core:login"), reverse("core:login"),
{"username": identifier_getter(user), "password": "plop"}, {"username": identifier_getter(user), "password": "plop"},
) )
assertRedirects(response, reverse("core:index")) assertRedirects(response, settings.LOGIN_REDIRECT_URL)
assert response.wsgi_request.user == user assert response.wsgi_request.user == user
@pytest.mark.django_db
class TestCGU:
def test_cgu_approval(self, client: Client):
user = baker.make(User, password=make_password("plop"), cgu_approved_at=None)
user_url = user.get_absolute_url()
res = client.post(
reverse("core:login"),
{"username": user.username, "password": "plop", "next": user_url},
)
assertRedirects(res, reverse("core:approve_cgu", query={"next": user_url}))
res = client.post(
reverse("core:approve_cgu"), {"cgu_approved_at": now(), "next": user_url}
)
assertRedirects(res, user_url)
user.refresh_from_db()
assert user.cgu_approved_at is not None
def test_access_cgu_when_already_approved(self, client: Client):
url = reverse("core:approve_cgu")
res = client.get(url)
assertRedirects(res, reverse("core:login"))
client.force_login(baker.make(User, cgu_approved_at=now()))
res = client.get(url)
assertRedirects(res, settings.LOGIN_REDIRECT_URL)
res = client.post(url, {"cgu_approved_at": now()})
assertRedirects(res, settings.LOGIN_REDIRECT_URL)
@pytest.mark.parametrize( @pytest.mark.parametrize(
("md", "html"), ("md", "html"),
[ [
+2
View File
@@ -31,6 +31,7 @@ from core.converters import (
TwoDigitMonthConverter, TwoDigitMonthConverter,
) )
from core.views import ( from core.views import (
CGUApprovalView,
FileDeleteView, FileDeleteView,
FileEditPropView, FileEditPropView,
FileEditView, FileEditView,
@@ -125,6 +126,7 @@ urlpatterns = [
name="password_reset_complete", name="password_reset_complete",
), ),
path("register/", UserCreationView.as_view(), name="register"), path("register/", UserCreationView.as_view(), name="register"),
path("cgu/", CGUApprovalView.as_view(), name="approve_cgu"),
# Group handling # Group handling
path("group/", GroupListView.as_view(), name="group_list"), path("group/", GroupListView.as_view(), name="group_list"),
path("group/new/", GroupCreateView.as_view(), name="group_new"), path("group/new/", GroupCreateView.as_view(), name="group_new"),
+63 -5
View File
@@ -30,9 +30,7 @@ from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.staticfiles.management.commands.collectstatic import ( from django.contrib.staticfiles.storage import staticfiles_storage
staticfiles_storage,
)
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import transaction from django.db import transaction
from django.forms import ( from django.forms import (
@@ -42,6 +40,9 @@ from django.forms import (
TextInput, TextInput,
Widget, Widget,
) )
from django.urls import reverse
from django.utils.functional import lazy
from django.utils.safestring import mark_safe
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from phonenumber_field.widgets import RegionalPhoneNumberWidget from phonenumber_field.widgets import RegionalPhoneNumberWidget
@@ -108,6 +109,43 @@ class FutureDateTimeField(forms.DateTimeField):
return {"min": widget.format_value(now())} return {"min": widget.format_value(now())}
class CGUApprovalField(forms.BooleanField):
"""Form field with a checkbox to approve the CGUs.
The checkbox must be checked to be valid.
If valid, then the value of the field is the current timestamp.
"""
default_error_messages = {"required": _("You must approve the terms of service.")}
__label = None
def __init__(self, *, label_suffix: str | None = "", **kwargs):
# Because the core app of the sith is so huge,
# and because we require a url from the latter,
# putting the reverse into the __init__ will result in it
# being evaluated at server startup time (even with reverse_lazy).
# This will result in a circular import.
# Thus, we must keep the label in its own property and force it to be lazy.
kwargs["label"] = lazy(self.get_label, str)
kwargs["required"] = True
super().__init__(label_suffix=label_suffix, **kwargs)
def to_python(self, value):
return now() if super().to_python(value) else None
def get_label(self):
if not self.__label:
url = reverse("core:page", kwargs={"page_name": settings.SITH_CGU_PAGE})
self.__label = mark_safe(
_(
"I have read and I approve the "
'<a href="%(url)s" target="_blank">Terms of Service</a>'
)
% {"url": url}
)
return self.__label
# Forms # Forms
@@ -146,8 +184,28 @@ class RegisteringForm(UserCreationForm):
class Meta: class Meta:
model = User model = User
fields = ("first_name", "last_name", "email") fields = ("first_name", "last_name", "email", "cgu_approved_at")
field_classes = {"email": AntiSpamEmailField} field_classes = {
"email": AntiSpamEmailField,
"cgu_approved_at": CGUApprovalField,
}
class CGUApprovalForm(forms.ModelForm):
class Meta:
model = User
fields = ["cgu_approved_at"]
field_classes = {"cgu_approved_at": CGUApprovalField}
def __init__(self, *args, instance: User | None = None, **kwargs):
if instance:
# If this form is displayed,
# then we want the user to explicitly check the button.
# So we pretend cgu were never approved (even if they were).
# If we didn't do that, the button would be initially checked,
# even if the approval was done before the last CGU version.
instance.cgu_approved_at = False
super().__init__(*args, instance=instance, **kwargs)
class UserProfileForm(forms.ModelForm): class UserProfileForm(forms.ModelForm):
+33 -3
View File
@@ -27,12 +27,12 @@ from datetime import timedelta
# This file contains all the views that concern the user model # This file contains all the views that concern the user model
from operator import itemgetter from operator import itemgetter
from smtplib import SMTPException from smtplib import SMTPException
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Any
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import login, views from django.contrib.auth import login, views
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import PasswordChangeForm, SetPasswordForm from django.contrib.auth.forms import SetPasswordForm
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
@@ -60,6 +60,7 @@ from honeypot.decorators import check_honeypot
from core.auth.mixins import CanEditMixin, CanEditPropMixin, CanViewMixin from core.auth.mixins import CanEditMixin, CanEditPropMixin, CanViewMixin
from core.models import Gift, Preferences, User from core.models import Gift, Preferences, User
from core.views.forms import ( from core.views.forms import (
CGUApprovalForm,
GiftForm, GiftForm,
LoginForm, LoginForm,
RegisteringForm, RegisteringForm,
@@ -71,6 +72,7 @@ from core.views.forms import (
from core.views.mixins import FragmentMixin, TabedViewMixin, UseFragmentsMixin from core.views.mixins import FragmentMixin, TabedViewMixin, UseFragmentsMixin
from counter.models import Refilling, Selling from counter.models import Refilling, Selling
from eboutic.models import Invoice from eboutic.models import Invoice
from sith import settings
from trombi.views import UserTrombiForm from trombi.views import UserTrombiForm
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -82,9 +84,16 @@ class SithLoginView(views.LoginView):
template_name = "core/login.jinja" template_name = "core/login.jinja"
authentication_form = LoginForm authentication_form = LoginForm
form_class = PasswordChangeForm
redirect_authenticated_user = True redirect_authenticated_user = True
def get_success_url(self) -> str:
redirect_to = self.get_redirect_url()
default_url = self.get_default_redirect_url()
if not self.request.user.approved_current_cgu:
query = {"next": redirect_to} if redirect_to else {}
return reverse("core:approve_cgu", query=query)
return redirect_to or default_url
class SithPasswordChangeView(views.PasswordChangeView): class SithPasswordChangeView(views.PasswordChangeView):
"""Allows a user to change its password.""" """Allows a user to change its password."""
@@ -188,6 +197,27 @@ class UserCreationView(FormView):
return super().form_valid(form) return super().form_valid(form)
class CGUApprovalView(views.RedirectURLMixin, UpdateView):
form_class = CGUApprovalForm
next_page = settings.LOGIN_REDIRECT_URL
template_name = "core/cgu_approve.jinja"
def dispatch(self, request, *args, **kwargs):
if self.request.user.is_anonymous:
return redirect("core:login")
if self.request.user.approved_current_cgu:
return redirect(self.get_success_url())
return super().dispatch(request, *args, **kwargs)
def get_object(self, *args, **kwargs):
return self.request.user
def get_context_data(self, **kwargs) -> dict[str, Any]:
return super().get_context_data(**kwargs) | {
self.redirect_field_name: self.get_redirect_url()
}
class UserMeRedirect(LoginRequiredMixin, RedirectView): class UserMeRedirect(LoginRequiredMixin, RedirectView):
def get_redirect_url(self, *args, **kwargs): def get_redirect_url(self, *args, **kwargs):
if remaining := kwargs.get("remaining_path"): if remaining := kwargs.get("remaining_path"):
+119 -72
View File
@@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-09 07:21+0200\n" "POT-Creation-Date: 2026-09-21 13:16+0200\n"
"PO-Revision-Date: 2016-07-18\n" "PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n" "Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n" "Language-Team: AE info <ae.info@utbm.fr>\n"
@@ -736,8 +736,9 @@ msgstr "Méthode de paiement"
#: 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/ue_detail.jinja sas/templates/sas/album.jinja #: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
#: pedagogy/templates/pedagogy/guide.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
@@ -931,8 +932,9 @@ msgstr "Outils"
#: counter/templates/counter/cash_summary_list.jinja #: counter/templates/counter/cash_summary_list.jinja
#: 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/ue_detail.jinja sas/templates/sas/album.jinja #: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
#: pedagogy/templates/pedagogy/guide.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"
@@ -1835,6 +1837,10 @@ msgstr ""
"Même si ce profil est caché, les utilisateurs sur cette liste pourront " "Même si ce profil est caché, les utilisateurs sur cette liste pourront "
"toujours le voir." "toujours le voir."
#: core/models.py
msgid "ToS approved"
msgstr "CGU approuvées"
#: core/models.py #: core/models.py
msgid "A user with that username already exists" msgid "A user with that username already exists"
msgstr "Un utilisateur de ce nom d'utilisateur existe déjà" msgstr "Un utilisateur de ce nom d'utilisateur existe déjà"
@@ -2109,7 +2115,8 @@ msgstr "R&D"
msgid "Site created by the IT Department of the AE" msgid "Site created by the IT Department of the AE"
msgstr "Site réalisé par le Pôle Informatique de l'AE" msgstr "Site réalisé par le Pôle Informatique de l'AE"
#: core/templates/core/base/header.jinja core/templates/core/login.jinja #: core/templates/core/base/header.jinja core/templates/core/cgu_approve.jinja
#: core/templates/core/login.jinja
#: core/templates/core/password_reset_complete.jinja #: core/templates/core/password_reset_complete.jinja
msgid "Login" msgid "Login"
msgstr "Connexion" msgstr "Connexion"
@@ -2207,6 +2214,18 @@ msgstr "FAQ"
msgid "Wiki" msgid "Wiki"
msgstr "Wiki" msgstr "Wiki"
#: core/templates/core/cgu_approve.jinja
msgid "Terms of Service"
msgstr "Conditions générales d'utilisation"
#: core/templates/core/cgu_approve.jinja
msgid ""
"To continue using our services, please read and approve the AE website's "
"terms of service"
msgstr ""
"Pour continuer à utiliser nos services, veuillez lire et approuver les "
"conditions générales d'utilisation du site AE."
#: core/templates/core/create.jinja #: core/templates/core/create.jinja
#, python-format #, python-format
msgid "Create %(name)s" msgid "Create %(name)s"
@@ -2233,6 +2252,7 @@ msgstr "Confirmation"
#: core/templates/core/file_delete_confirm.jinja #: core/templates/core/file_delete_confirm.jinja
#: counter/templates/counter/counter_click.jinja #: counter/templates/counter/counter_click.jinja
#: counter/templates/counter/fragments/delete_student_card.jinja #: counter/templates/counter/fragments/delete_student_card.jinja
#: pedagogy/templates/pedagogy/fragments/comment_report.jinja
#: sas/templates/sas/ask_picture_removal.jinja #: sas/templates/sas/ask_picture_removal.jinja
msgid "Cancel" msgid "Cancel"
msgstr "Annuler" msgstr "Annuler"
@@ -2259,6 +2279,7 @@ msgstr "Propriétés"
#: core/templates/core/file_delete_confirm.jinja #: core/templates/core/file_delete_confirm.jinja
#: counter/templates/counter/fragments/delete_student_card.jinja #: counter/templates/counter/fragments/delete_student_card.jinja
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
#, python-format #, python-format
msgid "Are you sure you want to delete \"%(obj)s\"?" msgid "Are you sure you want to delete \"%(obj)s\"?"
msgstr "Êtes-vous sûr de vouloir supprimer \"%(obj)s\" ?" msgstr "Êtes-vous sûr de vouloir supprimer \"%(obj)s\" ?"
@@ -3133,6 +3154,19 @@ msgstr "Appliquer les droits récursivement"
msgid "Ensure this timestamp is set in the future" msgid "Ensure this timestamp is set in the future"
msgstr "Assurez-vous que cet horodatage est dans le futur" msgstr "Assurez-vous que cet horodatage est dans le futur"
#: core/views/forms.py
msgid "You must approve the terms of service."
msgstr "Vous devez approuver les conditions générales d'utilisation"
#: core/views/forms.py
#, python-format
msgid ""
"I have read and I approve the <a href=\"%(url)s\" target=\"_blank\">Terms of "
"Service</a>"
msgstr ""
"J'ai lu et j'approuve les <a href=\"%(url)s\" target=\"_blank\">Conditions "
"Générales d'utilisation</a>"
#: core/views/forms.py #: core/views/forms.py
msgid "Username, email, or account number" msgid "Username, email, or account number"
msgstr "Nom d'utilisateur, email, ou numéro de compte AE" msgstr "Nom d'utilisateur, email, ou numéro de compte AE"
@@ -3778,8 +3812,8 @@ msgid "Emptied"
msgstr "Coffre vidé" 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/fragments/ue_comment_form.jinja
#: pedagogy/templates/pedagogy/moderation.jinja #: pedagogy/templates/pedagogy/moderation.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"
@@ -5109,6 +5143,80 @@ msgstr "signaler"
msgid "reporter" msgid "reporter"
msgstr "signalant" msgstr "signalant"
#: pedagogy/templates/pedagogy/fragments/comment_report.jinja
msgid "Report"
msgstr "Signaler"
#: pedagogy/templates/pedagogy/fragments/ue_comment_form.jinja
msgid "Leave comment"
msgstr "Laisser un commentaire"
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
#: trombi/templates/trombi/export.jinja
msgid "Comments"
msgstr "Commentaires"
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
#: pedagogy/templates/pedagogy/fragments/ue_details/grade.jinja
msgid "Global grade"
msgstr "Note globale"
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
#: pedagogy/templates/pedagogy/fragments/ue_details/grade.jinja
msgid "Utility"
msgstr "Utilité"
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
#: pedagogy/templates/pedagogy/fragments/ue_details/grade.jinja
msgid "Interest"
msgstr "Intérêt"
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
#: pedagogy/templates/pedagogy/fragments/ue_details/grade.jinja
msgid "Teaching"
msgstr "Enseignement"
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
#: pedagogy/templates/pedagogy/fragments/ue_details/grade.jinja
msgid "Work load"
msgstr "Charge de travail"
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
msgid "This comment has been reported"
msgstr "Ce commentaire a été signalé"
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
msgid "Report this comment"
msgstr "Signaler ce commentaire"
#: pedagogy/templates/pedagogy/fragments/ue_details/form.jinja
msgid ""
"You already posted a comment on this UE. If you want to comment again, "
"please modify or delete your previous comment."
msgstr ""
"Vous avez déjà commenté cette UE. Si vous voulez de nouveau commenter, "
"veuillez modifier ou supprimer votre commentaire précédent."
#: pedagogy/templates/pedagogy/fragments/ue_details/grade.jinja
msgid "Objectives"
msgstr "Objectifs"
#: pedagogy/templates/pedagogy/fragments/ue_details/grade.jinja
msgid "Program"
msgstr "Programme"
#: pedagogy/templates/pedagogy/fragments/ue_details/grade.jinja
msgid "Earned skills"
msgstr "Compétences acquises"
#: pedagogy/templates/pedagogy/fragments/ue_details/grade.jinja
msgid "Key concepts"
msgstr "Concepts clefs"
#: pedagogy/templates/pedagogy/fragments/ue_details/grade.jinja
msgid "UE manager: "
msgstr "Gestionnaire d'UE : "
#: 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."
@@ -5117,6 +5225,10 @@ msgstr "Un guide de tous les cours disponibles à l'UTBM."
msgid "Search UE" msgid "Search UE"
msgstr "Recherche d'UE" msgstr "Recherche d'UE"
#: pedagogy/templates/pedagogy/guide.jinja
msgid "Hide closed UEs"
msgstr "Cacher les UEs fermées"
#: pedagogy/templates/pedagogy/guide.jinja #: pedagogy/templates/pedagogy/guide.jinja
#, python-format #, python-format
msgid "%(display_name)s" msgid "%(display_name)s"
@@ -5189,71 +5301,6 @@ msgstr "TE : "
msgid "THE: " msgid "THE: "
msgstr "THE : " msgstr "THE : "
#: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Global grade"
msgstr "Note globale"
#: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Utility"
msgstr "Utilité"
#: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Interest"
msgstr "Intérêt"
#: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Teaching"
msgstr "Enseignement"
#: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Work load"
msgstr "Charge de travail"
#: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Objectives"
msgstr "Objectifs"
#: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Program"
msgstr "Programme"
#: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Earned skills"
msgstr "Compétences acquises"
#: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Key concepts"
msgstr "Concepts clefs"
#: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "UE manager: "
msgstr "Gestionnaire d'UE : "
#: pedagogy/templates/pedagogy/ue_detail.jinja pedagogy/tests/tests.py
msgid ""
"You already posted a comment on this UE. If you want to comment again, "
"please modify or delete your previous comment."
msgstr ""
"Vous avez déjà commenté cette UE. Si vous voulez de nouveau commenter, "
"veuillez modifier ou supprimer votre commentaire précédent."
#: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Leave comment"
msgstr "Laisser un commentaire"
#: pedagogy/templates/pedagogy/ue_detail.jinja
#: trombi/templates/trombi/export.jinja
msgid "Comments"
msgstr "Commentaires"
#: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "This comment has been reported"
msgstr "Ce commentaire a été signalé"
#: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Report this comment"
msgstr "Signaler ce commentaire"
#: pedagogy/templates/pedagogy/ue_edit.jinja #: pedagogy/templates/pedagogy/ue_edit.jinja
msgid "Edit UE" msgid "Edit UE"
msgstr "Éditer l'UE" msgstr "Éditer l'UE"
+3
View File
@@ -372,6 +372,9 @@ SITH_PDF_CLUB_ID = env.int("SITH_PDF_CLUB_ID", default=2)
# Main root for club pages # Main root for club pages
SITH_CLUB_ROOT_PAGE = "clubs" SITH_CLUB_ROOT_PAGE = "clubs"
SITH_CGU_PAGE = env.str("SITH_CGU_PAGE", default="legals/ri")
# Define the date in the year serving as # Define the date in the year serving as
# reference for the subscriptions calendar (month, day) # reference for the subscriptions calendar (month, day)
SITH_SEMESTER_START_AUTUMN = (8, 15) # 15 August SITH_SEMESTER_START_AUTUMN = (8, 15) # 15 August