Add bulk member upload functionality: implement MemberLoadView, update routes, templates, and create MassUploadForm for CSV uploads.

This commit is contained in:
2026-04-12 12:10:02 +02:00
parent 4b819d9dd9
commit b71bc2afc0
6 changed files with 110 additions and 19 deletions

View File

@@ -8,5 +8,5 @@ urlpatterns = [
path("add/", MemberAddView.as_view(), name="add"),
path("<int:pk>/edit/", MemberEditView.as_view(), name="edit"),
path("<int:pk>/delete/", MemberDeleteView.as_view(), name="delete"),
# path("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,12 +7,12 @@ 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 CreateView, DeleteView, UpdateView
from django.views.generic import CreateView, DeleteView, FormView, UpdateView
from django_filters.views import FilterView
from rules.contrib.views import PermissionRequiredMixin
from members.filters import MemberFilter
from members.forms import MemberForm
from members.forms import MassUploadForm, MemberForm
from members.models import Member
from ..mixins import HTMXViewMixin
@@ -112,4 +114,34 @@ class MemberDeleteView(HTMXViewMixin, PermissionRequiredMixin, SuccessMessageMix
return HttpResponseRedirect(self.get_success_url())
class MemberLoadView: ...
class MemberLoadView(PermissionRequiredMixin, HTMXViewMixin, SuccessMessageMixin, 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"
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

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

@@ -59,7 +59,7 @@
</div>
<div class="add">
<a class="btn btn-accent btn-sm grow hidden lg:flex" href="">
<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>

View File

@@ -25,7 +25,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 %}
@@ -81,7 +81,7 @@
</a>
{% endif %}
<a href="{% url "backend:members:delete" member.pk %}" 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>
@@ -101,7 +101,7 @@
</div>
{% endif %}
<form method="post">
<form method="post" hx-post>
{% csrf_token %}
<h2 class="page-subtitle">{% translate "Personal information" %}</h2>

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