From 528415c9d19ef9ef68189916e23a2e3686486fe4 Mon Sep 17 00:00:00 2001 From: Arash Farzaneh Taleghani Date: Wed, 26 Apr 2023 15:11:39 +0200 Subject: [PATCH 1/4] Implement new penalty rules --- lego/.DS_Store | Bin 0 -> 6148 bytes lego/apps/events/models.py | 34 +-------- lego/apps/events/tests/test_async_tasks.py | 8 +- lego/apps/events/tests/test_penalties.py | 71 +++++++++++++----- lego/apps/users/action_handlers.py | 6 +- .../0039_penalty_activation_date.py | 19 +++++ ...activation_date_penalty_activation_time.py | 18 +++++ lego/apps/users/models.py | 29 +++++-- lego/apps/users/notifications.py | 7 +- lego/apps/users/serializers/penalties.py | 1 + lego/apps/users/tests/test_models.py | 33 ++++---- lego/apps/users/tests/test_users_api.py | 5 +- lego/settings/lego.py | 2 +- 13 files changed, 154 insertions(+), 79 deletions(-) create mode 100644 lego/.DS_Store create mode 100644 lego/apps/users/migrations/0039_penalty_activation_date.py create mode 100644 lego/apps/users/migrations/0040_rename_activation_date_penalty_activation_time.py diff --git a/lego/.DS_Store b/lego/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..feb765f58dd9c653c00fbe16159d17dae5dbdeff GIT binary patch literal 6148 zcmeH~F$w}f3`G;&V!>uh%V|7-HyA`uuoqAeY-ANh>p8kVnIO1Yi^z{8f0CIX`-+{7 zi0JCJUy5`h(!xz;VPRy7ypf%p@w?vp%Wymm2h^-SiL|Qz9}?U&hnA}T>Ok-j0NSAJhPBTU&}0Q@4lPwtfoV(+8m;PM zh}FFvn&MmzEmdo~Xbc}3|ExB}z_hlDCL}P;E(|0<0wV&`ns;{pZ{VNi|4|E55+H#; zBcRQ?U$5{`aksubp4GQewRM3*{W!wQM*tGLisx`Q>?d15b7-lG3XC5Dj)8#$zDnQ$ D^obJ3 literal 0 HcmV?d00001 diff --git a/lego/apps/events/models.py b/lego/apps/events/models.py index 3983e79e1..b5a461498 100644 --- a/lego/apps/events/models.py +++ b/lego/apps/events/models.py @@ -232,13 +232,11 @@ def get_earliest_registration_time( if len(pools) == 0: return None reg_time: date = min(pool.activation_date for pool in pools) + if self.heed_penalties: if penalties is None: penalties = user.number_of_penalties() - if penalties == 2: - return reg_time + timedelta(hours=12) - elif penalties == 1: - return reg_time + timedelta(hours=3) + return reg_time + timedelta(hours=5) if penalties >= 1 else reg_time return reg_time def get_possible_pools( @@ -328,9 +326,6 @@ def register(self, registration: Registration) -> Registration: # Make the user follow the event FollowEvent.objects.get_or_create(follower=user, target=self) - if penalties >= 3: - return registration.add_to_waiting_list() - # If the event is merged or has only one pool we can skip a lot of logic if all_pools.count() == 1: return registration.add_to_pool(possible_pools[0]) @@ -469,8 +464,6 @@ def early_bump(self, opening_pool: Pool) -> None: for reg in self.waiting_registrations: if opening_pool.is_full: break - if self.heed_penalties and reg.user.number_of_penalties() >= 3: - continue if self.can_register(reg.user, opening_pool, future=True): reg.pool = opening_pool reg.save() @@ -491,8 +484,6 @@ def bump_on_pool_creation_or_expansion(self) -> None: for reg in self.waiting_registrations: if self.is_full or pool.is_full: break - if self.heed_penalties and reg.user.number_of_penalties() >= 3: - continue if self.can_register(reg.user, pool, future=True): reg.pool = pool reg.save() @@ -576,28 +567,9 @@ def pop_from_waiting_list( if to_pool: for registration in self.waiting_registrations: - if self.heed_penalties: - penalties: int = registration.user.number_of_penalties() - earliest_reg: Optional[date] = self.get_earliest_registration_time( - registration.user, [to_pool], penalties - ) - if penalties < 3 and earliest_reg and earliest_reg < timezone.now(): - if self.can_register(registration.user, to_pool): - return registration - elif self.can_register(registration.user, to_pool): + if self.can_register(registration.user, to_pool): return registration return None - - if self.heed_penalties: - for registration in self.waiting_registrations: - penalties = registration.user.number_of_penalties() - earliest_reg = self.get_earliest_registration_time( - registration.user, None, penalties - ) - if penalties < 3 and earliest_reg and earliest_reg < timezone.now(): - return registration - return None - return self.waiting_registrations.first() @staticmethod diff --git a/lego/apps/events/tests/test_async_tasks.py b/lego/apps/events/tests/test_async_tasks.py index 5aaaa5ece..64d8d1555 100644 --- a/lego/apps/events/tests/test_async_tasks.py +++ b/lego/apps/events/tests/test_async_tasks.py @@ -223,8 +223,9 @@ def test_isnt_bumped_without_permission(self): self.assertEqual(self.pool_two.registrations.count(), 0) self.assertEqual(self.event.waiting_registrations.count(), 1) + """ def test_isnt_bumped_with_penalties(self): - """Users should not be bumped if they have 3 penalties.""" + ""Users should not be bumped if they have 3 penalties."" self.event.start_time = timezone.now() + timedelta(days=1) self.event.merge_time = timezone.now() + timedelta(hours=12) self.event.save() @@ -252,6 +253,7 @@ def test_isnt_bumped_with_penalties(self): self.assertEqual(self.pool_two.registrations.count(), 0) self.assertEqual(self.event.waiting_registrations.count(), 1) + """ def test_isnt_bumped_if_activation_is_far_into_the_future(self): """Users should not be bumped if the pool is activated more than @@ -419,8 +421,9 @@ def test_is_bumped_with_multiple_penalties(self): self.assertIsNotNone(Registration.objects.get(id=registration.id).pool) self.assertEqual(self.event.number_of_registrations, 1) + """ def test_isnt_bumped_with_too_many_penalties(self): - """Tests that a user isn't bumped when going from 4 to 3 active penalties""" + ""Tests that a user isn't bumped when going from 4 to 3 active penalties"" user = get_dummy_users(1)[0] AbakusGroup.objects.get(name="Abakus").add_user(user) @@ -442,6 +445,7 @@ def test_isnt_bumped_with_too_many_penalties(self): self.assertIsNone(Registration.objects.get(id=registration.id).pool) self.assertEqual(self.event.number_of_registrations, 0) + """ def test_isnt_bumped_when_full(self): """Tests that a user isnt bumped when the event is full when penalties expire.""" diff --git a/lego/apps/events/tests/test_penalties.py b/lego/apps/events/tests/test_penalties.py index f518ea092..4ffe5b957 100644 --- a/lego/apps/events/tests/test_penalties.py +++ b/lego/apps/events/tests/test_penalties.py @@ -43,7 +43,7 @@ def test_get_earliest_registration_time_ignore_penalties(self): self.assertEqual(earliest_reg, current_time) def test_get_earliest_registration_time_one_penalty(self): - """Test method calculating the earliest registration time for user with one penalty""" + """Test method calculating the earliest registration time for user with one or more penalties""" event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") current_time = timezone.now() @@ -53,16 +53,22 @@ def test_get_earliest_registration_time_one_penalty(self): user = get_dummy_users(1)[0] AbakusGroup.objects.get(name="Webkom").add_user(user) - Penalty.objects.create(user=user, reason="test", weight=1, source_event=event) + Penalty.objects.create( + user=user, reason="first test penalty", weight=1, source_event=event + ) + Penalty.objects.create( + user=user, reason="second test penalty", weight=2, source_event=event + ) penalties = user.number_of_penalties() earliest_reg = event.get_earliest_registration_time( user, [webkom_pool], penalties ) - self.assertEqual(earliest_reg, current_time + timedelta(hours=3)) + self.assertEqual(earliest_reg, current_time + timedelta(hours=5)) + """ def test_get_earliest_registration_time_two_penalties(self): - """Test method calculating the earliest registration time for user with two penalties""" + ""Test method calculating the earliest registration time for user with two penalties"" event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") current_time = timezone.now() @@ -79,9 +85,10 @@ def test_get_earliest_registration_time_two_penalties(self): user, [webkom_pool], penalties ) self.assertEqual(earliest_reg, current_time + timedelta(hours=12)) + """ def test_cant_register_with_one_penalty_before_delay(self): - """Test that user can not register before (3 hour) delay when having one penalty""" + """Test that user can not register before (5 hour) delay when having one or more penalties""" event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") current_time = timezone.now() @@ -91,31 +98,42 @@ def test_cant_register_with_one_penalty_before_delay(self): user = get_dummy_users(1)[0] AbakusGroup.objects.get(name="Abakus").add_user(user) - Penalty.objects.create(user=user, reason="test", weight=1, source_event=event) + Penalty.objects.create( + user=user, reason="first test penalty", weight=1, source_event=event + ) + Penalty.objects.create( + user=user, reason="second test penalty", weight=2, source_event=event + ) with self.assertRaises(ValueError): registration = Registration.objects.get_or_create(event=event, user=user)[0] event.register(registration) def test_can_register_with_one_penalty_after_delay(self): - """Test that user can register after (3 hour) delay has passed having one penalty""" + """Test that user can register after (5 hour) delay has passed having one or more penalties""" event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") current_time = timezone.now() abakus_pool = event.pools.get(name="Abakusmember") - abakus_pool.activation_date = current_time - timedelta(hours=3) + abakus_pool.activation_date = current_time - timedelta(hours=5) abakus_pool.save() user = get_dummy_users(1)[0] AbakusGroup.objects.get(name="Abakus").add_user(user) - Penalty.objects.create(user=user, reason="test", weight=1, source_event=event) + Penalty.objects.create( + user=user, reason="first test penalty", weight=1, source_event=event + ) + Penalty.objects.create( + user=user, reason="second test penalty", weight=2, source_event=event + ) registration = Registration.objects.get_or_create(event=event, user=user)[0] event.register(registration) self.assertEqual(event.number_of_registrations, 1) + """ def test_cant_register_with_two_penalties_before_delay(self): - """Test that user can not register before (12 hour) delay when having two penalties""" + ""Test that user can not register before (12 hour) delay when having two penalties"" event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") current_time = timezone.now() @@ -130,9 +148,11 @@ def test_cant_register_with_two_penalties_before_delay(self): with self.assertRaises(ValueError): registration = Registration.objects.get_or_create(event=event, user=user)[0] event.register(registration) + """ + """ def test_can_register_with_two_penalties_after_delay(self): - """Test that user can register after (12 hour) delay when having two penalties""" + ""Test that user can register after (12 hour) delay when having two penalties"" event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") current_time = timezone.now() @@ -147,9 +167,11 @@ def test_can_register_with_two_penalties_after_delay(self): registration = Registration.objects.get_or_create(event=event, user=user)[0] event.register(registration) self.assertEqual(event.number_of_registrations, 1) + """ + """ def test_waiting_list_on_three_penalties(self): - """Test that user is registered to waiting list directly when having three penalties""" + ""Test that user is registered to waiting list directly when having three penalties"" event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") user = get_dummy_users(1)[0] @@ -160,9 +182,11 @@ def test_waiting_list_on_three_penalties(self): event.register(registration) self.assertEqual(event.number_of_registrations, 0) self.assertEqual(event.waiting_registrations.count(), 1) + """ + """ def test_waiting_list_on_more_than_three_penalties(self): - """Test that user is registered to waiting list directly having over three penalties""" + ""Test that user is registered to waiting list directly having over three penalties"" event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") user = get_dummy_users(1)[0] @@ -174,9 +198,11 @@ def test_waiting_list_on_more_than_three_penalties(self): event.register(registration) self.assertEqual(event.number_of_registrations, 0) self.assertEqual(event.waiting_registrations.count(), 1) + """ + """ def test_waiting_list_on_three_penalties_post_merge(self): - """Test that user is registered to waiting list with three penalties after merge""" + ""Test that user is registered to waiting list with three penalties after merge"" event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") event.merge_time = timezone.now() - timedelta(hours=24) event.save() @@ -189,9 +215,11 @@ def test_waiting_list_on_three_penalties_post_merge(self): event.register(registration) self.assertEqual(event.number_of_registrations, 0) self.assertEqual(event.waiting_registrations.count(), 1) + """ + """ def test_not_bumped_if_three_penalties(self): - """Test that user is not bumped on unregistration having three penalties""" + ""Test that user is not bumped on unregistration having three penalties"" event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") users = get_dummy_users(5) @@ -217,9 +245,11 @@ def test_not_bumped_if_three_penalties(self): self.assertIsNone(event.registrations.get(user=waiting_users[0]).pool) self.assertIsNotNone(event.registrations.get(user=waiting_users[1]).pool) + """ + """ def test_not_bumped_if_three_penalties_post_merge(self): - """Test that user is not bumped on unregistration having three penalties after merge""" + ""Test that user is not bumped on unregistration having three penalties after merge"" event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") users = get_dummy_users(7) @@ -251,9 +281,11 @@ def test_not_bumped_if_three_penalties_post_merge(self): self.assertIsNone(event.registrations.get(user=waiting_users[0]).pool) self.assertIsNotNone(event.registrations.get(user=waiting_users[1]).pool) + """ + """ def test_bumped_if_penalties_expire_while_waiting(self): - """Test that user gets bumped when penalties expire while on waiting list""" + ""Test that user gets bumped when penalties expire while on waiting list"" event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") for pool in event.pools.all(): @@ -288,7 +320,9 @@ def test_bumped_if_penalties_expire_while_waiting(self): self.assertIsNotNone(event.registrations.get(user=waiting_users[0]).pool) self.assertIsNone(event.registrations.get(user=waiting_users[1]).pool) + """ + """ def test_isnt_bumped_if_third_penalty_expires_but_reg_delay_is_still_active(self): event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") for pool in event.pools.all(): @@ -323,7 +357,9 @@ def test_isnt_bumped_if_third_penalty_expires_but_reg_delay_is_still_active(self self.assertIsNone(event.registrations.get(user=waiting_users[0]).pool) self.assertIsNotNone(event.registrations.get(user=waiting_users[1]).pool) + """ + """ def test_no_legal_bump(self): event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") users = get_dummy_users(5) @@ -353,6 +389,7 @@ def test_no_legal_bump(self): self.assertIsNone(event.registrations.get(user=users[3]).pool) self.assertIsNone(event.registrations.get(user=users[4]).pool) + """ def test_penalties_created_on_unregister(self): """Test that user gets penalties on unregister after limit""" diff --git a/lego/apps/users/action_handlers.py b/lego/apps/users/action_handlers.py index 3afc6af1c..ddcbc5b9d 100644 --- a/lego/apps/users/action_handlers.py +++ b/lego/apps/users/action_handlers.py @@ -53,7 +53,11 @@ def get_activity(self, penalty): object=penalty, target=penalty.user, time=penalty.created_at, - extra_context={"reason": penalty.reason, "weight": penalty.weight}, + extra_context={ + "reason": penalty.reason, + "weight": penalty.weight, + "expiration_date": penalty.exact_expiration.days, + }, ) def handle_create(self, instance, **kwargs): diff --git a/lego/apps/users/migrations/0039_penalty_activation_date.py b/lego/apps/users/migrations/0039_penalty_activation_date.py new file mode 100644 index 000000000..6fda139ca --- /dev/null +++ b/lego/apps/users/migrations/0039_penalty_activation_date.py @@ -0,0 +1,19 @@ +# Generated by Django 4.0.10 on 2023-04-26 09:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0038_alter_abakusgroup_type"), + ] + + operations = [ + migrations.AddField( + model_name="penalty", + name="activation_date", + field=models.DateTimeField( + default=None, null=True, verbose_name="date created" + ), + ), + ] diff --git a/lego/apps/users/migrations/0040_rename_activation_date_penalty_activation_time.py b/lego/apps/users/migrations/0040_rename_activation_date_penalty_activation_time.py new file mode 100644 index 000000000..079cdbd42 --- /dev/null +++ b/lego/apps/users/migrations/0040_rename_activation_date_penalty_activation_time.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.10 on 2023-04-26 13:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0039_penalty_activation_date'), + ] + + operations = [ + migrations.RenameField( + model_name='penalty', + old_name='activation_date', + new_name='activation_time', + ), + ] diff --git a/lego/apps/users/models.py b/lego/apps/users/models.py index 87176d0f5..bd29c72ee 100644 --- a/lego/apps/users/models.py +++ b/lego/apps/users/models.py @@ -495,30 +495,43 @@ class Penalty(BasisModel): source_event = models.ForeignKey( "events.Event", related_name="penalties", on_delete=models.CASCADE ) + activation_time = models.DateTimeField("date created", null=True, default=None) objects = UserPenaltyManager() # type: ignore - def expires(self): - dt = Penalty.penalty_offset(self.created_at) - ( - timezone.now() - self.created_at + # add ingore previous instances that dont have activation_time + def save(self, *args, **kwargs): + last_penalty_to_be_expired = Penalty.objects.filter(user=self.user).order_by( + "-activation_time" + ) + self.activation_time = ( + last_penalty_to_be_expired[0].exact_expiration + if len(last_penalty_to_be_expired) != 0 + else self.created_at ) - return dt.days + super().save(*args, **kwargs) + + def expires(self): + expirationDate = Penalty.penalty_offset( + self.activation_time, weight=self.weight + ) - (timezone.now() - self.activation_time) + return expirationDate.days @property def exact_expiration(self): """Returns the exact time of expiration""" - dt = Penalty.penalty_offset(self.created_at) - ( - timezone.now() - self.created_at + dt = Penalty.penalty_offset(self.activation_time, weight=self.weight) - ( + timezone.now() - self.activation_time ) return timezone.now() + dt @staticmethod - def penalty_offset(start_date, forwards=True): + def penalty_offset(start_date, forwards=True, weight=1): remaining_days = settings.PENALTY_DURATION.days offset_days = 0 multiplier = 1 if forwards else -1 - date_to_check = start_date + (multiplier * timedelta(days=offset_days)) + date_to_check = start_date + (multiplier * weight * timedelta(days=offset_days)) ignore_date = Penalty.ignore_date(date_to_check) while remaining_days > 0 or ignore_date: if not ignore_date: diff --git a/lego/apps/users/notifications.py b/lego/apps/users/notifications.py index f7e30161b..d0d965a75 100644 --- a/lego/apps/users/notifications.py +++ b/lego/apps/users/notifications.py @@ -21,6 +21,7 @@ def generate_mail(self): "weight": penalty.weight, "event": penalty.source_event.title, "reason": penalty.reason, + "expiration_date": penalty.exact_expiration.days, }, subject="Du har fått en ny prikk", plain_template="users/email/penalty.txt", @@ -32,7 +33,11 @@ def generate_push(self): return self._delay_push( template="users/push/penalty.txt", - context={"weight": penalty.weight, "event": penalty.source_event.title}, + context={ + "weight": penalty.weight, + "event": penalty.source_event.title, + "expiration_date": penalty.exact_expiration.days, + }, instance=penalty, ) diff --git a/lego/apps/users/serializers/penalties.py b/lego/apps/users/serializers/penalties.py index 6ceb052d4..db1e43dd9 100644 --- a/lego/apps/users/serializers/penalties.py +++ b/lego/apps/users/serializers/penalties.py @@ -13,5 +13,6 @@ class Meta: "reason", "weight", "source_event", + "activation_time", "exact_expiration", ) diff --git a/lego/apps/users/tests/test_models.py b/lego/apps/users/tests/test_models.py index 0726a54f5..d7fb08682 100644 --- a/lego/apps/users/tests/test_models.py +++ b/lego/apps/users/tests/test_models.py @@ -3,6 +3,7 @@ from django.test import override_settings from django.utils import timezone from django.utils.timezone import timedelta +from lego import settings from lego.apps.events.models import Event from lego.apps.files.models import File @@ -291,14 +292,14 @@ def test_count_weights(self): @mock.patch("django.utils.timezone.now", return_value=fake_time(2016, 10, 1)) def test_only_count_active_penalties(self, mock_now): Penalty.objects.create( - created_at=mock_now() - timedelta(days=20), + created_at=mock_now() - timedelta(days=10), user=self.test_user, reason="test", weight=1, source_event=self.source, ) Penalty.objects.create( - created_at=mock_now() - timedelta(days=19, hours=23, minutes=59), + created_at=mock_now() - timedelta(days=9, hours=23, minutes=59), user=self.test_user, reason="test", weight=1, @@ -309,20 +310,20 @@ def test_only_count_active_penalties(self, mock_now): @override_settings(PENALTY_IGNORE_WINTER=((12, 10), (1, 10))) @mock.patch("django.utils.timezone.now", return_value=fake_time(2016, 12, 10)) def test_frozen_penalties_count_as_active_winter(self, mock_now): - # This penalty is created slightly less than 20 days from the freeze-point. + # This penalty is created slightly less than 10 days from the freeze-point. # It should be counted as active. Penalty.objects.create( - created_at=mock_now() - timedelta(days=20, hours=23, minutes=59), + created_at=mock_now() - timedelta(days=10, hours=23, minutes=59), user=self.test_user, reason="active", weight=1, source_event=self.source, ) - # This penalty is created exactly 20 days from the freeze-point. + # This penalty is created exactly 10 days from the freeze-point. # It should be counted as inactive. Penalty.objects.create( - created_at=mock_now() - timedelta(days=21), + created_at=mock_now() - timedelta(days=11), user=self.test_user, reason="inactive", weight=1, @@ -335,20 +336,20 @@ def test_frozen_penalties_count_as_active_winter(self, mock_now): @override_settings(PENALTY_IGNORE_SUMMER=((6, 12), (8, 15))) @mock.patch("django.utils.timezone.now", return_value=fake_time(2016, 6, 12)) def test_frozen_penalties_count_as_active_summer(self, mock_now): - # This penalty is created slightly less than 20 days from the freeze-point. + # This penalty is created slightly less than 10 days from the freeze-point. # It should be counted as active. Penalty.objects.create( - created_at=mock_now() - timedelta(days=20, hours=23, minutes=59), + created_at=mock_now() - timedelta(days=10, hours=23, minutes=59), user=self.test_user, reason="active", weight=1, source_event=self.source, ) - # This penalty is created exactly 20 days from the freeze-point. + # This penalty is created exactly 10 days from the freeze-point. # It should be counted as inactive. Penalty.objects.create( - created_at=mock_now() - timedelta(days=21), + created_at=mock_now() - timedelta(days=11), user=self.test_user, reason="inactive", weight=1, @@ -364,7 +365,7 @@ def test_penalty_offset_is_calculated_correctly(self, mock_now): # This penalty is set to expire the day before the penalty freeze # It should not be active inactive = Penalty.objects.create( - created_at=mock_now().replace(day=1), + created_at=mock_now().replace(day=10), user=self.test_user, reason="inactive", weight=1, @@ -372,20 +373,22 @@ def test_penalty_offset_is_calculated_correctly(self, mock_now): ) self.assertEqual(self.test_user.number_of_penalties(), 0) self.assertEqual( - (inactive.exact_expiration.month, inactive.exact_expiration.day), (12, 21) + (inactive.exact_expiration.month, inactive.exact_expiration.day), + (12, inactive.created_at.day + settings.PENALTY_DURATION.days), ) # This penalty is set to expire the same day as the freeze active = Penalty.objects.create( - created_at=mock_now().replace(day=2), + created_at=mock_now().replace(day=5), user=self.test_user, reason="active", weight=1, source_event=self.source, ) - self.assertEqual(self.test_user.number_of_penalties(), 1) + # 19 = 11: end of holiday + 10: penalty time - 2: from activation time to start of holiday self.assertEqual( - (active.exact_expiration.month, active.exact_expiration.day), (1, 11) + (active.exact_expiration.month, active.exact_expiration.day), + (1, 19), ) diff --git a/lego/apps/users/tests/test_users_api.py b/lego/apps/users/tests/test_users_api.py index 098a683c3..38b612ba2 100644 --- a/lego/apps/users/tests/test_users_api.py +++ b/lego/apps/users/tests/test_users_api.py @@ -454,14 +454,14 @@ def test_self_unauthed(self): def test_own_penalties_serializer(self, mock_now): source = Event.objects.all().first() Penalty.objects.create( - created_at=mock_now() - timedelta(days=20), + created_at=mock_now() - timedelta(days=10), user=self.user, reason="test", weight=1, source_event=source, ) Penalty.objects.create( - created_at=mock_now() - timedelta(days=19, hours=23, minutes=59), + created_at=mock_now() - timedelta(days=9, hours=23, minutes=59), user=self.user, reason="test", weight=1, @@ -476,7 +476,6 @@ def test_own_penalties_serializer(self, mock_now): len(self.user.penalties.valid()), len(response.json()["penalties"]) ) self.assertEqual(len(response.json()["penalties"]), 1) - self.assertEqual(len(response.json()["penalties"][0]), 7) class UpdatePhotoConsentTestCase(BaseAPITestCase): diff --git a/lego/settings/lego.py b/lego/settings/lego.py index bf0b5646a..851830566 100644 --- a/lego/settings/lego.py +++ b/lego/settings/lego.py @@ -17,7 +17,7 @@ ADMINS = (("Webkom", "webkom@abakus.no"),) MANAGERS = ADMINS -PENALTY_DURATION = timedelta(days=20) +PENALTY_DURATION = timedelta(days=10) # Tuples for ignored (month, day) intervals PENALTY_IGNORE_SUMMER = ((6, 1), (8, 15)) PENALTY_IGNORE_WINTER = ((12, 1), (1, 10)) From 7b743f178fa35faa7f2ae02d1337733290700ec7 Mon Sep 17 00:00:00 2001 From: Arash Farzaneh Taleghani Date: Sun, 30 Apr 2023 23:14:46 +0200 Subject: [PATCH 2/4] Implement the 6 event rule --- .gitignore | 3 + lego/apps/events/tests/test_async_tasks.py | 58 ---- lego/apps/events/tests/test_penalties.py | 290 +----------------- ...ate.py => 0039_penalty_activation_time.py} | 4 +- ...activation_date_penalty_activation_time.py | 18 -- lego/apps/users/models.py | 78 ++++- lego/apps/users/tests/test_models.py | 115 ++++++- 7 files changed, 188 insertions(+), 378 deletions(-) rename lego/apps/users/migrations/{0039_penalty_activation_date.py => 0039_penalty_activation_time.py} (81%) delete mode 100644 lego/apps/users/migrations/0040_rename_activation_date_penalty_activation_time.py diff --git a/.gitignore b/.gitignore index 8e3486b6e..82bb15025 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,6 @@ google-credentials.json # vscode .vscode/ + +# mac +.DS_Store diff --git a/lego/apps/events/tests/test_async_tasks.py b/lego/apps/events/tests/test_async_tasks.py index 64d8d1555..a36d4291b 100644 --- a/lego/apps/events/tests/test_async_tasks.py +++ b/lego/apps/events/tests/test_async_tasks.py @@ -223,38 +223,6 @@ def test_isnt_bumped_without_permission(self): self.assertEqual(self.pool_two.registrations.count(), 0) self.assertEqual(self.event.waiting_registrations.count(), 1) - """ - def test_isnt_bumped_with_penalties(self): - ""Users should not be bumped if they have 3 penalties."" - self.event.start_time = timezone.now() + timedelta(days=1) - self.event.merge_time = timezone.now() + timedelta(hours=12) - self.event.save() - - self.pool_one.activation_date = timezone.now() - timedelta(days=1) - self.pool_one.save() - - self.pool_two.activation_date = timezone.now() + timedelta(minutes=30) - self.pool_two.save() - - users = get_dummy_users(2) - - Penalty.objects.create( - user=users[1], reason="test", weight=3, source_event=self.event - ) - - for user in users: - AbakusGroup.objects.get(name="Webkom").add_user(user) - registration = Registration.objects.get_or_create( - event=self.event, user=user - )[0] - self.event.register(registration) - - bump_waiting_users_to_new_pool() - - self.assertEqual(self.pool_two.registrations.count(), 0) - self.assertEqual(self.event.waiting_registrations.count(), 1) - """ - def test_isnt_bumped_if_activation_is_far_into_the_future(self): """Users should not be bumped if the pool is activated more than 35 minutes in the future.""" @@ -421,32 +389,6 @@ def test_is_bumped_with_multiple_penalties(self): self.assertIsNotNone(Registration.objects.get(id=registration.id).pool) self.assertEqual(self.event.number_of_registrations, 1) - """ - def test_isnt_bumped_with_too_many_penalties(self): - ""Tests that a user isn't bumped when going from 4 to 3 active penalties"" - - user = get_dummy_users(1)[0] - AbakusGroup.objects.get(name="Abakus").add_user(user) - - p1 = Penalty.objects.create( - user=user, reason="test", weight=1, source_event=self.event - ) - Penalty.objects.create( - user=user, reason="test2", weight=3, source_event=self.event - ) - - registration = Registration.objects.get_or_create(event=self.event, user=user)[ - 0 - ] - async_register(registration.id) - - make_penalty_expire(p1) - check_events_for_registrations_with_expired_penalties.delay() - - self.assertIsNone(Registration.objects.get(id=registration.id).pool) - self.assertEqual(self.event.number_of_registrations, 0) - """ - def test_isnt_bumped_when_full(self): """Tests that a user isnt bumped when the event is full when penalties expire.""" diff --git a/lego/apps/events/tests/test_penalties.py b/lego/apps/events/tests/test_penalties.py index 4ffe5b957..083fd1e97 100644 --- a/lego/apps/events/tests/test_penalties.py +++ b/lego/apps/events/tests/test_penalties.py @@ -43,7 +43,8 @@ def test_get_earliest_registration_time_ignore_penalties(self): self.assertEqual(earliest_reg, current_time) def test_get_earliest_registration_time_one_penalty(self): - """Test method calculating the earliest registration time for user with one or more penalties""" + """Test method calculating the earliest + registration time for user with one or more penalties""" event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") current_time = timezone.now() @@ -66,29 +67,9 @@ def test_get_earliest_registration_time_one_penalty(self): ) self.assertEqual(earliest_reg, current_time + timedelta(hours=5)) - """ - def test_get_earliest_registration_time_two_penalties(self): - ""Test method calculating the earliest registration time for user with two penalties"" - event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") - - current_time = timezone.now() - webkom_pool = event.pools.get(name="Webkom") - webkom_pool.activation_date = current_time - webkom_pool.save() - - user = get_dummy_users(1)[0] - AbakusGroup.objects.get(name="Webkom").add_user(user) - Penalty.objects.create(user=user, reason="test", weight=2, source_event=event) - penalties = user.number_of_penalties() - - earliest_reg = event.get_earliest_registration_time( - user, [webkom_pool], penalties - ) - self.assertEqual(earliest_reg, current_time + timedelta(hours=12)) - """ - def test_cant_register_with_one_penalty_before_delay(self): - """Test that user can not register before (5 hour) delay when having one or more penalties""" + """Test that user can not register + before (5 hour) delay when having one or more penalties""" event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") current_time = timezone.now() @@ -110,7 +91,8 @@ def test_cant_register_with_one_penalty_before_delay(self): event.register(registration) def test_can_register_with_one_penalty_after_delay(self): - """Test that user can register after (5 hour) delay has passed having one or more penalties""" + """Test that user can register after (5 hour) + delay has passed having one or more penalties""" event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") current_time = timezone.now() @@ -131,266 +113,6 @@ def test_can_register_with_one_penalty_after_delay(self): event.register(registration) self.assertEqual(event.number_of_registrations, 1) - """ - def test_cant_register_with_two_penalties_before_delay(self): - ""Test that user can not register before (12 hour) delay when having two penalties"" - event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") - - current_time = timezone.now() - abakus_pool = event.pools.get(name="Abakusmember") - abakus_pool.activation_date = current_time - abakus_pool.save() - - user = get_dummy_users(1)[0] - AbakusGroup.objects.get(name="Abakus").add_user(user) - Penalty.objects.create(user=user, reason="test", weight=2, source_event=event) - - with self.assertRaises(ValueError): - registration = Registration.objects.get_or_create(event=event, user=user)[0] - event.register(registration) - """ - - """ - def test_can_register_with_two_penalties_after_delay(self): - ""Test that user can register after (12 hour) delay when having two penalties"" - event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") - - current_time = timezone.now() - abakus_pool = event.pools.get(name="Abakusmember") - abakus_pool.activation_date = current_time - timedelta(hours=12) - abakus_pool.save() - - user = get_dummy_users(1)[0] - AbakusGroup.objects.get(name="Abakus").add_user(user) - Penalty.objects.create(user=user, reason="test", weight=2, source_event=event) - - registration = Registration.objects.get_or_create(event=event, user=user)[0] - event.register(registration) - self.assertEqual(event.number_of_registrations, 1) - """ - - """ - def test_waiting_list_on_three_penalties(self): - ""Test that user is registered to waiting list directly when having three penalties"" - event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") - - user = get_dummy_users(1)[0] - AbakusGroup.objects.get(name="Abakus").add_user(user) - Penalty.objects.create(user=user, reason="test", weight=3, source_event=event) - - registration = Registration.objects.get_or_create(event=event, user=user)[0] - event.register(registration) - self.assertEqual(event.number_of_registrations, 0) - self.assertEqual(event.waiting_registrations.count(), 1) - """ - - """ - def test_waiting_list_on_more_than_three_penalties(self): - ""Test that user is registered to waiting list directly having over three penalties"" - event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") - - user = get_dummy_users(1)[0] - AbakusGroup.objects.get(name="Abakus").add_user(user) - Penalty.objects.create(user=user, reason="test", weight=2, source_event=event) - Penalty.objects.create(user=user, reason="test2", weight=2, source_event=event) - - registration = Registration.objects.get_or_create(event=event, user=user)[0] - event.register(registration) - self.assertEqual(event.number_of_registrations, 0) - self.assertEqual(event.waiting_registrations.count(), 1) - """ - - """ - def test_waiting_list_on_three_penalties_post_merge(self): - ""Test that user is registered to waiting list with three penalties after merge"" - event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") - event.merge_time = timezone.now() - timedelta(hours=24) - event.save() - - user = get_dummy_users(1)[0] - AbakusGroup.objects.get(name="Abakus").add_user(user) - Penalty.objects.create(user=user, reason="test", weight=3, source_event=event) - - registration = Registration.objects.get_or_create(event=event, user=user)[0] - event.register(registration) - self.assertEqual(event.number_of_registrations, 0) - self.assertEqual(event.waiting_registrations.count(), 1) - """ - - """ - def test_not_bumped_if_three_penalties(self): - ""Test that user is not bumped on unregistration having three penalties"" - event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") - - users = get_dummy_users(5) - abakus_users = users[:5] - waiting_users = users[3:5] - - for user in abakus_users: - AbakusGroup.objects.get(name="Abakus").add_user(user) - for user in users: - registration = Registration.objects.get_or_create(event=event, user=user)[0] - event.register(registration) - - self.assertIsNone(event.registrations.get(user=waiting_users[0]).pool) - self.assertIsNone(event.registrations.get(user=waiting_users[1]).pool) - - Penalty.objects.create( - user=waiting_users[0], reason="test", weight=3, source_event=event - ) - registration_to_unregister = Registration.objects.get( - event=event, user=users[0] - ) - event.unregister(registration_to_unregister) - - self.assertIsNone(event.registrations.get(user=waiting_users[0]).pool) - self.assertIsNotNone(event.registrations.get(user=waiting_users[1]).pool) - """ - - """ - def test_not_bumped_if_three_penalties_post_merge(self): - ""Test that user is not bumped on unregistration having three penalties after merge"" - event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") - - users = get_dummy_users(7) - abakus_users = users[:5] - webkom_users = users[5:7] - waiting_users = users[3:5] - - for user in abakus_users: - AbakusGroup.objects.get(name="Abakus").add_user(user) - for user in webkom_users: - AbakusGroup.objects.get(name="Webkom").add_user(user) - for user in users: - registration = Registration.objects.get_or_create(event=event, user=user)[0] - event.register(registration) - - self.assertIsNone(event.registrations.get(user=waiting_users[0]).pool) - self.assertIsNone(event.registrations.get(user=waiting_users[1]).pool) - - event.merge_time = timezone.now() - timedelta(hours=24) - event.save() - Penalty.objects.create( - user=waiting_users[0], reason="test", weight=3, source_event=event - ) - - registration_to_unregister = Registration.objects.get( - event=event, user=webkom_users[0] - ) - event.unregister(registration_to_unregister) - - self.assertIsNone(event.registrations.get(user=waiting_users[0]).pool) - self.assertIsNotNone(event.registrations.get(user=waiting_users[1]).pool) - """ - - """ - def test_bumped_if_penalties_expire_while_waiting(self): - ""Test that user gets bumped when penalties expire while on waiting list"" - event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") - - for pool in event.pools.all(): - pool.activation_date = timezone.now() - timedelta(hours=12) - pool.save() - - users = get_dummy_users(5) - penalty_one = Penalty.objects.create( - user=users[0], reason="test", weight=1, source_event=event - ) - Penalty.objects.create( - user=users[0], reason="test", weight=2, source_event=event - ) - abakus_users = users[:5] - waiting_users = [users[0], users[4]] - - for user in abakus_users: - AbakusGroup.objects.get(name="Abakus").add_user(user) - for user in users: - registration = Registration.objects.get_or_create(event=event, user=user)[0] - event.register(registration) - - self.assertIsNone(event.registrations.get(user=waiting_users[0]).pool) - self.assertIsNone(event.registrations.get(user=waiting_users[1]).pool) - - penalty_one.created_at = timezone.now() - timedelta(days=365) - penalty_one.save() - registration_to_unregister = Registration.objects.get( - event=event, user=users[1] - ) - event.unregister(registration_to_unregister) - - self.assertIsNotNone(event.registrations.get(user=waiting_users[0]).pool) - self.assertIsNone(event.registrations.get(user=waiting_users[1]).pool) - """ - - """ - def test_isnt_bumped_if_third_penalty_expires_but_reg_delay_is_still_active(self): - event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") - for pool in event.pools.all(): - pool.activation_date = timezone.now() - timedelta(hours=6) - pool.save() - - users = get_dummy_users(5) - penalty_one = Penalty.objects.create( - user=users[0], reason="test", weight=1, source_event=event - ) - Penalty.objects.create( - user=users[0], reason="test", weight=2, source_event=event - ) - abakus_users = users[:5] - waiting_users = [users[0], users[4]] - - for user in abakus_users: - AbakusGroup.objects.get(name="Abakus").add_user(user) - for user in users: - registration = Registration.objects.get_or_create(event=event, user=user)[0] - event.register(registration) - - self.assertIsNone(event.registrations.get(user=waiting_users[0]).pool) - self.assertIsNone(event.registrations.get(user=waiting_users[1]).pool) - - penalty_one.created_at = timezone.now() - timedelta(days=365) - penalty_one.save() - registration_to_unregister = Registration.objects.get( - event=event, user=users[1] - ) - event.unregister(registration_to_unregister) - - self.assertIsNone(event.registrations.get(user=waiting_users[0]).pool) - self.assertIsNotNone(event.registrations.get(user=waiting_users[1]).pool) - """ - - """ - def test_no_legal_bump(self): - event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") - users = get_dummy_users(5) - for pool in event.pools.all(): - pool.activation_date = timezone.now() - pool.save() - - for user in users: - AbakusGroup.objects.get(name="Abakus").add_user(user) - registration = Registration.objects.get_or_create(event=event, user=user)[0] - event.register(registration) - - self.assertIsNone(event.registrations.get(user=users[3]).pool) - self.assertIsNone(event.registrations.get(user=users[4]).pool) - - Penalty.objects.create( - user=users[3], reason="test", weight=3, source_event=event - ) - Penalty.objects.create( - user=users[4], reason="test", weight=2, source_event=event - ) - - registration_to_unregister = Registration.objects.get( - event=event, user=users[0] - ) - event.unregister(registration_to_unregister) - - self.assertIsNone(event.registrations.get(user=users[3]).pool) - self.assertIsNone(event.registrations.get(user=users[4]).pool) - """ - def test_penalties_created_on_unregister(self): """Test that user gets penalties on unregister after limit""" event = Event.objects.get(title="POOLS_WITH_REGISTRATIONS") diff --git a/lego/apps/users/migrations/0039_penalty_activation_date.py b/lego/apps/users/migrations/0039_penalty_activation_time.py similarity index 81% rename from lego/apps/users/migrations/0039_penalty_activation_date.py rename to lego/apps/users/migrations/0039_penalty_activation_time.py index 6fda139ca..6cb29cf88 100644 --- a/lego/apps/users/migrations/0039_penalty_activation_date.py +++ b/lego/apps/users/migrations/0039_penalty_activation_time.py @@ -1,4 +1,4 @@ -# Generated by Django 4.0.10 on 2023-04-26 09:07 +# Generated by Django 4.0.10 on 2023-05-01 16:33 from django.db import migrations, models @@ -11,7 +11,7 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( model_name="penalty", - name="activation_date", + name="activation_time", field=models.DateTimeField( default=None, null=True, verbose_name="date created" ), diff --git a/lego/apps/users/migrations/0040_rename_activation_date_penalty_activation_time.py b/lego/apps/users/migrations/0040_rename_activation_date_penalty_activation_time.py deleted file mode 100644 index 079cdbd42..000000000 --- a/lego/apps/users/migrations/0040_rename_activation_date_penalty_activation_time.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.0.10 on 2023-04-26 13:39 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('users', '0039_penalty_activation_date'), - ] - - operations = [ - migrations.RenameField( - model_name='penalty', - old_name='activation_date', - new_name='activation_time', - ), - ] diff --git a/lego/apps/users/models.py b/lego/apps/users/models.py index bd29c72ee..466081eb2 100644 --- a/lego/apps/users/models.py +++ b/lego/apps/users/models.py @@ -8,7 +8,7 @@ ) from django.contrib.postgres.fields import ArrayField from django.db import models, transaction -from django.db.models import Q +from django.db.models import F, Q from django.utils import timezone from django.utils.timezone import datetime, timedelta @@ -487,6 +487,30 @@ def unanswered_surveys(self) -> list: ) return list(unanswered_surveys.values_list("id", flat=True)) + def check_for_deletable_penalty(self): + from lego.apps.events.models import Event + + # TODO: filter so that only one penalty is returned. the penalty now() is at + current_active_penalty = ( + Penalty.objects.valid().filter(user=self).order_by("activation_time") + ) + + # TODO: add check for all he events the user can be registered at + # get a list of all penalties and in the Event.filter() + # and check that the event is in the penalties events + eligable_passed_events = Event.objects.filter( + heed_penalties=True, + start_time__range=( + current_active_penalty[0].activation_time + - timedelta(hours=1) * F("unregistration_deadline_hours"), + current_active_penalty[0].exact_expiration + - timedelta(hours=1) * F("unregistration_deadline_hours"), + ), + ) + + if len(eligable_passed_events) >= 6: + current_active_penalty[0].delete_and_adjust_future_activation_times() + class Penalty(BasisModel): user = models.ForeignKey(User, related_name="penalties", on_delete=models.CASCADE) @@ -499,11 +523,11 @@ class Penalty(BasisModel): objects = UserPenaltyManager() # type: ignore - # add ingore previous instances that dont have activation_time def save(self, *args, **kwargs): - last_penalty_to_be_expired = Penalty.objects.filter(user=self.user).order_by( - "-activation_time" - ) + last_penalty_to_be_expired = Penalty.objects.filter( + user=self.user, activation_time__lte=timezone.now() + ).order_by("-activation_time") + self.activation_time = ( last_penalty_to_be_expired[0].exact_expiration if len(last_penalty_to_be_expired) != 0 @@ -511,27 +535,51 @@ def save(self, *args, **kwargs): ) super().save(*args, **kwargs) - def expires(self): - expirationDate = Penalty.penalty_offset( - self.activation_time, weight=self.weight - ) - (timezone.now() - self.activation_time) - return expirationDate.days + def default_save(self, *args, **kwargs): + super().save(*args, **kwargs) + + def delete_and_adjust_future_activation_times(self): + if self.weight == 1: + new_activation_time = timezone.now() + future_penalties = Penalty.objects.filter( + user=self.user, activation_time__gt=new_activation_time + ).order_by("activation_time") + + for penalty in future_penalties: + penalty.activation_time = new_activation_time + penalty.default_save() + new_activation_time = penalty.exact_expiration + + self.delete() + else: + self.weight -= 1 + self.activation_time = timezone.now() + future_penalties = Penalty.objects.filter( + user=self.user, activation_time__gt=self.activation_time + ).order_by("activation_time") + new_activation_time = self.exact_expiration + self.default_save() + + for penalty in future_penalties: + penalty.activation_time = new_activation_time + penalty.default_save() + new_activation_time = penalty.exact_expiration @property def exact_expiration(self): """Returns the exact time of expiration""" - dt = Penalty.penalty_offset(self.activation_time, weight=self.weight) - ( - timezone.now() - self.activation_time + return ( + Penalty.penalty_offset(self.activation_time, weight=self.weight) + + self.activation_time ) - return timezone.now() + dt @staticmethod def penalty_offset(start_date, forwards=True, weight=1): - remaining_days = settings.PENALTY_DURATION.days + remaining_days = settings.PENALTY_DURATION.days * weight offset_days = 0 multiplier = 1 if forwards else -1 - date_to_check = start_date + (multiplier * weight * timedelta(days=offset_days)) + date_to_check = start_date + (multiplier * timedelta(days=offset_days)) ignore_date = Penalty.ignore_date(date_to_check) while remaining_days > 0 or ignore_date: if not ignore_date: diff --git a/lego/apps/users/tests/test_models.py b/lego/apps/users/tests/test_models.py index d7fb08682..d8f95229b 100644 --- a/lego/apps/users/tests/test_models.py +++ b/lego/apps/users/tests/test_models.py @@ -3,8 +3,8 @@ from django.test import override_settings from django.utils import timezone from django.utils.timezone import timedelta -from lego import settings +from lego import settings from lego.apps.events.models import Event from lego.apps.files.models import File from lego.apps.users import constants @@ -307,6 +307,119 @@ def test_only_count_active_penalties(self, mock_now): ) self.assertEqual(self.test_user.number_of_penalties(), 1) + @mock.patch("django.utils.timezone.now", return_value=fake_time(2016, 10, 10)) + def test_penalty_deletion_after_6_events(self, mock_now): + """Tests that The first active penalty is + removed and the rest are adjusted after 6 events""" + Penalty.objects.create( + created_at=mock_now() - timedelta(days=8), + user=self.test_user, + reason="first test penalty", + weight=1, + source_event=self.source, + ) + Penalty.objects.create( + created_at=mock_now() - timedelta(days=5), + user=self.test_user, + reason="second test penalty", + weight=1, + source_event=self.source, + ) + Penalty.objects.create( + created_at=mock_now() - timedelta(days=5), + user=self.test_user, + reason="third test penalty", + weight=1, + source_event=self.source, + ) + + for _i in range(5): + Event.objects.create( + title="AbakomEvent", + event_type=0, + start_time=mock_now() - timedelta(days=6), + end_time=mock_now() - timedelta(days=6), + ) + + self.test_user.check_for_deletable_penalty() + + """Tests first that nothing is changed after 5 events""" + self.assertEqual(self.test_user.number_of_penalties(), 3) + + Event.objects.create( + title="AbakomEvent", + event_type=0, + start_time=mock_now() - timedelta(days=6), + end_time=mock_now() - timedelta(days=6), + ) + self.test_user.check_for_deletable_penalty() + """Tests that the changes happened after 6 events""" + self.assertEqual(self.test_user.number_of_penalties(), 2) + self.assertEqual( + ( + Penalty.objects.get(reason="second test penalty").activation_time.day, + Penalty.objects.get(reason="second test penalty").exact_expiration.day, + Penalty.objects.get(reason="third test penalty").exact_expiration.day, + ), + (10, 20, 30), + ) + + @mock.patch("django.utils.timezone.now", return_value=fake_time(2016, 10, 10)) + def test_penalty_weight_decrementing_after_6_events(self, mock_now): + """Tests that The weight of the first active penalty + is decremented and the rest are adjusted after 6 events""" + Penalty.objects.create( + created_at=mock_now() - timedelta(days=8), + user=self.test_user, + reason="first test penalty", + weight=2, + source_event=self.source, + ) + Penalty.objects.create( + created_at=mock_now() - timedelta(days=5), + user=self.test_user, + reason="second test penalty", + weight=1, + source_event=self.source, + ) + + for _i in range(5): + Event.objects.create( + title="AbakomEvent", + event_type=0, + start_time=mock_now() - timedelta(days=6), + end_time=mock_now() - timedelta(days=6), + ) + + self.test_user.check_for_deletable_penalty() + + """Tests first that nothing is changed after 5 events""" + self.assertEqual(self.test_user.number_of_penalties(), 3) + self.assertEqual( + ( + Penalty.objects.get(reason="first test penalty").exact_expiration.day, + Penalty.objects.get(reason="second test penalty").exact_expiration.day, + ), + (22, 1), + ) + + Event.objects.create( + title="AbakomEvent", + event_type=0, + start_time=mock_now() - timedelta(days=6), + end_time=mock_now() - timedelta(days=6), + ) + self.test_user.check_for_deletable_penalty() + """Tests that the changes happened after 6 events""" + self.assertEqual(self.test_user.number_of_penalties(), 2) + self.assertEqual( + ( + Penalty.objects.get(reason="first test penalty").exact_expiration.day, + Penalty.objects.get(reason="second test penalty").exact_expiration.day, + ), + (20, 30), + ) + @override_settings(PENALTY_IGNORE_WINTER=((12, 10), (1, 10))) @mock.patch("django.utils.timezone.now", return_value=fake_time(2016, 12, 10)) def test_frozen_penalties_count_as_active_winter(self, mock_now): From a2295e211599b3314729878935ed5f223fb137a7 Mon Sep 17 00:00:00 2001 From: Arash Farzaneh Taleghani Date: Tue, 2 May 2023 20:58:17 +0200 Subject: [PATCH 3/4] Add requested changes --- lego/apps/events/models.py | 6 +- lego/apps/events/tests/test_penalties.py | 25 ++++- lego/apps/users/managers.py | 4 +- .../0039_penalty_activation_time.py | 7 ++ lego/apps/users/models.py | 100 ++++++++++-------- lego/apps/users/tasks.py | 6 ++ lego/apps/users/tests/test_models.py | 95 ++++++++++++----- lego/settings/celery.py | 4 + lego/settings/lego.py | 1 + 9 files changed, 170 insertions(+), 78 deletions(-) diff --git a/lego/apps/events/models.py b/lego/apps/events/models.py index b5a461498..e42236d2d 100644 --- a/lego/apps/events/models.py +++ b/lego/apps/events/models.py @@ -236,7 +236,11 @@ def get_earliest_registration_time( if self.heed_penalties: if penalties is None: penalties = user.number_of_penalties() - return reg_time + timedelta(hours=5) if penalties >= 1 else reg_time + return ( + reg_time + timedelta(hours=settings.PENALTY_DELAY_DURATION) + if penalties >= 1 + else reg_time + ) return reg_time def get_possible_pools( diff --git a/lego/apps/events/tests/test_penalties.py b/lego/apps/events/tests/test_penalties.py index 083fd1e97..62d27cf00 100644 --- a/lego/apps/events/tests/test_penalties.py +++ b/lego/apps/events/tests/test_penalties.py @@ -1,4 +1,5 @@ from datetime import timedelta +from django.conf import settings from django.utils import timezone @@ -42,7 +43,7 @@ def test_get_earliest_registration_time_ignore_penalties(self): ) self.assertEqual(earliest_reg, current_time) - def test_get_earliest_registration_time_one_penalty(self): + def test_get_earliest_registration_time_one_or_more_penalty(self): """Test method calculating the earliest registration time for user with one or more penalties""" event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") @@ -57,6 +58,15 @@ def test_get_earliest_registration_time_one_penalty(self): Penalty.objects.create( user=user, reason="first test penalty", weight=1, source_event=event ) + penalties = user.number_of_penalties() + + earliest_reg = event.get_earliest_registration_time( + user, [webkom_pool], penalties + ) + self.assertEqual( + earliest_reg, + current_time + timedelta(hours=settings.PENALTY_DELAY_DURATION), + ) Penalty.objects.create( user=user, reason="second test penalty", weight=2, source_event=event ) @@ -65,9 +75,12 @@ def test_get_earliest_registration_time_one_penalty(self): earliest_reg = event.get_earliest_registration_time( user, [webkom_pool], penalties ) - self.assertEqual(earliest_reg, current_time + timedelta(hours=5)) + self.assertEqual( + earliest_reg, + current_time + timedelta(hours=settings.PENALTY_DELAY_DURATION), + ) - def test_cant_register_with_one_penalty_before_delay(self): + def test_cant_register_with_one_or_more_penalty_before_delay(self): """Test that user can not register before (5 hour) delay when having one or more penalties""" event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") @@ -90,14 +103,16 @@ def test_cant_register_with_one_penalty_before_delay(self): registration = Registration.objects.get_or_create(event=event, user=user)[0] event.register(registration) - def test_can_register_with_one_penalty_after_delay(self): + def test_can_register_with_one_or_more_penalty_after_delay(self): """Test that user can register after (5 hour) delay has passed having one or more penalties""" event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") current_time = timezone.now() abakus_pool = event.pools.get(name="Abakusmember") - abakus_pool.activation_date = current_time - timedelta(hours=5) + abakus_pool.activation_date = current_time - timedelta( + hours=settings.PENALTY_DELAY_DURATION + ) abakus_pool.save() user = get_dummy_users(1)[0] diff --git a/lego/apps/users/managers.py b/lego/apps/users/managers.py index f293d094b..12ee040bf 100644 --- a/lego/apps/users/managers.py +++ b/lego/apps/users/managers.py @@ -1,6 +1,8 @@ from django.contrib.auth.models import UserManager -from django.db.models import Q +from django.db.models import Q, F, DateTimeField, ExpressionWrapper from django.utils import timezone +from django.conf import settings + from mptt.managers import TreeManager diff --git a/lego/apps/users/migrations/0039_penalty_activation_time.py b/lego/apps/users/migrations/0039_penalty_activation_time.py index 6cb29cf88..21b099f8a 100644 --- a/lego/apps/users/migrations/0039_penalty_activation_time.py +++ b/lego/apps/users/migrations/0039_penalty_activation_time.py @@ -8,6 +8,12 @@ class Migration(migrations.Migration): ("users", "0038_alter_abakusgroup_type"), ] + def update_current_ative_penalties(apps, schema_editor): + from django.db.models import F + + Penalty = apps.get_model("users", "Penalty") + Penalty.objects.all().update(activation_time=F("created_at")) + operations = [ migrations.AddField( model_name="penalty", @@ -16,4 +22,5 @@ class Migration(migrations.Migration): default=None, null=True, verbose_name="date created" ), ), + migrations.RunPython(update_current_ative_penalties), ] diff --git a/lego/apps/users/models.py b/lego/apps/users/models.py index 466081eb2..ef14d5981 100644 --- a/lego/apps/users/models.py +++ b/lego/apps/users/models.py @@ -487,29 +487,39 @@ def unanswered_surveys(self) -> list: ) return list(unanswered_surveys.values_list("id", flat=True)) - def check_for_deletable_penalty(self): - from lego.apps.events.models import Event + def check_for_expirable_penalty(self): + from lego.apps.events.models import Event, Pool + from django.db.models import Subquery - # TODO: filter so that only one penalty is returned. the penalty now() is at current_active_penalty = ( - Penalty.objects.valid().filter(user=self).order_by("activation_time") + Penalty.objects.valid() + .filter( + user=self, + activation_time__lte=timezone.now(), + activation_time__gte=timezone.now() + - F("weight") * timedelta(days=settings.PENALTY_DURATION.days), + ) + .first() ) + if current_active_penalty is None: + return + + # TODO: add test for this + pools = Pool.objects.filter(permission_groups__in=self.all_groups).distinct() - # TODO: add check for all he events the user can be registered at - # get a list of all penalties and in the Event.filter() - # and check that the event is in the penalties events - eligable_passed_events = Event.objects.filter( + number_of_eligable_passed_events = Event.objects.filter( heed_penalties=True, start_time__range=( - current_active_penalty[0].activation_time + current_active_penalty.activation_time - timedelta(hours=1) * F("unregistration_deadline_hours"), - current_active_penalty[0].exact_expiration + current_active_penalty.exact_expiration - timedelta(hours=1) * F("unregistration_deadline_hours"), ), - ) + pools__in=Subquery(pools.values("pk")), + ).count() - if len(eligable_passed_events) >= 6: - current_active_penalty[0].delete_and_adjust_future_activation_times() + if number_of_eligable_passed_events >= 6: + current_active_penalty.expire_and_adjust_future_activation_times() class Penalty(BasisModel): @@ -519,51 +529,48 @@ class Penalty(BasisModel): source_event = models.ForeignKey( "events.Event", related_name="penalties", on_delete=models.CASCADE ) - activation_time = models.DateTimeField("date created", null=True, default=None) + activation_time = models.DateTimeField(null=True, default=None) + # add weightZ objects = UserPenaltyManager() # type: ignore - def save(self, *args, **kwargs): - last_penalty_to_be_expired = Penalty.objects.filter( - user=self.user, activation_time__lte=timezone.now() - ).order_by("-activation_time") + def save(self, manual_activation_time=False, *args, **kwargs): + if not manual_activation_time: + current_and_future_penalties = Penalty.objects.filter( + user=self.user, + activation_time__gte=timezone.now() + - F("weight") * settings.PENALTY_DURATION.days * timedelta(days=1), + ).order_by("-activation_time") + + self.activation_time = ( + current_and_future_penalties.first().exact_expiration + if current_and_future_penalties.exists() + else self.created_at + ) - self.activation_time = ( - last_penalty_to_be_expired[0].exact_expiration - if len(last_penalty_to_be_expired) != 0 - else self.created_at - ) super().save(*args, **kwargs) - def default_save(self, *args, **kwargs): - super().save(*args, **kwargs) + def expire_and_adjust_future_activation_times(self): + new_activation_time = timezone.now() + future_penalties = Penalty.objects.filter( + user=self.user, activation_time__gt=new_activation_time + ).order_by("activation_time") - def delete_and_adjust_future_activation_times(self): if self.weight == 1: - new_activation_time = timezone.now() - future_penalties = Penalty.objects.filter( - user=self.user, activation_time__gt=new_activation_time - ).order_by("activation_time") + self.activation_time = timezone.now() - timedelta(days=30) + self.created_at = timezone.now() - timedelta(days=30) - for penalty in future_penalties: - penalty.activation_time = new_activation_time - penalty.default_save() - new_activation_time = penalty.exact_expiration - - self.delete() else: self.weight -= 1 self.activation_time = timezone.now() - future_penalties = Penalty.objects.filter( - user=self.user, activation_time__gt=self.activation_time - ).order_by("activation_time") new_activation_time = self.exact_expiration - self.default_save() - for penalty in future_penalties: - penalty.activation_time = new_activation_time - penalty.default_save() - new_activation_time = penalty.exact_expiration + self.save(manual_activation_time=True) + + for penalty in future_penalties: + penalty.activation_time = new_activation_time + penalty.save(manual_activation_time=True) + new_activation_time = penalty.exact_expiration @property def exact_expiration(self): @@ -603,6 +610,11 @@ def ignore_date(date): return True +# Generated by Django 4.0.10 on 2023-05-01 16:33 + +from django.db import models + + class PhotoConsent(BasisModel): user = models.ForeignKey( User, related_name="photo_consents", on_delete=models.CASCADE diff --git a/lego/apps/users/tasks.py b/lego/apps/users/tasks.py index 94877fb64..17130c354 100644 --- a/lego/apps/users/tasks.py +++ b/lego/apps/users/tasks.py @@ -78,3 +78,9 @@ def send_inactive_reminder_mail_and_delete_users(self, logger_context=None): for user in users_to_notifiy_montly: send_inactive_notification(user) + + +@celery_app.task(serializer="json", bind=True, base=AbakusTask) +def expire_penalties_if_six_events_has_passed(self, logger_context=None): + me = "happy" + # go through all users with penalties and run the expire penalty function on them diff --git a/lego/apps/users/tests/test_models.py b/lego/apps/users/tests/test_models.py index d8f95229b..8b567e044 100644 --- a/lego/apps/users/tests/test_models.py +++ b/lego/apps/users/tests/test_models.py @@ -3,9 +3,10 @@ from django.test import override_settings from django.utils import timezone from django.utils.timezone import timedelta +from django.db.models import F, Q from lego import settings -from lego.apps.events.models import Event +from lego.apps.events.models import Event, Pool from lego.apps.files.models import File from lego.apps.users import constants from lego.apps.users.constants import ( @@ -289,28 +290,46 @@ def test_count_weights(self): self.assertEqual(self.test_user.number_of_penalties(), sum(weights)) - @mock.patch("django.utils.timezone.now", return_value=fake_time(2016, 10, 1)) + @mock.patch("django.utils.timezone.now", return_value=fake_time(2016, 10, 10)) def test_only_count_active_penalties(self, mock_now): Penalty.objects.create( - created_at=mock_now() - timedelta(days=10), + created_at=mock_now() - timedelta(days=21), user=self.test_user, reason="test", weight=1, source_event=self.source, ) Penalty.objects.create( - created_at=mock_now() - timedelta(days=9, hours=23, minutes=59), + created_at=mock_now() - timedelta(days=9), + user=self.test_user, + reason="test", + weight=1, + source_event=self.source, + ) + Penalty.objects.create( + created_at=mock_now() - timedelta(days=5), + user=self.test_user, + reason="test", + weight=1, + source_event=self.source, + ) + Penalty.objects.create( + created_at=mock_now(), user=self.test_user, reason="test", weight=1, source_event=self.source, ) - self.assertEqual(self.test_user.number_of_penalties(), 1) + + self.assertEqual(self.test_user.number_of_penalties(), 3) @mock.patch("django.utils.timezone.now", return_value=fake_time(2016, 10, 10)) def test_penalty_deletion_after_6_events(self, mock_now): """Tests that The first active penalty is removed and the rest are adjusted after 6 events""" + + self.test_user.check_for_expirable_penalty() + Penalty.objects.create( created_at=mock_now() - timedelta(days=8), user=self.test_user, @@ -333,27 +352,29 @@ def test_penalty_deletion_after_6_events(self, mock_now): source_event=self.source, ) - for _i in range(5): - Event.objects.create( + webkom_group = AbakusGroup.objects.get(name="Webkom") + webkom_group.add_user(self.test_user) + webkom_group.save() + self.test_user.save() + + for _i in range(6): + event = Event.objects.create( title="AbakomEvent", event_type=0, start_time=mock_now() - timedelta(days=6), end_time=mock_now() - timedelta(days=6), ) + pool = Pool.objects.create( + name="Pool1", + event=event, + capacity=0, + activation_date=timezone.now() - timedelta(days=1), + ) - self.test_user.check_for_deletable_penalty() + pool.permission_groups.set([webkom_group]) - """Tests first that nothing is changed after 5 events""" - self.assertEqual(self.test_user.number_of_penalties(), 3) - - Event.objects.create( - title="AbakomEvent", - event_type=0, - start_time=mock_now() - timedelta(days=6), - end_time=mock_now() - timedelta(days=6), - ) - self.test_user.check_for_deletable_penalty() - """Tests that the changes happened after 6 events""" + self.test_user.check_for_expirable_penalty() + # Tests that the changes happened after 6 events self.assertEqual(self.test_user.number_of_penalties(), 2) self.assertEqual( ( @@ -383,15 +404,28 @@ def test_penalty_weight_decrementing_after_6_events(self, mock_now): source_event=self.source, ) + webkom_group = AbakusGroup.objects.get(name="Webkom") + webkom_group.add_user(self.test_user) + webkom_group.save() + self.test_user.save() + for _i in range(5): - Event.objects.create( + event = Event.objects.create( title="AbakomEvent", event_type=0, start_time=mock_now() - timedelta(days=6), end_time=mock_now() - timedelta(days=6), ) + pool = Pool.objects.create( + name="Pool1", + event=event, + capacity=0, + activation_date=timezone.now() - timedelta(days=1), + ) - self.test_user.check_for_deletable_penalty() + pool.permission_groups.set([webkom_group]) + + self.test_user.check_for_expirable_penalty() """Tests first that nothing is changed after 5 events""" self.assertEqual(self.test_user.number_of_penalties(), 3) @@ -403,13 +437,22 @@ def test_penalty_weight_decrementing_after_6_events(self, mock_now): (22, 1), ) - Event.objects.create( + event = Event.objects.create( title="AbakomEvent", event_type=0, start_time=mock_now() - timedelta(days=6), end_time=mock_now() - timedelta(days=6), ) - self.test_user.check_for_deletable_penalty() + pool = Pool.objects.create( + name="Pool1", + event=event, + capacity=0, + activation_date=timezone.now() - timedelta(days=1), + ) + + pool.permission_groups.set([webkom_group]) + + self.test_user.check_for_expirable_penalty() """Tests that the changes happened after 6 events""" self.assertEqual(self.test_user.number_of_penalties(), 2) self.assertEqual( @@ -490,18 +533,16 @@ def test_penalty_offset_is_calculated_correctly(self, mock_now): (12, inactive.created_at.day + settings.PENALTY_DURATION.days), ) - # This penalty is set to expire the same day as the freeze active = Penalty.objects.create( - created_at=mock_now().replace(day=5), + created_at=mock_now().replace(day=15), user=self.test_user, reason="active", weight=1, source_event=self.source, ) - # 19 = 11: end of holiday + 10: penalty time - 2: from activation time to start of holiday self.assertEqual( (active.exact_expiration.month, active.exact_expiration.day), - (1, 19), + (1, 14), ) diff --git a/lego/settings/celery.py b/lego/settings/celery.py index 0d98bea98..6738df733 100644 --- a/lego/settings/celery.py +++ b/lego/settings/celery.py @@ -80,6 +80,10 @@ def on_setup_logging(**kwargs): "task": "lego.apps.users.tasks.send_inactive_reminder_mail_and_delete_users", "schedule": crontab(hour=7, minute=0, day_of_week=1), }, + "expire_penalties_if_possible": { + "task": "lego.apps.users.tasks.expire_penalties_if_six_events_has_passed", + "schedule": crontab(hour=2, minute=0), + }, "notify_user_of_unanswered_meeting_invitation": { "task": "lego.apps.meetings.tasks.notify_user_of_unanswered_meeting_invitation", "schedule": crontab(hour=10, minute=0), diff --git a/lego/settings/lego.py b/lego/settings/lego.py index 851830566..18ff0defc 100644 --- a/lego/settings/lego.py +++ b/lego/settings/lego.py @@ -18,6 +18,7 @@ MANAGERS = ADMINS PENALTY_DURATION = timedelta(days=10) +PENALTY_DELAY_DURATION = 3 # Tuples for ignored (month, day) intervals PENALTY_IGNORE_SUMMER = ((6, 1), (8, 15)) PENALTY_IGNORE_WINTER = ((12, 1), (1, 10)) From d531414e910bb5f765a7c5aadffcc5ea2f34ccfc Mon Sep 17 00:00:00 2001 From: Arash Farzaneh Taleghani Date: Mon, 8 May 2023 20:16:47 +0200 Subject: [PATCH 4/4] Implement penalty group --- lego/api/v1.py | 4 +- lego/apps/events/models.py | 18 +- lego/apps/events/tests/test_async_tasks.py | 28 +- lego/apps/events/tests/test_events_api.py | 8 +- lego/apps/events/tests/test_penalties.py | 30 +- lego/apps/events/tests/utils.py | 7 +- lego/apps/users/action_handlers.py | 25 +- lego/apps/users/filters.py | 11 +- lego/apps/users/managers.py | 77 ++++- .../0039_penalty_activation_time.py | 26 -- ...on_remove_penalty_source_event_and_more.py | 144 +++++++++ lego/apps/users/models.py | 149 +++++---- lego/apps/users/notifications.py | 4 +- lego/apps/users/serializers/penalties.py | 6 +- lego/apps/users/serializers/users.py | 18 +- lego/apps/users/tasks.py | 12 +- lego/apps/users/tests/test_models.py | 302 +++++++++++++----- lego/apps/users/tests/test_users_api.py | 12 +- lego/apps/users/views/penalties.py | 14 +- 19 files changed, 648 insertions(+), 247 deletions(-) delete mode 100644 lego/apps/users/migrations/0039_penalty_activation_time.py create mode 100644 lego/apps/users/migrations/0039_remove_penalty_reason_remove_penalty_source_event_and_more.py diff --git a/lego/api/v1.py b/lego/api/v1.py index f6edd1bf0..6c92999ac 100644 --- a/lego/api/v1.py +++ b/lego/api/v1.py @@ -70,7 +70,7 @@ PasswordResetPerformViewSet, PasswordResetRequestViewSet, ) -from lego.apps.users.views.penalties import PenaltyViewSet +from lego.apps.users.views.penalties import PenaltyGroupViewSet from lego.apps.users.views.registration import UserRegistrationRequestViewSet from lego.apps.users.views.student_confirmation import ( StudentConfirmationPerformViewSet, @@ -179,7 +179,7 @@ PasswordResetRequestViewSet, basename="password-reset-request", ) -router.register(r"penalties", PenaltyViewSet) +router.register(r"penalties", PenaltyGroupViewSet) router.register(r"podcasts", PodcastViewSet, basename="podcasts") router.register(r"polls", PollViewSet, basename="polls") router.register(r"quotes", QuoteViewSet) diff --git a/lego/apps/events/models.py b/lego/apps/events/models.py index e42236d2d..d9a50886e 100644 --- a/lego/apps/events/models.py +++ b/lego/apps/events/models.py @@ -32,7 +32,7 @@ from lego.apps.followers.models import FollowEvent from lego.apps.permissions.models import ObjectPermissionsModel from lego.apps.users.constants import AUTUMN, SPRING -from lego.apps.users.models import AbakusGroup, Penalty, User +from lego.apps.users.models import AbakusGroup, PenaltyGroup, User from lego.utils.models import BasisModel from lego.utils.youtube_validator import youtube_validator @@ -389,8 +389,10 @@ def unregister( and self.heed_penalties and self.passed_unregistration_deadline ): - if not registration.user.penalties.filter(source_event=self).exists(): - Penalty.objects.create( + if not registration.user.penalty_groups.filter( + source_event=self + ).exists(): + PenaltyGroup.objects.create( user=registration.user, reason=f"Meldte seg av {self.title} for sent.", weight=1, @@ -910,16 +912,18 @@ def handle_user_penalty(self, presence: constants.PRESENCE_CHOICES) -> None: and presence == constants.PRESENCE_CHOICES.NOT_PRESENT and self.event.penalty_weight_on_not_present ): - if not self.user.penalties.filter(source_event=self.event).exists(): - Penalty.objects.create( + if not self.user.penalty_groups.filter(source_event=self.event).exists(): + PenaltyGroup.objects.create( user=self.user, reason=f"Møtte ikke opp på {self.event.title}.", weight=self.event.penalty_weight_on_not_present, source_event=self.event, ) else: - for penalty in self.user.penalties.filter(source_event=self.event): - penalty.delete() + for penalty_group in self.user.penalty_groups.filter( + source_event=self.event + ): + penalty_group.delete() def add_to_pool(self, pool: Pool) -> Registration: allowed: bool = False diff --git a/lego/apps/events/tests/test_async_tasks.py b/lego/apps/events/tests/test_async_tasks.py index a36d4291b..7c8efc167 100644 --- a/lego/apps/events/tests/test_async_tasks.py +++ b/lego/apps/events/tests/test_async_tasks.py @@ -19,8 +19,8 @@ notify_user_when_payment_soon_overdue, set_all_events_ready_and_bump, ) -from lego.apps.events.tests.utils import get_dummy_users, make_penalty_expire -from lego.apps.users.models import AbakusGroup, Penalty +from lego.apps.events.tests.utils import get_dummy_users, make_penalty_group_expire +from lego.apps.users.models import AbakusGroup, PenaltyGroup from lego.utils.test_utils import BaseAPITestCase, BaseTestCase @@ -350,7 +350,7 @@ def test_is_automatically_bumped_after_penalty_expiration(self): user = get_dummy_users(1)[0] AbakusGroup.objects.get(name="Abakus").add_user(user) - p1 = Penalty.objects.create( + penalty_group = PenaltyGroup.objects.create( user=user, reason="test", weight=3, source_event=self.event ) @@ -359,7 +359,7 @@ def test_is_automatically_bumped_after_penalty_expiration(self): ] async_register(registration.id) - make_penalty_expire(p1) + make_penalty_group_expire(penalty_group) check_events_for_registrations_with_expired_penalties.delay() self.assertIsNotNone(Registration.objects.get(id=registration.id).pool) @@ -371,10 +371,10 @@ def test_is_bumped_with_multiple_penalties(self): user = get_dummy_users(1)[0] AbakusGroup.objects.get(name="Abakus").add_user(user) - p1 = Penalty.objects.create( + p1 = PenaltyGroup.objects.create( user=user, reason="test", weight=2, source_event=self.event ) - Penalty.objects.create( + PenaltyGroup.objects.create( user=user, reason="test2", weight=2, source_event=self.event ) @@ -383,7 +383,7 @@ def test_is_bumped_with_multiple_penalties(self): ] async_register(registration.id) - make_penalty_expire(p1) + make_penalty_group_expire(p1) check_events_for_registrations_with_expired_penalties.delay() self.assertIsNotNone(Registration.objects.get(id=registration.id).pool) @@ -396,7 +396,7 @@ def test_isnt_bumped_when_full(self): for user in users: AbakusGroup.objects.get(name="Abakus").add_user(user) - p1 = Penalty.objects.create( + p1 = PenaltyGroup.objects.create( user=users[1], reason="test", weight=3, source_event=self.event ) @@ -406,7 +406,7 @@ def test_isnt_bumped_when_full(self): )[0] async_register(registration.id) - make_penalty_expire(p1) + make_penalty_group_expire(p1) check_events_for_registrations_with_expired_penalties.delay() self.assertIsNone(Registration.objects.get(user=users[1]).pool) @@ -422,7 +422,7 @@ def test_isnt_bumped_when_not_first_in_line(self): for user in users: AbakusGroup.objects.get(name="Abakus").add_user(user) - p1 = Penalty.objects.create( + p1 = PenaltyGroup.objects.create( user=users[2], reason="test", weight=3, source_event=self.event ) @@ -432,7 +432,7 @@ def test_isnt_bumped_when_not_first_in_line(self): )[0] async_register(registration.id) - make_penalty_expire(p1) + make_penalty_group_expire(p1) check_events_for_registrations_with_expired_penalties.delay() self.assertIsNone(Registration.objects.get(user=users[1]).pool) @@ -448,7 +448,7 @@ def test_async_bump_post_merge(self): for user in users: AbakusGroup.objects.get(name="Abakus").add_user(user) - p1 = Penalty.objects.create( + penalty_group = PenaltyGroup.objects.create( user=users[1], reason="test", weight=3, source_event=self.event ) @@ -458,7 +458,7 @@ def test_async_bump_post_merge(self): )[0] async_register(registration.id) - make_penalty_expire(p1) + make_penalty_group_expire(penalty_group) check_events_for_registrations_with_expired_penalties.delay() self.assertIsNotNone(Registration.objects.get(user=users[1]).pool) @@ -526,7 +526,7 @@ def test_initiate_payment_in_waiting_list(self, mock_initiate_payment): user = get_dummy_users(1)[0] AbakusGroup.objects.get(name="Abakus").add_user(user) - Penalty.objects.create( + PenaltyGroup.objects.create( user=user, reason="test", weight=3, source_event=self.event ) diff --git a/lego/apps/events/tests/test_events_api.py b/lego/apps/events/tests/test_events_api.py index c25348afc..9606a134b 100644 --- a/lego/apps/events/tests/test_events_api.py +++ b/lego/apps/events/tests/test_events_api.py @@ -18,11 +18,11 @@ check_events_for_registrations_with_expired_penalties, stripe_webhook_event, ) -from lego.apps.events.tests.utils import get_dummy_users, make_penalty_expire +from lego.apps.events.tests.utils import get_dummy_users, make_penalty_group_expire from lego.apps.followers.models import FollowEvent from lego.apps.surveys.models import Submission, Survey from lego.apps.users.constants import GROUP_GRADE, PHOTO_CONSENT_DOMAINS -from lego.apps.users.models import AbakusGroup, Penalty, PhotoConsent, User +from lego.apps.users.models import AbakusGroup, PenaltyGroup, PhotoConsent, User from lego.utils.test_utils import BaseAPITestCase, BaseAPITransactionTestCase _test_event_data = [ @@ -1727,7 +1727,7 @@ def test_create_payment_intent_when_bump_from_waitlist(self, mock_notify): self.client.force_authenticate(self.abakus_user_4) - p1 = Penalty.objects.create( + p1 = PenaltyGroup.objects.create( user=self.abakus_user_4, reason="test", weight=3, source_event=self.event ) @@ -1742,7 +1742,7 @@ def test_create_payment_intent_when_bump_from_waitlist(self, mock_notify): res = self.get_payment_intent() self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN) - make_penalty_expire(p1) + make_penalty_group_expire(p1) check_events_for_registrations_with_expired_penalties.delay() res = self.get_payment_intent() diff --git a/lego/apps/events/tests/test_penalties.py b/lego/apps/events/tests/test_penalties.py index 62d27cf00..8de4653c0 100644 --- a/lego/apps/events/tests/test_penalties.py +++ b/lego/apps/events/tests/test_penalties.py @@ -1,11 +1,11 @@ from datetime import timedelta -from django.conf import settings +from django.conf import settings from django.utils import timezone from lego.apps.events import constants from lego.apps.events.models import Event, Registration -from lego.apps.users.models import AbakusGroup, Penalty +from lego.apps.users.models import AbakusGroup, PenaltyGroup from lego.utils.test_utils import BaseTestCase from .utils import get_dummy_users @@ -35,7 +35,9 @@ def test_get_earliest_registration_time_ignore_penalties(self): user = get_dummy_users(1)[0] AbakusGroup.objects.get(name="Webkom").add_user(user) - Penalty.objects.create(user=user, reason="test", weight=1, source_event=event) + PenaltyGroup.objects.create( + user=user, reason="test", weight=1, source_event=event + ) penalties = user.number_of_penalties() earliest_reg = event.get_earliest_registration_time( @@ -55,7 +57,7 @@ def test_get_earliest_registration_time_one_or_more_penalty(self): user = get_dummy_users(1)[0] AbakusGroup.objects.get(name="Webkom").add_user(user) - Penalty.objects.create( + PenaltyGroup.objects.create( user=user, reason="first test penalty", weight=1, source_event=event ) penalties = user.number_of_penalties() @@ -67,7 +69,7 @@ def test_get_earliest_registration_time_one_or_more_penalty(self): earliest_reg, current_time + timedelta(hours=settings.PENALTY_DELAY_DURATION), ) - Penalty.objects.create( + PenaltyGroup.objects.create( user=user, reason="second test penalty", weight=2, source_event=event ) penalties = user.number_of_penalties() @@ -92,10 +94,10 @@ def test_cant_register_with_one_or_more_penalty_before_delay(self): user = get_dummy_users(1)[0] AbakusGroup.objects.get(name="Abakus").add_user(user) - Penalty.objects.create( + PenaltyGroup.objects.create( user=user, reason="first test penalty", weight=1, source_event=event ) - Penalty.objects.create( + PenaltyGroup.objects.create( user=user, reason="second test penalty", weight=2, source_event=event ) @@ -117,10 +119,10 @@ def test_can_register_with_one_or_more_penalty_after_delay(self): user = get_dummy_users(1)[0] AbakusGroup.objects.get(name="Abakus").add_user(user) - Penalty.objects.create( + PenaltyGroup.objects.create( user=user, reason="first test penalty", weight=1, source_event=event ) - Penalty.objects.create( + PenaltyGroup.objects.create( user=user, reason="second test penalty", weight=2, source_event=event ) @@ -193,20 +195,20 @@ def test_only_correct_penalties_are_removed_on_presence_change(self): registration.set_presence(constants.PRESENCE_CHOICES.NOT_PRESENT) penalties_before = registration.user.number_of_penalties() - penalties_object_before = list(registration.user.penalties.all()) + penalties_object_before = list(registration.user.penalty_groups.all()) - Penalty.objects.create( + PenaltyGroup.objects.create( user=registration.user, reason="OTHER EVENT", weight=2, source_event=other_event, ) penalties_during = registration.user.number_of_penalties() - penalties_objects_during = list(registration.user.penalties.all()) + penalties_objects_during = list(registration.user.penalty_groups.all()) registration.set_presence(constants.PRESENCE_CHOICES.UNKNOWN) penalties_after = registration.user.number_of_penalties() - penalties_object_after = list(registration.user.penalties.all()) + penalties_object_after = list(registration.user.penalty_groups.all()) self.assertEqual(penalties_object_before[0].source_event, event) self.assertEqual(penalties_object_after[0].source_event, other_event) @@ -229,7 +231,7 @@ def test_able_to_register_when_not_heed_penalties_with_penalties(self): event.save() user = get_dummy_users(1)[0] AbakusGroup.objects.get(name="Webkom").add_user(user) - Penalty.objects.create( + PenaltyGroup.objects.create( user=user, reason="TEST", weight=3, source_event=other_event ) diff --git a/lego/apps/events/tests/utils.py b/lego/apps/events/tests/utils.py index f1f8afb23..39db1a3fe 100644 --- a/lego/apps/events/tests/utils.py +++ b/lego/apps/events/tests/utils.py @@ -4,7 +4,7 @@ import stripe -from lego.apps.users.models import AbakusGroup, User +from lego.apps.users.models import AbakusGroup, PenaltyGroup, User def get_dummy_users(n): @@ -33,6 +33,5 @@ def create_token(number, cvc, year=None): ) -def make_penalty_expire(penalty): - penalty.created_at = timezone.now() - timedelta(days=365) - penalty.save() +def make_penalty_group_expire(penalty_group: PenaltyGroup) -> None: + penalty_group.delete() diff --git a/lego/apps/users/action_handlers.py b/lego/apps/users/action_handlers.py index ddcbc5b9d..a241b117b 100644 --- a/lego/apps/users/action_handlers.py +++ b/lego/apps/users/action_handlers.py @@ -1,3 +1,5 @@ +from django.utils import timezone + from lego.apps.action_handlers.handler import Handler from lego.apps.action_handlers.registry import register_handler from lego.apps.feeds.activity import Activity @@ -5,7 +7,7 @@ from lego.apps.feeds.models import NotificationFeed, PersonalFeed, UserFeed from lego.apps.feeds.verbs import GroupJoinVerb, PenaltyVerb from lego.apps.users.constants import PUBLIC_GROUPS -from lego.apps.users.models import Membership, Penalty +from lego.apps.users.models import Membership, PenaltyGroup from lego.apps.users.notifications import PenaltyNotification @@ -43,20 +45,19 @@ def get_activity(self, membership): class PenaltyHandler(Handler): - model = Penalty + model = PenaltyGroup manager = feed_manager - def get_activity(self, penalty): + def get_activity(self, penalty_group): return Activity( - actor=penalty.source_event, + actor=penalty_group.source_event, verb=PenaltyVerb, - object=penalty, - target=penalty.user, - time=penalty.created_at, + object=penalty_group, + target=penalty_group.user, + time=penalty_group.created_at, extra_context={ - "reason": penalty.reason, - "weight": penalty.weight, - "expiration_date": penalty.exact_expiration.days, + "reason": penalty_group.reason, + "weight": penalty_group.weight, }, ) @@ -64,10 +65,6 @@ def handle_create(self, instance, **kwargs): activity = self.get_activity(instance) self.manager.add_activity(activity, [instance.user.pk], [NotificationFeed]) - # Send Notification - notification = PenaltyNotification(instance.user, penalty=instance) - notification.notify() - def handle_update(self, instance, **kwargs): activity = self.get_activity(instance) self.manager.add_activity(activity, [instance.user.pk], [NotificationFeed]) diff --git a/lego/apps/users/filters.py b/lego/apps/users/filters.py index 052e3e6b3..9e67c8779 100644 --- a/lego/apps/users/filters.py +++ b/lego/apps/users/filters.py @@ -2,7 +2,12 @@ from django.db.models.functions import Concat from django_filters.rest_framework import CharFilter, FilterSet -from lego.apps.users.models import AbakusGroup, Membership, MembershipHistory, Penalty +from lego.apps.users.models import ( + AbakusGroup, + Membership, + MembershipHistory, + PenaltyGroup, +) class MembershipFilterSet(FilterSet): @@ -43,7 +48,7 @@ class Meta: fields = ("user", "abakus_group", "role") -class PenaltyFilterSet(FilterSet): +class PenaltyGroupFilterSet(FilterSet): class Meta: - model = Penalty + model = PenaltyGroup fields = ("user", "source_event") diff --git a/lego/apps/users/managers.py b/lego/apps/users/managers.py index 12ee040bf..958eab339 100644 --- a/lego/apps/users/managers.py +++ b/lego/apps/users/managers.py @@ -1,8 +1,17 @@ +from __future__ import annotations + +from datetime import timedelta +from typing import TYPE_CHECKING + from django.contrib.auth.models import UserManager -from django.db.models import Q, F, DateTimeField, ExpressionWrapper -from django.utils import timezone -from django.conf import settings +from django.db import models +from django.db.models import Q, QuerySet +if TYPE_CHECKING: + from lego.apps.users.models import Penalty, PenaltyGroup + +from django.conf import settings +from django.utils import timezone from mptt.managers import TreeManager @@ -48,9 +57,65 @@ def get_by_natural_key(self, username, abakus_group_name): ) -class UserPenaltyManager(PersistentModelManager): - def valid(self): +class UserPenaltyManager(PersistentModelManager["Penalty"]): + def valid(self) -> QuerySet["Penalty"]: from lego.apps.users.models import Penalty offset = Penalty.penalty_offset(timezone.now(), False) - return super().filter(created_at__gt=timezone.now() - offset) + return super().filter(activation_time__gt=timezone.now() - offset) + + +class UserPenaltyGroupManager(PersistentModelManager["PenaltyGroup"]): + def create(self, *args, **kwargs) -> PenaltyGroup: + from lego.apps.users.models import Penalty + from lego.apps.users.notifications import PenaltyNotification + + penalty_group = super().create(*args, **kwargs) + + last_active_penalty = ( + Penalty.objects.filter( + penalty_group__user=penalty_group.user, + ) + .order_by("-activation_time") + .first() + ) + + new_activation_time = ( + last_active_penalty.exact_expiration + if last_active_penalty + else penalty_group.created_at + ) + + penalties = [] + + for _ in range(kwargs["weight"]): + penalty = Penalty(penalty_group=penalty_group) + penalty.activation_time = new_activation_time + + new_activation_time = penalty.exact_expiration + + penalties.append(penalty) + + Penalty.objects.bulk_create(penalties) + + # Send Notification + notification = PenaltyNotification(penalty_group.user, penalty=penalty_group) + notification.notify() + + return penalty_group + + def valid(self) -> QuerySet["PenaltyGroup"]: + from django.db.models import F + + from lego.apps.users.models import PenaltyGroup + + filtered_result = PenaltyGroup.objects.filter( + id__in=[ + pg.id + for pg in super().annotate(penalty_count=models.Count(F("penalties"))) + if pg.activation_time + > timezone.now() - timedelta(days=settings.PENALTY_DURATION.days) + ] + ) + + return filtered_result diff --git a/lego/apps/users/migrations/0039_penalty_activation_time.py b/lego/apps/users/migrations/0039_penalty_activation_time.py deleted file mode 100644 index 21b099f8a..000000000 --- a/lego/apps/users/migrations/0039_penalty_activation_time.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 4.0.10 on 2023-05-01 16:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("users", "0038_alter_abakusgroup_type"), - ] - - def update_current_ative_penalties(apps, schema_editor): - from django.db.models import F - - Penalty = apps.get_model("users", "Penalty") - Penalty.objects.all().update(activation_time=F("created_at")) - - operations = [ - migrations.AddField( - model_name="penalty", - name="activation_time", - field=models.DateTimeField( - default=None, null=True, verbose_name="date created" - ), - ), - migrations.RunPython(update_current_ative_penalties), - ] diff --git a/lego/apps/users/migrations/0039_remove_penalty_reason_remove_penalty_source_event_and_more.py b/lego/apps/users/migrations/0039_remove_penalty_reason_remove_penalty_source_event_and_more.py new file mode 100644 index 000000000..216477cf8 --- /dev/null +++ b/lego/apps/users/migrations/0039_remove_penalty_reason_remove_penalty_source_event_and_more.py @@ -0,0 +1,144 @@ +# Generated by Django 4.0.10 on 2023-09-28 13:55 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("events", "0036_alter_registration_presence"), + ("users", "0038_alter_abakusgroup_type"), + ] + + def generate_new_penalties(apps, schema_editor): + from django.utils import timezone + from django.utils.timezone import timedelta + + PenaltyGroup = apps.get_model("users", "PenaltyGroup") + Penalty = apps.get_model("users", "Penalty") + Event = apps.get_model("events", "Event") + + users_with_penalties = Penalty.objects.filter( + created_at__gt=timezone.now() - timedelta(days=10) + ).values_list("user", flat=True) + + Penalty.objects.all().delete() + + for user in users_with_penalties: + dummy_event = Event.objects.all()[0] + PenaltyGroup.objects.create( + user=user, + reason="Dette er en sammensetting av forrige prikkene", + source_event=dummy_event, + ) + + operations = [ + migrations.CreateModel( + name="PenaltyGroup", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created_at", + models.DateTimeField( + db_index=True, default=django.utils.timezone.now, editable=False + ), + ), + ( + "updated_at", + models.DateTimeField( + default=django.utils.timezone.now, editable=False + ), + ), + ( + "deleted", + models.BooleanField(db_index=True, default=False, editable=False), + ), + ("reason", models.CharField(max_length=1000)), + ("weight", models.IntegerField(default=1)), + ( + "created_by", + models.ForeignKey( + default=None, + editable=False, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "source_event", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="penalty_groups", + to="events.event", + ), + ), + ( + "updated_by", + models.ForeignKey( + default=None, + editable=False, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="penalty_groups", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + "default_manager_name": "objects", + }, + ), + migrations.RemoveField( + model_name="penalty", + name="reason", + ), + migrations.RemoveField( + model_name="penalty", + name="source_event", + ), + migrations.RemoveField( + model_name="penalty", + name="weight", + ), + migrations.AddField( + model_name="penalty", + name="activation_time", + field=models.DateTimeField(default=None, null=True), + ), + migrations.AddField( + model_name="penalty", + name="penalty_group", + field=models.ForeignKey( + default=None, + on_delete=django.db.models.deletion.CASCADE, + related_name="penalties", + to="users.penaltygroup", + ), + ), + migrations.RunPython(generate_new_penalties), + migrations.RemoveField( + model_name="penalty", + name="user", + ), + ] diff --git a/lego/apps/users/models.py b/lego/apps/users/models.py index ef14d5981..85fd938a2 100644 --- a/lego/apps/users/models.py +++ b/lego/apps/users/models.py @@ -1,5 +1,7 @@ -# mypy: ignore-errors +from __future__ import annotations + import operator +from typing import Tuple from django.conf import settings from django.contrib.auth.models import ( @@ -8,7 +10,7 @@ ) from django.contrib.postgres.fields import ArrayField from django.db import models, transaction -from django.db.models import F, Q +from django.db.models import F, Q, QuerySet from django.utils import timezone from django.utils.timezone import datetime, timedelta @@ -27,6 +29,7 @@ AbakusGroupManagerWithoutText, AbakusUserManager, MembershipManager, + UserPenaltyGroupManager, UserPenaltyManager, ) from lego.apps.users.permissions import ( @@ -445,8 +448,10 @@ def number_of_penalties(self) -> int: # Returns the total penalty weight for this user count = ( Penalty.objects.valid() - .filter(user=self) - .aggregate(models.Sum("weight"))["weight__sum"] + .filter( + penalty_group__user=self, + ) + .count() ) return count or 0 @@ -460,13 +465,13 @@ def has_registered_photo_consents_for_semester( is_consenting__isnull=True, ).exists() - def restricted_lookup(self): + def restricted_lookup(self) -> Tuple[list[User], list[None]]: """ Restricted mail """ return [self], [] - def announcement_lookup(self): + def announcement_lookup(self) -> list[User]: return [self] def unanswered_surveys(self) -> list: @@ -487,26 +492,27 @@ def unanswered_surveys(self) -> list: ) return list(unanswered_surveys.values_list("id", flat=True)) - def check_for_expirable_penalty(self): - from lego.apps.events.models import Event, Pool + def check_for_expirable_penalty(self) -> None: from django.db.models import Subquery + from lego.apps.events.models import Event, Pool + current_active_penalty = ( Penalty.objects.valid() .filter( - user=self, + penalty_group__user=self, activation_time__lte=timezone.now(), activation_time__gte=timezone.now() - - F("weight") * timedelta(days=settings.PENALTY_DURATION.days), + - timedelta(days=settings.PENALTY_DURATION.days), ) .first() ) if current_active_penalty is None: return - # TODO: add test for this pools = Pool.objects.filter(permission_groups__in=self.all_groups).distinct() + # TODO: i believe wrong. number_of_eligable_passed_events = Event.objects.filter( heed_penalties=True, start_time__range=( @@ -522,67 +528,96 @@ def check_for_expirable_penalty(self): current_active_penalty.expire_and_adjust_future_activation_times() -class Penalty(BasisModel): - user = models.ForeignKey(User, related_name="penalties", on_delete=models.CASCADE) +class PenaltyGroup(BasisModel): + user = models.ForeignKey( + User, related_name="penalty_groups", on_delete=models.CASCADE + ) reason = models.CharField(max_length=1000) - weight = models.IntegerField(default=1) source_event = models.ForeignKey( - "events.Event", related_name="penalties", on_delete=models.CASCADE + "events.Event", related_name="penalty_groups", on_delete=models.CASCADE ) - activation_time = models.DateTimeField(null=True, default=None) - # add weightZ + weight = models.IntegerField(default=1, null=False, blank=False) - objects = UserPenaltyManager() # type: ignore + objects = UserPenaltyGroupManager() # type: ignore - def save(self, manual_activation_time=False, *args, **kwargs): - if not manual_activation_time: - current_and_future_penalties = Penalty.objects.filter( - user=self.user, - activation_time__gte=timezone.now() - - F("weight") * settings.PENALTY_DURATION.days * timedelta(days=1), - ).order_by("-activation_time") - - self.activation_time = ( - current_and_future_penalties.first().exact_expiration - if current_and_future_penalties.exists() - else self.created_at - ) + @property + def exact_expiration(self) -> datetime: + last_active_penalty = self.penalties.order_by("-activation_time").first() - super().save(*args, **kwargs) + return last_active_penalty.exact_expiration - def expire_and_adjust_future_activation_times(self): - new_activation_time = timezone.now() - future_penalties = Penalty.objects.filter( - user=self.user, activation_time__gt=new_activation_time - ).order_by("activation_time") + @property + def activation_time(self) -> datetime: + first_active_penalty = self.penalties.order_by("activation_time").first() - if self.weight == 1: - self.activation_time = timezone.now() - timedelta(days=30) - self.created_at = timezone.now() - timedelta(days=30) + return first_active_penalty.activation_time + def delete(self, *args, **kwargs) -> None: + if ( + timezone.now() > self.activation_time + and timezone.now() < self.exact_expiration + ): + new_activation_time = timezone.now() else: - self.weight -= 1 - self.activation_time = timezone.now() - new_activation_time = self.exact_expiration + new_activation_time = self.activation_time - self.save(manual_activation_time=True) + penalties_to_be_changed = Penalty.objects.filter( + penalty_group__user=self.user, + activation_time__gt=self.activation_time, + ).order_by("activation_time") - for penalty in future_penalties: + for penalty in penalties_to_be_changed: penalty.activation_time = new_activation_time - penalty.save(manual_activation_time=True) + penalty.save() new_activation_time = penalty.exact_expiration + for penalty in self.penalties.all(): + print("asdfasdølfkjasdf") + penalty.delete() + + super().delete(*args, **kwargs) + + +class Penalty(BasisModel): + activation_time = models.DateTimeField(null=True, default=None) + penalty_group = models.ForeignKey( + PenaltyGroup, related_name="penalties", on_delete=models.CASCADE, default=None + ) + + objects = UserPenaltyManager() # type: ignore + @property - def exact_expiration(self): + def exact_expiration(self) -> datetime: """Returns the exact time of expiration""" - return ( - Penalty.penalty_offset(self.activation_time, weight=self.weight) - + self.activation_time - ) + # Shouldn't happen, but if activation_time is none, expire immediately + if not self.activation_time: + return self.created_at + return Penalty.penalty_offset(self.activation_time) + self.activation_time + + def expire_and_adjust_future_activation_times(self) -> None: + if ( + timezone.now() > self.activation_time + and timezone.now() < self.exact_expiration + ): + new_activation_time = timezone.now() + else: + new_activation_time = self.activation_time + + penalties_to_be_changed = Penalty.objects.filter( + penalty_group__user=self.penalty_group.user, + activation_time__gt=self.activation_time, + ).order_by("activation_time") + + for penalty in penalties_to_be_changed: + penalty.activation_time = new_activation_time + penalty.save() + new_activation_time = penalty.exact_expiration + + self.delete() @staticmethod - def penalty_offset(start_date, forwards=True, weight=1): - remaining_days = settings.PENALTY_DURATION.days * weight + def penalty_offset(start_date: datetime, forwards: bool = True) -> timedelta: + remaining_days = settings.PENALTY_DURATION.days offset_days = 0 multiplier = 1 if forwards else -1 @@ -600,7 +635,7 @@ def penalty_offset(start_date, forwards=True, weight=1): return timedelta(days=offset_days) @staticmethod - def ignore_date(date): + def ignore_date(date: datetime) -> bool: summer_from, summer_to = settings.PENALTY_IGNORE_SUMMER winter_from, winter_to = settings.PENALTY_IGNORE_WINTER if summer_from <= (date.month, date.day) <= summer_to: @@ -628,7 +663,9 @@ class Meta: unique_together = ("semester", "year", "domain", "user") @staticmethod - def get_consents(user, *, time=None): + def get_consents( + user: User, *, time: datetime | None = None + ) -> QuerySet[PhotoConsent]: now = timezone.now() consent_time = time if time is not None else now consent_semester = PhotoConsent.get_semester(consent_time) @@ -656,5 +693,5 @@ def get_consents(user, *, time=None): return PhotoConsent.objects.filter(user=user) @staticmethod - def get_semester(time: datetime): + def get_semester(time: datetime) -> str: return constants.AUTUMN if time.month > 7 else constants.SPRING diff --git a/lego/apps/users/notifications.py b/lego/apps/users/notifications.py index d0d965a75..606e6dba3 100644 --- a/lego/apps/users/notifications.py +++ b/lego/apps/users/notifications.py @@ -21,7 +21,7 @@ def generate_mail(self): "weight": penalty.weight, "event": penalty.source_event.title, "reason": penalty.reason, - "expiration_date": penalty.exact_expiration.days, + "expiration_date": penalty.exact_expiration, }, subject="Du har fått en ny prikk", plain_template="users/email/penalty.txt", @@ -36,7 +36,7 @@ def generate_push(self): context={ "weight": penalty.weight, "event": penalty.source_event.title, - "expiration_date": penalty.exact_expiration.days, + "expiration_date": penalty.exact_expiration, }, instance=penalty, ) diff --git a/lego/apps/users/serializers/penalties.py b/lego/apps/users/serializers/penalties.py index db1e43dd9..fae599497 100644 --- a/lego/apps/users/serializers/penalties.py +++ b/lego/apps/users/serializers/penalties.py @@ -1,11 +1,11 @@ from rest_framework import serializers -from lego.apps.users.models import Penalty +from lego.apps.users.models import PenaltyGroup -class PenaltySerializer(serializers.ModelSerializer): +class PenaltyGroupSerializer(serializers.ModelSerializer): class Meta: - model = Penalty + model = PenaltyGroup fields = ( "id", "created_at", diff --git a/lego/apps/users/serializers/users.py b/lego/apps/users/serializers/users.py index 8459dece9..acdfe5c04 100644 --- a/lego/apps/users/serializers/users.py +++ b/lego/apps/users/serializers/users.py @@ -4,13 +4,19 @@ from lego.apps.files.fields import ImageField from lego.apps.ical.models import ICalToken from lego.apps.users import constants -from lego.apps.users.models import AbakusGroup, Penalty, PhotoConsent, User +from lego.apps.users.models import ( + AbakusGroup, + Penalty, + PenaltyGroup, + PhotoConsent, + User, +) from lego.apps.users.serializers.abakus_groups import PublicAbakusGroupSerializer from lego.apps.users.serializers.memberships import ( MembershipSerializer, PastMembershipSerializer, ) -from lego.apps.users.serializers.penalties import PenaltySerializer +from lego.apps.users.serializers.penalties import PenaltyGroupSerializer from lego.apps.users.serializers.photo_consents import PhotoConsentSerializer from lego.utils.fields import PrimaryKeyRelatedFieldNoPKOpt @@ -27,8 +33,8 @@ class DetailedUserSerializer(serializers.ModelSerializer): ) def get_valid_penalties(self, user): - qs = Penalty.objects.valid().filter(user=user) - serializer = PenaltySerializer(instance=qs, many=True) + qs = Penalty.objects.valid().filter(penalty_group__user=user) + serializer = PenaltyGroupSerializer(instance=qs, many=True) return serializer.data class Meta: @@ -225,8 +231,8 @@ def get_user_ical_token(self, user): return ical_token.token def get_valid_penalties(self, user): - qs = Penalty.objects.valid().filter(user=user) - serializer = PenaltySerializer(instance=qs, many=True) + qs = PenaltyGroup.objects.valid().filter(user=user) + serializer = PenaltyGroupSerializer(instance=qs, many=True) return serializer.data def get_photo_consents(self, user): diff --git a/lego/apps/users/tasks.py b/lego/apps/users/tasks.py index 17130c354..967fd16c5 100644 --- a/lego/apps/users/tasks.py +++ b/lego/apps/users/tasks.py @@ -1,7 +1,8 @@ from math import ceil from django.conf import settings -from django.db.models import Q +from django.db.models import Count, Q +from django.db.models.functions import Length from django.utils import timezone from structlog import get_logger @@ -82,5 +83,12 @@ def send_inactive_reminder_mail_and_delete_users(self, logger_context=None): @celery_app.task(serializer="json", bind=True, base=AbakusTask) def expire_penalties_if_six_events_has_passed(self, logger_context=None): - me = "happy" # go through all users with penalties and run the expire penalty function on them + + self.setup_logger(logger_context) + + users = User.objects.annotate(penalties_count=Count("penalty_groups")).filter( + penalties_count__gt=0 + ) + for user in users: + user.check_for_expirable_penalty() diff --git a/lego/apps/users/tests/test_models.py b/lego/apps/users/tests/test_models.py index 8b567e044..fabb05ea8 100644 --- a/lego/apps/users/tests/test_models.py +++ b/lego/apps/users/tests/test_models.py @@ -3,7 +3,6 @@ from django.test import override_settings from django.utils import timezone from django.utils.timezone import timedelta -from django.db.models import F, Q from lego import settings from lego.apps.events.models import Event, Pool @@ -14,9 +13,17 @@ SOCIAL_MEDIA_DOMAIN, WEBSITE_DOMAIN, ) -from lego.apps.users.models import AbakusGroup, Membership, Penalty, PhotoConsent, User +from lego.apps.users.models import ( + AbakusGroup, + Membership, + Penalty, + PenaltyGroup, + PhotoConsent, + User, +) from lego.apps.users.registrations import Registrations -from lego.utils.test_utils import BaseTestCase, fake_time +from lego.apps.users.tasks import expire_penalties_if_six_events_has_passed +from lego.utils.test_utils import BaseAPITestCase, BaseTestCase, fake_time class AbakusGroupTestCase(BaseTestCase): @@ -224,6 +231,63 @@ def test_validate_student_confirmation_token(self): self.assertNotEqual(token["member"], False) +class AsyncTestCase(BaseAPITestCase): + fixtures = [ + "test_users.yaml", + "test_abakus_groups.yaml", + "test_companies.yaml", + "test_events.yaml", + ] + + def setUp(self): + self.test_user = User.objects.get(pk=1) + self.source = Event.objects.all().first() + + @mock.patch("django.utils.timezone.now", return_value=fake_time(2016, 10, 10)) + def test_async_expire_penalties_if_six_events_has_passed(self, mock_now): + PenaltyGroup.objects.create( + created_at=mock_now() - timedelta(days=8), + user=self.test_user, + reason="first_penalty", + weight=1, + source_event=self.source, + ) + + PenaltyGroup.objects.create( + created_at=mock_now() - timedelta(days=5), + user=self.test_user, + reason="second_penalty", + weight=2, + source_event=self.source, + ) + self.assertEqual(self.test_user.number_of_penalties(), 3) + + webkom_group = AbakusGroup.objects.get(name="Webkom") + webkom_group.add_user(self.test_user) + webkom_group.save() + self.test_user.save() + + for _i in range(6): + event = Event.objects.create( + title="AbakomEvent", + event_type=0, + start_time=mock_now(), + end_time=mock_now(), + ) + pool = Pool.objects.create( + name="Pool1", + event=event, + capacity=0, + activation_date=timezone.now() - timedelta(days=1), + ) + + pool.permission_groups.set([webkom_group]) + + expire_penalties_if_six_events_has_passed() + + self.assertEqual(self.test_user.number_of_penalties(), 2) + + class MembershipTestCase(BaseTestCase): fixtures = ["test_abakus_groups.yaml", "test_users.yaml"] @@ -266,54 +330,59 @@ def setUp(self): self.test_user = User.objects.get(pk=1) self.source = Event.objects.all().first() - def test_create_penalty(self): - penalty = Penalty.objects.create( - user=self.test_user, reason="test", weight=1, source_event=self.source + def test_create_penalty_group(self): + penalty_group = PenaltyGroup.objects.create( + user=self.test_user, reason="test", source_event=self.source, weight=1 ) + penalties = Penalty.objects.filter(penalty_group__user=self.test_user) + + self.assertEqual(len(penalties), 1) + self.assertEqual(self.test_user.number_of_penalties(), 1) - self.assertEqual(self.test_user, penalty.user) - self.assertEqual("test", penalty.reason) - self.assertEqual(1, penalty.weight) - self.assertEqual(self.source, penalty.source_event) - self.assertEqual(self.source.id, penalty.source_event.id) + self.assertEqual(self.test_user, penalty_group.user) + self.assertEqual("test", penalty_group.reason) + self.assertEqual(1, penalty_group.weight) + self.assertEqual(self.source, penalty_group.source_event) + self.assertEqual(self.source.id, penalty_group.source_event.id) def test_count_weights(self): weights = [1, 2] for weight in weights: - Penalty.objects.create( + PenaltyGroup.objects.create( user=self.test_user, reason="test", - weight=weight, source_event=self.source, + weight=weight, ) self.assertEqual(self.test_user.number_of_penalties(), sum(weights)) @mock.patch("django.utils.timezone.now", return_value=fake_time(2016, 10, 10)) def test_only_count_active_penalties(self, mock_now): - Penalty.objects.create( - created_at=mock_now() - timedelta(days=21), + PenaltyGroup.objects.create( + created_at=mock_now() - timedelta(days=11), user=self.test_user, reason="test", weight=1, source_event=self.source, ) - Penalty.objects.create( + + PenaltyGroup.objects.create( created_at=mock_now() - timedelta(days=9), user=self.test_user, reason="test", weight=1, source_event=self.source, ) - Penalty.objects.create( + PenaltyGroup.objects.create( created_at=mock_now() - timedelta(days=5), user=self.test_user, reason="test", weight=1, source_event=self.source, ) - Penalty.objects.create( + PenaltyGroup.objects.create( created_at=mock_now(), user=self.test_user, reason="test", @@ -324,34 +393,66 @@ def test_only_count_active_penalties(self, mock_now): self.assertEqual(self.test_user.number_of_penalties(), 3) @mock.patch("django.utils.timezone.now", return_value=fake_time(2016, 10, 10)) - def test_penalty_deletion_after_6_events(self, mock_now): - """Tests that The first active penalty is - removed and the rest are adjusted after 6 events""" - - self.test_user.check_for_expirable_penalty() - - Penalty.objects.create( + def test_penalty_group_activation_period(self, mock_now): + PenaltyGroup.objects.create( created_at=mock_now() - timedelta(days=8), user=self.test_user, - reason="first test penalty", + reason="first_penalty", weight=1, source_event=self.source, ) - Penalty.objects.create( - created_at=mock_now() - timedelta(days=5), + + PenaltyGroup.objects.create( + created_at=mock_now() - timedelta(days=2), + user=self.test_user, + reason="second_penalty", + weight=2, + source_event=self.source, + ) + + self.assertEqual( + ( + PenaltyGroup.objects.get(reason="first_penalty").activation_time.day, + PenaltyGroup.objects.get(reason="first_penalty").exact_expiration.day, + ), + (2, 12), + ) + self.assertEqual( + ( + PenaltyGroup.objects.get(reason="second_penalty").activation_time.day, + PenaltyGroup.objects.get(reason="second_penalty").exact_expiration.day, + ), + (12, 1), + ) + + @mock.patch("django.utils.timezone.now", return_value=fake_time(2016, 10, 10)) + def test_penalty_expiration_after_6_events_on_penalty_with_weight_one( + self, mock_now + ): + """Tests that The first active penalty is + expired and the rest are adjusted after 6 events""" + + self.test_user.check_for_expirable_penalty() + + PenaltyGroup.objects.create( + created_at=mock_now() - timedelta(days=8), user=self.test_user, - reason="second test penalty", + reason="first_penalty", weight=1, source_event=self.source, ) - Penalty.objects.create( + + PenaltyGroup.objects.create( created_at=mock_now() - timedelta(days=5), user=self.test_user, - reason="third test penalty", - weight=1, + reason="second_penalty", + weight=2, source_event=self.source, ) + # The timeline currently is: 1: 2-12, 2: 12-22, 3: 22-1 + self.assertEqual(self.test_user.number_of_penalties(), 3) + webkom_group = AbakusGroup.objects.get(name="Webkom") webkom_group.add_user(self.test_user) webkom_group.save() @@ -361,8 +462,8 @@ def test_penalty_deletion_after_6_events(self, mock_now): event = Event.objects.create( title="AbakomEvent", event_type=0, - start_time=mock_now() - timedelta(days=6), - end_time=mock_now() - timedelta(days=6), + start_time=mock_now(), + end_time=mock_now(), ) pool = Pool.objects.create( name="Pool1", @@ -375,31 +476,35 @@ def test_penalty_deletion_after_6_events(self, mock_now): self.test_user.check_for_expirable_penalty() # Tests that the changes happened after 6 events + # The timeline now should be is: 1: 10-20, 2: 20-30 self.assertEqual(self.test_user.number_of_penalties(), 2) self.assertEqual( ( - Penalty.objects.get(reason="second test penalty").activation_time.day, - Penalty.objects.get(reason="second test penalty").exact_expiration.day, - Penalty.objects.get(reason="third test penalty").exact_expiration.day, + Penalty.objects.valid()[0].activation_time.day, + Penalty.objects.valid()[0].exact_expiration.day, + Penalty.objects.valid()[1].exact_expiration.day, ), (10, 20, 30), ) @mock.patch("django.utils.timezone.now", return_value=fake_time(2016, 10, 10)) - def test_penalty_weight_decrementing_after_6_events(self, mock_now): + def test_penalty_expiration_after_6_events_on_penalty_with_weight_two( + self, mock_now + ): """Tests that The weight of the first active penalty is decremented and the rest are adjusted after 6 events""" - Penalty.objects.create( + PenaltyGroup.objects.create( created_at=mock_now() - timedelta(days=8), user=self.test_user, - reason="first test penalty", + reason="first_penalty", weight=2, source_event=self.source, ) - Penalty.objects.create( + + PenaltyGroup.objects.create( created_at=mock_now() - timedelta(days=5), user=self.test_user, - reason="second test penalty", + reason="second_penalty", weight=1, source_event=self.source, ) @@ -427,14 +532,15 @@ def test_penalty_weight_decrementing_after_6_events(self, mock_now): self.test_user.check_for_expirable_penalty() - """Tests first that nothing is changed after 5 events""" + # Tests first that nothing is changed after 5 events self.assertEqual(self.test_user.number_of_penalties(), 3) self.assertEqual( ( - Penalty.objects.get(reason="first test penalty").exact_expiration.day, - Penalty.objects.get(reason="second test penalty").exact_expiration.day, + Penalty.objects.valid()[0].exact_expiration.day, + Penalty.objects.valid()[1].exact_expiration.day, + Penalty.objects.valid()[2].exact_expiration.day, ), - (22, 1), + (12, 22, 1), ) event = Event.objects.create( @@ -449,53 +555,61 @@ def test_penalty_weight_decrementing_after_6_events(self, mock_now): capacity=0, activation_date=timezone.now() - timedelta(days=1), ) + self.test_user.check_for_expirable_penalty() + # Tests that the new event is not counted if + # the users does not have permission to the pool + self.assertEqual(self.test_user.number_of_penalties(), 3) pool.permission_groups.set([webkom_group]) self.test_user.check_for_expirable_penalty() - """Tests that the changes happened after 6 events""" + + # Tests that the changes happened after 6 events self.assertEqual(self.test_user.number_of_penalties(), 2) self.assertEqual( ( - Penalty.objects.get(reason="first test penalty").exact_expiration.day, - Penalty.objects.get(reason="second test penalty").exact_expiration.day, + Penalty.objects.valid()[0].exact_expiration.day, + Penalty.objects.valid()[1].exact_expiration.day, ), (20, 30), ) @override_settings(PENALTY_IGNORE_WINTER=((12, 10), (1, 10))) - @mock.patch("django.utils.timezone.now", return_value=fake_time(2016, 12, 10)) + @mock.patch("django.utils.timezone.now", return_value=fake_time(2016, 12, 20)) def test_frozen_penalties_count_as_active_winter(self, mock_now): - # This penalty is created slightly less than 10 days from the freeze-point. - # It should be counted as active. - Penalty.objects.create( - created_at=mock_now() - timedelta(days=10, hours=23, minutes=59), + penalty1 = PenaltyGroup.objects.create( + created_at=mock_now() - timedelta(days=19, hours=23, minutes=59), user=self.test_user, - reason="active", + reason="penalty1", weight=1, source_event=self.source, ) - # This penalty is created exactly 10 days from the freeze-point. - # It should be counted as inactive. - Penalty.objects.create( - created_at=mock_now() - timedelta(days=11), + penalty2 = PenaltyGroup.objects.create( + created_at=mock_now() - timedelta(days=20), user=self.test_user, - reason="inactive", + reason="penalty2", weight=1, source_event=self.source, ) - self.assertEqual(self.test_user.number_of_penalties(), 1) - self.assertEqual(self.test_user.penalties.valid().first().reason, "active") + self.assertEqual(self.test_user.number_of_penalties(), 2) + self.assertEqual( + (penalty1.activation_time.day, penalty1.activation_time.month), + (30, 11), + ) + self.assertEqual( + (penalty2.activation_time.day, penalty2.activation_time.month), + (11, 1), + ) @override_settings(PENALTY_IGNORE_SUMMER=((6, 12), (8, 15))) - @mock.patch("django.utils.timezone.now", return_value=fake_time(2016, 6, 12)) + @mock.patch("django.utils.timezone.now", return_value=fake_time(2016, 6, 22)) def test_frozen_penalties_count_as_active_summer(self, mock_now): # This penalty is created slightly less than 10 days from the freeze-point. # It should be counted as active. - Penalty.objects.create( - created_at=mock_now() - timedelta(days=10, hours=23, minutes=59), + penalty1 = PenaltyGroup.objects.create( + created_at=mock_now() - timedelta(days=19, hours=23, minutes=59), user=self.test_user, reason="active", weight=1, @@ -504,23 +618,30 @@ def test_frozen_penalties_count_as_active_summer(self, mock_now): # This penalty is created exactly 10 days from the freeze-point. # It should be counted as inactive. - Penalty.objects.create( - created_at=mock_now() - timedelta(days=11), + penalty2 = PenaltyGroup.objects.create( + created_at=mock_now() - timedelta(days=20), user=self.test_user, reason="inactive", weight=1, source_event=self.source, ) - self.assertEqual(self.test_user.number_of_penalties(), 1) - self.assertEqual(self.test_user.penalties.valid().first().reason, "active") + self.assertEqual(self.test_user.number_of_penalties(), 2) + self.assertEqual( + (penalty1.activation_time.day, penalty1.activation_time.month), + (2, 6), + ) + self.assertEqual( + (penalty2.activation_time.day, penalty2.activation_time.month), + (16, 8), + ) @override_settings(PENALTY_IGNORE_WINTER=((12, 22), (1, 10))) @mock.patch("django.utils.timezone.now", return_value=fake_time(2019, 12, 23)) def test_penalty_offset_is_calculated_correctly(self, mock_now): # This penalty is set to expire the day before the penalty freeze # It should not be active - inactive = Penalty.objects.create( + inactive = PenaltyGroup.objects.create( created_at=mock_now().replace(day=10), user=self.test_user, reason="inactive", @@ -533,7 +654,7 @@ def test_penalty_offset_is_calculated_correctly(self, mock_now): (12, inactive.created_at.day + settings.PENALTY_DURATION.days), ) - active = Penalty.objects.create( + active = PenaltyGroup.objects.create( created_at=mock_now().replace(day=15), user=self.test_user, reason="active", @@ -542,8 +663,45 @@ def test_penalty_offset_is_calculated_correctly(self, mock_now): ) self.assertEqual( (active.exact_expiration.month, active.exact_expiration.day), - (1, 14), + (1, 19), + ) + + @mock.patch("django.utils.timezone.now", return_value=fake_time(2019, 10, 10)) + def test_penalty_group_valid_with_one_weight(self, mock_now): + PenaltyGroup.objects.create( + created_at=mock_now() - timedelta(days=10), + user=self.test_user, + reason="test", + weight=1, + source_event=self.source, + ) + PenaltyGroup.objects.create( + created_at=mock_now() - timedelta(days=9, hours=59, seconds=59), + user=self.test_user, + reason="test", + weight=1, + source_event=self.source, + ) + + self.assertEqual(len(PenaltyGroup.objects.valid()), 1) + + @mock.patch("django.utils.timezone.now", return_value=fake_time(2019, 10, 10)) + def test_penalty_group_valid_with_two_weight(self, mock_now): + PenaltyGroup.objects.create( + created_at=mock_now() - timedelta(days=20), + user=self.test_user, + reason="test", + weight=2, + source_event=self.source, + ) + PenaltyGroup.objects.create( + created_at=mock_now() - timedelta(days=15), + user=self.test_user, + reason="test", + weight=2, + source_event=self.source, ) + self.assertEqual(len(PenaltyGroup.objects.valid()), 1) class PhotoConsentTestCase(BaseTestCase): diff --git a/lego/apps/users/tests/test_users_api.py b/lego/apps/users/tests/test_users_api.py index 38b612ba2..f519ee5d2 100644 --- a/lego/apps/users/tests/test_users_api.py +++ b/lego/apps/users/tests/test_users_api.py @@ -9,7 +9,7 @@ from lego.apps.files.models import File from lego.apps.users import constants from lego.apps.users.constants import AUTUMN, SOCIAL_MEDIA_DOMAIN, WEBSITE_DOMAIN -from lego.apps.users.models import AbakusGroup, Penalty, PhotoConsent, User +from lego.apps.users.models import AbakusGroup, PenaltyGroup, PhotoConsent, User from lego.apps.users.registrations import Registrations from lego.utils.test_utils import BaseAPITestCase, fake_time @@ -434,7 +434,7 @@ def test_self_authed(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( - len(self.user.penalties.valid()), len(response.json()["penalties"]) + len(self.user.penalty_groups.valid()), len(response.json()["penalties"]) ) data = response.json() self.assertEqual(self.user.id, data["id"]) @@ -453,27 +453,29 @@ def test_self_unauthed(self): @mock.patch("django.utils.timezone.now", return_value=fake_time(2016, 10, 1)) def test_own_penalties_serializer(self, mock_now): source = Event.objects.all().first() - Penalty.objects.create( + PenaltyGroup.objects.create( created_at=mock_now() - timedelta(days=10), user=self.user, reason="test", weight=1, source_event=source, ) - Penalty.objects.create( + PenaltyGroup.objects.create( created_at=mock_now() - timedelta(days=9, hours=23, minutes=59), user=self.user, reason="test", weight=1, source_event=source, ) + self.client.force_authenticate(user=self.user) + response = self.client.get(reverse("api:v1:user-me")) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( - len(self.user.penalties.valid()), len(response.json()["penalties"]) + len(self.user.penalty_groups.valid()), len(response.json()["penalties"]) ) self.assertEqual(len(response.json()["penalties"]), 1) diff --git a/lego/apps/users/views/penalties.py b/lego/apps/users/views/penalties.py index 50e660038..a649e9c4e 100644 --- a/lego/apps/users/views/penalties.py +++ b/lego/apps/users/views/penalties.py @@ -2,18 +2,18 @@ from rest_framework.viewsets import GenericViewSet from lego.apps.permissions.api.views import AllowedPermissionsMixin -from lego.apps.users.filters import PenaltyFilterSet -from lego.apps.users.models import Penalty -from lego.apps.users.serializers.penalties import PenaltySerializer +from lego.apps.users.filters import PenaltyGroupFilterSet +from lego.apps.users.models import PenaltyGroup +from lego.apps.users.serializers.penalties import PenaltyGroupSerializer -class PenaltyViewSet( +class PenaltyGroupViewSet( AllowedPermissionsMixin, mixins.CreateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, GenericViewSet, ): - queryset = Penalty.objects.all() - serializer_class = PenaltySerializer - filterset_class = PenaltyFilterSet + queryset = PenaltyGroup.objects.all() + serializer_class = PenaltyGroupSerializer + filterset_class = PenaltyGroupFilterSet