From d685e9ba2937e0ddcb18ff0c0660002797162b18 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Mon, 5 Dec 2016 20:18:03 +0100 Subject: [PATCH 01/54] Election bdd + first view --- core/management/commands/populate.py | 27 +++++++++- election/__init__.py | 0 election/admin.py | 3 ++ election/migrations/0001_initial.py | 51 ++++++++++++++++++ election/migrations/0002_candidate_program.py | 19 +++++++ election/migrations/__init__.py | 0 election/models.py | 54 +++++++++++++++++++ .../templates/election/election_list.jinja | 13 +++++ election/tests.py | 3 ++ election/urls.py | 7 +++ election/views.py | 19 +++++++ sith/settings.py | 1 + sith/urls.py | 1 + 13 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 election/__init__.py create mode 100644 election/admin.py create mode 100644 election/migrations/0001_initial.py create mode 100644 election/migrations/0002_candidate_program.py create mode 100644 election/migrations/__init__.py create mode 100644 election/models.py create mode 100644 election/templates/election/election_list.jinja create mode 100644 election/tests.py create mode 100644 election/urls.py create mode 100644 election/views.py diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 65884269..000fe922 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -15,6 +15,8 @@ from club.models import Club, Membership from subscription.models import Subscription from counter.models import Customer, ProductType, Product, Counter from com.models import Sith +from election.models import Election, Responsability, Candidate + class Command(BaseCommand): help = "Populate a new instance of the Sith AE" @@ -122,6 +124,14 @@ Welcome to the wiki page! skia.save() skia.view_groups=[Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id] skia.save() + # Adding user sli + sli = User(username='sli', last_name="Li", first_name="S", + email="sli@git.an", + date_of_birth="1942-06-12") + sli.set_password("plop") + sli.save() + skia.view_groups=[Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id] + sli.save() # Adding user public public = User(username='public', last_name="Not subscribed", first_name="Public", email="public@git.an", @@ -219,6 +229,14 @@ Cette page vise à documenter la syntaxe *Markdown* utilisée sur le site. duration=settings.SITH_SUBSCRIPTIONS[s.subscription_type]['duration'], start=s.subscription_start) s.save() + ## Sli + s = Subscription(member=Subscriber.objects.filter(pk=sli.pk).first(), subscription_type=list(settings.SITH_SUBSCRIPTIONS.keys())[0], + payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0]) + s.subscription_start = s.compute_start() + s.subscription_end = s.compute_end( + duration=settings.SITH_SUBSCRIPTIONS[s.subscription_type]['duration'], + start=s.subscription_start) + s.save() ## Comptable s = Subscription(member=User.objects.filter(pk=comptable.pk).first(), subscription_type=list(settings.SITH_SUBSCRIPTIONS.keys())[0], payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0]) @@ -329,5 +347,12 @@ Cette page vise à documenter la syntaxe *Markdown* utilisée sur le site. target_label=op[7], cheque_number=op[8]) operation.clean() operation.save() - + + # Create an election + el = Election(title="Élection 2017", description="La roue tourne", start_date='1942-06-12 10:28:45', end_date='7942-06-12 10:28:45') + el.save() + resp = Responsability(election=el, title="Co Respo Info", description="Ghetto++") + resp.save() + cand = Candidate(responsability=resp, subscriber=skia) + cand.save() diff --git a/election/__init__.py b/election/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/election/admin.py b/election/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/election/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/election/migrations/0001_initial.py b/election/migrations/0001_initial.py new file mode 100644 index 00000000..822d3f6e --- /dev/null +++ b/election/migrations/0001_initial.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('subscription', '0003_auto_20160902_1914'), + ] + + operations = [ + migrations.CreateModel( + name='Candidate', + fields=[ + ('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)), + ('votes', models.IntegerField(default=0, verbose_name='votes')), + ], + ), + migrations.CreateModel( + name='Election', + fields=[ + ('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)), + ('title', models.CharField(verbose_name='title', max_length=255)), + ('description', models.TextField(blank=True, null=True, verbose_name='description')), + ('start_date', models.DateTimeField(verbose_name='start date')), + ('end_date', models.DateTimeField(verbose_name='end date')), + ], + ), + migrations.CreateModel( + name='Responsability', + fields=[ + ('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)), + ('title', models.CharField(verbose_name='title', max_length=255)), + ('description', models.TextField(blank=True, null=True, verbose_name='description')), + ('blank_votes', models.IntegerField(default=0, verbose_name='blank votes')), + ('election', models.ForeignKey(to='election.Election', related_name='election', verbose_name='election')), + ], + ), + migrations.AddField( + model_name='candidate', + name='responsability', + field=models.ForeignKey(to='election.Responsability', related_name='responsability', verbose_name='responsability'), + ), + migrations.AddField( + model_name='candidate', + name='subscriber', + field=models.ForeignKey(related_name='candidate', to='subscription.Subscriber', blank=True, verbose_name='user'), + ), + ] diff --git a/election/migrations/0002_candidate_program.py b/election/migrations/0002_candidate_program.py new file mode 100644 index 00000000..c9bdb522 --- /dev/null +++ b/election/migrations/0002_candidate_program.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('election', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='candidate', + name='program', + field=models.TextField(verbose_name='description', null=True, blank=True), + ), + ] diff --git a/election/migrations/__init__.py b/election/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/election/models.py b/election/models.py new file mode 100644 index 00000000..edd8cf09 --- /dev/null +++ b/election/models.py @@ -0,0 +1,54 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from django.utils import timezone +from django.conf import settings + +from datetime import timedelta +from core.models import User +from subscription.models import Subscriber +from subscription.views import get_subscriber + + +class Election(models.Model): + """ + This class allow to create a new election + """ + title = models.CharField(_('title'), max_length=255) + description = models.TextField(_('description'), null=True, blank=True) + start_date = models.DateTimeField(_('start date'), blank=False) + end_date = models.DateTimeField(_('end date'), blank=False) + + def __str__(self): + return self.title + + @property + def is_active(self): + now = timezone.now() + return bool(now <= self.end_date and now >= self.start_date) + + def get_results(self): + pass + + +class Responsability(models.Model): + """ + """ + election = models.ForeignKey(Election, related_name='election', verbose_name=_("election")) + title = models.CharField(_('title'), max_length=255) + description = models.TextField(_('description'), null=True, blank=True) + blank_votes = models.IntegerField(_('blank votes'), default=0) + + def __str__(self): + return ("%s : %s") % (self.election.title, self.title) + + +class Candidate(models.Model): + """ + """ + responsability = models.ForeignKey(Responsability, related_name='responsability', verbose_name=_("responsability")) + subscriber = models.ForeignKey(Subscriber, verbose_name=_('user'), related_name='candidate', blank=True) + program = models.TextField(_('description'), null=True, blank=True) + votes = models.IntegerField(_('votes'), default=0) + + def __str__(self): + return ("%s : %s -> %s") % (self.election.title, self.title, self.subscriber.get_full_name()) diff --git a/election/templates/election/election_list.jinja b/election/templates/election/election_list.jinja new file mode 100644 index 00000000..2c10e9b9 --- /dev/null +++ b/election/templates/election/election_list.jinja @@ -0,0 +1,13 @@ +{% extends "core/base.jinja" %} + +{% block title %} +{% trans %}Election list{% endtrans %} +{% endblock %} + +{% block content %} + {% for el in object_list %} + {% if el.is_active %} +

{{el}}

+ {% endif %} + {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/election/tests.py b/election/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/election/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/election/urls.py b/election/urls.py new file mode 100644 index 00000000..ac646b59 --- /dev/null +++ b/election/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url, include + +from election.views import * + +urlpatterns = [ + url(r'^$', ElectionsListView.as_view(), name='election_list'), +] \ No newline at end of file diff --git a/election/views.py b/election/views.py new file mode 100644 index 00000000..91db1e73 --- /dev/null +++ b/election/views.py @@ -0,0 +1,19 @@ +from django.shortcuts import render +from django.views.generic import ListView, DetailView, RedirectView +from django.views.generic.edit import UpdateView, CreateView, DeleteView, FormView +from django.core.urlresolvers import reverse_lazy, reverse +from django.utils.translation import ugettext_lazy as _ +from django.conf import settings + +from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, CanCreateMixin +from election.models import Election, Responsability, Candidate + +# Display elections + + +class ElectionsListView(CanViewMixin, ListView): + """ + A list with all responsabilities and their candidates + """ + model = Election + template_name = 'election/election_list.jinja' diff --git a/sith/settings.py b/sith/settings.py index ba905351..c1680fbd 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -58,6 +58,7 @@ INSTALLED_APPS = ( 'rootplace', 'sas', 'com', + 'election', ) MIDDLEWARE_CLASSES = ( diff --git a/sith/urls.py b/sith/urls.py index dc09cdbe..d759979e 100644 --- a/sith/urls.py +++ b/sith/urls.py @@ -39,6 +39,7 @@ urlpatterns = [ url(r'^launderette/', include('launderette.urls', namespace="launderette", app_name="launderette")), url(r'^sas/', include('sas.urls', namespace="sas", app_name="sas")), url(r'^api/v1/', include('api.urls', namespace="api", app_name="api")), + url(r'^election/', include('election.urls', namespace="election", app_name="election")), url(r'^admin/', include(admin.site.urls)), url(r'^ajax_select/', include(ajax_select_urls)), url(r'^i18n/', include('django.conf.urls.i18n')), From 135fa00e25efca011d5af05a007145ff9c4463a5 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Mon, 5 Dec 2016 23:49:13 +0100 Subject: [PATCH 02/54] Fix database and add some view --- .../migrations/0003_auto_20161205_2235.py | 24 +++++++++++++++++ election/migrations/0004_election_electors.py | 20 ++++++++++++++ election/models.py | 8 ++++-- .../templates/election/election_detail.jinja | 27 +++++++++++++++++++ .../templates/election/election_list.jinja | 2 +- election/urls.py | 5 ++-- election/views.py | 11 ++++++++ 7 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 election/migrations/0003_auto_20161205_2235.py create mode 100644 election/migrations/0004_election_electors.py create mode 100644 election/templates/election/election_detail.jinja diff --git a/election/migrations/0003_auto_20161205_2235.py b/election/migrations/0003_auto_20161205_2235.py new file mode 100644 index 00000000..e1d80308 --- /dev/null +++ b/election/migrations/0003_auto_20161205_2235.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('election', '0002_candidate_program'), + ] + + operations = [ + migrations.AlterField( + model_name='candidate', + name='responsability', + field=models.ForeignKey(verbose_name='responsability', to='election.Responsability', related_name='candidate'), + ), + migrations.AlterField( + model_name='responsability', + name='election', + field=models.ForeignKey(verbose_name='election', to='election.Election', related_name='responsability'), + ), + ] diff --git a/election/migrations/0004_election_electors.py b/election/migrations/0004_election_electors.py new file mode 100644 index 00000000..c1f932da --- /dev/null +++ b/election/migrations/0004_election_electors.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('subscription', '0003_auto_20160902_1914'), + ('election', '0003_auto_20161205_2235'), + ] + + operations = [ + migrations.AddField( + model_name='election', + name='electors', + field=models.ManyToManyField(verbose_name='electors', related_name='election', blank=True, to='subscription.Subscriber'), + ), + ] diff --git a/election/models.py b/election/models.py index edd8cf09..3754f9b1 100644 --- a/election/models.py +++ b/election/models.py @@ -17,6 +17,7 @@ class Election(models.Model): description = models.TextField(_('description'), null=True, blank=True) start_date = models.DateTimeField(_('start date'), blank=False) end_date = models.DateTimeField(_('end date'), blank=False) + electors = models.ManyToManyField(Subscriber, related_name='election', verbose_name=_("electors"), blank=True) def __str__(self): return self.title @@ -26,6 +27,9 @@ class Election(models.Model): now = timezone.now() return bool(now <= self.end_date and now >= self.start_date) + def has_voted(self, user): + return self.electors.filter(id=user.id).exists() + def get_results(self): pass @@ -33,7 +37,7 @@ class Election(models.Model): class Responsability(models.Model): """ """ - election = models.ForeignKey(Election, related_name='election', verbose_name=_("election")) + election = models.ForeignKey(Election, related_name='responsability', verbose_name=_("election")) title = models.CharField(_('title'), max_length=255) description = models.TextField(_('description'), null=True, blank=True) blank_votes = models.IntegerField(_('blank votes'), default=0) @@ -45,7 +49,7 @@ class Responsability(models.Model): class Candidate(models.Model): """ """ - responsability = models.ForeignKey(Responsability, related_name='responsability', verbose_name=_("responsability")) + responsability = models.ForeignKey(Responsability, related_name='candidate', verbose_name=_("responsability")) subscriber = models.ForeignKey(Subscriber, verbose_name=_('user'), related_name='candidate', blank=True) program = models.TextField(_('description'), null=True, blank=True) votes = models.IntegerField(_('votes'), default=0) diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja new file mode 100644 index 00000000..798f66dc --- /dev/null +++ b/election/templates/election/election_detail.jinja @@ -0,0 +1,27 @@ +{% extends "core/base.jinja" %} + +{% block title %} +{% trans %}Election list{% endtrans %} +{% endblock %} + +{% block content %} + {% if object.has_voted(request.user) %} + A voté + {% endif %} +

{{object.title}}

+

{{object.description}}

+

{% trans %}End :{% endtrans %} {{object.end_date}}

+ {% for resp in object.responsability.all() %} +

{{resp.title}}

+ {% for user in resp.candidate.all() %} +

{{user.subscriber.first_name}} {{user.subscriber.last_name}} ({{user.subscriber.nick_name}})

+ {% if user.subscriber.profile_pict %} + {% trans %}Profile{% endtrans %} + {% endif %} + {% if user.program %} +

Programme

+

{{user.program}}

+ {% endif %} + {% endfor %} + {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/election/templates/election/election_list.jinja b/election/templates/election/election_list.jinja index 2c10e9b9..77e4e1f9 100644 --- a/election/templates/election/election_list.jinja +++ b/election/templates/election/election_list.jinja @@ -7,7 +7,7 @@ {% block content %} {% for el in object_list %} {% if el.is_active %} -

{{el}}

+

{{el}}

{% endif %} {% endfor %} {% endblock %} \ No newline at end of file diff --git a/election/urls.py b/election/urls.py index ac646b59..5582fedd 100644 --- a/election/urls.py +++ b/election/urls.py @@ -3,5 +3,6 @@ from django.conf.urls import url, include from election.views import * urlpatterns = [ - url(r'^$', ElectionsListView.as_view(), name='election_list'), -] \ No newline at end of file + url(r'^$', ElectionsListView.as_view(), name='list'), + url(r'^/(?P[0-9]+)/detail$', ElectionDetailView.as_view(), name='detail'), +] diff --git a/election/views.py b/election/views.py index 91db1e73..2bcfcc10 100644 --- a/election/views.py +++ b/election/views.py @@ -17,3 +17,14 @@ class ElectionsListView(CanViewMixin, ListView): """ model = Election template_name = 'election/election_list.jinja' + + +class ElectionDetailView(CanViewMixin, DetailView): + """ + Details an election responsability by responsability + """ + model = Election + template_name = 'election/election_detail.jinja' + pk_url_kwarg = "election_id" + +# Forms From fd3309fc5f388ba86db4b8b797b237856a4694b4 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Wed, 7 Dec 2016 23:55:05 +0100 Subject: [PATCH 03/54] Refactor election bdd --- core/management/commands/populate.py | 4 +-- election/migrations/0001_initial.py | 35 ++++++++++++------- election/migrations/0002_candidate_program.py | 19 ---------- .../migrations/0003_auto_20161205_2235.py | 24 ------------- election/migrations/0004_election_electors.py | 20 ----------- election/models.py | 32 +++++++++++++---- .../templates/election/election_detail.jinja | 12 +++---- 7 files changed, 56 insertions(+), 90 deletions(-) delete mode 100644 election/migrations/0002_candidate_program.py delete mode 100644 election/migrations/0003_auto_20161205_2235.py delete mode 100644 election/migrations/0004_election_electors.py diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 000fe922..e851732b 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -349,10 +349,10 @@ Cette page vise à documenter la syntaxe *Markdown* utilisée sur le site. operation.save() # Create an election - el = Election(title="Élection 2017", description="La roue tourne", start_date='1942-06-12 10:28:45', end_date='7942-06-12 10:28:45') + el = Election(title="Élection 2017", description="La roue tourne", start_proposal='1942-06-12 10:28:45', end_proposal='2042-06-12 10:28:45',start_date='1942-06-12 10:28:45', end_date='7942-06-12 10:28:45') el.save() resp = Responsability(election=el, title="Co Respo Info", description="Ghetto++") resp.save() - cand = Candidate(responsability=resp, subscriber=skia) + cand = Candidate(responsability=resp, user=skia) cand.save() diff --git a/election/migrations/0001_initial.py b/election/migrations/0001_initial.py index 822d3f6e..84363dce 100644 --- a/election/migrations/0001_initial.py +++ b/election/migrations/0001_initial.py @@ -2,28 +2,31 @@ from __future__ import unicode_literals from django.db import migrations, models +from django.conf import settings class Migration(migrations.Migration): dependencies = [ - ('subscription', '0003_auto_20160902_1914'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( name='Candidate', fields=[ - ('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)), - ('votes', models.IntegerField(default=0, verbose_name='votes')), + ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('program', models.TextField(blank=True, null=True, verbose_name='description')), ], ), migrations.CreateModel( name='Election', fields=[ - ('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)), - ('title', models.CharField(verbose_name='title', max_length=255)), + ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('title', models.CharField(max_length=255, verbose_name='title')), ('description', models.TextField(blank=True, null=True, verbose_name='description')), + ('start_proposal', models.DateTimeField(verbose_name='start proposal')), + ('end_proposal', models.DateTimeField(verbose_name='end proposal')), ('start_date', models.DateTimeField(verbose_name='start date')), ('end_date', models.DateTimeField(verbose_name='end date')), ], @@ -31,21 +34,29 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Responsability', fields=[ - ('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)), - ('title', models.CharField(verbose_name='title', max_length=255)), + ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('title', models.CharField(max_length=255, verbose_name='title')), ('description', models.TextField(blank=True, null=True, verbose_name='description')), - ('blank_votes', models.IntegerField(default=0, verbose_name='blank votes')), - ('election', models.ForeignKey(to='election.Election', related_name='election', verbose_name='election')), + ('election', models.ForeignKey(to='election.Election', verbose_name='election', related_name='responsability')), + ], + ), + migrations.CreateModel( + name='Vote', + fields=[ + ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('candidate', models.ManyToManyField(to='election.Candidate', related_name='vote', verbose_name='candidate')), + ('election', models.ForeignKey(to='election.Election', verbose_name='election', related_name='vote')), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, verbose_name='user', related_name='vote')), ], ), migrations.AddField( model_name='candidate', name='responsability', - field=models.ForeignKey(to='election.Responsability', related_name='responsability', verbose_name='responsability'), + field=models.ForeignKey(to='election.Responsability', verbose_name='responsability', related_name='candidate'), ), migrations.AddField( model_name='candidate', - name='subscriber', - field=models.ForeignKey(related_name='candidate', to='subscription.Subscriber', blank=True, verbose_name='user'), + name='user', + field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='user', related_name='candidate'), ), ] diff --git a/election/migrations/0002_candidate_program.py b/election/migrations/0002_candidate_program.py deleted file mode 100644 index c9bdb522..00000000 --- a/election/migrations/0002_candidate_program.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('election', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='candidate', - name='program', - field=models.TextField(verbose_name='description', null=True, blank=True), - ), - ] diff --git a/election/migrations/0003_auto_20161205_2235.py b/election/migrations/0003_auto_20161205_2235.py deleted file mode 100644 index e1d80308..00000000 --- a/election/migrations/0003_auto_20161205_2235.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('election', '0002_candidate_program'), - ] - - operations = [ - migrations.AlterField( - model_name='candidate', - name='responsability', - field=models.ForeignKey(verbose_name='responsability', to='election.Responsability', related_name='candidate'), - ), - migrations.AlterField( - model_name='responsability', - name='election', - field=models.ForeignKey(verbose_name='election', to='election.Election', related_name='responsability'), - ), - ] diff --git a/election/migrations/0004_election_electors.py b/election/migrations/0004_election_electors.py deleted file mode 100644 index c1f932da..00000000 --- a/election/migrations/0004_election_electors.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('subscription', '0003_auto_20160902_1914'), - ('election', '0003_auto_20161205_2235'), - ] - - operations = [ - migrations.AddField( - model_name='election', - name='electors', - field=models.ManyToManyField(verbose_name='electors', related_name='election', blank=True, to='subscription.Subscriber'), - ), - ] diff --git a/election/models.py b/election/models.py index 3754f9b1..3cbdb707 100644 --- a/election/models.py +++ b/election/models.py @@ -11,13 +11,14 @@ from subscription.views import get_subscriber class Election(models.Model): """ - This class allow to create a new election + This class allows to create a new election """ title = models.CharField(_('title'), max_length=255) description = models.TextField(_('description'), null=True, blank=True) + start_proposal = models.DateTimeField(_('start proposal'), blank=False) + end_proposal = models.DateTimeField(_('end proposal'), blank=False) start_date = models.DateTimeField(_('start date'), blank=False) end_date = models.DateTimeField(_('end date'), blank=False) - electors = models.ManyToManyField(Subscriber, related_name='election', verbose_name=_("electors"), blank=True) def __str__(self): return self.title @@ -27,8 +28,13 @@ class Election(models.Model): now = timezone.now() return bool(now <= self.end_date and now >= self.start_date) + @property + def is_proposal_active(self): + now = timezone.now() + return bool(now <= self.end_proposal and now >= self.start_proposal) + def has_voted(self, user): - return self.electors.filter(id=user.id).exists() + return self.vote.filter(user__id=user.id).exists() def get_results(self): pass @@ -36,11 +42,11 @@ class Election(models.Model): class Responsability(models.Model): """ + This class allows to create a new responsability """ election = models.ForeignKey(Election, related_name='responsability', verbose_name=_("election")) title = models.CharField(_('title'), max_length=255) description = models.TextField(_('description'), null=True, blank=True) - blank_votes = models.IntegerField(_('blank votes'), default=0) def __str__(self): return ("%s : %s") % (self.election.title, self.title) @@ -48,11 +54,23 @@ class Responsability(models.Model): class Candidate(models.Model): """ + This class is a component of responsability """ responsability = models.ForeignKey(Responsability, related_name='candidate', verbose_name=_("responsability")) - subscriber = models.ForeignKey(Subscriber, verbose_name=_('user'), related_name='candidate', blank=True) + user = models.ForeignKey(User, verbose_name=_('user'), related_name='candidate', blank=True) program = models.TextField(_('description'), null=True, blank=True) - votes = models.IntegerField(_('votes'), default=0) def __str__(self): - return ("%s : %s -> %s") % (self.election.title, self.title, self.subscriber.get_full_name()) + return ("%s : %s -> %s") % (self.election.title, self.title, self.user.get_full_name()) + + +class Vote(models.Model): + """ + This class allows to vote for candidates + """ + election = models.ForeignKey(Election, related_name='vote', verbose_name=_("election")) + candidate = models.ManyToManyField(Candidate, related_name='vote', verbose_name=_("candidate")) + user = models.ForeignKey(User, related_name='vote', verbose_name=_("user")) + + def __str__(self): + return "Vote" \ No newline at end of file diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index 798f66dc..8d1befa1 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -13,14 +13,14 @@

{% trans %}End :{% endtrans %} {{object.end_date}}

{% for resp in object.responsability.all() %}

{{resp.title}}

- {% for user in resp.candidate.all() %} -

{{user.subscriber.first_name}} {{user.subscriber.last_name}} ({{user.subscriber.nick_name}})

- {% if user.subscriber.profile_pict %} - {% trans %}Profile{% endtrans %} + {% for candidate in resp.candidate.all() %} +

{{candidate.user.first_name}} {{candidate.user.last_name}} ({{candidate.user.nick_name}})

+ {% if candidate.user.profile_pict %} + {% trans %}Profile{% endtrans %} {% endif %} - {% if user.program %} + {% if candidate.program %}

Programme

-

{{user.program}}

+

{{candidate.program}}

{% endif %} {% endfor %} {% endfor %} From d14af8e452aaad39cf25b3186df807eb49beb90a Mon Sep 17 00:00:00 2001 From: klmp200 Date: Tue, 13 Dec 2016 22:11:06 +0100 Subject: [PATCH 04/54] New bdd arch --- election/models.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/election/models.py b/election/models.py index 3cbdb707..3822e23c 100644 --- a/election/models.py +++ b/election/models.py @@ -15,8 +15,8 @@ class Election(models.Model): """ title = models.CharField(_('title'), max_length=255) description = models.TextField(_('description'), null=True, blank=True) - start_proposal = models.DateTimeField(_('start proposal'), blank=False) - end_proposal = models.DateTimeField(_('end proposal'), blank=False) + start_candidature = models.DateTimeField(_('start candidature'), blank=False) + end_candidature = models.DateTimeField(_('end candidature'), blank=False) start_date = models.DateTimeField(_('start date'), blank=False) end_date = models.DateTimeField(_('end date'), blank=False) @@ -29,22 +29,22 @@ class Election(models.Model): return bool(now <= self.end_date and now >= self.start_date) @property - def is_proposal_active(self): + def is_candidature_active(self): now = timezone.now() - return bool(now <= self.end_proposal and now >= self.start_proposal) + return bool(now <= self.end_candidature and now >= self.start_candidature) def has_voted(self, user): - return self.vote.filter(user__id=user.id).exists() + return self.has_voted.filter(id=user.id).exists() def get_results(self): pass -class Responsability(models.Model): +class Role(models.Model): """ - This class allows to create a new responsability + This class allows to create a new role avaliable for a candidature """ - election = models.ForeignKey(Election, related_name='responsability', verbose_name=_("election")) + election = models.ForeignKey(Election, related_name='role', verbose_name=_("election")) title = models.CharField(_('title'), max_length=255) description = models.TextField(_('description'), null=True, blank=True) @@ -52,25 +52,29 @@ class Responsability(models.Model): return ("%s : %s") % (self.election.title, self.title) -class Candidate(models.Model): +class Candidature(models.Model): """ This class is a component of responsability """ - responsability = models.ForeignKey(Responsability, related_name='candidate', verbose_name=_("responsability")) + role = models.ForeignKey(Role, related_name='candidature', verbose_name=_("role")) user = models.ForeignKey(User, verbose_name=_('user'), related_name='candidate', blank=True) program = models.TextField(_('description'), null=True, blank=True) + has_voted = models.ManyToManyField(User, verbose_name=_('has_voted'), related_name='has_voted') - def __str__(self): - return ("%s : %s -> %s") % (self.election.title, self.title, self.user.get_full_name()) + +class List(models.Model): + """ + To allow per list vote + """ + title = models.CharField(_('title')) class Vote(models.Model): """ This class allows to vote for candidates """ - election = models.ForeignKey(Election, related_name='vote', verbose_name=_("election")) - candidate = models.ManyToManyField(Candidate, related_name='vote', verbose_name=_("candidate")) - user = models.ForeignKey(User, related_name='vote', verbose_name=_("user")) + role = models.ForeignKey(Role, related_name='vote', verbose_name=_("role")) + candidature = models.ManyToManyField(Candidature, related_name='vote', verbose_name=_("candidature")) def __str__(self): return "Vote" \ No newline at end of file From a284637190278570f62f68e7abe199c5f339d926 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Tue, 13 Dec 2016 22:22:19 +0100 Subject: [PATCH 05/54] Normally fixs tests --- core/management/commands/populate.py | 44 ++++++++++++++-------------- election/migrations/0001_initial.py | 33 +++++++++++++-------- election/models.py | 4 +-- election/views.py | 2 +- 4 files changed, 44 insertions(+), 39 deletions(-) diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index e851732b..a43ee191 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -124,14 +124,6 @@ Welcome to the wiki page! skia.save() skia.view_groups=[Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id] skia.save() - # Adding user sli - sli = User(username='sli', last_name="Li", first_name="S", - email="sli@git.an", - date_of_birth="1942-06-12") - sli.set_password("plop") - sli.save() - skia.view_groups=[Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id] - sli.save() # Adding user public public = User(username='public', last_name="Not subscribed", first_name="Public", email="public@git.an", @@ -229,14 +221,6 @@ Cette page vise à documenter la syntaxe *Markdown* utilisée sur le site. duration=settings.SITH_SUBSCRIPTIONS[s.subscription_type]['duration'], start=s.subscription_start) s.save() - ## Sli - s = Subscription(member=Subscriber.objects.filter(pk=sli.pk).first(), subscription_type=list(settings.SITH_SUBSCRIPTIONS.keys())[0], - payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0]) - s.subscription_start = s.compute_start() - s.subscription_end = s.compute_end( - duration=settings.SITH_SUBSCRIPTIONS[s.subscription_type]['duration'], - start=s.subscription_start) - s.save() ## Comptable s = Subscription(member=User.objects.filter(pk=comptable.pk).first(), subscription_type=list(settings.SITH_SUBSCRIPTIONS.keys())[0], payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0]) @@ -327,6 +311,22 @@ Cette page vise à documenter la syntaxe *Markdown* utilisée sur le site. simple.save() woenzco = Company(name="Woenzel & co") woenzco.save() + # Adding user sli + sli = User(username='sli', last_name="Li", first_name="S", + email="sli@git.an", + date_of_birth="1942-06-12") + sli.set_password("plop") + sli.save() + skia.view_groups=[Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id] + sli.save() + ## Adding subscription for sli + s = Subscription(member=User.objects.filter(pk=sli.pk).first(), subscription_type=list(settings.SITH_SUBSCRIPTIONS.keys())[0], + payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0]) + s.subscription_start = s.compute_start() + s.subscription_end = s.compute_end( + duration=settings.SITH_SUBSCRIPTIONS[s.subscription_type]['duration'], + start=s.subscription_start) + s.save() operation_list = [ (27, "J'avais trop de bière", 'CASH', None, buying, 'USER', skia.id, "", None), @@ -349,10 +349,10 @@ Cette page vise à documenter la syntaxe *Markdown* utilisée sur le site. operation.save() # Create an election - el = Election(title="Élection 2017", description="La roue tourne", start_proposal='1942-06-12 10:28:45', end_proposal='2042-06-12 10:28:45',start_date='1942-06-12 10:28:45', end_date='7942-06-12 10:28:45') - el.save() - resp = Responsability(election=el, title="Co Respo Info", description="Ghetto++") - resp.save() - cand = Candidate(responsability=resp, user=skia) - cand.save() + # el = Election(title="Élection 2017", description="La roue tourne", start_proposal='1942-06-12 10:28:45', end_proposal='2042-06-12 10:28:45',start_date='1942-06-12 10:28:45', end_date='7942-06-12 10:28:45') + # el.save() + # resp = Responsability(election=el, title="Co Respo Info", description="Ghetto++") + # resp.save() + # cand = Candidate(responsability=resp, user=skia) + # cand.save() diff --git a/election/migrations/0001_initial.py b/election/migrations/0001_initial.py index 84363dce..9221e60e 100644 --- a/election/migrations/0001_initial.py +++ b/election/migrations/0001_initial.py @@ -13,10 +13,11 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Candidate', + name='Candidature', fields=[ ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), ('program', models.TextField(blank=True, null=True, verbose_name='description')), + ('has_voted', models.ManyToManyField(related_name='has_voted', to=settings.AUTH_USER_MODEL, verbose_name='has_voted')), ], ), migrations.CreateModel( @@ -25,38 +26,44 @@ class Migration(migrations.Migration): ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), ('title', models.CharField(max_length=255, verbose_name='title')), ('description', models.TextField(blank=True, null=True, verbose_name='description')), - ('start_proposal', models.DateTimeField(verbose_name='start proposal')), - ('end_proposal', models.DateTimeField(verbose_name='end proposal')), + ('start_candidature', models.DateTimeField(verbose_name='start candidature')), + ('end_candidature', models.DateTimeField(verbose_name='end candidature')), ('start_date', models.DateTimeField(verbose_name='start date')), ('end_date', models.DateTimeField(verbose_name='end date')), ], ), migrations.CreateModel( - name='Responsability', + name='List', + fields=[ + ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('title', models.CharField(max_length=255, verbose_name='title')), + ], + ), + migrations.CreateModel( + name='Role', fields=[ ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), ('title', models.CharField(max_length=255, verbose_name='title')), ('description', models.TextField(blank=True, null=True, verbose_name='description')), - ('election', models.ForeignKey(to='election.Election', verbose_name='election', related_name='responsability')), + ('election', models.ForeignKey(related_name='role', to='election.Election', verbose_name='election')), ], ), migrations.CreateModel( name='Vote', fields=[ ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), - ('candidate', models.ManyToManyField(to='election.Candidate', related_name='vote', verbose_name='candidate')), - ('election', models.ForeignKey(to='election.Election', verbose_name='election', related_name='vote')), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, verbose_name='user', related_name='vote')), + ('candidature', models.ManyToManyField(related_name='vote', to='election.Candidature', verbose_name='candidature')), + ('role', models.ForeignKey(related_name='vote', to='election.Role', verbose_name='role')), ], ), migrations.AddField( - model_name='candidate', - name='responsability', - field=models.ForeignKey(to='election.Responsability', verbose_name='responsability', related_name='candidate'), + model_name='candidature', + name='role', + field=models.ForeignKey(related_name='candidature', to='election.Role', verbose_name='role'), ), migrations.AddField( - model_name='candidate', + model_name='candidature', name='user', - field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='user', related_name='candidate'), + field=models.ForeignKey(blank=True, related_name='candidate', to=settings.AUTH_USER_MODEL, verbose_name='user'), ), ] diff --git a/election/models.py b/election/models.py index 3822e23c..6ed3328b 100644 --- a/election/models.py +++ b/election/models.py @@ -5,8 +5,6 @@ from django.conf import settings from datetime import timedelta from core.models import User -from subscription.models import Subscriber -from subscription.views import get_subscriber class Election(models.Model): @@ -66,7 +64,7 @@ class List(models.Model): """ To allow per list vote """ - title = models.CharField(_('title')) + title = models.CharField(_('title'), max_length=255) class Vote(models.Model): diff --git a/election/views.py b/election/views.py index 2bcfcc10..d8e91434 100644 --- a/election/views.py +++ b/election/views.py @@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy as _ from django.conf import settings from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, CanCreateMixin -from election.models import Election, Responsability, Candidate +from election.models import Election, Role, Candidature # Display elections From 52e69b0ac1a652291019ae899d47daf92cc73c25 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Wed, 14 Dec 2016 18:10:15 +0100 Subject: [PATCH 06/54] Refactor elections --- core/management/commands/populate.py | 17 +++++----- election/migrations/0001_initial.py | 34 +++++++++++-------- election/models.py | 19 ++++++----- .../templates/election/election_detail.jinja | 16 ++++----- election/urls.py | 2 +- 5 files changed, 49 insertions(+), 39 deletions(-) diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index a43ee191..61aee24e 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -15,8 +15,7 @@ from club.models import Club, Membership from subscription.models import Subscription from counter.models import Customer, ProductType, Product, Counter from com.models import Sith -from election.models import Election, Responsability, Candidate - +from election.models import Election, Role, Candidature, List class Command(BaseCommand): help = "Populate a new instance of the Sith AE" @@ -349,10 +348,12 @@ Cette page vise à documenter la syntaxe *Markdown* utilisée sur le site. operation.save() # Create an election - # el = Election(title="Élection 2017", description="La roue tourne", start_proposal='1942-06-12 10:28:45', end_proposal='2042-06-12 10:28:45',start_date='1942-06-12 10:28:45', end_date='7942-06-12 10:28:45') - # el.save() - # resp = Responsability(election=el, title="Co Respo Info", description="Ghetto++") - # resp.save() - # cand = Candidate(responsability=resp, user=skia) - # cand.save() + el = Election(title="Élection 2017", description="La roue tourne", start_candidature='1942-06-12 10:28:45', end_candidature='2042-06-12 10:28:45',start_date='1942-06-12 10:28:45', end_date='7942-06-12 10:28:45') + el.save() + liste = List(title="Candidature Libre", election=el) + liste.save() + resp = Role(election=el, title="Co Respo Info", description="Ghetto++") + resp.save() + cand = Candidature(role=resp, user=skia, liste=liste, program="Refesons le site AE") + cand.save() diff --git a/election/migrations/0001_initial.py b/election/migrations/0001_initial.py index 9221e60e..f533d513 100644 --- a/election/migrations/0001_initial.py +++ b/election/migrations/0001_initial.py @@ -15,17 +15,17 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Candidature', fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), - ('program', models.TextField(blank=True, null=True, verbose_name='description')), - ('has_voted', models.ManyToManyField(related_name='has_voted', to=settings.AUTH_USER_MODEL, verbose_name='has_voted')), + ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), + ('program', models.TextField(null=True, verbose_name='description', blank=True)), + ('has_voted', models.ManyToManyField(to=settings.AUTH_USER_MODEL, related_name='has_voted', verbose_name='has_voted')), ], ), migrations.CreateModel( name='Election', fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), ('title', models.CharField(max_length=255, verbose_name='title')), - ('description', models.TextField(blank=True, null=True, verbose_name='description')), + ('description', models.TextField(null=True, verbose_name='description', blank=True)), ('start_candidature', models.DateTimeField(verbose_name='start candidature')), ('end_candidature', models.DateTimeField(verbose_name='end candidature')), ('start_date', models.DateTimeField(verbose_name='start date')), @@ -35,35 +35,41 @@ class Migration(migrations.Migration): migrations.CreateModel( name='List', fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), ('title', models.CharField(max_length=255, verbose_name='title')), + ('election', models.ForeignKey(to='election.Election', related_name='list', verbose_name='election')), ], ), migrations.CreateModel( name='Role', fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), ('title', models.CharField(max_length=255, verbose_name='title')), - ('description', models.TextField(blank=True, null=True, verbose_name='description')), - ('election', models.ForeignKey(related_name='role', to='election.Election', verbose_name='election')), + ('description', models.TextField(null=True, verbose_name='description', blank=True)), + ('election', models.ForeignKey(to='election.Election', related_name='role', verbose_name='election')), ], ), migrations.CreateModel( name='Vote', fields=[ - ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), - ('candidature', models.ManyToManyField(related_name='vote', to='election.Candidature', verbose_name='candidature')), - ('role', models.ForeignKey(related_name='vote', to='election.Role', verbose_name='role')), + ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), + ('candidature', models.ManyToManyField(to='election.Candidature', related_name='vote', verbose_name='candidature')), + ('role', models.ForeignKey(to='election.Role', related_name='vote', verbose_name='role')), ], ), + migrations.AddField( + model_name='candidature', + name='liste', + field=models.ForeignKey(to='election.List', related_name='candidature', verbose_name='list'), + ), migrations.AddField( model_name='candidature', name='role', - field=models.ForeignKey(related_name='candidature', to='election.Role', verbose_name='role'), + field=models.ForeignKey(to='election.Role', related_name='candidature', verbose_name='role'), ), migrations.AddField( model_name='candidature', name='user', - field=models.ForeignKey(blank=True, related_name='candidate', to=settings.AUTH_USER_MODEL, verbose_name='user'), + field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, related_name='candidate', verbose_name='user'), ), ] diff --git a/election/models.py b/election/models.py index 6ed3328b..7a5902e1 100644 --- a/election/models.py +++ b/election/models.py @@ -32,7 +32,8 @@ class Election(models.Model): return bool(now <= self.end_candidature and now >= self.start_candidature) def has_voted(self, user): - return self.has_voted.filter(id=user.id).exists() + return False + # return self.has_voted.filter(id=user.id).exists() def get_results(self): pass @@ -50,6 +51,14 @@ class Role(models.Model): return ("%s : %s") % (self.election.title, self.title) +class List(models.Model): + """ + To allow per list vote + """ + title = models.CharField(_('title'), max_length=255) + election = models.ForeignKey(Election, related_name='list', verbose_name=_("election")) + + class Candidature(models.Model): """ This class is a component of responsability @@ -58,13 +67,7 @@ class Candidature(models.Model): user = models.ForeignKey(User, verbose_name=_('user'), related_name='candidate', blank=True) program = models.TextField(_('description'), null=True, blank=True) has_voted = models.ManyToManyField(User, verbose_name=_('has_voted'), related_name='has_voted') - - -class List(models.Model): - """ - To allow per list vote - """ - title = models.CharField(_('title'), max_length=255) + liste = models.ForeignKey(List, related_name='candidature', verbose_name=_('list')) class Vote(models.Model): diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index 8d1befa1..b97a5082 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -11,16 +11,16 @@

{{object.title}}

{{object.description}}

{% trans %}End :{% endtrans %} {{object.end_date}}

- {% for resp in object.responsability.all() %} -

{{resp.title}}

- {% for candidate in resp.candidate.all() %} -

{{candidate.user.first_name}} {{candidate.user.last_name}} ({{candidate.user.nick_name}})

- {% if candidate.user.profile_pict %} - {% trans %}Profile{% endtrans %} + {% for role in object.role.all() %} +

{{role.title}}

+ {% for candidature in role.candidature.all() %} +

{{candidature.user.first_name}} {{candidature.user.last_name}} ({{candidature.user.nick_name}})

+ {% if candidature.user.profile_pict %} +

{% trans %}Profile{% endtrans %}

{% endif %} - {% if candidate.program %} + {% if candidature.program %}

Programme

-

{{candidate.program}}

+

{{candidature.program}}

{% endif %} {% endfor %} {% endfor %} diff --git a/election/urls.py b/election/urls.py index 5582fedd..2e39e877 100644 --- a/election/urls.py +++ b/election/urls.py @@ -4,5 +4,5 @@ from election.views import * urlpatterns = [ url(r'^$', ElectionsListView.as_view(), name='list'), - url(r'^/(?P[0-9]+)/detail$', ElectionDetailView.as_view(), name='detail'), + url(r'^(?P[0-9]+)/detail$', ElectionDetailView.as_view(), name='detail'), ] From c604282b77e7dd1f6d34c58f715c76d9375ff084 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Mon, 19 Dec 2016 03:54:57 +0100 Subject: [PATCH 07/54] Nice display for elections --- core/management/commands/populate.py | 24 ++++++++++++ core/templates/core/user_tools.jinja | 6 +++ .../migrations/0002_auto_20161219_0002.py | 25 ++++++++++++ election/models.py | 2 +- .../templates/election/election_detail.jinja | 39 +++++++++++++------ .../templates/election/election_list.jinja | 4 +- election/views.py | 7 ++++ 7 files changed, 92 insertions(+), 15 deletions(-) create mode 100644 election/migrations/0002_auto_20161219_0002.py diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 61aee24e..2bdd2e46 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -318,6 +318,12 @@ Cette page vise à documenter la syntaxe *Markdown* utilisée sur le site. sli.save() skia.view_groups=[Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id] sli.save() + # Adding user Krophil + krophil = User(username='krophil', last_name="Phil'", first_name="Kro", + email="krophil@git.an", + date_of_birth="1942-06-12") + krophil.set_password("plop") + krophil.save() ## Adding subscription for sli s = Subscription(member=User.objects.filter(pk=sli.pk).first(), subscription_type=list(settings.SITH_SUBSCRIPTIONS.keys())[0], payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0]) @@ -326,6 +332,14 @@ Cette page vise à documenter la syntaxe *Markdown* utilisée sur le site. duration=settings.SITH_SUBSCRIPTIONS[s.subscription_type]['duration'], start=s.subscription_start) s.save() + ## Adding subscription for Krophil + s = Subscription(member=User.objects.filter(pk=krophil.pk).first(), subscription_type=list(settings.SITH_SUBSCRIPTIONS.keys())[0], + payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0]) + s.subscription_start = s.compute_start() + s.subscription_end = s.compute_end( + duration=settings.SITH_SUBSCRIPTIONS[s.subscription_type]['duration'], + start=s.subscription_start) + s.save() operation_list = [ (27, "J'avais trop de bière", 'CASH', None, buying, 'USER', skia.id, "", None), @@ -352,8 +366,18 @@ Cette page vise à documenter la syntaxe *Markdown* utilisée sur le site. el.save() liste = List(title="Candidature Libre", election=el) liste.save() + listeT = List(title="Troll", election=el) + listeT.save() + pres = Role(election=el, title="Président AE", description="Roi de l'AE") + pres.save() resp = Role(election=el, title="Co Respo Info", description="Ghetto++") resp.save() cand = Candidature(role=resp, user=skia, liste=liste, program="Refesons le site AE") cand.save() + cand = Candidature(role=resp, user=sli, liste=liste, program="Vasy je deviens mon propre adjoint") + cand.save() + cand = Candidature(role=resp, user=krophil, liste=listeT, program="Le Pôle Troll !") + cand.save() + cand = Candidature(role=pres, user=sli, liste=listeT, program="En fait j'aime pas l'info, je voulais faire GMC") + cand.save() diff --git a/core/templates/core/user_tools.jinja b/core/templates/core/user_tools.jinja index 1bbc3107..43f986bc 100644 --- a/core/templates/core/user_tools.jinja +++ b/core/templates/core/user_tools.jinja @@ -85,6 +85,12 @@
  • {{ m.club }}
  • {% endfor %} +
    +

    {% trans %}Elections{% endtrans %}

    + {% endblock %} diff --git a/election/migrations/0002_auto_20161219_0002.py b/election/migrations/0002_auto_20161219_0002.py new file mode 100644 index 00000000..5db7619c --- /dev/null +++ b/election/migrations/0002_auto_20161219_0002.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('election', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='candidature', + name='has_voted', + ), + migrations.AddField( + model_name='role', + name='has_voted', + field=models.ManyToManyField(to=settings.AUTH_USER_MODEL, related_name='has_voted', verbose_name='has voted'), + ), + ] diff --git a/election/models.py b/election/models.py index 7a5902e1..5a3e5da7 100644 --- a/election/models.py +++ b/election/models.py @@ -46,6 +46,7 @@ class Role(models.Model): election = models.ForeignKey(Election, related_name='role', verbose_name=_("election")) title = models.CharField(_('title'), max_length=255) description = models.TextField(_('description'), null=True, blank=True) + has_voted = models.ManyToManyField(User, verbose_name=('has voted'), related_name='has_voted') def __str__(self): return ("%s : %s") % (self.election.title, self.title) @@ -66,7 +67,6 @@ class Candidature(models.Model): role = models.ForeignKey(Role, related_name='candidature', verbose_name=_("role")) user = models.ForeignKey(User, verbose_name=_('user'), related_name='candidate', blank=True) program = models.TextField(_('description'), null=True, blank=True) - has_voted = models.ManyToManyField(User, verbose_name=_('has_voted'), related_name='has_voted') liste = models.ForeignKey(List, related_name='candidature', verbose_name=_('list')) diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index b97a5082..55e6603d 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -11,17 +11,34 @@

    {{object.title}}

    {{object.description}}

    {% trans %}End :{% endtrans %} {{object.end_date}}

    - {% for role in object.role.all() %} -

    {{role.title}}

    - {% for candidature in role.candidature.all() %} -

    {{candidature.user.first_name}} {{candidature.user.last_name}} ({{candidature.user.nick_name}})

    - {% if candidature.user.profile_pict %} -

    {% trans %}Profile{% endtrans %}

    - {% endif %} - {% if candidature.program %} -

    Programme

    -

    {{candidature.program}}

    - {% endif %} + + + {% set nb_list = object.list.all().count() + 1 -%} + {% for liste in object.list.all() %} + + {% set nb_list = nb_list + 1 -%} {% endfor %} + + + {% for role in object.role.all() %} + + + {% for liste in object.list.all() %} + + {% endfor %} + + {% endfor %} +
    {{liste.title}}{% trans %}Blank vote{% endtrans %}
    {{role.title}}
    +
      + {% for candidature in role.candidature.filter(liste=liste) %} +
    • + {{candidature.user.first_name}} {{candidature.user.last_name}} {{candidature.user.nick_name or ''}} + {% if candidature.user.profile_pict %} + {% trans %}Profile{% endtrans %} + {% endif %} +
    • + {% endfor %} +
    +
    {% endblock %} \ No newline at end of file diff --git a/election/templates/election/election_list.jinja b/election/templates/election/election_list.jinja index 77e4e1f9..2caa2d15 100644 --- a/election/templates/election/election_list.jinja +++ b/election/templates/election/election_list.jinja @@ -6,8 +6,6 @@ {% block content %} {% for el in object_list %} - {% if el.is_active %} -

    {{el}}

    - {% endif %} +

    {{el}}

    {% endfor %} {% endblock %} \ No newline at end of file diff --git a/election/views.py b/election/views.py index d8e91434..3ab2d51d 100644 --- a/election/views.py +++ b/election/views.py @@ -3,6 +3,7 @@ from django.views.generic import ListView, DetailView, RedirectView from django.views.generic.edit import UpdateView, CreateView, DeleteView, FormView from django.core.urlresolvers import reverse_lazy, reverse from django.utils.translation import ugettext_lazy as _ +from django.utils import timezone from django.conf import settings from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, CanCreateMixin @@ -18,6 +19,12 @@ class ElectionsListView(CanViewMixin, ListView): model = Election template_name = 'election/election_list.jinja' + def get_queryset(self): + qs = super(ElectionsListView, self).get_queryset() + today = timezone.now() + qs = qs.filter(end_date__gte=today, start_date__lte=today) + return qs + class ElectionDetailView(CanViewMixin, DetailView): """ From 2764f6d2d2f15d117d2cfee661d3bb119753469a Mon Sep 17 00:00:00 2001 From: klmp200 Date: Mon, 19 Dec 2016 16:45:38 +0100 Subject: [PATCH 08/54] Refactor List Model --- core/management/commands/populate.py | 15 +++---- election/migrations/0001_initial.py | 42 +++++++++---------- .../migrations/0002_auto_20161219_0002.py | 25 ----------- election/models.py | 12 ++++-- .../templates/election/election_detail.jinja | 11 +++-- 5 files changed, 45 insertions(+), 60 deletions(-) delete mode 100644 election/migrations/0002_auto_20161219_0002.py diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 2bdd2e46..02110ad0 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -15,7 +15,8 @@ from club.models import Club, Membership from subscription.models import Subscription from counter.models import Customer, ProductType, Product, Counter from com.models import Sith -from election.models import Election, Role, Candidature, List +from election.models import Election, Role, Candidature, ElectionList + class Command(BaseCommand): help = "Populate a new instance of the Sith AE" @@ -364,20 +365,20 @@ Cette page vise à documenter la syntaxe *Markdown* utilisée sur le site. # Create an election el = Election(title="Élection 2017", description="La roue tourne", start_candidature='1942-06-12 10:28:45', end_candidature='2042-06-12 10:28:45',start_date='1942-06-12 10:28:45', end_date='7942-06-12 10:28:45') el.save() - liste = List(title="Candidature Libre", election=el) + liste = ElectionList(title="Candidature Libre", election=el) liste.save() - listeT = List(title="Troll", election=el) + listeT = ElectionList(title="Troll", election=el) listeT.save() pres = Role(election=el, title="Président AE", description="Roi de l'AE") pres.save() resp = Role(election=el, title="Co Respo Info", description="Ghetto++") resp.save() - cand = Candidature(role=resp, user=skia, liste=liste, program="Refesons le site AE") + cand = Candidature(role=resp, user=skia, election_list=liste, program="Refesons le site AE") cand.save() - cand = Candidature(role=resp, user=sli, liste=liste, program="Vasy je deviens mon propre adjoint") + cand = Candidature(role=resp, user=sli, election_list=liste, program="Vasy je deviens mon propre adjoint") cand.save() - cand = Candidature(role=resp, user=krophil, liste=listeT, program="Le Pôle Troll !") + cand = Candidature(role=resp, user=krophil, election_list=listeT, program="Le Pôle Troll !") cand.save() - cand = Candidature(role=pres, user=sli, liste=listeT, program="En fait j'aime pas l'info, je voulais faire GMC") + cand = Candidature(role=pres, user=sli, election_list=listeT, program="En fait j'aime pas l'info, je voulais faire GMC") cand.save() diff --git a/election/migrations/0001_initial.py b/election/migrations/0001_initial.py index f533d513..bb784252 100644 --- a/election/migrations/0001_initial.py +++ b/election/migrations/0001_initial.py @@ -15,17 +15,16 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Candidature', fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), - ('program', models.TextField(null=True, verbose_name='description', blank=True)), - ('has_voted', models.ManyToManyField(to=settings.AUTH_USER_MODEL, related_name='has_voted', verbose_name='has_voted')), + ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), + ('program', models.TextField(blank=True, null=True, verbose_name='description')), ], ), migrations.CreateModel( name='Election', fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), - ('title', models.CharField(max_length=255, verbose_name='title')), - ('description', models.TextField(null=True, verbose_name='description', blank=True)), + ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), + ('title', models.CharField(verbose_name='title', max_length=255)), + ('description', models.TextField(blank=True, null=True, verbose_name='description')), ('start_candidature', models.DateTimeField(verbose_name='start candidature')), ('end_candidature', models.DateTimeField(verbose_name='end candidature')), ('start_date', models.DateTimeField(verbose_name='start date')), @@ -33,43 +32,44 @@ class Migration(migrations.Migration): ], ), migrations.CreateModel( - name='List', + name='ElectionList', fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), - ('title', models.CharField(max_length=255, verbose_name='title')), - ('election', models.ForeignKey(to='election.Election', related_name='list', verbose_name='election')), + ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), + ('title', models.CharField(verbose_name='title', max_length=255)), + ('election', models.ForeignKey(related_name='election_list', verbose_name='election', to='election.Election')), ], ), migrations.CreateModel( name='Role', fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), - ('title', models.CharField(max_length=255, verbose_name='title')), - ('description', models.TextField(null=True, verbose_name='description', blank=True)), - ('election', models.ForeignKey(to='election.Election', related_name='role', verbose_name='election')), + ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), + ('title', models.CharField(verbose_name='title', max_length=255)), + ('description', models.TextField(blank=True, null=True, verbose_name='description')), + ('election', models.ForeignKey(related_name='role', verbose_name='election', to='election.Election')), + ('has_voted', models.ManyToManyField(related_name='has_voted', to=settings.AUTH_USER_MODEL, verbose_name='has voted')), ], ), migrations.CreateModel( name='Vote', fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), - ('candidature', models.ManyToManyField(to='election.Candidature', related_name='vote', verbose_name='candidature')), - ('role', models.ForeignKey(to='election.Role', related_name='vote', verbose_name='role')), + ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), + ('candidature', models.ManyToManyField(related_name='vote', to='election.Candidature', verbose_name='candidature')), + ('role', models.ForeignKey(related_name='vote', verbose_name='role', to='election.Role')), ], ), migrations.AddField( model_name='candidature', - name='liste', - field=models.ForeignKey(to='election.List', related_name='candidature', verbose_name='list'), + name='election_list', + field=models.ForeignKey(related_name='candidature', verbose_name='election_list', to='election.ElectionList'), ), migrations.AddField( model_name='candidature', name='role', - field=models.ForeignKey(to='election.Role', related_name='candidature', verbose_name='role'), + field=models.ForeignKey(related_name='candidature', verbose_name='role', to='election.Role'), ), migrations.AddField( model_name='candidature', name='user', - field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, related_name='candidate', verbose_name='user'), + field=models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='candidate', blank=True, verbose_name='user'), ), ] diff --git a/election/migrations/0002_auto_20161219_0002.py b/election/migrations/0002_auto_20161219_0002.py deleted file mode 100644 index 5db7619c..00000000 --- a/election/migrations/0002_auto_20161219_0002.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models -from django.conf import settings - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('election', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='candidature', - name='has_voted', - ), - migrations.AddField( - model_name='role', - name='has_voted', - field=models.ManyToManyField(to=settings.AUTH_USER_MODEL, related_name='has_voted', verbose_name='has voted'), - ), - ] diff --git a/election/models.py b/election/models.py index 5a3e5da7..8e029bdf 100644 --- a/election/models.py +++ b/election/models.py @@ -38,6 +38,12 @@ class Election(models.Model): def get_results(self): pass + def can_view(self, obj): + return True + + def can_be_viewed_by(self, user): + return True + class Role(models.Model): """ @@ -52,12 +58,12 @@ class Role(models.Model): return ("%s : %s") % (self.election.title, self.title) -class List(models.Model): +class ElectionList(models.Model): """ To allow per list vote """ title = models.CharField(_('title'), max_length=255) - election = models.ForeignKey(Election, related_name='list', verbose_name=_("election")) + election = models.ForeignKey(Election, related_name='election_list', verbose_name=_("election")) class Candidature(models.Model): @@ -67,7 +73,7 @@ class Candidature(models.Model): role = models.ForeignKey(Role, related_name='candidature', verbose_name=_("role")) user = models.ForeignKey(User, verbose_name=_('user'), related_name='candidate', blank=True) program = models.TextField(_('description'), null=True, blank=True) - liste = models.ForeignKey(List, related_name='candidature', verbose_name=_('list')) + election_list = models.ForeignKey(ElectionList, related_name='candidature', verbose_name=_('election_list')) class Vote(models.Model): diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index 55e6603d..d48866eb 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -13,8 +13,8 @@

    {% trans %}End :{% endtrans %} {{object.end_date}}

    - {% set nb_list = object.list.all().count() + 1 -%} - {% for liste in object.list.all() %} + {% set nb_list = object.election_list.all().count() + 1 -%} + {% for liste in object.election_list.all() %} {% set nb_list = nb_list + 1 -%} {% endfor %} @@ -23,15 +23,18 @@ {% for role in object.role.all() %} - {% for liste in object.list.all() %} + {% for liste in object.election_list.all() %}
    {{liste.title}}
    {{role.title}}
      - {% for candidature in role.candidature.filter(liste=liste) %} + {% for candidature in role.candidature.filter(election_list=liste) %}
    • {{candidature.user.first_name}} {{candidature.user.last_name}} {{candidature.user.nick_name or ''}} {% if candidature.user.profile_pict %} +
      {% trans %}Profile{% endtrans %} {% endif %} +
      + {{candidature.program or ''}}
    • {% endfor %}
    From 7956067686649777ae66c00d22384076710b5500 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Mon, 19 Dec 2016 20:30:19 +0100 Subject: [PATCH 09/54] Election right update --- core/management/commands/populate.py | 8 +++++ election/migrations/0002_role_max_choice.py | 19 ++++++++++ .../migrations/0003_auto_20161219_1832.py | 35 +++++++++++++++++++ election/models.py | 14 ++++---- 4 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 election/migrations/0002_role_max_choice.py create mode 100644 election/migrations/0003_auto_20161219_1832.py diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 02110ad0..e69205c0 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -363,8 +363,16 @@ Cette page vise à documenter la syntaxe *Markdown* utilisée sur le site. operation.save() # Create an election + public_group = Group.objects.get(id=settings.SITH_GROUP_PUBLIC_ID) + subscriber_group = Group.objects.get(name=settings.SITH_MAIN_MEMBERS_GROUP) + ae_board_gorup = Group.objects.get(name=settings.SITH_MAIN_BOARD_GROUP) el = Election(title="Élection 2017", description="La roue tourne", start_candidature='1942-06-12 10:28:45', end_candidature='2042-06-12 10:28:45',start_date='1942-06-12 10:28:45', end_date='7942-06-12 10:28:45') el.save() + el.view_groups.add(public_group) + el.edit_groups.add(ae_board_gorup) + el.candidature_group.add(subscriber_group) + el.vote_group.add(subscriber_group) + el.save() liste = ElectionList(title="Candidature Libre", election=el) liste.save() listeT = ElectionList(title="Troll", election=el) diff --git a/election/migrations/0002_role_max_choice.py b/election/migrations/0002_role_max_choice.py new file mode 100644 index 00000000..ebce103d --- /dev/null +++ b/election/migrations/0002_role_max_choice.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('election', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='role', + name='max_choice', + field=models.IntegerField(verbose_name='max choice', default=1), + ), + ] diff --git a/election/migrations/0003_auto_20161219_1832.py b/election/migrations/0003_auto_20161219_1832.py new file mode 100644 index 00000000..55c03808 --- /dev/null +++ b/election/migrations/0003_auto_20161219_1832.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0016_auto_20161212_1922'), + ('election', '0002_role_max_choice'), + ] + + operations = [ + migrations.AddField( + model_name='election', + name='candidature_group', + field=models.ManyToManyField(related_name='candidate_election', blank=True, verbose_name='candidature group', to='core.Group'), + ), + migrations.AddField( + model_name='election', + name='edit_groups', + field=models.ManyToManyField(related_name='editable_election', blank=True, verbose_name='edit group', to='core.Group'), + ), + migrations.AddField( + model_name='election', + name='view_groups', + field=models.ManyToManyField(related_name='viewable_election', blank=True, verbose_name='view group', to='core.Group'), + ), + migrations.AddField( + model_name='election', + name='vote_group', + field=models.ManyToManyField(related_name='votable_election', blank=True, verbose_name='vote group', to='core.Group'), + ), + ] diff --git a/election/models.py b/election/models.py index 8e029bdf..7598e2a1 100644 --- a/election/models.py +++ b/election/models.py @@ -4,7 +4,7 @@ from django.utils import timezone from django.conf import settings from datetime import timedelta -from core.models import User +from core.models import User, Group class Election(models.Model): @@ -18,6 +18,11 @@ class Election(models.Model): start_date = models.DateTimeField(_('start date'), blank=False) end_date = models.DateTimeField(_('end date'), blank=False) + edit_groups = models.ManyToManyField(Group, related_name="editable_election", verbose_name=_("edit group"), blank=True) + view_groups = models.ManyToManyField(Group, related_name="viewable_election", verbose_name=_("view group"), blank=True) + vote_group = models.ManyToManyField(Group, related_name="votable_election", verbose_name=_("vote group"), blank=True) + candidature_group = models.ManyToManyField(Group, related_name="candidate_election", verbose_name=_("candidature group"), blank=True) + def __str__(self): return self.title @@ -38,11 +43,7 @@ class Election(models.Model): def get_results(self): pass - def can_view(self, obj): - return True - - def can_be_viewed_by(self, user): - return True + # Permissions class Role(models.Model): @@ -53,6 +54,7 @@ class Role(models.Model): title = models.CharField(_('title'), max_length=255) description = models.TextField(_('description'), null=True, blank=True) has_voted = models.ManyToManyField(User, verbose_name=('has voted'), related_name='has_voted') + max_choice = models.IntegerField(_('max choice'), default=1) def __str__(self): return ("%s : %s") % (self.election.title, self.title) From d72d8366cfdaf9a408b5ae89bdd83528181933c6 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Tue, 20 Dec 2016 16:00:14 +0100 Subject: [PATCH 10/54] Add new widget (not tested) and new bdd scheme for elections --- core/management/commands/populate.py | 4 +- core/widgets.py | 94 +++++++++++++++++++ .../migrations/0004_auto_20161219_2302.py | 43 +++++++++ election/models.py | 8 +- .../templates/election/election_detail.jinja | 13 +++ election/urls.py | 1 + election/views.py | 35 ++++++- 7 files changed, 191 insertions(+), 7 deletions(-) create mode 100644 core/widgets.py create mode 100644 election/migrations/0004_auto_20161219_2302.py diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index e69205c0..38ecfe10 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -370,8 +370,8 @@ Cette page vise à documenter la syntaxe *Markdown* utilisée sur le site. el.save() el.view_groups.add(public_group) el.edit_groups.add(ae_board_gorup) - el.candidature_group.add(subscriber_group) - el.vote_group.add(subscriber_group) + el.candidature_groups.add(subscriber_group) + el.vote_groups.add(subscriber_group) el.save() liste = ElectionList(title="Candidature Libre", election=el) liste.save() diff --git a/core/widgets.py b/core/widgets.py new file mode 100644 index 00000000..e1d6cd47 --- /dev/null +++ b/core/widgets.py @@ -0,0 +1,94 @@ +from django import forms +from django.utils.safestring import mark_safe + +class ChoiceWithOtherRenderer(forms.RadioSelect.renderer): + """RadioFieldRenderer that renders its last choice with a placeholder.""" + def __init__(self, *args, **kwargs): + super(ChoiceWithOtherRenderer, self).__init__(*args, **kwargs) + self.choices, self.other = self.choices[:-1], self.choices[-1] + + def __iter__(self): + for input in super(ChoiceWithOtherRenderer, self).__iter__(): + yield input + id = '%s_%s' % (self.attrs['id'], self.other[0]) if 'id' in self.attrs else '' + label_for = ' for="%s"' % id if id else '' + checked = '' if not self.other[0] == self.value else 'checked="true" ' + yield ' %s %%s' % ( + label_for, id, self.other[0], self.name, checked, self.other[1]) + +class ChoiceWithOtherWidget(forms.MultiWidget): + """MultiWidget for use with ChoiceWithOtherField.""" + def __init__(self, choices): + widgets = [ + forms.RadioSelect(choices=choices, renderer=ChoiceWithOtherRenderer), + forms.TextInput + ] + super(ChoiceWithOtherWidget, self).__init__(widgets) + + def decompress(self, value): + if not value: + return [None, None] + return value + + def format_output(self, rendered_widgets): + """Format the output by substituting the "other" choice into the first widget.""" + return rendered_widgets[0] % rendered_widgets[1] + +class ChoiceWithOtherField(forms.MultiValueField): + """ + ChoiceField with an option for a user-submitted "other" value. + + The last item in the choices array passed to __init__ is expected to be a choice for "other". This field's + cleaned data is a tuple consisting of the choice the user made, and the "other" field typed in if the choice + made was the last one. + + >>> class AgeForm(forms.Form): + ... age = ChoiceWithOtherField(choices=[ + ... (0, '15-29'), + ... (1, '30-44'), + ... (2, '45-60'), + ... (3, 'Other, please specify:') + ... ]) + ... + >>> # rendered as a RadioSelect choice field whose last choice has a text input + ... print AgeForm()['age'] +
      +
    • +
    • +
    • +
    • +
    + >>> form = AgeForm({'age_0': 2}) + >>> form.is_valid() + True + >>> form.cleaned_data + {'age': (u'2', u'')} + >>> form = AgeForm({'age_0': 3, 'age_1': 'I am 10 years old'}) + >>> form.is_valid() + True + >>> form.cleaned_data + {'age': (u'3', u'I am 10 years old')} + >>> form = AgeForm({'age_0': 1, 'age_1': 'This is bogus text which is ignored since I didn\\'t pick "other"'}) + >>> form.is_valid() + True + >>> form.cleaned_data + {'age': (u'1', u'')} + """ + def __init__(self, *args, **kwargs): + fields = [ + forms.ChoiceField(widget=forms.RadioSelect(renderer=ChoiceWithOtherRenderer), *args, **kwargs), + forms.CharField(required=False) + ] + widget = ChoiceWithOtherWidget(choices=kwargs['choices']) + kwargs.pop('choices') + self._was_required = kwargs.pop('required', True) + kwargs['required'] = False + super(ChoiceWithOtherField, self).__init__(widget=widget, fields=fields, *args, **kwargs) + + def compress(self, value): + if self._was_required and not value or value[0] in (None, ''): + raise forms.ValidationError(self.error_messages['required']) + if not value: + return [None, u''] + return (value[0], value[1] if value[0] == self.fields[0].choices[-1][0] else u'') \ No newline at end of file diff --git a/election/migrations/0004_auto_20161219_2302.py b/election/migrations/0004_auto_20161219_2302.py new file mode 100644 index 00000000..fabe3212 --- /dev/null +++ b/election/migrations/0004_auto_20161219_2302.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0016_auto_20161212_1922'), + ('election', '0003_auto_20161219_1832'), + ] + + operations = [ + migrations.RemoveField( + model_name='election', + name='candidature_group', + ), + migrations.RemoveField( + model_name='election', + name='vote_group', + ), + migrations.AddField( + model_name='election', + name='candidature_groups', + field=models.ManyToManyField(to='core.Group', verbose_name='candidature group', related_name='candidate_elections', blank=True), + ), + migrations.AddField( + model_name='election', + name='vote_groups', + field=models.ManyToManyField(to='core.Group', verbose_name='vote group', related_name='votable_elections', blank=True), + ), + migrations.AlterField( + model_name='election', + name='edit_groups', + field=models.ManyToManyField(to='core.Group', verbose_name='edit group', related_name='editable_elections', blank=True), + ), + migrations.AlterField( + model_name='election', + name='view_groups', + field=models.ManyToManyField(to='core.Group', verbose_name='view group', related_name='viewable_elections', blank=True), + ), + ] diff --git a/election/models.py b/election/models.py index 7598e2a1..f4b63b39 100644 --- a/election/models.py +++ b/election/models.py @@ -18,10 +18,10 @@ class Election(models.Model): start_date = models.DateTimeField(_('start date'), blank=False) end_date = models.DateTimeField(_('end date'), blank=False) - edit_groups = models.ManyToManyField(Group, related_name="editable_election", verbose_name=_("edit group"), blank=True) - view_groups = models.ManyToManyField(Group, related_name="viewable_election", verbose_name=_("view group"), blank=True) - vote_group = models.ManyToManyField(Group, related_name="votable_election", verbose_name=_("vote group"), blank=True) - candidature_group = models.ManyToManyField(Group, related_name="candidate_election", verbose_name=_("candidature group"), blank=True) + edit_groups = models.ManyToManyField(Group, related_name="editable_elections", verbose_name=_("edit group"), blank=True) + view_groups = models.ManyToManyField(Group, related_name="viewable_elections", verbose_name=_("view group"), blank=True) + vote_groups = models.ManyToManyField(Group, related_name="votable_elections", verbose_name=_("vote group"), blank=True) + candidature_groups = models.ManyToManyField(Group, related_name="candidate_elections", verbose_name=_("candidature group"), blank=True) def __str__(self): return self.title diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index d48866eb..89ab2042 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -11,14 +11,21 @@

    {{object.title}}

    {{object.description}}

    {% trans %}End :{% endtrans %} {{object.end_date}}

    + {% if object.election_list.exists() %} {% set nb_list = object.election_list.all().count() + 1 -%} {% for liste in object.election_list.all() %} + {% if object.is_candidature_active -%} + {% set nb_list = nb_list -%} + {% else -%} {% set nb_list = nb_list + 1 -%} + {% endif -%} {% endfor %} + {% if not object.is_candidature_active -%} + {% endif %} {% for role in object.role.all() %} @@ -40,8 +47,14 @@ {% endfor %} + {% if not object.is_candidature_active -%} + {% endif %} {% endfor %}
    {{liste.title}}{% trans %}Blank vote{% endtrans %}
    {{role.title}}
    + {% endif %} + {% if object.is_candidature_active -%} + candidature + {% endif -%} {% endblock %} \ No newline at end of file diff --git a/election/urls.py b/election/urls.py index 2e39e877..ba36777b 100644 --- a/election/urls.py +++ b/election/urls.py @@ -4,5 +4,6 @@ from election.views import * urlpatterns = [ url(r'^$', ElectionsListView.as_view(), name='list'), + url(r'^create$', PageCreateView.as_view(), name='create'), url(r'^(?P[0-9]+)/detail$', ElectionDetailView.as_view(), name='detail'), ] diff --git a/election/views.py b/election/views.py index 3ab2d51d..4dd06371 100644 --- a/election/views.py +++ b/election/views.py @@ -3,12 +3,25 @@ from django.views.generic import ListView, DetailView, RedirectView from django.views.generic.edit import UpdateView, CreateView, DeleteView, FormView from django.core.urlresolvers import reverse_lazy, reverse from django.utils.translation import ugettext_lazy as _ +from django.forms.models import modelform_factory +from django.forms import CheckboxSelectMultiple from django.utils import timezone from django.conf import settings +from django import forms from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, CanCreateMixin +from core.views.forms import SelectDateTime +from core.widgets import ChoiceWithOtherField from election.models import Election, Role, Candidature +from ajax_select.fields import AutoCompleteSelectField + +# Forms + + +class CandidateForm(forms.Form): + user = AutoCompleteSelectField('users', label=_('Refound this account'), help_text=None, required=True) + # Display elections @@ -34,4 +47,24 @@ class ElectionDetailView(CanViewMixin, DetailView): template_name = 'election/election_detail.jinja' pk_url_kwarg = "election_id" -# Forms + +class PageCreateView(CanCreateMixin, CreateView): + model = Election + form_class = modelform_factory(Election, + fields=['title', 'description', 'start_candidature', 'end_candidature', 'start_date', 'end_date', + 'edit_groups', 'view_groups', 'vote_groups', 'candidature_groups'], + widgets={ + 'edit_groups': CheckboxSelectMultiple, + 'view_groups': CheckboxSelectMultiple, + 'edit_groups': CheckboxSelectMultiple, + 'vote_groups': CheckboxSelectMultiple, + 'candidature_groups': CheckboxSelectMultiple, + 'start_date': SelectDateTime, + 'end_date': SelectDateTime, + 'start_candidature': SelectDateTime, + 'end_candidature': SelectDateTime, + }) + template_name = 'core/page_prop.jinja' + + def get_success_url(self, **kwargs): + return reverse_lazy('election:detail', kwargs={'election_id': self.object.id}) From 51bb6c84726e46dac9fa591838ce9a5086453146 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Tue, 20 Dec 2016 21:03:52 +0100 Subject: [PATCH 11/54] Can add ElectionList and start of candidature form --- election/models.py | 3 ++ .../templates/election/election_detail.jinja | 1 + election/urls.py | 5 +- election/views.py | 47 +++++++++++++++++-- 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/election/models.py b/election/models.py index f4b63b39..979c3beb 100644 --- a/election/models.py +++ b/election/models.py @@ -67,6 +67,9 @@ class ElectionList(models.Model): title = models.CharField(_('title'), max_length=255) election = models.ForeignKey(Election, related_name='election_list', verbose_name=_("election")) + def __str__(self): + return self.title + class Candidature(models.Model): """ diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index 89ab2042..5570d18c 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -54,6 +54,7 @@ {% endfor %}
    {% endif %} +
    {{candidate_form}}
    {% if object.is_candidature_active -%} candidature {% endif -%} diff --git a/election/urls.py b/election/urls.py index ba36777b..4254d1a9 100644 --- a/election/urls.py +++ b/election/urls.py @@ -1,9 +1,10 @@ -from django.conf.urls import url, include +from django.conf.urls import url from election.views import * urlpatterns = [ url(r'^$', ElectionsListView.as_view(), name='list'), - url(r'^create$', PageCreateView.as_view(), name='create'), + url(r'^create$', ElectionCreateView.as_view(), name='create'), + url(r'^list/create$', ElectionListCreateView.as_view(), name='create_list'), url(r'^(?P[0-9]+)/detail$', ElectionDetailView.as_view(), name='detail'), ] diff --git a/election/views.py b/election/views.py index 4dd06371..1561789b 100644 --- a/election/views.py +++ b/election/views.py @@ -4,6 +4,7 @@ from django.views.generic.edit import UpdateView, CreateView, DeleteView, FormVi from django.core.urlresolvers import reverse_lazy, reverse from django.utils.translation import ugettext_lazy as _ from django.forms.models import modelform_factory +from django.core.exceptions import PermissionDenied, ObjectDoesNotExist, ImproperlyConfigured from django.forms import CheckboxSelectMultiple from django.utils import timezone from django.conf import settings @@ -12,15 +13,23 @@ from django import forms from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, CanCreateMixin from core.views.forms import SelectDateTime from core.widgets import ChoiceWithOtherField -from election.models import Election, Role, Candidature +from election.models import Election, Role, Candidature, ElectionList from ajax_select.fields import AutoCompleteSelectField + # Forms class CandidateForm(forms.Form): - user = AutoCompleteSelectField('users', label=_('Refound this account'), help_text=None, required=True) + """ Form to candidate """ + user = AutoCompleteSelectField('users', label=_('User to candidate'), help_text=None, required=True) + program = forms.CharField(widget=forms.Textarea) + + def __init__(self, election_id, *args, **kwargs): + super(CandidateForm, self).__init__(*args, **kwargs) + self.fields['role'] = forms.ModelChoiceField(Role.objects.filter(election__id=election_id)) + self.fields['election_list'] = forms.ModelChoiceField(ElectionList.objects.filter(election__id=election_id)) # Display elections @@ -47,8 +56,15 @@ class ElectionDetailView(CanViewMixin, DetailView): template_name = 'election/election_detail.jinja' pk_url_kwarg = "election_id" + def get_context_data(self, **kwargs): + """ Add additionnal data to the template """ + kwargs = super(ElectionDetailView, self).get_context_data(**kwargs) + kwargs['candidate_form'] = CandidateForm(self.get_object().id) + return kwargs -class PageCreateView(CanCreateMixin, CreateView): +# Create views + +class ElectionCreateView(CanCreateMixin, CreateView): model = Election form_class = modelform_factory(Election, fields=['title', 'description', 'start_candidature', 'end_candidature', 'start_date', 'end_date', @@ -68,3 +84,28 @@ class PageCreateView(CanCreateMixin, CreateView): def get_success_url(self, **kwargs): return reverse_lazy('election:detail', kwargs={'election_id': self.object.id}) + + +class ElectionListCreateView(CanCreateMixin, CreateView): + model = ElectionList + form_class = modelform_factory(ElectionList, + fields=['title', 'election']) + template_name = 'core/page_prop.jinja' + + def form_valid(self, form): + """ + Verify that the user can vote on this election + """ + obj = form.instance + res = super(CreateView, self).form_valid + if obj.election: + for grp in obj.election.candidature_groups.all(): + if self.request.user.is_in_group(grp): + return res(form) + for grp in obj.election.edit_groups.all(): + if self.request.user.is_in_group(grp): + return res(form) + raise PermissionDenied + + def get_success_url(self, **kwargs): + return reverse_lazy('election:detail', kwargs={'election_id': self.object.election.id}) From 03754aba8a58c6bddce1072321081b1b9ec82bbc Mon Sep 17 00:00:00 2001 From: klmp200 Date: Tue, 20 Dec 2016 23:08:47 +0100 Subject: [PATCH 12/54] Can now add candidature --- .../templates/election/election_detail.jinja | 4 +- election/urls.py | 2 + election/views.py | 77 ++++++++++++++++++- 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index 5570d18c..6fa32fca 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -54,7 +54,9 @@ {% endfor %} {% endif %} -
    {{candidate_form}}
    +
    {{candidate_form}} + {% csrf_token %} +
    {% if object.is_candidature_active -%} candidature {% endif -%} diff --git a/election/urls.py b/election/urls.py index 4254d1a9..380fa1db 100644 --- a/election/urls.py +++ b/election/urls.py @@ -6,5 +6,7 @@ urlpatterns = [ url(r'^$', ElectionsListView.as_view(), name='list'), url(r'^create$', ElectionCreateView.as_view(), name='create'), url(r'^list/create$', ElectionListCreateView.as_view(), name='create_list'), + url(r'^role/create$', RoleCreateView.as_view(), name='create_role'), + url(r'^(?P[0-9]+)/candidate$', CandidatureCreateView.as_view(), name='candidate'), url(r'^(?P[0-9]+)/detail$', ElectionDetailView.as_view(), name='detail'), ] diff --git a/election/views.py b/election/views.py index 1561789b..d253e58d 100644 --- a/election/views.py +++ b/election/views.py @@ -1,4 +1,4 @@ -from django.shortcuts import render +from django.shortcuts import redirect from django.views.generic import ListView, DetailView, RedirectView from django.views.generic.edit import UpdateView, CreateView, DeleteView, FormView from django.core.urlresolvers import reverse_lazy, reverse @@ -62,8 +62,57 @@ class ElectionDetailView(CanViewMixin, DetailView): kwargs['candidate_form'] = CandidateForm(self.get_object().id) return kwargs + +# Form view + +class CandidatureCreateView(CanCreateMixin, FormView): + """ + View dedicated to a cundidature creation + """ + form_class = CandidateForm + template_name = 'core/page_prop.jinja' + + def dispatch(self, request, *arg, **kwargs): + self.election_id = kwargs['election_id'] + return super(CandidatureCreateView, self).dispatch(request, *arg, **kwargs) + + def get_form_kwargs(self): + kwargs = super(CandidatureCreateView, self).get_form_kwargs() + kwargs['election_id'] = self.election_id + return kwargs + + def create_candidature(self, data): + cand = Candidature( + role=data['role'], + user=data['user'], + election_list=data['election_list'], + program=data['program'] + ) + cand.save() + + def form_valid(self, form): + """ + Verify that the selected user is in candidate group + """ + data = form.clean() + res = super(FormView, self).form_valid(form) + data['election'] = Election.objects.get(id=self.election_id) + if data['user'].is_root: + self.create_candidature(data) + return res + for grp in data['election'].candidature_groups.all(): + if data['user'].is_in_group(grp): + self.create_candidature(data) + return res + return res + + def get_success_url(self, **kwargs): + return reverse_lazy('election:detail', kwargs={'election_id': self.election_id}) + + # Create views + class ElectionCreateView(CanCreateMixin, CreateView): model = Election form_class = modelform_factory(Election, @@ -86,6 +135,30 @@ class ElectionCreateView(CanCreateMixin, CreateView): return reverse_lazy('election:detail', kwargs={'election_id': self.object.id}) +class RoleCreateView(CanCreateMixin, CreateView): + model = Role + form_class = modelform_factory(Role, + fields=['title', 'election', 'title', 'description', 'max_choice']) + template_name = 'core/page_prop.jinja' + + def form_valid(self, form): + """ + Verify that the user can edit proprely + """ + obj = form.instance + res = super(CreateView, self).form_valid + if self.request.user.is_root: + return res(form) + if obj.election: + for grp in obj.election.edit_groups.all(): + if self.request.user.is_in_group(grp): + return res(form) + raise PermissionDenied + + def get_success_url(self, **kwargs): + return reverse_lazy('election:detail', kwargs={'election_id': self.object.election.id}) + + class ElectionListCreateView(CanCreateMixin, CreateView): model = ElectionList form_class = modelform_factory(ElectionList, @@ -99,6 +172,8 @@ class ElectionListCreateView(CanCreateMixin, CreateView): obj = form.instance res = super(CreateView, self).form_valid if obj.election: + if self.request.user.is_root: + return res(form) for grp in obj.election.candidature_groups.all(): if self.request.user.is_in_group(grp): return res(form) From e8ead338d0d68a531582fbd2de96a822a0960bd3 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Wed, 21 Dec 2016 17:33:16 +0100 Subject: [PATCH 13/54] Removed useless widget added previously + began voteform --- core/widgets.py | 94 ---------------------------------------------- election/models.py | 3 ++ election/urls.py | 1 + election/views.py | 58 +++++++++++++++++++++++++++- 4 files changed, 61 insertions(+), 95 deletions(-) delete mode 100644 core/widgets.py diff --git a/core/widgets.py b/core/widgets.py deleted file mode 100644 index e1d6cd47..00000000 --- a/core/widgets.py +++ /dev/null @@ -1,94 +0,0 @@ -from django import forms -from django.utils.safestring import mark_safe - -class ChoiceWithOtherRenderer(forms.RadioSelect.renderer): - """RadioFieldRenderer that renders its last choice with a placeholder.""" - def __init__(self, *args, **kwargs): - super(ChoiceWithOtherRenderer, self).__init__(*args, **kwargs) - self.choices, self.other = self.choices[:-1], self.choices[-1] - - def __iter__(self): - for input in super(ChoiceWithOtherRenderer, self).__iter__(): - yield input - id = '%s_%s' % (self.attrs['id'], self.other[0]) if 'id' in self.attrs else '' - label_for = ' for="%s"' % id if id else '' - checked = '' if not self.other[0] == self.value else 'checked="true" ' - yield ' %s %%s' % ( - label_for, id, self.other[0], self.name, checked, self.other[1]) - -class ChoiceWithOtherWidget(forms.MultiWidget): - """MultiWidget for use with ChoiceWithOtherField.""" - def __init__(self, choices): - widgets = [ - forms.RadioSelect(choices=choices, renderer=ChoiceWithOtherRenderer), - forms.TextInput - ] - super(ChoiceWithOtherWidget, self).__init__(widgets) - - def decompress(self, value): - if not value: - return [None, None] - return value - - def format_output(self, rendered_widgets): - """Format the output by substituting the "other" choice into the first widget.""" - return rendered_widgets[0] % rendered_widgets[1] - -class ChoiceWithOtherField(forms.MultiValueField): - """ - ChoiceField with an option for a user-submitted "other" value. - - The last item in the choices array passed to __init__ is expected to be a choice for "other". This field's - cleaned data is a tuple consisting of the choice the user made, and the "other" field typed in if the choice - made was the last one. - - >>> class AgeForm(forms.Form): - ... age = ChoiceWithOtherField(choices=[ - ... (0, '15-29'), - ... (1, '30-44'), - ... (2, '45-60'), - ... (3, 'Other, please specify:') - ... ]) - ... - >>> # rendered as a RadioSelect choice field whose last choice has a text input - ... print AgeForm()['age'] -
      -
    • -
    • -
    • -
    • -
    - >>> form = AgeForm({'age_0': 2}) - >>> form.is_valid() - True - >>> form.cleaned_data - {'age': (u'2', u'')} - >>> form = AgeForm({'age_0': 3, 'age_1': 'I am 10 years old'}) - >>> form.is_valid() - True - >>> form.cleaned_data - {'age': (u'3', u'I am 10 years old')} - >>> form = AgeForm({'age_0': 1, 'age_1': 'This is bogus text which is ignored since I didn\\'t pick "other"'}) - >>> form.is_valid() - True - >>> form.cleaned_data - {'age': (u'1', u'')} - """ - def __init__(self, *args, **kwargs): - fields = [ - forms.ChoiceField(widget=forms.RadioSelect(renderer=ChoiceWithOtherRenderer), *args, **kwargs), - forms.CharField(required=False) - ] - widget = ChoiceWithOtherWidget(choices=kwargs['choices']) - kwargs.pop('choices') - self._was_required = kwargs.pop('required', True) - kwargs['required'] = False - super(ChoiceWithOtherField, self).__init__(widget=widget, fields=fields, *args, **kwargs) - - def compress(self, value): - if self._was_required and not value or value[0] in (None, ''): - raise forms.ValidationError(self.error_messages['required']) - if not value: - return [None, u''] - return (value[0], value[1] if value[0] == self.fields[0].choices[-1][0] else u'') \ No newline at end of file diff --git a/election/models.py b/election/models.py index 979c3beb..8e3fbc92 100644 --- a/election/models.py +++ b/election/models.py @@ -80,6 +80,9 @@ class Candidature(models.Model): program = models.TextField(_('description'), null=True, blank=True) election_list = models.ForeignKey(ElectionList, related_name='candidature', verbose_name=_('election_list')) + def __str__(self): + return "%s : %s" % (self.role.title, self.user.username) + class Vote(models.Model): """ diff --git a/election/urls.py b/election/urls.py index 380fa1db..59ad8a42 100644 --- a/election/urls.py +++ b/election/urls.py @@ -8,5 +8,6 @@ urlpatterns = [ url(r'^list/create$', ElectionListCreateView.as_view(), name='create_list'), url(r'^role/create$', RoleCreateView.as_view(), name='create_role'), url(r'^(?P[0-9]+)/candidate$', CandidatureCreateView.as_view(), name='candidate'), + url(r'^(?P[0-9]+)/vote$', VoteFormView.as_view(), name='vote'), url(r'^(?P[0-9]+)/detail$', ElectionDetailView.as_view(), name='detail'), ] diff --git a/election/views.py b/election/views.py index d253e58d..e11955a0 100644 --- a/election/views.py +++ b/election/views.py @@ -11,8 +11,8 @@ from django.conf import settings from django import forms from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, CanCreateMixin +from django.views.generic.edit import FormMixin from core.views.forms import SelectDateTime -from core.widgets import ChoiceWithOtherField from election.models import Election, Role, Candidature, ElectionList from ajax_select.fields import AutoCompleteSelectField @@ -31,6 +31,25 @@ class CandidateForm(forms.Form): self.fields['role'] = forms.ModelChoiceField(Role.objects.filter(election__id=election_id)) self.fields['election_list'] = forms.ModelChoiceField(ElectionList.objects.filter(election__id=election_id)) + +class VoteForm(forms.Form): + def __init__(self, role_id, *args, **kwargs): + super(VoteForm, self).__init__(*args, **kwargs) + self.max_choice = Role.objects.get(id=role_id).max_choice + cand = Candidature.objects.filter(role__id=role_id) + if self.max_choice > 1: + self.fields['candidature'] = forms.ModelMultipleChoiceField(cand, required=False, + widget=forms.CheckboxSelectMultiple()) + else: + self.fields['candidature'] = forms.ModelChoiceField(cand, required=False, + widget=forms.RadioSelect(), empty_label=_("Blank vote")) + + def clean_candidature(self): + data = self.cleaned_data['candidature'] + if self.max_choice > 1 and len(data) > self.max_choice: + raise forms.ValidationError(_("You have selected too much candidate")) + return data + # Display elections @@ -110,6 +129,43 @@ class CandidatureCreateView(CanCreateMixin, FormView): return reverse_lazy('election:detail', kwargs={'election_id': self.election_id}) +class VoteFormView(CanCreateMixin, FormView): + """ + Alows users to vote + """ + form_class = VoteForm + template_name = 'core/page_prop.jinja' + + def dispatch(self, request, *arg, **kwargs): + self.election_id = kwargs['election_id'] + return super(VoteFormView, self).dispatch(request, *arg, **kwargs) + + def vote(self, data): + pass + + def get_form_kwargs(self): + kwargs = super(VoteFormView, self).get_form_kwargs() + kwargs['role_id'] = self.election_id + return kwargs + + def form_valid(self, form): + """ + Verify that the user is part in a vote group + """ + data = form.clean() + res = super(FormView, self).form_valid(form) + if self.request.user.is_root: + self.vote(data) + return res + for grp in data['role'].election.vote_groups.all(): + if self.request.user.is_in_group(grp): + self.vote(data) + return res + return res + + def get_success_url(self, **kwargs): + return reverse_lazy('election:detail', kwargs={'election_id': self.election_id}) + # Create views From 521b61517b45fde19eeed3e80b344d5ce52409dd Mon Sep 17 00:00:00 2001 From: klmp200 Date: Thu, 22 Dec 2016 00:15:16 +0100 Subject: [PATCH 14/54] Functionnal vote form --- election/models.py | 10 +++++---- election/views.py | 54 +++++++++++++++++++++++++++++++--------------- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/election/models.py b/election/models.py index 8e3fbc92..c972363a 100644 --- a/election/models.py +++ b/election/models.py @@ -36,13 +36,12 @@ class Election(models.Model): now = timezone.now() return bool(now <= self.end_candidature and now >= self.start_candidature) - def has_voted(self, user): - return False - # return self.has_voted.filter(id=user.id).exists() - def get_results(self): pass + def has_voted(self, user): + return False + # Permissions @@ -56,6 +55,9 @@ class Role(models.Model): has_voted = models.ManyToManyField(User, verbose_name=('has voted'), related_name='has_voted') max_choice = models.IntegerField(_('max choice'), default=1) + def user_has_voted(self, user): + return not self.has_voted.filter(id=user.id).exists() + def __str__(self): return ("%s : %s") % (self.election.title, self.title) diff --git a/election/views.py b/election/views.py index e11955a0..7acdde9b 100644 --- a/election/views.py +++ b/election/views.py @@ -1,4 +1,4 @@ -from django.shortcuts import redirect +from django.shortcuts import redirect, get_object_or_404 from django.views.generic import ListView, DetailView, RedirectView from django.views.generic.edit import UpdateView, CreateView, DeleteView, FormView from django.core.urlresolvers import reverse_lazy, reverse @@ -18,6 +18,29 @@ from election.models import Election, Role, Candidature, ElectionList from ajax_select.fields import AutoCompleteSelectField +# Custom form field + +class VoteCheckbox(forms.ModelMultipleChoiceField): + """ + Used to replace ModelMultipleChoiceField but with + automatic backend verification + """ + def __init__(self, queryset, max_choice, required=True, widget=None, label=None, + initial=None, help_text='', *args, **kwargs): + self.max_choice = max_choice + widget = forms.CheckboxSelectMultiple + super(VoteCheckbox, self).__init__(queryset, None, required, widget, label, + initial, help_text, *args, **kwargs) + + def clean(self, value): + qs = super(VoteCheckbox, self).clean(value) + self.validate(qs) + + def validate(self, qs): + if qs.count() > self.max_choice: + raise forms.ValidationError(_("You have selected too much candidate")) + + # Forms @@ -33,22 +56,17 @@ class CandidateForm(forms.Form): class VoteForm(forms.Form): - def __init__(self, role_id, *args, **kwargs): + def __init__(self, election, user, *args, **kwargs): super(VoteForm, self).__init__(*args, **kwargs) - self.max_choice = Role.objects.get(id=role_id).max_choice - cand = Candidature.objects.filter(role__id=role_id) - if self.max_choice > 1: - self.fields['candidature'] = forms.ModelMultipleChoiceField(cand, required=False, - widget=forms.CheckboxSelectMultiple()) - else: - self.fields['candidature'] = forms.ModelChoiceField(cand, required=False, - widget=forms.RadioSelect(), empty_label=_("Blank vote")) + for role in election.role.all(): + if role.user_has_voted(user): + cand = role.candidature + if role.max_choice > 1: + self.fields[role.title] = VoteCheckbox(cand, role.max_choice, required=False) + else: + self.fields[role.title] = forms.ModelChoiceField(cand, required=False, + widget=forms.RadioSelect(), empty_label=_("Blank vote")) - def clean_candidature(self): - data = self.cleaned_data['candidature'] - if self.max_choice > 1 and len(data) > self.max_choice: - raise forms.ValidationError(_("You have selected too much candidate")) - return data # Display elections @@ -138,14 +156,16 @@ class VoteFormView(CanCreateMixin, FormView): def dispatch(self, request, *arg, **kwargs): self.election_id = kwargs['election_id'] + self.election = get_object_or_404(Election, pk=self.election_id) return super(VoteFormView, self).dispatch(request, *arg, **kwargs) def vote(self, data): - pass + print(data) def get_form_kwargs(self): kwargs = super(VoteFormView, self).get_form_kwargs() - kwargs['role_id'] = self.election_id + kwargs['election'] = self.election + kwargs['user'] = self.request.user return kwargs def form_valid(self, form): From e6b37ef3326b88cf71430b9dd4191c4777159de3 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lenglet Date: Tue, 20 Dec 2016 16:03:26 +0100 Subject: [PATCH 15/54] Added date ranges in the elections list view. --- .../templates/election/election_list.jinja | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/election/templates/election/election_list.jinja b/election/templates/election/election_list.jinja index 2caa2d15..cad4b5e3 100644 --- a/election/templates/election/election_list.jinja +++ b/election/templates/election/election_list.jinja @@ -1,11 +1,27 @@ {% extends "core/base.jinja" %} {% block title %} -{% trans %}Election list{% endtrans %} -{% endblock %} +{%- trans %}Election list{% endtrans %} +{%- endblock %} + +{% block head %} +{{ super() -}} + +{%- endblock %} {% block content %} - {% for el in object_list %} -

    {{el}}

    - {% endfor %} -{% endblock %} \ No newline at end of file +

    {% trans %}Current elections{% endtrans %}

    +
    +
      + {%- for election in object_list %} +

      + {{election}} + {% trans %}From{% endtrans %} {{ election.start_date|date("l d F Y") }} {% trans %}to{% endtrans %} {{ election.end_date|date("l d F Y") }} +

      + {%- endfor %} +
    +{%- endblock %} \ No newline at end of file From 2095dd621ec0271a69648b0ae8a52032ca83e91c Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lenglet Date: Tue, 20 Dec 2016 21:15:40 +0100 Subject: [PATCH 16/54] Reworked election list view with datetime ranges and a description. --- .../templates/election/election_list.jinja | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/election/templates/election/election_list.jinja b/election/templates/election/election_list.jinja index cad4b5e3..3cceb2dc 100644 --- a/election/templates/election/election_list.jinja +++ b/election/templates/election/election_list.jinja @@ -10,18 +10,34 @@ small { font-size: smaller; } + + time { + font-weight: bolder; + } {%- endblock %} {% block content %}

    {% trans %}Current elections{% endtrans %}

    -
    -
      {%- for election in object_list %} +
      +
      +

      + {{ election }} +

      - {{election}} - {% trans %}From{% endtrans %} {{ election.start_date|date("l d F Y") }} {% trans %}to{% endtrans %} {{ election.end_date|date("l d F Y") }} + {% trans %}Applications open from{% endtrans %} + at + {% trans %}to{% endtrans %} + at

      +

      + {% trans %}Polls open from{% endtrans %} + at + {% trans %}to{% endtrans %} + at +

      +

      {{ election.description }}

      +
      {%- endfor %} -
    {%- endblock %} \ No newline at end of file From 6784d664030cd0cbe5a5211a55f27e0ae9be9148 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lenglet Date: Wed, 21 Dec 2016 18:19:43 +0100 Subject: [PATCH 17/54] Building the new vote/display for a election. --- .../templates/election/election_detail.jinja | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index 6fa32fca..c9205da2 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -1,10 +1,65 @@ {% extends "core/base.jinja" %} {% block title %} -{% trans %}Election list{% endtrans %} +{{object.title}} {% endblock %} +{% block head %} +{{ super() -}} + +{%- endblock %} + {% block content %} +

    {{ object.title }}

    +
    +
    +

    + {% trans %}Polls close {% endtrans %} + at +

    +
    +
    + + {%- set election_lists = object.election_list.all() -%} + + + {%- for election_list in election_lists %} + + {%- endfor %} + + {%- for role in object.role.all() %} + + + + {%- for election_list in election_lists %} + + {%- endfor %} + + + {%- endfor %} +
    {{election_list.title}}
    {{role.title}}
    +
      + {%- for candidature in election_list.candidature.filter(role=role) %} +
    • +
      + {%- if candidature.user.profile_pict %} + {% trans %}Profile{% endtrans %} + {%- endif %} +
      + {{ candidature.user.first_name }} {{candidature.user.nick_name or ''}} {{ candidature.user.last_name }} + {{ candidature.program or '' }} +
      +
      +
    • + {%- endfor %} +
    +
    +
    + {% if object.has_voted(request.user) %} A voté {% endif %} From 94d15684b71cf02f700dfa79f929663c95a87eb7 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lenglet Date: Wed, 21 Dec 2016 21:17:31 +0100 Subject: [PATCH 18/54] Added profile pictures for users --- core/fixtures/images/3.jpg | Bin 0 -> 25106 bytes core/fixtures/images/5.jpg | Bin 0 -> 23240 bytes core/fixtures/images/6.jpg | Bin 0 -> 33405 bytes core/fixtures/images/8.jpg | Bin 0 -> 23605 bytes core/management/commands/populate.py | 38 +++++++++++++++- .../templates/election/election_detail.jinja | 43 ++++++++++++++---- 6 files changed, 71 insertions(+), 10 deletions(-) create mode 100644 core/fixtures/images/3.jpg create mode 100644 core/fixtures/images/5.jpg create mode 100644 core/fixtures/images/6.jpg create mode 100644 core/fixtures/images/8.jpg diff --git a/core/fixtures/images/3.jpg b/core/fixtures/images/3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0e6a682ee1d515b8131b9a93e86123ff0b7bb50f GIT binary patch literal 25106 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

    x3EO!;5a-tSJ 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�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 literal 0 HcmV?d00001 diff --git a/core/fixtures/images/6.jpg b/core/fixtures/images/6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a5b1e8e55168b97da680d0418abdbb204dd431ac GIT binary patch literal 33405 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 I*5Aed0l>i(9smFU literal 0 HcmV?d00001 diff --git a/core/fixtures/images/8.jpg b/core/fixtures/images/8.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82df15f2764489740d8e97f3d99af65b339e9d6b GIT binary patch literal 23605 zcmb5UV|*pgw=cY7+s2M<+qP|cVq;?4$;7rbu{E)0Vq;<_H@~Cjo^$^9#nZb#{i17k zt*TXv-?cthKeqwMG7{1f01yxmfYjFy@VNmH1%QEq{&Rm_;9oZcGz0`VI0P&d6eKh} zEId3MEF2sHA}TThA_^iL95N;{3K}{F1_nG57B(h2HYz#>`ahF^fPH-f4gmuJ0fUYJ zhk*Y7`}*typg@BdfEa**pa4KoK)_HyK8FAV000O$*wLw`-d5fGdqg#g1v8axgrK|OOLB&sS6 zPTZ{gS$g?2JLZH;=otn|OsqMQ8D4ooXauCDTK%>^+K~cP)J1x?@=3gC^U()7M!K15 z({ogoHY`v6z49)`O(8{B;zv0MfEXO`#gZ_{Zb!C|zT={?Fy|h+y3pC!!x2u*#9f56 zBIf5zXdAcPBJ!B;Zb4}sa>2?GAbk`fB5&J{!xDR>kw9ifHo2cL{>}is&WUHbM5393 zQQ1@+#$ZuB55Fo33E0w-OK0+-%7gR)vM`S;D?7f${jg|*6rmM$-;BRhl>+1}Ok8T* z3K9@kvtr|1V7$p>+oG(=Kv0j%)9lDToZKZ&cqm`*(w!jQQMXP9Y=k4vQVGaR=+jJW z_U`wj2r{J!&e!O+Rh&p9xY`~LKNKy;MVo$ zMkKU+L7wOqzql5Tk2zjW0}z7#ch1>{_V=WCfyFnCUJ4aPz3+kxr1EP@z?g+Cd(Knj>(4lr_)!+~< z+JtgFtDqj#b1k-agL1L8q8fL}K)W6Do8CWa^$$}}0EC1#$P%d+L}{@SSmCB~%q4g6 zOmf@obpADzeOxGx3$zeYsa6YW;M{?FdNwU05(>P4FtuoS60HWHl*nE`w1i~yMj?Ds z`0;nM0B?|`KA&G^-^{Jw4&T(aV+UU3gjUBue?so)>%u{`_3USu?OWA_pAZEbnHlDg z5asy8kr+LX(4PTz1yU2BX6SqC)wW~8XW(BD<>32C#7ZK+2;il>^LyGB~OfNc)|5MC!_%6JzL#n zLY*giYt=sXJM}P5OsV-%w##-dt&m@Lw1z2@6uNN*#Rj{-ZCvySqWV!OD_UK|b$rB@RUnrx2Wpz2=@=|-2tmR7%pXKI^* zAy$Ct779aH@au(X#O{aYUo1ktG*3~;?o&J`36Nfl#&-7C`2W=7e`7V|C1Q!FHdNA# zE>quav_8EYLrD#o%!Dn6==KG_@ll-!CMA4Qoa;J=Y~>hNDnIUPrnGmI zD86NE|mrvyFuxA5)+c>o;}At z0h|Pi=@5;DiM0pGEO@)>Q@(K`KA@=oVJrVboWp~JzKvS@u|h(f>3OqdIlb+?zfbB^ zNaMy4YjldIL*H>(;QWy|;(6d$tSVN{jBr|x2Jk;<^oyKx*$QDA^(>|_ zxTrpwsg%QgCy{qR%^jo_2A z-cpj2noa=wAwYc@wt5jrp7cIq>{De2m|z!`iek)qn+ggJ9a=)r4`>CM-{GB#g6!Bh zKtJ-~&dTJZ8Zn5{H@hP6RHDKvR>>qvJPeTwlrgo8iFpEbA1oeOW94SMhm;Lfw$6{X z2Z%PrkyM4lfyOb@bD9`bSgAe|;(rT1{)-)8d0jFRh$fRquIbq1SZ8NQ-8DKTWVSLi zppP%p^~d$3nx?D9&Dr%~8w$}RqV$T2#&!K#IhcE8SUoKwvU7d7zwy=kjl_LiMM?3; zKzNQZ#_@^k4~9X2#=u}Kg3X{wbuLg31L;6W0xoJAg0+@l5O|JVWE38hIS`x(7a``q zp=9vPp2W2B7|%3VRFet_(+L=4rC3VPhETy36|85GjYq6o`df|&=AcHHpugJT+TXo!&+nsGEVcDU+F0#aP4d`wnK(xgH{lSoERjAp<e$1gUjM7h-A>14m_+fEm}& zy4#KgpQsDZO_fC!;P<5JhoC8U^h3uS53i0uDc9P!h>T4Gm4BvrWML^0Lbz5jr4bYt zIg+#E)3wBQt|#o?UUg0w*a&t z17qMqqyQ4Y!Z2XPCeV62&@=(L`j+Nyy z%(rnQrnVGA%CY4Rp=wRcOU@^iN%0{ly!JkS)yxy;yO1!~MhN!E{$tsv|Bq#Z5`coq zU#z0V9NZQ-dMF9BFZYO)#TJ=IYoIGlF63jF!8IU5shbR4P^N=}pYu3tka_JRcj9AA zBCb?|^l1Z}AhaN!h%o>QB`reqzYwUv1*9Ok0V~8nZ%|4bC1fdfbvmJIR|swKel@e& z1DF;{aZw8jj)MFzQy@^&Ng|Eyo#^C%fn&aUf)I}C?i_CLCnY=mMU%cW0H#c}<0?&& z6`Pvuznt|y;q*WJ1}S?u{`0q1A9$=5a9!gas=Zs@4^Br_krKKIqN15}?hPm-f2Lzr zP}L(P~oMEseA}s8_CzeVFB^8`~VJnUuM9M`%9|d;bLZxvGU^m0e z_yuH%iRm3OfiA<$g?&6s8ZQ~%D7Ie#&M0s@OKK^FX9 zQuCjCV~5-lex7AG^^1xXtkSuMmAXa`A5{-U**7=d88}@NcbRmy?PrO0nUsTEY3tEN zp>K#s8|ptB$5V_B_9_24!oC`d=jx^h_yq9$>ERScEh5S1H@^Pk{WB-vS@L(;aCkC9 z0mCkSe;CL1ifEar&%5-l=~OGx5BSM2D2Ep<%G9jASF{oRMvh+LKi|0mctS2se;(M6 zqqMvPO?i4e-#Q*{JyKs|m9M{x{!K>w1n_+VPPZPZ{$LzRjGj{N5E2Gl<@pwQZ06DN z|Jw_4i1!Y`N*X>bK^A6W?r0?3Ov88Gl&kRa!F#X_D#YB|-YSpHqM`Wmja#&%2`r>Z zl*3WK2@ggUj$Y+5+(PGhXGhFbZpEn_-w}M1=K-PVSVnc;$$K}$LQE|e5i(tOuZ{)v1 z)AZ|D=oUBv`gs!Ek9T3sOwNMQN0W7rWX2UlnTYh;i&|;FjQg#o)#Xy|*tP@BGSY24 z5wP%dnynW8Py^X{&~jRfCRZ+A;R6#1ftrGf;-yV~!^`>(ZAwZ-w=ej`Y@IbOCM6dj zaGwBjNLuF8=WL?Y-S&xGZ4v&$>GBhm%L&$t5$)I!i!$c*>9Rpw??)uc5B8dm@(zEgbsclk zrq=1K$a*%p-nrt}aWy|#{TCdKbg9@^of%Y=F1-=mWU9S}+b)@vbmh$tGYcIzRMzJ1 zBVxSv=BZO{MdbJ4x{so|=a^oP`8&d`vLjOZ+by9sW_Zm{K#=GuZSRa^y~wWgI@^xB;pMYm=ET!Mia`8rOZ-s3(9W7ydn3T_Ib3|L(MT?qM z-BJ7k;Rj@RX48q?M6#s`cYZ)YBRC6wdDkBJ0QWhw*bRRxs|IYJPe7_nfN9#mX;sU9 zjH7J9>GWK!p9SP zAD2&0Ck>|gGRTXUZmh=pTkL9dVBBdY^clqU93qfd#DBI-D6iO8?wBp-Woy9E^@>It zd%jWFv7#3Z;VdT=B;E8LCYw*s)xpk20%LRVd5cO~nN}p5a>$FduH@Vf;p>)MQvN`O zDjaoc!~6XU{|)R(_JTjU{i*CryYfQ! z?fPb`U;GobJCa>2G}fqZvl!=qm4qdcnywu&PIUHmGI@r3ymvfmw=$v#pGL}PxEOyc zEqzS}7lk2(DejP@x`>fQx)kPiT@R%K`Tf}PA`U3@xZjr>{;j&OUCb8*m?(IHOuFNu zwIlP!;S#?Q)ZH@HMO8ybsfe3x8YP4rvwU8{=Bz9k+b{-3Bp-LQkx%#%V&n>F;SQ z8fjUZB2#6jQ;HDA2suJTD%E9RcvAMdibYZzVR|}8LVOwr@H54j^4?FO{G(Fm@%zU= zCOlgp*)zVC*c*^598ThE(b1Je7W>G3%l~lMu@(zBvCzes}H6&G&s32-+lbhjtK zXG5=FSQsGp zgDWcr9nCM6aMM?;(2Y&D1R6EzLg8$;>*7db*MryPIM_5lS^iLs>OW>W=TF2tsSDnt zq^oYSL6NYIZ9fKGn2{@y-uJ$S&&_?g;@oy9+!I@8%W%8!PXDcDTAof-f8!J2s2cHev0{DH#{YVh zJGw1z{+dqw>%EAN|nl5o$Yf)yr2qpbvbt`LRW0V|M)?35QmgT4_w*NYN@;0R7-p3F2{OmmX?8$>h{rt zTcC&HfMQnAu*JK;M@U4?w(AvqcO-zANO{`pz_Ezln6=rgYsf0wQ64z=VGaxD35E(| z0eE`Ut+;OJ|AJX`;WS8JcqekMC^2a3Qhfag2MMTDZF%&a(H`pZxWFtzc3o_K;>TdP z-%y~28}62k9lypjs8B4pI~GNld^FrXlhtldp20+DX}H{Mcd#6)aEdt#9}ZTV60;ws zj=b~RE8D}p8jn8dae+cJt~~Yb73oyV)y3hAr~LC3CQ({eKP9hehE$jb#ba3`f*b59 z+dB6uws=9}lkG9!egeif(DvvG_~GR5uryW8jLPV-w-!O0s2fj=M%0yU?AUab%QYl8 zWK>qr%yZeMkn&)g27ihQBnK|Jv@-g6S}rcNAI78?+{<9bj(8zx&ZnJtGE1gS?wak1 zw*(hf{AXnOeD1v2*;Jib`pKKi#bs$1yWi$L;w35S%e>sAc3aIU#lyJs)mCp=UzJZi zOtH`#EVM?ayX1lg>nGScm(MgpA`cx=_HDe08>^7)*kK`r&}=$=>u>5j z7tzzvt5O-WIJT^SjC6DK0BRy0z-apnU;o>t3a*71I%5_&j^+jTrBG zrm~?rc8@v&W8jEG5Dp=2N`UD``ow&X91cp?Bq3KM!BCq6nbcYYMC4|UkKyqQvLYUv zvvlIQ)mzT`1jHj%l<58~^^*>l;2dKtt?7jo6rD7`W-)J8`_H`M-%1ewk!^q>p%&ao zX0cmZ_kWOI&-nK+e(aJjyp%mdx@37^LGMkVbk6-^%sBJl$0ZEYAcfv-E;g5iNV923 zm&)vkb{&5w_`#e`rhs}-F zYqBxvOKXdK2$pXRf%|9~hkTnh0ttT&4F{YD#}6>j^k=1;_=`=mfwS!tj+0G|`8We* zS3g{1f1B3A>5ak#>l}7Wa}ckM3=ybZIB+=zvo!SIIbG_?UEAaoukGQC*l;dint_st6r3h)>rG zfbYJi^t>i_HE8M#Elnh+oNE6B1UzuRCj8~!_D>yu(d-hVsiJM{-w%sBMz^wKvaZY<8!r*JMT9+*GX}jOCW)$|A(lf&nD5O zh~h2rrc)jUEK$%>{2o*#Yp>z34k?*4;WiG~w0RbEs7F3!0sOu!t_?_f4V`l0a4%Zo zXjrt8Ny$3@E0TC0PxV&1fji1ltQE&;7-7C?B{W;z;8{D<^ASJ;iMh zLEq?S@;BGND?Z8wZm|BKF76vo7*-w5va?WPt2$v zXvlQpFTPi&3vXT%>!9PW18lzAkf^8?mk}OEO435jDlEOHUi;=|T*J?$QRzn)gp8J_%1J@v8OklNVfQ`7r)==D%xtikGr`lz7)jZ@zQ zPLC_N2bV>jz`pqvIKS_gYoTAfCpU2OfTkX$-u*Iw;$H7Z*?l}Wsv`2MYmvU1x-<2Z zs_uE;UZt(Z5H`hAAmhRM{1YHs@0(ACCXeA@GQ?>%L`zCo(l<|m3bJWGQ#zFF0~aV@ zce>@TGd!|x_`NG2`B(Ea@9IuxnY;b#n%CS+gc7t&I|^%@(06?W)+>}eDI4qEs$eV2 z*d|e0II`H?i*Hg5;I6a|vD&%{?A8LxQ8nz!_WqXBar1tu4Y{eN>={4d#5u^5OPqbb z4J-i5gE%+SR)`TnHlcB|>&jj{DvoX3PgAw2mTvW%?mhuT*IqLfrAr0;zYI=hzdP{h zY`@#~lnnIu%c9=!Gl;BR3O0B9!=nKPOX{jpR@35#hc-?hyFzprhi(@l8}}URYVJ;1 zYMlAV-#@~PMQggu78ELlUqrBdh|W7piGAA! z&T%zwmDuV8K@CMuQbd;suomtFAt}>jJ7`vE{x>Lx3>v-)4~$ejND?exdN1ksA90?u zeOVm+1gu87URHdFNRk`B4RGxyORV{hYj&KVKeIR!scBow<6+aWs$g>VyGr}eY zxapzFESS(&R&bL+{;?=AV5gL?5Z{9+iL7bV<{NgHXp|t=Ii2XvPCq>(bU2Ot+fGEn zFB|R9F?J7Gf&JuqHqI!vR&dU7^@4KDG{q1OUV^^rhQUE3$0aHYXKHj~g{h}M{61{n z+MScxhKpQAP(gl24}}!I*sL<~nCafw6?(s-$j%(+L&`$d*a$s&;G;De)V*i?E8wna zuBAqoC2B&PK&*SXExp4iYSKE>bh2+$W`sZH+xp7Sof?J)XV3?*a@^|0no~XTbNwjp z5&qTM_MW=~lL~UOZVm<32``*(i6YTO8gpTLGwEk|w`xgb@d%zx&wK3(g&Jp#zWY_hhXH?o{`XRSGmr?Ma3d)7+~{j=U%t z{Wl+2X%!F@C}GMwYDFKrQIKM@AwoJ>xS`YQ!(JVJa`kOxirZQYT-QI#hOGn)v>S*# zxc5U}s)9)w-JDdjgS1JbT1}50G$xoE;pszHAF$b$Yf(O)^h2aSY>3pX4gA4>hR=_U zgq#~HDZNqUX*29c8xt=V)oV(Yq%HQO~@BJb1fAU z$F%~O(PB?rjrc{Lqw2oRTVw4|Oe$CeuxvnU73-sdBGIe+Mw3d!s$mii3-RGmnK$xR zc0VJGyMQh4xOl_)ovNArHgsA2-Gu3d|J-t(-306K$Z`nF$NpL}UCMYxncyEI-xZ&M zsb8gV@-C{9^!LR1?BF%W+-HL0O)YWaRpwc&&1YRNI#3G%5*TypnOICGph5oIe!s_1 z?v?+$v&v;aalq_HuWI$?SuLNEVMj3w;sR^ln)^xOh}q~Fv*_(GU2K@-<-pb&SlqeE zltV}YT&Lm5gSSPt;o1rLWy?mM2Ja(R6sLNvZU6LhzVbeC#}+$;Z#H9Wv$q>(XmTcR zz|YcjGc69FIA6-boT+n9li2P`lQ%WQx^VmQABfXl%fvF?_4@M0oPaIC2cD58YSqHh z9tE4VUi;+nmG&%0AdQ#D%xyu)06+>e^7_Z!dng6bI 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 S!NWkIl8O_X* - time { - font-weight: bolder; - } +time { + font-weight: bolder; +} + +th { + padding: 4px; + margin: 5px; + border: solid 1px darkgrey; + border-collapse: collapse; + vertical-align: top; + overflow: hidden; + text-overflow: ellipsis; +} + +.role { + +} + +.list-per-role { + +} + +.list-per-role__candidates { + list-style: none; + margin-left: 0; +} + +.candidate { + +} {%- endblock %} @@ -32,17 +59,17 @@ {%- endfor %} {%- for role in object.role.all() %} - + {{role.title}} {%- for election_list in election_lists %} - -

      + +
        {%- for candidature in election_list.candidature.filter(role=role) %}
      • -
        +
        {%- if candidature.user.profile_pict %} - {% trans %}Profile{% endtrans %} + {% trans %}Profile{% endtrans %} {%- endif %}
        {{ candidature.user.first_name }} {{candidature.user.nick_name or ''}} {{ candidature.user.last_name }} From 9dbff0cd5028e474e55803908a6ac7a287e58dbb Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lenglet Date: Thu, 22 Dec 2016 00:16:48 +0100 Subject: [PATCH 19/54] Finished election details. --- .../templates/election/election_detail.jinja | 252 ++++++++++++------ 1 file changed, 176 insertions(+), 76 deletions(-) diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index 6379072d..53cca0d4 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -8,46 +8,172 @@ {{ super() -}} {%- endblock %} {% block content %} -

        {{ object.title }}

        +

        {{ object.title }}

        +

        {{ object.description }}


        -

        - {% trans %}Polls close {% endtrans %} - at -

        +

        + {% trans %}Polls close {% endtrans %} + at +

        + {%- if object.has_voted(request.user) %} +

        + {% trans %}You already have submitted your vote.{% endtrans %} +

        + {%- endif %}
        @@ -57,89 +183,63 @@ th { {%- for election_list in election_lists %} {%- endfor %} + {%- for role in object.role.all() %} - - + + + + {%- for election_list in election_lists %} {%- endfor %} + {%- endfor %}
        {{election_list.title}}{% trans %}Blank vote{% endtrans %}
        {{role.title}}
        + {{role.title}} + {%- if role.max_choice > 1 %} + {% trans %}You may choose up to{% endtrans %} {{ role.max_choice }} {% trans %}people.{% endtrans %} + {%- endif %} +
          {%- for candidature in election_list.candidature.filter(role=role) %} -
        • +
        • - {%- if candidature.user.profile_pict %} - {% trans %}Profile{% endtrans %} - {%- endif %} -
          - {{ candidature.user.first_name }} {{candidature.user.nick_name or ''}} {{ candidature.user.last_name }} - {{ candidature.program or '' }} +
          + {%- if candidature.user.profile_pict %} + {% trans %}Profile{% endtrans %} + {%- endif %} +
          +
          + {{ candidature.user.first_name }} {{candidature.user.nick_name or ''}} {{ candidature.user.last_name }} + {{ candidature.program or '' }}
          + {%- if object.is_active %} + + + {% endif %}
        • {%- endfor %}
        + {%- if object.is_active %} + + + {% endif %} +
        - - {% if object.has_voted(request.user) %} - A voté - {% endif %} -

        {{object.title}}

        -

        {{object.description}}

        -

        {% trans %}End :{% endtrans %} {{object.end_date}}

        - {% if object.election_list.exists() %} - - - {% set nb_list = object.election_list.all().count() + 1 -%} - {% for liste in object.election_list.all() %} - - {% if object.is_candidature_active -%} - {% set nb_list = nb_list -%} - {% else -%} - {% set nb_list = nb_list + 1 -%} - {% endif -%} - {% endfor %} - {% if not object.is_candidature_active -%} - - {% endif %} - - {% for role in object.role.all() %} - - - {% for liste in object.election_list.all() %} - - {% endfor %} - {% if not object.is_candidature_active -%} - - {% endif %} - - {% endfor %} -
        {{liste.title}}{% trans %}Blank vote{% endtrans %}
        {{role.title}}
        -
          - {% for candidature in role.candidature.filter(election_list=liste) %} -
        • - {{candidature.user.first_name}} {{candidature.user.last_name}} {{candidature.user.nick_name or ''}} - {% if candidature.user.profile_pict %} -
          - {% trans %}Profile{% endtrans %} - {% endif %} -
          - {{candidature.program or ''}} -
        • - {% endfor %} -
        -
        - {% endif %} +
        + +
        {{candidate_form}} {% csrf_token %}
        - {% if object.is_candidature_active -%} - candidature - {% endif -%} {% endblock %} \ No newline at end of file From 67630fc9f860902bd145ee38c6706a9549db2825 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Thu, 22 Dec 2016 00:32:14 +0100 Subject: [PATCH 20/54] Vote template --- election/templates/election/vote_form.jinja | 13 +++++++++++++ election/views.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 election/templates/election/vote_form.jinja diff --git a/election/templates/election/vote_form.jinja b/election/templates/election/vote_form.jinja new file mode 100644 index 00000000..cf992b97 --- /dev/null +++ b/election/templates/election/vote_form.jinja @@ -0,0 +1,13 @@ +{% extends "core/base.jinja" %} + +{% block title %} +{% trans %}Vote{% endtrans %} +{% endblock %} + +{% block content %} +
        + {% csrf_token %} + {{ form.as_p() }} +

        +
        +{% endblock content %} \ No newline at end of file diff --git a/election/views.py b/election/views.py index 7acdde9b..6ebe3051 100644 --- a/election/views.py +++ b/election/views.py @@ -152,7 +152,7 @@ class VoteFormView(CanCreateMixin, FormView): Alows users to vote """ form_class = VoteForm - template_name = 'core/page_prop.jinja' + template_name = 'election/vote_form.jinja' def dispatch(self, request, *arg, **kwargs): self.election_id = kwargs['election_id'] From 177e39bd6edc8c14c8db0d264cc9f1bf7b7f8be5 Mon Sep 17 00:00:00 2001 From: Skia Date: Thu, 22 Dec 2016 00:29:54 +0100 Subject: [PATCH 21/54] Fix RuntimeWarning in populate --- core/management/commands/populate.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 84484a63..8a34b3c3 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -323,6 +323,21 @@ Cette page vise à documenter la syntaxe *Markdown* utilisée sur le site. comptes.save() simple = SimplifiedAccountingType(label = 'Je fais du simple 6', accounting_type = comptes, movement_type='DEBIT') simple.save() + + t = AccountingType(code='602', label="Gros test de malade", movement_type='DEBIT') + t.save() + Operation(journal=gj, date=date.today(), amount=32.3, remark="...", mode="CASH", done=True, accounting_type=t, target_type="USER", target_id=skia.id).save() + t = AccountingType(code='60', label="...", movement_type='DEBIT') + t.save() + Operation(journal=gj, date=date.today(), amount=32.3, remark="...", mode="CASH", done=True, accounting_type=t, target_type="USER", target_id=skia.id).save() + Operation(journal=gj, date=date.today(), amount=46.42, remark="An answer to life...", mode="CASH", done=True, accounting_type=t, target_type="USER", target_id=skia.id).save() + Operation(journal=gj, date=date.today(), amount=666.42, + remark="An answer to life...", mode="CASH", done=True, accounting_type=credit, target_type="USER", + target_id=skia.id).save() + Operation(journal=gj, date=date.today(), amount=42, + remark="An answer to life...", mode="CASH", done=False, accounting_type=debit, target_type="CLUB", + target_id=bar_club.id).save() + woenzco = Company(name="Woenzel & co") woenzco.save() # Adding user sli @@ -400,7 +415,9 @@ Cette page vise à documenter la syntaxe *Markdown* utilisée sur le site. public_group = Group.objects.get(id=settings.SITH_GROUP_PUBLIC_ID) subscriber_group = Group.objects.get(name=settings.SITH_MAIN_MEMBERS_GROUP) ae_board_gorup = Group.objects.get(name=settings.SITH_MAIN_BOARD_GROUP) - el = Election(title="Élection 2017", description="La roue tourne", start_candidature='1942-06-12 10:28:45', end_candidature='2042-06-12 10:28:45',start_date='1942-06-12 10:28:45', end_date='7942-06-12 10:28:45') + el = Election(title="Élection 2017", description="La roue tourne", start_candidature='1942-06-12 10:28:45+01', + end_candidature='2042-06-12 10:28:45+01',start_date='1942-06-12 10:28:45+01', + end_date='7942-06-12 10:28:45+01') el.save() el.view_groups.add(public_group) el.edit_groups.add(ae_board_gorup) From 938e2ce0a9db9918cf8eeae3233343efaa61e0f3 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Thu, 22 Dec 2016 00:54:22 +0100 Subject: [PATCH 22/54] Fix rights in election --- election/views.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/election/views.py b/election/views.py index 6ebe3051..ecba3a0d 100644 --- a/election/views.py +++ b/election/views.py @@ -134,9 +134,6 @@ class CandidatureCreateView(CanCreateMixin, FormView): data = form.clean() res = super(FormView, self).form_valid(form) data['election'] = Election.objects.get(id=self.election_id) - if data['user'].is_root: - self.create_candidature(data) - return res for grp in data['election'].candidature_groups.all(): if data['user'].is_in_group(grp): self.create_candidature(data) From 1f60fbd484a3bdfe1ef569bb9eb10cf9cd9dc3a4 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Thu, 22 Dec 2016 01:28:51 +0100 Subject: [PATCH 23/54] Uses election_detail for vote form --- core/management/commands/populate.py | 2 +- election/models.py | 2 +- .../templates/election/election_detail.jinja | 8 +++++ election/views.py | 29 ++++++++++--------- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 8a34b3c3..8a690eef 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -430,7 +430,7 @@ Cette page vise à documenter la syntaxe *Markdown* utilisée sur le site. listeT.save() pres = Role(election=el, title="Président AE", description="Roi de l'AE") pres.save() - resp = Role(election=el, title="Co Respo Info", description="Ghetto++") + resp = Role(election=el, title="Co Respo Info", max_choice=2, description="Ghetto++") resp.save() cand = Candidature(role=resp, user=skia, election_list=liste, program="Refesons le site AE") cand.save() diff --git a/election/models.py b/election/models.py index c972363a..e042bba5 100644 --- a/election/models.py +++ b/election/models.py @@ -56,7 +56,7 @@ class Role(models.Model): max_choice = models.IntegerField(_('max choice'), default=1) def user_has_voted(self, user): - return not self.has_voted.filter(id=user.id).exists() + return self.has_voted.filter(id=user.id).exists() def __str__(self): return ("%s : %s") % (self.election.title, self.title) diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index 53cca0d4..a7c14050 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -239,7 +239,15 @@ th {
        + {% trans %}Add a new list{% endtrans %} + {% trans %}Add a new role{% endtrans %}
        {{candidate_form}} {% csrf_token %} +

        +
        +
        + {{election_form.as_p()}} +

        + {% csrf_token %}
        {% endblock %} \ No newline at end of file diff --git a/election/views.py b/election/views.py index ecba3a0d..03c6f958 100644 --- a/election/views.py +++ b/election/views.py @@ -59,7 +59,7 @@ class VoteForm(forms.Form): def __init__(self, election, user, *args, **kwargs): super(VoteForm, self).__init__(*args, **kwargs) for role in election.role.all(): - if role.user_has_voted(user): + if not role.user_has_voted(user): cand = role.candidature if role.max_choice > 1: self.fields[role.title] = VoteCheckbox(cand, role.max_choice, required=False) @@ -97,6 +97,7 @@ class ElectionDetailView(CanViewMixin, DetailView): """ Add additionnal data to the template """ kwargs = super(ElectionDetailView, self).get_context_data(**kwargs) kwargs['candidate_form'] = CandidateForm(self.get_object().id) + kwargs['election_form'] = VoteForm(self.get_object(), self.request.user) return kwargs @@ -149,11 +150,10 @@ class VoteFormView(CanCreateMixin, FormView): Alows users to vote """ form_class = VoteForm - template_name = 'election/vote_form.jinja' + template_name = 'election/election_detail.jinja' def dispatch(self, request, *arg, **kwargs): - self.election_id = kwargs['election_id'] - self.election = get_object_or_404(Election, pk=self.election_id) + self.election = get_object_or_404(Election, pk=kwargs['election_id']) return super(VoteFormView, self).dispatch(request, *arg, **kwargs) def vote(self, data): @@ -171,17 +171,24 @@ class VoteFormView(CanCreateMixin, FormView): """ data = form.clean() res = super(FormView, self).form_valid(form) - if self.request.user.is_root: - self.vote(data) - return res - for grp in data['role'].election.vote_groups.all(): + for grp in self.election.vote_groups.all(): if self.request.user.is_in_group(grp): self.vote(data) return res return res def get_success_url(self, **kwargs): - return reverse_lazy('election:detail', kwargs={'election_id': self.election_id}) + return reverse_lazy('election:detail', kwargs={'election_id': self.election.id}) + + def get_context_data(self, **kwargs): + """ Add additionnal data to the template """ + kwargs = super(VoteFormView, self).get_context_data(**kwargs) + kwargs['candidate_form'] = CandidateForm(self.election.id) + kwargs['object'] = self.election + kwargs['election'] = self.election + kwargs['election_form'] = self.get_form() + return kwargs + # Create views @@ -220,8 +227,6 @@ class RoleCreateView(CanCreateMixin, CreateView): """ obj = form.instance res = super(CreateView, self).form_valid - if self.request.user.is_root: - return res(form) if obj.election: for grp in obj.election.edit_groups.all(): if self.request.user.is_in_group(grp): @@ -245,8 +250,6 @@ class ElectionListCreateView(CanCreateMixin, CreateView): obj = form.instance res = super(CreateView, self).form_valid if obj.election: - if self.request.user.is_root: - return res(form) for grp in obj.election.candidature_groups.all(): if self.request.user.is_in_group(grp): return res(form) From a27fd267d7bc9dc190e6fc5e45d8039535b2c38a Mon Sep 17 00:00:00 2001 From: klmp200 Date: Thu, 22 Dec 2016 01:31:40 +0100 Subject: [PATCH 24/54] Remove useless methods on elections --- election/models.py | 6 ------ election/templates/election/election_detail.jinja | 4 ++-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/election/models.py b/election/models.py index e042bba5..b6217403 100644 --- a/election/models.py +++ b/election/models.py @@ -36,12 +36,6 @@ class Election(models.Model): now = timezone.now() return bool(now <= self.end_candidature and now >= self.start_candidature) - def get_results(self): - pass - - def has_voted(self, user): - return False - # Permissions diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index a7c14050..f43e8a5c 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -169,12 +169,12 @@ th { {% trans %}Polls close {% endtrans %} at

        - {%- if object.has_voted(request.user) %} +{# {%- if object.has_voted(request.user) %}

        {% trans %}You already have submitted your vote.{% endtrans %}

        {%- endif %} - + #}
        {%- set election_lists = object.election_list.all() -%} From 97f835eb4e5e51b90a616f3df6abb73c3bbc194e Mon Sep 17 00:00:00 2001 From: klmp200 Date: Thu, 22 Dec 2016 01:57:17 +0100 Subject: [PATCH 25/54] Convert indent with space, fix populate and add an s --- core/management/commands/populate.py | 2 +- election/models.py | 8 +- .../templates/election/election_detail.jinja | 304 +++++++++--------- 3 files changed, 157 insertions(+), 157 deletions(-) diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 8a690eef..eb3d640a 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -346,7 +346,7 @@ Cette page vise à documenter la syntaxe *Markdown* utilisée sur le site. date_of_birth="1942-06-12") sli.set_password("plop") sli.save() - skia.view_groups=[Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id] + sli.view_groups=[Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id] sli.save() sli_profile_path = os.path.join(root_path, 'core/fixtures/images/5.jpg') with open(sli_profile_path, 'rb') as f: diff --git a/election/models.py b/election/models.py index b6217403..e4c03599 100644 --- a/election/models.py +++ b/election/models.py @@ -18,10 +18,10 @@ class Election(models.Model): start_date = models.DateTimeField(_('start date'), blank=False) end_date = models.DateTimeField(_('end date'), blank=False) - edit_groups = models.ManyToManyField(Group, related_name="editable_elections", verbose_name=_("edit group"), blank=True) - view_groups = models.ManyToManyField(Group, related_name="viewable_elections", verbose_name=_("view group"), blank=True) - vote_groups = models.ManyToManyField(Group, related_name="votable_elections", verbose_name=_("vote group"), blank=True) - candidature_groups = models.ManyToManyField(Group, related_name="candidate_elections", verbose_name=_("candidature group"), blank=True) + edit_groups = models.ManyToManyField(Group, related_name="editable_elections", verbose_name=_("edit groups"), blank=True) + view_groups = models.ManyToManyField(Group, related_name="viewable_elections", verbose_name=_("view groups"), blank=True) + vote_groups = models.ManyToManyField(Group, related_name="votable_elections", verbose_name=_("vote groups"), blank=True) + candidature_groups = models.ManyToManyField(Group, related_name="candidate_elections", verbose_name=_("candidature groups"), blank=True) def __str__(self): return self.title diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index f43e8a5c..2721226c 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -8,31 +8,31 @@ {{ super() -}} {%- endblock %} {% block content %} -

        {{ object.title }}

        -

        {{ object.description }}

        -
        -
        -

        - {% trans %}Polls close {% endtrans %} - at -

        -{# {%- if object.has_voted(request.user) %} -

        - {% trans %}You already have submitted your vote.{% endtrans %} -

        - {%- endif %} - #}
        -
        -
        - {%- set election_lists = object.election_list.all() -%} - - - {%- for election_list in election_lists %} - - {%- endfor %} - - - {%- for role in object.role.all() %} - - - - - - {%- for election_list in election_lists %} - - {%- endfor %} - - - - {%- endfor %} -
        {{election_list.title}}{% trans %}Blank vote{% endtrans %}
        - {{role.title}} - {%- if role.max_choice > 1 %} - {% trans %}You may choose up to{% endtrans %} {{ role.max_choice }} {% trans %}people.{% endtrans %} - {%- endif %} -
        -
          - {%- for candidature in election_list.candidature.filter(role=role) %} -
        • -
          -
          - {%- if candidature.user.profile_pict %} - {% trans %}Profile{% endtrans %} - {%- endif %} -
          -
          - {{ candidature.user.first_name }} {{candidature.user.nick_name or ''}} {{ candidature.user.last_name }} - {{ candidature.program or '' }} -
          -
          - {%- if object.is_active %} - - - {% endif %} -
        • - {%- endfor %} -
        -
        - {%- if object.is_active %} - - - {% endif %} -
        -
        -
        - -
        - {% trans %}Add a new list{% endtrans %} - {% trans %}Add a new role{% endtrans %} -
        {{candidate_form}} - {% csrf_token %} +

        {{ object.title }}

        +

        {{ object.description }}

        +
        +
        +

        + {% trans %}Polls close {% endtrans %} + at +

        +{# {%- if object.has_voted(request.user) %} +

        + {% trans %}You already have submitted your vote.{% endtrans %} +

        + {%- endif %} + #}
        +
        + + {%- set election_lists = object.election_list.all() -%} + + + {%- for election_list in election_lists %} + + {%- endfor %} + + + {%- for role in object.role.all() %} + + + + + + {%- for election_list in election_lists %} + + {%- endfor %} + + + + {%- endfor %} +
        {{election_list.title}}{% trans %}Blank vote{% endtrans %}
        + {{role.title}} + {%- if role.max_choice > 1 %} + {% trans %}You may choose up to{% endtrans %} {{ role.max_choice }} {% trans %}people.{% endtrans %} + {%- endif %} +
        +
          + {%- for candidature in election_list.candidature.filter(role=role) %} +
        • +
          +
          + {%- if candidature.user.profile_pict %} + {% trans %}Profile{% endtrans %} + {%- endif %} +
          +
          + {{ candidature.user.first_name }} {{candidature.user.nick_name or ''}} {{ candidature.user.last_name }} + {{ candidature.program or '' }} +
          +
          + {%- if object.is_active %} + + + {% endif %} +
        • + {%- endfor %} +
        +
        + {%- if object.is_active %} + + + {% endif %} +
        +
        +
        + +
        + {% trans %}Add a new list{% endtrans %} + {% trans %}Add a new role{% endtrans %} + {{candidate_form}} + {% csrf_token %}

        -
        -
        - {{election_form.as_p()}} +
        +
        + {{election_form.as_p()}}

        - {% csrf_token %} -
        + {% csrf_token %} + {% endblock %} \ No newline at end of file From 9ac1cab983d809c6a20355ba20216de210a1f437 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Thu, 22 Dec 2016 17:39:32 +0100 Subject: [PATCH 26/54] Add vote in database --- election/views.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/election/views.py b/election/views.py index 03c6f958..3a703935 100644 --- a/election/views.py +++ b/election/views.py @@ -11,9 +11,10 @@ from django.conf import settings from django import forms from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, CanCreateMixin +from django.db.models.query import QuerySet from django.views.generic.edit import FormMixin from core.views.forms import SelectDateTime -from election.models import Election, Role, Candidature, ElectionList +from election.models import Election, Role, Candidature, ElectionList, Vote from ajax_select.fields import AutoCompleteSelectField @@ -28,13 +29,14 @@ class VoteCheckbox(forms.ModelMultipleChoiceField): def __init__(self, queryset, max_choice, required=True, widget=None, label=None, initial=None, help_text='', *args, **kwargs): self.max_choice = max_choice - widget = forms.CheckboxSelectMultiple + widget = forms.CheckboxSelectMultiple() super(VoteCheckbox, self).__init__(queryset, None, required, widget, label, initial, help_text, *args, **kwargs) def clean(self, value): qs = super(VoteCheckbox, self).clean(value) self.validate(qs) + return qs def validate(self, qs): if qs.count() > self.max_choice: @@ -157,7 +159,18 @@ class VoteFormView(CanCreateMixin, FormView): return super(VoteFormView, self).dispatch(request, *arg, **kwargs) def vote(self, data): - print(data) + for key in data.keys(): + if isinstance(data[key], QuerySet): + if data[key].count() > 0: + vote = Vote(role=data[key].first().role) + vote.save() + for el in data[key]: + vote.candidature.add(el) + elif data[key] is not None: + vote = Vote(role=data[key].role) + vote.save() + vote.candidature.add(data[key]) + self.election.role.get(title=key).has_voted.add(self.request.user) def get_form_kwargs(self): kwargs = super(VoteFormView, self).get_form_kwargs() From a3a5a0446da68e4780a75a4bf9fd7cad59da4ec9 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lenglet Date: Thu, 22 Dec 2016 21:16:46 +0100 Subject: [PATCH 27/54] Finished main view. Some tuning are to be done. --- election/models.py | 3 + .../templates/election/election_detail.jinja | 190 +++++++++++------- election/views.py | 2 +- 3 files changed, 117 insertions(+), 78 deletions(-) diff --git a/election/models.py b/election/models.py index e4c03599..18343383 100644 --- a/election/models.py +++ b/election/models.py @@ -36,6 +36,9 @@ class Election(models.Model): now = timezone.now() return bool(now <= self.end_candidature and now >= self.start_candidature) + def has_voted(self, user): + return hasattr(user, 'has_voted') and user.has_voted.all() == list(self.role.all()) + # Permissions diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index 2721226c..ebcd4cb6 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -35,6 +35,10 @@ th { margin-bottom: 5px; } +.election__vote-form { + width: auto; +} + .role { } @@ -47,6 +51,10 @@ th { color: darkgreen; } +.role__error { + color: darkred; +} + .role .role_candidates { background: white; } @@ -78,6 +86,8 @@ th { width: 150px; height: 150px; + min-width: 150px; + min-height: 150px; background-color: lightgrey; } @@ -117,13 +127,15 @@ th { } .candidate__vote-choice { - margin-top: 5px; - padding: 15px; + margin-top: 5px; + padding: 15px; - border: solid 1px darkgrey; + border: solid 1px darkgrey; - text-align: center; - cursor: pointer; + color: dimgray; + text-align: center; + font-weight: bolder; + cursor: pointer; } .candidate__vote-choice:hover, .candidate__vote-choice:focus { @@ -131,10 +143,10 @@ th { } .candidate__vote-input:checked + .candidate__vote-choice { + padding: 14px; border-width: 2px; border-color: darkgreen; color: darkgreen; - font-weight: bolder; } .candidate__vote-input:checked + .candidate__vote-choice:hover, @@ -142,6 +154,16 @@ th { background: palegreen; } +.role_error .candidate__vote-input:checked + .candidate__vote-choice { + border-color: darkred; + color: darkred; +} + +.role_error .candidate__vote-input:checked + .candidate__vote-choice:hover, +.role_error .candidate__vote-input:checked + .candidate__vote-choice:focus { + background: indianred; +} + .election__sumbit-section { margin-top: 5px; } @@ -151,103 +173,117 @@ th { width: 100%; padding: 20px; background: white; - border: solid 15px orange; + border: solid 15px #4081cb; text-align: center; font-size: 200%; font-weight: bolder; cursor: pointer; } + +.election__sumbit-button:hover, +.election__sumbit-button:focus { + background-color: lightskyblue; +} {%- endblock %} {% block content %} -

        {{ object.title }}

        -

        {{ object.description }}

        +

        {{ election.title }}

        +

        {{ election.description }}


        {% trans %}Polls close {% endtrans %} at

        -{# {%- if object.has_voted(request.user) %} + {%- if election.has_voted(request.user) %}

        {% trans %}You already have submitted your vote.{% endtrans %}

        {%- endif %} - #}
        +
        - - {%- set election_lists = object.election_list.all() -%} - - - {%- for election_list in election_lists %} - - {%- endfor %} - - - {%- for role in object.role.all() %} - - - - - + + {% csrf_token %} +
        {{election_list.title}}{% trans %}Blank vote{% endtrans %}
        - {{role.title}} - {%- if role.max_choice > 1 %} - {% trans %}You may choose up to{% endtrans %} {{ role.max_choice }} {% trans %}people.{% endtrans %} - {%- endif %} -
        + {%- set election_lists = election.election_list.all() -%} + + + {%- for election_list in election_lists %} - + {%- endfor %} - - - - {%- endfor %} -
        {% trans %}Blank vote{% endtrans %} -
          - {%- for candidature in election_list.candidature.filter(role=role) %} -
        • -
          -
          - {%- if candidature.user.profile_pict %} - {% trans %}Profile{% endtrans %} - {%- endif %} -
          -
          - {{ candidature.user.first_name }} {{candidature.user.nick_name or ''}} {{ candidature.user.last_name }} - {{ candidature.program or '' }} -
          -
          - {%- if object.is_active %} - - - {% endif %} -
        • - {%- endfor %} -
        -
        {{election_list.title}} - {%- if object.is_active %} - - - {% endif %} -
        + + {%- for role in election.role.all() %} + {%- set count = [0] %} + {%- set role_data = election_form.data.getlist(role.title) if role.title in election_form.data else [] %} + + + + {{role.title}} + {%- if role.max_choice > 1 %} + {% trans %}You may choose up to{% endtrans %} {{ role.max_choice }} {% trans %}people.{% endtrans %} + {%- endif %} + {%- if election_form.errors[role.title] is defined %} + {%- for error in election_form.errors.as_data()[role.title] %} + {{ error.message }} + {%- endfor %} + {%- endif %} + + + + + {%- if election.is_active and role.max_choice == 1 %} + + + {%- set _ = count.append(count.pop() + 1) %} + {%- endif %} + + {%- for election_list in election_lists %} + +
          + {%- for candidature in election_list.candidature.filter(role=role) %} +
        • +
          +
          + {%- if candidature.user.profile_pict %} + {% trans %}Profile{% endtrans %} + {%- endif %} +
          +
          + {{ candidature.user.first_name }} {{candidature.user.nick_name or ''}} {{ candidature.user.last_name }} + {{ candidature.program or '' }} +
          +
          + {%- if election.is_active %} + + + {%- set _ = count.append(count.pop() + 1) %} + {%- endif %} +
        • + {%- endfor %} +
        + + {%- endfor %} + + + {%- endfor %} + +
        - +
        - {% trans %}Add a new list{% endtrans %} - {% trans %}Add a new role{% endtrans %} -
        {{candidate_form}} +
        + {% trans %}Add a new list{% endtrans %} + {% trans %}Add a new role{% endtrans %} +
        + {{candidate_form}} {% csrf_token %}

        -
        - {{election_form.as_p()}} -

        - {% csrf_token %} -
        {% endblock %} \ No newline at end of file diff --git a/election/views.py b/election/views.py index 3a703935..5368bb3f 100644 --- a/election/views.py +++ b/election/views.py @@ -40,7 +40,7 @@ class VoteCheckbox(forms.ModelMultipleChoiceField): def validate(self, qs): if qs.count() > self.max_choice: - raise forms.ValidationError(_("You have selected too much candidate")) + raise forms.ValidationError(_("You have selected too much candidates."), code='invalid') # Forms From dfcddbd1fa7a4801aba8b6e8eeca16b8815646f2 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lenglet Date: Thu, 22 Dec 2016 22:00:02 +0100 Subject: [PATCH 28/54] Hide election detail parts when user cannot candidate or edit. --- election/models.py | 6 ++ .../templates/election/election_detail.jinja | 61 +++++++++++++++---- election/views.py | 7 +-- 3 files changed, 57 insertions(+), 17 deletions(-) diff --git a/election/models.py b/election/models.py index 18343383..525dc7a4 100644 --- a/election/models.py +++ b/election/models.py @@ -39,6 +39,12 @@ class Election(models.Model): def has_voted(self, user): return hasattr(user, 'has_voted') and user.has_voted.all() == list(self.role.all()) + def can_candidate(self, user): + for group in self.candidature_groups.all(): + if user.is_in_group(group): + return True + return False + # Permissions diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index ebcd4cb6..43a07ce3 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -35,6 +35,17 @@ th { margin-bottom: 5px; } +.election__elector-infos { + margin: 0; + margin-bottom: 5px; + font-weight: bolder; + color: darkgreen; +} + +.election__vote { + margin-bottom: 5px; +} + .election__vote-form { width: auto; } @@ -135,10 +146,10 @@ th { color: dimgray; text-align: center; font-weight: bolder; - cursor: pointer; } -.candidate__vote-choice:hover, .candidate__vote-choice:focus { +.candidate__vote-input:not(:disabled:checked) + .candidate__vote-choice:hover, +.candidate__vote-input:not(:disabled:checked) + .candidate__vote-choice:focus { background: lightgrey; } @@ -149,8 +160,12 @@ th { color: darkgreen; } -.candidate__vote-input:checked + .candidate__vote-choice:hover, -.candidate__vote-input:checked + .candidate__vote-choice:focus { +.candidate__vote-input:not(:disabled) + .candidate__vote-choice { + cursor: pointer; +} + +.candidate__vote-input:checked:not(:disabled) + .candidate__vote-choice:hover, +.candidate__vote-input:checked:not(:disabled) + .candidate__vote-choice:focus { background: palegreen; } @@ -159,8 +174,8 @@ th { color: darkred; } -.role_error .candidate__vote-input:checked + .candidate__vote-choice:hover, -.role_error .candidate__vote-input:checked + .candidate__vote-choice:focus { +.role_error .candidate__vote-input:checked:not(:disabled) + .candidate__vote-choice:hover, +.role_error .candidate__vote-input:checked:not(:disabled) + .candidate__vote-choice:focus { background: indianred; } @@ -184,6 +199,18 @@ th { .election__sumbit-button:focus { background-color: lightskyblue; } + +.election__add-elements a { + display: inline-block; + border: solid 1px darkgrey; + height: 20px; + line-height: 20px; + padding: 10px; +} + +.election__add-candidature { + margin-top: 5px; +} {%- endblock %} @@ -202,7 +229,7 @@ th {

        {%- endif %} -
        +
        {% csrf_token %} @@ -234,7 +261,7 @@ th {
        {%- if election.is_active and role.max_choice == 1 %} - + @@ -258,7 +285,7 @@ th { {%- if election.is_active %} - + @@ -275,15 +302,23 @@ th {
        + {%- if not election.has_voted(request.user) %}
        + {%- endif %} + {%- if request.user.can_edit(election) %}
        {% trans %}Add a new list{% endtrans %} {% trans %}Add a new role{% endtrans %}
        -
        {{candidate_form}} - {% csrf_token %} -

        -
        + {%- endif %} + {%- if election.can_candidate(request.user) %} +
        +
        {{candidate_form}} + {% csrf_token %} +

        +
        +
        + {%- endif %} {% endblock %} \ No newline at end of file diff --git a/election/views.py b/election/views.py index 5368bb3f..b8072e86 100644 --- a/election/views.py +++ b/election/views.py @@ -137,10 +137,9 @@ class CandidatureCreateView(CanCreateMixin, FormView): data = form.clean() res = super(FormView, self).form_valid(form) data['election'] = Election.objects.get(id=self.election_id) - for grp in data['election'].candidature_groups.all(): - if data['user'].is_in_group(grp): - self.create_candidature(data) - return res + if(data['election'].can_candidate(data['user'])): + self.create_candidature(data) + return res return res def get_success_url(self, **kwargs): From 89362bae796d12bae81017ed0e1370238a0067a4 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lenglet Date: Thu, 22 Dec 2016 22:17:22 +0100 Subject: [PATCH 29/54] Hide inputs when user already voted. --- election/models.py | 2 +- .../templates/election/election_detail.jinja | 37 ++++++++++++------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/election/models.py b/election/models.py index 525dc7a4..79e593b1 100644 --- a/election/models.py +++ b/election/models.py @@ -37,7 +37,7 @@ class Election(models.Model): return bool(now <= self.end_candidature and now >= self.start_candidature) def has_voted(self, user): - return hasattr(user, 'has_voted') and user.has_voted.all() == list(self.role.all()) + return hasattr(user, 'has_voted') and list(user.has_voted.all()) == list(self.role.all()) def can_candidate(self, user): for group in self.candidature_groups.all(): diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index 43a07ce3..4eb2ad9f 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -23,21 +23,26 @@ th { .election__title { margin: 0; + margin-bottom: 5px; } .election__description { margin: 0; - margin-top: 5px; } .election__details { + margin-bottom: 5px; +} + +.election__details p { margin: 0; +} + +.election__details p:not(:last-child) { margin-bottom: 5px; } .election__elector-infos { - margin: 0; - margin-bottom: 5px; font-weight: bolder; color: darkgreen; } @@ -148,8 +153,8 @@ th { font-weight: bolder; } -.candidate__vote-input:not(:disabled:checked) + .candidate__vote-choice:hover, -.candidate__vote-input:not(:disabled:checked) + .candidate__vote-choice:focus { +.candidate__vote-input:not(:checked):not(:disabled) + .candidate__vote-choice:hover, +.candidate__vote-input:not(:checked):not(:disabled) + .candidate__vote-choice:focus { background: lightgrey; } @@ -180,7 +185,7 @@ th { } .election__sumbit-section { - margin-top: 5px; + margin-bottom: 5px; } .election__sumbit-button { @@ -200,6 +205,10 @@ th { background-color: lightskyblue; } +.election__add-elements { + margin-bottom: 5px; +} + .election__add-elements a { display: inline-block; border: solid 1px darkgrey; @@ -207,10 +216,6 @@ th { line-height: 20px; padding: 10px; } - -.election__add-candidature { - margin-top: 5px; -} {%- endblock %} @@ -218,8 +223,8 @@ th {

        {{ election.title }}

        {{ election.description }}


        -
        -

        +

        +

        {% trans %}Polls close {% endtrans %} at

        @@ -236,7 +241,9 @@ th { {%- set election_lists = election.election_list.all() -%} + {%- if not election.has_voted(request.user) %} {% trans %}Blank vote{% endtrans %} + {%- endif %} {%- for election_list in election_lists %} {{election_list.title}} {%- endfor %} @@ -248,7 +255,7 @@ th { {{role.title}} - {%- if role.max_choice > 1 %} + {%- if role.max_choice > 1 and not election.has_voted(request.user) %} {% trans %}You may choose up to{% endtrans %} {{ role.max_choice }} {% trans %}people.{% endtrans %} {%- endif %} {%- if election_form.errors[role.title] is defined %} @@ -259,6 +266,7 @@ th { + {%- if not election.has_voted(request.user) %} {%- if election.is_active and role.max_choice == 1 %} @@ -268,6 +276,7 @@ th { {%- set _ = count.append(count.pop() + 1) %} {%- endif %} + {%- endif %} {%- for election_list in election_lists %}
          @@ -284,7 +293,7 @@ th { {{ candidature.program or '' }}
        - {%- if election.is_active %} + {%- if election.is_active and not election.has_voted(request.user) %}
        - {%- if election.is_active and not election.has_voted(request.user) %} - + {%- if election.can_vote(user) %} + @@ -311,18 +321,18 @@ th { - {%- if not election.has_voted(request.user) %} + {%- if not election.has_voted(user) %}
        {%- endif %} - {%- if request.user.can_edit(election) %} + {%- if user.can_edit(election) %}
        {% trans %}Add a new list{% endtrans %} {% trans %}Add a new role{% endtrans %}
        {%- endif %} - {%- if election.can_candidate(request.user) %} + {%- if election.can_candidate(user) or user.can_edit(election) %}
        {{candidate_form}} {% csrf_token %} diff --git a/election/views.py b/election/views.py index b8072e86..95665f7a 100644 --- a/election/views.py +++ b/election/views.py @@ -100,6 +100,7 @@ class ElectionDetailView(CanViewMixin, DetailView): kwargs = super(ElectionDetailView, self).get_context_data(**kwargs) kwargs['candidate_form'] = CandidateForm(self.get_object().id) kwargs['election_form'] = VoteForm(self.get_object(), self.request.user) + kwargs['user'] = self.request.user return kwargs From 1a34dcdafe13d56f7e6a847821518173b691a953 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lenglet Date: Fri, 23 Dec 2016 01:28:55 +0100 Subject: [PATCH 34/54] Changed plain form action to reversed URL. --- election/templates/election/election_detail.jinja | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index 8f424f2a..c6377526 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -245,7 +245,7 @@ th { {%- endif %}
        - + {% csrf_token %} {%- set election_lists = election.election_list.all() -%} From 9bb5951dacd29f7bf547584bafabf88e91f89450 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lenglet Date: Fri, 23 Dec 2016 01:32:42 +0100 Subject: [PATCH 35/54] Removed request.user as user bc it already exists --- election/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/election/views.py b/election/views.py index 95665f7a..b8072e86 100644 --- a/election/views.py +++ b/election/views.py @@ -100,7 +100,6 @@ class ElectionDetailView(CanViewMixin, DetailView): kwargs = super(ElectionDetailView, self).get_context_data(**kwargs) kwargs['candidate_form'] = CandidateForm(self.get_object().id) kwargs['election_form'] = VoteForm(self.get_object(), self.request.user) - kwargs['user'] = self.request.user return kwargs From baf39d8f3b2fb36469c8468f0e7b4637e975a705 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lenglet Date: Fri, 23 Dec 2016 14:25:57 +0100 Subject: [PATCH 36/54] Hide input if user can not vote --- election/templates/election/election_detail.jinja | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index c6377526..02334bf4 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -251,7 +251,7 @@ th { {%- set election_lists = election.election_list.all() -%} - {%- if not election.has_voted(user) %} + {%- if not election.has_voted(user) and election.can_vote(user) %} {%- endif %} {%- for election_list in election_lists %} @@ -265,7 +265,7 @@ th {
        {% trans %}Blank vote{% endtrans %}
        {{role.title}} - {%- if role.max_choice > 1 and not election.has_voted(user) %} + {%- if role.max_choice > 1 and not election.has_voted(user) and election.can_vote(user) %} {% trans %}You may choose up to{% endtrans %} {{ role.max_choice }} {% trans %}people.{% endtrans %} {%- endif %} {%- if election_form.errors[role.title] is defined %} @@ -294,7 +294,7 @@ th {
      • - {%- if candidature.user.profile_pict %} + {%- if candidature.user.profile_pict and user.is_subscriber_viewable %} {% trans %}Profile{% endtrans %} {%- endif %}
        @@ -321,7 +321,7 @@ th {
      • - {%- if not election.has_voted(user) %} + {%- if not election.has_voted(user) and election.can_vote(user) %}
        From e8d54764bd32abf6e8464a37272bbae37106d713 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Fri, 23 Dec 2016 17:13:46 +0100 Subject: [PATCH 37/54] Add election results --- election/models.py | 25 +++++++++++++++++++++++++ election/views.py | 31 +++++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/election/models.py b/election/models.py index 0d382db0..c6df8537 100644 --- a/election/models.py +++ b/election/models.py @@ -56,6 +56,13 @@ class Election(models.Model): return True return False + @property + def get_results(self): + results = {} + for role in self.role.all(): + results[role.title] = role.get_results + return results + # Permissions @@ -72,6 +79,24 @@ class Role(models.Model): def user_has_voted(self, user): return self.has_voted.filter(id=user.id).exists() + @property + def get_results(self): + results = {} + total_vote = self.has_voted.count() * self.max_choice + if total_vote == 0: + return None + non_blank = 0 + for candidature in self.candidature.all(): + cand_results = {} + cand_results['vote'] = self.vote.filter(candidature=candidature).count() + cand_results['percent'] = cand_results['vote'] * 100 / total_vote + non_blank += cand_results['vote'] + results[candidature.user.username] = cand_results + results['total vote'] = total_vote + results['blank vote'] = {'vote': total_vote - non_blank, + 'percent': (total_vote - non_blank) * 100 / total_vote} + return results + def __str__(self): return ("%s : %s") % (self.election.title, self.title) diff --git a/election/views.py b/election/views.py index b8072e86..4bb27cde 100644 --- a/election/views.py +++ b/election/views.py @@ -67,7 +67,21 @@ class VoteForm(forms.Form): self.fields[role.title] = VoteCheckbox(cand, role.max_choice, required=False) else: self.fields[role.title] = forms.ModelChoiceField(cand, required=False, - widget=forms.RadioSelect(), empty_label=_("Blank vote")) + widget=forms.RadioSelect(), empty_label=_("Blank vote")) + + +class RoleForm(forms.ModelForm): + """ Form for creating a role """ + class Meta: + model = Role + fields = ['title', 'election', 'description', 'max_choice'] + + def clean(self): + cleaned_data = super(RoleForm, self).clean() + title = cleaned_data.get('title') + election = cleaned_data.get('election') + if Role.objects.filter(title=title, election=election).exists(): + raise forms.ValidationError(_("This role already exists for this election"), code='invalid') # Display elections @@ -110,10 +124,11 @@ class CandidatureCreateView(CanCreateMixin, FormView): View dedicated to a cundidature creation """ form_class = CandidateForm - template_name = 'core/page_prop.jinja' + template_name = 'election/election_detail.jinja' def dispatch(self, request, *arg, **kwargs): self.election_id = kwargs['election_id'] + self.election = get_object_or_404(Election, pk=self.election_id) return super(CandidatureCreateView, self).dispatch(request, *arg, **kwargs) def get_form_kwargs(self): @@ -142,6 +157,15 @@ class CandidatureCreateView(CanCreateMixin, FormView): return res return res + def get_context_data(self, **kwargs): + """ Add additionnal data to the template """ + kwargs = super(CandidatureCreateView, self).get_context_data(**kwargs) + kwargs['candidate_form'] = self.get_form() + kwargs['object'] = self.election + kwargs['election'] = self.election + kwargs['election_form'] = VoteForm(self.election, self.request.user) + return kwargs + def get_success_url(self, **kwargs): return reverse_lazy('election:detail', kwargs={'election_id': self.election_id}) @@ -229,8 +253,7 @@ class ElectionCreateView(CanCreateMixin, CreateView): class RoleCreateView(CanCreateMixin, CreateView): model = Role - form_class = modelform_factory(Role, - fields=['title', 'election', 'title', 'description', 'max_choice']) + form_class = RoleForm template_name = 'core/page_prop.jinja' def form_valid(self, form): From 1c761f9db29de9cf87828376429bc87e18c14672 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lenglet Date: Fri, 23 Dec 2016 20:40:54 +0100 Subject: [PATCH 38/54] Added results when the user is not voting --- election/models.py | 6 +-- .../templates/election/election_detail.jinja | 37 +++++++++++++------ election/views.py | 1 + 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/election/models.py b/election/models.py index c6df8537..00ca2f46 100644 --- a/election/models.py +++ b/election/models.py @@ -57,10 +57,10 @@ class Election(models.Model): return False @property - def get_results(self): + def results(self): results = {} for role in self.role.all(): - results[role.title] = role.get_results + results[role.title] = role.results return results # Permissions @@ -80,7 +80,7 @@ class Role(models.Model): return self.has_voted.filter(id=user.id).exists() @property - def get_results(self): + def results(self): results = {} total_vote = self.has_voted.count() * self.max_choice if total_vote == 0: diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index 02334bf4..c5e5fbfd 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -87,14 +87,18 @@ th { } .list-per-role__candidate:not(:last-child) { - margin-bottom: 5px; + margin-bottom: 15px; } -.candidate { +.candidate__infos { display: flex; flex-flow: row nowrap; } +.candidate__infos:not(:last-child) { + margin-bottom: 5px; +} + .candidate__picture-wrapper { display: flex; justify-content: center; @@ -113,7 +117,7 @@ th { max-height: 150px; } -.candidate__infos { +.candidate__details { margin-left: 5px; } @@ -184,6 +188,10 @@ th { background: indianred; } +.election__results { + text-align: center; +} + .election__sumbit-section { margin-bottom: 5px; } @@ -251,9 +259,7 @@ th { {%- set election_lists = election.election_list.all() -%} - {%- if not election.has_voted(user) and election.can_vote(user) %} {% trans %}Blank vote{% endtrans %} - {%- endif %} {%- for election_list in election_lists %} {{election_list.title}} {%- endfor %} @@ -276,29 +282,33 @@ th { - {%- if election.can_vote(user) %} - {%- if role.max_choice == 1 %} + {%- if role.max_choice == 1 and election.can_vote(user) %} {%- set _ = count.append(count.pop() + 1) %} {%- endif %} + {%- if election.has_voted(user) or not election.is_vote_active %} + {%- set results = election_results[role.title]['blank vote'] %} +
        + {{results.vote}} {% trans %}votes{% endtrans %} ( {{results.percent}} %) +
        + {%- endif %} - {%- endif %} {%- for election_list in election_lists %}
          {%- for candidature in election_list.candidature.filter(role=role) %} -
        • -
          +
        • +
          {%- if candidature.user.profile_pict and user.is_subscriber_viewable %} {% trans %}Profile{% endtrans %} {%- endif %}
          -
          +
          {{ candidature.user.first_name }} {{candidature.user.nick_name or ''}} {{ candidature.user.last_name }} {{ candidature.program or '' }}
          @@ -309,6 +319,11 @@ th { {% trans %}Choose{% endtrans %} {{ candidature.user.nick_name or candidature.user.first_name }} {%- set _ = count.append(count.pop() + 1) %} + {%- else %} + {%- set results = election_results[role.title][candidature.user.username] %} +
          + {{results.vote}} {% trans %}votes{% endtrans %} ( {{results.percent}} %) +
          {%- endif %}
        • {%- endfor %} diff --git a/election/views.py b/election/views.py index 4bb27cde..143ba56c 100644 --- a/election/views.py +++ b/election/views.py @@ -114,6 +114,7 @@ class ElectionDetailView(CanViewMixin, DetailView): kwargs = super(ElectionDetailView, self).get_context_data(**kwargs) kwargs['candidate_form'] = CandidateForm(self.get_object().id) kwargs['election_form'] = VoteForm(self.get_object(), self.request.user) + kwargs['election_results'] = self.get_object().results return kwargs From 64f5fef89ffc40a27ec25faf725e3a1bc44c1f89 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lenglet Date: Fri, 23 Dec 2016 21:53:54 +0100 Subject: [PATCH 39/54] Display results only when the polls close --- election/models.py | 4 ++++ .../templates/election/election_detail.jinja | 7 ++++-- election/views.py | 22 ++++++++++--------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/election/models.py b/election/models.py index 00ca2f46..642129c3 100644 --- a/election/models.py +++ b/election/models.py @@ -31,6 +31,10 @@ class Election(models.Model): now = timezone.now() return bool(now <= self.end_date and now >= self.start_date) + @property + def is_vote_finished(self): + return bool(timezone.now() > self.end_date) + @property def is_candidature_active(self): now = timezone.now() diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index c5e5fbfd..2f9f1256 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -235,6 +235,8 @@ th {

          {%- if election.is_vote_active %} {% trans %}Polls close {% endtrans %} + {%- elif election.is_vote_finished %} + {% trans %}Polls closed {% endtrans %} {%- else %} {% trans %}Polls will open {% endtrans %} at @@ -290,7 +292,7 @@ th { {%- set _ = count.append(count.pop() + 1) %} {%- endif %} - {%- if election.has_voted(user) or not election.is_vote_active %} + {%- if not election.is_vote_finished %} {%- set results = election_results[role.title]['blank vote'] %}

          {{results.vote}} {% trans %}votes{% endtrans %} ( {{results.percent}} %) @@ -319,7 +321,8 @@ th { {% trans %}Choose{% endtrans %} {{ candidature.user.nick_name or candidature.user.first_name }} {%- set _ = count.append(count.pop() + 1) %} - {%- else %} + {%- endif %} + {%- if not election.is_vote_finished %} {%- set results = election_results[role.title][candidature.user.username] %}
          {{results.vote}} {% trans %}votes{% endtrans %} ( {{results.percent}} %) diff --git a/election/views.py b/election/views.py index 143ba56c..65bf437e 100644 --- a/election/views.py +++ b/election/views.py @@ -182,19 +182,21 @@ class VoteFormView(CanCreateMixin, FormView): self.election = get_object_or_404(Election, pk=kwargs['election_id']) return super(VoteFormView, self).dispatch(request, *arg, **kwargs) - def vote(self, data): - for key in data.keys(): - if isinstance(data[key], QuerySet): - if data[key].count() > 0: - vote = Vote(role=data[key].first().role) + def vote(self, election_data): + for role_title in election_data.keys(): + # If we have a multiple choice field + if isinstance(election_data[role_title], QuerySet): + if election_data[role_title].count() > 0: + vote = Vote(role=election_data[role_title].first().role) vote.save() - for el in data[key]: + for el in election_data[role_title]: vote.candidature.add(el) - elif data[key] is not None: - vote = Vote(role=data[key].role) + # If we have a single choice + elif election_data[role_title] is not None: + vote = Vote(role=election_data[role_title].role) vote.save() - vote.candidature.add(data[key]) - self.election.role.get(title=key).has_voted.add(self.request.user) + vote.candidature.add(election_data[role_title]) + self.election.role.get(title=role_title).has_voted.add(self.request.user) def get_form_kwargs(self): kwargs = super(VoteFormView, self).get_form_kwargs() From da77c188710267d6263e340faa900f7ba8f4eb42 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lenglet Date: Fri, 23 Dec 2016 22:00:06 +0100 Subject: [PATCH 40/54] Really display results when election is finished. --- election/templates/election/election_detail.jinja | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index 2f9f1256..34f21dfb 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -292,10 +292,10 @@ th { {%- set _ = count.append(count.pop() + 1) %} {%- endif %} - {%- if not election.is_vote_finished %} + {%- if election.is_vote_finished %} {%- set results = election_results[role.title]['blank vote'] %}
          - {{results.vote}} {% trans %}votes{% endtrans %} ( {{results.percent}} %) + {{results.vote}} {% trans %}votes{% endtrans %} ({{results.percent}} %)
          {%- endif %} @@ -322,10 +322,10 @@ th { {%- set _ = count.append(count.pop() + 1) %} {%- endif %} - {%- if not election.is_vote_finished %} + {%- if election.is_vote_finished %} {%- set results = election_results[role.title][candidature.user.username] %}
          - {{results.vote}} {% trans %}votes{% endtrans %} ( {{results.percent}} %) + {{results.vote}} {% trans %}votes{% endtrans %} ({{results.percent}} %)
          {%- endif %}
        • From 37decde04d3b9516ce5d2a1f06d73751ed228cb7 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Fri, 23 Dec 2016 22:58:54 +0100 Subject: [PATCH 41/54] Adds an S in electionS --- .../migrations/0005_auto_20161223_2240.py | 70 +++++++++++++++++++ election/models.py | 22 +++--- .../templates/election/election_detail.jinja | 6 +- election/views.py | 6 +- 4 files changed, 87 insertions(+), 17 deletions(-) create mode 100644 election/migrations/0005_auto_20161223_2240.py diff --git a/election/migrations/0005_auto_20161223_2240.py b/election/migrations/0005_auto_20161223_2240.py new file mode 100644 index 00000000..0538f465 --- /dev/null +++ b/election/migrations/0005_auto_20161223_2240.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('election', '0004_auto_20161219_2302'), + ] + + operations = [ + migrations.AlterField( + model_name='candidature', + name='election_list', + field=models.ForeignKey(related_name='candidatures', to='election.ElectionList', verbose_name='election_list'), + ), + migrations.AlterField( + model_name='candidature', + name='role', + field=models.ForeignKey(related_name='candidatures', to='election.Role', verbose_name='role'), + ), + migrations.AlterField( + model_name='candidature', + name='user', + field=models.ForeignKey(verbose_name='user', to=settings.AUTH_USER_MODEL, related_name='candidates', blank=True), + ), + migrations.AlterField( + model_name='election', + name='candidature_groups', + field=models.ManyToManyField(to='core.Group', related_name='candidate_elections', blank=True, verbose_name='candidature groups'), + ), + migrations.AlterField( + model_name='election', + name='edit_groups', + field=models.ManyToManyField(to='core.Group', related_name='editable_elections', blank=True, verbose_name='edit groups'), + ), + migrations.AlterField( + model_name='election', + name='view_groups', + field=models.ManyToManyField(to='core.Group', related_name='viewable_elections', blank=True, verbose_name='view groups'), + ), + migrations.AlterField( + model_name='election', + name='vote_groups', + field=models.ManyToManyField(to='core.Group', related_name='votable_elections', blank=True, verbose_name='vote groups'), + ), + migrations.AlterField( + model_name='electionlist', + name='election', + field=models.ForeignKey(related_name='election_lists', to='election.Election', verbose_name='election'), + ), + migrations.AlterField( + model_name='role', + name='election', + field=models.ForeignKey(related_name='roles', to='election.Election', verbose_name='election'), + ), + migrations.AlterField( + model_name='vote', + name='candidature', + field=models.ManyToManyField(to='election.Candidature', related_name='votes', verbose_name='candidature'), + ), + migrations.AlterField( + model_name='vote', + name='role', + field=models.ForeignKey(related_name='votes', to='election.Role', verbose_name='role'), + ), + ] diff --git a/election/models.py b/election/models.py index 642129c3..9a0ff0f4 100644 --- a/election/models.py +++ b/election/models.py @@ -41,7 +41,7 @@ class Election(models.Model): return bool(now <= self.end_candidature and now >= self.start_candidature) def has_voted(self, user): - for role in self.role.all(): + for role in self.roles.all(): if role.user_has_voted(user): return True return False @@ -63,7 +63,7 @@ class Election(models.Model): @property def results(self): results = {} - for role in self.role.all(): + for role in self.roles.all(): results[role.title] = role.results return results @@ -74,7 +74,7 @@ class Role(models.Model): """ This class allows to create a new role avaliable for a candidature """ - election = models.ForeignKey(Election, related_name='role', verbose_name=_("election")) + election = models.ForeignKey(Election, related_name='roles', verbose_name=_("election")) title = models.CharField(_('title'), max_length=255) description = models.TextField(_('description'), null=True, blank=True) has_voted = models.ManyToManyField(User, verbose_name=('has voted'), related_name='has_voted') @@ -90,9 +90,9 @@ class Role(models.Model): if total_vote == 0: return None non_blank = 0 - for candidature in self.candidature.all(): + for candidature in self.candidatures.all(): cand_results = {} - cand_results['vote'] = self.vote.filter(candidature=candidature).count() + cand_results['vote'] = self.votes.filter(candidature=candidature).count() cand_results['percent'] = cand_results['vote'] * 100 / total_vote non_blank += cand_results['vote'] results[candidature.user.username] = cand_results @@ -110,7 +110,7 @@ class ElectionList(models.Model): To allow per list vote """ title = models.CharField(_('title'), max_length=255) - election = models.ForeignKey(Election, related_name='election_list', verbose_name=_("election")) + election = models.ForeignKey(Election, related_name='election_lists', verbose_name=_("election")) def __str__(self): return self.title @@ -120,10 +120,10 @@ class Candidature(models.Model): """ This class is a component of responsability """ - role = models.ForeignKey(Role, related_name='candidature', verbose_name=_("role")) - user = models.ForeignKey(User, verbose_name=_('user'), related_name='candidate', blank=True) + role = models.ForeignKey(Role, related_name='candidatures', verbose_name=_("role")) + user = models.ForeignKey(User, verbose_name=_('user'), related_name='candidates', blank=True) program = models.TextField(_('description'), null=True, blank=True) - election_list = models.ForeignKey(ElectionList, related_name='candidature', verbose_name=_('election_list')) + election_list = models.ForeignKey(ElectionList, related_name='candidatures', verbose_name=_('election_list')) def __str__(self): return "%s : %s" % (self.role.title, self.user.username) @@ -133,8 +133,8 @@ class Vote(models.Model): """ This class allows to vote for candidates """ - role = models.ForeignKey(Role, related_name='vote', verbose_name=_("role")) - candidature = models.ManyToManyField(Candidature, related_name='vote', verbose_name=_("candidature")) + role = models.ForeignKey(Role, related_name='votes', verbose_name=_("role")) + candidature = models.ManyToManyField(Candidature, related_name='votes', verbose_name=_("candidature")) def __str__(self): return "Vote" \ No newline at end of file diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index 34f21dfb..2b86e546 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -258,7 +258,7 @@ th {
          {% csrf_token %} - {%- set election_lists = election.election_list.all() -%} + {%- set election_lists = election.election_lists.all() -%} @@ -266,7 +266,7 @@ th { {%- endfor %} - {%- for role in election.role.all() %} + {%- for role in election.roles.all() %} {%- set count = [0] %} {%- set role_data = election_form.data.getlist(role.title) if role.title in election_form.data else [] %} @@ -302,7 +302,7 @@ th { {%- for election_list in election_lists %}
          {% trans %}Blank vote{% endtrans %}{{election_list.title}}
            - {%- for candidature in election_list.candidature.filter(role=role) %} + {%- for candidature in election_list.candidatures.filter(role=role) %}
          • diff --git a/election/views.py b/election/views.py index 65bf437e..58220c89 100644 --- a/election/views.py +++ b/election/views.py @@ -60,9 +60,9 @@ class CandidateForm(forms.Form): class VoteForm(forms.Form): def __init__(self, election, user, *args, **kwargs): super(VoteForm, self).__init__(*args, **kwargs) - for role in election.role.all(): + for role in election.roles.all(): if not role.user_has_voted(user): - cand = role.candidature + cand = role.candidatures if role.max_choice > 1: self.fields[role.title] = VoteCheckbox(cand, role.max_choice, required=False) else: @@ -196,7 +196,7 @@ class VoteFormView(CanCreateMixin, FormView): vote = Vote(role=election_data[role_title].role) vote.save() vote.candidature.add(election_data[role_title]) - self.election.role.get(title=role_title).has_voted.add(self.request.user) + self.election.roles.get(title=role_title).has_voted.add(self.request.user) def get_form_kwargs(self): kwargs = super(VoteFormView, self).get_form_kwargs() From 9d9c86ea0f529a25f5ca536d4df8e7506ba950e0 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Fri, 23 Dec 2016 23:49:00 +0100 Subject: [PATCH 42/54] Refactored has_voted --- .../migrations/0006_auto_20161223_2315.py | 25 +++++++++++++++++++ election/models.py | 22 ++++++---------- .../{vote_form.jinja => candidate_form.jinja} | 2 +- election/views.py | 13 +++++----- 4 files changed, 41 insertions(+), 21 deletions(-) create mode 100644 election/migrations/0006_auto_20161223_2315.py rename election/templates/election/{vote_form.jinja => candidate_form.jinja} (88%) diff --git a/election/migrations/0006_auto_20161223_2315.py b/election/migrations/0006_auto_20161223_2315.py new file mode 100644 index 00000000..a6190ce5 --- /dev/null +++ b/election/migrations/0006_auto_20161223_2315.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('election', '0005_auto_20161223_2240'), + ] + + operations = [ + migrations.RemoveField( + model_name='role', + name='has_voted', + ), + migrations.AddField( + model_name='election', + name='voters', + field=models.ManyToManyField(to=settings.AUTH_USER_MODEL, verbose_name='has voted', related_name='has_voted'), + ), + ] diff --git a/election/models.py b/election/models.py index 9a0ff0f4..28d18e0d 100644 --- a/election/models.py +++ b/election/models.py @@ -22,6 +22,7 @@ class Election(models.Model): view_groups = models.ManyToManyField(Group, related_name="viewable_elections", verbose_name=_("view groups"), blank=True) vote_groups = models.ManyToManyField(Group, related_name="votable_elections", verbose_name=_("vote groups"), blank=True) candidature_groups = models.ManyToManyField(Group, related_name="candidate_elections", verbose_name=_("candidature groups"), blank=True) + voters = models.ManyToManyField(User, verbose_name=('voters'), related_name='voted_elections') def __str__(self): return self.title @@ -40,12 +41,6 @@ class Election(models.Model): now = timezone.now() return bool(now <= self.end_candidature and now >= self.start_candidature) - def has_voted(self, user): - for role in self.roles.all(): - if role.user_has_voted(user): - return True - return False - def can_candidate(self, user): for group in self.candidature_groups.all(): if user.is_in_group(group): @@ -60,11 +55,15 @@ class Election(models.Model): return True return False + def has_voted(self, user): + return self.voters.filter(id=user.id).exists() + @property def results(self): results = {} + total_vote = self.voters.count() for role in self.roles.all(): - results[role.title] = role.results + results[role.title] = role.results(total_vote) return results # Permissions @@ -77,16 +76,11 @@ class Role(models.Model): election = models.ForeignKey(Election, related_name='roles', verbose_name=_("election")) title = models.CharField(_('title'), max_length=255) description = models.TextField(_('description'), null=True, blank=True) - has_voted = models.ManyToManyField(User, verbose_name=('has voted'), related_name='has_voted') max_choice = models.IntegerField(_('max choice'), default=1) - def user_has_voted(self, user): - return self.has_voted.filter(id=user.id).exists() - - @property - def results(self): + def results(self, total_vote): results = {} - total_vote = self.has_voted.count() * self.max_choice + total_vote *= self.max_choice if total_vote == 0: return None non_blank = 0 diff --git a/election/templates/election/vote_form.jinja b/election/templates/election/candidate_form.jinja similarity index 88% rename from election/templates/election/vote_form.jinja rename to election/templates/election/candidate_form.jinja index cf992b97..64a96d07 100644 --- a/election/templates/election/vote_form.jinja +++ b/election/templates/election/candidate_form.jinja @@ -1,7 +1,7 @@ {% extends "core/base.jinja" %} {% block title %} -{% trans %}Vote{% endtrans %} +{% trans %}Candidate{% endtrans %} {% endblock %} {% block content %} diff --git a/election/views.py b/election/views.py index 58220c89..a0c1892f 100644 --- a/election/views.py +++ b/election/views.py @@ -60,8 +60,8 @@ class CandidateForm(forms.Form): class VoteForm(forms.Form): def __init__(self, election, user, *args, **kwargs): super(VoteForm, self).__init__(*args, **kwargs) - for role in election.roles.all(): - if not role.user_has_voted(user): + if not election.has_voted(user): + for role in election.roles.all(): cand = role.candidatures if role.max_choice > 1: self.fields[role.title] = VoteCheckbox(cand, role.max_choice, required=False) @@ -112,9 +112,10 @@ class ElectionDetailView(CanViewMixin, DetailView): def get_context_data(self, **kwargs): """ Add additionnal data to the template """ kwargs = super(ElectionDetailView, self).get_context_data(**kwargs) - kwargs['candidate_form'] = CandidateForm(self.get_object().id) - kwargs['election_form'] = VoteForm(self.get_object(), self.request.user) - kwargs['election_results'] = self.get_object().results + kwargs['candidate_form'] = CandidateForm(self.object.id) + kwargs['election_form'] = VoteForm(self.object, self.request.user) + kwargs['election_results'] = self.object.results + print(self.object.results) return kwargs @@ -196,7 +197,7 @@ class VoteFormView(CanCreateMixin, FormView): vote = Vote(role=election_data[role_title].role) vote.save() vote.candidature.add(election_data[role_title]) - self.election.roles.get(title=role_title).has_voted.add(self.request.user) + self.election.voters.add(self.request.user) def get_form_kwargs(self): kwargs = super(VoteFormView, self).get_form_kwargs() From 02913d91e6725170a4d5468cb2da60a533bb46c1 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Sat, 24 Dec 2016 02:16:21 +0100 Subject: [PATCH 43/54] Refactors Candidate form --- .../templates/election/candidate_form.jinja | 13 +- .../templates/election/election_detail.jinja | 17 +-- election/views.py | 143 +++++++++--------- 3 files changed, 88 insertions(+), 85 deletions(-) diff --git a/election/templates/election/candidate_form.jinja b/election/templates/election/candidate_form.jinja index 64a96d07..617582a5 100644 --- a/election/templates/election/candidate_form.jinja +++ b/election/templates/election/candidate_form.jinja @@ -5,9 +5,12 @@ {% endblock %} {% block content %} - - {% csrf_token %} - {{ form.as_p() }} -

            - + {%- if election.can_candidate(user) or user.can_edit(election) %} +
            +
            {{form.as_p()}} +

            + {% csrf_token %} +
            +
            + {%- endif %} {% endblock content %} \ No newline at end of file diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index 2b86e546..6c0e6cb6 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -344,20 +344,15 @@ th { {%- endif %} - {%- if user.can_edit(election) %}
            - {% trans %}Add a new list{% endtrans %} - {% trans %}Add a new role{% endtrans %} -
            - {%- endif %} {%- if election.can_candidate(user) or user.can_edit(election) %} -
            -
            {{candidate_form}} - {% csrf_token %} -

            -
            -
            + {% trans %}Candidate{% endtrans %} {%- endif %} + {% trans %}Add a new list{% endtrans %} + {%- if user.can_edit(election) %} + {% trans %}Add a new role{% endtrans %} + {%- endif %} + {% endblock %} {% block script %} diff --git a/election/views.py b/election/views.py index a0c1892f..6a3c098f 100644 --- a/election/views.py +++ b/election/views.py @@ -21,7 +21,7 @@ from ajax_select.fields import AutoCompleteSelectField # Custom form field -class VoteCheckbox(forms.ModelMultipleChoiceField): +class LimitedCheckboxField(forms.ModelMultipleChoiceField): """ Used to replace ModelMultipleChoiceField but with automatic backend verification @@ -30,11 +30,11 @@ class VoteCheckbox(forms.ModelMultipleChoiceField): initial=None, help_text='', *args, **kwargs): self.max_choice = max_choice widget = forms.CheckboxSelectMultiple() - super(VoteCheckbox, self).__init__(queryset, None, required, widget, label, + super(LimitedCheckboxField, self).__init__(queryset, None, required, widget, label, initial, help_text, *args, **kwargs) def clean(self, value): - qs = super(VoteCheckbox, self).clean(value) + qs = super(LimitedCheckboxField, self).clean(value) self.validate(qs) return qs @@ -46,15 +46,26 @@ class VoteCheckbox(forms.ModelMultipleChoiceField): # Forms -class CandidateForm(forms.Form): +class CandidateForm(forms.ModelForm): """ Form to candidate """ - user = AutoCompleteSelectField('users', label=_('User to candidate'), help_text=None, required=True) - program = forms.CharField(widget=forms.Textarea) + class Meta: + model = Candidature + fields = ['user', 'role', 'program', 'election_list'] + widgets = { + 'program': forms.Textarea + } - def __init__(self, election_id, *args, **kwargs): + user = AutoCompleteSelectField('users', label=_('User to candidate'), help_text=None, required=True) + + def __init__(self, *args, **kwargs): + election_id = kwargs.pop('election_id', None) + can_edit = kwargs.pop('can_edit', False) super(CandidateForm, self).__init__(*args, **kwargs) - self.fields['role'] = forms.ModelChoiceField(Role.objects.filter(election__id=election_id)) - self.fields['election_list'] = forms.ModelChoiceField(ElectionList.objects.filter(election__id=election_id)) + if election_id: + self.fields['role'].queryset = Role.objects.filter(election__id=election_id).all() + self.fields['election_list'].queryset = ElectionList.objects.filter(election__id=election_id).all() + if not can_edit: + self.fields['user'].widget = forms.HiddenInput() class VoteForm(forms.Form): @@ -64,7 +75,7 @@ class VoteForm(forms.Form): for role in election.roles.all(): cand = role.candidatures if role.max_choice > 1: - self.fields[role.title] = VoteCheckbox(cand, role.max_choice, required=False) + self.fields[role.title] = LimitedCheckboxField(cand, role.max_choice, required=False) else: self.fields[role.title] = forms.ModelChoiceField(cand, required=False, widget=forms.RadioSelect(), empty_label=_("Blank vote")) @@ -112,66 +123,13 @@ class ElectionDetailView(CanViewMixin, DetailView): def get_context_data(self, **kwargs): """ Add additionnal data to the template """ kwargs = super(ElectionDetailView, self).get_context_data(**kwargs) - kwargs['candidate_form'] = CandidateForm(self.object.id) kwargs['election_form'] = VoteForm(self.object, self.request.user) kwargs['election_results'] = self.object.results - print(self.object.results) return kwargs # Form view -class CandidatureCreateView(CanCreateMixin, FormView): - """ - View dedicated to a cundidature creation - """ - form_class = CandidateForm - template_name = 'election/election_detail.jinja' - - def dispatch(self, request, *arg, **kwargs): - self.election_id = kwargs['election_id'] - self.election = get_object_or_404(Election, pk=self.election_id) - return super(CandidatureCreateView, self).dispatch(request, *arg, **kwargs) - - def get_form_kwargs(self): - kwargs = super(CandidatureCreateView, self).get_form_kwargs() - kwargs['election_id'] = self.election_id - return kwargs - - def create_candidature(self, data): - cand = Candidature( - role=data['role'], - user=data['user'], - election_list=data['election_list'], - program=data['program'] - ) - cand.save() - - def form_valid(self, form): - """ - Verify that the selected user is in candidate group - """ - data = form.clean() - res = super(FormView, self).form_valid(form) - data['election'] = Election.objects.get(id=self.election_id) - if(data['election'].can_candidate(data['user'])): - self.create_candidature(data) - return res - return res - - def get_context_data(self, **kwargs): - """ Add additionnal data to the template """ - kwargs = super(CandidatureCreateView, self).get_context_data(**kwargs) - kwargs['candidate_form'] = self.get_form() - kwargs['object'] = self.election - kwargs['election'] = self.election - kwargs['election_form'] = VoteForm(self.election, self.request.user) - return kwargs - - def get_success_url(self, **kwargs): - return reverse_lazy('election:detail', kwargs={'election_id': self.election_id}) - - class VoteFormView(CanCreateMixin, FormView): """ Alows users to vote @@ -223,7 +181,6 @@ class VoteFormView(CanCreateMixin, FormView): def get_context_data(self, **kwargs): """ Add additionnal data to the template """ kwargs = super(VoteFormView, self).get_context_data(**kwargs) - kwargs['candidate_form'] = CandidateForm(self.election.id) kwargs['object'] = self.election kwargs['election'] = self.election kwargs['election_form'] = self.get_form() @@ -232,6 +189,48 @@ class VoteFormView(CanCreateMixin, FormView): # Create views +class CandidatureCreateView(CanCreateMixin, CreateView): + """ + View dedicated to a cundidature creation + """ + form_class = CandidateForm + model = Candidature + template_name = 'election/candidate_form.jinja' + + def dispatch(self, request, *arg, **kwargs): + self.election = get_object_or_404(Election, pk=kwargs['election_id']) + return super(CandidatureCreateView, self).dispatch(request, *arg, **kwargs) + + def get_initial(self): + init = {} + self.can_edit = self.request.user.can_edit(self.election) + init['user'] = self.request.user.id + return init + + def get_form_kwargs(self): + kwargs = super(CandidatureCreateView, self).get_form_kwargs() + kwargs['election_id'] = self.election.id + kwargs['can_edit'] = self.can_edit + return kwargs + + def form_valid(self, form): + """ + Verify that the selected user is in candidate group + """ + obj = form.instance + obj.election = Election.objects.get(id=self.election.id) + if(obj.election.can_candidate(obj.user)) and (obj.user == self.request.user or self.can_edit): + return super(CreateView, self).form_valid(form) + raise PermissionDenied + + def get_context_data(self, **kwargs): + kwargs = super(CandidatureCreateView, self).get_context_data(**kwargs) + kwargs['election'] = self.election + return kwargs + + def get_success_url(self, **kwargs): + return reverse_lazy('election:detail', kwargs={'election_id': self.election.id}) + class ElectionCreateView(CanCreateMixin, CreateView): model = Election @@ -251,6 +250,14 @@ class ElectionCreateView(CanCreateMixin, CreateView): }) template_name = 'core/page_prop.jinja' + def form_valid(self, form): + """ + Verify that the user is suscribed + """ + res = super(CreateView, self).form_valid(form) + if self.request.user.is_subscribed(): + return res + def get_success_url(self, **kwargs): return reverse_lazy('election:detail', kwargs={'election_id': self.object.id}) @@ -265,11 +272,10 @@ class RoleCreateView(CanCreateMixin, CreateView): Verify that the user can edit proprely """ obj = form.instance - res = super(CreateView, self).form_valid if obj.election: for grp in obj.election.edit_groups.all(): if self.request.user.is_in_group(grp): - return res(form) + return super(CreateView, self).form_valid(form) raise PermissionDenied def get_success_url(self, **kwargs): @@ -287,14 +293,13 @@ class ElectionListCreateView(CanCreateMixin, CreateView): Verify that the user can vote on this election """ obj = form.instance - res = super(CreateView, self).form_valid if obj.election: for grp in obj.election.candidature_groups.all(): if self.request.user.is_in_group(grp): - return res(form) + return super(CreateView, self).form_valid(form) for grp in obj.election.edit_groups.all(): if self.request.user.is_in_group(grp): - return res(form) + return super(CreateView, self).form_valid(form) raise PermissionDenied def get_success_url(self, **kwargs): From d7387005c05a5bfca1115a2bb826906ffd8bdd63 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Sat, 24 Dec 2016 03:06:39 +0100 Subject: [PATCH 44/54] Better role creation --- .../templates/election/election_detail.jinja | 2 +- election/urls.py | 8 +++---- election/views.py | 22 ++++++++++++++++++- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index 6c0e6cb6..c8fdf077 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -350,7 +350,7 @@ th { {%- endif %} {% trans %}Add a new list{% endtrans %} {%- if user.can_edit(election) %} - {% trans %}Add a new role{% endtrans %} + {% trans %}Add a new role{% endtrans %} {%- endif %} {% endblock %} diff --git a/election/urls.py b/election/urls.py index 59ad8a42..33cfdb33 100644 --- a/election/urls.py +++ b/election/urls.py @@ -4,10 +4,10 @@ from election.views import * urlpatterns = [ url(r'^$', ElectionsListView.as_view(), name='list'), - url(r'^create$', ElectionCreateView.as_view(), name='create'), - url(r'^list/create$', ElectionListCreateView.as_view(), name='create_list'), - url(r'^role/create$', RoleCreateView.as_view(), name='create_role'), - url(r'^(?P[0-9]+)/candidate$', CandidatureCreateView.as_view(), name='candidate'), + url(r'^add$', ElectionCreateView.as_view(), name='create'), + url(r'^list/add$', ElectionListCreateView.as_view(), name='create_list'), + url(r'^(?P[0-9]+)/role/create$', RoleCreateView.as_view(), name='create_role'), + url(r'^(?P[0-9]+)/candidate/add$', CandidatureCreateView.as_view(), name='candidate'), url(r'^(?P[0-9]+)/vote$', VoteFormView.as_view(), name='vote'), url(r'^(?P[0-9]+)/detail$', ElectionDetailView.as_view(), name='detail'), ] diff --git a/election/views.py b/election/views.py index 6a3c098f..21bf5a87 100644 --- a/election/views.py +++ b/election/views.py @@ -31,7 +31,7 @@ class LimitedCheckboxField(forms.ModelMultipleChoiceField): self.max_choice = max_choice widget = forms.CheckboxSelectMultiple() super(LimitedCheckboxField, self).__init__(queryset, None, required, widget, label, - initial, help_text, *args, **kwargs) + initial, help_text, *args, **kwargs) def clean(self, value): qs = super(LimitedCheckboxField, self).clean(value) @@ -87,6 +87,12 @@ class RoleForm(forms.ModelForm): model = Role fields = ['title', 'election', 'description', 'max_choice'] + def __init__(self, *args, **kwargs): + election_id = kwargs.pop('election_id', None) + super(RoleForm, self).__init__(*args, **kwargs) + if election_id: + self.fields['election'].queryset = Election.objects.filter(id=election_id).all() + def clean(self): cleaned_data = super(RoleForm, self).clean() title = cleaned_data.get('title') @@ -267,6 +273,15 @@ class RoleCreateView(CanCreateMixin, CreateView): form_class = RoleForm template_name = 'core/page_prop.jinja' + def dispatch(self, request, *arg, **kwargs): + self.election = get_object_or_404(Election, pk=kwargs['election_id']) + return super(RoleCreateView, self).dispatch(request, *arg, **kwargs) + + def get_initial(self): + init = {} + init['election'] = self.election + return init + def form_valid(self, form): """ Verify that the user can edit proprely @@ -278,6 +293,11 @@ class RoleCreateView(CanCreateMixin, CreateView): return super(CreateView, self).form_valid(form) raise PermissionDenied + def get_form_kwargs(self): + kwargs = super(RoleCreateView, self).get_form_kwargs() + kwargs['election_id'] = self.election.id + return kwargs + def get_success_url(self, **kwargs): return reverse_lazy('election:detail', kwargs={'election_id': self.object.election.id}) From 4f62863599ca81ce582145da66a8f6b0c82de24b Mon Sep 17 00:00:00 2001 From: klmp200 Date: Sat, 24 Dec 2016 03:36:28 +0100 Subject: [PATCH 45/54] Pimp role and list forms and add edit for election --- core/templates/core/user_tools.jinja | 6 +- .../templates/election/election_detail.jinja | 3 +- election/urls.py | 3 +- election/views.py | 93 +++++++++++++------ 4 files changed, 75 insertions(+), 30 deletions(-) diff --git a/core/templates/core/user_tools.jinja b/core/templates/core/user_tools.jinja index 43f986bc..0259f24b 100644 --- a/core/templates/core/user_tools.jinja +++ b/core/templates/core/user_tools.jinja @@ -88,8 +88,10 @@

            {% trans %}Elections{% endtrans %}

            {% endblock %} diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index c8fdf077..c1d3d5f9 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -348,9 +348,10 @@ th { {%- if election.can_candidate(user) or user.can_edit(election) %} {% trans %}Candidate{% endtrans %} {%- endif %} - {% trans %}Add a new list{% endtrans %} + {% trans %}Add a new list{% endtrans %} {%- if user.can_edit(election) %} {% trans %}Add a new role{% endtrans %} + {% trans %}Edit{% endtrans %} {%- endif %} {% endblock %} diff --git a/election/urls.py b/election/urls.py index 33cfdb33..75d13c02 100644 --- a/election/urls.py +++ b/election/urls.py @@ -5,7 +5,8 @@ from election.views import * urlpatterns = [ url(r'^$', ElectionsListView.as_view(), name='list'), url(r'^add$', ElectionCreateView.as_view(), name='create'), - url(r'^list/add$', ElectionListCreateView.as_view(), name='create_list'), + url(r'^(?P[0-9]+)/edit$', ElectionUpdateView.as_view(), name='update'), + url(r'^(?P[0-9]+)/list/add$', ElectionListCreateView.as_view(), name='create_list'), url(r'^(?P[0-9]+)/role/create$', RoleCreateView.as_view(), name='create_role'), url(r'^(?P[0-9]+)/candidate/add$', CandidatureCreateView.as_view(), name='candidate'), url(r'^(?P[0-9]+)/vote$', VoteFormView.as_view(), name='vote'), diff --git a/election/views.py b/election/views.py index 21bf5a87..ce1a0964 100644 --- a/election/views.py +++ b/election/views.py @@ -101,6 +101,36 @@ class RoleForm(forms.ModelForm): raise forms.ValidationError(_("This role already exists for this election"), code='invalid') +class ElectionListForm(forms.ModelForm): + class Meta: + model = ElectionList + fields = ('title','election') + + def __init__(self, *args, **kwargs): + election_id = kwargs.pop('election_id', None) + super(ElectionListForm, self).__init__(*args, **kwargs) + if election_id: + self.fields['election'].queryset = Election.objects.filter(id=election_id).all() + + +class ElectionForm(forms.ModelForm): + class Meta: + model = Election + fields = ['title', 'description', 'start_candidature', 'end_candidature', 'start_date', 'end_date', + 'edit_groups', 'view_groups', 'vote_groups', 'candidature_groups'] + widgets = { + 'edit_groups': CheckboxSelectMultiple, + 'view_groups': CheckboxSelectMultiple, + 'edit_groups': CheckboxSelectMultiple, + 'vote_groups': CheckboxSelectMultiple, + 'candidature_groups': CheckboxSelectMultiple, + 'start_date': SelectDateTime, + 'end_date': SelectDateTime, + 'start_candidature': SelectDateTime, + 'end_candidature': SelectDateTime, + } + + # Display elections @@ -111,12 +141,6 @@ class ElectionsListView(CanViewMixin, ListView): model = Election template_name = 'election/election_list.jinja' - def get_queryset(self): - qs = super(ElectionsListView, self).get_queryset() - today = timezone.now() - qs = qs.filter(end_date__gte=today, start_date__lte=today) - return qs - class ElectionDetailView(CanViewMixin, DetailView): """ @@ -240,29 +264,19 @@ class CandidatureCreateView(CanCreateMixin, CreateView): class ElectionCreateView(CanCreateMixin, CreateView): model = Election - form_class = modelform_factory(Election, - fields=['title', 'description', 'start_candidature', 'end_candidature', 'start_date', 'end_date', - 'edit_groups', 'view_groups', 'vote_groups', 'candidature_groups'], - widgets={ - 'edit_groups': CheckboxSelectMultiple, - 'view_groups': CheckboxSelectMultiple, - 'edit_groups': CheckboxSelectMultiple, - 'vote_groups': CheckboxSelectMultiple, - 'candidature_groups': CheckboxSelectMultiple, - 'start_date': SelectDateTime, - 'end_date': SelectDateTime, - 'start_candidature': SelectDateTime, - 'end_candidature': SelectDateTime, - }) + form_class = ElectionForm template_name = 'core/page_prop.jinja' + def dispatch(self, request, *args, **kwargs): + if not request.user.is_subscribed(): + raise PermissionDenied + return super(ElectionCreateView, self).dispatch(request, *args, **kwargs) + def form_valid(self, form): """ - Verify that the user is suscribed + Allow every users that had passed the dispatch to create an election """ - res = super(CreateView, self).form_valid(form) - if self.request.user.is_subscribed(): - return res + return super(CreateView, self).form_valid(form) def get_success_url(self, **kwargs): return reverse_lazy('election:detail', kwargs={'election_id': self.object.id}) @@ -275,6 +289,8 @@ class RoleCreateView(CanCreateMixin, CreateView): def dispatch(self, request, *arg, **kwargs): self.election = get_object_or_404(Election, pk=kwargs['election_id']) + if self.election.is_vote_active or self.election.is_vote_finished: + raise PermissionDenied return super(RoleCreateView, self).dispatch(request, *arg, **kwargs) def get_initial(self): @@ -304,10 +320,25 @@ class RoleCreateView(CanCreateMixin, CreateView): class ElectionListCreateView(CanCreateMixin, CreateView): model = ElectionList - form_class = modelform_factory(ElectionList, - fields=['title', 'election']) + form_class = ElectionListForm template_name = 'core/page_prop.jinja' + def dispatch(self, request, *arg, **kwargs): + self.election = get_object_or_404(Election, pk=kwargs['election_id']) + if self.election.is_vote_finished: + raise PermissionDenied + return super(ElectionListCreateView, self).dispatch(request, *arg, **kwargs) + + def get_initial(self): + init = {} + init['election'] = self.election + return init + + def get_form_kwargs(self): + kwargs = super(ElectionListCreateView, self).get_form_kwargs() + kwargs['election_id'] = self.election.id + return kwargs + def form_valid(self, form): """ Verify that the user can vote on this election @@ -324,3 +355,13 @@ class ElectionListCreateView(CanCreateMixin, CreateView): def get_success_url(self, **kwargs): return reverse_lazy('election:detail', kwargs={'election_id': self.object.election.id}) + +# Update view + + +class ElectionUpdateView(CanEditMixin, UpdateView): + model = Election + form_class = ElectionForm + template_name = 'core/page_prop.jinja' + pk_url_kwarg = 'election_id' + From c07f49305b9bd82524535176a864346604fd011e Mon Sep 17 00:00:00 2001 From: klmp200 Date: Sat, 24 Dec 2016 18:29:26 +0100 Subject: [PATCH 46/54] Full CRUD for elections --- election/models.py | 11 ++ .../templates/election/create_template.jinja | 15 +++ .../templates/election/delete_template.jinja | 12 ++ .../templates/election/election_detail.jinja | 12 ++ .../templates/election/update_template.jinja | 15 +++ election/urls.py | 4 + election/views.py | 122 +++++++++++++++++- 7 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 election/templates/election/create_template.jinja create mode 100644 election/templates/election/delete_template.jinja create mode 100644 election/templates/election/update_template.jinja diff --git a/election/models.py b/election/models.py index 28d18e0d..097c1a58 100644 --- a/election/models.py +++ b/election/models.py @@ -41,6 +41,10 @@ class Election(models.Model): now = timezone.now() return bool(now <= self.end_candidature and now >= self.start_candidature) + @property + def is_vote_editable(self): + return bool(timezone.now() <= self.end_candidature) + def can_candidate(self, user): for group in self.candidature_groups.all(): if user.is_in_group(group): @@ -95,6 +99,10 @@ class Role(models.Model): 'percent': (total_vote - non_blank) * 100 / total_vote} return results + @property + def edit_groups(self): + return self.election.edit_groups + def __str__(self): return ("%s : %s") % (self.election.title, self.title) @@ -119,6 +127,9 @@ class Candidature(models.Model): program = models.TextField(_('description'), null=True, blank=True) election_list = models.ForeignKey(ElectionList, related_name='candidatures', verbose_name=_('election_list')) + def can_be_edited_by(self, user): + return (user == self.user) + def __str__(self): return "%s : %s" % (self.role.title, self.user.username) diff --git a/election/templates/election/create_template.jinja b/election/templates/election/create_template.jinja new file mode 100644 index 00000000..3836f39b --- /dev/null +++ b/election/templates/election/create_template.jinja @@ -0,0 +1,15 @@ +{% extends "core/base.jinja" %} + +{% block title %} +{% trans %}Create{% endtrans %} +{% endblock %} + +{% block content %} +

            {% trans %}Create{% endtrans %} +
            +
            {{form.as_p()}} +

            + {% csrf_token %} +
            +
            +{% endblock content %} \ No newline at end of file diff --git a/election/templates/election/delete_template.jinja b/election/templates/election/delete_template.jinja new file mode 100644 index 00000000..06ed72d4 --- /dev/null +++ b/election/templates/election/delete_template.jinja @@ -0,0 +1,12 @@ +{% extends "core/base.jinja" %} + +{% block title %} +{% trans %}Delete{% endtrans %} +{% endblock %} + +{% block content %} +
            {% csrf_token %} +

            {% trans %}Are you sure you want to delete {% endtrans %}"{{ object }}"?

            + +
            +{% endblock content %} \ No newline at end of file diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index c1d3d5f9..f87cd971 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -273,6 +273,10 @@ th {

          {{role.title}} + {% if user.can_edit(role) and election.is_vote_editable -%} + {% trans %}Edit{% endtrans %} + {% trans %}Delete{% endtrans %} + {%- endif -%} {%- if role.max_choice > 1 and not election.has_voted(user) and election.can_vote(user) %} {% trans %}You may choose up to{% endtrans %} {{ role.max_choice }} {% trans %}people.{% endtrans %} {%- endif %} @@ -313,6 +317,14 @@ th {
          {{ candidature.user.first_name }} {{candidature.user.nick_name or ''}} {{ candidature.user.last_name }} {{ candidature.program or '' }} + {%- if user.can_edit(candidature) -%} + {% if election.is_vote_editable %} + {% trans %}Edit{% endtrans %} + {% endif %} + {% if election.can_candidate -%} + {% trans %}Delete{% endtrans %} + {%- endif -%} + {%- endif -%}
          {%- if election.can_vote(user) %} diff --git a/election/templates/election/update_template.jinja b/election/templates/election/update_template.jinja new file mode 100644 index 00000000..604e55c4 --- /dev/null +++ b/election/templates/election/update_template.jinja @@ -0,0 +1,15 @@ +{% extends "core/base.jinja" %} + +{% block title %} +{% trans %}Edit{% endtrans %} +{% endblock %} + +{% block content %} +

          {% trans %}Edit{% endtrans %} +
          +
          {{form.as_p()}} +

          + {% csrf_token %} +
          +
          +{% endblock content %} \ No newline at end of file diff --git a/election/urls.py b/election/urls.py index 75d13c02..8140cb49 100644 --- a/election/urls.py +++ b/election/urls.py @@ -8,7 +8,11 @@ urlpatterns = [ url(r'^(?P[0-9]+)/edit$', ElectionUpdateView.as_view(), name='update'), url(r'^(?P[0-9]+)/list/add$', ElectionListCreateView.as_view(), name='create_list'), url(r'^(?P[0-9]+)/role/create$', RoleCreateView.as_view(), name='create_role'), + url(r'^(?P[0-9]+)/role/edit$', RoleUpdateView.as_view(), name='update_role'), + url(r'^(?P[0-9]+)/role/delete$', RoleDeleteView.as_view(), name='delete_role'), url(r'^(?P[0-9]+)/candidate/add$', CandidatureCreateView.as_view(), name='candidate'), + url(r'^(?P[0-9]+)/candidate/edit$', CandidatureUpdateView.as_view(), name='update_candidate'), + url(r'^(?P[0-9]+)/candidate/delete$', CandidatureDeleteView.as_view(), name='delete_candidate'), url(r'^(?P[0-9]+)/vote$', VoteFormView.as_view(), name='vote'), url(r'^(?P[0-9]+)/detail$', ElectionDetailView.as_view(), name='detail'), ] diff --git a/election/views.py b/election/views.py index ce1a0964..4dd56448 100644 --- a/election/views.py +++ b/election/views.py @@ -265,7 +265,7 @@ class CandidatureCreateView(CanCreateMixin, CreateView): class ElectionCreateView(CanCreateMixin, CreateView): model = Election form_class = ElectionForm - template_name = 'core/page_prop.jinja' + template_name = 'election/create_template.jinja' def dispatch(self, request, *args, **kwargs): if not request.user.is_subscribed(): @@ -285,11 +285,11 @@ class ElectionCreateView(CanCreateMixin, CreateView): class RoleCreateView(CanCreateMixin, CreateView): model = Role form_class = RoleForm - template_name = 'core/page_prop.jinja' + template_name = 'election/create_template.jinja' def dispatch(self, request, *arg, **kwargs): self.election = get_object_or_404(Election, pk=kwargs['election_id']) - if self.election.is_vote_active or self.election.is_vote_finished: + if self.election.is_vote_editable: raise PermissionDenied return super(RoleCreateView, self).dispatch(request, *arg, **kwargs) @@ -321,11 +321,11 @@ class RoleCreateView(CanCreateMixin, CreateView): class ElectionListCreateView(CanCreateMixin, CreateView): model = ElectionList form_class = ElectionListForm - template_name = 'core/page_prop.jinja' + template_name = 'election/create_template.jinja' def dispatch(self, request, *arg, **kwargs): self.election = get_object_or_404(Election, pk=kwargs['election_id']) - if self.election.is_vote_finished: + if not self.election.is_vote_editable: raise PermissionDenied return super(ElectionListCreateView, self).dispatch(request, *arg, **kwargs) @@ -362,6 +362,116 @@ class ElectionListCreateView(CanCreateMixin, CreateView): class ElectionUpdateView(CanEditMixin, UpdateView): model = Election form_class = ElectionForm - template_name = 'core/page_prop.jinja' + template_name = 'election/update_template.jinja' pk_url_kwarg = 'election_id' + def get_success_url(self, **kwargs): + return reverse_lazy('election:detail', kwargs={'election_id': self.object.id}) + + +class CandidatureUpdateView(CanEditMixin, UpdateView): + model = Candidature + form_class = CandidateForm + template_name = 'election/update_template.jinja' + pk_url_kwarg = 'candidature_id' + + def dispatch(self, request, *arg, **kwargs): + self.object = self.get_object() + if not self.object.role.election.is_vote_editable: + raise PermissionDenied + return super(CandidatureUpdateView, self).dispatch(request, *arg, **kwargs) + + def remove_fields(self): + self.form.fields.pop('role', None) + + def get(self, request, *args, **kwargs): + self.form = self.get_form() + self.remove_fields() + return self.render_to_response(self.get_context_data(form=self.form)) + + def post(self, request, *args, **kwargs): + self.form = self.get_form() + self.remove_fields() + if request.user.is_authenticated() and request.user.can_edit(self.object) and self.form.is_valid(): + return super(CandidatureUpdateView, self).form_valid(self.form) + return self.form_invalid(self.form) + + def get_form_kwargs(self): + kwargs = super(CandidatureUpdateView, self).get_form_kwargs() + kwargs['election_id'] = self.object.role.election.id + return kwargs + + def get_success_url(self, **kwargs): + return reverse_lazy('election:detail', kwargs={'election_id': self.object.role.election.id}) + + +class RoleUpdateView(CanEditMixin, UpdateView): + model = Role + form_class = RoleForm + template_name = 'election/update_template.jinja' + pk_url_kwarg = 'role_id' + + def dispatch(self, request, *arg, **kwargs): + self.object = self.get_object() + if not self.object.election.is_vote_editable: + raise PermissionDenied + return super(RoleUpdateView, self).dispatch(request, *arg, **kwargs) + + def remove_fields(self): + self.form.fields.pop('election', None) + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + self.form = self.get_form() + self.remove_fields() + return self.render_to_response(self.get_context_data(form=self.form)) + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + self.form = self.get_form() + self.remove_fields() + if request.user.is_authenticated() and request.user.can_edit(self.object) and self.form.is_valid(): + return super(RoleUpdateView, self).form_valid(self.form) + return self.form_invalid(self.form) + + def get_form_kwargs(self): + kwargs = super(RoleUpdateView, self).get_form_kwargs() + kwargs['election_id'] = self.object.election.id + return kwargs + + def get_success_url(self, **kwargs): + return reverse_lazy('election:detail', kwargs={'election_id': self.object.election.id}) + +# Delete Views + + +class CandidatureDeleteView(CanEditMixin, DeleteView): + model = Candidature + template_name = 'election/delete_template.jinja' + pk_url_kwarg = 'candidature_id' + + def dispatch(self, request, *arg, **kwargs): + self.object = self.get_object() + self.election = self.object.role.election + if not self.election.can_candidate: + raise PermissionDenied + return super(CandidatureDeleteView, self).dispatch(request, *arg, **kwargs) + + def get_success_url(self, **kwargs): + return reverse_lazy('election:detail', kwargs={'election_id': self.election.id}) + + +class RoleDeleteView(CanEditMixin, DeleteView): + model = Role + template_name = 'election/delete_template.jinja' + pk_url_kwarg = 'role_id' + + def dispatch(self, request, *arg, **kwargs): + self.object = self.get_object() + self.election = self.object.election + if not self.election.is_vote_editable: + raise PermissionDenied + return super(RoleDeleteView, self).dispatch(request, *arg, **kwargs) + + def get_success_url(self, **kwargs): + return reverse_lazy('election:detail', kwargs={'election_id': self.election.id}) From 4d067165aa8535d85435dcdca72b62652c4ae246 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Sun, 25 Dec 2016 20:09:18 +0100 Subject: [PATCH 47/54] Election's trad --- election/models.py | 2 +- .../templates/election/candidate_form.jinja | 2 + locale/fr/LC_MESSAGES/django.po | 286 ++++++++++++++---- 3 files changed, 233 insertions(+), 57 deletions(-) diff --git a/election/models.py b/election/models.py index 097c1a58..36c5301a 100644 --- a/election/models.py +++ b/election/models.py @@ -125,7 +125,7 @@ class Candidature(models.Model): role = models.ForeignKey(Role, related_name='candidatures', verbose_name=_("role")) user = models.ForeignKey(User, verbose_name=_('user'), related_name='candidates', blank=True) program = models.TextField(_('description'), null=True, blank=True) - election_list = models.ForeignKey(ElectionList, related_name='candidatures', verbose_name=_('election_list')) + election_list = models.ForeignKey(ElectionList, related_name='candidatures', verbose_name=_('election list')) def can_be_edited_by(self, user): return (user == self.user) diff --git a/election/templates/election/candidate_form.jinja b/election/templates/election/candidate_form.jinja index 617582a5..ccb6fa91 100644 --- a/election/templates/election/candidate_form.jinja +++ b/election/templates/election/candidate_form.jinja @@ -12,5 +12,7 @@ {% csrf_token %} + {%- else -%} + {% trans %}Candidature are closed for this election{% endtrans %} {%- endif %} {% endblock content %} \ No newline at end of file diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 88ab8e69..46ce07d5 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-12-23 20:10+0100\n" +"POT-Creation-Date: 2016-12-25 19:56+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Skia \n" "Language-Team: AE info \n" @@ -86,11 +86,12 @@ msgid "%(club_account)s on %(bank_account)s" msgstr "%(club_account)s sur %(bank_account)s" #: accounting/models.py:142 club/models.py:146 counter/models.py:399 -#: launderette/models.py:120 +#: election/models.py:18 launderette/models.py:120 msgid "start date" msgstr "date de début" #: accounting/models.py:143 club/models.py:147 counter/models.py:400 +#: election/models.py:19 msgid "end date" msgstr "date de fin" @@ -192,7 +193,7 @@ msgstr "Compte" msgid "Company" msgstr "Entreprise" -#: accounting/models.py:207 sith/settings.py:296 +#: accounting/models.py:207 sith/settings.py:297 msgid "Other" msgstr "Autre" @@ -321,6 +322,9 @@ msgstr "Compte en banque : " #: core/templates/core/user_edit.jinja:19 #: counter/templates/counter/last_ops.jinja:29 #: counter/templates/counter/last_ops.jinja:59 +#: election/templates/election/delete_template.jinja:4 +#: election/templates/election/election_detail.jinja:278 +#: election/templates/election/election_detail.jinja:325 #: launderette/templates/launderette/launderette_admin.jinja:16 #: launderette/views.py:154 sas/templates/sas/album.jinja:26 #: sas/templates/sas/moderation.jinja:18 sas/templates/sas/picture.jinja:66 @@ -356,6 +360,11 @@ msgstr "Nouveau compte club" #: counter/templates/counter/counter_list.jinja:17 #: counter/templates/counter/counter_list.jinja:32 #: counter/templates/counter/counter_list.jinja:47 +#: election/templates/election/election_detail.jinja:277 +#: election/templates/election/election_detail.jinja:322 +#: election/templates/election/election_detail.jinja:366 +#: election/templates/election/update_template.jinja:4 +#: election/templates/election/update_template.jinja:8 #: launderette/templates/launderette/launderette_list.jinja:16 #: sas/templates/sas/album.jinja:18 sas/templates/sas/picture.jinja:92 msgid "Edit" @@ -634,6 +643,8 @@ msgstr "Éditer l'opération" #: core/templates/core/pagerev_edit.jinja:24 #: core/templates/core/user_godfathers.jinja:35 #: counter/templates/counter/cash_register_summary.jinja:22 +#: election/templates/election/create_template.jinja:11 +#: election/templates/election/update_template.jinja:11 #: subscription/templates/subscription/subscription.jinja:23 msgid "Save" msgstr "Sauver" @@ -766,17 +777,19 @@ msgid "A club with that unix_name already exists" msgstr "Un club avec ce nom UNIX existe déjà." #: club/models.py:144 counter/models.py:397 counter/models.py:414 -#: eboutic/models.py:14 eboutic/models.py:47 launderette/models.py:87 -#: launderette/models.py:124 sas/models.py:131 +#: eboutic/models.py:14 eboutic/models.py:47 election/models.py:126 +#: launderette/models.py:87 launderette/models.py:124 sas/models.py:131 msgid "user" msgstr "nom d'utilisateur" -#: club/models.py:148 core/models.py:137 +#: club/models.py:148 core/models.py:137 election/models.py:125 +#: election/models.py:141 msgid "role" msgstr "rôle" #: club/models.py:150 core/models.py:33 counter/models.py:71 -#: counter/models.py:96 +#: counter/models.py:96 election/models.py:15 election/models.py:82 +#: election/models.py:127 msgid "description" msgstr "description" @@ -1016,7 +1029,8 @@ msgstr "Hebdomadaire" msgid "Call" msgstr "Appel" -#: com/models.py:30 +#: com/models.py:30 election/models.py:14 election/models.py:81 +#: election/models.py:114 msgid "title" msgstr "titre" @@ -1484,6 +1498,7 @@ msgstr "Un utilisateur de ce nom d'utilisateur existe déjà" #: core/templates/core/user_detail.jinja:14 #: core/templates/core/user_detail.jinja:16 #: core/templates/core/user_edit.jinja:17 +#: election/templates/election/election_detail.jinja:314 msgid "Profile" msgstr "Profil" @@ -2421,6 +2436,18 @@ msgstr "Modérer les fichiers" msgid "Moderate pictures" msgstr "Modérer les photos" +#: core/templates/core/user_tools.jinja:89 +msgid "Elections" +msgstr "Élections" + +#: core/templates/core/user_tools.jinja:91 +msgid "See avaliable elections" +msgstr "Voir les élections disponibles" + +#: core/templates/core/user_tools.jinja:93 +msgid "Create a new election" +msgstr "Créer une nouvelle élection" + #: core/views/files.py:49 msgid "Add a new folder" msgstr "Ajouter un nouveau dossier" @@ -2566,7 +2593,7 @@ msgstr "Bureau" #: eboutic/templates/eboutic/eboutic_main.jinja:24 #: eboutic/templates/eboutic/eboutic_makecommand.jinja:8 #: eboutic/templates/eboutic/eboutic_payment_result.jinja:4 -#: sith/settings.py:295 sith/settings.py:303 +#: sith/settings.py:296 sith/settings.py:304 msgid "Eboutic" msgstr "Eboutic" @@ -2607,8 +2634,8 @@ msgstr "quantité" msgid "Sith account" msgstr "Compte utilisateur" -#: counter/models.py:292 sith/settings.py:288 sith/settings.py:293 -#: sith/settings.py:315 +#: counter/models.py:292 sith/settings.py:289 sith/settings.py:294 +#: sith/settings.py:316 msgid "Credit card" msgstr "Carte bancaire" @@ -3091,6 +3118,161 @@ msgstr "Retourner à l'eboutic" msgid "You do not have enough money to buy the basket" msgstr "Vous n'avez pas assez d'argent pour acheter le panier" +#: election/models.py:16 +msgid "start candidature" +msgstr "début des candidatures" + +#: election/models.py:17 +msgid "end candidature" +msgstr "fin des candidatures" + +#: election/models.py:21 +msgid "edit groups" +msgstr "groupe d'édition" + +#: election/models.py:22 +msgid "view groups" +msgstr "groupe de vue" + +#: election/models.py:23 +msgid "vote groups" +msgstr "groupe de vote" + +#: election/models.py:24 +msgid "candidature groups" +msgstr "groupe de candidature" + +#: election/models.py:80 election/models.py:115 +msgid "election" +msgstr "élection" + +#: election/models.py:83 +msgid "max choice" +msgstr "choix maximums" + +#: election/models.py:128 +msgid "election list" +msgstr "liste électorale" + +#: election/models.py:142 +msgid "candidature" +msgstr "candidature" + +#: election/templates/election/candidate_form.jinja:4 +#: election/templates/election/candidate_form.jinja:11 +#: election/templates/election/election_detail.jinja:361 +msgid "Candidate" +msgstr "Candidater" + +#: election/templates/election/candidate_form.jinja:16 +msgid "Candidature are closed for this election" +msgstr "Les candidatures sont fermées pour cette élection" + +#: election/templates/election/create_template.jinja:4 +#: election/templates/election/create_template.jinja:8 +#: sas/templates/sas/main.jinja:41 +msgid "Create" +msgstr "Créer" + +#: election/templates/election/delete_template.jinja:9 +msgid "Are you sure you want to delete " +msgstr "Êtes-vous sûr de vouloir supprimer \"%(obj)s\" ?" + +#: election/templates/election/election_detail.jinja:237 +msgid "Polls close " +msgstr "Votes fermés" + +#: election/templates/election/election_detail.jinja:239 +msgid "Polls closed " +msgstr "Votes fermés" + +#: election/templates/election/election_detail.jinja:241 +msgid "Polls will open " +msgstr "Les votes ouvriront" + +#: election/templates/election/election_detail.jinja:243 +msgid "and will close " +msgstr "et fermeront" + +#: election/templates/election/election_detail.jinja:250 +msgid "You already have submitted your vote." +msgstr "Vous avez déjà soumis votre vote." + +#: election/templates/election/election_detail.jinja:252 +msgid "You have voted in this election." +msgstr "Vous avez voté pour cette élection." + +#: election/templates/election/election_detail.jinja:264 election/views.py:81 +msgid "Blank vote" +msgstr "Vote blanc" + +#: election/templates/election/election_detail.jinja:281 +msgid "You may choose up to" +msgstr "Vous pouvez choisir jusqu'à" + +#: election/templates/election/election_detail.jinja:281 +msgid "people." +msgstr "personne(s)" + +#: election/templates/election/election_detail.jinja:295 +msgid "Choose blank vote" +msgstr "Choisir de voter blanc" + +#: election/templates/election/election_detail.jinja:302 +#: election/templates/election/election_detail.jinja:340 +msgid "votes" +msgstr "votes" + +#: election/templates/election/election_detail.jinja:333 +#: launderette/templates/launderette/launderette_book.jinja:12 +msgid "Choose" +msgstr "Choisir" + +#: election/templates/election/election_detail.jinja:356 +msgid "Submit the vote !" +msgstr "Envoyer le vote !" + +#: election/templates/election/election_detail.jinja:363 +msgid "Add a new list" +msgstr "Ajouter une nouvelle liste" + +#: election/templates/election/election_detail.jinja:365 +msgid "Add a new role" +msgstr "Ajouter un nouveau rôle" + +#: election/templates/election/election_list.jinja:4 +msgid "Election list" +msgstr "Liste des électorale" + +#: election/templates/election/election_list.jinja:21 +msgid "Current elections" +msgstr "Élections actuelles" + +#: election/templates/election/election_list.jinja:29 +msgid "Applications open from" +msgstr "Candidatures ouverts à partir du" + +#: election/templates/election/election_list.jinja:31 +#: election/templates/election/election_list.jinja:37 +msgid "to" +msgstr "au" + +#: election/templates/election/election_list.jinja:35 +msgid "Polls open from" +msgstr "Votes ouverts du" + +#: election/views.py:43 +msgid "You have selected too much candidates." +msgstr "Vous avez sélectionné trop de candidats." + +#: election/views.py:58 +msgid "User to candidate" +msgstr "Utilisateur se présentant" + +#: election/views.py:101 +msgid "This role already exists for this election" +msgstr "Ce rôle existe déjà pour cette élection" + #: launderette/models.py:17 #: launderette/templates/launderette/launderette_book.jinja:5 #: launderette/templates/launderette/launderette_book_choose.jinja:4 @@ -3142,21 +3324,17 @@ msgstr "Machines" msgid "New machine" msgstr "Nouvelle machine" -#: launderette/templates/launderette/launderette_book.jinja:12 -msgid "Choose" -msgstr "Choisir" - #: launderette/templates/launderette/launderette_book.jinja:23 msgid "Washing and drying" msgstr "Lavage et séchage" #: launderette/templates/launderette/launderette_book.jinja:27 -#: sith/settings.py:436 +#: sith/settings.py:437 msgid "Washing" msgstr "Lavage" #: launderette/templates/launderette/launderette_book.jinja:31 -#: sith/settings.py:436 +#: sith/settings.py:437 msgid "Drying" msgstr "Séchage" @@ -3237,10 +3415,6 @@ msgstr "miniature" msgid "Upload" msgstr "Envoyer" -#: sas/templates/sas/main.jinja:41 -msgid "Create" -msgstr "Créer" - #: sas/templates/sas/moderation.jinja:4 sas/templates/sas/moderation.jinja:8 msgid "SAS moderation" msgstr "Modération du SAS" @@ -3294,145 +3468,145 @@ msgstr "Ajouter une personne" msgid "Apply rights recursively" msgstr "Appliquer les droits récursivement" -#: sith/settings.py:175 +#: sith/settings.py:176 msgid "English" msgstr "Anglais" -#: sith/settings.py:176 +#: sith/settings.py:177 msgid "French" msgstr "Français" -#: sith/settings.py:285 sith/settings.py:292 sith/settings.py:313 +#: sith/settings.py:286 sith/settings.py:293 sith/settings.py:314 msgid "Check" msgstr "Chèque" -#: sith/settings.py:286 sith/settings.py:294 sith/settings.py:314 +#: sith/settings.py:287 sith/settings.py:295 sith/settings.py:315 msgid "Cash" msgstr "Espèces" -#: sith/settings.py:287 +#: sith/settings.py:288 msgid "Transfert" msgstr "Virement" -#: sith/settings.py:300 +#: sith/settings.py:301 msgid "Belfort" msgstr "Belfort" -#: sith/settings.py:301 +#: sith/settings.py:302 msgid "Sevenans" msgstr "Sevenans" -#: sith/settings.py:302 +#: sith/settings.py:303 msgid "Montbéliard" msgstr "Montbéliard" -#: sith/settings.py:343 +#: sith/settings.py:344 msgid "One semester" msgstr "Un semestre, 15 €" -#: sith/settings.py:348 +#: sith/settings.py:349 msgid "Two semesters" msgstr "Deux semestres, 28 €" -#: sith/settings.py:353 +#: sith/settings.py:354 msgid "Common core cursus" msgstr "Cursus tronc commun, 45 €" -#: sith/settings.py:358 +#: sith/settings.py:359 msgid "Branch cursus" msgstr "Cursus branche, 45 €" -#: sith/settings.py:363 +#: sith/settings.py:364 msgid "Alternating cursus" msgstr "Cursus alternant, 30 €" -#: sith/settings.py:368 +#: sith/settings.py:369 msgid "Honorary member" msgstr "Membre honoraire, 0 €" -#: sith/settings.py:373 +#: sith/settings.py:374 msgid "Assidu member" msgstr "Membre d'Assidu, 0 €" -#: sith/settings.py:378 +#: sith/settings.py:379 msgid "Amicale/DOCEO member" msgstr "Membre de l'Amicale/DOCEO, 0 €" -#: sith/settings.py:383 +#: sith/settings.py:384 msgid "UT network member" msgstr "Cotisant du réseau UT, 0 €" -#: sith/settings.py:388 +#: sith/settings.py:389 msgid "CROUS member" msgstr "Membres du CROUS, 0 €" -#: sith/settings.py:393 +#: sith/settings.py:394 msgid "Sbarro/ESTA member" msgstr "Membre de Sbarro ou de l'ESTA, 15 €" -#: sith/settings.py:401 +#: sith/settings.py:402 msgid "President" msgstr "Président" -#: sith/settings.py:402 +#: sith/settings.py:403 msgid "Vice-President" msgstr "Vice-Président" -#: sith/settings.py:403 +#: sith/settings.py:404 msgid "Treasurer" msgstr "Trésorier" -#: sith/settings.py:404 +#: sith/settings.py:405 msgid "Communication supervisor" msgstr "Responsable com" -#: sith/settings.py:405 +#: sith/settings.py:406 msgid "Secretary" msgstr "Secrétaire" -#: sith/settings.py:406 +#: sith/settings.py:407 msgid "IT supervisor" msgstr "Responsable info" -#: sith/settings.py:407 +#: sith/settings.py:408 msgid "Board member" msgstr "Membre du bureau" -#: sith/settings.py:408 +#: sith/settings.py:409 msgid "Active member" msgstr "Membre actif" -#: sith/settings.py:409 +#: sith/settings.py:410 msgid "Curious" msgstr "Curieux" -#: sith/settings.py:443 +#: sith/settings.py:444 msgid "A fresh new to be moderated" msgstr "Une nouvelle toute neuve à modérer" -#: sith/settings.py:444 +#: sith/settings.py:445 msgid "New files to be moderated" msgstr "Nouveaux fichiers à modérer" -#: sith/settings.py:445 +#: sith/settings.py:446 msgid "New pictures/album to be moderated in the SAS" msgstr "Nouvelles photos/albums à modérer dans le SAS" -#: sith/settings.py:446 +#: sith/settings.py:447 msgid "You've been identified on some pictures" msgstr "Vous avez été identifié sur des photos" -#: sith/settings.py:447 +#: sith/settings.py:448 #, python-format msgid "You just refilled of %s €" msgstr "Vous avez rechargé votre compte de %s €" -#: sith/settings.py:448 +#: sith/settings.py:449 #, python-format msgid "You just bought %s" msgstr "Vous avez acheté %s" -#: sith/settings.py:449 +#: sith/settings.py:450 msgid "You have a notification" msgstr "Vous avez une notification" From e17fd22a37ba85a434d5b8fc453d241c58f2e597 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Sun, 25 Dec 2016 20:24:18 +0100 Subject: [PATCH 48/54] Fix populate after rebase --- core/management/commands/populate.py | 56 +++++++++++----------------- 1 file changed, 21 insertions(+), 35 deletions(-) diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index eb3d640a..5b88945a 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -323,23 +323,29 @@ Cette page vise à documenter la syntaxe *Markdown* utilisée sur le site. comptes.save() simple = SimplifiedAccountingType(label = 'Je fais du simple 6', accounting_type = comptes, movement_type='DEBIT') simple.save() - - t = AccountingType(code='602', label="Gros test de malade", movement_type='DEBIT') - t.save() - Operation(journal=gj, date=date.today(), amount=32.3, remark="...", mode="CASH", done=True, accounting_type=t, target_type="USER", target_id=skia.id).save() - t = AccountingType(code='60', label="...", movement_type='DEBIT') - t.save() - Operation(journal=gj, date=date.today(), amount=32.3, remark="...", mode="CASH", done=True, accounting_type=t, target_type="USER", target_id=skia.id).save() - Operation(journal=gj, date=date.today(), amount=46.42, remark="An answer to life...", mode="CASH", done=True, accounting_type=t, target_type="USER", target_id=skia.id).save() - Operation(journal=gj, date=date.today(), amount=666.42, - remark="An answer to life...", mode="CASH", done=True, accounting_type=credit, target_type="USER", - target_id=skia.id).save() - Operation(journal=gj, date=date.today(), amount=42, - remark="An answer to life...", mode="CASH", done=False, accounting_type=debit, target_type="CLUB", - target_id=bar_club.id).save() - woenzco = Company(name="Woenzel & co") woenzco.save() + + operation_list = [ + (27, "J'avais trop de bière", 'CASH', None, buying, 'USER', skia.id, "", None), + (4000, "Ceci n'est pas une opération... en fait si mais non", 'CHECK', None, debit,'COMPANY', woenzco.id, "", 23), + (22, "C'est de l'argent ?", 'CARD', None, credit, 'CLUB', troll.id, "", None), + (37, "Je paye CASH", 'CASH', None, debit2, 'OTHER', None, "tous les étudiants <3", None), + (300, "Paiement Guy", 'CASH', None, buying, 'USER', skia.id, "", None), + (32.3, "Essence", 'CASH', None, buying, 'OTHER', None, "station", None), + (46.42, "Allumette", 'CHECK', None, credit, 'CLUB', main_club.id, "", 57), + (666.42, "Subvention de far far away", 'CASH', None, comptes, 'CLUB', main_club.id, "", None), + (496, "Ça, c'est un 6", 'CARD', simple, None, 'USER', skia.id, "", None), + (17, "La Gargotte du Korrigan", 'CASH', None, debit2, 'CLUB', bar_club.id, "", None), + ] + for op in operation_list: + operation = Operation(journal=gj, date=date.today(), amount=op[0], + remark=op[1], mode=op[2], done=True, simpleaccounting_type=op[3], + accounting_type=op[4], target_type=op[5], target_id=op[6], + target_label=op[7], cheque_number=op[8]) + operation.clean() + operation.save() + # Adding user sli sli = User(username='sli', last_name="Li", first_name="S", email="sli@git.an", @@ -391,26 +397,6 @@ Cette page vise à documenter la syntaxe *Markdown* utilisée sur le site. start=s.subscription_start) s.save() - operation_list = [ - (27, "J'avais trop de bière", 'CASH', None, buying, 'USER', skia.id, "", None), - (4000, "Ceci n'est pas une opération... en fait si mais non", 'CHECK', None, debit,'COMPANY', woenzco.id, "", 23), - (22, "C'est de l'argent ?", 'CARD', None, credit, 'CLUB', troll.id, "", None), - (37, "Je paye CASH", 'CASH', None, debit2, 'OTHER', None, "tous les étudiants <3", None), - (300, "Paiement Guy", 'CASH', None, buying, 'USER', skia.id, "", None), - (32.3, "Essence", 'CASH', None, buying, 'OTHER', None, "station", None), - (46.42, "Allumette", 'CHECK', None, credit, 'CLUB', main_club.id, "", 57), - (666.42, "Subvention de far far away", 'CASH', None, comptes, 'CLUB', main_club.id, "", None), - (496, "Ça, c'est un 6", 'CARD', simple, None, 'USER', skia.id, "", None), - (17, "La Gargotte du Korrigan", 'CASH', None, debit2, 'CLUB', bar_club.id, "", None), - ] - for op in operation_list: - operation = Operation(journal=gj, date=date.today(), amount=op[0], - remark=op[1], mode=op[2], done=True, simpleaccounting_type=op[3], - accounting_type=op[4], target_type=op[5], target_id=op[6], - target_label=op[7], cheque_number=op[8]) - operation.clean() - operation.save() - # Create an election public_group = Group.objects.get(id=settings.SITH_GROUP_PUBLIC_ID) subscriber_group = Group.objects.get(name=settings.SITH_MAIN_MEMBERS_GROUP) From 5449a4fca2e864606cd22d55cd7673174c212a07 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Sun, 25 Dec 2016 22:04:31 +0100 Subject: [PATCH 49/54] Little permission fix --- election/templates/election/election_detail.jinja | 2 ++ election/views.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index f87cd971..ce052d8f 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -362,7 +362,9 @@ th { {%- endif %} {% trans %}Add a new list{% endtrans %} {%- if user.can_edit(election) %} + {% if election.is_vote_editable %} {% trans %}Add a new role{% endtrans %} + {% endif %} {% trans %}Edit{% endtrans %} {%- endif %} diff --git a/election/views.py b/election/views.py index 4dd56448..579b8cf2 100644 --- a/election/views.py +++ b/election/views.py @@ -289,7 +289,7 @@ class RoleCreateView(CanCreateMixin, CreateView): def dispatch(self, request, *arg, **kwargs): self.election = get_object_or_404(Election, pk=kwargs['election_id']) - if self.election.is_vote_editable: + if not self.election.is_vote_editable: raise PermissionDenied return super(RoleCreateView, self).dispatch(request, *arg, **kwargs) From 772a3b5827bf72497469f3082b142dedc1e00624 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Sun, 25 Dec 2016 22:09:59 +0100 Subject: [PATCH 50/54] Squashmigrations for elections --- election/migrations/0001_initial.py | 75 ----------- .../0001_squashed_0006_auto_20161223_2315.py | 117 ++++++++++++++++++ election/migrations/0002_role_max_choice.py | 19 --- .../migrations/0003_auto_20161219_1832.py | 35 ------ .../migrations/0004_auto_20161219_2302.py | 43 ------- .../migrations/0005_auto_20161223_2240.py | 70 ----------- .../migrations/0006_auto_20161223_2315.py | 25 ---- 7 files changed, 117 insertions(+), 267 deletions(-) delete mode 100644 election/migrations/0001_initial.py create mode 100644 election/migrations/0001_squashed_0006_auto_20161223_2315.py delete mode 100644 election/migrations/0002_role_max_choice.py delete mode 100644 election/migrations/0003_auto_20161219_1832.py delete mode 100644 election/migrations/0004_auto_20161219_2302.py delete mode 100644 election/migrations/0005_auto_20161223_2240.py delete mode 100644 election/migrations/0006_auto_20161223_2315.py diff --git a/election/migrations/0001_initial.py b/election/migrations/0001_initial.py deleted file mode 100644 index bb784252..00000000 --- a/election/migrations/0001_initial.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models -from django.conf import settings - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Candidature', - fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('program', models.TextField(blank=True, null=True, verbose_name='description')), - ], - ), - migrations.CreateModel( - name='Election', - fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('title', models.CharField(verbose_name='title', max_length=255)), - ('description', models.TextField(blank=True, null=True, verbose_name='description')), - ('start_candidature', models.DateTimeField(verbose_name='start candidature')), - ('end_candidature', models.DateTimeField(verbose_name='end candidature')), - ('start_date', models.DateTimeField(verbose_name='start date')), - ('end_date', models.DateTimeField(verbose_name='end date')), - ], - ), - migrations.CreateModel( - name='ElectionList', - fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('title', models.CharField(verbose_name='title', max_length=255)), - ('election', models.ForeignKey(related_name='election_list', verbose_name='election', to='election.Election')), - ], - ), - migrations.CreateModel( - name='Role', - fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('title', models.CharField(verbose_name='title', max_length=255)), - ('description', models.TextField(blank=True, null=True, verbose_name='description')), - ('election', models.ForeignKey(related_name='role', verbose_name='election', to='election.Election')), - ('has_voted', models.ManyToManyField(related_name='has_voted', to=settings.AUTH_USER_MODEL, verbose_name='has voted')), - ], - ), - migrations.CreateModel( - name='Vote', - fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('candidature', models.ManyToManyField(related_name='vote', to='election.Candidature', verbose_name='candidature')), - ('role', models.ForeignKey(related_name='vote', verbose_name='role', to='election.Role')), - ], - ), - migrations.AddField( - model_name='candidature', - name='election_list', - field=models.ForeignKey(related_name='candidature', verbose_name='election_list', to='election.ElectionList'), - ), - migrations.AddField( - model_name='candidature', - name='role', - field=models.ForeignKey(related_name='candidature', verbose_name='role', to='election.Role'), - ), - migrations.AddField( - model_name='candidature', - name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='candidate', blank=True, verbose_name='user'), - ), - ] diff --git a/election/migrations/0001_squashed_0006_auto_20161223_2315.py b/election/migrations/0001_squashed_0006_auto_20161223_2315.py new file mode 100644 index 00000000..44c681ff --- /dev/null +++ b/election/migrations/0001_squashed_0006_auto_20161223_2315.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.conf import settings + + +class Migration(migrations.Migration): + + replaces = [('election', '0001_initial'), ('election', '0002_role_max_choice'), ('election', '0003_auto_20161219_1832'), ('election', '0004_auto_20161219_2302'), ('election', '0005_auto_20161223_2240'), ('election', '0006_auto_20161223_2315')] + + dependencies = [ + ('core', '0016_auto_20161212_1922'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Candidature', + fields=[ + ('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)), + ('program', models.TextField(null=True, verbose_name='description', blank=True)), + ], + ), + migrations.CreateModel( + name='Election', + fields=[ + ('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)), + ('title', models.CharField(verbose_name='title', max_length=255)), + ('description', models.TextField(null=True, verbose_name='description', blank=True)), + ('start_candidature', models.DateTimeField(verbose_name='start candidature')), + ('end_candidature', models.DateTimeField(verbose_name='end candidature')), + ('start_date', models.DateTimeField(verbose_name='start date')), + ('end_date', models.DateTimeField(verbose_name='end date')), + ], + ), + migrations.CreateModel( + name='ElectionList', + fields=[ + ('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)), + ('title', models.CharField(verbose_name='title', max_length=255)), + ('election', models.ForeignKey(related_name='election_lists', verbose_name='election', to='election.Election')), + ], + ), + migrations.CreateModel( + name='Role', + fields=[ + ('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)), + ('title', models.CharField(verbose_name='title', max_length=255)), + ('description', models.TextField(null=True, verbose_name='description', blank=True)), + ('election', models.ForeignKey(related_name='role', verbose_name='election', to='election.Election')), + ('has_voted', models.ManyToManyField(related_name='has_voted', verbose_name='has voted', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Vote', + fields=[ + ('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)), + ('candidature', models.ManyToManyField(related_name='votes', verbose_name='candidature', to='election.Candidature')), + ('role', models.ForeignKey(related_name='votes', verbose_name='role', to='election.Role')), + ], + ), + migrations.AddField( + model_name='candidature', + name='election_list', + field=models.ForeignKey(related_name='candidatures', verbose_name='election_list', to='election.ElectionList'), + ), + migrations.AddField( + model_name='candidature', + name='role', + field=models.ForeignKey(related_name='candidatures', verbose_name='role', to='election.Role'), + ), + migrations.AddField( + model_name='candidature', + name='user', + field=models.ForeignKey(related_name='candidates', verbose_name='user', to=settings.AUTH_USER_MODEL, blank=True), + ), + migrations.AddField( + model_name='role', + name='max_choice', + field=models.IntegerField(default=1, verbose_name='max choice'), + ), + migrations.AddField( + model_name='election', + name='edit_groups', + field=models.ManyToManyField(related_name='editable_elections', verbose_name='edit groups', blank=True, to='core.Group'), + ), + migrations.AddField( + model_name='election', + name='view_groups', + field=models.ManyToManyField(related_name='viewable_elections', verbose_name='view groups', blank=True, to='core.Group'), + ), + migrations.AddField( + model_name='election', + name='candidature_groups', + field=models.ManyToManyField(related_name='candidate_elections', verbose_name='candidature groups', blank=True, to='core.Group'), + ), + migrations.AddField( + model_name='election', + name='vote_groups', + field=models.ManyToManyField(related_name='votable_elections', verbose_name='vote groups', blank=True, to='core.Group'), + ), + migrations.AlterField( + model_name='role', + name='election', + field=models.ForeignKey(related_name='roles', verbose_name='election', to='election.Election'), + ), + migrations.RemoveField( + model_name='role', + name='has_voted', + ), + migrations.AddField( + model_name='election', + name='voters', + field=models.ManyToManyField(related_name='has_voted', verbose_name='has voted', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/election/migrations/0002_role_max_choice.py b/election/migrations/0002_role_max_choice.py deleted file mode 100644 index ebce103d..00000000 --- a/election/migrations/0002_role_max_choice.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('election', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='role', - name='max_choice', - field=models.IntegerField(verbose_name='max choice', default=1), - ), - ] diff --git a/election/migrations/0003_auto_20161219_1832.py b/election/migrations/0003_auto_20161219_1832.py deleted file mode 100644 index 55c03808..00000000 --- a/election/migrations/0003_auto_20161219_1832.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0016_auto_20161212_1922'), - ('election', '0002_role_max_choice'), - ] - - operations = [ - migrations.AddField( - model_name='election', - name='candidature_group', - field=models.ManyToManyField(related_name='candidate_election', blank=True, verbose_name='candidature group', to='core.Group'), - ), - migrations.AddField( - model_name='election', - name='edit_groups', - field=models.ManyToManyField(related_name='editable_election', blank=True, verbose_name='edit group', to='core.Group'), - ), - migrations.AddField( - model_name='election', - name='view_groups', - field=models.ManyToManyField(related_name='viewable_election', blank=True, verbose_name='view group', to='core.Group'), - ), - migrations.AddField( - model_name='election', - name='vote_group', - field=models.ManyToManyField(related_name='votable_election', blank=True, verbose_name='vote group', to='core.Group'), - ), - ] diff --git a/election/migrations/0004_auto_20161219_2302.py b/election/migrations/0004_auto_20161219_2302.py deleted file mode 100644 index fabe3212..00000000 --- a/election/migrations/0004_auto_20161219_2302.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0016_auto_20161212_1922'), - ('election', '0003_auto_20161219_1832'), - ] - - operations = [ - migrations.RemoveField( - model_name='election', - name='candidature_group', - ), - migrations.RemoveField( - model_name='election', - name='vote_group', - ), - migrations.AddField( - model_name='election', - name='candidature_groups', - field=models.ManyToManyField(to='core.Group', verbose_name='candidature group', related_name='candidate_elections', blank=True), - ), - migrations.AddField( - model_name='election', - name='vote_groups', - field=models.ManyToManyField(to='core.Group', verbose_name='vote group', related_name='votable_elections', blank=True), - ), - migrations.AlterField( - model_name='election', - name='edit_groups', - field=models.ManyToManyField(to='core.Group', verbose_name='edit group', related_name='editable_elections', blank=True), - ), - migrations.AlterField( - model_name='election', - name='view_groups', - field=models.ManyToManyField(to='core.Group', verbose_name='view group', related_name='viewable_elections', blank=True), - ), - ] diff --git a/election/migrations/0005_auto_20161223_2240.py b/election/migrations/0005_auto_20161223_2240.py deleted file mode 100644 index 0538f465..00000000 --- a/election/migrations/0005_auto_20161223_2240.py +++ /dev/null @@ -1,70 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models -from django.conf import settings - - -class Migration(migrations.Migration): - - dependencies = [ - ('election', '0004_auto_20161219_2302'), - ] - - operations = [ - migrations.AlterField( - model_name='candidature', - name='election_list', - field=models.ForeignKey(related_name='candidatures', to='election.ElectionList', verbose_name='election_list'), - ), - migrations.AlterField( - model_name='candidature', - name='role', - field=models.ForeignKey(related_name='candidatures', to='election.Role', verbose_name='role'), - ), - migrations.AlterField( - model_name='candidature', - name='user', - field=models.ForeignKey(verbose_name='user', to=settings.AUTH_USER_MODEL, related_name='candidates', blank=True), - ), - migrations.AlterField( - model_name='election', - name='candidature_groups', - field=models.ManyToManyField(to='core.Group', related_name='candidate_elections', blank=True, verbose_name='candidature groups'), - ), - migrations.AlterField( - model_name='election', - name='edit_groups', - field=models.ManyToManyField(to='core.Group', related_name='editable_elections', blank=True, verbose_name='edit groups'), - ), - migrations.AlterField( - model_name='election', - name='view_groups', - field=models.ManyToManyField(to='core.Group', related_name='viewable_elections', blank=True, verbose_name='view groups'), - ), - migrations.AlterField( - model_name='election', - name='vote_groups', - field=models.ManyToManyField(to='core.Group', related_name='votable_elections', blank=True, verbose_name='vote groups'), - ), - migrations.AlterField( - model_name='electionlist', - name='election', - field=models.ForeignKey(related_name='election_lists', to='election.Election', verbose_name='election'), - ), - migrations.AlterField( - model_name='role', - name='election', - field=models.ForeignKey(related_name='roles', to='election.Election', verbose_name='election'), - ), - migrations.AlterField( - model_name='vote', - name='candidature', - field=models.ManyToManyField(to='election.Candidature', related_name='votes', verbose_name='candidature'), - ), - migrations.AlterField( - model_name='vote', - name='role', - field=models.ForeignKey(related_name='votes', to='election.Role', verbose_name='role'), - ), - ] diff --git a/election/migrations/0006_auto_20161223_2315.py b/election/migrations/0006_auto_20161223_2315.py deleted file mode 100644 index a6190ce5..00000000 --- a/election/migrations/0006_auto_20161223_2315.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models -from django.conf import settings - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('election', '0005_auto_20161223_2240'), - ] - - operations = [ - migrations.RemoveField( - model_name='role', - name='has_voted', - ), - migrations.AddField( - model_name='election', - name='voters', - field=models.ManyToManyField(to=settings.AUTH_USER_MODEL, verbose_name='has voted', related_name='has_voted'), - ), - ] From cd97901db141ab6daf6c211d0799f48d1f1f530a Mon Sep 17 00:00:00 2001 From: klmp200 Date: Sun, 25 Dec 2016 23:36:34 +0100 Subject: [PATCH 51/54] Some date fix --- election/templates/election/election_detail.jinja | 4 ++-- election/templates/election/election_list.jinja | 8 ++++---- election/views.py | 13 +++++++------ locale/fr/LC_MESSAGES/django.po | 15 ++++++++++++--- 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index ce052d8f..60a3bf23 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -239,10 +239,10 @@ th { {% trans %}Polls closed {% endtrans %} {%- else %} {% trans %}Polls will open {% endtrans %} - at + {% trans %} at {% endtrans %} {% trans %}and will close {% endtrans %} {%- endif %} - at + {% trans %} at {% endtrans %}

          {%- if election.has_voted(user) %}

          diff --git a/election/templates/election/election_list.jinja b/election/templates/election/election_list.jinja index 3cceb2dc..697516e5 100644 --- a/election/templates/election/election_list.jinja +++ b/election/templates/election/election_list.jinja @@ -27,15 +27,15 @@

          {% trans %}Applications open from{% endtrans %} - at + {% trans %} at {% endtrans %} {% trans %}to{% endtrans %} - at + {% trans %} at {% endtrans %}

          {% trans %}Polls open from{% endtrans %} - at + {% trans %} at {% endtrans %} {% trans %}to{% endtrans %} - at + {% trans %} at {% endtrans %}

          {{ election.description }}

          diff --git a/election/views.py b/election/views.py index 579b8cf2..cf0db1ba 100644 --- a/election/views.py +++ b/election/views.py @@ -117,19 +117,20 @@ class ElectionForm(forms.ModelForm): class Meta: model = Election fields = ['title', 'description', 'start_candidature', 'end_candidature', 'start_date', 'end_date', - 'edit_groups', 'view_groups', 'vote_groups', 'candidature_groups'] + 'edit_groups', 'view_groups', 'vote_groups', 'candidature_groups'] widgets = { 'edit_groups': CheckboxSelectMultiple, 'view_groups': CheckboxSelectMultiple, 'edit_groups': CheckboxSelectMultiple, 'vote_groups': CheckboxSelectMultiple, - 'candidature_groups': CheckboxSelectMultiple, - 'start_date': SelectDateTime, - 'end_date': SelectDateTime, - 'start_candidature': SelectDateTime, - 'end_candidature': SelectDateTime, + 'candidature_groups': CheckboxSelectMultiple } + start_date = forms.DateTimeField(['%Y-%m-%d %H:%M:%S'], label=_("Start date"), widget=SelectDateTime, required=True) + end_date = forms.DateTimeField(['%Y-%m-%d %H:%M:%S'], label=_("End date"), widget=SelectDateTime, required=True) + start_candidature = forms.DateTimeField(['%Y-%m-%d %H:%M:%S'], label=_("Start candidature"), widget=SelectDateTime, required=True) + end_candidature = forms.DateTimeField(['%Y-%m-%d %H:%M:%S'], label=_("End candidature"), widget=SelectDateTime, required=True) + # Display elections diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 46ce07d5..c7efb785 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-12-25 19:56+0100\n" +"POT-Creation-Date: 2016-12-25 23:30+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Skia \n" "Language-Team: AE info \n" @@ -362,7 +362,7 @@ msgstr "Nouveau compte club" #: counter/templates/counter/counter_list.jinja:47 #: election/templates/election/election_detail.jinja:277 #: election/templates/election/election_detail.jinja:322 -#: election/templates/election/election_detail.jinja:366 +#: election/templates/election/election_detail.jinja:368 #: election/templates/election/update_template.jinja:4 #: election/templates/election/update_template.jinja:8 #: launderette/templates/launderette/launderette_list.jinja:16 @@ -3190,6 +3190,15 @@ msgstr "Votes fermés" msgid "Polls will open " msgstr "Les votes ouvriront" +#: election/templates/election/election_detail.jinja:242 +#: election/templates/election/election_detail.jinja:245 +#: election/templates/election/election_list.jinja:30 +#: election/templates/election/election_list.jinja:32 +#: election/templates/election/election_list.jinja:36 +#: election/templates/election/election_list.jinja:38 +msgid " at " +msgstr " à " + #: election/templates/election/election_detail.jinja:243 msgid "and will close " msgstr "et fermeront" @@ -3236,7 +3245,7 @@ msgstr "Envoyer le vote !" msgid "Add a new list" msgstr "Ajouter une nouvelle liste" -#: election/templates/election/election_detail.jinja:365 +#: election/templates/election/election_detail.jinja:366 msgid "Add a new role" msgstr "Ajouter un nouveau rôle" From 61e67898e19255e1cef4bbb7bca5f19da45878e4 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Sun, 25 Dec 2016 23:49:02 +0100 Subject: [PATCH 52/54] Coherent create/edit/delete templates for elections --- .../templates/election/create_template.jinja | 15 --------------- .../templates/election/delete_template.jinja | 12 ------------ .../templates/election/update_template.jinja | 15 --------------- election/views.py | 16 ++++++++-------- 4 files changed, 8 insertions(+), 50 deletions(-) delete mode 100644 election/templates/election/create_template.jinja delete mode 100644 election/templates/election/delete_template.jinja delete mode 100644 election/templates/election/update_template.jinja diff --git a/election/templates/election/create_template.jinja b/election/templates/election/create_template.jinja deleted file mode 100644 index 3836f39b..00000000 --- a/election/templates/election/create_template.jinja +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "core/base.jinja" %} - -{% block title %} -{% trans %}Create{% endtrans %} -{% endblock %} - -{% block content %} -

          {% trans %}Create{% endtrans %} -
          -
          {{form.as_p()}} -

          - {% csrf_token %} -
          -
          -{% endblock content %} \ No newline at end of file diff --git a/election/templates/election/delete_template.jinja b/election/templates/election/delete_template.jinja deleted file mode 100644 index 06ed72d4..00000000 --- a/election/templates/election/delete_template.jinja +++ /dev/null @@ -1,12 +0,0 @@ -{% extends "core/base.jinja" %} - -{% block title %} -{% trans %}Delete{% endtrans %} -{% endblock %} - -{% block content %} -
          {% csrf_token %} -

          {% trans %}Are you sure you want to delete {% endtrans %}"{{ object }}"?

          - -
          -{% endblock content %} \ No newline at end of file diff --git a/election/templates/election/update_template.jinja b/election/templates/election/update_template.jinja deleted file mode 100644 index 604e55c4..00000000 --- a/election/templates/election/update_template.jinja +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "core/base.jinja" %} - -{% block title %} -{% trans %}Edit{% endtrans %} -{% endblock %} - -{% block content %} -

          {% trans %}Edit{% endtrans %} -
          -
          {{form.as_p()}} -

          - {% csrf_token %} -
          -
          -{% endblock content %} \ No newline at end of file diff --git a/election/views.py b/election/views.py index cf0db1ba..88235898 100644 --- a/election/views.py +++ b/election/views.py @@ -266,7 +266,7 @@ class CandidatureCreateView(CanCreateMixin, CreateView): class ElectionCreateView(CanCreateMixin, CreateView): model = Election form_class = ElectionForm - template_name = 'election/create_template.jinja' + template_name = 'core/create.jinja' def dispatch(self, request, *args, **kwargs): if not request.user.is_subscribed(): @@ -286,7 +286,7 @@ class ElectionCreateView(CanCreateMixin, CreateView): class RoleCreateView(CanCreateMixin, CreateView): model = Role form_class = RoleForm - template_name = 'election/create_template.jinja' + template_name = 'core/create.jinja' def dispatch(self, request, *arg, **kwargs): self.election = get_object_or_404(Election, pk=kwargs['election_id']) @@ -322,7 +322,7 @@ class RoleCreateView(CanCreateMixin, CreateView): class ElectionListCreateView(CanCreateMixin, CreateView): model = ElectionList form_class = ElectionListForm - template_name = 'election/create_template.jinja' + template_name = 'core/create.jinja' def dispatch(self, request, *arg, **kwargs): self.election = get_object_or_404(Election, pk=kwargs['election_id']) @@ -363,7 +363,7 @@ class ElectionListCreateView(CanCreateMixin, CreateView): class ElectionUpdateView(CanEditMixin, UpdateView): model = Election form_class = ElectionForm - template_name = 'election/update_template.jinja' + template_name = 'core/edit.jinja' pk_url_kwarg = 'election_id' def get_success_url(self, **kwargs): @@ -373,7 +373,7 @@ class ElectionUpdateView(CanEditMixin, UpdateView): class CandidatureUpdateView(CanEditMixin, UpdateView): model = Candidature form_class = CandidateForm - template_name = 'election/update_template.jinja' + template_name = 'core/edit.jinja' pk_url_kwarg = 'candidature_id' def dispatch(self, request, *arg, **kwargs): @@ -409,7 +409,7 @@ class CandidatureUpdateView(CanEditMixin, UpdateView): class RoleUpdateView(CanEditMixin, UpdateView): model = Role form_class = RoleForm - template_name = 'election/update_template.jinja' + template_name = 'core/edit.jinja' pk_url_kwarg = 'role_id' def dispatch(self, request, *arg, **kwargs): @@ -448,7 +448,7 @@ class RoleUpdateView(CanEditMixin, UpdateView): class CandidatureDeleteView(CanEditMixin, DeleteView): model = Candidature - template_name = 'election/delete_template.jinja' + template_name = 'core/delete_confirm.jinja' pk_url_kwarg = 'candidature_id' def dispatch(self, request, *arg, **kwargs): @@ -464,7 +464,7 @@ class CandidatureDeleteView(CanEditMixin, DeleteView): class RoleDeleteView(CanEditMixin, DeleteView): model = Role - template_name = 'election/delete_template.jinja' + template_name = 'core/delete_confirm.jinja' pk_url_kwarg = 'role_id' def dispatch(self, request, *arg, **kwargs): From bf4d0693c64172a1175fb736b6e8d61809d29d56 Mon Sep 17 00:00:00 2001 From: Skia Date: Mon, 26 Dec 2016 00:10:41 +0100 Subject: [PATCH 53/54] Reformat templates to fit with the rest of the Sith --- .../templates/election/candidate_form.jinja | 9 +++--- .../templates/election/election_detail.jinja | 32 ++++++++++--------- .../templates/election/election_list.jinja | 23 +++++++------ 3 files changed, 36 insertions(+), 28 deletions(-) diff --git a/election/templates/election/candidate_form.jinja b/election/templates/election/candidate_form.jinja index ccb6fa91..ad889b27 100644 --- a/election/templates/election/candidate_form.jinja +++ b/election/templates/election/candidate_form.jinja @@ -7,12 +7,13 @@ {% block content %} {%- if election.can_candidate(user) or user.can_edit(election) %}
          -
          {{form.as_p()}} -

          - {% csrf_token %} + + {% csrf_token %} + {{ form.as_p() }} +

          {%- else -%} {% trans %}Candidature are closed for this election{% endtrans %} {%- endif %} -{% endblock content %} \ No newline at end of file +{% endblock content %} diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index 60a3bf23..dea749ff 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -1,7 +1,7 @@ {% extends "core/base.jinja" %} {% block title %} -{{object.title}} +{{ object.title }} {% endblock %} {% block head %} @@ -239,10 +239,12 @@ th { {% trans %}Polls closed {% endtrans %} {%- else %} {% trans %}Polls will open {% endtrans %} - {% trans %} at {% endtrans %} + + {% trans %} at {% endtrans %} {% trans %}and will close {% endtrans %} {%- endif %} - {% trans %} at {% endtrans %} + + {% trans %} at {% endtrans %}

          {%- if election.has_voted(user) %}

          @@ -255,7 +257,7 @@ th { {%- endif %}

          -
          + {% csrf_token %} {%- set election_lists = election.election_lists.all() -%} @@ -263,7 +265,7 @@ th { {%- for election_list in election_lists %} - + {%- endfor %} {%- for role in election.roles.all() %} @@ -271,8 +273,8 @@ th { {%- set role_data = election_form.data.getlist(role.title) if role.title in election_form.data else [] %} - @@ -311,7 +313,7 @@ th {
          {%- if candidature.user.profile_pict and user.is_subscriber_viewable %} - {% trans %}Profile{% endtrans %} + {% trans %}Profile{% endtrans %} {%- endif %}
          @@ -337,7 +339,7 @@ th { {%- if election.is_vote_finished %} {%- set results = election_results[role.title][candidature.user.username] %}
          - {{results.vote}} {% trans %}votes{% endtrans %} ({{results.percent}} %) + {{ results.vote }} {% trans %}votes{% endtrans %} ({{ results.percent }} %)
          {%- endif %} @@ -358,14 +360,14 @@ th { {%- endif %}
          {%- if election.can_candidate(user) or user.can_edit(election) %} - {% trans %}Candidate{% endtrans %} + {% trans %}Candidate{% endtrans %} {%- endif %} - {% trans %}Add a new list{% endtrans %} + {% trans %}Add a new list{% endtrans %} {%- if user.can_edit(election) %} {% if election.is_vote_editable %} - {% trans %}Add a new role{% endtrans %} + {% trans %}Add a new role{% endtrans %} {% endif %} - {% trans %}Edit{% endtrans %} + {% trans %}Edit{% endtrans %} {%- endif %}
          {% endblock %} @@ -395,4 +397,4 @@ function setupRestrictions(role) { } } -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/election/templates/election/election_list.jinja b/election/templates/election/election_list.jinja index 697516e5..3bcb0110 100644 --- a/election/templates/election/election_list.jinja +++ b/election/templates/election/election_list.jinja @@ -26,18 +26,23 @@ {{ election }}

          - {% trans %}Applications open from{% endtrans %} - {% trans %} at {% endtrans %} - {% trans %}to{% endtrans %} - {% trans %} at {% endtrans %} + {% trans %}Applications open from{% endtrans %} + + {% trans %} at {% endtrans %} + {% trans %}to{% endtrans %} + + {% trans %} at {% endtrans %}

          - {% trans %}Polls open from{% endtrans %} - {% trans %} at {% endtrans %} - {% trans %}to{% endtrans %} - {% trans %} at {% endtrans %} + {% trans %}Polls open from{% endtrans %} + + {% trans %} at {% endtrans %} + {% trans %}to{% endtrans %} + + {% trans %} at {% endtrans %}

          {{ election.description }}

          {%- endfor %} -{%- endblock %} \ No newline at end of file +{%- endblock %} + From 3609952db597132bd031f821c650691ada2255a5 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Mon, 26 Dec 2016 00:29:39 +0100 Subject: [PATCH 54/54] Some translation fixs --- core/templates/core/user_tools.jinja | 2 +- locale/fr/LC_MESSAGES/django.po | 107 +++++++++++++-------------- 2 files changed, 53 insertions(+), 56 deletions(-) diff --git a/core/templates/core/user_tools.jinja b/core/templates/core/user_tools.jinja index 0259f24b..371dcd3f 100644 --- a/core/templates/core/user_tools.jinja +++ b/core/templates/core/user_tools.jinja @@ -88,7 +88,7 @@

          {% trans %}Elections{% endtrans %}

            -
          • {% trans %}See avaliable elections{% endtrans %}
          • +
          • {% trans %}See available elections{% endtrans %}
          • {%- if user.is_subscribed() -%}
          • {% trans %}Create a new election{% endtrans %}
          • {%- endif -%} diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index c7efb785..002c873b 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-12-25 23:30+0100\n" +"POT-Creation-Date: 2016-12-26 00:34+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Skia \n" "Language-Team: AE info \n" @@ -322,9 +322,8 @@ msgstr "Compte en banque : " #: core/templates/core/user_edit.jinja:19 #: counter/templates/counter/last_ops.jinja:29 #: counter/templates/counter/last_ops.jinja:59 -#: election/templates/election/delete_template.jinja:4 -#: election/templates/election/election_detail.jinja:278 -#: election/templates/election/election_detail.jinja:325 +#: election/templates/election/election_detail.jinja:280 +#: election/templates/election/election_detail.jinja:327 #: launderette/templates/launderette/launderette_admin.jinja:16 #: launderette/views.py:154 sas/templates/sas/album.jinja:26 #: sas/templates/sas/moderation.jinja:18 sas/templates/sas/picture.jinja:66 @@ -360,11 +359,9 @@ msgstr "Nouveau compte club" #: counter/templates/counter/counter_list.jinja:17 #: counter/templates/counter/counter_list.jinja:32 #: counter/templates/counter/counter_list.jinja:47 -#: election/templates/election/election_detail.jinja:277 -#: election/templates/election/election_detail.jinja:322 -#: election/templates/election/election_detail.jinja:368 -#: election/templates/election/update_template.jinja:4 -#: election/templates/election/update_template.jinja:8 +#: election/templates/election/election_detail.jinja:279 +#: election/templates/election/election_detail.jinja:324 +#: election/templates/election/election_detail.jinja:370 #: launderette/templates/launderette/launderette_list.jinja:16 #: sas/templates/sas/album.jinja:18 sas/templates/sas/picture.jinja:92 msgid "Edit" @@ -643,8 +640,6 @@ msgstr "Éditer l'opération" #: core/templates/core/pagerev_edit.jinja:24 #: core/templates/core/user_godfathers.jinja:35 #: counter/templates/counter/cash_register_summary.jinja:22 -#: election/templates/election/create_template.jinja:11 -#: election/templates/election/update_template.jinja:11 #: subscription/templates/subscription/subscription.jinja:23 msgid "Save" msgstr "Sauver" @@ -992,7 +987,7 @@ msgstr "Vous n'avez pas la permission de faire cela" msgid "Begin date" msgstr "Date de début" -#: club/views.py:166 com/views.py:81 counter/views.py:909 +#: club/views.py:166 com/views.py:81 counter/views.py:909 election/views.py:130 msgid "End date" msgstr "Date de fin" @@ -1199,7 +1194,7 @@ msgstr "Message d'info" msgid "Alert message" msgstr "Message d'alerte" -#: com/views.py:80 +#: com/views.py:80 election/views.py:129 msgid "Start date" msgstr "Date de début" @@ -1498,7 +1493,7 @@ msgstr "Un utilisateur de ce nom d'utilisateur existe déjà" #: core/templates/core/user_detail.jinja:14 #: core/templates/core/user_detail.jinja:16 #: core/templates/core/user_edit.jinja:17 -#: election/templates/election/election_detail.jinja:314 +#: election/templates/election/election_detail.jinja:316 msgid "Profile" msgstr "Profil" @@ -2441,7 +2436,7 @@ msgid "Elections" msgstr "Élections" #: core/templates/core/user_tools.jinja:91 -msgid "See avaliable elections" +msgid "See available elections" msgstr "Voir les élections disponibles" #: core/templates/core/user_tools.jinja:93 @@ -3148,7 +3143,7 @@ msgstr "élection" #: election/models.py:83 msgid "max choice" -msgstr "choix maximums" +msgstr "nombre de choix maxi" #: election/models.py:128 msgid "election list" @@ -3159,25 +3154,15 @@ msgid "candidature" msgstr "candidature" #: election/templates/election/candidate_form.jinja:4 -#: election/templates/election/candidate_form.jinja:11 -#: election/templates/election/election_detail.jinja:361 +#: election/templates/election/candidate_form.jinja:13 +#: election/templates/election/election_detail.jinja:363 msgid "Candidate" msgstr "Candidater" -#: election/templates/election/candidate_form.jinja:16 +#: election/templates/election/candidate_form.jinja:17 msgid "Candidature are closed for this election" msgstr "Les candidatures sont fermées pour cette élection" -#: election/templates/election/create_template.jinja:4 -#: election/templates/election/create_template.jinja:8 -#: sas/templates/sas/main.jinja:41 -msgid "Create" -msgstr "Créer" - -#: election/templates/election/delete_template.jinja:9 -msgid "Are you sure you want to delete " -msgstr "Êtes-vous sûr de vouloir supprimer \"%(obj)s\" ?" - #: election/templates/election/election_detail.jinja:237 msgid "Polls close " msgstr "Votes fermés" @@ -3188,70 +3173,70 @@ msgstr "Votes fermés" #: election/templates/election/election_detail.jinja:241 msgid "Polls will open " -msgstr "Les votes ouvriront" +msgstr "Les votes ouvriront " -#: election/templates/election/election_detail.jinja:242 -#: election/templates/election/election_detail.jinja:245 -#: election/templates/election/election_list.jinja:30 -#: election/templates/election/election_list.jinja:32 -#: election/templates/election/election_list.jinja:36 -#: election/templates/election/election_list.jinja:38 +#: election/templates/election/election_detail.jinja:243 +#: election/templates/election/election_detail.jinja:247 +#: election/templates/election/election_list.jinja:31 +#: election/templates/election/election_list.jinja:34 +#: election/templates/election/election_list.jinja:39 +#: election/templates/election/election_list.jinja:42 msgid " at " msgstr " à " -#: election/templates/election/election_detail.jinja:243 +#: election/templates/election/election_detail.jinja:244 msgid "and will close " msgstr "et fermeront" -#: election/templates/election/election_detail.jinja:250 +#: election/templates/election/election_detail.jinja:252 msgid "You already have submitted your vote." msgstr "Vous avez déjà soumis votre vote." -#: election/templates/election/election_detail.jinja:252 +#: election/templates/election/election_detail.jinja:254 msgid "You have voted in this election." -msgstr "Vous avez voté pour cette élection." +msgstr "Vous avez déjà voté pour cette élection." -#: election/templates/election/election_detail.jinja:264 election/views.py:81 +#: election/templates/election/election_detail.jinja:266 election/views.py:81 msgid "Blank vote" msgstr "Vote blanc" -#: election/templates/election/election_detail.jinja:281 +#: election/templates/election/election_detail.jinja:283 msgid "You may choose up to" msgstr "Vous pouvez choisir jusqu'à" -#: election/templates/election/election_detail.jinja:281 +#: election/templates/election/election_detail.jinja:283 msgid "people." msgstr "personne(s)" -#: election/templates/election/election_detail.jinja:295 +#: election/templates/election/election_detail.jinja:297 msgid "Choose blank vote" msgstr "Choisir de voter blanc" -#: election/templates/election/election_detail.jinja:302 -#: election/templates/election/election_detail.jinja:340 +#: election/templates/election/election_detail.jinja:304 +#: election/templates/election/election_detail.jinja:342 msgid "votes" msgstr "votes" -#: election/templates/election/election_detail.jinja:333 +#: election/templates/election/election_detail.jinja:335 #: launderette/templates/launderette/launderette_book.jinja:12 msgid "Choose" msgstr "Choisir" -#: election/templates/election/election_detail.jinja:356 +#: election/templates/election/election_detail.jinja:358 msgid "Submit the vote !" msgstr "Envoyer le vote !" -#: election/templates/election/election_detail.jinja:363 +#: election/templates/election/election_detail.jinja:365 msgid "Add a new list" msgstr "Ajouter une nouvelle liste" -#: election/templates/election/election_detail.jinja:366 +#: election/templates/election/election_detail.jinja:368 msgid "Add a new role" msgstr "Ajouter un nouveau rôle" #: election/templates/election/election_list.jinja:4 msgid "Election list" -msgstr "Liste des électorale" +msgstr "Liste des élections" #: election/templates/election/election_list.jinja:21 msgid "Current elections" @@ -3259,14 +3244,14 @@ msgstr "Élections actuelles" #: election/templates/election/election_list.jinja:29 msgid "Applications open from" -msgstr "Candidatures ouverts à partir du" +msgstr "Candidatures ouvertes à partir du" -#: election/templates/election/election_list.jinja:31 -#: election/templates/election/election_list.jinja:37 +#: election/templates/election/election_list.jinja:32 +#: election/templates/election/election_list.jinja:40 msgid "to" msgstr "au" -#: election/templates/election/election_list.jinja:35 +#: election/templates/election/election_list.jinja:37 msgid "Polls open from" msgstr "Votes ouverts du" @@ -3282,6 +3267,14 @@ msgstr "Utilisateur se présentant" msgid "This role already exists for this election" msgstr "Ce rôle existe déjà pour cette élection" +#: election/views.py:131 +msgid "Start candidature" +msgstr "Début des candidatures" + +#: election/views.py:132 +msgid "End candidature" +msgstr "Fin des candidatures" + #: launderette/models.py:17 #: launderette/templates/launderette/launderette_book.jinja:5 #: launderette/templates/launderette/launderette_book_choose.jinja:4 @@ -3424,6 +3417,10 @@ msgstr "miniature" msgid "Upload" msgstr "Envoyer" +#: sas/templates/sas/main.jinja:41 +msgid "Create" +msgstr "Créer" + #: sas/templates/sas/moderation.jinja:4 sas/templates/sas/moderation.jinja:8 msgid "SAS moderation" msgstr "Modération du SAS"
          {% trans %}Blank vote{% endtrans %}{{election_list.title}}{{ election_list.title }}
          - {{role.title}} + + {{ role.title }} {% if user.can_edit(role) and election.is_vote_editable -%} {% trans %}Edit{% endtrans %} {% trans %}Delete{% endtrans %} @@ -299,7 +301,7 @@ th { {%- if election.is_vote_finished %} {%- set results = election_results[role.title]['blank vote'] %}
          - {{results.vote}} {% trans %}votes{% endtrans %} ({{results.percent}} %) + {{ results.vote }} {% trans %}votes{% endtrans %} ({{ results.percent }} %)
          {%- endif %}