Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3eb9501
set up project and generate migrations directory
bamart-dev May 1, 2025
162a90c
implement Task class and helper methods
bamart-dev May 2, 2025
bfdc3da
update Task class
bamart-dev May 6, 2025
576e3d4
add line to register blueprint
bamart-dev May 7, 2025
72f192e
slight refactoring of Task model & add missing __init__.py file to ro…
bamart-dev May 7, 2025
9bd2194
update blueprint registration
bamart-dev May 7, 2025
021d831
update create_from_dict method to use .get() dict method for complete…
bamart-dev May 7, 2025
ec5e99a
implemented basic CRUD routes and created route helper functions
bamart-dev May 7, 2025
a4a722f
Unskipped/ran all wave 1 tests; all tests passing (Wave 1 complete)
bamart-dev May 7, 2025
875a36b
minor clean up of create_model code
bamart-dev May 7, 2025
697c2e5
refactor get_all_models to accept sort parameter
bamart-dev May 7, 2025
5eaa327
Unskipped/ran tests; all wave 2 tests passed (Wave 2 complete)
bamart-dev May 7, 2025
f555510
implement two additional patch routes
bamart-dev May 7, 2025
99b5f83
unskipped/ran all wave 3 tests; all tests passed (Wave 3 complete)
bamart-dev May 7, 2025
a91b775
add Slack API call to post task completion message in a channel (Wave…
bamart-dev May 8, 2025
435a6e4
modify blueprint registration
bamart-dev May 8, 2025
345e4aa
update create_model and validate_model
bamart-dev May 8, 2025
d316d44
update assert statements in 'not found' tests
bamart-dev May 8, 2025
cec9e2e
implement Goal model
bamart-dev May 8, 2025
7da4902
implemented routes
bamart-dev May 8, 2025
1d91331
implemented Goal model and ran flask migration/upgrade
bamart-dev May 8, 2025
8f5d8e3
unskipped and ran wave 5 tests; all tests passed (Wave 5 complete)
bamart-dev May 8, 2025
cec2e65
refactor goal and task models to establish relationship; ran flask db…
bamart-dev May 8, 2025
bf6c459
implemented add_tasks_to_goal and get_tasks_by_goal routes
bamart-dev May 9, 2025
f45898a
add comment
bamart-dev May 9, 2025
6ac392d
unskipped and ran all wave 6 tests; all tests passed (Wave 6 complete)
bamart-dev May 9, 2025
1045e75
add strict_slashes flag to goal/task routes
bamart-dev May 9, 2025
45b2aa4
refactor __init__.py to include flask-cors
bamart-dev Jun 19, 2025
5676ff0
Create env-example
bamart-dev Jun 19, 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
8 changes: 8 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
from flask import Flask
from flask_cors import CORS
from .db import db, migrate
from .models import task
from .routes.task_routes import bp as task_bp
from .routes.goal_routes import bp as goal_bp
from .models import task, goal
import os

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

app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('SQLALCHEMY_DATABASE_URI')
app.config['CORS_HEADERS'] = 'Content-Type'

if config:
# Merge `config` into the app's configuration
Expand All @@ -18,5 +24,7 @@ def create_app(config=None):
migrate.init_app(app, db)

# Register Blueprints here
app.register_blueprint(task_bp)
app.register_blueprint(goal_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 ..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")


def to_dict(self):
"""Return object attributes in dictionary format.

Returns a dictionary containing key value pairs corresponding
to the Goal object's attributes.
"""
return {
"id": self.id,
"title": self.title,
}


@classmethod
def create_from_dict(cls, goal_data):
"""Create new goal from dictionary.

Instantiates and returns a new Goal object with attributes
derived from the provided dictionary.
"""
return cls(
title = goal_data["title"],
)
43 changes: 42 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,46 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import ForeignKey
from datetime import datetime
from ..db import db
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]
goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id"))
goal: Mapped[Optional["Goal"]] = relationship(back_populates="tasks")
completed_at: Mapped[Optional[datetime]]

def to_dict(self):
"""Return object attributes in dictionary format.

When called on a Task object, the method returns a dictionary
containing key value pairs corresponding to the object's attributes.
"""
task = {
"id": self.id,
"title": self.title,
"description": self.description,
"is_complete": bool(self.completed_at),
}
if self.goal_id: # adds "goal_id" field if goal_id exists
task["goal_id"] = self.goal_id

return task


@classmethod
def create_from_dict(cls, task_data):
"""Create new task from dictionary items.

Instantiates and returns a new Task object with attribute values
derived from a provided dictionary.
"""
return cls(
title = task_data["title"],
description = task_data["description"],
completed_at = task_data.get("completed_at"),
)
Empty file added app/routes/__init__.py
Empty file.
79 changes: 78 additions & 1 deletion app/routes/goal_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,78 @@
from flask import Blueprint
from flask import Blueprint, request, Response, abort, make_response
from .route_utilities import validate_model, create_model, get_all_models
from app.db import db
from app.models.goal import Goal
from app.models.task import Task

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


@bp.post("", strict_slashes=False)
def create_goal():
request_body = request.get_json()

return create_model(Goal, request_body)


@bp.post("/<goal_id>/tasks", strict_slashes=False)
def add_tasks_to_goal(goal_id):
goal = validate_model(Goal, goal_id)
task_ids = request.get_json()["task_ids"]
task_list = []

for task_id in task_ids:
task = validate_model(Task, task_id)
task.goal_id = goal_id
task_list.append(task)

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

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


@bp.get("", strict_slashes=False)
def get_all_goals():
return get_all_models(Goal)


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

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


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

goal_tasks["tasks"] = [task.to_dict() for task in goal.tasks]

return goal_tasks


@bp.put("/<goal_id>", strict_slashes=False)
def update_goal(goal_id):
goal = validate_model(Goal, goal_id)
request_body = request.get_json()
try:
goal.title = request_body["title"]
except KeyError as e:
response = {"details": f"Invalid request: missing {e.args[0]}"}
abort(make_response(response, 400))

db.session.commit()

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


@bp.delete("/<goal_id>", strict_slashes=False)
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")
60 changes: 60 additions & 0 deletions app/routes/route_utilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from sqlalchemy import desc
from flask import abort, make_response, request
from app.db import db


def validate_model(cls, model_id):
"""Check given model ID and return model if ID is valid.

Returns a model matching the provided ID if ID is an integer and
a corresponding model exists.
"""
try:
model_id = int(model_id)
except ValueError:
message = {"error": f"{cls.__name__} ID ({model_id}) not valid."}
abort(make_response(message, 400))

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

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

return model


def create_model(cls, model_info):
"""Create model from provided dictionary and save in database.

Creates a model using the contents of a given dictionary, commits
it to the database, then returns a response. Raises KeyError if
attributes are missing from request.
"""
try:
model = cls.create_from_dict(model_info)
except KeyError:
response = {"details": "Invalid data"}
abort(make_response(response, 400))

db.session.add(model)
db.session.commit()
model_name = cls.__name__.lower()

return {f"{model_name}": model.to_dict()}, 201


def get_all_models(cls):
"""Retrieve all models from database."""
query = db.select(cls)
order = request.args.get("sort")

if order == "desc":
models = db.session.scalars(query.order_by(desc(cls.title)))
elif order == "asc":
models = db.session.scalars(query.order_by(cls.title))
else:
models = db.session.scalars(query.order_by(cls.id))

return [model.to_dict() for model in models]
87 changes: 86 additions & 1 deletion app/routes/task_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,86 @@
from flask import Blueprint
from datetime import datetime, timezone
from flask import Blueprint, request, Response, abort, make_response
from .route_utilities import validate_model, create_model, get_all_models
from app.db import db
from app.models.task import Task
import os
import requests

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


@bp.post("", strict_slashes=False)
def create_task():
request_body = request.get_json()

return create_model(Task, request_body)


@bp.get("", strict_slashes=False)
def get_all_tasks():
return get_all_models(Task)


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

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


@bp.put("/<task_id>", strict_slashes=False)
def update_task(task_id):
task = validate_model(Task, task_id)
request_body = request.get_json()
try:
task.title = request_body["title"]
task.description = request_body["description"]
task.completed_at = request_body.get("completed_at")
except KeyError as e:
response = {"details": f"Invalid request: missing {e.args[0]}"}
abort(make_response(response, 400))

db.session.commit()

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


@bp.patch("/<task_id>/mark_complete", strict_slashes=False)
def mark_task_complete(task_id):
task = validate_model(Task, task_id)

task.completed_at = datetime.now(tz=timezone.utc)
db.session.commit()

api_token = os.environ.get("SLACKBOT_API_KEY")
channel_id = os.environ.get("SLACK_CHANNEL_ID")
url = "https://slack.com/api/chat.postMessage"
body = {
"channel": f"{channel_id}",
"text": f"Someone just completed the task {task.title} :kirby_dance:",
}
headers = {"Authorization": f"Bearer {api_token}"}

requests.post(url, json=body, headers=headers)

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


@bp.patch("/<task_id>/mark_incomplete", strict_slashes=False)
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>", strict_slashes=False)
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 env-example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://postgres:postgres@localhost:5432/task_list_api_development
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