-
Notifications
You must be signed in to change notification settings - Fork 111
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
Olive - Task List - Sea Turtles #94
base: master
Are you sure you want to change the base?
Changes from all commits
b4764cd
35fe688
43e2ed0
b1da897
b22b6d7
eab53c8
3e363d9
c9280f4
cfa4707
d60a4a3
2db8172
084b107
c1c175e
ea54fd2
ee4f599
b2b24c5
22c97ef
c390f1a
e99c9b5
7766abf
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 |
---|---|---|
@@ -0,0 +1 @@ | ||
web: gunicorn 'app:create_app()' |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,4 +2,31 @@ | |
|
||
|
||
class Goal(db.Model): | ||
goal_id = db.Column(db.Integer, primary_key=True) | ||
id = db.Column(db.Integer, primary_key=True) | ||
title = db.Column(db.String) | ||
tasks = db.relationship("Task", backref="goal", lazy=True) | ||
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. If we check out the SQLAlchemy docs for loading items: https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html the docs say:
Doing some more digging about the possible lazy parameter values we might find further info like in https://medium.com/@ns2586/sqlalchemys-relationship-and-lazy-parameter-4a553257d9ef where we see the line:
Putting these together we can see that the default value for tasks = db.relationship("Task", back_populates="goal") |
||
|
||
|
||
def to_dict(self): | ||
return dict( | ||
id = self.id, | ||
title = self.title | ||
) | ||
|
||
def to_dict_with_tasks(self): | ||
tasks_info = [task.to_dict() for task in self.tasks] | ||
return dict( | ||
id = self.id, | ||
title = self.title, | ||
tasks = tasks_info | ||
) | ||
Comment on lines
+10
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. If we added a new attribute to def to_dict(self, include_tasks):
goal_dict = dict(
id = self.id,
title = self.title
)
if include_tasks:
tasks_info = [task.to_dict() for task in self.tasks]
goal_dict["tasks"] = tasks_info
return goal_dict |
||
|
||
@classmethod | ||
def from_dict(cls, data_dict): | ||
return cls( | ||
title = data_dict["title"], | ||
) | ||
|
||
def replace_details(self, data_dict): | ||
self.title = data_dict["title"] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,4 +2,38 @@ | |
|
||
|
||
class Task(db.Model): | ||
task_id = db.Column(db.Integer, primary_key=True) | ||
id = db.Column(db.Integer, primary_key=True, autoincrement=True) | ||
title = db.Column(db.String) | ||
description = db.Column(db.String) | ||
completed_at = db.Column(db.DateTime) | ||
goal_id = db.Column(db.Integer, db.ForeignKey("goal.id")) | ||
|
||
|
||
def to_dict(self): | ||
if self.goal_id: | ||
return dict( | ||
id = self.id, | ||
goal_id = self.goal_id, | ||
title = self.title, | ||
description = self.description, | ||
is_complete = bool(self.completed_at) | ||
) | ||
else: | ||
return dict( | ||
id = self.id, | ||
title = self.title, | ||
description = self.description, | ||
is_complete = bool(self.completed_at) | ||
) | ||
Comment on lines
+12
to
+27
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 feedback on |
||
@classmethod | ||
def from_dict(cls, data_dict): | ||
completed_time = data_dict["completed_at"] if "completed_at" in data_dict else 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. Nice handling for the optional parameter! |
||
return cls( | ||
title = data_dict["title"], | ||
description = data_dict["description"], | ||
completed_at = completed_time | ||
) | ||
|
||
def replace_details(self, data_dict): | ||
self.title = data_dict["title"] | ||
self.description = data_dict["description"] |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
from app.models.goal import Goal | ||
from app.models.task import Task | ||
from flask import Blueprint, jsonify, request | ||
from app import db | ||
from .routes_helper import get_record_by_id, make_goal_safely, replace_goal_safely | ||
|
||
goals_bp = Blueprint("goals_bp", __name__, url_prefix="/goals") | ||
|
||
# POST /goals | ||
@goals_bp.route("", methods = ["POST"]) | ||
def create_goal(): | ||
request_body = request.get_json() | ||
new_goal = make_goal_safely(request_body) | ||
|
||
db.session.add(new_goal) | ||
db.session.commit() | ||
|
||
return jsonify({"goal": new_goal.to_dict()}), 201 | ||
|
||
# GET /goals | ||
@goals_bp.route("", methods=["GET"]) | ||
def read_all_goals(): | ||
goals = Goal.query.all() | ||
|
||
result_list = [goal.to_dict() for goal in goals] | ||
|
||
return jsonify(result_list) | ||
|
||
# GET /goals/<id> | ||
@goals_bp.route("/<id>", methods=["Get"]) | ||
def read_goal_by_id(id): | ||
goal = get_record_by_id(Goal, id) | ||
return jsonify({"goal":goal.to_dict()}) | ||
|
||
# PUT /goals/<id> | ||
@goals_bp.route("/<id>", methods=["PUT"]) | ||
def replace_goal_by_id(id): | ||
request_body = request.get_json() | ||
goal = get_record_by_id(Goal, id) | ||
|
||
replace_goal_safely(goal, request_body) | ||
|
||
db.session.add(goal) | ||
db.session.commit() | ||
|
||
return jsonify({"goal":goal.to_dict()}) | ||
|
||
# DELETE /goals/<id> | ||
@goals_bp.route("/<id>", methods=["DELETE"]) | ||
def delete_goal_by_id(id): | ||
goal = get_record_by_id(Goal, id) | ||
|
||
db.session.delete(goal) | ||
db.session.commit() | ||
|
||
return jsonify({"details": f'Goal {goal.id} "{goal.title}" successfully deleted'}) | ||
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, I appreciate the informative messages 👍 |
||
|
||
# POST /goals/<id>/tasks | ||
@goals_bp.route("/<id>/tasks", methods=["POST"]) | ||
def post_tasks_to_goal(id): | ||
request_body = request.get_json() | ||
goal = get_record_by_id(Goal, id) | ||
|
||
task_ids = request_body["task_ids"] | ||
|
||
for id in task_ids: | ||
task = get_record_by_id(Task, id) | ||
task.goal = goal | ||
|
||
db.session.commit() | ||
|
||
task_list = [task.id for task in goal.tasks ] | ||
|
||
return(jsonify({"id":goal.id, "task_ids": task_list})) | ||
|
||
# GET /goals/<id>/tasks | ||
@goals_bp.route("/<id>/tasks", methods=["GET"]) | ||
def read_tasks_from_goal(id): | ||
goal = get_record_by_id(Goal, id) | ||
|
||
return jsonify(goal.to_dict_with_tasks()) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
from flask import jsonify, abort, make_response | ||
from app.models.goal import Goal | ||
from app.models.task import Task | ||
|
||
def error_message(message, status_code): | ||
abort(make_response(jsonify(dict(details=message)), status_code)) | ||
|
||
def get_record_by_id(cls, 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. Nice use of a more generic, flexible function! |
||
try: | ||
id = int(id) | ||
except ValueError: | ||
error_message(f"Invalid id {id}", 400) | ||
|
||
model = cls.query.get(id) | ||
if model: | ||
return model | ||
|
||
error_message(f"No model of type {cls} with id {id} found", 404) | ||
|
||
def make_task_safely(data_dict): | ||
try: | ||
return Task.from_dict(data_dict) | ||
except KeyError as err: | ||
error_message(f"Missing key: {err}", 400) | ||
|
||
def replace_task_safely(task, data_dict): | ||
try: | ||
task.replace_details(data_dict) | ||
except KeyError as err: | ||
error_message(f"Missing key: {err}", 400) | ||
|
||
def make_goal_safely(data_dict): | ||
try: | ||
return Goal.from_dict(data_dict) | ||
except KeyError as err: | ||
error_message(f"Missing key: {err}", 400) | ||
|
||
def replace_goal_safely(goal, data_dict): | ||
try: | ||
goal.replace_details(data_dict) | ||
except KeyError as err: | ||
error_message(f"Missing key: {err}", 400) | ||
Comment on lines
+20
to
+42
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. Since |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
from datetime import datetime | ||
from flask import Blueprint, jsonify, request | ||
from app.models.task import Task | ||
from app import db | ||
from .routes_helper import get_record_by_id, make_task_safely, replace_task_safely | ||
import os | ||
import requests | ||
from dotenv import load_dotenv | ||
|
||
load_dotenv() | ||
|
||
tasks_bp = Blueprint("tasks_bp", __name__, url_prefix="/tasks") | ||
|
||
def post_completed_task_to_slack(task): | ||
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 helper function for the Slack API call. |
||
API_KEY = os.environ.get('SLACKBOT_API_KEY') | ||
url = "https://slack.com/api/chat.postMessage" | ||
data = {"channel": "task-notifications", "text": f"Someone just completed the task {task.title}"} | ||
headers = {'Authorization' : f"Bearer {API_KEY}" } | ||
|
||
requests.post(url, data=data, headers=headers) | ||
|
||
# POST /tasks | ||
@tasks_bp.route("", methods = ["POST"]) | ||
def create_task(): | ||
request_body = request.get_json() | ||
new_task = make_task_safely(request_body) | ||
|
||
db.session.add(new_task) | ||
db.session.commit() | ||
|
||
return jsonify({"task": new_task.to_dict()}), 201 | ||
|
||
# GET /tasks | ||
@tasks_bp.route("", methods=["GET"]) | ||
def read_all_tasks(): | ||
sort_param = request.args.get("sort") | ||
|
||
if sort_param == 'asc': | ||
tasks = Task.query.order_by(Task.title.asc()) | ||
elif sort_param == 'desc': | ||
tasks = Task.query.order_by(Task.title.desc()) | ||
else: | ||
tasks = Task.query.all() | ||
|
||
result_list = [task.to_dict() for task in tasks] | ||
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. Great use of list comprehensions across the project. |
||
|
||
return jsonify(result_list) | ||
|
||
# GET /tasks/<id> | ||
@tasks_bp.route("/<id>", methods=["Get"]) | ||
def read_task_by_id(id): | ||
task = get_record_by_id(Task, id) | ||
return jsonify({"task":task.to_dict()}) | ||
|
||
# PUT /tasks/<id> | ||
@tasks_bp.route("/<id>", methods=["PUT"]) | ||
def replace_task_by_id(id): | ||
request_body = request.get_json() | ||
task = get_record_by_id(Task, id) | ||
|
||
replace_task_safely(task, request_body) | ||
|
||
db.session.add(task) | ||
db.session.commit() | ||
|
||
return jsonify({"task":task.to_dict()}) | ||
|
||
# DELETE /tasks/<id> | ||
@tasks_bp.route("/<id>", methods=["DELETE"]) | ||
def delete_task_by_id(id): | ||
task = get_record_by_id(Task, id) | ||
|
||
db.session.delete(task) | ||
db.session.commit() | ||
|
||
return jsonify({"details": f'Task {task.id} "{task.title}" successfully deleted'}) | ||
|
||
# PATCH /tasks/<id>/mark_complete | ||
@tasks_bp.route("/<id>/mark_complete", methods=["PATCH"]) | ||
def update_task_to_complete(id): | ||
task = get_record_by_id(Task, id) | ||
|
||
task.completed_at = datetime.now() | ||
|
||
db.session.commit() | ||
|
||
post_completed_task_to_slack(task) | ||
|
||
return jsonify({"task":task.to_dict()}) | ||
|
||
# PATCH /tasks/<id>/mark_incomplete | ||
@tasks_bp.route("/<id>/mark_incomplete", methods=["PATCH"]) | ||
def update_task_to_incomplete(id): | ||
task = get_record_by_id(Task, id) | ||
task.completed_at = None | ||
|
||
db.session.commit() | ||
|
||
return jsonify({"task":task.to_dict()}) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Generic single-database configuration. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# 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 | ||
|
||
[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 | ||
|
||
[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, I like the choice to divide the routes into their own files!