-
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
Sea Turtles - Tiffini H. #99
base: master
Are you sure you want to change the base?
Changes from all commits
b2e3e83
9cf350e
f01c1f8
04dcaea
484a576
0007e84
b632bf4
b88cb51
bd1ec2f
faa3165
cb32cc1
c84d03c
0a6acc2
3d21994
a43cd60
97b9776
3ce71c2
9054dc1
f3844c6
bd3256f
58d7595
8765a46
9fc8fa9
27ccc02
f831a72
e166d4a
3ea0767
0309358
ed07c6e
98512a1
51753f6
8e08494
8fe168f
b2cafb6
d4bac3f
7c6e7e4
ab086d2
b2a26e4
f58e31b
d91c67a
0a6677e
bc3e7a1
864a25a
147170a
4fe80da
16ba613
bac6ee6
06401c4
3d942c0
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 |
---|---|---|
@@ -0,0 +1,90 @@ | ||
from flask import Blueprint, request, jsonify, make_response, abort | ||
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. It looks like the 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. Oh man, I thought I caught all of those! Thanks, Kelsey! |
||
from app import db | ||
from app.models.goal import Goal | ||
from app.models.task import Task | ||
from app.route_helpers import error_message, validate_model_instance | ||
|
||
goals_bp = Blueprint("goals_bp", __name__, url_prefix="/goals") | ||
|
||
# create a new goal | ||
@goals_bp.route("", methods=["POST"]) | ||
def create_goal(): | ||
request_body = request.get_json() | ||
|
||
# check for request body | ||
try: | ||
new_goal = Goal(title=request_body["title"]) | ||
except: | ||
error_message("Invalid data", 400) | ||
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 an error message when data is missing! I'd recommend adding explicit info about what key or keys are missing in the response message to help guide the user to what was wrong in their request. The info could be as little as: create_message("Invalid data: title is missing", 400) 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. That's a really helpful reminder--thank you, Kelsey! |
||
|
||
# update database | ||
db.session.add(new_goal) | ||
db.session.commit() | ||
|
||
response_body = {"goal": new_goal.to_dict()} | ||
|
||
return make_response(jsonify(response_body), 201) | ||
|
||
# get all goals | ||
@goals_bp.route("", methods=["GET"]) | ||
def get_all_goals(): | ||
goals = Goal.query.all() | ||
goals_response = [goal.to_dict() for goal in goals] | ||
|
||
return make_response(jsonify(goals_response)) | ||
|
||
# get goal by id | ||
@goals_bp.route("/<goal_id>", methods=["GET"]) | ||
def get_goal_by_id(goal_id): | ||
goal = validate_model_instance(Goal, goal_id) | ||
return {"goal": goal.to_dict()} | ||
|
||
# update goal by id | ||
@goals_bp.route("/<goal_id>", methods=["PUT"]) | ||
def update_goal(goal_id): | ||
goal = validate_model_instance(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. When we get a KeyError trying to creating an object in |
||
|
||
db.session.commit() | ||
return {"goal": goal.to_dict()} | ||
|
||
# delete goal by id | ||
@goals_bp.route("/<goal_id>", methods=["DELETE"]) | ||
def delete_goal(goal_id): | ||
goal = validate_model_instance(Goal, goal_id) | ||
|
||
db.session.delete(goal) | ||
db.session.commit() | ||
|
||
return make_response({"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. This line looks a little long, I would consider breaking it over a couple lines, or pulling the message out into its own variable that we use on this line. 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 catch--thank you, Kelsey! |
||
|
||
# get tasks by goal id | ||
@goals_bp.route("/<goal_id>/tasks", methods=["GET"]) | ||
def get_tasks_for_goal(goal_id): | ||
goal = validate_model_instance(Goal, goal_id) | ||
task_list = [task.to_dict() for task in goal.tasks] | ||
|
||
goal_dict = goal.to_dict() | ||
goal_dict["tasks"] = task_list | ||
|
||
return jsonify(goal_dict) | ||
|
||
# post tasks to goal by goal id | ||
@goals_bp.route("/<goal_id>/tasks", methods=["POST"]) | ||
def post_tasks_to_goal(goal_id): | ||
goal = validate_model_instance(Goal, goal_id) | ||
request_body = request.get_json() | ||
|
||
for task_id in request_body["task_ids"]: | ||
task = Task.query.get(task_id) | ||
task.goal_id = goal_id | ||
task.goal = goal | ||
|
||
db.session.commit() | ||
|
||
return make_response({ | ||
"id": goal.goal_id, | ||
"task_ids": request_body["task_ids"] | ||
}) | ||
Comment on lines
+88
to
+90
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 works, but how could we modify the Goal's |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,3 +3,17 @@ | |
|
||
class Goal(db.Model): | ||
goal_id = db.Column(db.Integer, primary_key=True) | ||
title = db.Column(db.String, nullable=False) | ||
tasks = db.relationship("Task", back_populates="goal") | ||
|
||
def to_dict(self): | ||
goal_dict = { | ||
"id": self.goal_id, | ||
"title": self.title | ||
} | ||
|
||
if self.tasks: | ||
goal_dict["tasks"] = [task.task_id for task in self.tasks] | ||
Comment on lines
+15
to
+16
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 way to add the attribute that won't always exist! |
||
# goal_dict["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. We want to make sure to delete commented out code as part of clean up. When we are sharing a codebase with other folks, it's unclear what the intention of commented out code is, especially without extra comments explaining why it's there. We use versioning tools like git so we can confidently delete code and view our commit history if we need to recover something we wrote prior. 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. Noted--thank you for catching this! |
||
|
||
return goal_dict |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,3 +3,25 @@ | |
|
||
class Task(db.Model): | ||
task_id = db.Column(db.Integer, primary_key=True) | ||
title = db.Column(db.String) | ||
description = db.Column(db.String) | ||
completed_at = db.Column(db.DateTime, nullable=True, default=None) | ||
goal_id = db.Column(db.Integer, db.ForeignKey("goal.goal_id")) | ||
goal = db.relationship("Goal", back_populates="tasks") | ||
|
||
def to_dict(self): | ||
task_dict = { | ||
"id": self.task_id, | ||
"title": self.title, | ||
"description": self.description, | ||
} | ||
|
||
if not self.completed_at: | ||
task_dict["is_complete"] = 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. Nice choice to derive the value of |
||
else: | ||
task_dict["is_complete"] = True | ||
Comment on lines
+19
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. It isn't necessary, but we could use a ternary operator here: task_dict["is_complete"] = True if self.completed_at else False or the task_dict["is_complete"] = bool(self.completed_at) |
||
|
||
if self.goal_id: | ||
task_dict["goal_id"] = self.goal_id | ||
|
||
return task_dict |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
from flask import jsonify, make_response, abort | ||
from app.models.goal import Goal | ||
from app.models.task import Task | ||
import requests, os | ||
|
||
# helper function to generate error message | ||
def error_message(message, status_code): | ||
abort(make_response(jsonify(dict(details=message)), status_code)) | ||
|
||
# class/model-agnostic helper function to validate model instances | ||
def validate_model_instance(model, 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. Love to see these more generic functions that work with both models! |
||
if model == Task: | ||
model_name = "Task" | ||
elif model == Goal: | ||
model_name = "Goal" | ||
|
||
try: | ||
id = int(id) | ||
except: | ||
error_message(f"{model_name} #{id} invalid", 400) | ||
|
||
model_instance = model.query.get(id) | ||
|
||
if not model_instance: | ||
error_message(f"{model_name} #{id} not found", 404) | ||
|
||
return model_instance | ||
|
||
# helper function to post completion message to slack | ||
def post_slack_completion_message(task_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 helper for the Slack API call! |
||
task = validate_model_instance(Task, task_id) | ||
path = "https://slack.com/api/chat.postMessage" | ||
SLACK_API_KEY = os.environ.get("SLACK_BOT_TOKEN") | ||
|
||
request_headers = {"Authorization": f"Bearer {SLACK_API_KEY}"} | ||
request_body = { | ||
"channel": "C03EP2Q0WK1", | ||
"text": f"Someone just completed the task {task.title}" | ||
} | ||
|
||
requests.post(path, headers=request_headers, json=request_body) |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
from flask import Blueprint, request, jsonify, make_response, abort | ||
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 about |
||
from sqlalchemy import desc | ||
from app import db | ||
from app.models.task import Task | ||
from datetime import date | ||
from app.route_helpers import error_message, validate_model_instance, post_slack_completion_message | ||
|
||
tasks_bp = Blueprint("tasks_bp", __name__, url_prefix="/tasks") | ||
|
||
# create a new task | ||
@tasks_bp.route("", methods=["POST"]) | ||
def create_task(): | ||
request_body = request.get_json() | ||
|
||
try: | ||
new_task = Task( | ||
title=request_body["title"], | ||
description=request_body["description"] | ||
) | ||
except: | ||
error_message("Invalid data", 400) | ||
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 in |
||
|
||
if request_body.get("completed_at"): | ||
new_task.completed_at = request_body.get("completed_at") | ||
|
||
db.session.add(new_task) | ||
db.session.commit() | ||
|
||
response_body = {"task": new_task.to_dict()} | ||
|
||
return make_response(jsonify(response_body), 201) | ||
|
||
# retrieve all tasks | ||
@tasks_bp.route("", methods=["GET"]) | ||
def get_all_tasks(): | ||
sort_param = request.args.get("sort") | ||
|
||
if sort_param == "asc": | ||
tasks = Task.query.order_by(Task.title).all() | ||
elif sort_param == "desc": | ||
tasks = Task.query.order_by(desc(Task.title)).all() | ||
else: | ||
tasks = Task.query.all() | ||
|
||
tasks_response = [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! 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. Thank you! I've been working on using list comprehension more, so that's really exciting to hear! |
||
|
||
return jsonify(tasks_response) | ||
|
||
# retrieve one task by id | ||
@tasks_bp.route("/<task_id>", methods=["GET"]) | ||
def get_one_task_by_id(task_id): | ||
task = validate_model_instance(Task, task_id) | ||
|
||
return {"task": task.to_dict()} | ||
|
||
# update one task by id | ||
@tasks_bp.route("/<task_id>", methods=["PUT"]) | ||
def update_one_task_by_id(task_id): | ||
task = validate_model_instance(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()} | ||
|
||
# delete one task by id | ||
@tasks_bp.route("/<task_id>", methods=["DELETE"]) | ||
def delete_task_by_id(task_id): | ||
task = validate_model_instance(Task, task_id) | ||
|
||
db.session.delete(task) | ||
db.session.commit() | ||
|
||
return make_response({"details": f'Task {task_id} "{task.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 like the informative messages. |
||
|
||
# mark one task complete by id | ||
@tasks_bp.route("/<task_id>/mark_complete", methods=["PATCH"]) | ||
def mark_task_complete(task_id): | ||
task = validate_model_instance(Task, task_id) | ||
|
||
task.completed_at = date.today() | ||
response_body = {"task": task.to_dict()} | ||
|
||
db.session.commit() | ||
post_slack_completion_message(task_id) | ||
|
||
return make_response(jsonify(response_body), 200) | ||
|
||
# mark one task incomplete by id | ||
@tasks_bp.route("/<task_id>/mark_incomplete", methods=["PATCH"]) | ||
def mark_task_incomplete(task_id): | ||
task = validate_model_instance(Task, task_id) | ||
|
||
task.completed_at = None | ||
response_body = {"task": task.to_dict()} | ||
|
||
db.session.commit() | ||
return make_response(jsonify(response_body), 200) |
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!