Skip to content
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
5 changes: 4 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,8 @@ def create_app(test_config=None):
migrate.init_app(app, db)

# Register Blueprints here

from app.routes import tasks_bp
from app.routes import goals_bp
app.register_blueprint(tasks_bp)
app.register_blueprint(goals_bp)
return app
2 changes: 2 additions & 0 deletions app/models/goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@

class Goal(db.Model):
goal_id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String)
tasks = db.relationship('Task', backref='goal', lazy=True)
Comment on lines +7 to +8

Choose a reason for hiding this comment

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

Do we want to store goals with a NULL title? Consider setting nullable=False to ensure every goal requires a title.

9 changes: 7 additions & 2 deletions app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from flask import current_app
from app import db


# This the table
class Task(db.Model):
task_id = db.Column(db.Integer, primary_key=True)
# These are the columns
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, nullable=True)
Comment on lines +8 to +10

Choose a reason for hiding this comment

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

Our tests don't confirm this, but should tasks be able to store NULL titles and descriptions?

All columns are nullable by default, so we can remove the nullable = True attribute from the completed_at column and it will work the same way.

goal_id = db.Column(db.Integer, db.ForeignKey('goal.goal_id'), nullable=True)
Copy link

@audreyandoy audreyandoy Nov 19, 2021

Choose a reason for hiding this comment

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

We'll need to return task instances as dictionaries in our routes, so adding a helper method to do so will help us get rid of a lot of repetitive dictionary logic.

def to_dict(self):
    task = {
        "id": new_task.task_id,
        "title": new_task.title,
        "description": new_task.description,
        "is_complete": new_task.completed_at is not None
    }
    return task

333 changes: 332 additions & 1 deletion app/routes.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,333 @@
from flask import Blueprint
import re
from flask import Blueprint, jsonify, request
from flask.helpers import make_response

Choose a reason for hiding this comment

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

You're not using make_response anywhere else in this file so we can omit this import.

from app.models.task import Task
from app.models.goal import Goal
import requests
from datetime import datetime
from app import db
import os
from dotenv import load_dotenv

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

@tasks_bp.route("", methods=["POST"])
def create_task():
request_body = request.get_json()

if "title" not in request_body or "description" not in request_body or "completed_at" not in request_body:
return jsonify({"details": "Invalid data"}), 400

Choose a reason for hiding this comment

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

When a Flask app returns a dictionary, it automatically converts that dictionary into JSON. So there's no need to use jsonify() for dictionaries. We should use jsonify() for all other datatypes though!


new_task = Task(
title=request_body["title"],
description=request_body["description"],
completed_at=request_body["completed_at"]
)

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

response_body = {
"task": {
"id": new_task.task_id,
"title": new_task.title,
"description": new_task.description,
"is_complete": new_task.completed_at is not None

Choose a reason for hiding this comment

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

Nice! Great use the not operator to determine the value of is_complete.

}
}
Comment on lines +30 to +37

Choose a reason for hiding this comment

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

We can refactor this function by moving the dictionary logic into a helper function for the Task model.

Suggested change
response_body = {
"task": {
"id": new_task.task_id,
"title": new_task.title,
"description": new_task.description,
"is_complete": new_task.completed_at is not None
}
}
response_body = {
"task": task.to_dict()
}


return jsonify(response_body), 201

@tasks_bp.route("", methods=["GET"])
def read_tasks():
title_query = request.args.get("sort")

if title_query == "asc":
tasks = Task.query.order_by(Task.title.asc())
elif title_query == "desc":
tasks = Task.query.order_by(Task.title.desc())
else:
tasks = Task.query.all()

task_responses = []
for task in tasks:
task_responses.append(
{
"id": task.task_id,
"title": task.title,
"description": task.description,
"is_complete": task.completed_at is not None
}
)
Comment on lines +52 to +61

Choose a reason for hiding this comment

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

This would be a good spot to use list comprehension and the helper method for the task dictionary.

Suggested change
task_responses = []
for task in tasks:
task_responses.append(
{
"id": task.task_id,
"title": task.title,
"description": task.description,
"is_complete": task.completed_at is not None
}
)
task_responses = [task.to_dict() for task in tasks]


return jsonify(task_responses), 200

@tasks_bp.route("/<task_id>", methods=["GET"])
def read_task(task_id):
task_id = int(task_id)
task = Task.query.get(task_id)

if task == None:
return jsonify(None), 404

if not task.goal_id:
return jsonify({
"task": {
"id": task.task_id,
"title": task.title,
"description": task.description,
"is_complete": False if task.completed_at == None else True
}
}), 200
else:
return jsonify({
"task": {
"id": task.task_id,
"goal_id": task.goal_id,
"title": task.title,
"description": task.description,
"is_complete": False if task.completed_at == None else True
}
}), 200
Comment on lines +73 to +91
Copy link

@audreyandoy audreyandoy Nov 19, 2021

Choose a reason for hiding this comment

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

Notice how these conditionals return a dictionary with the same key value pairs (besides goal_id) ? Instead of explicitly writing out this same dictionary twice, we can write a "base" dictionary and add the goal_id to it if it exists for the task.

Suggested change
if not task.goal_id:
return jsonify({
"task": {
"id": task.task_id,
"title": task.title,
"description": task.description,
"is_complete": False if task.completed_at == None else True
}
}), 200
else:
return jsonify({
"task": {
"id": task.task_id,
"goal_id": task.goal_id,
"title": task.title,
"description": task.description,
"is_complete": False if task.completed_at == None else True
}
}), 200
task_dict = {
"id": task.task_id,
"title": task.title,
"description": task.description,
"is_complete": False if task.completed_at == None else True
}
if task.goal_id:
task_dict["goal_id] = task.goal_id
return task_dict, 200

Choose a reason for hiding this comment

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

We can also have this same logic within a Task class helper method as well!


@tasks_bp.route("/<task_id>",methods=["PUT"])
def update_task(task_id):
task_id = int(task_id)
task = Task.query.get(task_id)

updated_body = request.get_json()

if task == None:
return jsonify(None), 404

else:
task.title = updated_body["title"]
task.description = updated_body["description"]

db.session.commit()

task_dict = {}
task_dict = {
"task": {
"id": task.task_id,
"title": task.title,
"description": task.description,
"is_complete": task.completed_at is not None
}
}

return jsonify(task_dict), 200
Comment on lines +103 to +119

Choose a reason for hiding this comment

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

This logic doesn't need to be wrapped in an else clause. You can omit the else clause, unindent this code, and see that it works the same way.


@tasks_bp.route("/<task_id>", methods=["DELETE"])
def delete_task(task_id):
task_id = int(task_id)
task = Task.query.get(task_id)

if task == None:
return jsonify(None), 404

else:
db.session.delete(task)
db.session.commit()
Comment on lines +129 to +131

Choose a reason for hiding this comment

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

This also doesn't need to be wrapped in an else clause


response_body = {}

Choose a reason for hiding this comment

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

No need to create an empty dictionary first before adding keys and values to it.

response_body = {
"details": f'Task {task.task_id} "{task.title}" successfully deleted'}
return jsonify(response_body), 200

def slack_bot(title):

Choose a reason for hiding this comment

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

Nice helper function!

query_path = {'channel':'slack_api_test_channel', 'text': title}
header = {'Authorization': os.environ.get('BOT')}
response = requests.post('https://slack.com/api/chat.postMessage', params = query_path, headers = header)
return response.json()

@tasks_bp.route("/<task_id>/mark_complete", methods=["PATCH"])

Choose a reason for hiding this comment

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

👍

def update_complete(task_id):
task_id = int(task_id)
task = Task.query.get(task_id)

if task == None:
return jsonify(None), 404

task.completed_at = datetime.now()
db.session.commit()

slack_bot(task.title)

task_dict = {}

Choose a reason for hiding this comment

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

Suggested change
task_dict = {}

task_dict = {
"task": {
"id": task.task_id,
"title": task.title,
"description": task.description,
"is_complete": task.completed_at is not None
}
}

return jsonify(task_dict), 200

@tasks_bp.route("/<task_id>/mark_incomplete", methods=["PATCH"])

Choose a reason for hiding this comment

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

👍

def update_incomplete(task_id):
task_id = int(task_id)
task = Task.query.get(task_id)

if task == None:
return jsonify(None), 404

task.completed_at = None

db.session.commit()

task_dict = {}

Choose a reason for hiding this comment

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

Suggested change
task_dict = {}

task_dict = {
"task": {
"id": task.task_id,
"title": task.title,
"description": task.description,
"is_complete": task.completed_at is not None
}
}

return jsonify(task_dict), 200

Choose a reason for hiding this comment

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

Great work on the task routes! We can make routes.py much shorter if we separate the task and goal routes into separate files

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

@goals_bp.route("", methods=["POST"])

Choose a reason for hiding this comment

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

👍

def create_goal():
request_body = request.get_json()

if "title" not in request_body:
return jsonify({"details": "Invalid data"}), 400

new_goal = Goal(
title=request_body["title"]
)

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

response_body = {
"goal": {
"id": new_goal.goal_id,
"title": new_goal.title
}
}

return jsonify(response_body), 201

@goals_bp.route("", methods=["GET"])

Choose a reason for hiding this comment

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

👍

def read_goals():
title_query = request.args.get("sort")

if title_query == "asc":
goals = Goal.query.order_by(Goal.title.asc())
elif title_query == "desc":
goals = Goal.query.order_by(Goal.title.desc())
else:
goals = Goal.query.all()

goal_responses = []
for goal in goals:
goal_responses.append(
{
"id": goal.goal_id,
"title": goal.title
}
)

return jsonify(goal_responses), 200

@goals_bp.route("/<goal_id>", methods=["GET"])
def read_goal(goal_id):
goal_id = int(goal_id)
goal = Goal.query.get(goal_id)

goal_dict = {}
if goal == None:
return jsonify(None), 404
else:
goal_dict = {
"goal": {
"id": goal.goal_id,
"title": goal.title
}
}
Comment on lines +248 to +254

Choose a reason for hiding this comment

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

This logic doesn't need to be in the else logic. And it would also be helpful for the Goal model to have a dictionary helper method.


return jsonify(goal_dict), 200

@goals_bp.route("/<goal_id>",methods=["PUT"])

Choose a reason for hiding this comment

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

👍 Same comments in GET request apply here too.

def update_goal(goal_id):
goal_id = int(goal_id)
goal = Goal.query.get(goal_id)

updated_body = request.get_json()

if goal == None:
return jsonify(None), 404

else:
goal.title = updated_body["title"]

db.session.commit()

goal_dict = {}
goal_dict = {
"goal": {
"id": goal.goal_id,
"title": goal.title
}
}

return jsonify(goal_dict), 200

@goals_bp.route("/<goal_id>", methods=["DELETE"])

Choose a reason for hiding this comment

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

👍

def delete_goal(goal_id):
goal_id = int(goal_id)
goal = Goal.query.get(goal_id)


if goal == None:
return jsonify(None), 404

else:
db.session.delete(goal)
db.session.commit()

response_body = {}
response_body = {
"details": f'Goal {goal.goal_id} "{goal.title}" successfully deleted'}
return jsonify(response_body), 200

@goals_bp.route("/<goal_id>/tasks", methods=["POST", "GET"])

Choose a reason for hiding this comment

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

👍 Good work! This part was tricky!

def create_list(goal_id):
goal_id = int(goal_id)
goal = Goal.query.get(goal_id)

if goal == None:
return jsonify(None), 404

if request.method == "POST":
request_body = request.get_json()
task_ids = request_body["task_ids"]

for task_id in task_ids:
task = Task.query.get(task_id)
task.goal_id = goal.goal_id

db.session.commit()

return jsonify({"id": goal.goal_id, "task_ids": request_body["task_ids"]}), 200

elif request.method == "GET":
tasks_and_goals = []

for task in goal.tasks:
tasks_and_goals.append({
"id": task.task_id,
"goal_id": task.goal_id,
"title": task.title,
"description": task.description,
"is_complete": False if task.completed_at == None else True
})

return jsonify({"id": goal.goal_id, "title": goal.title, "tasks": tasks_and_goals}), 200

Choose a reason for hiding this comment

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

Great work practicing the single responsibility principle for the goal routes!

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