From fc773f375b933a3406f25b07473c7f09f009ec29 Mon Sep 17 00:00:00 2001
From: imperosol <thgirod@hotmail.com>
Date: Fri, 14 Mar 2025 19:19:20 +0100
Subject: [PATCH] refactor CandidatureForm

---
 election/forms.py                             | 25 +++-----
 ...didature_program_alter_candidature_user.py | 30 ++++++++++
 election/models.py                            | 42 ++++---------
 election/views.py                             | 60 +++++--------------
 4 files changed, 66 insertions(+), 91 deletions(-)
 create mode 100644 election/migrations/0005_alter_candidature_program_alter_candidature_user.py

diff --git a/election/forms.py b/election/forms.py
index 5032e859..0e901c4c 100644
--- a/election/forms.py
+++ b/election/forms.py
@@ -1,12 +1,6 @@
 from django import forms
-from django.db import transaction
-from django.db.models import QuerySet
-from django.shortcuts import get_object_or_404
-from django.urls import reverse_lazy
 from django.utils.translation import gettext_lazy as _
-from django.views.generic import FormView
 
-from core.auth.mixins import CanCreateMixin
 from core.views.forms import SelectDateTime
 from core.views.widgets.ajax_select import (
     AutoCompleteSelect,
@@ -14,7 +8,7 @@ from core.views.widgets.ajax_select import (
     AutoCompleteSelectUser,
 )
 from core.views.widgets.markdown import MarkdownInput
-from election.models import Candidature, Election, ElectionList, Role, Vote
+from election.models import Candidature, Election, ElectionList, Role
 
 
 class LimitedCheckboxField(forms.ModelMultipleChoiceField):
@@ -39,6 +33,8 @@ class LimitedCheckboxField(forms.ModelMultipleChoiceField):
 class CandidateForm(forms.ModelForm):
     """Form to candidate."""
 
+    required_css_class = "required"
+
     class Meta:
         model = Candidature
         fields = ["user", "role", "program", "election_list"]
@@ -52,17 +48,12 @@ class CandidateForm(forms.ModelForm):
             "election_list": AutoCompleteSelect,
         }
 
-    def __init__(self, *args, **kwargs):
-        election_id = kwargs.pop("election_id", None)
-        can_edit = kwargs.pop("can_edit", False)
+    def __init__(
+        self, *args, election: Election | None, can_edit: bool = False, **kwargs
+    ):
         super().__init__(*args, **kwargs)
-        if election_id:
-            self.fields["role"].queryset = Role.objects.filter(
-                election__id=election_id
-            ).all()
-            self.fields["election_list"].queryset = ElectionList.objects.filter(
-                election__id=election_id
-            ).all()
+        self.fields["role"].queryset = election.roles.select_related("election")
+        self.fields["election_list"].queryset = election.election_lists.all()
         if not can_edit:
             self.fields["user"].widget = forms.HiddenInput()
 
diff --git a/election/migrations/0005_alter_candidature_program_alter_candidature_user.py b/election/migrations/0005_alter_candidature_program_alter_candidature_user.py
new file mode 100644
index 00000000..92930b2d
--- /dev/null
+++ b/election/migrations/0005_alter_candidature_program_alter_candidature_user.py
@@ -0,0 +1,30 @@
+# Generated by Django 4.2.20 on 2025-03-14 18:18
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ("election", "0004_auto_20191006_0049"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="candidature",
+            name="program",
+            field=models.TextField(blank=True, default="", verbose_name="description"),
+        ),
+        migrations.AlterField(
+            model_name="candidature",
+            name="user",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="candidates",
+                to=settings.AUTH_USER_MODEL,
+                verbose_name="user",
+            ),
+        ),
+    ]
diff --git a/election/models.py b/election/models.py
index dc6ab474..a3abd019 100644
--- a/election/models.py
+++ b/election/models.py
@@ -22,21 +22,18 @@ class Election(models.Model):
         verbose_name=_("edit groups"),
         blank=True,
     )
-
     view_groups = models.ManyToManyField(
         Group,
         related_name="viewable_elections",
         verbose_name=_("view groups"),
         blank=True,
     )
-
     vote_groups = models.ManyToManyField(
         Group,
         related_name="votable_elections",
         verbose_name=_("vote groups"),
         blank=True,
     )
-
     candidature_groups = models.ManyToManyField(
         Group,
         related_name="candidate_elections",
@@ -45,7 +42,7 @@ class Election(models.Model):
     )
 
     voters = models.ManyToManyField(
-        User, verbose_name=("voters"), related_name="voted_elections"
+        User, verbose_name=_("voters"), related_name="voted_elections"
     )
     archived = models.BooleanField(_("archived"), default=False)
 
@@ -55,20 +52,20 @@ class Election(models.Model):
     @property
     def is_vote_active(self):
         now = timezone.now()
-        return bool(now <= self.end_date and now >= self.start_date)
+        return self.start_date <= now <= self.end_date
 
     @property
     def is_vote_finished(self):
-        return bool(timezone.now() > self.end_date)
+        return timezone.now() > self.end_date
 
     @property
     def is_candidature_active(self):
         now = timezone.now()
-        return bool(now <= self.end_candidature and now >= self.start_candidature)
+        return self.start_candidature <= now <= self.end_candidature
 
     @property
     def is_vote_editable(self):
-        return bool(timezone.now() <= self.end_candidature)
+        return timezone.now() <= self.end_candidature
 
     def can_candidate(self, user):
         for group_id in self.candidature_groups.values_list("pk", flat=True):
@@ -95,12 +92,6 @@ class Election(models.Model):
             results[role.title] = role.results(total_vote)
         return results
 
-    def delete(self, *args, **kwargs):
-        self.election_lists.all().delete()
-        super().delete(*args, **kwargs)
-
-    # Permissions
-
 
 class Role(OrderedModel):
     """This class allows to create a new role avaliable for a candidature."""
@@ -115,6 +106,9 @@ class Role(OrderedModel):
     description = models.TextField(_("description"), null=True, blank=True)
     max_choice = models.IntegerField(_("max choice"), default=1)
 
+    def __str__(self):
+        return f"{self.title} - {self.election.title}"
+
     def results(self, total_vote):
         results = {}
         total_vote *= self.max_choice
@@ -142,9 +136,6 @@ class Role(OrderedModel):
     def edit_groups(self):
         return self.election.edit_groups
 
-    def __str__(self):
-        return ("%s : %s") % (self.election.title, self.title)
-
 
 class ElectionList(models.Model):
     """To allow per list vote."""
@@ -163,11 +154,6 @@ class ElectionList(models.Model):
     def can_be_edited_by(self, user):
         return user.can_edit(self.election)
 
-    def delete(self, *args, **kwargs):
-        for candidature in self.candidatures.all():
-            candidature.delete()
-        super().delete(*args, **kwargs)
-
 
 class Candidature(models.Model):
     """This class is a component of responsability."""
@@ -182,10 +168,9 @@ class Candidature(models.Model):
         User,
         verbose_name=_("user"),
         related_name="candidates",
-        blank=True,
         on_delete=models.CASCADE,
     )
-    program = models.TextField(_("description"), null=True, blank=True)
+    program = models.TextField(_("description"), default="", blank=True)
     election_list = models.ForeignKey(
         ElectionList,
         related_name="candidatures",
@@ -196,13 +181,10 @@ class Candidature(models.Model):
     def __str__(self):
         return f"{self.role.title} : {self.user.username}"
 
-    def delete(self):
-        for vote in self.votes.all():
-            vote.delete()
-        super().delete()
-
     def can_be_edited_by(self, user):
-        return (user == self.user) or user.can_edit(self.role.election)
+        return (
+            (user == self.user) or user.can_edit(self.role.election)
+        ) and self.role.election.is_vote_editable
 
 
 class Vote(models.Model):
diff --git a/election/views.py b/election/views.py
index ed0570e5..84587a9e 100644
--- a/election/views.py
+++ b/election/views.py
@@ -155,26 +155,22 @@ class CandidatureCreateView(LoginRequiredMixin, CreateView):
 
     def dispatch(self, request, *arg, **kwargs):
         self.election = get_object_or_404(Election, pk=kwargs["election_id"])
+        self.can_edit = self.request.user.can_edit(self.election)
         return super().dispatch(request, *arg, **kwargs)
 
     def get_initial(self):
-        init = {}
-        self.can_edit = self.request.user.can_edit(self.election)
-        init["user"] = self.request.user.id
-        return init
+        return {"user": self.request.user.id}
 
     def get_form_kwargs(self):
-        kwargs = super().get_form_kwargs()
-        kwargs["election_id"] = self.election.id
-        kwargs["can_edit"] = self.can_edit
-        return kwargs
+        return super().get_form_kwargs() | {
+            "election": self.election,
+            "can_edit": self.can_edit,
+        }
 
-    def form_valid(self, form):
+    def form_valid(self, form: CandidateForm):
         """Verify that the selected user is in candidate group."""
         obj = form.instance
         obj.election = self.election
-        if not hasattr(obj, "user"):
-            obj.user = self.request.user
         if (obj.election.can_candidate(obj.user)) and (
             obj.user == self.request.user or self.can_edit
         ):
@@ -182,9 +178,7 @@ class CandidatureCreateView(LoginRequiredMixin, CreateView):
         raise PermissionDenied
 
     def get_context_data(self, **kwargs):
-        kwargs = super().get_context_data(**kwargs)
-        kwargs["election"] = self.election
-        return kwargs
+        return super().get_context_data(**kwargs) | {"election": self.election}
 
     def get_success_url(self, **kwargs):
         return reverse_lazy("election:detail", kwargs={"election_id": self.election.id})
@@ -302,45 +296,23 @@ class ElectionUpdateView(CanEditMixin, UpdateView):
         return reverse_lazy("election:detail", kwargs={"election_id": self.object.id})
 
 
-class CandidatureUpdateView(CanEditMixin, UpdateView):
+class CandidatureUpdateView(LoginRequiredMixin, CanEditMixin, UpdateView):
     model = Candidature
     form_class = CandidateForm
     template_name = "core/edit.jinja"
     pk_url_kwarg = "candidature_id"
 
-    def dispatch(self, request, *arg, **kwargs):
-        self.object = self.get_object()
-        if not self.object.role.election.is_vote_editable:
-            raise PermissionDenied
-        return super().dispatch(request, *arg, **kwargs)
-
-    def remove_fields(self):
-        self.form.fields.pop("role", None)
-
-    def get(self, request, *args, **kwargs):
-        self.form = self.get_form()
-        self.remove_fields()
-        return self.render_to_response(self.get_context_data(form=self.form))
-
-    def post(self, request, *args, **kwargs):
-        self.form = self.get_form()
-        self.remove_fields()
-        if (
-            request.user.is_authenticated
-            and request.user.can_edit(self.object)
-            and self.form.is_valid()
-        ):
-            return super().form_valid(self.form)
-        return self.form_invalid(self.form)
+    def get_form(self, *args, **kwargs):
+        form = super().get_form(*args, **kwargs)
+        form.fields.pop("role", None)
+        return form
 
     def get_form_kwargs(self):
-        kwargs = super().get_form_kwargs()
-        kwargs["election_id"] = self.object.role.election.id
-        return kwargs
+        return super().get_form_kwargs() | {"election": self.object.role.election}
 
     def get_success_url(self, **kwargs):
-        return reverse_lazy(
-            "election:detail", kwargs={"election_id": self.object.role.election.id}
+        return reverse(
+            "election:detail", kwargs={"election_id": self.object.role.election_id}
         )