Skip to content

Commit

Permalink
Merge pull request #10 from the-Bruce/feat/website-summary
Browse files Browse the repository at this point in the history
Feat website summary
  • Loading branch information
adamskrz authored Feb 14, 2024
2 parents 4ddb17c + 608f9b0 commit ea9ff58
Show file tree
Hide file tree
Showing 12 changed files with 305 additions and 53 deletions.
Empty file removed utils/management/__init__.py
Empty file.
Empty file.
18 changes: 0 additions & 18 deletions utils/management/commands/daily.py

This file was deleted.

19 changes: 0 additions & 19 deletions utils/management/commands/forum_migrate.py

This file was deleted.

2 changes: 1 addition & 1 deletion votes/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class STVVoteAdmin(admin.ModelAdmin):


class STVResultAdmin(admin.ModelAdmin):
readonly_fields = ["election", "full_log", "winners", "generated"]
readonly_fields = ["election", "full_log", "winners", "generated", "action_log"]


class ElectionAdmin(admin.ModelAdmin):
Expand Down
17 changes: 17 additions & 0 deletions votes/migrations/0004_stvresult_action_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 3.1.6 on 2023-02-26 18:50

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("votes", "0003_election_archived"),
]

operations = [
migrations.AddField(
model_name="stvresult",
name="action_log",
field=models.JSONField(blank=True, null=True),
),
]
3 changes: 3 additions & 0 deletions votes/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ class Meta:
class STVResult(models.Model):
election = models.OneToOneField(Election, on_delete=models.CASCADE)
full_log = models.TextField()
action_log = models.JSONField(
blank=True, null=True
) # Null for backwards compatability
winners = models.ManyToManyField(Candidate)
generated = models.DateTimeField(auto_now_add=True)

Expand Down
61 changes: 56 additions & 5 deletions votes/stv.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def __init__(self, candidates: Set[int], votes: List[Tuple[int]], seats: int):
self.seats = seats
self.rounds = 0
self.fulllog = []
self.actlog = []
print(candidates, votes, seats)
# Huge initial value
# (surplus should never be this high in our situation (its more votes than there are people in the world))
Expand All @@ -88,15 +89,15 @@ def withdraw(self, candidates: Set[int]):
def round(self):
self.rounds += 1
# B1
shortcircuit = False
electable = []
for candidate in self.candidates:
if candidate.status == States.ELECTED or candidate.status == States.HOPEFUL:
electable.append(candidate)
if len(electable) <= self.seats:
for i in electable:
i.status = States.ELECTED
self._report()
raise StopIteration("Election Finished")
shortcircuit = True

# B2a
wastage = Fraction(0)
Expand All @@ -115,6 +116,12 @@ def round(self):
# B2b
quota = Fraction(sum(scores.values()), self.seats + 1)

if shortcircuit:
# Defer shortcircuit until after scores calculated to log one extra line
self._log(scores, wastage, quota)
self._report()
raise StopIteration("Election Finished")

# B2c
elected = False
for candidate in self.candidates:
Expand All @@ -131,7 +138,7 @@ def round(self):
# B2e
if elected:
self.previous_surplus = surplus
self._log(scores, wastage)
self._log(scores, wastage, quota)
return

if surplus == 0 or surplus >= self.previous_surplus:
Expand All @@ -154,50 +161,94 @@ def round(self):
candidate.keep_factor * quota, scores[candidate]
)
self.previous_surplus = surplus
self._log(scores, wastage)
self._log(scores, wastage, quota)

def _choose(self, candidates):
if len(candidates) > 1:
a = secrets.choice(candidates)[0]
self._addlog("-Tiebreak-")
self._addlog(a)
self._addlog()
self._addaction(
"tiebreak",
{
"round": self.rounds,
"candidates": [str(candidate[0].id) for candidate in candidates],
"choice": str(a.id),
},
)
else:
a = candidates[0][0]
return a

def _addaction(self, type, details):
self.actlog.append({"type": type, "details": details})

def _addlog(self, *args):
string = " ".join(map(str, args))
self.fulllog.append(string)
print(string)

def _log(self, scores, wastage):
def _log(self, scores, wastage, quota):
self._addlog(self.rounds)
self._addlog("======")
candstates = {}
for i in self.candidates:
assert isinstance(i, Candidate)
self._addlog("Candidate:", i.id, i.keep_factor.limit_denominator(1000))
self._addlog("Status:", str(i.status))
self._addlog("Votes:", str(scores[i].limit_denominator(1000)))
self._addlog()
candstates[str(i.id)] = {
"keep_factor": float(i.keep_factor.limit_denominator(1000)),
"status": str(i.status),
"votes": float(scores[i].limit_denominator(1000)),
}
self._addlog("Wastage:", str(wastage.limit_denominator(1000)))
self._addlog("Threshold:", str(quota.limit_denominator(1000)))
self._addlog()

self._addaction(
"round",
{
"round": self.rounds,
"candidates": candstates,
"wastage": float(wastage.limit_denominator(1000)),
"threshold": float(quota.limit_denominator(1000)),
},
)

def _report(self):
self._addlog("**Election Results**")
self._addlog()
candstates = {"ELECTED": [], "DEFEATED": [], "WITHDRAWN": []}
self._addlog("ELECTED")
for i in filter(lambda x: x.status == States.ELECTED, self.candidates):
self._addlog(" Candidate", i.id)
candstates["ELECTED"].append(str(i.id))
self._addlog("DEFEATED")
for i in filter(lambda x: x.status == States.DEFEATED, self.candidates):
self._addlog(" Candidate", i.id)
candstates["DEFEATED"].append(str(i.id))
self._addlog("WITHDRAWN")
for i in filter(lambda x: x.status == States.WITHDRAWN, self.candidates):
self._addlog(" Candidate", i.id)
candstates["WITHDRAWN"].append(str(i.id))
self._addlog()
self._addaction("report", candstates)

def full_election(self):
# Log initial state
scores = {k: Fraction(0) for k in self.candidates}
wastage = Fraction(0)
for vote in self.votes:
if len(vote.prefs) > 0:
scores[vote.prefs[0]] += 1
else:
wastage += 1
quota = Fraction(sum(scores.values()), self.seats + 1)
self._log(scores, wastage, quota)

try:
while True:
self.round()
Expand Down
51 changes: 51 additions & 0 deletions votes/templates/votes/parts/table.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<div class="table-responsive">
<table class="table table-bordered table-sm table-hover">
<thead>
{% for row in table.head %}
<tr>
{% for content,width,style in row %}
{% if style == "header" %}
<th colspan="{{ width }}">{{ content }}</th>
{% else %}
<td colspan="{{ width }}">{{ content }}</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</thead>
<tbody>
{% for row in table.body %}
<tr>
{% for content,width,style in row %}
{% if style == "header" %}
<th colspan="{{ width }}">{{ content }}</th>
{% elif style == "changed" %}
<td colspan="{{ width }}"><strong>{{ content }}</strong></td>
{% elif style == "float" %}
<td colspan="{{ width }}">{{ content|floatformat }}</td>
{% else %}
<td colspan="{{ width }}">{{ content }}</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</tbody>
<tfoot>
{% for row in table.foot %}
<tr>
{% for content,width,style in row %}
{% if style == "header" %}
<th colspan="{{ width }}">{{ content }}</th>
{% elif style == "changed" %}
<td colspan="{{ width }}"><strong>{{ content }}</strong></td>
{% elif style == "float" %}
<td colspan="{{ width }}">{{ content|floatformat }}</td>
{% else %}
<td colspan="{{ width }}">{{ content }}</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</tfoot>
</table>
</div>
33 changes: 26 additions & 7 deletions votes/templates/votes/stv_results.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{% extends "votes/approval_results.html" %}
{% load vote_tags %}
{% block results %}
<h2>Total Turnout: {{ election.stvvote_set.count }}</h2>
<h2>Available seats: {{ election.seats }}</h2>
Expand All @@ -11,17 +12,35 @@ <h2>Winner{{ result.winners.all|pluralize }}</h2>
{% endfor %}
</div>
<h3>Breakdown</h3>
<div class="list-group mb-2">
{% for i in election.candidate_set.all|dictsort:"id" %}
<p class="list-group-item"><strong class="d-inline-block align-middle mr-2" style="width: 2rem">{{ i.id }}</strong><span class="d-inline-block align-middle">{{ i.name }}</span></p>
{% endfor %}

{% vote_breakdown_table election as vote_summary %}
{% if vote_summary %}
{% include "votes/parts/table.html" with table=vote_summary %}
{% else %}
<div class="card mb-2">
<div class="card-body text-muted">Unable to generate summary</div>
</div>
{% endif %}
<h3>Detailed Log</h3>
<button class="btn btn-primary " data-toggle="collapse" data-target="#detaillog" aria-expanded="false"
aria-controls="detaillog">Show / Hide
</button>
<div class="collapse" id="detaillog">
<ul class="list-group mb-2 mt-2">
{% for i in election.candidate_set.all|dictsort:"id" %}
<li class="list-group-item"><strong class="d-inline-block align-middle mr-2"
style="width: 2rem">{{ i.id }}</strong><span
class="d-inline-block align-middle">{{ i.name }}</span></li>
{% endfor %}
</ul>
<pre class="mb-0">{{ result.full_log }}</pre>
</div>
<pre>{{ result.full_log }}</pre>
{% endblock %}

{% block leftcontents %}
{{ block.super }}
{% if perms.votes.change_results %}
<a class="btn btn-block btn-outline-dark mb-3" href="{% url "admin:votes_stvresult_change" result.id %}">Edit Result</a>
<a class="btn btn-block btn-outline-dark mb-3" href="{% url "admin:votes_stvresult_change" result.id %}">Edit
Result</a>
{% endif %}
{% endblock %}
{% endblock %}
Loading

0 comments on commit ea9ff58

Please sign in to comment.