Compare commits
14 Commits
a19d3b091e
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| 303b8553c9 | |||
| cb10c8ccf5 | |||
| 17e91cd7f8 | |||
| 5898a3ac6d | |||
| b885bf6da5 | |||
| d2d50afdd7 | |||
| f4c5377727 | |||
| 97ee6c2500 | |||
| 26155de246 | |||
| 03f8a5eb35 | |||
| 093ca1b88d | |||
| 66ba195cb7 | |||
| 92101ca86a | |||
| 9c55fcd828 |
@@ -46,6 +46,7 @@ INSTALLED_APPS = [
|
||||
"constance",
|
||||
"tailwind",
|
||||
"django_filters",
|
||||
"django_htmx",
|
||||
"rules.apps.AutodiscoverRulesConfig",
|
||||
"theme.apps.ThemeConfig", # Tailwind theme app
|
||||
"members.apps.MembersConfig",
|
||||
@@ -60,6 +61,7 @@ MIDDLEWARE = [
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"django_htmx.middleware.HtmxMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "TeamForge.urls"
|
||||
@@ -145,6 +147,7 @@ CONSTANCE_CONFIG = {
|
||||
"TF_DEFAULT_SEASON_MONTH": (config("TF_DEFAULT_SEASON_MONTH", default=8, cast=int), "Default season start month", int),
|
||||
"TF_DEFAULT_SEASON_DAY": (config("TF_DEFAULT_SEASON_DAY", default=1, cast=int), "Default season start day", int),
|
||||
"TF_DEFAULT_SEASON_DURATION": (config("TF_DEFAULT_SEASON_DURATION", default="1y", cast=str), "Default season duration", str),
|
||||
"TF_ENABLE_TEAMS": (config("TF_ENABLE_TEAMS", default=True, cast=bool), "Enable teams", bool),
|
||||
}
|
||||
|
||||
PHONENUMBER_DEFAULT_FORMAT = "INTERNATIONAL"
|
||||
|
||||
@@ -5,8 +5,8 @@ from .views import MemberAddView, MemberDeleteView, MemberEditView, MemberListVi
|
||||
app_name = "members"
|
||||
urlpatterns = [
|
||||
path("", MemberListView.as_view(), name="list"),
|
||||
# path("add/", MemberAddView.as_view(), name="add"),
|
||||
path("add/", MemberAddView.as_view(), name="add"),
|
||||
# path("<int:pk>/edit/", MemberEditView.as_view(), name="edit"),
|
||||
# path("<int:pk>/delete/", MemberDeleteView.as_view(), name="delete"),
|
||||
path("<int:pk>/delete/", MemberDeleteView.as_view(), name="delete"),
|
||||
# path("load/", MemberLoadView.as_view(), name="load"),
|
||||
]
|
||||
|
||||
@@ -1,34 +1,90 @@
|
||||
from typing import Any
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import DeleteView, UpdateView, CreateView
|
||||
from django_filters.views import FilterView
|
||||
from rules.contrib.views import PermissionRequiredMixin
|
||||
|
||||
from members.filters import MemberFilter
|
||||
from members.models import Member
|
||||
from members.forms import MemberForm
|
||||
|
||||
|
||||
class MemberListView(PermissionRequiredMixin, FilterView):
|
||||
from ..mixins import HTMXViewMixin
|
||||
class MemberListView(HTMXViewMixin, PermissionRequiredMixin, FilterView):
|
||||
filterset_class = MemberFilter
|
||||
paginate_by = 50
|
||||
permission_denied_message = _("You do not have permission to view this page.")
|
||||
permission_required = "members.view_member"
|
||||
partial_name = "members/member_filter.html#content"
|
||||
menu_highlight = "members"
|
||||
|
||||
def handle_no_permission(self) -> HttpResponseRedirect:
|
||||
messages.error(self.request, self.get_permission_denied_message())
|
||||
return HttpResponseRedirect(reverse_lazy("backend:index"))
|
||||
|
||||
def get_filterset_kwargs(self, filterset_class) -> dict[str, Any]:
|
||||
kwargs = super().get_filterset_kwargs(filterset_class)
|
||||
|
||||
class MemberAddView: ...
|
||||
filter_values = {} if kwargs["data"] is None else kwargs["data"].dict()
|
||||
|
||||
if not filter_values:
|
||||
filter_values.update({"user__is_active": "true"})
|
||||
|
||||
kwargs["data"] = filter_values
|
||||
return kwargs
|
||||
|
||||
|
||||
class MemberAddView(HTMXViewMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView):
|
||||
model = Member
|
||||
form_class = MemberForm
|
||||
permission_required = "members.add_member"
|
||||
permission_denied_message = _("You do not have permission to view this page.")
|
||||
success_message = _("Member <strong>%(name)s</strong> has been created successfully.")
|
||||
success_url = reverse_lazy("backend:members:list")
|
||||
partial_name = "members/member_form.html#content"
|
||||
menu_highlight = "members"
|
||||
|
||||
def handle_no_permission(self) -> HttpResponseRedirect:
|
||||
messages.error(self.request, self.get_permission_denied_message())
|
||||
return HttpResponseRedirect(reverse_lazy("backend:index"))
|
||||
|
||||
def get_success_message(self, cleaned_data):
|
||||
return self.success_message % dict(cleaned_data, name=self.object.user.get_full_name())
|
||||
|
||||
|
||||
class MemberEditView: ...
|
||||
|
||||
|
||||
class MemberDeleteView: ...
|
||||
class MemberDeleteView(HTMXViewMixin, PermissionRequiredMixin, SuccessMessageMixin, DeleteView):
|
||||
model = Member
|
||||
success_message = _("Member <strong>%(name)s</strong> has been deleted successfully.")
|
||||
permission_required = "members.delete_member"
|
||||
permission_denied_message = _("You do not have permission to view this page.")
|
||||
success_url = reverse_lazy("backend:members:list")
|
||||
partial_name = "members/member_confirm_delete.html#content"
|
||||
menu_highlight = "members"
|
||||
|
||||
def handle_no_permission(self) -> HttpResponseRedirect:
|
||||
messages.error(self.request, self.get_permission_denied_message())
|
||||
return HttpResponseRedirect(reverse_lazy("backend:index"))
|
||||
|
||||
def get_success_message(self, cleaned_data):
|
||||
return self.success_message % dict(cleaned_data, name=self.object.user.get_full_name())
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
|
||||
# Soft delete user
|
||||
self.object.user.is_active = False
|
||||
self.object.user.save()
|
||||
|
||||
# Do not delete the member object
|
||||
messages.success(self.request, self.get_success_message({"name": self.object.user.get_full_name()}))
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
|
||||
class MemberLoadView: ...
|
||||
|
||||
110
backend/mixins.py
Normal file
110
backend/mixins.py
Normal file
@@ -0,0 +1,110 @@
|
||||
import json
|
||||
|
||||
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.
|
||||
Supports:
|
||||
- partial rendering via django-partials
|
||||
- HX-Redirect
|
||||
- HX-Push-URL
|
||||
- HX-Trigger events
|
||||
- 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()
|
||||
|
||||
print(response.headers)
|
||||
|
||||
# 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)
|
||||
@@ -24,5 +24,5 @@ class MemberAdmin(admin.ModelAdmin):
|
||||
fieldsets = [
|
||||
("GENERAL INFORMATION", {"fields": ["user", "family_members", "birthday", "license", "access_token"]}),
|
||||
("CONTACT_INFORMATION", {"fields": ["phone_number", "emergency_phone_number"]}),
|
||||
("METADATA", {"fields": ["created", "updated"]}),
|
||||
("METADATA", {"fields": ["notes", "created", "updated"]}),
|
||||
]
|
||||
|
||||
@@ -5,12 +5,13 @@ from .models import Member
|
||||
|
||||
|
||||
class MemberFilter(django_filters.FilterSet):
|
||||
user__first_name = django_filters.CharFilter(field_name="user__first_name", label=_("First name"))
|
||||
user__last_name = django_filters.CharFilter(field_name="user__last_name", label=_("Last name"))
|
||||
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' )
|
||||
|
||||
class Meta:
|
||||
model = Member
|
||||
fields = ["user__first_name", "user__last_name", "license"]
|
||||
fields = ["user__first_name", "user__last_name", "license", "user__is_active"]
|
||||
|
||||
|
||||
42
members/forms.py
Normal file
42
members/forms.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Member
|
||||
|
||||
class MemberForm(forms.ModelForm):
|
||||
first_name = forms.CharField(label=_("First name"), max_length=250)
|
||||
last_name = forms.CharField(label=_("Last name"), max_length=250)
|
||||
email = forms.EmailField(label=_("Email"))
|
||||
|
||||
admin = forms.BooleanField(label=_("Admin?"), required=False, help_text=_("If checked will mark this user as a site admin granting them all permissions"))
|
||||
|
||||
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput, required=False)
|
||||
password_confirmation = forms.CharField(label=_("Confirm password"), widget=forms.PasswordInput, required=False)
|
||||
|
||||
class Meta:
|
||||
model = Member
|
||||
fields = ["phone_number", "emergency_phone_number", "license", "birthday", "family_members"]
|
||||
localized_fields = fields
|
||||
|
||||
def save(self, commit: bool = True) -> Member:
|
||||
password = None
|
||||
|
||||
if self.cleaned_data["password"] is not None and self.cleaned_data["password"] != "" and self.cleaned_data["password"] == self.cleaned_data["password_confirmation"]:
|
||||
password = self.cleaned_data["password"]
|
||||
|
||||
member = Member.create(first_name=self.cleaned_data["first_name"], last_name=self.cleaned_data["last_name"], email=self.cleaned_data["email"], password=password, member=self.instance)
|
||||
member.phone_number = self.cleaned_data["phone_number"]
|
||||
member.emergency_phone_number = self.cleaned_data["emergency_phone_number"]
|
||||
member.license = self.cleaned_data["license"]
|
||||
member.birthday = self.cleaned_data["birthday"]
|
||||
|
||||
if self.cleaned_data["admin"]:
|
||||
member.user.is_superuser = True
|
||||
member.user.save(update_fields=["is_superuser"])
|
||||
|
||||
member.save(update_fields=["phone_number", "emergency_phone_number", "license", "birthday"])
|
||||
member.family_members.set(self.cleaned_data["family_members"])
|
||||
|
||||
return member
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import secrets
|
||||
import string
|
||||
from typing import Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
@@ -22,6 +27,7 @@ class Member(RulesModel):
|
||||
|
||||
phone_number = PhoneNumberField(_("phone number"), blank=True, null=True)
|
||||
emergency_phone_number = PhoneNumberField(_("emergency phone number"), blank=True, null=True)
|
||||
notes = models.TextField(_("notes"), blank=True, null=True)
|
||||
|
||||
access_token = models.CharField(_("access token"), max_length=255, blank=True, null=True)
|
||||
|
||||
@@ -44,3 +50,57 @@ class Member(RulesModel):
|
||||
|
||||
def __str__(self):
|
||||
return self.user.get_full_name()
|
||||
|
||||
@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
|
||||
}
|
||||
)
|
||||
|
||||
if not created:
|
||||
user.first_name = first_name
|
||||
user.last_name = last_name
|
||||
user.email = email
|
||||
user.username = email
|
||||
|
||||
if hasattr(user, "member"):
|
||||
member = user.member
|
||||
if password is not None and password != "":
|
||||
user.set_password(password)
|
||||
else:
|
||||
member = cls()
|
||||
|
||||
initial_password = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(20))
|
||||
|
||||
if password is None or password == "":
|
||||
password = initial_password
|
||||
member.notes = f"Initial password: {initial_password}"
|
||||
|
||||
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
|
||||
@@ -9,8 +9,10 @@ dependencies = [
|
||||
"django-constance>=4.3.4",
|
||||
"django-extensions>=4.1",
|
||||
"django-filter>=25.2",
|
||||
"django-htmx>=1.27.0",
|
||||
"django-phonenumber-field[phonenumbers]>=8.4.0",
|
||||
"django-tailwind[cookiecutter,honcho]>=4.4.2",
|
||||
"pillow>=12.1.0",
|
||||
"psycopg2-binary>=2.9.11",
|
||||
"python-decouple>=3.8",
|
||||
"rules>=3.5",
|
||||
|
||||
@@ -1 +1,20 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load rules %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% url "backend:members:list" as members_list %}
|
||||
|
||||
{% has_perm "members.member_manager" request.user as is_member_manager %}
|
||||
|
||||
{% if is_member_manager %}
|
||||
<li class="menu-title">Members</li>
|
||||
<li><a href="{{ members_list }}" class="menu-item {% if members_list in request.path %}menu-active{% endif %}" data-menu="members" hx-get="{% url "backend:members:list" %}" hx-target="#content"><i class="fa-solid fa-users"></i> Members</a></li>
|
||||
{% endif %}
|
||||
|
||||
<li class="menu-title mt-4">Navigation</li>
|
||||
<li><a href="#"><i class="fa-solid fa-house"></i> Dashboard</a></li>
|
||||
<li><a href="#"><i class="fa-solid fa-calendar"></i> Calendar</a></li>
|
||||
<li><a href="#"><i class="fa-solid fa-users"></i> Members</a></li>
|
||||
<li><a href="#"><i class="fa-solid fa-gear"></i> Settings</a></li>
|
||||
{% endblock sidebar %}
|
||||
29
templates/backend/partials/messages.html
Normal file
29
templates/backend/partials/messages.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{% if messages %}
|
||||
<div id="messages" class="flex flex-col p-2 m-4 -mt-4 gap-y-2" {% if request.htmx %} hx-swap-oob="true"{% endif %}>
|
||||
{% for message in messages %}
|
||||
{% if message.level == DEFAULT_MESSAGE_LEVELS.SUCCESS %}
|
||||
<div role="alert" class="alert alert-success">
|
||||
<i class="text-lg fa-solid fa-check"></i>
|
||||
<span>{{ message|safe }}</span>
|
||||
</div>
|
||||
{% elif message.level == DEFAULT_MESSAGE_LEVELS.WARNING %}
|
||||
<div role="alert" class="alert alert-warning">
|
||||
<i class="text-lg fa-solid fa-ban"></i>
|
||||
<span>{{ message|safe }}</span>
|
||||
</div>
|
||||
{% elif message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
|
||||
<div role="alert" class="alert alert-error">
|
||||
<i class="text-lg fa-solid fa-exclamation"></i>
|
||||
<span>{{ message|safe }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div role="alert" class="alert alert-info">
|
||||
<i class="text-lg fa-solid fa-info"></i>
|
||||
<span>{{ message|safe }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div id="messages"{% if request.htmx %} hx-swap-oob="true"{% endif %}></div>
|
||||
{% endif %}
|
||||
@@ -1,9 +1,11 @@
|
||||
{% load tailwind_tags %}
|
||||
{% load static %}
|
||||
{% load avatar %}
|
||||
{% load django_htmx %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en">
|
||||
<html lang="en" class="bg-base-200">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta viewport="width=device-width, initial-scale=1.0">
|
||||
@@ -22,48 +24,124 @@
|
||||
<link href="{% static "css/brands.css" %}" rel="stylesheet"/>
|
||||
|
||||
{% tailwind_css %}
|
||||
{% htmx_script %}
|
||||
|
||||
<style>
|
||||
.navbar-shrink {
|
||||
height: 3rem !important;
|
||||
transition: height 0.2s ease;
|
||||
}
|
||||
|
||||
.navbar-normal {
|
||||
height: 6rem;
|
||||
transition: height 0.2s ease;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="navbar bg-base-200 shadow-sm">
|
||||
<div class="flex-1">
|
||||
<label for="drawer-button" aria-label="open sidebar" class="btn btn-square btn-outline lg:hidden">
|
||||
<!-- Sidebar toggle icon -->
|
||||
<i class="fa-solid fa-bars"></i>
|
||||
<body class="flex flex-col h-screen" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||
<!-- NAVBAR -->
|
||||
<header id="mainNavbar" class="navbar-normal navbar bg-base-100 sticky top-0 z-50 shadow">
|
||||
<div class="flex-none lg:hidden">
|
||||
<!-- Mobile sidebar toggle -->
|
||||
<label for="sidebar-toggle" class="btn btn-square btn-ghost">
|
||||
<i class="fa-solid fa-bars text-xl"></i>
|
||||
</label>
|
||||
|
||||
<div class="flex flex-row items-center px-4 py-2 gap-4">
|
||||
{% if config.TF_CLUB_LOGO != "teamforge/logo.png" %}
|
||||
<img class="max-h-18 transform-gpu mask mask-circle" src="{% static config.TF_CLUB_LOGO %}" alt="{{ config.TF_CLUB_NAME }} Logo"/>
|
||||
{% else %}
|
||||
<img class="max-h-18 transform-gpu" src="{% static config.TF_CLUB_LOGO %}" alt="{{ config.TF_CLUB_NAME }} Logo"/>
|
||||
{% endif %}
|
||||
|
||||
<a class="font-jersey text-4xl">{{ config.TF_CLUB_NAME }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mr-2 flex flex-row gap-2">
|
||||
<div class="avatar avatar-placeholder">
|
||||
<div class="bg-neutral text-neutral-content w-12 rounded-full">
|
||||
<span>BS</span>
|
||||
<div class="flex-1 flex items-center gap-3">
|
||||
<img src="{% static config.TF_CLUB_LOGO %}" class="hidden lg:inline h-10 {% if config.TF_CLUB_LOGO != "teamforge/logo.png" %}mask mask-circle{% endif %}" alt="{{ config.TF_CLUB_NAME }} logo">
|
||||
<span class="text-xl font-bold font-jersey">{{ config.TF_CLUB_NAME }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row items-center gap-1 lg:gap-4">
|
||||
<!-- Notifications -->
|
||||
<button class="btn btn-ghost btn-circle">
|
||||
<i class="fa-solid fa-bell text-xl"></i>
|
||||
</button>
|
||||
|
||||
<!-- Avatar -->
|
||||
<div class="dropdown dropdown-end">
|
||||
{% avatar first_name=request.user.first_name last_name=request.user.last_name button=True %}
|
||||
<ul tabindex="-1" class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow">
|
||||
<li>
|
||||
<a class="justify-between">
|
||||
Profile
|
||||
<span class="badge">New</span>
|
||||
</a>
|
||||
</li>
|
||||
<li><a>Settings</a></li>
|
||||
<li><a>Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Login/Logout -->
|
||||
{% if user.is_authenticated %}
|
||||
<a href="" class="btn btn-outline btn-sm hidden lg:flex">Logout</a>
|
||||
{% else %}
|
||||
<a href="" class="btn btn-outline btn-sm hidden lg:flex">Login</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="drawer lg:drawer-open flex-1">
|
||||
<!-- Hidden checkbox for mobile sidebar -->
|
||||
<input type="checkbox" id="sidebar-toggle" class="drawer-toggle">
|
||||
|
||||
<!-- SIDEBAR -->
|
||||
<aside class="drawer-side z-60 lg:z-auto min-w-fit h-full lg:h-fit">
|
||||
<label for="sidebar-toggle" class="drawer-overlay"></label>
|
||||
|
||||
<div class="w-64 bg-base-100 border-r p-4 h-full lg:h-fit lg:border lg:border-base-300 lg:m-4 lg:mr-2 lg:rounded-xl lg:min-h-full">
|
||||
<ul class="menu w-full">
|
||||
{% block sidebar %}{% endblock sidebar %}
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- MAIN CONTENT-->
|
||||
<div class="drawer-content flex w-full">
|
||||
<main class="bg-base-100 border border-base-300 rounded-xl m-4 ml-2 p-6 w-full">
|
||||
{% include "backend/partials/messages.html" %}
|
||||
|
||||
<div id="content">
|
||||
{% block content %}
|
||||
<h1 class="text-3xl font-bold">Welcome!</h1>
|
||||
<p>This is your main content area.</p>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer lg:drawer-open">
|
||||
<input id="drawer-button" type="checkbox" class="drawer-toggle"/>
|
||||
<div class="drawer-content flex flex-col items-center justify-center">
|
||||
{% block content %}PAGE CONTENT{% endblock content %}
|
||||
</div>
|
||||
<!-- FOOTER -->
|
||||
<footer class="footer footer-center p-4 bg-neutral text-neutral-content mt-auto">
|
||||
<p>© {% now "Y" %} TeamForge — All rights reserved.</p>
|
||||
</footer>
|
||||
|
||||
<div class="drawer-side">
|
||||
<label for="drawer-button" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||
<div class="flex min-h-full flex-col items-start bg-base-200 py-4 px-8">
|
||||
{% block sidebar %}SIDEBAR CONTENT{% endblock sidebar %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.body.addEventListener("menuHighlight", (event) => {
|
||||
const active = event.detail.value;
|
||||
document.querySelectorAll(".menu-item").forEach(el => {
|
||||
el.classList.toggle("menu-active", el.dataset.menu === active);
|
||||
});
|
||||
});
|
||||
|
||||
{% comment %}// Shrinking navbar on scroll
|
||||
const navbar = document.getElementById("mainNavbar");
|
||||
let lastScroll = 0; window.addEventListener("scroll", () => {
|
||||
const current = window.scrollY;
|
||||
|
||||
if (current > lastScroll && current > 50) {
|
||||
navbar.classList.add("navbar-shrink");
|
||||
navbar.classList.remove("navbar-normal");
|
||||
} else {
|
||||
navbar.classList.add("navbar-normal");
|
||||
navbar.classList.remove("navbar-shrink");
|
||||
}
|
||||
|
||||
lastScroll = current;
|
||||
});{% endcomment %}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
27
templates/members/member_confirm_delete.html
Normal file
27
templates/members/member_confirm_delete.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends "backend/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load form_field %}
|
||||
{% load avatar %}
|
||||
{% load pagination %}
|
||||
|
||||
{% block content %}
|
||||
{% partialdef content inline %}
|
||||
<h1 class="page-title">{% translate "Members" %}</h1>
|
||||
|
||||
<div>
|
||||
{% blocktranslate with name=object.user.get_full_name %}
|
||||
Are you sure you want to delete member <span class="font-bold">{{ name }}</span>?
|
||||
{% endblocktranslate %}
|
||||
</div>
|
||||
|
||||
<form method="post" hx-post="{% url "backend:members:delete" object.pk %}" hx-target="#content">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="flex flex-row gap-2 mt-8">
|
||||
<a href="{% url "backend:members:list" %}" class="btn btn-neutral btn-outline grow lg:grow-0" hx-get="{% url "backend:members:list" %}" hx-target="#content">{% translate "Cancel" %}</a>
|
||||
<button type="submit" class="btn btn-error grow lg:grow-0"><i class="fa-solid fa-trash"></i>{% translate "Delete" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endpartialdef content %}
|
||||
{% endblock content %}
|
||||
@@ -1,3 +1,200 @@
|
||||
{% extends "backend/base.html" %}
|
||||
|
||||
{% block content %}test <i class="fa-solid fa-hockey-puck"></i>{% endblock content %}
|
||||
{% load i18n %}
|
||||
{% load form_field %}
|
||||
{% load avatar %}
|
||||
{% load pagination %}
|
||||
|
||||
{% block content %}
|
||||
{% partialdef content inline %}
|
||||
{% if request.htmx %}
|
||||
{% include "backend/partials/messages.html" %}
|
||||
{% endif %}
|
||||
|
||||
<h1 class="page-title">{% translate "Members" %}</h1>
|
||||
|
||||
<div class="lg:hidden collapse collapse-plus bg-base-100 border-neutral border">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-sm font-semibold"><i class="fa-solid fa-filter mr-2"></i>{% translate "Filter" %}{% if filter.is_bound %}<span class="ml-2 badge badge-sm badge-neutral">active</span>{% endif %}</div>
|
||||
<div class="collapse-content">
|
||||
<form class="flex flex-col gap-2" hx-get="{% url "backend:members:list" %}" hx-target="#content">
|
||||
{% for field in filter.form %}
|
||||
{% form_field field show_label=False size="small" %}
|
||||
{% endfor %}
|
||||
|
||||
<div class="flex flex-row w-full gap-2">
|
||||
<button type="submit" class="btn btn-sm btn-outline btn-neutral grow">
|
||||
<i class="fa-solid fa-filter"></i>{% translate "Filter" %}
|
||||
</button>
|
||||
|
||||
{% if filter.is_bound %}
|
||||
<a class="btn btn-outline btn-error btn-sm grow" href="{% url "backend:members:list" %}" hx-get="{% url "backend:members:list" %}" hx-target="#content">
|
||||
<i class="fa-solid fa-times"></i>{% translate "Clear" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action_bar">
|
||||
<div class="filter hidden lg:flex">
|
||||
<form hx-get="{% url "backend:members:list" %}" hx-target="#content">
|
||||
{% for field in filter.form %}
|
||||
{% form_field field show_label=False size="extra-small" %}
|
||||
{% endfor %}
|
||||
|
||||
<div class="flex flex-row w-full gap-2 lg:w-fit">
|
||||
<button type="submit">
|
||||
<i class="fa-solid fa-filter"></i>{% translate "Filter" %}
|
||||
</button>
|
||||
|
||||
{% if filter.is_bound %}
|
||||
<a class="btn btn-outline btn-error btn-xs grow" href="{% url "backend:members:list" %}" hx-get="{% url "backend:members:list" %}" hx-target="#content">
|
||||
<i class="fa-solid fa-times"></i>{% translate "Clear" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="add">
|
||||
<a class="btn btn-accent btn-sm grow hidden lg:flex" href="">
|
||||
<i class="fa-solid fa-file-upload"></i>{% translate "Load members from file" %}
|
||||
</a>
|
||||
|
||||
<a class="btn btn-neutral btn-outline btn-sm grow" href="{% url "backend:members:add" %}" hx-get="{% url "backend:members:add" %}" hx-target="#content">
|
||||
<i class="fa-solid fa-plus"></i>{% translate "Add member" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden lg:flex mt-4">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% translate "Member" %}</th>
|
||||
<th>{% translate "Birthday" %}</th>
|
||||
<th>{% translate "License" %}</th>
|
||||
<th>{% translate "Phone number(s)" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if object_list|length == 0 %}
|
||||
<tr>
|
||||
<td colspan="5">{% translate "No members found" %}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
{% for member in object_list %}
|
||||
<tr class="hover:bg-base-300">
|
||||
<td>
|
||||
<a href="">
|
||||
<div class="flex flex-row items-center gap-3">
|
||||
<div>
|
||||
{% avatar first_name=member.user.first_name last_name=member.user.last_name %}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col grow">
|
||||
<div class="font-bold">{{ member.user.get_full_name }}</div>
|
||||
<div class="text-sm opacity-50">{{ member.user.email }}</div>
|
||||
</div>
|
||||
|
||||
{% if member.user.is_superuser %}
|
||||
<div class="badge badge-sm badge-accent"><i class="fa-solid fa-user-shield"></i>{% translate "Admin" %}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not member.user.is_active %}
|
||||
<div class="badge badge-neutral badge-sm">{% translate "Inactive"%}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ member.birthday|date:"d M Y"|default:"-" }}</td>
|
||||
<td>{{ member.license|default:"-" }}</td>
|
||||
<td>
|
||||
<div class="flex flex-col gap-1">
|
||||
{% if member.phone_number %}
|
||||
<a href="{{ member.phone_number.as_rfc3966 }}" class="btn btn-info btn-xs">
|
||||
<i class="fa-solid fa-phone"></i>{{ member.phone_number }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if member.emergency_phone_number %}
|
||||
<a href="{{ member.emergency_phone_number.as_rfc3966 }}" class="btn btn-error btn-xs">
|
||||
<i class="fa-solid fa-star-of-life"></i>{{ member.emergency_phone_number }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex flex-row gap-2">
|
||||
<a class="btn btn-outline btn-sm" href="">
|
||||
<i class="fa-solid fa-eye"></i>{% translate "Details" %}
|
||||
</a>
|
||||
|
||||
<a class="btn btn-outline btn-error btn-sm" href="{% url "backend:members:delete" member.pk %}" hx-get="{% url "backend:members:delete" member.pk %}" hx-target="#content">
|
||||
<i class="fa-solid fa-trash"></i>{% translate "Delete" %}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="lg:hidden">
|
||||
{% if object_list|length == 0 %}
|
||||
<div class="text-center w-full">{% translate "No members found" %}</div>
|
||||
{% else %}
|
||||
<div class="flex flex-col gap-1">
|
||||
{% for member in object_list %}
|
||||
<a class="border border-base-300 rounded-lg p-2 flex flex-row gap-2 items-center" href="">
|
||||
<div>
|
||||
{% avatar first_name=member.user.first_name last_name=member.user.last_name width="sm" %}
|
||||
</div>
|
||||
|
||||
<div class="grow">
|
||||
<div class="font-semibold text-sm">{{ member.user.get_full_name }} {% if member.license %}#{{ member.license }}{% endif %}</div>
|
||||
<div class="opacity-50 text-xs">{{ member.birthday|date:"d M Y"|default:"" }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<i class="fa-solid fa-chevron-right"></i>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if is_paginated %}
|
||||
<div class="flex justify-center mt-4">
|
||||
<div class="join">
|
||||
{% if page_obj.has_previous %}
|
||||
<a class="join-item btn" href="?{% url_replace request "page" 1 %}" hx-get="?{% url_replace request "page" 1 %}" hx-target="#content">«<span
|
||||
class="hidden lg:inline"> {% translate "first" %}</span></a>
|
||||
<a class="join-item btn"
|
||||
href="?{% url_replace request "page" page_obj.previous_page_number %}" hx-get="?{% url_replace request "page" page_obj.previous_page_number %}" hx-target="#content"><span
|
||||
class="hidden lg:inline">{% translate "previous" %}</span><span
|
||||
class="lg:hidden"><</span></a>
|
||||
{% endif %}
|
||||
|
||||
<button class="join-item btn btn-disabled">
|
||||
{% blocktranslate with page=page_obj.number num_pages=page_obj.paginator.num_pages %}page {{ page }}
|
||||
of {{ num_pages }}{% endblocktranslate %}</button>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a class="join-item btn" href="?{% url_replace request "page" page_obj.next_page_number %}" hx-get="?{% url_replace request "page" page_obj.next_page_number %}" hx-target="#content"><span
|
||||
class="hidden lg:inline">{% translate "next" %}</span><span
|
||||
class="lg:hidden">></span></a>
|
||||
<a class="join-item btn" href="?{% url_replace request "page" page_obj.paginator.num_pages %}" hx-get="?{% url_replace request "page" page_obj.num_pages %}" hx-target="#content"><span
|
||||
class="hidden lg:inline"> {% translate "last" %}</span> »</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endpartialdef content %}
|
||||
{% endblock content %}
|
||||
157
templates/members/member_form.html
Normal file
157
templates/members/member_form.html
Normal file
@@ -0,0 +1,157 @@
|
||||
{% extends "backend/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load form_field %}
|
||||
{% load avatar %}
|
||||
|
||||
{% block content %}
|
||||
{% partialdef content inline %}
|
||||
<h1 class="page-title">{% translate "Members" %}</h1>
|
||||
|
||||
<div class="flex flex-row gap-2 items-center lg:hidden">
|
||||
<div class="text-3xl min-w-12">
|
||||
<a href="{% url "backend:members:list" %}" hx-get="{% url "backend:members:list" %}" hx-target="#content">
|
||||
<i class="fa-solid fa-angle-left"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-2 grow justify-center items-center">
|
||||
{% if member %}
|
||||
<div class="font-bold text-xl">{{ member.user.get_full_name }}</div>
|
||||
|
||||
{% if member.user.is_superuser %}
|
||||
<div class="tooltip" data-tip="{% translate "This user is a site admin" %}">
|
||||
<div class="badge badge-sm badge-accent">
|
||||
<i class="fa-solid fa-user-shield"></i>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="font-bold text-xl">{% translate "Create new member" %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="flex min-w-12 justify-end">
|
||||
{% if member %}
|
||||
<a class="btn btn-error btn-outline" href="{% url "backend:members:delete" member.pk %}">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if member %}
|
||||
<div class="mt-4 lg:hidden flex flex-row gap-2">
|
||||
<div class="mt-4 lg:hidden flex flex-row gap-2">
|
||||
{% if member.phone_number %}
|
||||
<a href="{{ member.phone_number.as_rfc3966 }}" class="btn btn-info btn-outline btn-sm grow">
|
||||
<i class="fa-solid fa-phone"></i>
|
||||
{{ member.phone }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if member.emergency_phone_number %}
|
||||
<a href="{{ member.emergency_phone_number.as_rfc3966 }}" class="btn btn-error btn-outline btn-sm grow">
|
||||
<i class="fa-solid fa-file-medical"></i>
|
||||
{{ member.emergency_phone_number }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if config.TF_ENABLE_TEAMS %}
|
||||
<a href="?member__user__first_name={{ member.user.first_name }}&member__user__last_name={{ member.user.last_name }}" class="btn btn-sm w-full mt-2 btn-outline btn-neutral lg:hidden">
|
||||
<i class="fa-solid fa-ticket"></i>{% translate "View team memberships" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex flex-row items-center mt-8 gap-x-3 hidden lg:flex">
|
||||
{% avatar first_name=member.user.first_name last_name=member.user.last_name %}
|
||||
|
||||
<h2 class="page-subtitle border-b-0! mt-0!">{{ member.user.get_full_name }}</h2>
|
||||
|
||||
{% if member.user.is_superuser %}
|
||||
<div class="badge badge-accent"><i class="fa-solid fa-user-shield"></i>{% translate "Admin" %}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="justify-end hidden gap-2 lg:flex lg:flex-row grow">
|
||||
{% if member.phone %}
|
||||
<a href="{{ member.phone.as_rfc3966 }}" class="btn btn-outline btn-info"><i class="fa-solid fa-phone"></i>{{ member.phone }}</a>
|
||||
{% endif %}
|
||||
|
||||
{% if member.emergency_phone %}
|
||||
<a href="{{ member.emergency_phone.as_rfc3966 }}" class="btn btn-outline btn-error"><i class="fa-solid fa-file-medical"></i>{{ member.emergency_phone }}</a>
|
||||
{% endif %}
|
||||
|
||||
{% if config.TF_ENABLE_TEAMS %}
|
||||
<a href="?member__user__first_name={{ member.user.first_name }}&member__user__last_name={{ member.user.last_name }}" class="btn btn-outline btn-neutral">
|
||||
<i class="fa-solid fa-ticket"></i>{% translate "Team Memberships" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<a href="{% url "backend:members:members_delete" member.id %}" class="btn btn-error btn-outline">
|
||||
<i class="fa-solid fa-trash"></i>{% translate "Delete" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<h2 class="page-subtitle border-b-0! hidden lg:flex">{% translate "Create new member" %}</h2>
|
||||
{% endif %}
|
||||
|
||||
{% if form.errors %}
|
||||
<div class="flex flex-row items-center gap-2 p-2 m-4 rounded-lg bg-error">
|
||||
<i class="mr-2 text-3xl fa-solid fa-exclamation-triangle text-error-content"></i>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div class="mb-1 font-semibold text-error-content">{% translate "Error" %}</div>
|
||||
<div class="text-sm text-error-content">{% translate "Please correct the errors below before saving again." %}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<h2 class="page-subtitle">{% translate "Personal information" %}</h2>
|
||||
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2">
|
||||
{% form_field form.first_name %}
|
||||
{% form_field form.last_name %}
|
||||
{% form_field form.birthday %}
|
||||
</div>
|
||||
|
||||
<h2 class="page-subtitle">{% translate "Contact information" %}</h2>
|
||||
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2">
|
||||
{% form_field form.email %}
|
||||
{% form_field form.phone_number %}
|
||||
{% form_field form.emergency_phone_number %}
|
||||
</div>
|
||||
|
||||
<h2 class="page-subtitle">{% translate "Family information" %}</h2>
|
||||
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2">
|
||||
{% form_field form.family_members %}
|
||||
</div>
|
||||
|
||||
<h2 class="page-subtitle">{% translate "Club information" %}</h2>
|
||||
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2">
|
||||
{% form_field form.license %}
|
||||
{% form_field form.admin show_as_toggle=True %}
|
||||
</div>
|
||||
|
||||
<h2 class="page-subtitle">{% translate "Password" %}</h2>
|
||||
<div class="mt-2 text-sm text-justify">{% blocktranslate %}Setting the password here will overwrite the current password for this member, after changing the member will be prompted to set a new password at the next login.<br /><br />If both fields are empty the current password will not be changed.{% endblocktranslate %}</div>
|
||||
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2">
|
||||
{% form_field form.password %}
|
||||
{% form_field form.password_confirmation %}
|
||||
</div>
|
||||
|
||||
<button class="w-full mt-8 btn btn-neutral" type="submit">
|
||||
<i class="fa-solid fa-floppy-disk"></i>{% translate "Save" %}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<script type="text/javascript">
|
||||
new Choices(document.querySelector("#id_family_members"));
|
||||
</script>
|
||||
{% endpartialdef content %}
|
||||
{% endblock content %}
|
||||
19
templates/templatetags/avatar.html
Normal file
19
templates/templatetags/avatar.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<div class="avatar avatar-placeholder font-semibold {% if button %}btn btn-circle btn-ghost{% endif %}" {% if button %}tabindex="0" role="button"{% endif %}>
|
||||
{% if width == "xl" %}
|
||||
<div class="w-24 rounded-lg" style="background-color: {{ background }}; color: {{ foreground }};">
|
||||
<span class="text-4xl">{{ name }}</span>
|
||||
</div>
|
||||
{% elif width == "lg" %}
|
||||
<div class="w-16 rounded-lg" style="background-color: {{ background }}; color: {{ foreground }};">
|
||||
<span class="text-2xl">{{ name }}</span>
|
||||
</div>
|
||||
{% elif width == "sm" %}
|
||||
<div class="w-8 rounded-lg" style="background-color: {{ background }}; color: {{ foreground }};">
|
||||
<span class="text-sm">{{ name }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rounded-lg {% if button %}w-10{% else %}w-12{% endif %}" style="background-color: {{ background }}; color: {{ foreground }};">
|
||||
<span class="{% if not button %}text-xl{% endif %}">{{ name }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
117
templates/templatetags/field.html
Normal file
117
templates/templatetags/field.html
Normal file
@@ -0,0 +1,117 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div class="w-full max-w-6xl {% if size_modifier %}lg:w-fit{% endif %}">
|
||||
<div class="flex flex-col gap-y-2">
|
||||
{% if show_label %}
|
||||
<div class="flex flex-row items-end min-h-6">
|
||||
<div class="ml-1 grow text-sm font-semibold {% if field.errors %}text-error{% else %}text-base-content{% endif %}">
|
||||
{{ field.label }}
|
||||
</div>
|
||||
|
||||
{% if field.field.required %}
|
||||
<div class="mr-1">
|
||||
<span class="badge badge-sm {% if field.errors %}badge-error text-error-content{% else %}badge-neutral text-neutral-content{% endif %}">{% translate "required" %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
{% if field_type == "input" %}
|
||||
<input
|
||||
type="{% if field.widget_type == "regionalphonenumber" %}tel{% elif field.widget_type == "datetime" %}datetime-local{% else %}{{ field.widget_type }}{% endif %}"
|
||||
class="input w-full {% if field.errors %}input-error text-error{% endif %} {% if size_modifier %}input-{{ size_modifier }}{% endif %}"
|
||||
name="{{ field.html_name }}"
|
||||
value="{% if field.widget_type == "date" or field.widget_type == "datetime" %}{% if field.value|date:"c"|default:"" != "" %}{{ field.value|date:"c"|default:"" }}{% else %}{{ field.value|default:"" }}{% endif %}{% else %}{% if field.value == None %}{{ field.value|default:"" }}{% else %}{{ field.value }}{% endif %}{% endif %}"
|
||||
{% if show_placeholder %}placeholder="{{ field.label|capfirst }}"{% endif %}
|
||||
/>
|
||||
|
||||
{% elif field_type == "checkbox" %}
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
{% if show_as_toggle %}
|
||||
class="toggle {% if size_modifier %}toggle-{{ size_modifier }}{% endif %}"
|
||||
{% else %}
|
||||
class="checkbox {% if size_modifier %}checkbox-{{ size_modifier }}{% endif %}"
|
||||
{% endif %}
|
||||
name="{{ field.html_name }}"
|
||||
{% if field.value %}checked="checked"{% endif %}
|
||||
/>
|
||||
|
||||
{% if show_help_text %}
|
||||
<div class="text-sm">
|
||||
{{ field.help_text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% elif field_type == "textarea" %}
|
||||
<textarea
|
||||
name="{{ field.html_name }}"
|
||||
class="{% if type == "markdownx" %}markdownx{% endif %} textarea textarea-bordered h-72 w-full {% if field.errors %}textarea-error text-error{% endif %} {% if size_modifier %}textarea-{{ size_modifier }}{% endif %}"
|
||||
{% if show_placeholder %}placeholder="{{ field.label }}"{% endif %}
|
||||
>{{ field.value|default:"" }}</textarea>
|
||||
|
||||
{% elif field_type == "select" %}
|
||||
<select
|
||||
class="select w-full {% if size_modifier %}select-{{ size_modifier }}{% endif %} {% if field.errors %}select-error text-error{% endif %}"
|
||||
name="{{ field.html_name }}"
|
||||
id="{{ field.auto_id }}"
|
||||
{% if field.widget_type == "selectmultiple" %}multiple{% endif %}
|
||||
>
|
||||
|
||||
{% if field.widget_type == "selectmultiple" %}
|
||||
{% if not show_label %}
|
||||
<option selected disabled>{{ field.label|capfirst }}</option>
|
||||
{% endif %}
|
||||
|
||||
{% for option_value, option_label in field.field.choices %}
|
||||
<option value="{{ option_value }}" {% if option_value in field.value %}selected="selected"{% endif %}>
|
||||
{{ option_label }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% if not show_label %}
|
||||
<option selected disabled>{{ field.label|capfirst }}</option>
|
||||
{% endif %}
|
||||
|
||||
{% for option_value, option_label in field.field.choices %}
|
||||
<option value="{{ option_value }}" {% if field.value|stringformat:"s" == option_value|stringformat:"s" %}selected="selected"{% endif %}>
|
||||
{{ option_label }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
</select>
|
||||
|
||||
{% elif field_type == "file" %}
|
||||
<input
|
||||
type="file"
|
||||
class="file-input w-full {% if size_modifier %}file-input-{{ size_modifier }}{% endif %}"
|
||||
name="{{ field.html_name }}"
|
||||
/>
|
||||
|
||||
{% if field.value %}
|
||||
<span class="my-1 text-xs opacity-50">{% translate "Current file:" %} {{ field.value }}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if field_type != "checkbox" and field.help_text and show_help_text or field.errors %}
|
||||
<div class="flex flex-col gap-1 ml-2">
|
||||
{% if field.errors %}
|
||||
<div class="text-xs text-error">
|
||||
{{ field.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if field.help_text %}
|
||||
<div class="text-xs self-start opacity-50 {% if field.errors %}text-error{% endif %}">
|
||||
{{ field.help_text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,7 +5,42 @@
|
||||
|
||||
@import "tailwindcss";
|
||||
|
||||
@plugin "daisyui";
|
||||
@plugin "daisyui" {
|
||||
}
|
||||
|
||||
@plugin "daisyui/theme" {
|
||||
name: "light";
|
||||
default: true;
|
||||
prefersdark: true;
|
||||
--color-base-100: oklch(100% 0 0);
|
||||
--color-base-200: oklch(98% 0 0);
|
||||
--color-base-300: oklch(95% 0 0);
|
||||
--color-base-content: oklch(21% 0.006 285.885);
|
||||
--color-primary: oklch(45% 0.24 277.023);
|
||||
--color-primary-content: oklch(93% 0.034 272.788);
|
||||
--color-secondary: oklch(65% 0.241 354.308);
|
||||
--color-secondary-content: oklch(94% 0.028 342.258);
|
||||
--color-accent: oklch(77% 0.152 181.912);
|
||||
--color-accent-content: oklch(38% 0.063 188.416);
|
||||
--color-neutral: oklch(14% 0.005 285.823);
|
||||
--color-neutral-content: oklch(92% 0.004 286.32);
|
||||
--color-info: oklch(74% 0.16 232.661);
|
||||
--color-info-content: oklch(29% 0.066 243.157);
|
||||
--color-success: oklch(76% 0.177 163.223);
|
||||
--color-success-content: oklch(37% 0.077 168.94);
|
||||
--color-warning: oklch(82% 0.189 84.429);
|
||||
--color-warning-content: oklch(41% 0.112 45.904);
|
||||
--color-error: oklch(71% 0.194 13.428);
|
||||
--color-error-content: oklch(27% 0.105 12.094);
|
||||
--radius-selector: 0.5rem;
|
||||
--radius-field: 0.5rem;
|
||||
--radius-box: 0.5rem;
|
||||
--size-selector: 0.25rem;
|
||||
--size-field: 0.25rem;
|
||||
--border: 1px;
|
||||
--depth: 0;
|
||||
--noise: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* A catch-all path to Django template files, JavaScript, and Python files
|
||||
@@ -20,3 +55,458 @@
|
||||
--font-jersey: Graduate, sans-serif;
|
||||
--font-sans: Open Sans, Noto Sans, Barlow Semi Condensed, Ubuntu, Fira Sans, Catamaran, Cabin, Roboto, sans-serif;
|
||||
}
|
||||
|
||||
@source inline("input-{xs,sm,md,lg,xl}");
|
||||
@source inline("select-{xs,sm,md,lg,xl}");
|
||||
|
||||
@layer components {
|
||||
h1.page-title {
|
||||
@apply text-3xl font-bold;
|
||||
@apply mb-12;
|
||||
|
||||
& > svg {
|
||||
@apply mr-2;
|
||||
}
|
||||
}
|
||||
|
||||
h2.page-subtitle {
|
||||
@apply text-xl font-bold;
|
||||
@apply mt-8;
|
||||
@apply border-b border-base-content;
|
||||
}
|
||||
|
||||
div.action_bar {
|
||||
@apply flex flex-col lg:flex-row;
|
||||
@apply items-center;
|
||||
|
||||
& > .filter {
|
||||
@apply grow w-full;
|
||||
}
|
||||
|
||||
& > .filter > form {
|
||||
@apply flex flex-col lg:flex-row gap-2;
|
||||
@apply items-end;
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
& > .filter > form > div > button {
|
||||
@apply btn btn-outline btn-xs;
|
||||
@apply grow;
|
||||
}
|
||||
|
||||
& > .add {
|
||||
@apply my-6 lg:my-0;
|
||||
@apply w-full lg:w-fit shrink-0;
|
||||
@apply flex flex-row flex-wrap gap-2;
|
||||
}
|
||||
|
||||
& > .add > a {
|
||||
@apply min-w-fit lg:w-fit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.choices {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0px;
|
||||
font-size: 16px
|
||||
}
|
||||
|
||||
.choices:focus {
|
||||
outline: 0
|
||||
}
|
||||
|
||||
.choices:last-child {
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
.choices.is-open {
|
||||
overflow: visible
|
||||
}
|
||||
|
||||
.choices.is-disabled .choices__inner,
|
||||
.choices.is-disabled .choices__input {
|
||||
background-color: #eaeaea;
|
||||
cursor: not-allowed;
|
||||
-webkit-user-select: none;
|
||||
user-select: none
|
||||
}
|
||||
|
||||
.choices.is-disabled .choices__item {
|
||||
cursor: not-allowed
|
||||
}
|
||||
|
||||
.choices [hidden] {
|
||||
display: none !important
|
||||
}
|
||||
|
||||
.choices[data-type*=select-one] {
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.choices[data-type*=select-one] .choices__inner {
|
||||
padding-bottom: 7.5px
|
||||
}
|
||||
|
||||
.choices[data-type*=select-one] .choices__input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
background-color: #fff;
|
||||
margin: 0
|
||||
}
|
||||
|
||||
.choices[data-type*=select-one] .choices__button {
|
||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==);
|
||||
padding: 0;
|
||||
background-size: 8px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
margin-top: -10px;
|
||||
margin-right: 25px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 10em;
|
||||
opacity: .25
|
||||
}
|
||||
|
||||
.choices[data-type*=select-one] .choices__button:focus,
|
||||
.choices[data-type*=select-one] .choices__button:hover {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
.choices[data-type*=select-one] .choices__button:focus {
|
||||
box-shadow: 0 0 0 2px #005f75
|
||||
}
|
||||
|
||||
.choices[data-type*=select-one] .choices__item[data-placeholder] .choices__button {
|
||||
display: none
|
||||
}
|
||||
|
||||
.choices[data-type*=select-one]::after {
|
||||
content: "";
|
||||
height: 0;
|
||||
width: 0;
|
||||
border-style: solid;
|
||||
border-color: #333 transparent transparent;
|
||||
border-width: 5px;
|
||||
position: absolute;
|
||||
right: 11.5px;
|
||||
top: 50%;
|
||||
margin-top: -2.5px;
|
||||
pointer-events: none
|
||||
}
|
||||
|
||||
.choices[data-type*=select-one].is-open::after {
|
||||
border-color: transparent transparent #333;
|
||||
margin-top: -7.5px
|
||||
}
|
||||
|
||||
.choices[data-type*=select-one][dir=rtl]::after {
|
||||
left: 11.5px;
|
||||
right: auto
|
||||
}
|
||||
|
||||
.choices[data-type*=select-one][dir=rtl] .choices__button {
|
||||
right: auto;
|
||||
left: 0;
|
||||
margin-left: 25px;
|
||||
margin-right: 0
|
||||
}
|
||||
|
||||
.choices[data-type*=select-multiple] .choices__inner,
|
||||
.choices[data-type*=text] .choices__inner {
|
||||
cursor: text
|
||||
}
|
||||
|
||||
.choices[data-type*=select-multiple] .choices__button,
|
||||
.choices[data-type*=text] .choices__button {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 0-4px 0 2px;
|
||||
padding-left: 16px;
|
||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjRkZGIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==);
|
||||
background-size: 8px;
|
||||
width: 8px;
|
||||
line-height: 1;
|
||||
opacity: .75;
|
||||
border-radius: 0
|
||||
}
|
||||
|
||||
.choices[data-type*=select-multiple] .choices__button:focus,
|
||||
.choices[data-type*=select-multiple] .choices__button:hover,
|
||||
.choices[data-type*=text] .choices__button:focus,
|
||||
.choices[data-type*=text] .choices__button:hover {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
.choices__inner {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 100%;
|
||||
background-color: var(--color-base-100);
|
||||
padding: 7.5px 7.5px 3.75px;
|
||||
border: 1px solid color-mix(in oklab, var(--color-base-content) 20%, #0000);
|
||||
border-radius: var(--radius-field);
|
||||
font-size: 14px;
|
||||
min-height: 44px;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
|
||||
.is-focused .choices__inner,
|
||||
.is-open .choices__inner {
|
||||
border-color: var(--color-base-content);
|
||||
isolation: isolate;
|
||||
border-radius: var(--radius-field);
|
||||
}
|
||||
|
||||
.is-focused {
|
||||
border: 2px solid var(--color-base-content);
|
||||
border-radius: calc(var(--radius-field) * 2);
|
||||
margin: -2px;
|
||||
padding: 2px; /* adjust offset */
|
||||
}
|
||||
|
||||
|
||||
|
||||
.is-open .choices__inner {
|
||||
border-radius: var(--radius-field);
|
||||
}
|
||||
|
||||
.is-flipped.is-open .choices__inner {
|
||||
border-radius: var(--radius-field);
|
||||
}
|
||||
|
||||
.choices__list {
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
list-style: none
|
||||
}
|
||||
|
||||
.choices__list--single {
|
||||
display: inline-block;
|
||||
padding: 4px 16px 4px 4px;
|
||||
width: 100%
|
||||
}
|
||||
|
||||
[dir=rtl] .choices__list--single {
|
||||
padding-right: 4px;
|
||||
padding-left: 16px
|
||||
}
|
||||
|
||||
.choices__list--single .choices__item {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
.choices__list--multiple {
|
||||
display: inline
|
||||
}
|
||||
|
||||
.choices__list--multiple .choices__item {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
border-radius: var(--radius-selector);
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-right: 3.75px;
|
||||
margin-bottom: 3.75px;
|
||||
background-color: var(--color-primary);
|
||||
border: 1px solid var(--color-primary);
|
||||
color: #fff;
|
||||
word-break: break-all;
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
.choices__list--multiple .choices__item[data-deletable] {
|
||||
padding-right: 5px
|
||||
}
|
||||
|
||||
[dir=rtl] .choices__list--multiple .choices__item {
|
||||
margin-right: 0;
|
||||
margin-left: 3.75px
|
||||
}
|
||||
|
||||
.choices__list--multiple .choices__item.is-highlighted {
|
||||
background-color: var(--color-secondary);
|
||||
border: 1px solid var(--color-secondary)
|
||||
}
|
||||
|
||||
.is-disabled .choices__list--multiple .choices__item {
|
||||
background-color: #aaa;
|
||||
border: 1px solid #919191
|
||||
}
|
||||
|
||||
.choices__list--dropdown,
|
||||
.choices__list[aria-expanded] {
|
||||
display: none;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
background-color: var(--color-base-100);
|
||||
border: 1px solid #ddd;
|
||||
top: 100%;
|
||||
margin-top: 3px;
|
||||
border-radius: var(--radius-field);
|
||||
overflow: hidden;
|
||||
word-break: break-all
|
||||
}
|
||||
|
||||
.is-active.choices__list--dropdown,
|
||||
.is-active.choices__list[aria-expanded] {
|
||||
display: block
|
||||
}
|
||||
|
||||
.is-open .choices__list--dropdown,
|
||||
.is-open .choices__list[aria-expanded] {
|
||||
border-color: #b7b7b7
|
||||
}
|
||||
|
||||
.is-flipped .choices__list--dropdown,
|
||||
.is-flipped .choices__list[aria-expanded] {
|
||||
top: auto;
|
||||
bottom: 100%;
|
||||
margin-top: 0;
|
||||
margin-bottom: -1px;
|
||||
border-radius: .25rem .25rem 0 0
|
||||
}
|
||||
|
||||
.choices__list--dropdown .choices__list,
|
||||
.choices__list[aria-expanded] .choices__list {
|
||||
position: relative;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
will-change: scroll-position
|
||||
}
|
||||
|
||||
.choices__list--dropdown .choices__item,
|
||||
.choices__list[aria-expanded] .choices__item {
|
||||
position: relative;
|
||||
padding: 10px;
|
||||
font-size: 14px
|
||||
}
|
||||
|
||||
[dir=rtl] .choices__list--dropdown .choices__item,
|
||||
[dir=rtl] .choices__list[aria-expanded] .choices__item {
|
||||
text-align: right
|
||||
}
|
||||
|
||||
@media (min-width:640px) {
|
||||
|
||||
.choices__list--dropdown .choices__item--selectable[data-select-text],
|
||||
.choices__list[aria-expanded] .choices__item--selectable[data-select-text] {
|
||||
padding-right: 100px
|
||||
}
|
||||
|
||||
.choices__list--dropdown .choices__item--selectable[data-select-text]::after,
|
||||
.choices__list[aria-expanded] .choices__item--selectable[data-select-text]::after {
|
||||
content: attr(data-select-text);
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%)
|
||||
}
|
||||
|
||||
[dir=rtl] .choices__list--dropdown .choices__item--selectable[data-select-text],
|
||||
[dir=rtl] .choices__list[aria-expanded] .choices__item--selectable[data-select-text] {
|
||||
text-align: right;
|
||||
padding-left: 100px;
|
||||
padding-right: 10px
|
||||
}
|
||||
|
||||
[dir=rtl] .choices__list--dropdown .choices__item--selectable[data-select-text]::after,
|
||||
[dir=rtl] .choices__list[aria-expanded] .choices__item--selectable[data-select-text]::after {
|
||||
right: auto;
|
||||
left: 10px
|
||||
}
|
||||
}
|
||||
|
||||
.choices__list--dropdown .choices__item--selectable.is-highlighted,
|
||||
.choices__list[aria-expanded] .choices__item--selectable.is-highlighted {
|
||||
background-color: var(--color-base-100)
|
||||
}
|
||||
|
||||
.choices__list--dropdown .choices__item--selectable.is-highlighted::after,
|
||||
.choices__list[aria-expanded] .choices__item--selectable.is-highlighted::after {
|
||||
opacity: .5
|
||||
}
|
||||
|
||||
.choices__item {
|
||||
cursor: default
|
||||
}
|
||||
|
||||
.choices__item--selectable {
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.choices__item--disabled {
|
||||
cursor: not-allowed;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
opacity: .5
|
||||
}
|
||||
|
||||
.choices__heading {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #f7f7f7;
|
||||
color: gray
|
||||
}
|
||||
|
||||
.choices__button {
|
||||
text-indent: -9999px;
|
||||
appearance: none;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.choices__button:focus,
|
||||
.choices__input:focus {
|
||||
outline: 0
|
||||
}
|
||||
|
||||
.choices__input {
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
background-color: var(--color-base-100);
|
||||
font-size: 14px;
|
||||
margin-bottom: 5px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
max-width: 100%;
|
||||
padding: 4px 0 4px 2px
|
||||
}
|
||||
|
||||
.choices__input::-webkit-search-cancel-button,
|
||||
.choices__input::-webkit-search-decoration,
|
||||
.choices__input::-webkit-search-results-button,
|
||||
.choices__input::-webkit-search-results-decoration {
|
||||
display: none
|
||||
}
|
||||
|
||||
.choices__input::-ms-clear,
|
||||
.choices__input::-ms-reveal {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0
|
||||
}
|
||||
|
||||
[dir=rtl] .choices__input {
|
||||
padding-right: 2px;
|
||||
padding-left: 0
|
||||
}
|
||||
|
||||
.choices__placeholder {
|
||||
opacity: .5
|
||||
}
|
||||
0
theme/templatetags/__init__.py
Normal file
0
theme/templatetags/__init__.py
Normal file
49
theme/templatetags/avatar.py
Normal file
49
theme/templatetags/avatar.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from hashlib import md5
|
||||
from math import sqrt
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
display_name = initials
|
||||
full_name = initials
|
||||
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
|
||||
}
|
||||
43
theme/templatetags/form_field.py
Normal file
43
theme/templatetags/form_field.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from django import template
|
||||
from django.forms import BoundField
|
||||
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}
|
||||
17
theme/templatetags/pagination.py
Normal file
17
theme/templatetags/pagination.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from typing import Optional
|
||||
|
||||
from django import template
|
||||
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()
|
||||
75
uv.lock
generated
75
uv.lock
generated
@@ -255,6 +255,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/40/6a02495c5658beb1f31eb09952d8aa12ef3c2a66342331ce3a35f7132439/django_filter-25.2-py3-none-any.whl", hash = "sha256:9c0f8609057309bba611062fe1b720b4a873652541192d232dd28970383633e3", size = 94145, upload-time = "2025-10-05T09:51:29.728Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django-htmx"
|
||||
version = "1.27.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asgiref" },
|
||||
{ name = "django" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/34/f2/8c3e28a5eed8e5226835c762892bfef74eda7e8629c65b49c186098eb303/django_htmx-1.27.0.tar.gz", hash = "sha256:036e5da801bfdf5f1ca815f21592cfb9f004a898f330c842f15e55c70e301a75", size = 65362, upload-time = "2025-11-28T23:18:55.049Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/23/ac/25d28489dc43224e260f4ebee7565f7ef1efe12af0f284a89500c19f75e2/django_htmx-1.27.0-py3-none-any.whl", hash = "sha256:13e1e13b87d39b57f95aae6e4987cb3df056d0b1373a41f4a94504a00298ffd8", size = 62126, upload-time = "2025-11-28T23:18:53.57Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django-phonenumber-field"
|
||||
version = "8.4.0"
|
||||
@@ -408,6 +421,64 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/37/71/364ea74338bde467bec6b6b0ab33b5ced57e473dfb427b96cc78da8e6af4/phonenumbers-9.0.21-py2.py3-none-any.whl", hash = "sha256:3a0f717fddf901a5a424f47c43fb72722cb45bd25ee87331987b00eafe6855bf", size = 2584216, upload-time = "2025-12-18T07:37:24.539Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "12.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg2-binary"
|
||||
version = "2.9.11"
|
||||
@@ -616,8 +687,10 @@ dependencies = [
|
||||
{ name = "django-constance" },
|
||||
{ name = "django-extensions" },
|
||||
{ name = "django-filter" },
|
||||
{ name = "django-htmx" },
|
||||
{ name = "django-phonenumber-field", extra = ["phonenumbers"] },
|
||||
{ name = "django-tailwind", extra = ["cookiecutter", "honcho"] },
|
||||
{ name = "pillow" },
|
||||
{ name = "psycopg2-binary" },
|
||||
{ name = "python-decouple" },
|
||||
{ name = "rules" },
|
||||
@@ -636,8 +709,10 @@ requires-dist = [
|
||||
{ name = "django-constance", specifier = ">=4.3.4" },
|
||||
{ name = "django-extensions", specifier = ">=4.1" },
|
||||
{ name = "django-filter", specifier = ">=25.2" },
|
||||
{ name = "django-htmx", specifier = ">=1.27.0" },
|
||||
{ name = "django-phonenumber-field", extras = ["phonenumbers"], specifier = ">=8.4.0" },
|
||||
{ name = "django-tailwind", extras = ["cookiecutter", "honcho"], specifier = ">=4.4.2" },
|
||||
{ name = "pillow", specifier = ">=12.1.0" },
|
||||
{ name = "psycopg2-binary", specifier = ">=2.9.11" },
|
||||
{ name = "python-decouple", specifier = ">=3.8" },
|
||||
{ name = "rules", specifier = ">=3.5" },
|
||||
|
||||
Reference in New Issue
Block a user