Compare commits
9 Commits
e55c88c742
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| 72e6388c0c | |||
| cb3da371d1 | |||
| 8f9357f1b8 | |||
| 4e4fe62f11 | |||
| b71bc2afc0 | |||
| 4b819d9dd9 | |||
| 7aa4a4816c | |||
| 3d94b9b2d8 | |||
| 95e46fe727 |
@@ -43,6 +43,7 @@ INSTALLED_APPS = [
|
|||||||
"django.contrib.sessions",
|
"django.contrib.sessions",
|
||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
|
"waffle",
|
||||||
"constance",
|
"constance",
|
||||||
"tailwind",
|
"tailwind",
|
||||||
"django_filters",
|
"django_filters",
|
||||||
@@ -62,6 +63,7 @@ MIDDLEWARE = [
|
|||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
"django_htmx.middleware.HtmxMiddleware",
|
"django_htmx.middleware.HtmxMiddleware",
|
||||||
|
"waffle.middleware.WaffleMiddleware",
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = "TeamForge.urls"
|
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_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_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_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_FORMAT = "INTERNATIONAL"
|
||||||
PHONENUMBER_DEFAULT_REGION = config("CM_CLUB_COUNTRY_CODE", default="BE", cast=str)
|
PHONENUMBER_DEFAULT_REGION = config("CM_CLUB_COUNTRY_CODE", default="BE", cast=str)
|
||||||
|
|
||||||
TAILWIND_APP_NAME = "theme"
|
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 = [
|
urlpatterns = [
|
||||||
path("", MemberListView.as_view(), name="list"),
|
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>/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"),
|
path("load/", MemberLoadView.as_view(), name="load"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import csv
|
||||||
|
import io
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@@ -5,15 +7,18 @@ from django.contrib.messages.views import SuccessMessageMixin
|
|||||||
from django.http import HttpResponse, HttpResponseRedirect
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
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 django_filters.views import FilterView
|
||||||
from rules.contrib.views import PermissionRequiredMixin
|
from rules.contrib.views import PermissionRequiredMixin
|
||||||
|
from waffle.mixins import WaffleFlagMixin
|
||||||
|
|
||||||
from members.filters import MemberFilter
|
from members.filters import MemberFilter
|
||||||
|
from members.forms import MassUploadForm, MemberForm
|
||||||
from members.models import Member
|
from members.models import Member
|
||||||
from members.forms import MemberForm
|
|
||||||
|
|
||||||
from ..mixins import HTMXViewMixin
|
from ..mixins import HTMXViewMixin
|
||||||
|
|
||||||
|
|
||||||
class MemberListView(HTMXViewMixin, PermissionRequiredMixin, FilterView):
|
class MemberListView(HTMXViewMixin, PermissionRequiredMixin, FilterView):
|
||||||
filterset_class = MemberFilter
|
filterset_class = MemberFilter
|
||||||
paginate_by = 50
|
paginate_by = 50
|
||||||
@@ -25,15 +30,15 @@ class MemberListView(HTMXViewMixin, PermissionRequiredMixin, FilterView):
|
|||||||
def handle_no_permission(self) -> HttpResponseRedirect:
|
def handle_no_permission(self) -> HttpResponseRedirect:
|
||||||
messages.error(self.request, self.get_permission_denied_message())
|
messages.error(self.request, self.get_permission_denied_message())
|
||||||
return HttpResponseRedirect(reverse_lazy("backend:index"))
|
return HttpResponseRedirect(reverse_lazy("backend:index"))
|
||||||
|
|
||||||
def get_filterset_kwargs(self, filterset_class) -> dict[str, Any]:
|
def get_filterset_kwargs(self, filterset_class) -> dict[str, Any]:
|
||||||
kwargs = super().get_filterset_kwargs(filterset_class)
|
kwargs = super().get_filterset_kwargs(filterset_class)
|
||||||
|
|
||||||
filter_values = {} if kwargs["data"] is None else kwargs["data"].dict()
|
filter_values = {} if kwargs["data"] is None else kwargs["data"].dict()
|
||||||
|
|
||||||
if not filter_values:
|
if not filter_values:
|
||||||
filter_values.update({"user__is_active": "true"})
|
filter_values.update({"user__is_active": "true"})
|
||||||
|
|
||||||
kwargs["data"] = filter_values
|
kwargs["data"] = filter_values
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
@@ -47,11 +52,11 @@ class MemberAddView(HTMXViewMixin, PermissionRequiredMixin, SuccessMessageMixin,
|
|||||||
success_url = reverse_lazy("backend:members:list")
|
success_url = reverse_lazy("backend:members:list")
|
||||||
partial_name = "members/member_form.html#content"
|
partial_name = "members/member_form.html#content"
|
||||||
menu_highlight = "members"
|
menu_highlight = "members"
|
||||||
|
|
||||||
def handle_no_permission(self) -> HttpResponseRedirect:
|
def handle_no_permission(self) -> HttpResponseRedirect:
|
||||||
messages.error(self.request, self.get_permission_denied_message())
|
messages.error(self.request, self.get_permission_denied_message())
|
||||||
return HttpResponseRedirect(reverse_lazy("backend:index"))
|
return HttpResponseRedirect(reverse_lazy("backend:index"))
|
||||||
|
|
||||||
def get_success_message(self, cleaned_data):
|
def get_success_message(self, cleaned_data):
|
||||||
return self.success_message % dict(cleaned_data, name=self.object.user.get_full_name())
|
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")
|
success_url = reverse_lazy("backend:members:list")
|
||||||
partial_name = "members/member_form.html#content"
|
partial_name = "members/member_form.html#content"
|
||||||
menu_highlight = "members"
|
menu_highlight = "members"
|
||||||
|
|
||||||
def handle_no_permission(self) -> HttpResponseRedirect:
|
def handle_no_permission(self) -> HttpResponseRedirect:
|
||||||
messages.error(self.request, self.get_permission_denied_message())
|
messages.error(self.request, self.get_permission_denied_message())
|
||||||
return HttpResponseRedirect(reverse_lazy("backend:index"))
|
return HttpResponseRedirect(reverse_lazy("backend:index"))
|
||||||
|
|
||||||
def get_success_message(self, cleaned_data):
|
def get_success_message(self, cleaned_data):
|
||||||
return self.success_message % dict(cleaned_data, name=self.object.user.get_full_name())
|
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):
|
class MemberDeleteView(HTMXViewMixin, PermissionRequiredMixin, SuccessMessageMixin, DeleteView):
|
||||||
model = Member
|
model = Member
|
||||||
@@ -82,24 +95,55 @@ class MemberDeleteView(HTMXViewMixin, PermissionRequiredMixin, SuccessMessageMix
|
|||||||
success_url = reverse_lazy("backend:members:list")
|
success_url = reverse_lazy("backend:members:list")
|
||||||
partial_name = "members/member_confirm_delete.html#content"
|
partial_name = "members/member_confirm_delete.html#content"
|
||||||
menu_highlight = "members"
|
menu_highlight = "members"
|
||||||
|
|
||||||
def handle_no_permission(self) -> HttpResponseRedirect:
|
def handle_no_permission(self) -> HttpResponseRedirect:
|
||||||
messages.error(self.request, self.get_permission_denied_message())
|
messages.error(self.request, self.get_permission_denied_message())
|
||||||
return HttpResponseRedirect(reverse_lazy("backend:index"))
|
return HttpResponseRedirect(reverse_lazy("backend:index"))
|
||||||
|
|
||||||
def get_success_message(self, cleaned_data):
|
def get_success_message(self, cleaned_data):
|
||||||
return self.success_message % dict(cleaned_data, name=self.object.user.get_full_name())
|
return self.success_message % dict(cleaned_data, name=self.object.user.get_full_name())
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
|
|
||||||
# Soft delete user
|
# Soft delete user
|
||||||
self.object.user.is_active = False
|
self.object.user.is_active = False
|
||||||
self.object.user.save()
|
self.object.user.save()
|
||||||
|
|
||||||
# Do not delete the member object
|
# Do not delete the member object
|
||||||
messages.success(self.request, self.get_success_message({"name": self.object.user.get_full_name()}))
|
messages.success(self.request, self.get_success_message({"name": self.object.user.get_full_name()}))
|
||||||
return HttpResponseRedirect(self.get_success_url())
|
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 django.urls import include, path
|
||||||
|
|
||||||
from .views import index
|
from .views import configuration, index
|
||||||
|
|
||||||
app_name = "backend"
|
app_name = "backend"
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", index, name="index"),
|
path("", index, name="index"),
|
||||||
path("members/", include("backend.members.urls")),
|
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.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.
|
# Create your views here.
|
||||||
def index(request):
|
def index(request):
|
||||||
return render(request, "backend/index.html")
|
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
|
from .models import Member
|
||||||
|
|
||||||
|
|
||||||
class MemberForm(forms.ModelForm):
|
class MemberForm(forms.ModelForm):
|
||||||
first_name = forms.CharField(label=_("First name"), max_length=250)
|
first_name = forms.CharField(label=_("First name"), max_length=250)
|
||||||
last_name = forms.CharField(label=_("Last name"), max_length=250)
|
last_name = forms.CharField(label=_("Last name"), max_length=250)
|
||||||
email = forms.EmailField(label=_("Email"))
|
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"))
|
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 = forms.CharField(label=_("Password"), widget=forms.PasswordInput, required=False)
|
||||||
password_confirmation = forms.CharField(label=_("Confirm password"), widget=forms.PasswordInput, required=False)
|
password_confirmation = forms.CharField(label=_("Confirm password"), widget=forms.PasswordInput, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Member
|
model = Member
|
||||||
fields = ["phone_number", "emergency_phone_number", "license", "birthday", "family_members"]
|
fields = ["phone_number", "emergency_phone_number", "license", "birthday", "family_members"]
|
||||||
localized_fields = fields
|
localized_fields = fields
|
||||||
|
|
||||||
def save(self, commit: bool = True) -> Member:
|
def save(self, commit: bool = True) -> Member:
|
||||||
password = None
|
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"]:
|
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"]
|
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 = 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.phone_number = self.cleaned_data["phone_number"]
|
||||||
member.emergency_phone_number = self.cleaned_data["emergency_phone_number"]
|
member.emergency_phone_number = self.cleaned_data["emergency_phone_number"]
|
||||||
member.license = self.cleaned_data["license"]
|
member.license = self.cleaned_data["license"]
|
||||||
member.birthday = self.cleaned_data["birthday"]
|
member.birthday = self.cleaned_data["birthday"]
|
||||||
|
|
||||||
if self.cleaned_data["admin"]:
|
if self.cleaned_data["admin"]:
|
||||||
member.user.is_superuser = True
|
member.user.is_superuser = True
|
||||||
member.user.save(update_fields=["is_superuser"])
|
member.user.save(update_fields=["is_superuser"])
|
||||||
|
|
||||||
member.save(update_fields=["phone_number", "emergency_phone_number", "license", "birthday"])
|
member.save(update_fields=["phone_number", "emergency_phone_number", "license", "birthday"])
|
||||||
member.family_members.set(self.cleaned_data["family_members"])
|
member.family_members.set(self.cleaned_data["family_members"])
|
||||||
|
|
||||||
return member
|
return member
|
||||||
|
|
||||||
|
|
||||||
|
class MassUploadForm(forms.Form):
|
||||||
|
csv_file = forms.FileField()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ dependencies = [
|
|||||||
"django-htmx>=1.27.0",
|
"django-htmx>=1.27.0",
|
||||||
"django-phonenumber-field[phonenumbers]>=8.4.0",
|
"django-phonenumber-field[phonenumbers]>=8.4.0",
|
||||||
"django-tailwind[cookiecutter,honcho]>=4.4.2",
|
"django-tailwind[cookiecutter,honcho]>=4.4.2",
|
||||||
|
"django-waffle>=5.0.0",
|
||||||
"pillow>=12.1.0",
|
"pillow>=12.1.0",
|
||||||
"psycopg2-binary>=2.9.11",
|
"psycopg2-binary>=2.9.11",
|
||||||
"python-decouple>=3.8",
|
"python-decouple>=3.8",
|
||||||
|
|||||||
@@ -4,17 +4,18 @@
|
|||||||
|
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
{% url "backend:members:list" as members_list %}
|
{% url "backend:members:list" as members_list %}
|
||||||
|
{% url "backend:configuration" as configuration %}
|
||||||
|
|
||||||
{% has_perm "members.member_manager" request.user as is_member_manager %}
|
{% has_perm "members.member_manager" request.user as is_member_manager %}
|
||||||
|
|
||||||
{% if is_member_manager %}
|
{% if is_member_manager %}
|
||||||
<li class="menu-title">Members</li>
|
<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 %}
|
{% endif %}
|
||||||
|
|
||||||
<li class="menu-title mt-4">Navigation</li>
|
{% if request.user.is_superuser %}
|
||||||
<li><a href="#"><i class="fa-solid fa-house"></i> Dashboard</a></li>
|
<li class="menu-title mt-4">Configuration</li>
|
||||||
<li><a href="#"><i class="fa-solid fa-calendar"></i> Calendar</a></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>
|
||||||
<li><a href="#"><i class="fa-solid fa-users"></i> Members</a></li>
|
{% endif %}
|
||||||
<li><a href="#"><i class="fa-solid fa-gear"></i> Settings</a></li>
|
|
||||||
{% endblock sidebar %}
|
{% 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 %}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
{% load form_field %}
|
{% load form_field %}
|
||||||
{% load avatar %}
|
{% load avatar %}
|
||||||
{% load pagination %}
|
{% load pagination %}
|
||||||
|
{% load waffle_tags %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% partialdef content inline %}
|
{% partialdef content inline %}
|
||||||
@@ -14,7 +15,7 @@
|
|||||||
<h1 class="page-title">{% translate "Members" %}</h1>
|
<h1 class="page-title">{% translate "Members" %}</h1>
|
||||||
|
|
||||||
<div class="lg:hidden collapse collapse-plus bg-base-100 border-neutral border">
|
<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-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">
|
<div class="collapse-content">
|
||||||
<form class="flex flex-col gap-2" hx-get="{% url "backend:members:list" %}" hx-target="#content">
|
<form class="flex flex-col gap-2" hx-get="{% url "backend:members:list" %}" hx-target="#content">
|
||||||
@@ -59,9 +60,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="add">
|
<div class="add">
|
||||||
<a class="btn btn-accent btn-sm grow hidden lg:flex" href="">
|
{% flag "TF_MASS_UPLOAD" %}
|
||||||
<i class="fa-solid fa-file-upload"></i>{% translate "Load members from file" %}
|
<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">
|
||||||
</a>
|
<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">
|
<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" %}
|
<i class="fa-solid fa-plus"></i>{% translate "Add member" %}
|
||||||
@@ -89,7 +92,7 @@
|
|||||||
{% for member in object_list %}
|
{% for member in object_list %}
|
||||||
<tr class="hover:bg-base-300">
|
<tr class="hover:bg-base-300">
|
||||||
<td>
|
<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 class="flex flex-row items-center gap-3">
|
||||||
<div>
|
<div>
|
||||||
{% avatar first_name=member.user.first_name last_name=member.user.last_name %}
|
{% avatar first_name=member.user.first_name last_name=member.user.last_name %}
|
||||||
@@ -105,7 +108,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not member.user.is_active %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@@ -129,7 +132,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="flex flex-row gap-2">
|
<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" %}
|
<i class="fa-solid fa-eye"></i>{% translate "Details" %}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -151,13 +154,23 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
{% for member in object_list %}
|
{% 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>
|
<div>
|
||||||
{% avatar first_name=member.user.first_name last_name=member.user.last_name width="sm" %}
|
{% avatar first_name=member.user.first_name last_name=member.user.last_name width="sm" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grow">
|
<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 class="opacity-50 text-xs">{{ member.birthday|date:"d M Y"|default:"" }}</div>
|
||||||
</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
|
<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="hidden lg:inline">{% translate "next" %}</span><span
|
||||||
class="lg:hidden">></span></a>
|
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>
|
class="hidden lg:inline"> {% translate "last" %}</span> »</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load form_field %}
|
{% load form_field %}
|
||||||
{% load avatar %}
|
{% load avatar %}
|
||||||
|
{% load waffle_tags %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% partialdef content inline %}
|
{% partialdef content inline %}
|
||||||
@@ -18,14 +19,6 @@
|
|||||||
<div class="flex flex-row gap-2 grow justify-center items-center">
|
<div class="flex flex-row gap-2 grow justify-center items-center">
|
||||||
{% if member %}
|
{% if member %}
|
||||||
<div class="font-bold text-xl">{{ member.user.get_full_name }}</div>
|
<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 %}
|
{% else %}
|
||||||
<div class="font-bold text-xl">{% translate "Create new member" %}</div>
|
<div class="font-bold text-xl">{% translate "Create new member" %}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -33,7 +26,7 @@
|
|||||||
|
|
||||||
<div class="flex min-w-12 justify-end">
|
<div class="flex min-w-12 justify-end">
|
||||||
{% if member %}
|
{% 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>
|
<i class="fa-solid fa-trash"></i>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -44,29 +37,28 @@
|
|||||||
|
|
||||||
{% if member %}
|
{% if member %}
|
||||||
<div class="mt-4 lg:hidden flex flex-row gap-2">
|
<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 %}
|
||||||
{% if member.phone_number %}
|
<a href="{{ member.phone_number.as_rfc3966 }}" class="btn btn-info btn-outline btn-sm grow">
|
||||||
<a href="{{ member.phone_number.as_rfc3966 }}" class="btn btn-info btn-outline btn-sm grow">
|
<i class="fa-solid fa-phone"></i>
|
||||||
<i class="fa-solid fa-phone"></i>
|
{{ member.phone_number }}
|
||||||
{{ 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>
|
</a>
|
||||||
{% endif %}
|
{% 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 %}
|
{% 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>
|
<h2 class="page-subtitle border-b-0! mt-0!">{{ member.user.get_full_name }}</h2>
|
||||||
@@ -76,21 +68,21 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="justify-end hidden gap-2 lg:flex lg:flex-row grow">
|
<div class="justify-end hidden gap-2 lg:flex lg:flex-row grow">
|
||||||
{% if member.phone %}
|
{% if member.phone_number %}
|
||||||
<a href="{{ member.phone.as_rfc3966 }}" class="btn btn-outline btn-info"><i class="fa-solid fa-phone"></i>{{ member.phone }}</a>
|
<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 %}
|
{% endif %}
|
||||||
|
|
||||||
{% if member.emergency_phone %}
|
{% if member.emergency_phone_number %}
|
||||||
<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>
|
<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 %}
|
{% 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">
|
<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" %}
|
<i class="fa-solid fa-ticket"></i>{% translate "Team Memberships" %}
|
||||||
</a>
|
</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" %}
|
<i class="fa-solid fa-trash"></i>{% translate "Delete" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,37 +102,38 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form method="post">
|
<form method="post" hx-post>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<h2 class="page-subtitle">{% translate "Personal information" %}</h2>
|
<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.first_name %}
|
||||||
{% form_field form.last_name %}
|
{% form_field form.last_name %}
|
||||||
{% form_field form.birthday %}
|
{% form_field form.birthday %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="page-subtitle">{% translate "Contact information" %}</h2>
|
<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.email %}
|
||||||
{% form_field form.phone_number %}
|
{% form_field form.phone_number %}
|
||||||
{% form_field form.emergency_phone_number %}
|
{% form_field form.emergency_phone_number %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="page-subtitle">{% translate "Family information" %}</h2>
|
<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 %}
|
{% form_field form.family_members %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="page-subtitle">{% translate "Club information" %}</h2>
|
<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.license %}
|
||||||
{% form_field form.admin show_as_toggle=True %}
|
{% form_field form.admin show_as_toggle=True %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="page-subtitle">{% translate "Password" %}</h2>
|
<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="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
|
||||||
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2">
|
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 %}
|
||||||
{% form_field form.password_confirmation %}
|
{% form_field form.password_confirmation %}
|
||||||
</div>
|
</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 {
|
.choices[data-type*=text] .choices__button {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0-4px 0 2px;
|
margin: 0 -4px 0 2px;
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjRkZGIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==);
|
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjRkZGIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==);
|
||||||
background-size: 8px;
|
background-size: 8px;
|
||||||
@@ -253,26 +253,16 @@
|
|||||||
border-radius: var(--radius-field);
|
border-radius: var(--radius-field);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
min-height: 44px;
|
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-focused .choices__inner,
|
||||||
.is-open .choices__inner {
|
.is-open .choices__inner {
|
||||||
border-color: var(--color-base-content);
|
border-color: var(--color-base-content);
|
||||||
isolation: isolate;
|
box-shadow: 0 0 0 2px var(--color-base-100), 0 0 0 4px var(--color-base-content);
|
||||||
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 {
|
.is-open .choices__inner {
|
||||||
border-radius: var(--radius-field);
|
border-radius: var(--radius-field);
|
||||||
}
|
}
|
||||||
@@ -281,6 +271,13 @@
|
|||||||
border-radius: var(--radius-field);
|
border-radius: var(--radius-field);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-focused {
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.choices__list {
|
.choices__list {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
@@ -344,16 +341,16 @@
|
|||||||
.choices__list--dropdown,
|
.choices__list--dropdown,
|
||||||
.choices__list[aria-expanded] {
|
.choices__list[aria-expanded] {
|
||||||
display: none;
|
display: none;
|
||||||
z-index: 1;
|
z-index: 10;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--color-base-100);
|
border: 1px solid color-mix(in oklab, var(--color-base-content) 20%, #0000);
|
||||||
border: 1px solid #ddd;
|
|
||||||
top: 100%;
|
top: 100%;
|
||||||
margin-top: 3px;
|
margin-top: 0.25rem;
|
||||||
border-radius: var(--radius-field);
|
border-radius: var(--radius-field);
|
||||||
overflow: hidden;
|
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,
|
.is-active.choices__list--dropdown,
|
||||||
@@ -363,7 +360,7 @@
|
|||||||
|
|
||||||
.is-open .choices__list--dropdown,
|
.is-open .choices__list--dropdown,
|
||||||
.is-open .choices__list[aria-expanded] {
|
.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,
|
.is-flipped .choices__list--dropdown,
|
||||||
@@ -371,8 +368,8 @@
|
|||||||
top: auto;
|
top: auto;
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: -1px;
|
margin-bottom: 0.25rem;
|
||||||
border-radius: .25rem .25rem 0 0
|
border-radius: var(--radius-field)
|
||||||
}
|
}
|
||||||
|
|
||||||
.choices__list--dropdown .choices__list,
|
.choices__list--dropdown .choices__list,
|
||||||
@@ -387,8 +384,9 @@
|
|||||||
.choices__list--dropdown .choices__item,
|
.choices__list--dropdown .choices__item,
|
||||||
.choices__list[aria-expanded] .choices__item {
|
.choices__list[aria-expanded] .choices__item {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 10px;
|
padding: 10px 12px;
|
||||||
font-size: 14px
|
font-size: 14px;
|
||||||
|
color: var(--color-base-content)
|
||||||
}
|
}
|
||||||
|
|
||||||
[dir=rtl] .choices__list--dropdown .choices__item,
|
[dir=rtl] .choices__list--dropdown .choices__item,
|
||||||
@@ -396,7 +394,7 @@
|
|||||||
text-align: right
|
text-align: right
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width:640px) {
|
@media (min-width: 640px) {
|
||||||
|
|
||||||
.choices__list--dropdown .choices__item--selectable[data-select-text],
|
.choices__list--dropdown .choices__item--selectable[data-select-text],
|
||||||
.choices__list[aria-expanded] .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--dropdown .choices__item--selectable.is-highlighted,
|
||||||
.choices__list[aria-expanded] .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,
|
.choices__list--dropdown .choices__item--selectable.is-highlighted::after,
|
||||||
@@ -473,7 +471,8 @@
|
|||||||
|
|
||||||
.choices__button:focus,
|
.choices__button:focus,
|
||||||
.choices__input:focus {
|
.choices__input:focus {
|
||||||
outline: 0
|
outline: 0;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.choices__input {
|
.choices__input {
|
||||||
|
|||||||
14
uv.lock
generated
14
uv.lock
generated
@@ -318,6 +318,18 @@ honcho = [
|
|||||||
{ name = "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]]
|
[[package]]
|
||||||
name = "honcho"
|
name = "honcho"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@@ -701,6 +713,7 @@ dependencies = [
|
|||||||
{ name = "django-htmx" },
|
{ name = "django-htmx" },
|
||||||
{ name = "django-phonenumber-field", extra = ["phonenumbers"] },
|
{ name = "django-phonenumber-field", extra = ["phonenumbers"] },
|
||||||
{ name = "django-tailwind", extra = ["cookiecutter", "honcho"] },
|
{ name = "django-tailwind", extra = ["cookiecutter", "honcho"] },
|
||||||
|
{ name = "django-waffle" },
|
||||||
{ name = "pillow" },
|
{ name = "pillow" },
|
||||||
{ name = "psycopg2-binary" },
|
{ name = "psycopg2-binary" },
|
||||||
{ name = "python-decouple" },
|
{ name = "python-decouple" },
|
||||||
@@ -723,6 +736,7 @@ requires-dist = [
|
|||||||
{ name = "django-htmx", specifier = ">=1.27.0" },
|
{ name = "django-htmx", specifier = ">=1.27.0" },
|
||||||
{ name = "django-phonenumber-field", extras = ["phonenumbers"], specifier = ">=8.4.0" },
|
{ name = "django-phonenumber-field", extras = ["phonenumbers"], specifier = ">=8.4.0" },
|
||||||
{ name = "django-tailwind", extras = ["cookiecutter", "honcho"], specifier = ">=4.4.2" },
|
{ name = "django-tailwind", extras = ["cookiecutter", "honcho"], specifier = ">=4.4.2" },
|
||||||
|
{ name = "django-waffle", specifier = ">=5.0.0" },
|
||||||
{ name = "pillow", specifier = ">=12.1.0" },
|
{ name = "pillow", specifier = ">=12.1.0" },
|
||||||
{ name = "psycopg2-binary", specifier = ">=2.9.11" },
|
{ name = "psycopg2-binary", specifier = ">=2.9.11" },
|
||||||
{ name = "python-decouple", specifier = ">=3.8" },
|
{ name = "python-decouple", specifier = ">=3.8" },
|
||||||
|
|||||||
Reference in New Issue
Block a user