Conversation
There was a problem hiding this comment.
Nice work overall! Please review the feedback and questions in the comments when you have time. Let me know here or on Slack if you have questions or there's anything I can clarify. =] There are some areas I would like you to look at, I'll leave specific info on those in Learn.
app/__init__.py
Outdated
| from .routes.task_routes import tasks_bp | ||
| from .routes.goal_routes import goals_bp |
There was a problem hiding this comment.
Did you run into an issue with putting the imports at the top of the file? Unless there is a specific need due to the project structure or requirements, our imports should always go at the top of the file.
from flask import Flask
from .db import db, migrate
from .models import task, goal
from .routes.task_routes import tasks_bp
from .routes.goal_routes import goals_bp
import os
app/__init__.py
Outdated
| from .routes.task_routes import tasks_bp | ||
| from .routes.goal_routes import goals_bp |
There was a problem hiding this comment.
To be more consistent with Flask and API patterns, we should name our blueprints bp, and in other files like __init__.py here we should import them with more specific names if necessary.
| @@ -1,5 +1,35 @@ | |||
| from sqlalchemy.orm import Mapped, mapped_column | |||
| from sqlalchemy.orm import Mapped, mapped_column, relationship | |||
| from sqlalchemy import String | |||
There was a problem hiding this comment.
I'm not seeing where this import is used. If an import is not required, we should remove it so that our files are not loading up extra code into our execution environment that will never be used. This makes our programs larger and run slower without any benefit.
This feedback applies to the Task model as well.
There was a problem hiding this comment.
If it was removed, the change to this line was not pushed up to the repo.
| id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) | ||
| title: Mapped[str] = mapped_column(String(100), nullable=False) | ||
|
|
||
| tasks = relationship("Task", back_populates="goal", cascade="all, delete-orphan") |
There was a problem hiding this comment.
Nice use of cascade for deleting child objects. From a design standpoint, is there ever a situation where you might want to delete a goal but keep the tasks? We could choose to disconnect the goal from the tasks and then delete the goal if we didn't necessarily want to delete all related tasks.
There was a problem hiding this comment.
Ah, yeah. you could do something like
tasks = relationship("Tasl", back_populates="goal",
cascade="save-update, merge, refresh-expire")
app/models/goal.py
Outdated
|
|
||
| class Goal(db.Model): | ||
| id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) | ||
| title: Mapped[str] = mapped_column(String(100), nullable=False) |
There was a problem hiding this comment.
nullable=False is the default value for an attribute in SQLAlchemy. It is more common to leave off that section if we are not changing the default behavior. This feedback applies to attributes on the Task model as well.
app/routes/goal_routes.py
Outdated
| for task_id in task_ids: | ||
| task = db.session.scalar(db.select(Task).where(Task.id == task_id)) | ||
| if task: | ||
| goal.tasks.append(task) |
There was a problem hiding this comment.
It is functionally about the same, but if we had a generic validate_model function, another approach could be to create our list of validated tasks, and then assign it to goal.tasks:
task_ids = request_body["task_ids"]
valid_tasks = [validate_model(Task, task_id) for task_id in task_ids]
goal.tasks = valid_tasks
app/routes/task_routes.py
Outdated
| def get_task(task_id): | ||
| task = get_task_by_id(task_id) | ||
|
|
||
| if not task: | ||
| return make_response(jsonify({"error": "Task not found"}), 404) | ||
|
|
||
| return jsonify({"task": task.to_dict(include_goal_id=True)}) |
There was a problem hiding this comment.
How does a user know if they typed the id in wrong or if the task actually doesn't exist? I recommend reflecting on the patterns we used in Learn and in-class projects for error handling and sharing information with users to create helpful and descriptive errors.
app/routes/task_routes.py
Outdated
| task = get_task_by_id(task_id) | ||
|
|
||
| if not task: | ||
| return make_response(jsonify({"error": "Task not found"}), 404) |
There was a problem hiding this comment.
This code is repeated exactly across several task routes, what could it look like share this code between the routes, allowing the file to better follow D.R.Y. principles?
app/routes/task_routes.py
Outdated
| # Optional: Log the Slack API response for debugging | ||
| print(f"Slack API Response: {slack_response.status_code}, {slack_response.text}") |
There was a problem hiding this comment.
Reviewing our project for commented code & debugging code left behind should be one of our final steps in completing a coding task. Debugging code should be removed prior to opening PRs so it does not get added to the production code base.
app/routes/task_routes.py
Outdated
| # Send a notification to Slack | ||
| slack_token = os.environ.get('SLACK_BOT_TOKEN') | ||
| if slack_token: | ||
| slack_data = { | ||
| 'channel': 'task-notifications', | ||
| 'text': f"Someone just completed the task {task.title}" | ||
| } | ||
| headers = { | ||
| 'Authorization': f'Bearer {slack_token}', | ||
| 'Content-Type': 'application/json' | ||
| } | ||
|
|
||
| # Make the POST request to Slack API | ||
| slack_response = requests.post( | ||
| 'https://slack.com/api/chat.postMessage', | ||
| json=slack_data, | ||
| headers=headers | ||
| ) |
There was a problem hiding this comment.
Thinking about separation of concerns, what responsibilities does this function have? I see it doing 2 things, both updating the database record, and sending a Slack message.
If we look at this as a growing project like we would be working on in the industry, what happens if we wanted to send a slack message when we call mark_task_incomplete? Right now, our only code to send a Slack message is deeply tied to marking a task complete, we cannot do one without the other.
How could we make our code more flexible so that our ability to send a message could be shared with other routes?
There was a problem hiding this comment.
I'll refactor my code to create a module that handles notifications
kelsey-steven-ada
left a comment
There was a problem hiding this comment.
It looks like some of the items I listed out in Learn as necessary to address have not been completed or could use further work. Please take a look at the original list and ensure all feedback from learn has been applied to both Goals and Tasks models and routes. I've added some new feedback around these items that are still in progress.
| @@ -1,22 +1,22 @@ | |||
| from flask import Flask | |||
| from .db import db, migrate | |||
| from .models import task, goal | |||
There was a problem hiding this comment.
Even though we don't use them in the function, we still want to import the models into __init__.py to make the migration system aware of the model classes. If we were to make changes to the model columns now and then try to migrate, nothing would happen because the Flask application cannot see the models.
app/__init__.py
Outdated
| from .routes.task_routes import bp as tasks_bp | ||
| from .routes.goal_routes import bp as goals_bp |
There was a problem hiding this comment.
These imports should be at the top of the file for consistent layout and understanding of what dependencies a file is bringing in. We should not import things inside functions unless there is a reason doing so is absolutely necessary.
| @@ -1,5 +1,35 @@ | |||
| from sqlalchemy.orm import Mapped, mapped_column | |||
| from sqlalchemy.orm import Mapped, mapped_column, relationship | |||
| from sqlalchemy import String | |||
There was a problem hiding this comment.
If it was removed, the change to this line was not pushed up to the repo.
app/models/task.py
Outdated
| completed_at: Mapped[Optional[datetime]] = mapped_column(nullable=True) | ||
|
|
||
| goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id"), nullable=True) |
There was a problem hiding this comment.
Since these two attributes, completed_at and goal_id are already using Optional to declare that they could be null, we should not also use the mapped_column(nullable=True) syntax because this duplicates effort. We should choose only one style of syntax for declaring something is optional and use it consistently across the project.
app/routes/goal_routes.py
Outdated
| return "", 204 | ||
|
|
||
| @bp.route("/<goal_id>", methods=["DELETE"]) | ||
| def delete_goal(goal_id): | ||
| goal = get_goal_by_id(goal_id) | ||
|
|
||
| if not goal: | ||
| return {"error": "Goal not found"}, 404 | ||
|
|
||
| db.session.delete(goal) | ||
| db.session.commit() | ||
|
|
||
| return "", 204 |
There was a problem hiding this comment.
I would like you to revisit the UPDATE and DELETE routes for Goals and Tasks. This passes the test suite since those tests check the status code is 204 and whether the update/delete took place in the database. However, it is not following the requirements for the UPDATE and DELETE routes. These should all:
- return
204 No Content, which means they should have no body at all. Currently an empty string is being sent. - have a mimetype of "application/json" to keep our response type consistent across the whole API
Please review the mechanisms we used in hello-books in Learn and in Flasky in-class for returning an empty body with "application/json" response type and update the UPDATE and DELETE routes for Goals and Tasks accordingly. If you have questions or would like to chat about the syntax, please reach out!
app/routes/goal_routes.py
Outdated
| goal = get_goal_by_id(goal_id) | ||
|
|
||
| if not goal: | ||
| return {"error": "Goal not found"}, 404 |
There was a problem hiding this comment.
From the prior feedback, I would like you to address error handling across all routes to use Flask's abort rather than returning directly. This is an old answer, but this stack overflow talks at a high level how these are different and might give you a place to start if you want to dig further into why we want to use abort: https://stackoverflow.com/questions/47288166/difference-between-flask-abort-or-returning-a-status
|
|
||
| def get_task_by_id(id): | ||
| query = db.select(Task).where(Task.id == id) | ||
| return db.session.scalar(query) |
There was a problem hiding this comment.
Nice updates to return the JSON and let the route handle adding the default 200 status code.
kelsey-steven-ada
left a comment
There was a problem hiding this comment.
Updates look good, nice work addressing the Model attributes and route return values!
| app.register_blueprint(tasks_bp) | ||
| app.register_blueprint(goals_bp) | ||
|
|
||
| @app.errorhandler(404) |
There was a problem hiding this comment.
Neat research into error handling to better control the user error messages!
No description provided.