480 lines
15 KiB
Python
480 lines
15 KiB
Python
import logging
|
|
logger = logging.getLogger(__name__)
|
|
|
|
import io
|
|
import json
|
|
import requests
|
|
import time
|
|
from datetime import datetime, timedelta
|
|
from rest_framework.exceptions import ValidationError
|
|
from dateutil import relativedelta
|
|
from uuid import uuid4
|
|
from PIL import Image, ImageDraw, ImageFont, ImageOps
|
|
from bleach.sanitizer import Cleaner
|
|
from PyPDF2 import PdfFileWriter, PdfFileReader
|
|
from reportlab.pdfgen import canvas
|
|
from reportlab.lib.pagesizes import letter
|
|
|
|
from django.db.models import Sum
|
|
from django.core.cache import cache
|
|
from django.utils.timezone import now, pytz
|
|
|
|
from . import models, serializers, utils_ldap, utils_stats, utils_auth, utils
|
|
|
|
STATIC_FOLDER = 'data/static/'
|
|
|
|
|
|
def today_alberta_tz():
|
|
return datetime.now(pytz.timezone('America/Edmonton')).date()
|
|
|
|
def now_alberta_tz():
|
|
return datetime.now(pytz.timezone('America/Edmonton'))
|
|
|
|
def alert_tanner(message):
|
|
try:
|
|
logger.info('Alerting Tanner: ' + message)
|
|
params = dict(spaceport=message)
|
|
requests.get('https://tbot.tannercollin.com/message', params=params, timeout=4)
|
|
except BaseException as e:
|
|
logger.error('Problem alerting Tanner: ' + str(e))
|
|
|
|
def num_months_spanned(d1, d2):
|
|
'''
|
|
Return number of month thresholds two dates span.
|
|
Order of arguments is same as subtraction
|
|
ie. Feb 2, Jan 29 returns 1
|
|
'''
|
|
return (d1.year - d2.year) * 12 + d1.month - d2.month
|
|
|
|
def num_months_difference(d1, d2):
|
|
'''
|
|
Return number of whole months between two dates.
|
|
Order of arguments is same as subtraction
|
|
ie. Feb 2, Jan 29 returns 0
|
|
'''
|
|
r = relativedelta.relativedelta(d1, d2)
|
|
return r.months + 12 * r.years
|
|
|
|
def calc_member_status(expire_date, fake_date=None):
|
|
'''
|
|
Return: member status
|
|
'''
|
|
today = fake_date or today_alberta_tz()
|
|
|
|
difference = num_months_difference(expire_date, today)
|
|
|
|
if today + timedelta(days=29) < expire_date:
|
|
return 'Prepaid'
|
|
elif difference <= -3:
|
|
return 'Former Member'
|
|
elif today - timedelta(days=29) >= expire_date:
|
|
return 'Overdue'
|
|
elif today < expire_date:
|
|
return 'Current'
|
|
elif today >= expire_date:
|
|
return 'Due'
|
|
else:
|
|
raise()
|
|
|
|
def add_months(date, num_months):
|
|
return date + relativedelta.relativedelta(months=num_months)
|
|
|
|
def fake_missing_membership_months(member):
|
|
'''
|
|
Add fake months on importing the member so the length of their membership
|
|
resolves to their imported expiry date
|
|
'''
|
|
start_date = member.current_start_date
|
|
expire_date = member.expire_date
|
|
|
|
missing_months = num_months_spanned(expire_date, start_date)
|
|
|
|
user = member.user if member.user else None
|
|
tx = False
|
|
for i in range(missing_months):
|
|
memo = '{} / {} month membership dues accounting old portal import, {} to {} - hidden'.format(
|
|
str(i+1), str(missing_months), start_date, expire_date
|
|
)
|
|
|
|
tx = models.Transaction.objects.create(
|
|
amount=0,
|
|
user=user,
|
|
memo=memo,
|
|
member_id=member.id,
|
|
reference_number='',
|
|
info_source='System',
|
|
payment_method='N/A',
|
|
category='Memberships:Fake Months',
|
|
account_type='Clearing',
|
|
number_of_membership_months=1,
|
|
date=add_months(start_date, i),
|
|
)
|
|
|
|
return tx, missing_months
|
|
|
|
def tally_membership_months(member, fake_date=None):
|
|
'''
|
|
Sum together member's dues and calculate their new expire date and status
|
|
Doesn't work if member is paused.
|
|
'''
|
|
if member.paused_date: return False
|
|
|
|
start_date = member.current_start_date
|
|
if not start_date: return False
|
|
|
|
txs = models.Transaction.objects.filter(
|
|
member_id=member.id,
|
|
date__gte=start_date,
|
|
)
|
|
total_months_agg = txs.aggregate(Sum('number_of_membership_months'))
|
|
total_months = total_months_agg['number_of_membership_months__sum'] or 0
|
|
|
|
expire_date = add_months(start_date, total_months)
|
|
status = calc_member_status(expire_date, fake_date)
|
|
|
|
member.expire_date = expire_date
|
|
member.status = status
|
|
|
|
if status == 'Former Member':
|
|
member.paused_date = expire_date
|
|
|
|
member.save()
|
|
return True
|
|
|
|
|
|
def gen_search_strings():
|
|
'''
|
|
Generate a cache dict of names to member ids for rapid string matching
|
|
'''
|
|
search_strings = {}
|
|
for m in models.Member.objects.order_by('-expire_date'):
|
|
string = '{} {} | {} {}'.format(
|
|
m.preferred_name,
|
|
m.last_name,
|
|
m.first_name,
|
|
m.last_name,
|
|
)
|
|
|
|
if m.old_email:
|
|
string += ' | ' + m.old_email
|
|
if m.user:
|
|
string += ' | ' + m.user.email
|
|
string += ' | ' + str(m.id)
|
|
|
|
string = string.lower()
|
|
search_strings[string] = m.id
|
|
cache.set('search_strings', search_strings)
|
|
|
|
|
|
LARGE_SIZE = 1080
|
|
MEDIUM_SIZE = 220
|
|
SMALL_SIZE = 110
|
|
|
|
def process_image_upload(upload, crop):
|
|
'''
|
|
Save an image upload in small, medium, large sizes and return filenames
|
|
'''
|
|
try:
|
|
pic = Image.open(upload)
|
|
except OSError:
|
|
raise serializers.ValidationError('Invalid image file.')
|
|
|
|
if pic.format == 'PNG':
|
|
ext = '.png'
|
|
elif pic.format == 'JPEG':
|
|
ext = '.jpg'
|
|
else:
|
|
raise serializers.ValidationError('Image must be a jpg or png.')
|
|
|
|
pic = ImageOps.exif_transpose(pic)
|
|
|
|
if crop:
|
|
crop = json.loads(crop)
|
|
pic_x, pic_y = pic.size
|
|
left = pic_x * crop['x']/100.0
|
|
top = pic_y * crop['y']/100.0
|
|
right = left + pic_x * crop['width']/100.0
|
|
bottom = top + pic_y * crop['height']/100.0
|
|
pic = pic.crop((left, top, right, bottom))
|
|
|
|
large = str(uuid4()) + ext
|
|
pic.thumbnail([LARGE_SIZE, LARGE_SIZE], Image.ANTIALIAS)
|
|
pic.save(STATIC_FOLDER + large)
|
|
|
|
medium = str(uuid4()) + ext
|
|
pic.thumbnail([MEDIUM_SIZE, MEDIUM_SIZE], Image.ANTIALIAS)
|
|
pic.save(STATIC_FOLDER + medium)
|
|
|
|
small = str(uuid4()) + ext
|
|
pic.thumbnail([SMALL_SIZE, SMALL_SIZE], Image.ANTIALIAS)
|
|
pic.save(STATIC_FOLDER + small)
|
|
|
|
return small, medium, large
|
|
|
|
|
|
CARD_TEMPLATE_FILE = 'misc/member_card_template.jpg'
|
|
CARD_PHOTO_SIZE = 425
|
|
CARD_PHOTO_MARGIN_TOP = 75
|
|
CARD_PHOTO_MARGIN_SIDE = 30
|
|
CARD_TEXT_SIZE_LIMIT = 550
|
|
|
|
def gen_card_photo(member):
|
|
card_template = Image.open(CARD_TEMPLATE_FILE)
|
|
|
|
member_photo = Image.open(STATIC_FOLDER + member.photo_large)
|
|
member_photo.thumbnail([CARD_PHOTO_SIZE, CARD_PHOTO_SIZE], Image.ANTIALIAS)
|
|
member_photo = ImageOps.expand(member_photo, border=10)
|
|
mx, my = member_photo.size
|
|
|
|
x = CARD_PHOTO_MARGIN_SIDE
|
|
y = CARD_PHOTO_MARGIN_TOP
|
|
card_template.paste(member_photo, (x, y))
|
|
|
|
draw = ImageDraw.Draw(card_template)
|
|
|
|
# check font size
|
|
font_sizes = (60, 72)
|
|
font = ImageFont.truetype('DejaVuSans-Bold.ttf', font_sizes[1])
|
|
size = draw.textsize(str(member.last_name), font=font)
|
|
if size[0] > CARD_TEXT_SIZE_LIMIT:
|
|
font_sizes = (36, 48)
|
|
|
|
font = ImageFont.truetype('DejaVuSans.ttf', font_sizes[0])
|
|
x = CARD_PHOTO_MARGIN_SIDE
|
|
y = my + CARD_PHOTO_MARGIN_TOP + CARD_PHOTO_MARGIN_SIDE
|
|
draw.text((x, y), str(member.first_name), (0,0,0), font=font)
|
|
|
|
font = ImageFont.truetype('DejaVuSans-Bold.ttf', font_sizes[1])
|
|
y = my + CARD_PHOTO_MARGIN_TOP + CARD_PHOTO_MARGIN_SIDE + font_sizes[1]
|
|
draw.text((x, y), str(member.last_name), (0,0,0), font=font)
|
|
|
|
font = ImageFont.truetype('DejaVuSans.ttf', 36)
|
|
draw.text((x, 800), 'Joined: ' + str(member.application_date or 'Unknown'), (0,0,0), font=font)
|
|
y = CARD_PHOTO_MARGIN_SIDE
|
|
draw.text((475, y), str(member.id), (0,0,0), font=font)
|
|
|
|
bio = io.BytesIO()
|
|
card_template.save(bio, 'JPEG', quality=95)
|
|
bio.seek(0)
|
|
|
|
return bio
|
|
|
|
|
|
ALLOWED_TAGS = [
|
|
'h3',
|
|
'p',
|
|
'br',
|
|
'strong',
|
|
'em',
|
|
'u',
|
|
'code',
|
|
'ol',
|
|
'li',
|
|
'ul',
|
|
'a',
|
|
]
|
|
|
|
clean = Cleaner(tags=ALLOWED_TAGS).clean
|
|
|
|
|
|
def is_request_from_protospace(request):
|
|
whitelist = ['24.66.110.96', '205.233.15.76', '205.233.15.69']
|
|
|
|
# set (not appended) directly by nginx so we can trust it
|
|
real_ip = request.META.get('HTTP_X_REAL_IP', False)
|
|
|
|
return real_ip in whitelist
|
|
|
|
def link_old_member(data, user):
|
|
'''
|
|
If a member claims they have an account on the old protospace portal,
|
|
go through and link their objects to their new user using the member_id
|
|
found with their email as a hint
|
|
|
|
Since this runs AFTER registration, we need to delete the user on any
|
|
failures or else the username will be taken when they try again
|
|
'''
|
|
|
|
try:
|
|
member = models.Member.objects.get(old_email__iexact=data['email'])
|
|
except models.Member.DoesNotExist:
|
|
msg = 'Unable to find email in old portal. Try a different one or ask a director to look up which one you used.'
|
|
logger.info(msg)
|
|
raise ValidationError(dict(email=msg))
|
|
except models.Member.MultipleObjectsReturned:
|
|
msg = 'Duplicate emails found. Talk to Tanner.'
|
|
logger.info(msg)
|
|
raise ValidationError(dict(email=msg))
|
|
|
|
if member.user:
|
|
msg = 'Old member already claimed.'
|
|
logger.info(msg)
|
|
raise ValidationError(dict(email=msg))
|
|
|
|
if utils_ldap.is_configured():
|
|
if data['request_id']: utils_stats.set_progress(data['request_id'], 'Finding LDAP account...')
|
|
result = utils_ldap.find_user(user.username)
|
|
if result == 200:
|
|
if utils_ldap.set_password(data) != 200:
|
|
msg = 'Problem connecting to LDAP server: set.'
|
|
alert_tanner(msg)
|
|
logger.info(msg)
|
|
raise ValidationError(dict(non_field_errors=msg))
|
|
elif result == 404:
|
|
if utils_ldap.create_user(data) != 200:
|
|
msg = 'Problem connecting to LDAP server: create.'
|
|
alert_tanner(msg)
|
|
logger.info(msg)
|
|
raise ValidationError(dict(non_field_errors=msg))
|
|
else:
|
|
msg = 'Problem connecting to LDAP server: find.'
|
|
alert_tanner(msg)
|
|
logger.info(msg)
|
|
raise ValidationError(dict(non_field_errors=msg))
|
|
|
|
|
|
if data['request_id']: utils_stats.set_progress(data['request_id'], 'Linking old member data...')
|
|
|
|
member.user = user
|
|
member.first_name = data['first_name'].title()
|
|
member.last_name = data['last_name'].title()
|
|
member.preferred_name = data['first_name'].title()
|
|
member.save()
|
|
|
|
models.Transaction.objects.filter(member_id=member.id).update(user=user)
|
|
models.Card.objects.filter(member_id=member.id).update(user=user)
|
|
models.Training.objects.filter(member_id=member.id).update(user=user)
|
|
|
|
def create_new_member(data, user):
|
|
members = models.Member.objects
|
|
if members.filter(old_email__iexact=data['email']).exists():
|
|
msg = 'Account was found in old portal.'
|
|
logger.info(msg)
|
|
raise ValidationError(dict(email=msg))
|
|
|
|
if utils_ldap.is_configured():
|
|
if data['request_id']: utils_stats.set_progress(data['request_id'], 'Creating LDAP account...')
|
|
result = utils_ldap.find_user(user.username)
|
|
if result == 200:
|
|
msg = 'Username was found in old portal.'
|
|
logger.info(msg)
|
|
raise ValidationError(dict(username=msg))
|
|
elif result == 404:
|
|
pass
|
|
else:
|
|
msg = 'Problem connecting to LDAP server.'
|
|
alert_tanner(msg)
|
|
logger.info(msg)
|
|
raise ValidationError(dict(non_field_errors=msg))
|
|
|
|
if utils_ldap.create_user(data) != 200:
|
|
msg = 'Problem connecting to LDAP server: create.'
|
|
alert_tanner(msg)
|
|
logger.info(msg)
|
|
raise ValidationError(dict(non_field_errors=msg))
|
|
|
|
if data['request_id']: utils_stats.set_progress(data['request_id'], 'Creating new member...')
|
|
|
|
models.Member.objects.create(
|
|
user=user,
|
|
first_name=data['first_name'].title(),
|
|
last_name=data['last_name'].title(),
|
|
preferred_name=data['first_name'].title(),
|
|
)
|
|
|
|
def register_user(data, user):
|
|
try:
|
|
if data['existing_member'] == 'true':
|
|
logger.info('Linking old member...')
|
|
link_old_member(data, user)
|
|
else:
|
|
logger.info('Creating new member...')
|
|
create_new_member(data, user)
|
|
except:
|
|
user.delete()
|
|
raise
|
|
|
|
auth_data = dict(
|
|
username=data['username'],
|
|
password=data['password1'],
|
|
email=data['email'],
|
|
first_name=data['first_name'],
|
|
)
|
|
|
|
if utils_auth.wiki_is_configured():
|
|
if data['request_id']: utils_stats.set_progress(data['request_id'], 'Creating Wiki account...')
|
|
if utils_auth.set_wiki_password(auth_data) != 200:
|
|
msg = 'Problem connecting to Wiki Auth server: set.'
|
|
utils.alert_tanner(msg)
|
|
logger.info(msg)
|
|
|
|
if utils_auth.discourse_is_configured():
|
|
if data['request_id']: utils_stats.set_progress(data['request_id'], 'Creating Discourse account...')
|
|
if utils_auth.set_discourse_password(auth_data) != 200:
|
|
msg = 'Problem connecting to Discourse Auth server: set.'
|
|
utils.alert_tanner(msg)
|
|
logger.info(msg)
|
|
if not user.member.discourse_username:
|
|
user.member.discourse_username = user.username
|
|
user.member.save()
|
|
|
|
if utils_auth.discourse_is_configured():
|
|
if data['request_id']: utils_stats.set_progress(data['request_id'], 'Adding to Discourse group...')
|
|
if utils_auth.add_discourse_group_members('protospace_members', [data['username']]) != 200:
|
|
msg = 'Problem connecting to Discourse Auth server: add.'
|
|
utils.alert_tanner(msg)
|
|
logger.info(msg)
|
|
|
|
if data['request_id']: utils_stats.set_progress(data['request_id'], 'Done!')
|
|
time.sleep(1)
|
|
|
|
|
|
BLANK_FORM = 'misc/blank_member_form.pdf'
|
|
def gen_member_forms(member):
|
|
serializer = serializers.MemberSerializer(member)
|
|
data = serializer.data
|
|
|
|
packet = io.BytesIO()
|
|
can = canvas.Canvas(packet, pagesize=letter)
|
|
can.drawString(34, 683, data['first_name'])
|
|
can.drawString(218, 683, data['last_name'])
|
|
can.drawString(403, 683, data['preferred_name'])
|
|
can.drawString(34, 626, data['email'])
|
|
can.drawString(332, 626, data['phone'])
|
|
can.drawString(34, 570, data['emergency_contact_name'])
|
|
can.drawString(332, 570, data['emergency_contact_phone'])
|
|
can.save()
|
|
packet.seek(0)
|
|
info_pdf = PdfFileReader(packet)
|
|
|
|
packet = io.BytesIO()
|
|
can = canvas.Canvas(packet, pagesize=letter)
|
|
can.drawRightString(600, 770, '{} {} ({})'.format(
|
|
data['first_name'],
|
|
data['last_name'],
|
|
data['id'],
|
|
))
|
|
can.save()
|
|
packet.seek(0)
|
|
topright_pdf = PdfFileReader(packet)
|
|
|
|
existing_pdf = PdfFileReader(open(BLANK_FORM, 'rb'))
|
|
output = PdfFileWriter()
|
|
page = existing_pdf.getPage(0)
|
|
page.mergePage(info_pdf.getPage(0))
|
|
page.mergePage(topright_pdf.getPage(0))
|
|
output.addPage(page)
|
|
page = existing_pdf.getPage(1)
|
|
page.mergePage(topright_pdf.getPage(0))
|
|
output.addPage(page)
|
|
page = existing_pdf.getPage(2)
|
|
page.mergePage(topright_pdf.getPage(0))
|
|
output.addPage(page)
|
|
|
|
file_name = str(uuid4()) + '.pdf'
|
|
outputStream = open(STATIC_FOLDER + file_name, 'wb')
|
|
output.write(outputStream)
|
|
|
|
member.member_forms = file_name
|
|
member.save()
|