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 %}
|
{% if not member %}
|
||||||
<h1>No membership records</h1>
|
<h1>No membership records</h1>
|
||||||
<p>This email address is not associated with a current membership.</p>
|
<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 %}
|
{% else %}
|
||||||
<h1>Membership details</h1>
|
<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 = [
|
urlpatterns = [
|
||||||
path('', views.index, name='membership'),
|
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/', views.onboard_index, name='monboard_index'),
|
||||||
path('onboard/signed', views.onboard_signed, name='monboard_signed'),
|
path('onboard/signed', views.onboard_signed, name='monboard_signed'),
|
||||||
path('onboard/search', views.onboard_search, name='monboard_search'),
|
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.shortcuts import render, redirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from . import mimport
|
||||||
from . import monboard
|
from . import monboard
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
import hmac
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def index(request):
|
def index(request):
|
||||||
try:
|
try:
|
||||||
@ -68,6 +71,80 @@ def index(request):
|
|||||||
|
|
||||||
return render(request, 'ssmembership/index.html', {'member': member, 'years': models.Member.YEARS})
|
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):
|
def onboard_index(request):
|
||||||
return render(request, 'ssmembership/onboard/index.html')
|
return render(request, 'ssmembership/onboard/index.html')
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user