From 60f984f758aa25980b9680cab91982108681ec9f Mon Sep 17 00:00:00 2001 From: Mathias Aas Date: Tue, 10 Oct 2023 22:45:36 +0200 Subject: [PATCH] fix according to comments --- backend/root/utils/mixins.py | 13 +++++++ backend/samfundet/models/general.py | 45 +++++++++++++------------ backend/samfundet/models/recruitment.py | 20 +++++------ backend/samfundet/views.py | 6 +++- 4 files changed, 51 insertions(+), 33 deletions(-) diff --git a/backend/root/utils/mixins.py b/backend/root/utils/mixins.py index 91a17c1cc..530feb631 100644 --- a/backend/root/utils/mixins.py +++ b/backend/root/utils/mixins.py @@ -162,3 +162,16 @@ def from_db(cls, db, field_names, values): # type: ignore # noqa: ANN001,ANN206 setattr(instance, instance._FTM_LOADED_FIELDS_NAME, loaded_fields) # noqa: FKA01 return instance + + + +class FullCleanSaveMixin(Model): + """Mixin to call full_clean() before save().""" + + class Meta: + abstract = True + + def save(self, *args: Any, **kwargs: Any) -> None: + self.full_clean() + super().save(*args, **kwargs) + diff --git a/backend/samfundet/models/general.py b/backend/samfundet/models/general.py index 433b199cc..5fcbea82a 100644 --- a/backend/samfundet/models/general.py +++ b/backend/samfundet/models/general.py @@ -16,6 +16,7 @@ from django.db import models from guardian.shortcuts import assign_perm from django.utils.translation import gettext as _ +from backend.root.utils.mixins import FullCleanSaveMixin from root.utils import permissions @@ -32,7 +33,7 @@ class Meta(AbstractNotification.Meta): abstract = False -class Tag(models.Model): +class Tag(FullCleanSaveMixin): # TODO make name case-insensitive # Kan tvinge alt til lowercase, er enklere. name = models.CharField(max_length=140) @@ -69,7 +70,7 @@ def save(self, *args: Any, **kwargs: Any) -> None: super().save(*args, **kwargs) -class Image(models.Model): +class Image(FullCleanSaveMixin): title = models.CharField(max_length=140) tags = models.ManyToManyField(Tag, blank=True, related_name='images') image = models.ImageField(upload_to='images/', blank=False, null=False) @@ -125,7 +126,7 @@ def impersonated_by(self) -> User: return self._impersonated_by -class UserPreference(models.Model): +class UserPreference(FullCleanSaveMixin): """Group all preferences and config per user.""" class Theme(models.TextChoices): @@ -150,7 +151,7 @@ def __str__(self) -> str: return f'UserPreference ({self.user})' -class Profile(models.Model): +class Profile(FullCleanSaveMixin): user = models.OneToOneField(User, on_delete=models.CASCADE, blank=True, null=True) nickname = models.CharField(max_length=30, blank=True, null=True) @@ -173,7 +174,7 @@ def save(self, *args: Any, **kwargs: Any) -> None: assign_perm(perm=permissions.SAMFUNDET_CHANGE_PROFILE, user_or_group=self.user, obj=self) -class Venue(models.Model): +class Venue(FullCleanSaveMixin): name = models.CharField(max_length=140, blank=True, null=True, unique=True) slug = models.SlugField(unique=True, null=True) description = models.TextField(blank=True, null=True) @@ -211,7 +212,7 @@ def __str__(self) -> str: return f'{self.name}' -class ClosedPeriod(models.Model): +class ClosedPeriod(FullCleanSaveMixin): message_nb = models.TextField(blank=True, null=True, verbose_name='Melding (norsk)') message_en = models.TextField(blank=True, null=True, verbose_name='Melding (engelsk)') @@ -233,7 +234,7 @@ def __str__(self) -> str: # GANGS ### -class Organization(models.Model): +class Organization(FullCleanSaveMixin): """ Object for mapping out the orgs with different gangs, eg. Samfundet, UKA, ISFiT """ @@ -247,7 +248,7 @@ def __str__(self) -> str: return self.name -class GangType(models.Model): +class GangType(FullCleanSaveMixin): """ Type of gang. eg. 'arrangerende', 'kunstnerisk' etc. """ @@ -265,7 +266,7 @@ def __str__(self) -> str: return f'{self.title_nb}' -class Gang(models.Model): +class Gang(FullCleanSaveMixin): name_nb = models.CharField(max_length=64, blank=True, null=True, verbose_name='Navn Norsk') name_en = models.CharField(max_length=64, blank=True, null=True, verbose_name='Navn Engelsk') abbreviation = models.CharField(max_length=8, blank=True, null=True, verbose_name='Forkortelse') @@ -304,7 +305,7 @@ def __str__(self) -> str: return f'{self.gang_type} - {self.name_nb}' -class InformationPage(models.Model): +class InformationPage(FullCleanSaveMixin): slug_field = models.SlugField( max_length=64, blank=True, @@ -333,7 +334,7 @@ def __str__(self) -> str: return f'{self.slug_field}' -class BlogPost(models.Model): +class BlogPost(FullCleanSaveMixin): title_nb = models.CharField(max_length=64, blank=True, null=True, verbose_name='Tittel (norsk)') text_nb = models.TextField(blank=True, null=True, verbose_name='Tekst (norsk)') @@ -356,7 +357,7 @@ def __str__(self) -> str: return f'{self.title_nb} {self.published_at}' -class Table(models.Model): +class Table(FullCleanSaveMixin): name_nb = models.CharField(max_length=64, unique=True, blank=True, null=True, verbose_name='Navn (norsk)') description_nb = models.CharField(max_length=64, blank=True, null=True, verbose_name='Beskrivelse (norsk)') @@ -381,7 +382,7 @@ def __str__(self) -> str: return f'{self.name_nb}' -class Reservation(models.Model): +class Reservation(FullCleanSaveMixin): user = models.ForeignKey(User, on_delete=models.CASCADE, blank=True, null=True) name = models.CharField(max_length=64, blank=True, verbose_name='Navn') email = models.EmailField(max_length=64, blank=True, verbose_name='Epost') @@ -408,7 +409,7 @@ def __str__(self) -> str: return f'{self.name}' -class FoodPreference(models.Model): +class FoodPreference(FullCleanSaveMixin): name_nb = models.CharField(max_length=64, unique=True, blank=True, null=True, verbose_name='Navn (norsk)') name_en = models.CharField(max_length=64, blank=True, null=True, verbose_name='Navn (engelsk)') @@ -423,7 +424,7 @@ def __str__(self) -> str: return f'{self.name_nb}' -class FoodCategory(models.Model): +class FoodCategory(FullCleanSaveMixin): name_nb = models.CharField(max_length=64, unique=True, blank=True, null=True, verbose_name='Navn (norsk)') name_en = models.CharField(max_length=64, blank=True, null=True, verbose_name='Navn (engelsk)') order = models.PositiveSmallIntegerField(blank=True, null=True, unique=True) @@ -439,7 +440,7 @@ def __str__(self) -> str: return f'{self.name_nb}' -class MenuItem(models.Model): +class MenuItem(FullCleanSaveMixin): name_nb = models.CharField(max_length=64, unique=True, blank=True, null=True, verbose_name='Navn (norsk)') description_nb = models.TextField(blank=True, null=True, verbose_name='Beskrivelse (norsk)') @@ -465,7 +466,7 @@ def __str__(self) -> str: return f'{self.name_nb}' -class Menu(models.Model): +class Menu(FullCleanSaveMixin): name_nb = models.CharField(max_length=64, unique=True, blank=True, null=True, verbose_name='Navn (norsk)') description_nb = models.TextField(blank=True, null=True, verbose_name='Beskrivelse (norsk)') @@ -485,7 +486,7 @@ def __str__(self) -> str: return f'{self.name_nb}' -class Saksdokument(models.Model): +class Saksdokument(FullCleanSaveMixin): title_nb = models.CharField(max_length=80, blank=True, null=True, verbose_name='Tittel (Norsk)') title_en = models.CharField(max_length=80, blank=True, null=True, verbose_name='Tittel (Engelsk)') publication_date = models.DateTimeField(blank=True, null=True) @@ -510,7 +511,7 @@ def __str__(self) -> str: return f'{self.title_nb}' -class Booking(models.Model): +class Booking(FullCleanSaveMixin): name = models.CharField(max_length=64, blank=True, null=True) text = models.TextField(blank=True, null=True) from_dt = models.DateTimeField(blank=True, null=True) @@ -562,7 +563,7 @@ def save(self, *args: Any, **kwargs: Any) -> None: super().save(*args, **kwargs) -class Infobox(models.Model): +class Infobox(FullCleanSaveMixin): title_nb = models.CharField(max_length=60, blank=False, null=False, verbose_name='Infoboks titel (norsk)') text_nb = models.CharField(max_length=255, blank=False, null=False, verbose_name='Infoboks tekst (norsk)') @@ -585,7 +586,7 @@ def __str__(self) -> str: return f'{self.title_nb}' -class TextItem(models.Model): +class TextItem(FullCleanSaveMixin): key = models.CharField(max_length=40, blank=False, null=False, unique=True, primary_key=True) text_nb = models.TextField() text_en = models.TextField() @@ -598,7 +599,7 @@ def __str__(self) -> str: return f'{self.key}' -class KeyValue(models.Model): +class KeyValue(FullCleanSaveMixin): """ Model for environment variables in the database. Should not be used for secrets. diff --git a/backend/samfundet/models/recruitment.py b/backend/samfundet/models/recruitment.py index bf0482f0a..ab1673094 100644 --- a/backend/samfundet/models/recruitment.py +++ b/backend/samfundet/models/recruitment.py @@ -8,10 +8,12 @@ from django.db import models +from backend.root.utils.mixins import FullCleanSaveMixin + from .general import Organization, User, Gang -class Recruitment(models.Model): +class Recruitment(FullCleanSaveMixin): name_nb = models.CharField(max_length=100, help_text='Name of the recruitment') name_en = models.CharField(max_length=100, help_text='Name of the recruitment') visible_from = models.DateTimeField(help_text='When it becomes visible for applicants') @@ -26,7 +28,7 @@ class Recruitment(models.Model): def is_active(self) -> bool: return self.visible_from < timezone.now() < self.actual_application_deadline - def save(self, *args: tuple, **kwargs: dict) -> None: + def clean(self, *args: tuple, **kwargs: dict) -> None: # All times should be in the future now = timezone.now() if any( @@ -53,13 +55,13 @@ def save(self, *args: tuple, **kwargs: dict) -> None: if self.reprioritization_deadline_for_applicant > self.reprioritization_deadline_for_groups: raise ValidationError('Reprioritization deadline for applicants should be before reprioritization deadline for groups') - super().save(*args, **kwargs) + super().clean() def __str__(self) -> str: return f'Recruitment: {self.name_en} at {self.organization}' -class RecruitmentPosition(models.Model): +class RecruitmentPosition(FullCleanSaveMixin): name_nb = models.CharField(max_length=100, help_text='Name of the position') name_en = models.CharField(max_length=100, help_text='Name of the position') @@ -96,7 +98,7 @@ def __str__(self) -> str: return f'Position: {self.name_en} in {self.recruitment}' -class InterviewRoom(models.Model): +class InterviewRoom(FullCleanSaveMixin): name = models.CharField(max_length=255, help_text='Name of the room') location = models.CharField(max_length=255, help_text='Physical location, eg. campus') start_time = models.DateTimeField(help_text='Start time of availability') @@ -114,7 +116,7 @@ def clean(self) -> None: super().clean() -class Interview(models.Model): +class Interview(FullCleanSaveMixin): # User visible fields interview_time = models.DateTimeField(help_text='The time of the interview', null=True, blank=True) interview_location = models.CharField(max_length=255, help_text='The location of the interview', null=True, blank=True) @@ -131,7 +133,7 @@ class Interview(models.Model): notes = models.TextField(help_text='Notes for the interview', null=True, blank=True) -class RecruitmentAdmission(models.Model): +class RecruitmentAdmission(FullCleanSaveMixin): admission_text = models.TextField(help_text='Admission text for the admission') recruitment_position = models.ForeignKey( RecruitmentPosition, on_delete=models.CASCADE, help_text='The recruitment position that is recruiting', related_name='admissions' @@ -180,8 +182,6 @@ def save(self, *args: tuple, **kwargs: dict) -> None: self.interview = shared_interview.interview else: # Create a new interview instance if needed - new_interview = Interview() - new_interview.save() - self.interview = new_interview + self.interview = Interview.objects.create() super(RecruitmentAdmission, self).save(*args, **kwargs) diff --git a/backend/samfundet/views.py b/backend/samfundet/views.py index 706593799..230b2d980 100644 --- a/backend/samfundet/views.py +++ b/backend/samfundet/views.py @@ -531,7 +531,11 @@ def get_queryset(self) -> QuerySet[User]: # Exclude users who have any admissions for the given recruitment that have an interview_time users_without_interviews = User.objects.filter(admissions__recruitment=recruitment).annotate( num_interviews=Count( - Case(When(admissions__recruitment=recruitment, then='admissions__interview__interview_time'), default=None, output_field=None) + Case( + When(admissions__recruitment=recruitment, then='admissions__interview__interview_time'), + default=None, + output_field=None, + ) ) ).filter(num_interviews=0) return users_without_interviews