From c48dfce314a3b67540174be6dc62144fa1bb50f7 Mon Sep 17 00:00:00 2001 From: vomaxHELLnO Date: Thu, 6 Aug 2015 12:37:32 +0300 Subject: [PATCH 01/13] Add VotingPoll and Vote models Issue #44 --- .../migrations/0006_auto_20150806_0437.py | 48 +++++++++++++++++++ pylab/core/models.py | 25 ++++++++++ 2 files changed, 73 insertions(+) create mode 100644 pylab/core/migrations/0006_auto_20150806_0437.py diff --git a/pylab/core/migrations/0006_auto_20150806_0437.py b/pylab/core/migrations/0006_auto_20150806_0437.py new file mode 100644 index 0000000..94856ce --- /dev/null +++ b/pylab/core/migrations/0006_auto_20150806_0437.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings +import django_extensions.db.fields +import django.utils.timezone +import autoslug.fields + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('core', '0005_auto_20150730_0231'), + ] + + operations = [ + migrations.CreateModel( + name='Vote', + fields=[ + ('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)), + ('created', django_extensions.db.fields.CreationDateTimeField(editable=False, default=django.utils.timezone.now, blank=True)), + ('modified', django_extensions.db.fields.ModificationDateTimeField(editable=False, default=django.utils.timezone.now, blank=True)), + ('vote_time', models.DateTimeField(null=True)), + ('points', models.IntegerField(verbose_name='Points', null=True)), + ('project', models.ForeignKey(to='core.Project')), + ('voter', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='VotingPoll', + fields=[ + ('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)), + ('created', django_extensions.db.fields.CreationDateTimeField(editable=False, default=django.utils.timezone.now, blank=True)), + ('modified', django_extensions.db.fields.ModificationDateTimeField(editable=False, default=django.utils.timezone.now, blank=True)), + ('slug', autoslug.fields.AutoSlugField(editable=False)), + ('title', models.CharField(verbose_name='Title', max_length=255)), + ('description', models.TextField(blank=True, verbose_name='Description')), + ('author', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='vote', + name='voting_poll', + field=models.ForeignKey(to='core.VotingPoll'), + ), + ] diff --git a/pylab/core/models.py b/pylab/core/models.py index 3a76dcb..6977bdc 100644 --- a/pylab/core/models.py +++ b/pylab/core/models.py @@ -56,3 +56,28 @@ def __str__(self): def get_absolute_url(self): return reverse('event-details', args=[self.starts.year, self.starts.month, self.starts.day, self.slug]) + + +class VotingPoll(models.Model): + created = CreationDateTimeField() + modified = ModificationDateTimeField() + author = models.ForeignKey(User) + slug = AutoSlugField(populate_from='title') + title = models.CharField(_("Title"), max_length=255) + description = models.TextField(_("Description"), blank=True) + + def __str__(self): + return self.title + + +class Vote(models.Model): + created = CreationDateTimeField() + modified = ModificationDateTimeField() + vote_time = models.DateTimeField(null=True) + voting_poll = models.ForeignKey(VotingPoll) + voter = models.ForeignKey(User) + project = models.ForeignKey(Project) + points = models.IntegerField(_("Points"), null=True) + + def __str__(self): + return '%s, %s' % (self.voting_poll.title, self.voter.get_full_name() or self.voter.get_username()) From c4c33f9426a7a14429268c6babf3548d6b8ac844 Mon Sep 17 00:00:00 2001 From: sirex Date: Thu, 6 Aug 2015 22:49:45 +0300 Subject: [PATCH 02/13] Fix 'relation "core_project" does not exist' error Sorry guys, that was my error, forgot to add dependency on website migrations, since website migration have to be run first, before core, to transfer Project model from website to core. --- pylab/core/migrations/0001_initial.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pylab/core/migrations/0001_initial.py b/pylab/core/migrations/0001_initial.py index 91d4dd4..73e8ef2 100644 --- a/pylab/core/migrations/0001_initial.py +++ b/pylab/core/migrations/0001_initial.py @@ -12,6 +12,7 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('website', '0002_auto_20150728_0303'), ] operations = [ From 35afe617422890704297808651cb64bd2dfdb159 Mon Sep 17 00:00:00 2001 From: vomaxHELLnO Date: Sat, 8 Aug 2015 12:25:15 +0300 Subject: [PATCH 03/13] Register VotingPoll model to django admin --- pylab/website/admin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pylab/website/admin.py b/pylab/website/admin.py index e45f3dd..654ec21 100644 --- a/pylab/website/admin.py +++ b/pylab/website/admin.py @@ -10,7 +10,7 @@ import allauth.socialaccount.admin as allauth -from pylab.core.models import Project, Event +import pylab.core.models as core_models class AdminSite(admin.AdminSite): @@ -50,8 +50,10 @@ def get_changeform_initial_data(self, request): site = AdminSite() site.register(auth_models.User, auth_admin.UserAdmin) -site.register(Project) -site.register(Event, EventAdmin) + +site.register(core_models.Project) +site.register(core_models.Event, EventAdmin) +site.register(core_models.VotingPoll) site.register(allauth.SocialApp, allauth.SocialAppAdmin) site.register(allauth.SocialToken, allauth.SocialTokenAdmin) From 5c0a3f52e39aad8f33ad7b2ecc74583ae482e3e4 Mon Sep 17 00:00:00 2001 From: vomaxHELLnO Date: Sun, 9 Aug 2015 02:18:38 +0300 Subject: [PATCH 04/13] Create voting page Not finished yet, should create tests and improve usability with JavaScript. Also max allowed total points should be assigned to user, then tracked and validated in form is_valid method. --- .../migrations/0007_auto_20150808_1812.py | 19 +++++++++++ pylab/core/models.py | 2 +- pylab/website/models.py | 0 .../templates/website/voting_page.html | 22 +++++++++++++ pylab/website/urls.py | 1 + pylab/website/views.py | 32 ++++++++++++++++--- 6 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 pylab/core/migrations/0007_auto_20150808_1812.py delete mode 100644 pylab/website/models.py create mode 100644 pylab/website/templates/website/voting_page.html diff --git a/pylab/core/migrations/0007_auto_20150808_1812.py b/pylab/core/migrations/0007_auto_20150808_1812.py new file mode 100644 index 0000000..9bbfee6 --- /dev/null +++ b/pylab/core/migrations/0007_auto_20150808_1812.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_auto_20150806_0437'), + ] + + operations = [ + migrations.AlterField( + model_name='vote', + name='points', + field=models.PositiveIntegerField(verbose_name='Points', null=True, blank=True), + ), + ] diff --git a/pylab/core/models.py b/pylab/core/models.py index 6977bdc..2b2ed55 100644 --- a/pylab/core/models.py +++ b/pylab/core/models.py @@ -77,7 +77,7 @@ class Vote(models.Model): voting_poll = models.ForeignKey(VotingPoll) voter = models.ForeignKey(User) project = models.ForeignKey(Project) - points = models.IntegerField(_("Points"), null=True) + points = models.PositiveIntegerField(_("Points"), null=True, blank=True) def __str__(self): return '%s, %s' % (self.voting_poll.title, self.voter.get_full_name() or self.voter.get_username()) diff --git a/pylab/website/models.py b/pylab/website/models.py deleted file mode 100644 index e69de29..0000000 diff --git a/pylab/website/templates/website/voting_page.html b/pylab/website/templates/website/voting_page.html new file mode 100644 index 0000000..774f659 --- /dev/null +++ b/pylab/website/templates/website/voting_page.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} +{% load trans from i18n %} +{% load markdown from websitetags %} + +{% block content %} + +

{{ voting_poll.title }}

+ +

{{ voting_poll.description|markdown }}

+ +
{% csrf_token %} + {{ form.management_form }} + {% for field in form %} +
+ {{ field.errors }} +

{{ field }} {{ field.instance.project.title }}

+
+ {% endfor %} + +
+ +{% endblock %} diff --git a/pylab/website/urls.py b/pylab/website/urls.py index 39dd7cb..1da3a68 100644 --- a/pylab/website/urls.py +++ b/pylab/website/urls.py @@ -15,6 +15,7 @@ url(r'^events/%s/$' % event, views.event_details, name='event-details'), url(r'^events/%s/create-next-weekly-event/$' % event, views.create_weekly_event, name='create-weekly-event'), url(r'^about/$', views.about, name='about'), + url(r'^vote/(?P%s)/$' % slug, views.voting_page, name='voting-page'), ] urlpatterns += [ diff --git a/pylab/website/views.py b/pylab/website/views.py index 4b6ddc3..1e3b989 100644 --- a/pylab/website/views.py +++ b/pylab/website/views.py @@ -1,11 +1,12 @@ -from django.shortcuts import render -from django.shortcuts import redirect +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.forms.models import modelformset_factory from django.shortcuts import get_object_or_404 +from django.shortcuts import redirect +from django.shortcuts import render from django.utils.translation import ugettext -from django.contrib.auth.decorators import login_required -from django.contrib import messages -from pylab.core.models import Project, Event +from pylab.core.models import Project, Event, Vote, VotingPoll from pylab.website.helpers import formrenderer from pylab.website.helpers.decorators import superuser_required from pylab.website.services import weeklyevents @@ -95,3 +96,24 @@ def create_weekly_event(request, year, month, day, slug): submit=ugettext("Announce"), ), }) + + +@login_required +def voting_page(request, voting_poll_slug): + voting_poll = get_object_or_404(VotingPoll, slug=voting_poll_slug) + VotePointsFormSet = modelformset_factory(Vote, fields=('points',), extra=0,) + vote_qs = Vote.objects.filter(voter=request.user, voting_poll__slug=voting_poll_slug) + + if request.method == 'POST': + form = VotePointsFormSet(request.POST, queryset=vote_qs) + if form.is_valid(): + instances = form.save() + messages.success(request, ugettext("Vote for „%s“ voting poll was saved successfully." % voting_poll)) + return redirect('project-list') + else: + form = VotePointsFormSet(queryset=vote_qs) + + return render(request, 'website/voting_page.html', { + 'voting_poll': voting_poll, + 'form': form, + }) From a5e599f4a7c2f20c4f0ed79366db985cba7ae85e Mon Sep 17 00:00:00 2001 From: vomaxHELLnO Date: Sun, 9 Aug 2015 12:25:01 +0300 Subject: [PATCH 05/13] Add template context debugging templatetag --- pylab/website/templatetags/debug.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 pylab/website/templatetags/debug.py diff --git a/pylab/website/templatetags/debug.py b/pylab/website/templatetags/debug.py new file mode 100644 index 0000000..0b88a73 --- /dev/null +++ b/pylab/website/templatetags/debug.py @@ -0,0 +1,8 @@ +from django import template + +register = template.Library() + + +@register.simple_tag(name='pdb', takes_context=True) +def pdb(context, *args, **kwargs): + import ipdb; ipdb.set_trace() From 197ef1017b60739313e13e475afbb46cbdf52bda Mon Sep 17 00:00:00 2001 From: vomaxHELLnO Date: Sun, 9 Aug 2015 13:41:42 +0300 Subject: [PATCH 06/13] Update voting form style --- pylab/website/forms.py | 11 +++++- pylab/website/static/css/main.scss | 16 ++++++++ .../templates/website/voting_page.html | 37 +++++++++++++------ pylab/website/views.py | 12 +++--- 4 files changed, 57 insertions(+), 19 deletions(-) diff --git a/pylab/website/forms.py b/pylab/website/forms.py index 2f8598e..f84d94a 100644 --- a/pylab/website/forms.py +++ b/pylab/website/forms.py @@ -2,7 +2,7 @@ from django.utils.translation import ugettext from django.utils.translation import ugettext_lazy as _ -from pylab.core.models import Project, Event +from pylab.core.models import Project, Event, Vote class ProjectForm(forms.ModelForm): @@ -49,3 +49,12 @@ def clean(self): if title and starts: self.check_existing_events(title, starts) + + +class VotePointsForm(forms.ModelForm): + class Meta: + model = Vote + fields = ('points',) + widgets = { + 'points': forms.NumberInput(attrs={'max': 99, 'class': 'vote-points-input'}), + } diff --git a/pylab/website/static/css/main.scss b/pylab/website/static/css/main.scss index 98687e7..4a3b8ce 100644 --- a/pylab/website/static/css/main.scss +++ b/pylab/website/static/css/main.scss @@ -50,3 +50,19 @@ .event-time { padding-left: 16px; } + +.vote-points-input { + width: 38px; + text-align: right; + font-weight: bold; +} + +.vote-points-form { + padding-top: 10px; + padding-left: 30px; + padding-bottom: 30px; +} + +.vote-button { + margin-top: 15px; +} diff --git a/pylab/website/templates/website/voting_page.html b/pylab/website/templates/website/voting_page.html index 774f659..855fbb8 100644 --- a/pylab/website/templates/website/voting_page.html +++ b/pylab/website/templates/website/voting_page.html @@ -2,21 +2,34 @@ {% load trans from i18n %} {% load markdown from websitetags %} + {% block content %} -

{{ voting_poll.title }}

+

{{ voting_poll.title }}

-

{{ voting_poll.description|markdown }}

+

{{ voting_poll.description|markdown }}

-
{% csrf_token %} - {{ form.management_form }} - {% for field in form %} -
- {{ field.errors }} -

{{ field }} {{ field.instance.project.title }}

-
- {% endfor %} - -
+
+
{% csrf_token %} + {{ formset.management_form }} + {% for form in formset %} +
+ {{ form.errors }} +

+ {% for hidden in form.hidden_fields %} + {{ hidden }} + {% endfor %} + {% for field in form.visible_fields %} +

+ {{ field.errors }} + {{ field }} {{ form.instance.project.title }} +
+ {% endfor %} +

+
+ {% endfor %} + +
+
{% endblock %} diff --git a/pylab/website/views.py b/pylab/website/views.py index 1e3b989..95a8656 100644 --- a/pylab/website/views.py +++ b/pylab/website/views.py @@ -101,19 +101,19 @@ def create_weekly_event(request, year, month, day, slug): @login_required def voting_page(request, voting_poll_slug): voting_poll = get_object_or_404(VotingPoll, slug=voting_poll_slug) - VotePointsFormSet = modelformset_factory(Vote, fields=('points',), extra=0,) + VotePointsFormSet = modelformset_factory(Vote, form=website_forms.VotePointsForm, extra=0,) vote_qs = Vote.objects.filter(voter=request.user, voting_poll__slug=voting_poll_slug) if request.method == 'POST': - form = VotePointsFormSet(request.POST, queryset=vote_qs) - if form.is_valid(): - instances = form.save() + formset = VotePointsFormSet(request.POST, queryset=vote_qs) + if formset.is_valid(): + instances = formset.save() messages.success(request, ugettext("Vote for „%s“ voting poll was saved successfully." % voting_poll)) return redirect('project-list') else: - form = VotePointsFormSet(queryset=vote_qs) + formset = VotePointsFormSet(queryset=vote_qs) return render(request, 'website/voting_page.html', { 'voting_poll': voting_poll, - 'form': form, + 'formset': formset, }) From 7e7c792ad8aa0a523bf757215c9cd0b426a38d5d Mon Sep 17 00:00:00 2001 From: vomaxHELLnO Date: Sun, 9 Aug 2015 14:59:28 +0300 Subject: [PATCH 07/13] Rename Vote field vote_time to voted And fill this field when vote form is saved. --- .../migrations/0008_auto_20150809_0654.py | 19 +++++++++++++++++++ pylab/core/models.py | 2 +- pylab/website/forms.py | 7 +++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 pylab/core/migrations/0008_auto_20150809_0654.py diff --git a/pylab/core/migrations/0008_auto_20150809_0654.py b/pylab/core/migrations/0008_auto_20150809_0654.py new file mode 100644 index 0000000..2a8e6c5 --- /dev/null +++ b/pylab/core/migrations/0008_auto_20150809_0654.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0007_auto_20150808_1812'), + ] + + operations = [ + migrations.RenameField( + model_name='vote', + old_name='vote_time', + new_name='voted', + ), + ] diff --git a/pylab/core/models.py b/pylab/core/models.py index 2b2ed55..589a94b 100644 --- a/pylab/core/models.py +++ b/pylab/core/models.py @@ -73,7 +73,7 @@ def __str__(self): class Vote(models.Model): created = CreationDateTimeField() modified = ModificationDateTimeField() - vote_time = models.DateTimeField(null=True) + voted = models.DateTimeField(null=True) voting_poll = models.ForeignKey(VotingPoll) voter = models.ForeignKey(User) project = models.ForeignKey(Project) diff --git a/pylab/website/forms.py b/pylab/website/forms.py index f84d94a..1592473 100644 --- a/pylab/website/forms.py +++ b/pylab/website/forms.py @@ -1,3 +1,5 @@ +from datetime import datetime + from django import forms from django.utils.translation import ugettext from django.utils.translation import ugettext_lazy as _ @@ -58,3 +60,8 @@ class Meta: widgets = { 'points': forms.NumberInput(attrs={'max': 99, 'class': 'vote-points-input'}), } + + def save(self, commit=True, *args, **kwargs): + vote = super(VotePointsForm, self).save(commit=False, *args, **kwargs) + vote.voted = datetime.now() + vote.save() From d75cba9d23ca43c7241330a94e2bdeedbf957461 Mon Sep 17 00:00:00 2001 From: vomaxHELLnO Date: Sun, 9 Aug 2015 16:37:11 +0300 Subject: [PATCH 08/13] Add voting page test Also fix pylint and html warnings. --- pylab/website/forms.py | 4 +- .../templates/website/voting_page.html | 42 +++++++++---------- pylab/website/templatetags/debug.py | 6 ++- pylab/website/tests/test_voting.py | 39 +++++++++++++++++ pylab/website/views.py | 2 +- setup.py | 1 + 6 files changed, 68 insertions(+), 26 deletions(-) create mode 100644 pylab/website/tests/test_voting.py diff --git a/pylab/website/forms.py b/pylab/website/forms.py index 1592473..e65a3eb 100644 --- a/pylab/website/forms.py +++ b/pylab/website/forms.py @@ -1,4 +1,4 @@ -from datetime import datetime +import datetime from django import forms from django.utils.translation import ugettext @@ -63,5 +63,5 @@ class Meta: def save(self, commit=True, *args, **kwargs): vote = super(VotePointsForm, self).save(commit=False, *args, **kwargs) - vote.voted = datetime.now() + vote.voted = datetime.datetime.now() vote.save() diff --git a/pylab/website/templates/website/voting_page.html b/pylab/website/templates/website/voting_page.html index 855fbb8..5582506 100644 --- a/pylab/website/templates/website/voting_page.html +++ b/pylab/website/templates/website/voting_page.html @@ -9,27 +9,27 @@

{{ voting_poll.title }}

{{ voting_poll.description|markdown }}

-
-
{% csrf_token %} - {{ formset.management_form }} - {% for form in formset %} -
- {{ form.errors }} -

- {% for hidden in form.hidden_fields %} - {{ hidden }} - {% endfor %} - {% for field in form.visible_fields %} -

- {{ field.errors }} - {{ field }} {{ form.instance.project.title }} -
- {% endfor %} -

-
- {% endfor %} - -
+
+
{% csrf_token %} + {{ formset.management_form }} + {% for form in formset %} +
+ {{ form.errors }} +

+ {% for hidden in form.hidden_fields %} + {{ hidden }} + {% endfor %} + {% for field in form.visible_fields %} +

+ {{ field.errors }} + {{ field }} {{ form.instance.project.title }} +
+ {% endfor %} +

+
+ {% endfor %} + +
{% endblock %} diff --git a/pylab/website/templatetags/debug.py b/pylab/website/templatetags/debug.py index 0b88a73..ae1d9fe 100644 --- a/pylab/website/templatetags/debug.py +++ b/pylab/website/templatetags/debug.py @@ -1,8 +1,10 @@ +import ipdb + from django import template register = template.Library() @register.simple_tag(name='pdb', takes_context=True) -def pdb(context, *args, **kwargs): - import ipdb; ipdb.set_trace() +def pdb(context, *args, **kwargs): # pylint: disable=unused-argument + ipdb.set_trace() diff --git a/pylab/website/tests/test_voting.py b/pylab/website/tests/test_voting.py new file mode 100644 index 0000000..d67e390 --- /dev/null +++ b/pylab/website/tests/test_voting.py @@ -0,0 +1,39 @@ +import datetime + +from django_webtest import WebTest + +from django.contrib.auth.models import User + +from pylab.core.models import Project, VotingPoll, Vote + + +class VotingTests(WebTest): + + def test_voting_page(self): + u1 = User.objects.create(username='u1') + + p1 = Project.objects.create(author=u1, title='Test title 1', description='Test description') + p2 = Project.objects.create(author=u1, title='Test title 2', description='Test description') + + vp = VotingPoll.objects.create(author=u1, title='Test voting poll', description='Test description') + + Vote.objects.create(voter=u1, voting_poll=vp, project=p1) + Vote.objects.create(voter=u1, voting_poll=vp, project=p2) + + resp = self.app.get('/vote/test-voting-poll/', user='u1') + self.assertEqual(resp.status_int, 200) + + time_before_vote = datetime.datetime.now() + + resp.form['form-0-points'].value = 3 + resp.form['form-1-points'].value = 2 + resp = resp.form.submit() + self.assertEqual(resp.status_int, 302) + + time_after_vote = datetime.datetime.now() + + self.assertEqual(list(Vote.objects.values_list('points', flat=True)), [3, 2]) + + for v in Vote.objects.all(): + self.assertLess(time_before_vote, v.voted) + self.assertGreater(time_after_vote, v.voted) diff --git a/pylab/website/views.py b/pylab/website/views.py index 95a8656..e54f623 100644 --- a/pylab/website/views.py +++ b/pylab/website/views.py @@ -107,7 +107,7 @@ def voting_page(request, voting_poll_slug): if request.method == 'POST': formset = VotePointsFormSet(request.POST, queryset=vote_qs) if formset.is_valid(): - instances = formset.save() + formset.save() messages.success(request, ugettext("Vote for „%s“ voting poll was saved successfully." % voting_poll)) return redirect('project-list') else: diff --git a/setup.py b/setup.py index 411b73f..c0ebfe8 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ 'django-allauth', 'factory_boy', 'fake-factory', + 'ipdb', 'unidecode', 'markdown', 'yattag', From df6bbb3454abbdc815139c9bb98f0cf08f5dab7a Mon Sep 17 00:00:00 2001 From: vomaxHELLnO Date: Sun, 9 Aug 2015 17:22:05 +0300 Subject: [PATCH 09/13] Add projects tests Also add about page test. --- pylab/website/tests/test_projects.py | 42 +++++++++++++++++++ ...est_index_page.py => test_static_pages.py} | 10 ++++- 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 pylab/website/tests/test_projects.py rename pylab/website/tests/{test_index_page.py => test_static_pages.py} (57%) diff --git a/pylab/website/tests/test_projects.py b/pylab/website/tests/test_projects.py new file mode 100644 index 0000000..13c1757 --- /dev/null +++ b/pylab/website/tests/test_projects.py @@ -0,0 +1,42 @@ +import django_webtest + +import django.contrib.auth.models as auth_models + +from pylab.core.models import Project + + +class StaticPagesTests(django_webtest.WebTest): + def setUp(self): + super().setUp() + auth_models.User.objects.create_user('u1') + + def test_project_creation_and_editing(self): + resp = self.app.get('/projects/create/', user='u1') + self.assertEqual(resp.status_int, 200) + + resp.form['title'] = 'Test project' + resp.form['description'] = 'Test description' + resp = resp.form.submit() + self.assertEqual(resp.status_int, 302) + + resp = self.app.get('/projects/test-project/', user='u1') + self.assertEqual(resp.status_int, 200) + + self.assertEqual( + list(Project.objects.values_list('title', 'description')), + [('Test project', 'Test description')] + ) + + resp = self.app.get('/projects/test-project/update/', user='u1') + resp.form['title'] = 'Test project2' + resp.form['description'] = 'Test description2' + resp = resp.form.submit() + self.assertEqual(resp.status_int, 302) + + self.assertEqual( + list(Project.objects.values_list('title', 'description')), + [('Test project2', 'Test description2')] + ) + + resp = self.app.get('/projects/test-project/', user='u1') + self.assertEqual(resp.status_int, 200) diff --git a/pylab/website/tests/test_index_page.py b/pylab/website/tests/test_static_pages.py similarity index 57% rename from pylab/website/tests/test_index_page.py rename to pylab/website/tests/test_static_pages.py index 8cb957e..bfd3444 100644 --- a/pylab/website/tests/test_index_page.py +++ b/pylab/website/tests/test_static_pages.py @@ -3,7 +3,7 @@ import django.contrib.auth.models as auth_models -class IndexPageTests(django_webtest.WebTest): +class StaticPagesTests(django_webtest.WebTest): def setUp(self): super().setUp() auth_models.User.objects.create_user('u1') @@ -15,3 +15,11 @@ def test_index_page_with_anonymous_user(self): def test_index_page_with_logged_in_user(self): resp = self.app.get('/', user='u1') self.assertEqual(resp.status_int, 200) + + def test_about_page_with_anonymous_user(self): + resp = self.app.get('/about/') + self.assertEqual(resp.status_int, 200) + + def test_about_page_with_logged_in_user(self): + resp = self.app.get('/about/', user='u1') + self.assertEqual(resp.status_int, 200) From b5e9b27e0039445412718a9cb3984f32a844e217 Mon Sep 17 00:00:00 2001 From: vomaxHELLnO Date: Sun, 9 Aug 2015 22:56:30 +0300 Subject: [PATCH 10/13] Show and dynamically update points left counter --- pylab/website/static/css/main.scss | 4 +++ pylab/website/static/js/update_points_left.js | 27 +++++++++++++++++++ pylab/website/templates/base.html | 2 ++ .../templates/website/voting_page.html | 10 +++++++ pylab/website/views.py | 1 + 5 files changed, 44 insertions(+) create mode 100644 pylab/website/static/js/update_points_left.js diff --git a/pylab/website/static/css/main.scss b/pylab/website/static/css/main.scss index 4a3b8ce..e920baa 100644 --- a/pylab/website/static/css/main.scss +++ b/pylab/website/static/css/main.scss @@ -66,3 +66,7 @@ .vote-button { margin-top: 15px; } + +#total-points { + display: none; +} diff --git a/pylab/website/static/js/update_points_left.js b/pylab/website/static/js/update_points_left.js new file mode 100644 index 0000000..969d811 --- /dev/null +++ b/pylab/website/static/js/update_points_left.js @@ -0,0 +1,27 @@ +var total_points = Number($('#total-points').html()); +function count_points_left () { + var sum = 0; + $('.vote-points-input').each(function() { sum += Number($(this).val()); }); + return total_points - sum; +}; +$('#points-left').html(count_points_left()); +$('.vote-points-input').each(function() { + this.oldValue = this.value; +}); + +function clear_value_if_zero(elem) { + if (elem.value === "0") { + elem.value = ""; + }; +}; + +$('.vote-points-input').change(function (ev) { + var points_left = count_points_left(); + if (points_left < 0) { + this.value = this.oldValue; + } else { + this.oldValue = this.value; + $('#points-left').html(points_left); + }; + clear_value_if_zero(this); +}); diff --git a/pylab/website/templates/base.html b/pylab/website/templates/base.html index e02d853..83853cd 100644 --- a/pylab/website/templates/base.html +++ b/pylab/website/templates/base.html @@ -55,5 +55,7 @@ + {% block scripts %} + {% endblock %} diff --git a/pylab/website/templates/website/voting_page.html b/pylab/website/templates/website/voting_page.html index 5582506..f57a89c 100644 --- a/pylab/website/templates/website/voting_page.html +++ b/pylab/website/templates/website/voting_page.html @@ -1,5 +1,6 @@ {% extends "base.html" %} {% load trans from i18n %} +{% load static from staticfiles %} {% load markdown from websitetags %} @@ -10,6 +11,10 @@

{{ voting_poll.title }}

{{ voting_poll.description|markdown }}

+ {% trans "Points left: " %} + + {{ total_points }} +
{% csrf_token %} {{ formset.management_form }} {% for form in formset %} @@ -33,3 +38,8 @@

{{ voting_poll.title }}

{% endblock %} + +{% block scripts %} + +{% endblock %} + diff --git a/pylab/website/views.py b/pylab/website/views.py index e54f623..ed851b3 100644 --- a/pylab/website/views.py +++ b/pylab/website/views.py @@ -116,4 +116,5 @@ def voting_page(request, voting_poll_slug): return render(request, 'website/voting_page.html', { 'voting_poll': voting_poll, 'formset': formset, + 'total_points': 15, }) From 17b795bac56d7d0bf5ffd17169e211d03fde3084 Mon Sep 17 00:00:00 2001 From: vomaxHELLnO Date: Mon, 10 Aug 2015 01:27:22 +0300 Subject: [PATCH 11/13] Check vote points sum when validating vote formset --- pylab/website/forms.py | 16 ++++++++++++++++ pylab/website/templates/website/voting_page.html | 1 + pylab/website/views.py | 10 ++++++++-- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/pylab/website/forms.py b/pylab/website/forms.py index e65a3eb..fb0b7f2 100644 --- a/pylab/website/forms.py +++ b/pylab/website/forms.py @@ -1,6 +1,7 @@ import datetime from django import forms +from django.forms.models import BaseModelFormSet from django.utils.translation import ugettext from django.utils.translation import ugettext_lazy as _ @@ -65,3 +66,18 @@ def save(self, commit=True, *args, **kwargs): vote = super(VotePointsForm, self).save(commit=False, *args, **kwargs) vote.voted = datetime.datetime.now() vote.save() + + +class BaseTotalPointsFormset(BaseModelFormSet): + def clean(self, *args, **kwargs): + super(BaseTotalPointsFormset, self).clean(*args, **kwargs) + total_points = 0 + + for form in self.forms: + if form.cleaned_data['points']: + total_points += form.cleaned_data['points'] + + if total_points < 0 or total_points > 15: + raise forms.ValidationError(ugettext( + "Sum of voting points is out of bounds. Expected from 0 to 15, but got %s." + ) % total_points) diff --git a/pylab/website/templates/website/voting_page.html b/pylab/website/templates/website/voting_page.html index f57a89c..2967c91 100644 --- a/pylab/website/templates/website/voting_page.html +++ b/pylab/website/templates/website/voting_page.html @@ -16,6 +16,7 @@

{{ voting_poll.title }}

{{ total_points }} {% csrf_token %} + {{ formset.non_form_errors }} {{ formset.management_form }} {% for form in formset %}
diff --git a/pylab/website/views.py b/pylab/website/views.py index ed851b3..36d0f9a 100644 --- a/pylab/website/views.py +++ b/pylab/website/views.py @@ -101,7 +101,13 @@ def create_weekly_event(request, year, month, day, slug): @login_required def voting_page(request, voting_poll_slug): voting_poll = get_object_or_404(VotingPoll, slug=voting_poll_slug) - VotePointsFormSet = modelformset_factory(Vote, form=website_forms.VotePointsForm, extra=0,) + total_points = 15 + VotePointsFormSet = modelformset_factory( + Vote, + form=website_forms.VotePointsForm, + formset=website_forms.BaseTotalPointsFormset, + extra=0, + ) vote_qs = Vote.objects.filter(voter=request.user, voting_poll__slug=voting_poll_slug) if request.method == 'POST': @@ -116,5 +122,5 @@ def voting_page(request, voting_poll_slug): return render(request, 'website/voting_page.html', { 'voting_poll': voting_poll, 'formset': formset, - 'total_points': 15, + 'total_points': total_points, }) From a67aa78d5e5e136bcd26cc54ff2fcd3b66a353a1 Mon Sep 17 00:00:00 2001 From: vomaxHELLnO Date: Mon, 10 Aug 2015 10:11:54 +0300 Subject: [PATCH 12/13] Add test for voting points sum form validation 15 total voting points is now hardcoded, but VotingPoll should store this value. --- pylab/website/tests/test_voting.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pylab/website/tests/test_voting.py b/pylab/website/tests/test_voting.py index d67e390..34c1eaf 100644 --- a/pylab/website/tests/test_voting.py +++ b/pylab/website/tests/test_voting.py @@ -37,3 +37,13 @@ def test_voting_page(self): for v in Vote.objects.all(): self.assertLess(time_before_vote, v.voted) self.assertGreater(time_after_vote, v.voted) + + resp = self.app.get('/vote/test-voting-poll/', user='u1') + self.assertEqual(resp.status_int, 200) + + time_before_vote = datetime.datetime.now() + + resp.form['form-0-points'].value = 30 # Vote points sum should be less or equal to 15 + resp.form['form-1-points'].value = 20 + resp = resp.form.submit() + self.assertEqual(resp.status_int, 200) From 8dacaf374cdb0e22325ef49aea221ce95e4da73e Mon Sep 17 00:00:00 2001 From: vomaxHELLnO Date: Mon, 10 Aug 2015 15:01:29 +0300 Subject: [PATCH 13/13] Update bootstrap version and download url Version updated from 3.3.1 to 3.3.5, because Travis-CI started to raise 503 service unavailable error when trying to download 3.3.5 Bootstrap version. --- config/assets.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/assets.cfg b/config/assets.cfg index 703871e..1884ed4 100644 --- a/config/assets.cfg +++ b/config/assets.cfg @@ -12,7 +12,7 @@ hash-name = false [bootstrap] recipe = hexagonit.recipe.download -url = https://github.com/twbs/bootstrap/releases/download/v3.3.1/bootstrap-3.3.1-dist.zip +url = https://github.com/twbs/bootstrap/releases/download/v3.3.5/bootstrap-3.3.5-dist.zip hash-name = false strip-top-level-dir = true