Skip to content

Commit

Permalink
Merge branch 'main' into remove-respondent-dict
Browse files Browse the repository at this point in the history
  • Loading branch information
LJBabbage authored Nov 21, 2024
2 parents f55cf14 + 23f1e44 commit 186a852
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 0 deletions.
39 changes: 39 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -821,6 +823,43 @@ paths:
description: The password reset counter has been reset
404:
description: The respondent does not exist
/enrolments/respondent/{party_uuid}:
get:
tags:
- enrolments
summary: get enrolment details
description: returns a list of all enrolments that match given parameters
parameters:
- name: party_uuid
in: path
required: true
description: The UUID of the respondent
schema:
type: string
format: uuid
responses:
200:
description: list of dict enrolments
content:
application/json:
schema:
type: array
items:
type: object
properties:
business_id:
type: string
format: uuid
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:
Expand Down
30 changes: 30 additions & 0 deletions ras_party/controllers/enrolments_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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_respondent_by_party_uuid,
query_respondent_enrolments,
)
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 respondent_enrolments(
session: session, party_uuid: UUID, business_id: UUID = None, survey_id: UUID = None, status: int = None
) -> list[Enrolment]:
"""
returns a list of respondent Enrolments. Business_id, survey_id and status can also be added as conditions
"""

respondent = query_respondent_by_party_uuid(party_uuid, session)
if not respondent:
raise NoResultFound

return query_respondent_enrolments(session, respondent.id, business_id, survey_id, status)
18 changes: 18 additions & 0 deletions ras_party/controllers/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -643,3 +643,21 @@ def count_enrolment_by_survey_business(business_id, survey_id, session):
.count()
)
return response


def query_respondent_enrolments(
session: session, respondent_id: int, business_id: UUID = None, survey_id: UUID = None, status: int = None
) -> list[Enrolment]:
"""
Query to return a list of respondent Enrolments. Business_id, survey_id and status can also be added as conditions
"""
additional_conditions = []

if business_id:
additional_conditions.append(Enrolment.business_id == business_id)
if survey_id:
additional_conditions.append(Enrolment.survey_id == survey_id)
if status:
additional_conditions.append(Enrolment.status == status)

return session.query(Enrolment).filter(and_(Enrolment.respondent_id == respondent_id, *additional_conditions)).all()
7 changes: 7 additions & 0 deletions ras_party/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,13 @@ class Enrolment(Base):
),
)

def to_dict(self) -> dict:
return {
"business_id": self.business_id,
"survey_id": self.survey_id,
"status": self.status.name,
}


class PendingSurveys(Base):
__tablename__ = "pending_surveys"
Expand Down
61 changes: 61 additions & 0 deletions ras_party/views/enrolments_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import logging
from uuid import UUID

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 respondent_enrolments
from ras_party.uuid_helper import is_valid_uuid4

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("/respondent/<party_uuid>", methods=["GET"])
def get_respondent_enrolments(party_uuid: UUID) -> Response:
json = request.get_json()
business_id = json.get("business_id")
survey_id = json.get("survey_id")
status = json.get("status")

if not is_valid_uuid4(party_uuid):
logger.error(f"party_id not a valid uuid {party_uuid}")
return BadRequest()

try:
enrolments = respondent_enrolments(
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)
2 changes: 2 additions & 0 deletions run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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/enrolments")
app.register_blueprint(info_view)
app.register_blueprint(error_handlers.blueprint)

Expand Down
6 changes: 6 additions & 0 deletions test/party_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,3 +444,9 @@ 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_respondent_enrolments(self, party_id, payload={}):
response = self.client.get(
f"/party-api/v1/enrolments/respondent/{party_id}", json=payload, headers=self.auth_headers
)
return response
123 changes: 123 additions & 0 deletions test/test_enrolments_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
from test.party_client import PartyTestClient

from sqlalchemy.exc import DataError
from sqlalchemy.orm.exc import NoResultFound

from ras_party.controllers.enrolments_controller import respondent_enrolments
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 = respondent_enrolments(party_uuid="b6f9d6e8-b840-4c95-a6ce-9ef145dd1f85")

self.assertEqual(len(enrolments), 2)
self.assertEqual(str(enrolments[0].business_id), "75d9af56-1225-4d43-b41d-1199f5f89daa")
self.assertEqual(str(enrolments[1].business_id), "98e2c9dd-a760-47dd-ba18-439fd5fb93a3")

def test_get_enrolments_party_id_and_business_id_and_survey_id(self):
enrolments = respondent_enrolments(
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.assertEqual(str(enrolments[0].respondent_id), "1")
self.assertEqual(str(enrolments[0].business_id), "75d9af56-1225-4d43-b41d-1199f5f89daa")
self.assertEqual(str(enrolments[0].survey_id), "9200d295-9d6e-41fe-b541-747ae67a279f")

def test_get_enrolments_party_id_enabled(self):
enrolments = respondent_enrolments(
party_uuid="5718649e-30bf-4c25-a2c0-aaa733e54ed6", status=EnrolmentStatus.ENABLED
)

self.assertEqual(len(enrolments), 1)
self.assertEqual(str(enrolments[0].business_id), "af25c9d5-6893-4342-9d24-4b88509e965f")
self.assertEqual(str(enrolments[0].survey_id), "9200d295-9d6e-41fe-b541-747ae67a279f")

def test_get_enrolments_party_id_disabled(self):
enrolments = respondent_enrolments(
party_uuid="5718649e-30bf-4c25-a2c0-aaa733e54ed6", status=EnrolmentStatus.DISABLED
)

self.assertEqual(len(enrolments), 1)
self.assertEqual(str(enrolments[0].business_id), "75d9af56-1225-4d43-b41d-1199f5f89daa")
self.assertEqual(str(enrolments[0].survey_id), "9200d295-9d6e-41fe-b541-747ae67a279f")

def test_get_enrolments_party_id_not_found_respondent(self):
with self.assertRaises(NoResultFound):
respondent_enrolments(party_uuid="e6a016da-f7e8-4cb0-88da-9d34a7c1382a")

def test_get_enrolments_party_id_data_error(self):
with self.assertRaises(DataError):
respondent_enrolments(party_uuid="malformed_id")

@with_db_session
def _add_enrolments(self, session):
businesses = {}

for respondent_enrolment in respondents_enrolments:
respondent = Respondent(party_uuid=respondent_enrolment["respondent"])
session.add(respondent)

for enrolment in respondent_enrolment["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)
52 changes: 52 additions & 0 deletions test/test_enrolments_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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.respondent_enrolments")
def test_get_enrolments(self, respondent_enrolments):
respondent_enrolments.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_respondent_enrolments("b146f595-62a0-4d6d-ba88-ef40cffdf8a7")

expected_response = [
{
"business_id": "79af714a-ee1d-446c-9f39-763296ec1f05",
"survey_id": "38553552-7d08-42e4-b86b-06f158c4b95e",
"status": "ENABLED",
}
]

self.assertEqual(expected_response, json.loads(response.data))

@patch("ras_party.views.enrolments_view.respondent_enrolments")
def test_get_enrolments_not_found_respondent(self, respondent_enrolments):
respondent_enrolments.side_effect = NoResultFound
response = self.get_respondent_enrolments("707778b9-cdb0-467a-9585-ee06bca47e2c")

self.assertEqual(404, response.status_code)

@patch("ras_party.views.enrolments_view.respondent_enrolments")
def test_get_enrolments_data_error(self, respondent_enrolments):
respondent_enrolments.side_effect = DataError("InvalidTextRepresentation", "party_uuid", "orig")
response = self.get_respondent_enrolments("malformed_id")

self.assertEqual(400, response.status_code)

def test_get_enrolments_no_params(self):
response = self.get_respondent_enrolments({})

self.assertEqual(400, response.status_code)

0 comments on commit 186a852

Please sign in to comment.