diff --git a/.gitignore b/.gitignore index 8e3486b6ed..82bb150257 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 64d8d1555c..a36d4291bb 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 4ffe5b9577..083fd1e979 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 6fda139ca5..6cb29cf88a 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 079cdbd426..0000000000 --- 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 bd29c72eee..9bab26d014 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,12 +535,42 @@ def save(self, *args, **kwargs): ) super().save(*args, **kwargs) + def default_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 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""" @@ -527,11 +581,11 @@ def exact_expiration(self): @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 d7fb08682c..d8f95229b9 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):