mirror of
https://github.com/ae-utbm/sith.git
synced 2026-06-05 15:49:21 +00:00
automatically apply election results
This commit is contained in:
+87
-2
@@ -1,12 +1,17 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from itertools import groupby, islice
|
||||||
|
from operator import attrgetter
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.timezone import localtime
|
from django.db import transaction
|
||||||
|
from django.db.models import Count
|
||||||
|
from django.forms.models import ModelChoiceIterator, ModelChoiceIteratorValue
|
||||||
|
from django.utils.timezone import localdate, localtime
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from club.forms import ClubRoleChoiceField
|
from club.forms import ClubRoleChoiceField
|
||||||
from club.models import ClubRole
|
from club.models import ClubRole, Membership
|
||||||
from club.widgets.ajax_select import AutoCompleteSelectMultipleClub
|
from club.widgets.ajax_select import AutoCompleteSelectMultipleClub
|
||||||
from core.models import User
|
from core.models import User
|
||||||
from core.views.forms import SelectDateTime
|
from core.views.forms import SelectDateTime
|
||||||
@@ -180,3 +185,83 @@ class ElectionCreateForm(ElectionForm):
|
|||||||
if commit:
|
if commit:
|
||||||
ElectionList.objects.create(title="Candidat⸱e libre", election=instance)
|
ElectionList.objects.create(title="Candidat⸱e libre", election=instance)
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class ElectionWinnerChoiceIterator(ModelChoiceIterator):
|
||||||
|
"""Iterate over the candidates that gathered enough votes"""
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
# for each role, yield only the N first candidates,
|
||||||
|
# where N is the election role max_choice
|
||||||
|
qs = (
|
||||||
|
self.queryset.annotate(nb_votes=Count("votes"))
|
||||||
|
.order_by("role__order", "-nb_votes")
|
||||||
|
.select_related("role", "user", "role__club_role", "role__club_role__club")
|
||||||
|
)
|
||||||
|
yield from (
|
||||||
|
(
|
||||||
|
f"{role.title} \u2013 {role.club_role.club.name}",
|
||||||
|
[self.choice(cand) for cand in islice(candidates, role.max_choice)],
|
||||||
|
)
|
||||||
|
for role, candidates in groupby(qs, key=attrgetter("role"))
|
||||||
|
)
|
||||||
|
|
||||||
|
def choice(self, obj: Candidature):
|
||||||
|
return (
|
||||||
|
ModelChoiceIteratorValue(self.field.prepare_value(obj), obj),
|
||||||
|
obj.user.get_full_name(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ElectionWinnerChoiceField(forms.ModelMultipleChoiceField):
|
||||||
|
"""Custom `ModelChoiceField` for `[ClubRole][club.models.ClubRole]`.
|
||||||
|
|
||||||
|
If only one club is involved, behave like the base `ModelChoiceField`.
|
||||||
|
If dealing with the roles of multiple clubs, group the roles
|
||||||
|
into a different `optgroup` for each club.
|
||||||
|
"""
|
||||||
|
|
||||||
|
iterator = ElectionWinnerChoiceIterator
|
||||||
|
widget = forms.CheckboxSelectMultiple
|
||||||
|
|
||||||
|
|
||||||
|
class ApplyElectionResultForm(forms.Form):
|
||||||
|
"""Form to select winners of an election, and automatically apply the results."""
|
||||||
|
|
||||||
|
candidates = ElectionWinnerChoiceField(Candidature.objects.none())
|
||||||
|
|
||||||
|
def __init__(self, *args, election: Election, **kwargs):
|
||||||
|
self.election = election
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
qs = Candidature.objects.filter(
|
||||||
|
role__election=election, role__club_role__isnull=False
|
||||||
|
)
|
||||||
|
# pass all candidates to the ModelChoiceField ;
|
||||||
|
# its inner choice iterator will take care of filtering only the winners.
|
||||||
|
self.fields["candidates"].queryset = qs
|
||||||
|
# By default, mark every candidate as selected.
|
||||||
|
# Election results are usually completely validated during the AG,
|
||||||
|
# so it makes more sense UX-wise to eventually unselect a candidate
|
||||||
|
# than to select everyone.
|
||||||
|
self.fields["candidates"].initial = qs.values_list("id", flat=True)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
if self.errors:
|
||||||
|
return
|
||||||
|
candidates: list[Candidature] = list(self.cleaned_data["candidates"])
|
||||||
|
with transaction.atomic():
|
||||||
|
Membership.objects.filter(
|
||||||
|
role__in=[c.role.club_role for c in candidates],
|
||||||
|
end_date=None,
|
||||||
|
start_date__lt=self.election.end_date,
|
||||||
|
).update(end_date=localdate())
|
||||||
|
memberships = [
|
||||||
|
Membership(
|
||||||
|
user_id=c.user_id,
|
||||||
|
club_id=c.role.club_role.club_id,
|
||||||
|
role=c.role.club_role,
|
||||||
|
)
|
||||||
|
for c in candidates
|
||||||
|
]
|
||||||
|
Membership.objects.bulk_create(memberships)
|
||||||
|
Membership._add_club_groups(memberships)
|
||||||
|
|||||||
+2
-2
@@ -135,9 +135,9 @@ class Role(OrderedModel):
|
|||||||
|
|
||||||
def results(self, total_vote: int) -> dict[str, dict[str, int | float]]:
|
def results(self, total_vote: int) -> dict[str, dict[str, int | float]]:
|
||||||
if total_vote == 0:
|
if total_vote == 0:
|
||||||
candidates = self.candidatures.values_list("user__username")
|
candidates = self.candidatures.values_list("user__username", flat=True)
|
||||||
return {
|
return {
|
||||||
key: {"vote": 0, "percent": 0} for key in ["blank_votes", *candidates]
|
key: {"vote": 0, "percent": 0} for key in ["blank vote", *candidates]
|
||||||
}
|
}
|
||||||
total_vote *= self.max_choice
|
total_vote *= self.max_choice
|
||||||
results = {"total vote": total_vote}
|
results = {"total vote": total_vote}
|
||||||
|
|||||||
@@ -29,13 +29,25 @@
|
|||||||
{% trans %}Polls closed {% endtrans %}
|
{% trans %}Polls closed {% endtrans %}
|
||||||
{%- else %}
|
{%- else %}
|
||||||
{% trans %}Polls will open {% endtrans %}
|
{% trans %}Polls will open {% endtrans %}
|
||||||
<time datetime="{{ election.start_date }}">{{ election.start_date|localtime|date(DATETIME_FORMAT)}}</time>
|
<time datetime="{{ election.start_date }}">{{ election.start_date|localtime|date(DATETIME_FORMAT) }}</time>
|
||||||
{% trans %} at {% endtrans %}<time>{{ election.start_date|localtime|time(DATETIME_FORMAT)}}</time>
|
{% trans %}at{% endtrans %}
|
||||||
|
<time>{{ election.start_date|localtime|time(DATETIME_FORMAT) }}</time>
|
||||||
{% trans %}and will close {% endtrans %}
|
{% trans %}and will close {% endtrans %}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
<time datetime="{{ election.end_date }}">{{ election.end_date|localtime|date(DATETIME_FORMAT)}}</time>
|
<time datetime="{{ election.end_date }}">{{ election.end_date|localtime|date(DATETIME_FORMAT) }}</time>
|
||||||
{% trans %} at {% endtrans %}<time>{{ election.end_date|localtime|time(DATETIME_FORMAT)}}</time>
|
{% trans %}at{% endtrans %}
|
||||||
|
<time>{{ election.end_date|localtime|time(DATETIME_FORMAT) }}</time>
|
||||||
</p>
|
</p>
|
||||||
|
{%- if election.is_vote_finished and user.can_edit(election) %}
|
||||||
|
<details class="accordion" name="apply-result">
|
||||||
|
<summary>{% trans %}Apply election result{% endtrans %}</summary>
|
||||||
|
<div
|
||||||
|
class="accordion-content aria-busy-grow"
|
||||||
|
hx-get="{{ url("election:apply_result", election_id=election.id) }}"
|
||||||
|
hx-trigger="toggle from:closest details once"
|
||||||
|
></div>
|
||||||
|
</details>
|
||||||
|
{% endif %}
|
||||||
{%- if user_has_voted %}
|
{%- if user_has_voted %}
|
||||||
<p class="election__elector-infos">
|
<p class="election__elector-infos">
|
||||||
{%- if election.is_vote_active %}
|
{%- if election.is_vote_active %}
|
||||||
@@ -47,17 +59,27 @@
|
|||||||
{%- endif %}
|
{%- endif %}
|
||||||
</section>
|
</section>
|
||||||
<section class="election_vote">
|
<section class="election_vote">
|
||||||
<form action="{{ url('election:vote', election.id) }}" method="post" class="election__vote-form" name="vote-form" id="vote-form">
|
<form
|
||||||
|
action="{{ url('election:vote', election.id) }}"
|
||||||
|
method="post"
|
||||||
|
class="election__vote-form"
|
||||||
|
name="vote-form"
|
||||||
|
id="vote-form"
|
||||||
|
>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<table class="election_table">
|
<table class="election_table">
|
||||||
<thead class="lists">
|
<thead class="lists">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="column" style="width: {{ 100 / (election_lists|length + 1) }}%">{% trans %}Blank vote{% endtrans %}</th>
|
<th class="column" style="width: {{ 100 / (election_lists|length + 1) }}%">
|
||||||
|
{% trans %}Blank vote{% endtrans %}
|
||||||
|
</th>
|
||||||
{%- for election_list in election_lists %}
|
{%- for election_list in election_lists %}
|
||||||
<th class="column" style="width: {{ 100 / (election_lists|length + 1) }}%">
|
<th class="column" style="width: {{ 100 / (election_lists|length + 1) }}%">
|
||||||
<span>{{ election_list.title }}</span>
|
<span>{{ election_list.title }}</span>
|
||||||
{% if user.can_edit(election_list) and election.is_vote_editable -%}
|
{% if user.can_edit(election_list) and election.is_vote_editable -%}
|
||||||
<a href="{{ url('election:delete_list', list_id=election_list.id) }}"><i class="fa-regular fa-trash-can delete-action"></i></a>
|
<a href="{{ url('election:delete_list', list_id=election_list.id) }}">
|
||||||
|
<i class="fa-regular fa-trash-can delete-action"></i>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</th>
|
</th>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
@@ -103,22 +125,45 @@
|
|||||||
<button disabled><i class="fa fa-arrow-down"></i></button>
|
<button disabled><i class="fa fa-arrow-down"></i></button>
|
||||||
<button disabled><i class="fa fa-caret-down"></i></button>
|
<button disabled><i class="fa fa-caret-down"></i></button>
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
<button type="button" onclick="window.location.replace('?role={{ role.id }}&action=bottom');"><i class="fa fa-arrow-down"></i></button>
|
<button
|
||||||
<button type="button" onclick="window.location.replace('?role={{ role.id }}&action=down');"><i class="fa fa-caret-down"></i></button>
|
type="button"
|
||||||
|
onclick="window.location.replace('?role={{ role.id }}&action=bottom');"
|
||||||
|
>
|
||||||
|
<i class="fa fa-arrow-down"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="window.location.replace('?role={{ role.id }}&action=down');"
|
||||||
|
>
|
||||||
|
<i class="fa fa-caret-down"></i>
|
||||||
|
</button>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- if loop.first -%}
|
{%- if loop.first -%}
|
||||||
<button disabled><i class="fa fa-caret-up"></i></button>
|
<button disabled><i class="fa fa-caret-up"></i></button>
|
||||||
<button disabled><i class="fa fa-arrow-up"></i></button>
|
<button disabled><i class="fa fa-arrow-up"></i></button>
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
<button type="button" onclick="window.location.replace('?role={{ role.id }}&action=up');"><i class="fa fa-caret-up"></i></button>
|
<button
|
||||||
<button type="button" onclick="window.location.replace('?role={{ role.id }}&action=top');"><i class="fa fa-arrow-up"></i></button>
|
type="button"
|
||||||
|
onclick="window.location.replace('?role={{ role.id }}&action=up');"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="fa fa-caret-up"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="window.location.replace('?role={{ role.id }}&action=top');"
|
||||||
|
><i class="fa fa-arrow-up"></i>
|
||||||
|
</button>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</div>
|
</div>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="role_candidates">
|
<tr class="role_candidates">
|
||||||
<td class="list_per_role" style="width: 100%; max-width: {{ 100 / (election_lists|length + 1) }}%">
|
<td
|
||||||
|
class="list_per_role"
|
||||||
|
style="width: 100%; max-width: {{ 100 / (election_lists|length + 1) }}%"
|
||||||
|
>
|
||||||
{%- if role.max_choice == 1 and show_vote_buttons %}
|
{%- if role.max_choice == 1 and show_vote_buttons %}
|
||||||
<div class="radio-btn">
|
<div class="radio-btn">
|
||||||
{% set input_id = "blank_vote_" + role.id|string %}
|
{% set input_id = "blank_vote_" + role.id|string %}
|
||||||
@@ -131,26 +176,46 @@
|
|||||||
{%- if election.is_vote_finished %}
|
{%- if election.is_vote_finished %}
|
||||||
{%- set results = election_results[role.title]['blank vote'] %}
|
{%- set results = election_results[role.title]['blank vote'] %}
|
||||||
<div class="election__results">
|
<div class="election__results">
|
||||||
<strong>{{ results.vote }} {% trans %}votes{% endtrans %} ({{ "%.2f" % results.percent }} %)</strong>
|
<strong>
|
||||||
|
{{ results.vote }} {% trans %}votes{% endtrans %} ({{ "%.2f" % results.percent }} %)
|
||||||
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</td>
|
</td>
|
||||||
{%- for election_list in election_lists %}
|
{%- for election_list in election_lists %}
|
||||||
<td class="list_per_role" style="width: 100%; max-width: {{ 100 / (election_lists|length + 1) }}%">
|
<td
|
||||||
|
class="list_per_role"
|
||||||
|
style="width: 100%; max-width: {{ 100 / (election_lists|length + 1) }}%"
|
||||||
|
>
|
||||||
<ul class="candidates">
|
<ul class="candidates">
|
||||||
{%- for candidature in election_list.candidatures.select_related("user", "user__profile_pict").filter(role=role) %}
|
{%- for candidature in election_list.candidatures.select_related("user", "user__profile_pict").filter(role=role) %}
|
||||||
<li class="candidate">
|
<li class="candidate">
|
||||||
{%- if show_vote_buttons %}
|
{%- if show_vote_buttons %}
|
||||||
{% set input_id = "candidature_" + candidature.id|string %}
|
{% set input_id = "candidature_" + candidature.id|string %}
|
||||||
<input id="{{ input_id }}" type="{{ 'checkbox' if role.max_choice > 1 else 'radio' }}" {{ 'checked' if candidature.id|string in role_data else '' }} {{ 'disabled' if user_has_voted else '' }} name="{{ role.title }}" value="{{ candidature.id }}">
|
<input
|
||||||
|
id="{{ input_id }}"
|
||||||
|
type="{{ 'checkbox' if role.max_choice > 1 else 'radio' }}"
|
||||||
|
{% if candidature.id|string in role_data %}checked{% endif %}
|
||||||
|
{% if user_has_voted %}disabled{% endif %}
|
||||||
|
name="{{ role.title }}"
|
||||||
|
value="{{ candidature.id }}"
|
||||||
|
>
|
||||||
<label for="{{ input_id }}">
|
<label for="{{ input_id }}">
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
<figure>
|
<figure>
|
||||||
{%- if user.can_view(candidature.user) %}
|
{%- if user.can_view(candidature.user) %}
|
||||||
{% if candidature.user.profile_pict %}
|
{% if candidature.user.profile_pict %}
|
||||||
<img class="candidate__picture" src="{{ candidature.user.profile_pict.get_download_url() }}" alt="{% trans %}Profile{% endtrans %}">
|
<img
|
||||||
|
class="candidate__picture"
|
||||||
|
src="{{ candidature.user.profile_pict.get_download_url() }}"
|
||||||
|
alt="{% trans %}Profile{% endtrans %}"
|
||||||
|
>
|
||||||
{% else %}
|
{% else %}
|
||||||
<img class="candidate__picture" src="{{ static('core/img/unknown.jpg') }}" alt="{% trans %}Profile{% endtrans %}">
|
<img
|
||||||
|
class="candidate__picture"
|
||||||
|
src="{{ static('core/img/unknown.jpg') }}"
|
||||||
|
alt="{% trans %}Profile{% endtrans %}"
|
||||||
|
>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
<figcaption class="candidate__details">
|
<figcaption class="candidate__details">
|
||||||
@@ -164,8 +229,12 @@
|
|||||||
{%- if user.can_edit(candidature) -%}
|
{%- if user.can_edit(candidature) -%}
|
||||||
{%- if election.is_vote_editable -%}
|
{%- if election.is_vote_editable -%}
|
||||||
<div class="edit_btns">
|
<div class="edit_btns">
|
||||||
<a href="{{url('election:update_candidate', candidature_id=candidature.id)}}"><i class="fa-regular fa-pen-to-square edit-action"></i>️</a>
|
<a href="{{ url('election:update_candidate', candidature_id=candidature.id) }}">
|
||||||
<a href="{{url('election:delete_candidate', candidature_id=candidature.id)}}"><i class="fa-regular fa-trash-can delete-action"></i></a>
|
<i class="fa-regular fa-pen-to-square edit-action"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ url('election:delete_candidate', candidature_id=candidature.id) }}">
|
||||||
|
<i class="fa-regular fa-trash-can delete-action"></i>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<div id="apply-election-result-fragment">
|
||||||
|
{% if not form.candidates.field.choices %}
|
||||||
|
<em>{% trans %}No result to apply{% endtrans %}</em>
|
||||||
|
<p>
|
||||||
|
{% trans trimmed %}
|
||||||
|
This may be because no role of this election
|
||||||
|
was linked to a club role.
|
||||||
|
{% endtrans %}
|
||||||
|
</p>
|
||||||
|
{% elif already_applied %}
|
||||||
|
<em>
|
||||||
|
{%- trans trimmed -%}
|
||||||
|
The results of this election have been applied
|
||||||
|
{%- endtrans -%}
|
||||||
|
</em>
|
||||||
|
<p>
|
||||||
|
{% for club in clubs %}
|
||||||
|
<a href="{{ url("club:club_members", club_id=club.id) }}" class="btn btn-blue">
|
||||||
|
<i class="fa fa-arrow-up-right-from-square"></i>
|
||||||
|
{% trans club=club.name %}{{ club }} members{% endtrans %}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-yellow">
|
||||||
|
<div class="alert-main">
|
||||||
|
<strong class="alert-title">{% trans %}Warning{% endtrans %}</strong>
|
||||||
|
<p>
|
||||||
|
{%- trans trimmed -%}
|
||||||
|
Only election roles linked to a club role will be automatically applied.
|
||||||
|
{%- endtrans -%}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{%- trans trimmed -%}
|
||||||
|
Don't forget to manually apply the eventual remaining roles afterward.
|
||||||
|
{%- endtrans -%}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
hx-post="{{ url("election:apply_result", election_id=form.election.id) }}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-target="#apply-election-result-fragment"
|
||||||
|
hx-disabled-elt="find input[type='submit']"
|
||||||
|
>
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form }}
|
||||||
|
<input type="submit" class="btn btn-blue">
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
@@ -40,7 +40,6 @@ class TestElectionDetail(TestElection):
|
|||||||
reverse("election:detail", args=str(self.election.id))
|
reverse("election:detail", args=str(self.election.id))
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "La roue tourne" in str(response.content)
|
|
||||||
|
|
||||||
|
|
||||||
class TestElectionUpdateView(TestElection):
|
class TestElectionUpdateView(TestElection):
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from election.views import (
|
from election.views import (
|
||||||
|
ApplyResultFragment,
|
||||||
CandidatureCreateView,
|
CandidatureCreateView,
|
||||||
CandidatureDeleteView,
|
CandidatureDeleteView,
|
||||||
CandidatureUpdateView,
|
CandidatureUpdateView,
|
||||||
@@ -56,4 +57,9 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
path("<int:election_id>/vote/", VoteFormView.as_view(), name="vote"),
|
path("<int:election_id>/vote/", VoteFormView.as_view(), name="vote"),
|
||||||
path("<int:election_id>/detail/", ElectionDetailView.as_view(), name="detail"),
|
path("<int:election_id>/detail/", ElectionDetailView.as_view(), name="detail"),
|
||||||
|
path(
|
||||||
|
"fragment/<int:election_id>/apply/",
|
||||||
|
ApplyResultFragment.as_view(),
|
||||||
|
name="apply_result",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.views.generic import DetailView, ListView
|
from django.views.generic import DetailView, ListView
|
||||||
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
|
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
|
||||||
|
|
||||||
|
from club.models import Membership
|
||||||
from core.auth.mixins import CanEditMixin, CanViewMixin
|
from core.auth.mixins import CanEditMixin, CanViewMixin
|
||||||
|
from core.views import FragmentMixin
|
||||||
from election.forms import (
|
from election.forms import (
|
||||||
|
ApplyElectionResultForm,
|
||||||
CandidateForm,
|
CandidateForm,
|
||||||
ElectionCreateForm,
|
ElectionCreateForm,
|
||||||
ElectionForm,
|
ElectionForm,
|
||||||
@@ -405,3 +408,45 @@ class ElectionListDeleteView(CanEditMixin, DeleteView):
|
|||||||
|
|
||||||
def get_success_url(self, **kwargs):
|
def get_success_url(self, **kwargs):
|
||||||
return reverse("election:detail", kwargs={"election_id": self.election.id})
|
return reverse("election:detail", kwargs={"election_id": self.election.id})
|
||||||
|
|
||||||
|
|
||||||
|
class ApplyResultFragment(
|
||||||
|
LoginRequiredMixin, UserPassesTestMixin, FragmentMixin, FormView
|
||||||
|
):
|
||||||
|
template_name = "election/fragments/apply_result.jinja"
|
||||||
|
form_class = ApplyElectionResultForm
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def election(self):
|
||||||
|
return get_object_or_404(Election, pk=self.kwargs["election_id"])
|
||||||
|
|
||||||
|
def test_func(self):
|
||||||
|
if not self.election.is_vote_finished:
|
||||||
|
return False
|
||||||
|
if self.request.user.has_perm("club.add_membership"):
|
||||||
|
return True
|
||||||
|
return self.election.edit_groups.filter(
|
||||||
|
id__in=self.request.user.all_groups
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
return super().get_form_kwargs() | {"election": self.election}
|
||||||
|
|
||||||
|
def form_valid(self, form: ApplyElectionResultForm):
|
||||||
|
form.save()
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
return super().get_context_data(**kwargs) | {
|
||||||
|
"already_applied": Membership.objects.filter(
|
||||||
|
role__election_roles__election=self.election,
|
||||||
|
end_date=None,
|
||||||
|
start_date__gte=self.election.end_date,
|
||||||
|
).exists(),
|
||||||
|
"clubs": self.election.clubs.all(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_success_url(self, **kwargs):
|
||||||
|
return reverse(
|
||||||
|
"election:apply_result", kwargs={"election_id": self.election.id}
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user