Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ad54db9
done with setup
xiwu5 Nov 3, 2025
eed2d44
Wave01: Setup Task Class
xiwu5 Nov 3, 2025
98ff737
finish 2 model helper methods and pass tests
xiwu5 Nov 3, 2025
9a6a4cc
Wave 01: DROP/CREATE DATABASE and flask db upgrade. Pass tests for cr…
xiwu5 Nov 3, 2025
8a113d6
Wave01:Complete test test_get_task_not_found() with assertion about r…
xiwu5 Nov 3, 2025
ac5a511
Wave01: pass update_task() tests
xiwu5 Nov 3, 2025
c682185
Wave01: pass delete_task() tests
xiwu5 Nov 3, 2025
78f6c63
Wave01 Done by fixing response message in create_task()
xiwu5 Nov 3, 2025
1c813c8
Wave02 done:add on sort_param
xiwu5 Nov 5, 2025
cedc9af
Wave03:endpoint "/<task_id>/mark_complete" done
xiwu5 Nov 5, 2025
758f699
Wave03:endpoint "/<task_id>/mark_incomplete" done!
xiwu5 Nov 5, 2025
d98c8f1
Wave03:Tests passed!! Edit assertions in tests for status 404
xiwu5 Nov 5, 2025
1a4723a
prepare refactoring by create create_model() under route_utilities.py
xiwu5 Nov 5, 2025
808a1cb
add on get_models_with_filters()
xiwu5 Nov 5, 2025
c72b42a
DRY a tiny bit with 2 helpers
xiwu5 Nov 5, 2025
817f3a3
edit response message of create_model()
xiwu5 Nov 5, 2025
9866cb2
modify endpoint"task_id/mark_complete" to make a call to Slack API.
xiwu5 Nov 5, 2025
6835b5a
Wave05: update Goal model with attributes and to_dict()/from_dict() …
xiwu5 Nov 6, 2025
cac21bd
Wave05:update methods in goal_routes.py to pass partial tests
xiwu5 Nov 6, 2025
7175a8b
Wave05:modify assertions under def test_get_goal_not_found()
xiwu5 Nov 6, 2025
a2df0e8
Wave05: update update_goal() under goal_routes.py
xiwu5 Nov 6, 2025
bd6d72e
Wave05: update def test_update_goal_not_found() in tests
xiwu5 Nov 6, 2025
322e95d
Wave05: Done by finish delete_goal()
xiwu5 Nov 6, 2025
6d864f3
Wave06:post "/<goal_id>/tasks" endpoint tests passed
xiwu5 Nov 6, 2025
51fd19b
Wave06: modify assertion in test
xiwu5 Nov 6, 2025
f1a8366
Wave06: Done by add goal_id to Task.to_dict()
xiwu5 Nov 6, 2025
df3d63c
Wave07:modify assertion in test
xiwu5 Nov 6, 2025
38b7ccd
Wave07: modify assertion for task_missing_id Case
xiwu5 Nov 6, 2025
2d970ed
Wave07: modify assertion for goal_invalid_id Case
xiwu5 Nov 6, 2025
c7d91f4
Wave07: modify assertion for goal_missing_id Case
xiwu5 Nov 6, 2025
fdc9f98
Wave07 done!
xiwu5 Nov 6, 2025
0cd4274
flask migrate and update
xiwu5 Nov 6, 2025
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
6 changes: 4 additions & 2 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
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

def create_app(config=None):
Expand All @@ -17,6 +19,6 @@ def create_app(config=None):
db.init_app(app)
migrate.init_app(app, db)

# Register Blueprints here

app.register_blueprint(tasks_bp)
app.register_blueprint(goals_bp)
return app
17 changes: 16 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
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(back_populates="goal")

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

def to_dict(self):
goal_as_dict = {}
goal_as_dict["id"] = self.id
goal_as_dict["title"] = self.title

return goal_as_dict

40 changes: 39 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import DateTime, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
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]] = mapped_column(
DateTime,
nullable=True,
default=None
)
goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id"), nullable=True)
goal: Mapped[Optional["Goal"]] = relationship("Goal", back_populates="tasks")

@classmethod
def from_dict(cls, task_data):
new_task = Task(title=task_data["title"],
description=task_data["description"])
return new_task

def to_dict(self):
task_as_dict = {}
task_as_dict["id"] = self.id
task_as_dict["title"] = self.title
task_as_dict["description"] = self.description
task_as_dict["is_complete"] = self.completed_at is not None

if getattr(self, "goal_id", None) is not None:
task_as_dict["goal_id"] = self.goal_id

return task_as_dict

@classmethod
def get_by_id(cls, task_id):
task = db.session.get(cls, task_id)

if task is None:
return {"message": f"Task with ID {task_id} not found"}, 404
return task, 200
83 changes: 82 additions & 1 deletion app/routes/goal_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,82 @@
from flask import Blueprint
from flask import Blueprint, request
from app.routes.route_utilities import create_model, get_models_with_filters, validate_model, create_no_content_response
from ..db import db
from app.models.goal import Goal
from app.models.task import Task


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

@goals_bp.post("")
def create_goal():
request_body = request.get_json()
return create_model(Goal, request_body)

@goals_bp.get("")
def get_all_goals():
title_param = request.args.get("title")
filters = {}
if title_param:
filters["title"] = title_param

goals_response = get_models_with_filters(Goal, filters)
return goals_response

@goals_bp.get("/<goal_id>")
def get_one_goal(goal_id):
goal = validate_model(Goal, goal_id)
return goal.to_dict()

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

goal.title = request_body["title"]
db.session.commit()

return goal.to_dict()

@goals_bp.post("/<goal_id>/tasks")
def post_tasks_to_goal(goal_id):
goal = validate_model(Goal, goal_id)
request_body = request.get_json()
task_ids = request_body.get("task_ids", [])

current_tasks = db.session.scalars(db.select(Task).where(Task.goal_id == goal.id)).all()
for task in current_tasks:
if task.id not in task_ids:
task.goal_id = None

if task_ids:
tasks = db.session.scalars(db.select(Task).where(Task.id.in_(task_ids))).all()
for task in tasks:
task.goal_id = goal.id

db.session.commit()

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


@goals_bp.get("/<goal_id>/tasks")
def get_tasks_for_goal(goal_id):
goal = validate_model(Goal, goal_id)

tasks = db.session.scalars(db.select(Task).where(Task.goal_id == goal.id))
tasks_list = []
for task in tasks:
task_dict = task.to_dict()
task_dict["goal_id"] = task.goal_id
tasks_list.append(task_dict)

response = goal.to_dict()
response["tasks"] = tasks_list
return response

@goals_bp.delete("/<goal_id>")
def delete_goal(goal_id):
goal = validate_model(Goal, goal_id)
db.session.delete(goal)
db.session.commit()

return create_no_content_response()
49 changes: 49 additions & 0 deletions app/routes/route_utilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from flask import abort, make_response
from app.models.task import Task
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


def create_model(cls, model_data):
try:
new_model = cls.from_dict(model_data)

except KeyError as error:
# response = {"message": f"Invalid request: missing {error.args[0]}"}
response = {"details": "Invalid data"}
abort(make_response(response, 400))

db.session.add(new_model)
db.session.commit()

return new_model.to_dict(), 201

def get_models_with_filters(cls, filters=None):
query = db.select(cls)

if filters:
for attribute, value in filters.items():
if hasattr(cls, attribute):
query = query.where(getattr(cls, attribute).ilike(f"%{value}%"))

models = db.session.scalars(query.order_by(cls.id))
models_response = [model.to_dict() for model in models]
return models_response

def create_no_content_response():
return make_response("", 204)
97 changes: 96 additions & 1 deletion app/routes/task_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,96 @@
from flask import Blueprint
from flask import Blueprint, Response, abort, make_response, request
from app.routes.route_utilities import create_model, validate_model, get_models_with_filters, create_no_content_response
from app.models.task import Task
from flask import Blueprint
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()
return create_model(Task, request_body)

@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}%"))

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

tasks = db.session.scalars(query)

tasks_response = []
for book in tasks:
tasks_response.append(book.to_dict())
return tasks_response


@tasks_bp.get("/<task_id>")
def get_one_task(task_id):
task = validate_model(Task, task_id)
return task.to_dict()

@tasks_bp.put("/<task_id>")
def update_task(task_id):
book = validate_model(Task, task_id)
request_body = request.get_json()

book.title = request_body["title"]
book.description = request_body["description"]
db.session.commit()

return create_no_content_response()

@tasks_bp.delete("/<task_id>")
def delete_task(task_id):
task = validate_model(Task, task_id)
db.session.delete(task)
db.session.commit()

return create_no_content_response()

@tasks_bp.patch("/<task_id>/mark_complete")
def mark_task_complete(task_id):
task = validate_model(Task, task_id)
task.completed_at = db.func.now()
db.session.commit()
# send a notification to Slack
# reference:https://docs.slack.dev/tools/python-slack-sdk/legacy/basic_usage/
try:
slack_token = os.environ.get("SLACK_API_TOKEN")
headers = {"Authorization": f"Bearer {slack_token}"} if slack_token else {}
message = {
"channel": "task-notifications",
"text": f"Someone just completed the task {task.title}"
}

requests.post("https://slack.com/api/chat.postMessage", json=message, headers=headers)
except Exception:
pass

return create_no_content_response()

@tasks_bp.patch("/<task_id>/mark_incomplete")
def mark_task_incomplete(task_id):
task = validate_model(Task, task_id)
task.completed_at = None
db.session.commit()

return create_no_content_response()

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.
50 changes: 50 additions & 0 deletions migrations/alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# A generic, single database configuration.

[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false


# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic,flask_migrate

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
Loading