mirror of
https://github.com/ae-utbm/sith.git
synced 2026-10-04 09:54:16 +00:00
Merge pull request #1488 from ae-utbm/cgu-approve
Force CGU/ToS approval on account creation
This commit is contained in:
13 files changed
+404
-186
No files matched your search
@@ -20,6 +20,7 @@
|
||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
#
|
||||
import itertools
|
||||
from datetime import date, datetime, timedelta
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
@@ -120,11 +121,6 @@ class Command(BaseCommand):
|
||||
)
|
||||
self.profiles_root = SithFile.objects.create(name="profiles", 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)
|
||||
sas = SithFile.objects.create(
|
||||
name="SAS", owner=root, id=settings.SITH_SAS_ROOT_DIR_ID
|
||||
@@ -256,6 +252,7 @@ class Command(BaseCommand):
|
||||
date_of_birth="1942-06-12",
|
||||
password="plop",
|
||||
)
|
||||
User.objects.all().update(cgu_approved_at=now())
|
||||
User.groups.through.objects.bulk_create(
|
||||
[
|
||||
User.groups.through(group=groups.counter_admin, user=counter),
|
||||
@@ -279,34 +276,7 @@ class Command(BaseCommand):
|
||||
]
|
||||
)
|
||||
|
||||
# Adding syntax help page
|
||||
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_pages(groups)
|
||||
|
||||
self._create_subscription(root)
|
||||
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):
|
||||
beers_type, cotis_type, refill_type, verre_type = (
|
||||
ProductType.objects.bulk_create(
|
||||
@@ -737,6 +749,10 @@ class Command(BaseCommand):
|
||||
s.save()
|
||||
|
||||
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(
|
||||
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
@@ -44,6 +44,7 @@ from django.core.files.base import ContentFile
|
||||
from django.core.mail import send_mail
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Exists, F, OuterRef, Q
|
||||
from django.db.models.aggregates import Max
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
@@ -291,6 +292,7 @@ class User(AbstractUser):
|
||||
),
|
||||
blank=True,
|
||||
)
|
||||
cgu_approved_at = models.DateTimeField(_("ToS approved at"), null=True, blank=False)
|
||||
godfathers = models.ManyToManyField("User", related_name="godchildren", blank=True)
|
||||
|
||||
objects = CustomUserManager()
|
||||
@@ -418,6 +420,14 @@ class User(AbstractUser):
|
||||
)
|
||||
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):
|
||||
if self.home is None:
|
||||
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")
|
||||
self.full_clean()
|
||||
if not self.id:
|
||||
super().save(
|
||||
*args, **kwargs
|
||||
) # Save a first time to correctly set _full_name
|
||||
# Save a first time to correctly set _full_name
|
||||
super().save(*args, **kwargs)
|
||||
# This reset the _full_name just before saving to maintain a coherent field quicker for queries than the
|
||||
# recursive method
|
||||
# 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()
|
||||
|
||||
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:
|
||||
self.name = self.name.split("/")[-1]
|
||||
if (
|
||||
|
||||
+11
-46
@@ -37,8 +37,7 @@ body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
>div,
|
||||
>form {
|
||||
form {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -49,67 +48,33 @@ body {
|
||||
max-width: 500px;
|
||||
margin-top: 20px;
|
||||
|
||||
>p,
|
||||
>div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
input[type="submit"] {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
|
||||
>label {
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: 500px) {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
max-width: 300px;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
>input,
|
||||
>p>input,
|
||||
>div>input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
|
||||
@media (min-width: 500px) {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
>.errorlist {
|
||||
.errorlist {
|
||||
color: red;
|
||||
text-align: center;
|
||||
margin: 10px 0 0 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
>.required>.helptext {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
>.required:last-of-type {
|
||||
box-sizing: border-box;
|
||||
div, fieldset {
|
||||
max-width: 300px;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.captcha {
|
||||
box-sizing: border-box;
|
||||
|
||||
>label {
|
||||
width: 100%;
|
||||
fieldset {
|
||||
margin-bottom: unset
|
||||
}
|
||||
|
||||
>img {
|
||||
width: 70px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
>input {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -35,28 +35,17 @@
|
||||
|
||||
{% csrf_token %}
|
||||
|
||||
<div>
|
||||
<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>
|
||||
{{ form }}
|
||||
|
||||
<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 %}">
|
||||
|
||||
|
||||
{# Assumes you setup the password_reset view in your URLconf #}
|
||||
<p>
|
||||
<a href="{{ url('core:password_reset') }}">{% trans %}Lost password?{% endtrans %}</a>
|
||||
|
||||
<a href="{{ url('core:register') }}">{% trans %}Create account{% endtrans %}</a>
|
||||
</p>
|
||||
<div>
|
||||
<a href="{{ url("core:password_reset") }}">{% trans %}Lost password?{% endtrans %}</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{ url("core:register") }}">{% trans %}Create account{% endtrans %}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -18,7 +18,17 @@
|
||||
<form action="{{ url('core:register') }}" method="post">
|
||||
{% csrf_token %}
|
||||
{% render_honeypot_field %}
|
||||
{{ form.as_p() }}
|
||||
<input type="submit" value="{% trans %}Register{% endtrans %}" />
|
||||
{% for field in form %}
|
||||
{% 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>
|
||||
{% endblock %}
|
||||
+38
-2
@@ -27,6 +27,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.core.mail import EmailMessage
|
||||
from django.test import Client, RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from django.views.generic import View
|
||||
from django.views.generic.base import ContextMixin
|
||||
from model_bakery import baker
|
||||
@@ -55,6 +56,7 @@ class TestUserRegistration:
|
||||
"password2": "plop",
|
||||
"captcha_0": "dummy-value",
|
||||
"captcha_1": "PASSED",
|
||||
"cgu_approved_at": now(),
|
||||
}
|
||||
|
||||
@pytest.fixture()
|
||||
@@ -92,6 +94,10 @@ class TestUserRegistration:
|
||||
({"first_name": ""}, "Ce champ est obligatoire."),
|
||||
({"last_name": ""}, "Ce champ est obligatoire."),
|
||||
({"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(
|
||||
@@ -150,7 +156,7 @@ class TestUserRegistration:
|
||||
class TestUserLogin:
|
||||
@pytest.fixture()
|
||||
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(
|
||||
"identifier_getter",
|
||||
@@ -191,10 +197,40 @@ class TestUserLogin:
|
||||
reverse("core:login"),
|
||||
{"username": identifier_getter(user), "password": "plop"},
|
||||
)
|
||||
assertRedirects(response, reverse("core:index"))
|
||||
assertRedirects(response, settings.LOGIN_REDIRECT_URL)
|
||||
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(
|
||||
("md", "html"),
|
||||
[
|
||||
|
||||
@@ -31,6 +31,7 @@ from core.converters import (
|
||||
TwoDigitMonthConverter,
|
||||
)
|
||||
from core.views import (
|
||||
CGUApprovalView,
|
||||
FileDeleteView,
|
||||
FileEditPropView,
|
||||
FileEditView,
|
||||
@@ -125,6 +126,7 @@ urlpatterns = [
|
||||
name="password_reset_complete",
|
||||
),
|
||||
path("register/", UserCreationView.as_view(), name="register"),
|
||||
path("cgu/", CGUApprovalView.as_view(), name="approve_cgu"),
|
||||
# Group handling
|
||||
path("group/", GroupListView.as_view(), name="group_list"),
|
||||
path("group/new/", GroupCreateView.as_view(), name="group_new"),
|
||||
|
||||
+63
-5
@@ -30,9 +30,7 @@ from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.staticfiles.management.commands.collectstatic import (
|
||||
staticfiles_storage,
|
||||
)
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.forms import (
|
||||
@@ -42,6 +40,9 @@ from django.forms import (
|
||||
TextInput,
|
||||
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.translation import gettext_lazy as _
|
||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||
@@ -108,6 +109,43 @@ class FutureDateTimeField(forms.DateTimeField):
|
||||
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
|
||||
|
||||
|
||||
@@ -146,8 +184,28 @@ class RegisteringForm(UserCreationForm):
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ("first_name", "last_name", "email")
|
||||
field_classes = {"email": AntiSpamEmailField}
|
||||
fields = ("first_name", "last_name", "email", "cgu_approved_at")
|
||||
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):
|
||||
|
||||
+33
-3
@@ -27,12 +27,12 @@ from datetime import timedelta
|
||||
# This file contains all the views that concern the user model
|
||||
from operator import itemgetter
|
||||
from smtplib import SMTPException
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import login, views
|
||||
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.messages.views import SuccessMessageMixin
|
||||
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.models import Gift, Preferences, User
|
||||
from core.views.forms import (
|
||||
CGUApprovalForm,
|
||||
GiftForm,
|
||||
LoginForm,
|
||||
RegisteringForm,
|
||||
@@ -71,6 +72,7 @@ from core.views.forms import (
|
||||
from core.views.mixins import FragmentMixin, TabedViewMixin, UseFragmentsMixin
|
||||
from counter.models import Refilling, Selling
|
||||
from eboutic.models import Invoice
|
||||
from sith import settings
|
||||
from trombi.views import UserTrombiForm
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -82,9 +84,16 @@ class SithLoginView(views.LoginView):
|
||||
|
||||
template_name = "core/login.jinja"
|
||||
authentication_form = LoginForm
|
||||
form_class = PasswordChangeForm
|
||||
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):
|
||||
"""Allows a user to change its password."""
|
||||
@@ -188,6 +197,27 @@ class UserCreationView(FormView):
|
||||
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):
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
if remaining := kwargs.get("remaining_path"):
|
||||
|
||||
+119
-72
@@ -6,7 +6,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"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"
|
||||
"Last-Translator: Maréchal <thomas.girod@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/last_ops.jinja
|
||||
#: election/templates/election/election_detail.jinja
|
||||
#: forum/templates/forum/macros.jinja pedagogy/templates/pedagogy/guide.jinja
|
||||
#: pedagogy/templates/pedagogy/ue_detail.jinja sas/templates/sas/album.jinja
|
||||
#: forum/templates/forum/macros.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
|
||||
#: trombi/templates/trombi/detail.jinja
|
||||
#: trombi/templates/trombi/edit_profile.jinja
|
||||
@@ -931,8 +932,9 @@ msgstr "Outils"
|
||||
#: counter/templates/counter/cash_summary_list.jinja
|
||||
#: counter/templates/counter/counter_list.jinja
|
||||
#: election/templates/election/election_detail.jinja
|
||||
#: forum/templates/forum/macros.jinja pedagogy/templates/pedagogy/guide.jinja
|
||||
#: pedagogy/templates/pedagogy/ue_detail.jinja sas/templates/sas/album.jinja
|
||||
#: forum/templates/forum/macros.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/edit_profile.jinja
|
||||
msgid "Edit"
|
||||
@@ -1835,6 +1837,10 @@ msgstr ""
|
||||
"Même si ce profil est caché, les utilisateurs sur cette liste pourront "
|
||||
"toujours le voir."
|
||||
|
||||
#: core/models.py
|
||||
msgid "ToS approved"
|
||||
msgstr "CGU approuvées"
|
||||
|
||||
#: core/models.py
|
||||
msgid "A user with that username already exists"
|
||||
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"
|
||||
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
|
||||
msgid "Login"
|
||||
msgstr "Connexion"
|
||||
@@ -2207,6 +2214,18 @@ msgstr "FAQ"
|
||||
msgid "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
|
||||
#, python-format
|
||||
msgid "Create %(name)s"
|
||||
@@ -2233,6 +2252,7 @@ msgstr "Confirmation"
|
||||
#: core/templates/core/file_delete_confirm.jinja
|
||||
#: counter/templates/counter/counter_click.jinja
|
||||
#: counter/templates/counter/fragments/delete_student_card.jinja
|
||||
#: pedagogy/templates/pedagogy/fragments/comment_report.jinja
|
||||
#: sas/templates/sas/ask_picture_removal.jinja
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
@@ -2259,6 +2279,7 @@ msgstr "Propriétés"
|
||||
|
||||
#: core/templates/core/file_delete_confirm.jinja
|
||||
#: counter/templates/counter/fragments/delete_student_card.jinja
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
|
||||
#, python-format
|
||||
msgid "Are you sure you want to delete \"%(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"
|
||||
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
|
||||
msgid "Username, email, or account number"
|
||||
msgstr "Nom d'utilisateur, email, ou numéro de compte AE"
|
||||
@@ -3778,8 +3812,8 @@ msgid "Emptied"
|
||||
msgstr "Coffre vidé"
|
||||
|
||||
#: 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/ue_detail.jinja
|
||||
#: trombi/templates/trombi/comment.jinja
|
||||
#: trombi/templates/trombi/user_tools.jinja
|
||||
msgid "Comment"
|
||||
@@ -5109,6 +5143,80 @@ msgstr "signaler"
|
||||
msgid "reporter"
|
||||
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
|
||||
msgid "A guide of courses available at 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"
|
||||
msgstr "Recherche d'UE"
|
||||
|
||||
#: pedagogy/templates/pedagogy/guide.jinja
|
||||
msgid "Hide closed UEs"
|
||||
msgstr "Cacher les UEs fermées"
|
||||
|
||||
#: pedagogy/templates/pedagogy/guide.jinja
|
||||
#, python-format
|
||||
msgid "%(display_name)s"
|
||||
@@ -5189,71 +5301,6 @@ msgstr "TE : "
|
||||
msgid "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
|
||||
msgid "Edit UE"
|
||||
msgstr "Éditer l'UE"
|
||||
|
||||
@@ -372,6 +372,9 @@ SITH_PDF_CLUB_ID = env.int("SITH_PDF_CLUB_ID", default=2)
|
||||
# Main root for club pages
|
||||
SITH_CLUB_ROOT_PAGE = "clubs"
|
||||
|
||||
SITH_CGU_PAGE = env.str("SITH_CGU_PAGE", default="legals/ri")
|
||||
|
||||
|
||||
# Define the date in the year serving as
|
||||
# reference for the subscriptions calendar (month, day)
|
||||
SITH_SEMESTER_START_AUTUMN = (8, 15) # 15 August
|
||||
|
||||
Reference in new issue
Block a user