Compare commits

...
Author SHA1 Message Date
dependabot[bot] 0034a675dc [UPDATE] Update mkdocstrings requirement
Updates the requirements on [mkdocstrings](https://github.com/mkdocstrings/mkdocstrings) to permit the latest version.
- [Release notes](https://github.com/mkdocstrings/mkdocstrings/releases)
- [Changelog](https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mkdocstrings/mkdocstrings/compare/1.0.6...1.0.8)

---
updated-dependencies:
- dependency-name: mkdocstrings
  dependency-version: 1.0.8
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-02 08:24:39 +00:00
thomas girod 37786d2475 Merge pull request #1537 from ae-utbm/toxic-domain
Toxic domain at model-level
2026-10-31 22:24:43 +01:00
imperosol e980c4d727 check email toxic domain at model-level 2026-10-31 18:50:04 +01:00
imperosol 4f989d82bb refactor update_spam_database 2026-10-31 18:50:04 +01:00
thomas girod 7dddb36254 Merge pull request #1534 from ae-utbm/preference-perm
fix: permission check on `UserVisibilityFormFragment`
2026-10-30 14:17:43 +01:00
imperosol 85c5f7b8dd fix: permission check on UserVisibilityFormFragment
+test
2026-10-30 14:14:29 +01:00
12 changed files with 80 additions and 17 deletions

No files matched your search

+2 -2
View File
@@ -2,7 +2,7 @@ from django import forms
from django.core.validators import EmailValidator from django.core.validators import EmailValidator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from antispam.models import ToxicDomain from antispam.models.models import ToxicDomain
class AntiSpamEmailValidator(EmailValidator): class AntiSpamEmailValidator(EmailValidator):
@@ -17,6 +17,6 @@ validate_antispam_email = AntiSpamEmailValidator()
class AntiSpamEmailField(forms.EmailField): class AntiSpamEmailField(forms.EmailField):
"""An email field that email addresses with a known toxic domain.""" """An email field that forbids email addresses with a known toxic domain."""
default_validators = [validate_antispam_email] default_validators = [validate_antispam_email]
@@ -38,14 +38,21 @@ class Command(BaseCommand):
return domains return domains
def _update_domains(self, domains: set[str]): def _update_domains(self, domains: set[str]):
existing = set(ToxicDomain.objects.values_list("domain", flat=True))
if existing == domains:
self.stdout.write("Domain database was up to date")
return
if to_remove := (existing - domains):
# Cleanup database # Cleanup database
ToxicDomain.objects.filter(is_externally_managed=True).delete() ToxicDomain.objects.filter(
domain__in=to_remove, is_externally_managed=True
).delete()
if to_create := (domains - existing):
# Create database # Create database
ToxicDomain.objects.bulk_create( ToxicDomain.objects.bulk_create(
[ [
ToxicDomain(domain=domain, is_externally_managed=True) ToxicDomain(domain=domain, is_externally_managed=True)
for domain in domains for domain in to_create
], ],
ignore_conflicts=True, ignore_conflicts=True,
) )
@@ -53,7 +60,9 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
if not self._should_update(force=options["force"]): if not self._should_update(force=options["force"]):
self.stdout.write("Domain database is up to date") self.stdout.write(
"No update required. Use --force if you want to do it anyway."
)
return return
self.stdout.write("Updating domain database") self.stdout.write("Updating domain database")
+4
View File
@@ -0,0 +1,4 @@
from antispam.models.fields import AntiSpamEmailField
from antispam.models.models import ToxicDomain
__all__ = ["AntiSpamEmailField", "ToxicDomain"]
+21
View File
@@ -0,0 +1,21 @@
from django.conf import settings
from django.db import models
from antispam import forms
class AntiSpamEmailField(models.EmailField):
"""An email field that forbids email addresses with a known toxic domain."""
default_validators = [forms.validate_antispam_email]
if settings.TESTING:
from model_bakery import baker
baker.generators.add(
AntiSpamEmailField, lambda: baker.random_gen.gen_email(max_length=100)
)
else: # pragma: no cover
# baker is only used in tests, so we don't need coverage for this part
pass
File renamed without changes.
View File
Whitespace-only changes.
+3 -2
View File
@@ -55,6 +55,7 @@ from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField from phonenumber_field.modelfields import PhoneNumberField
from PIL import Image, ImageOps from PIL import Image, ImageOps
from antispam.models import AntiSpamEmailField
from core.utils import get_last_promo from core.utils import get_last_promo
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -158,7 +159,7 @@ class User(AbstractUser):
first_name = models.CharField(_("first name"), max_length=64) first_name = models.CharField(_("first name"), max_length=64)
last_name = models.CharField(_("last name"), max_length=64) last_name = models.CharField(_("last name"), max_length=64)
email = models.EmailField(_("email address"), unique=True) email = AntiSpamEmailField(_("email address"), unique=True)
date_of_birth = models.DateField(_("date of birth"), blank=True, null=True) date_of_birth = models.DateField(_("date of birth"), blank=True, null=True)
nick_name = models.CharField(_("nick name"), max_length=64, null=True, blank=True) nick_name = models.CharField(_("nick name"), max_length=64, null=True, blank=True)
last_update = models.DateTimeField(_("last update"), auto_now=True) last_update = models.DateTimeField(_("last update"), auto_now=True)
@@ -267,7 +268,7 @@ class User(AbstractUser):
forum_signature = models.TextField( forum_signature = models.TextField(
_("forum signature"), max_length=256, blank=True, default="" _("forum signature"), max_length=256, blank=True, default=""
) )
second_email = models.EmailField(_("second email address"), null=True, blank=True) second_email = AntiSpamEmailField(_("second email address"), null=True, blank=True)
phone = PhoneNumberField(_("phone"), null=True, blank=True) phone = PhoneNumberField(_("phone"), null=True, blank=True)
parent_phone = PhoneNumberField(_("parent phone"), null=True, blank=True) parent_phone = PhoneNumberField(_("parent phone"), null=True, blank=True)
address = models.CharField(_("address"), max_length=128, blank=True, default="") address = models.CharField(_("address"), max_length=128, blank=True, default="")
+30
View File
@@ -472,6 +472,36 @@ def test_user_preferences(client: Client):
assert user.preferences.notify_on_click is True assert user.preferences.notify_on_click is True
@pytest.mark.django_db
@pytest.mark.parametrize("can_edit", [True, False])
def test_user_visibility(client: Client, monkeypatch, can_edit):
# The view is a CanView, so just patch User.can_be_edited_by for the test
monkeypatch.setattr("core.models.User.can_be_edited_by", lambda *args: can_edit)
client.force_login(subscriber_user.make())
user, other_user = subscriber_user.make(_quantity=2)
url = reverse("core:user_visibility_fragment", kwargs={"user_id": user.id})
response = client.get(url)
assert response.status_code == 200 if can_edit else 403
response = client.post(
url,
{
"show_my_stats": False,
"is_viewable": False,
"whitelisted_users": [other_user.id],
},
)
if not can_edit:
assert response.status_code == 403
return
assertRedirects(response, url)
user.refresh_from_db()
user.preferences.refresh_from_db()
assert user.is_viewable is False
assert list(user.whitelisted_users.all()) == [other_user]
assert user.preferences.show_my_stats is False
@pytest.mark.django_db @pytest.mark.django_db
def test_user_stats(client: Client): def test_user_stats(client: Client):
user = subscriber_user.make() user = subscriber_user.make()
+1 -5
View File
@@ -48,7 +48,6 @@ from django.utils.translation import gettext_lazy as _
from phonenumber_field.widgets import RegionalPhoneNumberWidget from phonenumber_field.widgets import RegionalPhoneNumberWidget
from PIL import Image from PIL import Image
from antispam.forms import AntiSpamEmailField
from core.models import Gift, Group, Page, PageRev, Preferences, SithFile, User from core.models import Gift, Group, Page, PageRev, Preferences, SithFile, User
from core.utils import resize_image from core.utils import resize_image
from core.views.widgets.ajax_select import ( from core.views.widgets.ajax_select import (
@@ -185,10 +184,7 @@ class RegisteringForm(UserCreationForm):
class Meta: class Meta:
model = User model = User
fields = ("first_name", "last_name", "email", "cgu_approved_at") fields = ("first_name", "last_name", "email", "cgu_approved_at")
field_classes = { field_classes = {"cgu_approved_at": CGUApprovalField}
"email": AntiSpamEmailField,
"cgu_approved_at": CGUApprovalField,
}
class CGUApprovalForm(forms.ModelForm): class CGUApprovalForm(forms.ModelForm):
+2 -2
View File
@@ -94,8 +94,8 @@ class FragmentMixin(TemplateResponseMixin, AllowFragment, ContextMixin):
return render( return render(
request, request,
"app/template.jinja", "app/template.jinja",
context={"fragment": fragment(request) context={"fragment": fragment(request)}
} )
# in urls.py # in urls.py
urlpatterns = [ urlpatterns = [
+3 -1
View File
@@ -502,7 +502,9 @@ class UserClubView(UserTabsMixin, CanViewMixin, DetailView):
current_tab = "clubs" current_tab = "clubs"
class UserVisibilityFormFragment(FragmentMixin, SuccessMessageMixin, UpdateView): class UserVisibilityFormFragment(
FragmentMixin, SuccessMessageMixin, CanEditMixin, UpdateView
):
model = User model = User
form_class = UserVisibilityForm form_class = UserVisibilityForm
template_name = "core/fragment/user_visibility.jinja" template_name = "core/fragment/user_visibility.jinja"
+1 -1
View File
@@ -83,7 +83,7 @@ tests = [
docs = [ docs = [
"mkdocs>=1.6.1,<2.0.0", "mkdocs>=1.6.1,<2.0.0",
"mkdocs-material>=9.7.7,<10.0.0", "mkdocs-material>=9.7.7,<10.0.0",
"mkdocstrings>=1.0.6,<2.0.0", "mkdocstrings>=1.0.8,<2.0.0",
"mkdocstrings-python>=2.0.9,<3.0.0", "mkdocstrings-python>=2.0.9,<3.0.0",
"mkdocs-include-markdown-plugin>=7.3.0,<8.0.0", "mkdocs-include-markdown-plugin>=7.3.0,<8.0.0",
] ]