diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a1b2c0f..ea79d70 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -28,7 +28,7 @@ test:
pep8:
stage: lint
script:
- - pip install -r requirements.txt
+ - pip install pep8
- pep8 --config=setup.cfg --count .
eslint:
diff --git a/global_static/css/about.css b/global_static/css/about.css
new file mode 100644
index 0000000..c9baa32
--- /dev/null
+++ b/global_static/css/about.css
@@ -0,0 +1,14 @@
+html {
+ width: 100%;
+ height: 100%;
+ font-size: 16px;
+}
+
+body {
+ background-color: #fcfcfc;
+ max-width: 750px;
+ margin: auto;
+ text-align: center;
+ color: #100;
+ font-family: sans-serif;
+}
diff --git a/infoscreen/templates/infoscreen_admin.html b/infoscreen/templates/infoscreen_admin.html
index 7517f22..45c49dd 100644
--- a/infoscreen/templates/infoscreen_admin.html
+++ b/infoscreen/templates/infoscreen_admin.html
@@ -163,7 +163,7 @@
-
+
diff --git a/locale/en/LC_MESSAGES/django.mo b/locale/en/LC_MESSAGES/django.mo
index a51bcac..f93aefd 100644
Binary files a/locale/en/LC_MESSAGES/django.mo and b/locale/en/LC_MESSAGES/django.mo differ
diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po
index 5dee4f9..b174773 100644
--- a/locale/en/LC_MESSAGES/django.po
+++ b/locale/en/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-05-30 20:32+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 \n"
"Language-Team: LANGUAGE \n"
@@ -50,7 +50,7 @@ msgid "External image"
msgstr "External image"
#: infoscreen/templates/infoscreen_admin.html:24
-#: members/templates/members_base.html:69
+#: members/templates/members_base.html:79
msgid "Log out"
msgstr "Log out"
@@ -259,7 +259,7 @@ msgstr "Error"
msgid "Back"
msgstr "Back"
-#: members/templates/member_add.html:8 members/templates/members_base.html:45
+#: members/templates/member_add.html:8 members/templates/members_base.html:55
msgid "Add member"
msgstr "Add member"
@@ -309,6 +309,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"
@@ -317,51 +342,58 @@ 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 duplicate resolver."
+msgstr ""
+
+#: members/templates/member_list.html:30
msgid "Download CSV"
msgstr "Download CSV"
#: members/templates/members_base.html:33
+#: members/templates/members_base.html:42
msgid "Member register of SIK ry"
msgstr "Member register of SIK ry"
-#: members/templates/members_base.html:42 webapp/templates/main_index.html:7
+#: members/templates/members_base.html:52 webapp/templates/main_index.html:7
msgid "Members"
msgstr "Members"
-#: members/templates/members_base.html:44
+#: members/templates/members_base.html:54
msgid "List members"
msgstr "List members"
-#: members/templates/members_base.html:46
+#: members/templates/members_base.html:56
msgid "Add multiple"
msgstr "Add multiple"
-#: members/templates/members_base.html:50
+#: members/templates/members_base.html:60
msgid "Payments"
msgstr "Payments"
-#: members/templates/members_base.html:52
+#: members/templates/members_base.html:62
msgid "List payments"
msgstr "List payments"
-#: members/templates/members_base.html:53 members/templates/payment_add.html:8
+#: members/templates/members_base.html:63 members/templates/payment_add.html:8
msgid "Add payment"
msgstr "Add payment"
-#: members/templates/members_base.html:57
+#: members/templates/members_base.html:67
msgid "Applications"
msgstr "Applications"
-#: members/templates/members_base.html:59
+#: members/templates/members_base.html:69
msgid "List applications"
msgstr "List applications"
-#: members/templates/members_base.html:60
+#: members/templates/members_base.html:70
msgid "Application form"
msgstr "Application form"
-#: members/templates/members_base.html:65 members/templates/settings.html:11
+#: members/templates/members_base.html:75 members/templates/settings.html:11
msgid "Settings"
msgstr "Settings"
@@ -381,96 +413,102 @@ msgstr "Payment events"
msgid "Language"
msgstr "Language"
-#: members/templates/settings.html:20 sikweb/settings-sample.py:178
-#: sikweb/settings.py:178
+#: members/templates/settings.html:20 sikweb/settings-sample.py:179
+#: sikweb/settings.py:177
msgid "Finnish"
msgstr "Finnish"
-#: members/templates/settings.html:21 sikweb/settings-sample.py:177
-#: sikweb/settings.py:177
+#: members/templates/settings.html:21 sikweb/settings-sample.py:178
+#: sikweb/settings.py:176
msgid "English"
msgstr "English"
-#: members/views.py:121 members/views.py:178 members/views.py:197
+#: 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:143
+#: members/views.py:150
msgid "Successfully added member"
msgstr "Successfully added member"
-#: members/views.py:164
+#: members/views.py:171
msgid "Successfully updated member"
msgstr "Successfully updated member"
-#: members/views.py:168
+#: members/views.py:175
msgid "Could not update member object"
msgstr "Could not update member object"
-#: members/views.py:182
+#: members/views.py:189
msgid "Successfully deleted member"
msgstr "Successfully deleted member"
-#: members/views.py:188
+#: members/views.py:195
msgid "Could not delete member object"
msgstr "Could not delete member object"
-#: members/views.py:231 members/views.py:265 members/views.py:283
+#: 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:252
+#: members/views.py:259
msgid "Successfully accepted application"
msgstr "Successfully accepted application"
-#: members/views.py:255
+#: members/views.py:262
msgid "Could not accept application object"
msgstr "Could not accept application object"
-#: members/views.py:269
+#: members/views.py:276
msgid "Successfully deleted application"
msgstr "Successfully deleted application"
-#: members/views.py:274
+#: members/views.py:281
msgid "Could not delete application object"
msgstr "Could not delete application object"
-#: members/views.py:338
+#: members/views.py:345
msgid "Successfully added payment for member"
msgstr "Successfully added payment for member"
-#: members/views.py:351 members/views.py:364 members/views.py:378
+#: 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:382
+#: members/views.py:389
msgid "Successfully deleted payment"
msgstr "Successfully deleted payment"
-#: members/views.py:387
+#: members/views.py:394
msgid "Could not delete payment object"
msgstr "Could not delete payment object"
-#: members/views.py:402
+#: members/views.py:409
msgid "Successfully updated payment"
msgstr "Successfully updated payment"
-#: members/views.py:405
+#: members/views.py:412
msgid "Could not update payment object"
msgstr "Could not update payment object"
-#: members/views.py:422
+#: members/views.py:429
msgid "Missing \"textfield\" POST request field"
msgstr "Missing \"textfield\" POST request field"
-#: members/views.py:427
+#: members/views.py:434
msgid "Successfully imported multiple members"
msgstr "Successfully imported multiple members"
-#: members/views.py:430
+#: 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"
@@ -483,6 +521,18 @@ msgstr "SIK Admin"
msgid "Aalto-yliopiston Sähköinsinöörikilta ry"
msgstr "Aalto-yliopiston Sähköinsinöörikilta ry"
+#: webapp/templates/login.html:25 webapp/templates/login.html:27
+msgid "Username"
+msgstr "Username"
+
+#: webapp/templates/login.html:31 webapp/templates/login.html:33
+msgid "Password"
+msgstr "Password"
+
+#: webapp/templates/login.html:43
+msgid "Log in"
+msgstr "Log in"
+
#: webapp/templates/main_index.html:8
msgid "Infoscreen"
msgstr "Infoscreen"
diff --git a/locale/fi/LC_MESSAGES/django.mo b/locale/fi/LC_MESSAGES/django.mo
index ca7e32c..1909567 100644
Binary files a/locale/fi/LC_MESSAGES/django.mo and b/locale/fi/LC_MESSAGES/django.mo differ
diff --git a/locale/fi/LC_MESSAGES/django.po b/locale/fi/LC_MESSAGES/django.po
index e4796e3..b4cdeac 100644
--- a/locale/fi/LC_MESSAGES/django.po
+++ b/locale/fi/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-05-30 20:32+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 \n"
"Language-Team: LANGUAGE \n"
@@ -51,7 +51,7 @@ msgid "External image"
msgstr "Ulkoinen kuva"
#: infoscreen/templates/infoscreen_admin.html:24
-#: members/templates/members_base.html:69
+#: members/templates/members_base.html:79
msgid "Log out"
msgstr "Kirjaudu ulos"
@@ -258,7 +258,7 @@ msgstr "Virhe"
msgid "Back"
msgstr "Takaisin"
-#: members/templates/member_add.html:8 members/templates/members_base.html:45
+#: members/templates/member_add.html:8 members/templates/members_base.html:55
msgid "Add member"
msgstr "Lisää jäsen"
@@ -311,6 +311,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ä"
@@ -319,51 +344,59 @@ 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 duplicate resolver."
+msgstr "Jäsenrekisterissä on duplikaattijäseniä.\n"
+" Käytä ongelman ratkaisuun duplikaattityökalua."
+
+#: members/templates/member_list.html:30
msgid "Download CSV"
msgstr "Lataa CSV"
#: members/templates/members_base.html:33
+#: members/templates/members_base.html:42
msgid "Member register of SIK ry"
msgstr "Aalto-yliopiston Sähköinsinöörikilta ry:n jäsenrekisteri"
-#: members/templates/members_base.html:42 webapp/templates/main_index.html:7
+#: members/templates/members_base.html:52 webapp/templates/main_index.html:7
msgid "Members"
msgstr "Jäsenet"
-#: members/templates/members_base.html:44
+#: members/templates/members_base.html:54
msgid "List members"
msgstr "Jäsenlistaus"
-#: members/templates/members_base.html:46
+#: members/templates/members_base.html:56
msgid "Add multiple"
msgstr "Lisää useita"
-#: members/templates/members_base.html:50
+#: members/templates/members_base.html:60
msgid "Payments"
msgstr "Maksutapahtumat"
-#: members/templates/members_base.html:52
+#: members/templates/members_base.html:62
msgid "List payments"
msgstr "Maksulistaus"
-#: members/templates/members_base.html:53 members/templates/payment_add.html:8
+#: members/templates/members_base.html:63 members/templates/payment_add.html:8
msgid "Add payment"
msgstr "Lisää maksu"
-#: members/templates/members_base.html:57
+#: members/templates/members_base.html:67
msgid "Applications"
msgstr "Jäsenhakemukset"
-#: members/templates/members_base.html:59
+#: members/templates/members_base.html:69
msgid "List applications"
msgstr "Hakemuslistaus"
-#: members/templates/members_base.html:60
+#: members/templates/members_base.html:70
msgid "Application form"
msgstr "Jäsenhakemuslomake"
-#: members/templates/members_base.html:65 members/templates/settings.html:11
+#: members/templates/members_base.html:75 members/templates/settings.html:11
msgid "Settings"
msgstr "Asetukset"
@@ -383,96 +416,102 @@ msgstr "Maksutapahtumat"
msgid "Language"
msgstr "Kieli"
-#: members/templates/settings.html:20 sikweb/settings-sample.py:178
-#: sikweb/settings.py:178
+#: members/templates/settings.html:20 sikweb/settings-sample.py:179
+#: sikweb/settings.py:177
msgid "Finnish"
msgstr "suomi"
-#: members/templates/settings.html:21 sikweb/settings-sample.py:177
-#: sikweb/settings.py:177
+#: members/templates/settings.html:21 sikweb/settings-sample.py:178
+#: sikweb/settings.py:176
msgid "English"
msgstr "englanti"
-#: members/views.py:121 members/views.py:178 members/views.py:197
+#: 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:143
+#: members/views.py:150
msgid "Successfully added member"
msgstr "Onnistuneesti lisättiin jäsen"
-#: members/views.py:164
+#: members/views.py:171
msgid "Successfully updated member"
msgstr "Onnistuneesti päivitettiin jäsen"
-#: members/views.py:168
+#: members/views.py:175
msgid "Could not update member object"
msgstr "Jäsenobjektia ei voitu päivittää"
-#: members/views.py:182
+#: members/views.py:189
msgid "Successfully deleted member"
msgstr "Onnistuneesti poistettiin jäsen"
-#: members/views.py:188
+#: members/views.py:195
msgid "Could not delete member object"
msgstr "Jäsenobjektia ei voitu poistaa"
-#: members/views.py:231 members/views.py:265 members/views.py:283
+#: 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:252
+#: members/views.py:259
msgid "Successfully accepted application"
msgstr "Onnistuneesti hyväksyttiin hakemus"
-#: members/views.py:255
+#: members/views.py:262
msgid "Could not accept application object"
msgstr "Hakemusobjektia ei voitu hyväksyä"
-#: members/views.py:269
+#: members/views.py:276
msgid "Successfully deleted application"
msgstr "Onnistuneesti poistettiin hakemus"
-#: members/views.py:274
+#: members/views.py:281
msgid "Could not delete application object"
msgstr "Hakemusobjektia ei voitu poistaa"
-#: members/views.py:338
+#: members/views.py:345
msgid "Successfully added payment for member"
msgstr "Onnistuneesti lisättiin maksutapahtuma jäsenelle"
-#: members/views.py:351 members/views.py:364 members/views.py:378
+#: 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:382
+#: members/views.py:389
msgid "Successfully deleted payment"
msgstr "Onnistuneesti poistettiin maksutapahtuma"
-#: members/views.py:387
+#: members/views.py:394
msgid "Could not delete payment object"
msgstr "Maksutapahtumaobjektia ei voitu poistaa"
-#: members/views.py:402
+#: members/views.py:409
msgid "Successfully updated payment"
msgstr "Onnistuneesti päivitettiin maksutapahtuma"
-#: members/views.py:405
+#: members/views.py:412
msgid "Could not update payment object"
msgstr "Maksutapahtumaobjektia ei voitu päivittää"
-#: members/views.py:422
+#: members/views.py:429
msgid "Missing \"textfield\" POST request field"
msgstr "Puuttuva \"textfield\" POST-kenttä"
-#: members/views.py:427
+#: members/views.py:434
msgid "Successfully imported multiple members"
msgstr "Onnistuneesti tuotu useita jäseniä"
-#: members/views.py:430
+#: 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"
@@ -485,6 +524,18 @@ msgstr "SIK Hallintapaneeli"
msgid "Aalto-yliopiston Sähköinsinöörikilta ry"
msgstr "Aalto-yliopiston Sähköinsinöörikilta ry"
+#: webapp/templates/login.html:25 webapp/templates/login.html:27
+msgid "Username"
+msgstr "Käyttäjänimi"
+
+#: webapp/templates/login.html:31 webapp/templates/login.html:33
+msgid "Password"
+msgstr "Salasana"
+
+#: webapp/templates/login.html:43
+msgid "Log in"
+msgstr "Kirjaudu sisään"
+
#: webapp/templates/main_index.html:8
msgid "Infoscreen"
msgstr "Infonäyttö"
diff --git a/members/admin.py b/members/admin.py
index bb6b8e0..2251173 100644
--- a/members/admin.py
+++ b/members/admin.py
@@ -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)
diff --git a/members/migrations/0012_memberconflict.py b/members/migrations/0012_memberconflict.py
new file mode 100644
index 0000000..f9ec985
--- /dev/null
+++ b/members/migrations/0012_memberconflict.py
@@ -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')),
+ ],
+ ),
+ ]
diff --git a/members/models.py b/members/models.py
index fcaffeb..1afdde3 100644
--- a/members/models.py
+++ b/members/models.py
@@ -103,7 +103,7 @@ class Member(BaseMember):
latest = payments.latest('date')
date = latest.date
return date
- except DoesNotExist:
+ except Payment.DoesNotExist:
return None
@staticmethod
@@ -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
diff --git a/members/serializers.py b/members/serializers.py
new file mode 100644
index 0000000..8273291
--- /dev/null
+++ b/members/serializers.py
@@ -0,0 +1,10 @@
+from rest_framework import serializers
+from members.models import Member
+
+
+class MemberSerializer(serializers.ModelSerializer):
+ paid = serializers.DateTimeField(source='last_paid')
+
+ class Meta:
+ model = Member
+ fields = ('id', 'first_name', 'last_name', 'email', 'POR', 'AYY', 'jas', 'created', 'paid')
diff --git a/members/static/css/members.css b/members/static/css/members.css
index 5f560f5..9cea9bd 100644
--- a/members/static/css/members.css
+++ b/members/static/css/members.css
@@ -1,86 +1,115 @@
html, body {
- font-size: 14px;
- width: 100%;
+ font-size: 14px;
+ width: 100%;
}
div {
- padding: 0.5rem;
+ padding: 0.5rem;
}
input {
- padding: 0.5rem;
- margin-top: 0.5rem;
- margin-bottom: 0.5rem;
+ padding: 0.5rem;
+ margin-top: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+#header {
+ display: block;
}
#wrapper {
- padding-right: 0;
- padding-top: 0;
- padding-bottom: 0;
+ padding-right: 0;
+ padding-top: 0;
+ padding-bottom: 0;
}
/* fixes for the sidebar layout */
#sidebar-wrapper {
- background: #202020;
- top: 0;
- padding: 0;
-
+ background: #202020;
+ top: 0;
+ padding: 0;
+ -webkit-transition: width 0.01s height 0.01s linear;
+ -moz-transition: width 0.01s height 0.01s linear;
+ -o-transition: width 0.01s height 0.01s linear;
+ transition: width 0.01s, height 0.01s, transform 0.01s;
}
.sidebar-nav {
- width: initial;
- left: 0;
- width: 100%;
- padding-top: 0.5rem;
- height: 100%;
- position: relative;
+ width: initial;
+ left: 0;
+ width: 100%;
+ padding-top: 0.5rem;
+ height: 100%;
+ position: relative;
}
.sidebar-nav li ul {
- padding-left: 0px;
+ padding-left: 0px;
}
.sidebar-nav li span {
- user-select: none;
- cursor: default;
+ user-select: none;
+ cursor: default;
+}
+
+/* Override bootstrap default margin-bottom */
+.navbar {
+ margin-bottom: 0;
}
@media (max-width: 767px) {
+ table {
+ table-layout: fixed;
+ }
- table {
- table-layout: fixed;
- }
+ #sidebar-wrapper {
+ position: initial;
+ width: 100%;
+ margin-left: 0;
+ text-align: center;
+ }
- #sidebar-wrapper {
- position: initial;
- width: 100%;
- margin-left: 0;
- text-align: center;
- }
+ #sidebar-wrapper ul li {
+ text-align: center;
+ }
- #sidebar-wrapper ul li {
- text-align: center;
- }
+ #header {
+ display: none;
+ }
- .logout-container {
- position: initial;
- margin-bottom: 1rem;
- }
+ .sidebar-nav {
+ display: none;
+ }
- #wrapper {
- width: 100%;
- }
+ #sidebar-collapse {
+ display: block;
+ }
- /* Force table to not be like tables anymore */
+ .logout-container {
+ position: initial;
+ margin-bottom: 1rem;
+ }
+
+ #wrapper {
+ width: 100%;
+ }
+
+ .page-content-container {
+ padding: 0;
+ }
+
+ /* Force table to not be like tables anymore */
table, thead, tbody, th, td, tr {
display: block;
}
thead tr {
- background-color: #CCCCCC;
+ background-color: #CCCCCC;
}
- tr { border: 1px solid #ccc; }
+ tr {
+ border: 1px solid #ccc;
+ }
td {
/* Behave like a "row" */
@@ -103,113 +132,135 @@ input {
}
@media (min-width: 769px) {
- .logout-container {
- bottom: 1rem;
- position: absolute;
- width: 100%;
- }
+ .logout-container {
+ bottom: 1rem;
+ position: absolute;
+ width: 100%;
+ }
- .logout-container input {
- width: 80%;
- }
+ .logout-container input {
+ width: 80%;
+ }
- #settings-button {
- position: absolute;
- bottom: 0;
- }
+ #settings-button {
+ position: absolute;
+ bottom: 0;
+ }
+
+ #sidebar-collapse {
+ display: none;
+ }
}
#download-csv {
- margin-left: 20px;
+ margin-left: 20px;
}
#header h1 {
- margin-bottom: 5rem;
- margin-top: 2rem;
- text-align: center;
+ margin-bottom: 5rem;
+ margin-top: 2rem;
+ text-align: center;
}
.table-button {
- margin-left: 5px;
- margin-right: 5px;
- margin-bottom: 5px;
+ margin-left: 5px;
+ margin-right: 5px;
+ margin-bottom: 5px;
}
.table-button-column {
- text-align: right;
+ text-align: right;
}
.pagination li {
- padding: 0.5rem;
- margin: 0.5rem;
+ padding: 0.5rem;
+ margin: 0.5rem;
}
#filter-collapser {
- float: right;
- margin-left: 1rem;
+ float: right;
+ margin-left: 1rem;
}
.filter-form {
}
.filter-row {
- float: right;
- display: table;
- background-color: rgba(122, 164, 232, 0.5);
+ float: right;
+ display: table;
+ background-color: rgba(122, 164, 232, 0.5);
}
.filter-group {
- display: inline-block;
- float: left;
- height: 100%;
+ display: inline-block;
+ float: left;
+ height: 100%;
}
.filter-field {
- display: table;
- clear: both;
- width: 100%;
- margin-left: 0;
- margin-right: 0;
+ display: table;
+ clear: both;
+ width: 100%;
+ margin-left: 0;
+ margin-right: 0;
}
.filter-field h5 {
- display: inline;
+ display: inline;
}
.filter-field input[type="datetime-local"] {
- display: inline;
- width: 50%;
- float: right;
+ display: inline;
+ width: 50%;
+ float: right;
}
.filter-button {
- margin: 0.5rem;
- display: block;
- width: 100%;
+ margin: 0.5rem;
+ display: block;
+ width: 100%;
}
.filter-field #search-filter {
- width: 100%;
+ width: 100%;
}
.inline-title {
- display: inline;
+ display: inline;
}
.ellipsis-menu {
- height: 2rem;
+ height: 2rem;
}
.data-table-button {
- width: 100%;
+ width: 100%;
}
.readonly {
- pointer-events: none;
+ pointer-events: none;
}
.large-textarea {
- width: 100%;
- max-width: 100%;
- height: 10rem !important;
-}
\ No newline at end of file
+ width: 100%;
+ 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 {
+}
diff --git a/members/templates/member_duplicates.html b/members/templates/member_duplicates.html
new file mode 100644
index 0000000..c6f2995
--- /dev/null
+++ b/members/templates/member_duplicates.html
@@ -0,0 +1,40 @@
+{% 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 }}
+
+
+
+
+ {% endfor %}
+
+
+{% endblock content %}
diff --git a/members/templates/member_list.html b/members/templates/member_list.html
index 221764f..18bc6cb 100644
--- a/members/templates/member_list.html
+++ b/members/templates/member_list.html
@@ -11,6 +11,13 @@
{% trans "Member register" %}
+ {% if is_member_conflict %}
+
+ {% blocktrans %}There are duplicate member entries in the register.
+ Please visit
duplicate resolver.{% endblocktrans %}
+
+ {% endif %}
+
{% if notification %}
{{ notification }}
diff --git a/members/templates/members_base.html b/members/templates/members_base.html
index 8e25f67..3957588 100644
--- a/members/templates/members_base.html
+++ b/members/templates/members_base.html
@@ -37,6 +37,16 @@
diff --git a/members/throttles.py b/members/throttles.py
new file mode 100644
index 0000000..0c1e17b
--- /dev/null
+++ b/members/throttles.py
@@ -0,0 +1,9 @@
+from rest_framework.throttling import UserRateThrottle
+
+
+class BurstRateThrottle(UserRateThrottle):
+ scope = 'burst'
+
+
+class SustainedRateThrottle(UserRateThrottle):
+ scope = 'sustained'
diff --git a/members/urls.py b/members/urls.py
index de099b3..9738af6 100644
--- a/members/urls.py
+++ b/members/urls.py
@@ -16,6 +16,11 @@ 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
# application
from members.views import application_form
@@ -96,9 +101,13 @@ urlpatterns = [
# favourite icon
url(r'^favicon\.ico$', favicon_view),
- # email validation
- # url(r'^validate/(?P[0-9A-Za-z_\-\']+)/(?P[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),
+ # 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 6fa608a..49545a8 100644
--- a/members/views.py
+++ b/members/views.py
@@ -7,20 +7,27 @@ from django.core.mail import send_mail
from django.conf import settings
from django.utils.translation import ugettext as _
-'''Email validation'''
+# Email validation
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils.http import urlsafe_base64_encode
from django.utils.encoding import force_bytes
from django.core.mail import send_mail
+# REST framework
+from members.serializers import MemberSerializer
+from rest_framework import generics
+from rest_framework import permissions
+from rest_framework.throttling import UserRateThrottle, AnonRateThrottle
+
import json
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
@@ -99,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)
@@ -449,27 +457,106 @@ 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,
- settings.DEFAULT_EMAIL_FROM,
- [email_to],
- fail_silently=False
- )
+ send_mail(subject,
+ message,
+ settings.DEFAULT_EMAIL_FROM,
+ [email_to],
+ fail_silently=False)
@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)
+ if not settings.ENABLE_AUTOMATIC_EMAILS:
+ return
+
+ 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)
+ if not settings.ENABLE_AUTOMATIC_EMAILS:
+ return
+
+ 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
+class MemberDetail(generics.RetrieveAPIView):
+ queryset = Member.objects.all()
+ serializer_class = MemberSerializer
+ permission_classes = (permissions.IsAdminUser, )
+ throttle_classes = (UserRateThrottle, AnonRateThrottle, )
diff --git a/sikweb/settings-sample.py b/sikweb/settings-sample.py
index af8aec8..755f6c4 100644
--- a/sikweb/settings-sample.py
+++ b/sikweb/settings-sample.py
@@ -138,6 +138,7 @@ REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
'rest_framework.permissions.DjangoModelPermissions',
+ 'rest_framework.permissions.IsAdminUser',
),
'DEFAULT_THROTTLE_CLASSES': (
'members.throttles.BurstRateThrottle',
@@ -157,7 +158,7 @@ EMAIL_PORT = 587
EMAIL_HOST_USER = '@gmail.com'
EMAIL_HOST_PASSWORD = ''
DEFAULT_EMAIL_FROM = 'SIK Viestintä '
-
+ENABLE_AUTOMATIC_EMAILS = False
# ReCaptcha
# http://www.yaconiello.com/blog/integrating-google-recaptcha-to-django/
diff --git a/templates/about.html b/templates/about.html
index 76391ad..7faf8f3 100644
--- a/templates/about.html
+++ b/templates/about.html
@@ -3,13 +3,10 @@
{% load static %}
-
+
SIKWEB 2.0
- Git revision
- {{ REVISION }}
- Git tag
{{ TAG }}
diff --git a/webapp/static/css/login.css b/webapp/static/css/login.css
index 5c16cf1..8d20848 100644
--- a/webapp/static/css/login.css
+++ b/webapp/static/css/login.css
@@ -1,7 +1,34 @@
+html {
+ width: 100%;
+ height: 100%;
+}
+
+@media (max-width: 760px) {
+ html {
+ font-size: 24px;
+ }
+}
+
+@media (min-width: 760px) {
+ html {
+ font-size: 16px;
+ }
+}
+
+body {
+ max-width: 760px;
+ margin: auto;
+ font-size: 1rem;
+}
+
+h1 {
+ padding-bottom: 3rem;
+ padding-top: 3rem;
+ font-size: 3rem;
+}
+
#content-body {
- width: 50vw;
- max-width: 800px;
- margin: 40px auto;
+ width: 100%;
}
#login-button {
diff --git a/webapp/templates/login.html b/webapp/templates/login.html
index 8688195..7d2d763 100644
--- a/webapp/templates/login.html
+++ b/webapp/templates/login.html
@@ -5,8 +5,8 @@
-
SIK - Login
+
@@ -19,18 +19,18 @@