diff --git a/TeamForge/asgi.py b/TeamForge/asgi.py index 5ca4e15..f32bd51 100644 --- a/TeamForge/asgi.py +++ b/TeamForge/asgi.py @@ -11,6 +11,6 @@ import os from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'TeamForge.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "TeamForge.settings") application = get_asgi_application() diff --git a/TeamForge/settings.py b/TeamForge/settings.py index db77022..3f9a4ed 100644 --- a/TeamForge/settings.py +++ b/TeamForge/settings.py @@ -14,7 +14,6 @@ from pathlib import Path from decouple import Csv, config from dj_database_url import parse as db_url -from django.template.context_processors import static # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent diff --git a/TeamForge/urls.py b/TeamForge/urls.py index de4aa5a..48efc4e 100644 --- a/TeamForge/urls.py +++ b/TeamForge/urls.py @@ -14,10 +14,11 @@ Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ + from django.contrib import admin from django.urls import include, path urlpatterns = [ - path('backend/', include('backend.urls')), - path('admin/', admin.site.urls), + path("backend/", include("backend.urls")), + path("admin/", admin.site.urls), ] diff --git a/TeamForge/wsgi.py b/TeamForge/wsgi.py index 26824a0..1b9eab2 100644 --- a/TeamForge/wsgi.py +++ b/TeamForge/wsgi.py @@ -11,6 +11,6 @@ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'TeamForge.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "TeamForge.settings") application = get_wsgi_application() diff --git a/backend/members/urls.py b/backend/members/urls.py index beb8cce..80754cb 100644 --- a/backend/members/urls.py +++ b/backend/members/urls.py @@ -1,4 +1,4 @@ -from django.urls import include, path +from django.urls import path from .views import MemberAddView, MemberDeleteView, MemberEditView, MemberListView, MemberLoadView diff --git a/backend/mixins.py b/backend/mixins.py index 0880a40..b975082 100644 --- a/backend/mixins.py +++ b/backend/mixins.py @@ -4,19 +4,20 @@ from django.http import HttpResponse from django.template.loader import render_to_string from django_htmx.http import HttpResponseClientRedirect, HttpResponseClientRefresh + class HTMXPartialMixin: """Mixin that automatically switches to a partial template when the request is made via HTMX.""" - + partial_template_name = None - + def get_template_names(self): # If HTMX request and a partial is defined, return it if getattr(self.request, "htmx", False) and self.partial_template_name: return [self.partial_template_name] - + return super().get_template_names() - - + + class HTMXViewMixin: """ A full-featured HTMX integration mixin for Django CBVs. @@ -28,81 +29,81 @@ class HTMXViewMixin: - HX-Refresh - Graceful fallback to normal Django rendering """ - + # Name of the partial block: template.html#partial_name partial_name = None - + # Optional: automatically push URL on GET htmx_push_url = None - + # Optional: name of the menu item to highlight menu_highlight = None - + # Optional: trigger events after rendering htmx_trigger = None htmx_trigger_after_settle = None htmx_trigger_after_swap = None - + # Optional: redirect target for HTMX htmx_redirect_url = None - + # Optional: refresh the page htmx_refresh = False - + def render_partial(self, context): """Render a django-partials block.""" request = self.request return render_to_string(self.partial_name, context, request=request) - + def apply_htmx_headers(self, response): """Attach HX-* headers to the response.""" request = self.request - + if request.htmx: is_get = request.method == "GET" is_pagination = "page" in request.GET - + if is_get and not is_pagination: # Push the current path unless overridden response.headers["HX-Push-Url"] = self.htmx_push_url or request.get_full_path() # Build HX-Trigger payload trigger_payload = {} - + # 1. User-defined triggers if self.htmx_trigger: trigger_payload.update(json.loads(self.htmx_trigger)) - + # 2. Auto menu highlight trigger if self.menu_highlight: trigger_payload["menuHighlight"] = self.menu_highlight - + # Emit HX-Trigger if anything is present if trigger_payload: response.headers["HX-Trigger"] = json.dumps(trigger_payload) - + if self.htmx_trigger_after_settle: response.headers["HX-Trigger-After-Settle"] = self.htmx_trigger_after_settle - + if self.htmx_trigger_after_swap: response.headers["HX-Trigger-After-Swap"] = self.htmx_trigger_after_swap - + return response - + def render_to_response(self, context, **response_kwargs): """Renders HTMX response, applying headers and handling directives""" request = self.request - + if not request.htmx: response = super().render_to_response(context, **response_kwargs) return self.apply_htmx_headers(response) - + if self.htmx_redirect_url: return HttpResponseClientRedirect(self.htmx_redirect_url) - + if self.htmx_refresh: return HttpResponseClientRefresh() - + html = self.render_partial(context) response = HttpResponse(html, **response_kwargs) - return self.apply_htmx_headers(response) \ No newline at end of file + return self.apply_htmx_headers(response) diff --git a/backend/urls.py b/backend/urls.py index 2f7a005..e1f40c8 100644 --- a/backend/urls.py +++ b/backend/urls.py @@ -3,8 +3,4 @@ from django.urls import include, path from .views import configuration, index app_name = "backend" -urlpatterns = [ - path("", index, name="index"), - path("members/", include("backend.members.urls")), - path("configuration", configuration, name="configuration") -] +urlpatterns = [path("", index, name="index"), path("members/", include("backend.members.urls")), path("configuration", configuration, name="configuration")] diff --git a/manage.py b/manage.py index b10688e..dd04b2a 100755 --- a/manage.py +++ b/manage.py @@ -1,22 +1,19 @@ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" + import os import sys def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'TeamForge.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "TeamForge.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc + raise ImportError("Couldn't import Django. Are you sure it's installed and available on your PYTHONPATH environment variable? Did you forget to activate a virtual environment?") from exc execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/members/apps.py b/members/apps.py index e5e55eb..5985282 100644 --- a/members/apps.py +++ b/members/apps.py @@ -5,5 +5,4 @@ class MembersConfig(AppConfig): name = "members" def ready(self): - # noinspection PyUnusedImports - import members.signals + import members.signals # noqa: F401 diff --git a/members/filters.py b/members/filters.py index 0e6820d..d60d750 100644 --- a/members/filters.py +++ b/members/filters.py @@ -8,10 +8,18 @@ class MemberFilter(django_filters.FilterSet): user__first_name = django_filters.CharFilter(field_name="user__first_name", label=_("First name"), lookup_expr="icontains") user__last_name = django_filters.CharFilter(field_name="user__last_name", label=_("Last name"), lookup_expr="icontains") license = django_filters.CharFilter(label=_("License"), lookup_expr="icontains") - user__is_active = django_filters.TypedChoiceFilter( field_name='user__is_active', label=_("Active?"), initial="true", choices=( ('', 'All users'), ('true', 'Active users'), ('false', 'Inactive users'), ), coerce=lambda x: x.lower() == 'true' ) - + user__is_active = django_filters.TypedChoiceFilter( + field_name="user__is_active", + label=_("Active?"), + initial="true", + choices=( + ("", "All users"), + ("true", "Active users"), + ("false", "Inactive users"), + ), + coerce=lambda x: x.lower() == "true", + ) + class Meta: model = Member fields = ["user__first_name", "user__last_name", "license", "user__is_active"] - - \ No newline at end of file diff --git a/members/migrations/0001_initial.py b/members/migrations/0001_initial.py index 2f47b5e..5b65344 100644 --- a/members/migrations/0001_initial.py +++ b/members/migrations/0001_initial.py @@ -7,7 +7,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/members/migrations/0002_member_family.py b/members/migrations/0002_member_family.py index f98ee47..e585d00 100644 --- a/members/migrations/0002_member_family.py +++ b/members/migrations/0002_member_family.py @@ -4,7 +4,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("members", "0001_initial"), ] diff --git a/members/migrations/0003_member_created_member_updated_alter_member_family.py b/members/migrations/0003_member_created_member_updated_alter_member_family.py index 86cb857..615cd8b 100644 --- a/members/migrations/0003_member_created_member_updated_alter_member_family.py +++ b/members/migrations/0003_member_created_member_updated_alter_member_family.py @@ -5,7 +5,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("members", "0002_member_family"), ] @@ -29,8 +28,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="member", name="family", - field=models.ManyToManyField( - blank=True, to="members.member", verbose_name="family" - ), + field=models.ManyToManyField(blank=True, to="members.member", verbose_name="family"), ), ] diff --git a/members/migrations/0004_member_access_token_member_birthday_and_more.py b/members/migrations/0004_member_access_token_member_birthday_and_more.py index 2747b2c..cd94680 100644 --- a/members/migrations/0004_member_access_token_member_birthday_and_more.py +++ b/members/migrations/0004_member_access_token_member_birthday_and_more.py @@ -5,7 +5,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("members", "0003_member_created_member_updated_alter_member_family"), ] @@ -14,9 +13,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name="member", name="access_token", - field=models.CharField( - blank=True, max_length=255, null=True, verbose_name="access token" - ), + field=models.CharField(blank=True, max_length=255, null=True, verbose_name="access token"), ), migrations.AddField( model_name="member", @@ -37,9 +34,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name="member", name="license", - field=models.CharField( - blank=True, max_length=20, null=True, verbose_name="license" - ), + field=models.CharField(blank=True, max_length=20, null=True, verbose_name="license"), ), migrations.AddField( model_name="member", diff --git a/members/migrations/0005_remove_member_family_member_family_members.py b/members/migrations/0005_remove_member_family_member_family_members.py index c14fcbc..e76fd6e 100644 --- a/members/migrations/0005_remove_member_family_member_family_members.py +++ b/members/migrations/0005_remove_member_family_member_family_members.py @@ -4,7 +4,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("members", "0004_member_access_token_member_birthday_and_more"), ] @@ -17,8 +16,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name="member", name="family_members", - field=models.ManyToManyField( - blank=True, to="members.member", verbose_name="family members" - ), + field=models.ManyToManyField(blank=True, to="members.member", verbose_name="family members"), ), ] diff --git a/members/migrations/0006_member_notes.py b/members/migrations/0006_member_notes.py index c8a7261..8d74231 100644 --- a/members/migrations/0006_member_notes.py +++ b/members/migrations/0006_member_notes.py @@ -4,15 +4,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('members', '0005_remove_member_family_member_family_members'), + ("members", "0005_remove_member_family_member_family_members"), ] operations = [ migrations.AddField( - model_name='member', - name='notes', - field=models.TextField(blank=True, null=True, verbose_name='notes'), + model_name="member", + name="notes", + field=models.TextField(blank=True, null=True, verbose_name="notes"), ), ] diff --git a/members/models.py b/members/models.py index 1cd7c36..5ee068b 100644 --- a/members/models.py +++ b/members/models.py @@ -54,26 +54,19 @@ class Member(RulesModel): @classmethod def create(cls, first_name: str, last_name: str, email: str, password: Optional[str] = None, member: Optional["Member"] = None) -> "Member": """Creates a new member based on the provided details""" - + if member is not None and member.pk is not None: member.user.first_name = first_name member.user.last_name = last_name member.user.email = email member.user.username = email - + if password is not None and password != "": member.user.set_password(password) - + else: # First check to see if a user already exists in the system - user, created = get_user_model().objects.get_or_create( - username=email, - defaults={ - "first_name": first_name, - "last_name": last_name, - "email": email - } - ) + user, created = get_user_model().objects.get_or_create(username=email, defaults={"first_name": first_name, "last_name": last_name, "email": email}) if not created: user.first_name = first_name @@ -102,11 +95,11 @@ class Member(RulesModel): user.set_password(password) member.user = user - + if not member.user.is_active: member.user.is_active = True - + member.user.save() member.save() - - return member \ No newline at end of file + + return member diff --git a/members/rules.py b/members/rules.py index 565c622..5f71c37 100644 --- a/members/rules.py +++ b/members/rules.py @@ -6,4 +6,4 @@ from django.contrib.auth.models import AbstractUser @rules.predicate def is_member_manager(user: Optional[AbstractUser]) -> bool: - return user.has_perm('members.member_manager') \ No newline at end of file + return user.has_perm("members.member_manager") diff --git a/teams/admin.py b/teams/admin.py index 8c38f3f..846f6b4 100644 --- a/teams/admin.py +++ b/teams/admin.py @@ -1,3 +1 @@ -from django.contrib import admin - # Register your models here. diff --git a/teams/migrations/0001_initial.py b/teams/migrations/0001_initial.py index a03234d..b4b213d 100644 --- a/teams/migrations/0001_initial.py +++ b/teams/migrations/0001_initial.py @@ -5,7 +5,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [] diff --git a/teams/migrations/0002_team_teamrole_teammembership_team_members_and_more.py b/teams/migrations/0002_team_teamrole_teammembership_team_members_and_more.py index 16d8dad..fe8b3e9 100644 --- a/teams/migrations/0002_team_teamrole_teammembership_team_members_and_more.py +++ b/teams/migrations/0002_team_teamrole_teammembership_team_members_and_more.py @@ -9,95 +9,94 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('members', '0006_member_notes'), - ('teams', '0001_initial'), + ("members", "0006_member_notes"), + ("teams", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Team', + name="Team", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255, unique=True, verbose_name='name')), - ('short_name', models.CharField(blank=True, help_text='An optional short name for the team', max_length=255, null=True, unique=True, verbose_name='short name')), - ('slug', django_extensions.db.fields.AutoSlugField(blank=True, editable=False, max_length=255, populate_from='name', unique=True, verbose_name='slug')), - ('logo', models.ImageField(blank=True, null=True, upload_to='teams/logo/', verbose_name='logo')), - ('created', models.DateTimeField(auto_now_add=True)), - ('updated', models.DateTimeField(auto_now=True)), + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=255, unique=True, verbose_name="name")), + ("short_name", models.CharField(blank=True, help_text="An optional short name for the team", max_length=255, null=True, unique=True, verbose_name="short name")), + ("slug", django_extensions.db.fields.AutoSlugField(blank=True, editable=False, max_length=255, populate_from="name", unique=True, verbose_name="slug")), + ("logo", models.ImageField(blank=True, null=True, upload_to="teams/logo/", verbose_name="logo")), + ("created", models.DateTimeField(auto_now_add=True)), + ("updated", models.DateTimeField(auto_now=True)), ], options={ - 'verbose_name': 'team', - 'verbose_name_plural': 'teams', - 'ordering': ['name'], + "verbose_name": "team", + "verbose_name_plural": "teams", + "ordering": ["name"], }, bases=(rules.contrib.models.RulesModelMixin, models.Model), ), migrations.CreateModel( - name='TeamRole', + name="TeamRole", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255, unique=True, verbose_name='name')), - ('abbreviation', models.CharField(help_text='An abbreviated version of the role name', max_length=255, unique=True, verbose_name='abbreviation')), - ('staff_role', models.BooleanField(default=False, help_text='A staff role is any supporting function for a given team (e.g., coach, team manager, ...)', verbose_name='staff role')), - ('admin_role', models.BooleanField(default=False, help_text='An admin role is a role that has administrative privileges and can change team information and settings', verbose_name='admin role')), - ('sort_order', models.PositiveIntegerField(default=10, help_text='The order in which the role should be displayed (low to high)', verbose_name='sort order')), - ('created', models.DateTimeField(auto_now_add=True)), - ('updated', models.DateTimeField(auto_now=True)), + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=255, unique=True, verbose_name="name")), + ("abbreviation", models.CharField(help_text="An abbreviated version of the role name", max_length=255, unique=True, verbose_name="abbreviation")), + ("staff_role", models.BooleanField(default=False, help_text="A staff role is any supporting function for a given team (e.g., coach, team manager, ...)", verbose_name="staff role")), + ("admin_role", models.BooleanField(default=False, help_text="An admin role is a role that has administrative privileges and can change team information and settings", verbose_name="admin role")), + ("sort_order", models.PositiveIntegerField(default=10, help_text="The order in which the role should be displayed (low to high)", verbose_name="sort order")), + ("created", models.DateTimeField(auto_now_add=True)), + ("updated", models.DateTimeField(auto_now=True)), ], options={ - 'verbose_name': 'team role', - 'verbose_name_plural': 'team roles', - 'ordering': ['sort_order'], + "verbose_name": "team role", + "verbose_name_plural": "team roles", + "ordering": ["sort_order"], }, bases=(rules.contrib.models.RulesModelMixin, models.Model), ), migrations.CreateModel( - name='TeamMembership', + name="TeamMembership", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('number', models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(99)], verbose_name='number')), - ('captain', models.BooleanField(default=False, verbose_name='captain')), - ('alternate_captain', models.BooleanField(default=False, verbose_name='alternate captain')), - ('created', models.DateTimeField(auto_now_add=True)), - ('modified', models.DateTimeField(auto_now=True)), - ('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team_memberships', to='members.member', verbose_name='member')), - ('season', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team_memberships', to='teams.season', verbose_name='season')), - ('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='teams.team', verbose_name='team')), - ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team_memberships', to='teams.teamrole', verbose_name='role')), + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("number", models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(99)], verbose_name="number")), + ("captain", models.BooleanField(default=False, verbose_name="captain")), + ("alternate_captain", models.BooleanField(default=False, verbose_name="alternate captain")), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ("member", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="team_memberships", to="members.member", verbose_name="member")), + ("season", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="team_memberships", to="teams.season", verbose_name="season")), + ("team", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="teams.team", verbose_name="team")), + ("role", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="team_memberships", to="teams.teamrole", verbose_name="role")), ], options={ - 'verbose_name': 'team membership', - 'verbose_name_plural': 'team memberships', - 'ordering': ['team__name', 'role__sort_order', 'number', 'member__user__last_name', 'member__user__first_name'], + "verbose_name": "team membership", + "verbose_name_plural": "team memberships", + "ordering": ["team__name", "role__sort_order", "number", "member__user__last_name", "member__user__first_name"], }, bases=(rules.contrib.models.RulesModelMixin, models.Model), ), migrations.AddField( - model_name='team', - name='members', - field=models.ManyToManyField(through='teams.TeamMembership', to='members.member', verbose_name='members'), + model_name="team", + name="members", + field=models.ManyToManyField(through="teams.TeamMembership", to="members.member", verbose_name="members"), ), migrations.CreateModel( - name='TeamPicture', + name="TeamPicture", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('picture', models.ImageField(upload_to=teams.models.team_season_file_path, verbose_name='picture')), - ('created', models.DateTimeField(auto_now_add=True)), - ('modified', models.DateTimeField(auto_now=True)), - ('season', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='teams.season', verbose_name='season')), - ('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='teams.team', verbose_name='team')), + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("picture", models.ImageField(upload_to=teams.models.team_season_file_path, verbose_name="picture")), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ("season", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="teams.season", verbose_name="season")), + ("team", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="teams.team", verbose_name="team")), ], options={ - 'verbose_name': 'team picture', - 'verbose_name_plural': 'team pictures', - 'ordering': ['team__name', 'season__start_date'], + "verbose_name": "team picture", + "verbose_name_plural": "team pictures", + "ordering": ["team__name", "season__start_date"], }, bases=(rules.contrib.models.RulesModelMixin, models.Model), ), migrations.AddConstraint( - model_name='teammembership', - constraint=models.UniqueConstraint(fields=('team', 'season', 'number'), name='unique_team_membership_number', violation_error_message='A team can only have one member for a given season and number.'), + model_name="teammembership", + constraint=models.UniqueConstraint(fields=("team", "season", "number"), name="unique_team_membership_number", violation_error_message="A team can only have one member for a given season and number."), ), ] diff --git a/teams/models.py b/teams/models.py index 0018b7c..60dd451 100644 --- a/teams/models.py +++ b/teams/models.py @@ -18,6 +18,7 @@ def team_season_file_path(instance: "TeamPicture", filename: str) -> str: """Generates the file path for a team picture based on the team name and season.""" return f"teams/picture/{instance.team.slug}/{instance.season.start_date.year}/{filename}" + class Season(RulesModel): start_date = models.DateField(_("start date")) end_date = models.DateField(_("end date")) @@ -52,7 +53,6 @@ class Season(RulesModel): if season is None: raise cls.DoesNotExist(f"No Season covers date {current_date}") - if values_only: return season.date_range return season @@ -87,77 +87,80 @@ class Season(RulesModel): return date_value.replace(year=year, month=month, day=day) + class TeamRole(RulesModel): name = models.CharField(_("name"), max_length=255, unique=True) abbreviation = models.CharField(_("abbreviation"), max_length=255, unique=True, help_text=_("An abbreviated version of the role name")) - + staff_role = models.BooleanField(_("staff role"), default=False, help_text=_("A staff role is any supporting function for a given team (e.g., coach, team manager, ...)")) admin_role = models.BooleanField(_("admin role"), default=False, help_text=_("An admin role is a role that has administrative privileges and can change team information and settings")) - + sort_order = models.PositiveIntegerField(_("sort order"), default=10, help_text=_("The order in which the role should be displayed (low to high)")) - + created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) - + class Meta: verbose_name = _("team role") verbose_name_plural = _("team roles") ordering = ["sort_order"] rules_permissions = {"add": is_superuser, "view": is_superuser, "change": is_superuser, "delete": is_superuser} - + def __str__(self): return f"{self.name} ({self.abbreviation})" - + + class Team(RulesModel): name = models.CharField(_("name"), max_length=255, unique=True) short_name = models.CharField(_("short name"), max_length=255, unique=True, blank=True, null=True, help_text=_("An optional short name for the team")) slug = AutoSlugField(_("slug"), max_length=255, unique=True, populate_from="name") logo = models.ImageField(_("logo"), upload_to="teams/logo/", blank=True, null=True) - + members = models.ManyToManyField("members.Member", verbose_name=_("members"), through="TeamMembership") - + created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) - + class Meta: verbose_name = _("team") verbose_name_plural = _("teams") ordering = ["name"] rules_permissions = {"add": is_superuser, "view": is_superuser, "change": is_superuser, "delete": is_superuser} - + def __str__(self): return self.name - + @property def initials(self) -> str: words = self.name.split() if len(words) == 1: return words[0][0].upper() - + return "".join(word[0].upper() for word in words[:2]) - + @property def member_count(self) -> int: season = Season.for_date() return self.members.filter(team_memberships__season=season).count() - + def get_short_name(self) -> str: """Returns the short name of the team, or the name if no short name is set.""" return self.short_name if self.short_name and self.short_name != "" else self.name + class TeamMembership(RulesModel): team = models.ForeignKey(Team, on_delete=models.CASCADE, verbose_name=_("team")) member = models.ForeignKey("members.Member", on_delete=models.CASCADE, related_name="team_memberships", verbose_name=_("member")) season = models.ForeignKey(Season, on_delete=models.CASCADE, related_name="team_memberships", verbose_name=_("season")) role = models.ForeignKey(TeamRole, on_delete=models.CASCADE, related_name="team_memberships", verbose_name=_("role")) - + number = models.PositiveIntegerField(_("number"), blank=True, null=True, validators=[MinValueValidator(1), MaxValueValidator(99)]) captain = models.BooleanField(_("captain"), default=False) alternate_captain = models.BooleanField(_("alternate captain"), default=False) - + created = models.DateTimeField(auto_now_add=True) modified = models.DateTimeField(auto_now=True) - + class Meta: verbose_name = _("team membership") verbose_name_plural = _("team memberships") @@ -166,23 +169,24 @@ class TeamMembership(RulesModel): models.UniqueConstraint(fields=["team", "season", "number"], name="unique_team_membership_number", violation_error_message=_("A team can only have one member for a given season and number.")), ] rules_permissions = {"add": is_superuser | is_team_admin, "view": is_superuser | is_team_admin, "change": is_superuser | is_team_admin, "delete": is_superuser | is_team_admin} - + def __str__(self): return f"{self.team.name} - {self.member.user.get_full_name()} ({self.role.abbreviation})" + class TeamPicture(RulesModel): team = models.ForeignKey(Team, on_delete=models.CASCADE, verbose_name=_("team")) season = models.ForeignKey(Season, on_delete=models.CASCADE, verbose_name=_("season")) picture = models.ImageField(_("picture"), upload_to=team_season_file_path) - + created = models.DateTimeField(auto_now_add=True) modified = models.DateTimeField(auto_now=True) - + class Meta: verbose_name = _("team picture") verbose_name_plural = _("team pictures") ordering = ["team__name", "season__start_date"] rules_permissions = {"add": is_superuser, "view": is_superuser, "change": is_superuser, "delete": is_superuser} - + def __str__(self): - return f"{self.team.name} - {self.season.start_date.year}" \ No newline at end of file + return f"{self.team.name} - {self.season.start_date.year}" diff --git a/teams/rules.py b/teams/rules.py index 8b193ba..c4e53ac 100644 --- a/teams/rules.py +++ b/teams/rules.py @@ -4,7 +4,7 @@ import rules from django.contrib.auth.models import AbstractUser if TYPE_CHECKING: - from .models import TeamMembership, Season + from .models import TeamMembership @rules.predicate @@ -18,10 +18,10 @@ def is_team_admin(user: AbstractUser | None, teammembership: "TeamMembership | N team membership. """ from .models import TeamMembership, Season - + if user is None or teammembership is None: return False - + return TeamMembership.objects.filter(team=teammembership.team, member__user=user, role__admin_role=True, season=Season.for_date()).exists() @@ -34,10 +34,10 @@ def is_a_team_admin(user: AbstractUser | None) -> bool: :return: A boolean indicating whether the user is a team admin for the given team membership. """ - + from .models import Season, TeamMembership - + if user is None: return False - - return TeamMembership.objects.filter(member__user=user, role__admin_role=True, season=Season.for_date()).exists() \ No newline at end of file + + return TeamMembership.objects.filter(member__user=user, role__admin_role=True, season=Season.for_date()).exists() diff --git a/teams/tests.py b/teams/tests.py index b203afa..cd96e35 100644 --- a/teams/tests.py +++ b/teams/tests.py @@ -4,7 +4,6 @@ from django.contrib.auth import get_user_model from django.test import TestCase from django.utils import timezone -from members.models import Member from teams.models import Season, Team, TeamMembership, TeamPicture, TeamRole User = get_user_model() @@ -14,6 +13,7 @@ User = get_user_model() # Helpers # --------------------------------------------------------------------------- + def make_member(email, first="Test", last="User"): user = User.objects.create_user(username=email, email=email, first_name=first, last_name=last) return user.member @@ -35,6 +35,7 @@ def make_team(name="Test Team"): # Season # --------------------------------------------------------------------------- + class SeasonStrTest(TestCase): def test_str(self): season = Season(start_date=datetime.date(2025, 9, 1), end_date=datetime.date(2026, 8, 31)) @@ -127,6 +128,7 @@ class SeasonAddMonthsTest(TestCase): # TeamRole # --------------------------------------------------------------------------- + class TeamRoleStrTest(TestCase): def test_str(self): role = TeamRole(name="Coach", abbreviation="C") @@ -143,6 +145,7 @@ class TeamRoleStrTest(TestCase): # Team # --------------------------------------------------------------------------- + class TeamStrTest(TestCase): def test_str(self): team = Team(name="Red Dragons") @@ -199,6 +202,7 @@ class TeamMemberCountTest(TestCase): # TeamMembership # --------------------------------------------------------------------------- + class TeamMembershipStrTest(TestCase): def setUp(self): today = timezone.now().date() @@ -209,15 +213,14 @@ class TeamMembershipStrTest(TestCase): self.team = make_team("Blue Hawks") self.role = make_role("Forward", "F") self.member = make_member("player@test.com", "John", "Doe") - self.membership = TeamMembership.objects.create( - team=self.team, member=self.member, season=self.season, role=self.role - ) + self.membership = TeamMembership.objects.create(team=self.team, member=self.member, season=self.season, role=self.role) def test_str(self): self.assertEqual(str(self.membership), "Blue Hawks - John Doe (F)") def test_unique_number_constraint(self): from django.db import IntegrityError + member2 = make_member("player2@test.com", "Jane", "Doe") TeamMembership.objects.create(team=self.team, member=member2, season=self.season, role=self.role, number=7) with self.assertRaises(IntegrityError): @@ -232,6 +235,7 @@ class TeamMembershipStrTest(TestCase): # TeamPicture # --------------------------------------------------------------------------- + class TeamPictureStrTest(TestCase): def setUp(self): self.season = Season.objects.create( @@ -242,4 +246,4 @@ class TeamPictureStrTest(TestCase): self.picture = TeamPicture(team=self.team, season=self.season, picture="teams/picture/green-eagles/2025/photo.jpg") def test_str(self): - self.assertEqual(str(self.picture), "Green Eagles - 2025") \ No newline at end of file + self.assertEqual(str(self.picture), "Green Eagles - 2025") diff --git a/theme/apps.py b/theme/apps.py index bec7464..dd24136 100644 --- a/theme/apps.py +++ b/theme/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class ThemeConfig(AppConfig): - name = 'theme' + name = "theme" diff --git a/theme/templatetags/avatar.py b/theme/templatetags/avatar.py index e57896f..58b5d9e 100644 --- a/theme/templatetags/avatar.py +++ b/theme/templatetags/avatar.py @@ -5,29 +5,33 @@ from django import template register = template.Library() + def calculate_brightness(background_color: dict) -> float: """Calculates the brightness of a background image.""" r_coefficient = 0.241 g_coefficient = 0.691 b_coefficient = 0.068 - - return sqrt(r_coefficient ** 2 * background_color["R"] + g_coefficient ** 2 * background_color["G"] + b_coefficient ** 2 * background_color["B"]) * 100 + + return sqrt(r_coefficient**2 * background_color["R"] + g_coefficient**2 * background_color["G"] + b_coefficient**2 * background_color["B"]) * 100 + def foreground(background_color: dict) -> dict: """Calculates the foreground color based on the background.""" black = {"R": 0, "G": 0, "B": 0} white = {"R": 255, "G": 255, "B": 255} - + return black if calculate_brightness(background_color) > 210 else white + def background(text: str) -> dict: """Calculates the background color based on the text.""" hash_value = md5(text.encode("utf-8")).hexdigest() hash_value_values = (hash_value[:8], hash_value[8:16], hash_value[16:24]) background_color = tuple(int(value, 16) % 256 for value in hash_value_values) - + return {"R": background_color[0], "G": background_color[1], "B": background_color[2]} + @register.inclusion_tag("templatetags/avatar.html") def avatar(first_name: str = "", last_name: str = "", initials: str = "", width: str = "md", button: bool = False) -> dict: if initials: @@ -36,14 +40,14 @@ def avatar(first_name: str = "", last_name: str = "", initials: str = "", width: else: display_name = f"{first_name[0]}{last_name[0]}" full_name = f"{first_name} {last_name}" - + avatar_background = background(full_name) avatar_foreground = foreground(avatar_background) - + return { "name": display_name, "width": width, "button": button, "background": "#%02x%02x%02x" % (avatar_background["R"], avatar_background["G"], avatar_background["B"]), # noqa: UP031 "foreground": "#%02x%02x%02x" % (avatar_foreground["R"], avatar_foreground["G"], avatar_foreground["B"]), # noqa: UP031 - } \ No newline at end of file + } diff --git a/theme/templatetags/form_field.py b/theme/templatetags/form_field.py index 0359175..cbd40d2 100644 --- a/theme/templatetags/form_field.py +++ b/theme/templatetags/form_field.py @@ -4,40 +4,41 @@ from typing import Optional register = template.Library() + @register.inclusion_tag("templatetags/field.html") def form_field(field: BoundField, label: Optional[str] = None, help_text: Optional[str] = None, show_label: bool = True, show_help_text: bool = True, show_placeholder: bool = True, show_as_toggle: bool = False, size: str = "full") -> dict: if label is not None: field.label = label - + if help_text is not None: field.help_text = help_text - + field_type = None match field.widget_type: case "select" | "nullbooleanselect" | "radioselect" | "selectmultiple": field_type = "select" - + case "checkbox": field_type = "checkbox" - + case "textarea" | "markdownx": field_type = "textarea" - + case "clearablefile": field_type = "file" - + case _: field_type = "input" - + size_modifier = None match size: case "extra-small": size_modifier = "xs" - + case "small": size_modifier = "sm" - + case _: pass - - return {"field": field, "field_type": field_type, "size_modifier": size_modifier, "show_label": show_label, "show_help_text": show_help_text, "show_placeholder": show_placeholder, "show_as_toggle": show_as_toggle} \ No newline at end of file + + return {"field": field, "field_type": field_type, "size_modifier": size_modifier, "show_label": show_label, "show_help_text": show_help_text, "show_placeholder": show_placeholder, "show_as_toggle": show_as_toggle} diff --git a/theme/templatetags/pagination.py b/theme/templatetags/pagination.py index 906b66f..7f5bf05 100644 --- a/theme/templatetags/pagination.py +++ b/theme/templatetags/pagination.py @@ -5,13 +5,14 @@ from django.http import HttpRequest register = template.Library() + @register.simple_tag def url_replace(request: HttpRequest, field: str, value: str | int, default_field: Optional[str] = None, default_value: Optional[str | int] = None) -> str: """Updates the given field in the GET parameters with the supplied field. If it does not exist, the field is added.""" dict_ = request.GET.copy() dict_[field] = value - + if default_field is not None and default_field not in dict_.keys(): dict_[default_field] = default_value - - return dict_.urlencode() \ No newline at end of file + + return dict_.urlencode()