Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
1cab6bc
Sets up task list API project, includes migrations
rileydrellishak Oct 30, 2025
d3793c9
Completes attributes for Task model
rileydrellishak Oct 30, 2025
d22d79e
First migration after creating Task class
rileydrellishak Oct 30, 2025
3615b4d
Implements from_dict class method for Task, but need to figure out ho…
rileydrellishak Oct 30, 2025
e0c8a6f
implements class method from_dict and instance method to_dict, remove…
rileydrellishak Oct 31, 2025
9cd3719
registers task bp in app, creates skeleton task routes for get, post,…
rileydrellishak Oct 31, 2025
d19585b
creates route_utilities to make a validate_model helper function, imp…
rileydrellishak Oct 31, 2025
0c92b29
Implements routes for get all tasks and get one task by id, handles i…
rileydrellishak Oct 31, 2025
ebc03a5
Implements create task route, but the from_dict function is a bit fun…
rileydrellishak Oct 31, 2025
1809340
implements update task routes, passes all tests for put route
rileydrellishak Oct 31, 2025
dc216e3
Implements delete task route, all tests pass with appropriate error m…
rileydrellishak Oct 31, 2025
81d74c8
Deletes extra whitespace lines. Finished wave 1 but the from_dict cla…
rileydrellishak Oct 31, 2025
f0bd81b
adds a message to the end of route_utilities about dir() and __dir__,…
rileydrellishak Oct 31, 2025
47af6c2
Merge pull request #1 from rileydrellishak/wave-1
rileydrellishak Oct 31, 2025
5d3fc52
Implements sorting for get all tasks route
rileydrellishak Oct 31, 2025
d7e367b
Merge pull request #2 from rileydrellishak/wave-2
rileydrellishak Oct 31, 2025
b696355
Implements mark_complete method, passes wave 3 tests with it.
rileydrellishak Oct 31, 2025
0d1ad37
Implements mark_incompleete and passes tests. I feel like I can combi…
rileydrellishak Oct 31, 2025
61a6d6a
Combined mark complete and incomplete in same route
rileydrellishak Oct 31, 2025
01981b3
Merge pull request #3 from rileydrellishak/wave-3
rileydrellishak Oct 31, 2025
6e30cfc
Separated mark incomplete and mark complete back into two separate en…
rileydrellishak Oct 31, 2025
1007818
Fixed complete/incomplete (I had them swapped), successfully posts to…
rileydrellishak Oct 31, 2025
be1c331
Merge pull request #4 from rileydrellishak/wave-4
rileydrellishak Oct 31, 2025
cb4f79e
Implements Goal model, to_dict, from_dict methods for Goal. Will crea…
rileydrellishak Nov 2, 2025
30cda81
Registers goal model bp, implements get one goal by id, implements mi…
rileydrellishak Nov 2, 2025
ddb8362
Creates model utilities file, updates goal routes and tests for wave 5
rileydrellishak Nov 2, 2025
62f8a66
Merge pull request #5 from rileydrellishak/wave-5
rileydrellishak Nov 2, 2025
7411f0a
Establishes one to many relationship for goal to tasks, migration inc…
rileydrellishak Nov 2, 2025
16f9531
implements send list of tasks id to assign to goal
rileydrellishak Nov 2, 2025
740983a
implements get tasks from a goal id
rileydrellishak Nov 2, 2025
a6ad135
Adjusts task to_dict method to include goal id
rileydrellishak Nov 2, 2025
ab64733
Merge pull request #6 from rileydrellishak/wave-6
rileydrellishak Nov 2, 2025
f1b5163
Implements create_model method to generalize creating a model from a …
rileydrellishak Nov 3, 2025
825a44d
Merge pull request #7 from rileydrellishak/route-helper-create-instance
rileydrellishak Nov 3, 2025
b2d63bf
Clean up import statements
rileydrellishak Nov 3, 2025
46ca677
more import cleanup
rileydrellishak Nov 3, 2025
6814fa3
get all tasks list comprehension
rileydrellishak Nov 3, 2025
7fcc9c2
list comprehension for get_tasks_from_goal_id route
rileydrellishak Nov 3, 2025
b1c6715
patch method was titled incorrectly
rileydrellishak Nov 3, 2025
a1296e3
list comprehension for get all goals route
rileydrellishak Nov 3, 2025
e3d612c
Merge pull request #8 from rileydrellishak/optional-list-comprehensions
rileydrellishak Nov 3, 2025
16f1c1c
Implements wave 7 assert statements and cleans up comments
rileydrellishak Nov 3, 2025
b26db88
Merge pull request #9 from rileydrellishak/test-wave-07
rileydrellishak Nov 3, 2025
89c1804
Updates wave 5 test to match updated patch method
rileydrellishak Nov 3, 2025
ab55763
Adds tasks attribute to Goal, adds goal_id and goal attributes to Task
rileydrellishak Nov 4, 2025
6adb2dd
Registers goal routes bp
rileydrellishak Nov 4, 2025
5c7c412
create_model route helper, goal routes for create, get all, and delet…
rileydrellishak Nov 4, 2025
067a983
post task ids to goal route implemented, passes tests
rileydrellishak Nov 4, 2025
d9739cb
updates wave 6 methods because I thought I really messed them up
rileydrellishak Nov 4, 2025
f58bef1
Merge branch 'main' into redo-wave-6
rileydrellishak Nov 4, 2025
3746f1b
Merge pull request #10 from rileydrellishak/redo-wave-6
rileydrellishak Nov 4, 2025
468abc7
deleted a double line
rileydrellishak Nov 4, 2025
2f9fe6a
writes the patch method to update goal title
rileydrellishak Nov 4, 2025
61aa819
generalizes patch to include more than one attribute to update
rileydrellishak Nov 4, 2025
0e90274
Merge pull request #11 from rileydrellishak/redo-wave-5
rileydrellishak Nov 4, 2025
478dedc
fixes reassigning attribute value when making patch request
rileydrellishak Nov 4, 2025
f4221e2
Merge branch 'main' into redo-wave-5
rileydrellishak Nov 4, 2025
cf97276
minor fix to for attr, value in request_body.items() goal patch method
rileydrellishak Nov 4, 2025
c15d21f
implements get_models_with_filters that also sorts the query by title…
rileydrellishak Nov 4, 2025
23c44c8
adds three goals fixture for optional feature testing
rileydrellishak Nov 5, 2025
b0bfe9c
expands upon get_models_with_filters to include logic to sort by any …
rileydrellishak Nov 5, 2025
4daac79
writes tests to test filtering and sorting method
rileydrellishak Nov 5, 2025
c882bda
finishes tests for query params to determine sort direction and by wh…
rileydrellishak Nov 5, 2025
8570627
Merge pull request #12 from rileydrellishak/query-params-helper
rileydrellishak Nov 5, 2025
c5367e6
added more tests to test_query_params_refactor for 100% test coverage
rileydrellishak Nov 5, 2025
15883a2
final run through of methods to make sure they all worked, updated th…
rileydrellishak Nov 6, 2025
5abd3a3
updated tests to show that the put and patch methods persisted data
rileydrellishak Nov 6, 2025
e196203
moved slack post message to its own helper function in route utilities
rileydrellishak Nov 6, 2025
05a1453
adds if not slack token line to send_slack_message helper function
rileydrellishak Nov 7, 2025
947dfd3
another update to the slack token get
rileydrellishak 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
7 changes: 4 additions & 3 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 @@ -10,13 +12,12 @@ def create_app(config=None):
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('SQLALCHEMY_DATABASE_URI')

if config:
# Merge `config` into the app's configuration
# to override the app's default settings for testing
app.config.update(config)

db.init_app(app)
migrate.init_app(app, db)

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

return app
15 changes: 14 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
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 = cls(title=goal_data['title'])
return new_goal

def to_dict(self):
return {
'id': self.id,
'title': self.title
}
41 changes: 40 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,44 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import ForeignKey
from ..db import db
from typing import Optional
from datetime import datetime

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(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_data):
if 'is_complete' not in task_data.keys() or task_data['is_complete'] is False:
task_data['is_complete'] = None

new_task = Task(
title=task_data['title'],
description=task_data['description'],
completed_at=task_data['is_complete'],
goal_id=task_data.get('goal_id', None)
)

return new_task

def to_dict(self):
task_dict = {}

if self.completed_at is None or self.completed_at is False:
task_dict['is_complete'] = False
else:
task_dict['is_complete'] = True

if self.goal_id:
task_dict['goal_id'] = self.goal_id

task_dict['id'] = self.id
task_dict['title'] = self.title
task_dict['description'] = self.description

return task_dict
66 changes: 65 additions & 1 deletion app/routes/goal_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,65 @@
from flask import Blueprint
from flask import Blueprint, request, Response
from ..models.goal import Goal
from ..models.task import Task
from ..db import db
from .route_utilities import validate_model, create_model, get_models_with_filters

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

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

@bp.get('')
def get_all_goals():
return get_models_with_filters(Goal, request.args)

@bp.get('/<goal_id>')
def get_goal_by_id(goal_id):
goal = validate_model(Goal, goal_id)
return goal.to_dict()

@bp.put('/<goal_id>')
def update_goal_title(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 post_task_ids_to_goal(goal_id):
goal = validate_model(Goal, goal_id)
request_body = request.get_json()

goal.tasks = []

for id in request_body['task_ids']:
task = validate_model(Task, id)
task.goal_id = goal.id

db.session.commit()

response = {
'id': goal.id,
'task_ids': [task.id for task in goal.tasks]
}
return response, 200

@bp.get('/<goal_id>/tasks')
def get_tasks_for_specific_goal(goal_id):
goal = validate_model(Goal, goal_id)
response = goal.to_dict()
response['tasks'] = [task.to_dict() for task in goal.tasks]
return response, 200
75 changes: 75 additions & 0 deletions app/routes/route_utilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from flask import abort, make_response, Response
from ..db import db
import os
import requests

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:
response = {'details': f'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:
sort_by = filters.get('sort_by', 'title')
direction = filters.get('sort', None)

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

if hasattr(cls, sort_by):
sort_column = getattr(cls, sort_by)
if direction == 'desc':
query = query.order_by(sort_column.desc())
else:
query = query.order_by(sort_column)

else:
query = query.order_by(cls.title)

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

def send_slack_message(task):
slack_token = os.environ.get("SLACK_BOT_TOKEN")
if not slack_token:
return Response(status=204, mimetype='application/json')

channel_and_message = {
'channel': 'task-notifications',
'text': f'Someone just completed the task {task.title}'
}
headers = {
'Authorization': slack_token
}
requests.post('https://slack.com/api/chat.postMessage', data=channel_and_message, json=channel_and_message, headers=headers)

return Response(status=204, mimetype='application/json')
60 changes: 59 additions & 1 deletion app/routes/task_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,59 @@
from flask import Blueprint
from flask import Blueprint, request, Response
from app.models.task import Task
from ..db import db
from app.routes.route_utilities import validate_model, create_model, get_models_with_filters, send_slack_message
from datetime import datetime

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

@bp.get('')
def get_all_tasks():
return get_models_with_filters(Task, request.args), 200

@bp.get('/<task_id>')
def get_one_task_by_id(task_id):
task = validate_model(Task, task_id)
return task.to_dict()

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

@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']
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')

@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.patch('/<task_id>/mark_complete')
def mark_task_complete(task_id):
task = validate_model(Task, task_id)
task.completed_at = datetime.now().date()

db.session.commit()

return send_slack_message(task)
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