From 2fc2fb43e10ee65ea3d6ff9be4aae7735b55fd1c Mon Sep 17 00:00:00 2001 From: Yingtong Li Date: Thu, 17 Jan 2019 23:26:17 +1100 Subject: [PATCH] Implement membership import --- .../jinja2/ssmembership/import/complete.html | 27 ++++++ .../jinja2/ssmembership/import/index.html | 45 +++++++++ .../jinja2/ssmembership/import/review.html | 94 +++++++++++++++++++ ssmembership/jinja2/ssmembership/index.html | 1 + ssmembership/mimport.py | 60 ++++++++++++ ssmembership/urls.py | 4 + ssmembership/views.py | 77 +++++++++++++++ 7 files changed, 308 insertions(+) create mode 100644 ssmembership/jinja2/ssmembership/import/complete.html create mode 100644 ssmembership/jinja2/ssmembership/import/index.html create mode 100644 ssmembership/jinja2/ssmembership/import/review.html create mode 100644 ssmembership/mimport.py diff --git a/ssmembership/jinja2/ssmembership/import/complete.html b/ssmembership/jinja2/ssmembership/import/complete.html new file mode 100644 index 0000000..068b9b7 --- /dev/null +++ b/ssmembership/jinja2/ssmembership/import/complete.html @@ -0,0 +1,27 @@ +{% extends 'ssmain/base.html' %} + +{# + Society Self-Service + Copyright © 2018-2019 Yingtong Li (RunasSudo) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +#} + +{% block content %} +

Membership renewal

+ +

Your membership renewal has been successful processed!

+ +

You can view and edit your membership details by logging in.

+{% endblock %} diff --git a/ssmembership/jinja2/ssmembership/import/index.html b/ssmembership/jinja2/ssmembership/import/index.html new file mode 100644 index 0000000..d95b8b0 --- /dev/null +++ b/ssmembership/jinja2/ssmembership/import/index.html @@ -0,0 +1,45 @@ +{% extends 'ssmain/base.html' %} + +{# + Society Self-Service + Copyright © 2018-2019 Yingtong Li (RunasSudo) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +#} + +{% block content %} +

Membership renewal

+ +

To renew an existing membership, please enter your details below:

+ +
+
+ +
+ +
+
+
+ +
+ +
Enter the email address that is registered with MUMUS. This is the email which currently receives the MUMUS Bulletin. For most people, this will be your Monash student email; however, some people may have been registered using personal emails.
+
+
+
+
+ + +
+{% endblock %} diff --git a/ssmembership/jinja2/ssmembership/import/review.html b/ssmembership/jinja2/ssmembership/import/review.html new file mode 100644 index 0000000..781c8c9 --- /dev/null +++ b/ssmembership/jinja2/ssmembership/import/review.html @@ -0,0 +1,94 @@ +{% extends 'ssmain/base.html' %} + +{# + Society Self-Service + Copyright © 2018-2019 Yingtong Li (RunasSudo) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +#} + +{% block content %} +

Membership renewal

+ + {% if not member %} +

The details you entered do not match our records, or the membership has already been renewed. Click here to try again.

+ {% else %} +

Please check the following details and update them if necessary:

+ +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ {% if errors %} +
    + {% for error in errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} + + + + +
+ {% endif %} +{% endblock %} + +{% block script %} + {{ super() }} + {% if member %} + + {% endif %} +{% endblock %} diff --git a/ssmembership/jinja2/ssmembership/index.html b/ssmembership/jinja2/ssmembership/index.html index 409ea76..13c2b65 100644 --- a/ssmembership/jinja2/ssmembership/index.html +++ b/ssmembership/jinja2/ssmembership/index.html @@ -24,6 +24,7 @@ {% if not member %}

No membership records

This email address is not associated with a current membership.

+

Click here to renew an existing membership.

{% else %}

Membership details

diff --git a/ssmembership/mimport.py b/ssmembership/mimport.py new file mode 100644 index 0000000..15981da --- /dev/null +++ b/ssmembership/mimport.py @@ -0,0 +1,60 @@ +# Society Self-Service +# Copyright © 2018-2019 Yingtong Li (RunasSudo) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import sqlite3 + +from django.utils import timezone + +from . import models + +import datetime + +def by_email(email): + conn = sqlite3.connect('file:members.db?mode=ro', uri=True) + cur = conn.cursor() + + cur.execute('SELECT * FROM members WHERE email=?', (email,)) + result = cur.fetchone() + conn.close() + + if not result: + return None + + member = models.Member() + + # id, student_id, email, first_name, last_name, year, is_msa, phone, date + member.student_id = result[1] + member.email = result[2] + member.first_name = result[3] + member.last_name = result[4] + member.year = {'Year A': 0, 'Year 1': 1, 'Year 2': 2, 'Year 3B': 3, 'Year 4C': 4, 'Year 5D': 5, 'BMedSci': 97, 'PhD': 98, 'Intermission': 99}[result[5]] + member.is_msa = result[6] + member.phone = result[7] + + # Calculate expiration date + member.expires = timezone.now().date().replace(month=3, day=20) + member.expires = member.expires.replace(year=member.expires.year+1) + if member.expires < timezone.now().date(): # Add 1 year if after Mar 20, else add 2 years + member.expires = member.expires.replace(year=member.expires.year+1) + + return member + +def delete_by_email(email): + conn = sqlite3.connect('file:members.db', uri=True) + cur = conn.cursor() + cur.execute('DELETE FROM members WHERE email=?', (email,)) + conn.commit() + conn.close() diff --git a/ssmembership/urls.py b/ssmembership/urls.py index 81c6cfe..adc1f20 100644 --- a/ssmembership/urls.py +++ b/ssmembership/urls.py @@ -20,6 +20,10 @@ from . import views urlpatterns = [ path('', views.index, name='membership'), + path('import/', views.import_index, name='mimport_index'), + path('import/signed', views.import_signed, name='mimport_signed'), + path('import/search', views.import_search, name='mimport_search'), + path('import/save', views.import_save, name='mimport_save'), path('onboard/', views.onboard_index, name='monboard_index'), path('onboard/signed', views.onboard_signed, name='monboard_signed'), path('onboard/search', views.onboard_search, name='monboard_search'), diff --git a/ssmembership/views.py b/ssmembership/views.py index 10d56a3..69d9e7a 100644 --- a/ssmembership/views.py +++ b/ssmembership/views.py @@ -24,9 +24,12 @@ from django.http import HttpResponse from django.shortcuts import render, redirect from django.urls import reverse +from . import mimport from . import monboard from . import models +import hmac + @login_required def index(request): try: @@ -68,6 +71,80 @@ def index(request): return render(request, 'ssmembership/index.html', {'member': member, 'years': models.Member.YEARS}) +def import_index(request): + return render(request, 'ssmembership/import/index.html') + +def import_signed(request): + if 'email' not in request.GET: + return HttpResponse('Expected an email address', status=400) + if 'sig' not in request.GET: + return HttpResponse('Expected a signature parameter', status=400) + + sig_expected = hmac.digest(settings.SECRET_KEY_MEMBERSIG.encode('utf-8'), request.GET['email'].encode('utf-8'), 'sha256').hex() + if sig_expected != request.GET['sig']: + return HttpResponse('Invalid signature', status=403) + + member = mimport.by_email(request.GET['email']) + return render(request, 'ssmembership/import/review.html', { + 'member': member, + 'years': models.Member.YEARS, + 'email_orig': member.email if member else None, + 'sig': sig_expected + }) + +@ratelimit(key='ip', rate='100/h') +def import_search(request): + if request.method != 'POST': + return redirect(reverse('import_index')) + + if request.limited: + return HttpResponse('Too many requests', status=429) + + member = mimport.by_email(request.POST['email']) + if member and member.student_id != request.POST['student_id']: + member = None + + return render(request, 'ssmembership/import/review.html', { + 'member': member, + 'years': models.Member.YEARS, + 'email_orig': member.email if member else None, + 'sig': hmac.digest(settings.SECRET_KEY_MEMBERSIG.encode('utf-8'), member.email.encode('utf-8'), 'sha256').hex() if member else None + }) + +def import_save(request): + if request.method != 'POST': + return redirect(reverse('import_index')) + + sig_expected = hmac.digest(settings.SECRET_KEY_MEMBERSIG.encode('utf-8'), request.POST['email_orig'].encode('utf-8'), 'sha256').hex() + if sig_expected != request.POST['sig']: + return HttpResponse('Invalid signature', status=403) + + member = mimport.by_email(request.POST['email_orig']) + + if member: + member.student_id = request.POST['student_id'] + member.email = request.POST['email'] + member.first_name = request.POST['first_name'] + member.last_name = request.POST['last_name'] + member.phone = request.POST['phone'] + member.year = int(request.POST['year']) + member.is_msa = True if request.POST['is_msa'] == '1' else '0' + + errors = member.validation_problems() + if not member or len(errors) > 0: + return render(request, 'ssmembership/import/review.html', { + 'member': member, + 'years': models.Member.YEARS, + 'email_orig': request.POST['email_orig'], + 'sig': request.POST['sig'], + 'errors': errors + }) + + with transaction.atomic(): + member.save() + mimport.delete_by_email(request.POST['email_orig']) + return render(request, 'ssmembership/import/complete.html') + def onboard_index(request): return render(request, 'ssmembership/onboard/index.html')