Implement membership import

This commit is contained in:
Yingtong Li 2019-01-17 23:26:17 +11:00
parent fc110e4060
commit 2fc2fb43e1
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
7 changed files with 308 additions and 0 deletions

View File

@ -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 <https://www.gnu.org/licenses/>.
#}
{% block content %}
<h1>Membership renewal</h1>
<p>Your membership renewal has been successful processed!</p>
<p>You can view and edit your membership details by <a href="{{ url('membership') }}">logging in</a>.</p>
{% endblock %}

View File

@ -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 <https://www.gnu.org/licenses/>.
#}
{% block content %}
<h1>Membership renewal</h1>
<p>To renew an existing membership, please enter your details below:</p>
<form class="ui form" method="POST" action="{{ url('mimport_search') }}">
<div class="ui required inline grid field">
<label class="three wide column">Student ID</label>
<div class="nine wide column">
<input type="text" name="student_id" placeholder="28000000">
</div>
</div>
<div class="ui required inline grid field">
<label class="three wide column">Email</label>
<div class="nine wide column">
<input type="text" name="email" placeholder="abcd0001@student.monash.edu">
<div style="margin-top: 1.5em;">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.</div>
</div>
</div>
<div class="ui divider"></div>
<div class="ui error message"></div>
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
<input class="ui primary button" type="submit" name='submit' value="Continue">
</form>
{% endblock %}

View File

@ -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 <https://www.gnu.org/licenses/>.
#}
{% block content %}
<h1>Membership renewal</h1>
{% if not member %}
<p>The details you entered do not match our records, or the membership has already been renewed. <a href="{{ url('mimport_index') }}">Click here</a> to try again.</p>
{% else %}
<p>Please check the following details and update them if necessary:</p>
<form class="ui form" method="POST" action="{{ url('mimport_save') }}">
<div class="ui required inline grid field">
<label class="three wide column">Student ID</label>
<input class="nine wide column" type="text" name="student_id" value="{{ member.student_id }}">
</div>
<div class="ui required inline grid field">
<label class="three wide column">Student email</label>
<input class="nine wide column" type="text" name="email" value="{{ member.email }}">
</div>
<div class="ui divider"></div>
<div class="ui required inline grid field">
<label class="three wide column">First name</label>
<input class="nine wide column" type="text" name="first_name" value="{{ member.first_name }}">
</div>
<div class="ui required inline grid field">
<label class="three wide column">Last name</label>
<input class="nine wide column" type="text" name="last_name" value="{{ member.last_name }}">
</div>
<div class="ui required inline grid field">
<label class="three wide column">Phone number</label>
<input class="nine wide column" type="text" name="phone" value="{{ member.phone }}">
</div>
<div class="ui divider"></div>
<div class="ui required inline grid field">
<label class="three wide column">Year level</label>
<select id="drop_year" class="ui dropdown eleven wide column" name="year">
<option value="">Year level</option>
{% for year in years %}
<option value="{{ year[0] }}">{{ year[1] }}</option>
{% endfor %}
</select>
</div>
<div class="ui required inline grid field">
<label class="three wide column">MSA membership</label>
<select id="drop_msa" class="ui dropdown eleven wide column" name="is_msa">
<option value="">MSA membership</option>
<option value="0">No, I am not an MSA member</option>
<option value="1">Yes, I am an MSA member</option>
</select>
</div>
<div class="ui divider"></div>
{% if errors %}
<div class="ui visible error message"><ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul></div>
{% endif %}
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
<input type="hidden" name="email_orig" value="{{ email_orig }}">
<input type="hidden" name="sig" value="{{ sig }}">
<input class="ui primary button" type="submit" name='submit' value="Continue">
</form>
{% endif %}
{% endblock %}
{% block script %}
{{ super() }}
{% if member %}
<script>
$('.ui.dropdown').dropdown();
$('#drop_year').dropdown('set selected', '{{ member.year }}');
$('#drop_msa').dropdown('set selected', '{{ member.is_msa }}');
</script>
{% endif %}
{% endblock %}

View File

@ -24,6 +24,7 @@
{% if not member %}
<h1>No membership records</h1>
<p>This email address is not associated with a current membership.</p>
<p><a href="{{ url('mimport_index') }}">Click here</a> to renew an existing membership.</p>
{% else %}
<h1>Membership details</h1>

60
ssmembership/mimport.py Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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()

View File

@ -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'),

View File

@ -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')