-
Notifications
You must be signed in to change notification settings - Fork 97
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
Spruce - Sarah Olivas #82
base: master
Are you sure you want to change the base?
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.
Great job!
All your tests are passing and your code is very readable.
One main thing I'd focus on going forward is identifying repetitive parts of the code and moving them to helper methods or class instance methods. We can create our own additional classes to help us organize common code and represent reusable ideas. Especially for CRUD models, there is a lot of very similar code. Even if it's not exactly the same, thinking about how to move that similar code into shared routines can help up organize and test our code. Our testing in this project focused on testing through the flask client helper, but we can write smaller unit tests similar to the tests we used in Video Party and Swap Meet, which are easier to write when the logic is pulled out into small units of code that focus on single responsibilities.
Specifically with respect to writing web apis with Flask and SqlAlchemy, keep looking at other examples and looking through the documentation for other ideas about helper methods and implementation patterns. Using decorator methods can also be a useful strategy for common tasks if we split our routes into separate methods.
Overall, you did really well, and the rest will come with continued practice and experimentation. Keep it up!!! 🎉
@@ -15,12 +15,10 @@ def create_app(test_config=None): | |||
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False | |||
|
|||
if test_config is None: | |||
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get( | |||
"SQLALCHEMY_DATABASE_URI") | |||
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("SQLALCHEMY_DATABASE_URI") |
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'm curious what prompted unwrapping these lines. Doing so exceeds the typical 80 column limit. Are you running a formatting tool?
from .routes import tasks_bp | ||
app.register_blueprint(tasks_bp) | ||
|
||
from .routes import goals_bp | ||
app.register_blueprint(goals_bp) |
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.
👍
One thing we can do to help our routes file from getting too large is to consider making multiple files containing routes separated by the resource type. We might have a routes
folder instead of a routes.py
file, and inside that folder (along with a __init__.py
) we could have a file per resource, so task.py
and goal.py
. Where each would have a blueprint and endpoints for that resource. When we have one blueprint per file, we often name the blueprint simply bp
rather than including the resource name as part of it.
Then here, we could import and register the blueprints like
from .routes import task, goal
app.register_blueprint(task.bp)
app.register_blueprint(goal.bp)
class Goal(db.Model): | ||
goal_id = db.Column(db.Integer, primary_key=True) | ||
goal_id = db.Column(db.Integer, primary_key=True, autoincrement = True) |
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.
According to the SqlAlchemy docs about autoincrement:
The default value is the string "auto" which indicates that a single-column primary key that is of an INTEGER type with no stated client-side or python-side defaults should receive auto increment semantics automatically; all other varieties of primary key columns will not.
This means that here, since the primary is not a compound key, the type is integer, and there's no specified default value, the desired autoincrement behavior will apply even without listing it. It doesn't hurt anything to list it, but it's not required.
class Goal(db.Model): | ||
goal_id = db.Column(db.Integer, primary_key=True) | ||
goal_id = db.Column(db.Integer, primary_key=True, autoincrement = True) | ||
title = db.Column(db.String) |
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.
Should we be able to create a goal with a NULL title? Consider adding nullable=False
here.
def goal_and_task_dict(self): | ||
return{ | ||
"id": self.goal_id, | ||
"title": self.title, | ||
"tasks": [task.task_and_goal_dict() for task in self.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.
👍
Nice job having two different methods to provide the two different needed output styles. We could have used the goal_dict
version to get the base values, then add in tasks
, but given the size this seems ok.
Notice that as applications grow, we may end up with a large variety of endpoints that may need slightly different outputs. In that situation, rather than adding method after method to model class itself, we often will make helper classes whose sole responsibility is knowing how to render a model instance into a certain output configuration. Sometimes we call these helper classes data transfer objects (DTOs).
|
||
|
||
@goals_bp.route("", methods=["GET", "POST"]) | ||
def handle_goals(): |
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.
Similar feedback about splitting these functions, and moving validation and dictionary-handling logic around that I made for Task
will also apply to Goal
. These are common themes for any model with CRUD operations.
for task_id in request_body["task_ids"]: | ||
task = Task.query.get(task_id) | ||
if task is None: | ||
return jsonify(None), 404 | ||
goal.tasks.append(task) | ||
db.session.commit() |
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 job checking whether the each requested task id lookup is successful before trying to append it to the task list. However, you may want to wait to perform the appending and commiting until after you have validated all the tasks, rather than task by task. With the task-by-task approach, we could end up partially modifying the tasks in a goal, but then fail partway through the list, leaving the goal partially modified. It's often desirable to ensure that either the entire change has been applied, or none of the change has.
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.
Appending to the tasks realtionship is absolutely a valid way to associate a task with a goal. Notice that it's not entirely clear from the project description or tests whether POSTing to the task list is adding tasks to the goal (in addition to anything that was already there), or replacing the tasks in the goal. Which perspective does your implementation take? How could we implement the other approach?
@@ -0,0 +1,39 @@ | |||
"""empty message |
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.
Don't forget to add a message to your migrations with -m
when running the migration step.
@@ -41,18 +42,17 @@ def test_get_goal(client, one_goal): | |||
} | |||
} | |||
|
|||
@pytest.mark.skip(reason="test to be completed by student") | |||
# @pytest.mark.skip(reason="test to be completed by student") | |||
def test_get_goal_not_found(client): | |||
pass |
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.
We should remove the pass
statement after supplying the test body.
# ---- Complete Assertions Here ---- | ||
assert response.status_code == 404 | ||
assert response_body == None | ||
assert Goal.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.
An alternative to directly accessing the DB here would be to make an additional GET request. Doing so keeps us at the same level of abstraction (exercising the web API) while still checking that a change has been made (deletion of the goal) without assuming how the deletion is occuring (maybe we want to use a deleted flag rather than actually deleting the row).
No description provided.