Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
cd51c9e
add goal model
sashavershkova May 1, 2025
4fe1180
add task model
sashavershkova May 2, 2025
8d73e73
add to_dict and from_dict methods to task model
sashavershkova May 2, 2025
9e6a054
add retrieve_model_instance_by_id
sashavershkova May 2, 2025
4a5bd48
add create_model_inst_from_dict_with_response
sashavershkova May 2, 2025
b3c7ea2
add create_task, get_all_tasks
sashavershkova May 2, 2025
c458c79
add get_task_by_id route
sashavershkova May 2, 2025
4297d4a
add update_by_id endpoint
sashavershkova May 2, 2025
3369211
add delete_by_id endpoint
sashavershkova May 2, 2025
f153509
add delete all tasks endpoint
sashavershkova May 2, 2025
d3262f5
add patch_by_id endpoint
sashavershkova May 2, 2025
341a24b
pass tests in wave1, fix bugs
sashavershkova May 2, 2025
ac870a4
refactor hepler functions
sashavershkova May 3, 2025
bb437d1
refactor get_all_tasks, add params
sashavershkova May 3, 2025
593d5b4
add endpoints for mark complete and mark incomplete
sashavershkova May 3, 2025
73b7846
add slak api bot to mark_complete
sashavershkova May 8, 2025
a2ed59d
add model goal
sashavershkova May 8, 2025
4469846
add methods to goal model
sashavershkova May 8, 2025
eaf8e4f
add post rout to goal, fix somw typos
sashavershkova May 8, 2025
7229f14
add get_one_goal
sashavershkova May 8, 2025
05c2d3c
add update and delete routes for goal, passed all tests
sashavershkova May 8, 2025
272f4f2
add raletionship between task and goal
sashavershkova May 8, 2025
3b42148
fixed goal method, passed all tests
sashavershkova May 9, 2025
34c7be9
fix some typos
sashavershkova May 9, 2025
f7cd9ea
add dotenv to init
sashavershkova May 9, 2025
87e9274
final version
sashavershkova May 9, 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: 7 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@

from flask import Flask
from .db import db, migrate
from .models import task, goal
from app.routes.task_routes import bp as task_bp
from app.routes.goal_routes import bp as goal_bp

import os

def create_app(config=None):
Expand All @@ -18,5 +22,8 @@ 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, include_empty_tasks=False):
goal_as_dict = {}
goal_as_dict["id"] = self.id
goal_as_dict["title"] = self.title

if self.tasks:
goal_as_dict["tasks"] = [task.to_dict() for task in self.tasks]
if not self.tasks and include_empty_tasks == True:
goal_as_dict["tasks"] = []

return goal_as_dict

# def to_dict_mandatory_tasks_list(self):
# goal_as_dict = self.to_dict()
# goal_as_dict["tasks"] = [task.to_dict() for task in self.tasks]
# return goal_as_dict


@classmethod
def from_dict(cls, goal_data):
return cls(title=goal_data["title"])
31 changes: 30 additions & 1 deletion app/models/task.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 ForeignKey
from datetime import datetime
from typing import Optional
from ..db import db

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

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

if self.goal:
task_as_dict["goal_id"] = self.goal.id

return task_as_dict

@classmethod
def from_dict(cls, dict_data):
return cls(title=dict_data["title"], description=dict_data["description"])
77 changes: 76 additions & 1 deletion app/routes/goal_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,76 @@
from flask import Blueprint
from flask import Blueprint, request, Response, make_response, abort
from sqlalchemy import desc
from datetime import datetime, timezone
import requests
from app.routes.routes_helper_utilities import create_model_inst_from_dict_with_response, retrieve_model_inst_by_id, nested_dict
from app.models.task import Task
from app.models.goal import Goal
from app.db import db

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

# POST ONE GOAL, RETURN {"GOAL": {GOAL DICTIONARY}}
@bp.post("")
def create_goal():
request_body = request.get_json()
return create_model_inst_from_dict_with_response(Goal, request_body)

# ADD EXISTING TASK IDS TO EXISTING GOAL, RETURN {"ID":..., "TASK_IDS": [...]}
@bp.post("/<goal_id>/tasks")
def add_task_ids_to_goal(goal_id):
goal = retrieve_model_inst_by_id(Goal, goal_id)
request_body = request.get_json()

# remove connection of existing task from that goal
query = db.select(Task).where(Task.goal_id==1)
old_tasks = db.session.scalars(query)
for task in old_tasks:
task.goal_id = None

# add connections of new tasks to the goal
for task_id in request_body["task_ids"]:
task = retrieve_model_inst_by_id(Task, task_id)
task.goal_id = goal_id

db.session.commit()
return {"id": int(goal_id),
"task_ids": [task.id for task in goal.tasks]}

# GET TASKS FOR ONE GOAL, RETURN GOAL DICTIONARY WITH TASKS LIST
@bp.get("/<goal_id>/tasks")
def get_tasks_of_one_goal(goal_id):
goal = retrieve_model_inst_by_id(Goal, goal_id)
return goal.to_dict(include_empty_tasks=True)

# GET ALL GOALS
@bp.get("")
def get_saved_goals():
goals = db.session.scalars(db.select(Goal).order_by(Goal.id))
return [goal.to_dict() for goal in goals]

# GET GOAL BY ID
@bp.get("/<goal_id>")
def get_one_goal(goal_id):
goal = retrieve_model_inst_by_id(Goal, goal_id)
return nested_dict(Goal, goal)

# UPDATE GOAL TITLE
@bp.put("/<goal_id>")
def update_one_goal(goal_id):
goal = retrieve_model_inst_by_id(Goal, goal_id)
request_body = request.get_json()

goal.title = request_body["title"]
db.session.commit()
return Response(status=204, mimetype="application/json")

# DELETE ONE GOAL
@bp.delete("/<goal_id>")
def delete_one_goal(goal_id):
goal = retrieve_model_inst_by_id(Goal, goal_id)
db.session.delete(goal)
db.session.commit()

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


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

def retrieve_model_inst_by_id(cls, model_id):
try:
model_id = int(model_id)
except:
response_message = {"message": f"{cls.__name__} id <{model_id}> is invalid."}
abort(make_response(response_message, 400))

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

if not model:
response_message = {"message": f"{cls.__name__} with id <{model_id}> is not found."}
abort(make_response(response_message, 404))

return model

def create_model_inst_from_dict_with_response(cls, inst_data):
try:
new_instance = cls.from_dict(inst_data)
except KeyError as error:
response = {"details": "Invalid data"}
abort(make_response(response, 400))

db.session.add(new_instance)
db.session.commit()
response = {f"{cls.__name__.lower()}": new_instance.to_dict()}
return response, 201

def nested_dict(cls, instance):
return {f"{cls.__name__.lower()}": instance.to_dict()}

132 changes: 131 additions & 1 deletion app/routes/task_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,131 @@
from flask import Blueprint
from flask import Blueprint, request, Response, make_response, abort
from sqlalchemy import desc
from datetime import datetime, timezone
import requests
from app.routes.routes_helper_utilities import create_model_inst_from_dict_with_response, retrieve_model_inst_by_id
from app.models.task import Task
from app.db import db
import os

slack_token = os.environ.get("SLACKBOT_TOKEN")

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


# CREATE ONE TASK
@bp.post("")
def create_task():
request_body = request.get_json()
return create_model_inst_from_dict_with_response(Task, request_body)


# READ ALL TASKS
@bp.get("")
def get_all_tasks():
query = db.select(Task)

sort_param = request.args.get("sort")
if sort_param=="desc":
query = query.order_by(desc(Task.title))
else:
query = query.order_by(Task.title)

query = query.order_by(Task.id)
tasks = db.session.scalars(query)

response = []
for task in tasks:
response.append(task.to_dict())

return response


# READ ONE TASK BY ID
@bp.get("/<task_id>")
def get_task_by_id(task_id):
task = retrieve_model_inst_by_id(Task, task_id)

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


# UPDATE ONE TASK
@bp.put("/<task_id>")
def update_by_id(task_id):
task = retrieve_model_inst_by_id(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")


# PATCH ONE TASK
@bp.patch("/<task_id>")
def patch_by_id(task_id):
task = retrieve_model_inst_by_id(Task, task_id)
request_body = request.get_json()

if "title" in request_body:
task.title = request_body["title"]
if "description" in request_body:
task.description = request_body["description"]
if "completed_at" in request_body:
task.completed_at = request_body["completed_at"]

db.session.commit()

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


# PATCH ONE TASK MARK IT AS COMPLETE
@bp.patch("/<task_id>/mark_complete")
def patch_by_id_mark_complete(task_id):
task = retrieve_model_inst_by_id(Task, task_id)

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

# send notification to Slack
headers = {"Authorization": f"Bearer {slack_token}"}
data = {
"channel": "C08NTC26TM1",
"text": f"Someone just completed the task -- {task.title}: {task.description}"
}

requests.post("https://slack.com/api/chat.postMessage", headers=headers, data=data)

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


# PATCH ONE TASK MARK IT AS INCOMPLETE
@bp.patch("/<task_id>/mark_incomplete")
def patch_by_id_mark_incomplete(task_id):
task = retrieve_model_inst_by_id(Task, task_id)

task.completed_at = None
db.session.commit()

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


# DELETE ONE TASK
@bp.delete("/<task_id>")
def delete_by_id(task_id):
task = retrieve_model_inst_by_id(Task, task_id)

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

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


# DELETE ALL TASKS
@bp.delete("")
def delete_all_tasks():
db.session.query(Task).delete()
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