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:
+
+
+{% 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:
+
+
+ {% 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')