Skip to content
5 changes: 5 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from flask import Flask
from .db import db, migrate
from .models import task, goal
from app.routes.task_routes import tasks_bp
from app.routes.goal_routes import goals_bp
import os


def create_app(config=None):
app = Flask(__name__)

Expand All @@ -18,5 +21,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
20 changes: 19 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db import db

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,
}

if self.tasks:
goal_as_dict = {
"id": self.id,
"title": self.title,
"tasks": [task.id for task in self.tasks]
}

return goal_as_dict
31 changes: 30 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import ForeignKey
from datetime import datetime
from typing import Optional
from ..db import db

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]]

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

goal: Mapped[Optional["Goal"]] = relationship("Goal", back_populates="tasks")

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

if self.goal:
task_as_dict ={
"id": self.id,
"title": self.title,
"description": self.description,
"is_complete": bool(self.completed_at),
"goal_id": self.goal_id # just for wave 6
Copy link
Collaborator

Choose a reason for hiding this comment

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

We create the task dictionary and then we return it, could we add a step in between creation and returning where we conditionally add the goal_id key only if a goal_id actually exists? What could that look like?

}

return task_as_dict
114 changes: 113 additions & 1 deletion app/routes/goal_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,113 @@
from flask import Blueprint
from flask import Blueprint, request, make_response, abort, jsonify
from app.models.goal import Goal
from app.routes.task_routes import validate_task
from ..db import db

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

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

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

title = request_body["title"]

new_goal= Goal(title=title)
db.session.add(new_goal)
db.session.commit()

return {"goal": new_goal.to_dict()}, 201

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

goals_response = []
goals_response = [goal.to_dict() for goal in goals]
return goals_response

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

return {
"goal":goal.to_dict()
}, 200

def validate_goal(goal_id):
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is very similar to our validate_task function, how could we refactor these functions to reduce repetition?

Copy link
Author

Choose a reason for hiding this comment

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

I know I can create validate_models in bass.py then impor it in goal_routes and task_routes!

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

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

goal.title = request_body.get("title", goal.title)
db.session.commit()

return {
"goal":goal.to_dict()
}, 200

@goals_bp.delete("/<goal_id>")
def delete_goal(goal_id):
goal = validate_goal(goal_id)

db.session.delete(goal)
db.session.commit()

return jsonify({
"details": f'Goal {goal.id} "{goal.title}" successfully deleted'
}), 200

@goals_bp.post("/<goal_id>/tasks")
def add_tasks_to_goal(goal_id):
goal=validate_goal(goal_id)
request_body=request.get_json()

task_list= request_body["task_ids"]

for task in task_list:
task = validate_task(task)
task.goal_id = goal_id
db.session.commit()

task_ids =[]
for task in goal.tasks:
task_ids.append(task.id)

response = {
"id": goal.id,
"task_ids": task_ids
}
return response, 200

@goals_bp.get("/<goal_id>/tasks")
def get_tasks_for_goal(goal_id):
goal = validate_goal(goal_id)
response = {
"id": goal.id,
"title": goal.title,
"tasks":[task.to_dict() for task in goal.tasks]
}
return response




177 changes: 176 additions & 1 deletion app/routes/task_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,176 @@
from flask import Blueprint
from flask import Blueprint, abort, make_response, request, Response, jsonify
Copy link
Collaborator

Choose a reason for hiding this comment

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

jsonify is not required to convert lists and dictionaries to JSON in more recent versions of Flask. I suggest removing any instances of it across the project. Unless it is necessary for the response, we should not use it since it's one more statement we need to consider and maintain over time which isn't changing the flow of our code.

from app.models.task import Task
from sqlalchemy import desc
from datetime import datetime
from ..db import db
import os
import requests

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

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

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


Comment on lines +22 to +23
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would remove one of these blank lines; two blank lines is a convention used to separate top level functions, so many readers would see that spacing, assume the function is ending, and have to do some extra thought about what the code is actually doing.

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)
}
}
Comment on lines +28 to +35
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since we have a to_dict function for Tasks, we should use it across the file when we send back a task record to reduce repeated code.

response = {"task": new_task.to_dict()}


return response, 201

@tasks_bp.get("")
def get_all_tasks():
query = db.select(Task)

title_param = request.args.get("title")
if title_param:
query = query.where(Task.title.ilike(f"%{title_param}%"))

description_param = request.args.get("description")
if description_param:
query = query.where(Task.description.ilike(f"%{description_param}%"))

completed_at_param = request.args.get("completed_at")
if completed_at_param == "true":
query = query.where(Task.completed_at.isnot(None))
elif completed_at_param == "false":
query = query.where(Task.completed_at.is_(None))

sort_param = request.args.get("sort")
if sort_param == "asc":
query = query.order_by(Task.title)
if sort_param == "desc":
query = query.order_by(desc(Task.title))
else:
query = query.order_by(Task.id)

tasks = db.session.scalars(query)

tasks_response = []
for task in tasks:
tasks_response.append({
"id": task.id,
"title": task.title,
"description": task.description,
"is_complete": bool(task.completed_at)
})
Comment on lines +67 to +74
Copy link
Collaborator

Choose a reason for hiding this comment

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

This would be a great place to use a list comprehension in conjunction with our Task to_dict function:

tasks_response = [task.to_dict() for task in tasks]


return tasks_response, 200

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

return {
"task":task.to_dict()
}, 200

def validate_task(task_id):
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would consider moving this to the bottom of the file as a helper function that's used by multiple routes, it's a little out of place in between a couple routes.

try:
task_id = int(task_id)
except:
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.put("/<task_id>")
def update_task(task_id):
task = validate_task(task_id)
request_body = request.get_json()

task.title = request_body.get("title", task.title)
task.description = request_body.get("description", task.description)
task.completed_at = request_body.get("completed_at", task.completed_at)
Comment on lines +106 to +108
Copy link
Collaborator

Choose a reason for hiding this comment

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

Neat use of the original values as defaults if a value is missing.

db.session.commit()

return {
"task":task.to_dict()
}, 200

@tasks_bp.delete("/<task_id>")
def delete_task(task_id):
task = validate_task(task_id)
if not task:
return jsonify({"message": f"task {task_id} not found"}), 404
Comment on lines +118 to +119
Copy link
Collaborator

Choose a reason for hiding this comment

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

We make this same check inside the validate_task function. Since we're duplicating work, we can remove these 2 lines without changing the flow of our code. This feedback applies to the function below as well.

db.session.delete(task)
db.session.commit()

return jsonify({
"details": f'Task {task.id} "{task.title}" successfully deleted'
}), 200


def mark_task_complete(task_id):
task = validate_task(task_id)
if not task:
return abort(404, description="Task not found")

task.completed_at = datetime.now()
db.session.commit()

return {
"task":task.to_dict()
}, 200

@tasks_bp.patch("/<task_id>/mark_complete")
def patch_complete(task_id):
task = validate_task(task_id)
task.completed_at = datetime.now().isoformat()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Were you running into issues with the datetime data? completed_at is an optional datetime attribute, there should be no need to cast the datetime you get from datetime.now() to a string with isoformat before saving the record.

db.session.commit()

url = "https://slack.com/api/chat.postMessage"
headers = {
"Authorization": f"Bearer {os.environ.get('SLACK_API_KEY')}",
"Content-Type": "application/json"
}
data = {
"channel" : "#api-test-channel",
"text": f"Someone just completed the task {task.title}"
}

response = requests.post(url, headers=headers, json=data)

print("Slack API Response:", response.status_code, response.json())

response_data = {"task": task.to_dict()}

return response_data, 200

@tasks_bp.patch("/<task_id>/mark_incomplete")
def mark_task_incomplete(task_id):
task = validate_task(task_id)
if not task:
return abort(404, description="Task not found")

task.completed_at = None
db.session.commit()

return {
"task":task.to_dict()
}, 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