From 7981a05a46e65bfa61a54bf5f5373820c91c90c5 Mon Sep 17 00:00:00 2001 From: Adam Goldsmith Date: Mon, 30 Mar 2020 14:01:39 -0400 Subject: [PATCH] Setup poetry, apply Black and isort styling --- common.py | 30 ++- doorUpdater.py | 310 +++++++++++++++++++----------- events.py | 59 +++--- lib/MembershipWorks.py | 170 +++++++++-------- lib/hid/Credential.py | 7 +- lib/hid/DoorController.py | 149 +++++++++------ lib/mw_models.py | 265 +++++++++++++++----------- membershipViewer.py | 65 ++++--- poetry.lock | 383 ++++++++++++++++++++++++++++++++++++++ pyproject.toml | 31 +++ sqlExport.py | 34 ++-- ucsAccounts.py | 114 +++++++----- 12 files changed, 1135 insertions(+), 482 deletions(-) create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/common.py b/common.py index 6c593f0..c9f5d64 100644 --- a/common.py +++ b/common.py @@ -1,11 +1,15 @@ -from ruamel.yaml import YAML import os +from ruamel.yaml import YAML + from lib.hid.DoorController import DoorController from lib.MembershipWorks import MembershipWorks - -from passwords import DOOR_USERNAME, DOOR_PASSWORD -from passwords import MEMBERSHIPWORKS_USERNAME, MEMBERSHIPWORKS_PASSWORD +from passwords import ( + DOOR_PASSWORD, + DOOR_USERNAME, + MEMBERSHIPWORKS_PASSWORD, + MEMBERSHIPWORKS_USERNAME, +) try: with open(os.path.dirname(os.path.abspath(__file__)) + "/config.yaml") as f: @@ -14,13 +18,19 @@ except NameError: with open("config.yaml") as f: config = YAML().load(f) -doors = {doorName: DoorController(doorData['ip'], - DOOR_USERNAME, DOOR_PASSWORD, - name=doorName, access=doorData['access']) - for doorName, doorData in config["doors"].items()} +doors = { + doorName: DoorController( + doorData["ip"], + DOOR_USERNAME, + DOOR_PASSWORD, + name=doorName, + access=doorData["access"], + ) + for doorName, doorData in config["doors"].items() +} -memberLevels = config['memberLevels'] -doorSpecificSchedules = config['doorSpecificSchedules'] +memberLevels = config["memberLevels"] +doorSpecificSchedules = config["doorSpecificSchedules"] membershipworks = MembershipWorks() membershipworks.login(MEMBERSHIPWORKS_USERNAME, MEMBERSHIPWORKS_PASSWORD) diff --git a/doorUpdater.py b/doorUpdater.py index 57ccc7a..e8a19b9 100755 --- a/doorUpdater.py +++ b/doorUpdater.py @@ -2,15 +2,27 @@ import copy -from common import doors, membershipworks, memberLevels, doorSpecificSchedules +from common import doors, doorSpecificSchedules, memberLevels, membershipworks from lib.hid.Credential import Credential from lib.hid.DoorController import ROOT, E -class Member(): - def __init__(self, forename="", surname="", membershipWorksID="", - middleName="", email="", phone="", - cardholderID=None, doorAccess=[], - credentials=set(), levels=[], extraLevels=[], schedules=[]): + +class Member: + def __init__( + self, + forename="", + surname="", + membershipWorksID="", + middleName="", + email="", + phone="", + cardholderID=None, + doorAccess=[], + credentials=set(), + levels=[], + extraLevels=[], + schedules=[], + ): self.forename = forename self.surname = surname self.membershipWorksID = membershipWorksID @@ -38,68 +50,87 @@ Schedules: {self.schedules} class MembershipworksMember(Member): def __init__(self, data, formerMember=False): - super().__init__(data["First Name"], - data["Last Name"], - membershipWorksID=data["Account ID"], - email=data["Email"], - phone=data["Phone"]) + super().__init__( + data["First Name"], + data["Last Name"], + membershipWorksID=data["Account ID"], + email=data["Email"], + phone=data["Phone"], + ) if data["Access Card Number"] != "": - self.credentials = set([Credential( - code=(data["Access Card Facility Code"], - data["Access Card Number"]))]) + self.credentials = set( + [ + Credential( + code=( + data["Access Card Facility Code"], + data["Access Card Number"], + ) + ) + ] + ) else: self.credentials = set() - self.onHold=data["Account on Hold"] != "" - self.limitedOperations=data['Access Permitted During Limited Operations'] == "Y" - self.formerMember=formerMember + self.onHold = data["Account on Hold"] != "" + self.limitedOperations = ( + data["Access Permitted During Limited Operations"] == "Y" + ) + self.formerMember = formerMember levels = {k: v for k, v in memberLevels.items() if data[k] == k} self.levels = list(levels.keys()) self.schedules = list(levels.values()) self.extraLevels = { - schedule: sum((doors for prop, doors in props.items() - if data[prop] == "Y"), - []) + schedule: sum( + (doors for prop, doors in props.items() if data[prop] == "Y"), [] + ) for schedule, props in doorSpecificSchedules.items() } self.doorAccess = [ - door for door, doorData in doors.items() + door + for door, doorData in doors.items() if data["Access " + doorData.access + "?"] == "Y" ] def to_DoorMember(self, door): doorLevels = [k for k, v in self.extraLevels.items() if door.name in v] - if (door.name not in self.doorAccess + if ( + door.name not in self.doorAccess or self.onHold or self.formerMember - or not self.limitedOperations): + or not self.limitedOperations + ): schedules = [] else: schedules = self.schedules + doorLevels - dm = DoorMember(door, - forename=self.forename, - surname=self.surname, - membershipWorksID=self.membershipWorksID, - email=self.email, - phone=self.phone, - levels=self.levels + doorLevels, - doorAccess=self.doorAccess, - credentials=self.credentials, - schedules=schedules) + dm = DoorMember( + door, + forename=self.forename, + surname=self.surname, + membershipWorksID=self.membershipWorksID, + email=self.email, + phone=self.phone, + levels=self.levels + doorLevels, + doorAccess=self.doorAccess, + credentials=self.credentials, + schedules=schedules, + ) return dm def __str__(self): - return super().__str__() + f"""OnHold? {self.onHold} + return ( + super().__str__() + + f"""OnHold? {self.onHold} Limited Operations Access? {self.limitedOperations} Former Member? {self.formerMember} """ + ) class DoorMember(Member): @@ -109,65 +140,83 @@ class DoorMember(Member): @classmethod def from_cardholder(cls, data, door): - ch = cls(door=door, - forename=data.get('forename', ""), - surname=data.get('surname', ""), - membershipWorksID=data.attrib.get('custom2', ""), - middleName=data.attrib.get('middleName', ""), - email=data.attrib.get('email', ""), - phone=data.attrib.get('phone', ""), - cardholderID=data.attrib['cardholderID']) + ch = cls( + door=door, + forename=data.get("forename", ""), + surname=data.get("surname", ""), + membershipWorksID=data.attrib.get("custom2", ""), + middleName=data.attrib.get("middleName", ""), + email=data.attrib.get("email", ""), + phone=data.attrib.get("phone", ""), + cardholderID=data.attrib["cardholderID"], + ) ch.credentials = set( - Credential(hex=(c.attrib['rawCardNumber'])) - for c in data.findall('{*}Credential')) + Credential(hex=(c.attrib["rawCardNumber"])) + for c in data.findall("{*}Credential") + ) - ch.levels = data.attrib.get('custom1', "").split('|') - ch.schedules = [r.attrib['scheduleName'] - for r in data.findall('{*}Role')] + ch.levels = data.attrib.get("custom1", "").split("|") + ch.schedules = [r.attrib["scheduleName"] for r in data.findall("{*}Role")] return ch def attribs(self): - return {"forename": self.forename, - "surname": self.surname, - "middleName": self.middleName, - "email": self.email, - "phone": self.phone, - "custom1": "|".join(self.levels).replace("&", "and"), - "custom2": self.membershipWorksID} + return { + "forename": self.forename, + "surname": self.surname, + "middleName": self.middleName, + "email": self.email, + "phone": self.phone, + "custom1": "|".join(self.levels).replace("&", "and"), + "custom2": self.membershipWorksID, + } def make_schedules(self, schedulesMap): roles = [ - E.Role({"roleID": self.cardholderID, + E.Role( + { + "roleID": self.cardholderID, "scheduleID": schedulesMap[schedule], - "resourceID": "0"}) - for schedule in self.schedules] + "resourceID": "0", + } + ) + for schedule in self.schedules + ] - return E.RoleSet({"action": "UD", "roleSetID": self.cardholderID}, - E.Roles(*roles)) + return E.RoleSet( + {"action": "UD", "roleSetID": self.cardholderID}, E.Roles(*roles) + ) def make_credentials(self, newCredentials, cardFormats): out = [ E.Credential( - {"formatName": str(credential.code[0]), - "cardNumber": str(credential.code[1]), - "formatID": cardFormats[str(credential.code[0])], - "isCard": "true", - "cardholderID": self.cardholderID}) - for credential in newCredentials] + { + "formatName": str(credential.code[0]), + "cardNumber": str(credential.code[1]), + "formatID": cardFormats[str(credential.code[0])], + "isCard": "true", + "cardholderID": self.cardholderID, + } + ) + for credential in newCredentials + ] return E.Credentials({"action": "AD"}, *out) def update_door(door, members): cardFormats = door.get_cardFormats() - cardholders = {member.membershipWorksID: member - for member in [DoorMember.from_cardholder(ch, door) - for ch in door.get_cardholders()]} + cardholders = { + member.membershipWorksID: member + for member in [ + DoorMember.from_cardholder(ch, door) for ch in door.get_cardholders() + ] + } schedulesMap = door.get_scheduleMap() - allCredentials = set(Credential(hex=c.attrib['rawCardNumber']) - for c in door.get_credentials()) + allCredentials = set( + Credential(hex=c.attrib["rawCardNumber"]) for c in door.get_credentials() + ) # TODO: can I combine requests? for membershipworksMember in members: @@ -176,12 +225,12 @@ def update_door(door, members): if member.membershipWorksID not in cardholders: print("- Adding Member {member.forename} {member.surname}:") print(f" - {member.attribs()}") - resp = door.doXMLRequest(ROOT( - E.Cardholders( - {"action": "AD"}, - E.Cardholder(member.attribs())))) - member.cardholderID = resp.find('{*}Cardholders/{*}Cardholder') \ - .attrib["cardholderID"] + resp = door.doXMLRequest( + ROOT(E.Cardholders({"action": "AD"}, E.Cardholder(member.attribs()))) + ) + member.cardholderID = resp.find("{*}Cardholders/{*}Cardholder").attrib[ + "cardholderID" + ] # create a dummy ch to force an update # TODO: probably a cleaner way to do this @@ -197,10 +246,14 @@ def update_door(door, members): print(f"- Updating profile for {member.forename} {member.surname}") print(f" - Old: {ch.attribs()}") print(f" - New: {member.attribs()}") - door.doXMLRequest(ROOT( - E.Cardholders( - {"action": "UD", "cardholderID": member.cardholderID}, - E.CardHolder(member.attribs())))) + door.doXMLRequest( + ROOT( + E.Cardholders( + {"action": "UD", "cardholderID": member.cardholderID}, + E.CardHolder(member.attribs()), + ) + ) + ) if member.credentials != ch.credentials: print(f"- Updating card for {member.forename} {member.surname}") @@ -210,69 +263,106 @@ def update_door(door, members): newCards = member.credentials allNewCards = set( - card for m in members if m != membershipworksMember - for card in m.credentials) + card + for m in members + if m != membershipworksMember + for card in m.credentials + ) # cards removed, and won't be reassigned to someone else for card in (oldCards - newCards) - allNewCards: - door.doXMLRequest(ROOT(E.Credentials( - {"action": "UD", - "rawCardNumber": card.hex, - "isCard": "true"}, - E.Credential({"cardholderID": ""})))) + door.doXMLRequest( + ROOT( + E.Credentials( + { + "action": "UD", + "rawCardNumber": card.hex, + "isCard": "true", + }, + E.Credential({"cardholderID": ""}), + ) + ) + ) if newCards - oldCards: # cards added for card in newCards & allNewCards: # new card exists in another member - print([m for m in members - for card in m.credentials - if card in newCards]) + print( + [ + m + for m in members + for card in m.credentials + if card in newCards + ] + ) raise Exception(f"Duplicate Card in input data! {card}") # card existed in door, and needs to be reassigned for card in newCards & allCredentials: - door.doXMLRequest(ROOT(E.Credentials( - {"action": "UD", - "rawCardNumber": card.hex, - "isCard": "true"}, - E.Credential({"cardholderID": member.cardholderID})))) + door.doXMLRequest( + ROOT( + E.Credentials( + { + "action": "UD", + "rawCardNumber": card.hex, + "isCard": "true", + }, + E.Credential({"cardholderID": member.cardholderID}), + ) + ) + ) # cards that never existed, and need to be created if newCards - allCredentials: - door.doXMLRequest(ROOT(member.make_credentials( - newCards - allCredentials, cardFormats))) + door.doXMLRequest( + ROOT( + member.make_credentials( + newCards - allCredentials, cardFormats + ) + ) + ) if member.schedules != ch.schedules: - print("- Updating schedule for" + - f" {member.forename} {member.surname}:" + - f" {ch.schedules} -> {member.schedules}") + print( + "- Updating schedule for" + + f" {member.forename} {member.surname}:" + + f" {ch.schedules} -> {member.schedules}" + ) door.doXMLRequest(ROOT(member.make_schedules(schedulesMap))) # TODO: delete cardholders that are no longer members? def main(): - membershipworks_attributes = \ + membershipworks_attributes = ( "_id,nam,phn,eml,lvl,lbl,xws,xms,xsc,xas,xfd,xac,xcf,xeh,xse,xlo" + ) memberData = membershipworks.get_members( - ['Members', 'CMS Staff', 'Misc. Access'], membershipworks_attributes) + ["Members", "CMS Staff", "Misc. Access"], membershipworks_attributes + ) members = [MembershipworksMember(m) for m in memberData] formerMemberData = membershipworks.get_members( - ['Former Members'], membershipworks_attributes) - formerMembers = [MembershipworksMember(m, formerMember=True) - for m in formerMemberData] + ["Former Members"], membershipworks_attributes + ) + formerMembers = [ + MembershipworksMember(m, formerMember=True) for m in formerMemberData + ] for formerMember in formerMembers: member = next( - (m for m in members - if m.membershipWorksID == formerMember.membershipWorksID), - None) + ( + m + for m in members + if m.membershipWorksID == formerMember.membershipWorksID + ), + None, + ) # member exists in another folder if member is not None: member.formerMember = True - else: # member is only a former member + else: # member is only a former member formerMember.formerMember = True members.append(formerMember) @@ -281,5 +371,5 @@ def main(): update_door(door, members) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/events.py b/events.py index 216debc..89accb6 100755 --- a/events.py +++ b/events.py @@ -1,27 +1,34 @@ #!/usr/bin/env python3 -from collections import defaultdict -from lxml import etree import os import re -import requests -from common import * +import requests +from lxml import etree + +from common import doors +from lib.hid.DoorController import E, E_plain + def getStrings(door): """Parses out the message strings from source.""" - r = requests.get('https://' + door.ip + '/html/en_EN/en_EN.js', - auth=requests.auth.HTTPDigestAuth(door.username, door.password), - verify=False) + r = requests.get( + "https://" + door.ip + "/html/en_EN/en_EN.js", + auth=requests.auth.HTTPDigestAuth(door.username, door.password), + verify=False, + ) regex = re.compile(r'([0-9]+)="([^"]*)') - strings = [regex.search(s) for s in r.text.split(';') - if s.startswith('localeStrings.eventDetails')] + strings = [ + regex.search(s) + for s in r.text.split(";") + if s.startswith("localeStrings.eventDetails") + ] print({int(g.group(1)): g.group(2) for g in strings}) + def getMessages(door): # get parameters for messages to get? # honestly not really sure why this is required, their API is confusing - parXMLIn = E_plain.VertXMessage( - E.EventMessages({"action": "LR"})) + parXMLIn = E_plain.VertXMessage(E.EventMessages({"action": "LR"})) parXMLOut = door.doXMLRequest(parXMLIn) etree.dump(parXMLOut) @@ -29,8 +36,9 @@ def getMessages(door): # read last log tree = etree.ElementTree(file="logs/" + door.name + ".xml") root = tree.getroot() - recordCount = int(parXMLOut[0].attrib["historyRecordMarker"]) - \ - int(root[0][0].attrib["recordMarker"]) + recordCount = int(parXMLOut[0].attrib["historyRecordMarker"]) - int( + root[0][0].attrib["recordMarker"] + ) else: # first run for this door root = None @@ -42,26 +50,35 @@ def getMessages(door): print("Getting", recordCount, "records") # get the actual messages eventsXMLIn = E_plain.VertXMessage( - E.EventMessages({"action": "LR", - "recordCount": str(recordCount), - "historyRecordMarker": parXMLOut[0].attrib["historyRecordMarker"], - "historyTimestamp": parXMLOut[0].attrib["historyTimestamp"]})) + E.EventMessages( + { + "action": "LR", + "recordCount": str(recordCount), + "historyRecordMarker": parXMLOut[0].attrib["historyRecordMarker"], + "historyTimestamp": parXMLOut[0].attrib["historyTimestamp"], + } + ) + ) eventsXMLOut = door.doXMLRequest(eventsXMLIn) - #TODO: handle modeRecords=true + # TODO: handle modeRecords=true for index, event in enumerate(eventsXMLOut[0]): - event.attrib["recordMarker"] = str(int(parXMLOut[0].attrib["historyRecordMarker"]) - index) + event.attrib["recordMarker"] = str( + int(parXMLOut[0].attrib["historyRecordMarker"]) - index + ) if root is None: tree = etree.ElementTree(eventsXMLOut) else: for event in reversed(eventsXMLOut[0]): root[0].insert(0, event) - tree.write("logs/" + doorName + ".xml") + tree.write("logs/" + door.name + ".xml") + def main(): for door in doors.values(): getMessages(door) -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/lib/MembershipWorks.py b/lib/MembershipWorks.py index 30537fc..82b20e5 100644 --- a/lib/MembershipWorks.py +++ b/lib/MembershipWorks.py @@ -1,44 +1,48 @@ import csv from io import StringIO + import requests BASE_URL = "https://api.membershipworks.com/v1/" # extracted from `SF._is.crm` in https://cdn.membershipworks.com/all.js -CRM = {0: "Note", - 4: "Profile Updated", - 8: "Scheduled/Reminder Email", - 9: "Renewal Notice", - 10: "Join Date", - 11: "Next Renewal Date", - 12: "Membership Payment", - 13: "Donation", - 14: "Event Activity", - 15: "Conversation", - 16: "Contact Change", - 17: "Label Change", - 18: "Other Payment", - 19: "Cart Payment", - 20: "Payment Failed", - 21: "Billing Updated", - 22: "Form Checkout", - 23: "Event Payment", - 24: "Invoice", - 25: "Invoice Payment", - 26: "Renewal", - 27: "Payment"} +CRM = { + 0: "Note", + 4: "Profile Updated", + 8: "Scheduled/Reminder Email", + 9: "Renewal Notice", + 10: "Join Date", + 11: "Next Renewal Date", + 12: "Membership Payment", + 13: "Donation", + 14: "Event Activity", + 15: "Conversation", + 16: "Contact Change", + 17: "Label Change", + 18: "Other Payment", + 19: "Cart Payment", + 20: "Payment Failed", + 21: "Billing Updated", + 22: "Form Checkout", + 23: "Event Payment", + 24: "Invoice", + 25: "Invoice Payment", + 26: "Renewal", + 27: "Payment", +} # Types of fields, extracted from a html snippet in all.js + some guessing typ = { 1: "Text input", - 2: "Password", # inferred from data + 2: "Password", # inferred from data 3: "Simple text area", 4: "Rich text area", 7: "Address", 8: "Check box", 9: "Select", 11: "Display value stored in field (ie. read only)", - 12: "Required waiver/terms"} + 12: "Required waiver/terms", +} # more constants, this time extracted from the members csv export in all.js staticFlags = { @@ -55,14 +59,16 @@ staticFlags = { "spy": {"lbl": "Billing method"}, "rid": {"lbl": "Auto recurring billing ID"}, "ipa": {"lbl": "IP address"}, - "_id": {"lbl": "Account ID"} + "_id": {"lbl": "Account ID"}, } class MembershipWorksRemoteError(Exception): def __init__(self, reason, r): super().__init__( - f"Error when attempting {reason}: {r.status_code} {r.reason}\n{r.text}") + f"Error when attempting {reason}: {r.status_code} {r.reason}\n{r.text}" + ) + class MembershipWorks: def __init__(self): @@ -72,25 +78,24 @@ class MembershipWorks: def login(self, username, password): """Authenticate against the membershipworks api""" - r = requests.post(BASE_URL + 'usr', - data={"_st": "all", - "eml": username, - "org": "10000", - "pwd": password}) - if r.status_code != 200 or 'SF' not in r.json(): - raise MembershipWorksRemoteError('login', r) + r = requests.post( + BASE_URL + "usr", + data={"_st": "all", "eml": username, "org": "10000", "pwd": password}, + ) + if r.status_code != 200 or "SF" not in r.json(): + raise MembershipWorksRemoteError("login", r) self.org_info = r.json() - self.auth_token = self.org_info['SF'] - self.org_num = self.org_info['org'] + self.auth_token = self.org_info["SF"] + self.org_num = self.org_info["org"] def _inject_auth(self, kwargs): # TODO: should probably be a decorator or something if self.auth_token is None: - raise RuntimeError('Not Logged in to MembershipWorks') + raise RuntimeError("Not Logged in to MembershipWorks") # add auth token to params - if 'params' not in kwargs: - kwargs['params'] = {} - kwargs['params']["SF"] = self.auth_token + if "params" not in kwargs: + kwargs["params"] = {} + kwargs["params"]["SF"] = self.auth_token def _get(self, *args, **kwargs): self._inject_auth(kwargs) @@ -115,14 +120,14 @@ class MembershipWorks: # csv export # anm: member signup, acc: member manage, adm: admin manage - for screen_type in ['anm', 'acc', 'adm']: - for box in self.org_info['tpl'][screen_type]: - for element in box['box']: - if (type(element['dat']) != str): - for field in element['dat']: - if '_id' in field: - if field['_id'] not in fields: - fields[field['_id']] = field + for screen_type in ["anm", "acc", "adm"]: + for box in self.org_info["tpl"][screen_type]: + for element in box["box"]: + if type(element["dat"]) != str: + for field in element["dat"]: + if "_id" in field: + if field["_id"] not in fields: + fields[field["_id"]] = field return fields @@ -131,36 +136,37 @@ class MembershipWorks: This is terrible, and there might be a better way to do this. """ - ret = {"folders": {}, - "levels": {}, - "addons": {}, - "labels": {}} + ret = {"folders": {}, "levels": {}, "addons": {}, "labels": {}} - for dek in self.org_info['dek']: + for dek in self.org_info["dek"]: # TODO: there must be a better way. this is stupid - if dek['dek'] == 1: - ret["folders"][dek['lbl']] = dek['_id'] - elif 'cur' in dek: - ret["levels"][dek['lbl']] = dek['_id'] - elif 'mux' in dek: - ret["addons"][dek['lbl']] = dek['_id'] + if dek["dek"] == 1: + ret["folders"][dek["lbl"]] = dek["_id"] + elif "cur" in dek: + ret["levels"][dek["lbl"]] = dek["_id"] + elif "mux" in dek: + ret["addons"][dek["lbl"]] = dek["_id"] else: - ret["labels"][dek['lbl']] = dek['_id'] + ret["labels"][dek["lbl"]] = dek["_id"] return ret def get_member_ids(self, folders): folder_map = self._parse_flags()["folders"] - r = self._get(BASE_URL + "ylp", params={ - "lbl": ",".join([folder_map[f] for f in folders]), - "org": self.org_num, - "var": "_id,nam,ctc"}) - if r.status_code != 200 or 'usr' not in r.json(): - raise MembershipWorksRemoteError('user listing', r) + r = self._get( + BASE_URL + "ylp", + params={ + "lbl": ",".join([folder_map[f] for f in folders]), + "org": self.org_num, + "var": "_id,nam,ctc", + }, + ) + if r.status_code != 200 or "usr" not in r.json(): + raise MembershipWorksRemoteError("user listing", r) # get list of member ID matching the search - return [user['uid'] for user in r.json()['usr']] + return [user["uid"] for user in r.json()["usr"]] # TODO: has issues with aliasing header names: # ex: "Personal Studio Space" Label vs Membership Addon/Field @@ -173,13 +179,17 @@ class MembershipWorks: # get members CSV # TODO: maybe can just use previous get instead? would return JSON - r = self._post(BASE_URL + "csv", - data={"_rt": "946702800", # unknown - "mux": "", # unknown - "tid": ",".join(ids), # ids of members to get - "var": columns}) + r = self._post( + BASE_URL + "csv", + data={ + "_rt": "946702800", # unknown + "mux": "", # unknown + "tid": ",".join(ids), # ids of members to get + "var": columns, + }, + ) if r.status_code != 200: - raise MembershipWorksRemoteError('csv generation', r) + raise MembershipWorksRemoteError("csv generation", r) return list(csv.DictReader(StringIO(r.text))) def get_transactions(self, start_date, end_date, json=False): @@ -190,13 +200,17 @@ class MembershipWorks: json gets a different version of the transactions list, which contains a different set information """ - r = self._get(BASE_URL + "csv", - params={'crm': '12,13,14,18,19', # transaction types, see CRM - **({'txl': ''} if json else {}), - 'sdp': start_date.strftime('%s'), - 'edp': end_date.strftime('%s')}) + r = self._get( + BASE_URL + "csv", + params={ + "crm": "12,13,14,18,19", # transaction types, see CRM + **({"txl": ""} if json else {}), + "sdp": start_date.strftime("%s"), + "edp": end_date.strftime("%s"), + }, + ) if r.status_code != 200: - raise MembershipWorksRemoteError('csv generation', r) + raise MembershipWorksRemoteError("csv generation", r) if json: return r.json() else: diff --git a/lib/hid/Credential.py b/lib/hid/Credential.py index 70b9a3f..90ab8f0 100644 --- a/lib/hid/Credential.py +++ b/lib/hid/Credential.py @@ -3,6 +3,7 @@ import bitstring # Reference for H10301 card format: # https://www.hidglobal.com/sites/default/files/hid-understanding_card_data_formats-wp-en.pdf + class Credential: def __init__(self, code=None, hex=None): if code is None and hex is None: @@ -11,8 +12,10 @@ class Credential: raise TypeError("Cannot set both code and hex for a Credential") elif code is not None: self.bits = bitstring.pack( - '0b000000, 0b0, uint:8=facility, uint:16=number, 0b0', - facility=code[0], number=code[1]) + "0b000000, 0b0, uint:8=facility, uint:16=number, 0b0", + facility=code[0], + number=code[1], + ) self.bits[6] = self.bits[7:19].count(1) % 2 # even parity self.bits[31] = not (self.bits[19:31].count(1) % 2) # odd parity elif hex is not None: diff --git a/lib/hid/DoorController.py b/lib/hid/DoorController.py index 88a9260..63bf874 100644 --- a/lib/hid/DoorController.py +++ b/lib/hid/DoorController.py @@ -1,30 +1,37 @@ import csv from io import StringIO -import urllib3 import requests +import urllib3 from lxml import etree from lxml.builder import ElementMaker E_plain = ElementMaker(nsmap={"hid": "http://www.hidglobal.com/VertX"}) -E = ElementMaker(namespace="http://www.hidglobal.com/VertX", - nsmap={"hid": "http://www.hidglobal.com/VertX"}) -E_corp = ElementMaker(namespace="http://www.hidcorp.com/VertX", #stupid - nsmap={"hid": "http://www.hidcorp.com/VertX"}) +E = ElementMaker( + namespace="http://www.hidglobal.com/VertX", + nsmap={"hid": "http://www.hidglobal.com/VertX"}, +) +E_corp = ElementMaker( + namespace="http://www.hidcorp.com/VertX", # stupid + nsmap={"hid": "http://www.hidcorp.com/VertX"}, +) ROOT = E_plain.VertXMessage -fieldnames = "CardNumber,CardFormat,PinRequired,PinCode,ExtendedAccess,ExpiryDate,Forename,Initial,Surname,Email,Phone,Custom1,Custom2,Schedule1,Schedule2,Schedule3,Schedule4,Schedule5,Schedule6,Schedule7,Schedule8".split(",") +fieldnames = "CardNumber,CardFormat,PinRequired,PinCode,ExtendedAccess,ExpiryDate,Forename,Initial,Surname,Email,Phone,Custom1,Custom2,Schedule1,Schedule2,Schedule3,Schedule4,Schedule5,Schedule6,Schedule7,Schedule8".split( + "," +) # TODO: where should this live? # it's fine, ssl certs are for losers anyway urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + class RemoteError(Exception): def __init__(self, r): - super().__init__( - f"Door Updating Error: {r.status_code} {r.reason}\n{r.text}") + super().__init__(f"Door Updating Error: {r.status_code} {r.reason}\n{r.text}") -class DoorController(): + +class DoorController: def __init__(self, ip, username, password, name="", access=""): self.ip = ip self.username = username @@ -35,59 +42,68 @@ class DoorController(): def doImport(self, params=None, files=None): """Send a request to the door control import script""" r = requests.post( - 'https://' + self.ip + '/cgi-bin/import.cgi', + "https://" + self.ip + "/cgi-bin/import.cgi", params=params, files=files, auth=requests.auth.HTTPDigestAuth(self.username, self.password), timeout=60, - verify=False) # ignore insecure SSL + verify=False, + ) # ignore insecure SSL xml = etree.XML(r.content) - if r.status_code != 200 \ - or len(xml.findall("{http://www.hidglobal.com/VertX}Error")) > 0: + if ( + r.status_code != 200 + or len(xml.findall("{http://www.hidglobal.com/VertX}Error")) > 0 + ): raise RemoteError(r) def doCSVImport(self, csv): """Do the CSV import procedure on a door control""" self.doImport({"task": "importInit"}) - self.doImport({"task": "importCardsPeople", "name": "cardspeopleschedule.csv"}, - {"importCardsPeopleButton": ("cardspeopleschedule.csv", csv, 'text/csv')}) + self.doImport( + {"task": "importCardsPeople", "name": "cardspeopleschedule.csv"}, + {"importCardsPeopleButton": ("cardspeopleschedule.csv", csv, "text/csv")}, + ) self.doImport({"task": "importDone"}) def doXMLRequest(self, xml, prefix=b''): if not isinstance(xml, bytes): xml = etree.tostring(xml) r = requests.get( - 'https://' + self.ip + '/cgi-bin/vertx_xml.cgi', - params={'XML': prefix + xml}, + "https://" + self.ip + "/cgi-bin/vertx_xml.cgi", + params={"XML": prefix + xml}, auth=requests.auth.HTTPDigestAuth(self.username, self.password), - verify=False) + verify=False, + ) resp_xml = etree.XML(r.content) # probably meed to be more sane about this - if r.status_code != 200 \ - or len(resp_xml.findall("{*}Error")) > 0: + if r.status_code != 200 or len(resp_xml.findall("{*}Error")) > 0: raise RemoteError(r) return resp_xml def get_scheduleMap(self): schedules = self.doXMLRequest( - ROOT(E.Schedules({"action": "LR", - "recordOffset": "0", - "recordCount": "8"}))) - return {fmt.attrib["scheduleName"]: fmt.attrib["scheduleID"] - for fmt in schedules[0]} + ROOT(E.Schedules({"action": "LR", "recordOffset": "0", "recordCount": "8"})) + ) + return { + fmt.attrib["scheduleName"]: fmt.attrib["scheduleID"] for fmt in schedules[0] + } def get_schedules(self): # TODO: might be able to do in one request - schedules = self.doXMLRequest(ROOT( - E.Schedules({"action": "LR"}))) + schedules = self.doXMLRequest(ROOT(E.Schedules({"action": "LR"}))) etree.dump(schedules) - data = self.doXMLRequest(ROOT( - *[E.Schedules({"action": "LR", - "scheduleID": schedule.attrib["scheduleID"]}) - for schedule in schedules[0]])) - return ROOT(E_corp.Schedules({"action": "AD"}, - *[s[0] for s in data])) + data = self.doXMLRequest( + ROOT( + *[ + E.Schedules( + {"action": "LR", "scheduleID": schedule.attrib["scheduleID"]} + ) + for schedule in schedules[0] + ] + ) + ) + return ROOT(E_corp.Schedules({"action": "AD"}, *[s[0] for s in data])) def set_schedules(self, schedules): # clear all people @@ -100,8 +116,11 @@ class DoorController(): # clear all schedules delXML = ROOT( - *[E.Schedules({"action": "DD", "scheduleID": str(ii)}) - for ii in range(1, 8)]) + *[ + E.Schedules({"action": "DD", "scheduleID": str(ii)}) + for ii in range(1, 8) + ] + ) try: self.doXMLRequest(delXML) except RemoteError: @@ -113,21 +132,27 @@ class DoorController(): def get_cardFormats(self): cardFormats = self.doXMLRequest( - ROOT(E.CardFormats({"action": "LR", - "responseFormat": "expanded"}))) + ROOT(E.CardFormats({"action": "LR", "responseFormat": "expanded"})) + ) - return {fmt[0].attrib["value"]: fmt.attrib["formatID"] - for fmt in cardFormats[0].findall('{*}CardFormat[{*}FixedField]')} + return { + fmt[0].attrib["value"]: fmt.attrib["formatID"] + for fmt in cardFormats[0].findall("{*}CardFormat[{*}FixedField]") + } def set_cardFormat(self, formatName, templateID, facilityCode): # TODO: add ability to delete formats # delete example: el = ROOT( - E.CardFormats({"action": "AD"}, - E.CardFormat({"formatName": formatName, - "templateID": str(templateID)}, - E.FixedField({"value": str(facilityCode)})))) + E.CardFormats( + {"action": "AD"}, + E.CardFormat( + {"formatName": formatName, "templateID": str(templateID)}, + E.FixedField({"value": str(facilityCode)}), + ), + ) + ) return self.doXMLRequest(el) def get_records(self, req, count, params={}): @@ -142,37 +167,41 @@ class DoorController(): # poorly if the numbers line up poorly (ie an exact multiple # of the returned record limit) while moreRecords: - res = self.doXMLRequest(ROOT( - req({ - "action": "LR", - "recordCount": str(count - recordCount + 1), - "recordOffset": str(recordCount - 1 - if recordCount > 0 else 0), - **params - }))) + res = self.doXMLRequest( + ROOT( + req( + { + "action": "LR", + "recordCount": str(count - recordCount + 1), + "recordOffset": str( + recordCount - 1 if recordCount > 0 else 0 + ), + **params, + } + ) + ) + ) result = result[:-1] + list(res[0]) - recordCount += int(res[0].get('recordCount')) - 1 - moreRecords = res[0].get('moreRecords') == 'true' + recordCount += int(res[0].get("recordCount")) - 1 + moreRecords = res[0].get("moreRecords") == "true" return result def get_cardholders(self): - return self.get_records(E.Cardholders, 1000, - {"responseFormat": "expanded"}) + return self.get_records(E.Cardholders, 1000, {"responseFormat": "expanded"}) def get_credentials(self): return self.get_records(E.Credentials, 1000) def get_lock(self): - el = ROOT( - E.Doors({"action": "LR", "responseFormat": "status"})) + el = ROOT(E.Doors({"action": "LR", "responseFormat": "status"})) xml = self.doXMLRequest(el) - relayState = xml.find('./{*}Doors/{*}Door').attrib['relayState'] + relayState = xml.find("./{*}Doors/{*}Door").attrib["relayState"] return "unlocked" if relayState == "set" else "locked" def set_lock(self, lock=True): el = ROOT( - E.Doors({"action": "CM", - "command": "lockDoor" if lock else "unlockDoor"})) + E.Doors({"action": "CM", "command": "lockDoor" if lock else "unlockDoor"}) + ) return self.doXMLRequest(el) diff --git a/lib/mw_models.py b/lib/mw_models.py index 13f6eb1..512ad6b 100644 --- a/lib/mw_models.py +++ b/lib/mw_models.py @@ -1,20 +1,26 @@ from datetime import datetime -from peewee import (BooleanField, FixedCharField, CompositeKey, DateField, - DateTimeField, DecimalField, ForeignKeyField, Model, - MySQLDatabase, TextField) +from peewee import ( + BooleanField, + CompositeKey, + DateField, + DateTimeField, + DecimalField, + FixedCharField, + ForeignKeyField, + Model, + MySQLDatabase, + TextField, +) import passwords database = MySQLDatabase( **passwords.MEMBERSHIPWORKS_DB, - **{ - "charset": "utf8", - "sql_mode": "PIPES_AS_CONCAT", - "use_unicode": True, - } + **{"charset": "utf8", "sql_mode": "PIPES_AS_CONCAT", "use_unicode": True,} ) + class BaseModel(Model): _csv_headers_override = {} _date_fields = {} @@ -23,9 +29,9 @@ class BaseModel(Model): return self.insert(**self.__data__) def upsert_instance(self): - return self.insert_instance() \ - .on_conflict(action="update", - preserve=list(self._meta.fields.values())) + return self.insert_instance().on_conflict( + action="update", preserve=list(self._meta.fields.values()) + ) def magic_save(self): if self._meta.primary_key is False: @@ -35,8 +41,7 @@ class BaseModel(Model): @classmethod def _headers_map(cls): - return {field.column_name: name - for name, field in cls._meta.fields.items()} + return {field.column_name: name for name, field in cls._meta.fields.items()} @classmethod def _remap_headers(cls, data): @@ -64,140 +69,176 @@ class BaseModel(Model): class Meta: database = database + class Label(BaseModel): label_id = FixedCharField(24, primary_key=True) label = TextField(null=True) class Meta: - table_name = 'labels' + table_name = "labels" + class Member(BaseModel): uid = FixedCharField(24, primary_key=True) - year_of_birth = TextField(column_name='Year of Birth', null=True) - account_name = TextField(column_name='Account Name', null=True) - first_name = TextField(column_name='First Name', null=True) - last_name = TextField(column_name='Last Name', null=True) - phone = TextField(column_name='Phone', null=True) - email = TextField(column_name='Email', null=True) - address_street = TextField(column_name='Address (Street)', null=True) - address_city = TextField(column_name='Address (City)', null=True) - address_state_province = TextField(column_name='Address (State/Province)', null=True) - address_postal_code = TextField(column_name='Address (Postal Code)', null=True) - address_country = TextField(column_name='Address (Country)', null=True) - profile_description = TextField(column_name='Profile description', null=True) - website = TextField(column_name='Website', null=True) - fax = TextField(column_name='Fax', null=True) - contact_person = TextField(column_name='Contact Person', null=True) - password = TextField(column_name='Password', null=True) - position_relation = TextField(column_name='Position/relation', null=True) - parent_account_id = TextField(column_name='Parent Account ID', null=True) - gift_membership_purchased_by = TextField(column_name='Gift Membership purchased by', null=True) - purchased_gift_membership_for = TextField(column_name='Purchased Gift Membership for', null=True) - closet_storage = TextField(column_name='Closet Storage #', null=True) - storage_shelf = TextField(column_name='Storage Shelf #', null=True) - personal_studio_space = TextField(column_name='Personal Studio Space #', null=True) - access_permitted_shops_during_extended_hours = BooleanField(column_name='Access Permitted Shops During Extended Hours?', null=True) - access_front_door_and_studio_space_during_extended_hours = BooleanField(column_name='Access Front Door and Studio Space During Extended Hours?', null=True) - access_wood_shop = BooleanField(column_name='Access Wood Shop?', null=True) - access_metal_shop = BooleanField(column_name='Access Metal Shop?', null=True) - access_storage_closet = BooleanField(column_name='Access Storage Closet?', null=True) - access_studio_space = BooleanField(column_name='Access Studio Space?', null=True) - access_front_door = BooleanField(column_name='Access Front Door?', null=True) - access_card_number = TextField(column_name='Access Card Number', null=True) - access_card_facility_code = TextField(column_name='Access Card Facility Code', null=True) - auto_billing_id = TextField(column_name='Auto Billing ID', null=True) - billing_method = TextField(column_name='Billing Method', null=True) - renewal_date = DateField(column_name='Renewal Date', null=True) - join_date = DateField(column_name='Join Date', null=True) - admin_note = TextField(column_name='Admin note', null=True) - profile_gallery_image_url = TextField(column_name='Profile gallery image URL', null=True) - business_card_image_url = TextField(column_name='Business card image URL', null=True) - instagram = TextField(column_name='Instagram', null=True) - pinterest = TextField(column_name='Pinterest', null=True) - youtube = TextField(column_name='Youtube', null=True) - yelp = TextField(column_name='Yelp', null=True) - google = TextField(column_name='Google+', null=True) - bbb = TextField(column_name='BBB', null=True) - twitter = TextField(column_name='Twitter', null=True) - facebook = TextField(column_name='Facebook', null=True) - linked_in = TextField(column_name='LinkedIn', null=True) - do_not_show_street_address_in_profile = TextField(column_name='Do not show street address in profile', null=True) - do_not_list_in_directory = TextField(column_name='Do not list in directory', null=True) - how_did_you_hear = TextField(column_name='HowDidYouHear', null=True) - authorize_charge = TextField(column_name='authorizeCharge', null=True) - policy_agreement = TextField(column_name='policyAgreement', null=True) - waiver_form_signed_and_on_file_date = DateField(column_name='Waiver form signed and on file date.', null=True) - membership_agreement_signed_and_on_file_date = DateField(column_name='Membership Agreement signed and on file date.', null=True) - ip_address = TextField(column_name='IP Address', null=True) - audit_date = DateField(column_name='Audit Date', null=True) - agreement_version = TextField(column_name='Agreement Version', null=True) - paperwork_status = TextField(column_name='Paperwork status', null=True) - membership_agreement_dated = BooleanField(column_name='Membership agreement dated', null=True) - membership_agreement_acknowledgement_page_filled_out = BooleanField(column_name='Membership Agreement Acknowledgement Page Filled Out', null=True) - membership_agreement_signed = BooleanField(column_name='Membership Agreement Signed', null=True) - liability_form_filled_out = BooleanField(column_name='Liability Form Filled Out', null=True) + year_of_birth = TextField(column_name="Year of Birth", null=True) + account_name = TextField(column_name="Account Name", null=True) + first_name = TextField(column_name="First Name", null=True) + last_name = TextField(column_name="Last Name", null=True) + phone = TextField(column_name="Phone", null=True) + email = TextField(column_name="Email", null=True) + address_street = TextField(column_name="Address (Street)", null=True) + address_city = TextField(column_name="Address (City)", null=True) + address_state_province = TextField( + column_name="Address (State/Province)", null=True + ) + address_postal_code = TextField(column_name="Address (Postal Code)", null=True) + address_country = TextField(column_name="Address (Country)", null=True) + profile_description = TextField(column_name="Profile description", null=True) + website = TextField(column_name="Website", null=True) + fax = TextField(column_name="Fax", null=True) + contact_person = TextField(column_name="Contact Person", null=True) + password = TextField(column_name="Password", null=True) + position_relation = TextField(column_name="Position/relation", null=True) + parent_account_id = TextField(column_name="Parent Account ID", null=True) + gift_membership_purchased_by = TextField( + column_name="Gift Membership purchased by", null=True + ) + purchased_gift_membership_for = TextField( + column_name="Purchased Gift Membership for", null=True + ) + closet_storage = TextField(column_name="Closet Storage #", null=True) + storage_shelf = TextField(column_name="Storage Shelf #", null=True) + personal_studio_space = TextField(column_name="Personal Studio Space #", null=True) + access_permitted_shops_during_extended_hours = BooleanField( + column_name="Access Permitted Shops During Extended Hours?", null=True + ) + access_front_door_and_studio_space_during_extended_hours = BooleanField( + column_name="Access Front Door and Studio Space During Extended Hours?", + null=True, + ) + access_wood_shop = BooleanField(column_name="Access Wood Shop?", null=True) + access_metal_shop = BooleanField(column_name="Access Metal Shop?", null=True) + access_storage_closet = BooleanField( + column_name="Access Storage Closet?", null=True + ) + access_studio_space = BooleanField(column_name="Access Studio Space?", null=True) + access_front_door = BooleanField(column_name="Access Front Door?", null=True) + access_card_number = TextField(column_name="Access Card Number", null=True) + access_card_facility_code = TextField( + column_name="Access Card Facility Code", null=True + ) + auto_billing_id = TextField(column_name="Auto Billing ID", null=True) + billing_method = TextField(column_name="Billing Method", null=True) + renewal_date = DateField(column_name="Renewal Date", null=True) + join_date = DateField(column_name="Join Date", null=True) + admin_note = TextField(column_name="Admin note", null=True) + profile_gallery_image_url = TextField( + column_name="Profile gallery image URL", null=True + ) + business_card_image_url = TextField( + column_name="Business card image URL", null=True + ) + instagram = TextField(column_name="Instagram", null=True) + pinterest = TextField(column_name="Pinterest", null=True) + youtube = TextField(column_name="Youtube", null=True) + yelp = TextField(column_name="Yelp", null=True) + google = TextField(column_name="Google+", null=True) + bbb = TextField(column_name="BBB", null=True) + twitter = TextField(column_name="Twitter", null=True) + facebook = TextField(column_name="Facebook", null=True) + linked_in = TextField(column_name="LinkedIn", null=True) + do_not_show_street_address_in_profile = TextField( + column_name="Do not show street address in profile", null=True + ) + do_not_list_in_directory = TextField( + column_name="Do not list in directory", null=True + ) + how_did_you_hear = TextField(column_name="HowDidYouHear", null=True) + authorize_charge = TextField(column_name="authorizeCharge", null=True) + policy_agreement = TextField(column_name="policyAgreement", null=True) + waiver_form_signed_and_on_file_date = DateField( + column_name="Waiver form signed and on file date.", null=True + ) + membership_agreement_signed_and_on_file_date = DateField( + column_name="Membership Agreement signed and on file date.", null=True + ) + ip_address = TextField(column_name="IP Address", null=True) + audit_date = DateField(column_name="Audit Date", null=True) + agreement_version = TextField(column_name="Agreement Version", null=True) + paperwork_status = TextField(column_name="Paperwork status", null=True) + membership_agreement_dated = BooleanField( + column_name="Membership agreement dated", null=True + ) + membership_agreement_acknowledgement_page_filled_out = BooleanField( + column_name="Membership Agreement Acknowledgement Page Filled Out", null=True + ) + membership_agreement_signed = BooleanField( + column_name="Membership Agreement Signed", null=True + ) + liability_form_filled_out = BooleanField( + column_name="Liability Form Filled Out", null=True + ) _csv_headers_override = { - 'Account ID': 'uid', - 'Please tell us how you heard about the Claremont MakerSpace and what tools or shops you are most excited to start using:': 'how_did_you_hear', - 'Yes - I authorize TwinState MakerSpaces, Inc. to charge my credit card for the membership and other options that I have selected.': 'authorize_charge', - 'I have read the Claremont MakerSpace Membership Agreement & Policies, and agree to all terms stated therein.': 'policy_agreement' + "Account ID": "uid", + "Please tell us how you heard about the Claremont MakerSpace and what tools or shops you are most excited to start using:": "how_did_you_hear", + "Yes - I authorize TwinState MakerSpaces, Inc. to charge my credit card for the membership and other options that I have selected.": "authorize_charge", + "I have read the Claremont MakerSpace Membership Agreement & Policies, and agree to all terms stated therein.": "policy_agreement", } _date_fields = { - 'Join Date': '%b %d, %Y', - 'Renewal Date': '%b %d, %Y', - 'Audit Date': '%m/%d/%Y', - 'Membership Agreement signed and on file date.': '%m/%d/%Y', - 'Waiver form signed and on file date.': '%m/%d/%Y' + "Join Date": "%b %d, %Y", + "Renewal Date": "%b %d, %Y", + "Audit Date": "%m/%d/%Y", + "Membership Agreement signed and on file date.": "%m/%d/%Y", + "Waiver form signed and on file date.": "%m/%d/%Y", } class Meta: - table_name = 'members' + table_name = "members" + class MemberLabel(BaseModel): - uid = ForeignKeyField(Member, column_name='uid', backref='labels') - label_id = ForeignKeyField(Label, backref='members') + uid = ForeignKeyField(Member, column_name="uid", backref="labels") + label_id = ForeignKeyField(Label, backref="members") class Meta: - table_name = 'member_labels' - primary_key = CompositeKey('label_id', 'uid') + table_name = "member_labels" + primary_key = CompositeKey("label_id", "uid") + class Transaction(BaseModel): sid = FixedCharField(27, null=True) - uid = ForeignKeyField(Member, column_name='uid', backref='transactions', null=True) + uid = ForeignKeyField(Member, column_name="uid", backref="transactions", null=True) timestamp = DateTimeField() type = TextField(null=True) sum = DecimalField(13, 4, null=True) fee = DecimalField(13, 4, null=True) event_id = TextField(null=True) - for_ = TextField(column_name='For', null=True) - items = TextField(column_name='Items', null=True) - discount_code = TextField(column_name='Discount Code', null=True) - note = TextField(column_name='Note', null=True) - name = TextField(column_name='Name', null=True) - contact_person = TextField(column_name='Contact Person', null=True) - full_address = TextField(column_name='Full Address', null=True) - street = TextField(column_name='Street', null=True) - city = TextField(column_name='City', null=True) - state_province = TextField(column_name='State/Province', null=True) - postal_code = TextField(column_name='Postal Code', null=True) - country = TextField(column_name='Country', null=True) - phone = TextField(column_name='Phone', null=True) - email = TextField(column_name='Email', null=True) + for_ = TextField(column_name="For", null=True) + items = TextField(column_name="Items", null=True) + discount_code = TextField(column_name="Discount Code", null=True) + note = TextField(column_name="Note", null=True) + name = TextField(column_name="Name", null=True) + contact_person = TextField(column_name="Contact Person", null=True) + full_address = TextField(column_name="Full Address", null=True) + street = TextField(column_name="Street", null=True) + city = TextField(column_name="City", null=True) + state_province = TextField(column_name="State/Province", null=True) + postal_code = TextField(column_name="Postal Code", null=True) + country = TextField(column_name="Country", null=True) + phone = TextField(column_name="Phone", null=True) + email = TextField(column_name="Email", null=True) - _csv_headers_override = { - '_dp': 'timestamp', - 'Transaction Type': 'type' - } + _csv_headers_override = {"_dp": "timestamp", "Transaction Type": "type"} @classmethod def from_csv_dict(cls, data): txn = data.copy() # can't use '%s' format string, have to use the special function - txn['_dp'] = datetime.fromtimestamp(txn['_dp']) + txn["_dp"] = datetime.fromtimestamp(txn["_dp"]) return super().from_csv_dict(txn) class Meta: - table_name = 'transactions' + table_name = "transactions" primary_key = False diff --git a/membershipViewer.py b/membershipViewer.py index 841aba1..60941de 100644 --- a/membershipViewer.py +++ b/membershipViewer.py @@ -1,70 +1,85 @@ #!/usr/bin/env python3 -import re import http +import re from flask import Flask, render_template, request +from common import doors, membershipworks + app = Flask(__name__) -from common import doors, membershipworks def parse_list(member, regex): data_list = [] for key, value in member.items(): match = re.match(regex, key) - if match is not None and value != '': + if match is not None and value != "": data_list.append(match.group(1)) return ", ".join(data_list) + def parse_members(members): data = [] for member in members: props = { - 'Name': member['Account Name'], - 'Renewal Date': member['Renewal Date'], - 'Card Number': member['Access Card Facility Code'] + '-' \ - + member['Access Card Number'], - 'Account on Hold': "Yes" if member['Account on Hold'] != '' else "No" } + "Name": member["Account Name"], + "Renewal Date": member["Renewal Date"], + "Card Number": member["Access Card Facility Code"] + + "-" + + member["Access Card Number"], + "Account on Hold": "Yes" if member["Account on Hold"] != "" else "No", + } - props['Certifications'] = parse_list(member, 'Certified: (.*)') - props['Door Access'] = parse_list(member, 'Access (.*)\?') - props['Memebership Level'] = parse_list(member, 'CMS (.*)') + props["Certifications"] = parse_list(member, "Certified: (.*)") + props["Door Access"] = parse_list(member, "Access (.*)\?") + props["Memebership Level"] = parse_list(member, "CMS (.*)") data.append(props) return data + @app.route("/") def main(): # maybe not now: membership agreement signed # TODO: renewal date check - term = request.args.get('term', '') + term = request.args.get("term", "") if len(term) < 3: - return render_template("members.html", - error="Enter at least 3 characters to search") + return render_template( + "members.html", error="Enter at least 3 characters to search" + ) data = membershipworks.get_members( - ['members', 'staff'], - "lvl,xws,xms,xsc,xas,xfd,xac,phn,eml,lbl,xcf,nam,end") + ["members", "staff"], "lvl,xws,xms,xsc,xas,xfd,xac,phn,eml,lbl,xcf,nam,end" + ) members = parse_members(data) - members = [member for member in members - if term.lower() in member['Name'].lower()] - headers = ['Name', 'Certifications', 'Door Access', 'Memebership Level', - 'Card Number', 'Renewal Date', 'Account on Hold'] + members = [member for member in members if term.lower() in member["Name"].lower()] + headers = [ + "Name", + "Certifications", + "Door Access", + "Memebership Level", + "Card Number", + "Renewal Date", + "Account on Hold", + ] if len(members) > 4: return render_template( - "members.html", error="Too many results, please be more specific.") + "members.html", error="Too many results, please be more specific." + ) return render_template("members.html", headers=headers, members=members) -@app.route('/frontDoor/', methods=['POST']) + +@app.route("/frontDoor/", methods=["POST"]) def unlockLockDoor(lock): - doors['Front Door'].lockOrUnlockDoor(lock != 'unlock') - return ('', http.HTTPStatus.NO_CONTENT) + doors["Front Door"].lockOrUnlockDoor(lock != "unlock") + return ("", http.HTTPStatus.NO_CONTENT) + if __name__ == "__main__": - app.run(debug=True, host='0.0.0.0') + app.run(debug=True, host="0.0.0.0") diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..c08f7d9 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,383 @@ +[[package]] +category = "dev" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +name = "appdirs" +optional = false +python-versions = "*" +version = "1.4.3" + +[[package]] +category = "dev" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.3.0" + +[package.extras] +azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] +dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] +docs = ["sphinx", "zope.interface"] +tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] + +[[package]] +category = "main" +description = "Simple construction, analysis and modification of binary data." +name = "bitstring" +optional = false +python-versions = "*" +version = "3.1.6" + +[[package]] +category = "dev" +description = "The uncompromising code formatter." +name = "black" +optional = false +python-versions = ">=3.6" +version = "19.10b0" + +[package.dependencies] +appdirs = "*" +attrs = ">=18.1.0" +click = ">=6.5" +pathspec = ">=0.6,<1" +regex = "*" +toml = ">=0.9.4" +typed-ast = ">=1.4.0" + +[package.extras] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] + +[[package]] +category = "main" +description = "Python package for providing Mozilla's CA Bundle." +name = "certifi" +optional = false +python-versions = "*" +version = "2020.4.5.1" + +[[package]] +category = "main" +description = "Universal encoding detector for Python 2 and 3" +name = "chardet" +optional = false +python-versions = "*" +version = "3.0.4" + +[[package]] +category = "dev" +description = "Composable command line interface toolkit" +name = "click" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "7.1.1" + +[[package]] +category = "main" +description = "Internationalized Domain Names in Applications (IDNA)" +name = "idna" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.9" + +[[package]] +category = "dev" +description = "A Python utility / library to sort Python imports." +name = "isort" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "4.3.21" + +[package.extras] +pipfile = ["pipreqs", "requirementslib"] +pyproject = ["toml"] +requirements = ["pipreqs", "pip-api"] +xdg_home = ["appdirs (>=1.4.0)"] + +[[package]] +category = "main" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +name = "lxml" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" +version = "4.5.0" + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["beautifulsoup4"] +source = ["Cython (>=0.29.7)"] + +[[package]] +category = "main" +description = "Python interface to MySQL" +name = "mysqlclient" +optional = false +python-versions = "*" +version = "1.4.6" + +[[package]] +category = "dev" +description = "Utility library for gitignore style pattern matching of file paths." +name = "pathspec" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.8.0" + +[[package]] +category = "main" +description = "a little orm" +name = "peewee" +optional = false +python-versions = "*" +version = "3.13.3" + +[[package]] +category = "dev" +description = "Alternative regular expression module, to replace re." +name = "regex" +optional = false +python-versions = "*" +version = "2020.4.4" + +[[package]] +category = "main" +description = "Python HTTP for Humans." +name = "requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.23.0" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<4" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] + +[[package]] +category = "main" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +name = "ruamel.yaml" +optional = false +python-versions = "*" +version = "0.16.10" + +[package.dependencies] +[package.dependencies."ruamel.yaml.clib"] +python = "<3.9" +version = ">=0.1.2" + +[package.extras] +docs = ["ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +category = "main" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +marker = "platform_python_implementation == \"CPython\" and python_version < \"3.9\"" +name = "ruamel.yaml.clib" +optional = false +python-versions = "*" +version = "0.2.0" + +[[package]] +category = "dev" +description = "Python Library for Tom's Obvious, Minimal Language" +name = "toml" +optional = false +python-versions = "*" +version = "0.10.0" + +[[package]] +category = "dev" +description = "a fork of Python 2 and 3 ast modules with type comment support" +name = "typed-ast" +optional = false +python-versions = "*" +version = "1.4.1" + +[[package]] +category = "main" +description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "urllib3" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "1.25.9" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] + +[metadata] +content-hash = "5fe506efe8fd4c42bb9ef248033a7bd074d9f66e5b6a03727aa9cc554ae1d8fb" +python-versions = "^3.7" + +[metadata.files] +appdirs = [ + {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"}, + {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"}, +] +attrs = [ + {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, + {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, +] +bitstring = [ + {file = "bitstring-3.1.6-py2-none-any.whl", hash = "sha256:e392819965e7e0246e3cf6a51d5a54e731890ae03ebbfa3cd0e4f74909072096"}, + {file = "bitstring-3.1.6-py3-none-any.whl", hash = "sha256:7b60b0c300d0d3d0a24ec84abfda4b0eaed3dc56dc90f6cbfe497166c9ad8443"}, + {file = "bitstring-3.1.6.tar.gz", hash = "sha256:c97a8e2a136e99b523b27da420736ae5cb68f83519d633794a6a11192f69f8bf"}, +] +black = [ + {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, + {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, +] +certifi = [ + {file = "certifi-2020.4.5.1-py2.py3-none-any.whl", hash = "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304"}, + {file = "certifi-2020.4.5.1.tar.gz", hash = "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"}, +] +chardet = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] +click = [ + {file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"}, + {file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"}, +] +idna = [ + {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, + {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, +] +isort = [ + {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, + {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, +] +lxml = [ + {file = "lxml-4.5.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0701f7965903a1c3f6f09328c1278ac0eee8f56f244e66af79cb224b7ef3801c"}, + {file = "lxml-4.5.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:06d4e0bbb1d62e38ae6118406d7cdb4693a3fa34ee3762238bcb96c9e36a93cd"}, + {file = "lxml-4.5.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5828c7f3e615f3975d48f40d4fe66e8a7b25f16b5e5705ffe1d22e43fb1f6261"}, + {file = "lxml-4.5.0-cp27-cp27m-win32.whl", hash = "sha256:afdb34b715daf814d1abea0317b6d672476b498472f1e5aacbadc34ebbc26e89"}, + {file = "lxml-4.5.0-cp27-cp27m-win_amd64.whl", hash = "sha256:585c0869f75577ac7a8ff38d08f7aac9033da2c41c11352ebf86a04652758b7a"}, + {file = "lxml-4.5.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:8a0ebda56ebca1a83eb2d1ac266649b80af8dd4b4a3502b2c1e09ac2f88fe128"}, + {file = "lxml-4.5.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:fe976a0f1ef09b3638778024ab9fb8cde3118f203364212c198f71341c0715ca"}, + {file = "lxml-4.5.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7bc1b221e7867f2e7ff1933165c0cec7153dce93d0cdba6554b42a8beb687bdb"}, + {file = "lxml-4.5.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d068f55bda3c2c3fcaec24bd083d9e2eede32c583faf084d6e4b9daaea77dde8"}, + {file = "lxml-4.5.0-cp35-cp35m-win32.whl", hash = "sha256:e4aa948eb15018a657702fee0b9db47e908491c64d36b4a90f59a64741516e77"}, + {file = "lxml-4.5.0-cp35-cp35m-win_amd64.whl", hash = "sha256:1f2c4ec372bf1c4a2c7e4bb20845e8bcf8050365189d86806bad1e3ae473d081"}, + {file = "lxml-4.5.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5d467ce9c5d35b3bcc7172c06320dddb275fea6ac2037f72f0a4d7472035cea9"}, + {file = "lxml-4.5.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:95e67224815ef86924fbc2b71a9dbd1f7262384bca4bc4793645794ac4200717"}, + {file = "lxml-4.5.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ebec08091a22c2be870890913bdadd86fcd8e9f0f22bcb398abd3af914690c15"}, + {file = "lxml-4.5.0-cp36-cp36m-win32.whl", hash = "sha256:deadf4df349d1dcd7b2853a2c8796593cc346600726eff680ed8ed11812382a7"}, + {file = "lxml-4.5.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f2b74784ed7e0bc2d02bd53e48ad6ba523c9b36c194260b7a5045071abbb1012"}, + {file = "lxml-4.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fa071559f14bd1e92077b1b5f6c22cf09756c6de7139370249eb372854ce51e6"}, + {file = "lxml-4.5.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:edc15fcfd77395e24543be48871c251f38132bb834d9fdfdad756adb6ea37679"}, + {file = "lxml-4.5.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fd52e796fee7171c4361d441796b64df1acfceb51f29e545e812f16d023c4bbc"}, + {file = "lxml-4.5.0-cp37-cp37m-win32.whl", hash = "sha256:90ed0e36455a81b25b7034038e40880189169c308a3df360861ad74da7b68c1a"}, + {file = "lxml-4.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:df533af6f88080419c5a604d0d63b2c33b1c0c4409aba7d0cb6de305147ea8c8"}, + {file = "lxml-4.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b4b2c63cc7963aedd08a5f5a454c9f67251b1ac9e22fd9d72836206c42dc2a72"}, + {file = "lxml-4.5.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e5d842c73e4ef6ed8c1bd77806bf84a7cb535f9c0cf9b2c74d02ebda310070e1"}, + {file = "lxml-4.5.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:63dbc21efd7e822c11d5ddbedbbb08cd11a41e0032e382a0fd59b0b08e405a3a"}, + {file = "lxml-4.5.0-cp38-cp38-win32.whl", hash = "sha256:4235bc124fdcf611d02047d7034164897ade13046bda967768836629bc62784f"}, + {file = "lxml-4.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:d5b3c4b7edd2e770375a01139be11307f04341ec709cf724e0f26ebb1eef12c3"}, + {file = "lxml-4.5.0.tar.gz", hash = "sha256:8620ce80f50d023d414183bf90cc2576c2837b88e00bea3f33ad2630133bbb60"}, +] +mysqlclient = [ + {file = "mysqlclient-1.4.6-cp36-cp36m-win_amd64.whl", hash = "sha256:4c82187dd6ab3607150fbb1fa5ef4643118f3da122b8ba31c3149ddd9cf0cb39"}, + {file = "mysqlclient-1.4.6-cp37-cp37m-win_amd64.whl", hash = "sha256:9e6080a7aee4cc6a06b58b59239f20f1d259c1d2fddf68ddeed242d2311c7087"}, + {file = "mysqlclient-1.4.6-cp38-cp38-win_amd64.whl", hash = "sha256:f646f8d17d02be0872291f258cce3813497bc7888cd4712a577fd1e719b2f213"}, + {file = "mysqlclient-1.4.6.tar.gz", hash = "sha256:f3fdaa9a38752a3b214a6fe79d7cae3653731a53e577821f9187e67cbecb2e16"}, +] +pathspec = [ + {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, + {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, +] +peewee = [ + {file = "peewee-3.13.3.tar.gz", hash = "sha256:1269a9736865512bd4056298003aab190957afe07d2616cf22eaf56cb6398369"}, +] +regex = [ + {file = "regex-2020.4.4-cp27-cp27m-win32.whl", hash = "sha256:90742c6ff121a9c5b261b9b215cb476eea97df98ea82037ec8ac95d1be7a034f"}, + {file = "regex-2020.4.4-cp27-cp27m-win_amd64.whl", hash = "sha256:24f4f4062eb16c5bbfff6a22312e8eab92c2c99c51a02e39b4eae54ce8255cd1"}, + {file = "regex-2020.4.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:08119f707f0ebf2da60d2f24c2f39ca616277bb67ef6c92b72cbf90cbe3a556b"}, + {file = "regex-2020.4.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c9423a150d3a4fc0f3f2aae897a59919acd293f4cb397429b120a5fcd96ea3db"}, + {file = "regex-2020.4.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:c087bff162158536387c53647411db09b6ee3f9603c334c90943e97b1052a156"}, + {file = "regex-2020.4.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:1cbe0fa0b7f673400eb29e9ef41d4f53638f65f9a2143854de6b1ce2899185c3"}, + {file = "regex-2020.4.4-cp36-cp36m-win32.whl", hash = "sha256:0ce9537396d8f556bcfc317c65b6a0705320701e5ce511f05fc04421ba05b8a8"}, + {file = "regex-2020.4.4-cp36-cp36m-win_amd64.whl", hash = "sha256:7e1037073b1b7053ee74c3c6c0ada80f3501ec29d5f46e42669378eae6d4405a"}, + {file = "regex-2020.4.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4385f12aa289d79419fede43f979e372f527892ac44a541b5446617e4406c468"}, + {file = "regex-2020.4.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a58dd45cb865be0ce1d5ecc4cfc85cd8c6867bea66733623e54bd95131f473b6"}, + {file = "regex-2020.4.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:ccccdd84912875e34c5ad2d06e1989d890d43af6c2242c6fcfa51556997af6cd"}, + {file = "regex-2020.4.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ea4adf02d23b437684cd388d557bf76e3afa72f7fed5bbc013482cc00c816948"}, + {file = "regex-2020.4.4-cp37-cp37m-win32.whl", hash = "sha256:2294f8b70e058a2553cd009df003a20802ef75b3c629506be20687df0908177e"}, + {file = "regex-2020.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:e91ba11da11cf770f389e47c3f5c30473e6d85e06d7fd9dcba0017d2867aab4a"}, + {file = "regex-2020.4.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5635cd1ed0a12b4c42cce18a8d2fb53ff13ff537f09de5fd791e97de27b6400e"}, + {file = "regex-2020.4.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:23069d9c07e115537f37270d1d5faea3e0bdded8279081c4d4d607a2ad393683"}, + {file = "regex-2020.4.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c162a21e0da33eb3d31a3ac17a51db5e634fc347f650d271f0305d96601dc15b"}, + {file = "regex-2020.4.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:fb95debbd1a824b2c4376932f2216cc186912e389bdb0e27147778cf6acb3f89"}, + {file = "regex-2020.4.4-cp38-cp38-win32.whl", hash = "sha256:2a3bf8b48f8e37c3a40bb3f854bf0121c194e69a650b209628d951190b862de3"}, + {file = "regex-2020.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:5bfed051dbff32fd8945eccca70f5e22b55e4148d2a8a45141a3b053d6455ae3"}, + {file = "regex-2020.4.4.tar.gz", hash = "sha256:295badf61a51add2d428a46b8580309c520d8b26e769868b922750cf3ce67142"}, +] +requests = [ + {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"}, + {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"}, +] +"ruamel.yaml" = [ + {file = "ruamel.yaml-0.16.10-py2.py3-none-any.whl", hash = "sha256:0962fd7999e064c4865f96fb1e23079075f4a2a14849bcdc5cdba53a24f9759b"}, + {file = "ruamel.yaml-0.16.10.tar.gz", hash = "sha256:099c644a778bf72ffa00524f78dd0b6476bca94a1da344130f4bf3381ce5b954"}, +] +"ruamel.yaml.clib" = [ + {file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9c6d040d0396c28d3eaaa6cb20152cb3b2f15adf35a0304f4f40a3cf9f1d2448"}, + {file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d55386129291b96483edcb93b381470f7cd69f97585829b048a3d758d31210a"}, + {file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-win32.whl", hash = "sha256:8073c8b92b06b572e4057b583c3d01674ceaf32167801fe545a087d7a1e8bf52"}, + {file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-win_amd64.whl", hash = "sha256:615b0396a7fad02d1f9a0dcf9f01202bf9caefee6265198f252c865f4227fcc6"}, + {file = "ruamel.yaml.clib-0.2.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:a0ff786d2a7dbe55f9544b3f6ebbcc495d7e730df92a08434604f6f470b899c5"}, + {file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:ea4362548ee0cbc266949d8a441238d9ad3600ca9910c3fe4e82ee3a50706973"}, + {file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:77556a7aa190be9a2bd83b7ee075d3df5f3c5016d395613671487e79b082d784"}, + {file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-win32.whl", hash = "sha256:392b7c371312abf27fb549ec2d5e0092f7ef6e6c9f767bfb13e83cb903aca0fd"}, + {file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-win_amd64.whl", hash = "sha256:ed5b3698a2bb241b7f5cbbe277eaa7fe48b07a58784fba4f75224fd066d253ad"}, + {file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7aee724e1ff424757b5bd8f6c5bbdb033a570b2b4683b17ace4dbe61a99a657b"}, + {file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d0d3ac228c9bbab08134b4004d748cf9f8743504875b3603b3afbb97e3472947"}, + {file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-win32.whl", hash = "sha256:f9dcc1ae73f36e8059589b601e8e4776b9976effd76c21ad6a855a74318efd6e"}, + {file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1e77424825caba5553bbade750cec2277ef130647d685c2b38f68bc03453bac6"}, + {file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d10e9dd744cf85c219bf747c75194b624cc7a94f0c80ead624b06bfa9f61d3bc"}, + {file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:550168c02d8de52ee58c3d8a8193d5a8a9491a5e7b2462d27ac5bf63717574c9"}, + {file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-win32.whl", hash = "sha256:57933a6986a3036257ad7bf283529e7c19c2810ff24c86f4a0cfeb49d2099919"}, + {file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b1b7fcee6aedcdc7e62c3a73f238b3d080c7ba6650cd808bce8d7761ec484070"}, + {file = "ruamel.yaml.clib-0.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:be018933c2f4ee7de55e7bd7d0d801b3dfb09d21dad0cce8a97995fd3e44be30"}, + {file = "ruamel.yaml.clib-0.2.0.tar.gz", hash = "sha256:b66832ea8077d9b3f6e311c4a53d06273db5dc2db6e8a908550f3c14d67e718c"}, +] +toml = [ + {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, + {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, + {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, +] +typed-ast = [ + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, + {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, + {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, + {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, + {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, + {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, + {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, + {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, +] +urllib3 = [ + {file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"}, + {file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a29c725 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,31 @@ +[tool.black] + +[tool.isort] +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +line_length = 88 + +[tool.poetry] +name = "memberPlumbing" +version = "0.1.0" +description = "" +authors = ["Adam Goldsmith "] + +[tool.poetry.dependencies] +python = "^3.7" +requests = "^2.23.0" +"ruamel.yaml" = "^0.16.10" +bitstring = "^3.1.6" +lxml = "^4.5.0" +peewee = "^3.13.2" +mysqlclient = "^1.4.6" + +[tool.poetry.dev-dependencies] +black = "^19.10b0" +isort = "^4.3.21" + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/sqlExport.py b/sqlExport.py index ba5dc8f..5b45d10 100755 --- a/sqlExport.py +++ b/sqlExport.py @@ -3,7 +3,8 @@ from datetime import datetime from common import membershipworks -from lib.mw_models import database, Label, Member, MemberLabel, Transaction +from lib.mw_models import Label, Member, MemberLabel, Transaction, database + @database.atomic() def main(): @@ -11,32 +12,31 @@ def main(): database.create_tables([Label, Member, MemberLabel, Transaction]) print("Updating labels") - labels = membershipworks._parse_flags()['labels'] - Label \ - .insert_many([{'label_id': v, 'label': k} for k, v in labels.items()]) \ - .on_conflict(action="update", preserve=[Label.label]) \ - .execute() + labels = membershipworks._parse_flags()["labels"] + Label.insert_many( + [{"label_id": v, "label": k} for k, v in labels.items()] + ).on_conflict(action="update", preserve=[Label.label]).execute() print("Getting/Updating members...") members = membershipworks.get_all_members() for m in members: # replace flags by booleans - for flag in [dek['lbl'] for dek in membershipworks.org_info['dek']]: + for flag in [dek["lbl"] for dek in membershipworks.org_info["dek"]]: if flag in m: m[flag] = m[flag] == flag for field_id, field in membershipworks._all_fields().items(): # convert checkboxes to real booleans - if field.get('typ') == 8 and field['lbl'] in m: # check box - m[field['lbl']] = True if m[field['lbl']] == 'Y' else False + if field.get("typ") == 8 and field["lbl"] in m: # check box + m[field["lbl"]] = True if m[field["lbl"]] == "Y" else False for member in members: # create/update member Member.from_csv_dict(member).magic_save() # update member's labels - for label, label_id in membershipworks._parse_flags()['labels'].items(): - ml = MemberLabel(uid=member['Account ID'], label_id=label_id) + for label, label_id in membershipworks._parse_flags()["labels"].items(): + ml = MemberLabel(uid=member["Account ID"], label_id=label_id) if member[label]: ml.magic_save() else: @@ -49,14 +49,18 @@ def main(): transactions_json = membershipworks.get_transactions(start_date, now, json=True) # this is terrible, but as long as the dates are the same, should be fiiiine transactions = [{**j, **v} for j, v in zip(transactions_csv, transactions_json)] - assert all([t['Account ID'] == t.get('uid', '') - and t['Payment ID'] == t.get('sid', '') - for t in transactions]) + assert all( + [ + t["Account ID"] == t.get("uid", "") and t["Payment ID"] == t.get("sid", "") + for t in transactions + ] + ) for transaction in transactions: Transaction.from_csv_dict(transaction).magic_save() # TODO: folders, levels, addons -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/ucsAccounts.py b/ucsAccounts.py index d380a4d..bbc4db7 100755 --- a/ucsAccounts.py +++ b/ucsAccounts.py @@ -8,82 +8,98 @@ from common import membershipworks LDAP_BASE = "cn=users,dc=sawtooth,dc=claremontmakerspace,dc=org" GROUP_BASE = "cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org" -GROUPS_REGEX = "|".join(['Certified: .*', 'Access .*\?', 'CMS .*', 'Volunteer: .*']) +GROUPS_REGEX = "|".join(["Certified: .*", "Access .*\?", "CMS .*", "Volunteer: .*"]) RAND_PW_LEN = 20 + def makeGroups(members): - groups = [key.replace(':', '.').replace('?', '') - for key in members[0].keys() - if re.match(GROUPS_REGEX, key) is not None] + groups = [ + key.replace(":", ".").replace("?", "") + for key in members[0].keys() + if re.match(GROUPS_REGEX, key) is not None + ] for group in groups: - subprocess.call(["udm", "groups/group", "create", - "--position", GROUP_BASE, - "--set", "name=" + group]) + subprocess.call( + [ + "udm", + "groups/group", + "create", + "--position", + GROUP_BASE, + "--set", + "name=" + group, + ] + ) + def makeSets(props): - return sum([["--set", key + "=" + value] - for key, value in props.items()], []) + return sum([["--set", key + "=" + value] for key, value in props.items()], []) + def makeAppendGroups(member): - groups = [key.replace(':', '.').replace('?', '') - for key, value in member.items() - if re.match(GROUPS_REGEX, key) is not None and value != ''] - return sum([["--append", "groups=cn=" + group + ',' + GROUP_BASE] - for group in groups], []) + groups = [ + key.replace(":", ".").replace("?", "") + for key, value in member.items() + if re.match(GROUPS_REGEX, key) is not None and value != "" + ] + return sum( + [["--append", "groups=cn=" + group + "," + GROUP_BASE] for group in groups], [] + ) + def main(): - members = membershipworks.get_members(['Members', 'CMS Staff'], - "lvl,phn,eml,lbl,nam,end,_id") + members = membershipworks.get_members( + ["Members", "CMS Staff"], "lvl,phn,eml,lbl,nam,end,_id" + ) makeGroups(members) for member in members: - randomPass = ''.join(random.choice(string.ascii_letters + string.digits) - for x in range(0, RAND_PW_LEN)) + randomPass = "".join( + random.choice(string.ascii_letters + string.digits) + for x in range(0, RAND_PW_LEN) + ) username = member["Account Name"].lower().replace(" ", ".") props = { - "title": "", # Title + "title": "", # Title "firstname": member["First Name"], - "lastname": member["Last Name"], # (c) - "username": username, # (cmr) - "description": "", # Description - "password": randomPass, # (c) Password - #"mailPrimaryAddress": member["Email"], # Primary e-mail address - #"displayName": "", # Display name - #"birthday": "", # Birthdate - #"jpegPhoto": "", # Picture of the user (JPEG format) - + "lastname": member["Last Name"], # (c) + "username": username, # (cmr) + "description": "", # Description + "password": randomPass, # (c) Password + # "mailPrimaryAddress": member["Email"], # Primary e-mail address + # "displayName": "", # Display name + # "birthday": "", # Birthdate + # "jpegPhoto": "", # Picture of the user (JPEG format) "employeeNumber": member["Account ID"], - #"employeeType": "", # Employee type - - "homedrive": "H:", # Windows home drive - "sambahome": "\\\\ucs\\" + username, # Windows home path - "profilepath": "%LOGONSERVER%\\%USERNAME%\\windows-profiles\\default", # Windows profile directory - + # "employeeType": "", # Employee type + "homedrive": "H:", # Windows home drive + "sambahome": "\\\\ucs\\" + username, # Windows home path + "profilepath": "%LOGONSERVER%\\%USERNAME%\\windows-profiles\\default", # Windows profile directory "disabled": "1" if member["Account on Hold"] != "" else "0", - #"userexpiry": member["Renewal Date"], - - "pwdChangeNextLogin": "1", # User has to change password on next login - - #"sambaLogonHours": "", # Permitted times for Windows logins - + # "userexpiry": member["Renewal Date"], + "pwdChangeNextLogin": "1", # User has to change password on next login + # "sambaLogonHours": "", # Permitted times for Windows logins "e-mail": member["Email"], # ([]) E-mail address - "phone": member["Phone"], # Telephone number - #"PasswordRecoveryMobile": member["Phone"], # Mobile phone number - "PasswordRecoveryEmail": member["Email"] + "phone": member["Phone"], # Telephone number + # "PasswordRecoveryMobile": member["Phone"], # Mobile phone number + "PasswordRecoveryEmail": member["Email"], } - subprocess.call(["udm", "users/user", "create", - "--position", LDAP_BASE] + makeSets(props)) + subprocess.call( + ["udm", "users/user", "create", "--position", LDAP_BASE] + makeSets(props) + ) # remove props we don't want to reset props.pop("password") props.pop("pwdChangeNextLogin") - subprocess.call(["udm", "users/user", "modify", - "--dn", "uid=" + username + "," + LDAP_BASE] - + makeSets(props) - + makeAppendGroups(member)) + subprocess.call( + ["udm", "users/user", "modify", "--dn", "uid=" + username + "," + LDAP_BASE] + + makeSets(props) + + makeAppendGroups(member) + ) + if __name__ == "__main__": main()