-
Notifications
You must be signed in to change notification settings - Fork 146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Tigers - Maria S. #134
base: master
Are you sure you want to change the base?
Tigers - Maria S. #134
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good work!
Even though I left several comments, including a few bugs and deviations from the spec this code is generally correctly implemented, cleanly written, and straightforward to read, which is good enough for a Green!
|
||
def to_dict(self): | ||
task_as_dict = {} | ||
task_as_dict["is_complete"] = False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't match the specification, as it means that the response will always have "is_complete"
set to False
, when it should be True
if self.completed_at
is not None
.
While you fix this in your response to PATCH /tasks/<task_id>/mark_complete
, this means that if I called PATCH /tasks/13/mark_complete
and then GET /tasks/13/mark_complete
, the second call will say that "is_complete"
is false
in contradiction to the first call which says it's true
. So even though it has just been marked complete and has an completed_at
column in the database, a user would be led to believe that it has not been completed though they marked it as such.
There are a few different ways you could have set "is_complete"
here, a very succinct way is bool(self.completed_at)
.
@@ -1,5 +1,35 @@ | |||
from app import db | |||
from sqlalchemy import Column, ForeignKey, Integer, Table | |||
from sqlalchemy.orm import relationship | |||
|
|||
|
|||
class Task(db.Model): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other than that issue I pointed out, this model class for Task
otherwise looks good!
"task_ids" : list_task_id | ||
} | ||
|
||
return jsonify (new_goal), 200 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case where you are returning a dictionary, you don't need jsonify
because Flask will convert dictionaries to JSON objects by default. You would need it if you were returning, say, a list, like you do for GET /tasks
.
Likewise, if your status code is going to be 200, you don't need to supply it in your return
statement, as that is the default. Now in that case, an argument could be made that being explicit here has benefits in... well, being explicit, but I think the benefit of that is marginal here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, watch spacing! There's an extra space between jsonify
and its argument list, which is very minor but can cause a bit of confusion, as the standard style is no space between a function call and its argument list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a few other places where you use jsonify
on a dictionary, but I'll not point them all out to avoid repeating myself.
try: | ||
response_body = { | ||
"id":goal.goal_id, | ||
"title": goal.title, | ||
"tasks": [task.to_dict() for task in goal.tasks] | ||
} | ||
for task in response_body["tasks"]: | ||
task["goal_id"] = goal.goal_id | ||
|
||
# return make_response(jsonify(response_body), 200) | ||
except: | ||
response_body = { | ||
"id":goal.goal_id, | ||
"title": goal.title, | ||
"tasks": [] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This block is a bit more complicated than you really need. Putting this code in a try-except
block would make sense if goal.tasks
could be None
but that actually won't happen as Flask returns an empty list in the case that no tasks are associated with the goal. This means that [task.to_dict() for task in goal.tasks]
(good use of a list comprehension, by the way!) will resolve to an empty list itself, so no error will be thrown.
Like I said, this would be different if goal.tasks
would return None
, which then would necessitate this code. It could be defended as defensive programming, though, though I feel that would be a bit over-defensive.
Likewise, your Task.to_dict()
method already sets the "goal_id"
key, so the for
loop that follows is unnecessary.
So doing both of those changes brings this down to:
response_body = {
"id": goal.goal_id,
"title": goal.title,
"tasks": [task.to_dict() for task in goal.tasks]
return response_body
goal_as_dict["title"] = self.title | ||
|
||
if self.tasks: | ||
goal_as_dict["tasks"] = self.tasks.title |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code causes a 500 error when calling GET /goals/<goal_id>
if the goal has tasks associated with it. My guess is you meant something like [task.title for task in self.tasks]
, but your code here means you're trying to access the title
attribute of the list (really an InstrumentedList
) rather than its elements.
That being said, rather than returning the title of the tasks here, if you used this for , I would recommend returning the tasks themselves, using the code you wrote for GET /goals/<goal_id>/tasks
, [task.to_dict() for task in self.tasks]
. Handling this here would have greatly reduced the amount of code needed for your GET /goals/<goal_id>/tasks
route as you could just return goal.to_dict
.
I do recognize that you cannot return the "tasks"
key here as it will cause some failures in the tests you wrote for GET /goals/<goal_id>
that don't expect this key. However, I think that could be better fixed by updating the test or writing the test in a more future-proof way I'll describe in a comment on the test.
if sort_query == "desc": | ||
tasks = Task.query.order_by(Task.title.desc()) | ||
elif sort_query == "asc": | ||
tasks = Task.query.order_by(Task.title.asc()) | ||
elif title_query: | ||
tasks = Task.query.filter_by(title=title_query) | ||
elif description_query: | ||
tasks = Task.query.filter_by(description=description_query) | ||
elif completed_query: | ||
tasks = Task.query.filter_by(completed_at=completed_query) | ||
else: | ||
tasks = Task.query.all() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good extra work on getting these other columns!
tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") | ||
goals_bp = Blueprint("goals", __name__, url_prefix="/goals") | ||
|
||
@goals_bp.route("/<goal_id>/tasks", methods=["POST"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I originally had a comment here that your file seemed to lack organization, but as I've read more, I now get that it's just these two /goals/<goal_id>/tasks
functions that are out-of-place. I would recommend putting these after your /tasks/<task_id>/mark_(in)complete
routes.
A common pattern is to arrange routes by increasing size of endpoint URL then by HTTP verbs, which you do seem to follow below. Moving this to after the task completion routes would maintain that organization, which makes your code more maintainable as there's an obvious pattern methods are organized in.
(Though it can be hard to get a good organization that works, I admit!)
@@ -2,7 +2,7 @@ | |||
import pytest | |||
|
|||
|
|||
@pytest.mark.skip(reason="No way to test this feature yet") | |||
# @pytest.mark.skip(reason="No way to test this feature yet") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor style preference: Just delete these unneeded decorators rather than commenting them out. They are mainly there as scaffolding code, and now that the code works, we can remove them.
assert response_body == { | ||
"goal": { | ||
"id": 1, | ||
"title": "Updated Goal Title" | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, as mentioned in your Goal
model class, this test fails if you fill in the "tasks"
key. Rather than not returning this information, I would recommend updating this test.
A simple way would be to add the "tasks"
key:
assert response_body = {
"goal": {
"id": 1,
"title": "Updated Goal Title",
"tasks": []
}
}
Which is OK in our case. But what would happen if we added a new key? And then another. It could be tiring to constantly update here, especially since the "title"
key is the key we really care about for this test. So we could just check that directly with assert response_body["goal"]["title"] == "Updated Goal Title"
, and then this test will pass no matter what additional keys we add.
However, there is value in not just testing that the "title"
updated but that the other keys did not change. In that case, rather than comparing to a literal here, I would do something like this, where I compare it to the goal as it appears before updating:
get_response = client.get("/goals/1/")
EXPECTED_TITLE = "Updated Goal Title"
expected_response = get_response.get_json()
expected_response["title"] = EXPECTED_TITLE
put_response = client.put("/goals/1/", json={
"title": EXPECTED_TITLE
})
put_response_body = put_response.get_json()
assert put_response.status_code == 200
assert "goal" in put_response_body
assert put_response_body == expected response
goal = Goal.query.get(1)
assert goal.title == EXPECTED_TITLE
But there's an even better way! You'd have to change the one_goal
fixture in conftest.py to return the goal that got created:
@pytest.fixture
def one_goal(app):
new_goal = Goal(title="Build a habit of going outside daily")
db.session.add(new_goal)
db.session.commit()
return new_goal
Then , that new_goal
will be passed in to your test as one_goal
. Which you can then use instead of having to make a call to GET /goals/1
:
EXPECTED_TITLE = "Updated Goal Title"
expected_response = one_goal.to_dict()
expected_response["title"] = EXPECTED_TITLE
response = client.put("/goals/1/", json={
"title": EXPECTED_TITLE
})
response_body = put_response.get_json()
assert response.status_code == 200
assert "goal" in response_body
assert response_body == expected response
goal = Goal.query.get(1)
assert goal.title == EXPECTED_TITLE
Of course we instructors (well, not me personally) wrote the conftest.py file so this isn't really on you.
goal = Goal.query.get(1) | ||
assert goal.title == "Updated Goal Title" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Otherwise, I do like the test design here, especially going to the database to verify that it updated there.
No description provided.