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 }} +
+
+
+
{% csrf_token %} +

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

+ + + + +
+
+
+ {% 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 @@
-

SIK Admin

+

SIK Admin

{% csrf_token %}
- +
- +
- +
- +
@@ -40,7 +40,7 @@
- +