From 06516ad0cd54d9cabb63762a6ae74293187d8493 Mon Sep 17 00:00:00 2001 From: Adam Goldsmith Date: Fri, 21 Feb 2020 17:22:58 -0500 Subject: [PATCH] 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 --- doorUpdater.py | 2 +- lib/MembershipWorks.py | 140 ++++++++++++++++++++++++++++++++++++++--- ucsAccounts.py | 2 +- 3 files changed, 133 insertions(+), 11 deletions(-) diff --git a/doorUpdater.py b/doorUpdater.py index ad31ba7..fd2fdef 100755 --- a/doorUpdater.py +++ b/doorUpdater.py @@ -231,7 +231,7 @@ def update_door(door, members): def main(): 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") members = [MembershipworksMember(m) for m in memberData] diff --git a/lib/MembershipWorks.py b/lib/MembershipWorks.py index 5bfc031..7903468 100644 --- a/lib/MembershipWorks.py +++ b/lib/MembershipWorks.py @@ -4,6 +4,61 @@ import requests BASE_URL = "https://api.membershipworks.com/v1/" +# extracted from `SF._is.crm` in https://cdn.membershipworks.com/all.js +CRM = {0: "Note", + 4: "Profile Updated", + 8: "Scheduled/Reminder Email", + 9: "Renewal Notice", + 10: "Join Date", + 11: "Next Renewal Date", + 12: "Membership Payment", + 13: "Donation", + 14: "Event Activity", + 15: "Conversation", + 16: "Contact Change", + 17: "Label Change", + 18: "Other Payment", + 19: "Cart Payment", + 20: "Payment Failed", + 21: "Billing Updated", + 22: "Form Checkout", + 23: "Event Payment", + 24: "Invoice", + 25: "Invoice Payment", + 26: "Renewal", + 27: "Payment"} + +# 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): def __init__(self, reason, r): super().__init__( @@ -11,6 +66,7 @@ class MembershipWorksRemoteError(Exception): class MembershipWorks: def __init__(self): + self.org_info = None self.auth_token = None self.org_num = None @@ -23,8 +79,9 @@ class MembershipWorks: "pwd": password}) if r.status_code != 200 or 'SF' not in r.json(): raise MembershipWorksRemoteError('login', r) - self.auth_token = r.json()['SF'] - self.org_num = r.json()['org'] + self.org_info = r.json() + self.auth_token = self.org_info['SF'] + self.org_num = self.org_info['org'] def _inject_auth(self, kwargs): # TODO: should probably be a decorator or something @@ -45,13 +102,55 @@ class MembershipWorks: # TODO: should probably do some error handling in here 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): - # TODO: this is hardcoded for CMS - folder_map = { - 'members': '5ae37979f033bfe8534f8799', - 'staff': '5771675edcdf126302a2f6b9', - 'misc': '5b69ee9bf033bf8e7346c434' - } + folder_map = self._parse_flags()["folders"] r = self._get(BASE_URL + "ylp", params={ "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(): 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']] + # TODO: has issues with aliasing header names: + # ex: "Personal Studio Space" Label vs Membership Addon/Field def get_members(self, folders, columns): """ Pull the members csv from the membershipworks api folders: a list of the names of the folders to get @@ -80,3 +181,24 @@ class MembershipWorks: if r.status_code != 200: raise MembershipWorksRemoteError('csv generation', r) 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 diff --git a/ucsAccounts.py b/ucsAccounts.py index e34f849..d380a4d 100755 --- a/ucsAccounts.py +++ b/ucsAccounts.py @@ -32,7 +32,7 @@ def makeAppendGroups(member): for group in groups], []) def main(): - members = membershipworks.get_members(['members', 'staff'], + members = membershipworks.get_members(['Members', 'CMS Staff'], "lvl,phn,eml,lbl,nam,end,_id") makeGroups(members)