diff --git a/docs/action_network.rst b/docs/action_network.rst index a8056c218d..851441b0e7 100644 --- a/docs/action_network.rst +++ b/docs/action_network.rst @@ -99,6 +99,26 @@ You can then call various endpoints: # Get a specific wrapper specific_wrapper = an.get_wrapper('wrapper_id') + + # Get all surveys + all_surveys = an.get_surveys() + + # Get a specific survey + specific_survey = an.get_survey('survey_id') + + # Create a new survey + new_survey = an.create_survey( + title='My Survey', + description='This is a survey about volunteering', + call_to_action='Take Survey' + ) + + # Update an existing survey + updated_survey = an.update_survey( + 'survey_id', + title='Updated Survey Title', + description='Updated description' + ) *********** SQL Mirror diff --git a/parsons/action_network/action_network.py b/parsons/action_network/action_network.py index 8c74da6486..cec78580fd 100644 --- a/parsons/action_network/action_network.py +++ b/parsons/action_network/action_network.py @@ -44,7 +44,7 @@ def _get_entry_list(self, object_name, limit=None, per_page=25, filter=None): # returns a list of entries for a given object, such as people, tags, or actions # Filter can only be applied to people, petitions, events, forms, fundraising_pages, # event_campaigns, campaigns, advocacy_campaigns, signatures, attendances, submissions, - # donations and outreaches. + # donations, outreaches, and surveys. # See Action Network API docs for more info: https://actionnetwork.org/docs/v2/ count = 0 page = 1 @@ -1788,6 +1788,198 @@ def update_submission(self, form_id, submission_id, data): f"forms/{form_id}/submissions/{submission_id}", data=json.dumps(data) ) + # Surveys + def get_surveys(self, limit=None, per_page=25, page=None, filter=None): + """ + Get surveys. + + `Args:` + limit: + The number of entries to return. When None, returns all entries. + per_page: + The number of entries per page to return. 25 maximum. + page: + Which page of results to return + filter: + The OData query for filtering results. E.g. "modified_date gt '2014-03-25'". + When None, no filter is applied. + + `Returns:` + Parsons Table + A Table with all of the survey entries + `Documentation Reference`: + https://actionnetwork.org/docs/v2/surveys + """ + if page: + return self._get_page("surveys", page, per_page, filter) + return self._get_entry_list("surveys", limit, per_page, filter) + + def get_survey(self, survey_id): + """ + Get a specific survey. + + `Args:` + survey_id: + The unique id of the survey + `Returns:` + Parsons Table + A Table with the survey entry + `Documentation Reference`: + https://actionnetwork.org/docs/v2/surveys + """ + response = self.api.get_request(f"surveys/{survey_id}") + return Table([response]) + + def create_survey(self, title, description=None, call_to_action=None, + browser_url=None, featured_image_url=None, origin_system=None, + identifiers=None, background_processing=False, **kwargs): + """ + Create a new survey. + + `Args:` + title: str + The title of the survey (required) + description: str + The description of the survey + call_to_action: str + The call to action text for the survey + browser_url: str + The URL where the survey can be accessed + featured_image_url: str + URL of the featured image for the survey + origin_system: str + The name of the system where this survey originated + identifiers: list + A list of identifiers for the survey + background_processing: bool + Whether to process this request in the background + **kwargs: + Additional fields to include in the survey payload + + `Returns:` + A JSON response with the created survey + `Documentation Reference`: + https://actionnetwork.org/docs/v2/surveys + """ + payload = {"title": title} + + if description is not None: + payload["description"] = description + if call_to_action is not None: + payload["call_to_action"] = call_to_action + if browser_url is not None: + payload["browser_url"] = browser_url + if featured_image_url is not None: + payload["featured_image_url"] = featured_image_url + if origin_system is not None: + payload["origin_system"] = origin_system + if identifiers is not None: + payload["identifiers"] = identifiers + + # Add any additional fields from kwargs + payload.update(kwargs) + + if background_processing: + payload["action_network:background_processing"] = background_processing + + return self.api.post_request("surveys", data=json.dumps(payload)) + + def update_survey(self, survey_id, title=None, description=None, call_to_action=None, + browser_url=None, featured_image_url=None, background_processing=False, + **kwargs): + """ + Update an existing survey. + + `Args:` + survey_id: str + The unique id of the survey to update + title: str + The new title of the survey + description: str + The new description of the survey + call_to_action: str + The new call to action text for the survey + browser_url: str + The new URL where the survey can be accessed + featured_image_url: str + New URL of the featured image for the survey + background_processing: bool + Whether to process this request in the background + **kwargs: + Additional fields to update in the survey + + `Returns:` + A JSON response with the updated survey + `Documentation Reference`: + https://actionnetwork.org/docs/v2/surveys + """ + payload = {} + + if title is not None: + payload["title"] = title + if description is not None: + payload["description"] = description + if call_to_action is not None: + payload["call_to_action"] = call_to_action + if browser_url is not None: + payload["browser_url"] = browser_url + if featured_image_url is not None: + payload["featured_image_url"] = featured_image_url + + # Add any additional fields from kwargs + payload.update(kwargs) + + if background_processing: + payload["action_network:background_processing"] = background_processing + + return self.api.put_request(f"surveys/{survey_id}", data=json.dumps(payload)) + + def get_survey_responses(self, survey_id, limit=None, per_page=25, page=None, filter=None): + """ + Get responses for a specific survey. + + `Args:` + survey_id: str + The unique id of the survey + limit: + The number of entries to return. When None, returns all entries. + per_page: + The number of entries per page to return. 25 maximum. + page: + Which page of results to return + filter: + The OData query for filtering results. E.g. "modified_date gt '2014-03-25'". + When None, no filter is applied. + + `Returns:` + Parsons Table + A Table with all response entries for the survey + `Documentation Reference`: + https://actionnetwork.org/docs/v2/responses + """ + if page: + return self._get_page(f"surveys/{survey_id}/responses", page, per_page, filter) + return self._get_entry_list(f"surveys/{survey_id}/responses", limit, per_page, filter) + + def get_survey_response(self, survey_id, response_id): + """ + Get a specific response for a survey. + + `Args:` + survey_id: str + The unique id of the survey + response_id: str + The unique id of the response + + `Returns:` + Parsons Table + A Table with the response entry + `Documentation Reference`: + https://actionnetwork.org/docs/v2/responses + """ + response = self.api.get_request(f"surveys/{survey_id}/responses/{response_id}") + return Table([response]) + # Tags def get_tags(self, limit=None, per_page=None): """ diff --git a/test/test_action_network/test_action_network.py b/test/test_action_network/test_action_network.py index e1d2fd9e0e..3e3264c8a9 100644 --- a/test/test_action_network/test_action_network.py +++ b/test/test_action_network/test_action_network.py @@ -3319,6 +3319,192 @@ def setUp(self, m): ], }, } + self.fake_survey_id_1 = "action_network:fake_survey_id_1" + self.fake_survey_id_2 = "action_network:fake_survey_id_2" + self.fake_surveys = { + "total_pages": 1, + "per_page": 25, + "page": 1, + "total_records": 2, + "_links": { + "next": {"href": f"{self.api_url}/surveys?page=2"}, + "action_network:surveys": [ + {"href": f"{self.api_url}/surveys/{self.fake_survey_id_1}"}, + {"href": f"{self.api_url}/surveys/{self.fake_survey_id_2}"}, + ], + "curies": [ + {"name": "osdi", "templated": True}, + {"name": "action_network", "templated": True}, + ], + "self": {"href": f"{self.api_url}/surveys"}, + }, + "_embedded": { + "action_network:surveys": [ + { + "identifiers": [self.fake_survey_id_1], + "origin_system": "Action Network", + "created_date": self.fake_datetime, + "modified_date": self.fake_datetime, + "title": "Test Survey 1", + "description": "

This is a test survey.

", + "call_to_action": "Take Survey", + "browser_url": "https://actionnetwork.org/surveys/test-survey-1", + "featured_image_url": "https://actionnetwork.org/images/test-survey-1.jpg", + "total_responses": 5, + "action_network:hidden": False, + "_links": { + "self": {"href": f"{self.api_url}/surveys/{self.fake_survey_id_1}"}, + "action_network:responses": { + "href": f"{self.api_url}/surveys/{self.fake_survey_id_1}/responses" + }, + }, + }, + { + "identifiers": [self.fake_survey_id_2], + "origin_system": "External System", + "created_date": self.fake_datetime, + "modified_date": self.fake_datetime, + "title": "Test Survey 2", + "description": "

Another test survey.

", + "total_responses": 10, + "action_network:hidden": False, + "_links": { + "self": {"href": f"{self.api_url}/surveys/{self.fake_survey_id_2}"}, + "action_network:responses": { + "href": f"{self.api_url}/surveys/{self.fake_survey_id_2}/responses" + }, + }, + }, + ] + }, + } + self.fake_survey = { + "identifiers": [self.fake_survey_id_1], + "origin_system": "Action Network", + "created_date": self.fake_datetime, + "modified_date": self.fake_datetime, + "title": "Test Survey 1", + "description": "

This is a test survey.

", + "call_to_action": "Take Survey", + "browser_url": "https://actionnetwork.org/surveys/test-survey-1", + "featured_image_url": "https://actionnetwork.org/images/test-survey-1.jpg", + "total_responses": 5, + "action_network:hidden": False, + "_links": { + "self": {"href": f"{self.api_url}/surveys/{self.fake_survey_id_1}"}, + "action_network:responses": { + "href": f"{self.api_url}/surveys/{self.fake_survey_id_1}/responses" + }, + }, + } + self.fake_response_id_1 = "action_network:fake_response_id_1" + self.fake_response_id_2 = "action_network:fake_response_id_2" + self.fake_survey_responses = { + "total_pages": 1, + "per_page": 25, + "page": 1, + "total_records": 2, + "_links": { + "next": {"href": f"{self.api_url}/surveys/{self.fake_survey_id_1}/responses?page=2"}, + "action_network:responses": [ + {"href": f"{self.api_url}/surveys/{self.fake_survey_id_1}/responses/{self.fake_response_id_1}"}, + {"href": f"{self.api_url}/surveys/{self.fake_survey_id_1}/responses/{self.fake_response_id_2}"}, + ], + "curies": [ + {"name": "osdi", "templated": True}, + {"name": "action_network", "templated": True}, + ], + "self": {"href": f"{self.api_url}/surveys/{self.fake_survey_id_1}/responses"}, + }, + "_embedded": { + "action_network:responses": [ + { + "identifiers": [self.fake_response_id_1], + "origin_system": "Action Network", + "created_date": self.fake_datetime, + "modified_date": self.fake_datetime, + "custom_fields": { + "question_1": "Test answer 1", + "question_2": "Test answer 2" + }, + "_embedded": { + "osdi:person": { + "given_name": "Test", + "family_name": "Responder", + "email_addresses": [ + { + "primary": True, + "address": "test@example.com", + "status": "subscribed" + } + ], + "identifiers": [self.fake_person_id_1] + } + }, + "_links": { + "self": {"href": f"{self.api_url}/surveys/{self.fake_survey_id_1}/responses/{self.fake_response_id_1}"}, + "osdi:person": {"href": f"{self.api_url}/people/{self.fake_person_id_1}"}, + }, + }, + { + "identifiers": [self.fake_response_id_2], + "origin_system": "Action Network", + "created_date": self.fake_datetime, + "modified_date": self.fake_datetime, + "custom_fields": { + "question_1": "Another test answer", + "question_2": "Another answer" + }, + "_embedded": { + "osdi:person": { + "given_name": "Another", + "family_name": "Responder", + "email_addresses": [ + { + "primary": True, + "address": "another@example.com", + "status": "subscribed" + } + ], + "identifiers": [self.fake_person_id_2] + } + }, + "_links": { + "self": {"href": f"{self.api_url}/surveys/{self.fake_survey_id_1}/responses/{self.fake_response_id_2}"}, + "osdi:person": {"href": f"{self.api_url}/people/{self.fake_person_id_2}"}, + }, + }, + ] + }, + } + self.fake_survey_response = { + "identifiers": [self.fake_response_id_1], + "origin_system": "Action Network", + "created_date": self.fake_datetime, + "modified_date": self.fake_datetime, + "custom_fields": { + "question_1": "Test answer 1", + "question_2": "Test answer 2" + }, + "_embedded": { + "osdi:person": { + "given_name": "Test", + "family_name": "Responder", + "email_addresses": [ + { + "primary": True, + "address": "test@example.com", + "status": "subscribed" + } + ], + "identifiers": [self.fake_person_id_1] + } + }, + "_links": { + "self": {"href": f"{self.api_url}/surveys/{self.fake_survey_id_1}/responses/{self.fake_response_id_1}"}, + "osdi:person": {"href": f"{self.api_url}/people/{self.fake_person_id_1}"}, + }, + } @requests_mock.Mocker() def test_get_page(self, m): @@ -4233,6 +4419,89 @@ def test_update_submission(self, m): self.fake_submission, ) + # Surveys + @requests_mock.Mocker() + def test_get_surveys(self, m): + m.get( + f"{self.api_url}/surveys?page=1&per_page=25", + text=json.dumps(self.fake_surveys), + ) + m.get( + f"{self.api_url}/surveys?page=2&per_page=25", + text=json.dumps({"_embedded": {"action_network:surveys": []}}), + ) + assert_matching_tables( + self.an.get_surveys(), + Table(self.fake_surveys["_embedded"]["action_network:surveys"]), + ) + + @requests_mock.Mocker() + def test_get_survey(self, m): + m.get( + f"{self.api_url}/surveys/{self.fake_survey_id_1}", + text=json.dumps(self.fake_survey), + ) + result = self.an.get_survey(self.fake_survey_id_1) + # Verify it returns a Parsons Table + assert isinstance(result, Table), f"Expected Table, got {type(result)}" + # Verify the Table contains the survey data + expected_table = Table([self.fake_survey]) + assert_matching_tables(result, expected_table) + + @requests_mock.Mocker() + def test_create_survey(self, m): + m.post( + f"{self.api_url}/surveys", + text=json.dumps(self.fake_survey), + ) + response = self.an.create_survey( + title="New Survey", + description="A new survey" + ) + assert response == self.fake_survey + + @requests_mock.Mocker() + def test_update_survey(self, m): + updated_survey = self.fake_survey.copy() + updated_survey["title"] = "Updated Survey Title" + m.put( + f"{self.api_url}/surveys/{self.fake_survey_id_1}", + text=json.dumps(updated_survey), + ) + response = self.an.update_survey( + self.fake_survey_id_1, + title="Updated Survey Title" + ) + assert response == updated_survey + + @requests_mock.Mocker() + def test_get_survey_responses(self, m): + m.get( + f"{self.api_url}/surveys/{self.fake_survey_id_1}/responses?page=1&per_page=25", + text=json.dumps(self.fake_survey_responses), + ) + m.get( + f"{self.api_url}/surveys/{self.fake_survey_id_1}/responses?page=2&per_page=25", + text=json.dumps({"_embedded": {"action_network:responses": []}}), + ) + assert_matching_tables( + self.an.get_survey_responses(self.fake_survey_id_1), + Table(self.fake_survey_responses["_embedded"]["action_network:responses"]), + ) + + @requests_mock.Mocker() + def test_get_survey_response(self, m): + m.get( + f"{self.api_url}/surveys/{self.fake_survey_id_1}/responses/{self.fake_response_id_1}", + text=json.dumps(self.fake_survey_response), + ) + result = self.an.get_survey_response(self.fake_survey_id_1, self.fake_response_id_1) + # Verify it returns a Parsons Table + assert isinstance(result, Table), f"Expected Table, got {type(result)}" + # Verify the Table contains the response data + expected_table = Table([self.fake_survey_response]) + assert_matching_tables(result, expected_table) + # Tags @requests_mock.Mocker() def test_get_tags(self, m):