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 d2347f2..0636f3d 100644 --- a/infoscreen/views.py +++ b/infoscreen/views.py @@ -184,8 +184,6 @@ def create_image_item(request, *args, **kwargs): def create_video_item(request, *args, **kwargs): """Create video Infoscreen item.""" form = UploadFileForm(request.POST, request.FILES) - print(form.errors) - print("hurdurr") if not form.is_valid(): return HttpResponseBadRequest('{"status": "failure",' '"error": "invalid data supplied"}') diff --git a/members/forms.py b/members/forms.py index 1ac3a4d..5622123 100644 --- a/members/forms.py +++ b/members/forms.py @@ -5,6 +5,15 @@ 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): """Member model form.""" @@ -13,7 +22,67 @@ class MemberForm(forms.ModelForm): """Meta for Member model form.""" 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 c845aa2..cbcb9d4 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) @@ -30,8 +30,6 @@ class BaseMember(models.Model): @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) 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 @@
- {{ 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 ad35248..664e5cd 100644 --- a/members/urls.py +++ b/members/urls.py @@ -18,8 +18,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 @@ -93,6 +92,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), @@ -108,10 +110,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 ca976c8..ef1931d 100644 --- a/members/views.py +++ b/members/views.py @@ -8,6 +8,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 @@ -27,13 +28,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. @@ -518,23 +524,63 @@ def import_csv(request, *args, **kwargs): """Get csv data imported to page and create members based on that.""" 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")) - return HttpResponseRedirect( - '/members/list?notification={}' - .format(html.escape(notification))) - else: - return render(request, - 'error.html', - {'error': _('Failed to import 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))) + except Exception as ex: + logging.exception('Failed to save models after "add many."') + return error_view(request, _('Failed to import members')) @ensure_csrf_cookie @@ -558,62 +604,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): - """Check for duplicate members.""" - 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): - """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)}) - - 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 to default email.""" send_mail(subject, @@ -653,27 +643,6 @@ def email_on_accept(sender, instance, created, **kwargs): logging.error('Failed to send email to accepted member!') -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) - - 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): - """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.""" 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 "" 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/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 8136262..c517fbc 100644 --- a/webapp/urls.py +++ b/webapp/urls.py @@ -23,7 +23,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 f8adc8b..d398898 100644 --- a/webapp/views.py +++ b/webapp/views.py @@ -75,10 +75,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/')