From c52b76534c45693f4e114b3cdca105aa2e615dc2 Mon Sep 17 00:00:00 2001 From: Adam Goldsmith Date: Thu, 6 Feb 2020 17:48:09 -0500 Subject: [PATCH] Refactor MembershipWorks handling into separate class/file --- MembershipWorks.py | 82 +++++++++++++++++++++++++++++++++++++++++++++ common.py | 59 +++----------------------------- doorUpdater.py | 6 ++-- membershipViewer.py | 5 ++- ucsAccounts.py | 4 +-- 5 files changed, 93 insertions(+), 63 deletions(-) create mode 100644 MembershipWorks.py diff --git a/MembershipWorks.py b/MembershipWorks.py new file mode 100644 index 0000000..5bfc031 --- /dev/null +++ b/MembershipWorks.py @@ -0,0 +1,82 @@ +import csv +from io import StringIO +import requests + +BASE_URL = "https://api.membershipworks.com/v1/" + +class MembershipWorksRemoteError(Exception): + def __init__(self, reason, r): + super().__init__( + f"Error when attempting {reason}: {r.status_code} {r.reason}\n{r.text}") + +class MembershipWorks: + def __init__(self): + self.auth_token = None + self.org_num = None + + def login(self, username, password): + """Authenticate against the membershipworks api""" + r = requests.post(BASE_URL + 'usr', + data={"_st": "all", + "eml": username, + "org": "10000", + "pwd": password}) + if r.status_code != 200 or 'SF' not in r.json(): + raise MembershipWorksRemoteError('login', r) + self.auth_token = r.json()['SF'] + self.org_num = r.json()['org'] + + def _inject_auth(self, kwargs): + # TODO: should probably be a decorator or something + if self.auth_token is None: + raise RuntimeError('Not Logged in to MembershipWorks') + # add auth token to params + if 'params' not in kwargs: + kwargs['params'] = {} + kwargs['params']["SF"] = self.auth_token + + def _get(self, *args, **kwargs): + self._inject_auth(kwargs) + # TODO: should probably do some error handling in here + return requests.get(*args, **kwargs) + + def _post(self, *args, **kwargs): + self._inject_auth(kwargs) + # TODO: should probably do some error handling in here + return requests.post(*args, **kwargs) + + def get_member_ids(self, folders): + # TODO: this is hardcoded for CMS + folder_map = { + 'members': '5ae37979f033bfe8534f8799', + 'staff': '5771675edcdf126302a2f6b9', + 'misc': '5b69ee9bf033bf8e7346c434' + } + + r = self._get(BASE_URL + "ylp", params={ + "lbl": ",".join([folder_map[f] for f in folders]), + "org": self.org_num, + "var": "_id,nam,ctc"}) + if r.status_code != 200 or 'usr' not in r.json(): + raise MembershipWorksRemoteError('user listing', r) + + # get list of member/staff IDs + return [user['uid'] for user in r.json()['usr']] + + 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 + (see folder_map in this function for mapping to ids) + columns: which columns to get""" + ids = self.get_member_ids(folders) + + # get members CSV + # TODO: maybe can just use previous get instead? would return JSON + r = self._post(BASE_URL + "csv", + data={"_rt": "946702800", # unknown + "mux": "", # unknown + "tid": ",".join(ids), # ids of members to get + "var": columns}) + if r.status_code != 200: + raise MembershipWorksRemoteError('csv generation', r) + return list(csv.DictReader(StringIO(r.text))) diff --git a/common.py b/common.py index 8fc469c..307ad6b 100644 --- a/common.py +++ b/common.py @@ -1,15 +1,12 @@ -import csv from ruamel.yaml import YAML import urllib3 import os -import sys -from io import StringIO -import requests from hid.DoorController import DoorController -from passwords import (DOOR_USERNAME, DOOR_PASSWORD, - MEMBERSHIPWORKS_USERNAME, MEMBERSHIPWORKS_PASSWORD) +from passwords import DOOR_USERNAME, DOOR_PASSWORD +from passwords import MEMBERSHIPWORKS_USERNAME, MEMBERSHIPWORKS_PASSWORD +from MembershipWorks import MembershipWorks # it's fine, ssl certs are for losers anyway urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -28,51 +25,5 @@ doors = {doorName: DoorController(doorData['ip'], memberLevels = config['memberLevels'] doorSpecificSchedules = config['doorSpecificSchedules'] -def getMembershipworksData(folders, columns): - """ Pull the members csv from the membershipworks api - folders: a list of the names of the folders to get - (see folder_map in this function for mapping to ids) - columns: which columns to get""" - BASE_URL = "https://api.membershipworks.com/v1/" - folder_map = {'members': '5ae37979f033bfe8534f8799', - 'staff': '5771675edcdf126302a2f6b9', - 'misc': '5b69ee9bf033bf8e7346c434'} - - # login - r = requests.post(BASE_URL + 'usr', - data={"_st": "all", - "eml": MEMBERSHIPWORKS_USERNAME, - "org": "10000", - "pwd": MEMBERSHIPWORKS_PASSWORD}) - if r.status_code != 200 or 'SF' not in r.json(): - print("MembershipWorks Login Error: ", r.status_code, r.reason) - print(r.text) - sys.exit(1) - login_data = r.json() - - # get list of member/staff IDs - r = requests.get(BASE_URL + "ylp", - params={"SF": login_data['SF'], - "lbl": ",".join([folder_map[f] for f in folders]), - "org": login_data['org'], - "var": "_id,nam,ctc"}) - if r.status_code != 200 or 'usr' not in r.json(): - print("MembershipWorks User Listing Error: ", r.status_code, r.reason) - print(r.text) - sys.exit(1) - ids = [user['uid'] for user in r.json()['usr']] - - # get members CSV - # TODO: maybe can just use previous get instead? would return JSON - r = requests.post(BASE_URL + "csv", - params={"SF": login_data['SF']}, - data={"_rt": "946702800", # unknown - "mux": "", # unknown - "tid": ",".join(ids), # ids of members to get - "var": columns}) - if r.status_code != 200: - print("MembershipWorks CSV Generation Error: ", r.status_code, r.reason) - print(r.text) - sys.exit(1) - - return list(csv.DictReader(StringIO(r.text))) +membershipworks = MembershipWorks() +membershipworks.login(MEMBERSHIPWORKS_USERNAME, MEMBERSHIPWORKS_PASSWORD) diff --git a/doorUpdater.py b/doorUpdater.py index fc98a12..8bba418 100755 --- a/doorUpdater.py +++ b/doorUpdater.py @@ -1,11 +1,9 @@ #!/usr/bin/env python3 -from common import (doors, getMembershipworksData, memberLevels, - doorSpecificSchedules) +from common import doors, membershipworks, memberLevels, doorSpecificSchedules from hid.Credential import Credential from hid.DoorController import ROOT, E - class Member(): def __init__(self, forename="", surname="", membershipWorksID="", middleName="", email="", phone="", @@ -230,7 +228,7 @@ def update_door(door, members): def main(): - memberData = getMembershipworksData( + memberData = membershipworks.get_members( ['members', 'staff', 'misc'], "_id,nam,phn,eml,lvl,lbl,xws,xms,xsc,xas,xfd,xac,xcf,xeh,xse") diff --git a/membershipViewer.py b/membershipViewer.py index a905d8f..841aba1 100644 --- a/membershipViewer.py +++ b/membershipViewer.py @@ -6,8 +6,7 @@ from flask import Flask, render_template, request app = Flask(__name__) -from common import * -from hid.DoorController import DoorController +from common import doors, membershipworks def parse_list(member, regex): data_list = [] @@ -46,7 +45,7 @@ def main(): return render_template("members.html", error="Enter at least 3 characters to search") - data = getMembershipworksData( + data = membershipworks.get_members( ['members', 'staff'], "lvl,xws,xms,xsc,xas,xfd,xac,phn,eml,lbl,xcf,nam,end") diff --git a/ucsAccounts.py b/ucsAccounts.py index ea83093..e34f849 100755 --- a/ucsAccounts.py +++ b/ucsAccounts.py @@ -4,7 +4,7 @@ import re import string import subprocess -from common import getMembershipworksData +from common import membershipworks LDAP_BASE = "cn=users,dc=sawtooth,dc=claremontmakerspace,dc=org" GROUP_BASE = "cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org" @@ -32,7 +32,7 @@ def makeAppendGroups(member): for group in groups], []) def main(): - members = getMembershipworksData(['members', 'staff'], + members = membershipworks.get_members(['members', 'staff'], "lvl,phn,eml,lbl,nam,end,_id") makeGroups(members)