From 3161801d40978df70727a4b50d4a59556af5c4ab Mon Sep 17 00:00:00 2001 From: Scott Johnson Date: Wed, 8 Feb 2017 14:17:21 -0500 Subject: [PATCH 01/10] Initial Membership Service Added memberships submodule. Created MembershipService and associated methods. Created initial Membership model, will expand after tests are created and I can see exactly what's in the objects returned from MemberSuite. --- .../memberships/__init__.py | 0 membersuite_api_client/memberships/models.py | 11 +++ .../memberships/services.py | 70 +++++++++++++++++++ .../tests/test_memberships.py | 11 +++ 4 files changed, 92 insertions(+) create mode 100644 membersuite_api_client/memberships/__init__.py create mode 100644 membersuite_api_client/memberships/models.py create mode 100644 membersuite_api_client/memberships/services.py create mode 100644 membersuite_api_client/tests/test_memberships.py diff --git a/membersuite_api_client/memberships/__init__.py b/membersuite_api_client/memberships/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/membersuite_api_client/memberships/models.py b/membersuite_api_client/memberships/models.py new file mode 100644 index 0000000..b96a875 --- /dev/null +++ b/membersuite_api_client/memberships/models.py @@ -0,0 +1,11 @@ +from membersuite_api_client.utils import convert_ms_object +from membersuite_api_client.exceptions import ExecuteMSQLError + + +class Membership(object): + + def __init__(self, membership): + """Create a Membership model from MemberSuite Member object + """ + self.id = membership["ID"] + self.owner = membership["Owner"] diff --git a/membersuite_api_client/memberships/services.py b/membersuite_api_client/memberships/services.py new file mode 100644 index 0000000..c867f54 --- /dev/null +++ b/membersuite_api_client/memberships/services.py @@ -0,0 +1,70 @@ +""" + The service for connecting to MemberSuite for MembershipService + + http://api.docs.membersuite.com/#References/Objects/Membership.htm + +""" + +from .models import Membership +from ..utils import convert_ms_object + + +class MembershipService(object): + + def __init__(self, client): + """ + Accepts a ConciergeClient to connect with MemberSuite + """ + self.client = client + + def get_memberships_for_org(self, org): + """ + Retrieve all memberships associated with an organization + """ + if not self.client.session_id: + self.client.request_session() + + query = "SELECT Objects() FROM Membership WHERE Owner == {}".format( + org.account_num + ) + result = self.client.client.runSQL(query) + msql_result = result['body']['ExecuteMSQLResult'] + + if not msql_result['Errors']: + return self.package_memberships(msql_result) + else: + return None + + def get_all_memberships(self): + """ + Retrieve all memberships + """ + if not self.client.session_id: + self.client.request_session() + + query = "SELECT Objects() FROM Membership" + result = self.client.client.runSQL(query) + msql_result = result['body']['ExecuteMSQLResult'] + + if not msql_result['Errors']: + return self.package_memberships(msql_result) + else: + return None + + def package_memberships(self, msql_result): + """ + Loops through MS objects returned from queries to turn them into + Membership objects and pack them into a list for later use. + """ + + obj_result = msql_result['ResultValue']['ObjectSearchResult'] + objects = obj_result['Objects']['MemberSuiteObject'] + + membership_list = [] + for obj in objects: + sane_obj = convert_ms_object( + obj['Fields']['KeyValueOfstringanyType']) + membership = Membership(sane_obj) + membership_list.append(membership) + + return membership_list diff --git a/membersuite_api_client/tests/test_memberships.py b/membersuite_api_client/tests/test_memberships.py new file mode 100644 index 0000000..8e63989 --- /dev/null +++ b/membersuite_api_client/tests/test_memberships.py @@ -0,0 +1,11 @@ +import unittest + +from .base import BaseTestCase +from ..memberships.services import MembershipService + + +class MembershipServiceTestCase(BaseTestCase): + + def setUp(self): + super(MembershipServiceTestCase, self).setUp() + self.service = MembershipService(self.client) From 3f2104c43867321e82652a0eb3e22e6e6fba07e3 Mon Sep 17 00:00:00 2001 From: Scott Johnson Date: Thu, 9 Feb 2017 13:16:10 -0500 Subject: [PATCH 02/10] Working Memberships! Working version of memberships models and methods. Test passing. Needs to figure out a sane test for get_all_memberships() that doesn't require pulling all 942+ memberships (a number that will change once data goes live). May have to wait until we have a sandbox created that has unchanging data. @todo - Add tests for membership products. --- membersuite_api_client/client.py | 5 +- membersuite_api_client/memberships/models.py | 48 ++++++++- .../memberships/services.py | 102 ++++++++++++++---- membersuite_api_client/tests/test.py | 79 ++++++++++++++ .../tests/test_memberships.py | 22 ++++ 5 files changed, 230 insertions(+), 26 deletions(-) create mode 100644 membersuite_api_client/tests/test.py diff --git a/membersuite_api_client/client.py b/membersuite_api_client/client.py index 11353c5..a551a36 100644 --- a/membersuite_api_client/client.py +++ b/membersuite_api_client/client.py @@ -131,13 +131,14 @@ def query_orgs(self, parameters=None, since_when=None): else: return None - def runSQL(self, query, start_record=0): + def runSQL(self, query, start_record=0, limit_to=400): concierge_request_header = self.construct_concierge_header( url="http://membersuite.com/contracts/" "IConciergeAPIService/ExecuteMSQL") result = self.client.service.ExecuteMSQL( _soapheaders=[concierge_request_header], msqlStatement=query, - startRecord=start_record + startRecord=start_record, + maximumNumberOfRecordsToReturn=limit_to, ) return result diff --git a/membersuite_api_client/memberships/models.py b/membersuite_api_client/memberships/models.py index b96a875..ab26981 100644 --- a/membersuite_api_client/memberships/models.py +++ b/membersuite_api_client/memberships/models.py @@ -1,11 +1,49 @@ -from membersuite_api_client.utils import convert_ms_object -from membersuite_api_client.exceptions import ExecuteMSQLError - - class Membership(object): def __init__(self, membership): - """Create a Membership model from MemberSuite Member object + """ Create a Membership model from MemberSuite Member object """ self.id = membership["ID"] self.owner = membership["Owner"] + self.membership_directory_opt_out = \ + membership["MembershipDirectoryOptOut"] + self.receives_member_benefits = membership["ReceivesMemberBenefits"] + self.current_dues_amount = membership["CurrentDuesAmount"] + self.expiration_date = membership["ExpirationDate"] + self.type = membership["Type"] + self.product = membership["Product"] + self.last_modified_date = membership["LastModifiedDate"] + + self.status = membership["Status"] + self.join_date = membership["JoinDate"] + self.termination_date = membership["TerminationDate"] + self.renewal_date = membership["RenewalDate"] + + +class MembershipProduct(object): + + def __init__(self, membership_type): + """ Create a MembershipType model from MembershipDuesProduct object + """ + self.id = membership_type["ID"] + self.name = membership_type["Name"] + +STATUSES = { + '6faf90e4-0069-cf2c-650f-0b3c15a7d3aa': 'Expired', + '6faf90e4-0069-cd19-6a43-0b3c15a7c287': 'New Member', + '6faf90e4-0069-c450-a9c8-0b3c6a781755': 'Pending', + '6faf90e4-0069-c7d5-2e88-0b3c15a7cf8a': 'Reinstated', + '6faf90e4-0069-c2ef-6d51-0b3c15a7cb7f': 'Renewed', + '6faf90e4-0069-cebb-b501-0b3c15a7d742': 'Terminated', +} + +TYPES = { + '6faf90e4-006a-c1e7-1ac8-0b3c2f7cb3bc': 'Business', + '6faf90e4-006a-c2ae-40f5-0b3c5c99549d': 'International Institution', + '6faf90e4-006a-c242-d6ab-0b3c5c994f57': 'North American Institution', + '6faf90e4-006a-c6b0-10c0-0b3c2f7cc1c5': 'Other', + '6faf90e4-006a-c71f-2f32-0b3c2f7cbbee': 'Campus', + '6faf90e4-006a-c85d-2efa-0b3c2f7ca9cc': 'HEASC Membership', + '6faf90e4-006a-c9c7-5aa2-0b3bc8775e74': 'New Member', + '6faf90e4-006a-c904-255d-0b3bc8774c95': 'Regained Membership', +} diff --git a/membersuite_api_client/memberships/services.py b/membersuite_api_client/memberships/services.py index c867f54..e465fde 100644 --- a/membersuite_api_client/memberships/services.py +++ b/membersuite_api_client/memberships/services.py @@ -7,6 +7,8 @@ from .models import Membership from ..utils import convert_ms_object +from zeep.exceptions import TransportError +import datetime class MembershipService(object): @@ -17,54 +19,116 @@ def __init__(self, client): """ self.client = client - def get_memberships_for_org(self, org): + def get_memberships_for_org(self, account_num): """ Retrieve all memberships associated with an organization """ if not self.client.session_id: self.client.request_session() - query = "SELECT Objects() FROM Membership WHERE Owner == {}".format( - org.account_num - ) - result = self.client.client.runSQL(query) - msql_result = result['body']['ExecuteMSQLResult'] + query = "SELECT Objects() FROM Membership " \ + "WHERE Owner = '%s'" % account_num + result = self.client.runSQL(query) + msql_result = result["body"]["ExecuteMSQLResult"] + obj_result = msql_result["ResultValue"]["ObjectSearchResult"] + if obj_result['Objects']: + objects = obj_result['Objects']['MemberSuiteObject'] + if not msql_result["Errors"] and len(objects): + return self.package_memberships(objects) + return None + + def get_all_memberships(self, since_when=None, results=None, + start_record=0): + """ + Retrieve all memberships updated since "since_when" - if not msql_result['Errors']: - return self.package_memberships(msql_result) + Must loop over 400 indexes at a time. Recursively calls itself until + a non-full queryset is received, returning a joined set each time. + """ + print(datetime.datetime.now(), start_record) + if not self.client.session_id: + self.client.request_session() + + query = "SELECT Objects() FROM Membership" + if since_when: + query += " WHERE LastModifiedDate > '{since_when} 00:00:00'" \ + " ORDER BY LocalID"\ + .format(since_when=since_when.isoformat()) else: - return None + query += " ORDER BY LocalID" + + try: + result = self.client.runSQL(query, start_record=start_record, + limit_to=200) + except TransportError: + # API Intermittently fails and kicks a 504, + # this is a way to retry if that happens. + result = self.get_all_memberships( + since_when=since_when, + results=results, + start_record=start_record, + ) + + msql_result = result['body']["ExecuteMSQLResult"] + if (not msql_result['Errors'] and msql_result["ResultValue"] + ["ObjectSearchResult"]["Objects"]): + new_results = msql_result["ResultValue"]["ObjectSearchResult"]\ + ["Objects"]["MemberSuiteObject"] + \ + (results or []) + # Check if the queryset was completely full. If so, there may be + # More results we need to query + if len(new_results) >= 200: + new_results = self.get_all_memberships( + since_when=since_when, + results=new_results, + start_record=start_record + 200) + return new_results + else: + return results - def get_all_memberships(self): + def get_all_membership_products(self): """ - Retrieve all memberships + Retrieves membership product objects """ if not self.client.session_id: self.client.request_session() - query = "SELECT Objects() FROM Membership" - result = self.client.client.runSQL(query) + query = "SELECT Objects() FROM MembershipDuesProduct" + result = self.client.runSQL(query) msql_result = result['body']['ExecuteMSQLResult'] - if not msql_result['Errors']: return self.package_memberships(msql_result) else: return None - def package_memberships(self, msql_result): + def package_memberships(self, object_list): """ Loops through MS objects returned from queries to turn them into Membership objects and pack them into a list for later use. """ + membership_list = [] + for obj in object_list: + if type(obj) != str: + sane_obj = convert_ms_object( + obj['Fields']['KeyValueOfstringanyType']) + membership = Membership(sane_obj) + membership_list.append(membership) + + return membership_list + def package_membership_products(self, msql_result): + """ + Loops through MS objects returned from queries to turn them into + MembershipProduct objects and pack them into a list for later use. + """ obj_result = msql_result['ResultValue']['ObjectSearchResult'] objects = obj_result['Objects']['MemberSuiteObject'] - membership_list = [] + product_list = [] for obj in objects: sane_obj = convert_ms_object( obj['Fields']['KeyValueOfstringanyType']) - membership = Membership(sane_obj) - membership_list.append(membership) + product = Membership(sane_obj) + product_list.append(product) - return membership_list + return product_list diff --git a/membersuite_api_client/tests/test.py b/membersuite_api_client/tests/test.py new file mode 100644 index 0000000..9d065cf --- /dev/null +++ b/membersuite_api_client/tests/test.py @@ -0,0 +1,79 @@ +stuff = \ + {'JoinDate': datetime.datetime(2006, 4, 7, 0, 0), + 'MembershipDirectoryOptOut': False, 'LastOrder': None, + 'FlowDownEventID': None, 'Sections': { + 'MemberSuiteObject': [] + }, 'WasLocalIDGenerated': False, 'CurrentDuesAmount': Decimal('1385.0000'), + 'FlowDownDepth': None, 'Product': '6faf90e4-0071-c199-6069-0b3c2f7d5505', + 'RemindedThru': None, + 'ExpirationDate': datetime.datetime(2015, 2, 16, 0, 0), + 'LastOrderLineItemID': None, 'AutomaticallyPayForRenewal': False, + 'Type': '6faf90e4-006a-c71f-2f32-0b3c2f7cbbee', + 'CreatedDate': datetime.datetime(2017, 1, 11, 13, 28, 16), + 'ID': '6faf90e4-0074-cbb5-c1d2-0b3c539859ef', 'Chapters': { + 'MemberSuiteObject': [] + }, 'BillTo': None, 'Name': 'American University', + 'LastModifiedDate': datetime.datetime(2017, 1, 11, 13, 28, 16), + 'TerminationReason': '6faf90e4-006e-c3d1-317c-0b3c3019b932', + 'ReinstatementDate': None, 'FlowDownRelationship': None, + 'DoNotRenew': False, + 'TerminationDate': datetime.datetime(2015, 4, 2, 0, 0), + 'LockedForDeletion': False, 'Order': None, 'LocalID': 792, + 'DateApproved': None, 'CreatedBy': '00000000-0003-4d55-9a15-8d95e2c323ab', + 'SecurityLock': None, + 'MembershipOrganization': '6faf90e4-0068-cf7b-a4f8-0b3bc8772288', + 'RenewalDate': datetime.datetime(2016, 3, 1, 0, 0), + 'OrderLineItemID': None, + 'BilledThru': None, + 'LastModifiedBy': '00000000-0003-4d55-9a15-8d95e2c323ab', + 'SavedPaymentMethod': None, 'SystemTimestamp': 'AAAAABfQUjg=', + 'IsInherited': False, 'FlowDownDate': None, + 'Status': '6faf90e4-0069-c2ef-6d51-0b3c15a7cb7f', + 'LateFeeGracePeriod': None, + 'Owner': '6faf90e4-0007-c578-8310-0b3c53985743', 'AddOns': { + 'MemberSuiteObject': [] + }, 'ParentMembership': None, 'ReferredBy': None, + 'RevenueRecognitionDate': None, 'Notes': None, + 'ReceivesMemberBenefits': True, + 'Approved': True, 'FlowDownTopLevelParent': None, 'Keywords': None, + 'OrderHistory': { + 'MemberSuiteObject': [] + }, 'IsConfiguration': False, 'IsSealed': False} + +{'LastOrderLineItemID': '97390c9e-cfbb-47ef-a31d-c13186512a7e', + 'ID': '6faf90e4-0074-cb61-9346-0b3c551b208a', 'SavedPaymentMethod': None, + 'OrderHistory': { + 'MemberSuiteObject': [] + }, 'Owner': '6faf90e4-0006-cfe7-90c5-0b3c5499ee6f', 'LocalID': 1053, + 'ParentMembership': None, 'LateFeeGracePeriod': None, + 'LockedForDeletion': False, + 'CreatedBy': '00000000-0003-cb64-f377-0b3b10a261a2', + 'WasLocalIDGenerated': False, 'IsInherited': False, + 'LastModifiedDate': datetime.datetime(2017, 2, 2, 8, 13, 20), + 'Type': '6faf90e4-006a-c6b0-10c0-0b3c2f7cc1c5', + 'Order': '6faf90e4-002e-cc15-5860-0b3c551b1d43', 'FlowDownRelationship': None, + 'CurrentDuesAmount': None, 'DoNotRenew': False, + 'RevenueRecognitionDate': datetime.datetime(2017, 1, 12, 0, 0), + 'CreatedDate': datetime.datetime(2017, 1, 12, 22, 14, 11), + 'FlowDownEventID': None, 'RemindedThru': None, 'RenewalDate': None, + 'LastModifiedBy': '00000000-0003-cb64-f377-0b3b10a261a2', + 'FlowDownDate': None, 'IsSealed': False, 'AddOns': { + 'MemberSuiteObject': [] +}, 'Product': '6faf90e4-0071-c86c-cf54-0b3bc8778159', 'Sections': { + 'MemberSuiteObject': [] +}, 'ReinstatementDate': None, + 'OrderLineItemID': '97390c9e-cfbb-47ef-a31d-c13186512a7e', + 'SystemTimestamp': 'AAAAABg5DuM=', 'Notes': None, + 'AutomaticallyPayForRenewal': False, 'FlowDownTopLevelParent': None, + 'Approved': True, + 'MembershipOrganization': '6faf90e4-0068-cf7b-a4f8-0b3bc8772288', + 'Name': 'Hector Test', 'SecurityLock': None, + 'DateApproved': datetime.datetime(2017, 1, 12, 0, 0), 'TerminationDate': None, + 'FlowDownDepth': None, 'Status': None, 'Chapters': { + 'MemberSuiteObject': [] +}, 'Keywords': None, 'LastOrder': '6faf90e4-002e-cc15-5860-0b3c551b1d43', + 'JoinDate': datetime.datetime(2017, 1, 12, 0, 0), 'BillTo': None, + 'ReferredBy': None, 'BilledThru': None, + 'ExpirationDate': datetime.datetime(2017, 12, 31, 0, 0), + 'ReceivesMemberBenefits': True, 'MembershipDirectoryOptOut': False, + 'TerminationReason': None, 'IsConfiguration': False} diff --git a/membersuite_api_client/tests/test_memberships.py b/membersuite_api_client/tests/test_memberships.py index 8e63989..24f9c30 100644 --- a/membersuite_api_client/tests/test_memberships.py +++ b/membersuite_api_client/tests/test_memberships.py @@ -9,3 +9,25 @@ class MembershipServiceTestCase(BaseTestCase): def setUp(self): super(MembershipServiceTestCase, self).setUp() self.service = MembershipService(self.client) + + def test_get_membership_for_org(self): + """ + Get membership info for a test org + """ + # Test org with a membership + test_org_id = "6faf90e4-0007-c578-8310-0b3c53985743" + membership_list = self.service.get_memberships_for_org(test_org_id) + self.assertEqual(membership_list[0].id, + '6faf90e4-0074-cbb5-c1d2-0b3c539859ef') + + # Test org without a membership + test_org_id = "6faf90e4-0007-c9dc-98b7-0b3c53985743" + membership_list = self.service.get_memberships_for_org(test_org_id) + self.assertFalse(membership_list) + + # def test_get_all_memberships(self): + # """ + # Get all memberships + # """ + # membership_list = self.service.get_all_memberships() + # print(len(membership_list)) From 177cdd0ade4c51c9de071c45b8679fd71cdb37ce Mon Sep 17 00:00:00 2001 From: Scott Johnson Date: Thu, 9 Feb 2017 13:50:48 -0500 Subject: [PATCH 03/10] Adding tests, fixing MembershipProducts Fixed MembershipProducts model and related methods in services. Added tests for that, and figured out a test for the get_all_memberships() method. --- .travis.yml | 2 +- .../memberships/services.py | 8 +++---- .../tests/test_memberships.py | 22 ++++++++++++++----- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index b10fc8e..f527fb1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ install: - pip install -r requirements_test.txt - pip install Django==$DJANGO_VERSION -script: nosetests --with-coverage +script: nosetests --with-coverage --nologcapture after_success: coveralls diff --git a/membersuite_api_client/memberships/services.py b/membersuite_api_client/memberships/services.py index e465fde..d5442ee 100644 --- a/membersuite_api_client/memberships/services.py +++ b/membersuite_api_client/memberships/services.py @@ -5,10 +5,9 @@ """ -from .models import Membership +from .models import Membership, MembershipProduct from ..utils import convert_ms_object from zeep.exceptions import TransportError -import datetime class MembershipService(object): @@ -45,7 +44,6 @@ def get_all_memberships(self, since_when=None, results=None, Must loop over 400 indexes at a time. Recursively calls itself until a non-full queryset is received, returning a joined set each time. """ - print(datetime.datetime.now(), start_record) if not self.client.session_id: self.client.request_session() @@ -97,7 +95,7 @@ def get_all_membership_products(self): result = self.client.runSQL(query) msql_result = result['body']['ExecuteMSQLResult'] if not msql_result['Errors']: - return self.package_memberships(msql_result) + return self.package_membership_products(msql_result) else: return None @@ -128,7 +126,7 @@ def package_membership_products(self, msql_result): for obj in objects: sane_obj = convert_ms_object( obj['Fields']['KeyValueOfstringanyType']) - product = Membership(sane_obj) + product = MembershipProduct(sane_obj) product_list.append(product) return product_list diff --git a/membersuite_api_client/tests/test_memberships.py b/membersuite_api_client/tests/test_memberships.py index 24f9c30..c2475d8 100644 --- a/membersuite_api_client/tests/test_memberships.py +++ b/membersuite_api_client/tests/test_memberships.py @@ -25,9 +25,19 @@ def test_get_membership_for_org(self): membership_list = self.service.get_memberships_for_org(test_org_id) self.assertFalse(membership_list) - # def test_get_all_memberships(self): - # """ - # Get all memberships - # """ - # membership_list = self.service.get_all_memberships() - # print(len(membership_list)) + def test_get_all_memberships(self): + """ + Get all memberships beginning with index 700, which should ensure + we run two API calls, one for 200, and one for under 200, + and are able to test the recursion without having to query all + 942+ records (which takes about 10 minutes) + """ + membership_list = self.service.get_all_memberships(start_record=700) + self.assertTrue(len(membership_list) > 200) + + def test_get_all_membership_products(self): + """ + Test if we can retrieve all 103 MembershipProduct objects + """ + membership_product_list = self.service.get_all_membership_products() + self.assertTrue(len(membership_product_list) == 103) From 4d10f7a5c7a174504db093cee1e0e74efd44f376 Mon Sep 17 00:00:00 2001 From: Scott Johnson Date: Thu, 9 Feb 2017 14:17:51 -0500 Subject: [PATCH 04/10] Initial Org Port, remove debug file Removed rogue file where I was dumping a test object from a MS response. Added iniitial org stuff before I noticed that. --- .../organizations/__init__.py | 0 .../organizations/models.py | 6 ++ .../organizations/services.py | 100 ++++++++++++++++++ membersuite_api_client/tests/test.py | 79 -------------- 4 files changed, 106 insertions(+), 79 deletions(-) create mode 100644 membersuite_api_client/organizations/__init__.py create mode 100644 membersuite_api_client/organizations/models.py create mode 100644 membersuite_api_client/organizations/services.py delete mode 100644 membersuite_api_client/tests/test.py diff --git a/membersuite_api_client/organizations/__init__.py b/membersuite_api_client/organizations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/membersuite_api_client/organizations/models.py b/membersuite_api_client/organizations/models.py new file mode 100644 index 0000000..2555149 --- /dev/null +++ b/membersuite_api_client/organizations/models.py @@ -0,0 +1,6 @@ +class Organization(object): + + def __init__(self, org): + """Create an Organization model from MemberSuite Organization object + """ + self.account_num = org["ID"] diff --git a/membersuite_api_client/organizations/services.py b/membersuite_api_client/organizations/services.py new file mode 100644 index 0000000..bdf44dd --- /dev/null +++ b/membersuite_api_client/organizations/services.py @@ -0,0 +1,100 @@ +""" + The service for connecting to MemberSuite for OrganizationService + + http://api.docs.membersuite.com/#References/Objects/Organization.htm + +""" + +from .models import Organization +from ..utils import convert_ms_object +from zeep.exceptions import TransportError + + +class OrganizationService(object): + + def __init__(self, client): + """ + Accepts a ConciergeClient to connect with MemberSuite + """ + self.client = client + + def query_orgs(self, parameters=None, since_when=None, get_all=False, + results=None, start_record=0): + """ + Constructs request to MemberSuite to query organization objects + based on parameters provided. + Must loop over 400 indexes at a time. Recursively calls itself until + a non-full queryset is received, returning a joined set each time. + """ + print("STARTING INDEX: ", start_record) + concierge_request_header = self.construct_concierge_header( + url="http://membersuite.com/contracts/" + "IConciergeAPIService/ExecuteMSQL") + + query = "SELECT Objects() FROM Organization " + if parameters and not get_all: + query += "WHERE" + for key in parameters: + query += " %s = '%s' AND" % (key, parameters[key]) + query = query[:-4] + + if since_when: + query += " AND LastModifiedDate > '{since_when} 00:00:00'" \ + .format(since_when=since_when.isoformat()) + elif since_when and not get_all: + query += "WHERE LastModifiedDate > '{since_when} 00:00:00'".format( + since_when=since_when.isoformat()) + try: + result = self.client.service.ExecuteMSQL( + _soapheaders=[concierge_request_header], + msqlStatement=query, + startRecord=start_record, + maximumNumberOfRecordsToReturn=400, + ) + except TransportError: + # API Intermittently fails and kicks a 504, + # this is a way to retry if that happens. + result = self.query_orgs(parameters=parameters, + since_when=since_when, + get_all=get_all, + results=results, + start_record=start_record) + return result + + # Check that we don't have an empty set returned + if (result["body"]["ExecuteMSQLResult"]["ResultValue"] + ["ObjectSearchResult"]["TotalRowCount"] > 0): + if results: + new_results = (results.append(result["body"] + ["ExecuteMSQLResult"] + ["ResultValue"] + ["ObjectSearchResult"] + ["Objects"] + ["MemberSuiteObject"])) + else: + new_results = result["body"]["ExecuteMSQLResult"]\ + ["ResultValue"]["ObjectSearchResult"]["Objects"]\ + ["MemberSuiteObject"] + # If set was empty, just return the existing results + # (empty list if first iteration) + else: + new_results = results + + # Check if the queryset was completely full. If so, there may be + # More results we need to query + if len(result["body"]["ExecuteMSQLResult"]["ResultValue"] + ["ObjectSearchResult"]["Objects"]["MemberSuiteObject"]) == 400: + # Call this function again recursively, passing the existing + # results and the new index where to start + new_results = self.query_orgs(parameters=parameters, + since_when=since_when, + get_all=get_all, + results=new_results, + start_record=start_record + 400) + return self.package_organizations(new_results) + + def package_organizations(self, org_list): + """ + Loops through MS objects returned from queries to turn them into + Organization objects and pack them into a list for later use. + """ diff --git a/membersuite_api_client/tests/test.py b/membersuite_api_client/tests/test.py deleted file mode 100644 index 9d065cf..0000000 --- a/membersuite_api_client/tests/test.py +++ /dev/null @@ -1,79 +0,0 @@ -stuff = \ - {'JoinDate': datetime.datetime(2006, 4, 7, 0, 0), - 'MembershipDirectoryOptOut': False, 'LastOrder': None, - 'FlowDownEventID': None, 'Sections': { - 'MemberSuiteObject': [] - }, 'WasLocalIDGenerated': False, 'CurrentDuesAmount': Decimal('1385.0000'), - 'FlowDownDepth': None, 'Product': '6faf90e4-0071-c199-6069-0b3c2f7d5505', - 'RemindedThru': None, - 'ExpirationDate': datetime.datetime(2015, 2, 16, 0, 0), - 'LastOrderLineItemID': None, 'AutomaticallyPayForRenewal': False, - 'Type': '6faf90e4-006a-c71f-2f32-0b3c2f7cbbee', - 'CreatedDate': datetime.datetime(2017, 1, 11, 13, 28, 16), - 'ID': '6faf90e4-0074-cbb5-c1d2-0b3c539859ef', 'Chapters': { - 'MemberSuiteObject': [] - }, 'BillTo': None, 'Name': 'American University', - 'LastModifiedDate': datetime.datetime(2017, 1, 11, 13, 28, 16), - 'TerminationReason': '6faf90e4-006e-c3d1-317c-0b3c3019b932', - 'ReinstatementDate': None, 'FlowDownRelationship': None, - 'DoNotRenew': False, - 'TerminationDate': datetime.datetime(2015, 4, 2, 0, 0), - 'LockedForDeletion': False, 'Order': None, 'LocalID': 792, - 'DateApproved': None, 'CreatedBy': '00000000-0003-4d55-9a15-8d95e2c323ab', - 'SecurityLock': None, - 'MembershipOrganization': '6faf90e4-0068-cf7b-a4f8-0b3bc8772288', - 'RenewalDate': datetime.datetime(2016, 3, 1, 0, 0), - 'OrderLineItemID': None, - 'BilledThru': None, - 'LastModifiedBy': '00000000-0003-4d55-9a15-8d95e2c323ab', - 'SavedPaymentMethod': None, 'SystemTimestamp': 'AAAAABfQUjg=', - 'IsInherited': False, 'FlowDownDate': None, - 'Status': '6faf90e4-0069-c2ef-6d51-0b3c15a7cb7f', - 'LateFeeGracePeriod': None, - 'Owner': '6faf90e4-0007-c578-8310-0b3c53985743', 'AddOns': { - 'MemberSuiteObject': [] - }, 'ParentMembership': None, 'ReferredBy': None, - 'RevenueRecognitionDate': None, 'Notes': None, - 'ReceivesMemberBenefits': True, - 'Approved': True, 'FlowDownTopLevelParent': None, 'Keywords': None, - 'OrderHistory': { - 'MemberSuiteObject': [] - }, 'IsConfiguration': False, 'IsSealed': False} - -{'LastOrderLineItemID': '97390c9e-cfbb-47ef-a31d-c13186512a7e', - 'ID': '6faf90e4-0074-cb61-9346-0b3c551b208a', 'SavedPaymentMethod': None, - 'OrderHistory': { - 'MemberSuiteObject': [] - }, 'Owner': '6faf90e4-0006-cfe7-90c5-0b3c5499ee6f', 'LocalID': 1053, - 'ParentMembership': None, 'LateFeeGracePeriod': None, - 'LockedForDeletion': False, - 'CreatedBy': '00000000-0003-cb64-f377-0b3b10a261a2', - 'WasLocalIDGenerated': False, 'IsInherited': False, - 'LastModifiedDate': datetime.datetime(2017, 2, 2, 8, 13, 20), - 'Type': '6faf90e4-006a-c6b0-10c0-0b3c2f7cc1c5', - 'Order': '6faf90e4-002e-cc15-5860-0b3c551b1d43', 'FlowDownRelationship': None, - 'CurrentDuesAmount': None, 'DoNotRenew': False, - 'RevenueRecognitionDate': datetime.datetime(2017, 1, 12, 0, 0), - 'CreatedDate': datetime.datetime(2017, 1, 12, 22, 14, 11), - 'FlowDownEventID': None, 'RemindedThru': None, 'RenewalDate': None, - 'LastModifiedBy': '00000000-0003-cb64-f377-0b3b10a261a2', - 'FlowDownDate': None, 'IsSealed': False, 'AddOns': { - 'MemberSuiteObject': [] -}, 'Product': '6faf90e4-0071-c86c-cf54-0b3bc8778159', 'Sections': { - 'MemberSuiteObject': [] -}, 'ReinstatementDate': None, - 'OrderLineItemID': '97390c9e-cfbb-47ef-a31d-c13186512a7e', - 'SystemTimestamp': 'AAAAABg5DuM=', 'Notes': None, - 'AutomaticallyPayForRenewal': False, 'FlowDownTopLevelParent': None, - 'Approved': True, - 'MembershipOrganization': '6faf90e4-0068-cf7b-a4f8-0b3bc8772288', - 'Name': 'Hector Test', 'SecurityLock': None, - 'DateApproved': datetime.datetime(2017, 1, 12, 0, 0), 'TerminationDate': None, - 'FlowDownDepth': None, 'Status': None, 'Chapters': { - 'MemberSuiteObject': [] -}, 'Keywords': None, 'LastOrder': '6faf90e4-002e-cc15-5860-0b3c551b1d43', - 'JoinDate': datetime.datetime(2017, 1, 12, 0, 0), 'BillTo': None, - 'ReferredBy': None, 'BilledThru': None, - 'ExpirationDate': datetime.datetime(2017, 12, 31, 0, 0), - 'ReceivesMemberBenefits': True, 'MembershipDirectoryOptOut': False, - 'TerminationReason': None, 'IsConfiguration': False} From a75a7ca2274fa8e41e1e9e32aabbe3a25c1de2ab Mon Sep 17 00:00:00 2001 From: Scott Johnson Date: Thu, 9 Feb 2017 14:35:19 -0500 Subject: [PATCH 05/10] Modify Membership Recursive Queries Added parameters limit_to, depth, and max_depth to get_all_memberships(). This allows tests to specify 1 result per iteration, and limit the number of iterations. Depth tracks the current iteration count. Added a check to compare depth to max_depth to decide if it should recur again. --- .../memberships/services.py | 32 +++++++++++++------ .../organizations/services.py | 15 ++++++++- .../tests/test_memberships.py | 11 +++---- 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/membersuite_api_client/memberships/services.py b/membersuite_api_client/memberships/services.py index d5442ee..421acc9 100644 --- a/membersuite_api_client/memberships/services.py +++ b/membersuite_api_client/memberships/services.py @@ -37,12 +37,14 @@ def get_memberships_for_org(self, account_num): return None def get_all_memberships(self, since_when=None, results=None, - start_record=0): + start_record=0, limit_to=200, + depth=1, max_depth=None): """ Retrieve all memberships updated since "since_when" - Must loop over 400 indexes at a time. Recursively calls itself until - a non-full queryset is received, returning a joined set each time. + Loop over queries of size limit_to until either a non-full queryset + is returned, or max_depth is reached (used in tests). Then the + recursion collapses to return a single concatenated list. """ if not self.client.session_id: self.client.request_session() @@ -56,8 +58,12 @@ def get_all_memberships(self, since_when=None, results=None, query += " ORDER BY LocalID" try: - result = self.client.runSQL(query, start_record=start_record, - limit_to=200) + result = self.client.runSQL( + query=query, + start_record=start_record, + limit_to=limit_to, + ) + except TransportError: # API Intermittently fails and kicks a 504, # this is a way to retry if that happens. @@ -65,6 +71,9 @@ def get_all_memberships(self, since_when=None, results=None, since_when=since_when, results=results, start_record=start_record, + limit_to=limit_to, + depth=depth, + max_depth=max_depth, ) msql_result = result['body']["ExecuteMSQLResult"] @@ -75,11 +84,15 @@ def get_all_memberships(self, since_when=None, results=None, (results or []) # Check if the queryset was completely full. If so, there may be # More results we need to query - if len(new_results) >= 200: + if len(new_results) >= limit_to and depth < max_depth: new_results = self.get_all_memberships( since_when=since_when, results=new_results, - start_record=start_record + 200) + start_record=start_record + limit_to, + limit_to=limit_to, + depth=depth+1, + max_depth=max_depth + ) return new_results else: return results @@ -121,12 +134,11 @@ def package_membership_products(self, msql_result): """ obj_result = msql_result['ResultValue']['ObjectSearchResult'] objects = obj_result['Objects']['MemberSuiteObject'] - product_list = [] for obj in objects: sane_obj = convert_ms_object( - obj['Fields']['KeyValueOfstringanyType']) + obj['Fields']['KeyValueOfstringanyType'] + ) product = MembershipProduct(sane_obj) product_list.append(product) - return product_list diff --git a/membersuite_api_client/organizations/services.py b/membersuite_api_client/organizations/services.py index bdf44dd..a5fd2ab 100644 --- a/membersuite_api_client/organizations/services.py +++ b/membersuite_api_client/organizations/services.py @@ -45,6 +45,11 @@ def query_orgs(self, parameters=None, since_when=None, get_all=False, query += "WHERE LastModifiedDate > '{since_when} 00:00:00'".format( since_when=since_when.isoformat()) try: + result = self.client.runSQL( + query=query, + start_record=start_record, + limit_to=limit_to, + ) result = self.client.service.ExecuteMSQL( _soapheaders=[concierge_request_header], msqlStatement=query, @@ -93,8 +98,16 @@ def query_orgs(self, parameters=None, since_when=None, get_all=False, start_record=start_record + 400) return self.package_organizations(new_results) - def package_organizations(self, org_list): + def package_organizations(self, obj_list): """ Loops through MS objects returned from queries to turn them into Organization objects and pack them into a list for later use. """ + org_list = [] + for obj in obj_list: + sane_obj = convert_ms_object( + obj['Fields']['KeyValueOfstringanyType'] + ) + org = Organization(sane_obj) + org_list.append(org) + return org_list diff --git a/membersuite_api_client/tests/test_memberships.py b/membersuite_api_client/tests/test_memberships.py index c2475d8..359f970 100644 --- a/membersuite_api_client/tests/test_memberships.py +++ b/membersuite_api_client/tests/test_memberships.py @@ -27,13 +27,12 @@ def test_get_membership_for_org(self): def test_get_all_memberships(self): """ - Get all memberships beginning with index 700, which should ensure - we run two API calls, one for 200, and one for under 200, - and are able to test the recursion without having to query all - 942+ records (which takes about 10 minutes) + Does the get_all_memberships() method work? """ - membership_list = self.service.get_all_memberships(start_record=700) - self.assertTrue(len(membership_list) > 200) + membership_list = self.service.get_all_memberships( + limit_to=1, max_depth=2 + ) + self.assertEqual(len(membership_list), 2) def test_get_all_membership_products(self): """ From 4380dc1891422e434c47acb3c6601fba5eef4004 Mon Sep 17 00:00:00 2001 From: Scott Johnson Date: Thu, 9 Feb 2017 15:19:00 -0500 Subject: [PATCH 06/10] Cleaning up Membership Stuff Added test to make sure we're getting proper Membership objects. Required modifying get_all_memberships() to call the package method before concatenation...if it was called in the return statement as usual it broke due to recursion (tried packaging parts of the results that were already converted). --- membersuite_api_client/memberships/services.py | 10 +++++++--- membersuite_api_client/tests/test_memberships.py | 5 +++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/membersuite_api_client/memberships/services.py b/membersuite_api_client/memberships/services.py index 421acc9..7faeec0 100644 --- a/membersuite_api_client/memberships/services.py +++ b/membersuite_api_client/memberships/services.py @@ -79,9 +79,13 @@ def get_all_memberships(self, since_when=None, results=None, msql_result = result['body']["ExecuteMSQLResult"] if (not msql_result['Errors'] and msql_result["ResultValue"] ["ObjectSearchResult"]["Objects"]): - new_results = msql_result["ResultValue"]["ObjectSearchResult"]\ - ["Objects"]["MemberSuiteObject"] + \ - (results or []) + new_results = self.package_memberships(msql_result + ["ResultValue"] + ["ObjectSearchResult"] + ["Objects"] + ["MemberSuiteObject"] + ) + (results or []) + # Check if the queryset was completely full. If so, there may be # More results we need to query if len(new_results) >= limit_to and depth < max_depth: diff --git a/membersuite_api_client/tests/test_memberships.py b/membersuite_api_client/tests/test_memberships.py index 359f970..30e3e2f 100644 --- a/membersuite_api_client/tests/test_memberships.py +++ b/membersuite_api_client/tests/test_memberships.py @@ -2,6 +2,7 @@ from .base import BaseTestCase from ..memberships.services import MembershipService +from ..memberships.models import Membership, MembershipProduct class MembershipServiceTestCase(BaseTestCase): @@ -17,6 +18,7 @@ def test_get_membership_for_org(self): # Test org with a membership test_org_id = "6faf90e4-0007-c578-8310-0b3c53985743" membership_list = self.service.get_memberships_for_org(test_org_id) + self.assertEqual(type(membership_list[0]), Membership) self.assertEqual(membership_list[0].id, '6faf90e4-0074-cbb5-c1d2-0b3c539859ef') @@ -33,6 +35,7 @@ def test_get_all_memberships(self): limit_to=1, max_depth=2 ) self.assertEqual(len(membership_list), 2) + self.assertEqual(type(membership_list[0]), Membership) def test_get_all_membership_products(self): """ @@ -40,3 +43,5 @@ def test_get_all_membership_products(self): """ membership_product_list = self.service.get_all_membership_products() self.assertTrue(len(membership_product_list) == 103) + self.assertEqual(type(membership_product_list[0]), + MembershipProduct) From e66349d3fbcf75a3250b1dc46362aba3714bc2af Mon Sep 17 00:00:00 2001 From: Scott Johnson Date: Thu, 9 Feb 2017 15:37:32 -0500 Subject: [PATCH 07/10] Organization and tests Added Organization Service and associated tests. Just need to flesh out Organization model more and we're done! --- membersuite_api_client/client.py | 36 ------- .../organizations/services.py | 93 +++++++++---------- membersuite_api_client/tests/test_client.py | 25 ----- .../tests/test_memberships.py | 2 - .../tests/test_organizations.py | 30 ++++++ 5 files changed, 73 insertions(+), 113 deletions(-) create mode 100644 membersuite_api_client/tests/test_organizations.py diff --git a/membersuite_api_client/client.py b/membersuite_api_client/client.py index a551a36..56111aa 100644 --- a/membersuite_api_client/client.py +++ b/membersuite_api_client/client.py @@ -95,42 +95,6 @@ def construct_concierge_header(self, url): return concierge_request_header - def query_orgs(self, parameters=None, since_when=None): - """ - Constructs request to MemberSuite to query organization objects - based on parameters provided. - - parameters: A dictionary of key-value pairs (field name: value) - """ - concierge_request_header = self.construct_concierge_header( - url="http://membersuite.com/contracts/" - "IConciergeAPIService/ExecuteMSQL") - - query = "SELECT Objects() FROM Organization " - if parameters: - query += "WHERE" - for key in parameters: - query += " %s = '%s' AND" % (key, parameters[key]) - query = query[:-4] - - if since_when: - query += " AND LastModifiedDate > '{since_when} 00:00:00'"\ - .format(since_when=since_when.isoformat()) - elif since_when: - query += "WHERE LastModifiedDate > '{since_when} 00:00:00'".format( - since_when=since_when.isoformat()) - - result = self.client.service.ExecuteMSQL( - _soapheaders=[concierge_request_header], - msqlStatement=query, - startRecord=0 - ) - if result["body"]["ExecuteMSQLResult"]["ResultValue"]["ObjectSearchResult"]["Objects"]: - return(result["body"]["ExecuteMSQLResult"]["ResultValue"] - ["ObjectSearchResult"]["Objects"]["MemberSuiteObject"]) - else: - return None - def runSQL(self, query, start_record=0, limit_to=400): concierge_request_header = self.construct_concierge_header( url="http://membersuite.com/contracts/" diff --git a/membersuite_api_client/organizations/services.py b/membersuite_api_client/organizations/services.py index a5fd2ab..8b1108e 100644 --- a/membersuite_api_client/organizations/services.py +++ b/membersuite_api_client/organizations/services.py @@ -18,18 +18,19 @@ def __init__(self, client): """ self.client = client - def query_orgs(self, parameters=None, since_when=None, get_all=False, - results=None, start_record=0): + def get_orgs(self, parameters=None, get_all=False, since_when=None, + results=None, start_record=0, limit_to=200, depth=1, + max_depth=None): """ Constructs request to MemberSuite to query organization objects based on parameters provided. - Must loop over 400 indexes at a time. Recursively calls itself until - a non-full queryset is received, returning a joined set each time. + + Loop over queries of size limit_to until either a non-full queryset + is returned, or max_depth is reached (used in tests). Then the + recursion collapses to return a single concatenated list. """ - print("STARTING INDEX: ", start_record) - concierge_request_header = self.construct_concierge_header( - url="http://membersuite.com/contracts/" - "IConciergeAPIService/ExecuteMSQL") + if not self.client.session_id: + self.client.request_session() query = "SELECT Objects() FROM Organization " if parameters and not get_all: @@ -50,53 +51,45 @@ def query_orgs(self, parameters=None, since_when=None, get_all=False, start_record=start_record, limit_to=limit_to, ) - result = self.client.service.ExecuteMSQL( - _soapheaders=[concierge_request_header], - msqlStatement=query, - startRecord=start_record, - maximumNumberOfRecordsToReturn=400, - ) + except TransportError: # API Intermittently fails and kicks a 504, # this is a way to retry if that happens. - result = self.query_orgs(parameters=parameters, - since_when=since_when, - get_all=get_all, - results=results, - start_record=start_record) - return result + result = self.get_orgs( + parameters=parameters, + get_all=get_all, + since_when=since_when, + results=results, + start_record=start_record, + limit_to=limit_to, + depth=depth, + max_depth=max_depth, + ) - # Check that we don't have an empty set returned - if (result["body"]["ExecuteMSQLResult"]["ResultValue"] - ["ObjectSearchResult"]["TotalRowCount"] > 0): - if results: - new_results = (results.append(result["body"] - ["ExecuteMSQLResult"] - ["ResultValue"] - ["ObjectSearchResult"] - ["Objects"] - ["MemberSuiteObject"])) - else: - new_results = result["body"]["ExecuteMSQLResult"]\ - ["ResultValue"]["ObjectSearchResult"]["Objects"]\ - ["MemberSuiteObject"] - # If set was empty, just return the existing results - # (empty list if first iteration) + msql_result = result['body']["ExecuteMSQLResult"] + if not msql_result['Errors'] and \ + msql_result["ResultValue"]["ObjectSearchResult"]["Objects"]: + new_results = self.package_organizations(msql_result["ResultValue"] + ["ObjectSearchResult"] + ["Objects"] + ["MemberSuiteObject"] + ) + (results or []) + # Check if the queryset was completely full. If so, there may be + # More results we need to query + if len(new_results) >= limit_to and depth < max_depth: + new_results = self.get_orgs( + parameters=parameters, + get_all=get_all, + since_when=since_when, + results=new_results, + start_record=start_record + limit_to, + limit_to=limit_to, + depth=depth + 1, + max_depth=max_depth + ) + return new_results else: - new_results = results - - # Check if the queryset was completely full. If so, there may be - # More results we need to query - if len(result["body"]["ExecuteMSQLResult"]["ResultValue"] - ["ObjectSearchResult"]["Objects"]["MemberSuiteObject"]) == 400: - # Call this function again recursively, passing the existing - # results and the new index where to start - new_results = self.query_orgs(parameters=parameters, - since_when=since_when, - get_all=get_all, - results=new_results, - start_record=start_record + 400) - return self.package_organizations(new_results) + return None def package_organizations(self, obj_list): """ diff --git a/membersuite_api_client/tests/test_client.py b/membersuite_api_client/tests/test_client.py index 5d2d9ed..4decff2 100644 --- a/membersuite_api_client/tests/test_client.py +++ b/membersuite_api_client/tests/test_client.py @@ -57,31 +57,6 @@ def test_request_session(self): self.assertEqual(get_session_id(result=response), client.session_id) - def test_query_orgs(self): - """ - Can we call the search method and receive an org object back? - """ - client = ConciergeClient(access_key=MS_ACCESS_KEY, - secret_key=MS_SECRET_KEY, - association_id=MS_ASSOCIATION_ID) - - # Send a login request to receive a session id - session_id = client.request_session() - self.assertTrue(session_id) - parameters = { - 'Name': 'AASHE Test Campus', - } - response = client.query_orgs(parameters) - self.assertEqual(response[0]["Fields"]["KeyValueOfstringanyType"] - [28]["Value"], - 'AASHE Test Campus') - - # Test querying orgs modified in the last day - # (there should be zero in our sandbox) - since_when = datetime.date.today() - datetime.timedelta(1) - response = client.query_orgs(parameters, since_when) - # self.assertFalse(response) - if __name__ == '__main__': unittest.main() diff --git a/membersuite_api_client/tests/test_memberships.py b/membersuite_api_client/tests/test_memberships.py index 30e3e2f..eaa3a18 100644 --- a/membersuite_api_client/tests/test_memberships.py +++ b/membersuite_api_client/tests/test_memberships.py @@ -1,5 +1,3 @@ -import unittest - from .base import BaseTestCase from ..memberships.services import MembershipService from ..memberships.models import Membership, MembershipProduct diff --git a/membersuite_api_client/tests/test_organizations.py b/membersuite_api_client/tests/test_organizations.py new file mode 100644 index 0000000..815142c --- /dev/null +++ b/membersuite_api_client/tests/test_organizations.py @@ -0,0 +1,30 @@ +from .base import BaseTestCase +from ..organizations.services import OrganizationService +from ..organizations.models import Organization + + +class OrganizationServiceTestCase(BaseTestCase): + + def setUp(self): + super(OrganizationServiceTestCase, self).setUp() + self.service = OrganizationService(self.client) + + def test_get_orgs(self): + """ + Can we call the get_orgs method and receive an org object back? + """ + # Fetch just one org by name + parameters = { + 'Name': 'AASHE Test Campus', + } + org_list = self.service.get_orgs(parameters) + self.assertEqual(len(org_list), 1) + self.assertEqual(type(org_list[0]), Organization) + + # Fetch all orgs using get_all=True + # But limit to 1 result per iteration, 2 iterations + org_list = self.service.get_orgs(get_all=True, + limit_to=1, + max_depth=2) + self.assertEqual(len(org_list), 2) + self.assertEqual(type(org_list[0]), Organization) From a73591e39d8e702a89919bfcec198d76fa6adb8f Mon Sep 17 00:00:00 2001 From: Scott Johnson Date: Thu, 9 Feb 2017 15:57:41 -0500 Subject: [PATCH 08/10] Org Tests, OrganizationType model Apparently the possible organization types have changed since last I worked on this. As such, decided to abstract that to a metadata class of its own rather than doing a local dict matching. Expanded Organization attributes to match what is needed in the ISS. --- .../organizations/models.py | 43 +++++++++++++++++++ .../organizations/services.py | 31 ++++++++++++- .../tests/test_organizations.py | 10 ++++- 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/membersuite_api_client/organizations/models.py b/membersuite_api_client/organizations/models.py index 2555149..4a51904 100644 --- a/membersuite_api_client/organizations/models.py +++ b/membersuite_api_client/organizations/models.py @@ -4,3 +4,46 @@ def __init__(self, org): """Create an Organization model from MemberSuite Organization object """ self.account_num = org["ID"] + self.membersuite_id = org["LocalID"] + self.org_name = org["Name"] + self.picklist_name = org["SortName"] or '' + + address = org["Mailing_Address"] + if address: + self.street1 = address["Line1"] or '' + self.street2 = address["Line2"] or '' + self.city = address["City"] or '' + self.state = address["State"] or '' + self.country = address["Country"] + self.postal_code = address["PostalCode"] or '' + self.latitude = address["GeocodeLat"] or '' + self.longitude = address["GeocodeLong"] or '' + + self.website = org["WebSite"] or '' + self.exclude_from_website = False + self.is_defunct = (org["Status"] == 'Defunct') + + self.org_type = org["Type"] + + self.stars_participant_status = ( + 'STARS participant' if org["STARSCharterParticipant__c"] else '' + ) + + self.primary_email = org['EmailAddress'] or '' + + +class OrganizationType(object): + + def __init__(self, org_type): + """Create an OrganizationType model + from MemberSuite OrganizationType object + """ + self.id = org_type["ID"] + self.Name = org_type["Name"] + +STATUSES = { + '6faf90e4-01f3-c54c-f01a-0b3bc87640ab': 'Active', + '6faf90e4-01f3-c0f1-4593-0b3c3ca7ff6c': 'Deceased', + '6faf90e4-01f3-c7ad-174c-0b3c52b7f497': 'Defunct', + '6faf90e4-01f3-cd50-ffed-0b3c3ca7f4fd': 'Retired', +} \ No newline at end of file diff --git a/membersuite_api_client/organizations/services.py b/membersuite_api_client/organizations/services.py index 8b1108e..a476702 100644 --- a/membersuite_api_client/organizations/services.py +++ b/membersuite_api_client/organizations/services.py @@ -5,7 +5,7 @@ """ -from .models import Organization +from .models import Organization, OrganizationType from ..utils import convert_ms_object from zeep.exceptions import TransportError @@ -91,6 +91,21 @@ def get_orgs(self, parameters=None, get_all=False, since_when=None, else: return None + def get_org_types(self): + """ + Retrieves all current OrganizationType objects + """ + if not self.client.session_id: + self.client.request_session() + + query = "SELECT Objects() FROM OrganizationType" + result = self.client.runSQL(query=query) + msql_result = result['body']["ExecuteMSQLResult"] + return self.package_org_types(msql_result["ResultValue"] + ["ObjectSearchResult"] + ["Objects"]["MemberSuiteObject"] + ) + def package_organizations(self, obj_list): """ Loops through MS objects returned from queries to turn them into @@ -104,3 +119,17 @@ def package_organizations(self, obj_list): org = Organization(sane_obj) org_list.append(org) return org_list + + def package_org_types(self, obj_list): + """ + Loops through MS objects returned from queries to turn them into + OrganizationType objects and pack them into a list for later use. + """ + org_type_list = [] + for obj in obj_list: + sane_obj = convert_ms_object( + obj['Fields']['KeyValueOfstringanyType'] + ) + org = OrganizationType(sane_obj) + org_type_list.append(org) + return org_type_list diff --git a/membersuite_api_client/tests/test_organizations.py b/membersuite_api_client/tests/test_organizations.py index 815142c..b0a6007 100644 --- a/membersuite_api_client/tests/test_organizations.py +++ b/membersuite_api_client/tests/test_organizations.py @@ -1,6 +1,6 @@ from .base import BaseTestCase from ..organizations.services import OrganizationService -from ..organizations.models import Organization +from ..organizations.models import Organization, OrganizationType class OrganizationServiceTestCase(BaseTestCase): @@ -28,3 +28,11 @@ def test_get_orgs(self): max_depth=2) self.assertEqual(len(org_list), 2) self.assertEqual(type(org_list[0]), Organization) + + def test_get_org_types(self): + """ + Test fetching all org type objects + """ + org_type_list = self.service.get_org_types() + self.assertTrue(len(org_type_list)) + self.assertTrue(type(org_type_list[0]), OrganizationType) From 106927a73f043e5eafa35670a9dd8784debed3a8 Mon Sep 17 00:00:00 2001 From: Scott Johnson Date: Thu, 9 Feb 2017 16:10:20 -0500 Subject: [PATCH 09/10] Remove utils tests Our other tests call this incidentally because we're constantly converting objects and pulling session IDs. --- membersuite_api_client/tests/test_utils.py | 40 ---------------------- 1 file changed, 40 deletions(-) delete mode 100644 membersuite_api_client/tests/test_utils.py diff --git a/membersuite_api_client/tests/test_utils.py b/membersuite_api_client/tests/test_utils.py deleted file mode 100644 index e3c13bf..0000000 --- a/membersuite_api_client/tests/test_utils.py +++ /dev/null @@ -1,40 +0,0 @@ -import os -import unittest - -from ..client import ConciergeClient -from ..utils import convert_ms_object - - -MS_ACCESS_KEY = os.environ["MS_ACCESS_KEY"] -MS_SECRET_KEY = os.environ["MS_SECRET_KEY"] -MS_ASSOCIATION_ID = os.environ["MS_ASSOCIATION_ID"] - - -class UtilsTestCase(unittest.TestCase): - - def test_convert_ms_object(self): - """ - Can we parse the list of dicts for org attributes into a dict? - """ - client = ConciergeClient(access_key=MS_ACCESS_KEY, - secret_key=MS_SECRET_KEY, - association_id=MS_ASSOCIATION_ID) - - # Send a login request to receive a session id - session_id = client.request_session() - self.assertTrue(session_id) - parameters = { - 'Name': 'AASHE Test Campus', - } - response = client.query_orgs(parameters) - self.assertEqual(response[0]["Fields"]["KeyValueOfstringanyType"] - [28]["Value"], - 'AASHE Test Campus') - converted_dict = convert_ms_object( - response[0]["Fields"]["KeyValueOfstringanyType"]) - - self.assertEqual(converted_dict["Name"], "AASHE Test Campus") - - -if __name__ == '__main__': - unittest.main() From c35bd81c75ac6fde6654a80aa434495747d2e4bd Mon Sep 17 00:00:00 2001 From: Robert Erb Date: Thu, 9 Feb 2017 17:08:38 -0500 Subject: [PATCH 10/10] Update .travis.yml --- .travis.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index f527fb1..c29eea1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,15 +4,9 @@ python: - '3.4' - '3.5' -env: - - DJANGO_VERSION=1.4.3 - - DJANGO_VERSION=1.8 - - DJANGO_VERSION=1.10 - install: - pip install -r requirements.txt - pip install -r requirements_test.txt - - pip install Django==$DJANGO_VERSION script: nosetests --with-coverage --nologcapture