From b71bc2afc0f3a427b848fd1888e9adbb6042dd5b Mon Sep 17 00:00:00 2001 From: Bernard Siebens Date: Sun, 12 Apr 2026 12:10:02 +0200 Subject: [PATCH] Add bulk member upload functionality: implement `MemberLoadView`, update routes, templates, and create `MassUploadForm` for CSV uploads. --- backend/members/urls.py | 2 +- backend/members/views.py | 38 +++++++++++++++++-- members/forms.py | 25 +++++++------ templates/members/member_filter.html | 2 +- templates/members/member_form.html | 6 +-- templates/members/member_load.html | 56 ++++++++++++++++++++++++++++ 6 files changed, 110 insertions(+), 19 deletions(-) create mode 100644 templates/members/member_load.html diff --git a/backend/members/urls.py b/backend/members/urls.py index 837375c..beb8cce 100644 --- a/backend/members/urls.py +++ b/backend/members/urls.py @@ -8,5 +8,5 @@ urlpatterns = [ path("add/", MemberAddView.as_view(), name="add"), path("/edit/", MemberEditView.as_view(), name="edit"), path("/delete/", MemberDeleteView.as_view(), name="delete"), - # path("load/", MemberLoadView.as_view(), name="load"), + path("load/", MemberLoadView.as_view(), name="load"), ] diff --git a/backend/members/views.py b/backend/members/views.py index 6fa6d69..34d6577 100644 --- a/backend/members/views.py +++ b/backend/members/views.py @@ -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) diff --git a/members/forms.py b/members/forms.py index ff6a270..c263c47 100644 --- a/members/forms.py +++ b/members/forms.py @@ -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 - - \ No newline at end of file + + +class MassUploadForm(forms.Form): + csv_file = forms.FileField() diff --git a/templates/members/member_filter.html b/templates/members/member_filter.html index 14e3de5..e55bca1 100644 --- a/templates/members/member_filter.html +++ b/templates/members/member_filter.html @@ -59,7 +59,7 @@
- diff --git a/templates/members/member_form.html b/templates/members/member_form.html index 1e749e4..6e67c87 100644 --- a/templates/members/member_form.html +++ b/templates/members/member_form.html @@ -25,7 +25,7 @@
{% if member %} - + {% else %} @@ -81,7 +81,7 @@ {% endif %} - + {% translate "Delete" %}
@@ -101,7 +101,7 @@
{% endif %} -
+ {% csrf_token %}

{% translate "Personal information" %}

diff --git a/templates/members/member_load.html b/templates/members/member_load.html new file mode 100644 index 0000000..4396d9d --- /dev/null +++ b/templates/members/member_load.html @@ -0,0 +1,56 @@ +{% extends "backend/base.html" %} + +{% load i18n %} +{% load form_field %} +{% load avatar %} + +{% block content %} + {% partialdef content inline %} +

{% translate "Members" %}

+ + + +
+ + + {% blocktranslate %} + Data should be formatted as a .csv file with the following information in the different columns: +
    +
  • First name
  • +
  • Last name
  • +
  • Email
  • +
  • Birthday (YYYY-MM-DD)
  • +
  • License number
  • + +
+ {% endblocktranslate %} +
+
+ + {% if form.errors %} +
+ + +
+
{% translate "Error" %}
+
{% translate "Please correct the errors below before saving again." %}
+
+
+ {% endif %} + + + {% csrf_token %} + +
+ {% form_field form.csv_file %} +
+ + +
+ {% endpartialdef content %} +{% endblock content %} \ No newline at end of file