Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
fbc58bc
initial project setup
scnkera Oct 31, 2024
01d780f
Uncommented pytest skips
scnkera Nov 17, 2024
c3754de
Configured get requests for all tasks and a single task
scnkera Nov 17, 2024
f5428ea
Configured base.py file for the database models
scnkera Nov 17, 2024
84e8ad7
Added sqlalchemy, migrated, made Task a database backed model, genera…
scnkera Nov 17, 2024
180915f
Updated Task model and wrote POST and GET /tasks database requests
scnkera Nov 17, 2024
6206764
Updated POST, GET, PUT, and DELETE /tasks database requests
scnkera Nov 17, 2024
6c9189d
Created a seed.py file to seed data into the database
scnkera Nov 17, 2024
fed3ffb
Updated app innit configuration
scnkera Nov 18, 2024
65fd551
Uncommented out code
scnkera Nov 18, 2024
27da05e
Updated from_dict class method
scnkera Nov 22, 2024
669eabb
Added typing import
scnkera Dec 5, 2024
fbcd83a
Updated task_routes.py
scnkera Dec 6, 2024
3f84419
Added test assertions
scnkera Dec 6, 2024
439cb63
Updated create task
scnkera Dec 8, 2024
30f4b8b
Commented out pytest skip
scnkera Dec 9, 2024
1ec5409
Updated validate_model and create model functions
scnkera Dec 9, 2024
c9689de
Shifted validate_model_id function to utilities
scnkera Dec 9, 2024
0c73a51
Refactored validate_model
scnkera Dec 10, 2024
49b7d99
Implemented error handling for create_task function
scnkera Dec 10, 2024
2af0d4b
Refactored create_task function
scnkera Dec 10, 2024
0fe6ecb
Commented out pytest.skip
scnkera Dec 10, 2024
d701a6c
Added mark_task_complete and mark_task_incomplete functions
scnkera Dec 10, 2024
f9b850a
Updated standard validation and creation models
scnkera Dec 10, 2024
6b5c893
Updated mark_task_complete and mark_task_incomplete functions
scnkera Dec 12, 2024
107267f
Changed response structure
scnkera Dec 12, 2024
00e7719
Commented out pytest.skips
scnkera Dec 12, 2024
e0f272f
Updated response messages
scnkera Dec 12, 2024
2f24b23
Commented out pytest.skips
scnkera Dec 13, 2024
aa4d01c
Implemented slack API call in the mark_task_complete function
scnkera Dec 15, 2024
3edced5
Updated models
scnkera Dec 15, 2024
56e1dd2
Updated a dynmaic cls name return for create_model function
scnkera Dec 15, 2024
333d5db
Registered the goals blueprint
scnkera Dec 15, 2024
4492290
Updated tests and added assertions
scnkera Dec 15, 2024
8ce5719
Created create_goal, get_all_goals, get_single_goal, update_goal, del…
scnkera Dec 15, 2024
062a77b
Refactored code and imported validate_model and create_model
scnkera Dec 15, 2024
0dcd543
Commented out pytest.skips
scnkera Dec 15, 2024
07a5916
Added new route goals/<goal_id>/tasks
scnkera Dec 17, 2024
192a7eb
Updated comments
scnkera Dec 31, 2024
7362dd1
Updated conftest
scnkera Dec 31, 2024
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
14 changes: 10 additions & 4 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
from flask import Flask
from .routes.task_routes import tasks_bp
from .routes.goal_routes import goals_bp
from .db import db, migrate
from .models import task, goal
from .models import task
from .models import goal
import os

def create_app(config=None):
# __name__ stores the name of the module we're in
app = Flask(__name__)

app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('SQLALCHEMY_DATABASE_URI')

# app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql+psycopg2://postgres:postgres@localhost:5432/task_list_api_development'

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
return app
18 changes: 17 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db import db
from typing import List

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 dict(
id=self.id,
title=self.title,
)

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

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

def to_dict(self):

task_dict = dict(
id=self.id,
title=self.title,
description=self.description,
is_complete= True if self.completed_at else False
)

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

return task_dict

@classmethod
def from_dict(cls, task_data):
if "completed_at" in task_data:
return cls(
title=task_data["title"],
description=task_data["description"],
completed_at=task_data["completed_at"]
)

return cls(
title=task_data["title"],
description=task_data["description"],
)
90 changes: 89 additions & 1 deletion app/routes/goal_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,89 @@
from flask import Blueprint
from flask import Blueprint, abort, make_response, request, Response
from ..db import db
from ..models.goal import Goal
from ..models.task import Task
import datetime
import requests
import os
from sqlalchemy import desc, asc
from .route_utilities import validate_model, create_model

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():
query = db.select(Goal).order_by(Goal.id)
goals = db.session.scalars(query)

response=[goal.to_dict() for goal in goals]

return response, 200

@goals_bp.get("/<goal_id>")
def get_single_goal(goal_id):
goal = validate_model(Goal, goal_id)
response = {"goal": goal.to_dict()}

return response, 200

@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()
response = {"title": goal.title}

return response, 200

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

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

response = {"details": f"Goal {goal.id} \"{goal.title}\" successfully deleted"}

return response, 200

@goals_bp.post("/<goal_id>/tasks")
def create_task_ids_by_goal(goal_id):
request_body = request.get_json()

goal = validate_model(Goal, goal_id)

try:
task_ids = request_body["task_ids"]

for task_id in task_ids:
task = validate_model(Task, task_id)
goal.tasks.append(task)

db.session.commit()

except KeyError as error:
response = {"details": "Invalid data"}
abort(make_response(response, 400))

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

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

goal_dict = goal.to_dict()
goal_dict["tasks"] = []

for task in goal.tasks:
goal_dict["tasks"].append(task.to_dict())

return goal_dict
51 changes: 51 additions & 0 deletions app/routes/route_utilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from flask import abort, make_response
from ..db import db
from app.models.task import Task
from app.models.goal import Goal
from sqlalchemy import desc, asc

def validate_model(cls, model_id):

# checks for valid input
try:
model_id = int(model_id)
except:
abort(make_response({"msg": f"{cls.__name__} id {model_id} is invalid"}, 400))


# returns task with the corresponding task_id
query = db.select(cls).where(cls.id == model_id)
model = db.session.scalar(query)

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

return model

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

except KeyError as error:
response = {"details": "Invalid data"}
abort(make_response(response, 400))

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

return {cls.__name__.lower(): new_model.to_dict()}, 201

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

if filters:
order_by = getattr(cls, filters.get("order_by", "title"))
if filters.get("sort") == "desc":
query = query.order_by(db.desc(order_by))
else:
query = query.order_by(db.asc(order_by))

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

return models_response
89 changes: 88 additions & 1 deletion app/routes/task_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,88 @@
from flask import Blueprint
from flask import Blueprint, abort, make_response, request, Response
from ..db import db
from app.models.task import Task
from .route_utilities import validate_model, create_model, get_models_with_filters
import datetime
import requests
import os

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():

return get_models_with_filters(Task, request.args)

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

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

@tasks_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 {"task": task.to_dict()}

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

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

response = {"details": f"Task {task_id} \"{task.title}\" successfully deleted"}

return response

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

db.session.commit()

# Slack bot API call
url = "https://slack.com/api/chat.postMessage"
API_KEY = os.environ.get("SLACK_BOT_OAUTH")
header = {"Authorization": f"Bearer {API_KEY}"}
request_body = {
"channel": "task-notifications",
"text": f"Someone just completed the task {task.title}!"
}

slack_post = requests.post(url, headers=header, params=request_body)

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

@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 {"task": task.to_dict()}


def implement_slack_api(task):
url = "https://slack.com/api/chat.postMessage"
API_KEY = os.environ.get("SLACK_BOT_OAUTH")
header = {"Authorization": f"Bearer {API_KEY}"}
request_body = {
"channel": "task-notifications",
"text": f"{task.title} completed!",
}
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