Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from .db import db, migrate
from .models import task, goal
import os
from .routes.task_routes import tasks_bp
from .routes.goal_routes import goals_bp
Comment on lines +5 to +6

Choose a reason for hiding this comment

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

Don't forget that the convention for naming blueprints is to name them bp. With each blueprint being named the same thing we will need to import them under an alias like so:

from .routes.task_routes import bp as tasks_bp


def create_app(config=None):
app = Flask(__name__)
Expand All @@ -18,5 +20,7 @@ def create_app(config=None):
migrate.init_app(app, db)

# Register Blueprints here
app.register_blueprint(tasks_bp)
app.register_blueprint(goals_bp)

return app
24 changes: 23 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .task import Task
from ..db import db
from typing import List

class Goal(db.Model):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
title: Mapped[str]
tasks: Mapped[List["Task"]] = relationship("Task", back_populates="goal")

def to_dict(self):
goal_as_dict = {
"id": self.id,
"title": self.title,
"tasks": [task.to_dict() for task in self.tasks] # Include tasks in the goal's dict

Choose a reason for hiding this comment

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

We have an endpoint that will provide us with the same data that goal_as_dict["tasks"] will give us so we don't need to put it on the dictionary representation of the object. If I client has the information to retrieve a Goal (i.e. goal.id) then they have the information to get the same information from our endpoint.

}
return goal_as_dict

@classmethod
def from_dict(cls, goal_data):
return cls(title=goal_data["title"])

def add_tasks(self, task_ids: List[int]):
tasks = Task.query.filter(Task.id.in_(task_ids)).all()
for task in tasks:
task.goal_id = self.id # Assign each task's goal_id
Comment on lines +23 to +26

Choose a reason for hiding this comment

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

Nice work here, Somy! This is good work! Since we are searching the database we don't have to worry about validating any of the tasks!

db.session.commit()
26 changes: 25 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db import db
from datetime import datetime
from typing import Optional
from sqlalchemy import ForeignKey

class Task(db.Model):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
title: Mapped[str]
description: Mapped[str]
completed_at: Mapped[Optional[datetime]] = mapped_column(nullable=True)

Choose a reason for hiding this comment

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

If we want a column to be nullable then we would want to use Optional. Without Optional we don't need nullable=False.

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

def to_dict(self):
return {
"id": self.id,
"title": self.title,
"description": self.description,
"is_complete": bool(self.completed_at)

Choose a reason for hiding this comment

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

⭐️

}

@classmethod
def from_dict(cls, task_data):
return cls(
title=task_data["title"],
description=task_data["description"],
completed_at=task_data.get("completed_at")
)
120 changes: 119 additions & 1 deletion app/routes/goal_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,119 @@
from flask import Blueprint
from flask import Blueprint, abort, make_response, request, Response
from app.models.goal import Goal
from ..db import db
from datetime import datetime, timezone

goals_bp = Blueprint("goals_bp", __name__, url_prefix="/goals")

##refactor validate!!
@goals_bp.post("/<goal_id>/tasks")
def create_goal_with_task(goal_id):
goal = validate_model(Goal, goal_id)

request_body = request.get_json()
request_body["goal_id"] = goal.id

try:
new_task = Task.from_dict(request_body)
except KeyError as error:
response = {"message": f"Invalid requests: missing {error.ergs[0]}"}
abort(make_response(response, 400))
Comment on lines +16 to +20

Choose a reason for hiding this comment

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

This logic looks very similar to the logic we wrote in our create routes, what refactoring could be done here to move this code out of our routes into a helper function to support maintainability and scalability? (Hint: We did a refactor like this in Flasky).

db.session.add(new_task)
db.session.commit()

return make_response(new_task.to_dict(), 201)

@goals_bp.post("")
def create_goal():
request_body = request.get_json()

# Validate request data
if "title" not in request_body:
response = {"details": "Invalid data"}
return make_response(response, 400)

title = request_body["title"]

# Create new goal instance
new_goal = Goal(title=title)
Comment on lines +28 to +38

Choose a reason for hiding this comment

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

Another place where our refactored mentioned above could be of use.


# Add new goal to the database session and commit
db.session.add(new_goal)
db.session.commit()

response = {
"goal": {

Choose a reason for hiding this comment

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

We essentially just want an outer dictionary structure with a key of the class name, perhaps we could move this into a helper function or add it to a class in order to DRY up some of our code given that we have this same logic throughout the project?

"id": new_goal.id,
"title": new_goal.title,
}
}

return response, 201

@goals_bp.get('/<goal_id>')
def get_one_goal(goal_id):
goal = validate_goal(goal_id)

return {
"goal":{
"id": goal.id,
"title": goal.title,
}
}


@goals_bp.get("")
def get_all_goals():
query = db.select(Goal).order_by(Goal.id)
goals = db.session.scalars(query)

goals_response = []
for goal in goals:
goals_response.append(
{
"id": goal.id,
"title": goal.title,
}
)
return goals_response

@goals_bp.put("/<goal_id>")
def update_goal(goal_id):
goal = validate_goal(goal_id)
request_body = request.get_json()

goal.title = request_body["title"]

Choose a reason for hiding this comment

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

Isn't this code similar to our other .put method? How could we use the hasattr and setattr functions that Python provides in order to make DRY up our routes?


db.session.commit()

return {
"goal": {
"id": goal.id,
"title": goal.title,
}
}, 200

@goals_bp.delete("<goal_id>")
def delete_task(goal_id):
goal = validate_goal(goal_id)
db.session.delete(goal)
db.session.commit()

return {"details":f'Goal {goal_id} "{goal.title}" successfully deleted'},200
# Response(status=204, mimetype="application/json")

def validate_goal(goal_id):
try:
goal_id = int(goal_id)
except ValueError:
response = {"message": f"goal {goal_id} invalid"}
abort(make_response(response,400))

query = db.select(Goal).where(Goal.id == goal_id)
goal = db.session.scalar(query)

if not goal:
response = {"message": f"goal {goal_id} not found"}
abort(make_response(response, 404))

return goal
Comment on lines +105 to +119

Choose a reason for hiding this comment

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

I see we have a validate_model function in our utilities file. I am assuming you meant to replace this with that function instead?

19 changes: 19 additions & 0 deletions app/routes/route_utilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from flask import abort, make_response
from ..db import db

def validate_model(cls, model_id):
try:
model_id = int(model_id)
except:
response = {"message": f"{cls.__name__} {model_id} invalid"}
abort(make_response(response , 400))

query = db.select(cls).where(cls.id == model_id)
model = db.session.scalar(query)

if not model:
response = {"message": f"{cls.__name__} {model_id} not found"}
abort(make_response(response, 404))

return model

174 changes: 173 additions & 1 deletion app/routes/task_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,173 @@
from flask import Blueprint
from flask import Blueprint, abort, make_response, request, Response
from app.models.task import Task
from ..db import db
from datetime import datetime, timezone

tasks_bp = Blueprint("tasks_bp", __name__, url_prefix="/tasks")


@tasks_bp.post("")
def create_task():
request_body = request.get_json()

#validate request data
if "title" not in request_body or "description" not in request_body:
response = {"details": "Invalid data"}
return make_response(response,400)

title = request_body["title"]
description = request_body["description"]
completed_at = request_body.get("completed_at")

new_task = Task(title=title, description=description, completed_at=completed_at)
db.session.add(new_task)
db.session.commit()

response = {
"task":{
"id": new_task.id,
"title": new_task.title,
"description": new_task.description,
"is_complete": bool(new_task.completed_at)
}
}
return response, 201

@tasks_bp.get('/<task_id>')
def get_one_task(task_id):
task = validate_task(task_id)

return {
"task":{
"id": task.id,
"title": task.title,
"description": task.description,
"is_complete": bool(task.completed_at)
}
}

@tasks_bp.get("")
def get_all_tasks():

sort_param = request.args.get("sort", "").lower()

if sort_param and sort_param.lower() == "asc":
query = db.select(Task).order_by(Task.title)

elif sort_param and sort_param.lower() == "desc":
query = db.select(Task).order_by(Task.title.desc())
else:
query = db.select(Task).order_by(Task.title)

tasks = db.session.scalars(query)
Comment on lines +52 to +62

Choose a reason for hiding this comment

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

Remember in class we wrote a function called get_model_with_filters though the implementation we had doesn't work in this project, the logic could be modified used here so we could DRY up our code when getting multiple records of a table. IT would also expand our functionality beyond what's required our the README.



tasks_response = []
for task in tasks:
tasks_response.append(
{
"id": task.id,
"title": task.title,
"description": task.description,
"is_complete": bool(task.completed_at)

Choose a reason for hiding this comment

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

}
)
return tasks_response

@tasks_bp.put("/<task_id>")
def update_task(task_id):
task = validate_task(task_id)
request_body = request.get_json()

# Validate request data
if "title" not in request_body or "description" not in request_body:
return make_response({"details": "Invalid data"}, 400)

task.title = request_body["title"]
task.description = request_body["description"]

# Keep completed_at unchanged if not provided
if "completed_at" in request_body:
task.completed_at = request_body["completed_at"]

db.session.commit()

return {
"task": {
"id": task.id,
"title": task.title,
"description": task.description,
"is_complete": bool(task.completed_at) # Convert completed_at to boolean
}
}, 200
Comment on lines +95 to +102

Choose a reason for hiding this comment

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

Is there a reason we don't use our to_dict method here?


@tasks_bp.delete("<task_id>")
def delete_task(task_id):
task = validate_task(task_id)
db.session.delete(task)
db.session.commit()

return {"details":f'Task {task_id} "{task.title}" successfully deleted'},200
# Response(status=204, mimetype="application/json")

def validate_task(task_id):
try:
task_id = int(task_id)
except ValueError:
response = {"message": f"task {task_id} invalid"}
abort(make_response(response,400))

query = db.select(Task).where(Task.id == task_id)
task = db.session.scalar(query)

if not task:
response = {"message": f"task {task_id} not found"}
abort(make_response(response, 404))

return task

@tasks_bp.patch("/<task_id>/mark_complete")
def mark_complete(task_id):
task = validate_task(task_id)

# Update completed_at to mark the task as completed
task.completed_at = datetime.now(timezone.utc)

# Commit the changes to the database
db.session.commit()

# Prepare the response
response = {
"task": {
"id": task.id,
"title": task.title,
"description": task.description,
"is_complete": bool(task.completed_at)
}
}
return response, 200
Comment on lines +129 to +148

Choose a reason for hiding this comment

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

Looks like you didn't do the slack implementation for this route.


@tasks_bp.patch("/<task_id>/mark_incomplete")
def mark_incomplete(task_id):
task = validate_task(task_id)

# Update completed_at to mark the task as completed
task.completed_at = None

# Commit the changes to the database
db.session.commit()

# Prepare the response
response = {
"task": {
"id": task.id,
"title": task.title,
"description": task.description,
"is_complete": bool(task.completed_at)
}
}
return response, 200




1 change: 1 addition & 0 deletions migrations/README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Single-database configuration for Flask.
Loading