Skip to content

Possum - Mumina A.#16

Open
MuminaA wants to merge 17 commits intoAda-C24:mainfrom
MuminaA:main
Open

Possum - Mumina A.#16
MuminaA wants to merge 17 commits intoAda-C24:mainfrom
MuminaA:main

Conversation

@MuminaA
Copy link
Copy Markdown

@MuminaA MuminaA commented Nov 7, 2025

  • Task list requirements completed
  • All tests passing
  • Deployed to render
  • one-to-many relationship created for goal and tasks
  • code refactored

Copy link
Copy Markdown

@anselrognlie anselrognlie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall, but please review my comments, and let me know if you have any questions.

Comment thread tests/test_wave_01.py
Comment on lines +152 to +153
assert isinstance(response_body, dict)
assert "message" in response_body
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two checks aren't really essential. Being able to access the result like a dict (by key) and the presense of the key itself would be enforced by the final check for the value of response_body["message"]. And if we really want to make sure that it's actually a dictionary, with only the expected keys, we could compare the result with a dictionary literal containing only the data we expect to see.

    assert response_body == {"message": "Task 1 not found"}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like a number of following tests took a similar approach. Similar feedback applies.

Comment thread tests/test_wave_05.py
# assertion 2 goes here
assert isinstance(response_body, dict)
assert "message" in response_body
assert response_body["message"] == "Goal 1 not found"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In subsequent tests, you checked the status code, but we should do so here as well.

Comment thread tests/test_wave_05.py
Comment on lines +191 to +192
query = db.select(Goal).where(Goal.id == 1)
assert db.session.scalar(query) == None
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other tests, you checked the message string in the response, but we should do so here as well.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't really need to perform a database lookup here. The follow-up request already received a 404, meaning the record was not found (so it was deleted). Querying the database, or sending a follow up request are two ways that can be used to check a result. Querying the database requires this test to make assumptions about how the deletion was handled (that it was actually removed), while performing a follow-up request allows the backend its own implementation (maybe it just marks a record as deleted without actually deleting it). So a case can be made for either approach, but we don't really need both in a single test.

Comment thread tests/test_wave_05.py
assert isinstance(response_body, dict)
assert "message" in response_body
assert response_body["message"] == "Goal 1 not found"
assert db.session.scalars(db.select(Goal)).all() == []
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔎 It would be particularly surprising for the attempt to delete an unknown Goal to result in a Goal record being added to the database. So there's no need to confirm that the database is still empty.

Comment thread tests/test_wave_07.py
# Test that the correct status code and response message are returned
response = e.value.get_response()
assert response.status_code == 400
assert response.json == {"message": "Task One invalid"}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Nice job making use of the response we're able to extract from the error value.

Comment thread app/routes/goal_routes.py
Comment on lines +22 to +25
goal_response = []

for goal in goals:
goal_response.append(goal.to_dict())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List comprehension opportunity.

Comment thread app/models/task.py
Comment on lines +16 to +18
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .goal import Goal
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Nice use of this snippet to suppress the squiggles in VS Code.

Comment thread app/routes/goal_routes.py
return Response(status=204, mimetype='application/json')

@bp.post('/<goal_id>/tasks')
def create_task_with_goal(goal_id):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While a typical reading of this endpoint (POST /goals/id/tasks) would be to create a Task associated with the specified goal, that's not how it was defined. A name such as associate_tasks_to_goal might be more descriptive.

Comment thread app/routes/goal_routes.py
Comment on lines +64 to +71
# remove all current tasks from this goal
for task in goal.tasks:
task.goal_id = None

# For each task ID, find the task and associate it with the goal
for task_id in task_ids:
task = validate_model(Task, task_id)
task.goal_id = goal.id
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we need to validate each Task id anyway, if we stored the resulting tasks in a list, the replacement of the tasks for the goal could be accomplished using the tasks property as

    goal.tasks = tasks

which avoids the need to manually clear the goal association from the existing tasks.

Comment thread app/routes/goal_routes.py
Comment on lines +84 to +85
response = goal.to_dict()
response["tasks"] = tasks
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Nice reuse of your to_dict methods to build up your response. Rather than leaving this logic here in the route, we could move it to an additional Goal method. Perhaps to_dict_with_tasks, or we could add a parameter to the main Goal to_dict that determines whether or not to include the Task details.

Copy link
Copy Markdown

@anselrognlie anselrognlie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Thanks for the updates. Looks good!

Comment thread app/models/task.py

# Turn this Task object into a dictionary so we can send it as JSON in a response
def to_dict(self, include_goal=False):
def to_dict(self, include_goal: Optional[bool] = None):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 If you still wanted to have a way for a caller to override the goal behavior, this is a reasonable way to do it.

As I already wrote, I think it's a little odd that goal_id is conditionally included at all (I would have always included it, and just let it return null for tasks not part of a goal for shape consistency), but it does make sense for the way the waves are structured.

Comment thread app/routes/goal_routes.py
def get_task_for_goal(goal_id):
goal = validate_model(Goal, goal_id)
tasks = [task.to_dict(include_goal=True) for task in goal.tasks]
tasks = [task.to_dict() for task in goal.tasks]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Modifying the default behavior in to_dict lets us omit the extra arguemnt where it was previous needed, and it makes this behavior uniform across any other endpoints that currently (or would in future) be part of a goal.

Comment thread tests/test_wave_06.py

def test_to_dict_includes_goal_id_when_set(app, one_task_belongs_to_one_goal):
task = db.session.scalar(db.select(Task).where(Task.id == 1))
assert task is not None
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We wouldn't really need to assert this result. If something did go wrong (or there were an issue with our query) so that a Task wasn't returned, the following line would crash and fail the test. Only the asserts below that are relatd to the to_dict result are needed.

But what we should really do is have the one_task_belongs_to_one_goal actually return the task it associated in the fixture code (or even a tuple of the related goal and task!). Then that task would be available through the one_task_belongs_to_one_goal parameter, without us needing to look up the task ourselves. This is why we can use client in other tests. the client fixture returns the nedded value, which becomes the value of the parameter within the test itself.

Comment thread tests/test_wave_06.py
Comment on lines +129 to +130
task = db.session.scalar(db.select(Task).where(Task.id == 1))
assert task is not None
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the comment above, if one_task returned the task it created, we wouldn't need to look up the task here. But even if we do look it up, we don't need to assert it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants