-
Notifications
You must be signed in to change notification settings - Fork 44
Maybellene Aung C22 task list #42
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 12 commits
2619e34
f153a4d
7fb6281
5376631
6cd79bc
4ab176d
4fc1c43
011c739
e5fa34d
5a6b411
224ce9e
ff1ccbb
3234ded
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,31 @@ | ||||||||||||||||||||
from sqlalchemy.orm import Mapped, mapped_column | ||||||||||||||||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship | ||||||||||||||||||||
from ..db import db | ||||||||||||||||||||
from flask import abort, make_response | ||||||||||||||||||||
|
||||||||||||||||||||
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 obj_to_dict(self): | ||||||||||||||||||||
goal_as_dict = {} | ||||||||||||||||||||
goal_as_dict["id"] = self.id | ||||||||||||||||||||
goal_as_dict["title"] = self.title | ||||||||||||||||||||
|
||||||||||||||||||||
return goal_as_dict | ||||||||||||||||||||
Comment on lines
+11
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. Can also directly return a dictionary literal
Suggested change
|
||||||||||||||||||||
|
||||||||||||||||||||
|
||||||||||||||||||||
@classmethod | ||||||||||||||||||||
def obj_from_dict(cls, goal_data): | ||||||||||||||||||||
title = goal_data.get("title", None) | ||||||||||||||||||||
|
||||||||||||||||||||
if not title: | ||||||||||||||||||||
response = {"details": "Invalid data"} | ||||||||||||||||||||
abort(make_response(response, 400)) | ||||||||||||||||||||
|
||||||||||||||||||||
new_goal = cls( | ||||||||||||||||||||
title=title | ||||||||||||||||||||
) | ||||||||||||||||||||
|
||||||||||||||||||||
return new_goal | ||||||||||||||||||||
|
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 ..db import db | ||||||
from datetime import datetime | ||||||
from typing import Optional | ||||||
from flask import make_response, abort | ||||||
from sqlalchemy import ForeignKey | ||||||
|
||||||
class Task(db.Model): | ||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) | ||||||
title: Mapped[str] | ||||||
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. As with |
||||||
description: Mapped[str] | ||||||
completed_at: Mapped[Optional[datetime]] | ||||||
goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id")) | ||||||
goal: Mapped[Optional["Goal"]] = relationship(back_populates="tasks") | ||||||
|
||||||
|
||||||
# book object to dict representation | ||||||
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. Task object? |
||||||
def obj_to_dict(self): | ||||||
task_as_dict = {} | ||||||
task_as_dict["id"] = self.id | ||||||
if self.goal: | ||||||
task_as_dict["goal_id"] = self.goal.id | ||||||
task_as_dict["title"] = self.title | ||||||
task_as_dict["description"] = self.description | ||||||
task_as_dict["is_complete"] = True if self.completed_at is not None else False | ||||||
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. Consider encapsulating this logic into a helper function, maybe something like
Suggested change
|
||||||
|
||||||
return task_as_dict | ||||||
|
||||||
@classmethod | ||||||
# create instance from request body | ||||||
def obj_from_dict(cls, task_data): | ||||||
title = task_data.get("title", None) | ||||||
description = task_data.get("description", None) | ||||||
|
||||||
if not title or not description: | ||||||
response = {"details": "Invalid data"} | ||||||
abort(make_response(response, 400)) | ||||||
|
||||||
new_task = cls( | ||||||
title=title, | ||||||
description=description, | ||||||
completed_at=task_data.get("completed_at", None) | ||||||
|
||||||
) | ||||||
|
||||||
return new_task |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1 +1,89 @@ | ||||||
from flask import Blueprint | ||||||
from flask import Blueprint, Response, request, abort, make_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. Response, abort, and make_response are imported but never accessed so should be removed. |
||||||
from ..models.goal import Goal | ||||||
from ..db import db | ||||||
from ..models.task import Task | ||||||
from .route_utilities import validate_model | ||||||
|
||||||
bp = Blueprint("goals_bp", __name__, url_prefix="/goals") | ||||||
|
||||||
@bp.post("") | ||||||
def create_goal(): | ||||||
request_body = request.get_json() | ||||||
new_goal = Goal.obj_from_dict(request_body) | ||||||
|
||||||
db.session.add(new_goal) | ||||||
db.session.commit() | ||||||
response = {"goal": new_goal.obj_to_dict()} | ||||||
return response, 201 | ||||||
|
||||||
@bp.post("/<goal_id>/tasks") | ||||||
def add_tasks_to_goals(goal_id): | ||||||
goal = validate_model(Goal, goal_id) | ||||||
request_body = request.get_json() | ||||||
task_ids = request_body["task_ids"] | ||||||
tasks = Task.query.filter(Task.id.in_(task_ids)).all() | ||||||
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. The Review the docs here Here, I'd probably use your validate_model method since
Suggested change
|
||||||
goal.tasks = tasks | ||||||
|
||||||
db.session.commit() | ||||||
response = { | ||||||
"id": goal.id, | ||||||
"task_ids": task_ids | ||||||
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. Notice that You'd have to grab tasks from goal like: tasks_from_goal = goal.tasks
task_ids_from_goal = [task.id for task in tasks_from_goal]
return {
"id": int(goal_id),
"task_ids": task_ids_from_goal
} |
||||||
} | ||||||
|
||||||
return response, 200 | ||||||
|
||||||
|
||||||
@bp.get("") | ||||||
def get_all_goals(): | ||||||
query = db.select(Goal).order_by(Goal.id) | ||||||
goals = db.session.scalars(query) | ||||||
|
||||||
goals_response = [goal.obj_to_dict() for goal in goals] | ||||||
return goals_response | ||||||
|
||||||
@bp.get("/<goal_id>") | ||||||
def get_one_goal(goal_id): | ||||||
goal = validate_model(Goal, goal_id) | ||||||
|
||||||
return {"goal": goal.obj_to_dict()}, 200 | ||||||
|
||||||
# get all tasks of a certain goal | ||||||
@bp.get("/<goal_id>/tasks") | ||||||
def get_all_tasks_of_goal(goal_id): | ||||||
goal = validate_model(Goal, goal_id) | ||||||
|
||||||
tasks_list = [task.obj_to_dict() for task in goal.tasks] | ||||||
print(tasks_list) | ||||||
response = { | ||||||
"id": goal.id, | ||||||
"title": goal.title, | ||||||
"tasks": tasks_list | ||||||
} | ||||||
|
||||||
return response, 200 | ||||||
|
||||||
|
||||||
@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"] | ||||||
|
||||||
db.session.commit() | ||||||
|
||||||
response = {"goal": goal.obj_to_dict()} | ||||||
return response, 200 | ||||||
|
||||||
|
||||||
@bp.delete("/<goal_id>") | ||||||
def delete_one_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 | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
from flask import abort, make_response | ||
from ..db import db | ||
|
||
def validate_model(cls, model_id): | ||
try: | ||
model_id = int(model_id) | ||
except: | ||
response = {"message": f"{cls.__name__} {model_id} is 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} was not found"} | ||
abort(make_response(response, 404)) | ||
|
||
return model |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,118 @@ | ||
from flask import Blueprint | ||
from flask import Blueprint, request, Response, abort, make_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. Response, is imported but never accessed so should be removed. |
||
from ..models.task import Task | ||
from ..db import db | ||
from datetime import datetime | ||
import requests | ||
import os | ||
|
||
|
||
bp = Blueprint("tasks_bp", __name__, url_prefix="/tasks") | ||
|
||
# make a POST request to /tasks with the following HTTP request body | ||
@bp.post("") | ||
def create_task(): | ||
request_body = request.get_json() | ||
new_task = Task.obj_from_dict(request_body) | ||
|
||
db.session.add(new_task) | ||
db.session.commit() | ||
|
||
response = {"task": new_task.obj_to_dict()} | ||
|
||
return response, 201 | ||
|
||
# GET request to /tasks | ||
@bp.get("") | ||
def get_all_task(): | ||
query = db.select(Task) | ||
sort = request.args.get("sort") | ||
|
||
if sort == "asc": | ||
query = query.order_by(Task.title.asc()) | ||
elif sort == "desc": | ||
query = query.order_by(Task.title.desc()) | ||
else: | ||
query = query.order_by(Task.id) | ||
tasks = db.session.scalars(query) | ||
|
||
tasks_response = [task.obj_to_dict() for task in tasks] | ||
|
||
return tasks_response | ||
|
||
@bp.get("/<task_id>") | ||
def get_one_task(task_id): | ||
task = validate_model(Task, task_id) | ||
|
||
return {"task": task.obj_to_dict()}, 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.obj_to_dict()} | ||
|
||
@bp.patch("/<task_id>/mark_complete") | ||
def update_complete(task_id): | ||
task = validate_model(Task, task_id) | ||
task.completed_at = datetime.utcnow() | ||
|
||
db.session.commit() | ||
|
||
path = "https://slack.com/api/chat.postMessage" | ||
token = os.environ.get("SLACK_BOT_TOKEN") | ||
channel_id = "C07V4J7ABF1" | ||
Comment on lines
+67
to
+69
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 job using variables to reference these values so you don't have string literals embedded below. Since these are constant variables, they should be named with all capital letters. |
||
|
||
headers = { | ||
"Authorization": f"Bearer {token}" | ||
} | ||
data = { | ||
"channel": channel_id, | ||
"text": f"Someone just completed the task {task.title}" | ||
} | ||
|
||
requests.post(path, headers=headers, json=data) | ||
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. Prefer all the logic relating to making a call to the Slack API be encapsulated in a helper function (maybe something like |
||
|
||
response = {"task": task.obj_to_dict()} | ||
return response, 200 | ||
|
||
|
||
@bp.patch("/<task_id>/mark_incomplete") | ||
def update_incomplete(task_id): | ||
task = validate_model(Task, task_id) | ||
task.completed_at = None | ||
|
||
db.session.commit() | ||
|
||
response = {"task": task.obj_to_dict()} | ||
return response, 200 | ||
|
||
@bp.delete("/<task_id>") | ||
def delete_task(task_id): | ||
task = validate_model(Task, task_id) | ||
db.session.delete(task) | ||
db.session.commit() | ||
|
||
return {"details": f"Task {task_id} \"{task.title}\" successfully deleted"} | ||
|
||
def validate_model(Task, task_id): | ||
try: | ||
task_id = int(task_id) | ||
except: | ||
response = {"message": f"Task {task_id} is invalid"} | ||
abort(make_response(response, 400)) | ||
|
||
query = db.select(Task).where(Task.id == task_id) | ||
task = db.session.scalar(query) | ||
|
||
if not task: | ||
response = {"message": f"Task {task_id} was not found"} | ||
abort(make_response(response, 404)) | ||
|
||
return task | ||
Comment on lines
+103
to
+117
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. Looks like this duplicates the logic in |
||
|
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.
Consider whether the this column for
Goal
should be nullable. It feels unconventional to allow someone to create a goal that gets saved to the DB without a title.