diff --git a/.gitignore b/.gitignore index ecda5902..27793c50 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,9 @@ node_modules/ # compiled documentation site/ +# rollup-bundle-visualizer report +.bundle-size-report.html + ### Redis ### # Ignore redis binary dump (dump.rdb) files diff --git a/biome.json b/biome.json index de2077a9..4b50821d 100644 --- a/biome.json +++ b/biome.json @@ -7,7 +7,7 @@ }, "files": { "ignoreUnknown": false, - "includes": ["**/static/**"] + "includes": ["**/static/**", "vite.config.mts"] }, "formatter": { "enabled": true, diff --git a/com/views.py b/com/views.py index ea5b742d..456ba6ec 100644 --- a/com/views.py +++ b/com/views.py @@ -244,9 +244,8 @@ class NewsListView(TemplateView): .filter( date_of_birth__month=localdate().month, date_of_birth__day=localdate().day, - is_viewable=True, + role__in=["STUDENT", "FORMER STUDENT"], ) - .filter(role__in=["STUDENT", "FORMER STUDENT"]) .order_by("-date_of_birth"), key=lambda u: u.date_of_birth.year, ) diff --git a/core/admin.py b/core/admin.py index a21086a0..74c0c0ab 100644 --- a/core/admin.py +++ b/core/admin.py @@ -63,6 +63,7 @@ class UserAdmin(admin.ModelAdmin): "scrub_pict", "user_permissions", "groups", + "whitelisted_users", ) inlines = (UserBanInline,) search_fields = ["first_name", "last_name", "username"] diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 7bb2d0ef..4c1c5379 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -116,7 +116,11 @@ class Command(BaseCommand): ) main_club.board_group.permissions.add( *Permission.objects.filter( - codename__in=["view_subscription", "add_subscription"] + codename__in=[ + "view_subscription", + "add_subscription", + "view_hidden_user", + ] ) ) bar_club = Club.objects.create( diff --git a/core/migrations/0049_user_whitelisted_users.py b/core/migrations/0049_user_whitelisted_users.py new file mode 100644 index 00000000..6970caea --- /dev/null +++ b/core/migrations/0049_user_whitelisted_users.py @@ -0,0 +1,37 @@ +# Generated by Django 5.2.12 on 2026-03-14 08:39 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [("core", "0048_alter_user_options")] + + operations = [ + migrations.AddField( + model_name="user", + name="whitelisted_users", + field=models.ManyToManyField( + blank=True, + help_text=( + "Even if this profile is hidden, " + "the users in this list will still be able to see it." + ), + related_name="visible_by_whitelist", + to=settings.AUTH_USER_MODEL, + verbose_name="whitelisted users", + ), + ), + migrations.AlterField( + model_name="preferences", + name="show_my_stats", + field=models.BooleanField( + default=False, + help_text=( + "Allow subscribers (or whitelisted users " + "if your profile is hidden) to access your AE account stats." + ), + verbose_name="show your stats to others", + ), + ), + ] diff --git a/core/models.py b/core/models.py index 3b533751..0f0ef10e 100644 --- a/core/models.py +++ b/core/models.py @@ -131,7 +131,7 @@ class UserQuerySet(models.QuerySet): if user.has_perm("core.view_hidden_user"): return self if user.has_perm("core.view_user"): - return self.filter(is_viewable=True) + return self.filter(Q(is_viewable=True) | Q(whitelisted_users=user)) if user.is_anonymous: return self.none() return self.filter(id=user.id) @@ -279,6 +279,16 @@ class User(AbstractUser): ), default=True, ) + whitelisted_users = models.ManyToManyField( + "User", + related_name="visible_by_whitelist", + verbose_name=_("whitelisted users"), + help_text=_( + "Even if this profile is hidden, " + "the users in this list will still be able to see it." + ), + blank=True, + ) godfathers = models.ManyToManyField("User", related_name="godchildren", blank=True) objects = CustomUserManager() @@ -518,7 +528,7 @@ class User(AbstractUser): self.username = user_name return user_name - def is_owner(self, obj): + def is_owner(self, obj: models.Model): """Determine if the object is owned by the user.""" if hasattr(obj, "is_owned_by") and obj.is_owned_by(self): return True @@ -526,7 +536,7 @@ class User(AbstractUser): return True return self.is_root - def can_edit(self, obj): + def can_edit(self, obj: models.Model): """Determine if the object can be edited by the user.""" if hasattr(obj, "can_be_edited_by") and obj.can_be_edited_by(self): return True @@ -540,11 +550,9 @@ class User(AbstractUser): pks = list(obj.edit_groups.values_list("id", flat=True)) if any(self.is_in_group(pk=pk) for pk in pks): return True - if isinstance(obj, User) and obj == self: - return True return self.is_owner(obj) - def can_view(self, obj): + def can_view(self, obj: models.Model): """Determine if the object can be viewed by the user.""" if hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self): return True @@ -563,14 +571,35 @@ class User(AbstractUser): return True return self.can_edit(obj) - def can_be_edited_by(self, user): - return user.is_root or user.is_board_member + def can_be_edited_by(self, user: User): + return user == self or user.is_root or user.is_board_member def can_be_viewed_by(self, user: User) -> bool: + """Check if the given user can be viewed by this user. + + Given users A and B. A can be viewed by B if : + + - A and B are the same user + - or B has the permission to view hidden users + - or B can view users in general and A didn't hide its profile + - or B is in A's whitelist. + """ + + def is_in_whitelist(u: User): + if ( + hasattr(self, "_prefetched_objects_cache") + and "whitelisted_users" in self._prefetched_objects_cache + ): + return u in self.whitelisted_users.all() + return self.whitelisted_users.contains(u) + return ( user.id == self.id or user.has_perm("core.view_hidden_user") - or (user.has_perm("core.view_user") and self.is_viewable) + or ( + user.has_perm("core.view_user") + and (self.is_viewable or is_in_whitelist(user)) + ) ) def get_mini_item(self): @@ -750,7 +779,14 @@ class Preferences(models.Model): User, related_name="_preferences", on_delete=models.CASCADE ) receive_weekmail = models.BooleanField(_("receive the Weekmail"), default=False) - show_my_stats = models.BooleanField(_("show your stats to others"), default=False) + show_my_stats = models.BooleanField( + _("show your stats to others"), + help_text=_( + "Allow subscribers (or whitelisted users " + "if your profile is hidden) to access your AE account stats." + ), + default=False, + ) notify_on_click = models.BooleanField( _("get a notification for every click"), default=False ) diff --git a/core/static/bundled/core/components/nfc-input-index.ts b/core/static/bundled/core/components/nfc-input-index.ts index 7a1100ed..d1efdc4b 100644 --- a/core/static/bundled/core/components/nfc-input-index.ts +++ b/core/static/bundled/core/components/nfc-input-index.ts @@ -26,7 +26,6 @@ export class NfcInput extends inheritHtmlElement("input") { window.alert(gettext("Unsupported NFC card")); }); - // biome-ignore lint/correctness/noUndeclaredVariables: browser API ndef.addEventListener("reading", (event: NDEFReadingEvent) => { this.removeAttribute("scan"); this.node.value = event.serialNumber.replace(/:/g, "").toUpperCase(); diff --git a/core/static/core/forms.scss b/core/static/core/forms.scss index be876c9b..2abffbba 100644 --- a/core/static/core/forms.scss +++ b/core/static/core/forms.scss @@ -157,6 +157,7 @@ form { margin-bottom: .25rem; font-size: 80%; display: block; + max-width: calc(100% - calc(var(--nf-input-size) * 2)) } fieldset { diff --git a/core/static/user/user_edit.scss b/core/static/user/user_edit.scss index 20995da6..e428956a 100644 --- a/core/static/user/user_edit.scss +++ b/core/static/user/user_edit.scss @@ -5,17 +5,6 @@ } .profile { - &-visible { - display: flex; - flex-direction: column; - align-items: center; - gap: 5px; - padding-top: 10px; - input[type="checkbox"]+label { - max-width: unset; - } - } - &-pictures { box-sizing: border-box; display: flex; diff --git a/core/static/user/user_preferences.scss b/core/static/user/user_preferences.scss index c61142d0..ded5e08d 100644 --- a/core/static/user/user_preferences.scss +++ b/core/static/user/user_preferences.scss @@ -19,28 +19,6 @@ } } } - - &-cards, - &-trombi { - >p { - display: flex; - flex-direction: column; - align-items: flex-start; - text-align: justify; - gap: 5px; - margin: 0; - - >input, - >select { - min-width: 300px; - } - } - } - - &-submit-btn { - margin-top: 10px !important; - max-width: 100px; - } } .justify { diff --git a/core/templates/core/base/notifications.jinja b/core/templates/core/base/notifications.jinja index f941a1a6..89fb7aad 100644 --- a/core/templates/core/base/notifications.jinja +++ b/core/templates/core/base/notifications.jinja @@ -1,14 +1,11 @@