import datetime import io import requests from rest_framework.exceptions import ValidationError from dateutil import relativedelta from uuid import uuid4 from PIL import Image 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 . import models, serializers, utils_ldap try: from . import old_models except ImportError: print('Running without old portal data...') old_models = None STATIC_FOLDER = 'data/static/' 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: status, if we should pause them ''' today = fake_date or datetime.date.today() difference = num_months_difference(expire_date, today) #if today + datetime.timedelta(days=29) < expire_date: if difference >= 1: return 'Prepaid', False elif difference <= -3: return 'Overdue', True elif difference <= -1: return 'Overdue', False elif today < expire_date: return 'Current', False elif today >= expire_date: return 'Due', False 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, former = calc_member_status(expire_date, fake_date) member.expire_date = expire_date member.status = status if former: 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.all(): string = '{} {}'.format( m.preferred_name, m.last_name, ).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): ''' 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.') 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 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 ''' if not old_models: raise ValidationError(dict(email='Unable to link, old DB wasn\'t imported.')) old_members = old_models.Members.objects.using('old_portal') try: old_member = old_members.get(email=data['email']) except old_models.Members.DoesNotExist: raise ValidationError(dict(email='Unable to find email in old portal.')) member = models.Member.objects.get(id=old_member.id) if member.user: raise ValidationError(dict(email='Old member already claimed.')) if utils_ldap.is_configured(): result = utils_ldap.find_user(user.username) if result == 200: pass elif result == 404: raise ValidationError(dict(username='Unable to find username in old portal.')) else: raise ValidationError(dict(non_field_errors='Problem connecting to LDAP server: find.')) if utils_ldap.set_password(data) != 200: raise ValidationError(dict(non_field_errors='Problem connecting to LDAP server: set.')) member.user = user member.first_name = data['first_name'] member.last_name = data['last_name'] member.preferred_name = data['first_name'] member.save() transactions = models.Transaction.objects.filter(member_id=member.id) for t in transactions: t.user = user t.save() cards = models.Card.objects.filter(member_id=member.id) for c in cards: c.user = user c.save() training = models.Training.objects.filter(member_id=member.id) for t in training: t.user = user t.save() def create_new_member(data, user): if old_models: old_members = old_models.Members.objects.using('old_portal') if old_members.filter(email=data['email']).exists(): raise ValidationError(dict(email='Account was found in old portal.')) if utils_ldap.is_configured(): result = utils_ldap.find_user(user.username) if result == 200: raise ValidationError(dict(username='Username was found in old portal.')) elif result == 404: pass else: raise ValidationError(dict(non_field_errors='Problem connecting to LDAP server.')) if utils_ldap.create_user(data) != 200: raise ValidationError(dict(non_field_errors='Problem connecting to LDAP server: create.')) models.Member.objects.create( user=user, first_name=data['first_name'], last_name=data['last_name'], preferred_name=data['first_name'], ) def register_user(data, user): try: if data['existing_member'] == 'true': link_old_member(data, user) else: create_new_member(data, user) except: user.delete() raise 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.drawRightString(580, 770, '{} {} ({})'.format( data['first_name'], data['last_name'], data['id'], )) can.drawString(34, 683, data['first_name']) can.drawString(218, 683, data['last_name']) can.drawString(403, 683, data['preferred_name']) can.drawString(34, 654, data['street_address']) can.drawString(275, 654, data['city']) can.drawString(459, 654, data['postal_code']) 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) new_pdf = PdfFileReader(packet) existing_pdf = PdfFileReader(open(BLANK_FORM, 'rb')) output = PdfFileWriter() page = existing_pdf.getPage(0) page.mergePage(new_pdf.getPage(0)) output.addPage(page) page = existing_pdf.getPage(1) output.addPage(page) page = existing_pdf.getPage(2) 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()