Skip to content

Otters - Jodi Denney #104

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

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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()'
14 changes: 1 addition & 13 deletions ada-project-docs/wave_01.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@ As a client, I want to be able to make a `POST` request to `/tasks` with the fol
```json
{
"title": "A Brand New Task",
"description": "Test Description",
"completed_at": null
"description": "Test Description"
}
```

Expand Down Expand Up @@ -227,14 +226,3 @@ If the HTTP request is missing `description`, we should also get this response:
}
```

#### Missing `completed_at`

If the HTTP request is missing `completed_at`, we should also get this response:

`400 Bad Request`

```json
{
"details": "Invalid data"
}
```
4 changes: 4 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,9 @@ def create_app(test_config=None):
migrate.init_app(app, db)

# Register Blueprints here
from .tasks_routes import tasks_bp
app.register_blueprint(tasks_bp)
from .goal_routes import goals_bp
app.register_blueprint(goals_bp)

return app
121 changes: 121 additions & 0 deletions app/goal_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@

from flask import Blueprint, jsonify, make_response, request, abort
from app import db
from app.models.goal import Goal
from app.models.task import Task
from datetime import datetime
import os
from app.tasks_routes import validate_task_id

goals_bp = Blueprint("goal_bp", __name__, url_prefix="/goals")


def validate_goal_id(goal_id):
try:
id = int(goal_id)
except:
abort(make_response(
{"message": f"goal {goal_id} invalid. Must be numerical"}, 400))

goal = Goal.query.get(goal_id)

if not goal:
abort(make_response({"message": f"Goal {goal_id} not found"}, 404))

return goal
Comment on lines +13 to +25

Choose a reason for hiding this comment

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

Nice helper method here! This code is also very similar to validate_task_id in task_routes file -- could we modify this code to be able to work for both Models? 🤔



@goals_bp.route("", methods=["GET"])
def get_goals():
title_query = request.args.get("title")
if title_query:
goals = Goal.query.filter_by(title=title_query)
else:
goals = Goal.query.all()
print(goals)

Choose a reason for hiding this comment

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

Remember to remove your print statements when you submit 😉

goal_response = []
for goal in goals:
goal_response.append(goal.to_json())
return jsonify(goal_response)


@goals_bp.route("/<goal_id>", methods=["GET"])
def get_single_goal(goal_id):
goal = validate_goal_id(goal_id)
return{"goal": goal.to_json()}

Choose a reason for hiding this comment

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

Beautiful use of the to_json method! I see that in some endpoints, you use jsonify but it's not consistently used. I recommend using it here as well! Flask will handle this dictionary correctly, but using a consistent pattern helps someone who's reading the code understand that the same thing is happening here.

Additionally, remember to give it an explicit status code!



@goals_bp.route("", methods=["POST"])
def create_goal():
request_body = request.get_json()
try:
new_goal = Goal(title=request_body["title"])
except KeyError:
return make_response({"details": "Invalid data"}, 400)

db.session.add(new_goal)
db.session.commit()

return {"goal": new_goal.to_json()}, 201


@goals_bp.route("/<goal_id>", methods=["PUT"])
def update_goal(goal_id):
found_goal = validate_goal_id(goal_id)

request_body = request.get_json()

found_goal.title = request_body["title"]

db.session.commit()

return {"goal": found_goal.to_json()}, 200


@goals_bp.route("/<goal_id>", methods=["DELETE"])
def delete_goal(goal_id):
found_goal = validate_goal_id(goal_id)

db.session.delete(found_goal)
db.session.commit()

return jsonify({"details": f'Goal {found_goal.goal_id} "{found_goal.title}" successfully deleted'})


@ goals_bp.route("/<goal_id>/tasks", methods=["POST"])
def add_tasks(goal_id):
goal = validate_goal_id(goal_id)
request_body = request.get_json()
task_ids = request_body["task_ids"]

for task_id in task_ids:
valid_task = validate_task_id(task_id)
goal.tasks.append(valid_task)
# tasks = []
# for id in task_ids:
# tasks.append(validate_task_id(id))
# for task in tasks:
# task.goal_id = goal.goal_id

Comment on lines +94 to +99

Choose a reason for hiding this comment

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

Also remember to remove commented out code!

db.session.commit()
return jsonify({"id": goal.goal_id,
"task_ids": task_ids})


@ goals_bp.route("/<goal_id>/tasks", methods=["GET"])
def get_tasks(goal_id):
goal = validate_goal_id(goal_id)
tasks = []

for task in goal.tasks:
tasks.append(({
"id": task.task_id,
"title": task.title,
"description": task.description,
"is_complete": bool(task.completed_at)}))
if task.goal_id:
tasks[-1]["goal_id"] = task.goal_id

return jsonify({"id": goal.goal_id,
"title": goal.title,
"tasks": tasks})
8 changes: 7 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,10 @@


class Goal(db.Model):
goal_id = db.Column(db.Integer, primary_key=True)
goal_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
title = db.Column(db.String)
tasks = db.relationship("Task", back_populates="goal")

def to_json(self):
return {"id": self.goal_id,
"title": self.title}
Comment on lines +9 to +11

Choose a reason for hiding this comment

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

Great instance method! 🎉

7 changes: 6 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,9 @@


class Task(db.Model):
task_id = db.Column(db.Integer, primary_key=True)
task_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.goal_id'))
goal = db.relationship("Goal", back_populates="tasks")
1 change: 0 additions & 1 deletion app/routes.py

This file was deleted.

152 changes: 152 additions & 0 deletions app/tasks_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
from flask import Blueprint, jsonify, make_response, request, abort
from app import db
from app.models.task import Task
from datetime import datetime
import sys
import os
import requests
from dotenv import load_dotenv
load_dotenv()

tasks_bp = Blueprint("tasks_bp", __name__, url_prefix="/tasks")

SLACK_TOKEN = os.environ.get("SLACK_TOKEN")

Choose a reason for hiding this comment

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

Great job using a constant to call the token from your env file! ✨



def validate_task_id(task_id):
try:
task_id = int(task_id)
except:
abort(make_response(
{"message": f"Task {task_id} invalid. Must be numerical"}, 400))

task = Task.query.get(task_id)

if not task:
abort(make_response({"message": f"Task {task_id} not found"}, 404))

return task
Comment on lines +16 to +28

Choose a reason for hiding this comment

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

Love to see this validation helper method up top! Great use of try/except and appropriate status codes as well.



@tasks_bp.route("", methods=["GET"])
def get_tasks():
sort_query = request.args.get("sort")
title_query = request.args.get("title")
if title_query:
tasks = Task.query.filter_by(title=title_query)
elif sort_query == "desc":
tasks = Task.query.order_by(Task.title.desc()).all()
elif sort_query == "asc":
tasks = Task.query.order_by(Task.title.asc()).all()
else:
tasks = Task.query.all()
Comment on lines +35 to +42

Choose a reason for hiding this comment

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

Great sorting logic with title_query up top and the catch all in the last else statement.

One nit: in Python, the scope of tasks extend outside this if/else block from L35-42. In many other languages (including Javascript!), it would be block-scoped, therefore, it's good practice to set your mutable variables in the highest block level, in this case, setting tasks = [] above L35.

tasks_response = []
for task in tasks:
tasks_response.append({
"id": task.task_id,
"title": task.title,
"description": task.description,
"is_complete": bool(task.completed_at)})

Choose a reason for hiding this comment

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

Excellent use of the boolean casting here for is_complete!

return jsonify(tasks_response)


@tasks_bp.route("/<task_id>", methods=["GET"])
def get_single_task(task_id):
task = validate_task_id(task_id)
task_reply = {"task": {
"id": task.task_id,
"title": task.title,
"description": task.description,
"is_complete": bool(task.completed_at)}}
if task.goal_id:
task_reply["task"]["goal_id"] = task.goal_id
Comment on lines +61 to +62

Choose a reason for hiding this comment

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

Love the efficiency in using a conditional to check if a key exists and then appending task_reply appropriately!

return task_reply


@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 KeyError:
return {"details": "Invalid data"}, 400

if "completed_at" in request_body:
new_task.completed_at = request_body["completed_at"]

db.session.add(new_task)
db.session.commit()

return {"task": {
"id": new_task.task_id,
"title": new_task.title,
"description": new_task.description,
"is_complete": bool(new_task.completed_at)}}, 201


@tasks_bp.route("/<task_id>", methods=["PUT"])
def update_task(task_id):
found_task = validate_task_id(task_id)

request_body = request.get_json()

found_task.title = request_body["title"]
found_task.description = request_body["description"]
if "completed_at" in request_body:
found_task.completed_at = request_body["completed_at"]
db.session.commit()

return {"task":
{"id": found_task.task_id,
"title": found_task.title,
"description": found_task.description,
"is_complete": bool(found_task.completed_at)}}, 200


@tasks_bp.route("/<task_id>/mark_complete", methods=["PATCH"])
def update_task_status(task_id):
found_task = validate_task_id(task_id)

found_task.completed_at = datetime.now()

db.session.commit()

message = f"Someone just completed task {found_task.title}"

headers = {"Authorization": "Bearer " + SLACK_TOKEN}
params = {"channel": "task-notifications", "text": message}

requests.post('https://slack.com/api/chat.postMessage',
data=params, headers=headers)

return {"task":
{"id": found_task.task_id,
"title": found_task.title,
"description": found_task.description,
"is_complete": bool(found_task.completed_at)}}, 200


@tasks_bp.route("/<task_id>/mark_incomplete", methods=["PATCH"])
def incomplete_task_status(task_id):
found_task = validate_task_id(task_id)

found_task.completed_at = None

db.session.commit()

return {"task":
{"id": found_task.task_id,
"title": found_task.title,
"description": found_task.description,
"is_complete": bool(found_task.completed_at)}}, 200
Comment on lines +138 to +142

Choose a reason for hiding this comment

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

We return this kind of response four times in this code and a great place to DRY up our code!



@tasks_bp.route("/<task_id>", methods=["DELETE"])
def delete_task(task_id):
found_task = validate_task_id(task_id)

db.session.delete(found_task)
db.session.commit()

return jsonify({"details": f'Task {found_task.task_id} "{found_task.title}" successfully deleted'})
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