Compare commits
10 Commits
master
...
feat-membe
Author | SHA1 | Date | |
---|---|---|---|
9139d1f81a | |||
b8bd3facc3 | |||
168a8c17bc | |||
c8841f10b6 | |||
bac810cb39 | |||
990b707cd7 | |||
59b9bddd8a | |||
3dfea0f8a0 | |||
05c3440a33 | |||
2fc2fb43e1 |
@ -25,6 +25,7 @@ DEBUG = True
|
|||||||
|
|
||||||
ALLOWED_HOSTS = []
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
|
ORG_NAME = 'Society'
|
||||||
PROMO_LOGO_URL = 'https://placehold.it/2000x500'
|
PROMO_LOGO_URL = 'https://placehold.it/2000x500'
|
||||||
PROMO_LOGO_LINK = 'https://example.com'
|
PROMO_LOGO_LINK = 'https://example.com'
|
||||||
PROMO_GROUPS_MANDATORY = ['All Years']
|
PROMO_GROUPS_MANDATORY = ['All Years']
|
||||||
@ -121,6 +122,7 @@ AUTHENTICATION_BACKENDS = (
|
|||||||
)
|
)
|
||||||
LOGIN_URL = 'login'
|
LOGIN_URL = 'login'
|
||||||
LOGIN_REDIRECT_URL = 'index'
|
LOGIN_REDIRECT_URL = 'index'
|
||||||
|
|
||||||
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = None # FIXME
|
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = None # FIXME
|
||||||
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = None # FIXME
|
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = None # FIXME
|
||||||
#SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS = {'hd': 'monash.edu'} # To restrict to a particular domain
|
#SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS = {'hd': 'monash.edu'} # To restrict to a particular domain
|
||||||
@ -128,6 +130,13 @@ SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = None # FIXME
|
|||||||
GOOGLE_API_KEY = None # FIXME
|
GOOGLE_API_KEY = None # FIXME
|
||||||
GOOGLE_CALENDAR_ID = None # FIXME
|
GOOGLE_CALENDAR_ID = None # FIXME
|
||||||
|
|
||||||
|
AWS_KEY_ID = None # FIXME
|
||||||
|
AWS_SECRET = None # FIXME
|
||||||
|
AWS_REGION = 'us-east-1'
|
||||||
|
AWS_SENDER_EMAIL = 'postmaster@example.com' # FIXME
|
||||||
|
|
||||||
|
RATELIMIT_KEY = 'ip' # https://django-ratelimit.readthedocs.io/en/stable/keys.html#common-keys e.g. 'header:CF-Connecting-IP'
|
||||||
|
|
||||||
SOCIAL_AUTH_PIPELINE = (
|
SOCIAL_AUTH_PIPELINE = (
|
||||||
'social_core.pipeline.social_auth.social_details',
|
'social_core.pipeline.social_auth.social_details',
|
||||||
'social_core.pipeline.social_auth.social_uid',
|
'social_core.pipeline.social_auth.social_uid',
|
||||||
|
1
ssmembership/jinja2/ssmembership/email/base.html
Symbolic link
1
ssmembership/jinja2/ssmembership/email/base.html
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../../../sspromotions/jinja2/sspromotions/email/base.html
|
59
ssmembership/jinja2/ssmembership/email/import.html
Normal file
59
ssmembership/jinja2/ssmembership/email/import.html
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
{% extends 'ssmembership/email/base.html' %}
|
||||||
|
|
||||||
|
{#
|
||||||
|
Society Self-Service
|
||||||
|
Copyright © 2018-2019 Yingtong Li (RunasSudo)
|
||||||
|
|
||||||
|
Design by SendWithUs (Apache 2.0 licence)
|
||||||
|
|
||||||
|
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 %}
|
||||||
|
<tr> <!-- main Email content -->
|
||||||
|
<th class="small-12 large-12 columns first last">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<b><h5>Membership renewal</h5></b>
|
||||||
|
<p>Dear {{ name }},</p>
|
||||||
|
<p>From 2019, {{ import('django.conf').settings.ORG_NAME }} is required by law to review its membership annually. You can renew your membership for free by clicking the button below or visiting <a href="{{ baseurl }}{{ url('mimport_index') }}">{{ baseurl }}{{ url('mimport_index') }}</a>. The process is very quick and should take less than a minute.</p>
|
||||||
|
<p>By making sure your details are up to date, you'll also be able to receive personalised weekly emails with relevant news and events from around the Monash Medicine community.</p>
|
||||||
|
<div class="button">
|
||||||
|
<a href="{{ baseurl }}{{ renew_url }}" style="background-color:#f7931d;border:0px solid #f7931d;border-radius:3px;color:#ffffff;display:inline-block;font-family:sans-serif;font-size:16px;font-weight:bold;line-height:35px;text-align:center;text-decoration:none;width:300px;-webkit-text-size-adjust:none;mso-hide:all;">Renew membership for free</a>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<p>If you do not renew your membership by <b>31 March 2019</b>, your membership will expire, and you will not be able to buy tickets to {{ import('django.conf').settings.ORG_NAME }} events at member prices or run for election within {{ import('django.conf').settings.ORG_NAME }} without paying a membership fee. Please make sure to renew your membership by 31 March 2019 to avoid this.</p>
|
||||||
|
<p>If you do not want to renew your membership, or you are no longer a Monash medical student, simply ignore this email.</p>
|
||||||
|
<p>If you encounter any issues renewing your membership, or have any other questions, please contact the Secretary, Yingtong Li, at <a href="mailto:{{ import('django.conf').settings.AWS_SENDER_EMAIL }}">{{ import('django.conf').settings.AWS_SENDER_EMAIL }}</a>.</p>
|
||||||
|
<p style="font-size: x-small;">Please note that emails are being sent in stages. If other students have not received this email, please let them know that this is normal, and they should receive their email within 7 days. Otherwise, contact <a href="mailto:{{ import('django.conf').settings.AWS_SENDER_EMAIL }}">{{ import('django.conf').settings.AWS_SENDER_EMAIL }}</a>.</p>
|
||||||
|
</th>
|
||||||
|
<th class="expander"></th>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr> <!-- This container adds whitespace gap at the bottom of main content -->
|
||||||
|
<th class="small-12 large-12 columns first last">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
 
|
||||||
|
</th>
|
||||||
|
<th class="expander"></th>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
{% endblock content %}
|
35
ssmembership/jinja2/ssmembership/email/import.txt
Normal file
35
ssmembership/jinja2/ssmembership/email/import.txt
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{#
|
||||||
|
Society Self-Service
|
||||||
|
Copyright © 2018-2019 Yingtong Li (RunasSudo)
|
||||||
|
|
||||||
|
Design by SendWithUs (Apache 2.0 licence)
|
||||||
|
|
||||||
|
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/>.
|
||||||
|
#}
|
||||||
|
Dear {{ name }},
|
||||||
|
|
||||||
|
From 2019, {{ import('django.conf').settings.ORG_NAME }} is required by law to review its membership annually. You can renew your membership for free by going to the link below or visiting {{ baseurl }}{{ url('mimport_index') }}. The process is very quick and should take less than a minute.
|
||||||
|
|
||||||
|
By making sure your details are up to date, you'll also be able to receive personalised weekly emails with relevant news and events from around the Monash Medicine community.
|
||||||
|
|
||||||
|
Renew membership for free:
|
||||||
|
{{ baseurl }}{{ renew_url }}
|
||||||
|
|
||||||
|
If you do not renew your membership by **20 March 2019**, your membership will expire, and you will not be able to buy tickets to {{ import('django.conf').settings.ORG_NAME }} events at member prices or run for election within {{ import('django.conf').settings.ORG_NAME }} without paying a membership fee. Please make sure to renew your membership by 20 March 2019 to avoid this.
|
||||||
|
|
||||||
|
If you do not want to renew your membership, or you are no longer a Monash medical student, simply ignore this email.
|
||||||
|
|
||||||
|
If you encounter any issues renewing your membership, or have any other questions, please contact the Secretary, Yingtong Li, at {{ import('django.conf').settings.AWS_SENDER_EMAIL }}.
|
||||||
|
|
||||||
|
Please note that emails are being sent in stages. If other students have not received this email, please let them know that this is normal, and they should receive their email within 7 days. Otherwise, contact {{ import('django.conf').settings.AWS_SENDER_EMAIL }}.
|
29
ssmembership/jinja2/ssmembership/import/complete.html
Normal file
29
ssmembership/jinja2/ssmembership/import/complete.html
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{% 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 title %}Membership renewal complete{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Membership renewal complete</h1>
|
||||||
|
|
||||||
|
<p>Your membership renewal has been successfully processed.</p>
|
||||||
|
|
||||||
|
<p>You can view and edit your membership details by <a href="{{ url('membership') }}">logging in</a>.</p>
|
||||||
|
{% endblock %}
|
47
ssmembership/jinja2/ssmembership/import/index.html
Normal file
47
ssmembership/jinja2/ssmembership/import/index.html
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{% 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 title %}Membership renewal{% endblock %}
|
||||||
|
|
||||||
|
{% 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 %}
|
122
ssmembership/jinja2/ssmembership/import/review.html
Normal file
122
ssmembership/jinja2/ssmembership/import/review.html
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
{% 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 title %}Membership renewal{% endblock %}
|
||||||
|
|
||||||
|
{% 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') }}">
|
||||||
|
{% if errors %}
|
||||||
|
<div class="ui visible error message"><ul>
|
||||||
|
{% for error in errors %}
|
||||||
|
<li>{{ error }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul></div>
|
||||||
|
{% endif %}
|
||||||
|
<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>
|
||||||
|
<h2>MUMUS Mail</h2>
|
||||||
|
|
||||||
|
<div class="ui required inline grid field">
|
||||||
|
<label class="three wide column">Opt-in/out</label>
|
||||||
|
<select id="drop_bulletin_subscribe" class="ui dropdown eleven wide column" name="bulletin_subscribe">
|
||||||
|
<option value="0">Do not send me MUMUS Mail (not recommended)</option>
|
||||||
|
<option value="1" selected>Keep me updated with MUMUS Mail</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="ui inline grid field">
|
||||||
|
<label class="three wide column">Subscriptions</label>
|
||||||
|
<div class="eleven wide column">
|
||||||
|
{% for group in import('sspromotions.models').Group.objects.all() %}
|
||||||
|
{% if group.subscribable %}
|
||||||
|
<div class="field" style="display: inline; margin-right: 1em;">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input type="checkbox" name="bulletin_group_{{ group.id }}" id="bulletin_group_{{ group.id }}">
|
||||||
|
<label for="bulletin_group_{{ group.id }}">{{ group.name }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<p style="margin-top: 0.5em;">MUMUS Mail is personalised for you. Choose the groups that you would like to see first in each edition of MUMUS Mail.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<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', '{{ 1 if member.is_msa else 0 }}');
|
||||||
|
</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>
|
||||||
|
|
||||||
|
107
ssmembership/management/commands/sendimportemail.py
Normal file
107
ssmembership/management/commands/sendimportemail.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
# 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 boto3
|
||||||
|
from botocore.exceptions import ClientError
|
||||||
|
import premailer
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.template import loader
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from ssmembership.mimport import get_members
|
||||||
|
|
||||||
|
import hmac
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Send emails for membership import (renewal)'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('ids', nargs='*', type=int, help='Members with ID numbers equal to these values will be emailed (default all)')
|
||||||
|
parser.add_argument('--render', action='store_true', help='Render to stdout instead of sending emails')
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
template_html = loader.get_template('ssmembership/email/import.html')
|
||||||
|
template_txt = loader.get_template('ssmembership/email/import.txt')
|
||||||
|
|
||||||
|
members = get_members()
|
||||||
|
|
||||||
|
if len(options['ids']) > 0:
|
||||||
|
members = [member for member in members if member[0] in options['ids']]
|
||||||
|
else:
|
||||||
|
raise Exception('Must provide IDs')
|
||||||
|
|
||||||
|
client = boto3.client('ses', aws_access_key_id=settings.AWS_KEY_ID, aws_secret_access_key=settings.AWS_SECRET, region_name=settings.AWS_REGION)
|
||||||
|
|
||||||
|
def send_mail(**kwargs):
|
||||||
|
for i in range(0, 10):
|
||||||
|
try:
|
||||||
|
client.send_email(**kwargs)
|
||||||
|
return
|
||||||
|
except ClientError as e:
|
||||||
|
if e['Error']['Code'] == 'Throttling' and e['Error']['Message'] == 'Maximum sending rate exceeded.':
|
||||||
|
wait_time = max(10*(2**i), 5000)
|
||||||
|
self.stdout.write(self.style.NOTICE('Reached maximum sending rate, waiting {} ms'.format(wait_time)))
|
||||||
|
time.sleep(wait_time/1000)
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
raise Exception('Reached maximum number of retries')
|
||||||
|
|
||||||
|
for member in members:
|
||||||
|
#_id, student_id, email, first_name, last_name, year, is_msa, phone, date
|
||||||
|
sig = hmac.new(settings.SECRET_KEY_MEMBERSIG.encode('utf-8'), member[2].encode('utf-8'), 'sha256').hexdigest()
|
||||||
|
renew_url = reverse('mimport_signed') + '?' + urllib.parse.urlencode({'email': member[2], 'sig': sig})
|
||||||
|
|
||||||
|
template_args = {
|
||||||
|
'name': member[3].strip() + ' ' + member[4].strip(),
|
||||||
|
'renew_url': renew_url,
|
||||||
|
'baseurl': 'https://' + settings.ALLOWED_HOSTS[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
content_html = premailer.Premailer(template_html.render(template_args), cssutils_logging_level=logging.ERROR).transform()
|
||||||
|
content_txt = template_txt.render(template_args)
|
||||||
|
|
||||||
|
if options['render']:
|
||||||
|
self.stdout.write(content_html)
|
||||||
|
else:
|
||||||
|
self.stdout.write('Emailing {} at {}'.format(member[0], member[2]))
|
||||||
|
send_mail(
|
||||||
|
Destination={
|
||||||
|
'ToAddresses': [member[2]],
|
||||||
|
},
|
||||||
|
Message={
|
||||||
|
'Body': {
|
||||||
|
'Html': {
|
||||||
|
'Charset': 'utf-8',
|
||||||
|
'Data': content_html,
|
||||||
|
},
|
||||||
|
'Text': {
|
||||||
|
'Charset': 'utf-8',
|
||||||
|
'Data': content_txt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Subject': {
|
||||||
|
'Charset': 'utf-8',
|
||||||
|
'Data': settings.ORG_NAME + ' membership renewal',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Source='{} <{}>'.format(settings.ORG_NAME, settings.AWS_SENDER_EMAIL),
|
||||||
|
)
|
70
ssmembership/mimport.py
Normal file
70
ssmembership/mimport.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# 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 get_members():
|
||||||
|
conn = sqlite3.connect('file:members.db?mode=ro', uri=True)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
cur.execute('SELECT * FROM members')
|
||||||
|
result = cur.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
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=31)
|
||||||
|
member.expires = member.expires.replace(year=member.expires.year+1)
|
||||||
|
if member.expires < timezone.now().date(): # Add 1 year if after Mar 31, 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,98 @@ 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.new(settings.SECRET_KEY_MEMBERSIG.encode('utf-8'), request.GET['email'].encode('utf-8'), 'sha256').hexdigest()
|
||||||
|
if not hmac.compare_digest(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=settings.RATELIMIT_KEY, 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.new(settings.SECRET_KEY_MEMBERSIG.encode('utf-8'), member.email.encode('utf-8'), 'sha256').hexdigest() if member else None
|
||||||
|
})
|
||||||
|
|
||||||
|
def import_save(request):
|
||||||
|
if request.method != 'POST':
|
||||||
|
return redirect(reverse('import_index'))
|
||||||
|
|
||||||
|
sig_expected = hmac.new(settings.SECRET_KEY_MEMBERSIG.encode('utf-8'), request.POST['email_orig'].encode('utf-8'), 'sha256').hexdigest()
|
||||||
|
if not hmac.compare_digest(sig_expected, request.POST['sig']):
|
||||||
|
return HttpResponse('Invalid signature', status=403)
|
||||||
|
|
||||||
|
member = mimport.by_email(request.POST['email_orig'])
|
||||||
|
|
||||||
|
if not member:
|
||||||
|
return render(request, 'ssmembership/import/review.html', {
|
||||||
|
'member': 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 models.Member.objects.filter(email=request.POST['email']).count() > 0:
|
||||||
|
errors.append('Member with this email already exists')
|
||||||
|
|
||||||
|
if 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()
|
||||||
|
|
||||||
|
# Update bulletin
|
||||||
|
import sspromotions.models
|
||||||
|
sspromotions.models.BulletinSubscription.set_member_subscribed(member, True if request.POST['bulletin_subscribe'] == '1' else False)
|
||||||
|
for group in sspromotions.models.Group.objects.filter(subscribable=True).all():
|
||||||
|
if ('bulletin_group_' + str(group.id)) in request.POST and request.POST['bulletin_group_' + str(group.id)]:
|
||||||
|
group.subscribe_member(member, True)
|
||||||
|
else:
|
||||||
|
group.subscribe_member(member, False)
|
||||||
|
|
||||||
|
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…
Reference in New Issue
Block a user