Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce "too late" as an event presence status #3564

Merged
merged 3 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lego/apps/events/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
class PRESENCE_CHOICES(models.TextChoices):
UNKNOWN = "UNKNOWN"
PRESENT = "PRESENT"
LATE = "LATE"
NOT_PRESENT = "NOT_PRESENT"


Expand Down
26 changes: 26 additions & 0 deletions lego/apps/events/migrations/0040_alter_registration_presence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 4.0.10 on 2024-03-06 22:04

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("events", "0039_remove_event_use_contact_tracing"),
]

operations = [
migrations.AlterField(
model_name="registration",
name="presence",
field=models.CharField(
choices=[
("UNKNOWN", "Unknown"),
("PRESENT", "Present"),
("LATE", "Late"),
("NOT_PRESENT", "Not Present"),
],
default="UNKNOWN",
max_length=20,
),
),
]
41 changes: 31 additions & 10 deletions lego/apps/events/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from lego.apps.files.models import FileField
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.constants import AUTUMN, PENALTY_TYPES, PENALTY_WEIGHTS, SPRING
from lego.apps.users.models import AbakusGroup, Membership, Penalty, User
from lego.utils.models import BasisModel
from lego.utils.youtube_validator import youtube_validator
Expand Down Expand Up @@ -930,26 +930,47 @@ def set_presence(self, presence: constants.PRESENCE_CHOICES) -> None:
"""Wrap this method in a transaction"""
if presence not in constants.PRESENCE_CHOICES:
raise ValueError("Illegal presence choice")

self.presence = presence
self.handle_user_penalty(presence)
self.save()

def delete_presence_penalties_for_event(self) -> None:
for penalty in self.user.penalties.filter(
source_event=self.event, type=PENALTY_TYPES.PRESENCE
):
penalty.delete()

def handle_user_penalty(self, presence: constants.PRESENCE_CHOICES) -> None:
"""
Previous penalties related to the event are deleted since the
newest presence is the only one that matters
"""

ivarnakken marked this conversation as resolved.
Show resolved Hide resolved
if (
self.event.heed_penalties
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(
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,
)
self.delete_presence_penalties_for_event()
Penalty.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,
type=PENALTY_TYPES.PRESENCE,
)
elif self.event.heed_penalties and presence == constants.PRESENCE_CHOICES.LATE:
self.delete_presence_penalties_for_event()
Penalty.objects.create(
user=self.user,
reason=f"Møtte for sent opp på {self.event.title}.",
weight=PENALTY_WEIGHTS.LATE_PRESENCE,
source_event=self.event,
type=PENALTY_TYPES.PRESENCE,
)
else:
for penalty in self.user.penalties.filter(source_event=self.event):
penalty.delete()
self.delete_presence_penalties_for_event()

def add_to_pool(self, pool: Pool) -> Registration:
allowed: bool = False
Expand Down
12 changes: 5 additions & 7 deletions lego/apps/events/serializers/registrations.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from django.db import transaction
from rest_framework import serializers

from rest_framework_jwt.serializers import ImpersonateAuthTokenSerializer
Expand Down Expand Up @@ -65,13 +64,12 @@ class Meta:
)

def update(self, instance, validated_data):
with transaction.atomic():
presence = validated_data.pop("presence", None)
super().update(instance, validated_data)
if presence:
instance.set_presence(presence)
presence = validated_data.pop("presence", None)
super().update(instance, validated_data)
if presence:
instance.set_presence(presence)

return instance
return instance


class RegistrationAnonymizedReadSerializer(BasisModelSerializer):
Expand Down
66 changes: 65 additions & 1 deletion lego/apps/events/tests/test_penalties.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from lego.apps.events import constants
from lego.apps.events.models import Event, Registration
from lego.apps.users.constants import LATE_PRESENCE_PENALTY_WEIGHT
from lego.apps.users.models import AbakusGroup, Penalty
from lego.utils.test_utils import BaseTestCase

Expand Down Expand Up @@ -398,6 +399,19 @@ def test_penalties_created_when_not_present(self):
self.assertEqual(penalties_before, 0)
self.assertEqual(penalties_after, event.penalty_weight_on_not_present)

def test_penalties_created_when_late_present(self):
"""Test that user gets penalties when late present"""
event = Event.objects.get(title="POOLS_WITH_REGISTRATIONS")

registration = event.registrations.first()
penalties_before = registration.user.number_of_penalties()

registration.set_presence(constants.PRESENCE_CHOICES.LATE)

penalties_after = registration.user.number_of_penalties()
self.assertEqual(penalties_before, 0)
self.assertEqual(penalties_after, LATE_PRESENCE_PENALTY_WEIGHT)

def test_penalties_removed_when_not_present_changes(self):
"""Test that penalties for not_present gets removed when resetting presence"""
event = Event.objects.get(title="POOLS_WITH_REGISTRATIONS")
Expand All @@ -411,8 +425,23 @@ def test_penalties_removed_when_not_present_changes(self):
self.assertEqual(penalties_before, event.penalty_weight_on_not_present)
self.assertEqual(penalties_after, 0)

def test_penalties_removed_when_late_present_changes(self):
"""Test that penalties for late presence gets removed when changing to present"""
event = Event.objects.get(title="POOLS_WITH_REGISTRATIONS")
registration = event.registrations.first()
registration.set_presence(constants.PRESENCE_CHOICES.LATE)

penalties_before = registration.user.number_of_penalties()
registration.set_presence(constants.PRESENCE_CHOICES.PRESENT)

penalties_after = registration.user.number_of_penalties()
self.assertEqual(penalties_before, LATE_PRESENCE_PENALTY_WEIGHT)
self.assertEqual(penalties_after, 0)

def test_only_correct_penalties_are_removed_on_presence_change(self):
"""Test that only penalties for given event are removed when changing presence"""
"""
Test that only penalties of type presence for given event are removed when changing presence
"""
event = Event.objects.get(title="POOLS_WITH_REGISTRATIONS")
other_event = Event.objects.get(title="POOLS_NO_REGISTRATIONS")
registration = event.registrations.first()
Expand Down Expand Up @@ -447,6 +476,41 @@ def test_only_correct_penalties_are_removed_on_presence_change(self):
)
self.assertEqual(penalties_after, other_event.penalty_weight_on_not_present)

def test_only_correct_penalties_are_removed_on_presence_change_on_same_event(self):
"""Test that only penalties of type presence are removed when changing presence"""
event = Event.objects.get(title="POOLS_WITH_REGISTRATIONS")
registration = event.registrations.first()

registration.set_presence(constants.PRESENCE_CHOICES.NOT_PRESENT)
penalties_before = registration.user.number_of_penalties()
penalties_object_before = list(registration.user.penalties.all())

# Default penalty type is other
Penalty.objects.create(
user=registration.user,
reason="SAME EVENT",
weight=2,
source_event=event,
)
penalties_during = registration.user.number_of_penalties()
penalties_objects_during = list(registration.user.penalties.all())

registration.set_presence(constants.PRESENCE_CHOICES.UNKNOWN)
penalties_after = registration.user.number_of_penalties()
penalties_object_after = list(registration.user.penalties.all())

self.assertEqual(penalties_object_before[0].source_event, event)
self.assertEqual(penalties_object_after[0].source_event, event)
self.assertEqual(len(penalties_object_before), 1)
self.assertEqual(len(penalties_objects_during), 2)
self.assertEqual(len(penalties_object_after), 1)
self.assertEqual(penalties_before, event.penalty_weight_on_not_present)
self.assertEqual(
penalties_during,
event.penalty_weight_on_not_present + event.penalty_weight_on_not_present,
)
self.assertEqual(penalties_after, event.penalty_weight_on_not_present)

def test_able_to_register_when_not_heed_penalties_with_penalties(self):
"""Test that user is able to register when heed_penalties is false and user has penalties"""
event = Event.objects.get(title="POOLS_WITH_REGISTRATIONS")
Expand Down
17 changes: 15 additions & 2 deletions lego/apps/users/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from enum import Enum

from django.db import models

MALE = "male"
FEMALE = "female"
OTHER = "other"
Expand Down Expand Up @@ -95,8 +97,6 @@ def values(cls) -> list[str]:
FSGroup.MSSECCLO: FOURTH_GRADE_KOMTEK,
}

STUDENT_EMAIL_DOMAIN = "stud.ntnu.no"

ivarnakken marked this conversation as resolved.
Show resolved Hide resolved
GROUP_COMMITTEE = "komite"
GROUP_INTEREST = "interesse"
GROUP_BOARD = "styre"
Expand Down Expand Up @@ -142,3 +142,16 @@ def values(cls) -> list[str]:
(LIGHT_THEME, LIGHT_THEME),
(DARK_THEME, DARK_THEME),
)


LATE_PRESENCE_PENALTY_WEIGHT = 1
ivarnakken marked this conversation as resolved.
Show resolved Hide resolved


class PENALTY_WEIGHTS(models.TextChoices):
LATE_PRESENCE = 1


class PENALTY_TYPES(models.TextChoices):
PRESENCE = "presence"
PAYMENT = "payment"
OTHER = "other"
25 changes: 25 additions & 0 deletions lego/apps/users/migrations/0042_penalty_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 4.0.10 on 2024-03-14 10:27

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("users", "0041_user_linkedin_id_alter_user_github_username"),
]

operations = [
migrations.AddField(
model_name="penalty",
name="type",
field=models.CharField(
choices=[
("presence", "Presence"),
("payment", "Payment"),
("other", "Other"),
],
default="other",
max_length=50,
),
),
]
5 changes: 5 additions & 0 deletions lego/apps/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,11 @@ class Penalty(BasisModel):
source_event = models.ForeignKey(
"events.Event", related_name="penalties", on_delete=models.CASCADE
)
type = models.CharField(
max_length=50,
choices=constants.PENALTY_TYPES.choices,
default=constants.PENALTY_TYPES.OTHER,
)

objects = UserPenaltyManager() # type: ignore

Expand Down