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("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,12 +7,12 @@ 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 CreateView, DeleteView, UpdateView 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 members.filters import MemberFilter from members.filters import MemberFilter
from members.forms import MemberForm from members.forms import MassUploadForm, MemberForm
from members.models import Member from members.models import Member
from ..mixins import HTMXViewMixin from ..mixins import HTMXViewMixin
@@ -112,4 +114,34 @@ class MemberDeleteView(HTMXViewMixin, PermissionRequiredMixin, SuccessMessageMix
return HttpResponseRedirect(self.get_success_url()) 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 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

@@ -59,7 +59,7 @@
</div> </div>
<div class="add"> <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" %} <i class="fa-solid fa-file-upload"></i>{% translate "Load members from file" %}
</a> </a>

View File

@@ -25,7 +25,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 %}
@@ -81,7 +81,7 @@
</a> </a>
{% endif %} {% 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" %} <i class="fa-solid fa-trash"></i>{% translate "Delete" %}
</a> </a>
</div> </div>
@@ -101,7 +101,7 @@
</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>

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