Skip to content

Pull Request from Yasiel#25

Open
YasielLopez wants to merge 11 commits intoAda-C23:mainfrom
YasielLopez:main
Open

Pull Request from Yasiel#25
YasielLopez wants to merge 11 commits intoAda-C23:mainfrom
YasielLopez:main

Conversation

@YasielLopez
Copy link

No description provided.

Copy link

@kelsey-steven-ada kelsey-steven-ada left a comment

Choose a reason for hiding this comment

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

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
Comment on lines 18 to 19
from .routes.task_routes import tasks_bp
from .routes.goal_routes import goals_bp

Choose a reason for hiding this comment

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

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
Comment on lines 18 to 19
from .routes.task_routes import tasks_bp
from .routes.goal_routes import goals_bp

Choose a reason for hiding this comment

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

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

Choose a reason for hiding this comment

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

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.

Copy link
Author

Choose a reason for hiding this comment

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

Removed.

Choose a reason for hiding this comment

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

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")

Choose a reason for hiding this comment

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

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.

Copy link
Author

Choose a reason for hiding this comment

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

Ah, yeah. you could do something like

tasks = relationship("Tasl", back_populates="goal",
cascade="save-update, merge, refresh-expire")

Copy link
Author

Choose a reason for hiding this comment

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

*Task


class Goal(db.Model):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
title: Mapped[str] = mapped_column(String(100), nullable=False)

Choose a reason for hiding this comment

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

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.

Comment on lines 92 to 95
for task_id in task_ids:
task = db.session.scalar(db.select(Task).where(Task.id == task_id))
if task:
goal.tasks.append(task)

Choose a reason for hiding this comment

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

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

Comment on lines 44 to 50
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)})

Choose a reason for hiding this comment

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

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.

Comment on lines 70 to 73
task = get_task_by_id(task_id)

if not task:
return make_response(jsonify({"error": "Task not found"}), 404)

Choose a reason for hiding this comment

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

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?

Comment on lines 109 to 110
# Optional: Log the Slack API response for debugging
print(f"Slack API Response: {slack_response.status_code}, {slack_response.text}")

Choose a reason for hiding this comment

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

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.

Comment on lines 90 to 107
# 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
)

Choose a reason for hiding this comment

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

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?

Copy link
Author

Choose a reason for hiding this comment

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

I'll refactor my code to create a module that handles notifications

Copy link

@kelsey-steven-ada kelsey-steven-ada left a comment

Choose a reason for hiding this comment

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

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

Choose a reason for hiding this comment

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

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
Comment on lines 16 to 17
from .routes.task_routes import bp as tasks_bp
from .routes.goal_routes import bp as goals_bp

Choose a reason for hiding this comment

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

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

Choose a reason for hiding this comment

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

If it was removed, the change to this line was not pushed up to the repo.

Comment on lines 11 to 13
completed_at: Mapped[Optional[datetime]] = mapped_column(nullable=True)

goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id"), nullable=True)

Choose a reason for hiding this comment

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

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.

Comment on lines 57 to 69
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

Choose a reason for hiding this comment

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

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!

goal = get_goal_by_id(goal_id)

if not goal:
return {"error": "Goal not found"}, 404

Choose a reason for hiding this comment

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

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)

Choose a reason for hiding this comment

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

Nice updates to return the JSON and let the route handle adding the default 200 status code.

Copy link

@kelsey-steven-ada kelsey-steven-ada left a comment

Choose a reason for hiding this comment

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

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)

Choose a reason for hiding this comment

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

Neat research into error handling to better control the user error messages!

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