Setup poetry, apply Black and isort styling

This commit is contained in:
Adam Goldsmith 2020-03-30 14:01:39 -04:00
parent 525fd24a22
commit 7981a05a46
12 changed files with 1135 additions and 482 deletions

View File

@ -1,11 +1,15 @@
from ruamel.yaml import YAML
import os import os
from ruamel.yaml import YAML
from lib.hid.DoorController import DoorController from lib.hid.DoorController import DoorController
from lib.MembershipWorks import MembershipWorks from lib.MembershipWorks import MembershipWorks
from passwords import (
from passwords import DOOR_USERNAME, DOOR_PASSWORD DOOR_PASSWORD,
from passwords import MEMBERSHIPWORKS_USERNAME, MEMBERSHIPWORKS_PASSWORD DOOR_USERNAME,
MEMBERSHIPWORKS_PASSWORD,
MEMBERSHIPWORKS_USERNAME,
)
try: try:
with open(os.path.dirname(os.path.abspath(__file__)) + "/config.yaml") as f: 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: with open("config.yaml") as f:
config = YAML().load(f) config = YAML().load(f)
doors = {doorName: DoorController(doorData['ip'], doors = {
DOOR_USERNAME, DOOR_PASSWORD, doorName: DoorController(
name=doorName, access=doorData['access']) doorData["ip"],
for doorName, doorData in config["doors"].items()} DOOR_USERNAME,
DOOR_PASSWORD,
name=doorName,
access=doorData["access"],
)
for doorName, doorData in config["doors"].items()
}
memberLevels = config['memberLevels'] memberLevels = config["memberLevels"]
doorSpecificSchedules = config['doorSpecificSchedules'] doorSpecificSchedules = config["doorSpecificSchedules"]
membershipworks = MembershipWorks() membershipworks = MembershipWorks()
membershipworks.login(MEMBERSHIPWORKS_USERNAME, MEMBERSHIPWORKS_PASSWORD) membershipworks.login(MEMBERSHIPWORKS_USERNAME, MEMBERSHIPWORKS_PASSWORD)

View File

@ -2,15 +2,27 @@
import copy 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.Credential import Credential
from lib.hid.DoorController import ROOT, E from lib.hid.DoorController import ROOT, E
class Member():
def __init__(self, forename="", surname="", membershipWorksID="", class Member:
middleName="", email="", phone="", def __init__(
cardholderID=None, doorAccess=[], self,
credentials=set(), levels=[], extraLevels=[], schedules=[]): forename="",
surname="",
membershipWorksID="",
middleName="",
email="",
phone="",
cardholderID=None,
doorAccess=[],
credentials=set(),
levels=[],
extraLevels=[],
schedules=[],
):
self.forename = forename self.forename = forename
self.surname = surname self.surname = surname
self.membershipWorksID = membershipWorksID self.membershipWorksID = membershipWorksID
@ -38,68 +50,87 @@ Schedules: {self.schedules}
class MembershipworksMember(Member): class MembershipworksMember(Member):
def __init__(self, data, formerMember=False): def __init__(self, data, formerMember=False):
super().__init__(data["First Name"], super().__init__(
data["Last Name"], data["First Name"],
membershipWorksID=data["Account ID"], data["Last Name"],
email=data["Email"], membershipWorksID=data["Account ID"],
phone=data["Phone"]) email=data["Email"],
phone=data["Phone"],
)
if data["Access Card Number"] != "": if data["Access Card Number"] != "":
self.credentials = set([Credential( self.credentials = set(
code=(data["Access Card Facility Code"], [
data["Access Card Number"]))]) Credential(
code=(
data["Access Card Facility Code"],
data["Access Card Number"],
)
)
]
)
else: else:
self.credentials = set() self.credentials = set()
self.onHold=data["Account on Hold"] != "" self.onHold = data["Account on Hold"] != ""
self.limitedOperations=data['Access Permitted During Limited Operations'] == "Y" self.limitedOperations = (
self.formerMember=formerMember data["Access Permitted During Limited Operations"] == "Y"
)
self.formerMember = formerMember
levels = {k: v for k, v in memberLevels.items() if data[k] == k} levels = {k: v for k, v in memberLevels.items() if data[k] == k}
self.levels = list(levels.keys()) self.levels = list(levels.keys())
self.schedules = list(levels.values()) self.schedules = list(levels.values())
self.extraLevels = { self.extraLevels = {
schedule: sum((doors for prop, doors in props.items() schedule: sum(
if data[prop] == "Y"), (doors for prop, doors in props.items() if data[prop] == "Y"), []
[]) )
for schedule, props in doorSpecificSchedules.items() for schedule, props in doorSpecificSchedules.items()
} }
self.doorAccess = [ self.doorAccess = [
door for door, doorData in doors.items() door
for door, doorData in doors.items()
if data["Access " + doorData.access + "?"] == "Y" if data["Access " + doorData.access + "?"] == "Y"
] ]
def to_DoorMember(self, door): def to_DoorMember(self, door):
doorLevels = [k for k, v in self.extraLevels.items() if door.name in v] 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.onHold
or self.formerMember or self.formerMember
or not self.limitedOperations): or not self.limitedOperations
):
schedules = [] schedules = []
else: else:
schedules = self.schedules + doorLevels schedules = self.schedules + doorLevels
dm = DoorMember(door, dm = DoorMember(
forename=self.forename, door,
surname=self.surname, forename=self.forename,
membershipWorksID=self.membershipWorksID, surname=self.surname,
email=self.email, membershipWorksID=self.membershipWorksID,
phone=self.phone, email=self.email,
levels=self.levels + doorLevels, phone=self.phone,
doorAccess=self.doorAccess, levels=self.levels + doorLevels,
credentials=self.credentials, doorAccess=self.doorAccess,
schedules=schedules) credentials=self.credentials,
schedules=schedules,
)
return dm return dm
def __str__(self): def __str__(self):
return super().__str__() + f"""OnHold? {self.onHold} return (
super().__str__()
+ f"""OnHold? {self.onHold}
Limited Operations Access? {self.limitedOperations} Limited Operations Access? {self.limitedOperations}
Former Member? {self.formerMember} Former Member? {self.formerMember}
""" """
)
class DoorMember(Member): class DoorMember(Member):
@ -109,65 +140,83 @@ class DoorMember(Member):
@classmethod @classmethod
def from_cardholder(cls, data, door): def from_cardholder(cls, data, door):
ch = cls(door=door, ch = cls(
forename=data.get('forename', ""), door=door,
surname=data.get('surname', ""), forename=data.get("forename", ""),
membershipWorksID=data.attrib.get('custom2', ""), surname=data.get("surname", ""),
middleName=data.attrib.get('middleName', ""), membershipWorksID=data.attrib.get("custom2", ""),
email=data.attrib.get('email', ""), middleName=data.attrib.get("middleName", ""),
phone=data.attrib.get('phone', ""), email=data.attrib.get("email", ""),
cardholderID=data.attrib['cardholderID']) phone=data.attrib.get("phone", ""),
cardholderID=data.attrib["cardholderID"],
)
ch.credentials = set( ch.credentials = set(
Credential(hex=(c.attrib['rawCardNumber'])) Credential(hex=(c.attrib["rawCardNumber"]))
for c in data.findall('{*}Credential')) for c in data.findall("{*}Credential")
)
ch.levels = data.attrib.get('custom1', "").split('|') ch.levels = data.attrib.get("custom1", "").split("|")
ch.schedules = [r.attrib['scheduleName'] ch.schedules = [r.attrib["scheduleName"] for r in data.findall("{*}Role")]
for r in data.findall('{*}Role')]
return ch return ch
def attribs(self): def attribs(self):
return {"forename": self.forename, return {
"surname": self.surname, "forename": self.forename,
"middleName": self.middleName, "surname": self.surname,
"email": self.email, "middleName": self.middleName,
"phone": self.phone, "email": self.email,
"custom1": "|".join(self.levels).replace("&", "and"), "phone": self.phone,
"custom2": self.membershipWorksID} "custom1": "|".join(self.levels).replace("&", "and"),
"custom2": self.membershipWorksID,
}
def make_schedules(self, schedulesMap): def make_schedules(self, schedulesMap):
roles = [ roles = [
E.Role({"roleID": self.cardholderID, E.Role(
{
"roleID": self.cardholderID,
"scheduleID": schedulesMap[schedule], "scheduleID": schedulesMap[schedule],
"resourceID": "0"}) "resourceID": "0",
for schedule in self.schedules] }
)
for schedule in self.schedules
]
return E.RoleSet({"action": "UD", "roleSetID": self.cardholderID}, return E.RoleSet(
E.Roles(*roles)) {"action": "UD", "roleSetID": self.cardholderID}, E.Roles(*roles)
)
def make_credentials(self, newCredentials, cardFormats): def make_credentials(self, newCredentials, cardFormats):
out = [ out = [
E.Credential( E.Credential(
{"formatName": str(credential.code[0]), {
"cardNumber": str(credential.code[1]), "formatName": str(credential.code[0]),
"formatID": cardFormats[str(credential.code[0])], "cardNumber": str(credential.code[1]),
"isCard": "true", "formatID": cardFormats[str(credential.code[0])],
"cardholderID": self.cardholderID}) "isCard": "true",
for credential in newCredentials] "cardholderID": self.cardholderID,
}
)
for credential in newCredentials
]
return E.Credentials({"action": "AD"}, *out) return E.Credentials({"action": "AD"}, *out)
def update_door(door, members): def update_door(door, members):
cardFormats = door.get_cardFormats() cardFormats = door.get_cardFormats()
cardholders = {member.membershipWorksID: member cardholders = {
for member in [DoorMember.from_cardholder(ch, door) member.membershipWorksID: member
for ch in door.get_cardholders()]} for member in [
DoorMember.from_cardholder(ch, door) for ch in door.get_cardholders()
]
}
schedulesMap = door.get_scheduleMap() schedulesMap = door.get_scheduleMap()
allCredentials = set(Credential(hex=c.attrib['rawCardNumber']) allCredentials = set(
for c in door.get_credentials()) Credential(hex=c.attrib["rawCardNumber"]) for c in door.get_credentials()
)
# TODO: can I combine requests? # TODO: can I combine requests?
for membershipworksMember in members: for membershipworksMember in members:
@ -176,12 +225,12 @@ def update_door(door, members):
if member.membershipWorksID not in cardholders: if member.membershipWorksID not in cardholders:
print("- Adding Member {member.forename} {member.surname}:") print("- Adding Member {member.forename} {member.surname}:")
print(f" - {member.attribs()}") print(f" - {member.attribs()}")
resp = door.doXMLRequest(ROOT( resp = door.doXMLRequest(
E.Cardholders( ROOT(E.Cardholders({"action": "AD"}, E.Cardholder(member.attribs())))
{"action": "AD"}, )
E.Cardholder(member.attribs())))) member.cardholderID = resp.find("{*}Cardholders/{*}Cardholder").attrib[
member.cardholderID = resp.find('{*}Cardholders/{*}Cardholder') \ "cardholderID"
.attrib["cardholderID"] ]
# create a dummy ch to force an update # create a dummy ch to force an update
# TODO: probably a cleaner way to do this # 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"- Updating profile for {member.forename} {member.surname}")
print(f" - Old: {ch.attribs()}") print(f" - Old: {ch.attribs()}")
print(f" - New: {member.attribs()}") print(f" - New: {member.attribs()}")
door.doXMLRequest(ROOT( door.doXMLRequest(
E.Cardholders( ROOT(
{"action": "UD", "cardholderID": member.cardholderID}, E.Cardholders(
E.CardHolder(member.attribs())))) {"action": "UD", "cardholderID": member.cardholderID},
E.CardHolder(member.attribs()),
)
)
)
if member.credentials != ch.credentials: if member.credentials != ch.credentials:
print(f"- Updating card for {member.forename} {member.surname}") print(f"- Updating card for {member.forename} {member.surname}")
@ -210,69 +263,106 @@ def update_door(door, members):
newCards = member.credentials newCards = member.credentials
allNewCards = set( allNewCards = set(
card for m in members if m != membershipworksMember card
for card in m.credentials) for m in members
if m != membershipworksMember
for card in m.credentials
)
# cards removed, and won't be reassigned to someone else # cards removed, and won't be reassigned to someone else
for card in (oldCards - newCards) - allNewCards: for card in (oldCards - newCards) - allNewCards:
door.doXMLRequest(ROOT(E.Credentials( door.doXMLRequest(
{"action": "UD", ROOT(
"rawCardNumber": card.hex, E.Credentials(
"isCard": "true"}, {
E.Credential({"cardholderID": ""})))) "action": "UD",
"rawCardNumber": card.hex,
"isCard": "true",
},
E.Credential({"cardholderID": ""}),
)
)
)
if newCards - oldCards: # cards added if newCards - oldCards: # cards added
for card in newCards & allNewCards: # new card exists in another member for card in newCards & allNewCards: # new card exists in another member
print([m for m in members print(
for card in m.credentials [
if card in newCards]) m
for m in members
for card in m.credentials
if card in newCards
]
)
raise Exception(f"Duplicate Card in input data! {card}") raise Exception(f"Duplicate Card in input data! {card}")
# card existed in door, and needs to be reassigned # card existed in door, and needs to be reassigned
for card in newCards & allCredentials: for card in newCards & allCredentials:
door.doXMLRequest(ROOT(E.Credentials( door.doXMLRequest(
{"action": "UD", ROOT(
"rawCardNumber": card.hex, E.Credentials(
"isCard": "true"}, {
E.Credential({"cardholderID": member.cardholderID})))) "action": "UD",
"rawCardNumber": card.hex,
"isCard": "true",
},
E.Credential({"cardholderID": member.cardholderID}),
)
)
)
# cards that never existed, and need to be created # cards that never existed, and need to be created
if newCards - allCredentials: if newCards - allCredentials:
door.doXMLRequest(ROOT(member.make_credentials( door.doXMLRequest(
newCards - allCredentials, cardFormats))) ROOT(
member.make_credentials(
newCards - allCredentials, cardFormats
)
)
)
if member.schedules != ch.schedules: if member.schedules != ch.schedules:
print("- Updating schedule for" + print(
f" {member.forename} {member.surname}:" + "- Updating schedule for"
f" {ch.schedules} -> {member.schedules}") + f" {member.forename} {member.surname}:"
+ f" {ch.schedules} -> {member.schedules}"
)
door.doXMLRequest(ROOT(member.make_schedules(schedulesMap))) door.doXMLRequest(ROOT(member.make_schedules(schedulesMap)))
# TODO: delete cardholders that are no longer members? # TODO: delete cardholders that are no longer members?
def main(): def main():
membershipworks_attributes = \ membershipworks_attributes = (
"_id,nam,phn,eml,lvl,lbl,xws,xms,xsc,xas,xfd,xac,xcf,xeh,xse,xlo" "_id,nam,phn,eml,lvl,lbl,xws,xms,xsc,xas,xfd,xac,xcf,xeh,xse,xlo"
)
memberData = membershipworks.get_members( 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] members = [MembershipworksMember(m) for m in memberData]
formerMemberData = membershipworks.get_members( formerMemberData = membershipworks.get_members(
['Former Members'], membershipworks_attributes) ["Former Members"], membershipworks_attributes
formerMembers = [MembershipworksMember(m, formerMember=True) )
for m in formerMemberData] formerMembers = [
MembershipworksMember(m, formerMember=True) for m in formerMemberData
]
for formerMember in formerMembers: for formerMember in formerMembers:
member = next( member = next(
(m for m in members (
if m.membershipWorksID == formerMember.membershipWorksID), m
None) for m in members
if m.membershipWorksID == formerMember.membershipWorksID
),
None,
)
# member exists in another folder # member exists in another folder
if member is not None: if member is not None:
member.formerMember = True member.formerMember = True
else: # member is only a former member else: # member is only a former member
formerMember.formerMember = True formerMember.formerMember = True
members.append(formerMember) members.append(formerMember)
@ -281,5 +371,5 @@ def main():
update_door(door, members) update_door(door, members)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@ -1,27 +1,34 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from collections import defaultdict
from lxml import etree
import os import os
import re 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): def getStrings(door):
"""Parses out the message strings from source.""" """Parses out the message strings from source."""
r = requests.get('https://' + door.ip + '/html/en_EN/en_EN.js', r = requests.get(
auth=requests.auth.HTTPDigestAuth(door.username, door.password), "https://" + door.ip + "/html/en_EN/en_EN.js",
verify=False) auth=requests.auth.HTTPDigestAuth(door.username, door.password),
verify=False,
)
regex = re.compile(r'([0-9]+)="([^"]*)') regex = re.compile(r'([0-9]+)="([^"]*)')
strings = [regex.search(s) for s in r.text.split(';') strings = [
if s.startswith('localeStrings.eventDetails')] 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}) print({int(g.group(1)): g.group(2) for g in strings})
def getMessages(door): def getMessages(door):
# get parameters for messages to get? # get parameters for messages to get?
# honestly not really sure why this is required, their API is confusing # honestly not really sure why this is required, their API is confusing
parXMLIn = E_plain.VertXMessage( parXMLIn = E_plain.VertXMessage(E.EventMessages({"action": "LR"}))
E.EventMessages({"action": "LR"}))
parXMLOut = door.doXMLRequest(parXMLIn) parXMLOut = door.doXMLRequest(parXMLIn)
etree.dump(parXMLOut) etree.dump(parXMLOut)
@ -29,8 +36,9 @@ def getMessages(door):
# read last log # read last log
tree = etree.ElementTree(file="logs/" + door.name + ".xml") tree = etree.ElementTree(file="logs/" + door.name + ".xml")
root = tree.getroot() root = tree.getroot()
recordCount = int(parXMLOut[0].attrib["historyRecordMarker"]) - \ recordCount = int(parXMLOut[0].attrib["historyRecordMarker"]) - int(
int(root[0][0].attrib["recordMarker"]) root[0][0].attrib["recordMarker"]
)
else: else:
# first run for this door # first run for this door
root = None root = None
@ -42,26 +50,35 @@ def getMessages(door):
print("Getting", recordCount, "records") print("Getting", recordCount, "records")
# get the actual messages # get the actual messages
eventsXMLIn = E_plain.VertXMessage( eventsXMLIn = E_plain.VertXMessage(
E.EventMessages({"action": "LR", E.EventMessages(
"recordCount": str(recordCount), {
"historyRecordMarker": parXMLOut[0].attrib["historyRecordMarker"], "action": "LR",
"historyTimestamp": parXMLOut[0].attrib["historyTimestamp"]})) "recordCount": str(recordCount),
"historyRecordMarker": parXMLOut[0].attrib["historyRecordMarker"],
"historyTimestamp": parXMLOut[0].attrib["historyTimestamp"],
}
)
)
eventsXMLOut = door.doXMLRequest(eventsXMLIn) eventsXMLOut = door.doXMLRequest(eventsXMLIn)
#TODO: handle modeRecords=true # TODO: handle modeRecords=true
for index, event in enumerate(eventsXMLOut[0]): 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: if root is None:
tree = etree.ElementTree(eventsXMLOut) tree = etree.ElementTree(eventsXMLOut)
else: else:
for event in reversed(eventsXMLOut[0]): for event in reversed(eventsXMLOut[0]):
root[0].insert(0, event) root[0].insert(0, event)
tree.write("logs/" + doorName + ".xml") tree.write("logs/" + door.name + ".xml")
def main(): def main():
for door in doors.values(): for door in doors.values():
getMessages(door) getMessages(door)
if __name__ == '__main__':
if __name__ == "__main__":
main() main()

View File

@ -1,44 +1,48 @@
import csv import csv
from io import StringIO from io import StringIO
import requests import requests
BASE_URL = "https://api.membershipworks.com/v1/" BASE_URL = "https://api.membershipworks.com/v1/"
# extracted from `SF._is.crm` in https://cdn.membershipworks.com/all.js # extracted from `SF._is.crm` in https://cdn.membershipworks.com/all.js
CRM = {0: "Note", CRM = {
4: "Profile Updated", 0: "Note",
8: "Scheduled/Reminder Email", 4: "Profile Updated",
9: "Renewal Notice", 8: "Scheduled/Reminder Email",
10: "Join Date", 9: "Renewal Notice",
11: "Next Renewal Date", 10: "Join Date",
12: "Membership Payment", 11: "Next Renewal Date",
13: "Donation", 12: "Membership Payment",
14: "Event Activity", 13: "Donation",
15: "Conversation", 14: "Event Activity",
16: "Contact Change", 15: "Conversation",
17: "Label Change", 16: "Contact Change",
18: "Other Payment", 17: "Label Change",
19: "Cart Payment", 18: "Other Payment",
20: "Payment Failed", 19: "Cart Payment",
21: "Billing Updated", 20: "Payment Failed",
22: "Form Checkout", 21: "Billing Updated",
23: "Event Payment", 22: "Form Checkout",
24: "Invoice", 23: "Event Payment",
25: "Invoice Payment", 24: "Invoice",
26: "Renewal", 25: "Invoice Payment",
27: "Payment"} 26: "Renewal",
27: "Payment",
}
# Types of fields, extracted from a html snippet in all.js + some guessing # Types of fields, extracted from a html snippet in all.js + some guessing
typ = { typ = {
1: "Text input", 1: "Text input",
2: "Password", # inferred from data 2: "Password", # inferred from data
3: "Simple text area", 3: "Simple text area",
4: "Rich text area", 4: "Rich text area",
7: "Address", 7: "Address",
8: "Check box", 8: "Check box",
9: "Select", 9: "Select",
11: "Display value stored in field (ie. read only)", 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 # more constants, this time extracted from the members csv export in all.js
staticFlags = { staticFlags = {
@ -55,14 +59,16 @@ staticFlags = {
"spy": {"lbl": "Billing method"}, "spy": {"lbl": "Billing method"},
"rid": {"lbl": "Auto recurring billing ID"}, "rid": {"lbl": "Auto recurring billing ID"},
"ipa": {"lbl": "IP address"}, "ipa": {"lbl": "IP address"},
"_id": {"lbl": "Account ID"} "_id": {"lbl": "Account ID"},
} }
class MembershipWorksRemoteError(Exception): class MembershipWorksRemoteError(Exception):
def __init__(self, reason, r): def __init__(self, reason, r):
super().__init__( 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: class MembershipWorks:
def __init__(self): def __init__(self):
@ -72,25 +78,24 @@ class MembershipWorks:
def login(self, username, password): def login(self, username, password):
"""Authenticate against the membershipworks api""" """Authenticate against the membershipworks api"""
r = requests.post(BASE_URL + 'usr', r = requests.post(
data={"_st": "all", BASE_URL + "usr",
"eml": username, data={"_st": "all", "eml": username, "org": "10000", "pwd": password},
"org": "10000", )
"pwd": password}) if r.status_code != 200 or "SF" not in r.json():
if r.status_code != 200 or 'SF' not in r.json(): raise MembershipWorksRemoteError("login", r)
raise MembershipWorksRemoteError('login', r)
self.org_info = r.json() self.org_info = r.json()
self.auth_token = self.org_info['SF'] self.auth_token = self.org_info["SF"]
self.org_num = self.org_info['org'] self.org_num = self.org_info["org"]
def _inject_auth(self, kwargs): def _inject_auth(self, kwargs):
# TODO: should probably be a decorator or something # TODO: should probably be a decorator or something
if self.auth_token is None: 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 # add auth token to params
if 'params' not in kwargs: if "params" not in kwargs:
kwargs['params'] = {} kwargs["params"] = {}
kwargs['params']["SF"] = self.auth_token kwargs["params"]["SF"] = self.auth_token
def _get(self, *args, **kwargs): def _get(self, *args, **kwargs):
self._inject_auth(kwargs) self._inject_auth(kwargs)
@ -115,14 +120,14 @@ class MembershipWorks:
# csv export # csv export
# anm: member signup, acc: member manage, adm: admin manage # anm: member signup, acc: member manage, adm: admin manage
for screen_type in ['anm', 'acc', 'adm']: for screen_type in ["anm", "acc", "adm"]:
for box in self.org_info['tpl'][screen_type]: for box in self.org_info["tpl"][screen_type]:
for element in box['box']: for element in box["box"]:
if (type(element['dat']) != str): if type(element["dat"]) != str:
for field in element['dat']: for field in element["dat"]:
if '_id' in field: if "_id" in field:
if field['_id'] not in fields: if field["_id"] not in fields:
fields[field['_id']] = field fields[field["_id"]] = field
return fields return fields
@ -131,36 +136,37 @@ class MembershipWorks:
This is terrible, and there might be a better way to do this. This is terrible, and there might be a better way to do this.
""" """
ret = {"folders": {}, ret = {"folders": {}, "levels": {}, "addons": {}, "labels": {}}
"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 # TODO: there must be a better way. this is stupid
if dek['dek'] == 1: if dek["dek"] == 1:
ret["folders"][dek['lbl']] = dek['_id'] ret["folders"][dek["lbl"]] = dek["_id"]
elif 'cur' in dek: elif "cur" in dek:
ret["levels"][dek['lbl']] = dek['_id'] ret["levels"][dek["lbl"]] = dek["_id"]
elif 'mux' in dek: elif "mux" in dek:
ret["addons"][dek['lbl']] = dek['_id'] ret["addons"][dek["lbl"]] = dek["_id"]
else: else:
ret["labels"][dek['lbl']] = dek['_id'] ret["labels"][dek["lbl"]] = dek["_id"]
return ret return ret
def get_member_ids(self, folders): def get_member_ids(self, folders):
folder_map = self._parse_flags()["folders"] folder_map = self._parse_flags()["folders"]
r = self._get(BASE_URL + "ylp", params={ r = self._get(
"lbl": ",".join([folder_map[f] for f in folders]), BASE_URL + "ylp",
"org": self.org_num, params={
"var": "_id,nam,ctc"}) "lbl": ",".join([folder_map[f] for f in folders]),
if r.status_code != 200 or 'usr' not in r.json(): "org": self.org_num,
raise MembershipWorksRemoteError('user listing', r) "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 # 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: # TODO: has issues with aliasing header names:
# ex: "Personal Studio Space" Label vs Membership Addon/Field # ex: "Personal Studio Space" Label vs Membership Addon/Field
@ -173,13 +179,17 @@ class MembershipWorks:
# get members CSV # get members CSV
# TODO: maybe can just use previous get instead? would return JSON # TODO: maybe can just use previous get instead? would return JSON
r = self._post(BASE_URL + "csv", r = self._post(
data={"_rt": "946702800", # unknown BASE_URL + "csv",
"mux": "", # unknown data={
"tid": ",".join(ids), # ids of members to get "_rt": "946702800", # unknown
"var": columns}) "mux": "", # unknown
"tid": ",".join(ids), # ids of members to get
"var": columns,
},
)
if r.status_code != 200: if r.status_code != 200:
raise MembershipWorksRemoteError('csv generation', r) raise MembershipWorksRemoteError("csv generation", r)
return list(csv.DictReader(StringIO(r.text))) return list(csv.DictReader(StringIO(r.text)))
def get_transactions(self, start_date, end_date, json=False): 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, json gets a different version of the transactions list,
which contains a different set information which contains a different set information
""" """
r = self._get(BASE_URL + "csv", r = self._get(
params={'crm': '12,13,14,18,19', # transaction types, see CRM BASE_URL + "csv",
**({'txl': ''} if json else {}), params={
'sdp': start_date.strftime('%s'), "crm": "12,13,14,18,19", # transaction types, see CRM
'edp': end_date.strftime('%s')}) **({"txl": ""} if json else {}),
"sdp": start_date.strftime("%s"),
"edp": end_date.strftime("%s"),
},
)
if r.status_code != 200: if r.status_code != 200:
raise MembershipWorksRemoteError('csv generation', r) raise MembershipWorksRemoteError("csv generation", r)
if json: if json:
return r.json() return r.json()
else: else:

View File

@ -3,6 +3,7 @@ import bitstring
# Reference for H10301 card format: # Reference for H10301 card format:
# https://www.hidglobal.com/sites/default/files/hid-understanding_card_data_formats-wp-en.pdf # https://www.hidglobal.com/sites/default/files/hid-understanding_card_data_formats-wp-en.pdf
class Credential: class Credential:
def __init__(self, code=None, hex=None): def __init__(self, code=None, hex=None):
if code is None and hex is 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") raise TypeError("Cannot set both code and hex for a Credential")
elif code is not None: elif code is not None:
self.bits = bitstring.pack( self.bits = bitstring.pack(
'0b000000, 0b0, uint:8=facility, uint:16=number, 0b0', "0b000000, 0b0, uint:8=facility, uint:16=number, 0b0",
facility=code[0], number=code[1]) facility=code[0],
number=code[1],
)
self.bits[6] = self.bits[7:19].count(1) % 2 # even parity 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 self.bits[31] = not (self.bits[19:31].count(1) % 2) # odd parity
elif hex is not None: elif hex is not None:

View File

@ -1,30 +1,37 @@
import csv import csv
from io import StringIO from io import StringIO
import urllib3
import requests import requests
import urllib3
from lxml import etree from lxml import etree
from lxml.builder import ElementMaker from lxml.builder import ElementMaker
E_plain = ElementMaker(nsmap={"hid": "http://www.hidglobal.com/VertX"}) E_plain = ElementMaker(nsmap={"hid": "http://www.hidglobal.com/VertX"})
E = ElementMaker(namespace="http://www.hidglobal.com/VertX", E = ElementMaker(
nsmap={"hid": "http://www.hidglobal.com/VertX"}) namespace="http://www.hidglobal.com/VertX",
E_corp = ElementMaker(namespace="http://www.hidcorp.com/VertX", #stupid nsmap={"hid": "http://www.hidglobal.com/VertX"},
nsmap={"hid": "http://www.hidcorp.com/VertX"}) )
E_corp = ElementMaker(
namespace="http://www.hidcorp.com/VertX", # stupid
nsmap={"hid": "http://www.hidcorp.com/VertX"},
)
ROOT = E_plain.VertXMessage 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? # TODO: where should this live?
# it's fine, ssl certs are for losers anyway # it's fine, ssl certs are for losers anyway
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
class RemoteError(Exception): class RemoteError(Exception):
def __init__(self, r): def __init__(self, r):
super().__init__( super().__init__(f"Door Updating Error: {r.status_code} {r.reason}\n{r.text}")
f"Door Updating Error: {r.status_code} {r.reason}\n{r.text}")
class DoorController():
class DoorController:
def __init__(self, ip, username, password, name="", access=""): def __init__(self, ip, username, password, name="", access=""):
self.ip = ip self.ip = ip
self.username = username self.username = username
@ -35,59 +42,68 @@ class DoorController():
def doImport(self, params=None, files=None): def doImport(self, params=None, files=None):
"""Send a request to the door control import script""" """Send a request to the door control import script"""
r = requests.post( r = requests.post(
'https://' + self.ip + '/cgi-bin/import.cgi', "https://" + self.ip + "/cgi-bin/import.cgi",
params=params, params=params,
files=files, files=files,
auth=requests.auth.HTTPDigestAuth(self.username, self.password), auth=requests.auth.HTTPDigestAuth(self.username, self.password),
timeout=60, timeout=60,
verify=False) # ignore insecure SSL verify=False,
) # ignore insecure SSL
xml = etree.XML(r.content) xml = etree.XML(r.content)
if r.status_code != 200 \ if (
or len(xml.findall("{http://www.hidglobal.com/VertX}Error")) > 0: r.status_code != 200
or len(xml.findall("{http://www.hidglobal.com/VertX}Error")) > 0
):
raise RemoteError(r) raise RemoteError(r)
def doCSVImport(self, csv): def doCSVImport(self, csv):
"""Do the CSV import procedure on a door control""" """Do the CSV import procedure on a door control"""
self.doImport({"task": "importInit"}) self.doImport({"task": "importInit"})
self.doImport({"task": "importCardsPeople", "name": "cardspeopleschedule.csv"}, self.doImport(
{"importCardsPeopleButton": ("cardspeopleschedule.csv", csv, 'text/csv')}) {"task": "importCardsPeople", "name": "cardspeopleschedule.csv"},
{"importCardsPeopleButton": ("cardspeopleschedule.csv", csv, "text/csv")},
)
self.doImport({"task": "importDone"}) self.doImport({"task": "importDone"})
def doXMLRequest(self, xml, prefix=b'<?xml version="1.0" encoding="UTF-8"?>'): def doXMLRequest(self, xml, prefix=b'<?xml version="1.0" encoding="UTF-8"?>'):
if not isinstance(xml, bytes): if not isinstance(xml, bytes):
xml = etree.tostring(xml) xml = etree.tostring(xml)
r = requests.get( r = requests.get(
'https://' + self.ip + '/cgi-bin/vertx_xml.cgi', "https://" + self.ip + "/cgi-bin/vertx_xml.cgi",
params={'XML': prefix + xml}, params={"XML": prefix + xml},
auth=requests.auth.HTTPDigestAuth(self.username, self.password), auth=requests.auth.HTTPDigestAuth(self.username, self.password),
verify=False) verify=False,
)
resp_xml = etree.XML(r.content) resp_xml = etree.XML(r.content)
# probably meed to be more sane about this # probably meed to be more sane about this
if r.status_code != 200 \ if r.status_code != 200 or len(resp_xml.findall("{*}Error")) > 0:
or len(resp_xml.findall("{*}Error")) > 0:
raise RemoteError(r) raise RemoteError(r)
return resp_xml return resp_xml
def get_scheduleMap(self): def get_scheduleMap(self):
schedules = self.doXMLRequest( schedules = self.doXMLRequest(
ROOT(E.Schedules({"action": "LR", ROOT(E.Schedules({"action": "LR", "recordOffset": "0", "recordCount": "8"}))
"recordOffset": "0", )
"recordCount": "8"}))) return {
return {fmt.attrib["scheduleName"]: fmt.attrib["scheduleID"] fmt.attrib["scheduleName"]: fmt.attrib["scheduleID"] for fmt in schedules[0]
for fmt in schedules[0]} }
def get_schedules(self): def get_schedules(self):
# TODO: might be able to do in one request # TODO: might be able to do in one request
schedules = self.doXMLRequest(ROOT( schedules = self.doXMLRequest(ROOT(E.Schedules({"action": "LR"})))
E.Schedules({"action": "LR"})))
etree.dump(schedules) etree.dump(schedules)
data = self.doXMLRequest(ROOT( data = self.doXMLRequest(
*[E.Schedules({"action": "LR", ROOT(
"scheduleID": schedule.attrib["scheduleID"]}) *[
for schedule in schedules[0]])) E.Schedules(
return ROOT(E_corp.Schedules({"action": "AD"}, {"action": "LR", "scheduleID": schedule.attrib["scheduleID"]}
*[s[0] for s in data])) )
for schedule in schedules[0]
]
)
)
return ROOT(E_corp.Schedules({"action": "AD"}, *[s[0] for s in data]))
def set_schedules(self, schedules): def set_schedules(self, schedules):
# clear all people # clear all people
@ -100,8 +116,11 @@ class DoorController():
# clear all schedules # clear all schedules
delXML = ROOT( 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: try:
self.doXMLRequest(delXML) self.doXMLRequest(delXML)
except RemoteError: except RemoteError:
@ -113,21 +132,27 @@ class DoorController():
def get_cardFormats(self): def get_cardFormats(self):
cardFormats = self.doXMLRequest( cardFormats = self.doXMLRequest(
ROOT(E.CardFormats({"action": "LR", ROOT(E.CardFormats({"action": "LR", "responseFormat": "expanded"}))
"responseFormat": "expanded"}))) )
return {fmt[0].attrib["value"]: fmt.attrib["formatID"] return {
for fmt in cardFormats[0].findall('{*}CardFormat[{*}FixedField]')} fmt[0].attrib["value"]: fmt.attrib["formatID"]
for fmt in cardFormats[0].findall("{*}CardFormat[{*}FixedField]")
}
def set_cardFormat(self, formatName, templateID, facilityCode): def set_cardFormat(self, formatName, templateID, facilityCode):
# TODO: add ability to delete formats # TODO: add ability to delete formats
# delete example: <hid:CardFormats action="DD" formatID="7-1-244"/> # delete example: <hid:CardFormats action="DD" formatID="7-1-244"/>
el = ROOT( el = ROOT(
E.CardFormats({"action": "AD"}, E.CardFormats(
E.CardFormat({"formatName": formatName, {"action": "AD"},
"templateID": str(templateID)}, E.CardFormat(
E.FixedField({"value": str(facilityCode)})))) {"formatName": formatName, "templateID": str(templateID)},
E.FixedField({"value": str(facilityCode)}),
),
)
)
return self.doXMLRequest(el) return self.doXMLRequest(el)
def get_records(self, req, count, params={}): def get_records(self, req, count, params={}):
@ -142,37 +167,41 @@ class DoorController():
# poorly if the numbers line up poorly (ie an exact multiple # poorly if the numbers line up poorly (ie an exact multiple
# of the returned record limit) # of the returned record limit)
while moreRecords: while moreRecords:
res = self.doXMLRequest(ROOT( res = self.doXMLRequest(
req({ ROOT(
"action": "LR", req(
"recordCount": str(count - recordCount + 1), {
"recordOffset": str(recordCount - 1 "action": "LR",
if recordCount > 0 else 0), "recordCount": str(count - recordCount + 1),
**params "recordOffset": str(
}))) recordCount - 1 if recordCount > 0 else 0
),
**params,
}
)
)
)
result = result[:-1] + list(res[0]) result = result[:-1] + list(res[0])
recordCount += int(res[0].get('recordCount')) - 1 recordCount += int(res[0].get("recordCount")) - 1
moreRecords = res[0].get('moreRecords') == 'true' moreRecords = res[0].get("moreRecords") == "true"
return result return result
def get_cardholders(self): def get_cardholders(self):
return self.get_records(E.Cardholders, 1000, return self.get_records(E.Cardholders, 1000, {"responseFormat": "expanded"})
{"responseFormat": "expanded"})
def get_credentials(self): def get_credentials(self):
return self.get_records(E.Credentials, 1000) return self.get_records(E.Credentials, 1000)
def get_lock(self): def get_lock(self):
el = ROOT( el = ROOT(E.Doors({"action": "LR", "responseFormat": "status"}))
E.Doors({"action": "LR", "responseFormat": "status"}))
xml = self.doXMLRequest(el) 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" return "unlocked" if relayState == "set" else "locked"
def set_lock(self, lock=True): def set_lock(self, lock=True):
el = ROOT( el = ROOT(
E.Doors({"action": "CM", E.Doors({"action": "CM", "command": "lockDoor" if lock else "unlockDoor"})
"command": "lockDoor" if lock else "unlockDoor"})) )
return self.doXMLRequest(el) return self.doXMLRequest(el)

View File

@ -1,20 +1,26 @@
from datetime import datetime from datetime import datetime
from peewee import (BooleanField, FixedCharField, CompositeKey, DateField, from peewee import (
DateTimeField, DecimalField, ForeignKeyField, Model, BooleanField,
MySQLDatabase, TextField) CompositeKey,
DateField,
DateTimeField,
DecimalField,
FixedCharField,
ForeignKeyField,
Model,
MySQLDatabase,
TextField,
)
import passwords import passwords
database = MySQLDatabase( database = MySQLDatabase(
**passwords.MEMBERSHIPWORKS_DB, **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): class BaseModel(Model):
_csv_headers_override = {} _csv_headers_override = {}
_date_fields = {} _date_fields = {}
@ -23,9 +29,9 @@ class BaseModel(Model):
return self.insert(**self.__data__) return self.insert(**self.__data__)
def upsert_instance(self): def upsert_instance(self):
return self.insert_instance() \ return self.insert_instance().on_conflict(
.on_conflict(action="update", action="update", preserve=list(self._meta.fields.values())
preserve=list(self._meta.fields.values())) )
def magic_save(self): def magic_save(self):
if self._meta.primary_key is False: if self._meta.primary_key is False:
@ -35,8 +41,7 @@ class BaseModel(Model):
@classmethod @classmethod
def _headers_map(cls): def _headers_map(cls):
return {field.column_name: name return {field.column_name: name for name, field in cls._meta.fields.items()}
for name, field in cls._meta.fields.items()}
@classmethod @classmethod
def _remap_headers(cls, data): def _remap_headers(cls, data):
@ -64,140 +69,176 @@ class BaseModel(Model):
class Meta: class Meta:
database = database database = database
class Label(BaseModel): class Label(BaseModel):
label_id = FixedCharField(24, primary_key=True) label_id = FixedCharField(24, primary_key=True)
label = TextField(null=True) label = TextField(null=True)
class Meta: class Meta:
table_name = 'labels' table_name = "labels"
class Member(BaseModel): class Member(BaseModel):
uid = FixedCharField(24, primary_key=True) uid = FixedCharField(24, primary_key=True)
year_of_birth = TextField(column_name='Year of Birth', null=True) year_of_birth = TextField(column_name="Year of Birth", null=True)
account_name = TextField(column_name='Account Name', null=True) account_name = TextField(column_name="Account Name", null=True)
first_name = TextField(column_name='First Name', null=True) first_name = TextField(column_name="First Name", null=True)
last_name = TextField(column_name='Last Name', null=True) last_name = TextField(column_name="Last Name", null=True)
phone = TextField(column_name='Phone', null=True) phone = TextField(column_name="Phone", null=True)
email = TextField(column_name='Email', null=True) email = TextField(column_name="Email", null=True)
address_street = TextField(column_name='Address (Street)', null=True) address_street = TextField(column_name="Address (Street)", null=True)
address_city = TextField(column_name='Address (City)', null=True) address_city = TextField(column_name="Address (City)", null=True)
address_state_province = TextField(column_name='Address (State/Province)', null=True) address_state_province = TextField(
address_postal_code = TextField(column_name='Address (Postal Code)', null=True) column_name="Address (State/Province)", null=True
address_country = TextField(column_name='Address (Country)', null=True) )
profile_description = TextField(column_name='Profile description', null=True) address_postal_code = TextField(column_name="Address (Postal Code)", null=True)
website = TextField(column_name='Website', null=True) address_country = TextField(column_name="Address (Country)", null=True)
fax = TextField(column_name='Fax', null=True) profile_description = TextField(column_name="Profile description", null=True)
contact_person = TextField(column_name='Contact Person', null=True) website = TextField(column_name="Website", null=True)
password = TextField(column_name='Password', null=True) fax = TextField(column_name="Fax", null=True)
position_relation = TextField(column_name='Position/relation', null=True) contact_person = TextField(column_name="Contact Person", null=True)
parent_account_id = TextField(column_name='Parent Account ID', null=True) password = TextField(column_name="Password", null=True)
gift_membership_purchased_by = TextField(column_name='Gift Membership purchased by', null=True) position_relation = TextField(column_name="Position/relation", null=True)
purchased_gift_membership_for = TextField(column_name='Purchased Gift Membership for', null=True) parent_account_id = TextField(column_name="Parent Account ID", null=True)
closet_storage = TextField(column_name='Closet Storage #', null=True) gift_membership_purchased_by = TextField(
storage_shelf = TextField(column_name='Storage Shelf #', null=True) column_name="Gift Membership purchased by", 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) purchased_gift_membership_for = TextField(
access_front_door_and_studio_space_during_extended_hours = BooleanField(column_name='Access Front Door and Studio Space During Extended Hours?', null=True) column_name="Purchased Gift Membership for", null=True
access_wood_shop = BooleanField(column_name='Access Wood Shop?', null=True) )
access_metal_shop = BooleanField(column_name='Access Metal Shop?', null=True) closet_storage = TextField(column_name="Closet Storage #", null=True)
access_storage_closet = BooleanField(column_name='Access Storage Closet?', null=True) storage_shelf = TextField(column_name="Storage Shelf #", null=True)
access_studio_space = BooleanField(column_name='Access Studio Space?', null=True) personal_studio_space = TextField(column_name="Personal Studio Space #", null=True)
access_front_door = BooleanField(column_name='Access Front Door?', null=True) access_permitted_shops_during_extended_hours = BooleanField(
access_card_number = TextField(column_name='Access Card Number', null=True) column_name="Access Permitted Shops During Extended Hours?", 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) access_front_door_and_studio_space_during_extended_hours = BooleanField(
billing_method = TextField(column_name='Billing Method', null=True) column_name="Access Front Door and Studio Space During Extended Hours?",
renewal_date = DateField(column_name='Renewal Date', null=True) null=True,
join_date = DateField(column_name='Join Date', null=True) )
admin_note = TextField(column_name='Admin note', null=True) access_wood_shop = BooleanField(column_name="Access Wood Shop?", null=True)
profile_gallery_image_url = TextField(column_name='Profile gallery image URL', null=True) access_metal_shop = BooleanField(column_name="Access Metal Shop?", null=True)
business_card_image_url = TextField(column_name='Business card image URL', null=True) access_storage_closet = BooleanField(
instagram = TextField(column_name='Instagram', null=True) column_name="Access Storage Closet?", null=True
pinterest = TextField(column_name='Pinterest', null=True) )
youtube = TextField(column_name='Youtube', null=True) access_studio_space = BooleanField(column_name="Access Studio Space?", null=True)
yelp = TextField(column_name='Yelp', null=True) access_front_door = BooleanField(column_name="Access Front Door?", null=True)
google = TextField(column_name='Google+', null=True) access_card_number = TextField(column_name="Access Card Number", null=True)
bbb = TextField(column_name='BBB', null=True) access_card_facility_code = TextField(
twitter = TextField(column_name='Twitter', null=True) column_name="Access Card Facility Code", null=True
facebook = TextField(column_name='Facebook', null=True) )
linked_in = TextField(column_name='LinkedIn', null=True) auto_billing_id = TextField(column_name="Auto Billing ID", null=True)
do_not_show_street_address_in_profile = TextField(column_name='Do not show street address in profile', null=True) billing_method = TextField(column_name="Billing Method", null=True)
do_not_list_in_directory = TextField(column_name='Do not list in directory', null=True) renewal_date = DateField(column_name="Renewal Date", null=True)
how_did_you_hear = TextField(column_name='HowDidYouHear', null=True) join_date = DateField(column_name="Join Date", null=True)
authorize_charge = TextField(column_name='authorizeCharge', null=True) admin_note = TextField(column_name="Admin note", null=True)
policy_agreement = TextField(column_name='policyAgreement', null=True) profile_gallery_image_url = TextField(
waiver_form_signed_and_on_file_date = DateField(column_name='Waiver form signed and on file date.', null=True) column_name="Profile gallery image URL", 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) business_card_image_url = TextField(
audit_date = DateField(column_name='Audit Date', null=True) column_name="Business card image URL", null=True
agreement_version = TextField(column_name='Agreement Version', null=True) )
paperwork_status = TextField(column_name='Paperwork status', null=True) instagram = TextField(column_name="Instagram", null=True)
membership_agreement_dated = BooleanField(column_name='Membership agreement dated', null=True) pinterest = TextField(column_name="Pinterest", null=True)
membership_agreement_acknowledgement_page_filled_out = BooleanField(column_name='Membership Agreement Acknowledgement Page Filled Out', null=True) youtube = TextField(column_name="Youtube", null=True)
membership_agreement_signed = BooleanField(column_name='Membership Agreement Signed', null=True) yelp = TextField(column_name="Yelp", null=True)
liability_form_filled_out = BooleanField(column_name='Liability Form Filled Out', 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 = { _csv_headers_override = {
'Account ID': 'uid', "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', "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', "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' "I have read the Claremont MakerSpace Membership Agreement & Policies, and agree to all terms stated therein.": "policy_agreement",
} }
_date_fields = { _date_fields = {
'Join Date': '%b %d, %Y', "Join Date": "%b %d, %Y",
'Renewal Date': '%b %d, %Y', "Renewal Date": "%b %d, %Y",
'Audit Date': '%m/%d/%Y', "Audit Date": "%m/%d/%Y",
'Membership Agreement signed and on file date.': '%m/%d/%Y', "Membership Agreement signed and on file date.": "%m/%d/%Y",
'Waiver form signed and on file date.': '%m/%d/%Y' "Waiver form signed and on file date.": "%m/%d/%Y",
} }
class Meta: class Meta:
table_name = 'members' table_name = "members"
class MemberLabel(BaseModel): class MemberLabel(BaseModel):
uid = ForeignKeyField(Member, column_name='uid', backref='labels') uid = ForeignKeyField(Member, column_name="uid", backref="labels")
label_id = ForeignKeyField(Label, backref='members') label_id = ForeignKeyField(Label, backref="members")
class Meta: class Meta:
table_name = 'member_labels' table_name = "member_labels"
primary_key = CompositeKey('label_id', 'uid') primary_key = CompositeKey("label_id", "uid")
class Transaction(BaseModel): class Transaction(BaseModel):
sid = FixedCharField(27, null=True) 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() timestamp = DateTimeField()
type = TextField(null=True) type = TextField(null=True)
sum = DecimalField(13, 4, null=True) sum = DecimalField(13, 4, null=True)
fee = DecimalField(13, 4, null=True) fee = DecimalField(13, 4, null=True)
event_id = TextField(null=True) event_id = TextField(null=True)
for_ = TextField(column_name='For', null=True) for_ = TextField(column_name="For", null=True)
items = TextField(column_name='Items', null=True) items = TextField(column_name="Items", null=True)
discount_code = TextField(column_name='Discount Code', null=True) discount_code = TextField(column_name="Discount Code", null=True)
note = TextField(column_name='Note', null=True) note = TextField(column_name="Note", null=True)
name = TextField(column_name='Name', null=True) name = TextField(column_name="Name", null=True)
contact_person = TextField(column_name='Contact Person', null=True) contact_person = TextField(column_name="Contact Person", null=True)
full_address = TextField(column_name='Full Address', null=True) full_address = TextField(column_name="Full Address", null=True)
street = TextField(column_name='Street', null=True) street = TextField(column_name="Street", null=True)
city = TextField(column_name='City', null=True) city = TextField(column_name="City", null=True)
state_province = TextField(column_name='State/Province', null=True) state_province = TextField(column_name="State/Province", null=True)
postal_code = TextField(column_name='Postal Code', null=True) postal_code = TextField(column_name="Postal Code", null=True)
country = TextField(column_name='Country', null=True) country = TextField(column_name="Country", null=True)
phone = TextField(column_name='Phone', null=True) phone = TextField(column_name="Phone", null=True)
email = TextField(column_name='Email', null=True) email = TextField(column_name="Email", null=True)
_csv_headers_override = { _csv_headers_override = {"_dp": "timestamp", "Transaction Type": "type"}
'_dp': 'timestamp',
'Transaction Type': 'type'
}
@classmethod @classmethod
def from_csv_dict(cls, data): def from_csv_dict(cls, data):
txn = data.copy() txn = data.copy()
# can't use '%s' format string, have to use the special function # 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) return super().from_csv_dict(txn)
class Meta: class Meta:
table_name = 'transactions' table_name = "transactions"
primary_key = False primary_key = False

View File

@ -1,70 +1,85 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import re
import http import http
import re
from flask import Flask, render_template, request from flask import Flask, render_template, request
from common import doors, membershipworks
app = Flask(__name__) app = Flask(__name__)
from common import doors, membershipworks
def parse_list(member, regex): def parse_list(member, regex):
data_list = [] data_list = []
for key, value in member.items(): for key, value in member.items():
match = re.match(regex, key) 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)) data_list.append(match.group(1))
return ", ".join(data_list) return ", ".join(data_list)
def parse_members(members): def parse_members(members):
data = [] data = []
for member in members: for member in members:
props = { props = {
'Name': member['Account Name'], "Name": member["Account Name"],
'Renewal Date': member['Renewal Date'], "Renewal Date": member["Renewal Date"],
'Card Number': member['Access Card Facility Code'] + '-' \ "Card Number": member["Access Card Facility Code"]
+ member['Access Card Number'], + "-"
'Account on Hold': "Yes" if member['Account on Hold'] != '' else "No" } + member["Access Card Number"],
"Account on Hold": "Yes" if member["Account on Hold"] != "" else "No",
}
props['Certifications'] = parse_list(member, 'Certified: (.*)') props["Certifications"] = parse_list(member, "Certified: (.*)")
props['Door Access'] = parse_list(member, 'Access (.*)\?') props["Door Access"] = parse_list(member, "Access (.*)\?")
props['Memebership Level'] = parse_list(member, 'CMS (.*)') props["Memebership Level"] = parse_list(member, "CMS (.*)")
data.append(props) data.append(props)
return data return data
@app.route("/") @app.route("/")
def main(): def main():
# maybe not now: membership agreement signed # maybe not now: membership agreement signed
# TODO: renewal date check # TODO: renewal date check
term = request.args.get('term', '') term = request.args.get("term", "")
if len(term) < 3: if len(term) < 3:
return render_template("members.html", return render_template(
error="Enter at least 3 characters to search") "members.html", error="Enter at least 3 characters to search"
)
data = membershipworks.get_members( data = membershipworks.get_members(
['members', 'staff'], ["members", "staff"], "lvl,xws,xms,xsc,xas,xfd,xac,phn,eml,lbl,xcf,nam,end"
"lvl,xws,xms,xsc,xas,xfd,xac,phn,eml,lbl,xcf,nam,end") )
members = parse_members(data) members = parse_members(data)
members = [member for member in members members = [member for member in members if term.lower() in member["Name"].lower()]
if term.lower() in member['Name'].lower()] headers = [
headers = ['Name', 'Certifications', 'Door Access', 'Memebership Level', "Name",
'Card Number', 'Renewal Date', 'Account on Hold'] "Certifications",
"Door Access",
"Memebership Level",
"Card Number",
"Renewal Date",
"Account on Hold",
]
if len(members) > 4: if len(members) > 4:
return render_template( 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) return render_template("members.html", headers=headers, members=members)
@app.route('/frontDoor/<lock>', methods=['POST'])
@app.route("/frontDoor/<lock>", methods=["POST"])
def unlockLockDoor(lock): def unlockLockDoor(lock):
doors['Front Door'].lockOrUnlockDoor(lock != 'unlock') doors["Front Door"].lockOrUnlockDoor(lock != "unlock")
return ('', http.HTTPStatus.NO_CONTENT) return ("", http.HTTPStatus.NO_CONTENT)
if __name__ == "__main__": if __name__ == "__main__":
app.run(debug=True, host='0.0.0.0') app.run(debug=True, host="0.0.0.0")

383
poetry.lock generated Normal file
View File

@ -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"},
]

31
pyproject.toml Normal file
View File

@ -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 <adam@adamgoldsmith.name>"]
[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"

View File

@ -3,7 +3,8 @@
from datetime import datetime from datetime import datetime
from common import membershipworks 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() @database.atomic()
def main(): def main():
@ -11,32 +12,31 @@ def main():
database.create_tables([Label, Member, MemberLabel, Transaction]) database.create_tables([Label, Member, MemberLabel, Transaction])
print("Updating labels") print("Updating labels")
labels = membershipworks._parse_flags()['labels'] labels = membershipworks._parse_flags()["labels"]
Label \ Label.insert_many(
.insert_many([{'label_id': v, 'label': k} for k, v in labels.items()]) \ [{"label_id": v, "label": k} for k, v in labels.items()]
.on_conflict(action="update", preserve=[Label.label]) \ ).on_conflict(action="update", preserve=[Label.label]).execute()
.execute()
print("Getting/Updating members...") print("Getting/Updating members...")
members = membershipworks.get_all_members() members = membershipworks.get_all_members()
for m in members: for m in members:
# replace flags by booleans # 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: if flag in m:
m[flag] = m[flag] == flag m[flag] = m[flag] == flag
for field_id, field in membershipworks._all_fields().items(): for field_id, field in membershipworks._all_fields().items():
# convert checkboxes to real booleans # convert checkboxes to real booleans
if field.get('typ') == 8 and field['lbl'] in m: # check box if field.get("typ") == 8 and field["lbl"] in m: # check box
m[field['lbl']] = True if m[field['lbl']] == 'Y' else False m[field["lbl"]] = True if m[field["lbl"]] == "Y" else False
for member in members: for member in members:
# create/update member # create/update member
Member.from_csv_dict(member).magic_save() Member.from_csv_dict(member).magic_save()
# update member's labels # update member's labels
for label, label_id in membershipworks._parse_flags()['labels'].items(): for label, label_id in membershipworks._parse_flags()["labels"].items():
ml = MemberLabel(uid=member['Account ID'], label_id=label_id) ml = MemberLabel(uid=member["Account ID"], label_id=label_id)
if member[label]: if member[label]:
ml.magic_save() ml.magic_save()
else: else:
@ -49,14 +49,18 @@ def main():
transactions_json = membershipworks.get_transactions(start_date, now, json=True) 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 # 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)] transactions = [{**j, **v} for j, v in zip(transactions_csv, transactions_json)]
assert all([t['Account ID'] == t.get('uid', '') assert all(
and t['Payment ID'] == t.get('sid', '') [
for t in transactions]) t["Account ID"] == t.get("uid", "") and t["Payment ID"] == t.get("sid", "")
for t in transactions
]
)
for transaction in transactions: for transaction in transactions:
Transaction.from_csv_dict(transaction).magic_save() Transaction.from_csv_dict(transaction).magic_save()
# TODO: folders, levels, addons # TODO: folders, levels, addons
if __name__ == '__main__':
if __name__ == "__main__":
main() main()

View File

@ -8,82 +8,98 @@ from common import membershipworks
LDAP_BASE = "cn=users,dc=sawtooth,dc=claremontmakerspace,dc=org" LDAP_BASE = "cn=users,dc=sawtooth,dc=claremontmakerspace,dc=org"
GROUP_BASE = "cn=groups,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 RAND_PW_LEN = 20
def makeGroups(members): def makeGroups(members):
groups = [key.replace(':', '.').replace('?', '') groups = [
for key in members[0].keys() key.replace(":", ".").replace("?", "")
if re.match(GROUPS_REGEX, key) is not None] for key in members[0].keys()
if re.match(GROUPS_REGEX, key) is not None
]
for group in groups: for group in groups:
subprocess.call(["udm", "groups/group", "create", subprocess.call(
"--position", GROUP_BASE, [
"--set", "name=" + group]) "udm",
"groups/group",
"create",
"--position",
GROUP_BASE,
"--set",
"name=" + group,
]
)
def makeSets(props): def makeSets(props):
return sum([["--set", key + "=" + value] return sum([["--set", key + "=" + value] for key, value in props.items()], [])
for key, value in props.items()], [])
def makeAppendGroups(member): def makeAppendGroups(member):
groups = [key.replace(':', '.').replace('?', '') groups = [
for key, value in member.items() key.replace(":", ".").replace("?", "")
if re.match(GROUPS_REGEX, key) is not None and value != ''] for key, value in member.items()
return sum([["--append", "groups=cn=" + group + ',' + GROUP_BASE] if re.match(GROUPS_REGEX, key) is not None and value != ""
for group in groups], []) ]
return sum(
[["--append", "groups=cn=" + group + "," + GROUP_BASE] for group in groups], []
)
def main(): def main():
members = membershipworks.get_members(['Members', 'CMS Staff'], members = membershipworks.get_members(
"lvl,phn,eml,lbl,nam,end,_id") ["Members", "CMS Staff"], "lvl,phn,eml,lbl,nam,end,_id"
)
makeGroups(members) makeGroups(members)
for member in members: for member in members:
randomPass = ''.join(random.choice(string.ascii_letters + string.digits) randomPass = "".join(
for x in range(0, RAND_PW_LEN)) random.choice(string.ascii_letters + string.digits)
for x in range(0, RAND_PW_LEN)
)
username = member["Account Name"].lower().replace(" ", ".") username = member["Account Name"].lower().replace(" ", ".")
props = { props = {
"title": "", # Title "title": "", # Title
"firstname": member["First Name"], "firstname": member["First Name"],
"lastname": member["Last Name"], # (c) "lastname": member["Last Name"], # (c)
"username": username, # (cmr) "username": username, # (cmr)
"description": "", # Description "description": "", # Description
"password": randomPass, # (c) Password "password": randomPass, # (c) Password
#"mailPrimaryAddress": member["Email"], # Primary e-mail address # "mailPrimaryAddress": member["Email"], # Primary e-mail address
#"displayName": "", # Display name # "displayName": "", # Display name
#"birthday": "", # Birthdate # "birthday": "", # Birthdate
#"jpegPhoto": "", # Picture of the user (JPEG format) # "jpegPhoto": "", # Picture of the user (JPEG format)
"employeeNumber": member["Account ID"], "employeeNumber": member["Account ID"],
#"employeeType": "", # Employee type # "employeeType": "", # Employee type
"homedrive": "H:", # Windows home drive
"homedrive": "H:", # Windows home drive "sambahome": "\\\\ucs\\" + username, # Windows home path
"sambahome": "\\\\ucs\\" + username, # Windows home path "profilepath": "%LOGONSERVER%\\%USERNAME%\\windows-profiles\\default", # Windows profile directory
"profilepath": "%LOGONSERVER%\\%USERNAME%\\windows-profiles\\default", # Windows profile directory
"disabled": "1" if member["Account on Hold"] != "" else "0", "disabled": "1" if member["Account on Hold"] != "" else "0",
#"userexpiry": member["Renewal Date"], # "userexpiry": member["Renewal Date"],
"pwdChangeNextLogin": "1", # User has to change password on next login
"pwdChangeNextLogin": "1", # User has to change password on next login # "sambaLogonHours": "", # Permitted times for Windows logins
#"sambaLogonHours": "", # Permitted times for Windows logins
"e-mail": member["Email"], # ([]) E-mail address "e-mail": member["Email"], # ([]) E-mail address
"phone": member["Phone"], # Telephone number "phone": member["Phone"], # Telephone number
#"PasswordRecoveryMobile": member["Phone"], # Mobile phone number # "PasswordRecoveryMobile": member["Phone"], # Mobile phone number
"PasswordRecoveryEmail": member["Email"] "PasswordRecoveryEmail": member["Email"],
} }
subprocess.call(["udm", "users/user", "create", subprocess.call(
"--position", LDAP_BASE] + makeSets(props)) ["udm", "users/user", "create", "--position", LDAP_BASE] + makeSets(props)
)
# remove props we don't want to reset # remove props we don't want to reset
props.pop("password") props.pop("password")
props.pop("pwdChangeNextLogin") props.pop("pwdChangeNextLogin")
subprocess.call(["udm", "users/user", "modify", subprocess.call(
"--dn", "uid=" + username + "," + LDAP_BASE] ["udm", "users/user", "modify", "--dn", "uid=" + username + "," + LDAP_BASE]
+ makeSets(props) + makeSets(props)
+ makeAppendGroups(member)) + makeAppendGroups(member)
)
if __name__ == "__main__": if __name__ == "__main__":
main() main()