forked from CMS/memberPlumbing
lib/membershipworks: Rework to be more generic, add more methods
parse out folder, label, and attribute information from the json returned on login, allowing less hardcoded values Add get_transaction and get_all_members methods
This commit is contained in:
parent
f95493e3a6
commit
06516ad0cd
@ -231,7 +231,7 @@ def update_door(door, members):
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
memberData = membershipworks.get_members(
|
memberData = membershipworks.get_members(
|
||||||
['members', 'staff', 'misc'],
|
['Members', 'CMS Staff', 'Misc. Access'],
|
||||||
"_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")
|
||||||
|
|
||||||
members = [MembershipworksMember(m) for m in memberData]
|
members = [MembershipworksMember(m) for m in memberData]
|
||||||
|
@ -4,6 +4,61 @@ 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
|
||||||
|
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
|
||||||
|
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"}
|
||||||
|
|
||||||
|
# more constants, this time extracted from the members csv export in all.js
|
||||||
|
staticFlags = {
|
||||||
|
"pos": {"lbl": "Position/relation (contacts)"},
|
||||||
|
"nte": {"lbl": "Admin note (contacts)"},
|
||||||
|
"pwd": {"lbl": "Password"},
|
||||||
|
"lgo": {"lbl": "Business card image URLs"},
|
||||||
|
"pfx": {"lbl": "Profile gallery image URLs"},
|
||||||
|
"lvl": {"lbl": "Membership levels"},
|
||||||
|
"aon": {"lbl": "Membership add-ons"},
|
||||||
|
"lbl": {"lbl": "Labels"},
|
||||||
|
"joi": {"lbl": "Join date"},
|
||||||
|
"end": {"lbl": "Renewal date"},
|
||||||
|
"spy": {"lbl": "Billing method"},
|
||||||
|
"rid": {"lbl": "Auto recurring billing ID"},
|
||||||
|
"ipa": {"lbl": "IP address"},
|
||||||
|
"_id": {"lbl": "Account ID"}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class MembershipWorksRemoteError(Exception):
|
class MembershipWorksRemoteError(Exception):
|
||||||
def __init__(self, reason, r):
|
def __init__(self, reason, r):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@ -11,6 +66,7 @@ class MembershipWorksRemoteError(Exception):
|
|||||||
|
|
||||||
class MembershipWorks:
|
class MembershipWorks:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self.org_info = None
|
||||||
self.auth_token = None
|
self.auth_token = None
|
||||||
self.org_num = None
|
self.org_num = None
|
||||||
|
|
||||||
@ -23,8 +79,9 @@ class MembershipWorks:
|
|||||||
"pwd": password})
|
"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.auth_token = r.json()['SF']
|
self.org_info = r.json()
|
||||||
self.org_num = r.json()['org']
|
self.auth_token = self.org_info['SF']
|
||||||
|
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
|
||||||
@ -45,13 +102,55 @@ class MembershipWorks:
|
|||||||
# TODO: should probably do some error handling in here
|
# TODO: should probably do some error handling in here
|
||||||
return requests.post(*args, **kwargs)
|
return requests.post(*args, **kwargs)
|
||||||
|
|
||||||
|
def _all_fields(self):
|
||||||
|
"""Parse out a list of fields from the org data.
|
||||||
|
|
||||||
|
Is this terrible? Yes. Also, not dissimilar to how MW does it
|
||||||
|
in all.js.
|
||||||
|
"""
|
||||||
|
fields = staticFlags.copy()
|
||||||
|
|
||||||
|
# TODO: this will take the later option, if the same field
|
||||||
|
# used in mulitple places. I don't know which lbl is used for
|
||||||
|
# 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
|
||||||
|
|
||||||
|
return fields
|
||||||
|
|
||||||
|
def _parse_flags(self):
|
||||||
|
"""Parse the flags out of the org data.
|
||||||
|
|
||||||
|
This is terrible, and there might be a better way to do this.
|
||||||
|
"""
|
||||||
|
ret = {"folders": {},
|
||||||
|
"levels": {},
|
||||||
|
"addons": {},
|
||||||
|
"labels": {}}
|
||||||
|
|
||||||
|
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']
|
||||||
|
else:
|
||||||
|
ret["labels"][dek['lbl']] = dek['_id']
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
def get_member_ids(self, folders):
|
def get_member_ids(self, folders):
|
||||||
# TODO: this is hardcoded for CMS
|
folder_map = self._parse_flags()["folders"]
|
||||||
folder_map = {
|
|
||||||
'members': '5ae37979f033bfe8534f8799',
|
|
||||||
'staff': '5771675edcdf126302a2f6b9',
|
|
||||||
'misc': '5b69ee9bf033bf8e7346c434'
|
|
||||||
}
|
|
||||||
|
|
||||||
r = self._get(BASE_URL + "ylp", params={
|
r = self._get(BASE_URL + "ylp", params={
|
||||||
"lbl": ",".join([folder_map[f] for f in folders]),
|
"lbl": ",".join([folder_map[f] for f in folders]),
|
||||||
@ -60,9 +159,11 @@ class MembershipWorks:
|
|||||||
if r.status_code != 200 or 'usr' not in r.json():
|
if r.status_code != 200 or 'usr' not in r.json():
|
||||||
raise MembershipWorksRemoteError('user listing', r)
|
raise MembershipWorksRemoteError('user listing', r)
|
||||||
|
|
||||||
# get list of member/staff IDs
|
# 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
|
||||||
def get_members(self, folders, columns):
|
def get_members(self, folders, columns):
|
||||||
""" Pull the members csv from the membershipworks api
|
""" Pull the members csv from the membershipworks api
|
||||||
folders: a list of the names of the folders to get
|
folders: a list of the names of the folders to get
|
||||||
@ -80,3 +181,24 @@ class MembershipWorks:
|
|||||||
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)))
|
||||||
|
|
||||||
|
# TODO: doesn't return as much info as csv?
|
||||||
|
def get_transactions(self, start_date, end_date):
|
||||||
|
"""Get the transactions between start_date and end_date
|
||||||
|
Dates can be datetime.date or datetime.datetime"""
|
||||||
|
r = self._get(BASE_URL + "csv",
|
||||||
|
params={'crm': '12,13,14,18,19', # transaction types, see CRM
|
||||||
|
'txl': '', # changes output type?
|
||||||
|
# without this, returns a csv with more info
|
||||||
|
'sdp': start_date.strftime('%s'),
|
||||||
|
'edp': end_date.strftime('%s')})
|
||||||
|
if r.status_code != 200:
|
||||||
|
raise MembershipWorksRemoteError('csv generation', r)
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
def get_all_members(self):
|
||||||
|
"""Get all the data for all the members"""
|
||||||
|
folders = self._parse_flags()["folders"].keys()
|
||||||
|
fields = self._all_fields()
|
||||||
|
members = self.get_members(folders, ",".join(fields.keys()))
|
||||||
|
return members
|
||||||
|
@ -32,7 +32,7 @@ def makeAppendGroups(member):
|
|||||||
for group in groups], [])
|
for group in groups], [])
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
members = membershipworks.get_members(['members', 'staff'],
|
members = membershipworks.get_members(['Members', 'CMS Staff'],
|
||||||
"lvl,phn,eml,lbl,nam,end,_id")
|
"lvl,phn,eml,lbl,nam,end,_id")
|
||||||
makeGroups(members)
|
makeGroups(members)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user