Merge branch 'thubrecht/authens' into 'master'
Authens See merge request klub-dev-ens/kadenios!3
This commit is contained in:
commit
ad790b4676
43 changed files with 1544 additions and 227 deletions
|
@ -4,6 +4,7 @@ from django.utils import timezone
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Election, Option, Question
|
||||
from .utils import check_csv
|
||||
|
||||
|
||||
class ElectionForm(forms.ModelForm):
|
||||
|
@ -21,19 +22,43 @@ class ElectionForm(forms.ModelForm):
|
|||
|
||||
class Meta:
|
||||
model = Election
|
||||
fields = ["name", "description", "start_date", "end_date"]
|
||||
fields = ["name", "description", "restricted", "start_date", "end_date"]
|
||||
|
||||
|
||||
class UploadVotersForm(forms.Form):
|
||||
csv_file = forms.FileField(label=_("Sélectionnez un fichier .csv"))
|
||||
|
||||
def clean_csv_file(self):
|
||||
csv_file = self.cleaned_data["csv_file"]
|
||||
if csv_file.name.lower().endswith(".csv"):
|
||||
for error in check_csv(csv_file):
|
||||
self.add_error(None, error)
|
||||
else:
|
||||
self.add_error(
|
||||
None,
|
||||
_("Extension de fichier invalide, il faut un fichier au format CSV."),
|
||||
)
|
||||
csv_file.seek(0)
|
||||
return csv_file
|
||||
|
||||
|
||||
class VoterMailForm(forms.Form):
|
||||
objet = forms.CharField()
|
||||
message = forms.CharField(widget=forms.Textarea)
|
||||
|
||||
|
||||
class QuestionForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = ["text"]
|
||||
widgets = {"text": forms.TextInput}
|
||||
|
||||
|
||||
class OptionForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Option
|
||||
fields = ["text"]
|
||||
widgets = {"text": forms.TextInput}
|
||||
|
||||
|
||||
class VoteForm(forms.ModelForm):
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
# Generated by Django 2.2.17 on 2020-11-20 15:31
|
||||
# Generated by Django 2.2.17 on 2020-12-20 16:11
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -10,40 +13,243 @@ class Migration(migrations.Migration):
|
|||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("auth", "0011_update_proxy_permissions"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Election',
|
||||
name="User",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='nom')),
|
||||
('short_name', models.SlugField(unique=True, verbose_name='nom bref')),
|
||||
('description', models.TextField(blank=True, verbose_name='description')),
|
||||
('start_time', models.DateTimeField(verbose_name='date et heure de début')),
|
||||
('end_time', models.DateTimeField(verbose_name='date et heure de fin')),
|
||||
('results_public', models.BooleanField(default=False, verbose_name='résultats publics')),
|
||||
('tallied', models.BooleanField(default=False, verbose_name='dépouillée')),
|
||||
('archived', models.BooleanField(default=False, verbose_name='archivée')),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||
(
|
||||
"last_login",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="last login"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_superuser",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
||||
verbose_name="superuser status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"username",
|
||||
models.CharField(
|
||||
error_messages={
|
||||
"unique": "A user with that username already exists."
|
||||
},
|
||||
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
||||
max_length=150,
|
||||
unique=True,
|
||||
validators=[
|
||||
django.contrib.auth.validators.UnicodeUsernameValidator()
|
||||
],
|
||||
verbose_name="username",
|
||||
),
|
||||
),
|
||||
(
|
||||
"first_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=30, verbose_name="first name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"last_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=150, verbose_name="last name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(
|
||||
blank=True, max_length=254, verbose_name="email address"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_staff",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates whether the user can log into this admin site.",
|
||||
verbose_name="staff status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_active",
|
||||
models.BooleanField(
|
||||
default=True,
|
||||
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
|
||||
verbose_name="active",
|
||||
),
|
||||
),
|
||||
(
|
||||
"date_joined",
|
||||
models.DateTimeField(
|
||||
default=django.utils.timezone.now, verbose_name="date joined"
|
||||
),
|
||||
),
|
||||
(
|
||||
"groups",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.Group",
|
||||
verbose_name="groups",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_permissions",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Specific permissions for this user.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.Permission",
|
||||
verbose_name="user permissions",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "user",
|
||||
"verbose_name_plural": "users",
|
||||
"abstract": False,
|
||||
},
|
||||
managers=[
|
||||
("objects", django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Question',
|
||||
name="Election",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('text', models.TextField(verbose_name='question')),
|
||||
('election', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='elections.Election')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255, verbose_name="nom")),
|
||||
("short_name", models.SlugField(unique=True, verbose_name="nom bref")),
|
||||
(
|
||||
"description",
|
||||
models.TextField(blank=True, verbose_name="description"),
|
||||
),
|
||||
(
|
||||
"start_date",
|
||||
models.DateTimeField(verbose_name="date et heure de début"),
|
||||
),
|
||||
("end_date", models.DateTimeField(verbose_name="date et heure de fin")),
|
||||
(
|
||||
"results_public",
|
||||
models.BooleanField(
|
||||
default=False, verbose_name="résultats publics"
|
||||
),
|
||||
),
|
||||
(
|
||||
"tallied",
|
||||
models.BooleanField(default=False, verbose_name="dépouillée"),
|
||||
),
|
||||
(
|
||||
"archived",
|
||||
models.BooleanField(default=False, verbose_name="archivée"),
|
||||
),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["-start_date", "-end_date"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Option',
|
||||
name="Question",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('text', models.TextField(verbose_name='texte')),
|
||||
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='options', to='elections.Question')),
|
||||
('voters', models.ManyToManyField(related_name='votes', to=settings.AUTH_USER_MODEL)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("text", models.TextField(verbose_name="question")),
|
||||
(
|
||||
"max_votes",
|
||||
models.PositiveSmallIntegerField(
|
||||
default=0, verbose_name="nombre maximal de votes reçus"
|
||||
),
|
||||
),
|
||||
(
|
||||
"election",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="questions",
|
||||
to="elections.Election",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["id"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Option",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("text", models.TextField(verbose_name="texte")),
|
||||
(
|
||||
"nb_votes",
|
||||
models.PositiveSmallIntegerField(
|
||||
default=0, verbose_name="nombre de votes reçus"
|
||||
),
|
||||
),
|
||||
(
|
||||
"question",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="options",
|
||||
to="elections.Question",
|
||||
),
|
||||
),
|
||||
(
|
||||
"voters",
|
||||
models.ManyToManyField(
|
||||
related_name="votes", to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["id"],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
# Generated by Django 2.2.17 on 2020-12-18 14:52
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("elections", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="election",
|
||||
old_name="end_time",
|
||||
new_name="end",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="election",
|
||||
old_name="start_time",
|
||||
new_name="start",
|
||||
),
|
||||
]
|
36
elections/migrations/0002_auto_20201220_1735.py
Normal file
36
elections/migrations/0002_auto_20201220_1735.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Generated by Django 2.2.17 on 2020-12-20 16:35
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("elections", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="election",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="registered_voters",
|
||||
to="elections.Election",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="election",
|
||||
name="created_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="elections_created",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,23 +0,0 @@
|
|||
# Generated by Django 2.2.17 on 2020-12-18 19:54
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("elections", "0002_auto_20201218_1452"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="election",
|
||||
old_name="end",
|
||||
new_name="end_date",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="election",
|
||||
old_name="start",
|
||||
new_name="start_date",
|
||||
),
|
||||
]
|
20
elections/migrations/0003_election_restricted.py
Normal file
20
elections/migrations/0003_election_restricted.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 2.2.17 on 2020-12-20 17:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("elections", "0002_auto_20201220_1735"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="election",
|
||||
name="restricted",
|
||||
field=models.BooleanField(
|
||||
default=True, verbose_name="restreint le vote à une liste de personnes"
|
||||
),
|
||||
),
|
||||
]
|
25
elections/migrations/0004_auto_20201220_1847.py
Normal file
25
elections/migrations/0004_auto_20201220_1847.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 2.2.17 on 2020-12-20 17:47
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("elections", "0003_election_restricted"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="election",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="registered_voters",
|
||||
to="elections.Election",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,20 +0,0 @@
|
|||
# Generated by Django 2.2.17 on 2020-12-19 16:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("elections", "0003_auto_20201218_1954"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="option",
|
||||
name="nb_votes",
|
||||
field=models.PositiveSmallIntegerField(
|
||||
default=0, verbose_name="nombre de votes reçus"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,20 +0,0 @@
|
|||
# Generated by Django 2.2.17 on 2020-12-19 17:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("elections", "0004_option_nb_votes"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="question",
|
||||
name="max_votes",
|
||||
field=models.PositiveSmallIntegerField(
|
||||
default=0, verbose_name="nombre maximal de votes reçus"
|
||||
),
|
||||
),
|
||||
]
|
20
elections/migrations/0005_user_full_name.py
Normal file
20
elections/migrations/0005_user_full_name.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 2.2.17 on 2020-12-23 17:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("elections", "0004_auto_20201220_1847"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="full_name",
|
||||
field=models.CharField(
|
||||
blank=True, max_length=150, verbose_name="Nom et Prénom"
|
||||
),
|
||||
),
|
||||
]
|
20
elections/migrations/0006_election_sent_mail.py
Normal file
20
elections/migrations/0006_election_sent_mail.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 2.2.17 on 2020-12-23 19:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("elections", "0005_user_full_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="election",
|
||||
name="sent_mail",
|
||||
field=models.BooleanField(
|
||||
default=False, verbose_name="mail avec les identifiants envoyé"
|
||||
),
|
||||
),
|
||||
]
|
21
elections/migrations/0007_election_voters.py
Normal file
21
elections/migrations/0007_election_voters.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 2.2.17 on 2020-12-24 00:18
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("elections", "0006_election_sent_mail"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="election",
|
||||
name="voters",
|
||||
field=models.ManyToManyField(
|
||||
related_name="cast_elections", to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
]
|
21
elections/migrations/0008_question_voters.py
Normal file
21
elections/migrations/0008_question_voters.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 2.2.17 on 2020-12-24 00:38
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("elections", "0007_election_voters"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="question",
|
||||
name="voters",
|
||||
field=models.ManyToManyField(
|
||||
related_name="cast_questions", to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,3 +1,5 @@
|
|||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
|
||||
|
@ -35,7 +37,7 @@ class RestrictAccessMixin(SelectElectionMixin):
|
|||
return qs.none()
|
||||
|
||||
|
||||
class OpenElectionOnly(RestrictAccessMixin):
|
||||
class OpenElectionOnlyMixin(RestrictAccessMixin):
|
||||
"""N'autorise la vue que lorsque l'élection est ouverte"""
|
||||
|
||||
def get_filters(self):
|
||||
|
@ -51,9 +53,12 @@ class OpenElectionOnly(RestrictAccessMixin):
|
|||
return filters
|
||||
|
||||
|
||||
class CreatorOnlyMixin(RestrictAccessMixin):
|
||||
class CreatorOnlyMixin(LoginRequiredMixin, RestrictAccessMixin):
|
||||
"""Restreint l'accès au créateurice de l'élection"""
|
||||
|
||||
def get_next_url(self):
|
||||
return reverse("kadenios")
|
||||
|
||||
def get_filters(self):
|
||||
filters = super().get_filters()
|
||||
# TODO: change the way we collect the user according to the model used
|
||||
|
@ -71,7 +76,7 @@ class CreatorOnlyEditMixin(CreatorOnlyMixin, SingleObjectMixin):
|
|||
return filters
|
||||
|
||||
|
||||
class AdministratorOnlyMixin:
|
||||
class AdministratorOnlyMixin(LoginRequiredMixin):
|
||||
"""Restreint l'accès aux admins"""
|
||||
|
||||
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
User = get_user_model()
|
||||
from .staticdefs import CONNECTION_METHODS
|
||||
|
||||
# #############################################################################
|
||||
# Models regarding an election
|
||||
# #############################################################################
|
||||
|
||||
|
||||
class Election(models.Model):
|
||||
|
@ -13,16 +18,38 @@ class Election(models.Model):
|
|||
start_date = models.DateTimeField(_("date et heure de début"))
|
||||
end_date = models.DateTimeField(_("date et heure de fin"))
|
||||
|
||||
restricted = models.BooleanField(
|
||||
_("restreint le vote à une liste de personnes"), default=True
|
||||
)
|
||||
|
||||
sent_mail = models.BooleanField(
|
||||
_("mail avec les identifiants envoyé"), default=False
|
||||
)
|
||||
|
||||
created_by = models.ForeignKey(
|
||||
User, on_delete=models.SET_NULL, blank=True, null=True
|
||||
settings.AUTH_USER_MODEL,
|
||||
related_name="elections_created",
|
||||
on_delete=models.SET_NULL,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
voters = models.ManyToManyField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
related_name="cast_elections",
|
||||
)
|
||||
|
||||
results_public = models.BooleanField(_("résultats publics"), default=False)
|
||||
tallied = models.BooleanField(_("dépouillée"), default=False)
|
||||
# TODO : cache tally or recompute it each time ?
|
||||
|
||||
archived = models.BooleanField(_("archivée"), default=False)
|
||||
|
||||
@property
|
||||
def preferred_method(self):
|
||||
if self.restricted:
|
||||
return "PWD"
|
||||
return "CAS"
|
||||
|
||||
class Meta:
|
||||
ordering = ["-start_date", "-end_date"]
|
||||
|
||||
|
@ -37,6 +64,11 @@ class Question(models.Model):
|
|||
_("nombre maximal de votes reçus"), default=0
|
||||
)
|
||||
|
||||
voters = models.ManyToManyField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
related_name="cast_questions",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ["id"]
|
||||
|
||||
|
@ -47,7 +79,7 @@ class Option(models.Model):
|
|||
)
|
||||
text = models.TextField(_("texte"), blank=False)
|
||||
voters = models.ManyToManyField(
|
||||
User,
|
||||
settings.AUTH_USER_MODEL,
|
||||
related_name="votes",
|
||||
)
|
||||
# For now, we store the amount of votes received after the election is tallied
|
||||
|
@ -55,3 +87,40 @@ class Option(models.Model):
|
|||
|
||||
class Meta:
|
||||
ordering = ["id"]
|
||||
|
||||
|
||||
# #############################################################################
|
||||
# Modification of the base User Model
|
||||
# #############################################################################
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
election = models.ForeignKey(
|
||||
Election,
|
||||
related_name="registered_voters",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
full_name = models.CharField(_("Nom et Prénom"), max_length=150, blank=True)
|
||||
|
||||
def get_username(self):
|
||||
return "__".join(self.username.split("__")[1:])
|
||||
|
||||
def can_vote(self, request, election):
|
||||
# Si c'est un·e utilisateur·ice CAS, iel peut voter dans les élections
|
||||
# ouvertes à tou·te·s
|
||||
if self.election is None:
|
||||
# If the user is connected via CAS, request.session["CASCONNECTED"] is set
|
||||
# to True by authens
|
||||
return not election.restricted and request.session.get("CASCONNECTED")
|
||||
|
||||
# Pour les élections restreintes, il faut y être associé
|
||||
return election.restricted and (self.election == election)
|
||||
|
||||
def get_prefix(self):
|
||||
return self.username.split("__")[0]
|
||||
|
||||
def connection_method(self):
|
||||
method = self.username.split("__")[0]
|
||||
return CONNECTION_METHODS.get(method, _("identifiants spécifiques"))
|
||||
|
|
19
elections/staticdefs.py
Normal file
19
elections/staticdefs.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
MAIL_VOTERS = (
|
||||
"Dear {full_name},\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"Election URL: {election_url}\n"
|
||||
"\n"
|
||||
"Your voter ID: {username}\n"
|
||||
"Your password: {password}\n"
|
||||
"\n"
|
||||
"-- \n"
|
||||
"Kadenios"
|
||||
)
|
||||
|
||||
CONNECTION_METHODS = {
|
||||
"pwd": _("mot de passe"),
|
||||
"cas": _("CAS"),
|
||||
}
|
|
@ -37,6 +37,47 @@
|
|||
</div>
|
||||
<hr>
|
||||
|
||||
{# Indications de connexion #}
|
||||
{% if election.start_date < current_time and election.end_date > current_time and not can_vote %}
|
||||
<div class="message is-warning">
|
||||
<div class="message-body">
|
||||
{% if election.restricted %}
|
||||
{% trans "Pour voter lors de cette élection, vous devez vous connecter à l'aide des identifiants reçus par mail." %}
|
||||
{% else %}
|
||||
{% trans "Pour voter lors de cette élection, vous devez vous connecter à l'aide du CAS élève, d'autres restrictions peuvent s'appliquer et votre vote pourra être supprimé si vous n'avez pas le droit de vote." %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-half">
|
||||
<div class="tile is-ancestor">
|
||||
<div class="tile is-parent">
|
||||
{% if election.restricted %}
|
||||
<a class="tile is-child notification is-primary" href="{% url 'auth.election' election.pk %}?next={% url 'election.view' election.pk %}">
|
||||
<div class="subtitle has-text-centered mb-2">
|
||||
<span class="icon has-text-white">
|
||||
<i class="fas fa-unlock"></i>
|
||||
</span>
|
||||
<span class="ml-3">{% trans "Connexion par identifiants" %}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<a class="tile is-child notification is-primary" href="{% url 'authens:login.cas' %}">
|
||||
<div class="subtitle has-text-centered mb-2">
|
||||
<span class="icon has-text-white">
|
||||
<i class="fas fa-school"></i>
|
||||
</span>
|
||||
<span class="ml-3">{% trans "Connexion via CAS" %}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Description de l'élection #}
|
||||
<div class="message is-primary">
|
||||
<div class="message-body">{{ election.description|linebreaksbr }}</div>
|
||||
|
@ -46,7 +87,7 @@
|
|||
{% for q in election.questions.all %}
|
||||
<div class="panel" id="q_{{ q.pk }}">
|
||||
<div class="panel-heading is-size-6">
|
||||
{% if election.start_date < current_time and election.end_date > current_time %}
|
||||
{% if can_vote and election.start_date < current_time and election.end_date > current_time %}
|
||||
<a class="tag is-small is-outlined is-light is-danger" href="{% url 'election.vote' q.pk %}">
|
||||
<span class="icon">
|
||||
<i class="fas fa-vote-yea"></i>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
{% block content %}
|
||||
|
||||
<div class="level">
|
||||
<div class="level is-block-tablet is-block-desktop is-flex-fullhd">
|
||||
<div class="level-left">
|
||||
{# Titre de l'élection #}
|
||||
<div class="level-item">
|
||||
|
@ -22,9 +22,9 @@
|
|||
</div>
|
||||
|
||||
<div class="level-right">
|
||||
{% if election.start_date > current_time %}
|
||||
|
||||
{# Lien pour la modification #}
|
||||
{# Lien pour la modification et l'upload des votant·e·s #}
|
||||
{% if election.start_date > current_time %}
|
||||
<div class="level-item">
|
||||
<a class="button is-light is-outlined is-primary" href="{% url 'election.update' election.pk %}">
|
||||
<span class="icon">
|
||||
|
@ -33,10 +33,22 @@
|
|||
<span>{% trans "Modifier" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if election.restricted %}
|
||||
<div class="level-item">
|
||||
<a class="button is-light is-outlined is-primary" href="{% url 'election.upload-voters' election.pk %}">
|
||||
<span class="icon">
|
||||
<i class="fas fa-file-import"></i>
|
||||
</span>
|
||||
<span>{% trans "Gestion de la liste de votant·e·s" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% elif election.end_date < current_time %}
|
||||
{% if not election.tallied %}
|
||||
|
||||
{# Lien pour le dépouillement #}
|
||||
{% if not election.tallied %}
|
||||
<div class="level-item">
|
||||
<a class="button is-light is-outlined is-primary" href="{% url 'election.tally' election.pk %}">
|
||||
<span class="icon">
|
||||
|
@ -45,9 +57,9 @@
|
|||
<span>{% trans "Dépouiller" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
{# Lien pour la publication des résultats #}
|
||||
{% else %}
|
||||
<div class="level-item">
|
||||
<a class="button is-outlined is-primary" href="{% url 'election.publish' election.pk %}">
|
||||
<span class="icon">
|
||||
|
|
|
@ -31,6 +31,8 @@
|
|||
<h1 class="title">{% trans "Modification d'une élection" %}</h1>
|
||||
<hr>
|
||||
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-two-thirds">
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
|
@ -55,5 +57,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
8
elections/templates/elections/election_voters.html
Normal file
8
elections/templates/elections/election_voters.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
{% endblock %}
|
51
elections/templates/elections/mail_voters.html
Normal file
51
elections/templates/elections/mail_voters.html
Normal file
|
@ -0,0 +1,51 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="title">{% trans "Composition du mail aux votant·e·s" %}</h1>
|
||||
<hr>
|
||||
|
||||
<div class="message is-warning">
|
||||
<div class="message-body">
|
||||
{% trans "Assurez-vous que la liste des votant·e·s est complète, en effet, une fois le mail envoyé, il ne sera plus possible de la modifier." %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="message is-primary">
|
||||
<div class="message-body">
|
||||
{% trans "Rajoutez le message à envoyer aux électeurs entre 'Dear {full_name}' et l'url de l'élection." %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-two-thirds">
|
||||
<form action="" method="post" enctype="multipart/form-data" id="import-voters">
|
||||
{% csrf_token %}
|
||||
|
||||
{% include "forms/form.html" %}
|
||||
|
||||
<div class="field is-grouped is-centered">
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-fullwidth is-outlined is-primary is-light">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-check"></i>
|
||||
</span>
|
||||
<span>{% trans "Envoyer" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<a class="button is-primary" href="{% url 'election.upload-voters' election.pk %}">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-undo-alt"></i>
|
||||
</span>
|
||||
<span>{% trans "Retour" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -13,6 +13,8 @@
|
|||
<h1 class="title">{% trans "Modification d'une option" %}</h1>
|
||||
<hr>
|
||||
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-two-thirds">
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
|
@ -37,5 +39,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
<h1 class="title">{% trans "Modification d'une question" %}</h1>
|
||||
<hr>
|
||||
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-two-thirds">
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
|
@ -37,5 +39,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
125
elections/templates/elections/upload_voters.html
Normal file
125
elections/templates/elections/upload_voters.html
Normal file
|
@ -0,0 +1,125 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block extra_head %}
|
||||
<script>
|
||||
$(document).ready(function($) {
|
||||
const fileInput = document.querySelector('#import-voters input[type=file]');
|
||||
fileInput.onchange = () => {
|
||||
if (fileInput.files.length > 0) {
|
||||
const fileName = document.querySelector('#import-voters .file-name');
|
||||
fileName.textContent = fileInput.files[0].name;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="level is-flex-widescreen">
|
||||
<div class="level-left">
|
||||
<div class="item-level">
|
||||
<h1 class="title">{% trans "Gestion de la liste de votant·e·s" %}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="level-right">
|
||||
{% if not election.sent_mail %}
|
||||
<div class="level-item">
|
||||
<a class="button is-light is-outlined is-primary" href="{% url 'election.mail-voters' election.pk %}">
|
||||
<span class="icon">
|
||||
<i class="fas fa-envelope-open"></i>
|
||||
</span>
|
||||
<span>{% trans "Envoyer le mail d'annonce" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="level-item">
|
||||
<a class="button is-primary" href="{% url 'election.admin' election.pk %}">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-undo-alt"></i>
|
||||
</span>
|
||||
<span>{% trans "Retour" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
{# Si on a déjà envoyé le mail avec les identifiants, on ne peut plus changer la liste #}
|
||||
{% if not election.sent_mail %}
|
||||
<div class="message is-warning">
|
||||
<div class="message-body">
|
||||
{% trans "Importez un fichier au format CSV, avec sur la première colonne le login, sur la deuxième, le nom et prénom et enfin l'adresse email sur la troisième. Soit :<br><br><pre>Login_1,Prénom/Nom_1,mail_1@machin.test<br>Login_2,Prénom/Nom_2,mail_2@bidule.test<br>...</pre>" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-two-thirds">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="notification is-danger">
|
||||
<button class="delete"></button>
|
||||
{% for error in form.non_field_errors %}
|
||||
{{ error }}<br>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="" method="post" enctype="multipart/form-data" id="import-voters">
|
||||
{% csrf_token %}
|
||||
|
||||
{% include "forms/form.html" with errors=False %}
|
||||
|
||||
<div class="field is-grouped is-centered">
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-fullwidth is-outlined is-primary is-light">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-check"></i>
|
||||
</span>
|
||||
<span>{% trans "Importer" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
{% endif %}
|
||||
|
||||
{# Liste des votant·e·s #}
|
||||
{% if voters %}
|
||||
<h3 class="subtitle">{% trans "Liste des votant·e·s pour cette élection" %}</h3>
|
||||
<hr>
|
||||
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-two-thirds">
|
||||
<table class="table is-fullwidth is-bordered is-striped has-text-centered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Login" %}</th>
|
||||
<th>{% trans "Nom" %}</th>
|
||||
<th>{% trans "Email" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for v in voters %}
|
||||
<tr>
|
||||
<td>{{ v.get_username }}</td>
|
||||
<td>{{ v.full_name }}</td>
|
||||
<td>{{ v.email }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endblock %}
|
|
@ -7,6 +7,16 @@ urlpatterns = [
|
|||
path("create/", views.ElectionCreateView.as_view(), name="election.create"),
|
||||
path("created/", views.ElectionListView.as_view(), name="election.created"),
|
||||
path("admin/<int:pk>", views.ElectionAdminView.as_view(), name="election.admin"),
|
||||
path(
|
||||
"mail-voters/<int:pk>",
|
||||
views.ElectionMailVotersView.as_view(),
|
||||
name="election.mail-voters",
|
||||
),
|
||||
path(
|
||||
"upload-voters/<int:pk>",
|
||||
views.ElectionUploadVotersView.as_view(),
|
||||
name="election.upload-voters",
|
||||
),
|
||||
path("update/<int:pk>", views.ElectionUpdateView.as_view(), name="election.update"),
|
||||
path("tally/<int:pk>", views.ElectionTallyView.as_view(), name="election.tally"),
|
||||
path(
|
||||
|
|
113
elections/utils.py
Normal file
113
elections/utils.py
Normal file
|
@ -0,0 +1,113 @@
|
|||
import csv
|
||||
import io
|
||||
import random
|
||||
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.mail import EmailMessage, get_connection
|
||||
from django.core.validators import validate_email
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import User
|
||||
|
||||
|
||||
def create_users(election, csv_file):
|
||||
"""Crée les votant·e·s pour l'élection donnée, en remplissant les champs
|
||||
`username`, `election` et `full_name`.
|
||||
"""
|
||||
dialect = csv.Sniffer().sniff(csv_file.readline().decode("utf-8"))
|
||||
csv_file.seek(0)
|
||||
reader = csv.reader(io.StringIO(csv_file.read().decode("utf-8")), dialect)
|
||||
for (username, full_name, email) in reader:
|
||||
election.registered_voters.create(
|
||||
username=f"{election.id}__{username}", email=email, full_name=full_name
|
||||
)
|
||||
|
||||
|
||||
def check_csv(csv_file):
|
||||
"""Vérifie que le fichier donnant la liste de votant·e·s est bien formé"""
|
||||
try:
|
||||
dialect = csv.Sniffer().sniff(csv_file.readline().decode("utf-8"))
|
||||
except csv.Error:
|
||||
return [
|
||||
_(
|
||||
"Format invalide. Vérifiez que le fichier est bien formé (i.e. "
|
||||
"chaque ligne de la forme 'login,nom,email')."
|
||||
)
|
||||
]
|
||||
csv_file.seek(0)
|
||||
reader = csv.reader(io.StringIO(csv_file.read().decode("utf-8")), dialect)
|
||||
|
||||
errors = []
|
||||
users = {}
|
||||
line_nb = 0
|
||||
for line in reader:
|
||||
line_nb += 1
|
||||
if len(line) != 3:
|
||||
errors.append(
|
||||
_("La ligne {} n'a pas le bon nombre d'éléments.").format(line_nb)
|
||||
)
|
||||
else:
|
||||
if line[0] == "":
|
||||
errors.append(
|
||||
_("Valeur manquante dans la ligne {} : 'login'.").format(line_nb)
|
||||
)
|
||||
else:
|
||||
if line[0] in users:
|
||||
errors.append(
|
||||
_("Doublon dans les logins : lignes {} et {}.").format(
|
||||
line_nb, users[line[0]]
|
||||
)
|
||||
)
|
||||
else:
|
||||
users[line[0]] = line_nb
|
||||
if line[1] == "":
|
||||
errors.append(
|
||||
_("Valeur manquante dans la ligne {} : 'nom'.").format(line_nb)
|
||||
)
|
||||
try:
|
||||
validate_email(line[2])
|
||||
except ValidationError:
|
||||
errors.append(
|
||||
_("Adresse mail invalide à la ligne {} : '{}'.").format(
|
||||
line_nb, line[2]
|
||||
)
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def generate_password():
|
||||
random.seed()
|
||||
alphabet = "abcdefghjkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||
password = ""
|
||||
for i in range(15):
|
||||
password += random.choice(alphabet)
|
||||
|
||||
return password
|
||||
|
||||
|
||||
def send_mail(election, mail_form):
|
||||
"""Envoie le mail d'annonce de l'élection avec identifiants et mot de passe
|
||||
aux votant·e·s, le mdp est généré en même temps que le mail est envoyé.
|
||||
"""
|
||||
voters = list(election.registered_voters.all())
|
||||
url = f"https://kadenios.eleves.ens.fr/elections/view/{election.id}"
|
||||
messages = []
|
||||
for v in voters:
|
||||
password = generate_password()
|
||||
v.password = make_password(password)
|
||||
messages.append(
|
||||
EmailMessage(
|
||||
subject=mail_form.cleaned_data["objet"],
|
||||
body=mail_form.cleaned_data["message"].format(
|
||||
full_name=v.full_name,
|
||||
election_url=url,
|
||||
username=v.get_username,
|
||||
password=password,
|
||||
),
|
||||
to=[v.email],
|
||||
)
|
||||
)
|
||||
get_connection(fail_silently=False).send_messages(messages)
|
||||
User.objects.bulk_update(voters, ["password"])
|
|
@ -2,7 +2,7 @@ from django.contrib import messages
|
|||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
|
||||
# from django.db.models import Count, Prefetch
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.text import slugify
|
||||
|
@ -10,14 +10,24 @@ from django.utils.translation import gettext_lazy as _
|
|||
from django.views.generic import (
|
||||
CreateView,
|
||||
DetailView,
|
||||
FormView,
|
||||
ListView,
|
||||
RedirectView,
|
||||
UpdateView,
|
||||
)
|
||||
|
||||
from .forms import ElectionForm, OptionForm, OptionFormSet, QuestionForm
|
||||
from .mixins import CreatorOnlyEditMixin, CreatorOnlyMixin, OpenElectionOnly
|
||||
from .forms import (
|
||||
ElectionForm,
|
||||
OptionForm,
|
||||
OptionFormSet,
|
||||
QuestionForm,
|
||||
UploadVotersForm,
|
||||
VoterMailForm,
|
||||
)
|
||||
from .mixins import CreatorOnlyEditMixin, CreatorOnlyMixin, OpenElectionOnlyMixin
|
||||
from .models import Election, Option, Question
|
||||
from .staticdefs import MAIL_VOTERS
|
||||
from .utils import create_users, send_mail
|
||||
|
||||
# TODO: access control *everywhere*
|
||||
|
||||
|
@ -63,11 +73,13 @@ class ElectionCreateView(SuccessMessageMixin, CreateView):
|
|||
return super().form_valid(form)
|
||||
|
||||
|
||||
# TODO : only the creator can edit the election and view the admin panel
|
||||
class ElectionAdminView(CreatorOnlyMixin, DetailView):
|
||||
model = Election
|
||||
template_name = "elections/election_admin.html"
|
||||
|
||||
def get_next_url(self):
|
||||
return reverse("election.view", args=[self.object.pk])
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs.update({"current_time": timezone.now()})
|
||||
return super().get_context_data(**kwargs)
|
||||
|
@ -76,6 +88,72 @@ class ElectionAdminView(CreatorOnlyMixin, DetailView):
|
|||
return super().get_queryset().prefetch_related("questions__options")
|
||||
|
||||
|
||||
class ElectionUploadVotersView(CreatorOnlyEditMixin, SuccessMessageMixin, FormView):
|
||||
model = Election
|
||||
form_class = UploadVotersForm
|
||||
success_message = _("Liste de votant·e·s importée avec succès !")
|
||||
template_name = "elections/upload_voters.html"
|
||||
|
||||
def get_queryset(self):
|
||||
# On ne peut ajouter une liste d'électeurs que sur une élection restreinte
|
||||
return super().get_queryset().filter(restricted=True)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("election.upload-voters", args=[self.object.pk])
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["voters"] = self.object.registered_voters.all()
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
# On crée les comptes nécessaires à l'élection, en supprimant ceux
|
||||
# existant déjà pour ne pas avoir de doublons
|
||||
self.object.registered_voters.all().delete()
|
||||
create_users(self.object, form.cleaned_data["csv_file"])
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class ElectionMailVotersView(CreatorOnlyEditMixin, SuccessMessageMixin, FormView):
|
||||
model = Election
|
||||
form_class = VoterMailForm
|
||||
success_message = _("Mail d'annonce envoyé avec succès !")
|
||||
template_name = "elections/mail_voters.html"
|
||||
|
||||
def get_queryset(self):
|
||||
# On ne peut envoyer un mail que sur une élection restreinte qui n'a pas
|
||||
# déjà vu son mail envoyé
|
||||
return super().get_queryset().filter(restricted=True, sent_mail=False)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("election.upload-voters", args=[self.object.pk])
|
||||
|
||||
def get_initial(self):
|
||||
return {"objet": f"Vote : {self.object.name}", "message": MAIL_VOTERS}
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
self.object.sent_mail = True
|
||||
send_mail(self.object, form)
|
||||
self.object.save()
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class ElectionListView(CreatorOnlyMixin, ListView):
|
||||
model = Election
|
||||
template_name = "elections/election_list.html"
|
||||
|
@ -257,7 +335,7 @@ class DelOptionView(CreatorOnlyEditMixin, BackgroundUpdateView):
|
|||
|
||||
|
||||
# #############################################################################
|
||||
# Common Views
|
||||
# Public Views
|
||||
# #############################################################################
|
||||
|
||||
|
||||
|
@ -265,17 +343,17 @@ class ElectionView(DetailView):
|
|||
model = Election
|
||||
template_name = "elections/election.html"
|
||||
|
||||
def get_next_url(self):
|
||||
return self.request.path
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs.update({"current_time": timezone.now()})
|
||||
return super().get_context_data(**kwargs)
|
||||
# context = super().get_context_data(**kwargs)
|
||||
# if self.object.tallied:
|
||||
# options_qs = Option.objects.annotate(nb_votes=Count("voters"))
|
||||
# questions = self.election.question.prefetch_related(
|
||||
# Prefetch("options", queryset=options_qs)
|
||||
# )
|
||||
# context["questions"] = questions
|
||||
# return context
|
||||
user = self.request.user
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["current_time"] = timezone.now()
|
||||
context["can_vote"] = user.is_authenticated and user.can_vote(
|
||||
self.request, context["election"]
|
||||
)
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
|
@ -286,10 +364,18 @@ class ElectionView(DetailView):
|
|||
)
|
||||
|
||||
|
||||
class VoteView(OpenElectionOnly, DetailView):
|
||||
class ElectionVotersView(DetailView):
|
||||
model = Election
|
||||
template_name = "elections/election_voters.html"
|
||||
|
||||
|
||||
class VoteView(OpenElectionOnlyMixin, DetailView):
|
||||
model = Question
|
||||
template_name = "elections/vote.html"
|
||||
|
||||
def get_next_url(self):
|
||||
return reverse("election.view", args=[self.object.election.pk])
|
||||
|
||||
def get_success_url(self):
|
||||
questions = list(self.object.election.questions.all())
|
||||
q_index = questions.index(self.object)
|
||||
|
@ -302,6 +388,14 @@ class VoteView(OpenElectionOnly, DetailView):
|
|||
|
||||
return reverse("election.vote", args=[q_next])
|
||||
|
||||
def get_object(self):
|
||||
question = super().get_object()
|
||||
# Seulement les utilisateur·ice·s ayant le droit de voter dans l'élection
|
||||
# peuvent voir la page
|
||||
if not self.request.user.can_vote(self.request, question.election):
|
||||
raise Http404
|
||||
return question
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
vote_form = OptionFormSet(instance=self.object)
|
||||
|
@ -316,6 +410,10 @@ class VoteView(OpenElectionOnly, DetailView):
|
|||
for v in vote_form:
|
||||
v.record_vote(self.request.user)
|
||||
|
||||
# On enregistre le vote pour la question et l'élection
|
||||
self.object.voters.add(self.request.user)
|
||||
self.object.election.voters.add(self.request.user)
|
||||
|
||||
messages.success(self.request, _("Votre choix a bien été enregistré !"))
|
||||
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
|
|
@ -12,6 +12,8 @@ https://docs.djangoproject.com/en/2.2/ref/settings/
|
|||
|
||||
import os
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
@ -35,6 +37,7 @@ INSTALLED_APPS = [
|
|||
"django.contrib.messages",
|
||||
"kadenios.apps.IgnoreSrcStaticFilesConfig",
|
||||
"elections",
|
||||
"authens",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
@ -68,7 +71,7 @@ TEMPLATES = [
|
|||
WSGI_APPLICATION = "kadenios.wsgi.application"
|
||||
|
||||
|
||||
# Password validation
|
||||
# Password validation and authentication
|
||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
|
@ -86,6 +89,17 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||
},
|
||||
]
|
||||
|
||||
AUTH_USER_MODEL = "elections.User"
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
"shared.auth.backends.PwdBackend",
|
||||
"shared.auth.backends.CASBackend",
|
||||
"shared.auth.backends.ElectionBackend",
|
||||
]
|
||||
|
||||
LOGIN_URL = reverse_lazy("authens:login")
|
||||
LOGIN_REDIRECT_URL = "/"
|
||||
|
||||
AUTHENS_USE_OLDCAS = False # On n'utilise que le CAS normal pour l'instant
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
||||
|
@ -101,6 +115,11 @@ USE_L10N = True
|
|||
USE_TZ = True
|
||||
|
||||
|
||||
# Mail configuration
|
||||
|
||||
DEFAULT_FROM_EMAIL = "Kadenios <klub-dev@ens.fr>"
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@ urlpatterns = [
|
|||
path("", HomeView.as_view(), name="kadenios"),
|
||||
path("admin/", admin.site.urls),
|
||||
path("elections/", include("elections.urls")),
|
||||
path("auth/", include("shared.auth.urls")),
|
||||
path("authens/", include("authens.urls")),
|
||||
]
|
||||
|
||||
if "debug_toolbar" in settings.INSTALLED_APPS:
|
||||
|
|
56
shared/auth/backends.py
Normal file
56
shared/auth/backends.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
from authens.backends import ENSCASBackend
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
|
||||
class CASBackend(ENSCASBackend):
|
||||
"""ENS CAS authentication backend, customized to get the full name at connection."""
|
||||
|
||||
def clean_cas_login(self, cas_login):
|
||||
return f"cas__{cas_login.strip().lower()}"
|
||||
|
||||
def create_user(self, username, attributes):
|
||||
email = attributes.get("email")
|
||||
name = attributes.get("name")
|
||||
|
||||
return UserModel.objects.create_user(
|
||||
username=username, email=email, full_name=name
|
||||
)
|
||||
|
||||
|
||||
class PwdBackend(ModelBackend):
|
||||
"""Password authentication"""
|
||||
|
||||
def authenticate(self, request, username=None, password=None):
|
||||
if username is None or password is None:
|
||||
return None
|
||||
|
||||
return super().authenticate(
|
||||
request, username=f"pwd__{username}", password=password
|
||||
)
|
||||
|
||||
|
||||
class ElectionBackend(ModelBackend):
|
||||
"""Authentication for a specific election.
|
||||
|
||||
Given a login and an election, we check if the user `{election.id}__{login}`
|
||||
exists, and then if the password matches.
|
||||
"""
|
||||
|
||||
def authenticate(self, request, login=None, password=None, election_id=None):
|
||||
if login is None or password is None or election_id is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
user = UserModel.objects.get(
|
||||
username=f"{election_id}__{login}", election=election_id
|
||||
)
|
||||
except UserModel.DoesNotExist:
|
||||
return None
|
||||
|
||||
if user.check_password(password):
|
||||
return user
|
||||
return None
|
63
shared/auth/forms.py
Normal file
63
shared/auth/forms.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
from django import forms
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth import forms as auth_forms
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
|
||||
class ElectionAuthForm(forms.Form):
|
||||
"""Adapts Django's AuthenticationForm to allow for an election specific login."""
|
||||
|
||||
login = auth_forms.UsernameField(label=_("Identifiant"), max_length=255)
|
||||
password = forms.CharField(
|
||||
label=_("Mot de passe"),
|
||||
strip=False,
|
||||
widget=forms.PasswordInput(attrs={"autocomplete": "current-password"}),
|
||||
)
|
||||
election_id = forms.IntegerField(widget=forms.HiddenInput())
|
||||
|
||||
def __init__(self, request=None, *args, **kwargs):
|
||||
self.request = request
|
||||
self.user_cache = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
login = self.cleaned_data.get("login")
|
||||
password = self.cleaned_data.get("password")
|
||||
election_id = self.cleaned_data.get("election_id")
|
||||
|
||||
if login is not None and password:
|
||||
self.user_cache = authenticate(
|
||||
self.request,
|
||||
login=login,
|
||||
password=password,
|
||||
election_id=election_id,
|
||||
)
|
||||
if self.user_cache is None:
|
||||
raise self.get_invalid_login_error()
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
def get_user(self):
|
||||
# Necessary API for LoginView
|
||||
return self.user_cache
|
||||
|
||||
def get_invalid_login_error(self):
|
||||
return forms.ValidationError(
|
||||
_(
|
||||
"Aucun·e électeur·ice avec cet identifiant et mot de passe n'existe "
|
||||
"pour cette élection. Vérifiez que les informations rentrées sont "
|
||||
"correctes, les champs sont sensibles à la casse."
|
||||
),
|
||||
code="invalid_login",
|
||||
)
|
||||
|
||||
|
||||
class PwdResetForm(auth_forms.PasswordResetForm):
|
||||
"""Restricts the search for password users, i.e. whose username starts with pwd__."""
|
||||
|
||||
def get_users(self, email):
|
||||
users = super().get_users(email)
|
||||
return (u for u in users if u.username.split("__")[0] == "pwd")
|
11
shared/auth/urls.py
Normal file
11
shared/auth/urls.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"election/<int:election_id>/login",
|
||||
views.ElectionLoginView.as_view(),
|
||||
name="auth.election",
|
||||
),
|
||||
]
|
19
shared/auth/views.py
Normal file
19
shared/auth/views.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
from django.contrib.auth import views as auth_views
|
||||
|
||||
from .forms import ElectionAuthForm
|
||||
|
||||
# #############################################################################
|
||||
# Election Specific Login
|
||||
# #############################################################################
|
||||
|
||||
|
||||
class ElectionLoginView(auth_views.LoginView):
|
||||
template_name = "auth/election_login.html"
|
||||
authentication_form = ElectionAuthForm
|
||||
|
||||
def get_initial(self):
|
||||
return {"election_id": self.kwargs.get("election_id")}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs.update({"election_id": self.kwargs.get("election_id")})
|
||||
return super().get_context_data(**kwargs)
|
41
shared/templates/auth/election_login.html
Normal file
41
shared/templates/auth/election_login.html
Normal file
|
@ -0,0 +1,41 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block auth %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="title">{% trans "Connexion par mot de passe" %}</h1>
|
||||
<hr>
|
||||
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-two-thirds">
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% include "forms/form.html" with errors=True %}
|
||||
|
||||
<div class="field is-grouped is-centered">
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-fullwidth is-outlined is-primary is-light">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-check"></i>
|
||||
</span>
|
||||
<span>{% trans "Enregistrer" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<a class="button is-primary" href="{% url 'election.view' election_id %}">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-undo-alt"></i>
|
||||
</span>
|
||||
<span>{% trans "Retour" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
37
shared/templates/authens/login_switch.html
Normal file
37
shared/templates/authens/login_switch.html
Normal file
|
@ -0,0 +1,37 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block auth %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="title">{% trans "Choisissez la méthode de connexion" %}</h1>
|
||||
<hr>
|
||||
|
||||
<div class="tile is-ancestor">
|
||||
<div class="tile is-parent">
|
||||
<a class="tile is-child notification is-primary" href="{% url "authens:login.cas" %}?next={{ next }}">
|
||||
<div class="subtitle has-text-centered mb-2">
|
||||
<span class="icon has-text-white">
|
||||
<i class="fas fa-school"></i>
|
||||
</span>
|
||||
<span class="ml-3">{% trans "Connexion via CAS" %}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="tile is-parent">
|
||||
<a class="tile is-child notification" href="{% url "authens:login.pwd" %}?next={{ next }}">
|
||||
<div class="subtitle has-text-centered mb-2">
|
||||
<span class="icon">
|
||||
<i class="fas fa-key"></i>
|
||||
</span>
|
||||
<span class="ml-3">{% trans "Connexion par mot de passe" %}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
56
shared/templates/authens/pwd_login.html
Normal file
56
shared/templates/authens/pwd_login.html
Normal file
|
@ -0,0 +1,56 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block auth %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="title">{% trans "Connexion par mot de passe" %}</h1>
|
||||
<hr>
|
||||
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-two-thirds">
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% include "forms/form.html" with errors=True %}
|
||||
|
||||
<div class="field is-grouped is-centered">
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-fullwidth is-outlined is-primary is-light">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-check"></i>
|
||||
</span>
|
||||
<span>{% trans "Enregistrer" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<a class="button is-primary" href="{% url 'authens:login' %}?next={{ next }}">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-undo-alt"></i>
|
||||
</span>
|
||||
<span>{% trans "Retour" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-centered">
|
||||
<div class="control">
|
||||
<div class="help has-text-centered">
|
||||
<span>{% trans "Mot de passe oublié :" %}</span>
|
||||
<a class="tag has-text-primary" href="{% url 'authens:reset.pwd' %}">
|
||||
<span>{% trans "Réinitialiser mon mot de passe." %}</span>
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-lock-open"></i>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
42
shared/templates/authens/pwd_reset.html
Normal file
42
shared/templates/authens/pwd_reset.html
Normal file
|
@ -0,0 +1,42 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block auth %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="title">{% trans "Réinitialisation du mot de passe" %}</h1>
|
||||
<hr>
|
||||
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-two-thirds">
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% include "forms/form.html" with errors=True %}
|
||||
|
||||
<div class="field is-grouped is-centered">
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-fullwidth is-outlined is-primary is-light">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-check"></i>
|
||||
</span>
|
||||
<span>{% trans "Envoyer un mail" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<a class="button is-primary" href="{% url 'authens:login.pwd' %}?next={{ next }}">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-undo-alt"></i>
|
||||
</span>
|
||||
<span>{% trans "Retour" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
33
shared/templates/authens/pwd_reset_confirm.html
Normal file
33
shared/templates/authens/pwd_reset_confirm.html
Normal file
|
@ -0,0 +1,33 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block auth %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="title">{% trans "Réinitialisation du mot de passe" %}</h1>
|
||||
<hr>
|
||||
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-two-thirds">
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% include "forms/form.html" with errors=True %}
|
||||
|
||||
<div class="field is-grouped is-centered">
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-fullwidth is-outlined is-primary is-light">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-check"></i>
|
||||
</span>
|
||||
<span>{% trans "Enregistrer" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -39,21 +39,44 @@
|
|||
<nav class="level has-background-primary">
|
||||
<div class="level-left pl-4">
|
||||
<div class="level-item">
|
||||
<a href="{% url "kadenios" %}">
|
||||
<h1 class="has-text-primary-light is-size-1 is-family-secondary">Kadenios</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right pr-5">
|
||||
<div class="level-item">
|
||||
<a class="icon is-size-1 has-text-white" href="">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block auth %}
|
||||
<div class="level-right pr-5">
|
||||
{% if user.is_authenticated %}
|
||||
<div class="level-item mr-5">
|
||||
<div class="tag">
|
||||
{% blocktrans with name=user.get_username connection=user.connection_method %}Connecté·e en tant que {{ name }} par {{ connection }}{% endblocktrans %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="level-item ml-3">
|
||||
<a class="icon is-size-1 has-text-white" href="{% url 'authens:logout' %}?next={% if view.get_next_url %}{{ view.get_next_url }}{% else %}/{% endif %}">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<div class="level-item">
|
||||
<a class="tag has-text-primary is-size-5" href="{% url 'authens:login' %}?next={{ request.path }}">
|
||||
<span>{% trans "Se connecter" %}</span>
|
||||
<span class="icon">
|
||||
<i class="fas fa-sign-in-alt"></i>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</nav>
|
||||
{% block layout %}
|
||||
<div class="main-content">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-two-thirds">
|
||||
<div class="column is-two-thirds-fullhd is-12-desktop is-12-widescreen">
|
||||
<section class="section pt-3">
|
||||
|
||||
{% if messages %}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
{% load bulma_utils i18n %}
|
||||
|
||||
<label class="label {% if field.field.required %}{{ form.required_css_class }}{% endif %}">
|
||||
{{ field.label }}
|
||||
{{ field.label_tag }}
|
||||
</label>
|
||||
|
||||
<div class="control">
|
||||
|
||||
<div class="file has-name">
|
||||
<label class="file-label">
|
||||
{{ field|bulmafy:'file-input' }}
|
||||
<span class="file-cta">
|
||||
|
@ -16,7 +17,11 @@
|
|||
{% trans "Choisissez un fichier..." %}
|
||||
</span>
|
||||
</span>
|
||||
<span class="file-name">
|
||||
{% trans "Aucun fichier sélectionné" %}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{% for error in field.errors %}
|
||||
<span class="help is-danger {{ form.error_css_class }}">{{ error }}</span>
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
{% if errors %}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="message is-danger">
|
||||
<div class="message-header">
|
||||
<button class="delete" aria-label="delete"></button>
|
||||
</div>
|
||||
<div class="message-body">
|
||||
{% for non_field_error in form.non_field_errors %}
|
||||
{{ non_field_error }}
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
{% endfor %}
|
||||
|
||||
{% if field.help_text %}
|
||||
<p class="help">
|
||||
<div class="help">
|
||||
{{ field.help_text|safe }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
42
shared/templates/registration/logged_out.html
Normal file
42
shared/templates/registration/logged_out.html
Normal file
|
@ -0,0 +1,42 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block auth %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="title">{% trans "Déconnexion réussie" %}</h1>
|
||||
<hr>
|
||||
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-two-thirds">
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% include "forms/form.html" with errors=True %}
|
||||
|
||||
<div class="field is-grouped is-centered">
|
||||
<div class="control is-expanded">
|
||||
<a class="button is-fullwidth is-outlined is-primary is-light" href="{% url 'authens:login' %}">
|
||||
<span>{% trans "Se reconnecter" %}</span>
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-unlock"></i>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<a class="button is-primary" href="{% url 'kadenios' %}">
|
||||
<span>{% trans "Accueil" %}</span>
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-home"></i>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
Loading…
Add table
Reference in a new issue