Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2a8316c
complete project setup.
trimakichan Oct 30, 2025
79dcce8
Create Task model and CRUD operations in task_routes.py; pass all Wav…
trimakichan Oct 31, 2025
7858222
Complete Wave 2: Add task title sorting feature with passing tests
trimakichan Oct 31, 2025
4ac848d
adds mimetiype assersions in the test_update_task and test_delete_tas…
trimakichan Oct 31, 2025
4a7f81b
implements task sorting feature and enhance error handling in task ro…
trimakichan Nov 1, 2025
b14a587
refactors task routes to use a unified blueprint and implement model …
trimakichan Nov 2, 2025
653e6b6
adds model validation tests for task; includes checks for valid, miss…
trimakichan Nov 2, 2025
55140e9
adds mark complete and mark incomplete endpoints; updates tests for t…
trimakichan Nov 2, 2025
8037972
completes wave 4: add a function to send a msg to slack channel.
trimakichan Nov 2, 2025
0d2efde
completes wave 5: add goal model and CRUD points.
trimakichan Nov 2, 2025
e4afe78
refactors: streamline goal and task retrieval responses using list co…
trimakichan Nov 3, 2025
883e667
creates a create model function in route_utilities.py and apply it in…
trimakichan Nov 4, 2025
d6bd5f8
creates one(Goal) to many relationship(Task) in the schema
trimakichan Nov 4, 2025
275dd77
completes wave 6: update Goal and Task models to include task details…
trimakichan Nov 4, 2025
d63eef1
creates a slack_service file in a services folder and move the logic …
trimakichan Nov 4, 2025
121ed0c
completes wave 7 tests
trimakichan Nov 4, 2025
0f2a2e4
refactor Task model methods and enhance route utilities for sorting t…
trimakichan Nov 5, 2025
c71feba
refactors goal and task routes to utilize update_model for cleaner co…
trimakichan Nov 5, 2025
7c7e7f5
refactors goal routes and utilities to streamline goal retrieval with…
trimakichan Nov 5, 2025
7a48598
added home_routes.py and register the bluepring in the init file.
trimakichan Nov 5, 2025
1c1c3fd
Tighten conditional logic in task.py and fix typo.
trimakichan Nov 6, 2025
01928c3
fix: correct HTML syntax in home route welcome message
trimakichan Nov 6, 2025
f426d14
fix: implement filtering logic in get_models_with_filters function
trimakichan Nov 6, 2025
94db92a
fix: update home page message and clean up task_routes imports
trimakichan Nov 6, 2025
ca58c0b
fix: improve error handling in create_task_with_goal and add addition…
trimakichan Nov 6, 2025
38ea3c3
fix: remove unused imports from route utilities and task routes
trimakichan Nov 6, 2025
800a2e1
refactor: remove outdated comment in get_models_with_filters function
trimakichan Nov 7, 2025
818a24f
fix: correct environment variable name for test database URI in app f…
trimakichan Nov 7, 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
10 changes: 8 additions & 2 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import os
from flask import Flask
from .db import db, migrate
from .models import task, goal
import os
from .db import db, migrate
from app.routes.task_routes import bp as tasks_bp
from app.routes.goal_routes import bp as goals_bp
from app.routes.home_routes import bp as home_bp

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

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

return app
31 changes: 30 additions & 1 deletion app/models/goal.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 String
from typing import TYPE_CHECKING
from ..db import db

if TYPE_CHECKING:
from .task import Task

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

def to_dict(self, include_tasks=False, ids_only=False):
if ids_only:
return {
"id": self.id,
"task_ids": [task.id for task in self.tasks]
}

goal_as_dict = {
"id": self.id,
"title": self.title,
}

if include_tasks:
goal_as_dict["tasks"] = [task.to_dict() for task in self.tasks]

return goal_as_dict

@classmethod
def from_dict(cls, goal_dict):
new_goal = cls(title=goal_dict["title"])
return new_goal
32 changes: 31 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import String, ForeignKey
from datetime import datetime
from typing import Optional, TYPE_CHECKING
from ..db import db

if TYPE_CHECKING:
from .goal import Goal
class Task(db.Model):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
title: Mapped[str]= mapped_column(String(255))
description: Mapped[str]
completed_at: Mapped[Optional[datetime]]
goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id"))
goal: Mapped[Optional["Goal"]] = relationship(back_populates="tasks")

def to_dict(self):
task_as_dict = {
"id": self.id,
"title": self.title,
"description": self.description,
"is_complete": True if self.completed_at else False,
}

if self.goal_id is not None:
task_as_dict["goal_id"] = self.goal_id

return task_as_dict

@classmethod
def from_dict(cls,task_dict):
return cls(
title=task_dict["title"],
description=task_dict["description"],
)
61 changes: 60 additions & 1 deletion app/routes/goal_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,60 @@
from flask import Blueprint
from flask import Blueprint, request, abort, make_response, Response
from .route_utilities import validate_model, create_model, update_model, get_models_with_filters
from ..models.goal import Goal
from ..models.task import Task
from app.db import db

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

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

@bp.post("/<goal_id>/tasks")
def create_task_with_goal(goal_id):
goal = validate_model(Goal, goal_id)
request_body = request.get_json()

try:
task_ids = request_body["task_ids"]
except KeyError:
invalid_msg = {"details": "Invalid data"}
abort(make_response(invalid_msg, 400))

tasks = [validate_model(Task, task_id) for task_id in task_ids]

goal.tasks = tasks
db.session.commit()

return goal.to_dict(ids_only=True)

@bp.get("")
def get_all_goals():
return get_models_with_filters(Goal, filters=request.args)

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

return goal.to_dict()

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

return goal.to_dict(include_tasks=True)

@bp.put("/<goal_id>")
def update_goal(goal_id):
required_keys = ["title"]
return update_model(Goal, goal_id, required_keys)

@bp.delete("/<goal_id>")
def delete_goal(goal_id):
goal = validate_model(Goal, goal_id)

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

return Response(status=204, mimetype="application/json")
8 changes: 8 additions & 0 deletions app/routes/home_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from flask import Blueprint

bp = Blueprint("home_bp", __name__)


@bp.get("/")
def home_page():
return "<h1>✨Welcome to Makiko’s Task List API✨</h1>"
74 changes: 74 additions & 0 deletions app/routes/route_utilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from flask import abort, make_response, Response, request
from ..db import db

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

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

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

return model

def create_model(cls, model_data):
try:
new_model =cls.from_dict(model_data)
except KeyError as error:
invalid_msg = {"details": "Invalid data"}
abort(make_response(invalid_msg, 400))

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

return new_model.to_dict(), 201

def update_model(cls, model_id, required_keys):
model = validate_model(cls, model_id)
request_body = request.get_json()

for key in required_keys:
if key not in request_body:
invalid_msg = {"details": "Invalid data"}
abort(make_response(invalid_msg, 400))

for key in required_keys:
setattr(model,key,request_body[key])

db.session.commit()

return Response(status=204, mimetype="application/json")

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

if filters:
for attribute, value in filters.items():
if attribute == "sort":
query = apply_sort_to_query(cls, query, value)
elif 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 apply_sort_to_query(cls, query, sort_type):
valid_sort = {
"asc": cls.title,
"desc": cls.title.desc()
}
sort_type = sort_type.casefold().strip()

if sort_type not in valid_sort:
invalid_msg = {"message": "Invalid sort value. Valid options: asc, desc."}
abort(make_response(invalid_msg, 400))

return query.order_by(valid_sort[sort_type])

58 changes: 57 additions & 1 deletion app/routes/task_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,57 @@
from flask import Blueprint
from flask import Blueprint, request, Response
from .route_utilities import validate_model, create_model, get_models_with_filters, update_model
from datetime import datetime
from app.models.task import Task
from app.services.slack_service import send_msg_slack
from app.db import db

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

@bp.post("")
def create_task():
request_body = request.get_json()
return create_model(Task, request_body)

@bp.get("")
def get_all_tasks():
return get_models_with_filters(Task, filters=request.args)

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

return task.to_dict()

@bp.put("/<task_id>")
def update_task(task_id):
required_keys = ["title", "description"]
return update_model(Task, task_id, required_keys)

@bp.patch("/<task_id>/mark_complete")
def mark_complete_task(task_id):
task = validate_model(Task, task_id)
task.completed_at = datetime.now()

db.session.commit()

send_msg_slack(task)

return Response(status=204, mimetype="application/json")

@bp.patch("/<task_id>/mark_incomplete")
def mark_incomplete_task(task_id):
task = validate_model(Task, task_id)
task.completed_at = None

db.session.commit()

return Response(status=204, mimetype="application/json")

@bp.delete("/<task_id>")
def delete_task(task_id):
task = validate_model(Task,task_id)

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

return Response(status=204, mimetype="application/json")
19 changes: 19 additions & 0 deletions app/services/slack_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import os
import requests


SLACK_URL = "https://slack.com/api/chat.postMessage"
SLACK_API_KEY = os.environ.get('SLACK_API_KEY')

def send_msg_slack(task):
json_body = {
"channel": "C09N95RPR34",
"text": f"Someone just completed the task {task.title}"
}

headers = {
"Authorization": f"Bearer {SLACK_API_KEY}",
"Content-Type": "application/json"
}

requests.post(SLACK_URL, json=json_body, headers=headers)
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