Skip to content

Commit

Permalink
Fix the set of milestones when a milestone answer session is created (#…
Browse files Browse the repository at this point in the history
…191)

- MilestoneAnswer
  - An answer is now created for every milestone when an answer session is created
  - The initial value of the answer is `-1` which indicates the user has not yet submitted an answer for this milestone
  - Add `milestone_group_id` field for convenience when calculating statistics / feedback
- get_milestone_groups
  - Now returns the milestones according to the MilestoneAnswers for the current session
  - This means the set of milestones is fixed and doesn't change even if a milestone expected_age changes such that it would no longer be selected for that session
- update_milestone_answer
  - no longer creates an answer session if there is no existing unexpired one
  - either updates the existing answer for the supplied session, or returns 404 or 401
- Milestone component
  - all answers now exist, replace check for existence with check for `-1` to determine if the answer has already been answered
- add types-python-dateutil to mypy pre-commit hook dependencies
  - remove out-dated comment & type ignores from related code
- resolves #189
  • Loading branch information
lkeegan authored Nov 27, 2024
1 parent 07b46c4 commit 78c1a9e
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 55 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ repos:
rev: v1.13.0
hooks:
- id: mypy
additional_dependencies: ["sqlmodel"]
additional_dependencies: ["sqlmodel", "types-python-dateutil"]
args:
[
--ignore-missing-imports,
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/components/Milestone.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ const breadcrumbdata = $derived([
</Button>
<Button
color="light"
disabled={selectedAnswer === undefined}
disabled={!selectedAnswer || selectedAnswer < 0}
on:click={nextMilestone}
class="m-1 mt-4 text-gray-700 dark:text-gray-400"
>
Expand Down
1 change: 1 addition & 0 deletions mondey_backend/src/mondey_backend/models/milestones.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ class MilestoneAnswer(SQLModel, table=True):
milestone_id: int | None = Field(
default=None, foreign_key="milestone.id", primary_key=True
)
milestone_group_id: int = Field(default=None, foreign_key="milestonegroup.id")
answer: int


Expand Down
21 changes: 7 additions & 14 deletions mondey_backend/src/mondey_backend/routers/milestones.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
from ..models.milestones import SubmittedMilestoneImage
from .utils import add
from .utils import get
from .utils import get_child_age_in_months
from .utils import get_db_child
from .utils import get_or_create_current_milestone_answer_session
from .utils import submitted_milestone_image_path
from .utils import write_image_file

Expand Down Expand Up @@ -52,25 +52,18 @@ def get_milestone_groups(
current_active_user: CurrentActiveUserDep,
child_id: int,
):
delta_months = 6
child = get_db_child(session, current_active_user, child_id)

child_age_months = get_child_age_in_months(child)
milestone_answer_session = get_or_create_current_milestone_answer_session(
session, current_active_user, child
)
milestone_ids = list(milestone_answer_session.answers.keys())
print("milestone_ids", milestone_ids)
milestone_groups = session.exec(
select(MilestoneGroup)
.order_by(col(MilestoneGroup.order))
.options(
lazyload(
MilestoneGroup.milestones.and_(
(
child_age_months
>= col(Milestone.expected_age_months) - delta_months
)
& (
child_age_months
<= col(Milestone.expected_age_months) + delta_months
)
)
MilestoneGroup.milestones.and_(col(Milestone.id).in_(milestone_ids))
)
)
).all()
Expand Down
16 changes: 5 additions & 11 deletions mondey_backend/src/mondey_backend/routers/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from ..models.children import Child
from ..models.children import ChildCreate
from ..models.children import ChildPublic
from ..models.milestones import MilestoneAnswer
from ..models.milestones import MilestoneAnswerPublic
from ..models.milestones import MilestoneAnswerSession
from ..models.milestones import MilestoneAnswerSessionPublic
Expand Down Expand Up @@ -124,8 +123,9 @@ async def delete_child_image(
def get_current_milestone_answer_session(
session: SessionDep, current_active_user: CurrentActiveUserDep, child_id: int
):
child = get_db_child(session, current_active_user, child_id)
milestone_answer_session = get_or_create_current_milestone_answer_session(
session, current_active_user, child_id
session, current_active_user, child
)
return milestone_answer_session

Expand All @@ -146,15 +146,9 @@ def update_milestone_answer(
raise HTTPException(401)
milestone_answer = milestone_answer_session.answers.get(answer.milestone_id)
if milestone_answer is None:
milestone_answer = MilestoneAnswer(
answer_session_id=milestone_answer_session.id,
milestone_id=answer.milestone_id,
answer=answer.answer,
)
add(session, milestone_answer)
else:
milestone_answer.answer = answer.answer
session.commit()
raise HTTPException(401)
milestone_answer.answer = answer.answer
session.commit()
return milestone_answer

# Endpoints for answers to user question
Expand Down
32 changes: 25 additions & 7 deletions mondey_backend/src/mondey_backend/routers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,26 +139,44 @@ def _session_has_expired(milestone_answer_session: MilestoneAnswerSession) -> bo


def get_or_create_current_milestone_answer_session(
session: SessionDep, current_active_user: User, child_id: int
session: SessionDep, current_active_user: User, child: Child
) -> MilestoneAnswerSession:
get_db_child(session, current_active_user, child_id)
milestone_answer_session = session.exec(
select(MilestoneAnswerSession)
.where(
(col(MilestoneAnswerSession.user_id) == current_active_user.id)
& (col(MilestoneAnswerSession.child_id) == child_id)
)
.where(col(MilestoneAnswerSession.user_id) == current_active_user.id)
.where(col(MilestoneAnswerSession.child_id) == child.id)
.order_by(col(MilestoneAnswerSession.created_at).desc())
).first()
if milestone_answer_session is None or _session_has_expired(
milestone_answer_session
):
milestone_answer_session = MilestoneAnswerSession(
child_id=child_id,
child_id=child.id,
user_id=current_active_user.id,
created_at=datetime.datetime.now(),
)
add(session, milestone_answer_session)
delta_months = 6
child_age_months = get_child_age_in_months(child)
milestones = session.exec(
select(Milestone)
.where(
child_age_months >= col(Milestone.expected_age_months) - delta_months
)
.where(
child_age_months <= col(Milestone.expected_age_months) + delta_months
)
).all()
for milestone in milestones:
session.add(
MilestoneAnswer(
answer_session_id=milestone_answer_session.id,
milestone_id=milestone.id,
milestone_group_id=milestone.group_id,
answer=-1,
)
)
session.commit()
return milestone_answer_session


Expand Down
40 changes: 28 additions & 12 deletions mondey_backend/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import pytest
import pytest_asyncio
from dateutil.relativedelta import relativedelta # type: ignore
from dateutil.relativedelta import relativedelta
from fastapi import FastAPI
from fastapi.testclient import TestClient
from PIL import Image
Expand Down Expand Up @@ -81,12 +81,8 @@ def private_dir(tmp_path_factory: pytest.TempPathFactory):
def children():
today = datetime.datetime.today()

# README: this is not entirel stable for all dates. For example, for
# 26th november = today, nine-months ago give 1st March, which then gives
# you 8 months instead of 9 if done as today - datetime.timedelta(days=9 * 30)
# Hence: use dateutil.relativedelta which takes care of the 31 vs 30 vs 28 days stuff
nine_months_ago = today - relativedelta(months=9) # type: ignore
twenty_months_ago = today - relativedelta(months=20) # type: ignore
nine_months_ago = today - relativedelta(months=9)
twenty_months_ago = today - relativedelta(months=20)

return [
# ~9month old child for user (id 3)
Expand Down Expand Up @@ -235,13 +231,29 @@ def session(children: list[dict]):
),
)
)
session.add(MilestoneAnswer(answer_session_id=1, milestone_id=1, answer=1))
session.add(MilestoneAnswer(answer_session_id=1, milestone_id=2, answer=0))
session.add(
MilestoneAnswer(
answer_session_id=1, milestone_id=1, milestone_group_id=1, answer=1
)
)
session.add(
MilestoneAnswer(
answer_session_id=1, milestone_id=2, milestone_group_id=1, answer=0
)
)
# add another (current) milestone answer session for child 1 / user (id 3) with 2 answers to the same questions
session.add(MilestoneAnswerSession(child_id=1, user_id=3, created_at=today))
# add two milestone answers
session.add(MilestoneAnswer(answer_session_id=2, milestone_id=1, answer=3))
session.add(MilestoneAnswer(answer_session_id=2, milestone_id=2, answer=2))
session.add(
MilestoneAnswer(
answer_session_id=2, milestone_id=1, milestone_group_id=1, answer=3
)
)
session.add(
MilestoneAnswer(
answer_session_id=2, milestone_id=2, milestone_group_id=1, answer=2
)
)
# add an (expired) milestone answer session for child 3 / admin user (id 1) with 1 answer
session.add(
MilestoneAnswerSession(
Expand All @@ -250,7 +262,11 @@ def session(children: list[dict]):
created_at=datetime.datetime(today.year - 1, 1, 1),
)
)
session.add(MilestoneAnswer(answer_session_id=3, milestone_id=7, answer=2))
session.add(
MilestoneAnswer(
answer_session_id=3, milestone_id=7, milestone_group_id=2, answer=2
)
)

# add user questions for admin
user_questions = [
Expand Down
17 changes: 8 additions & 9 deletions mondey_backend/tests/routers/test_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,22 +183,21 @@ def test_get_milestone_answers_child1_current_answer_session(user_client: TestCl
assert _is_approx_now(response.json()["created_at"])


def test_update_milestone_answer_current_answer_session_no_answer_session(
def test_update_milestone_answer_no_current_answer_session(
user_client: TestClient,
):
current_answer_session = user_client.get("/users/milestone-answers/1").json()
assert current_answer_session["child_id"] == 1
assert "6" not in current_answer_session["answers"]
new_answer = {"milestone_id": 6, "answer": 2}
current_answer_session = user_client.get("/users/milestone-answers/2").json()
assert current_answer_session["child_id"] == 2
assert current_answer_session["answers"]["3"]["answer"] == -1
assert current_answer_session["answers"]["4"]["answer"] == -1
new_answer = {"milestone_id": 3, "answer": 2}
response = user_client.put(
f"/users/milestone-answers/{current_answer_session['id']}", json=new_answer
)
assert response.status_code == 200
assert response.json() == new_answer
assert (
user_client.get("/users/milestone-answers/1").json()["answers"]["6"]
== new_answer
)
new_answer_session = user_client.get("/users/milestone-answers/2").json()
assert new_answer_session["answers"]["3"] == new_answer


def test_update_milestone_answer_update_existing_answer(user_client: TestClient):
Expand Down

0 comments on commit 78c1a9e

Please sign in to comment.