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