Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • stustanet/wahlfang
  • 011892/wahlfang
  • 014449/wahlfang
  • 015384/wahlfang
4 results
Show changes
Commits on Source (7)
Showing
with 501 additions and 168 deletions
......@@ -3,9 +3,53 @@ image: python:3.8-buster
stages:
- test
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
all_tests:
stage: test
before_script:
- apt-get update && apt-get install -y build-essential libldap2-dev libsasl2-dev
- pip3 install virtualenv
- virtualenv -q .venv
- source .venv/bin/activate
- pip install -U -r requirements.txt
- pip install -U -r requirements_dev.txt
script:
- make test
bandit:
stage: test
before_script:
- pip3 install virtualenv
- virtualenv -q .venv
- source .venv/bin/activate
- pip install -U bandit
script:
- make bandit
mypy:
stage: test
before_script:
- apt-get update && apt-get install -y build-essential libldap2-dev libsasl2-dev
- pip3 install virtualenv
- virtualenv -q .venv
- source .venv/bin/activate
- pip install -U -r requirements.txt
- pip install -U -r requirements_dev.txt
script:
- make mypy
allow_failure: true
pylint:
stage: test
before_script:
- apt-get update && apt-get install -y build-essential libldap2-dev libsasl2-dev
- pip3 install virtualenv
- virtualenv -q .venv
- source .venv/bin/activate
- pip install -U -r requirements.txt
- pip install -U pylint pylint-django
script:
- apt-get update && apt-get install -y build-essential python3-dev libldap2-dev libsasl2-dev
- pip3 install -r requirements.txt
- python3 -Wa manage.py test --verbosity=2
- make pylint
allow_failure: true
......@@ -11,7 +11,7 @@ ignore=migrations
# Add files or directories matching the regex patterns to the blacklist. The
# regex matches against base names, not paths.
ignore-patterns=venv/**/*.py
ignore-patterns=
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use.
......
.PHONY: lint
lint: pylint mypy bandit
.PHONY: pylint
pylint:
pylint ./**/*.py
.PHONY: mypy
mypy:
mypy --ignore-missing-imports .
.PHONY: bandit
bandit:
bandit -r .
.PHONY: test
test:
python3 manage.py test
# Wahlfang
> A self-hostable online voting tool developed to include all the
> features you would need to hold any online election you can dream of
StuStaNet Online Wahl-Tool
Developed by [StuStaNet](https://stustanet.de) Wahlfang is a small-ish Django project
which aims at being an easy to use solution for online elections. From simple one-time
votes about where to grab a coffee to large and long meetings with multiple different
votes and elections - Wahlfang does it all.
## Setup
If you would like a new feature or have a bug to report please open an issue over
at our [Gitlab](https://gitlab.stusta.de/stustanet/wahlfang/-/issues).
## Getting Started
```bash
$ cd Wahlfang
$ cd wahlfang
$ pip3 install -r requirements.txt
$ python3 manage.py migrate
$ python manage.py runserver
```
### Admin Access
### Management Access
Creating a superuser (for testing):
Creating a local election management user:
```bash
$ python3 manage.py createsuperuser
$ python3 manage.py crreate_admin
```
The admin interface is accessible at [http://127.0.0.1:8000/admin/](http://127.0.0.1:8000/admin/).
......@@ -39,13 +48,13 @@ $ python3 manage.py create_voter --election_id 1 --voter_id 1337
You can then login with the printed access code.
## Run Development Server
## Contributing
Starting the server:
```bash
$ python3 manage.py runserver
```
If some model changed, you might have make and/or apply migrations again:
If some model changed, you might have to make and/or apply migrations again:
```bash
$ python3 manage.py makemigrations
$ python3 manage.py migrate
......
......@@ -22,9 +22,12 @@ def management_login_required(function=None, redirect_field_name=REDIRECT_FIELD_
class ManagementBackend(BaseBackend):
def authenticate(self, request, username=None, password=None):
def authenticate(self, request, **kwargs):
username = kwargs.pop('username', None)
password = kwargs.pop('password', None)
if username is None or password is None:
return
return None
try:
user = ElectionManager.objects.get(username=username)
......@@ -37,6 +40,8 @@ class ManagementBackend(BaseBackend):
user.backend = 'management.authentication.ManagementBackend'
return user
return None
def get_user(self, user_id):
return ElectionManager.objects.filter(pk=user_id).first()
......
......@@ -8,6 +8,7 @@ from django import forms
from django.core.validators import validate_email
from django.utils import timezone
from management.models import ElectionManager
from vote.models import Election, Application, Session, Voter, OpenVote
......@@ -54,7 +55,7 @@ class ChangeElectionPublicStateForm(forms.ModelForm):
}
class TemplateStringForm(forms.ModelForm):
class TemplateStringForm:
def clean_email_text(self, test_data: List[str], field: str):
"""
checks if the cleaned_text fails when formatted on the test_data.
......@@ -62,7 +63,7 @@ class TemplateStringForm(forms.ModelForm):
test_data_dict = {i: "" for i in test_data}
cleaned_text = self.cleaned_data[field]
try:
test = cleaned_text.format(**test_data_dict)
cleaned_text.format(**test_data_dict)
except (KeyError, ValueError, IndexError):
x = re.findall(r"\{\w*\}", cleaned_text)
test_data = set(x) - set([f"{{{i}}}" for i in test_data])
......@@ -70,7 +71,20 @@ class TemplateStringForm(forms.ModelForm):
return cleaned_text
class AddSessionForm(TemplateStringForm):
class AddSessionForm(forms.ModelForm, TemplateStringForm):
variables = {
"{name}": "Voter's name if set",
"{title}": "Session's title",
"{access_code}": "Access code/token for the voter to login",
"{login_url}": "URL which instantly logs user in",
"{base_url}": "Basically vote.stusta.de",
"{start_time}": "Start time if datetime is set",
"{start_date}": "Start date if datetime is set",
"{start_time_en}": "Start time in english format e.g. 02:23 PM",
"{start_date_en}": "Start date in english format e.g. 12/12/2020",
"{meeting_link}": "Meeting link if set"
}
email = forms.EmailField(required=False, label="",
widget=forms.EmailInput(attrs={"class": "emailinput form-control",
"placeholder": "your@email.de"}))
......@@ -107,16 +121,73 @@ class AddSessionForm(TemplateStringForm):
"start_date", "meeting_link", "start_date_en", "start_time_en"]
return self.clean_email_text(test_data, 'invite_text')
def _save_m2m(self):
super()._save_m2m()
if not self.user.sessions.filter(pk=self.instance.pk):
self.user.sessions.add(self.instance)
self.user.save()
def save(self, commit=True):
instance = super().save(commit=commit)
self.user.sessions.add(instance)
self.instance = super().save(commit=False)
if commit:
self.user.save()
self.instance.save()
self._save_m2m()
else:
self.save_m2m = self._save_m2m
return self.instance
class SessionSettingsForm(AddSessionForm):
add_election_manager = forms.CharField(max_length=256, required=False, label='')
class Meta:
model = Session
fields = ('start_date', 'meeting_link', 'invite_text')
labels = {
'start_date': 'Meeting start (optional)',
'meeting_link': 'Link to meeting call platform (optional)',
# will be set by html
'invite_text': ''
}
def clean_add_election_manager(self):
value = self.data['add_election_manager']
if not ElectionManager.objects.filter(username=value).exists():
raise forms.ValidationError(f'Cannot find election manager with username {value}')
return ElectionManager.objects.get(username=value)
def _save_m2m(self):
super()._save_m2m()
self.cleaned_data['add_election_manager'].sessions.add(self.instance)
self.cleaned_data['add_election_manager'].save()
def save(self, commit=True):
self.instance = super().save(commit=False)
if commit:
self.instance.save()
self._save_m2m()
else:
self.save_m2m = self._save_m2m
return self.instance
return instance
class AddElectionForm(forms.ModelForm, TemplateStringForm):
variables = {
"{name}": "Voter's name if set",
"{title}": "Session's title",
"{url}": "URL to the election",
"{end_time}": "End time if datetime is set",
"{end_date}": "End date if datetime is set",
"{end_time_en}": "End time in english format e.g. 02:23 PM",
"{end_date_en}": "End date in english format e.g. 12/12/2020",
}
class AddElectionForm(TemplateStringForm):
email = forms.EmailField(required=False, label="",
widget=forms.EmailInput(attrs={"class": "emailinput form-control",
"placeholder": "your@email.de"}))
......@@ -296,8 +367,8 @@ class CSVUploaderForm(forms.Form):
raise forms.ValidationError(f'Duplicate email in csv: {row["email"]}')
data[row['email']] = row['name']
except UnicodeDecodeError:
raise forms.ValidationError('File seems to be not in CSV format.')
except UnicodeDecodeError as e:
raise forms.ValidationError('File seems to be not in CSV format.') from e
self.cleaned_data['csv_data'] = data
return self.cleaned_data
......
......@@ -6,21 +6,21 @@ from django.core.management.base import BaseCommand
from django.core.validators import validate_email
from management.models import ElectionManager
from management.utils import is_valid_stusta_email
class Command(BaseCommand):
help = 'Create a new management login'
def add_arguments(self, parser):
parser.add_argument('-u', '--username', type=str, required=True)
parser.add_argument('-e', '--email', type=str, required=True)
parser.add_argument('-u', '--username', type=str, required=False)
parser.add_argument('-e', '--email', type=str, required=False)
def handle(self, *args, **options):
username = options['username']
email = options['email']
username = options['username'] or input('Username: ') # nosec
email = options['email'] or input('E-Mail: ') # nosec
validate_email(email)
domain = email.split('@')[1]
if domain not in ['stusta.de', 'stusta.mhn.de', 'stustanet.de', 'stusta.net']:
if not is_valid_stusta_email(email):
self.stdout.write(self.style.ERROR('Email must be a @stusta.de or @stusta.mhn.de email'))
return
......
from django.conf import settings
from django.contrib.auth.base_user import AbstractBaseUser
from django.db import models
from management.utils import is_valid_stusta_email
from vote.models import Session, Election
......@@ -19,7 +19,7 @@ class ElectionManager(AbstractBaseUser):
@property
def stusta_email(self):
if self.email and self.email.split('@')[-1] in settings.VALID_STUSTA_EMAIL_SUFFIXES:
if self.email and is_valid_stusta_email(self.email):
return self.email
return f'{self.username}@stusta.de'
......
......@@ -66,3 +66,32 @@ th.monospace {
overflow-y: auto;
}
.voter-table > .list-group-item {
border-width: 0 0 1px;
}
.hamburger {
width: 20px;
height: 2px;
box-shadow: inset 0 0 0 32px, 0 -6px, 0 6px;
margin: 10px 3px;
box-sizing: border-box;
display: inline-block;
vertical-align: middle;
position: relative;
font-style: normal;
color: currentColor;
text-align: left;
text-indent: -9999px;
direction: ltr;
}
.hamburger:before {
content: "";
pointer-events: none;
}
.hamburger:after {
content: "";
pointer-events: none;
}
......@@ -23,9 +23,9 @@
<div
class="card-header mycard-header cursor-pointer{% if not form.remind_text.value %} collapsed{% endif %}"
data-toggle="collapse" href="#collapseOne">
<a class="card-title">
<span class="card-title">
Advanced Options
</a>
</span>
</div>
<div id="collapseOne" class="card-body collapse{% if form.remind_text.value %} show{% endif %}">
{{ form.disable_abstention|as_crispy_field }}
......@@ -33,7 +33,7 @@
{{ form.send_emails_on_start|as_crispy_field }}
<h5>Remind email template text</h5>
The template has be written the python format string format. The following variables are available:<br>
<table class="table">
<table class="table table-responsive">
<thead>
<tr>
<th scope="col">Name</th>
......@@ -41,7 +41,7 @@
</tr>
</thead>
<tbody>
{% for key, val in vars.items %}
{% for key, val in variables.items %}
<tr>
<th scope="row" class="monospace">{{ key }}</th>
<td>{{ val }}</td>
......@@ -81,7 +81,6 @@
</div>
<br>
<button type="submit" id="id_btn_start" class="btn btn-success" name="submit_type" value="commit">
Submit
</button>
......@@ -90,8 +89,8 @@
</div>
</div>
</div>
<script src="{% static "js/jquery-3.4.1.slim.min.js" %}"
integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"></script>
<script src="{% static "bootstrap-4.4.1-dist/js/bootstrap.min.js" %}"
integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"></script>
<script src="{% static "js/jquery-3.5.1.slim.min.js" %}"
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"></script>
<script src="{% static "bootstrap-4.5.3-dist/js/bootstrap.min.js" %}"
integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s"></script>
{% endblock %}
......@@ -22,14 +22,12 @@
<div
class="mycard-header card-header cursor-pointer{% if not form.invite_text.value %} collapsed{% endif %}"
data-toggle="collapse" href="#collapseOne">
<a class="card-title">
Advanced Options
</a>
<span class="card-title">Advanced Options</span>
</div>
<div id="collapseOne" class="card-body collapse{% if form.invite_text.value %} show{% endif %}">
<h5>Invite email template text</h5>
The template has be written the python format string format. The following variables are available:<br>
<table class="table">
<table class="table table-responsive">
<thead>
<tr>
<th scope="col">Name</th>
......@@ -37,7 +35,7 @@
</tr>
</thead>
<tbody>
{% for key, val in vars.items %}
{% for key, val in variables.items %}
<tr>
<th scope="row" class="monospace">{{ key }}</th>
<td>{{ val }}</td>
......@@ -82,8 +80,8 @@
</div>
</div>
</div>
<script src="{% static "js/jquery-3.4.1.slim.min.js" %}"
integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"></script>
<script src="{% static "bootstrap-4.4.1-dist/js/bootstrap.min.js" %}"
integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"></script>
<script src="{% static "js/jquery-3.5.1.slim.min.js" %}"
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"></script>
<script src="{% static "bootstrap-4.5.3-dist/js/bootstrap.min.js" %}"
integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s"></script>
{% endblock %}
......@@ -9,7 +9,7 @@
<link rel="icon" type="image/png" href="{% static 'img/favicon32.png' %}" sizes="32x32"/>
<link rel="icon" type="image/png" href="{% static 'img/favicon64.png' %}" sizes="64x64"/>
<link rel="icon" type="image/png" href="{% static 'img/favicon128.png' %}" sizes="128x128"/>
<link rel="stylesheet" href="{% static "bootstrap-4.4.1-dist/css/bootstrap.min.css" %}"/>
<link rel="stylesheet" href="{% static "bootstrap-4.5.3-dist/css/bootstrap.min.css" %}"/>
<link rel="stylesheet" href="{% static "vote/css/style.css" %}"/>
<link rel="stylesheet" href="{% static "management/css/style.css" %}"/>
<title>{% block title %}StuStaNet Wahlfang{% endblock %}</title>
......@@ -68,8 +68,9 @@
</div>
{% comment "Not needed :)" %}
<script src="{% static "js/jquery-3.4.1.slim.min.js" %}" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"></script>
<script src="{% static "bootstrap-4.4.1-dist/js/bootstrap.min.js" %}" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"></script>
<script src="{% static "js/jquery-3.5.1.slim.min.js" %}"
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"></script>
<script src="{% static "bootstrap-4.5.3-dist/js/bootstrap.min.js" %}" integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s"></script>
{% endcomment %}
{% block footer_scripts %}
{% endblock %}
......
......@@ -55,9 +55,9 @@
</div>
{% endfor %}
<script src="{% static "js/jquery-3.4.1.slim.min.js" %}"
integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"></script>
<script src="{% static "bootstrap-4.4.1-dist/js/bootstrap.min.js" %}"
integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"></script>
<script src="{% static "js/jquery-3.5.1.slim.min.js" %}"
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"></script>
<script src="{% static "bootstrap-4.5.3-dist/js/bootstrap.min.js" %}"
integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s"></script>
{% endblock %}
......@@ -7,60 +7,100 @@
<div class="row justify-content-center">
<div class="col-12">
<div class="card shadow ">
<div class="card-header bg-dark text-light">
<div class="card-header bg-dark text-light">
<h4 class="d-inline">{{ session.title }}</h4>
<a class="btn btn-success d-inline float-right ml-2"
href="{% url 'management:add_election' pk=session.id %}">Add Election</a>
<div class="d-inline float-right dropdown ml-2">
<button
class="btn btn-outline-secondary dropdown-toggle"
type="button"
id="dropdown-session-options"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
<i class="hamburger"></i>
</button>
<div class="dropdown-menu" aria-labelledby="dropdown-session-options">
<a class="dropdown-item" href="{% url 'management:session_settings' pk=session.pk %}">Session Settings</a>
</div>
</div>
<a class="btn btn-success d-inline float-right"
href="{% url 'management:add_election' pk=session.pk %}">Add Election</a>
{% if session.meeting_link %}
<br/>
<small>Meeting on <a href="{{ session.meeting_link }}">{{ session.meeting_link }}</a></small>
{% endif %}
{% if session.start_date %}
<small>- {{ session.start_date }}</small>
{% endif %}
</div>
<div class="card-body">
<div class="list-group">
{% for election in elections %}
<div class="list-group-item list-group-item-action">
{% if not elections %}
<div class="list-group-item">
<span>There are no elections for this session</span>
</div>
{% else %}
{% for election in elections %}
<div class="list-group-item list-group-item-action">
<a class="main-link" href="{% url 'management:election' pk=election.id %}"></a>
<span><b>{{ election.title }}</b></span>
<button type="button" class="close btn btn-danger btn-lg float-right" data-toggle="modal"
data-target="#deleteModel{{ election.pk }}"
aria-label="remove election from session">
<span aria-hidden="true">&times;</span>
</button>
<a class="main-link" href="{% url 'management:election' pk=election.pk %}"></a>
<span><b>{{ election.title }}</b></span>
<button type="button" class="close btn btn-danger btn-lg float-right" data-toggle="modal"
data-target="#deleteModel{{ election.pk }}"
aria-label="remove election from session">
<span aria-hidden="true">&times;</span>
</button>
<small class="float-right">
{% if not election.started and election.start_date %}
<span class="right-margin">Starts at {{ election.start_date|date:"Y-m-d H:i:s" }}</span>
{% elif not election.started %}
<span class="right-margin">Needs to be started manually</span>
{% elif election.is_open and election.end_date %}
<span class="right-margin">
<small class="float-right">
{% if not election.started and election.start_date %}
<span class="right-margin">Starts at {{ election.start_date|date:"Y-m-d H:i:s" }}</span>
{% elif not election.started %}
<span class="right-margin">Needs to be started manually</span>
{% elif election.is_open and election.end_date %}
<span class="right-margin">
Open until {{ election.end_date|date:"Y-m-d H:i:s" }}
</span>
{% elif election.closed %}
<span class="right-margin">Closed</span>
{% else %}
<span class="right-margin">Open, needs to be closed manually</span>
{% endif %}
</small>
</div>
{% endfor %}
{% elif election.closed %}
<span class="right-margin">Closed</span>
{% else %}
<span class="right-margin">Open, needs to be closed manually</span>
{% endif %}
</small>
</div>
{% endfor %}
{% endif %}
</div>
</div>
</div>
<div class="card shadow my-4">
<div class="card-header bg-white">
<h4 class="d-inline">Voters</h4>
<div class="d-inline float-right dropdown">
<button
class="btn btn-success dropdown-toggle"
type="button"
id="dropdown-voter-options"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
<i class="hamburger"></i>
</button>
<div class="dropdown-menu" aria-labelledby="dropdown-voter-options">
<a class="dropdown-item"
href="{% url 'management:add_voters' pk=session.pk %}">Add Voters</a>
<a class="dropdown-item"
href="{% url 'management:import_csv' pk=session.pk %}">Import from CSV</a>
<a class="dropdown-item"
href="{% url 'management:add_tokens' pk=session.pk %}">Add Tokens</a>
<button type="button" class="dropdown-item" data-toggle="modal"
data-target="#downloadToken"
aria-label="download tokens">
<span aria-hidden="true">Download Tokens</span>
</button>
</div>
</div>
</div>
<div class="card-body">
<h4>Voters
<a class="btn btn-success d-inline float-right ml-2"
href="{% url 'management:import_csv' pk=session.id %}">Import from CSV</a>
<a class="btn btn-success d-inline float-right ml-2"
href="{% url 'management:add_tokens' pk=session.id %}">Add Tokens</a>
<a class="btn btn-success d-inline float-right ml-2"
href="{% url 'management:add_voters' pk=session.id %}">Add Voters</a>
</h4>
<br>
<div class="list-group list-group-flush">
<div class="list-group-item">
<span class="w-25 font-weight-bold">E-Mail</span>
......@@ -81,17 +121,11 @@
{% endfor %}
</div>
</div>
<br>
<button type="button" class="btn btn-dark d-inline float-left ml-2" data-toggle="modal"
data-target="#downloadToken"
aria-label="download tokens">
<span aria-hidden="true">Download Tokens</span>
</button>
</div>
</div>
</div>
</div>
{% for election in elections %}
<div class="modal fade" id="deleteModel{{ election.pk }}" role="dialog">
<div class="modal-dialog">
......@@ -114,6 +148,7 @@
</div>
</div>
{% endfor %}
<div class="modal fade" id="downloadToken" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
......@@ -134,9 +169,12 @@
</div>
</div>
</div>
<script src="{% static "js/jquery-3.4.1.slim.min.js" %}"
integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"></script>
<script src="{% static "bootstrap-4.4.1-dist/js/bootstrap.min.js" %}"
integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"></script>
<script src="{% static "js/jquery-3.5.1.slim.min.js" %}"
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"></script>
<script src="{% static "js/popper-1.16.1.min.js" %}"
integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN"></script>
<script src="{% static "bootstrap-4.5.3-dist/js/bootstrap.min.js" %}"
integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s"></script>
<script src="{% static "management/js/session.js" %}"></script>
{% endblock %}
{% extends 'management/base.html' %}
{% load static %}
{% load crispy_forms_filters %}
{% block content %}
<div class="row justify-content-center">
<div class="col-12">
<div class="card shadow">
<div class="card-body">
<h4>Session Settings</h4>
<form class="user" action="{% url 'management:session_settings' session.pk %}" method="post">
{% csrf_token %}
{{ form|as_crispy_errors }}
{{ form.start_date|as_crispy_field }}
{{ form.meeting_link|as_crispy_field }}
<div class="card mb-0">
<div
class="mycard-header card-header cursor-pointer{% if not form.invite_text.value and not form.add_election_manager.value %} collapsed{% endif %}"
data-toggle="collapse" href="#collapseOne">
<span class="card-title">Advanced Options</span>
</div>
<div id="collapseOne" class="card-body collapse{% if form.invite_text.value %} show{% endif %}">
<h5>Additional election managers</h5>
To add an additional election manager to this session enter his/her username in the following field.
This means he/she will also have full access to this session and be able to modify it, create new
elections as well as add new voters to it.<br>
Current election managers are:
<div class="list-group list-group-flush">
{% for manager in session.managers.all %}
<div class="list-group-item">
{{ manager.username }}{% if manager.username == user.username %} (you){% endif %}</div>
{% endfor %}
</div>
{{ form.add_election_manager|as_crispy_field }}
<hr>
<h5>Invite email template text</h5>
The template has be written the python format string format. The following variables are available:<br>
<table class="table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Meaning</th>
</tr>
</thead>
<tbody>
{% for key, val in variables.items %}
<tr>
<th scope="row" class="monospace">{{ key }}</th>
<td>{{ val }}</td>
</tr>
{% endfor %}
</tbody>
</table>
Here is an example:<br><br>
<p class="monospace">Dear,<br><br>You have been invited to our awesome meeting {title}. We are meeting
on {meeting_link}. It
takes place on the {start_date_en} at {start_time_en}. You can login with the following link:
&lt;a href="{login_url}"&gt;{login_url}&lt;/a&gt;.
You can also use the following access code on {base_url}: {access_code}<br><br>
Best regards,<br>
Your awesome Organizers
</p>
<p>
{{ form.invite_text|as_crispy_field }}
</p>
<br>
<h6>Send test mail</h6>
<div class="form-row">
<div class="col-8">
{{ form.email|as_crispy_field }}
</div>
<div class="col">
<button type="submit" id="id_btn_start" class="btn btn-warning btn-block" name="submit_type"
value="test">
Send test mail
</button>
</div>
</div>
</div>
</div>
<br>
<button type="submit" id="id_btn_start" class="btn btn-success">Save</button>
</form>
</div>
</div>
</div>
</div>
<script src="{% static "js/jquery-3.5.1.slim.min.js" %}"
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"></script>
<script src="{% static "bootstrap-4.5.3-dist/js/bootstrap.min.js" %}"
integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s"></script>
{% endblock %}
......@@ -7,10 +7,18 @@ app_name = 'management'
urlpatterns = [
path('', views.index, name='index'),
# Session
path('meeting/<int:pk>', views.session_detail, name='session'),
path('meeting/<int:pk>/settings', views.session_settings, name='session_settings'),
path('meeting/<int:pk>/delete_session', views.delete_session, name='delete_session'),
path('meeting/<int:pk>/add_voters', views.add_voters, name='add_voters'),
path('meeting/<int:pk>/add_tokens', views.add_tokens, name='add_tokens'),
path('meeting/<int:pk>/add_election', views.add_election, name='add_election'),
path('meeting/<int:pk>', views.session_detail, name='session'),
path('meeting/<int:pk>/print_token', views.print_token, name='print_token'),
path('meeting/<int:pk>/import_csv', views.import_csv, name='import_csv'),
# Election
path('election/<int:pk>/add_application', views.election_upload_application, name='add_application'),
path('election/<int:pk>/edit/<int:application_id>', views.election_upload_application, name='edit_application'),
path('election/<int:pk>/edit/<int:application_id>/delete_application', views.election_delete_application,
......@@ -20,9 +28,6 @@ urlpatterns = [
path('election/<int:pk>/delete_election', views.delete_election, name='delete_election'),
path('election/<int:pk>/export_csv', views.export_csv, name='export_csv'),
path('election/<int:pk>/export_json', views.export_json, name='export_json'),
path('meeting/<int:pk>/delete_session', views.delete_session, name='delete_session'),
path('meeting/<int:pk>/print_token', views.print_token, name='print_token'),
path('meeting/<int:pk>/import_csv', views.import_csv, name='import_csv'),
# account management stuff
path('login', views.LoginView.as_view(), name='login'),
......
from django.conf import settings
def is_valid_stusta_email(email: str) -> bool:
if not isinstance(email, str) or '@' not in email:
return False
return email.split('@')[-1] in settings.VALID_STUSTA_EMAIL_SUFFIXES
import csv
import json
import logging
import os
from argparse import Namespace
......@@ -12,6 +11,7 @@ from django.conf import settings
from django.contrib import messages
from django.contrib.auth import views as auth_views
from django.http import Http404, HttpResponse
from django.http.response import HttpResponseNotFound, JsonResponse
from django.shortcuts import render, redirect
from django.template.loader import get_template
from django.urls import reverse
......@@ -22,8 +22,18 @@ from latex.build import PdfLatexBuilder
from ratelimit.decorators import ratelimit
from management.authentication import management_login_required
from management.forms import StartElectionForm, AddElectionForm, AddSessionForm, AddVotersForm, ApplicationUploadForm, \
StopElectionForm, ChangeElectionPublicStateForm, AddTokensForm, CSVUploaderForm
from management.forms import (
StartElectionForm,
AddElectionForm,
AddSessionForm,
AddVotersForm,
ApplicationUploadForm,
StopElectionForm,
ChangeElectionPublicStateForm,
AddTokensForm,
CSVUploaderForm,
SessionSettingsForm
)
from vote.models import Election, Application, Voter
logger = logging.getLogger('management.view')
......@@ -53,39 +63,19 @@ def index(request):
if request.POST.get("submit_type") != "test":
ses = form.save()
return redirect('management:session', ses.id)
else:
messages.add_message(request, messages.INFO, 'Test email sent.')
test_session = Namespace(**{
"title": form.cleaned_data['title'],
"invite_text": form.cleaned_data['invite_text'],
"start_date": form.cleaned_data['start_date'] if form.data[
'start_date'] else timezone.now(),
'meeting_link': form.cleaned_data['meeting_link'],
})
test_voter = Namespace(**{
"name": "Testname",
"email": form.cleaned_data['email'],
"session": test_session,
})
test_voter.email_user = partial(Voter.email_user, test_voter)
Voter.send_invitation(test_voter, "mock-up-access-token", manager.stusta_email)
variables = {
"{name}": "Voter's name if set",
"{title}": "Session's title",
"{access_code}": "Access code/token for the voter to login",
"{login_url}": "URL which instantly logs user in",
"{base_url}": "Basically vote.stusta.de",
"{start_time}": "Start time if datetime is set",
"{start_date}": "Start date if datetime is set",
"{start_time_en}": "Start time in english format e.g. 02:23 PM",
"{start_date_en}": "Start date in english format e.g. 12/12/2020",
"{meeting_link}": "Meeting link if set"
}
return render(request, template_name='management/add_session.html', context={'form': form, 'vars': variables})
messages.add_message(request, messages.INFO, 'Test email sent.')
Voter.send_test_invitation(
title=form.cleaned_data['title'],
start_date=form.cleaned_data['start_date'] if form.cleaned_data['start_date'] else timezone.now(),
meeting_link=form.cleaned_data['meeting_link'],
invite_text=form.cleaned_data['invite_text'],
to_email=form.cleaned_data['email'],
from_email=manager.stusta_email
)
return render(request, template_name='management/add_session.html',
context={'form': form, 'variables': form.variables})
context = {
'sessions': manager.sessions.order_by('-pk')
......@@ -105,27 +95,50 @@ def session_detail(request, pk=None):
return render(request, template_name='management/session.html', context=context)
@management_login_required
def session_settings(request, pk=None):
manager = request.user
session = manager.sessions.get(pk=pk)
form = SessionSettingsForm(instance=session, request=request, user=request.user, data=request.POST or None)
if request.POST:
if form.is_valid():
if request.POST.get("submit_type") == "test":
messages.add_message(request, messages.INFO, 'Test email sent.')
Voter.send_test_invitation(
title=form.cleaned_data['title'],
start_date=form.cleaned_data['start_date'] if form.cleaned_data['start_date'] else timezone.now(),
meeting_link=form.cleaned_data['meeting_link'],
invite_text=form.cleaned_data['invite_text'],
to_email=form.cleaned_data['email'],
from_email=manager.stusta_email
)
else:
form.save()
context = {
'session': session,
'elections': session.elections.order_by('pk'),
'voters': session.participants.all(),
'variables': form.variables,
'form': form
}
return render(request, template_name='management/session_settings.html', context=context)
@management_login_required
def add_election(request, pk=None):
# todo add chron job script that sends emails
# todo apply changes to session
manager = request.user
session = manager.sessions.get(id=pk)
session = manager.sessions.get(pk=pk)
context = {
'session': session,
'vars': {
"{name}": "Voter's name if set",
"{title}": "Session's title",
"{url}": "URL to the election",
"{end_time}": "End time if datetime is set",
"{end_date}": "End date if datetime is set",
"{end_time_en}": "End time in english format e.g. 02:23 PM",
"{end_date_en}": "End date in english format e.g. 12/12/2020",
}
}
form = AddElectionForm(session=session, request=request, user=manager, data=request.POST if request.POST else None)
context['form'] = form
context['variables'] = form.variables
if request.POST and form.is_valid():
if request.POST.get("submit_type") == "test":
messages.add_message(request, messages.INFO, 'Test email sent.')
......@@ -243,7 +256,7 @@ def election_upload_application(request, pk, application_id=None):
try:
instance = election.applications.get(pk=application_id)
except Application.DoesNotExist:
raise Http404('Application does not exist')
return HttpResponseNotFound('Application does not exist')
else:
instance = None
......@@ -269,12 +282,12 @@ def election_upload_application(request, pk, application_id=None):
def election_delete_application(request, pk, application_id):
e = Election.objects.filter(session__in=request.user.sessions.all(), pk=pk)
if not e.exists():
raise Http404('Election does not exist')
return HttpResponseNotFound('Election does not exist')
e = e.first()
try:
a = e.applications.get(pk=application_id)
except Application.DoesNotExist:
raise Http404('Application does not exist')
return HttpResponseNotFound('Application does not exist')
a.delete()
return redirect('management:election', pk=pk)
......@@ -296,7 +309,7 @@ def delete_voter(request, pk):
def delete_election(request, pk):
e = Election.objects.filter(session__in=request.user.sessions.all(), pk=pk)
if not e.exists():
raise Http404('Election does not exist')
return HttpResponseNotFound('Election does not exist')
e = e.first()
session = e.session
e.delete()
......@@ -308,7 +321,7 @@ def delete_election(request, pk):
def delete_session(request, pk):
s = request.user.sessions.filter(pk=pk)
if not s.exists():
raise Http404('Session does not exist')
return HttpResponseNotFound('Session does not exist')
s = s.first()
s.delete()
return redirect('management:index')
......@@ -318,7 +331,7 @@ def delete_session(request, pk):
def print_token(request, pk):
session = request.user.sessions.filter(pk=pk)
if not session.exists():
raise Http404('Session does not exist')
return HttpResponseNotFound('Session does not exist')
session = session.first()
participants = session.participants
tokens = [participant.new_access_token() for participant in participants.all() if participant.is_anonymous]
......@@ -359,7 +372,7 @@ def generate_pdf(template_name: str, context: Dict, tex_path: str):
def import_csv(request, pk):
session = request.user.sessions.filter(pk=pk)
if not session.exists():
raise Http404('Session does not exist')
return HttpResponseNotFound('Session does not exist')
session = session.first()
if request.method == 'POST':
......@@ -376,7 +389,7 @@ def import_csv(request, pk):
def export_csv(request, pk):
e = Election.objects.filter(session__in=request.user.sessions.all(), pk=pk)
if not e.exists():
raise Http404('Election does not exist')
return HttpResponseNotFound('Election does not exist')
e = e.first()
response = HttpResponse(content_type='text/csv')
......@@ -401,7 +414,7 @@ def export_csv(request, pk):
def export_json(request, pk):
e = Election.objects.filter(session__in=request.user.sessions.all(), pk=pk)
if not e.exists():
raise Http404('Election does not exist')
return HttpResponseNotFound('Election does not exist')
e = e.first()
json_data = []
......@@ -417,9 +430,7 @@ def export_json(request, pk):
appl_data["elected"] = i < e.max_votes_yes
json_data.append(appl_data)
json_str = json.dumps(json_data)
response = HttpResponse(json_str, content_type='application/json')
response = JsonResponse(data=json_data)
response['Content-Disposition'] = 'attachment; filename=result.json'
return response
bandit
flake8
pylint
pylint-django~=2.3.0
django-stubs