Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add proportional to results page #218

Merged
merged 16 commits into from
Jan 23, 2025
Merged
142 changes: 90 additions & 52 deletions approval_polls/templates/results.html
Original file line number Diff line number Diff line change
@@ -1,58 +1,96 @@
{% extends 'base.html' %}

Check failure on line 1 in approval_polls/templates/results.html

View check run for this annotation

Trunk.io / Trunk Check

djlint

Incorrect formatting, autoformat by running 'trunk fmt'
{% load filters %}
{% block content %}
<div class="container">
<h1 class="mb-4">{{ poll.question }}</h1>
<div class="mb-4">
<h2 class="h4 text-muted">
{{ poll.total_ballots }} ballot{{ poll.total_ballots|pluralize }}
{% if poll.is_closed and poll.total_votes == 0 %}<small class="d-block mt-2">No votes in this poll</small>{% endif %}
</h2>
</div>
<div class="mb-4">
{% for choice in choices %}
<div class="mb-4 {% if choice in leading_choices %}border border-success rounded p-3{% endif %}">
<h3 class="h5 {% if choice in leading_choices %}text-success font-weight-bold{% endif %}">
{{ choice.choice_text }}
{% if choice in leading_choices %}
<span class="badge bg-success ms-2">
{% if poll.is_closed %}
Winner
{% else %}
Leading
{% endif %}
<i class="bi bi-trophy-fill ms-1"></i>
</span>
{% endif %}
</h3>
<p class="text-muted">
{{ choice.vote_count }} vote{{ choice.vote_count|pluralize }}
({{ choice.percentage|to_percent_str }})
{% if choice in leading_choices %}
<span class="text-success font-weight-bold">
<i class="bi bi-arrow-up-circle-fill"></i>
</span>
{% endif %}
</p>
<div class="progress" style="height: 25px;">
<div class="progress-bar {% if choice in leading_choices %}bg-success{% endif %}"
role="progressbar"
style="width: {% widthratio choice.vote_count poll.total_ballots 100 %}%"
aria-valuenow="{% widthratio choice.vote_count poll.total_ballots 100 %}"
aria-valuemin="0"
aria-valuemax="{{ poll.total_ballots }}">
<span class="font-weight-bold">{% widthratio choice.vote_count poll.total_ballots 100 %}%</span>
</div>
<div class="container">
<h1 class="mb-4">{{ poll.question }}</h1>
<div class="mb-4">
<h2 class="h4 text-muted">
{{ poll.total_ballots }} ballot{{ poll.total_ballots|pluralize }}
{% if poll.is_closed and poll.total_votes == 0 %}
<small class="d-block mt-2">No votes in this poll</small>
{% endif %}
</h2>
</div>

<!-- Approval Voting Results Section -->
<div class="mb-4">
{% for choice in choices %}
<div class="mb-4 {% if choice in leading_choices %}border border-success rounded p-3{% endif %}">
<h3 class="h5 {% if choice in leading_choices %}text-success font-weight-bold{% endif %}">
{{ choice.choice_text }}
{% if choice in leading_choices %}
<span class="badge bg-success ms-2">
{% if poll.is_closed %}
Winner
{% else %}
Leading
{% endif %}
<i class="bi bi-trophy-fill ms-1"></i>
</span>
{% endif %}
</h3>
<p class="text-muted">
{{ choice.vote_count }} vote{{ choice.vote_count|pluralize }}
({{ choice.percentage|to_percent_str }})
{% if choice in leading_choices %}
<span class="text-success font-weight-bold">
<i class="bi bi-arrow-up-circle-fill"></i>
</span>
{% endif %}
</p>
<div class="progress" style="height: 25px;">
<div class="progress-bar {% if choice in leading_choices %}bg-success{% endif %}"
role="progressbar"
style="width: {% widthratio choice.vote_count poll.total_ballots 100 %}%"
aria-valuenow="{% widthratio choice.vote_count poll.total_ballots 100 %}"
aria-valuemin="0"
aria-valuemax="{{ poll.total_ballots }}">
<span class="font-weight-bold">{% widthratio choice.vote_count poll.total_ballots 100 %}%</span>
</div>
</div>
{% endfor %}
</div>
<div class="text-center mt-4">
{% if 'invitation' in request.META.HTTP_REFERER %}
<a href="{{ request.META.HTTP_REFERER }}" class="btn btn-primary">Back to poll</a>
{% else %}
<a href="{% url 'detail' poll.id %}" class="btn btn-primary">Back to poll</a>
{% endif %}
</div>
</div>
{% endfor %}
</div>

<!-- Proportional Voting Results Section -->
<div class="mt-5">
<h2 class="h4">Proportional Voting Results</h2>
{% if proportional_results %}
<div class="mb-4">
{% for result in proportional_results %}
<div class="mb-4">
<h3 class="h5">
{{ result.choice_text }}
</h3>
<p class="text-muted">
Proportional Votes: {{ result.proportional_votes|floatformat:2 }}
({{ result.proportional_percentage|floatformat:2 }}%)
</p>
<div class="progress" style="height: 25px;">
<div class="progress-bar {% if choice in leading_choices %}bg-success{% endif %}"
role="progressbar"
style="width: {{ result.proportional_percentage|floatformat:2 }}%;"
aria-valuenow="{{ result.proportional_percentage|floatformat:2 }}"
aria-valuemin="0"
aria-valuemax="100">
<span class="font-weight-bold">{{ result.proportional_percentage|floatformat:2 }}%</span>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p>No proportional voting results available.</p>
{% endif %}
</div>

<!-- Back to Poll Button -->
<div class="text-center mt-4">
{% if 'invitation' in request.META.HTTP_REFERER %}
<a href="{{ request.META.HTTP_REFERER }}" class="btn btn-primary">Back to poll</a>
{% else %}
<a href="{% url 'detail' poll.id %}" class="btn btn-primary">Back to poll</a>
{% endif %}
</div>
{% endblock %}
</div>
{% endblock %}
53 changes: 39 additions & 14 deletions approval_polls/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import datetime

Check failure on line 1 in approval_polls/views.py

View check run for this annotation

Trunk.io / Trunk Check

isort

Incorrect formatting, autoformat by running 'trunk fmt'

Check failure on line 1 in approval_polls/views.py

View check run for this annotation

Trunk.io / Trunk Check

black

Incorrect formatting, autoformat by running 'trunk fmt'
import json
import re

Expand All @@ -8,7 +8,7 @@
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.db.models import Count
from django.db.models import Count, Prefetch
fsargent marked this conversation as resolved.
Show resolved Hide resolved
from django.http import HttpResponseRedirect, HttpResponseServerError
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
Expand All @@ -17,7 +17,7 @@
from django.views import generic
from django.views.decorators.http import require_http_methods

from approval_polls.models import Ballot, Poll, PollTag, Subscription, VoteInvitation
from approval_polls.models import Ballot, Poll, PollTag, Subscription, VoteInvitation, Vote

from .forms import ManageSubscriptionsForm, NewUsernameForm

Expand Down Expand Up @@ -266,7 +266,9 @@
return HttpResponseServerError(f"An error occurred: {str(e)}")


from django.db.models import Count, Prefetch

Check failure on line 269 in approval_polls/views.py

View check run for this annotation

Trunk.io / Trunk Check

ruff(E402)

[new] Module level import not at top of file

Check failure on line 269 in approval_polls/views.py

View check run for this annotation

Trunk.io / Trunk Check

flake8(F811)

[new] redefinition of unused 'Prefetch' from line 11

Check failure on line 269 in approval_polls/views.py

View check run for this annotation

Trunk.io / Trunk Check

ruff(F811)

[new] Redefinition of unused `Count` from line 11

Check failure on line 269 in approval_polls/views.py

View check run for this annotation

Trunk.io / Trunk Check

ruff(F811)

[new] Redefinition of unused `Prefetch` from line 11

Check failure on line 269 in approval_polls/views.py

View check run for this annotation

Trunk.io / Trunk Check

flake8(F811)

[new] redefinition of unused 'Count' from line 11

fsargent marked this conversation as resolved.
Show resolved Hide resolved
class ResultsView(generic.DetailView):

Check failure on line 271 in approval_polls/views.py

View check run for this annotation

Trunk.io / Trunk Check

flake8(E302)

[new] expected 2 blank lines, found 1
model = Poll
template_name = "results.html"

Expand All @@ -277,31 +279,54 @@
context = super().get_context_data(**kwargs)
poll = self.object

# Annotate choices with vote count and order by votes
choices = poll.choice_set.annotate(vote_count=Count("vote")).order_by(
"-vote_count"
)

# Calculate max votes
# Approval voting logic (original)
choices = poll.choice_set.annotate(vote_count=Count("vote")).order_by("-vote_count")
max_votes = choices.first().vote_count if choices.exists() else 0
leading_choices = [choice for choice in choices if choice.vote_count == max_votes]

# Determine leading choices
leading_choices = [
choice for choice in choices if choice.vote_count == max_votes
# Proportional voting logic (separate)
ballots = poll.ballot_set.prefetch_related(
Prefetch("vote_set", queryset=Vote.objects.select_related("choice"))
)
proportional_votes = {choice.id: 0 for choice in poll.choice_set.all()}
total_proportional_votes = 0

for ballot in ballots:
approved_choices = ballot.vote_set.all().values_list("choice_id", flat=True)
num_approved = len(approved_choices)
if num_approved > 0:
weight = 1 / num_approved
for choice_id in approved_choices:
proportional_votes[choice_id] += weight
total_proportional_votes += weight

# Create a separate list for proportional results
proportional_results = [
{
"choice": choice,
"proportional_votes": proportional_votes[choice.id],
"proportional_percentage": (
proportional_votes[choice.id] / total_proportional_votes * 100
if total_proportional_votes > 0
else 0
),
}
for choice in poll.choice_set.all()
]

# Add both approval and proportional data to context
context.update(
{
"choices": choices,
"choices": choices, # Approval results
"leading_choices": leading_choices,
"max_votes": max_votes,
"proportional_results": proportional_results, # Proportional results
"total_proportional_votes": total_proportional_votes,
}
)

return context


@require_http_methods(["POST"])

Check failure on line 329 in approval_polls/views.py

View check run for this annotation

Trunk.io / Trunk Check

flake8(E302)

[new] expected 2 blank lines, found 1
def vote(request, poll_id):
poll = get_object_or_404(Poll, pk=poll_id)

Expand Down
Loading