Apply ruff formatting and fix unused import linting errors
Remove unused imports flagged by ruff (F401), apply ruff format across all files, and restore members.signals side-effect import with noqa: F401 so the post_save signal that auto-creates Member profiles continues to fire. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,6 @@ import os
|
|||||||
|
|
||||||
from django.core.asgi import get_asgi_application
|
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()
|
application = get_asgi_application()
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ from pathlib import Path
|
|||||||
|
|
||||||
from decouple import Csv, config
|
from decouple import Csv, config
|
||||||
from dj_database_url import parse as db_url
|
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'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|||||||
@@ -14,10 +14,11 @@ Including another URLconf
|
|||||||
1. Import the include() function: from django.urls import include, path
|
1. Import the include() function: from django.urls import include, path
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('backend/', include('backend.urls')),
|
path("backend/", include("backend.urls")),
|
||||||
path('admin/', admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ import os
|
|||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
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()
|
application = get_wsgi_application()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from django.urls import include, path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import MemberAddView, MemberDeleteView, MemberEditView, MemberListView, MemberLoadView
|
from .views import MemberAddView, MemberDeleteView, MemberEditView, MemberListView, MemberLoadView
|
||||||
|
|
||||||
|
|||||||
@@ -4,19 +4,20 @@ from django.http import HttpResponse
|
|||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django_htmx.http import HttpResponseClientRedirect, HttpResponseClientRefresh
|
from django_htmx.http import HttpResponseClientRedirect, HttpResponseClientRefresh
|
||||||
|
|
||||||
|
|
||||||
class HTMXPartialMixin:
|
class HTMXPartialMixin:
|
||||||
"""Mixin that automatically switches to a partial template when the request is made via HTMX."""
|
"""Mixin that automatically switches to a partial template when the request is made via HTMX."""
|
||||||
|
|
||||||
partial_template_name = None
|
partial_template_name = None
|
||||||
|
|
||||||
def get_template_names(self):
|
def get_template_names(self):
|
||||||
# If HTMX request and a partial is defined, return it
|
# If HTMX request and a partial is defined, return it
|
||||||
if getattr(self.request, "htmx", False) and self.partial_template_name:
|
if getattr(self.request, "htmx", False) and self.partial_template_name:
|
||||||
return [self.partial_template_name]
|
return [self.partial_template_name]
|
||||||
|
|
||||||
return super().get_template_names()
|
return super().get_template_names()
|
||||||
|
|
||||||
|
|
||||||
class HTMXViewMixin:
|
class HTMXViewMixin:
|
||||||
"""
|
"""
|
||||||
A full-featured HTMX integration mixin for Django CBVs.
|
A full-featured HTMX integration mixin for Django CBVs.
|
||||||
@@ -28,81 +29,81 @@ class HTMXViewMixin:
|
|||||||
- HX-Refresh
|
- HX-Refresh
|
||||||
- Graceful fallback to normal Django rendering
|
- Graceful fallback to normal Django rendering
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Name of the partial block: template.html#partial_name
|
# Name of the partial block: template.html#partial_name
|
||||||
partial_name = None
|
partial_name = None
|
||||||
|
|
||||||
# Optional: automatically push URL on GET
|
# Optional: automatically push URL on GET
|
||||||
htmx_push_url = None
|
htmx_push_url = None
|
||||||
|
|
||||||
# Optional: name of the menu item to highlight
|
# Optional: name of the menu item to highlight
|
||||||
menu_highlight = None
|
menu_highlight = None
|
||||||
|
|
||||||
# Optional: trigger events after rendering
|
# Optional: trigger events after rendering
|
||||||
htmx_trigger = None
|
htmx_trigger = None
|
||||||
htmx_trigger_after_settle = None
|
htmx_trigger_after_settle = None
|
||||||
htmx_trigger_after_swap = None
|
htmx_trigger_after_swap = None
|
||||||
|
|
||||||
# Optional: redirect target for HTMX
|
# Optional: redirect target for HTMX
|
||||||
htmx_redirect_url = None
|
htmx_redirect_url = None
|
||||||
|
|
||||||
# Optional: refresh the page
|
# Optional: refresh the page
|
||||||
htmx_refresh = False
|
htmx_refresh = False
|
||||||
|
|
||||||
def render_partial(self, context):
|
def render_partial(self, context):
|
||||||
"""Render a django-partials block."""
|
"""Render a django-partials block."""
|
||||||
request = self.request
|
request = self.request
|
||||||
return render_to_string(self.partial_name, context, request=request)
|
return render_to_string(self.partial_name, context, request=request)
|
||||||
|
|
||||||
def apply_htmx_headers(self, response):
|
def apply_htmx_headers(self, response):
|
||||||
"""Attach HX-* headers to the response."""
|
"""Attach HX-* headers to the response."""
|
||||||
request = self.request
|
request = self.request
|
||||||
|
|
||||||
if request.htmx:
|
if request.htmx:
|
||||||
is_get = request.method == "GET"
|
is_get = request.method == "GET"
|
||||||
is_pagination = "page" in request.GET
|
is_pagination = "page" in request.GET
|
||||||
|
|
||||||
if is_get and not is_pagination:
|
if is_get and not is_pagination:
|
||||||
# Push the current path unless overridden
|
# Push the current path unless overridden
|
||||||
response.headers["HX-Push-Url"] = self.htmx_push_url or request.get_full_path()
|
response.headers["HX-Push-Url"] = self.htmx_push_url or request.get_full_path()
|
||||||
|
|
||||||
# Build HX-Trigger payload
|
# Build HX-Trigger payload
|
||||||
trigger_payload = {}
|
trigger_payload = {}
|
||||||
|
|
||||||
# 1. User-defined triggers
|
# 1. User-defined triggers
|
||||||
if self.htmx_trigger:
|
if self.htmx_trigger:
|
||||||
trigger_payload.update(json.loads(self.htmx_trigger))
|
trigger_payload.update(json.loads(self.htmx_trigger))
|
||||||
|
|
||||||
# 2. Auto menu highlight trigger
|
# 2. Auto menu highlight trigger
|
||||||
if self.menu_highlight:
|
if self.menu_highlight:
|
||||||
trigger_payload["menuHighlight"] = self.menu_highlight
|
trigger_payload["menuHighlight"] = self.menu_highlight
|
||||||
|
|
||||||
# Emit HX-Trigger if anything is present
|
# Emit HX-Trigger if anything is present
|
||||||
if trigger_payload:
|
if trigger_payload:
|
||||||
response.headers["HX-Trigger"] = json.dumps(trigger_payload)
|
response.headers["HX-Trigger"] = json.dumps(trigger_payload)
|
||||||
|
|
||||||
if self.htmx_trigger_after_settle:
|
if self.htmx_trigger_after_settle:
|
||||||
response.headers["HX-Trigger-After-Settle"] = self.htmx_trigger_after_settle
|
response.headers["HX-Trigger-After-Settle"] = self.htmx_trigger_after_settle
|
||||||
|
|
||||||
if self.htmx_trigger_after_swap:
|
if self.htmx_trigger_after_swap:
|
||||||
response.headers["HX-Trigger-After-Swap"] = self.htmx_trigger_after_swap
|
response.headers["HX-Trigger-After-Swap"] = self.htmx_trigger_after_swap
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def render_to_response(self, context, **response_kwargs):
|
def render_to_response(self, context, **response_kwargs):
|
||||||
"""Renders HTMX response, applying headers and handling directives"""
|
"""Renders HTMX response, applying headers and handling directives"""
|
||||||
request = self.request
|
request = self.request
|
||||||
|
|
||||||
if not request.htmx:
|
if not request.htmx:
|
||||||
response = super().render_to_response(context, **response_kwargs)
|
response = super().render_to_response(context, **response_kwargs)
|
||||||
return self.apply_htmx_headers(response)
|
return self.apply_htmx_headers(response)
|
||||||
|
|
||||||
if self.htmx_redirect_url:
|
if self.htmx_redirect_url:
|
||||||
return HttpResponseClientRedirect(self.htmx_redirect_url)
|
return HttpResponseClientRedirect(self.htmx_redirect_url)
|
||||||
|
|
||||||
if self.htmx_refresh:
|
if self.htmx_refresh:
|
||||||
return HttpResponseClientRefresh()
|
return HttpResponseClientRefresh()
|
||||||
|
|
||||||
html = self.render_partial(context)
|
html = self.render_partial(context)
|
||||||
response = HttpResponse(html, **response_kwargs)
|
response = HttpResponse(html, **response_kwargs)
|
||||||
return self.apply_htmx_headers(response)
|
return self.apply_htmx_headers(response)
|
||||||
|
|||||||
@@ -3,8 +3,4 @@ from django.urls import include, path
|
|||||||
from .views import configuration, index
|
from .views import configuration, index
|
||||||
|
|
||||||
app_name = "backend"
|
app_name = "backend"
|
||||||
urlpatterns = [
|
urlpatterns = [path("", index, name="index"), path("members/", include("backend.members.urls")), path("configuration", configuration, name="configuration")]
|
||||||
path("", index, name="index"),
|
|
||||||
path("members/", include("backend.members.urls")),
|
|
||||||
path("configuration", configuration, name="configuration")
|
|
||||||
]
|
|
||||||
|
|||||||
11
manage.py
11
manage.py
@@ -1,22 +1,19 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
"""Django's command-line utility for administrative tasks."""
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Run administrative tasks."""
|
"""Run administrative tasks."""
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'TeamForge.settings')
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "TeamForge.settings")
|
||||||
try:
|
try:
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
raise ImportError(
|
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
|
||||||
"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)
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -5,5 +5,4 @@ class MembersConfig(AppConfig):
|
|||||||
name = "members"
|
name = "members"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
# noinspection PyUnusedImports
|
import members.signals # noqa: F401
|
||||||
import members.signals
|
|
||||||
|
|||||||
@@ -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__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")
|
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")
|
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:
|
class Meta:
|
||||||
model = Member
|
model = Member
|
||||||
fields = ["user__first_name", "user__last_name", "license", "user__is_active"]
|
fields = ["user__first_name", "user__last_name", "license", "user__is_active"]
|
||||||
|
|
||||||
|
|
||||||
@@ -7,7 +7,6 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("members", "0001_initial"),
|
("members", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("members", "0002_member_family"),
|
("members", "0002_member_family"),
|
||||||
]
|
]
|
||||||
@@ -29,8 +28,6 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="member",
|
model_name="member",
|
||||||
name="family",
|
name="family",
|
||||||
field=models.ManyToManyField(
|
field=models.ManyToManyField(blank=True, to="members.member", verbose_name="family"),
|
||||||
blank=True, to="members.member", verbose_name="family"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("members", "0003_member_created_member_updated_alter_member_family"),
|
("members", "0003_member_created_member_updated_alter_member_family"),
|
||||||
]
|
]
|
||||||
@@ -14,9 +13,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="member",
|
model_name="member",
|
||||||
name="access_token",
|
name="access_token",
|
||||||
field=models.CharField(
|
field=models.CharField(blank=True, max_length=255, null=True, verbose_name="access token"),
|
||||||
blank=True, max_length=255, null=True, verbose_name="access token"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="member",
|
model_name="member",
|
||||||
@@ -37,9 +34,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="member",
|
model_name="member",
|
||||||
name="license",
|
name="license",
|
||||||
field=models.CharField(
|
field=models.CharField(blank=True, max_length=20, null=True, verbose_name="license"),
|
||||||
blank=True, max_length=20, null=True, verbose_name="license"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="member",
|
model_name="member",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("members", "0004_member_access_token_member_birthday_and_more"),
|
("members", "0004_member_access_token_member_birthday_and_more"),
|
||||||
]
|
]
|
||||||
@@ -17,8 +16,6 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="member",
|
model_name="member",
|
||||||
name="family_members",
|
name="family_members",
|
||||||
field=models.ManyToManyField(
|
field=models.ManyToManyField(blank=True, to="members.member", verbose_name="family members"),
|
||||||
blank=True, to="members.member", verbose_name="family members"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,15 +4,14 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('members', '0005_remove_member_family_member_family_members'),
|
("members", "0005_remove_member_family_member_family_members"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='member',
|
model_name="member",
|
||||||
name='notes',
|
name="notes",
|
||||||
field=models.TextField(blank=True, null=True, verbose_name='notes'),
|
field=models.TextField(blank=True, null=True, verbose_name="notes"),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -54,26 +54,19 @@ class Member(RulesModel):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, first_name: str, last_name: str, email: str, password: Optional[str] = None, member: Optional["Member"] = None) -> "Member":
|
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"""
|
"""Creates a new member based on the provided details"""
|
||||||
|
|
||||||
if member is not None and member.pk is not None:
|
if member is not None and member.pk is not None:
|
||||||
member.user.first_name = first_name
|
member.user.first_name = first_name
|
||||||
member.user.last_name = last_name
|
member.user.last_name = last_name
|
||||||
member.user.email = email
|
member.user.email = email
|
||||||
member.user.username = email
|
member.user.username = email
|
||||||
|
|
||||||
if password is not None and password != "":
|
if password is not None and password != "":
|
||||||
member.user.set_password(password)
|
member.user.set_password(password)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# First check to see if a user already exists in the system
|
# First check to see if a user already exists in the system
|
||||||
user, created = get_user_model().objects.get_or_create(
|
user, created = get_user_model().objects.get_or_create(username=email, defaults={"first_name": first_name, "last_name": last_name, "email": email})
|
||||||
username=email,
|
|
||||||
defaults={
|
|
||||||
"first_name": first_name,
|
|
||||||
"last_name": last_name,
|
|
||||||
"email": email
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not created:
|
if not created:
|
||||||
user.first_name = first_name
|
user.first_name = first_name
|
||||||
@@ -102,11 +95,11 @@ class Member(RulesModel):
|
|||||||
|
|
||||||
user.set_password(password)
|
user.set_password(password)
|
||||||
member.user = user
|
member.user = user
|
||||||
|
|
||||||
if not member.user.is_active:
|
if not member.user.is_active:
|
||||||
member.user.is_active = True
|
member.user.is_active = True
|
||||||
|
|
||||||
member.user.save()
|
member.user.save()
|
||||||
member.save()
|
member.save()
|
||||||
|
|
||||||
return member
|
return member
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ from django.contrib.auth.models import AbstractUser
|
|||||||
|
|
||||||
@rules.predicate
|
@rules.predicate
|
||||||
def is_member_manager(user: Optional[AbstractUser]) -> bool:
|
def is_member_manager(user: Optional[AbstractUser]) -> bool:
|
||||||
return user.has_perm('members.member_manager')
|
return user.has_perm("members.member_manager")
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
|||||||
@@ -9,95 +9,94 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('members', '0006_member_notes'),
|
("members", "0006_member_notes"),
|
||||||
('teams', '0001_initial'),
|
("teams", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Team',
|
name="Team",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
("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')),
|
("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')),
|
("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')),
|
("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')),
|
("logo", models.ImageField(blank=True, null=True, upload_to="teams/logo/", verbose_name="logo")),
|
||||||
('created', models.DateTimeField(auto_now_add=True)),
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
('updated', models.DateTimeField(auto_now=True)),
|
("updated", models.DateTimeField(auto_now=True)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'team',
|
"verbose_name": "team",
|
||||||
'verbose_name_plural': 'teams',
|
"verbose_name_plural": "teams",
|
||||||
'ordering': ['name'],
|
"ordering": ["name"],
|
||||||
},
|
},
|
||||||
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='TeamRole',
|
name="TeamRole",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
("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')),
|
("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')),
|
("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')),
|
("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')),
|
("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')),
|
("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)),
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
('updated', models.DateTimeField(auto_now=True)),
|
("updated", models.DateTimeField(auto_now=True)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'team role',
|
"verbose_name": "team role",
|
||||||
'verbose_name_plural': 'team roles',
|
"verbose_name_plural": "team roles",
|
||||||
'ordering': ['sort_order'],
|
"ordering": ["sort_order"],
|
||||||
},
|
},
|
||||||
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='TeamMembership',
|
name="TeamMembership",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
("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')),
|
("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')),
|
("captain", models.BooleanField(default=False, verbose_name="captain")),
|
||||||
('alternate_captain', models.BooleanField(default=False, verbose_name='alternate captain')),
|
("alternate_captain", models.BooleanField(default=False, verbose_name="alternate captain")),
|
||||||
('created', models.DateTimeField(auto_now_add=True)),
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
('modified', models.DateTimeField(auto_now=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')),
|
("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')),
|
("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')),
|
("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')),
|
("role", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="team_memberships", to="teams.teamrole", verbose_name="role")),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'team membership',
|
"verbose_name": "team membership",
|
||||||
'verbose_name_plural': 'team memberships',
|
"verbose_name_plural": "team memberships",
|
||||||
'ordering': ['team__name', 'role__sort_order', 'number', 'member__user__last_name', 'member__user__first_name'],
|
"ordering": ["team__name", "role__sort_order", "number", "member__user__last_name", "member__user__first_name"],
|
||||||
},
|
},
|
||||||
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='team',
|
model_name="team",
|
||||||
name='members',
|
name="members",
|
||||||
field=models.ManyToManyField(through='teams.TeamMembership', to='members.member', verbose_name='members'),
|
field=models.ManyToManyField(through="teams.TeamMembership", to="members.member", verbose_name="members"),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='TeamPicture',
|
name="TeamPicture",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
("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')),
|
("picture", models.ImageField(upload_to=teams.models.team_season_file_path, verbose_name="picture")),
|
||||||
('created', models.DateTimeField(auto_now_add=True)),
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
('modified', models.DateTimeField(auto_now=True)),
|
("modified", models.DateTimeField(auto_now=True)),
|
||||||
('season', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='teams.season', verbose_name='season')),
|
("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')),
|
("team", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="teams.team", verbose_name="team")),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'team picture',
|
"verbose_name": "team picture",
|
||||||
'verbose_name_plural': 'team pictures',
|
"verbose_name_plural": "team pictures",
|
||||||
'ordering': ['team__name', 'season__start_date'],
|
"ordering": ["team__name", "season__start_date"],
|
||||||
},
|
},
|
||||||
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
bases=(rules.contrib.models.RulesModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.AddConstraint(
|
migrations.AddConstraint(
|
||||||
model_name='teammembership',
|
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.'),
|
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."),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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."""
|
"""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}"
|
return f"teams/picture/{instance.team.slug}/{instance.season.start_date.year}/{filename}"
|
||||||
|
|
||||||
|
|
||||||
class Season(RulesModel):
|
class Season(RulesModel):
|
||||||
start_date = models.DateField(_("start date"))
|
start_date = models.DateField(_("start date"))
|
||||||
end_date = models.DateField(_("end date"))
|
end_date = models.DateField(_("end date"))
|
||||||
@@ -52,7 +53,6 @@ class Season(RulesModel):
|
|||||||
if season is None:
|
if season is None:
|
||||||
raise cls.DoesNotExist(f"No Season covers date {current_date}")
|
raise cls.DoesNotExist(f"No Season covers date {current_date}")
|
||||||
|
|
||||||
|
|
||||||
if values_only:
|
if values_only:
|
||||||
return season.date_range
|
return season.date_range
|
||||||
return season
|
return season
|
||||||
@@ -87,77 +87,80 @@ class Season(RulesModel):
|
|||||||
|
|
||||||
return date_value.replace(year=year, month=month, day=day)
|
return date_value.replace(year=year, month=month, day=day)
|
||||||
|
|
||||||
|
|
||||||
class TeamRole(RulesModel):
|
class TeamRole(RulesModel):
|
||||||
name = models.CharField(_("name"), max_length=255, unique=True)
|
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"))
|
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, ...)"))
|
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"))
|
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)"))
|
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)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
updated = models.DateTimeField(auto_now=True)
|
updated = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("team role")
|
verbose_name = _("team role")
|
||||||
verbose_name_plural = _("team roles")
|
verbose_name_plural = _("team roles")
|
||||||
ordering = ["sort_order"]
|
ordering = ["sort_order"]
|
||||||
rules_permissions = {"add": is_superuser, "view": is_superuser, "change": is_superuser, "delete": is_superuser}
|
rules_permissions = {"add": is_superuser, "view": is_superuser, "change": is_superuser, "delete": is_superuser}
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name} ({self.abbreviation})"
|
return f"{self.name} ({self.abbreviation})"
|
||||||
|
|
||||||
|
|
||||||
class Team(RulesModel):
|
class Team(RulesModel):
|
||||||
name = models.CharField(_("name"), max_length=255, unique=True)
|
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"))
|
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")
|
slug = AutoSlugField(_("slug"), max_length=255, unique=True, populate_from="name")
|
||||||
logo = models.ImageField(_("logo"), upload_to="teams/logo/", blank=True, null=True)
|
logo = models.ImageField(_("logo"), upload_to="teams/logo/", blank=True, null=True)
|
||||||
|
|
||||||
members = models.ManyToManyField("members.Member", verbose_name=_("members"), through="TeamMembership")
|
members = models.ManyToManyField("members.Member", verbose_name=_("members"), through="TeamMembership")
|
||||||
|
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
updated = models.DateTimeField(auto_now=True)
|
updated = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("team")
|
verbose_name = _("team")
|
||||||
verbose_name_plural = _("teams")
|
verbose_name_plural = _("teams")
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
rules_permissions = {"add": is_superuser, "view": is_superuser, "change": is_superuser, "delete": is_superuser}
|
rules_permissions = {"add": is_superuser, "view": is_superuser, "change": is_superuser, "delete": is_superuser}
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def initials(self) -> str:
|
def initials(self) -> str:
|
||||||
words = self.name.split()
|
words = self.name.split()
|
||||||
if len(words) == 1:
|
if len(words) == 1:
|
||||||
return words[0][0].upper()
|
return words[0][0].upper()
|
||||||
|
|
||||||
return "".join(word[0].upper() for word in words[:2])
|
return "".join(word[0].upper() for word in words[:2])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def member_count(self) -> int:
|
def member_count(self) -> int:
|
||||||
season = Season.for_date()
|
season = Season.for_date()
|
||||||
return self.members.filter(team_memberships__season=season).count()
|
return self.members.filter(team_memberships__season=season).count()
|
||||||
|
|
||||||
def get_short_name(self) -> str:
|
def get_short_name(self) -> str:
|
||||||
"""Returns the short name of the team, or the name if no short name is set."""
|
"""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
|
return self.short_name if self.short_name and self.short_name != "" else self.name
|
||||||
|
|
||||||
|
|
||||||
class TeamMembership(RulesModel):
|
class TeamMembership(RulesModel):
|
||||||
team = models.ForeignKey(Team, on_delete=models.CASCADE, verbose_name=_("team"))
|
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"))
|
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"))
|
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"))
|
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)])
|
number = models.PositiveIntegerField(_("number"), blank=True, null=True, validators=[MinValueValidator(1), MaxValueValidator(99)])
|
||||||
captain = models.BooleanField(_("captain"), default=False)
|
captain = models.BooleanField(_("captain"), default=False)
|
||||||
alternate_captain = models.BooleanField(_("alternate captain"), default=False)
|
alternate_captain = models.BooleanField(_("alternate captain"), default=False)
|
||||||
|
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
modified = models.DateTimeField(auto_now=True)
|
modified = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("team membership")
|
verbose_name = _("team membership")
|
||||||
verbose_name_plural = _("team memberships")
|
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.")),
|
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}
|
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):
|
def __str__(self):
|
||||||
return f"{self.team.name} - {self.member.user.get_full_name()} ({self.role.abbreviation})"
|
return f"{self.team.name} - {self.member.user.get_full_name()} ({self.role.abbreviation})"
|
||||||
|
|
||||||
|
|
||||||
class TeamPicture(RulesModel):
|
class TeamPicture(RulesModel):
|
||||||
team = models.ForeignKey(Team, on_delete=models.CASCADE, verbose_name=_("team"))
|
team = models.ForeignKey(Team, on_delete=models.CASCADE, verbose_name=_("team"))
|
||||||
season = models.ForeignKey(Season, on_delete=models.CASCADE, verbose_name=_("season"))
|
season = models.ForeignKey(Season, on_delete=models.CASCADE, verbose_name=_("season"))
|
||||||
picture = models.ImageField(_("picture"), upload_to=team_season_file_path)
|
picture = models.ImageField(_("picture"), upload_to=team_season_file_path)
|
||||||
|
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
modified = models.DateTimeField(auto_now=True)
|
modified = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("team picture")
|
verbose_name = _("team picture")
|
||||||
verbose_name_plural = _("team pictures")
|
verbose_name_plural = _("team pictures")
|
||||||
ordering = ["team__name", "season__start_date"]
|
ordering = ["team__name", "season__start_date"]
|
||||||
rules_permissions = {"add": is_superuser, "view": is_superuser, "change": is_superuser, "delete": is_superuser}
|
rules_permissions = {"add": is_superuser, "view": is_superuser, "change": is_superuser, "delete": is_superuser}
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.team.name} - {self.season.start_date.year}"
|
return f"{self.team.name} - {self.season.start_date.year}"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import rules
|
|||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .models import TeamMembership, Season
|
from .models import TeamMembership
|
||||||
|
|
||||||
|
|
||||||
@rules.predicate
|
@rules.predicate
|
||||||
@@ -18,10 +18,10 @@ def is_team_admin(user: AbstractUser | None, teammembership: "TeamMembership | N
|
|||||||
team membership.
|
team membership.
|
||||||
"""
|
"""
|
||||||
from .models import TeamMembership, Season
|
from .models import TeamMembership, Season
|
||||||
|
|
||||||
if user is None or teammembership is None:
|
if user is None or teammembership is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return TeamMembership.objects.filter(team=teammembership.team, member__user=user, role__admin_role=True, season=Season.for_date()).exists()
|
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
|
:return: A boolean indicating whether the user is a team admin for the given
|
||||||
team membership.
|
team membership.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .models import Season, TeamMembership
|
from .models import Season, TeamMembership
|
||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return TeamMembership.objects.filter(member__user=user, role__admin_role=True, season=Season.for_date()).exists()
|
return TeamMembership.objects.filter(member__user=user, role__admin_role=True, season=Season.for_date()).exists()
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from django.contrib.auth import get_user_model
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from members.models import Member
|
|
||||||
from teams.models import Season, Team, TeamMembership, TeamPicture, TeamRole
|
from teams.models import Season, Team, TeamMembership, TeamPicture, TeamRole
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
@@ -14,6 +13,7 @@ User = get_user_model()
|
|||||||
# Helpers
|
# Helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def make_member(email, first="Test", last="User"):
|
def make_member(email, first="Test", last="User"):
|
||||||
user = User.objects.create_user(username=email, email=email, first_name=first, last_name=last)
|
user = User.objects.create_user(username=email, email=email, first_name=first, last_name=last)
|
||||||
return user.member
|
return user.member
|
||||||
@@ -35,6 +35,7 @@ def make_team(name="Test Team"):
|
|||||||
# Season
|
# Season
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class SeasonStrTest(TestCase):
|
class SeasonStrTest(TestCase):
|
||||||
def test_str(self):
|
def test_str(self):
|
||||||
season = Season(start_date=datetime.date(2025, 9, 1), end_date=datetime.date(2026, 8, 31))
|
season = Season(start_date=datetime.date(2025, 9, 1), end_date=datetime.date(2026, 8, 31))
|
||||||
@@ -127,6 +128,7 @@ class SeasonAddMonthsTest(TestCase):
|
|||||||
# TeamRole
|
# TeamRole
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TeamRoleStrTest(TestCase):
|
class TeamRoleStrTest(TestCase):
|
||||||
def test_str(self):
|
def test_str(self):
|
||||||
role = TeamRole(name="Coach", abbreviation="C")
|
role = TeamRole(name="Coach", abbreviation="C")
|
||||||
@@ -143,6 +145,7 @@ class TeamRoleStrTest(TestCase):
|
|||||||
# Team
|
# Team
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TeamStrTest(TestCase):
|
class TeamStrTest(TestCase):
|
||||||
def test_str(self):
|
def test_str(self):
|
||||||
team = Team(name="Red Dragons")
|
team = Team(name="Red Dragons")
|
||||||
@@ -199,6 +202,7 @@ class TeamMemberCountTest(TestCase):
|
|||||||
# TeamMembership
|
# TeamMembership
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TeamMembershipStrTest(TestCase):
|
class TeamMembershipStrTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
today = timezone.now().date()
|
today = timezone.now().date()
|
||||||
@@ -209,15 +213,14 @@ class TeamMembershipStrTest(TestCase):
|
|||||||
self.team = make_team("Blue Hawks")
|
self.team = make_team("Blue Hawks")
|
||||||
self.role = make_role("Forward", "F")
|
self.role = make_role("Forward", "F")
|
||||||
self.member = make_member("player@test.com", "John", "Doe")
|
self.member = make_member("player@test.com", "John", "Doe")
|
||||||
self.membership = TeamMembership.objects.create(
|
self.membership = TeamMembership.objects.create(team=self.team, member=self.member, season=self.season, role=self.role)
|
||||||
team=self.team, member=self.member, season=self.season, role=self.role
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_str(self):
|
def test_str(self):
|
||||||
self.assertEqual(str(self.membership), "Blue Hawks - John Doe (F)")
|
self.assertEqual(str(self.membership), "Blue Hawks - John Doe (F)")
|
||||||
|
|
||||||
def test_unique_number_constraint(self):
|
def test_unique_number_constraint(self):
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
|
|
||||||
member2 = make_member("player2@test.com", "Jane", "Doe")
|
member2 = make_member("player2@test.com", "Jane", "Doe")
|
||||||
TeamMembership.objects.create(team=self.team, member=member2, season=self.season, role=self.role, number=7)
|
TeamMembership.objects.create(team=self.team, member=member2, season=self.season, role=self.role, number=7)
|
||||||
with self.assertRaises(IntegrityError):
|
with self.assertRaises(IntegrityError):
|
||||||
@@ -232,6 +235,7 @@ class TeamMembershipStrTest(TestCase):
|
|||||||
# TeamPicture
|
# TeamPicture
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TeamPictureStrTest(TestCase):
|
class TeamPictureStrTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.season = Season.objects.create(
|
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")
|
self.picture = TeamPicture(team=self.team, season=self.season, picture="teams/picture/green-eagles/2025/photo.jpg")
|
||||||
|
|
||||||
def test_str(self):
|
def test_str(self):
|
||||||
self.assertEqual(str(self.picture), "Green Eagles - 2025")
|
self.assertEqual(str(self.picture), "Green Eagles - 2025")
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
|
|
||||||
class ThemeConfig(AppConfig):
|
class ThemeConfig(AppConfig):
|
||||||
name = 'theme'
|
name = "theme"
|
||||||
|
|||||||
@@ -5,29 +5,33 @@ from django import template
|
|||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
def calculate_brightness(background_color: dict) -> float:
|
def calculate_brightness(background_color: dict) -> float:
|
||||||
"""Calculates the brightness of a background image."""
|
"""Calculates the brightness of a background image."""
|
||||||
r_coefficient = 0.241
|
r_coefficient = 0.241
|
||||||
g_coefficient = 0.691
|
g_coefficient = 0.691
|
||||||
b_coefficient = 0.068
|
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:
|
def foreground(background_color: dict) -> dict:
|
||||||
"""Calculates the foreground color based on the background."""
|
"""Calculates the foreground color based on the background."""
|
||||||
black = {"R": 0, "G": 0, "B": 0}
|
black = {"R": 0, "G": 0, "B": 0}
|
||||||
white = {"R": 255, "G": 255, "B": 255}
|
white = {"R": 255, "G": 255, "B": 255}
|
||||||
|
|
||||||
return black if calculate_brightness(background_color) > 210 else white
|
return black if calculate_brightness(background_color) > 210 else white
|
||||||
|
|
||||||
|
|
||||||
def background(text: str) -> dict:
|
def background(text: str) -> dict:
|
||||||
"""Calculates the background color based on the text."""
|
"""Calculates the background color based on the text."""
|
||||||
hash_value = md5(text.encode("utf-8")).hexdigest()
|
hash_value = md5(text.encode("utf-8")).hexdigest()
|
||||||
hash_value_values = (hash_value[:8], hash_value[8:16], hash_value[16:24])
|
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)
|
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]}
|
return {"R": background_color[0], "G": background_color[1], "B": background_color[2]}
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag("templatetags/avatar.html")
|
@register.inclusion_tag("templatetags/avatar.html")
|
||||||
def avatar(first_name: str = "", last_name: str = "", initials: str = "", width: str = "md", button: bool = False) -> dict:
|
def avatar(first_name: str = "", last_name: str = "", initials: str = "", width: str = "md", button: bool = False) -> dict:
|
||||||
if initials:
|
if initials:
|
||||||
@@ -36,14 +40,14 @@ def avatar(first_name: str = "", last_name: str = "", initials: str = "", width:
|
|||||||
else:
|
else:
|
||||||
display_name = f"{first_name[0]}{last_name[0]}"
|
display_name = f"{first_name[0]}{last_name[0]}"
|
||||||
full_name = f"{first_name} {last_name}"
|
full_name = f"{first_name} {last_name}"
|
||||||
|
|
||||||
avatar_background = background(full_name)
|
avatar_background = background(full_name)
|
||||||
avatar_foreground = foreground(avatar_background)
|
avatar_foreground = foreground(avatar_background)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"name": display_name,
|
"name": display_name,
|
||||||
"width": width,
|
"width": width,
|
||||||
"button": button,
|
"button": button,
|
||||||
"background": "#%02x%02x%02x" % (avatar_background["R"], avatar_background["G"], avatar_background["B"]), # noqa: UP031
|
"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
|
"foreground": "#%02x%02x%02x" % (avatar_foreground["R"], avatar_foreground["G"], avatar_foreground["B"]), # noqa: UP031
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,40 +4,41 @@ from typing import Optional
|
|||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag("templatetags/field.html")
|
@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:
|
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:
|
if label is not None:
|
||||||
field.label = label
|
field.label = label
|
||||||
|
|
||||||
if help_text is not None:
|
if help_text is not None:
|
||||||
field.help_text = help_text
|
field.help_text = help_text
|
||||||
|
|
||||||
field_type = None
|
field_type = None
|
||||||
match field.widget_type:
|
match field.widget_type:
|
||||||
case "select" | "nullbooleanselect" | "radioselect" | "selectmultiple":
|
case "select" | "nullbooleanselect" | "radioselect" | "selectmultiple":
|
||||||
field_type = "select"
|
field_type = "select"
|
||||||
|
|
||||||
case "checkbox":
|
case "checkbox":
|
||||||
field_type = "checkbox"
|
field_type = "checkbox"
|
||||||
|
|
||||||
case "textarea" | "markdownx":
|
case "textarea" | "markdownx":
|
||||||
field_type = "textarea"
|
field_type = "textarea"
|
||||||
|
|
||||||
case "clearablefile":
|
case "clearablefile":
|
||||||
field_type = "file"
|
field_type = "file"
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
field_type = "input"
|
field_type = "input"
|
||||||
|
|
||||||
size_modifier = None
|
size_modifier = None
|
||||||
match size:
|
match size:
|
||||||
case "extra-small":
|
case "extra-small":
|
||||||
size_modifier = "xs"
|
size_modifier = "xs"
|
||||||
|
|
||||||
case "small":
|
case "small":
|
||||||
size_modifier = "sm"
|
size_modifier = "sm"
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
pass
|
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}
|
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}
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ from django.http import HttpRequest
|
|||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
@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:
|
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."""
|
"""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_ = request.GET.copy()
|
||||||
dict_[field] = value
|
dict_[field] = value
|
||||||
|
|
||||||
if default_field is not None and default_field not in dict_.keys():
|
if default_field is not None and default_field not in dict_.keys():
|
||||||
dict_[default_field] = default_value
|
dict_[default_field] = default_value
|
||||||
|
|
||||||
return dict_.urlencode()
|
return dict_.urlencode()
|
||||||
|
|||||||
Reference in New Issue
Block a user