Repository: ephios-dev/ephios

Issue:
Manage qualification grants for user
As a Manager I want to manage qualification grants while creating or editing a user. I expect the page to have formset with forms for qualification grants. The formset is pre-populated with the existing grants for that user (if editing a user) with the option to delete each grant. I also want to have a button to enter a new grant. I expect that this adds a new form to the formset where I can enter the desired qualification and the expiration date.

Hint: have a look at pretix, they use a jquery library to manage the client-side stuff of the formsets (adding a new form, deleting)


Discussion:

Resolving patch:
diff --git a/ephios/settings.py b/ephios/settings.py
--- a/ephios/settings.py
+++ b/ephios/settings.py
@@ -49,6 +49,8 @@
     "bootstrap4",
     "guardian",
     "django_select2",
+    "jquery",
+    "djangoformsetjs",
     "ephios.user_management",
     "ephios.event_management",
     "ephios.contrib",
diff --git a/ephios/user_management/forms.py b/ephios/user_management/forms.py
--- a/ephios/user_management/forms.py
+++ b/ephios/user_management/forms.py
@@ -2,13 +2,19 @@
 from django.contrib.auth.forms import ReadOnlyPasswordHashField
 from django.contrib.auth.models import Group
 from django.contrib.auth.password_validation import validate_password
-from django.forms import BooleanField, ModelForm, ModelMultipleChoiceField
+from django.forms import (
+    BooleanField,
+    ModelForm,
+    ModelMultipleChoiceField,
+    inlineformset_factory,
+    TextInput,
+)
 from django.utils.translation import gettext as _
-from django_select2.forms import Select2MultipleWidget
+from django_select2.forms import Select2MultipleWidget, Select2Widget
 from guardian.shortcuts import assign_perm, remove_perm
 
 from ephios.helpers.widgets import CustomDateInput
-from ephios.user_management.models import UserProfile
+from ephios.user_management.models import UserProfile, QualificationGrant
 from ephios.user_management.widgets import MultiUserProfileWidget
 
 
@@ -173,3 +179,26 @@ def save(self, commit=True):
         userprofile.groups.set(self.cleaned_data["groups"])
         userprofile.save()
         return userprofile
+
+
+class QualificationGrantForm(ModelForm):
+    model = QualificationGrant
+
+    class Meta:
+        fields = ["qualification", "expires"]
+        widgets = {"qualification": Select2Widget, "expires": CustomDateInput(format="%Y-%m-%d")}
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        instance = getattr(self, "instance", None)
+        if instance and instance.pk:
+            self.fields["qualification"].disabled = True
+            self.fields["qualification"].widget = TextInput(
+                attrs={"class": "form-control-plaintext"}
+            )
+            self.initial["qualification"] = instance.qualification.title
+
+
+QualificationGrantFormset = inlineformset_factory(
+    UserProfile, QualificationGrant, form=QualificationGrantForm, extra=0,
+)
diff --git a/ephios/user_management/views.py b/ephios/user_management/views.py
--- a/ephios/user_management/views.py
+++ b/ephios/user_management/views.py
@@ -1,13 +1,22 @@
 from django.contrib import messages
 from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
 from django.contrib.auth.models import Group
+from django.shortcuts import redirect
 from django.urls import reverse
 from django.utils.translation import gettext as _
-from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView
+from django.views.generic import (
+    CreateView,
+    DeleteView,
+    DetailView,
+    ListView,
+    UpdateView,
+    TemplateView,
+)
+from django.views.generic.detail import SingleObjectMixin
 from guardian.shortcuts import get_objects_for_group
 
 from ephios.user_management import mail
-from ephios.user_management.forms import GroupForm, UserProfileForm
+from ephios.user_management.forms import GroupForm, UserProfileForm, QualificationGrantFormset
 from ephios.user_management.models import UserProfile
 
 
@@ -21,47 +30,75 @@ class UserProfileListView(PermissionRequiredMixin, ListView):
     permission_required = "user_management.view_userprofile"
 
 
-class UserProfileCreateView(PermissionRequiredMixin, CreateView):
+class UserProfileCreateView(PermissionRequiredMixin, TemplateView):
     template_name = "user_management/userprofile_form.html"
     permission_required = "user_management.add_userprofile"
     model = UserProfile
-    form_class = UserProfileForm
 
-    def get_success_url(self):
-        messages.success(self.request, _("User added successfully."))
-        return reverse("user_management:userprofile_list")
-
-    def form_valid(self, form):
-        response = super().form_valid(form)
-        userprofile = self.object
-        if userprofile.is_active:
-            mail.send_account_creation_info(userprofile)
-        return response
-
-
-class UserProfileUpdateView(PermissionRequiredMixin, UpdateView):
+    def get_context_data(self, **kwargs):
+        kwargs.setdefault("userprofile_form", UserProfileForm(self.request.POST or None))
+        kwargs.setdefault(
+            "qualification_formset", QualificationGrantFormset(self.request.POST or None)
+        )
+        return super().get_context_data(**kwargs)
+
+    def post(self, request, *args, **kwargs):
+        userprofile_form = UserProfileForm(self.request.POST)
+        qualification_formset = QualificationGrantFormset(self.request.POST)
+        if all((userprofile_form.is_valid(), qualification_formset.is_valid())):
+            userprofile = userprofile_form.save()
+            qualification_formset.instance = userprofile
+            qualification_formset.save()
+            messages.success(self.request, _("User added successfully."))
+            if userprofile.is_active:
+                mail.send_account_creation_info(userprofile)
+            return redirect(reverse("user_management:userprofile_list"))
+        else:
+            return self.render_to_response(
+                self.get_context_data(
+                    userprofile_form=userprofile_form, qualification_formset=qualification_formset
+                )
+            )
+
+
+class UserProfileUpdateView(PermissionRequiredMixin, SingleObjectMixin, TemplateView):
     model = UserProfile
     permission_required = "user_management.change_userprofile"
     template_name = "user_management/userprofile_form.html"
-    form_class = UserProfileForm
-
-    def get_success_url(self):
-        messages.success(self.request, _("User updated successfully."))
-        return reverse("user_management:userprofile_list")
 
-    def form_valid(self, form):
-        response = super().form_valid(form)
-        userprofile = self.object
-        if userprofile.is_active:
-            mail.send_account_update_info(userprofile)
-        return response
-
-    def get_form_kwargs(self):
-        kwargs = super().get_form_kwargs()
-        kwargs["initial"] = {
-            "groups": self.object.groups.all(),
-        }
-        return kwargs
+    def get_userprofile_form(self):
+        return UserProfileForm(
+            self.request.POST or None,
+            initial={"groups": self.get_object().groups.all(),},
+            instance=self.object,
+        )
+
+    def get_qualification_formset(self):
+        return QualificationGrantFormset(self.request.POST or None, instance=self.object)
+
+    def get_context_data(self, **kwargs):
+        self.object = self.get_object()
+        kwargs.setdefault("userprofile_form", self.get_userprofile_form())
+        kwargs.setdefault("qualification_formset", self.get_qualification_formset())
+        return super().get_context_data(**kwargs)
+
+    def post(self, request, *args, **kwargs):
+        self.object = self.get_object()
+        userprofile_form = self.get_userprofile_form()
+        qualification_formset = self.get_qualification_formset()
+        if all((userprofile_form.is_valid(), qualification_formset.is_valid())):
+            userprofile = userprofile_form.save()
+            qualification_formset.save()
+            messages.success(self.request, _("User updated successfully."))
+            if userprofile.is_active:
+                mail.send_account_update_info(userprofile)
+            return redirect(reverse("user_management:userprofile_list"))
+        else:
+            return self.render_to_response(
+                self.get_context_data(
+                    userprofile_form=userprofile_form, qualification_formset=qualification_formset
+                )
+            )
 
 
 class GroupListView(PermissionRequiredMixin, ListView):


Test patch: