Compare commits
10 Commits
6f07c32fb1
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| 72e6388c0c | |||
| cb3da371d1 | |||
| 8f9357f1b8 | |||
| 4e4fe62f11 | |||
| b71bc2afc0 | |||
| 4b819d9dd9 | |||
| 7aa4a4816c | |||
| 3d94b9b2d8 | |||
| 95e46fe727 | |||
| e55c88c742 |
@@ -43,6 +43,7 @@ INSTALLED_APPS = [
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"waffle",
|
||||
"constance",
|
||||
"tailwind",
|
||||
"django_filters",
|
||||
@@ -62,6 +63,7 @@ MIDDLEWARE = [
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"django_htmx.middleware.HtmxMiddleware",
|
||||
"waffle.middleware.WaffleMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "TeamForge.urls"
|
||||
@@ -147,10 +149,12 @@ 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"
|
||||
PHONENUMBER_DEFAULT_REGION = config("CM_CLUB_COUNTRY_CODE", default="BE", cast=str)
|
||||
|
||||
TAILWIND_APP_NAME = "theme"
|
||||
|
||||
WAFFLE_CREATE_MISSING_FLAGS = True
|
||||
WAFFLE_CREATE_MISSING_SWITCHES = True
|
||||
|
||||
12
backend/forms.py
Normal file
12
backend/forms.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ConfigurationForm(forms.Form):
|
||||
"""Form instance that holds configuration values for the application."""
|
||||
|
||||
club_name = forms.CharField(label=_("Club name"), max_length=250)
|
||||
club_location = forms.CharField(label=_("Club location"), max_length=250, help_text=_("Changing this setting will set a new home game location and will update already existing games"))
|
||||
club_logo = forms.ImageField(label=_("Club logo"), required=False)
|
||||
enable_teams = forms.BooleanField(label=_("Enable teams"), required=False)
|
||||
enable_activities = forms.BooleanField(label=_("Enable activities"), required=False)
|
||||
@@ -6,7 +6,7 @@ app_name = "members"
|
||||
urlpatterns = [
|
||||
path("", MemberListView.as_view(), name="list"),
|
||||
path("add/", MemberAddView.as_view(), name="add"),
|
||||
# path("<int:pk>/edit/", MemberEditView.as_view(), name="edit"),
|
||||
path("<int:pk>/edit/", MemberEditView.as_view(), name="edit"),
|
||||
path("<int:pk>/delete/", MemberDeleteView.as_view(), name="delete"),
|
||||
# path("load/", MemberLoadView.as_view(), name="load"),
|
||||
path("load/", MemberLoadView.as_view(), name="load"),
|
||||
]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import csv
|
||||
import io
|
||||
from typing import Any
|
||||
|
||||
from django.contrib import messages
|
||||
@@ -5,15 +7,18 @@ 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.views.generic import CreateView, DeleteView, FormView, UpdateView
|
||||
from django_filters.views import FilterView
|
||||
from rules.contrib.views import PermissionRequiredMixin
|
||||
from waffle.mixins import WaffleFlagMixin
|
||||
|
||||
from members.filters import MemberFilter
|
||||
from members.forms import MassUploadForm, MemberForm
|
||||
from members.models import Member
|
||||
from members.forms import MemberForm
|
||||
|
||||
from ..mixins import HTMXViewMixin
|
||||
|
||||
|
||||
class MemberListView(HTMXViewMixin, PermissionRequiredMixin, FilterView):
|
||||
filterset_class = MemberFilter
|
||||
paginate_by = 50
|
||||
@@ -25,15 +30,15 @@ class MemberListView(HTMXViewMixin, PermissionRequiredMixin, FilterView):
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -47,11 +52,11 @@ class MemberAddView(HTMXViewMixin, PermissionRequiredMixin, SuccessMessageMixin,
|
||||
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())
|
||||
|
||||
@@ -65,14 +70,22 @@ class MemberEditView(HTMXViewMixin, PermissionRequiredMixin, SuccessMessageMixin
|
||||
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())
|
||||
|
||||
def get_initial(self):
|
||||
initial = super().get_initial()
|
||||
user = self.get_object().user
|
||||
|
||||
initial.update({"first_name": user.first_name, "last_name": user.last_name, "email": user.email, "admin": user.is_superuser})
|
||||
|
||||
return initial
|
||||
|
||||
|
||||
class MemberDeleteView(HTMXViewMixin, PermissionRequiredMixin, SuccessMessageMixin, DeleteView):
|
||||
model = Member
|
||||
@@ -82,24 +95,55 @@ class MemberDeleteView(HTMXViewMixin, PermissionRequiredMixin, SuccessMessageMix
|
||||
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: ...
|
||||
class MemberLoadView(PermissionRequiredMixin, HTMXViewMixin, SuccessMessageMixin, WaffleFlagMixin, FormView):
|
||||
form_class = MassUploadForm
|
||||
permission_required = "members.add_member"
|
||||
permission_denied_message = _("You do not have permission to view this page.")
|
||||
success_url = reverse_lazy("backend:members:list")
|
||||
success_message = _("Members have been added successfully.")
|
||||
partial_name = "members/member_load.html#content"
|
||||
menu_highlight = "members"
|
||||
template_name = "members/member_load.html"
|
||||
waffle_flag = "TF_MASS_UPLOAD"
|
||||
|
||||
def handle_no_permission(self) -> HttpResponseRedirect:
|
||||
messages.error(self.request, self.get_permission_denied_message())
|
||||
return HttpResponseRedirect(reverse_lazy("backend:index"))
|
||||
|
||||
def form_valid(self, form: MassUploadForm) -> HttpResponse:
|
||||
member_data = self.request.FILES["members_data"]
|
||||
|
||||
with io.TextIOWrapper(member_data.file) as csvfile:
|
||||
reader = csv.reader(csvfile)
|
||||
|
||||
for row in reader:
|
||||
member_information = {"first_name": row[0], "last_name": row[1], "email": row[2], "birthday": row[3], "license": row[4]}
|
||||
member = Member.create(first_name=member_information["fist_name"], last_name=member_information["last_name"], email=member_information["email"])
|
||||
|
||||
member.license = member_information["license"]
|
||||
if member_information["birthday"] is not None and member_information["birthday"] != "":
|
||||
member.birthday = member_information["birthday"]
|
||||
|
||||
member.save(update_fields=["license", "birthday"])
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from django.urls import include, path
|
||||
|
||||
from .views import index
|
||||
from .views import configuration, index
|
||||
|
||||
app_name = "backend"
|
||||
urlpatterns = [
|
||||
path("", index, name="index"),
|
||||
path("members/", include("backend.members.urls")),
|
||||
path("configuration", configuration, name="configuration")
|
||||
]
|
||||
|
||||
@@ -1,6 +1,59 @@
|
||||
from pathlib import Path
|
||||
|
||||
from constance import config
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||
from django.core.cache import cache
|
||||
from django.core.files.storage import default_storage
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from waffle.models import Switch
|
||||
|
||||
from backend.forms import ConfigurationForm
|
||||
|
||||
|
||||
# Create your views here.
|
||||
def index(request):
|
||||
return render(request, "backend/index.html")
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(lambda u: u.is_superuser)
|
||||
def configuration(request: HttpRequest) -> HttpResponse:
|
||||
switches = {
|
||||
"enable_teams": Switch.objects.get_or_create(name="TF_TEAMS", defaults={"active": False})[0],
|
||||
"enable_activities": Switch.objects.get_or_create(name="TF_ACTIVITIES", defaults={"active": False})[0],
|
||||
}
|
||||
|
||||
initial_data = {
|
||||
"club_name": config.TF_CLUB_NAME,
|
||||
"club_location": config.TF_CLUB_HOME,
|
||||
"club_logo": config.TF_CLUB_LOGO,
|
||||
"enable_teams": switches["enable_teams"].active,
|
||||
"enable_activities": switches["enable_activities"].active,
|
||||
}
|
||||
|
||||
form = ConfigurationForm(initial=initial_data)
|
||||
|
||||
if request.method == "POST":
|
||||
form = ConfigurationForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
config.TF_CLUB_NAME = form.cleaned_data["club_name"]
|
||||
config.TF_CLUB_HOME = form.cleaned_data["club_location"]
|
||||
|
||||
if form.cleaned_data["club_logo"] is not None:
|
||||
default_storage.save(str(Path(settings.STATIC_ROOT) / form.cleaned_data["club_logo"].name), form.cleaned_data["club_logo"])
|
||||
config.TF_CLUB_LOGO = form.cleaned_data["club_logo"].name
|
||||
|
||||
for switch_key in switches.keys():
|
||||
if switches[switch_key].active != form.cleaned_data[switch_key]:
|
||||
switches[switch_key].active = form.cleaned_data[switch_key]
|
||||
|
||||
Switch.objects.bulk_update(switches.values(), ["active"])
|
||||
cache.clear()
|
||||
|
||||
messages.success(request=request, message=_("Settings have been saved successfully"))
|
||||
|
||||
return render(request, "backend/configuration.html", {"form": form})
|
||||
|
||||
@@ -3,40 +3,43 @@ 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
|
||||
|
||||
|
||||
|
||||
|
||||
class MassUploadForm(forms.Form):
|
||||
csv_file = forms.FileField()
|
||||
|
||||
@@ -12,6 +12,7 @@ dependencies = [
|
||||
"django-htmx>=1.27.0",
|
||||
"django-phonenumber-field[phonenumbers]>=8.4.0",
|
||||
"django-tailwind[cookiecutter,honcho]>=4.4.2",
|
||||
"django-waffle>=5.0.0",
|
||||
"pillow>=12.1.0",
|
||||
"psycopg2-binary>=2.9.11",
|
||||
"python-decouple>=3.8",
|
||||
|
||||
@@ -4,17 +4,18 @@
|
||||
|
||||
{% block sidebar %}
|
||||
{% url "backend:members:list" as members_list %}
|
||||
{% url "backend:configuration" as configuration %}
|
||||
|
||||
{% 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>
|
||||
<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>
|
||||
{% if request.user.is_superuser %}
|
||||
<li class="menu-title mt-4">Configuration</li>
|
||||
<li><a href="{% url "backend:configuration" %}" class="menu-item {% if configuration in request.path %}menu-active{% endif %}" data-menu="configuration"><i class="fa-solid fa-screwdriver-wrench"></i> Settings</a></li>
|
||||
{% endif %}
|
||||
{% endblock sidebar %}
|
||||
76
templates/backend/configuration.html
Normal file
76
templates/backend/configuration.html
Normal file
@@ -0,0 +1,76 @@
|
||||
{% extends "backend/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load form_field %}
|
||||
{% load avatar %}
|
||||
{% load waffle_tags %}
|
||||
|
||||
{% block content %}
|
||||
{% partialdef content inline %}
|
||||
<h1 class="page-title">{% translate "Configuration" %}</h1>
|
||||
|
||||
{% blocktranslate %}
|
||||
<article class="prose text-justify max-w-none">
|
||||
<p>Use the below options to configure your TeamForge instance. Options marked as required will need to be set in order for TeamForge to function properly.</p>
|
||||
<p>You can also use this form to enable or disable certain pieces of functionality for your users.</p>
|
||||
</article>
|
||||
{% endblocktranslate %}
|
||||
|
||||
{% 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 "General configuration" %}</h2>
|
||||
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{% form_field form.club_name %}
|
||||
{% form_field form.club_location %}
|
||||
{% form_field form.club_logo %}
|
||||
</div>
|
||||
|
||||
<h2 class="page-subtitle">{% translate "Module configuration" %}</h2>
|
||||
<div class="grid grid-cols-1 gap-1 mt-2 gap-x-4 lg:grid-cols-3 xl:grid-cols-5">
|
||||
<div class="flex flex-row gap-2">
|
||||
<span class="font-semibold">{% translate "Teams" %}</span>
|
||||
{% form_field form.enable_teams show_label=False show_as_toggle=True %}
|
||||
|
||||
{% switch "TF_TEAMS" %}
|
||||
<div class="badge badge-success"><i class="fa-solid fa-check"></i>{% translate "Enabled" %}</div>
|
||||
{% else %}
|
||||
<div class="badge badge-error"><i class="fa-solid fa-times"></i>{% translate "Disabled" %}</div>
|
||||
{% endswitch %}
|
||||
</div>
|
||||
|
||||
<div></div>
|
||||
|
||||
<div class="flex flex-row gap-2">
|
||||
<span class="font-semibold">{% translate "Activities" %}</span>
|
||||
{% form_field form.enable_activities show_label=False show_as_toggle=True %}
|
||||
|
||||
{% switch "TF_ACTIVITIES" %}
|
||||
<div class="badge badge-success"><i class="fa-solid fa-check"></i>{% translate "Enabled" %}</div>
|
||||
{% else %}
|
||||
<div class="badge badge-error"><i class="fa-solid fa-times"></i>{% translate "Disabled" %}</div>
|
||||
{% endswitch %}
|
||||
</div>
|
||||
</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 %}
|
||||
@@ -39,7 +39,7 @@
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col h-screen" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||
<body class="flex flex-col h-dvh overflow-hidden" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||
<!-- NAVBAR -->
|
||||
<header id="mainNavbar" class="navbar-shrink navbar bg-base-100 sticky top-0 z-50 shadow">
|
||||
<div class="flex-none lg:hidden">
|
||||
@@ -84,7 +84,7 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="drawer lg:drawer-open flex-1">
|
||||
<div class="drawer lg:drawer-open flex-1 min-h-0">
|
||||
<!-- Hidden checkbox for mobile sidebar -->
|
||||
<input type="checkbox" id="sidebar-toggle" class="drawer-toggle">
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
<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">
|
||||
<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">
|
||||
<ul class="menu w-full">
|
||||
{% block sidebar %}{% endblock sidebar %}
|
||||
</ul>
|
||||
@@ -100,8 +100,8 @@
|
||||
</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">
|
||||
<div class="drawer-content flex w-full overflow-y-auto">
|
||||
<main class="bg-base-100 border border-base-300 rounded-xl m-4 ml-2 p-6 w-full h-fit">
|
||||
{% include "backend/partials/messages.html" %}
|
||||
|
||||
<div id="content">
|
||||
@@ -115,7 +115,7 @@
|
||||
</div>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer class="footer footer-center p-4 bg-neutral text-neutral-content mt-auto">
|
||||
<footer class="footer footer-center p-4 bg-neutral text-neutral-content">
|
||||
<p>© {% now "Y" %} TeamForge — All rights reserved.</p>
|
||||
</footer>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
{% load form_field %}
|
||||
{% load avatar %}
|
||||
{% load pagination %}
|
||||
{% load waffle_tags %}
|
||||
|
||||
{% block content %}
|
||||
{% partialdef content inline %}
|
||||
@@ -14,7 +15,7 @@
|
||||
<h1 class="page-title">{% translate "Members" %}</h1>
|
||||
|
||||
<div class="lg:hidden collapse collapse-plus bg-base-100 border-neutral border">
|
||||
<input type="checkbox" />
|
||||
<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">
|
||||
@@ -59,9 +60,11 @@
|
||||
</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>
|
||||
{% flag "TF_MASS_UPLOAD" %}
|
||||
<a class="btn btn-accent btn-sm grow hidden lg:flex" href="{% url "backend:members:load" %}" hx-get="{% url "backend:members:load" %}" hx-target="#content">
|
||||
<i class="fa-solid fa-file-upload"></i>{% translate "Load members from file" %}
|
||||
</a>
|
||||
{% endflag %}
|
||||
|
||||
<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" %}
|
||||
@@ -89,7 +92,7 @@
|
||||
{% for member in object_list %}
|
||||
<tr class="hover:bg-base-300">
|
||||
<td>
|
||||
<a href="">
|
||||
<a href="{% url "backend:members:edit" member.pk %}" hx-get="{% url "backend:members:edit" member.pk %}" hx-target="#content">
|
||||
<div class="flex flex-row items-center gap-3">
|
||||
<div>
|
||||
{% avatar first_name=member.user.first_name last_name=member.user.last_name %}
|
||||
@@ -105,7 +108,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% if not member.user.is_active %}
|
||||
<div class="badge badge-neutral badge-sm">{% translate "Inactive"%}</div>
|
||||
<div class="badge badge-neutral badge-sm">{% translate "Inactive" %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
@@ -129,7 +132,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex flex-row gap-2">
|
||||
<a class="btn btn-outline btn-sm" href="">
|
||||
<a class="btn btn-outline btn-sm" href="{% url "backend:members:edit" member.pk %}" hx-get="{% url "backend:members:edit" member.pk %}" hx-target="#content">
|
||||
<i class="fa-solid fa-eye"></i>{% translate "Details" %}
|
||||
</a>
|
||||
|
||||
@@ -151,13 +154,23 @@
|
||||
{% 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="">
|
||||
<a class="border border-base-300 rounded-lg p-2 flex flex-row gap-2 items-center" href="{% url "backend:members:edit" member.pk %}" hx-get="{% url "backend:members:edit" member.pk %}" hx-target="#content">
|
||||
<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="font-semibold text-sm">
|
||||
{{ member.user.get_full_name }}
|
||||
{% if member.license %}
|
||||
#{{ member.license }}
|
||||
{% endif %}
|
||||
{% if member.user.is_superuser %}
|
||||
<div class="badge badge-xs badge-accent">
|
||||
<i class="fa-solid fa-user-shield"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="opacity-50 text-xs">{{ member.birthday|date:"d M Y"|default:"" }}</div>
|
||||
</div>
|
||||
|
||||
@@ -190,7 +203,7 @@
|
||||
<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
|
||||
<a class="join-item btn" href="?{% url_replace request "page" page_obj.paginator.num_pages %}" hx-get="?{% url_replace request "page" page_obj.paginator.num_pages %}" hx-target="#content"><span
|
||||
class="hidden lg:inline"> {% translate "last" %}</span> »</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
{% load i18n %}
|
||||
{% load form_field %}
|
||||
{% load avatar %}
|
||||
{% load waffle_tags %}
|
||||
|
||||
{% block content %}
|
||||
{% partialdef content inline %}
|
||||
@@ -18,14 +19,6 @@
|
||||
<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 %}
|
||||
@@ -33,7 +26,7 @@
|
||||
|
||||
<div class="flex min-w-12 justify-end">
|
||||
{% if member %}
|
||||
<a class="btn btn-error btn-outline" href="{% url "backend:members:delete" member.pk %}">
|
||||
<a class="btn btn-error btn-outline" 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>
|
||||
</a>
|
||||
{% else %}
|
||||
@@ -44,29 +37,28 @@
|
||||
|
||||
{% 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" %}
|
||||
{% 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_number }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex flex-row items-center mt-8 gap-x-3 hidden lg:flex">
|
||||
{% 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-star-of-life"></i>
|
||||
{{ member.emergency_phone_number }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% switch "TF_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>
|
||||
{% endswitch %}
|
||||
|
||||
<div class="hidden 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>
|
||||
@@ -76,21 +68,21 @@
|
||||
{% 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>
|
||||
{% if member.phone_number %}
|
||||
<a href="{{ member.phone_number.as_rfc3966 }}" class="btn btn-outline btn-info"><i class="fa-solid fa-phone"></i>{{ member.phone_number }}</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>
|
||||
{% if member.emergency_phone_number %}
|
||||
<a href="{{ member.emergency_phone_number.as_rfc3966 }}" class="btn btn-outline btn-error"><i class="fa-solid fa-star-of-life"></i>{{ member.emergency_phone_number }}</a>
|
||||
{% endif %}
|
||||
|
||||
{% if config.TF_ENABLE_TEAMS %}
|
||||
{% switch "TF_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 %}
|
||||
{% endswitch %}
|
||||
|
||||
<a href="{% url "backend:members:members_delete" member.id %}" class="btn btn-error btn-outline">
|
||||
<a href="{% url "backend:members:delete" member.pk %}" class="btn btn-error btn-outline" hx-get="{% url "backend:members:delete" member.pk %}" hx-target="#content">
|
||||
<i class="fa-solid fa-trash"></i>{% translate "Delete" %}
|
||||
</a>
|
||||
</div>
|
||||
@@ -110,37 +102,38 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post">
|
||||
<form method="post" hx-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">
|
||||
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{% 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">
|
||||
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{% 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">
|
||||
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{% 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">
|
||||
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{% 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">
|
||||
<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 xl:grid-cols-3">
|
||||
{% form_field form.password %}
|
||||
{% form_field form.password_confirmation %}
|
||||
</div>
|
||||
|
||||
56
templates/members/member_load.html
Normal file
56
templates/members/member_load.html
Normal file
@@ -0,0 +1,56 @@
|
||||
{% extends "backend/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load form_field %}
|
||||
{% load avatar %}
|
||||
|
||||
{% block content %}
|
||||
{% partialdef content inline %}
|
||||
<h1 class="page-title">{% translate "Members" %}</h1>
|
||||
|
||||
<h2 class="page-subtitle border-b-0! hidden lg:flex">{% translate "Bulk load new member information" %}</h2>
|
||||
|
||||
<div class="alert alert-info mt-2 text-sm text-justify">
|
||||
<i class="text-lg fa-solid fa-info"></i>
|
||||
<span>
|
||||
{% blocktranslate %}
|
||||
Data should be formatted as a .csv file with the following information in the different columns:
|
||||
<ul class="my-2 list-disc list-inside">
|
||||
<li>First name</li>
|
||||
<li>Last name</li>
|
||||
<li>Email</li>
|
||||
<li>Birthday (YYYY-MM-DD)</li>
|
||||
<li>License number</li>
|
||||
<!-- <li>Team (short name)</li>
|
||||
<li>Role (abbreviation)</li>
|
||||
<li>Number</li>
|
||||
<li>Position (C or A, depending on captain or assistant captain, leave empty if neither)</li> -->
|
||||
</ul>
|
||||
{% endblocktranslate %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% 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" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mt-4">
|
||||
{% form_field form.csv_file %}
|
||||
</div>
|
||||
|
||||
<button class="w-full mt-8 btn btn-neutral" type="submit">
|
||||
<i class="fa-solid fa-floppy-disk"></i>{% translate "Save" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endpartialdef content %}
|
||||
{% endblock content %}
|
||||
@@ -226,7 +226,7 @@
|
||||
.choices[data-type*=text] .choices__button {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 0-4px 0 2px;
|
||||
margin: 0 -4px 0 2px;
|
||||
padding-left: 16px;
|
||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjRkZGIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==);
|
||||
background-size: 8px;
|
||||
@@ -253,26 +253,16 @@
|
||||
border-radius: var(--radius-field);
|
||||
font-size: 14px;
|
||||
min-height: 44px;
|
||||
overflow: hidden
|
||||
overflow: hidden;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease, background-color 0.15s ease;
|
||||
}
|
||||
|
||||
|
||||
.is-focused .choices__inner,
|
||||
.is-open .choices__inner {
|
||||
border-color: var(--color-base-content);
|
||||
isolation: isolate;
|
||||
border-radius: var(--radius-field);
|
||||
box-shadow: 0 0 0 2px var(--color-base-100), 0 0 0 4px var(--color-base-content);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
@@ -281,6 +271,13 @@
|
||||
border-radius: var(--radius-field);
|
||||
}
|
||||
|
||||
.is-focused {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.choices__list {
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
@@ -344,16 +341,16 @@
|
||||
.choices__list--dropdown,
|
||||
.choices__list[aria-expanded] {
|
||||
display: none;
|
||||
z-index: 1;
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
background-color: var(--color-base-100);
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid color-mix(in oklab, var(--color-base-content) 20%, #0000);
|
||||
top: 100%;
|
||||
margin-top: 3px;
|
||||
margin-top: 0.25rem;
|
||||
border-radius: var(--radius-field);
|
||||
overflow: hidden;
|
||||
word-break: break-all
|
||||
word-break: break-all;
|
||||
box-shadow: 0 10px 25px color-mix(in oklab, var(--color-base-content) 12%, transparent)
|
||||
}
|
||||
|
||||
.is-active.choices__list--dropdown,
|
||||
@@ -363,7 +360,7 @@
|
||||
|
||||
.is-open .choices__list--dropdown,
|
||||
.is-open .choices__list[aria-expanded] {
|
||||
border-color: #b7b7b7
|
||||
border-color: color-mix(in oklab, var(--color-base-content) 20%, #0000)
|
||||
}
|
||||
|
||||
.is-flipped .choices__list--dropdown,
|
||||
@@ -371,8 +368,8 @@
|
||||
top: auto;
|
||||
bottom: 100%;
|
||||
margin-top: 0;
|
||||
margin-bottom: -1px;
|
||||
border-radius: .25rem .25rem 0 0
|
||||
margin-bottom: 0.25rem;
|
||||
border-radius: var(--radius-field)
|
||||
}
|
||||
|
||||
.choices__list--dropdown .choices__list,
|
||||
@@ -387,8 +384,9 @@
|
||||
.choices__list--dropdown .choices__item,
|
||||
.choices__list[aria-expanded] .choices__item {
|
||||
position: relative;
|
||||
padding: 10px;
|
||||
font-size: 14px
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
color: var(--color-base-content)
|
||||
}
|
||||
|
||||
[dir=rtl] .choices__list--dropdown .choices__item,
|
||||
@@ -396,7 +394,7 @@
|
||||
text-align: right
|
||||
}
|
||||
|
||||
@media (min-width:640px) {
|
||||
@media (min-width: 640px) {
|
||||
|
||||
.choices__list--dropdown .choices__item--selectable[data-select-text],
|
||||
.choices__list[aria-expanded] .choices__item--selectable[data-select-text] {
|
||||
@@ -430,7 +428,7 @@
|
||||
|
||||
.choices__list--dropdown .choices__item--selectable.is-highlighted,
|
||||
.choices__list[aria-expanded] .choices__item--selectable.is-highlighted {
|
||||
background-color: var(--color-base-100)
|
||||
background-color: color-mix(in oklab, var(--color-primary) 10%, var(--color-base-100))
|
||||
}
|
||||
|
||||
.choices__list--dropdown .choices__item--selectable.is-highlighted::after,
|
||||
@@ -473,7 +471,8 @@
|
||||
|
||||
.choices__button:focus,
|
||||
.choices__input:focus {
|
||||
outline: 0
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.choices__input {
|
||||
|
||||
14
uv.lock
generated
14
uv.lock
generated
@@ -318,6 +318,18 @@ honcho = [
|
||||
{ name = "honcho" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django-waffle"
|
||||
version = "5.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/22/e1/6f533da0d4ac89f427dfd9410e39bfc14ae3a23335ecd549d76be4b2a834/django_waffle-5.0.0.tar.gz", hash = "sha256:62f9d00eedf68dafb82657beab56e601bddedc1ea1ccfef91d83df8658708509", size = 37761, upload-time = "2025-06-12T07:38:54.895Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/d2/6f0d664bd35a3fdd0403655c7c32ec290704923f11541ef356b180cd8fbf/django_waffle-5.0.0-py3-none-any.whl", hash = "sha256:3312851d9d926b76b9e90712355781700a383b82b5bf2b61e1f1be97532c0f3d", size = 48137, upload-time = "2025-06-12T07:38:53.698Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "honcho"
|
||||
version = "2.0.0"
|
||||
@@ -701,6 +713,7 @@ dependencies = [
|
||||
{ name = "django-htmx" },
|
||||
{ name = "django-phonenumber-field", extra = ["phonenumbers"] },
|
||||
{ name = "django-tailwind", extra = ["cookiecutter", "honcho"] },
|
||||
{ name = "django-waffle" },
|
||||
{ name = "pillow" },
|
||||
{ name = "psycopg2-binary" },
|
||||
{ name = "python-decouple" },
|
||||
@@ -723,6 +736,7 @@ requires-dist = [
|
||||
{ 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 = "django-waffle", specifier = ">=5.0.0" },
|
||||
{ name = "pillow", specifier = ">=12.1.0" },
|
||||
{ name = "psycopg2-binary", specifier = ">=2.9.11" },
|
||||
{ name = "python-decouple", specifier = ">=3.8" },
|
||||
|
||||
Reference in New Issue
Block a user