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))