Remove conflict resolver and add dynamic payments

This commit is contained in:
Jan Tuomi
2017-09-20 23:17:55 +03:00
parent 7a435bcbc6
commit b0edaae32e
10 changed files with 197 additions and 129 deletions
+70 -1
View File
@@ -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):
@@ -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'),
),
]
+1 -2
View File
@@ -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)
+1 -1
View File
@@ -9,7 +9,7 @@
</div>
<div class="alert alert-danger">
{{ error }}
{{ error|safe }}
</div>
<div>
<button onclick="window.history.back();" class="btn btn-primary">{% trans "Back" %}</button>
+11 -2
View File
@@ -5,7 +5,7 @@
{% block content %}
<div>
<div>
<h3> Lisää useampi jäsen </h3>
<h3>{% trans "Add many members" %}</h3>
</div>
<div>
@@ -25,9 +25,18 @@
</div>
<form name="memberTextForm" action="/members/import_csv" method="POST">{% csrf_token %}
<div>
<div class="form-group">
<label>{% trans "Data" %}</label>
<textarea name="textfield" class="form-control large-textarea" placeholder="Teemu, Teekkari, teemu.teekkari@notmail.dom, Otaniemi, 0, 0"></textarea>
</div>
<div class="form-group">
<label>{% trans "Payment source" %}</label>
<select name="payment_source" class="form-control">
<option value="AYY">{% trans "AYY" %}</option>
<option value="bank_transfer">{% trans "Bank transfer" %}</option>
<option value="cash">{% trans "Cash payment" %}</option>
</select>
</div>
<div>
<button type="submit" class="btn btn-primary">{% trans "Send" %}</button>
</div>
@@ -0,0 +1,26 @@
{% extends "members_base.html" %}
{% load i18n %}
{% block content %}
<div>
<div>
<h3>{% trans "Confirm adding these entries?" %}</h3>
</div>
<div class="form-group">
<label>{% trans "Members" %}</label>
{{ members|safe }}
</div>
<div class="form-group">
<label>{% trans "Payments" %}</label>
{{ payments|safe }}
</div>
</div>
<form name="memberTextForm" action="/members/add_many_confirm" method="POST">{% csrf_token %}
<div>
<button type="submit" class="btn btn-primary">{% trans "Send" %}</button>
</div>
</form>
</div>
{% endblock content %}
-40
View File
@@ -1,40 +0,0 @@
{% extends "members_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block content %}
<div>
<div>
<h3>{% trans "Conflicting member entries" %}</h3>
</div>
<div>
<p>{% blocktrans %}Found conflicting member entries. Choose how to handle the problematic data.{% endblocktrans %}</p>
{% for conflict in conflicts %}
<div class="conflict-row">
<div class="col-md-6">
<table class="table readonly table-conflict bg-info" >
{{ conflict.first_member_form }}
</table>
</div>
<div class="col-md-6">
<table class="table readonly table-conflict bg-info" >
{{ conflict.second_member_form }}
</table>
</div>
<div>
<form action="/members/resolve_conflict" method="POST">{% csrf_token %}
<p>{% blocktrans %}Which one has the correct information for this member?{% endblocktrans %}</p>
<input type="hidden" name="id" value="{{ conflict.id }}">
<button type="submit" name="action" value="first" class="btn btn-primary">{% trans "Accept first and remove second" %}</button>
<button type="submit" name="action" value="second" class="btn btn-primary">{% trans "Accept second and remove first" %}</button>
<button type="submit" name="action" value="both" class="btn btn-primary">{% trans "Accept both as two members" %}</button>
</form>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock content %}
+4 -8
View File
@@ -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<index>\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<pk>\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)
]
+58 -74
View File
@@ -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()