Skip to content

Commit

Permalink
Add proportional to results page (#218)
Browse files Browse the repository at this point in the history
* Add proportional to results page

* Modify template.

* Trunk formatting. Fix little nits.

* Fixed styling. LGTM

* Trunk formatting. Fix little nits.

* Sanitize branchnames for deployment

* Sanitize branchnames for deployment

* Change url name

* change fly review specs and have them autosuspend.

* Set django settings module

* Set django settings module

* Fix fly allowed hosts issue

* Add fly url to CORS CSRF whitelists.

* Add https to origins

* Finally think we're ready for review.
  • Loading branch information
fsargent authored Jan 23, 2025
1 parent e9b46d2 commit 8e0cd7d
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 17 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/fly-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
# Set these to your Fly.io organization and preferred region.
FLY_REGION: iad
FLY_REGION: sjc
FLY_ORG: personal

jobs:
Expand All @@ -36,4 +36,6 @@ jobs:
id: deploy
uses: superfly/[email protected]
with:
secrets: DEBUG=true
name: vote-es-pr-${{ github.event.number }}
config: fly.pr-review.toml
secrets: DJANGO_SETTINGS_MODULE=approval_polls.settings
18 changes: 15 additions & 3 deletions approval_polls/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,14 @@
# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts
APP_NAME = env("FLY_APP_NAME", str, "")
ALLOWED_HOSTS = [f"{APP_NAME}.fly.dev", "vote.electionscience.org"] # ← Updated!
print("Allowed Hosts: ", ALLOWED_HOSTS)
print("APP_NAME: ", APP_NAME)

if DEBUG:
db_path = os.path.join(BASE_DIR, "db.sqlite3")
ALLOWED_HOSTS.extend(["localhost", "0.0.0.0", "127.0.0.1"]) # trunk-ignore(bandit)


if not DEBUG:
COMPRESS_OFFLINE = True
LIBSASS_OUTPUT_STYLE = "compressed"
Expand All @@ -49,9 +52,18 @@
# We recommend adjusting this value in production.
profiles_sample_rate=1.0,
)
CSRF_TRUSTED_ORIGINS = ["https://vote.electionscience.org"]
CSRF_ALLOWED_ORIGINS = ["https://vote.electionscience.org"]
CORS_ORIGINS_WHITELIST = ["https://vote.electionscience.org"]
CSRF_TRUSTED_ORIGINS = [
"https://vote.electionscience.org",
f"https://{APP_NAME}.fly.dev",
]
CSRF_ALLOWED_ORIGINS = [
"https://vote.electionscience.org",
f"https://{APP_NAME}.fly.dev",
]
CORS_ORIGINS_WHITELIST = [
"https://vote.electionscience.org",
f"https://{APP_NAME}.fly.dev",
]


if "test" in sys.argv or "pytest" in sys.argv:
Expand Down
92 changes: 89 additions & 3 deletions approval_polls/templates/results.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ <h2 class="h4 text-muted">
{% 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 %}">
Expand All @@ -29,9 +30,7 @@ <h3 class="h5 {% if choice in leading_choices %}text-success font-weight-bold{%
{{ 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>
<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;">
Expand All @@ -47,6 +46,56 @@ <h3 class="h5 {% if choice in leading_choices %}text-success font-weight-bold{%
</div>
{% endfor %}
</div>
<!-- Proportional Voting Results Section -->
{% if proportional_results and poll.total_votes != 0 %}
<div class="mt-5">
<div class="accordion" id="proportionalResultsAccordion">
<div class="accordion-item">
<h2 class="accordion-header" id="headingProportional">
<button class="accordion-button collapsed"
type="button"
data-bs-toggle="collapse"
data-bs-target="#collapseProportional"
aria-expanded="false"
aria-controls="collapseProportional">View Proportional Voting Results</button>
</h2>
<div id="collapseProportional"
class="accordion-collapse collapse"
aria-labelledby="headingProportional"
data-bs-parent="#proportionalResultsAccordion">
<div class="accordion-body">
<p>
When electing multiple candidates to a board or committee <a href="https://en.wikipedia.org/wiki/Sequential_proportional_approval_voting">Proportional Approval Voting</a> ensures that no single voting group dominates the outcome, promoting fair representation and reflecting the diverse preferences of all voters.
</p>
<!-- Pie Chart Container -->
<div style="width: 50%; max-width: 400px; margin: 20px auto;">
<canvas id="proportionalChart"></canvas>
</div>
<table class="table table-bordered">
<thead>
<tr>
<th>Choice</th>
<th>Proportional Votes</th>
<th>Percentage</th>
</tr>
</thead>
<tbody>
{% for result in proportional_results %}
<tr>
<td>{{ result.choice_text }}</td>
<td>{{ result.proportional_votes|floatformat:2 }}</td>
<td>{{ result.proportional_percentage|floatformat:2 }}%</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<!-- 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>
Expand All @@ -55,4 +104,41 @@ <h3 class="h5 {% if choice in leading_choices %}text-success font-weight-bold{%
{% endif %}
</div>
</div>
<!-- JavaScript for Pie Chart -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const ctx = document.getElementById('proportionalChart').getContext('2d');
const data = {
labels: [
{% for result in proportional_results %}"{{ result.choice_text }}",{% endfor %}
],
datasets: [{
data: [
{% for result in proportional_results %}{{ result.proportional_percentage|floatformat:2 }},{% endfor %}
],
backgroundColor: [
'rgba(75, 192, 192, 0.2)',
'rgba(255, 99, 132, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(255, 159, 64, 0.2)'
],
borderColor: [
'rgba(75, 192, 192, 1)',
'rgba(255, 99, 132, 1)',
'rgba(255, 206, 86, 1)',
'rgba(54, 162, 235, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)'
],
borderWidth: 1
}]
};
const config = {
type: 'pie',
data: data,
};
const proportionalChart = new Chart(ctx, config);
</script>
{% endblock %}
52 changes: 43 additions & 9 deletions approval_polls/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
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,14 @@
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,
Vote,
VoteInvitation,
)

from .forms import ManageSubscriptionsForm, NewUsernameForm

Expand Down Expand Up @@ -277,27 +284,54 @@ def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
poll = self.object

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

# Calculate max votes
max_votes = choices.first().vote_count if choices.exists() else 0

# Determine leading choices
leading_choices = [
choice for choice in choices if choice.vote_count == max_votes
]

# Proportional voting logic
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

proportional_results = [
{
"choice_text": choice.choice_text,
"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 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


Expand Down
33 changes: 33 additions & 0 deletions fly.pr-review.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# fly.toml app configuration file generated for vote-electionscience-org on 2024-01-27T11:17:40-08:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#

app = 'vote-electionscience-org'
primary_region = 'sjc'
console_command = '/code/manage.py shell'

[build]

[env]
PORT = '8000'

[http_service]
auto_stop_machines = "suspend"
internal_port = 8000
force_https = true
auto_start_machines = true
min_machines_running = 0
processes = ['app']

[[vm]]
kind = 'shared-cpu-1x'
memory_mb = 256

[[statics]]
guest_path = '/code/static'
url_prefix = '/static/'

[mounts]
source = "litefs"
destination = "/data"

0 comments on commit 8e0cd7d

Please sign in to comment.