Better usage of cache for groups and clubs related operations (#634)

* Better usage of cache for group retrieval

* Cache clearing on object deletion or update

* replace signals by save and delete override

* add is_anonymous check in is_owned_by

Add in many is_owned_by(self, user) methods that user is not anonymous. Since many of those functions do db queries, this should reduce a little bit the load of the db.

* Stricter usage of User.is_in_group

Constrain the parameters that can be passed to the function to make sure only a str or an int can be used. Also force to explicitly specify if the group id or the group name is used.

* write test and correct bugs

* remove forgotten populate commands

* Correct test
This commit is contained in:
thomas girod
2023-05-02 12:36:59 +02:00
committed by GitHub
parent 96dede5077
commit ef968f3673
50 changed files with 1315 additions and 699 deletions

View File

@ -25,6 +25,7 @@
import sys
from django.apps import AppConfig
from django.core.cache import cache
from django.core.signals import request_started
@ -33,26 +34,17 @@ class SithConfig(AppConfig):
verbose_name = "Core app of the Sith"
def ready(self):
from core.models import User
from club.models import Club
from forum.models import Forum
import core.signals
def clear_cached_groups(**kwargs):
User._group_ids = {}
User._group_name = {}
cache.clear()
def clear_cached_memberships(**kwargs):
User._club_memberships = {}
Club._memberships = {}
Forum._club_memberships = {}
print("Connecting signals!", file=sys.stderr)
request_started.connect(
clear_cached_groups, weak=False, dispatch_uid="clear_cached_groups"
)
request_started.connect(
clear_cached_memberships,
weak=False,
dispatch_uid="clear_cached_memberships",
)
# TODO: there may be a need to add more cache clearing

View File

@ -155,12 +155,10 @@ class Command(BaseCommand):
Counter(name="Eboutic", club=main_club, type="EBOUTIC").save()
Counter(name="AE", club=main_club, type="OFFICE").save()
home_root.view_groups.set(
[Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first()]
)
club_root.view_groups.set(
[Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first()]
)
ae_members = Group.objects.get(name=settings.SITH_MAIN_MEMBERS_GROUP)
home_root.view_groups.set([ae_members])
club_root.view_groups.set([ae_members])
home_root.save()
club_root.save()
@ -220,9 +218,7 @@ Welcome to the wiki page!
)
skia.set_password("plop")
skia.save()
skia.view_groups = [
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id
]
skia.view_groups = [ae_members.id]
skia.save()
skia_profile_path = (
root_path
@ -261,9 +257,7 @@ Welcome to the wiki page!
)
public.set_password("plop")
public.save()
public.view_groups = [
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id
]
public.view_groups = [ae_members.id]
public.save()
# Adding user Subscriber
subscriber = User(
@ -277,9 +271,7 @@ Welcome to the wiki page!
)
subscriber.set_password("plop")
subscriber.save()
subscriber.view_groups = [
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id
]
subscriber.view_groups = [ae_members.id]
subscriber.save()
# Adding user old Subscriber
old_subscriber = User(
@ -293,9 +285,7 @@ Welcome to the wiki page!
)
old_subscriber.set_password("plop")
old_subscriber.save()
old_subscriber.view_groups = [
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id
]
old_subscriber.view_groups = [ae_members.id]
old_subscriber.save()
# Adding user Counter admin
counter = User(
@ -309,9 +299,7 @@ Welcome to the wiki page!
)
counter.set_password("plop")
counter.save()
counter.view_groups = [
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id
]
counter.view_groups = [ae_members.id]
counter.groups.set(
[
Group.objects.filter(id=settings.SITH_GROUP_COUNTER_ADMIN_ID)
@ -332,9 +320,7 @@ Welcome to the wiki page!
)
comptable.set_password("plop")
comptable.save()
comptable.view_groups = [
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id
]
comptable.view_groups = [ae_members.id]
comptable.groups.set(
[
Group.objects.filter(id=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
@ -355,9 +341,7 @@ Welcome to the wiki page!
)
u.set_password("plop")
u.save()
u.view_groups = [
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id
]
u.view_groups = [ae_members.id]
u.save()
# Adding user Richard Batsbak
richard = User(
@ -394,9 +378,7 @@ Welcome to the wiki page!
richard_profile.save()
richard.profile_pict = richard_profile
richard.save()
richard.view_groups = [
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id
]
richard.view_groups = [ae_members.id]
richard.save()
# Adding syntax help page
p = Page(name="Aide_sur_la_syntaxe")
@ -428,7 +410,7 @@ Welcome to the wiki page!
default_subscription = "un-semestre"
# Root
s = Subscription(
member=User.objects.filter(pk=root.pk).first(),
member=root,
subscription_type=default_subscription,
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
)
@ -528,7 +510,7 @@ Welcome to the wiki page!
Club(
name="Woenzel'UT", unix_name="woenzel", address="Woenzel", parent=guyut
).save()
Membership(user=skia, club=main_club, role=3, description="").save()
Membership(user=skia, club=main_club, role=3).save()
troll = Club(
name="Troll Penché",
unix_name="troll",
@ -855,9 +837,7 @@ Welcome to the wiki page!
)
sli.set_password("plop")
sli.save()
sli.view_groups = [
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id
]
sli.view_groups = [ae_members.id]
sli.save()
sli_profile_path = (
root_path
@ -934,7 +914,6 @@ Welcome to the wiki page!
Membership(
user=comunity,
club=bar_club,
start_date=timezone.now(),
role=settings.SITH_CLUB_ROLES_ID["Board member"],
).save()
# Adding user tutu

View File

@ -23,12 +23,12 @@
#
#
import importlib
from typing import Union, Optional, List
from django.db import models
from django.core.cache import cache
from django.core.mail import send_mail
from django.contrib.auth.models import (
AbstractBaseUser,
PermissionsMixin,
UserManager,
Group as AuthGroup,
GroupManager as AuthGroupManager,
@ -40,7 +40,7 @@ from django.core import validators
from django.core.exceptions import ValidationError, PermissionDenied
from django.urls import reverse
from django.conf import settings
from django.db import transaction
from django.db import models, transaction
from django.contrib.staticfiles.storage import staticfiles_storage
from django.utils.html import escape
from django.utils.functional import cached_property
@ -50,7 +50,7 @@ from core import utils
from phonenumber_field.modelfields import PhoneNumberField
from datetime import datetime, timedelta, date
from datetime import timedelta, date
import unicodedata
@ -90,14 +90,24 @@ class Group(AuthGroup):
"""
return reverse("core:group_list")
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
cache.set(f"sith_group_{self.id}", self)
cache.set(f"sith_group_{self.name.replace(' ', '_')}", self)
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
cache.delete(f"sith_group_{self.id}")
cache.delete(f"sith_group_{self.name.replace(' ', '_')}")
class MetaGroup(Group):
"""
MetaGroups are dynamically created groups.
Generaly used with clubs where creating a club creates two groups:
Generally used with clubs where creating a club creates two groups:
* club-SITH_BOARD_SUFFIX
* club-SITH_MEMBER_SUFFIX
* club-SITH_BOARD_SUFFIX
* club-SITH_MEMBER_SUFFIX
"""
#: Assign a manager in a way that MetaGroup.objects only return groups with is_meta=False
@ -110,6 +120,32 @@ class MetaGroup(Group):
super(MetaGroup, self).__init__(*args, **kwargs)
self.is_meta = True
@cached_property
def associated_club(self):
"""
Return the group associated with this meta group
The result of this function is cached
:return: The associated club if it exists, else None
:rtype: club.models.Club | None
"""
from club.models import Club
if self.name.endswith(settings.SITH_BOARD_SUFFIX):
# replace this with str.removesuffix as soon as Python
# is upgraded to 3.10
club_name = self.name[: -len(settings.SITH_BOARD_SUFFIX)]
elif self.name.endswith(settings.SITH_MEMBER_SUFFIX):
club_name = self.name[: -len(settings.SITH_MEMBER_SUFFIX)]
else:
return None
club = cache.get(f"sith_club_{club_name}")
if club is None:
club = Club.objects.filter(unix_name=club_name).first()
cache.set(f"sith_club_{club_name}", club)
return club
class RealGroup(Group):
"""
@ -134,6 +170,43 @@ def validate_promo(value):
)
def get_group(*, pk: int = None, name: str = None) -> Optional[Group]:
"""
Search for a group by its primary key or its name.
Either one of the two must be set.
The result is cached for the default duration (should be 5 minutes).
:param pk: The primary key of the group
:param name: The name of the group
:return: The group if it exists, else None
:raises ValueError: If no group matches the criteria
"""
if pk is None and name is None:
raise ValueError("Either pk or name must be set")
if name is not None:
name = name.replace(" ", "_") # avoid errors with memcached backend
pk_or_name: Union[str, int] = pk if pk is not None else name
group = cache.get(f"sith_group_{pk_or_name}")
if group == "not_found":
# Using None as a cache value is a little bit tricky,
# so we use a special string to represent None
return None
elif group is not None:
return group
# if this point is reached, the group is not in cache
if pk is not None:
group = Group.objects.filter(pk=pk).first()
else:
group = Group.objects.filter(name=name).first()
if group is not None:
cache.set(f"sith_group_{group.id}", group)
cache.set(f"sith_group_{group.name.replace(' ', '_')}", group)
else:
cache.set(f"sith_group_{pk_or_name}", "not_found")
return group
class User(AbstractBaseUser):
"""
Defines the base user class, useable in every app
@ -295,7 +368,6 @@ class User(AbstractBaseUser):
objects = UserManager()
USERNAME_FIELD = "username"
# REQUIRED_FIELDS = ['email']
def promo_has_logo(self):
return utils.file_exist("./core/static/core/img/promo_%02d.png" % self.promo)
@ -336,94 +408,72 @@ class User(AbstractBaseUser):
else:
return 0
_club_memberships = {}
_group_names = {}
_group_ids = {}
def is_in_group(self, *, pk: int = None, name: str = None) -> bool:
"""
Check if this user is in the given group.
Either a group id or a group name must be provided.
If both are passed, only the id will be considered.
def is_in_group(self, group_name):
"""If the user is in the group passed in argument (as string or by id)"""
group_id = 0
g = None
if isinstance(group_name, int): # Handle the case where group_name is an ID
if group_name in User._group_ids.keys():
g = User._group_ids[group_name]
else:
g = Group.objects.filter(id=group_name).first()
User._group_ids[group_name] = g
else:
if group_name in User._group_names.keys():
g = User._group_names[group_name]
else:
g = Group.objects.filter(name=group_name).first()
User._group_names[group_name] = g
if g:
group_name = g.name
group_id = g.id
The group will be fetched using the given parameter.
If no group is found, return False.
If a group is found, check if this user is in the latter.
:return: True if the user is the group, else False
"""
if pk is not None:
group: Optional[Group] = get_group(pk=pk)
elif name is not None:
group: Optional[Group] = get_group(name=name)
else:
raise ValueError("You must either provide the id or the name of the group")
if group is None:
return False
if group_id == settings.SITH_GROUP_PUBLIC_ID:
if group.id == settings.SITH_GROUP_PUBLIC_ID:
return True
if group_id == settings.SITH_GROUP_SUBSCRIBERS_ID:
if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:
return self.is_subscribed
if group_id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:
if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:
return self.was_subscribed
if (
group_name == settings.SITH_MAIN_MEMBERS_GROUP
): # We check the subscription if asked
return self.is_subscribed
if group_name[-len(settings.SITH_BOARD_SUFFIX) :] == settings.SITH_BOARD_SUFFIX:
name = group_name[: -len(settings.SITH_BOARD_SUFFIX)]
if name in User._club_memberships.keys():
mem = User._club_memberships[name]
else:
from club.models import Club
c = Club.objects.filter(unix_name=name).first()
mem = c.get_membership_for(self)
User._club_memberships[name] = mem
if mem:
return mem.role > settings.SITH_MAXIMUM_FREE_ROLE
return False
if (
group_name[-len(settings.SITH_MEMBER_SUFFIX) :]
== settings.SITH_MEMBER_SUFFIX
):
name = group_name[: -len(settings.SITH_MEMBER_SUFFIX)]
if name in User._club_memberships.keys():
mem = User._club_memberships[name]
else:
from club.models import Club
c = Club.objects.filter(unix_name=name).first()
mem = c.get_membership_for(self)
User._club_memberships[name] = mem
if mem:
if group.id == settings.SITH_GROUP_ROOT_ID:
return self.is_root
if group.is_meta:
# check if this group is associated with a club
group.__class__ = MetaGroup
club = group.associated_club
if club is None:
return False
membership = club.get_membership_for(self)
if membership is None:
return False
if group.name.endswith(settings.SITH_MEMBER_SUFFIX):
return True
return False
if group_id == settings.SITH_GROUP_ROOT_ID and self.is_superuser:
return membership.role > settings.SITH_MAXIMUM_FREE_ROLE
return group in self.cached_groups
@property
def cached_groups(self) -> List[Group]:
"""
Get the list of groups this user is in.
The result is cached for the default duration (should be 5 minutes)
:return: A list of all the groups this user is in
"""
groups = cache.get(f"user_{self.id}_groups")
if groups is None:
groups = list(self.groups.all())
cache.set(f"user_{self.id}_groups", groups)
return groups
@cached_property
def is_root(self) -> bool:
if self.is_superuser:
return True
return group_name in self.cached_groups_names
@cached_property
def cached_groups_names(self):
return [g.name for g in self.groups.all()]
@cached_property
def is_root(self):
return (
self.is_superuser
or self.groups.filter(id=settings.SITH_GROUP_ROOT_ID).exists()
)
root_id = settings.SITH_GROUP_ROOT_ID
return any(g.id == root_id for g in self.cached_groups)
@cached_property
def is_board_member(self):
from club.models import Club
return (
Club.objects.filter(unix_name=settings.SITH_MAIN_CLUB["unix_name"])
.first()
.has_rights_in_club(self)
)
main_club = settings.SITH_MAIN_CLUB["unix_name"]
return self.is_in_group(name=main_club + settings.SITH_BOARD_SUFFIX)
@cached_property
def can_read_subscription_history(self):
@ -434,8 +484,8 @@ class User(AbstractBaseUser):
for club in Club.objects.filter(
id__in=settings.SITH_CAN_READ_SUBSCRIPTION_HISTORY
).all():
if club.has_rights_in_club(self):
):
if club in self.clubs_with_rights:
return True
return False
@ -443,10 +493,8 @@ class User(AbstractBaseUser):
def can_create_subscription(self):
from club.models import Club
for club in Club.objects.filter(
id__in=settings.SITH_CAN_CREATE_SUBSCRIPTIONS
).all():
if club.has_rights_in_club(self):
for club in Club.objects.filter(id__in=settings.SITH_CAN_CREATE_SUBSCRIPTIONS):
if club in self.clubs_with_rights:
return True
return False
@ -464,11 +512,11 @@ class User(AbstractBaseUser):
@cached_property
def is_banned_alcohol(self):
return self.is_in_group(settings.SITH_GROUP_BANNED_ALCOHOL_ID)
return self.is_in_group(pk=settings.SITH_GROUP_BANNED_ALCOHOL_ID)
@cached_property
def is_banned_counter(self):
return self.is_in_group(settings.SITH_GROUP_BANNED_COUNTER_ID)
return self.is_in_group(pk=settings.SITH_GROUP_BANNED_COUNTER_ID)
@cached_property
def age(self) -> int:
@ -598,9 +646,9 @@ class User(AbstractBaseUser):
"""
if hasattr(obj, "is_owned_by") and obj.is_owned_by(self):
return True
if hasattr(obj, "owner_group") and self.is_in_group(obj.owner_group.name):
if hasattr(obj, "owner_group") and self.is_in_group(pk=obj.owner_group.id):
return True
if self.is_superuser or self.is_in_group(settings.SITH_GROUP_ROOT_ID):
if self.is_root:
return True
return False
@ -611,8 +659,8 @@ class User(AbstractBaseUser):
if hasattr(obj, "can_be_edited_by") and obj.can_be_edited_by(self):
return True
if hasattr(obj, "edit_groups"):
for g in obj.edit_groups.all():
if self.is_in_group(g.name):
for pk in obj.edit_groups.values_list("pk", flat=True):
if self.is_in_group(pk=pk):
return True
if isinstance(obj, User) and obj == self:
return True
@ -627,15 +675,15 @@ class User(AbstractBaseUser):
if hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self):
return True
if hasattr(obj, "view_groups"):
for g in obj.view_groups.all():
if self.is_in_group(g.name):
for pk in obj.view_groups.values_list("pk", flat=True):
if self.is_in_group(pk=pk):
return True
if self.can_edit(obj):
return True
return False
def can_be_edited_by(self, user):
return user.is_in_group(settings.SITH_MAIN_BOARD_GROUP) or user.is_root
return user.is_root or user.is_board_member
def can_be_viewed_by(self, user):
return (user.was_subscribed and self.is_subscriber_viewable) or user.is_root
@ -656,10 +704,6 @@ class User(AbstractBaseUser):
escape(self.get_display_name()),
)
@cached_property
def subscribed(self):
return self.is_in_group(settings.SITH_MAIN_MEMBERS_GROUP)
@cached_property
def preferences(self):
try:
@ -682,17 +726,16 @@ class User(AbstractBaseUser):
@cached_property
def clubs_with_rights(self):
return [
m.club.id
for m in self.memberships.filter(
models.Q(end_date__isnull=True) | models.Q(end_date__gte=timezone.now())
).all()
if m.club.has_rights_in_club(self)
]
"""
:return: the list of clubs where the user has rights
:rtype: list[club.models.Club]
"""
memberships = self.memberships.ongoing().board().select_related("club")
return [m.club for m in memberships]
@cached_property
def is_com_admin(self):
return self.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID)
return self.is_in_group(pk=settings.SITH_GROUP_COM_ADMIN_ID)
class AnonymousUser(AuthAnonymousUser):
@ -747,21 +790,18 @@ class AnonymousUser(AuthAnonymousUser):
def favorite_topics(self):
raise PermissionDenied
def is_in_group(self, group_name):
def is_in_group(self, *, pk: int = None, name: str = None) -> bool:
"""
The anonymous user is only the public group
The anonymous user is only in the public group
"""
group_id = 0
if isinstance(group_name, int): # Handle the case where group_name is an ID
g = Group.objects.filter(id=group_name).first()
if g:
group_name = g.name
group_id = g.id
else:
return False
if group_id == settings.SITH_GROUP_PUBLIC_ID:
return True
return False
allowed_id = settings.SITH_GROUP_PUBLIC_ID
if pk is not None:
return pk == allowed_id
elif name is not None:
group = get_group(name=name)
return group is not None and group.id == allowed_id
else:
raise ValueError("You must either provide the id or the name of the group")
def is_owner(self, obj):
return False
@ -880,13 +920,13 @@ class SithFile(models.Model):
verbose_name = _("file")
def is_owned_by(self, user):
if hasattr(self, "profile_of") and user.is_in_group(
settings.SITH_MAIN_BOARD_GROUP
):
if user.is_anonymous:
return False
if hasattr(self, "profile_of") and user.is_board_member:
return True
if user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID):
if user.is_com_admin:
return True
if self.is_in_sas and user.is_in_group(settings.SITH_GROUP_SAS_ADMIN_ID):
if self.is_in_sas and user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID):
return True
return user.id == self.owner.id
@ -1493,6 +1533,8 @@ class Gift(models.Model):
return self.label
def is_owned_by(self, user):
if user.is_anonymous:
return False
return user.is_board_member or user.is_root

17
core/signals.py Normal file
View File

@ -0,0 +1,17 @@
from django.core.cache import cache
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from core.models import User
@receiver(m2m_changed, sender=User.groups.through, dispatch_uid="user_groups_changed")
def user_groups_changed(sender, instance: User, **kwargs):
"""
Clear the cached clubs of the user
"""
# As a m2m relationship doesn't live within the model
# but rather on an intermediary table, there is no
# model method to override, meaning we must use
# a signal to invalidate the cache when a user is removed from a club
cache.delete(f"user_{instance.id}_groups")

View File

@ -61,7 +61,7 @@
{% if not file.home_of and not file.home_of_club and file.parent %}
<p><a href="{{ url('core:file_delete', file_id=file.id, popup=popup) }}">{% trans %}Delete{% endtrans %}</a></p>
{% endif %}
{% if user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %}
{% if user.is_com_admin %}
<p><a href="{{ url('core:file_moderate', file_id=file.id) }}">{% trans %}Moderate{% endtrans %}</a></p>
{% endif %}
{% endblock %}

View File

@ -67,7 +67,10 @@
</tbody>
</table>
{% if profile.mailing_subscriptions.exists() and (profile.id == user.id or user.is_root or user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID)) %}
{% if
profile.mailing_subscriptions.exists()
and (profile.id == user.id or user.is_root or user.is_com_admin)
%}
<h4>{% trans %}Subscribed mailing lists{% endtrans %}</h4>
{% for sub in profile.mailing_subscriptions.all() %}
<p>{{ sub.mailing.email }} <a href="{{ url('club:mailing_subscription_delete', mailing_subscription_id=sub.id) }}">{% trans %}Unsubscribe{% endtrans %}</a></p>

View File

@ -136,7 +136,12 @@
</div>
</div>
</main>
{% if user.memberships.filter(end_date=None).exists() or user.is_in_group(settings.SITH_MAIN_BOARD_GROUP) or user == profile or user.is_in_group(settings.SITH_BAR_MANAGER_BOARD_GROUP) %}
{% if
user == profile
or user.memberships.ongoing().exists()
or user.is_board_member
or user.is_in_group(name=settings.SITH_BAR_MANAGER_BOARD_GROUP)
%}
{# if the user is member of a club, he can view the subscription state #}
<hr>
{% if profile.is_subscribed %}

View File

@ -35,7 +35,7 @@
{%- else -%}
<em>{% trans %}To edit your profile picture, ask a member of the AE{% endtrans %}</em>
{%- endif -%}
{%- if user.is_in_group(settings.SITH_MAIN_BOARD_GROUP) and form.instance.profile_pict.id -%}
{%- if user.is_board_member and form.instance.profile_pict.id -%}
<a href="{{ url('core:file_delete', file_id=form.instance.profile_pict.id, popup='') }}">
{%- trans -%}Delete{%- endtrans -%}
</a>
@ -55,7 +55,7 @@
<div class="profile-picture-edit">
<p>{{ form["avatar_pict"].label }}</p>
{{ form["avatar_pict"] }}
{%- if user.is_in_group(settings.SITH_MAIN_BOARD_GROUP) and form.instance.avatar_pict.id -%}
{%- if user.is_board_member and form.instance.avatar_pict.id -%}
<a href="{{ url('core:file_delete', file_id=form.instance.avatar_pict.id, popup='') }}">
{%- trans -%}Delete{%- endtrans -%}
</a>
@ -75,7 +75,7 @@
<div class="profile-picture-edit">
<p>{{ form["scrub_pict"].label }}</p>
{{ form["scrub_pict"] }}
{%- if user.is_in_group(settings.SITH_MAIN_BOARD_GROUP) and form.instance.scrub_pict.id -%}
{%- if user.is_board_member and form.instance.scrub_pict.id -%}
<a href="{{ url('core:file_delete', file_id=form.instance.scrub_pict.id, popup='') }}">
{%- trans -%}Delete{%-endtrans -%}
</a>

View File

@ -35,18 +35,21 @@
{% endif %}
{% set is_admin_on_a_counter = false %}
{% for b in settings.SITH_COUNTER_BARS if user.is_in_group(b[1] + " admin") %}
{% for b in settings.SITH_COUNTER_BARS if user.is_in_group(name=b[1] + " admin") %}
{% set is_admin_on_a_counter = true %}
{% endfor %}
{% if
user.is_in_group(settings.SITH_GROUP_COUNTER_ADMIN_ID) or user.is_root
or is_admin_on_a_counter
is_admin_on_a_counter
or user.is_root
or user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)
%}
<div>
<h4>{% trans %}Counters{% endtrans %}</h4>
<ul>
{% if user.is_in_group(settings.SITH_GROUP_COUNTER_ADMIN_ID) or user.is_root %}
{% if user.is_root
or user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)
%}
<li><a href="{{ url('counter:admin_list') }}">{% trans %}General counters management{% endtrans %}</a></li>
<li><a href="{{ url('counter:product_list') }}">{% trans %}Products management{% endtrans %}</a></li>
<li><a href="{{ url('counter:producttype_list') }}">{% trans %}Product types management{% endtrans %}</a></li>
@ -57,7 +60,7 @@
</ul>
<ul>
{% for b in settings.SITH_COUNTER_BARS %}
{% if user.is_in_group(b[1]+" admin") %}
{% if user.is_in_group(name=b[1]+" admin") %}
{% set c = Counter.objects.filter(id=b[0]).first() %}
<li class="rows counter">
@ -85,13 +88,16 @@
{% endif %}
{% if
user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) or user.is_root
or user.memberships.filter(end_date=None).filter(role__gte=7).all() | length > 10
user.is_root
or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
or user.memberships.ongoing().filter(role__gte=7).count() > 10
%}
<div>
<h4>{% trans %}Accounting{% endtrans %}</h4>
<ul>
{% if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) or user.is_root %}
{% if user.is_root
or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
%}
<li><a href="{{ url('accounting:refound_account') }}">{% trans %}Refound Account{% endtrans %}</a></li>
<li><a href="{{ url('accounting:bank_list') }}">{% trans %}General accounting{% endtrans %}</a></li>
<li><a href="{{ url('accounting:co_list') }}">{% trans %}Company list{% endtrans %}</a></li>
@ -118,11 +124,15 @@
</div>
{% endif %}
{% if user.is_in_group(settings.SITH_GROUP_SAS_ADMIN_ID) or user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) or user.is_root %}
{% if
user.is_root
or user.is_com_admin
or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID)
%}
<div>
<h4>{% trans %}Communication{% endtrans %}</h4>
<ul>
{% if user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) or user.is_root %}
{% if user.is_com_admin or user.is_root %}
<li><a href="{{ url('com:weekmail_article') }}">{% trans %}Create weekmail article{% endtrans %}</a></li>
<li><a href="{{ url('com:weekmail') }}">{% trans %}Weekmail{% endtrans %}</a></li>
<li><a href="{{ url('com:weekmail_destinations') }}">{% trans %}Weekmail destinations{% endtrans %}</a></li>
@ -135,7 +145,7 @@
<li><a href="{{ url('com:poster_list') }}">{% trans %}Posters{% endtrans %}</a></li>
<li><a href="{{ url('com:screen_list') }}">{% trans %}Screens{% endtrans %}</a></li>
{% endif %}
{% if user.is_in_group(settings.SITH_GROUP_SAS_ADMIN_ID) %}
{% if user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID) %}
<li><a href="{{ url('sas:moderation') }}">{% trans %}Moderate pictures{% endtrans %}</a></li>
{% endif %}
</ul>
@ -153,7 +163,10 @@
</div>
{% endif %}
{% if user.is_in_group(settings.SITH_GROUP_PEDAGOGY_ADMIN_ID) or user.is_root %}
{% if
user.is_root
or user.is_in_group(pk=settings.SITH_GROUP_PEDAGOGY_ADMIN_ID)
%}
<div>
<h4>{% trans %}Pedagogy{% endtrans %}</h4>
<ul>

View File

@ -15,13 +15,18 @@
#
import os
from datetime import timedelta
from django.core.cache import cache
from django.test import Client, TestCase
from django.urls import reverse
from django.core.management import call_command
from django.utils.timezone import now
from core.models import User, Group, Page
from club.models import Membership
from core.models import User, Group, Page, AnonymousUser
from core.markdown import markdown
from sith import settings
"""
to run these tests :
@ -457,3 +462,150 @@ class FileHandlingTest(TestCase):
)
self.assertTrue(response.status_code == 200)
self.assertTrue("ls</a>" in str(response.content))
class UserIsInGroupTest(TestCase):
"""
Test that the User.is_in_group() and AnonymousUser.is_in_group()
work as intended
"""
@classmethod
def setUpTestData(cls):
from club.models import Club
cls.root_group = Group.objects.get(name="Root")
cls.public = Group.objects.get(name="Public")
cls.subscribers = Group.objects.get(name="Subscribers")
cls.old_subscribers = Group.objects.get(name="Old subscribers")
cls.accounting_admin = Group.objects.get(name="Accounting admin")
cls.com_admin = Group.objects.get(name="Communication admin")
cls.counter_admin = Group.objects.get(name="Counter admin")
cls.banned_alcohol = Group.objects.get(name="Banned from buying alcohol")
cls.banned_counters = Group.objects.get(name="Banned from counters")
cls.banned_subscription = Group.objects.get(name="Banned to subscribe")
cls.sas_admin = Group.objects.get(name="SAS admin")
cls.club = Club.objects.create(
name="Fake Club",
unix_name="fake-club",
address="Fake address",
)
cls.main_club = Club.objects.get(id=1)
def setUp(self) -> None:
self.toto = User.objects.create(
username="toto", first_name="a", last_name="b", email="a.b@toto.fr"
)
self.skia = User.objects.get(username="skia")
def assert_in_public_group(self, user):
self.assertTrue(user.is_in_group(pk=self.public.id))
self.assertTrue(user.is_in_group(name=self.public.name))
def assert_in_club_metagroups(self, user, club):
meta_groups_board = club.unix_name + settings.SITH_BOARD_SUFFIX
meta_groups_members = club.unix_name + settings.SITH_MEMBER_SUFFIX
self.assertFalse(user.is_in_group(name=meta_groups_board))
self.assertFalse(user.is_in_group(name=meta_groups_members))
def assert_only_in_public_group(self, user):
self.assert_in_public_group(user)
for group in (
self.root_group,
self.banned_counters,
self.accounting_admin,
self.sas_admin,
self.subscribers,
self.old_subscribers,
):
self.assertFalse(user.is_in_group(pk=group.pk))
self.assertFalse(user.is_in_group(name=group.name))
meta_groups_board = self.club.unix_name + settings.SITH_BOARD_SUFFIX
meta_groups_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX
self.assertFalse(user.is_in_group(name=meta_groups_board))
self.assertFalse(user.is_in_group(name=meta_groups_members))
def test_anonymous_user(self):
"""
Test that anonymous users are only in the public group
"""
user = AnonymousUser()
self.assert_only_in_public_group(user)
def test_not_subscribed_user(self):
"""
Test that users who never subscribed are only in the public group
"""
self.assert_only_in_public_group(self.toto)
def test_wrong_parameter_fail(self):
"""
Test that when neither the pk nor the name argument is given,
the function raises a ValueError
"""
with self.assertRaises(ValueError):
self.toto.is_in_group()
def test_number_queries(self):
"""
Test that the number of db queries is stable
and that less queries are made when making a new call
"""
# make sure Skia is in at least one group
self.skia.groups.add(Group.objects.first().pk)
skia_groups = self.skia.groups.all()
group_in = skia_groups.first()
cache.clear()
# Test when the user is in the group
with self.assertNumQueries(2):
self.skia.is_in_group(pk=group_in.id)
with self.assertNumQueries(0):
self.skia.is_in_group(pk=group_in.id)
ids = skia_groups.values_list("pk", flat=True)
group_not_in = Group.objects.exclude(pk__in=ids).first()
cache.clear()
# Test when the user is not in the group
with self.assertNumQueries(2):
self.skia.is_in_group(pk=group_not_in.id)
with self.assertNumQueries(0):
self.skia.is_in_group(pk=group_not_in.id)
def test_cache_properly_cleared_membership(self):
"""
Test that when the membership of a user end,
the cache is properly invalidated
"""
membership = Membership.objects.create(
club=self.club, user=self.toto, end_date=None
)
meta_groups_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX
cache.clear()
self.assertTrue(self.toto.is_in_group(name=meta_groups_members))
self.assertEqual(
membership, cache.get(f"membership_{self.club.id}_{self.toto.id}")
)
membership.end_date = now() - timedelta(minutes=5)
membership.save()
cached_membership = cache.get(f"membership_{self.club.id}_{self.toto.id}")
self.assertEqual(cached_membership, "not_member")
self.assertFalse(self.toto.is_in_group(name=meta_groups_members))
def test_cache_properly_cleared_group(self):
"""
Test that when a user is removed from a group,
the is_in_group_method return False when calling it again
"""
self.toto.groups.add(self.com_admin.pk)
self.assertTrue(self.toto.is_in_group(pk=self.com_admin.pk))
self.toto.groups.remove(self.com_admin.pk)
self.assertFalse(self.toto.is_in_group(pk=self.com_admin.pk))
def test_not_existing_group(self):
"""
Test that searching for a not existing group
returns False
"""
self.assertFalse(self.skia.is_in_group(name="This doesn't exist"))

View File

@ -254,10 +254,11 @@ class UserTabsMixin(TabedViewMixin):
if user.customer and (
user == self.request.user
or self.request.user.is_in_group(
settings.SITH_GROUP_ACCOUNTING_ADMIN_ID
pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID
)
or self.request.user.is_in_group(
settings.SITH_BAR_MANAGER["unix_name"] + settings.SITH_BOARD_SUFFIX
name=settings.SITH_BAR_MANAGER["unix_name"]
+ settings.SITH_BOARD_SUFFIX
)
or self.request.user.is_root
):
@ -488,9 +489,9 @@ class UserStatsView(UserTabsMixin, CanViewMixin, DetailView):
if not (
profile == request.user
or request.user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
or request.user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
or request.user.is_in_group(
settings.SITH_BAR_MANAGER["unix_name"] + settings.SITH_BOARD_SUFFIX
name=settings.SITH_BAR_MANAGER["unix_name"] + settings.SITH_BOARD_SUFFIX
)
or request.user.is_root
):
@ -772,9 +773,9 @@ class UserAccountBase(UserTabsMixin, DetailView):
res = super(UserAccountBase, self).dispatch(request, *arg, **kwargs)
if (
self.object == request.user
or request.user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
or request.user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
or request.user.is_in_group(
settings.SITH_BAR_MANAGER["unix_name"] + settings.SITH_BOARD_SUFFIX
name=settings.SITH_BAR_MANAGER["unix_name"] + settings.SITH_BOARD_SUFFIX
)
or request.user.is_root
):