Implement membership import
This commit is contained in:
parent
fc110e4060
commit
2fc2fb43e1
27
ssmembership/jinja2/ssmembership/import/complete.html
Normal file
27
ssmembership/jinja2/ssmembership/import/complete.html
Normal 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 %}
|
45
ssmembership/jinja2/ssmembership/import/index.html
Normal file
45
ssmembership/jinja2/ssmembership/import/index.html
Normal 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 %}
|
94
ssmembership/jinja2/ssmembership/import/review.html
Normal file
94
ssmembership/jinja2/ssmembership/import/review.html
Normal 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 %}
|
@ -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
60
ssmembership/mimport.py
Normal 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()
|
@ -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'),
|
||||
|
@ -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')
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user