-
Notifications
You must be signed in to change notification settings - Fork 44
Olga_Karaivanska_task_list_api #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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): | ||
__tablename__ = 'goal' | ||
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 { | ||
"id": self.id, | ||
"title": self.title | ||
} | ||
|
||
@classmethod | ||
def from_dict(cls, goal_data): | ||
return cls(title=goal_data["title"]) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,32 @@ | ||
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 | ||
|
||
|
||
class Task(db.Model): | ||
__tablename__ = 'task' | ||
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") | ||
|
||
def to_dict(self): | ||
return { | ||
"id": self.id, | ||
"title": self.title, | ||
"description": self.description, | ||
"is_complete": self.completed_at is not None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ⭐️ |
||
} | ||
|
||
@classmethod | ||
def from_dict(cls, task_data): | ||
return cls( | ||
title=task_data["title"], | ||
description=task_data["description"], | ||
completed_at=datetime.now() if task_data.get("is_complete") else None, | ||
goal_id=task_data.get("goal_id", None) | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,94 @@ | ||
from flask import Blueprint | ||
from flask import Blueprint, request, abort, make_response | ||
from app.models.goal import Goal | ||
from app.models.task import Task | ||
from .route_utilities import validate_model, create_model, get_models_with_filters | ||
from ..db import db | ||
|
||
bp = Blueprint("goal_bp", __name__, url_prefix="/goals") | ||
|
||
@bp.post("") | ||
def create_goal(): | ||
request_body = request.get_json() | ||
if "title" not in request_body or not request_body["title"]: | ||
return {"details": "Invalid data"}, 400 | ||
|
||
goal_data = {"title": request_body.get("title")} | ||
Comment on lines
+12
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This logic could go inside of our |
||
goal_dict, status_code = create_model(Goal, goal_data) | ||
|
||
return {"goal": goal_dict}, status_code | ||
|
||
@bp.get("") | ||
def get_goal(): | ||
|
||
goals_response = get_models_with_filters(Goal) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice work! |
||
return goals_response, 200 | ||
|
||
|
||
@bp.get("/<goal_id>") | ||
def get_one_goal(goal_id): | ||
goal = validate_model(Goal, goal_id) | ||
|
||
return {"goal":goal.to_dict()} | ||
|
||
|
||
@bp.put("<goal_id>") | ||
def update_one_goal(goal_id): | ||
goal = validate_model(Goal, goal_id) | ||
request_body = request.get_json() | ||
|
||
goal.title = request_body["title"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this code similar to our other |
||
db.session.commit() | ||
|
||
return {"goal": goal.to_dict()}, 200 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We essentially just want an outer dictionary structure with a |
||
|
||
|
||
@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 | ||
|
||
@bp.post("/<goal_id>/tasks") | ||
def associate_tasks_with_goal(goal_id): | ||
goal = validate_model(Goal, goal_id) | ||
request_body = request.get_json() | ||
task_ids = request_body.get("task_ids", []) | ||
|
||
for task_id in task_ids: | ||
task = validate_model(Task,task_id) | ||
task.goal_id = goal.id | ||
|
||
db.session.commit() | ||
|
||
return { | ||
"id": goal.id, | ||
"task_ids": task_ids | ||
}, 200 | ||
|
||
@bp.get("/<goal_id>/tasks") | ||
def get_tasks_of_goal(goal_id): | ||
goal = validate_model(Goal, goal_id) | ||
|
||
tasks_data = [ | ||
{ | ||
"id": task.id, | ||
"goal_id": task.goal_id, | ||
"title": task.title, | ||
"description": task.description, | ||
"is_complete": bool(task.completed_at) | ||
} | ||
Comment on lines
+79
to
+85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Couldn't we use our |
||
for task in goal.tasks | ||
] | ||
|
||
return { | ||
"id": goal.id, | ||
"title": goal.title, | ||
"tasks": tasks_data | ||
}, 200 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
from flask import abort, make_response | ||
from ..db import db | ||
|
||
def validate_model(cls, model_id): | ||
try: | ||
model_id = int(model_id) | ||
except: | ||
abort(make_response({"message":f"{cls.__name__} id {(model_id)} invalid"}, 400)) | ||
|
||
query = db.select(cls).where(cls.id == model_id) | ||
model = db.session.scalar(query) | ||
|
||
if not model: | ||
abort(make_response({ "message": 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, ValueError): | ||
response = {"details": "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, sort_by=None, sort_order='asc'): | ||
query = db.select(cls) | ||
|
||
if filters: | ||
for attribute, value in filters.items(): | ||
if hasattr(cls, attribute): | ||
query = query.where(getattr(cls, attribute).ilike(f"%{value}%")) | ||
if sort_by and hasattr(cls, sort_by): | ||
sort_attr = getattr(cls, sort_by) | ||
if sort_order == 'asc': | ||
query = query.order_by(sort_attr.asc()) | ||
elif sort_order == 'desc': | ||
query = query.order_by(sort_attr.desc()) | ||
else: | ||
query = query.order_by(cls,id) | ||
|
||
|
||
models = db.session.scalars(query.order_by(cls.id)) | ||
models_response = [model.to_dict() for model in models] | ||
|
||
return models_response | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice work with these functions! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,98 @@ | ||
from flask import Blueprint | ||
from flask import Blueprint, abort, make_response, Response, request | ||
import requests | ||
from app.models.task import Task | ||
from datetime import datetime | ||
from sqlalchemy import asc, desc | ||
from .route_utilities import validate_model, create_model, get_models_with_filters | ||
from ..db import db | ||
import os | ||
|
||
|
||
bp = Blueprint("tasks_bp", __name__, url_prefix="/tasks") | ||
|
||
@bp.post("") | ||
def create_task(): | ||
request_body = request.get_json() | ||
|
||
if "title" not in request_body or "description" not in request_body: | ||
return {"details": "Invalid data"}, 400 | ||
|
||
task_data = {"title": request_body.get("title"), | ||
"description": request_body.get("description"), | ||
"completed_at": request_body.get("completed_at")} | ||
Comment on lines
+20
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't necessarily need to do this part, we could pass in the request body since it has the |
||
|
||
task_dict, status_code = create_model(Task, task_data) | ||
return {"task": task_dict}, status_code | ||
|
||
@bp.get("") | ||
def get_task(): | ||
query_params = request.args.to_dict() | ||
sort_order = query_params.pop('sort', 'asc') | ||
sort_by = query_params.pop('sort_by', 'title') | ||
tasks_response = get_models_with_filters(Task, sort_by=sort_by, sort_order=sort_order) | ||
return tasks_response, 200 | ||
|
||
|
||
@bp.get("/<task_id>") | ||
def get_one_task(task_id): | ||
task = validate_model(Task, task_id) | ||
response = {"task": task.to_dict()} | ||
|
||
if task.goal_id is not None: | ||
response["task"]["goal_id"] = task.goal_id | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would put this as |
||
return response, 200 | ||
|
||
@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()}, 200 | ||
|
||
@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, 200 | ||
|
||
|
||
@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() | ||
|
||
slack_token = os.environ.get("SLACK_BOT_TOKEN") | ||
url = "https://slack.com/api/chat.postMessage" | ||
headers = {"Authorization": f"Bearer {slack_token}"} | ||
request_body = { | ||
"channel": "task-notifications", | ||
"text": f"Someone just completed the task '{task.title}'" | ||
} | ||
requests.post(url, json=request_body, headers=headers) | ||
|
||
return{"task": task.to_dict()}, 200 | ||
Comment on lines
+76
to
+85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would move this logic into a helper function and then that helper function into our utilities file to help with separation of concerns and readability. |
||
|
||
|
||
@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()} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Single-database configuration for Flask. |
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work on getting these columns made!