diff --git a/.dockerignore b/.dockerignore index f5394573..7c8187f3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,7 +4,12 @@ db.sqlite3 _build /.direnv node_modules/ -/test-results/ -/playwright-report/ -/playwright/.cache/ celerybeat-schedule +Dockerfile +docker-compose.yml +LICENSE +flake.nix +flake.lock +_config.yml +README.md + diff --git a/core/management/commands/migrate_staff.py b/core/management/commands/migrate_staff.py index 32699e8b..b430b9c3 100644 --- a/core/management/commands/migrate_staff.py +++ b/core/management/commands/migrate_staff.py @@ -1,32 +1,47 @@ from django.core.management.base import BaseCommand from django.db import IntegrityError from django.conf import settings -from core.models import StaffMember, User # Update 'your_app' with the actual name of your app +from core.models import ( + StaffMember, + User, +) # Update 'your_app' with the actual name of your app class Command(BaseCommand): - help = 'Populate staff members based on METROPOLIS_STAFFS and bio settings.' - - def handle(self, *args, **options): - try: - for position, user_ids in settings.METROPOLIS_STAFFS.items(): - for user_id in user_ids: - try: - user = User.objects.get(pk=user_id) - bio = settings.METROPOLIS_STAFF_BIO.get(user_id, "") - if not bio: - print(f"Bio for user {user.username} ID:{user.id} is empty") - staff_member, created = StaffMember.objects.get_or_create( - user=user, bio=bio - ) - staff_member.positions = list(staff_member.positions) + [position] - staff_member.save() - except User.DoesNotExist: - self.stdout.write(self.style.ERROR(f"User {user_id} does not exist")) - except IntegrityError: - self.stdout.write(self.style.WARNING(f"StaffMember for user {user_id} already exists")) - - except AttributeError: - self.stdout.write(self.style.SUCCESS("METROPOLIS_STAFFS does not exist anymore")) - - self.stdout.write(self.style.SUCCESS("Command completed successfully")) + help = "Populate staff members based on METROPOLIS_STAFFS and bio settings." + + def handle(self, *args, **options): + try: + for position, user_ids in settings.METROPOLIS_STAFFS.items(): + for user_id in user_ids: + try: + user = User.objects.get(pk=user_id) + bio = settings.METROPOLIS_STAFF_BIO.get(user_id, "") + if not bio: + print( + f"Bio for user {user.username} ID:{user.id} is empty" + ) + staff_member, created = StaffMember.objects.get_or_create( + user=user, bio=bio + ) + staff_member.positions = list(staff_member.positions) + [ + position + ] + staff_member.save() + except User.DoesNotExist: + self.stdout.write( + self.style.ERROR(f"User {user_id} does not exist") + ) + except IntegrityError: + self.stdout.write( + self.style.WARNING( + f"StaffMember for user {user_id} already exists" + ) + ) + + except AttributeError: + self.stdout.write( + self.style.SUCCESS("METROPOLIS_STAFFS does not exist anymore") + ) + + self.stdout.write(self.style.SUCCESS("Command completed successfully")) diff --git a/core/migrations/0070_remove_staffmember_unique_staff_member_and_more.py b/core/migrations/0070_remove_staffmember_unique_staff_member_and_more.py new file mode 100644 index 00000000..26347d11 --- /dev/null +++ b/core/migrations/0070_remove_staffmember_unique_staff_member_and_more.py @@ -0,0 +1,80 @@ +# Generated by Django 5.0 on 2024-01-04 12:02 + +import core.utils.fields +import django.db.models.functions.text +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ("core", "0069_staffmember_alter_user_options_and_more"), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="staffmember", + name="unique_staff_member", + ), + migrations.AlterField( + model_name="staffmember", + name="positions", + field=core.utils.fields.ChoiceArrayField( + base_field=models.CharField( + choices=[ + ("Project Manager", "Project Manager"), + ("Frontend Developer", "Frontend Developer"), + ("Backend Developer", "Backend Developer"), + ("App Developer", "App Developer"), + ("Graphic Designer", "Graphic Designer"), + ("Content Creator", "Content Creator"), + ("Doodle Developer", "Doodle Developer"), + ] + ), + help_text="The positions the user had/does hold.", + size=None, + ), + ), + migrations.AlterField( + model_name="staffmember", + name="positions_leading", + field=core.utils.fields.ChoiceArrayField( + base_field=models.CharField( + choices=[ + ("Frontend Developer", "Frontend Developer"), + ("Backend Developer", "Backend Developer"), + ("App Developer", "App Developer"), + ("Graphic Designer", "Graphic Designer"), + ("Content Creator", "Content Creator"), + ("Doodle Developer", "Doodle Developer"), + ] + ), + blank=True, + null=True, + size=None, + ), + ), + migrations.AlterField( + model_name="staffmember", + name="years", + field=core.utils.fields.ChoiceArrayField( + base_field=models.CharField( + choices=[ + ("2021-2022", "2021-2022"), + ("2022-2023", "2022-2023"), + ("2023-2024", "2023-2024"), + ("2024-2025", "2024-2025"), + ] + ), + help_text="The years the user was a staff member. Used to determine if the user is an alumni.", + size=None, + ), + ), + migrations.AddConstraint( + model_name="user", + constraint=models.UniqueConstraint( + django.db.models.functions.text.Lower("username"), + name="username-lower-check", + ), + ), + ] diff --git a/core/models/user.py b/core/models/user.py index 52762b64..80e19c83 100644 --- a/core/models/user.py +++ b/core/models/user.py @@ -10,7 +10,7 @@ from .course import Term from .post import Announcement from ..utils import calculate_years -from ..utils.fields import SetField, ArrayField +from ..utils.fields import SetField, ChoiceArrayField # Create your models here. @@ -115,11 +115,11 @@ class StaffMember(models.Model): null=False, help_text="The users staff bio (displayed on the staff page).", ) - positions = ArrayField( + positions = ChoiceArrayField( base_field=CharField(choices=settings.METROPOLIS_POSITIONS), help_text="The positions the user had/does hold.", ) - positions_leading = ArrayField( + positions_leading = ChoiceArrayField( blank=True, null=True, base_field=CharField( @@ -131,7 +131,7 @@ class StaffMember(models.Model): ), ) - years = ArrayField( + years = ChoiceArrayField( base_field=CharField(choices=calculate_years(fmt="generate")), help_text="The years the user was a staff member. Used to determine if the user is an alumni.", ) @@ -150,9 +150,3 @@ def is_alumni(self): class Meta: verbose_name = "Staff Member" verbose_name_plural = "Staff Members" - constraints = [ - models.UniqueConstraint( - fields=["user"], - name="unique_staff_member", - ) - ] diff --git a/core/utils/fields.py b/core/utils/fields.py index 60395a92..163a008b 100644 --- a/core/utils/fields.py +++ b/core/utils/fields.py @@ -4,12 +4,20 @@ from django.conf import settings from django.core.exceptions import ValidationError -from django.db import models, connection -from django.db.models.fields import PositiveIntegerRelDbTypeMixin, SmallIntegerField, CharField +from django.db import models +from django.db.models.fields import PositiveIntegerRelDbTypeMixin, SmallIntegerField from django.forms import DateInput, DateField from django.utils import timezone from django.utils.dateparse import parse_date from django.utils.translation import gettext_lazy as _ +import json + +from django import forms +from django.db.models import JSONField as DjangoJSONField +from django.contrib.postgres.fields import ( + ArrayField as DjangoArrayField, +) +from django.db.models import Field class MonthDayFormField(DateField): @@ -158,30 +166,24 @@ def _get_val_from_obj(self, obj): return getattr(obj, self.attname) -import json - -from django import forms -from django.db.models import JSONField as DjangoJSONField -from django.contrib.postgres.fields import ( - ArrayField as DjangoArrayField, -) -from django.db.models import Field - - class JSONField(DjangoJSONField): pass +class _TypedMultipleChoiceField(forms.TypedMultipleChoiceField): + def __init__(self, *args, **kwargs): + kwargs.pop("base_field", None) + kwargs.pop("max_length", None) + super().__init__(*args, **kwargs) + + class ArrayField(DjangoArrayField): def formfield(self, **kwargs): defaults = { "form_class": forms.MultipleChoiceField, "choices": self.base_field.choices, } - kwargs.pop("base_field", None) defaults.update(kwargs) - - # Update the 'base_field' attribute with the instance of the base field return super(ArrayField, self).formfield(**defaults) @@ -240,3 +242,29 @@ def formfield(self, **kwargs): # care for it. # pylint:disable=bad-super-call return super().formfield(**defaults) + + +class ChoiceArrayField(ArrayField): # credit goes to https://github.com/anyidea + """ + A field that allows us to store an array of choices. + + Uses Django 4.2's postgres ArrayField + and a TypeMultipleChoiceField for its formfield. + + Usage: + + choices = ChoiceArrayField( + models.CharField(max_length=..., choices=(...,)), blank=[...], default=[...] + ) + """ + + def formfield(self, **kwargs): + defaults = { + "form_class": _TypedMultipleChoiceField, + "choices": self.base_field.choices, + "coerce": self.base_field.to_python, + } + defaults.update(kwargs) + # Skip our parent's formfield implementation completely as we don't care for it. + # pylint:disable=bad-super-call + return super().formfield(**defaults)