diff --git a/Makefile b/Makefile index 970ed916..d6bc2cc6 100644 --- a/Makefile +++ b/Makefile @@ -4,13 +4,13 @@ build: pipenv install --dev lint: - pipenv check -i 42194 -i 70612 -i 70624 -i 72731 + pipenv check -i 42194 -i 70612 -i 70624 -i 72731 -i 73655 pipenv run isort . pipenv run black --line-length 120 . pipenv run flake8 --exclude=./scripts lint-check: - pipenv check -i 42194 -i 70612 -i 70624 -i 72731 + pipenv check -i 42194 -i 70612 -i 70624 -i 72731 -i 73655 pipenv run isort . --check-only pipenv run black --line-length 120 --check . pipenv run flake8 --exclude=./scripts diff --git a/openapi.yaml b/openapi.yaml index fdd94e6e..e7149f2e 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -11,6 +11,8 @@ tags: description: Respondent endpoints - name: businesses description: Business endpoints + - name: enrolments + description: enrolments endpoints - name: info description: Information endpoints - name: misc @@ -821,6 +823,37 @@ paths: description: The password reset counter has been reset 404: description: The respondent does not exist + /enrolments: + get: + tags: + - enrolments + summary: get enrolment details + description: returns a list of all enrolments that match given parameters + responses: + 200: + description: list of dict enrolments + content: + application/json: + schema: + type: array + items: + type: object + properties: + business_id: + type: string + format: uuid + respondent_id: + type: integer + status: + type: string + example: ENABLED + survey_id: + type: string + format: uuid + 400: + description: Missing of malformed parameters + 404: + description: Respondent doesn't exist /batch/respondents: delete: tags: diff --git a/ras_party/controllers/enrolments_controller.py b/ras_party/controllers/enrolments_controller.py new file mode 100644 index 00000000..42993b46 --- /dev/null +++ b/ras_party/controllers/enrolments_controller.py @@ -0,0 +1,33 @@ +import logging +from uuid import UUID + +import structlog +from flask import session +from sqlalchemy.orm.exc import NoResultFound + +from ras_party.controllers.queries import ( + query_enrolments_by_parameters, + query_respondent_by_party_uuid, +) +from ras_party.models.models import Enrolment +from ras_party.support.session_decorator import with_query_only_db_session + +logger = structlog.wrap_logger(logging.getLogger(__name__)) + + +@with_query_only_db_session +def enrolments_by_parameters( + session: session, party_uuid: UUID = None, business_id: UUID = None, survey_id: UUID = None, status: int = None +) -> list[Enrolment]: + """ + returns a list of Enrolments based on provided parameters + """ + + if party_uuid: + respondent = query_respondent_by_party_uuid(party_uuid, session) + if not respondent: + raise NoResultFound + + respondent_id = respondent.id if party_uuid else None + + return query_enrolments_by_parameters(session, respondent_id, business_id, survey_id, status) diff --git a/ras_party/controllers/queries.py b/ras_party/controllers/queries.py index 4e05315b..84ca6601 100644 --- a/ras_party/controllers/queries.py +++ b/ras_party/controllers/queries.py @@ -643,3 +643,23 @@ def count_enrolment_by_survey_business(business_id, survey_id, session): .count() ) return response + + +def query_enrolments_by_parameters( + session: session, respondent_id: int = None, business_id: UUID = None, survey_id: UUID = None, status: int = None +) -> list[Enrolment]: + """ + Query to return a list of enrolments based on parameters + """ + conditions = [] + + if respondent_id: + conditions.append(Enrolment.respondent_id == respondent_id) + if business_id: + conditions.append(Enrolment.business_id == business_id) + if survey_id: + conditions.append(Enrolment.survey_id == survey_id) + if status: + conditions.append(Enrolment.status == status) + + return session.query(Enrolment).filter(and_(*conditions)).all() diff --git a/ras_party/models/models.py b/ras_party/models/models.py index 325d9e03..1bacf9ab 100644 --- a/ras_party/models/models.py +++ b/ras_party/models/models.py @@ -390,6 +390,14 @@ class Enrolment(Base): ), ) + def to_dict(self) -> dict: + return { + "business_id": self.business_id, + "respondent_id": self.respondent_id, + "survey_id": self.survey_id, + "status": self.status.name, + } + class PendingSurveys(Base): __tablename__ = "pending_surveys" diff --git a/ras_party/views/enrolments_view.py b/ras_party/views/enrolments_view.py new file mode 100644 index 00000000..2cd1e09f --- /dev/null +++ b/ras_party/views/enrolments_view.py @@ -0,0 +1,60 @@ +import logging + +import structlog +from flask import Blueprint, Response, current_app, make_response, request +from flask_httpauth import HTTPBasicAuth +from sqlalchemy.exc import DataError +from sqlalchemy.orm.exc import NoResultFound +from werkzeug.exceptions import BadRequest, NotFound + +from ras_party.controllers.enrolments_controller import enrolments_by_parameters + +logger = structlog.wrap_logger(logging.getLogger(__name__)) +enrolments_view = Blueprint("enrolments_view", __name__) +auth = HTTPBasicAuth() + + +@enrolments_view.before_request +@auth.login_required +def before_respondent_view(): + pass + + +@auth.get_password +def get_pw(username): + config_username = current_app.config["SECURITY_USER_NAME"] + config_password = current_app.config["SECURITY_USER_PASSWORD"] + if username == config_username: + return config_password + + +@enrolments_view.route("/enrolments", methods=["GET"]) +def get_enrolments() -> Response: + json = request.get_json() + party_uuid = json.get("party_uuid") + business_id = json.get("business_id") + survey_id = json.get("survey_id") + status = json.get("status") + + if not (party_uuid or business_id or survey_id): + logger.error("No parameters passed to get_enrolments") + return BadRequest() + + try: + enrolments = enrolments_by_parameters( + party_uuid=party_uuid, business_id=business_id, survey_id=survey_id, status=status + ) + except NoResultFound: + logger.error(f"Respondent not found for party_uuid {party_uuid}") + return NotFound() + except DataError: + logger.error( + "Data error, enrolment search parameters are not valid", + party_uuid=party_uuid, + business_id=business_id, + survey_id=survey_id, + status=status, + ) + return BadRequest() + + return make_response([enrolment.to_dict() for enrolment in enrolments], 200) diff --git a/run.py b/run.py index 9fe34d56..4eceb3b8 100644 --- a/run.py +++ b/run.py @@ -29,6 +29,7 @@ def create_app(config=None): from ras_party.views.account_view import account_view from ras_party.views.batch_request import batch_request from ras_party.views.business_view import business_view + from ras_party.views.enrolments_view import enrolments_view from ras_party.views.info_view import info_view from ras_party.views.party_view import party_view from ras_party.views.pending_survey_view import pending_survey_view @@ -40,6 +41,7 @@ def create_app(config=None): app.register_blueprint(respondent_view, url_prefix="/party-api/v1") app.register_blueprint(batch_request, url_prefix="/party-api/v1") app.register_blueprint(pending_survey_view, url_prefix="/party-api/v1") + app.register_blueprint(enrolments_view, url_prefix="/party-api/v1") app.register_blueprint(info_view) app.register_blueprint(error_handlers.blueprint) diff --git a/test/party_client.py b/test/party_client.py index c18fcb5b..3bb82816 100644 --- a/test/party_client.py +++ b/test/party_client.py @@ -444,3 +444,7 @@ def get_respondents_by_survey_and_business_id(self, survey_id, business_id): def get_respondents_by_party_id(self, party_id): response = self.client.get(f"/party-api/v1/respondents/party_id/{party_id}", headers=self.auth_headers) return response + + def get_enrolments(self, payload): + response = self.client.get("/party-api/v1/enrolments", json=payload, headers=self.auth_headers) + return response diff --git a/test/test_enrolments_controller.py b/test/test_enrolments_controller.py new file mode 100644 index 00000000..8b2f816f --- /dev/null +++ b/test/test_enrolments_controller.py @@ -0,0 +1,138 @@ +from test.party_client import PartyTestClient + +from sqlalchemy.exc import DataError +from sqlalchemy.orm.exc import NoResultFound + +from ras_party.controllers.enrolments_controller import enrolments_by_parameters +from ras_party.models.models import ( + Business, + BusinessRespondent, + Enrolment, + EnrolmentStatus, + Respondent, +) +from ras_party.support.session_decorator import with_db_session + +respondents_enrolments = [ + { + "respondent": "b6f9d6e8-b840-4c95-a6ce-9ef145dd1f85", + "enrolment_details": [ + { + "business": "75d9af56-1225-4d43-b41d-1199f5f89daa", + "survey_id": "9200d295-9d6e-41fe-b541-747ae67a279f", + "status": EnrolmentStatus.ENABLED, + }, + { + "business": "98e2c9dd-a760-47dd-ba18-439fd5fb93a3", + "survey_id": "c641f6ad-a5eb-4d82-a647-7cd586549bbc", + "status": EnrolmentStatus.ENABLED, + }, + ], + }, + { + "respondent": "5718649e-30bf-4c25-a2c0-aaa733e54ed6", + "enrolment_details": [ + { + "business": "af25c9d5-6893-4342-9d24-4b88509e965f", + "survey_id": "9200d295-9d6e-41fe-b541-747ae67a279f", + "status": EnrolmentStatus.ENABLED, + }, + { + "business": "75d9af56-1225-4d43-b41d-1199f5f89daa", + "survey_id": "9200d295-9d6e-41fe-b541-747ae67a279f", + "status": EnrolmentStatus.DISABLED, + }, + ], + }, +] + + +class TestEnrolments(PartyTestClient): + + def setUp(self): + self._add_enrolments() + + def test_get_enrolments_party_id(self): + enrolments = enrolments_by_parameters(party_uuid="b6f9d6e8-b840-4c95-a6ce-9ef145dd1f85") + + self.assertEqual(len(enrolments), 2) + self.assertIn(str(enrolments[0].business_id), "75d9af56-1225-4d43-b41d-1199f5f89daa") + self.assertIn(str(enrolments[1].business_id), "98e2c9dd-a760-47dd-ba18-439fd5fb93a3") + + def test_get_enrolments_business_id(self): + enrolments = enrolments_by_parameters(business_id="75d9af56-1225-4d43-b41d-1199f5f89daa") + + self.assertEqual(len(enrolments), 2) + self.assertIn(str(enrolments[0].survey_id), "9200d295-9d6e-41fe-b541-747ae67a279f") + self.assertIn(str(enrolments[1].survey_id), "9200d295-9d6e-41fe-b541-747ae67a279f") + + def test_get_enrolments_survey_id(self): + enrolments = enrolments_by_parameters(survey_id="9200d295-9d6e-41fe-b541-747ae67a279f") + + self.assertEqual(len(enrolments), 3) + self.assertIn(str(enrolments[0].business_id), "75d9af56-1225-4d43-b41d-1199f5f89daa") + self.assertIn(str(enrolments[1].business_id), "af25c9d5-6893-4342-9d24-4b88509e965f") + self.assertIn(str(enrolments[2].business_id), "75d9af56-1225-4d43-b41d-1199f5f89daa") + + def test_get_enrolments_party_id_and_business_id_and_survey_id(self): + enrolments = enrolments_by_parameters( + party_uuid="b6f9d6e8-b840-4c95-a6ce-9ef145dd1f85", + business_id="75d9af56-1225-4d43-b41d-1199f5f89daa", + survey_id="9200d295-9d6e-41fe-b541-747ae67a279f", + ) + + self.assertEqual(len(enrolments), 1) + self.assertIn(str(enrolments[0].respondent_id), "b6f9d6e8-b840-4c95-a6ce-9ef145dd1f85") + self.assertIn(str(enrolments[0].business_id), "75d9af56-1225-4d43-b41d-1199f5f89daa") + self.assertIn(str(enrolments[0].survey_id), "9200d295-9d6e-41fe-b541-747ae67a279f") + + def test_get_enrolments_party_id_enabled(self): + enrolments = enrolments_by_parameters( + party_uuid="5718649e-30bf-4c25-a2c0-aaa733e54ed6", status=EnrolmentStatus.ENABLED + ) + + self.assertEqual(len(enrolments), 1) + self.assertIn(str(enrolments[0].business_id), "af25c9d5-6893-4342-9d24-4b88509e965f") + self.assertIn(str(enrolments[0].survey_id), "9200d295-9d6e-41fe-b541-747ae67a279f") + + def test_get_enrolments_party_id_disabled(self): + enrolments = enrolments_by_parameters( + party_uuid="5718649e-30bf-4c25-a2c0-aaa733e54ed6", status=EnrolmentStatus.DISABLED + ) + + self.assertEqual(len(enrolments), 1) + self.assertIn(str(enrolments[0].business_id), "75d9af56-1225-4d43-b41d-1199f5f89daa") + self.assertIn(str(enrolments[0].survey_id), "9200d295-9d6e-41fe-b541-747ae67a279f") + + def test_get_enrolments_party_id_not_found_respondent(self): + with self.assertRaises(NoResultFound): + enrolments_by_parameters(party_uuid="e6a016da-f7e8-4cb0-88da-9d34a7c1382a") + + def test_get_enrolments_party_id_data_error(self): + with self.assertRaises(DataError): + enrolments_by_parameters(party_uuid="malformed_id") + + @with_db_session + def _add_enrolments(self, session): + businesses = {} + + for respondent_enrolments in respondents_enrolments: + respondent = Respondent(party_uuid=respondent_enrolments["respondent"]) + session.add(respondent) + + for enrolment in respondent_enrolments["enrolment_details"]: + if not (business := businesses.get(enrolment["business"])): + business = Business(party_uuid=enrolment["business"]) + session.add(business) + businesses[enrolment["business"]] = business + + business_respondent = BusinessRespondent(business=business, respondent=respondent) + session.add(business_respondent) + session.flush() + enrolment = Enrolment( + business_id=business.party_uuid, + survey_id=enrolment["survey_id"], + respondent_id=respondent.id, + status=enrolment["status"], + ) + session.add(enrolment) diff --git a/test/test_enrolments_view.py b/test/test_enrolments_view.py new file mode 100644 index 00000000..b4d8c2b5 --- /dev/null +++ b/test/test_enrolments_view.py @@ -0,0 +1,53 @@ +import json +from test.party_client import PartyTestClient +from unittest.mock import patch + +from sqlalchemy.exc import DataError +from sqlalchemy.orm.exc import NoResultFound + +from ras_party.models.models import Enrolment, EnrolmentStatus + + +class TestEnrolmentsView(PartyTestClient): + + @patch("ras_party.views.enrolments_view.enrolments_by_parameters") + def test_get_enrolments(self, enrolments_by_parameters): + enrolments_by_parameters.return_value = [ + Enrolment( + business_id="79af714a-ee1d-446c-9f39-763296ec1f05", + survey_id="38553552-7d08-42e4-b86b-06f158c4b95e", + respondent_id=1, + status=EnrolmentStatus.ENABLED, + ) + ] + response = self.get_enrolments({"party_uuid": "b146f595-62a0-4d6d-ba88-ef40cffdf8a7"}) + + expected_response = [ + { + "business_id": "79af714a-ee1d-446c-9f39-763296ec1f05", + "respondent_id": 1, + "survey_id": "38553552-7d08-42e4-b86b-06f158c4b95e", + "status": "ENABLED", + } + ] + + self.assertEqual(expected_response, json.loads(response.data)) + + @patch("ras_party.views.enrolments_view.enrolments_by_parameters") + def test_get_enrolments_not_found_respondent(self, enrolments_by_parameters): + enrolments_by_parameters.side_effect = NoResultFound + response = self.get_enrolments({"party_uuid": "707778b9-cdb0-467a-9585-ee06bca47e2c"}) + + self.assertEqual(404, response.status_code) + + @patch("ras_party.views.enrolments_view.enrolments_by_parameters") + def test_get_enrolments_data_error(self, enrolments_by_parameters): + enrolments_by_parameters.side_effect = DataError("InvalidTextRepresentation", "party_uuid", "orig") + response = self.get_enrolments({"party_uuid": "malformed_id"}) + + self.assertEqual(400, response.status_code) + + def test_get_enrolments_no_params(self): + response = self.get_enrolments({}) + + self.assertEqual(400, response.status_code)