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_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
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 = [ 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"),
] ]

View File

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

View File

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

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

View File

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

View File

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

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 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">&gt;</span></a> 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> class="hidden lg:inline"> {% translate "last" %}</span> &raquo;</a>
{% endif %} {% endif %}
</div> </div>

View File

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

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