Skip to content
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

Open
wants to merge 49 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
b2e3e83
Completed setup
tiffinihyatt May 5, 2022
9cf350e
Created Task model
tiffinihyatt May 5, 2022
f01c1f8
Registered tasks_bp in __init__ file
tiffinihyatt May 5, 2022
04dcaea
Created create_task POST endpoint
tiffinihyatt May 5, 2022
484a576
Added to_dict method to Task model, passed wave 01
tiffinihyatt May 5, 2022
0007e84
Created get_all_tasks GET endpoint
tiffinihyatt May 6, 2022
b632bf4
Created validate_task helper function
tiffinihyatt May 6, 2022
b88cb51
Created get_one_task_by_id GET endpoint
tiffinihyatt May 6, 2022
bd1ec2f
Created update_one_task_by_id PUT endpoint
tiffinihyatt May 6, 2022
faa3165
Created delete_task_by_id DELETE endpoint
tiffinihyatt May 6, 2022
cb32cc1
Refactored create_task endpoint to handle missing title or description
tiffinihyatt May 6, 2022
c84d03c
Refactored validate_task helper function for consistent error messaging
tiffinihyatt May 6, 2022
0a6acc2
Adds ascending alphabetical sorting to get_all_tasks endpoint.
tiffinihyatt May 11, 2022
3d21994
Adds descending alphabetical sorting to get_all_tasks endpoint, passe…
tiffinihyatt May 11, 2022
a43cd60
Refactors Task model to_dict() method
tiffinihyatt May 12, 2022
97b9776
Creates mark_task_complete endpoint
tiffinihyatt May 12, 2022
3ce71c2
Creates mark_task_incomplete endpoint
tiffinihyatt May 12, 2022
9054dc1
Refactors create_task endpoint to allow for completion functionality
tiffinihyatt May 12, 2022
f3844c6
Refactors update_one_task_by_id endpoint to allow for completion func…
tiffinihyatt May 12, 2022
bd3256f
Refactors create_task and update_one_task_by_id endpoints (in progress)
tiffinihyatt May 12, 2022
58d7595
Refactors create_task endpoint to check for completed_at value, passe…
tiffinihyatt May 12, 2022
8765a46
Creates post_slack_completion_message helper function, passes wave 04
tiffinihyatt May 12, 2022
9fc8fa9
Create Goal model
tiffinihyatt May 12, 2022
27ccc02
Separate route files into goal_routes.py and task_routes.py
tiffinihyatt May 12, 2022
f831a72
Create goals bp, register in __init__ file
tiffinihyatt May 12, 2022
e166d4a
Create to_dict() method for Goal model
tiffinihyatt May 12, 2022
3ea0767
Add create_goal endpoint
tiffinihyatt May 13, 2022
0309358
Add get_all_goals() endpoint
tiffinihyatt May 13, 2022
ed07c6e
Create validate_goal helper function
tiffinihyatt May 13, 2022
98512a1
Create get_goal_by_id endpoint
tiffinihyatt May 13, 2022
51753f6
Create update_goal endpoint
tiffinihyatt May 13, 2022
8e08494
Refactor update_goal endpoint to upgrade db
tiffinihyatt May 13, 2022
8fe168f
Create delete_goal() endpoint
tiffinihyatt May 13, 2022
b2cafb6
Refactor create_goal() endpoint to handle empty request body, pass wa…
tiffinihyatt May 13, 2022
d4bac3f
Create one-to-many relationship between goals and tasks
tiffinihyatt May 13, 2022
7c6e7e4
Fix foreign key attribute error in Task model
tiffinihyatt May 13, 2022
ab086d2
Create get_tasks_for_goal endpoint
tiffinihyatt May 13, 2022
b2a26e4
Create post_tasks_to_goal endpoint
tiffinihyatt May 13, 2022
f58e31b
Pass all tests
tiffinihyatt May 13, 2022
d91c67a
Add Procfile for Heroku deployment
tiffinihyatt May 13, 2022
0a6677e
Add slack-sdk to project requirements
tiffinihyatt May 13, 2022
bc3e7a1
Add error_message helper function in route_helpers file
tiffinihyatt May 13, 2022
864a25a
Refactor endpoints to use error_message() helper function
tiffinihyatt May 13, 2022
147170a
Create validate_model_instance() helper function
tiffinihyatt May 13, 2022
4fe80da
Refactor task_routes.py to use helper functions
tiffinihyatt May 13, 2022
16ba613
Refactor goal_routes.py to use helper function
tiffinihyatt May 13, 2022
bac6ee6
Refactor post_slack_completion_message helper function
tiffinihyatt May 14, 2022
06401c4
Remove unused imports
tiffinihyatt May 14, 2022
3d942c0
Refactor Goal to_dict method to include associated task ids
tiffinihyatt May 14, 2022
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
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: gunicorn 'app:create_app()'
5 changes: 5 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,10 @@ def create_app(test_config=None):
migrate.init_app(app, db)

# Register Blueprints here
from .task_routes import tasks_bp
app.register_blueprint(tasks_bp)

from .goal_routes import goals_bp
app.register_blueprint(goals_bp)
Comment on lines +33 to +37

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!


return app
90 changes: 90 additions & 0 deletions app/goal_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from flask import Blueprint, request, jsonify, make_response, abort

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the abort function isn't directly used in this file anymore, so we should remove the import as part of our clean up & refactor steps.

Copy link
Author

Choose a reason for hiding this comment

The 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)

Choose a reason for hiding this comment

The 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)

Copy link
Author

Choose a reason for hiding this comment

The 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"]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we get a KeyError trying to creating an object in create_goal we catch the error and return some info to the user rather than letting the default 500 error without troubleshooting info pass through. It could be helpful for users to do something similar here as well.


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'})

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works, but how could we modify the Goal's to_dict function to conditionally return this format, to help us consolidate code that handles creating a dictionary representation of a Goal to the goal.py file?

14 changes: 14 additions & 0 deletions app/models/goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Choose a reason for hiding this comment

The 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"] = []

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted--thank you for catching this!


return goal_dict
22 changes: 22 additions & 0 deletions app/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice choice to derive the value of is_complete from completed_at!

else:
task_dict["is_complete"] = True
Comment on lines +19 to +22

Choose a reason for hiding this comment

The 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 bool function:

task_dict["is_complete"] = bool(self.completed_at)


if self.goal_id:
task_dict["goal_id"] = self.goal_id

return task_dict
41 changes: 41 additions & 0 deletions app/route_helpers.py
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):

Choose a reason for hiding this comment

The 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):

Choose a reason for hiding this comment

The 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)
1 change: 0 additions & 1 deletion app/routes.py

This file was deleted.

101 changes: 101 additions & 0 deletions app/task_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from flask import Blueprint, request, jsonify, make_response, abort

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The feedback about abort in goal_routes.py applies here as well.

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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The feedback in create_goal applies here as well.


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]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great use of list comprehensions across the project!

Copy link
Author

Choose a reason for hiding this comment

The 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'})

Choose a reason for hiding this comment

The 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)
1 change: 1 addition & 0 deletions migrations/README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Generic single-database configuration.
45 changes: 45 additions & 0 deletions migrations/alembic.ini
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
Loading