Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ef5618a
Add initial migration configuration files for Alembic
tsztin0217 Oct 30, 2025
86d366d
Implement task creation endpoint and define Task model with return_di…
tsztin0217 Oct 31, 2025
f41620a
Add task retrieval functions to task_routes.py
tsztin0217 Oct 31, 2025
7911d54
Implement update and delete task endpoints in task_routes.py
tsztin0217 Oct 31, 2025
e873b07
Refactor task retrieval logic into validate_task function for improve…
tsztin0217 Oct 31, 2025
281568b
Refactor task creation logic into separate function for improved clar…
tsztin0217 Oct 31, 2025
cb14871
Refactor Task model methods for improved clarity and add from_dict me…
tsztin0217 Nov 1, 2025
e80aa93
Refactor Task model to update completed_at column type and add commen…
tsztin0217 Nov 1, 2025
789340d
Refactor Task model and task_routes to improve method documentation a…
tsztin0217 Nov 2, 2025
7f0f333
Add sorting functionality to task retrieval and update tests to remov…
tsztin0217 Nov 2, 2025
a8dcc4f
Implement task completion and incompletion endpoints with correspondi…
tsztin0217 Nov 4, 2025
586d1d0
Refactor tests for missing task scenarios to include response body as…
tsztin0217 Nov 4, 2025
e8dabb8
Add Slack notification for task completion
tsztin0217 Nov 4, 2025
43b401b
Refactor task routes to use centralized model validation and improve …
tsztin0217 Nov 4, 2025
5fb92da
Add goal model and implement CRUD operations for goals
tsztin0217 Nov 5, 2025
1518918
Refactor goal and task routes to improve response handling and add go…
tsztin0217 Nov 5, 2025
7def879
Implement goal-task relationship in models and add task creation endp…
tsztin0217 Nov 5, 2025
cebf1aa
Fix task association logic in goal routes and update response status …
tsztin0217 Nov 5, 2025
2a63792
Enhance goal and task routes with improved model validation and task …
tsztin0217 Nov 5, 2025
595b077
Refactor tests to remove skip markers and add assertions for error re…
tsztin0217 Nov 6, 2025
68c292b
Refactor goal and task retrieval to support filtering and sorting thr…
tsztin0217 Nov 6, 2025
dd3d8db
Enhance goal and task retrieval with additional filtering options for…
tsztin0217 Nov 6, 2025
0b85b25
Remove description filtering from goal retrieval since goal doesn't h…
tsztin0217 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
4 changes: 4 additions & 0 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 bp as tasks_bp
from .routes.goal_routes import bp as goals_bp
import os

def create_app(config=None):
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
23 changes: 22 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db import db
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .task import Task



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_dict):
"""Create a Goal instance from a dictionary."""
return cls(
title=goal_dict["title"]
)

def to_dict(self):
"""Return a dictionary representation of the Goal."""
return {
"id": self.id,
"title": self.title
}
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.orm import Mapped, mapped_column, relationship
from sqlalchemy import ForeignKey, DateTime
from ..db import db
from datetime import datetime
from typing import Optional, TYPE_CHECKING
if TYPE_CHECKING:
from .goal import Goal


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)
goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id"))
goal: Mapped[Optional["Goal"]] = relationship(back_populates="tasks")

@classmethod
def from_dict(cls, task_dict):
"""Create a Task instance from a dictionary."""
goal_id = task_dict.get("goal_id")
is_complete = task_dict.get("is_complete", False)
completed_at = datetime.now() if is_complete else None

return cls(
title=task_dict["title"],
description=task_dict["description"],
completed_at=completed_at,
goal_id=goal_id
)

def to_dict(self):
"""Return a dictionary representation of the Task."""
task_dict = {
"id": self.id,
"title": self.title,
"description": self.description,
"is_complete": self.completed_at is not None
}
if self.goal:
task_dict["goal_id"] = self.goal.id

return task_dict
94 changes: 93 additions & 1 deletion app/routes/goal_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,93 @@
from flask import Blueprint
from flask import Blueprint
from .route_utilities import validate_model, create_model, get_models_with_filters
from app.models.goal import Goal
from app.models.task import Task
from ..db import db
from flask import request, make_response, abort, Response
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.get("")
def get_all_goals():
return get_models_with_filters(Goal,
sort=request.args.get("sort"),
title=request.args.get("title"))

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

response = goal.to_dict()

return response


@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 Response(status=204, mimetype="application/json")

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

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

# retrieve python dictionary from request body
request_body = request.get_json()

# get list of task ids from dictionary
task_ids = request_body.get("task_ids", [])

for task in goal.tasks:
task.goal = None
task.goal_id = None

# associate each task with the goal
for task_id in task_ids:
task = validate_model(Task, task_id)
task.goal = goal
task.goal_id = goal.id


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


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

return response, 200

@bp.get("/<goal_id>/tasks")
def get_tasks_of_one_goal(goal_id):
goal = validate_model(Goal, goal_id)
tasks = [task.to_dict() for task in goal.tasks]

response = {
"id": goal.id,
"title": goal.title,
"tasks": tasks
}

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

def validate_model(cls, model_id):
"""Retrieves a model instance by ID or aborts with 400/404."""
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:
response = {"details": "Invalid data"}
abort(make_response(response, 400))

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

response = new_model.to_dict()

return response, 201

def get_models_with_filters(cls, sort=None, title=None, description=None):
"""Return list of model dicts, optionally sorted and filtered.

If the requested filter/sort field doesn't exist on `cls`, the function will
ignore that filter/sort and return unfiltered/unordered results (i.e. "just return all").
"""
query = db.select(cls)

# Apply filter using getattr; if the attribute doesn't exist, ignore the filter
if title:
attr = getattr(cls, "title", None)
if attr is not None:
query = query.where(attr.ilike(f"%{title}%"))

if description:
attr = getattr(cls, "description", None)
if attr is not None:
query = query.where(attr.ilike(f"%{description}%"))

if sort == "asc":
query = query.order_by(cls.title.asc())
else:
query = query.order_by(cls.title.desc())

models = db.session.scalars(query)

response = [model.to_dict() for model in models]

return response
86 changes: 85 additions & 1 deletion app/routes/task_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,85 @@
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
import requests
import os
from .route_utilities import validate_model, create_model, get_models_with_filters

path = "https://slack.com/api/chat.postMessage"
token = os.environ.get("SLACK_API_TOKEN")


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



@bp.post("")
def create_task():
"""Create a task with the data from the request body."""
request_body = request.get_json()
return create_model(Task, request_body)

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


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

response = task.to_dict()

return response

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

request_body = request.get_json()

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

db.session.commit()

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

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

query_params = {
"channel": "#test-slack-api",
"text": f"Someone just completed the task {task.title}"
}

headers = {"Authorization": f"Bearer {token}"}

requests.post(path, json=query_params, headers=headers)

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

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