diff --git a/management/consumers.py b/management/consumers.py index 3c662b4f94f4d954f2b3bd9ee1a7d5ed2390ced8..49530b092ba7d10b50cbfe1a40199fb74ec63f6b 100644 --- a/management/consumers.py +++ b/management/consumers.py @@ -15,7 +15,7 @@ class ElectionConsumer(AsyncWebsocketConsumer): async def send_reload(self, event): await self.send(text_data=json.dumps({ - 'reload': True, + 'reload': event['id'], })) @@ -24,7 +24,8 @@ class SessionConsumer(AsyncWebsocketConsumer): async def connect(self): session = self.scope['url_route']['kwargs']['pk'] # reload if a new voter logged in, or if a election of the session was changed (like added) - self.groups = ["Login-Session-" + session, "Session-" + session] # pylint: disable=W0201 + self.groups = ["Login-Session-" + session, "Session-" + session, + "SessionAlert-" + session] # pylint: disable=W0201 for group in self.groups: await self.channel_layer.group_add(group, self.channel_name) await self.accept() @@ -35,5 +36,15 @@ class SessionConsumer(AsyncWebsocketConsumer): async def send_reload(self, event): await self.send(text_data=json.dumps({ - 'reload': True, + 'reload': event['id'], + })) + + async def send_alert(self, event): + await self.send(text_data=json.dumps({ + 'alert': {'title': event.get('title', 'Alert'), 'msg': event['msg'], 'reload': event.get('reload')}, + })) + + async def send_succ(self, event): + await self.send(text_data=json.dumps({ + 'succ': event['msg'], })) diff --git a/management/forms.py b/management/forms.py index b641bb9e1a6cb0e1ffdd6fe01d2bf5e964d2d808..5b4ffad0ebfaef2139afaf724075e5411e477378 100644 --- a/management/forms.py +++ b/management/forms.py @@ -309,14 +309,11 @@ class AddVotersForm(forms.Form): self.session = session def save(self) -> List[Tuple[Voter, str]]: - voters = [ + voters_codes = [ Voter.from_data(email=email, session=self.session) for email in self.cleaned_data['voters_list'] ] - - for voter, code in voters: - voter.send_invitation(code, self.session.managers.all().first().sender_email) - - return voters + self.session.managers.all().first().send_invite_bulk_threaded(voters_codes) + return voters_codes def clean_voters_list(self): lines = self.cleaned_data['voters_list'].splitlines() @@ -393,6 +390,8 @@ class CSVUploaderForm(forms.Form): return data def save(self): - for email, name in self.cleaned_data['csv_data'].items(): - voter, code = Voter.from_data(session=self.session, email=email, name=name) - voter.send_invitation(code, self.session.managers.all().first().sender_email) + voters_codes = [ + Voter.from_data(session=self.session, email=email, name=name) + for email, name in self.cleaned_data['csv_data'].items() + ] + self.session.managers.all().first().send_invite_bulk_threaded(voters_codes) diff --git a/management/models.py b/management/models.py index 6c30731dbf3405d0cdea6c712aa0f73adba36640..66dab86ee4d55849c5674e36467508389a0faf07 100644 --- a/management/models.py +++ b/management/models.py @@ -1,9 +1,15 @@ +import threading +import time +from typing import List, Tuple + +from asgiref.sync import async_to_sync +from channels.layers import get_channel_layer from django.conf import settings from django.contrib.auth.base_user import AbstractBaseUser from django.db import models from management.utils import is_valid_sender_email -from vote.models import Session, Election +from vote.models import Session, Election, Voter class ElectionManager(AbstractBaseUser): @@ -30,3 +36,47 @@ class ElectionManager(AbstractBaseUser): def get_election(self, pk): return Election.objects.filter(session__in=self.sessions).filter(pk=pk).first() + + def send_invite_bulk_threaded(self, voters_codes: List[Tuple[Voter, str]]): + def runner(): + failed_voters = list(filter(lambda i: i[0] is not None, + (voter.send_invitation(code, self.sender_email) for voter, code in + voters_codes))) + + def wait_heuristic(): + # heuristic sleep to avoid a channel message before the manager's websocket reconnected + # after x send emails this sleep should be unnecessary + if len(failed_voters) < 10: + time.sleep(1) + + group = "SessionAlert-" + str(voters_codes[0][0].session.pk) + if len(failed_voters) == 0: + wait_heuristic() + # send message that tells the manager that all emails have been sent successfully + async_to_sync(get_channel_layer().group_send)( + group, + {'type': 'send_succ', 'msg': "Emails send successfully!"} + ) + return + failed_emails_str = "".join( + [f"<tr><td>{voter.email}</td><td>{e}</td></tr>" for voter, e in failed_voters]) + + msg = 'The following email addresses failed to send and thus are probably unassigned addresses. ' \ + 'Please check them again on correctness.<table class="width100"><tr><th>Email</th>' \ + '<th>Error</th></tr>{}</table>' + + # delete voters where the email could not be sent + for voter, _ in failed_voters: + voter.invalid_email = True + voter.save() # may trigger a lot of automatic reloads + wait_heuristic() + async_to_sync(get_channel_layer().group_send)( + group, + {'type': 'send_alert', 'msg': msg.format(failed_emails_str), 'title': 'Error during email sending', + 'reload': '#voterCard'} + ) + + thread = threading.Thread( + target=runner, + args=()) + thread.start() diff --git a/management/static/management/css/style.css b/management/static/management/css/style.css index 5d48b7460eff5b4e0ca8b3756b1b2cc6826776c6..d3bc3b88731195a6ed87d7d43830b1391715881f 100644 --- a/management/static/management/css/style.css +++ b/management/static/management/css/style.css @@ -28,6 +28,14 @@ height: 100% } +.width100 { + width: 100% +} + +.hide{ + display: none +} + .myclose { padding: 0; background-color: transparent; diff --git a/management/templates/management/base.html b/management/templates/management/base.html index 92ffa6504b3fef8bde1a579d9d29ade90f346dff..72437dc6c233756ab823b78c9b7c878245ce0f0e 100644 --- a/management/templates/management/base.html +++ b/management/templates/management/base.html @@ -43,6 +43,22 @@ </div> </nav> + <div class="modal fade" id="alertModal" role="dialog"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h4 class="modal-title" id="alertModalTitle">Alert</h4> + <button type="button" class="close" data-dismiss="modal">×</button> + </div> + <div class="modal-body" id="alertModalBody"> + <p>text</p> + </div> + <div class="modal-footer"> + </div> + </div> + </div> + </div> + <article class="container-md mb-4" role="main"> <div class="row pt-3 justify-content-center"> <div class="col-12"> @@ -60,6 +76,14 @@ {% endfor %} </div> {% endif %} + <div class="messages"> + <div class="alert alert-dismissible fade show alert-success hide" role="alert" id="message-success"> + <div>some text</div> + <button type="button" class="close" data-dismiss="alert" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + </div> </div> </div> {% block content_pre %}{% endblock %} diff --git a/management/templates/management/election.html b/management/templates/management/election.html index ffcbe4b110b1d671a7b8c72b8e4709c1fe9824fa..f695a61e8f8f9963a7d19e7dd93e67bc1660a26d 100644 --- a/management/templates/management/election.html +++ b/management/templates/management/election.html @@ -134,7 +134,7 @@ </div> </div> <div class="card shadow mt-4"> - <div class="card-body"> + <div class="card-body" id="votes"> <h4>Voters</h4> <hr> <table class="table"> diff --git a/management/templates/management/results.html b/management/templates/management/results.html index d5fad3d589519f8403c4387824f189aadb1bf58b..293956cb7ba0815701c14c119131027f48be0f09 100644 --- a/management/templates/management/results.html +++ b/management/templates/management/results.html @@ -3,7 +3,7 @@ <thead> <tr> <th scope="col">#</th> - <th scope="col">Applicant</th> + <th scope="col">{% if election.voters_self_apply %}Applicant{% else %}Option{% endif %}</th> <th scope="col">Yes</th> <th scope="col">No</th> {% if not election.disable_abstention %} diff --git a/management/templates/management/session.html b/management/templates/management/session.html index 5d4ecd582b75ccffbb1e102bab12c5050a00f97c..0a23bafbb91b6ef83c9b0d720bda3dd2d32a372a 100644 --- a/management/templates/management/session.html +++ b/management/templates/management/session.html @@ -4,7 +4,7 @@ {% block content %} <div class="row justify-content-center"> <div class="col-12"> - <div class="card shadow "> + <div class="card shadow"> <div class="card-header bg-dark text-light"> <h4 class="d-inline">{{ session.title }}</h4> <div class="d-inline float-right dropdown ml-2"> @@ -33,7 +33,7 @@ <small>Starts on {{ session.start_date }}</small> {% endif %} </div> - <div class="card-body pt-0"> + <div class="card-body pt-0" id="electionCard"> <div class="list-group"> {% if not existing_elections %} <div class="list-group-item mt-3"> @@ -96,21 +96,24 @@ </div> </div> </div> - <div class="card-body"> - <div class="list-group list-group-flush"> - {% if voters %} - <div class="list-group-item"> - <span class="w-25 font-weight-bold">E-Mail</span> - <span data-toggle="tooltip" data-placement="right" - title="Mail addresses turn green, as soon as their invite link has been clicked."> + {# The following div is only needed to update the voter's list#} + <div id="voterCard"> + <div class="card-body"> + <div class="list-group list-group-flush"> + {% if voters %} + <div class="list-group-item"> + <span class="w-25 font-weight-bold">E-Mail</span> + <span data-toggle="tooltip" data-placement="right" + title="Mail addresses turn green, as soon as their invite link has been clicked. + Mail addresses are red, if the address is invalid and no email was sent."> <img class="pl-1 pb-1" src="{% static "img/question-circle.svg" %}" height="25pt" alt="[?]"> </span> - </div> - <div class="voter-table"> - {% for voter in voters %} - <div class="list-group-item"> - <span class="w-25 {% if voter.logged_in %}text-success{% endif %}"> {{ voter }}</span> - <span class="float-right"> + </div> + <div class="voter-table"> + {% for voter in voters %} + <div class="list-group-item"> + <span class="w-25 {% if voter.logged_in %}text-success{% elif voter.invalid_email %}text-danger{% endif %}"> {{ voter }}</span> + <span class="float-right"> <form action="{% url 'management:delete_voter' voter.pk %}" method="post"> {% csrf_token %} <button type="submit" class="close btn btn-danger" aria-label="remove voter"> @@ -118,14 +121,15 @@ </button> </form> </span> - </div> - {% endfor %} - </div> - {% else %} - <div class="list-group-item"> - <span>No voters were added yet</span> - </div> - {% endif %} + </div> + {% endfor %} + </div> + {% else %} + <div class="list-group-item"> + <span>No voters were added yet</span> + </div> + {% endif %} + </div> </div> </div> </div> diff --git a/requirements.txt b/requirements.txt index 5e827c6837b55a87d58f24a429d765c4fdae0939..ca2e66755f589a1bff67cbc1c42a36bcb34763ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ django-auth-ldap==2.2.* qrcode==6.1 latex==0.7.* django_prometheus==2.1.* -channels==3.0.* \ No newline at end of file +channels==3.0.* +channels-redis==3.2.* \ No newline at end of file diff --git a/requirements_dev.txt b/requirements_dev.txt index c28317fef46fee4da801642ac9b798736bf7da00..a93b314e63d0c40239e8e0f65d54d6c609b513f0 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,4 +1,4 @@ bandit==1.6.2 -pylint +pylint==2.7.* pylint-django~=2.3.0 django-stubs diff --git a/static/js/reload.js b/static/js/reload.js index 21e9c0eaf6be7358d75e93ed911fe129b71c13eb..34526a90255000ece1816a71368d516b4a03d644 100644 --- a/static/js/reload.js +++ b/static/js/reload.js @@ -5,10 +5,9 @@ $(document).ready(() => { setup_date_reload(); } - function reload() { - console.log("Reloading") - //wait a random time, to not overload the server if everyone reloads at the same time - window.setTimeout(() => $("#content").load(location.pathname + " #content", reload_callback), Math.random() * 1000) + function reload(reload_id="#content") { + console.log("Reloading " + reload_id) + $(reload_id).load(location.pathname + " " + reload_id, reload_callback) } function setup_date_reload() { @@ -16,11 +15,11 @@ $(document).ready(() => { clearTimeout(timeout); const now_ms = new Date().getTime(); const times = $(".time").text().split('|').map(u_time => parseInt(u_time)); - const wait_ms = times.map(time => (time + 5) * 1000 - now_ms).filter(t => t > 10 * 1000); + const wait_ms = times.map(time => (time + 1) * 1000 - now_ms).filter(t => t > 1000); const min_ms = Math.min(...wait_ms); if (min_ms < 24 * 60 * 60 * 1000) { console.log("Reloading in " + (min_ms / 1000) + "s"); - timeout = setTimeout(reload, min_ms); + timeout = setTimeout(reload.bind(this, '#electionCard'), min_ms); } } @@ -29,7 +28,19 @@ $(document).ready(() => { ws.onmessage = function (e) { const message = JSON.parse(e.data) if (message.reload) { - reload(); + reload(message.reload); + }else if (message.alert){ + if (message.alert.reload) + // we want to reload the voters because the list might be outdated due to the deletion of + // voters (optional idea: mark the invalid voters red) + reload(message.alert.reload); + $('#alertModalBody').find('p').html(message.alert.msg); + $('#alertModalTitle').html(message.alert.title); + $('#alertModal').modal('show'); + }else if (message.succ){ + let succ_div = $('#message-success'); + succ_div.find('div').html(message.succ); + succ_div.toggleClass('hide'); } } ws.onopen = function (e) { @@ -42,7 +53,9 @@ $(document).ready(() => { console.error("Websocket Closed. Site will not reload automatically"); } } - + //$('#alertModal').on('hidden.bs.modal', function (e) { + // reload(); + //}); setup_date_reload(); setup_websocket(); }) diff --git a/vote/consumers.py b/vote/consumers.py index 628fbfa62b79d011f2326bdd133c35203730e1d2..0dc4b9a22c1f24e2687f2658cd312588034aa4a6 100644 --- a/vote/consumers.py +++ b/vote/consumers.py @@ -18,7 +18,7 @@ class VoteConsumer(AsyncWebsocketConsumer): async def send_reload(self, event): await self.send(text_data=json.dumps({ - 'reload': True, + 'reload': event['id'], })) def get_session_key(self): diff --git a/vote/forms.py b/vote/forms.py index 7d767f1b4f152367dca236a2ab262a0ac0accfd8..1c3f878265bb8895c94c4ca8df69e73c176f99de 100644 --- a/vote/forms.py +++ b/vote/forms.py @@ -128,7 +128,7 @@ class VoteForm(forms.Form): group = "Election-" + str(self.election.pk) async_to_sync(get_channel_layer().group_send)( group, - {'type': 'send_reload'} + {'type': 'send_reload', 'id': '#votes'} ) return votes diff --git a/vote/migrations/0026_voter_invalid_email.py b/vote/migrations/0026_voter_invalid_email.py new file mode 100644 index 0000000000000000000000000000000000000000..f8daf92b0cd494dc45c04f591d357813e11003cb --- /dev/null +++ b/vote/migrations/0026_voter_invalid_email.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.8 on 2021-05-01 16:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('vote', '0025_session_spectator_token'), + ] + + operations = [ + migrations.AddField( + model_name='voter', + name='invalid_email', + field=models.BooleanField(default=False), + ), + ] diff --git a/vote/models.py b/vote/models.py index a142f0b574e11f390df9449fcc1f86eacc82d645..85151aad8e46d310727ef071ae85f5e83fb69cb3 100644 --- a/vote/models.py +++ b/vote/models.py @@ -6,6 +6,7 @@ from argparse import Namespace from datetime import datetime from functools import partial from io import BytesIO +from typing import Tuple, Optional import PIL from PIL import Image @@ -173,7 +174,7 @@ class Election(models.Model): group = "Session-" + str(self.session.pk) async_to_sync(get_channel_layer().group_send)( group, - {'type': 'send_reload'} + {'type': 'send_reload', 'id': '#electionCard'} ) def __str__(self): @@ -184,6 +185,7 @@ class Voter(models.Model): voter_id = models.AutoField(primary_key=True) password = models.CharField(max_length=256) email = models.EmailField(null=True, blank=True) + invalid_email = models.BooleanField(default=False) session = models.ForeignKey(Session, related_name='participants', on_delete=models.CASCADE) logged_in = models.BooleanField(default=False) name = models.CharField(max_length=256, blank=True, null=True) @@ -216,7 +218,7 @@ class Voter(models.Model): group = "Login-Session-" + str(self.session.pk) async_to_sync(get_channel_layer().group_send)( group, - {'type': 'send_reload'} + {'type': 'send_reload', 'id': '#voterCard'} ) def set_password(self, raw_password=None): @@ -267,10 +269,15 @@ class Voter(models.Model): email = email_name + '@' + domain_part.lower() return email - def email_user(self, subject, message, from_email=None, **kwargs): + def email_user(self, subject, message, from_email=None, **kwargs) -> Tuple[Optional['Voter'], Optional[str]]: """Send an email to this user.""" if self.email is not None: - send_mail(subject, message, from_email, [self.email], **kwargs) + try: + send_mail(subject, message, from_email, [self.email], **kwargs) + except Exception as e: # pylint: disable=W0703 + return self, str(e) + # None means everything is ok + return None, None @property def is_authenticated(self): @@ -301,8 +308,8 @@ class Voter(models.Model): def get_username(self): return str(self) - @classmethod - def send_test_invitation(cls, title: str, invite_text: str, start_date: datetime, meeting_link: str, to_email: str, + @staticmethod + def send_test_invitation(title: str, invite_text: str, start_date: datetime, meeting_link: str, to_email: str, from_email: str): test_session = Namespace(**{ "title": title, @@ -320,9 +327,9 @@ class Voter(models.Model): Voter.send_invitation(test_voter, "mock-up-access-token", from_email) - def send_invitation(self, access_code: str, from_email: str): + def send_invitation(self, access_code: str, from_email: str) -> Tuple[Optional['Voter'], Optional[str]]: if not self.email: - return + return None, None subject = f'Invitation for {self.session.title}' if self.session.invite_text: if self.session.start_date: @@ -354,12 +361,12 @@ class Voter(models.Model): } body_html = render_to_string('vote/mails/invitation.j2', context=context) - self.email_user( + return self.email_user( subject=subject, message=strip_tags(body_html), from_email=from_email, html_message=body_html.replace('\n', '<br/>'), - fail_silently=True + fail_silently=False ) def send_reminder(self, from_email: str, election): @@ -421,7 +428,7 @@ class Voter(models.Model): return voter_id, password @classmethod - def from_data(cls, session, email=None, name=None): + def from_data(cls, session, email=None, name=None) -> Tuple['Voter', str]: voter = Voter( session=session, email=email, @@ -439,6 +446,7 @@ class Voter(models.Model): def new_access_token(self): password = self.set_password() + self.logged_in = False self.save() return self.get_access_code(self, password) diff --git a/vote/templates/vote/index.html b/vote/templates/vote/index.html index df56c2748eafdac5f73e1abf03e55433ec9eac91..08ec37e7a9f08046faeec35f13f9272a18a2559d 100644 --- a/vote/templates/vote/index.html +++ b/vote/templates/vote/index.html @@ -13,54 +13,56 @@ {% endif %} </div> </div> - {% if open_elections %} - <div class="card shadow mb-2"> - <div class="card-header"> - <h4>Open Elections</h4> + <div id="electionCard"> + {% if open_elections %} + <div class="card shadow mb-2"> + <div class="card-header"> + <h4>Open Elections</h4> + </div> + <div class="card-body"> + {% for election, can_vote, edit in open_elections %} + {% include 'vote/index_election_item.html' %} + {% endfor %} + </div> </div> - <div class="card-body"> - {% for election, can_vote, edit in open_elections %} - {% include 'vote/index_election_item.html' %} - {% endfor %} + {% endif %} + {% if upcoming_elections %} + <div class="card shadow mb-2"> + <div class="card-header"> + <h4>Upcoming Elections</h4> + </div> + <div class="card-body"> + {% for election, can_vote, edit in upcoming_elections %} + {% include 'vote/index_election_item.html' %} + {% endfor %} + </div> </div> - </div> - {% endif %} - {% if upcoming_elections %} - <div class="card shadow mb-2"> - <div class="card-header"> - <h4>Upcoming Elections</h4> - </div> - <div class="card-body"> - {% for election, can_vote, edit in upcoming_elections %} - {% include 'vote/index_election_item.html' %} - {% endfor %} + {% endif %} + {% if published_elections %} + <div class="card shadow mb-2"> + <div class="card-header"> + <h4>Published Results</h4> + </div> + <div class="card-body"> + {% for election, can_vote, edit in published_elections %} + {% include 'vote/index_election_item.html' %} + {% endfor %} + </div> </div> - </div> - {% endif %} - {% if published_elections %} - <div class="card shadow mb-2"> - <div class="card-header"> - <h4>Published Results</h4> + {% endif %} + {% if closed_elections %} + <div class="card shadow mb-2"> + <div class="card-header"> + <h4>Closed Elections</h4> + </div> + <div class="card-body"> + {% for election, can_vote, edit in closed_elections %} + {% include 'vote/index_election_item.html' %} + {% endfor %} + </div> </div> - <div class="card-body"> - {% for election, can_vote, edit in published_elections %} - {% include 'vote/index_election_item.html' %} - {% endfor %} - </div> - </div> - {% endif %} - {% if closed_elections %} - <div class="card shadow mb-2"> - <div class="card-header"> - <h4>Closed Elections</h4> - </div> - <div class="card-body"> - {% for election, can_vote, edit in closed_elections %} - {% include 'vote/index_election_item.html' %} - {% endfor %} - </div> - </div> - {% endif %} + {% endif %} + </div> </div> </div> {% endblock %} @@ -69,7 +71,7 @@ {# Automatic reload of the page: #} {# - either if the start date / end date of a election is due#} {# - or if the admin started / stopped one election and the page is notified with a websocket#} - <script src="{% static "js/jquery-3.5.1.slim.min.js" %}" - integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"></script> + <script src="{% static "js/jquery-3.5.1.min.js" %}" + integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="></script> <script src="{% static "js/reload.js" %}"></script> {% endblock %} diff --git a/vote/templates/vote/index_election_item.html b/vote/templates/vote/index_election_item.html index d9ceaf28df2b99941e38912f6f77ba9da57d2d06..442113ab1b82247ada2d71caf77daf010dc13251 100644 --- a/vote/templates/vote/index_election_item.html +++ b/vote/templates/vote/index_election_item.html @@ -32,7 +32,7 @@ <thead class="thead-dark"> <tr> <th scope="col">#</th> - <th scope="col">Applicant</th> + <th scope="col">{% if election.voters_self_apply %}Applicant{% else %}Option{% endif %}</th> <th scope="col">Yes</th> <th scope="col">No</th> <th scope="col">Abstention</th> diff --git a/vote/templates/vote/spectator.html b/vote/templates/vote/spectator.html index 6b352bde4a70d63846ef909a01c61cc2a15f97b3..a77fc3da78d963de9cf2062454e64a5e2458714d 100644 --- a/vote/templates/vote/spectator.html +++ b/vote/templates/vote/spectator.html @@ -14,54 +14,56 @@ {% endif %} </div> </div> - {% if open_elections %} - <div class="card shadow mb-2"> - <div class="card-header"> - <h4>Open Elections</h4> + <div id="electionCard"> + {% if open_elections %} + <div class="card shadow mb-2"> + <div class="card-header"> + <h4>Open Elections</h4> + </div> + <div class="card-body"> + {% for election in open_elections %} + {% include 'vote/spectator_election_item.html' %} + {% endfor %} + </div> </div> - <div class="card-body"> - {% for election in open_elections %} - {% include 'vote/spectator_election_item.html' %} - {% endfor %} + {% endif %} + {% if upcoming_elections %} + <div class="card shadow mb-2"> + <div class="card-header"> + <h4>Upcoming Elections</h4> + </div> + <div class="card-body"> + {% for election in upcoming_elections %} + {% include 'vote/spectator_election_item.html' %} + {% endfor %} + </div> </div> - </div> - {% endif %} - {% if upcoming_elections %} - <div class="card shadow mb-2"> - <div class="card-header"> - <h4>Upcoming Elections</h4> - </div> - <div class="card-body"> - {% for election in upcoming_elections %} - {% include 'vote/spectator_election_item.html' %} - {% endfor %} + {% endif %} + {% if published_elections %} + <div class="card shadow mb-2"> + <div class="card-header"> + <h4>Published Results</h4> + </div> + <div class="card-body"> + {% for election in published_elections %} + {% include 'vote/spectator_election_item.html' %} + {% endfor %} + </div> </div> - </div> - {% endif %} - {% if published_elections %} - <div class="card shadow mb-2"> - <div class="card-header"> - <h4>Published Results</h4> + {% endif %} + {% if closed_elections %} + <div class="card shadow mb-2"> + <div class="card-header"> + <h4>Closed Elections</h4> + </div> + <div class="card-body"> + {% for election in closed_elections %} + {% include 'vote/spectator_election_item.html' %} + {% endfor %} + </div> </div> - <div class="card-body"> - {% for election in published_elections %} - {% include 'vote/spectator_election_item.html' %} - {% endfor %} - </div> - </div> - {% endif %} - {% if closed_elections %} - <div class="card shadow mb-2"> - <div class="card-header"> - <h4>Closed Elections</h4> - </div> - <div class="card-body"> - {% for election in closed_elections %} - {% include 'vote/spectator_election_item.html' %} - {% endfor %} - </div> - </div> - {% endif %} + {% endif %} + </div> </div> </div> {% endblock %} @@ -70,7 +72,7 @@ {# Automatic reload of the page: #} {# - either if the start date / end date of a election is due#} {# - or if the admin started / stopped one election and the page is notified with a websocket#} - <script src="{% static "js/jquery-3.5.1.slim.min.js" %}" - integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"></script> + <script src="{% static "js/jquery-3.5.1.min.js" %}" + integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="></script> <script src="{% static "js/reload.js" %}"></script> {% endblock %} diff --git a/vote/templates/vote/spectator_election_item.html b/vote/templates/vote/spectator_election_item.html index 538b2943dc1463af9939a01f07bb0f87aae23c6d..1559961fe0dd5bdf49094ec836fe4febeaff31f6 100644 --- a/vote/templates/vote/spectator_election_item.html +++ b/vote/templates/vote/spectator_election_item.html @@ -18,7 +18,7 @@ <thead class="thead-dark"> <tr> <th scope="col">#</th> - <th scope="col">Applicant</th> + <th scope="col">{% if election.voters_self_apply %}Applicant{% else %}Option{% endif %}</th> <th scope="col">Yes</th> <th scope="col">No</th> <th scope="col">Abstention</th>