mirror of
https://github.com/ae-utbm/sith.git
synced 2026-09-26 06:04:21 +00:00
use DatetimeField to store cgu approval
Au lieu de dire si l'utilisateur a approuvé les CGUs ou non, on dit quand est-ce qu'il les a approuvées. Si la date de la dernière approbation est antérieure à la date de la dernière révision des CGUs, alors on demande à nouveau de les approuver.
This commit is contained in:
@@ -252,7 +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=True)
|
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),
|
||||||
|
|||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.2.17 on 2026-09-15 11:02
|
# Generated by Django 5.2.17 on 2026-09-25 12:26
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ class Migration(migrations.Migration):
|
|||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="user",
|
model_name="user",
|
||||||
name="cgu_approved",
|
name="cgu_approved_at",
|
||||||
field=models.BooleanField(default=False, verbose_name="ToS approved"),
|
field=models.DateTimeField(null=True, verbose_name="ToS approved at"),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
+10
-1
@@ -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,7 +292,7 @@ class User(AbstractUser):
|
|||||||
),
|
),
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
cgu_approved = models.BooleanField(_("ToS approved"), default=False)
|
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()
|
||||||
@@ -419,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()
|
||||||
|
|||||||
@@ -27,9 +27,9 @@
|
|||||||
{% endtrans %}
|
{% endtrans %}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
{{ form.cgu_approved.errors }}
|
{{ form.cgu_approved_at.errors }}
|
||||||
{{ form.cgu_approved }}
|
{{ form.cgu_approved_at }}
|
||||||
{{ form.cgu_approved.label_tag() }}
|
{{ form.cgu_approved_at.label_tag() }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="hidden" name="next" value="{{ next }}">
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
|||||||
@@ -19,15 +19,15 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% render_honeypot_field %}
|
{% render_honeypot_field %}
|
||||||
{% for field in form %}
|
{% for field in form %}
|
||||||
{% if field.name not in ["cgu_approved", "captcha"] %}
|
{% if field.name not in ["cgu_approved_at", "captcha"] %}
|
||||||
<div>{{ field.as_field_group() }}</div>
|
<div>{{ field.as_field_group() }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="captcha">{{ form.captcha.as_field_group() }}</div>
|
<div class="captcha">{{ form.captcha.as_field_group() }}</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
{{ form.cgu_approved.errors }}
|
{{ form.cgu_approved_at.errors }}
|
||||||
{{ form.cgu_approved }}
|
{{ form.cgu_approved_at }}
|
||||||
{{ form.cgu_approved.label_tag() }}
|
{{ form.cgu_approved_at.label_tag() }}
|
||||||
</div>
|
</div>
|
||||||
<input type="submit" class="btn btn-blue" value="{% trans %}Register{% endtrans %}" />
|
<input type="submit" class="btn btn-blue" value="{% trans %}Register{% endtrans %}" />
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -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,7 +56,7 @@ class TestUserRegistration:
|
|||||||
"password2": "plop",
|
"password2": "plop",
|
||||||
"captcha_0": "dummy-value",
|
"captcha_0": "dummy-value",
|
||||||
"captcha_1": "PASSED",
|
"captcha_1": "PASSED",
|
||||||
"cgu_approved": True,
|
"cgu_approved_at": now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
@@ -94,7 +95,7 @@ class TestUserRegistration:
|
|||||||
({"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": ""},
|
{"cgu_approved_at": False},
|
||||||
"Vous devez approuver les conditions générales d'utilisation",
|
"Vous devez approuver les conditions générales d'utilisation",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -155,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"), cgu_approved=True)
|
return baker.make(User, password=make_password("plop"), cgu_approved_at=now())
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"identifier_getter",
|
"identifier_getter",
|
||||||
@@ -203,7 +204,7 @@ class TestUserLogin:
|
|||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
class TestCGU:
|
class TestCGU:
|
||||||
def test_cgu_approval(self, client: Client):
|
def test_cgu_approval(self, client: Client):
|
||||||
user = baker.make(User, password=make_password("plop"), cgu_approved=False)
|
user = baker.make(User, password=make_password("plop"), cgu_approved_at=None)
|
||||||
user_url = user.get_absolute_url()
|
user_url = user.get_absolute_url()
|
||||||
res = client.post(
|
res = client.post(
|
||||||
reverse("core:login"),
|
reverse("core:login"),
|
||||||
@@ -211,11 +212,11 @@ class TestCGU:
|
|||||||
)
|
)
|
||||||
assertRedirects(res, reverse("core:approve_cgu", query={"next": user_url}))
|
assertRedirects(res, reverse("core:approve_cgu", query={"next": user_url}))
|
||||||
res = client.post(
|
res = client.post(
|
||||||
reverse("core:approve_cgu"), {"cgu_approved": True, "next": user_url}
|
reverse("core:approve_cgu"), {"cgu_approved_at": now(), "next": user_url}
|
||||||
)
|
)
|
||||||
assertRedirects(res, user_url)
|
assertRedirects(res, user_url)
|
||||||
user.refresh_from_db()
|
user.refresh_from_db()
|
||||||
assert user.cgu_approved
|
assert user.cgu_approved_at is not None
|
||||||
|
|
||||||
def test_access_cgu_when_already_approved(self, client: Client):
|
def test_access_cgu_when_already_approved(self, client: Client):
|
||||||
url = reverse("core:approve_cgu")
|
url = reverse("core:approve_cgu")
|
||||||
@@ -223,10 +224,10 @@ class TestCGU:
|
|||||||
res = client.get(url)
|
res = client.get(url)
|
||||||
assertRedirects(res, reverse("core:login"))
|
assertRedirects(res, reverse("core:login"))
|
||||||
|
|
||||||
client.force_login(baker.make(User, cgu_approved=True))
|
client.force_login(baker.make(User, cgu_approved_at=now()))
|
||||||
res = client.get(url)
|
res = client.get(url)
|
||||||
assertRedirects(res, settings.LOGIN_REDIRECT_URL)
|
assertRedirects(res, settings.LOGIN_REDIRECT_URL)
|
||||||
res = client.post(url, {"cgu_approved": True})
|
res = client.post(url, {"cgu_approved_at": now()})
|
||||||
assertRedirects(res, settings.LOGIN_REDIRECT_URL)
|
assertRedirects(res, settings.LOGIN_REDIRECT_URL)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+26
-4
@@ -110,6 +110,12 @@ class FutureDateTimeField(forms.DateTimeField):
|
|||||||
|
|
||||||
|
|
||||||
class CGUApprovalField(forms.BooleanField):
|
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.")}
|
default_error_messages = {"required": _("You must approve the terms of service.")}
|
||||||
__label = None
|
__label = None
|
||||||
|
|
||||||
@@ -124,6 +130,9 @@ class CGUApprovalField(forms.BooleanField):
|
|||||||
kwargs["required"] = True
|
kwargs["required"] = True
|
||||||
super().__init__(label_suffix=label_suffix, **kwargs)
|
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):
|
def get_label(self):
|
||||||
if not self.__label:
|
if not self.__label:
|
||||||
url = reverse("core:page", kwargs={"page_name": settings.SITH_CGU_PAGE})
|
url = reverse("core:page", kwargs={"page_name": settings.SITH_CGU_PAGE})
|
||||||
@@ -175,15 +184,28 @@ class RegisteringForm(UserCreationForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = ("first_name", "last_name", "email", "cgu_approved")
|
fields = ("first_name", "last_name", "email", "cgu_approved_at")
|
||||||
field_classes = {"email": AntiSpamEmailField, "cgu_approved": CGUApprovalField}
|
field_classes = {
|
||||||
|
"email": AntiSpamEmailField,
|
||||||
|
"cgu_approved_at": CGUApprovalField,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CGUApprovalForm(forms.ModelForm):
|
class CGUApprovalForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = ["cgu_approved"]
|
fields = ["cgu_approved_at"]
|
||||||
field_classes = {"cgu_approved": CGUApprovalField}
|
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):
|
||||||
|
|||||||
+2
-2
@@ -89,7 +89,7 @@ class SithLoginView(views.LoginView):
|
|||||||
def get_success_url(self) -> str:
|
def get_success_url(self) -> str:
|
||||||
redirect_to = self.get_redirect_url()
|
redirect_to = self.get_redirect_url()
|
||||||
default_url = self.get_default_redirect_url()
|
default_url = self.get_default_redirect_url()
|
||||||
if not self.request.user.cgu_approved:
|
if not self.request.user.approved_current_cgu:
|
||||||
query = {"next": redirect_to} if redirect_to else {}
|
query = {"next": redirect_to} if redirect_to else {}
|
||||||
return reverse("core:approve_cgu", query=query)
|
return reverse("core:approve_cgu", query=query)
|
||||||
return redirect_to or default_url
|
return redirect_to or default_url
|
||||||
@@ -205,7 +205,7 @@ class CGUApprovalView(views.RedirectURLMixin, UpdateView):
|
|||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
if self.request.user.is_anonymous:
|
if self.request.user.is_anonymous:
|
||||||
return redirect("core:login")
|
return redirect("core:login")
|
||||||
if self.request.user.cgu_approved:
|
if self.request.user.approved_current_cgu:
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user