From 064b07e75ad72c84571b98612df0692359c29162 Mon Sep 17 00:00:00 2001 From: Aarni Date: Wed, 13 Sep 2017 19:06:59 +0300 Subject: [PATCH 01/20] Ohlhafv model --- webapp/models.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/webapp/models.py b/webapp/models.py index 993702d..04e19b6 100644 --- a/webapp/models.py +++ b/webapp/models.py @@ -111,6 +111,26 @@ class Official(User): phone_number = PhoneNumberField(_('Phone number')) +#Ohlhafv +class OhlhafvChallenge(models.Model): + ''' + Model containing all info about ohlhafv challenge + ''' + SERIES_CHOICES = ( + ('0.33 L', '0.33 L'), + ('0.5 L', '0.5 L'), + ('1.0 L', '1.0 L'), + ) + class Meta: + verbose_name = _('OhlhafvChallenge') + + challenger = models.CharField(max_length=256) + victim = models.CharField(max_length=256) + challenger_email = models.EmailField() + victim_email = models.EmailField() + series = models.CharField(choices=SERIES_CHOICES, max_length=10) + message = models.TextField() + auditlog.register(Tag) auditlog.register(Feed) From 68b37dea9102f2dec03c1d98aacb5d45a2aae35f Mon Sep 17 00:00:00 2001 From: Aarni Date: Wed, 13 Sep 2017 19:21:47 +0300 Subject: [PATCH 02/20] Registration model --- webapp/models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/webapp/models.py b/webapp/models.py index 04e19b6..1927028 100644 --- a/webapp/models.py +++ b/webapp/models.py @@ -35,6 +35,13 @@ class Feed(BaseFeed): class Event(BaseFeed): start_time = models.DateTimeField(default=timezone.now) end_time = models.DateTimeField(default=timezone.now) + registration = models.ForeignKey('Registration', on_delete=models.CASCADE) + + +class Registration(models.Model): + name = models.CharField(max_length=256) + email = models.EmailField() + options = models.JSONField() class BaseRole(models.Model): @@ -111,6 +118,7 @@ class Official(User): phone_number = PhoneNumberField(_('Phone number')) + #Ohlhafv class OhlhafvChallenge(models.Model): ''' From 62be06ea6e8530de7a91a42943a070a0740e4b4b Mon Sep 17 00:00:00 2001 From: Aarni Date: Wed, 13 Sep 2017 19:24:13 +0300 Subject: [PATCH 03/20] Remove copypasta verbose_name --- webapp/models.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/webapp/models.py b/webapp/models.py index 1927028..ff4e890 100644 --- a/webapp/models.py +++ b/webapp/models.py @@ -129,8 +129,6 @@ class OhlhafvChallenge(models.Model): ('0.5 L', '0.5 L'), ('1.0 L', '1.0 L'), ) - class Meta: - verbose_name = _('OhlhafvChallenge') challenger = models.CharField(max_length=256) victim = models.CharField(max_length=256) From c0e8ba21ee777648132e29d1ecf5c9385c2acd09 Mon Sep 17 00:00:00 2001 From: henu Date: Wed, 20 Sep 2017 17:50:57 +0300 Subject: [PATCH 04/20] Create header template file --- webapp/templates/sik_header.html | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 webapp/templates/sik_header.html diff --git a/webapp/templates/sik_header.html b/webapp/templates/sik_header.html new file mode 100644 index 0000000..e69de29 From f006c6ceef86041151b757a31824ba34748af576 Mon Sep 17 00:00:00 2001 From: Aarni Date: Wed, 20 Sep 2017 18:01:44 +0300 Subject: [PATCH 05/20] Fix JSONField and add migration files --- webapp/migrations/0012_auto_20170913_1934.py | 58 ++++++++++++++++++++ webapp/migrations/0013_auto_20170920_1800.py | 45 +++++++++++++++ webapp/models.py | 5 +- 3 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 webapp/migrations/0012_auto_20170913_1934.py create mode 100644 webapp/migrations/0013_auto_20170920_1800.py diff --git a/webapp/migrations/0012_auto_20170913_1934.py b/webapp/migrations/0012_auto_20170913_1934.py new file mode 100644 index 0000000..4604c90 --- /dev/null +++ b/webapp/migrations/0012_auto_20170913_1934.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-09-13 16:34 +from __future__ import unicode_literals + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('webapp', '0011_auto_20170913_1841'), + ] + + operations = [ + migrations.CreateModel( + name='OhlhafvChallenge', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('challenger', models.CharField(max_length=255)), + ('victim', models.CharField(max_length=255)), + ('challenger_email', models.EmailField(max_length=254)), + ('victim_email', models.EmailField(max_length=254)), + ('series', models.CharField(choices=[('0.33 L', '0.33 L'), ('0.5 L', '0.5 L'), ('1.0 L', '1.0 L')], max_length=10)), + ('message', models.TextField()), + ], + ), + migrations.CreateModel( + name='Registration', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('email', models.EmailField(max_length=254)), + ('options', django.contrib.postgres.fields.jsonb.JSONField()), + ], + ), + migrations.AlterField( + model_name='baserole', + name='name', + field=models.CharField(max_length=255, verbose_name='Name'), + ), + migrations.AlterField( + model_name='kaehmyform', + name='name', + field=models.CharField(max_length=255, verbose_name='Name'), + ), + migrations.AlterField( + model_name='kaehmymessage', + name='name', + field=models.CharField(max_length=255, verbose_name='Name'), + ), + migrations.AddField( + model_name='event', + name='registration', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='webapp.Registration'), + ), + ] diff --git a/webapp/migrations/0013_auto_20170920_1800.py b/webapp/migrations/0013_auto_20170920_1800.py new file mode 100644 index 0000000..d367aab --- /dev/null +++ b/webapp/migrations/0013_auto_20170920_1800.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-09-20 15:00 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('webapp', '0012_auto_20170913_1934'), + ] + + operations = [ + migrations.AlterField( + model_name='baserole', + name='name', + field=models.CharField(max_length=256, verbose_name='Name'), + ), + migrations.AlterField( + model_name='kaehmyform', + name='name', + field=models.CharField(max_length=256, verbose_name='Name'), + ), + migrations.AlterField( + model_name='kaehmymessage', + name='name', + field=models.CharField(max_length=256, verbose_name='Name'), + ), + migrations.AlterField( + model_name='ohlhafvchallenge', + name='challenger', + field=models.CharField(max_length=256), + ), + migrations.AlterField( + model_name='ohlhafvchallenge', + name='victim', + field=models.CharField(max_length=256), + ), + migrations.AlterField( + model_name='registration', + name='name', + field=models.CharField(max_length=256), + ), + ] diff --git a/webapp/models.py b/webapp/models.py index ff4e890..bf2f9af 100644 --- a/webapp/models.py +++ b/webapp/models.py @@ -7,6 +7,7 @@ from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import User from auditlog.registry import auditlog from phonenumber_field.modelfields import PhoneNumberField +from django.contrib.postgres.fields import JSONField class Tag(models.Model): @@ -35,13 +36,13 @@ class Feed(BaseFeed): class Event(BaseFeed): start_time = models.DateTimeField(default=timezone.now) end_time = models.DateTimeField(default=timezone.now) - registration = models.ForeignKey('Registration', on_delete=models.CASCADE) + registration = models.ForeignKey('Registration', on_delete=models.CASCADE, null=True) class Registration(models.Model): name = models.CharField(max_length=256) email = models.EmailField() - options = models.JSONField() + options = JSONField() class BaseRole(models.Model): From 5e4a41d52a7dd2a812022e29c2d14ebe8f814ac5 Mon Sep 17 00:00:00 2001 From: Aarni Date: Wed, 20 Sep 2017 18:06:01 +0300 Subject: [PATCH 06/20] Unite some CharField max_lengths --- webapp/models.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/webapp/models.py b/webapp/models.py index bf2f9af..569089e 100644 --- a/webapp/models.py +++ b/webapp/models.py @@ -40,7 +40,7 @@ class Event(BaseFeed): class Registration(models.Model): - name = models.CharField(max_length=256) + name = models.CharField(max_length=255) email = models.EmailField() options = JSONField() @@ -49,7 +49,7 @@ class BaseRole(models.Model): ''' Base model for occupations/roles ''' - name = models.CharField(_('Name'), max_length=256) + name = models.CharField(_('Name'), max_length=255) is_board = models.BooleanField(_('Board member')) @@ -81,7 +81,7 @@ class KaehmyMessage(MessageParent): Model representing a kaehmymessage. Every message relates to certain kaehmyform or parent message. ''' - name = models.CharField(_('Name'), max_length=256) + name = models.CharField(_('Name'), max_length=255) email = models.EmailField(_('Email')) message = models.TextField(_('Message')) parent = models.ForeignKey('MessageParent', related_name='messages') @@ -92,7 +92,7 @@ class KaehmyForm(MessageParent): Model representing a form for kaehmy. Allows user to choose from existing roles or to create custom ones. ''' - name = models.CharField(_('Name'), max_length=256) + name = models.CharField(_('Name'), max_length=255) email = models.EmailField(_('Email')) year = models.IntegerField(_('Year')) @@ -131,8 +131,8 @@ class OhlhafvChallenge(models.Model): ('1.0 L', '1.0 L'), ) - challenger = models.CharField(max_length=256) - victim = models.CharField(max_length=256) + challenger = models.CharField(max_length=255) + victim = models.CharField(max_length=255) challenger_email = models.EmailField() victim_email = models.EmailField() series = models.CharField(choices=SERIES_CHOICES, max_length=10) From e18f28ab7f04acd2b5363622fb311bf1b8e4d9b0 Mon Sep 17 00:00:00 2001 From: Aarni Date: Wed, 20 Sep 2017 18:09:09 +0300 Subject: [PATCH 07/20] Migration file for max_length change --- webapp/migrations/0014_auto_20170920_1807.py | 45 ++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 webapp/migrations/0014_auto_20170920_1807.py diff --git a/webapp/migrations/0014_auto_20170920_1807.py b/webapp/migrations/0014_auto_20170920_1807.py new file mode 100644 index 0000000..633abb4 --- /dev/null +++ b/webapp/migrations/0014_auto_20170920_1807.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-09-20 15:07 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('webapp', '0013_auto_20170920_1800'), + ] + + operations = [ + migrations.AlterField( + model_name='baserole', + name='name', + field=models.CharField(max_length=255, verbose_name='Name'), + ), + migrations.AlterField( + model_name='kaehmyform', + name='name', + field=models.CharField(max_length=255, verbose_name='Name'), + ), + migrations.AlterField( + model_name='kaehmymessage', + name='name', + field=models.CharField(max_length=255, verbose_name='Name'), + ), + migrations.AlterField( + model_name='ohlhafvchallenge', + name='challenger', + field=models.CharField(max_length=255), + ), + migrations.AlterField( + model_name='ohlhafvchallenge', + name='victim', + field=models.CharField(max_length=255), + ), + migrations.AlterField( + model_name='registration', + name='name', + field=models.CharField(max_length=255), + ), + ] From 33215343ec774796b49972212a0065a354f388a5 Mon Sep 17 00:00:00 2001 From: henu Date: Wed, 20 Sep 2017 18:42:56 +0300 Subject: [PATCH 08/20] Add header logo and header css files --- webapp/static/css/sik_header.css | 10 ++++++++++ webapp/static/img/logo_header.png | Bin 0 -> 63396 bytes 2 files changed, 10 insertions(+) create mode 100644 webapp/static/css/sik_header.css create mode 100644 webapp/static/img/logo_header.png diff --git a/webapp/static/css/sik_header.css b/webapp/static/css/sik_header.css new file mode 100644 index 0000000..717d2e1 --- /dev/null +++ b/webapp/static/css/sik_header.css @@ -0,0 +1,10 @@ +.header-content { + +} + +.header-content .logo img { + display: block; + width: 80%; + height: auto; + margin: auto; +} diff --git a/webapp/static/img/logo_header.png b/webapp/static/img/logo_header.png new file mode 100644 index 0000000000000000000000000000000000000000..f7491f3d79a7d820aa6ba2707d8475971912658a GIT binary patch literal 63396 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 Date: Wed, 20 Sep 2017 18:43:34 +0300 Subject: [PATCH 09/20] Add logo to header --- webapp/templates/sik_header.html | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/webapp/templates/sik_header.html b/webapp/templates/sik_header.html index e69de29..a4ff871 100644 --- a/webapp/templates/sik_header.html +++ b/webapp/templates/sik_header.html @@ -0,0 +1,11 @@ +{% load i18n %} + + +
+ +
+ +
+
From ec5c206c8e830dea450fbcd10ffe5b8c207741ef Mon Sep 17 00:00:00 2001 From: henu Date: Wed, 20 Sep 2017 18:44:03 +0300 Subject: [PATCH 10/20] Include header to base html file --- webapp/templates/base.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/webapp/templates/base.html b/webapp/templates/base.html index 16c8cce..d864e21 100644 --- a/webapp/templates/base.html +++ b/webapp/templates/base.html @@ -22,6 +22,9 @@ +
+ {% include "sik_header.html" %} +
- {{ error }} + {{ error|safe }}
diff --git a/members/templates/member_add_many.html b/members/templates/member_add_many.html index fc083eb..9d9b4e8 100644 --- a/members/templates/member_add_many.html +++ b/members/templates/member_add_many.html @@ -5,7 +5,7 @@ {% block content %}
-

Lisää useampi jäsen

+

{% trans "Add many members" %}

@@ -25,9 +25,18 @@
{% csrf_token %} -
+
+
+
+ + +
diff --git a/members/templates/member_add_many_confirm.html b/members/templates/member_add_many_confirm.html new file mode 100644 index 0000000..2cff70a --- /dev/null +++ b/members/templates/member_add_many_confirm.html @@ -0,0 +1,26 @@ +{% extends "members_base.html" %} + +{% load i18n %} + +{% block content %} +
+
+

{% trans "Confirm adding these entries?" %}

+
+ +
+ + {{ members|safe }} +
+
+ + {{ payments|safe }} +
+
+ {% csrf_token %} +
+ +
+ +
+{% endblock content %} diff --git a/members/templates/member_duplicates.html b/members/templates/member_duplicates.html deleted file mode 100644 index c6f2995..0000000 --- a/members/templates/member_duplicates.html +++ /dev/null @@ -1,40 +0,0 @@ -{% extends "members_base.html" %} - -{% load i18n %} -{% load bootstrap3 %} - -{% block content %} -
-
-

{% trans "Conflicting member entries" %}

-
- -
-

{% blocktrans %}Found conflicting member entries. Choose how to handle the problematic data.{% endblocktrans %}

- - {% for conflict in conflicts %} -
-
- - {{ conflict.first_member_form }} -
-
-
- - {{ conflict.second_member_form }} -
-
-
-
{% csrf_token %} -

{% blocktrans %}Which one has the correct information for this member?{% endblocktrans %}

- - - - -
-
-
- {% endfor %} -
-
-{% endblock content %} diff --git a/members/urls.py b/members/urls.py index 9738af6..397409a 100644 --- a/members/urls.py +++ b/members/urls.py @@ -16,8 +16,7 @@ from members.views import member_update from members.views import member_delete_confirm from members.views import member_delete from members.views import payment_list -from members.views import member_duplicates -from members.views import resolve_conflict +from members.views import add_many_confirm # rest api from members.views import MemberDetail @@ -89,6 +88,9 @@ urlpatterns = [ # delete confirmation view url(r'^delete_payment_confirm/(?P\d+)$', payment_delete_confirm), + # post endpoint for confirming multiple entries + url(r'^add_many_confirm$', add_many_confirm), + # settings page url(r'^settings$', settings_page), @@ -104,10 +106,4 @@ urlpatterns = [ # rest api url url(r'^api/members/(?P\d+)$', MemberDetail.as_view()), - # member duplicate resolution view - url(r'^duplicates$', member_duplicates), - - # post target for resolving a conflict - url(r'^resolve_conflict$', resolve_conflict) - ] diff --git a/members/views.py b/members/views.py index da03f3d..2792dd6 100644 --- a/members/views.py +++ b/members/views.py @@ -6,6 +6,7 @@ from django.http import HttpResponse, HttpResponseRedirect from django.core.mail import send_mail from django.conf import settings from django.utils.translation import ugettext as _ +from django.forms.models import model_to_dict # Email validation from django.db.models.signals import post_save @@ -25,13 +26,18 @@ import requests import logging import html import csv +import pickle from smtplib import SMTPAuthenticationError from members.models import Member, Request, Payment, MemberConflict -from members.forms import MemberForm, PaymentForm, ApplicationForm +from members.forms import MemberForm, PaymentForm, ApplicationForm, CSVValidationError from members.tables import MemberTable, PaymentTable, RequestTable +def error_view(request, message): + return render(request, 'error.html', {'error': str(message)}) + + def validate_recaptcha(response): ''' Recaptcha is used in member applications @@ -421,16 +427,61 @@ def settings_page(request, *args, **kwargs): def import_csv(request, *args, **kwargs): try: data = request.POST['textfield'] + payment_source = request.POST['payment_source'] except: return render(request, 'error.html', {'error': _('Missing "textfield" POST request field')}) - success = Member.from_csv(data) - if success: - logging.info('Imported CSV data:\n'.format(data)) - notification = "{}.".format(_("Successfully imported multiple members")) + try: + result = MemberForm.csv_to_models(data, payment_source=payment_source) + except CSVValidationError as ex: + logging.exception('Model validation error') + return error_view(request, ex.form_errors) + except Exception as ex: + logging.exception('Other error in CSV import') + return error_view(request, ex) + + member_table = MemberTable(result.members, + request=request, + exclude=['id', 'options'], + attrs={'class': 'table table-bordered table-hover'}) + + member_table_html = convert_table_to_html(member_table, request) + + payment_table = PaymentTable(result.payments, + request=request, + exclude=['id', 'options'], + attrs={'class': 'table table-bordered table-hover'}) + + payment_table_html = convert_table_to_html(payment_table, request) + + request.session['models'] = result + context = { + 'members': member_table_html, + 'payments': payment_table_html + } + return render(request, 'member_add_many_confirm.html', context) + + +@ensure_csrf_cookie +@require_http_methods(["POST"]) +@permission_required('members.change_member', login_url='/login') +def add_many_confirm(request, *args, **kwargs): + models = request.session['models'] + + try: + members, payments = models.members, models.payments + for member in members: + member.save() + + for payment in payments: + payment.save() + + msg = "Successfully imported {} members and {} payments." + notification = _(msg).format(len(members), len(payments)) return HttpResponseRedirect('/members/list?notification={}'.format(html.escape(notification))) - else: - return render(request, 'error.html', {'error': _('Failed to import members')}) + except Exception as ex: + logging.exception('Failed to save models after "add many."') + return error_view(request, _('Failed to import members')) @ensure_csrf_cookie @@ -452,54 +503,6 @@ def export_csv(request, *args, **kwargs): return response -@ensure_csrf_cookie -@require_http_methods(["GET"]) -@permission_required('members.change_member', login_url='/login') -def member_duplicates(request, *args, **kwargs): - conflicts = MemberConflict.objects.all() - context = { - 'conflicts': conflicts - } - - return render(request, 'member_duplicates.html', context) - - -@ensure_csrf_cookie -@require_http_methods(["POST"]) -@permission_required('members.change_member', login_url='/login') -def resolve_conflict(request, *args, **kwargs): - action = request.POST.get('action', None) - if action not in ['first', 'second', 'both']: - return render(request, 'error.html', {'error': '{}: {}'.format(('Incorrect action value'), action)}) - - id = request.POST.get('id', None) - if id is None: - return render(request, 'error.html', {'error': '{}: {}'.format(('Incorrect id value'), id)}) - - conflict = MemberConflict.objects.get(id=id) - first_member = conflict.first_member - second_member = conflict.second_member - - if action == 'first': - for payment in second_member.payments.all(): - payment.member = first_member - payment.save() - second_member.delete() - elif action == 'second': - for payment in first_member.payments.all(): - payment.member = second_member - payment.save() - first_member.delete() - - conflict.delete() - - if MemberConflict.objects.exists(): - return HttpResponseRedirect('/members/duplicates') - else: - notification = _('Successfully resolved all member conflicts.') - return HttpResponseRedirect('/members/list?notification={}'.format(html.escape(notification))) - - def send_mail_wrapper(subject, message, email_to): send_mail(subject, message, @@ -536,25 +539,6 @@ def email_on_accept(sender, instance, created, **kwargs): logging.error('Failed to send email to accepted member!') -def check_for_duplicates(instance): - name_candidates = Member.objects.filter(first_name=instance.first_name, - last_name=instance.last_name) - email_candidates = Member.objects.filter(email=instance.email) - - candidates = name_candidates | email_candidates - duplicates = candidates.exclude(id=instance.id) - - if len(duplicates) > 0: - conflict = MemberConflict(first_member=instance, - second_member=duplicates[0]) - conflict.save() - - -@receiver(post_save, sender=Member) -def duplicate_receiver(sender, instance, created, **kwargs): - check_for_duplicates(instance) - - # Can be used to retrieve single member information via REST API class MemberDetail(generics.RetrieveAPIView): queryset = Member.objects.all() diff --git a/setup.sh b/setup.sh index 8f71754..6ac4281 100755 --- a/setup.sh +++ b/setup.sh @@ -16,7 +16,7 @@ then USE_NPM="false" fi -$INTERACTIVE || echo "Running in non-interactive mode." && env +$INTERACTIVE || (echo "Running in non-interactive mode." && env) $INTERACTIVE && read -p "Are these programs installed? [y/n]" -n 1 -r || REPLY="y" echo "" From 5d1238c23d313ea41a49822406af27a803cf2d10 Mon Sep 17 00:00:00 2001 From: Jan Tuomi Date: Wed, 20 Sep 2017 23:19:45 +0300 Subject: [PATCH 18/20] Change prints to logs --- coffee_scale/mqtt.py | 4 ++-- infoscreen/views.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/coffee_scale/mqtt.py b/coffee_scale/mqtt.py index 76751bc..3a984fb 100644 --- a/coffee_scale/mqtt.py +++ b/coffee_scale/mqtt.py @@ -48,10 +48,10 @@ def on_message(client, userdata, msg): def on_disconnect(client, userdata, rc): if rc != 0: - print("Unexpected disconnection.") + logging.warning("MQTT unexpectedly disconnected.") else: client.loop_stop(force=False) - print("Disconnected") + logging.warning("MQTT disconnected.") def get_latest(): diff --git a/infoscreen/views.py b/infoscreen/views.py index 055cee6..4bd878a 100644 --- a/infoscreen/views.py +++ b/infoscreen/views.py @@ -165,8 +165,6 @@ def create_image_item(request, *args, **kwargs): @permission_required('infoscreen.change_infoinstance', login_url='/login') def create_video_item(request, *args, **kwargs): form = UploadFileForm(request.POST, request.FILES) - print(form.errors) - print("hurdurr") if not form.is_valid(): return HttpResponseBadRequest('{"status": "failure",' '"error": "invalid data supplied"}') From 0b779c47993b343cbb27e548a0022398a776179e Mon Sep 17 00:00:00 2001 From: Jan Tuomi Date: Wed, 20 Sep 2017 23:27:09 +0300 Subject: [PATCH 19/20] Fix pep8 errors --- webapp/forms.py | 2 +- webapp/models.py | 2 +- webapp/tables.py | 2 +- webapp/urls.py | 2 +- webapp/views.py | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/webapp/forms.py b/webapp/forms.py index 0e0930f..0007eab 100644 --- a/webapp/forms.py +++ b/webapp/forms.py @@ -8,4 +8,4 @@ class OhlhafvForm(forms.ModelForm): class Meta: model = OhlhafvChallenge - fields = ['challenger', 'challenger_email', 'victim', 'victim_email', 'series', 'message'] \ No newline at end of file + fields = ['challenger', 'challenger_email', 'victim', 'victim_email', 'series', 'message'] diff --git a/webapp/models.py b/webapp/models.py index 569089e..4a434c6 100644 --- a/webapp/models.py +++ b/webapp/models.py @@ -120,7 +120,7 @@ class Official(User): phone_number = PhoneNumberField(_('Phone number')) -#Ohlhafv +# Ohlhafv class OhlhafvChallenge(models.Model): ''' Model containing all info about ohlhafv challenge diff --git a/webapp/tables.py b/webapp/tables.py index 40e8742..186f011 100644 --- a/webapp/tables.py +++ b/webapp/tables.py @@ -5,4 +5,4 @@ from webapp.models import OhlhafvChallenge class OhlhafvTable(tables.Table): class Meta: - model = OhlhafvChallenge \ No newline at end of file + model = OhlhafvChallenge diff --git a/webapp/urls.py b/webapp/urls.py index 87f2d50..e96fb6e 100644 --- a/webapp/urls.py +++ b/webapp/urls.py @@ -21,7 +21,7 @@ urlpatterns = [ # git revision url(r'^about', about_view), - #ohlhafv + # ohlhafv url(r'^ohlhafv$', ohlhafv_view), url(r'^ohlhafv/submit', ohlhafv_submit), url(r'^ohlhafv/list', ohlhafv_list), diff --git a/webapp/views.py b/webapp/views.py index c001339..0b522ea 100644 --- a/webapp/views.py +++ b/webapp/views.py @@ -66,10 +66,10 @@ def ohlhafv_submit(request, *args, **kwargs): form = OhlhafvForm(request.POST) if form.is_valid(): form.save() - #return HttpResponseRedirect('/list/') + # return HttpResponseRedirect('/list/') else: pass - #return render(request, 'error.html', {'error': form.errors}) + # return render(request, 'error.html', {'error': form.errors}) return HttpResponseRedirect('/ohlhafv/list/') From a99544f967b540aef755cca718c1c6bfb1e4e1b8 Mon Sep 17 00:00:00 2001 From: Jan Tuomi Date: Wed, 20 Sep 2017 23:35:41 +0300 Subject: [PATCH 20/20] Fix more pep8 badlings --- infoscreen/hsl_fetcher.py | 2 +- infoscreen/models.py | 4 ++-- members/views.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/infoscreen/hsl_fetcher.py b/infoscreen/hsl_fetcher.py index 9828391..8cc29e1 100644 --- a/infoscreen/hsl_fetcher.py +++ b/infoscreen/hsl_fetcher.py @@ -46,7 +46,7 @@ class HSLFetcher: ("https://api.reittiopas.fi/hsl/prod/?userhash={}" "&request=stop&code={}&dep_limit=20&time={}") .format(settings.HSL_USERHASH, element['code'], time) - ).read().decode("utf-8") + ).read().decode("utf-8") parsed = json.loads(src)[0] arr.append({ diff --git a/infoscreen/models.py b/infoscreen/models.py index 04421da..6805637 100644 --- a/infoscreen/models.py +++ b/infoscreen/models.py @@ -372,8 +372,8 @@ class Rotation(models.Model): # to avoid excluding items with no expire_date) now = timezone.now() instances = self.instances.all() - filtered = filter(lambda i: (i.item.expire_date or now) - >= now, list(instances)) + filtered = filter(lambda i: (i.item.expire_date or now) >= now, + list(instances)) instance_list = list(map(lambda i: i.get_dict(), filtered)) return { diff --git a/members/views.py b/members/views.py index ef1931d..7d3f33c 100644 --- a/members/views.py +++ b/members/views.py @@ -410,8 +410,8 @@ def payment_submit(request, *args, **kwargs): "Saved new payment to member register with the following info: {}" .format(form)) notification = "{} {}.".format( - _("Successfully added payment for member"), - form.cleaned_data['member']) + _("Successfully added payment for member"), + form.cleaned_data['member']) return HttpResponseRedirect( '/members/payments?notification={}' .format(html.escape(notification)))
{% include "navigation.html" %} {% block content %} From 593b380ecf141d8a0351924f9c68daf9abff75a6 Mon Sep 17 00:00:00 2001 From: henu Date: Wed, 20 Sep 2017 18:52:36 +0300 Subject: [PATCH 11/20] Add link to header img --- webapp/templates/sik_header.html | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/webapp/templates/sik_header.html b/webapp/templates/sik_header.html index a4ff871..17ef251 100644 --- a/webapp/templates/sik_header.html +++ b/webapp/templates/sik_header.html @@ -3,9 +3,6 @@ From e294707aed29734da672a2564ec985c5eaa9deaa Mon Sep 17 00:00:00 2001 From: henu Date: Wed, 20 Sep 2017 18:52:57 +0300 Subject: [PATCH 12/20] Increase logo width to 100% --- webapp/static/css/sik_header.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/webapp/static/css/sik_header.css b/webapp/static/css/sik_header.css index 717d2e1..37685f6 100644 --- a/webapp/static/css/sik_header.css +++ b/webapp/static/css/sik_header.css @@ -2,9 +2,13 @@ } +.header-content .logo { + +} + .header-content .logo img { display: block; - width: 80%; + width: 100%; height: auto; margin: auto; } From 7a435bcbc6feb783488c68579f739ac4f42bc569 Mon Sep 17 00:00:00 2001 From: Aarni Date: Wed, 20 Sep 2017 19:27:36 +0300 Subject: [PATCH 13/20] Simple Ohlhafv challenge machine --- webapp/forms.py | 11 ++++++++ webapp/tables.py | 8 ++++++ webapp/templates/ohlhafv.html | 21 +++++++++++++++ webapp/templates/ohlhafv_list.html | 20 ++++++++++++++ webapp/urls.py | 8 ++++++ webapp/views.py | 43 ++++++++++++++++++++++++++++++ 6 files changed, 111 insertions(+) create mode 100644 webapp/forms.py create mode 100644 webapp/tables.py create mode 100644 webapp/templates/ohlhafv.html create mode 100644 webapp/templates/ohlhafv_list.html diff --git a/webapp/forms.py b/webapp/forms.py new file mode 100644 index 0000000..0e0930f --- /dev/null +++ b/webapp/forms.py @@ -0,0 +1,11 @@ +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from webapp.models import OhlhafvChallenge + + +class OhlhafvForm(forms.ModelForm): + + class Meta: + model = OhlhafvChallenge + fields = ['challenger', 'challenger_email', 'victim', 'victim_email', 'series', 'message'] \ No newline at end of file diff --git a/webapp/tables.py b/webapp/tables.py new file mode 100644 index 0000000..40e8742 --- /dev/null +++ b/webapp/tables.py @@ -0,0 +1,8 @@ +import django_tables2 as tables +from django.utils.translation import ugettext as _ +from webapp.models import OhlhafvChallenge + + +class OhlhafvTable(tables.Table): + class Meta: + model = OhlhafvChallenge \ No newline at end of file diff --git a/webapp/templates/ohlhafv.html b/webapp/templates/ohlhafv.html new file mode 100644 index 0000000..05decc5 --- /dev/null +++ b/webapp/templates/ohlhafv.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% load bootstrap3 %} +{% load i18n %} + +{% block content %} +
+

{% trans "Ohlhafv" %}

+ +
+
{% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + {% endbuttons %} +
+
+
+{% endblock content %} diff --git a/webapp/templates/ohlhafv_list.html b/webapp/templates/ohlhafv_list.html new file mode 100644 index 0000000..a21e7e5 --- /dev/null +++ b/webapp/templates/ohlhafv_list.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% load static %} +{% load i18n %} +{% load django_tables2 %} + +{% block content %} +
+ +
+

{% trans "All challenges" %}

+
+ +
+ {% trans "Total challenges:" %} {{ challenge_count }} +
+ + {{ table|safe }} +
+{% endblock content %} diff --git a/webapp/urls.py b/webapp/urls.py index 2dd136d..87f2d50 100644 --- a/webapp/urls.py +++ b/webapp/urls.py @@ -5,6 +5,9 @@ from webapp.views import admin_index from webapp.views import login_view from webapp.views import logout_view from webapp.views import about_view +from webapp.views import ohlhafv_view +from webapp.views import ohlhafv_submit +from webapp.views import ohlhafv_list urlpatterns = [ # main @@ -17,4 +20,9 @@ urlpatterns = [ # git revision url(r'^about', about_view), + + #ohlhafv + url(r'^ohlhafv$', ohlhafv_view), + url(r'^ohlhafv/submit', ohlhafv_submit), + url(r'^ohlhafv/list', ohlhafv_list), ] diff --git a/webapp/views.py b/webapp/views.py index d8e0ac5..c001339 100644 --- a/webapp/views.py +++ b/webapp/views.py @@ -2,9 +2,13 @@ from django.shortcuts import render, redirect from django.contrib.auth import login, logout, authenticate from django.views.decorators.http import require_http_methods from django.views.decorators.csrf import ensure_csrf_cookie +from django.http import HttpResponse, HttpResponseRedirect from django.contrib.auth.decorators import permission_required from django.conf import settings import logging +from webapp.models import OhlhafvChallenge +from webapp.forms import OhlhafvForm +from webapp.tables import OhlhafvTable @require_http_methods(["GET"]) @@ -48,3 +52,42 @@ def logout_view(request, *args, **kwargs): @require_http_methods(["GET"]) def about_view(request, *args, **kwargs): return render(request, "about.html", {}) + + +@require_http_methods(["GET"]) +def ohlhafv_view(request, *args, **kwargs): + form = OhlhafvForm() + return render(request, 'ohlhafv.html', {'form': form}) + + +@ensure_csrf_cookie +@require_http_methods(["POST"]) +def ohlhafv_submit(request, *args, **kwargs): + form = OhlhafvForm(request.POST) + if form.is_valid(): + form.save() + #return HttpResponseRedirect('/list/') + else: + pass + #return render(request, 'error.html', {'error': form.errors}) + return HttpResponseRedirect('/ohlhafv/list/') + + +@ensure_csrf_cookie +@require_http_methods(["GET"]) +def ohlhafv_list(request, *args, **kwargs): + challenges = OhlhafvChallenge.objects.all() + + table = OhlhafvTable(challenges, + request=request, + exclude=['id', 'challenger_email', 'victim_email'], + attrs={'class': 'table table-bordered table-hover'}) + + table.paginate(page=request.GET.get('page', 1), per_page=25) + table_html = table.as_html(request) + + context = { + 'table': table_html, + 'challenge_count': len(challenges), + } + return render(request, 'ohlhafv_list.html', context) From ecc0ac965ef363b0fba01de69b81e26361df8177 Mon Sep 17 00:00:00 2001 From: henu Date: Wed, 20 Sep 2017 20:08:26 +0300 Subject: [PATCH 14/20] Fix infoscreen pep8 and add docstrings --- infoscreen/admin.py | 2 + infoscreen/apps.py | 4 ++ infoscreen/hsl_fetcher.py | 36 ++++++++---- infoscreen/models.py | 112 ++++++++++++++++++++++++++++++++------ infoscreen/tests.py | 30 +++++----- infoscreen/urls.py | 2 + infoscreen/views.py | 43 ++++++++++++--- 7 files changed, 177 insertions(+), 52 deletions(-) diff --git a/infoscreen/admin.py b/infoscreen/admin.py index b4f9217..d7b314c 100644 --- a/infoscreen/admin.py +++ b/infoscreen/admin.py @@ -1,3 +1,5 @@ +"""Admin site registers.""" + from django.contrib import admin from infoscreen.models import Rotation, InfoItem, InfoInstance from infoscreen.models import ImageInfoItem, ExternalImageInfoItem, ABBInfoItem diff --git a/infoscreen/apps.py b/infoscreen/apps.py index 0789a89..8b14bab 100644 --- a/infoscreen/apps.py +++ b/infoscreen/apps.py @@ -1,5 +1,9 @@ +"""Django apps configuration file.""" + from django.apps import AppConfig class InfoscreenConfig(AppConfig): + """Infoscreen app configuration.""" + name = 'infoscreen' diff --git a/infoscreen/hsl_fetcher.py b/infoscreen/hsl_fetcher.py index 89c6aec..9828391 100644 --- a/infoscreen/hsl_fetcher.py +++ b/infoscreen/hsl_fetcher.py @@ -1,3 +1,5 @@ +"""File containing Infoscreen HSL data fetcher classes.""" + import urllib.request import json import logging @@ -9,36 +11,49 @@ from infoscreen.models import HSLDataModel class HSLFetcher: + """Main class of Infoscreen HSL fetcher.""" last_fetched = datetime.fromtimestamp(0) # epoch INTERVAL = 1 # minutes - logging.info("Set up scheduled HSL API fetch every {} minutes".format(INTERVAL)) + logging.info( + "Set up scheduled HSL API fetch every {} minutes".format(INTERVAL)) def fetch_if_needed(self): - if datetime.now() - HSLFetcher.last_fetched > timedelta(minutes=HSLFetcher.INTERVAL): + """Check if new fetch from HSL API is needed.""" + if (datetime.now() - HSLFetcher.last_fetched > + timedelta(minutes=HSLFetcher.INTERVAL)): self.fetch() def fetch(self): + """Fetch data from HSL API.""" location_coords = (2545565, 6675319) src = urllib.request.urlopen( - "https://api.reittiopas.fi/hsl/prod/?userhash={}&request=stops_area¢er_coordinate={},{}" - .format(settings.HSL_USERHASH, location_coords[0], location_coords[1]))\ + ("https://api.reittiopas.fi/hsl/prod/?userhash={}" + "&request=stops_area¢er_coordinate={},{}") + .format(settings.HSL_USERHASH, location_coords[0], + location_coords[1]))\ .read().decode("utf-8") data = json.loads(src) arr = [] - time = datetime.now() + timedelta(minutes=settings.HSL_DEPARTURE_THRESHOLD) + time = (datetime.now() + + timedelta(minutes=settings.HSL_DEPARTURE_THRESHOLD)) time = "{0:02d}{0:02d}".format(time.hour, time.minute) for element in data: src = urllib.request.urlopen( - "https://api.reittiopas.fi/hsl/prod/?userhash={}&request=stop&code={}&dep_limit=20&time={}" - .format(settings.HSL_USERHASH, element['code'], time)).read().decode("utf-8") + ("https://api.reittiopas.fi/hsl/prod/?userhash={}" + "&request=stop&code={}&dep_limit=20&time={}") + .format(settings.HSL_USERHASH, element['code'], time) + ).read().decode("utf-8") parsed = json.loads(src)[0] - arr.append({"name": parsed['name_fi'], "lines": parsed['lines'], - "dist": element['dist'], "departures": parsed['departures']}) + arr.append({ + "name": parsed['name_fi'], + "lines": parsed['lines'], + "dist": element['dist'], + "departures": parsed['departures']}) model_arr = HSLDataModel.objects.all() count = len(model_arr) @@ -53,4 +68,5 @@ class HSLFetcher: now = datetime.now() HSLFetcher.last_fetched = now - logging.info("Fetched HSL timetable data with size {} bytes.".format(len(src))) + logging.info( + "Fetched HSL timetable data with size {} bytes.".format(len(src))) diff --git a/infoscreen/models.py b/infoscreen/models.py index 5f47cc5..04421da 100644 --- a/infoscreen/models.py +++ b/infoscreen/models.py @@ -1,3 +1,5 @@ +"""File containing Infoscreen models.""" + from datetime import datetime from django.db import models @@ -9,31 +11,40 @@ from django.utils.translation import ugettext as _ class InfoItem(models.Model): + """Abstract model representing single Infoscreen item.""" class __meta__: abstract = True name = models.CharField(max_length=255) - expire_date = models.DateTimeField(blank=True, null=True) # None means never expiring item + # expire_date = None means never expiring item + expire_date = models.DateTimeField(blank=True, null=True) display_name = "Default item" def get_template_url(self): - raise NotImplementedError("inheriting classes must implement get_template_url") + """Get infoscreen template url.""" + raise NotImplementedError( + "inheriting classes must implement get_template_url") @staticmethod def get_create_template_url(): - raise NotImplementedError("inheriting classes must implement get_create_template_url") + """Get create infoscreen template url command.""" + raise NotImplementedError( + "inheriting classes must implement get_create_template_url") @classmethod def create_from_dict(cls, d): + """Convert given dict to model.""" item = cls() item.update_from_dict(d) return item def update_from_dict(self, d): + """Update model based on given dict.""" try: expire_date = d.pop('expire_date', None) - self.expire_date = datetime.strptime(expire_date, "%Y-%m-%d %H:%M:%S") + self.expire_date = datetime.strptime( + expire_date, "%Y-%m-%d %H:%M:%S") except: pass @@ -48,6 +59,7 @@ class InfoItem(models.Model): self.save() def get_dict(self): + """Convert django model to dict and return it.""" return { 'id': self.id, 'name': self.name, @@ -59,65 +71,86 @@ class InfoItem(models.Model): } def delete(self): - # since generic foreign keys suck, delete info items pointing here manually - InfoInstance.objects.filter(item_id=self.id, item_type=ContentType.objects.get_for_model(self)).delete() + """Delete infoinstance object.""" + # since generic foreign keys suck, delete info + # items pointing here manually + InfoInstance.objects.filter( + item_id=self.id, + item_type=ContentType.objects.get_for_model(self)).delete() super().delete() @classmethod def get_subclasses(cls): + """Get item subclasses.""" for subclass in cls.__subclasses__(): yield from subclass.get_subclasses() yield subclass def __str__(self): + """Return class name.""" return self.name class ABBInfoItem(InfoItem): + """Class for ABB Infoscreen item.""" + display_name = _("ABB jobs") def get_template_url(self): + """Return ABB infoitem template url.""" return "/static/html/abb.html" @staticmethod def get_create_template_url(): + """Call create ABB infoitem template url command.""" return "/static/html/abb_create.html" class ApyInfoItem(InfoItem): + """Class for APY Infoscreen item.""" + display_name = _("APY Item") def get_template_url(self): + """Return APY infoitem template url.""" return "/static/html/apy.html" @staticmethod def get_create_template_url(): + """Call create APY infoitem template url command.""" return "/static/html/apy_create.html" class ExternalWebsiteInfoItem(InfoItem): + """Class for external website info item.""" + display_name = _("External website") url = models.URLField() def get_template_url(self): + """Return external website infoitem template url.""" return "/static/html/external_website.html?url={}".format(self.name) @staticmethod def get_create_template_url(): + """Call create external website infoitem template url command.""" return "/static/html/external_website_create.html" def get_dict(self): + """Convert django model to dict and return it.""" d = super().get_dict() d["options"] = {'url': self.url} return d @classmethod def create_from_dict(cls, d): + """Convert given dict to model.""" item = cls() item.update_from_dict(d) return item def get_list(self): + """Return list containing infoitem data.""" return { 'id': self.id, 'name': self.name, @@ -125,9 +158,11 @@ class ExternalWebsiteInfoItem(InfoItem): } def update_from_dict(self, d): + """Update model based on given dict.""" try: expire_date = d.pop('expire_date', None) - self.expire_date = datetime.strptime(expire_date, "%Y-%m-%d %H:%M:%S") + self.expire_date = datetime.strptime( + expire_date, "%Y-%m-%d %H:%M:%S") except: pass @@ -144,99 +179,130 @@ class ExternalWebsiteInfoItem(InfoItem): class SossoInfoItem(InfoItem): + """Class for Sosso Infoscreen item.""" + display_name = _("Sössö articles") def get_template_url(self): + """Return Sosso infoitem template url.""" return "/static/html/sosso.html" @staticmethod def get_create_template_url(): + """Call create Sosso infoitem template url command.""" return "/static/html/sosso_create.html" class EventInfoItem(InfoItem): + """Class for Event Infoscreen item.""" + display_name = _("Events") def get_template_url(self): + """Return Event infoitem template url.""" return "/static/html/events.html" @staticmethod def get_create_template_url(): + """Call create Event infoitem template url command.""" return "/static/html/events_create.html" class ImageInfoItem(InfoItem): + """Class for Image Infoscreen item.""" + display_name = _("Image") img = models.ImageField(upload_to="infoimages/") def get_template_url(self): - # get param to avoid angular from optimizing same template with different options + """Return Image infoitem template url.""" + # get param to avoid angular from optimizing same template + # with different options return "/static/html/generic_image.html?img={}".format(self.name) @staticmethod def get_create_template_url(): + """Call create Image infoitem template url command.""" return "/static/html/generic_image_create.html" def get_dict(self): + """Convert django model to dict and return it.""" d = super().get_dict() d["options"] = {'img': self.img.url} return d class VideoInfoItem(InfoItem): + """Class for Video Infoscreen item.""" + display_name = ("Video") video = models.FileField(upload_to="infovideos/") def get_template_url(self): + """Return Video infoitem template url.""" return "/static/html/generic_video.html?video={}".format(self.name) @staticmethod def get_create_template_url(): + """Call create Video infoitem template url command.""" return "/static/html/generic_video_create.html" def get_dict(self): + """Convert django model to dict and return it.""" d = super().get_dict() d["options"] = {'video': self.video.url} return d class HslInfoItem(InfoItem): + """Class for HSL Infoscreen item.""" + display_name = _("HSL timetables") def get_template_url(self): + """Return HSL infoitem template url.""" return "/static/html/hsl.html" @staticmethod def get_create_template_url(): + """Call create HSL infoitem template url command.""" return "/static/html/hsl_create.html" class ExternalImageInfoItem(InfoItem): + """Class for External Image Infoscreen item.""" + display_name = _("External image") url = models.URLField() def get_template_url(self): + """Return External Image infoitem template url.""" return "/static/html/generic_image.html?img={}".format(self.name) @staticmethod def get_create_template_url(): + """Call create External Image infoitem template url command.""" return "/static/html/generic_external_image_create.html" def get_dict(self): + """Convert django model to dict and return it.""" d = super().get_dict() d["options"] = {'img': self.url} return d @classmethod def create_from_dict(cls, d): + """Convert given dict to model.""" item = cls() item.update_from_dict(d) return item def update_from_dict(self, d): + """Update model based on given dict.""" try: expire_date = d.pop('expire_date', None) - self.expire_date = datetime.strptime(expire_date, "%Y-%m-%d %H:%M:%S") + self.expire_date = datetime.strptime( + expire_date, "%Y-%m-%d %H:%M:%S") except: pass @@ -253,6 +319,8 @@ class ExternalImageInfoItem(InfoItem): class InfoInstance(models.Model): + """Class for Info instance in Infoscreen.""" + rotation = models.ForeignKey('Rotation', related_name='instances') duration = models.FloatField(default=15.0) # seconds # generic relation to some kind of InfoItem @@ -262,6 +330,7 @@ class InfoInstance(models.Model): @classmethod def create_from_dict(cls, d): + """Convert given dict to model.""" try: rotation = Rotation.objects.get(pk=int(d["rotation_id"])) ct = ContentType.objects.get_for_id(int(d["item_type"])) @@ -279,6 +348,7 @@ class InfoInstance(models.Model): raise RuntimeError("error while adding instance to db") def get_dict(self): + """Convert django model to dict and return it.""" return { 'id': self.id, 'item': self.item.get_dict(), @@ -286,17 +356,24 @@ class InfoInstance(models.Model): } def __str__(self): - return "{}: {} ({}s)".format(self.rotation.name, self.item.name, self.duration) + """Return model name.""" + return "{}: {} ({}s)".format( + self.rotation.name, self.item.name, self.duration) class Rotation(models.Model): + """Class for rotation model.""" + name = models.CharField(max_length=255) def get_dict(self): - # exclude expired items from rotation (note: using tricky syntax to avoid excluding items with no expire_date) + """Convert django model to dict and return it.""" + # exclude expired items from rotation (note: using tricky syntax + # to avoid excluding items with no expire_date) now = timezone.now() instances = self.instances.all() - filtered = filter(lambda i: (i.item.expire_date or now) >= now, list(instances)) + filtered = filter(lambda i: (i.item.expire_date or now) + >= now, list(instances)) instance_list = list(map(lambda i: i.get_dict(), filtered)) return { @@ -306,29 +383,32 @@ class Rotation(models.Model): } def get_list(self): + """Return list containing infoitem data.""" return { 'id': self.id, 'name': self.name, } def __str__(self): + """Return model name.""" return self.name class ImageUploadForm(forms.Form): - ''' - Form used to handle imageuploads to - infoscreen app - ''' + """Form used to handle imageuploads to infoscreen app.""" + name = forms.CharField() image = forms.ImageField() class UploadFileForm(forms.Form): + """Form used for uploading file.""" name = forms.CharField() video = forms.FileField() class HSLDataModel(models.Model): + """Model representing HSL data.""" + data = models.TextField(default="", editable=False) diff --git a/infoscreen/tests.py b/infoscreen/tests.py index 561adba..69ee0fa 100644 --- a/infoscreen/tests.py +++ b/infoscreen/tests.py @@ -1,41 +1,37 @@ +"""File containing Infoscreen tests.""" + from django.test import TestCase from infoscreen.models import Rotation from infoscreen.models import SossoInfoItem -from django.http import HttpRequest, HttpResponse +from django.http import HttpRequest import infoscreen.views class InfoscreenTestCase(TestCase): - ''' - Test cases for testing infoscreen methods - ''' + """Test cases for testing infoscreen methods.""" def setUp(self): - ''' - Create some dummy models - ''' + """Create some dummy models.""" Rotation.objects.create(name="test_rot") SossoInfoItem.objects.create() def test_rotation_created(self): - ''' - Check if the dummy model actually exists - ''' + """Check if the dummy model actually exists.""" rot = Rotation.objects.get(name="test_rot") self.assertIsNotNone(rot) def test_sosso_infoitem_created(self): - ''' - Check if the dummy model actually exists - ''' + """Check if the dummy model actually exists.""" item = SossoInfoItem.objects.get() self.assertIsNotNone(item) def test_get_infoitems(self): - ''' - Check if infoItems returns a response with non-zero content length - That would mean that something meaningful has been included in the response - ''' + """ + Check if infoItems returns a response with non-zero content length. + + That would mean that something meaningful has been included + in the response. + """ req = HttpRequest() resp = infoscreen.views.info_items(req) content = resp.content.decode('utf-8') diff --git a/infoscreen/urls.py b/infoscreen/urls.py index 1548bde..5a16fe7 100644 --- a/infoscreen/urls.py +++ b/infoscreen/urls.py @@ -1,3 +1,5 @@ +"""File containing infoscreen urls.""" + from django.conf.urls import url from infoscreen.views import index diff --git a/infoscreen/views.py b/infoscreen/views.py index 055cee6..d2347f2 100644 --- a/infoscreen/views.py +++ b/infoscreen/views.py @@ -1,3 +1,5 @@ +"""File containing infoscreen views.""" + from django.shortcuts import render from django.http import HttpResponse, HttpResponseBadRequest from django.views.decorators.csrf import ensure_csrf_cookie @@ -13,7 +15,8 @@ import threading import requests from infoscreen.models import Rotation, InfoItem, InfoInstance -from infoscreen.models import ABBInfoItem, ExternalImageInfoItem, ImageInfoItem, SossoInfoItem, HslInfoItem +from infoscreen.models import (ABBInfoItem, ExternalImageInfoItem, + ImageInfoItem, SossoInfoItem, HslInfoItem) from infoscreen.models import EventInfoItem from infoscreen.models import ExternalWebsiteInfoItem from infoscreen.models import ImageUploadForm @@ -24,15 +27,18 @@ from infoscreen.hsl_fetcher import HSLFetcher def index(request, idx, *args, **kwargs): + """Render infoscreen index page.""" return render(request, 'infoscreen_index.html', {'rotation': idx}) @permission_required('infoscreen.change_infoinstance', login_url='/login') def admin(request, *args, **kwargs): + """Render infoscreen admin page.""" return render(request, 'infoscreen_admin.html', {}) def default(request, *args, **kwargs): + """Try getting first rotation item.""" try: first = Rotation.objects.all()[0].id except: @@ -41,11 +47,14 @@ def default(request, *args, **kwargs): def get_apy_json(request): - return HttpResponse(requests.get("https://api-diilikone.apy.fi/deals/top-groups").text) + """Render APY diilikone page.""" + return HttpResponse( + requests.get("https://api-diilikone.apy.fi/deals/top-groups").text) @require_http_methods(["GET"]) def rotation(request, idx, *args, **kwargs): + """Get rotation.""" try: rotation = Rotation.objects.get(pk=idx) except Rotation.DoesNotExist: @@ -57,6 +66,7 @@ def rotation(request, idx, *args, **kwargs): def create_item_generator(model): + """Create Infoscreen item generator.""" @ensure_csrf_cookie @require_http_methods(["POST"]) @permission_required('infoscreen.change_infoinstance', login_url='/login') @@ -64,16 +74,19 @@ def create_item_generator(model): try: data = json.loads(request.body.decode("utf-8")) except ValueError: - return HttpResponseBadRequest('{"status":"failure","error":"invalid json supplied"}') + return HttpResponseBadRequest( + '{"status":"failure","error":"invalid json supplied"}') try: model.create_from_dict(data) return HttpResponse('{"status":"success"}') except RuntimeError as e: - return HttpResponseBadRequest(json.dumps({"status": "failure", "error": str(e)})) + return HttpResponseBadRequest( + json.dumps({"status": "failure", "error": str(e)})) return create_item def delete_item_generator(model): + """Delete Infoscreen item generator.""" @ensure_csrf_cookie @require_http_methods(["DELETE"]) @permission_required('infoscreen.change_infoinstance', login_url='/login') @@ -100,6 +113,7 @@ def delete_item_generator(model): @permission_required('infoscreen.change_infoinstance', login_url='/login') @require_http_methods(["DELETE"]) def delete_info_item(request, *args, **kwargs): + """Delete info item.""" type_id = kwargs.pop("type_id", 0) idx = kwargs.pop("idx", 0) if True: @@ -120,12 +134,14 @@ def delete_info_item(request, *args, **kwargs): @require_http_methods(["GET"]) def rotations(request, *args, **kwargs): + """Return rotation lists.""" rotations = list(map(lambda r: r.get_list(), Rotation.objects.all())) return HttpResponse(json.dumps(rotations)) @require_http_methods(["GET"]) def info_types(request, *args, **kwargs): + """Return info item types.""" types = [] classes = InfoItem.get_subclasses() for c in classes: @@ -137,6 +153,7 @@ def info_types(request, *args, **kwargs): def info_items(request, *args, **kwargs): + """Return Infoscreen items.""" items = [] classes = InfoItem.get_subclasses() for c in classes: @@ -149,6 +166,7 @@ def info_items(request, *args, **kwargs): @ensure_csrf_cookie @permission_required('infoscreen.change_infoinstance', login_url='/login') def create_image_item(request, *args, **kwargs): + """Create image Infoscreen item.""" form = ImageUploadForm(request.POST, request.FILES) if not form.is_valid(): return HttpResponseBadRequest('{"status": "failure",' @@ -164,6 +182,7 @@ def create_image_item(request, *args, **kwargs): @ensure_csrf_cookie @permission_required('infoscreen.change_infoinstance', login_url='/login') def create_video_item(request, *args, **kwargs): + """Create video Infoscreen item.""" form = UploadFileForm(request.POST, request.FILES) print(form.errors) print("hurdurr") @@ -181,6 +200,7 @@ def create_video_item(request, *args, **kwargs): @ensure_csrf_cookie @permission_required('infoscreen.add_rotation', login_url='/login') def create_rotation(request, *args, **kwargs): + """Create rotation.""" try: data = json.loads(request.body.decode("utf-8")) except: @@ -191,7 +211,8 @@ def create_rotation(request, *args, **kwargs): Rotation.objects.create(name=name) resp = HttpResponse(status=200) except: - resp = HttpResponse('{"error" : "could not create rotation!"}', status=400) + resp = HttpResponse( + '{"error" : "could not create rotation!"}', status=400) return resp @@ -200,7 +221,7 @@ def create_rotation(request, *args, **kwargs): @ensure_csrf_cookie @permission_required('infoscreen.delete_rotation', login_url='/login') def delete_rotation(request, *args, **kwargs): - + """Delete rotation.""" id = kwargs.pop("id", 0) logging.warning("Deleting rotation with id={}".format(id)) @@ -208,13 +229,15 @@ def delete_rotation(request, *args, **kwargs): Rotation.objects.filter(id=id).delete() resp = HttpResponse(status=200) except: - resp = HttpResponse('{"error" : "could not delete rotation!"}', status=400) + resp = HttpResponse( + '{"error" : "could not delete rotation!"}', status=400) return resp @require_http_methods(["GET"]) def hsl_timetable_settings(request, *args, **kwargs): + """Set HSL timetable settings.""" d = {"departure_threshold": settings.HSL_DEPARTURE_THRESHOLD, "hurry_threshold": settings.HSL_HURRY_THRESHOLD} resp = json.dumps(d) @@ -223,7 +246,7 @@ def hsl_timetable_settings(request, *args, **kwargs): @require_http_methods(["GET"]) def CurrentHSLView(request, *args, **kwargs): - + """Get HSL data and return it.""" fetcher = HSLFetcher() fetcherThread = threading.Thread(target=fetcher.fetch_if_needed, args=[]) fetcherThread.setDaemon(False) @@ -231,7 +254,9 @@ def CurrentHSLView(request, *args, **kwargs): data = HSLDataModel.objects.all() if len(data) < 1: - return HttpResponse('{"error" : "Could not find timetables from database."}', status=500) + return HttpResponse( + '{"error" : "Could not find timetables from database."}', + status=500) return HttpResponse(data[len(data) - 1].data, status=200) From 08710b3705cdc018c4a8b9622f2c11f5a9f5aae4 Mon Sep 17 00:00:00 2001 From: henu Date: Wed, 20 Sep 2017 21:00:58 +0300 Subject: [PATCH 15/20] Fix members pep8 and add docstrings --- members/admin.py | 2 + members/apps.py | 4 + members/forms.py | 11 ++ members/models.py | 44 +++++--- members/serializers.py | 9 +- members/tables.py | 23 +++- members/tests.py | 12 ++- members/throttles.py | 6 ++ members/urls.py | 8 +- members/views.py | 235 +++++++++++++++++++++++++++++++---------- 10 files changed, 271 insertions(+), 83 deletions(-) diff --git a/members/admin.py b/members/admin.py index b150c1d..6607a19 100644 --- a/members/admin.py +++ b/members/admin.py @@ -1,3 +1,5 @@ +"""Admin site registers for Members app.""" + from django.contrib import admin from members.models import Member, Request, Payment, MemberConflict diff --git a/members/apps.py b/members/apps.py index f773ade..77801d7 100644 --- a/members/apps.py +++ b/members/apps.py @@ -1,5 +1,9 @@ +"""App configurations for members app.""" + from django.apps import AppConfig class MembersConfig(AppConfig): + """Class for Members app configurations.""" + name = 'members' diff --git a/members/forms.py b/members/forms.py index a6e47ce..1ac3a4d 100644 --- a/members/forms.py +++ b/members/forms.py @@ -1,3 +1,5 @@ +"""File containing member forms.""" + from django import forms from django.utils.translation import ugettext_lazy as _ @@ -5,15 +7,21 @@ from members.models import Member, Payment, Request class MemberForm(forms.ModelForm): + """Member model form.""" class Meta: + """Meta for Member model form.""" + model = Member fields = ['first_name', 'last_name', 'email', 'AYY', 'jas', 'POR'] class PaymentForm(forms.ModelForm): + """Payment model form.""" class Meta: + """Meta for Payment model form.""" + model = Payment fields = ['date', 'source', 'member'] labels = { @@ -22,7 +30,10 @@ class PaymentForm(forms.ModelForm): class ApplicationForm(forms.ModelForm): + """Member application model form.""" class Meta: + """Meta for application model form.""" + model = Request fields = ['first_name', 'last_name', 'email', 'AYY', 'jas', 'POR'] diff --git a/members/models.py b/members/models.py index a8c46d0..c845aa2 100644 --- a/members/models.py +++ b/members/models.py @@ -1,31 +1,36 @@ +"""File containing Members app models.""" + from django.db import models from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from datetime import datetime import csv -import logging class BaseMember(models.Model): - ''' - Base model for member. - ''' + """Abstract base model for member.""" + first_name = models.CharField(_("First name"), max_length=127) last_name = models.CharField(_("Last name"), max_length=127) email = models.EmailField(_("Email")) - POR = models.CharField(_("Place of residence"), max_length=255) # place of residence + POR = models.CharField(_("Place of residence"), + max_length=255) # place of residence AYY = models.BooleanField(_("AYY"), default=False) jas = models.BooleanField(_("JAS"), default=False) class Meta: + """Meta for base member model.""" + abstract = True def __str__(self): + """Return member last name, first name and email.""" return "{} {}, {}".format(self.last_name, self.first_name, self.email) @staticmethod def from_csv(data): + """Construct member model from csv data.""" print("Imported CSV data: {}".format(data)) clean_data = data.strip().split('\n') csv_reader = csv.reader(clean_data) @@ -50,6 +55,7 @@ class BaseMember(models.Model): return True def as_array(self): + """Return member model as an array.""" return [ self.first_name, self.last_name, @@ -61,21 +67,20 @@ class BaseMember(models.Model): class Request(BaseMember): - ''' - Member request model represents one member request. - ''' + """Member request model represents one member request.""" + submitted = models.DateTimeField(_('Submitted'), default=timezone.now) def to_member(self): + """Convert array to member model.""" member = Member.from_array(self.as_array()) return member class Payment(models.Model): - ''' - Payment model representing one payment event - ''' + """Payment model representing one payment event.""" + date = models.DateTimeField(_('Date'), default=datetime.now) source = models.CharField(_('Source'), choices=[ ('AYY', _('AYY')), @@ -90,16 +95,17 @@ class Payment(models.Model): related_name='payments') def __str__(self): + """Return payment id and date.""" return 'Payment no. {}, {}'.format(self.id, str(self.date)) class Member(BaseMember): - ''' - Member model represets one member on the registry. - ''' + """Member model represets one member on the registry.""" + created = models.DateTimeField(_('Created'), default=datetime.now) def last_paid(self): + """Return member's last payment.""" try: payments = Payment.objects.filter(member=self) latest = payments.latest('date') @@ -110,6 +116,7 @@ class Member(BaseMember): @staticmethod def from_array(array): + """Create member from array.""" if len(array) != 6: raise Exception("Invalid array length for member instantiation") @@ -124,16 +131,21 @@ class Member(BaseMember): class MemberConflict(models.Model): + """Model representing member conflict situation.""" - first_member = models.ForeignKey('Member', related_name='%(class)s_first_member') - second_member = models.ForeignKey('Member', related_name='%(class)s_second_member') + first_member = models.ForeignKey( + 'Member', related_name='%(class)s_first_member') + second_member = models.ForeignKey( + 'Member', related_name='%(class)s_second_member') @property def first_member_form(self): + """Get first member form.""" return MemberForm(instance=self.first_member) @property def second_member_form(self): + """Get second member form.""" return MemberForm(instance=self.second_member) diff --git a/members/serializers.py b/members/serializers.py index 8273291..9d1ff21 100644 --- a/members/serializers.py +++ b/members/serializers.py @@ -1,10 +1,17 @@ +"""File containing member serializers.""" + from rest_framework import serializers from members.models import Member class MemberSerializer(serializers.ModelSerializer): + """Model serializer for member.""" + paid = serializers.DateTimeField(source='last_paid') class Meta: + """Meta of member serializer.""" + model = Member - fields = ('id', 'first_name', 'last_name', 'email', 'POR', 'AYY', 'jas', 'created', 'paid') + fields = ('id', 'first_name', 'last_name', 'email', + 'POR', 'AYY', 'jas', 'created', 'paid') diff --git a/members/tables.py b/members/tables.py index 1910c2d..a6274ec 100644 --- a/members/tables.py +++ b/members/tables.py @@ -1,3 +1,5 @@ +"""File containing member application django tables.""" + import django_tables2 as tables from django.utils.translation import ugettext as _ @@ -5,43 +7,56 @@ from members.models import Member, Payment, Request class MemberTable(tables.Table): + """Table for member.""" - last_paid = tables.DateTimeColumn(accessor='last_paid', verbose_name=_('Last paid')) + last_paid = tables.DateTimeColumn( + accessor='last_paid', verbose_name=_('Last paid')) options = tables.TemplateColumn( - '' + + ('') + _('Edit') + '', verbose_name=_('Options') ) class Meta: + """Meta for member table.""" + model = Member class PaymentTable(tables.Table): + """Table for payments.""" member = tables.Column(accessor='member', verbose_name=_('Member')) options = tables.TemplateColumn( - '' + + ('') + _('Edit') + '', verbose_name=_('Options') ) class Meta: + """Meta for payment table.""" + model = Payment class RequestTable(tables.Table): + """Table for member applications.""" options = tables.TemplateColumn( - '' + + ('') + _('Edit') + '', verbose_name=_('Options') ) class Meta: + """Meta for request table.""" + model = Request diff --git a/members/tests.py b/members/tests.py index 025c3d9..3987d38 100644 --- a/members/tests.py +++ b/members/tests.py @@ -1,28 +1,34 @@ +"""File containing Member app tests.""" + from django.test import TestCase, Client from django.contrib.auth.models import User -import time - from members.models import Member class MemberRegisterTestCase(TestCase): + """Tests member registration.""" def setUp(self): + """Setup testing environment by creating member and admin.""" memb = Member.objects.create(first_name="Tidus", last_name="Tester") - test_admin = User.objects.create_superuser('test_admin', 'myemail@test.com', 'password123') + test_admin = User.objects.create_superuser( + 'test_admin', 'myemail@test.com', 'password123') self.c = Client() def test_member_created(self): + """Test member creation.""" exists = Member.objects.filter(first_name="Tidus").exists() self.assertTrue(exists) def test_import_csv_single_line(self): + """Test csv import only with single line in csv file.""" data = 'Teppo, Tulppu, teppo@tulppu.fi, Ankkalinna, 0, 0' response = self.c.post('/members/import_csv', {'textarea': data}) self.assertIn(response.status_code, [200, 302]) def test_import_csv_multi_line(self): + """Test csv import with multilined csv.""" data = ('Teppo, Tulppu, teppo@tulppu.fi, Ankkalinna, 0, 0\n' 'Reiska, Remontti, remontti@reiska.fi, Värisilmä, 1, 1') diff --git a/members/throttles.py b/members/throttles.py index 0c1e17b..3004a82 100644 --- a/members/throttles.py +++ b/members/throttles.py @@ -1,9 +1,15 @@ +"""File containing throttle rates for API.""" + from rest_framework.throttling import UserRateThrottle class BurstRateThrottle(UserRateThrottle): + """Class for burst rate throttle.""" + scope = 'burst' class SustainedRateThrottle(UserRateThrottle): + """Class for sustained rate throttle.""" + scope = 'sustained' diff --git a/members/urls.py b/members/urls.py index 9738af6..ad35248 100644 --- a/members/urls.py +++ b/members/urls.py @@ -1,3 +1,5 @@ +"""File containing Member application URLs.""" + from django.conf.urls import url from django.views.generic.base import RedirectView @@ -32,7 +34,8 @@ from members.views import application_form_success # from members.views import validateEmail, validate_success, validate_fail -favicon_view = RedirectView.as_view(url='static/img/favicon.ico', permanent=True) +favicon_view = RedirectView.as_view( + url='static/img/favicon.ico', permanent=True) urlpatterns = [ @@ -75,7 +78,8 @@ urlpatterns = [ url(r'^application/success$', application_form_success), # delete confirmation view for applications - url(r'^delete_application_confirm/(?P\d+)$', application_delete_confirm), + url(r'^delete_application_confirm/(?P\d+)$', + application_delete_confirm), # list all payment events url(r'^payments$', payment_list), diff --git a/members/views.py b/members/views.py index da03f3d..ca976c8 100644 --- a/members/views.py +++ b/members/views.py @@ -1,3 +1,5 @@ +"""File containing Members application views.""" + from django.shortcuts import render from django.contrib.auth.decorators import permission_required from django.views.decorators.http import require_http_methods @@ -33,13 +35,12 @@ from members.tables import MemberTable, PaymentTable, RequestTable def validate_recaptcha(response): - ''' - Recaptcha is used in member applications + """ + Recaptcha is used in member applications. :param response: :return: Boolean, success or not - ''' - + """ values = { 'secret': settings.GOOGLE_RECAPTCHA_SECRET_KEY, 'response': response, @@ -56,6 +57,7 @@ def validate_recaptcha(response): def send_mail_wrapper(subject, message): + """Call send_mail function.""" send_mail(subject, message, 'no-reply@sahkoinsinoorikilta.fi', @@ -64,7 +66,9 @@ def send_mail_wrapper(subject, message): def convert_table_to_html(table, request): - ''' + """ + Convert table to html. + This is a horrible hack for converting a table object to raw html. Even with extensive research I wasn't able to find a way to add a path prefix "e.g. /members/list" to the query strings "e.g. ?sort=foo", so I @@ -76,7 +80,7 @@ def convert_table_to_html(table, request): :param table: Table object from members.tables :param request: HttpRequest :return: Raw html string - ''' + """ table_as_html = table.as_html(request) path = request.path @@ -88,6 +92,7 @@ def convert_table_to_html(table, request): @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def member_list(request, *args, **kwargs): + """Render members list.""" members = Member.objects.all() table = MemberTable(members, @@ -111,6 +116,7 @@ def member_list(request, *args, **kwargs): @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def member_add(request, *args, **kwargs): + """Render add member page.""" form = MemberForm() return render(request, 'member_add.html', {'form': form}) @@ -119,19 +125,23 @@ def member_add(request, *args, **kwargs): @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def member_delete_confirm(request, *args, **kwargs): + """Render member deletion confirmation page.""" i = kwargs.pop('index', None) if i is None: - return render(request, 'error.html', {'error': _('No member id specified')}) + return render(request, 'error.html', + {'error': _('No member id specified')}) else: member = Member.objects.get(id=i) form = MemberForm(instance=member) - return render(request, 'member_delete_confirm.html', {'member_id': i, 'form': form}) + return render(request, 'member_delete_confirm.html', + {'member_id': i, 'form': form}) @ensure_csrf_cookie @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def member_add_many(request, *args, **kwargs): + """Render add multiple members page.""" return render(request, 'member_add_many.html', {}) @@ -139,15 +149,18 @@ def member_add_many(request, *args, **kwargs): @require_http_methods(["POST"]) @permission_required('members.change_member', login_url='/login') def member_submit(request, *args, **kwargs): + """Add member based on data gained from member form.""" form = MemberForm(request.POST) if form.is_valid(): form.save() - logging.info("Saved new member to member register with the following info: {}".format(form)) + logging.info("Saved new member to member register" + "with the following info: {}".format(form)) notification = "{} {} {}.".format(_("Successfully added member"), form.cleaned_data['last_name'], form.cleaned_data['first_name']) - return HttpResponseRedirect('/members/list?notification={}'.format(html.escape(notification))) + return HttpResponseRedirect( + '/members/list?notification={}'.format(html.escape(notification))) else: return render(request, 'error.html', {'error': form.errors}) @@ -156,6 +169,7 @@ def member_submit(request, *args, **kwargs): @require_http_methods(["POST"]) @permission_required('members.change_member', login_url='/login') def member_update(request, *args, **kwargs): + """Update member information.""" form = MemberForm(request.POST) if form.is_valid(): id = request.POST['id'] @@ -163,51 +177,68 @@ def member_update(request, *args, **kwargs): form = MemberForm(request.POST, instance=member) form.save() - logging.info("Updated member in member register with the following info: {}".format(form)) + logging.info( + "Updated member in member register with the following info: {}" + .format(form)) notification = "{} {} {}.".format(_("Successfully updated member"), member.last_name, member.first_name) - return HttpResponseRedirect('/members/list?notification={}'.format(html.escape(notification))) + return HttpResponseRedirect( + '/members/list?notification={}'.format(html.escape(notification))) else: - return render(request, 'error.html', {'error': _('Could not update member object')}) + return render( + request, + 'error.html', + {'error': _('Could not update member object')}) @ensure_csrf_cookie @require_http_methods(["POST"]) @permission_required('members.change_member', login_url='/login') def member_delete(request, *args, **kwargs): + """Delete member.""" try: id = request.POST['id'] except KeyError: - return render(request, 'error.html', {'error': _('No member id specified')}) + return render(request, + 'error.html', {'error': _('No member id specified')}) try: member = Member.objects.get(id=id) notification = "{} {} {}.".format(_("Successfully deleted member"), member.last_name, member.first_name) member.delete() - logging.info("Delete member in member register with the following id: {}".format(id)) - return HttpResponseRedirect('/members/list?notification={}'.format(html.escape(notification))) + logging.info( + "Delete member in member register with the following id: {}" + .format(id)) + return HttpResponseRedirect( + '/members/list?notification={}'.format(html.escape(notification))) except: - return render(request, 'error.html', {'error': _('Could not delete member object')}) + return render(request, + 'error.html', + {'error': _('Could not delete member object')}) @ensure_csrf_cookie @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def member_edit(request, *args, **kwargs): + """Edit member information.""" i = kwargs.pop('index', None) if i is None: - return render(request, 'error.html', {'error': _('No member id specified')}) + return render( + request, 'error.html', {'error': _('No member id specified')}) else: member = Member.objects.get(id=i) form = MemberForm(instance=member) - return render(request, 'member_edit.html', {'member_id': i, 'form': form}) + return render( + request, 'member_edit.html', {'member_id': i, 'form': form}) @ensure_csrf_cookie @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def application_list(request, *args, **kwargs): + """List member applications not yet processed.""" applications = Request.objects.all() application_count = len(applications) table = RequestTable(applications, @@ -229,19 +260,25 @@ def application_list(request, *args, **kwargs): @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def application_edit(request, *args, **kwargs): + """Edit member request information.""" i = kwargs.pop('index', None) if i is None: - return render(request, 'error.html', {'error': _('No application id specified')}) + return render( + request, 'error.html', {'error': _('No application id specified')}) else: application = Request.objects.get(id=i) form = ApplicationForm(instance=application) - return render(request, 'application_edit.html', {'application_id': i, 'form': form}) + return render( + request, + 'application_edit.html', + {'application_id': i, 'form': form}) @ensure_csrf_cookie @require_http_methods(["POST"]) @permission_required('members.change_member', login_url='/login') def application_accept(request, *args, **kwargs): + """Accept application.""" form = ApplicationForm(request.POST) if form.is_valid(): id = request.POST['id'] @@ -251,52 +288,75 @@ def application_accept(request, *args, **kwargs): member.save() application.delete() - logging.info("Accepted application in member register with the following info: {}".format(form)) - notification = "{} {}.".format(_("Successfully accepted application"), str(application)) - return HttpResponseRedirect('/members/list?notification={}'.format(html.escape(notification))) + logging.info( + "Accepted application in member " + "register with the following info: {}" + .format(form)) + notification = "{} {}.".format(_("Successfully accepted application"), + str(application)) + return HttpResponseRedirect( + '/members/list?notification={}'.format(html.escape(notification))) else: - return render(request, 'error.html', {'error': _('Could not accept application object')}) + return render(request, + 'error.html', + {'error': _('Could not accept application object')}) @ensure_csrf_cookie @require_http_methods(["POST"]) @permission_required('members.change_member', login_url='/login') def application_delete(request, *args, **kwargs): + """Delete member application.""" try: id = request.POST['id'] except KeyError: - return render(request, 'error.html', {'error': _('No application id specified')}) + return render( + request, 'error.html', {'error': _('No application id specified')}) try: application = Request.objects.get(id=id) - notification = "{} {}.".format(_("Successfully deleted application"), str(application)) + notification = "{} {}.".format(_("Successfully deleted application"), + str(application)) application.delete() - logging.info("Delete application in member register with the following id: {}".format(id)) - return HttpResponseRedirect('/members/applications?notification={}'.format(html.escape(notification))) + logging.info( + "Delete application in member register with the following id: {}" + .format(id)) + return HttpResponseRedirect( + '/members/applications?notification={}' + .format(html.escape(notification))) except: - return render(request, 'error.html', {'error': _('Could not delete application object')}) + return render(request, + 'error.html', + {'error': _('Could not delete application object')}) @ensure_csrf_cookie @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def application_delete_confirm(request, *args, **kwargs): + """Confirm application deletion.""" i = kwargs.pop('index', None) if i is None: - return render(request, 'error.html', {'error': _('No application id specified')}) + return render(request, + 'error.html', + {'error': _('No application id specified')}) else: application = Request.objects.get(id=i) form = ApplicationForm(instance=application) - return render(request, 'application_delete_confirm.html', {'application_id': i, 'form': form}) + return render(request, + 'application_delete_confirm.html', + {'application_id': i, 'form': form}) @ensure_csrf_cookie def application_form(request, *args, **kwargs): + """Render member application form.""" return render(request, 'application_index.html', {}) @ensure_csrf_cookie def application_form_success(request, *args, **kwargs): + """Render application Successfully sent page.""" return render(request, 'application_success.html', {}) @@ -304,6 +364,7 @@ def application_form_success(request, *args, **kwargs): @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def payment_list(request, *args, **kwargs): + """Render list of payments.""" payments = Payment.objects.all() table = PaymentTable(payments, @@ -326,6 +387,7 @@ def payment_list(request, *args, **kwargs): @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def payment_add(request, *args, **kwargs): + """Render add payment form.""" form = PaymentForm() return render(request, 'payment_add.html', {'form': form}) @@ -334,13 +396,19 @@ def payment_add(request, *args, **kwargs): @require_http_methods(["POST"]) @permission_required('members.change_member', login_url='/login') def payment_submit(request, *args, **kwargs): + """Submit payment.""" form = PaymentForm(request.POST) if form.is_valid(): form.save() - logging.info("Saved new payment to member register with the following info: {}".format(form)) - notification = "{} {}.".format(_("Successfully added payment for member"), - form.cleaned_data['member']) - return HttpResponseRedirect('/members/payments?notification={}'.format(html.escape(notification))) + logging.info( + "Saved new payment to member register with the following info: {}" + .format(form)) + notification = "{} {}.".format( + _("Successfully added payment for member"), + form.cleaned_data['member']) + return HttpResponseRedirect( + '/members/payments?notification={}' + .format(html.escape(notification))) else: return render(request, 'error.html', {'error': form.errors}) @@ -349,51 +417,71 @@ def payment_submit(request, *args, **kwargs): @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def payment_edit(request, *args, **kwargs): + """Edit payment.""" i = kwargs.pop('index', None) if i is None: - return render(request, 'error.html', {'error': _('No payment id specified')}) + return render(request, + 'error.html', + {'error': _('No payment id specified')}) else: payment = Payment.objects.get(id=i) form = PaymentForm(instance=payment) - return render(request, 'payment_edit.html', {'payment_id': i, 'form': form}) + return render(request, + 'payment_edit.html', + {'payment_id': i, 'form': form}) @ensure_csrf_cookie @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def payment_delete_confirm(request, *args, **kwargs): + """Render payment delete confirmation page.""" i = kwargs.pop('index', None) if i is None: - return render(request, 'error.html', {'error': _('No payment id specified')}) + return render(request, + 'error.html', + {'error': _('No payment id specified')}) else: payment = Payment.objects.get(id=i) form = PaymentForm(instance=payment) - return render(request, 'payment_delete_confirm.html', {'payment_id': i, 'form': form}) + return render(request, + 'payment_delete_confirm.html', + {'payment_id': i, 'form': form}) @ensure_csrf_cookie @require_http_methods(["POST"]) @permission_required('members.change_member', login_url='/login') def payment_delete(request, *args, **kwargs): + """Delete payment.""" try: id = request.POST['id'] except KeyError: - return render(request, 'error.html', {'error': _('No payment id specified')}) + return render(request, + 'error.html', + {'error': _('No payment id specified')}) try: payment = Payment.objects.get(id=id) - notification = "{} {}.".format(_("Successfully deleted payment"), str(payment)) + notification = "{} {}.".format( + _("Successfully deleted payment"), str(payment)) payment.delete() - logging.info("Delete payment '{}' in member register".format(str(payment))) - return HttpResponseRedirect('/members/payments?notification={}'.format(html.escape(notification))) + logging.info( + "Delete payment '{}' in member register".format(str(payment))) + return HttpResponseRedirect( + '/members/payments?notification={}' + .format(html.escape(notification))) except: - return render(request, 'error.html', {'error': _('Could not delete payment object')}) + return render(request, + 'error.html', + {'error': _('Could not delete payment object')}) @ensure_csrf_cookie @require_http_methods(["POST"]) @permission_required('members.change_member', login_url='/login') def payment_update(request, *args, **kwargs): + """Update payment information.""" form = PaymentForm(request.POST) if form.is_valid(): id = request.POST['id'] @@ -401,17 +489,25 @@ def payment_update(request, *args, **kwargs): form = PaymentForm(request.POST, instance=payment) form.save() - logging.info("Updated member in member register with the following info: {}".format(form)) - notification = "{} {}.".format(_("Successfully updated payment"), str(payment)) - return HttpResponseRedirect('/members/payments?notification={}'.format(html.escape(notification))) + logging.info( + "Updated member in member register with the following info: {}" + .format(form)) + notification = "{} {}.".format( + _("Successfully updated payment"), str(payment)) + return HttpResponseRedirect( + '/members/payments?notification={}' + .format(html.escape(notification))) else: - return render(request, 'error.html', {'error': _('Could not update payment object')}) + return render(request, + 'error.html', + {'error': _('Could not update payment object')}) @ensure_csrf_cookie @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def settings_page(request, *args, **kwargs): + """Render member app settings page.""" return render(request, 'settings.html', {}) @@ -419,30 +515,40 @@ def settings_page(request, *args, **kwargs): @require_http_methods(["POST"]) @permission_required('members.change_member', login_url='/login') def import_csv(request, *args, **kwargs): + """Get csv data imported to page and create members based on that.""" try: data = request.POST['textfield'] except: - return render(request, 'error.html', {'error': _('Missing "textfield" POST request field')}) + return render(request, + 'error.html', + {'error': _('Missing "textfield" POST request field')}) success = Member.from_csv(data) if success: logging.info('Imported CSV data:\n'.format(data)) - notification = "{}.".format(_("Successfully imported multiple members")) - return HttpResponseRedirect('/members/list?notification={}'.format(html.escape(notification))) + notification = "{}.".format( + _("Successfully imported multiple members")) + return HttpResponseRedirect( + '/members/list?notification={}' + .format(html.escape(notification))) else: - return render(request, 'error.html', {'error': _('Failed to import members')}) + return render(request, + 'error.html', + {'error': _('Failed to import members')}) @ensure_csrf_cookie @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def export_csv(request, *args, **kwargs): + """Export members as csv.""" response = HttpResponse() response['Content-type'] = 'text/csv' response['Accept'] = 'text/csv' response['Content-Disposition'] = 'filename; filename=members.csv' writer = csv.writer(response, csv.excel) - response.write(u'\ufeff'.encode('utf8')) # BOM (optional...Excel needs it to open UTF-8 file properly) + # BOM (optional...Excel needs it to open UTF-8 file properly) + response.write(u'\ufeff'.encode('utf8')) for obj in Member.objects.all(): data = obj.as_array() field_list = map(lambda d: str(d), data) @@ -456,6 +562,7 @@ def export_csv(request, *args, **kwargs): @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def member_duplicates(request, *args, **kwargs): + """Check for duplicate members.""" conflicts = MemberConflict.objects.all() context = { 'conflicts': conflicts @@ -468,13 +575,19 @@ def member_duplicates(request, *args, **kwargs): @require_http_methods(["POST"]) @permission_required('members.change_member', login_url='/login') def resolve_conflict(request, *args, **kwargs): + """Resolve duplicate member conflict.""" action = request.POST.get('action', None) if action not in ['first', 'second', 'both']: - return render(request, 'error.html', {'error': '{}: {}'.format(('Incorrect action value'), action)}) + return render(request, + 'error.html', + {'error': '{}: {}'.format(('Incorrect action value'), + action)}) id = request.POST.get('id', None) if id is None: - return render(request, 'error.html', {'error': '{}: {}'.format(('Incorrect id value'), id)}) + return render(request, + 'error.html', + {'error': '{}: {}'.format(('Incorrect id value'), id)}) conflict = MemberConflict.objects.get(id=id) first_member = conflict.first_member @@ -497,10 +610,12 @@ def resolve_conflict(request, *args, **kwargs): return HttpResponseRedirect('/members/duplicates') else: notification = _('Successfully resolved all member conflicts.') - return HttpResponseRedirect('/members/list?notification={}'.format(html.escape(notification))) + return HttpResponseRedirect( + '/members/list?notification={}'.format(html.escape(notification))) def send_mail_wrapper(subject, message, email_to): + """Send mail to default email.""" send_mail(subject, message, settings.DEFAULT_EMAIL_FROM, @@ -510,6 +625,7 @@ def send_mail_wrapper(subject, message, email_to): @receiver(post_save, sender=Request) def email_on_request(sender, instance, created, **kwargs): + """Send email validation.""" if not settings.ENABLE_AUTOMATIC_EMAILS: return @@ -524,6 +640,7 @@ def email_on_request(sender, instance, created, **kwargs): @receiver(post_save, sender=Member) def email_on_accept(sender, instance, created, **kwargs): + """Send email to accepted member.""" if not settings.ENABLE_AUTOMATIC_EMAILS: return @@ -537,6 +654,7 @@ def email_on_accept(sender, instance, created, **kwargs): def check_for_duplicates(instance): + """Check for member duplicates.""" name_candidates = Member.objects.filter(first_name=instance.first_name, last_name=instance.last_name) email_candidates = Member.objects.filter(email=instance.email) @@ -552,11 +670,14 @@ def check_for_duplicates(instance): @receiver(post_save, sender=Member) def duplicate_receiver(sender, instance, created, **kwargs): + """Call check_for_duplicates function.""" check_for_duplicates(instance) # Can be used to retrieve single member information via REST API class MemberDetail(generics.RetrieveAPIView): + """Member detail rest API view.""" + queryset = Member.objects.all() serializer_class = MemberSerializer permission_classes = (permissions.IsAdminUser, ) From 43e54af14124c29f40438fb46953b8b8cb2cabb8 Mon Sep 17 00:00:00 2001 From: henu Date: Wed, 20 Sep 2017 21:15:46 +0300 Subject: [PATCH 16/20] Fix webapp and sikweb py files pep8 --- sikweb/.ci-settings.py | 2 ++ sikweb/settings-sample.py | 17 ++++++---- sikweb/urls.py | 3 +- webapp/admin.py | 2 ++ webapp/apps.py | 5 +++ webapp/models.py | 71 ++++++++++++++++++++++++--------------- webapp/tests.py | 2 ++ webapp/translation.py | 18 +++++++--- webapp/urls.py | 2 ++ webapp/utils.py | 3 ++ webapp/views.py | 11 +++++- 11 files changed, 97 insertions(+), 39 deletions(-) diff --git a/sikweb/.ci-settings.py b/sikweb/.ci-settings.py index 8717ed4..043f3ce 100644 --- a/sikweb/.ci-settings.py +++ b/sikweb/.ci-settings.py @@ -1,3 +1,5 @@ +"""File containing CI settings.""" + from sikweb.default_settings import * DATABASES = { diff --git a/sikweb/settings-sample.py b/sikweb/settings-sample.py index 2013527..9ad9d7d 100644 --- a/sikweb/settings-sample.py +++ b/sikweb/settings-sample.py @@ -79,7 +79,7 @@ LOGGING = { # Application definition INSTALLED_APPS = [ - 'modeltranslation', # has to be before admin for translation admin stuff to work + 'modeltranslation', # has to be before admin for translation admin to work 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -105,7 +105,8 @@ NOSE_ARGS = [ '--with-coverage', '--cover-package=webapp,members,infoscreen', '--exclude-dir={}'.format(os.path.join(BASE_DIR, 'members', 'migrations')), - '--exclude-dir={}'.format(os.path.join(BASE_DIR, 'infoscreen', 'migrations')), + '--exclude-dir={}'.format(os.path.join(BASE_DIR, + 'infoscreen', 'migrations')), '--exclude-dir={}'.format(os.path.join(BASE_DIR, 'webapp', 'migrations')), ] @@ -182,16 +183,20 @@ else: AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + 'NAME': 'django.contrib.auth.password_validation.' + 'UserAttributeSimilarityValidator', }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + 'NAME': 'django.contrib.auth.password_validation.' + 'MinimumLengthValidator', }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + 'NAME': 'django.contrib.auth.password_validation.' + 'CommonPasswordValidator', }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + 'NAME': 'django.contrib.auth.password_validation.' + 'NumericPasswordValidator', }, ] diff --git a/sikweb/urls.py b/sikweb/urls.py index 0113b57..2e0793c 100644 --- a/sikweb/urls.py +++ b/sikweb/urls.py @@ -41,5 +41,6 @@ urlpatterns = [ # staticfiles default view for static files in development url(r'^static/(?P.*)$', static_views.serve), - url(r'^media/(?P.*)$', static_serve, {'document_root': settings.MEDIA_ROOT}), + url(r'^media/(?P.*)$', + static_serve, {'document_root': settings.MEDIA_ROOT}), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/webapp/admin.py b/webapp/admin.py index b532141..03faf69 100644 --- a/webapp/admin.py +++ b/webapp/admin.py @@ -1,3 +1,5 @@ +"""File containing webapp app admin registers.""" + from django.contrib import admin from webapp.models import Official, Role from webapp.models import Feed, Tag, BaseFeed, Event diff --git a/webapp/apps.py b/webapp/apps.py index 55fa92c..151670c 100644 --- a/webapp/apps.py +++ b/webapp/apps.py @@ -1,8 +1,13 @@ +"""Webapp app configurations.""" + from django.apps import AppConfig class WebappConfig(AppConfig): + """Webapp configurations.""" + name = 'webapp' def ready(self): + """Import translations.""" import webapp.translations diff --git a/webapp/models.py b/webapp/models.py index 569089e..812d9f9 100644 --- a/webapp/models.py +++ b/webapp/models.py @@ -1,3 +1,5 @@ +"""Webapp app models.""" + from django.db import models from django.utils import timezone from datetime import timedelta @@ -11,15 +13,16 @@ from django.contrib.postgres.fields import JSONField class Tag(models.Model): + """Model for tag.""" + slug = models.SlugField(primary_key=True) name = models.CharField(max_length=127) icon = models.ImageField() class BaseFeed(models.Model): - ''' - model containing something showing on some info feed - ''' + """Model containing something showing on some info feed.""" + tags = models.ManyToManyField(Tag, related_name="feeds", blank=True) visible = models.BooleanField(default=True) title = models.CharField(max_length=255) @@ -28,47 +31,52 @@ class BaseFeed(models.Model): class Feed(BaseFeed): + """Model representing feed.""" publish_time = models.DateTimeField(default=timezone.now) autohide = models.DateTimeField(default=month_from_now) class Event(BaseFeed): + """Model for event.""" + start_time = models.DateTimeField(default=timezone.now) end_time = models.DateTimeField(default=timezone.now) - registration = models.ForeignKey('Registration', on_delete=models.CASCADE, null=True) + registration = models.ForeignKey( + 'Registration', on_delete=models.CASCADE, null=True) class Registration(models.Model): + """Model for event registration.""" + name = models.CharField(max_length=255) email = models.EmailField() options = JSONField() class BaseRole(models.Model): - ''' - Base model for occupations/roles - ''' + """Base model for occupations/roles.""" + name = models.CharField(_('Name'), max_length=255) is_board = models.BooleanField(_('Board member')) class PresetRole(BaseRole): - ''' - Model representing a preset occupation in the guild - ''' + """Model representing a preset occupation in the guild.""" + description = models.TextField(_('Description')) summary = models.TextField(_('Summary')) class PresetKaehmyRole(PresetRole): + """Model for kaehmy role.""" + form = models.ForeignKey('KaehmyForm', related_name='preset_roles') class CustomKaehmyRole(BaseRole): - ''' - Model representing a user-specified custom occupation - ''' + """Model representing a user-specified custom occupation.""" + form = models.ForeignKey('KaehmyForm', related_name='custom_roles') @@ -77,10 +85,12 @@ class MessageParent(models.Model): class KaehmyMessage(MessageParent): - ''' + """ Model representing a kaehmymessage. + Every message relates to certain kaehmyform or parent message. - ''' + """ + name = models.CharField(_('Name'), max_length=255) email = models.EmailField(_('Email')) message = models.TextField(_('Message')) @@ -88,21 +98,28 @@ class KaehmyMessage(MessageParent): class KaehmyForm(MessageParent): - ''' + """ Model representing a form for kaehmy. + Allows user to choose from existing roles or to create custom ones. - ''' + """ + name = models.CharField(_('Name'), max_length=255) email = models.EmailField(_('Email')) year = models.IntegerField(_('Year')) class Role(PresetRole): - ''' + """ + Model for Role. + Model representing an active or historical occupation - in an official's history - ''' + in an official's history. + """ + class Meta: + """Meta class for Role model.""" + verbose_name = _('Role') start_date = models.DateField(_('Start date')) @@ -111,20 +128,20 @@ class Role(PresetRole): class Official(User): - ''' - Model representing a guild official - ''' + """Model representing a guild official.""" + class Meta: + """Meta class for Official class.""" + verbose_name = _('Official') phone_number = PhoneNumberField(_('Phone number')) -#Ohlhafv +# Ohlhafv class OhlhafvChallenge(models.Model): - ''' - Model containing all info about ohlhafv challenge - ''' + """Model containing all info about ohlhafv challenge.""" + SERIES_CHOICES = ( ('0.33 L', '0.33 L'), ('0.5 L', '0.5 L'), diff --git a/webapp/tests.py b/webapp/tests.py index 7ce503c..1418e07 100644 --- a/webapp/tests.py +++ b/webapp/tests.py @@ -1,3 +1,5 @@ +"""Tests for webapp.""" + from django.test import TestCase # Create your tests here. diff --git a/webapp/translation.py b/webapp/translation.py index 678be16..9ee016d 100644 --- a/webapp/translation.py +++ b/webapp/translation.py @@ -1,22 +1,32 @@ +"""Translation classes.""" + from modeltranslation.translator import register, TranslationOptions from webapp.models import BaseFeed, Feed, Tag, Event @register(BaseFeed) class BaseFeedTranslationOptions(TranslationOptions): - fields = ('title', 'description', 'content') + """Class for base feed translation options.""" + + fields = ('title', 'description', 'content') @register(Feed) class FeedTranslationOptions(TranslationOptions): - fields = () + """Class for feed translation options.""" + + fields = () @register(Event) class EventTranslationOptions(TranslationOptions): - fields = () + """Class for event translation options.""" + + fields = () @register(Tag) class TagTranslationOptions(TranslationOptions): - fields = ('name',) + """Class for tag translation options.""" + + fields = ('name',) diff --git a/webapp/urls.py b/webapp/urls.py index 2dd136d..a7eaa70 100644 --- a/webapp/urls.py +++ b/webapp/urls.py @@ -1,3 +1,5 @@ +"""Webapp urls.""" + from django.conf.urls import url from webapp.views import main_index diff --git a/webapp/utils.py b/webapp/utils.py index 0886e08..ceb83c7 100644 --- a/webapp/utils.py +++ b/webapp/utils.py @@ -1,6 +1,9 @@ +"""Webapp utils.""" + from django.utils import timezone from datetime import timedelta def month_from_now(): + """Return date one month from now.""" return timezone.now() + timedelta(days=30) diff --git a/webapp/views.py b/webapp/views.py index d8e0ac5..dfcf5b0 100644 --- a/webapp/views.py +++ b/webapp/views.py @@ -1,3 +1,5 @@ +"""Webapp views.""" + from django.shortcuts import render, redirect from django.contrib.auth import login, logout, authenticate from django.views.decorators.http import require_http_methods @@ -9,6 +11,7 @@ import logging @require_http_methods(["GET"]) def main_index(request, *args, **kwargs): + """Render main page.""" return render(request, "main_index.html", {}) @@ -16,11 +19,13 @@ def main_index(request, *args, **kwargs): @ensure_csrf_cookie @permission_required('members.change_member', login_url='/login') def admin_index(request, *args, **kwargs): + """Render admin main page.""" return render(request, "admin_index.html", {}) @require_http_methods(["GET", "POST"]) def login_view(request, *args, **kwargs): + """Render login view.""" if request.method == "POST": uname = request.POST.get("username", None) pw = request.POST.get("passwd", None) @@ -29,7 +34,9 @@ def login_view(request, *args, **kwargs): login(request, user) original_site = request.GET.get("next", None) or "/" return redirect(original_site) - return render(request, "login.html", {"error": "☹ Kirjautuminen kosahti. Yritä uudelleen!"}) + return render(request, + "login.html", + {"error": "☹ Kirjautuminen kosahti. Yritä uudelleen!"}) # user got here by a get request user = request.user @@ -41,10 +48,12 @@ def login_view(request, *args, **kwargs): @require_http_methods(["POST"]) def logout_view(request, *args, **kwargs): + """Logout user and return to main page.""" logout(request) return redirect("/") @require_http_methods(["GET"]) def about_view(request, *args, **kwargs): + """Render about page.""" return render(request, "about.html", {}) From b0edaae32eb08b2b57fbaf90bcd32683b750e9e4 Mon Sep 17 00:00:00 2001 From: Jan Tuomi Date: Wed, 20 Sep 2017 23:17:55 +0300 Subject: [PATCH 17/20] Remove conflict resolver and add dynamic payments --- members/forms.py | 71 +++++++++- members/migrations/0014_auto_20170920_1457.py | 25 ++++ members/models.py | 3 +- members/templates/error.html | 2 +- members/templates/member_add_many.html | 13 +- .../templates/member_add_many_confirm.html | 26 ++++ members/templates/member_duplicates.html | 40 ------ members/urls.py | 12 +- members/views.py | 132 ++++++++---------- setup.sh | 2 +- 10 files changed, 197 insertions(+), 129 deletions(-) create mode 100644 members/migrations/0014_auto_20170920_1457.py create mode 100644 members/templates/member_add_many_confirm.html delete mode 100644 members/templates/member_duplicates.html diff --git a/members/forms.py b/members/forms.py index a6e47ce..221b8b7 100644 --- a/members/forms.py +++ b/members/forms.py @@ -3,12 +3,81 @@ from django.utils.translation import ugettext_lazy as _ from members.models import Member, Payment, Request +import csv +import datetime +import logging + + +class CSVValidationError(Exception): + def __init__(self, form_errors): + self.form_errors = form_errors + class MemberForm(forms.ModelForm): class Meta: model = Member - fields = ['first_name', 'last_name', 'email', 'AYY', 'jas', 'POR'] + fields = ['first_name', 'last_name', 'email', 'POR', 'AYY', 'jas'] + + class ImportResult: + def __init__(self, members, payments): + self.members = members + self.payments = payments + + def clean_email(self): + email = self.cleaned_data['email'] + + if Member.objects.filter(email=email).exists(): + raise forms.ValidationError('Member with email "{}" already exists.'.format(email), code='exists') + + return email + + def clean_jas(self): + return bool(int(self.data['jas'])) + + def clean_AYY(self): + return bool(int(self.data['AYY'])) + + @staticmethod + def csv_to_models(data, payment_source='AYY'): + clean_data = data.strip().split('\n') + clean_data = [row.rstrip(',') for row in clean_data] + csv_reader = csv.DictReader(clean_data, fieldnames=MemberForm.Meta.fields) + + members = [] + payments = [] + for line in csv_reader: + for key, value in line.items(): + line[key] = value.strip() + + email = line['email'] + member_exists = False + if Member.objects.filter(email=email).exists(): + member_exists = True + + if not member_exists: + form = MemberForm(line) + if not form.is_valid(): + raise CSVValidationError(form.errors) + + model = form.save(commit=False) + members.append(model) + + else: + member = Member.objects.get(email=email) + payment_data = { + 'source': payment_source, + 'member': member.id, + 'date': datetime.datetime.now(), + } + form = PaymentForm(payment_data) + if not form.is_valid(): + raise CSVValidationError(form.errors) + + model = form.save(commit=False) + payments.append(model) + + return MemberForm.ImportResult(members, payments) class PaymentForm(forms.ModelForm): diff --git a/members/migrations/0014_auto_20170920_1457.py b/members/migrations/0014_auto_20170920_1457.py new file mode 100644 index 0000000..1397954 --- /dev/null +++ b/members/migrations/0014_auto_20170920_1457.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-09-20 11:57 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0013_auto_20170601_1822'), + ] + + operations = [ + migrations.AlterField( + model_name='member', + name='email', + field=models.EmailField(max_length=254, unique=True, verbose_name='Email'), + ), + migrations.AlterField( + model_name='request', + name='email', + field=models.EmailField(max_length=254, unique=True, verbose_name='Email'), + ), + ] diff --git a/members/models.py b/members/models.py index a8c46d0..8cbcae8 100644 --- a/members/models.py +++ b/members/models.py @@ -13,7 +13,7 @@ class BaseMember(models.Model): ''' first_name = models.CharField(_("First name"), max_length=127) last_name = models.CharField(_("Last name"), max_length=127) - email = models.EmailField(_("Email")) + email = models.EmailField(_("Email"), unique=True) POR = models.CharField(_("Place of residence"), max_length=255) # place of residence AYY = models.BooleanField(_("AYY"), default=False) jas = models.BooleanField(_("JAS"), default=False) @@ -26,7 +26,6 @@ class BaseMember(models.Model): @staticmethod def from_csv(data): - print("Imported CSV data: {}".format(data)) clean_data = data.strip().split('\n') csv_reader = csv.reader(clean_data) diff --git a/members/templates/error.html b/members/templates/error.html index b8c30ae..745eb56 100644 --- a/members/templates/error.html +++ b/members/templates/error.html @@ -9,7 +9,7 @@