Compare commits

..

7 Commits

12 changed files with 344 additions and 90 deletions

View File

@@ -149,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
View 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)

View File

@@ -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"),
]

View File

@@ -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)

View File

@@ -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")
]

View File

@@ -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})

View File

@@ -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()

View File

@@ -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 %}

View 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 %}

View File

@@ -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">&gt;</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> &raquo;</a>
{% endif %}
</div>

View File

@@ -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>

View 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 %}