diff --git a/.github/workflows/fly-review.yml b/.github/workflows/fly-review.yml
index db3f246..b08e4e3 100644
--- a/.github/workflows/fly-review.yml
+++ b/.github/workflows/fly-review.yml
@@ -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:
@@ -36,4 +36,6 @@ jobs:
id: deploy
uses: superfly/fly-pr-review-apps@1.2.1
with:
- secrets: DEBUG=true
+ name: vote-es-pr-${{ github.event.number }}
+ config: fly.pr-review.toml
+ secrets: DJANGO_SETTINGS_MODULE=approval_polls.settings
diff --git a/approval_polls/settings.py b/approval_polls/settings.py
index cc83875..c1d4145 100644
--- a/approval_polls/settings.py
+++ b/approval_polls/settings.py
@@ -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"
@@ -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:
diff --git a/approval_polls/templates/results.html b/approval_polls/templates/results.html
index 7f7f699..1fbe713 100644
--- a/approval_polls/templates/results.html
+++ b/approval_polls/templates/results.html
@@ -9,6 +9,7 @@
{% if poll.is_closed and poll.total_votes == 0 %}No votes in this poll{% endif %}
+
{% for choice in choices %}
@@ -29,9 +30,7 @@
-
-
+
{% endif %}
@@ -47,6 +46,56 @@
+
+
+
+
+
+
+ When electing multiple candidates to a board or committee Proportional Approval Voting ensures that no single voting group dominates the outcome, promoting fair representation and reflecting the diverse preferences of all voters.
+
+
+
+
+
+
+
+
+ Choice |
+ Proportional Votes |
+ Percentage |
+
+
+
+ {% for result in proportional_results %}
+
+ {{ result.choice_text }} |
+ {{ result.proportional_votes|floatformat:2 }} |
+ {{ result.proportional_percentage|floatformat:2 }}% |
+
+ {% endfor %}
+
+
+
+
+
+
+
+ {% endif %}
+
{% if 'invitation' in request.META.HTTP_REFERER %}
Back to poll
@@ -55,4 +104,41 @@
+
{% endblock %}
diff --git a/approval_polls/views.py b/approval_polls/views.py
index 9e970f4..2dc6ff9 100644
--- a/approval_polls/views.py
+++ b/approval_polls/views.py
@@ -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
@@ -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
@@ -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
diff --git a/fly.pr-review.toml b/fly.pr-review.toml
new file mode 100644
index 0000000..cd1893f
--- /dev/null
+++ b/fly.pr-review.toml
@@ -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"