feat: questionnaire shape update

This commit is contained in:
Chris Milne 2026-06-11 12:24:36 +01:00
parent c268097306
commit 0a7f9092c7
7 changed files with 85 additions and 24 deletions

View file

@ -16,6 +16,7 @@ Endpoints:
- [PATCH](/org/contact): [root user]: Updates the (contact_type) contact for an org(id). Any number of details can be changed. - [PATCH](/org/contact): [root user]: Updates the (contact_type) contact for an org(id). Any number of details can be changed.
""" """
from datetime import datetime, timezone
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, status from fastapi import APIRouter, status
@ -29,6 +30,7 @@ from src.contact.models import Contact
from src.contact.schemas import ContactAddress from src.contact.schemas import ContactAddress
from src.contact.exceptions import ContactNotFoundException from src.contact.exceptions import ContactNotFoundException
from src.database import db_dependency from src.database import db_dependency
from src.organisation.schemas_questionnaires import QuestionnaireQuestionsVersion0
from src.user.dependencies import ( from src.user.dependencies import (
user_model_body_dependency, user_model_body_dependency,
user_model_claims_dependency, user_model_claims_dependency,
@ -64,6 +66,7 @@ from src.organisation.schemas import (
OrgPatchRootResponse, OrgPatchRootResponse,
Questionnaire, Questionnaire,
OrgPatchContactResponse, OrgPatchContactResponse,
QuestionnaireMetadata,
) )
router = APIRouter( router = APIRouter(
@ -88,7 +91,9 @@ router = APIRouter(
}, },
}, },
) )
async def get_org_by_id(org_model: org_model_root_claim_query_dependency): async def get_org_by_id(
db: db_dependency, org_model: org_model_root_claim_query_dependency
):
""" """
Returns organisation details including key member email addresses Returns organisation details including key member email addresses
""" """
@ -143,10 +148,21 @@ async def create_org(
ALl organisations are given the "partial" status on creation. See update_questionnaire() for more details. ALl organisations are given the "partial" status on creation. See update_questionnaire() for more details.
""" """
if request_model.intake_questionnaire: if request_model.intake_questionnaire:
intake_questionnaire = request_model.intake_questionnaire.model_dump() questionnaire_questions = request_model.intake_questionnaire.model_dump()
else: else:
intake_questionnaire = None questionnaire_questions = QuestionnaireQuestionsVersion0().model_dump()
org_model = Org(name=request_model.name, intake_questionnaire=intake_questionnaire)
questionnaire_metadata = QuestionnaireMetadata(version=0, submission_date=None)
intake_questionnaire = Questionnaire(
metadata=questionnaire_metadata,
questions=questionnaire_questions,
)
org_model = Org(
name=request_model.name,
intake_questionnaire=intake_questionnaire.model_dump(mode="json"),
)
org_model.status = "partial" org_model.status = "partial"
@ -204,18 +220,27 @@ async def update_questionnaire(
final "are you sure" check before setting the org to be in "submitted" status, awaiting admin approval. final "are you sure" check before setting the org to be in "submitted" status, awaiting admin approval.
""" """
update_data = request_model.intake_questionnaire.model_dump(exclude_none=True) update_data = request_model.intake_questionnaire.model_dump(exclude_none=True)
questionnaire_model = Questionnaire(**org_model.intake_questionnaire) questionnaire = org_model.intake_questionnaire
questions_model = QuestionnaireQuestionsVersion0(**questionnaire["questions"])
for key, value in update_data.items(): for key, value in update_data.items():
if hasattr(questionnaire_model, key): if hasattr(questions_model, key):
setattr(questionnaire_model, key, value) setattr(questions_model, key, value)
else: else:
raise UnprocessableContentException("Invalid keys in update request") raise UnprocessableContentException("Invalid keys in update request")
metadata = QuestionnaireMetadata(version=questionnaire["metadata"]["version"])
# Allows for partially completed questionnaires to be saved without being submitted for review # Allows for partially completed questionnaires to be saved without being submitted for review
if not request_model.partial: if not request_model.partial:
org_model.status = "submitted" org_model.status = "submitted"
metadata.submission_date = datetime.now(timezone.utc)
org_model.intake_questionnaire = questionnaire_model.model_dump() questionnaire_model = Questionnaire(
metadata=metadata,
questions=questions_model,
)
org_model.intake_questionnaire = questionnaire_model.model_dump(mode="json")
db.flush() db.flush()
response = OrgPatchQuestionnaireResponse(**org_model.__dict__) response = OrgPatchQuestionnaireResponse(**org_model.__dict__)
db.commit() db.commit()

View file

@ -7,6 +7,7 @@ Models follow the nomenclature of:
""" """
from typing import Optional from typing import Optional
from datetime import datetime
from pydantic import EmailStr, ConfigDict from pydantic import EmailStr, ConfigDict
@ -21,12 +22,17 @@ from src.schemas import (
from src.contact.schemas import ContactModel from src.contact.schemas import ContactModel
from src.organisation.constants import Status, ContactType from src.organisation.constants import Status, ContactType
from src.organisation.schemas_questionnaires import QuestionnaireQuestionsVersion0
class QuestionnaireMetadata(CustomBaseModel):
version: int
submission_date: Optional[datetime] = None
class Questionnaire(CustomBaseModel): class Questionnaire(CustomBaseModel):
question_one: Optional[str] = None metadata: QuestionnaireMetadata
question_two: Optional[str] = None questions: QuestionnaireQuestionsVersion0
question_three: Optional[str] = None
class ContactSummary(CustomBaseModel): class ContactSummary(CustomBaseModel):
@ -47,7 +53,7 @@ class OrgSchema(OrgIDMixin):
class OrgPostOrgRequest(CustomBaseModel): class OrgPostOrgRequest(CustomBaseModel):
name: str name: str
intake_questionnaire: Optional[Questionnaire] = None intake_questionnaire: Optional[QuestionnaireQuestionsVersion0] = None
class OrgPostOrgResponse(CustomBaseModel): class OrgPostOrgResponse(CustomBaseModel):
@ -57,7 +63,7 @@ class OrgPostOrgResponse(CustomBaseModel):
class OrgPatchQuestionnaireRequest(OrgIDMixin): class OrgPatchQuestionnaireRequest(OrgIDMixin):
intake_questionnaire: Questionnaire intake_questionnaire: QuestionnaireQuestionsVersion0
partial: bool partial: bool

View file

@ -0,0 +1,13 @@
from typing import Optional
from src.schemas import CustomBaseModel
class QuestionnaireQuestions(CustomBaseModel):
pass
class QuestionnaireQuestionsVersion0(QuestionnaireQuestions):
question_one: Optional[str] = None
question_two: Optional[str] = None
question_three: Optional[str] = None

View file

@ -103,7 +103,10 @@ def _seed(db):
owner_contact_id=2, owner_contact_id=2,
security_contact_id=3, security_contact_id=3,
status="approved", status="approved",
intake_questionnaire={"question_two": "answer two"}, intake_questionnaire={
"metadata": {"version": 0, "submission_date": None},
"questions": {"question_two": "answer two"},
},
) )
) )
db.add(Service(name="Test Service", api_key="123456789")) db.add(Service(name="Test Service", api_key="123456789"))

View file

@ -32,7 +32,10 @@ async def test_get_org_auth_root_su(default_client: AsyncClient, db_session):
owner_contact_id=2, owner_contact_id=2,
security_contact_id=3, security_contact_id=3,
status="approved", status="approved",
intake_questionnaire={}, intake_questionnaire={
"metadata": {"version": 0, "submission_date": None},
"questions": {},
},
) )
) )
db_session.flush() db_session.flush()

View file

@ -103,9 +103,13 @@ async def test_patch_org_questionnaire_partial_success(
assert data["status"] == "partial" assert data["status"] == "partial"
assert "intake_questionnaire" in data assert "intake_questionnaire" in data
assert isinstance(data["intake_questionnaire"], dict) assert isinstance(data["intake_questionnaire"], dict)
assert data["intake_questionnaire"]["question_one"] == "new answer one" metadata = data["intake_questionnaire"]["metadata"]
assert data["intake_questionnaire"]["question_two"] == "answer two" assert metadata["version"] == 0
assert data["intake_questionnaire"]["question_three"] is None assert metadata["submission_date"] is None
questions = data["intake_questionnaire"]["questions"]
assert questions["question_one"] == "new answer one"
assert questions["question_two"] == "answer two"
assert questions["question_three"] is None
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -172,9 +176,13 @@ async def test_patch_org_questionnaire_submit_success(
assert data["status"] == "submitted" assert data["status"] == "submitted"
assert "intake_questionnaire" in data assert "intake_questionnaire" in data
assert isinstance(data["intake_questionnaire"], dict) assert isinstance(data["intake_questionnaire"], dict)
assert data["intake_questionnaire"]["question_one"] == "new answer one" metadata = data["intake_questionnaire"]["metadata"]
assert data["intake_questionnaire"]["question_two"] == "answer two" assert metadata["version"] == 0
assert data["intake_questionnaire"]["question_three"] is None assert metadata["submission_date"] is not None
questions = data["intake_questionnaire"]["questions"]
assert questions["question_one"] == "new answer one"
assert questions["question_two"] == "answer two"
assert questions["question_three"] is None
@pytest.mark.parametrize( @pytest.mark.parametrize(

View file

@ -161,9 +161,12 @@ async def test_get_self_orgs_dynamic(default_client: AsyncClient):
"security_contact": {"email": "security@test.org", "id": 3}, "security_contact": {"email": "security@test.org", "id": 3},
"billing_contact": {"email": "billing@test.org", "id": 1}, "billing_contact": {"email": "billing@test.org", "id": 1},
"intake_questionnaire": { "intake_questionnaire": {
"question_one": None, "questions": {
"question_three": None, "question_one": None,
"question_two": "answer two", "question_three": None,
"question_two": "answer two",
},
"metadata": {"version": 0, "submission_date": None},
}, },
} }
] ]