From ef968f36731069078a3673d76d3affcc546944a6 Mon Sep 17 00:00:00 2001 From: thomas girod <56346771+imperosol@users.noreply.github.com> Date: Tue, 2 May 2023 12:36:59 +0200 Subject: [PATCH] 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 --- accounting/models.py | 28 +- .../accounting/bank_account_details.jinja | 2 +- .../accounting/bank_account_list.jinja | 2 +- .../accounting/club_account_details.jinja | 4 +- accounting/templates/accounting/co_list.jinja | 7 +- .../accounting/journal_details.jinja | 11 +- .../templates/accounting/label_list.jinja | 4 +- accounting/views.py | 2 +- club/forms.py | 4 +- club/models.py | 203 +++-- club/templates/club/club_members.jinja | 16 +- club/tests.py | 770 +++++++++++------- club/views.py | 4 +- com/models.py | 32 +- com/templates/com/news_detail.jinja | 2 +- com/templates/com/news_edit.jinja | 2 +- com/templates/com/news_list.jinja | 2 +- com/tests.py | 107 ++- com/views.py | 15 +- core/apps.py | 14 +- core/management/commands/populate.py | 51 +- core/models.py | 302 ++++--- core/signals.py | 17 + core/templates/core/file_detail.jinja | 2 +- core/templates/core/user_clubs.jinja | 5 +- core/templates/core/user_detail.jinja | 7 +- core/templates/core/user_edit.jinja | 6 +- core/templates/core/user_tools.jinja | 37 +- core/tests.py | 154 +++- core/views/user.py | 13 +- counter/models.py | 35 +- counter/views.py | 27 +- doc/TW_Skia/Rapport.tex | 2 +- election/models.py | 15 +- election/views.py | 36 +- forum/models.py | 9 +- forum/templates/forum/forum.jinja | 6 +- forum/templates/forum/main.jinja | 5 +- launderette/models.py | 8 +- .../launderette/launderette_book_choose.jinja | 2 +- .../launderette/launderette_main.jinja | 2 +- pedagogy/models.py | 2 +- pedagogy/tests.py | 2 +- sas/models.py | 7 +- sas/templates/sas/album.jinja | 2 +- sas/templates/sas/main.jinja | 2 +- sas/views.py | 12 +- sith/settings.py | 5 +- stock/models.py | 8 +- subscription/models.py | 2 +- 50 files changed, 1315 insertions(+), 699 deletions(-) create mode 100644 core/signals.py diff --git a/accounting/models.py b/accounting/models.py index 8579e436..e10a029d 100644 --- a/accounting/models.py +++ b/accounting/models.py @@ -66,7 +66,7 @@ class Company(models.Model): """ Method to see if that object can be edited by the given user """ - if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): + if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): return True return False @@ -117,7 +117,9 @@ class BankAccount(models.Model): """ Method to see if that object can be edited by the given user """ - if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): + if user.is_anonymous: + return False + if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): return True m = self.club.get_membership_for(user) if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]: @@ -154,7 +156,9 @@ class ClubAccount(models.Model): """ Method to see if that object can be edited by the given user """ - if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): + if user.is_anonymous: + return False + if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): return True return False @@ -225,7 +229,9 @@ class GeneralJournal(models.Model): """ Method to see if that object can be edited by the given user """ - if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): + if user.is_anonymous: + return False + if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): return True if self.club_account.can_be_edited_by(user): return True @@ -235,7 +241,7 @@ class GeneralJournal(models.Model): """ Method to see if that object can be edited by the given user """ - if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): + if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): return True if self.club_account.can_be_edited_by(user): return True @@ -414,7 +420,9 @@ class Operation(models.Model): """ Method to see if that object can be edited by the given user """ - if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): + if user.is_anonymous: + return False + if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): return True if self.journal.closed: return False @@ -427,7 +435,7 @@ class Operation(models.Model): """ Method to see if that object can be edited by the given user """ - if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): + if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): return True if self.journal.closed: return False @@ -483,7 +491,9 @@ class AccountingType(models.Model): """ Method to see if that object can be edited by the given user """ - if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): + if user.is_anonymous: + return False + if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): return True return False @@ -554,6 +564,8 @@ class Label(models.Model): ) def is_owned_by(self, user): + if user.is_anonymous: + return False return self.club_account.is_owned_by(user) def can_be_edited_by(self, user): diff --git a/accounting/templates/accounting/bank_account_details.jinja b/accounting/templates/accounting/bank_account_details.jinja index 9ab06c99..eb7ac8b1 100644 --- a/accounting/templates/accounting/bank_account_details.jinja +++ b/accounting/templates/accounting/bank_account_details.jinja @@ -12,7 +12,7 @@


{% trans %}Bank account: {% endtrans %}{{ object.name }}

- {% if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) and not object.club_accounts.exists() %} + {% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) and not object.club_accounts.exists() %} {% trans %}Delete{% endtrans %} {% endif %}

{% trans %}Infos{% endtrans %}

diff --git a/accounting/templates/accounting/bank_account_list.jinja b/accounting/templates/accounting/bank_account_list.jinja index 0fcab4a5..0bd654b8 100644 --- a/accounting/templates/accounting/bank_account_list.jinja +++ b/accounting/templates/accounting/bank_account_list.jinja @@ -9,7 +9,7 @@

{% trans %}Accounting{% endtrans %}

- {% if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %} + {% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}

{% trans %}Manage simplified types{% endtrans %}

{% trans %}Manage accounting types{% endtrans %}

{% trans %}New bank account{% endtrans %}

diff --git a/accounting/templates/accounting/club_account_details.jinja b/accounting/templates/accounting/club_account_details.jinja index 13626950..f9ceec23 100644 --- a/accounting/templates/accounting/club_account_details.jinja +++ b/accounting/templates/accounting/club_account_details.jinja @@ -16,7 +16,7 @@ {% if user.is_root and not object.journals.exists() %} {% trans %}Delete{% endtrans %} {% endif %} - {% if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %} + {% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}

{% trans %}New label{% endtrans %}

{% endif %}

{% trans %}Label list{% endtrans %}

@@ -56,7 +56,7 @@ {% endif %} {% trans %}View{% endtrans %} {% trans %}Edit{% endtrans %} - {% if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) and j.operations.count() == 0 %} + {% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) and j.operations.count() == 0 %} {% trans %}Delete{% endtrans %} {% endif %} diff --git a/accounting/templates/accounting/co_list.jinja b/accounting/templates/accounting/co_list.jinja index 85b61c2c..0cca32c6 100644 --- a/accounting/templates/accounting/co_list.jinja +++ b/accounting/templates/accounting/co_list.jinja @@ -6,11 +6,12 @@ {% block content %}
- {% 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) + %}

{% trans %}Create new company{% endtrans %}

{% endif %} - -
+
diff --git a/accounting/templates/accounting/journal_details.jinja b/accounting/templates/accounting/journal_details.jinja index 2e76f39d..274e678f 100644 --- a/accounting/templates/accounting/journal_details.jinja +++ b/accounting/templates/accounting/journal_details.jinja @@ -84,10 +84,13 @@ {% endif %} diff --git a/accounting/templates/accounting/label_list.jinja b/accounting/templates/accounting/label_list.jinja index f790c608..c15a9373 100644 --- a/accounting/templates/accounting/label_list.jinja +++ b/accounting/templates/accounting/label_list.jinja @@ -13,7 +13,7 @@


{% trans %}Back to club account{% endtrans %}

- {% if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %} + {% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}

{% trans %}New label{% endtrans %}

{% endif %} {% if object.labels.all() %} @@ -21,7 +21,7 @@
- - {% if o.journal.club_account.bank_account.name != "AE TI" and o.journal.club_account.bank_account.name != "TI" or user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %} - {% if not o.journal.closed %} - {% trans %}Edit{% endtrans %} - {% endif %} + {% + if o.journal.club_account.bank_account.name not in ["AE TI", "TI"] + or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) + %} + {% if not o.journal.closed %} + {% trans %}Edit{% endtrans %} + {% endif %} {% endif %} {% trans %}Generate{% endtrans %}
- - - - - {% if users_old %} - - {% endif %} + + + + + + {% if users_old %} + + {% endif %} + {% for m in members %} diff --git a/club/tests.py b/club/tests.py index 30070ba1..0cc05e89 100644 --- a/club/tests.py +++ b/club/tests.py @@ -13,376 +13,564 @@ # OR WITHIN THE LOCAL FILE "LICENSE" # # +from datetime import timedelta from django.conf import settings +from django.core.cache import cache from django.test import TestCase from django.utils import timezone, html +from django.utils.timezone import now, localtime from django.utils.translation import gettext as _ from django.urls import reverse from django.core.management import call_command -from django.core.exceptions import ValidationError, NON_FIELD_ERRORS -from core.models import User +from core.models import User, AnonymousUser from club.models import Club, Membership, Mailing from club.forms import MailingForm -from sith.settings import SITH_BAR_MANAGER - - -# Create your tests here. +from sith.settings import SITH_BAR_MANAGER, SITH_MAIN_CLUB_ID class ClubTest(TestCase): + """ + Set up data for test cases related to clubs and membership + The generated dataset is the one created by the populate command, + plus the following modifications : + + - `self.club` is a dummy club recreated for each test + - `self.club` has two board members : skia (role 3) and comptable (role 10) + - `self.club` has one regular member : richard + - `self.club` has one former member : sli (who had role 2) + - None of the `self.club` members are in the AE club. + """ + + @classmethod + def setUpTestData(cls): + # subscribed users - initial members + cls.skia = User.objects.get(username="skia") + cls.richard = User.objects.get(username="rbatsbak") + cls.comptable = User.objects.get(username="comptable") + cls.sli = User.objects.get(username="sli") + + # subscribed users - not initial members + cls.krophil = User.objects.get(username="krophil") + cls.subscriber = User.objects.get(username="subscriber") + + # old subscriber + cls.old_subscriber = User.objects.get(username="old_subscriber") + + # not subscribed + cls.public = User.objects.get(username="public") + + cls.ae = Club.objects.filter(pk=SITH_MAIN_CLUB_ID)[0] + def setUp(self): - self.skia = User.objects.filter(username="skia").first() - self.rbatsbak = User.objects.filter(username="rbatsbak").first() - self.guy = User.objects.filter(username="guy").first() - self.bdf = Club.objects.filter(unix_name=SITH_BAR_MANAGER["unix_name"]).first() + # by default, Skia is in the AE, which creates side effect + self.skia.memberships.all().delete() - def test_create_add_user_to_club_from_root_ok(self): - self.client.login(username="root", password="plop") - self.client.post( - reverse("club:club_members", kwargs={"club_id": self.bdf.id}), - {"users": self.skia.id, "start_date": "12/06/2016", "role": 3}, + # create a fake club + self.club = Club.objects.create( + name="Fake Club", + unix_name="fake-club", + address="5 rue de la République, 90000 Belfort", ) - response = self.client.get( - reverse("club:club_members", kwargs={"club_id": self.bdf.id}) + self.members_url = reverse( + "club:club_members", kwargs={"club_id": self.club.id} ) - self.assertTrue(response.status_code == 200) - self.assertTrue( - "S' Kia\\n " - in str(response.content) + a_month_ago = now() - timedelta(days=30) + yesterday = now() - timedelta(days=1) + Membership.objects.create( + club=self.club, user=self.skia, start_date=a_month_ago, role=3 + ) + Membership.objects.create(club=self.club, user=self.richard, role=1) + Membership.objects.create( + club=self.club, user=self.comptable, start_date=a_month_ago, role=10 ) - def test_create_add_multiple_user_to_club_from_root_ok(self): + # sli was a member but isn't anymore + Membership.objects.create( + club=self.club, + user=self.sli, + start_date=a_month_ago, + end_date=yesterday, + role=2, + ) + cache.clear() + + +class MembershipQuerySetTest(ClubTest): + def test_ongoing(self): + """ + Test that the ongoing queryset method returns the memberships that + are not ended. + """ + current_members = self.club.members.ongoing() + expected = [ + self.skia.memberships.get(club=self.club), + self.comptable.memberships.get(club=self.club), + self.richard.memberships.get(club=self.club), + ] + self.assertEqual(len(current_members), len(expected)) + for member in current_members: + self.assertIn(member, expected) + + def test_board(self): + """ + Test that the board queryset method returns the memberships + of user in the club board + """ + board_members = list(self.club.members.board()) + expected = [ + self.skia.memberships.get(club=self.club), + self.comptable.memberships.get(club=self.club), + # sli is no more member, but he was in the board + self.sli.memberships.get(club=self.club), + ] + self.assertEqual(len(board_members), len(expected)) + for member in board_members: + self.assertIn(member, expected) + + def test_ongoing_board(self): + """ + Test that combining ongoing and board returns users + who are currently board members of the club + """ + members = list(self.club.members.ongoing().board()) + expected = [ + self.skia.memberships.get(club=self.club), + self.comptable.memberships.get(club=self.club), + ] + self.assertEqual(len(members), len(expected)) + for member in members: + self.assertIn(member, expected) + + def test_update_invalidate_cache(self): + """ + Test that the `update` queryset method properly invalidate cache + """ + mem_skia = self.skia.memberships.get(club=self.club) + cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia) + self.skia.memberships.update(end_date=localtime(now()).date()) + self.assertEqual( + cache.get(f"membership_{mem_skia.club_id}_{mem_skia.user_id}"), "not_member" + ) + + mem_richard = self.richard.memberships.get(club=self.club) + cache.set( + f"membership_{mem_richard.club_id}_{mem_richard.user_id}", mem_richard + ) + self.richard.memberships.update(role=5) + new_mem = self.richard.memberships.get(club=self.club) + self.assertNotEqual(new_mem, "not_member") + self.assertEqual(new_mem.role, 5) + + def test_delete_invalidate_cache(self): + """ + Test that the `delete` queryset properly invalidate cache + """ + + mem_skia = self.skia.memberships.get(club=self.club) + mem_comptable = self.comptable.memberships.get(club=self.club) + cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia) + cache.set( + f"membership_{mem_comptable.club_id}_{mem_comptable.user_id}", mem_comptable + ) + + # should delete the subscriptions of skia and comptable + self.club.members.ongoing().board().delete() + + self.assertEqual( + cache.get(f"membership_{mem_skia.club_id}_{mem_skia.user_id}"), "not_member" + ) + self.assertEqual( + cache.get(f"membership_{mem_comptable.club_id}_{mem_comptable.user_id}"), + "not_member", + ) + + +class ClubModelTest(ClubTest): + def assert_membership_just_started(self, user: User, role: int): + """ + Assert that the given membership is active and started today + """ + membership = user.memberships.ongoing().filter(club=self.club).first() + self.assertIsNotNone(membership) + self.assertEqual(localtime(now()).date(), membership.start_date) + self.assertIsNone(membership.end_date) + self.assertEqual(membership.role, role) + self.assertEqual(membership.club.get_membership_for(user), membership) + member_group = self.club.unix_name + settings.SITH_MEMBER_SUFFIX + board_group = self.club.unix_name + settings.SITH_BOARD_SUFFIX + self.assertTrue(user.is_in_group(name=member_group)) + self.assertTrue(user.is_in_group(name=board_group)) + + def assert_membership_just_ended(self, user: User): + """ + Assert that the given user have a membership which ended today + """ + today = localtime(now()).date() + self.assertIsNotNone( + user.memberships.filter(club=self.club, end_date=today).first() + ) + self.assertIsNone(self.club.get_membership_for(user)) + + def test_access_unauthorized(self): + """ + Test that users who never subscribed and anonymous users + cannot see the page + """ + response = self.client.post(self.members_url) + self.assertEqual(response.status_code, 403) + + self.client.login(username="public", password="plop") + response = self.client.post(self.members_url) + self.assertEqual(response.status_code, 403) + + def test_display(self): + """ + Test that a GET request return a page where the requested + information are displayed. + """ + self.client.login(username=self.skia.username, password="plop") + response = self.client.get(self.members_url) + self.assertEqual(response.status_code, 200) + expected_html = ( + "
{% trans %}User{% endtrans %}{% trans %}Role{% endtrans %}{% trans %}Description{% endtrans %}{% trans %}Since{% endtrans %}{% trans %}Mark as old{% endtrans %}
{% trans %}User{% endtrans %}{% trans %}Role{% endtrans %}{% trans %}Description{% endtrans %}{% trans %}Since{% endtrans %}{% trans %}Mark as old{% endtrans %}
Responsable info
" + "" + "" + "" + ) + memberships = self.club.members.ongoing().order_by("-role") + input_id = 0 + for membership in memberships.select_related("user"): + user = membership.user + expected_html += ( + f"" + f"" + f"" + f"" + expected_html += "
UtilisateurRôleDescriptionDepuisMarquer comme ancien
" + f"{user.get_display_name()}{settings.SITH_CLUB_ROLES[membership.role]}{membership.description}{membership.start_date}" + ) + if membership.role <= 3: # 3 is the role of skia + expected_html += ( + '' + ) + input_id += 1 + expected_html += "
" + self.assertInHTML(expected_html, response.content.decode()) + + def test_root_add_one_club_member(self): + """ + Test that root users can add members to clubs, one at a time + """ self.client.login(username="root", password="plop") - self.client.post( - reverse("club:club_members", kwargs={"club_id": self.bdf.id}), + response = self.client.post( + self.members_url, + {"users": self.subscriber.id, "role": 3}, + ) + self.assertRedirects(response, self.members_url) + self.subscriber.refresh_from_db() + self.assert_membership_just_started(self.subscriber, role=3) + + def test_root_add_multiple_club_member(self): + """ + Test that root users can add multiple members at once to clubs + """ + self.client.login(username="root", password="plop") + response = self.client.post( + self.members_url, { - "users": "|%d|%d|" % (self.skia.id, self.rbatsbak.id), - "start_date": "12/06/2016", + "users": f"|{self.subscriber.id}|{self.krophil.id}|", "role": 3, }, ) - response = self.client.get( - reverse("club:club_members", kwargs={"club_id": self.bdf.id}) - ) - self.assertTrue(response.status_code == 200) - content = str(response.content) - self.assertTrue( - "S' Kia\\n Responsable info" - in content - ) - self.assertTrue( - "Richard Batsbak\\n Responsable info" - in content - ) + self.assertRedirects(response, self.members_url) + self.subscriber.refresh_from_db() + self.assert_membership_just_started(self.subscriber, role=3) + self.assert_membership_just_started(self.krophil, role=3) - def test_create_add_user_to_club_from_root_fail_not_subscriber(self): + def test_add_unauthorized_members(self): + """ + Test that users who are not currently subscribed + cannot be members of clubs. + """ self.client.login(username="root", password="plop") response = self.client.post( - reverse("club:club_members", kwargs={"club_id": self.bdf.id}), - {"users": self.guy.id, "start_date": "12/06/2016", "role": 3}, + self.members_url, + {"users": self.public.id, "role": 1}, ) - self.assertTrue(response.status_code == 200) + self.assertIsNone(self.public.memberships.filter(club=self.club).first()) self.assertTrue('
- {% 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 #}
{% if profile.is_subscribed %} diff --git a/core/templates/core/user_edit.jinja b/core/templates/core/user_edit.jinja index 1bac52dc..f189d339 100644 --- a/core/templates/core/user_edit.jinja +++ b/core/templates/core/user_edit.jinja @@ -35,7 +35,7 @@ {%- else -%} {% trans %}To edit your profile picture, ask a member of the AE{% endtrans %} {%- 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 -%} {%- trans -%}Delete{%- endtrans -%} @@ -55,7 +55,7 @@

{{ form["avatar_pict"].label }}

{{ 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 -%} {%- trans -%}Delete{%- endtrans -%} @@ -75,7 +75,7 @@

{{ form["scrub_pict"].label }}

{{ 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 -%} {%- trans -%}Delete{%-endtrans -%} diff --git a/core/templates/core/user_tools.jinja b/core/templates/core/user_tools.jinja index 66e64d58..4e1e69a2 100644 --- a/core/templates/core/user_tools.jinja +++ b/core/templates/core/user_tools.jinja @@ -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) %}

{% trans %}Counters{% endtrans %}

    {% 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() %}
  • @@ -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 %}

    {% trans %}Accounting{% endtrans %}

    {% 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) + %}

    {% trans %}Communication{% endtrans %}

    @@ -153,7 +163,10 @@
    {% 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) + %}

    {% trans %}Pedagogy{% endtrans %}

      diff --git a/core/tests.py b/core/tests.py index 47ee4f8a..4c84662b 100644 --- a/core/tests.py +++ b/core/tests.py @@ -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" 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")) diff --git a/core/views/user.py b/core/views/user.py index ffc6a623..eeba4845 100644 --- a/core/views/user.py +++ b/core/views/user.py @@ -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 ): diff --git a/counter/models.py b/counter/models.py index f0e96099..c6389349 100644 --- a/counter/models.py +++ b/counter/models.py @@ -228,7 +228,9 @@ class ProductType(models.Model): """ Method to see if that object can be edited by the given user """ - if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): + if user.is_anonymous: + return False + if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): return True return False @@ -294,9 +296,11 @@ class Product(models.Model): """ Method to see if that object can be edited by the given user """ + if user.is_anonymous: + return False if user.is_in_group( - settings.SITH_GROUP_ACCOUNTING_ADMIN_ID - ) or user.is_in_group(settings.SITH_GROUP_COUNTER_ADMIN_ID): + pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID + ) or user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID): return True return False @@ -318,8 +322,8 @@ class Product(models.Model): """ if not self.buying_groups.exists(): return True - for group in self.buying_groups.all(): - if user.is_in_group(group.name): + for group_id in self.buying_groups.values_list("pk", flat=True): + if user.is_in_group(pk=group_id): return True return False @@ -402,18 +406,17 @@ class Counter(models.Model): return reverse("counter:details", kwargs={"counter_id": self.id}) def is_owned_by(self, user): + if user.is_anonymous: + return False mem = self.club.get_membership_for(user) if mem and mem.role >= 7: return True - return user.is_in_group(settings.SITH_GROUP_COUNTER_ADMIN_ID) + return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID) def can_be_viewed_by(self, user): if self.type == "BAR": return True - return ( - user.is_in_group(settings.SITH_MAIN_BOARD_GROUP) - or user in self.sellers.all() - ) + return user.is_board_member or user in self.sellers.all() def gen_token(self): """Generate a new token for this counter""" @@ -621,6 +624,8 @@ class Refilling(models.Model): ) def is_owned_by(self, user): + if user.is_anonymous: + return False return user.is_owner(self.counter) and self.payment_method != "CARD" def delete(self, *args, **kwargs): @@ -713,6 +718,8 @@ class Selling(models.Model): ) def is_owned_by(self, user): + if user.is_anonymous: + return False return user.is_owner(self.counter) and self.payment_method != "CARD" def can_be_viewed_by(self, user): @@ -953,7 +960,9 @@ class CashRegisterSummary(models.Model): """ Method to see if that object can be edited by the given user """ - if user.is_in_group(settings.SITH_GROUP_COUNTER_ADMIN_ID): + if user.is_anonymous: + return False + if user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID): return True return False @@ -1022,7 +1031,9 @@ class Eticket(models.Model): """ Method to see if that object can be edited by the given user """ - return user.is_in_group(settings.SITH_GROUP_COUNTER_ADMIN_ID) + if user.is_anonymous: + return False + return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID) def get_hash(self, string): import hashlib diff --git a/counter/views.py b/counter/views.py index e0e5b658..9e6df235 100644 --- a/counter/views.py +++ b/counter/views.py @@ -87,8 +87,8 @@ class CounterAdminMixin(View): edit_club = [] def _test_group(self, user): - for g in self.edit_group: - if user.is_in_group(g): + for grp_id in self.edit_group: + if user.is_in_group(pk=grp_id): return True return False @@ -486,14 +486,12 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): pid = str(pid) price = self.get_price(pid) total = self.sum_basket(request) - product = self.get_product(pid) - can_buy = False - if not product.buying_groups.exists(): - can_buy = True - else: - for g in product.buying_groups.all(): - if self.customer.user.is_in_group(g.name): - can_buy = True + product: Product = self.get_product(pid) + user: User = self.customer.user + buying_groups = list(product.buying_groups.values_list("pk", flat=True)) + can_buy = len(buying_groups) == 0 or any( + user.is_in_group(pk=group_id) for group_id in buying_groups + ) if not can_buy: request.session["not_allowed"] = True return False @@ -514,18 +512,17 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): ): request.session["not_allowed"] = True return False - if product.limit_age >= 18 and not self.customer.user.date_of_birth: + if product.limit_age >= 18 and not user.date_of_birth: request.session["no_age"] = True return False - if product.limit_age >= 18 and self.customer.user.is_banned_alcohol: + if product.limit_age >= 18 and user.is_banned_alcohol: request.session["not_allowed"] = True return False - if self.customer.user.is_banned_counter: + if user.is_banned_counter: request.session["not_allowed"] = True return False if ( - self.customer.user.date_of_birth - and self.customer.user.get_age() < product.limit_age + user.date_of_birth and self.customer.user.get_age() < product.limit_age ): # Check if affordable request.session["too_young"] = True return False diff --git a/doc/TW_Skia/Rapport.tex b/doc/TW_Skia/Rapport.tex index 7295fbb8..49b84bfa 100644 --- a/doc/TW_Skia/Rapport.tex +++ b/doc/TW_Skia/Rapport.tex @@ -342,7 +342,7 @@ tous les autres. {% if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
    • Accounting
    • {% endif %} -{% if user.is_in_group(settings.SITH_MAIN_BOARD_GROUP) or user.is_root %} +{% if user.is_board_member or user.is_root %}
    • Subscriptions
    • Counters management
    • {% endif %} diff --git a/election/models.py b/election/models.py index 537cc6e9..f100a1d6 100644 --- a/election/models.py +++ b/election/models.py @@ -73,16 +73,16 @@ class Election(models.Model): return bool(timezone.now() <= self.end_candidature) def can_candidate(self, user): - for group in self.candidature_groups.all(): - if user.is_in_group(group): + for group_id in self.candidature_groups.values_list("pk", flat=True): + if user.is_in_group(pk=group_id): return True return False def can_vote(self, user): if not self.is_vote_active or self.has_voted(user): return False - for group in self.vote_groups.all(): - if user.is_in_group(group): + for group_id in self.vote_groups.values_list("pk", flat=True): + if user.is_in_group(pk=group_id): return True return False @@ -97,10 +97,9 @@ class Election(models.Model): results[role.title] = role.results(total_vote) return results - def delete(self): - for election_list in self.election_lists.all(): - election_list.delete() - super(Election, self).delete() + def delete(self, *args, **kwargs): + self.election_lists.all().delete() + super(Election, self).delete(*args, **kwargs) # Permissions diff --git a/election/views.py b/election/views.py index f36482fa..a223c78c 100644 --- a/election/views.py +++ b/election/views.py @@ -2,14 +2,14 @@ from django.shortcuts import get_object_or_404 from django.views.generic import ListView, DetailView from django.views.generic.edit import UpdateView, CreateView from django.views.generic.edit import DeleteView, FormView -from django.urls import reverse_lazy +from django.urls import reverse_lazy, reverse from django.utils.translation import gettext_lazy as _ from django.core.exceptions import PermissionDenied from django.db import transaction -from django.forms import CheckboxSelectMultiple from django.shortcuts import redirect from django import forms +from core.models import User from core.views import CanViewMixin, CanEditMixin, CanCreateMixin from django.db.models.query import QuerySet from core.views.forms import SelectDateTime, MarkdownInput @@ -224,8 +224,8 @@ class ElectionDetailView(CanViewMixin, DetailView): pk_url_kwarg = "election_id" def get(self, request, *arg, **kwargs): - r = super(ElectionDetailView, self).get(request, *arg, **kwargs) - election = self.get_object() + response = super(ElectionDetailView, self).get(request, *arg, **kwargs) + election: Election = self.get_object() if request.user.can_edit(election) and election.is_vote_editable: action = request.GET.get("action", None) role = request.GET.get("role", None) @@ -239,9 +239,9 @@ class ElectionDetailView(CanViewMixin, DetailView): elif action == "top": Role.objects.get(id=role).top() return redirect( - reverse_lazy("election:detail", kwargs={"election_id": election.id}) + reverse("election:detail", kwargs={"election_id": election.id}) ) - return r + return response def get_context_data(self, **kwargs): """Add additionnal data to the template""" @@ -295,8 +295,8 @@ class VoteFormView(CanCreateMixin, FormView): """ data = form.clean() res = super(FormView, self).form_valid(form) - for grp in self.election.vote_groups.all(): - if self.request.user.is_in_group(grp): + for grp_id in self.election.vote_groups.values_list("pk", flat=True): + if self.request.user.is_in_group(pk=grp_id): self.vote(data) return res return res @@ -401,12 +401,13 @@ class RoleCreateView(CanCreateMixin, CreateView): def form_valid(self, form): """ - Verify that the user can edit proprely + Verify that the user can edit properly """ - obj = form.instance + obj: Role = form.instance + user: User = self.request.user if obj.election: - for grp in obj.election.edit_groups.all(): - if self.request.user.is_in_group(grp): + for grp_id in obj.election.edit_groups.values_list("pk", flat=True): + if user.is_in_group(pk=grp_id): return super(CreateView, self).form_valid(form) raise PermissionDenied @@ -446,13 +447,14 @@ class ElectionListCreateView(CanCreateMixin, CreateView): """ Verify that the user can vote on this election """ - obj = form.instance + obj: ElectionList = form.instance + user: User = self.request.user if obj.election: - for grp in obj.election.candidature_groups.all(): - if self.request.user.is_in_group(grp): + for grp_id in obj.election.candidature_groups.values_list("pk", flat=True): + if user.is_in_group(pk=grp_id): return super(CreateView, self).form_valid(form) - for grp in obj.election.edit_groups.all(): - if self.request.user.is_in_group(grp): + for grp_id in obj.election.edit_groups.values_list("pk", flat=True): + if user.is_in_group(pk=grp_id): return super(CreateView, self).form_valid(form) raise PermissionDenied diff --git a/forum/models.py b/forum/models.py index 9b92129d..3894caa7 100644 --- a/forum/models.py +++ b/forum/models.py @@ -161,7 +161,9 @@ class Forum(models.Model): # divided by 3 the number of requests on the main forum page # after the first load def is_owned_by(self, user): - if user.is_in_group(settings.SITH_GROUP_FORUM_ADMIN_ID): + if user.is_anonymous: + return False + if user.is_in_group(pk=settings.SITH_GROUP_FORUM_ADMIN_ID): return True try: m = Forum._club_memberships[self.id][user.id] @@ -337,7 +339,10 @@ class ForumMessage(models.Model): def is_last_in_topic(self): return bool(self.id == self.topic.messages.order_by("date").last().id) - def is_owned_by(self, user): # Anyone can create a topic: it's better to + def is_owned_by(self, user): + if user.is_anonymous: + return False + # Anyone can create a topic: it's better to # check the rights at the forum level, since it's more controlled return self.topic.forum.is_owned_by(user) or user.id == self.author.id diff --git a/forum/templates/forum/forum.jinja b/forum/templates/forum/forum.jinja index 1242e50d..57031b19 100644 --- a/forum/templates/forum/forum.jinja +++ b/forum/templates/forum/forum.jinja @@ -10,7 +10,11 @@

      {{ forum.name }}

      - {% if user.is_in_group(settings.SITH_GROUP_FORUM_ADMIN_ID) or user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) or user.can_edit(forum) %} + {% + if user.is_com_admin + or user.is_in_group(pk=settings.SITH_GROUP_FORUM_ADMIN_ID) + or user.can_edit(forum) + %} {% trans %}New forum{% endtrans %}
      {% endif %} {% if not forum.is_category %} diff --git a/forum/templates/forum/main.jinja b/forum/templates/forum/main.jinja index 8bedc310..e6a62785 100644 --- a/forum/templates/forum/main.jinja +++ b/forum/templates/forum/main.jinja @@ -17,7 +17,10 @@ {% trans %}Favorite topics{% endtrans %} {{ display_search_bar(request) }}

      - {% if user.is_in_group(settings.SITH_GROUP_FORUM_ADMIN_ID) or user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %} + {% if + user.is_com_admin + or user.is_in_group(pk=settings.SITH_GROUP_FORUM_ADMIN_ID) + %}

      {% trans %}New forum{% endtrans %}

      diff --git a/launderette/models.py b/launderette/models.py index 7d8b2267..3ca12b2d 100644 --- a/launderette/models.py +++ b/launderette/models.py @@ -42,6 +42,8 @@ class Launderette(models.Model): """ Method to see if that object can be edited by the given user """ + if user.is_anonymous: + return False launderette_club = Club.objects.filter( unix_name=settings.SITH_LAUNDERETTE_MANAGER["unix_name"] ).first() @@ -60,7 +62,7 @@ class Launderette(models.Model): return False def can_be_viewed_by(self, user): - return user.is_in_group(settings.SITH_MAIN_MEMBERS_GROUP) + return user.is_subscribed def __str__(self): return self.name @@ -101,6 +103,8 @@ class Machine(models.Model): """ Method to see if that object can be edited by the given user """ + if user.is_anonymous: + return False launderette_club = Club.objects.filter( unix_name=settings.SITH_LAUNDERETTE_MANAGER["unix_name"] ).first() @@ -155,6 +159,8 @@ class Token(models.Model): """ Method to see if that object can be edited by the given user """ + if user.is_anonymous: + return False launderette_club = Club.objects.filter( unix_name=settings.SITH_LAUNDERETTE_MANAGER["unix_name"] ).first() diff --git a/launderette/templates/launderette/launderette_book_choose.jinja b/launderette/templates/launderette/launderette_book_choose.jinja index af30c6c1..d80505b4 100644 --- a/launderette/templates/launderette/launderette_book_choose.jinja +++ b/launderette/templates/launderette/launderette_book_choose.jinja @@ -5,7 +5,7 @@ {% endblock %} {% block content %} -{% if request.user.is_in_group(settings.SITH_MAIN_MEMBERS_GROUP) %} +{% if request.user.is_subscribed %}