Merge branch 'feature-duplicate-resolver' into 'develop'

Feature duplicate resolver

See merge request !35
This commit is contained in:
Jan Tuomi
2017-05-29 23:22:04 +03:00
12 changed files with 314 additions and 59 deletions
Binary file not shown.
+59 -22
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-28 21:51+0300\n"
"POT-Creation-Date: 2017-05-29 22:48+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -289,6 +289,31 @@ msgstr "Are you sure you want to delete this member?"
msgid "Yes, I'm sure"
msgstr "Yes, I'm sure"
#: members/templates/member_duplicates.html:9
msgid "Conflicting member entries"
msgstr ""
#: members/templates/member_duplicates.html:13
msgid ""
"Found conflicting member entries. Choose how to handle the problematic data."
msgstr ""
#: members/templates/member_duplicates.html:29
msgid "Which one has the correct information for this member?"
msgstr ""
#: members/templates/member_duplicates.html:31
msgid "Accept first and remove second"
msgstr ""
#: members/templates/member_duplicates.html:32
msgid "Accept second and remove first"
msgstr ""
#: members/templates/member_duplicates.html:33
msgid "Accept both as two members"
msgstr ""
#: members/templates/member_edit.html:9
msgid "Edit member"
msgstr "Edit member"
@@ -297,7 +322,13 @@ msgstr "Edit member"
msgid "Member register"
msgstr "Member register"
#: members/templates/member_list.html:23
#: members/templates/member_list.html:16
msgid ""
"There are duplicate member entries in the register.\n"
" Please visit <a href=\"/members/duplicates\">duplicate resolver</a>."
msgstr ""
#: members/templates/member_list.html:30
msgid "Download CSV"
msgstr "Download CSV"
@@ -372,86 +403,92 @@ msgstr "Finnish"
msgid "English"
msgstr "English"
#: members/views.py:127 members/views.py:184 members/views.py:203
#: members/views.py:128 members/views.py:185 members/views.py:204
msgid "No member id specified"
msgstr "No member id specified"
#: members/views.py:149
#: members/views.py:150
msgid "Successfully added member"
msgstr "Successfully added member"
#: members/views.py:170
#: members/views.py:171
msgid "Successfully updated member"
msgstr "Successfully updated member"
#: members/views.py:174
#: members/views.py:175
msgid "Could not update member object"
msgstr "Could not update member object"
#: members/views.py:188
#: members/views.py:189
msgid "Successfully deleted member"
msgstr "Successfully deleted member"
#: members/views.py:194
#: members/views.py:195
msgid "Could not delete member object"
msgstr "Could not delete member object"
#: members/views.py:237 members/views.py:271 members/views.py:289
#: members/views.py:238 members/views.py:272 members/views.py:290
msgid "No application id specified"
msgstr "No application id specified"
#: members/views.py:258
#: members/views.py:259
msgid "Successfully accepted application"
msgstr "Successfully accepted application"
#: members/views.py:261
#: members/views.py:262
msgid "Could not accept application object"
msgstr "Could not accept application object"
#: members/views.py:275
#: members/views.py:276
msgid "Successfully deleted application"
msgstr "Successfully deleted application"
#: members/views.py:280
#: members/views.py:281
msgid "Could not delete application object"
msgstr "Could not delete application object"
#: members/views.py:344
#: members/views.py:345
msgid "Successfully added payment for member"
msgstr "Successfully added payment for member"
#: members/views.py:357 members/views.py:370 members/views.py:384
#: members/views.py:358 members/views.py:371 members/views.py:385
msgid "No payment id specified"
msgstr "No payment id specified"
#: members/views.py:388
#: members/views.py:389
msgid "Successfully deleted payment"
msgstr "Successfully deleted payment"
#: members/views.py:393
#: members/views.py:394
msgid "Could not delete payment object"
msgstr "Could not delete payment object"
#: members/views.py:408
#: members/views.py:409
msgid "Successfully updated payment"
msgstr "Successfully updated payment"
#: members/views.py:411
#: members/views.py:412
msgid "Could not update payment object"
msgstr "Could not update payment object"
#: members/views.py:428
#: members/views.py:429
msgid "Missing \"textfield\" POST request field"
msgstr "Missing \"textfield\" POST request field"
#: members/views.py:433
#: members/views.py:434
msgid "Successfully imported multiple members"
msgstr "Successfully imported multiple members"
#: members/views.py:436
#: members/views.py:437
msgid "Failed to import members"
msgstr "Failed to import members"
#: members/views.py:497
#, fuzzy
#| msgid "Successfully deleted member"
msgid "Successfully resolved all member conflicts."
msgstr "Successfully deleted member"
#: templates/footer.html:7
msgid "Copyright Aalto-yliopiston Sähköinsinöörikilta ry"
msgstr "Copyright Aalto-yliopiston Sähköinsinöörikilta ry"
Binary file not shown.
+60 -22
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-28 21:51+0300\n"
"POT-Creation-Date: 2017-05-29 22:48+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -293,6 +293,31 @@ msgstr "Oletko varma, että haluat poistaa tämän jäsenen?"
msgid "Yes, I'm sure"
msgstr "Kyllä, olen varma"
#: members/templates/member_duplicates.html:9
msgid "Conflicting member entries"
msgstr "Ongelmalliset jäsentiedot"
#: members/templates/member_duplicates.html:13
msgid ""
"Found conflicting member entries. Choose how to handle the problematic data."
msgstr "Ongelmallista jäsendataa havaittu. Valitse, miten ongelmat ratkaistaan."
#: members/templates/member_duplicates.html:29
msgid "Which one has the correct information for this member?"
msgstr "Kummassa on jäsenen oikeat tiedot?"
#: members/templates/member_duplicates.html:31
msgid "Accept first and remove second"
msgstr "Hyväksy ensimmäinen ja poista toinen"
#: members/templates/member_duplicates.html:32
msgid "Accept second and remove first"
msgstr "Hyväksy toinen ja poista ensimmäinen"
#: members/templates/member_duplicates.html:33
msgid "Accept both as two members"
msgstr "Hyväksy molemmat kahtena jäsenenä"
#: members/templates/member_edit.html:9
msgid "Edit member"
msgstr "Muokkaa jäsentä"
@@ -301,7 +326,14 @@ msgstr "Muokkaa jäsentä"
msgid "Member register"
msgstr "Jäsenrekisteri"
#: members/templates/member_list.html:23
#: members/templates/member_list.html:16
msgid ""
"There are duplicate member entries in the register.\n"
" Please visit <a href=\"/members/duplicates\">duplicate resolver</a>."
msgstr "Jäsenrekisterissä on duplikaattijäseniä.\n"
" Käytä ongelman ratkaisuun <a href=\"/members/duplicates\">duplikaattityökalua</a>."
#: members/templates/member_list.html:30
msgid "Download CSV"
msgstr "Lataa CSV"
@@ -376,86 +408,92 @@ msgstr "suomi"
msgid "English"
msgstr "englanti"
#: members/views.py:127 members/views.py:184 members/views.py:203
#: members/views.py:128 members/views.py:185 members/views.py:204
msgid "No member id specified"
msgstr "Jäsenen ID ei määritelty"
#: members/views.py:149
#: members/views.py:150
msgid "Successfully added member"
msgstr "Onnistuneesti lisättiin jäsen"
#: members/views.py:170
#: members/views.py:171
msgid "Successfully updated member"
msgstr "Onnistuneesti päivitettiin jäsen"
#: members/views.py:174
#: members/views.py:175
msgid "Could not update member object"
msgstr "Jäsenobjektia ei voitu päivittää"
#: members/views.py:188
#: members/views.py:189
msgid "Successfully deleted member"
msgstr "Onnistuneesti poistettiin jäsen"
#: members/views.py:194
#: members/views.py:195
msgid "Could not delete member object"
msgstr "Jäsenobjektia ei voitu poistaa"
#: members/views.py:237 members/views.py:271 members/views.py:289
#: members/views.py:238 members/views.py:272 members/views.py:290
msgid "No application id specified"
msgstr "Hakemuksen ID ei määritelty"
#: members/views.py:258
#: members/views.py:259
msgid "Successfully accepted application"
msgstr "Onnistuneesti hyväksyttiin hakemus"
#: members/views.py:261
#: members/views.py:262
msgid "Could not accept application object"
msgstr "Hakemusobjektia ei voitu hyväksyä"
#: members/views.py:275
#: members/views.py:276
msgid "Successfully deleted application"
msgstr "Onnistuneesti poistettiin hakemus"
#: members/views.py:280
#: members/views.py:281
msgid "Could not delete application object"
msgstr "Hakemusobjektia ei voitu poistaa"
#: members/views.py:344
#: members/views.py:345
msgid "Successfully added payment for member"
msgstr "Onnistuneesti lisättiin maksutapahtuma jäsenelle"
#: members/views.py:357 members/views.py:370 members/views.py:384
#: members/views.py:358 members/views.py:371 members/views.py:385
msgid "No payment id specified"
msgstr "Maksutapahtuman ID ei määritelty"
#: members/views.py:388
#: members/views.py:389
msgid "Successfully deleted payment"
msgstr "Onnistuneesti poistettiin maksutapahtuma"
#: members/views.py:393
#: members/views.py:394
msgid "Could not delete payment object"
msgstr "Maksutapahtumaobjektia ei voitu poistaa"
#: members/views.py:408
#: members/views.py:409
msgid "Successfully updated payment"
msgstr "Onnistuneesti päivitettiin maksutapahtuma"
#: members/views.py:411
#: members/views.py:412
msgid "Could not update payment object"
msgstr "Maksutapahtumaobjektia ei voitu päivittää"
#: members/views.py:428
#: members/views.py:429
msgid "Missing \"textfield\" POST request field"
msgstr "Puuttuva \"textfield\" POST-kenttä"
#: members/views.py:433
#: members/views.py:434
msgid "Successfully imported multiple members"
msgstr "Onnistuneesti tuotu useita jäseniä"
#: members/views.py:436
#: members/views.py:437
msgid "Failed to import members"
msgstr "Jäsenten tuonti epäonnistui"
#: members/views.py:497
#, fuzzy
#| msgid "Successfully deleted member"
msgid "Successfully resolved all member conflicts."
msgstr "Onnistuneesti poistettiin jäsen"
#: templates/footer.html:7
msgid "Copyright Aalto-yliopiston Sähköinsinöörikilta ry"
msgstr "Copyright Aalto-yliopiston Sähköinsinöörikilta ry"
+2 -1
View File
@@ -1,7 +1,8 @@
from django.contrib import admin
from members.models import Member, Request, Payment
from members.models import Member, Request, Payment, MemberConflict
# Register your models here.
admin.site.register(Member)
admin.site.register(Request)
admin.site.register(Payment)
admin.site.register(MemberConflict)
+24
View File
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-05-28 20:32
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('members', '0011_auto_20170526_2013'),
]
operations = [
migrations.CreateModel(
name='MemberConflict',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberconflict_first_member', to='members.Member')),
('second_member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberconflict_second_member', to='members.Member')),
],
),
]
+18
View File
@@ -119,3 +119,21 @@ class Member(BaseMember):
AYY=bool(array[4]),
jas=bool(array[5]),
)
class MemberConflict(models.Model):
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):
return MemberForm(instance=self.first_member)
@property
def second_member_form(self):
return MemberForm(instance=self.second_member)
# To avoid problems with a cyclical import, this is at the bottom of the file
from members.forms import MemberForm # nopep8
+18
View File
@@ -246,3 +246,21 @@ input {
max-width: 100%;
height: 10rem !important;
}
.conflict-row {
overflow: auto;
margin-bottom: 2rem;
border: 1px dotted black;
}
.table-conflict {
}
.conflict-row>div>table>tbody>tr>th,
.conflict-row>div>table>tbody>tr>td {
border-top: none;
padding: 1rem;
}
.conflict-row>div {
}
+40
View File
@@ -0,0 +1,40 @@
{% 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 %}
+7
View File
@@ -11,6 +11,13 @@
<h2>{% trans "Member register" %}</h2>
</div>
{% if is_member_conflict %}
<div class="alert alert-warning">
{% blocktrans %}There are duplicate member entries in the register.
Please visit <a href="/members/duplicates">duplicate resolver</a>.{% endblocktrans %}
</div>
{% endif %}
{% if notification %}
<div class="alert alert-success">
{{ notification }}
+7 -4
View File
@@ -16,6 +16,8 @@ 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
# rest api
from members.views import MemberDetail
@@ -102,9 +104,10 @@ urlpatterns = [
# rest api url
url(r'^api/members/(?P<pk>\d+)$', MemberDetail.as_view()),
# email validation
# url(r'^validate/(?P<uidb64>[0-9A-Za-z_\-\']+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', validateEmail, name='user-activation-link'),
# url(r'^validate/success/$', validate_success),
# url(r'^validate/failure/$', validate_fail),
# member duplicate resolution view
url(r'^duplicates$', member_duplicates),
# post target for resolving a conflict
url(r'^resolve_conflict$', resolve_conflict)
]
+79 -10
View File
@@ -25,8 +25,9 @@ import requests
import logging
import html
import csv
from smtplib import SMTPAuthenticationError
from members.models import Member, Request, Payment
from members.models import Member, Request, Payment, MemberConflict
from members.forms import MemberForm, PaymentForm, ApplicationForm
# Logger function, you can use the same idea when implementing other loggers to other apps
@@ -105,7 +106,8 @@ def member_list(request, *args, **kwargs):
context = {
'table': table_html,
'member_count': len(members),
'notification': request.GET.get('notification', None)
'notification': request.GET.get('notification', None),
'is_member_conflict': MemberConflict.objects.exists()
}
return render(request, 'member_list.html', context)
@@ -455,6 +457,48 @@ 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':
second_member.delete()
elif action == 'second':
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,
@@ -465,18 +509,43 @@ def send_mail_wrapper(subject, message, email_to):
@receiver(post_save, sender=Request)
def email_on_request(sender, instance, created, **kwargs):
if created:
subject = 'Test1'
message = 'Please validate your email address\r\n'
send_mail_wrapper(subject, message, instance.email)
try:
if created:
subject = 'Test1'
message = 'Please validate your email address\r\n'
send_mail_wrapper(subject, message, instance.email)
except SMTPAuthenticationError:
memberlogger.error('Failed to send email to accepted request!')
@receiver(post_save, sender=Member)
def email_on_accept(sender, instance, created, **kwargs):
if created:
subject = 'Test2'
message = 'Jäsenhakemuksesi on hyväksytty!!!\r\n'
send_mail_wrapper(subject, message, instance.email)
try:
if created:
subject = 'Test2'
message = 'Jäsenhakemuksesi on hyväksytty!!!\r\n'
send_mail_wrapper(subject, message, instance.email)
except SMTPAuthenticationError:
memberlogger.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