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

Scissors - Araceli #68

Open
wants to merge 6 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()"
10 changes: 6 additions & 4 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,10 @@ def create_app(test_config=None):
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

if test_config is None:
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
"SQLALCHEMY_DATABASE_URI")
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("SQLALCHEMY_DATABASE_URI")
else:
app.config["TESTING"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
"SQLALCHEMY_TEST_DATABASE_URI")
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("SQLALCHEMY_TEST_DATABASE_URI")

# Import models here for Alembic setup
from app.models.task import Task
Expand All @@ -30,5 +28,9 @@ def create_app(test_config=None):
migrate.init_app(app, db)

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

return app
10 changes: 9 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
from flask import current_app
from app import db

# from app.models.task import Task

class Goal(db.Model):
goal_id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String)

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

Choose a reason for hiding this comment

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

This is a great helper function, but I suspect it wasn't working as you expected because the tab level is outside of the class.


38 changes: 36 additions & 2 deletions app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,40 @@
from flask import current_app
from flask import request, current_app
from app import db
# from app.models.goal import Goal


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, nullable=True)
goal_id = db.Column(db.Integer, db.ForeignKey("goal.goal_id"), nullable=True)
goal = db.relationship("Goal", backref=db.backref("tasks"), lazy=True)

def to_dict(self):
return {
"id": self.task_id,
"title": self.title,
"description": self.description,
"is_complete": bool(self.completed_at)
}

def to_dict_goal(self):
return {
"id": self.task_id,
"goal_id": self.goal_id,
"title": self.title,
"description": self.description,
"is_complete": bool(self.completed_at)
}
Comment on lines +14 to +29

Choose a reason for hiding this comment

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

Same issue as Goal, but these are great helper functions!



# def completed_task(self):
# if self.completed_at == None:
# completed = False
# else:
# completed = True




263 changes: 262 additions & 1 deletion app/routes.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,263 @@
from flask import Blueprint
from flask.wrappers import Response
from app import db
from app.models.task import Task, to_dict, to_dict_goal
from app.models.goal import Goal, to_json
from flask import Blueprint, request, make_response, jsonify
from datetime import datetime
from dotenv import load_dotenv
import os
import requests

tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks")
goals_bp = Blueprint("goals", __name__, url_prefix="/goals")
load_dotenv()

# tasks

@tasks_bp.route("", methods=["POST"], strict_slashes=False)
def create_task():

request_body = request.get_json()

response = {"details": "Invalid data"}

if "title" not in request_body.keys() or "description" not in request_body.keys() or "completed_at" not in request_body.keys():

return jsonify(response), 400

else:

Choose a reason for hiding this comment

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

It's not necessary to include this else statement. If the if statement passes, the function will return and the code after this point will not run.

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()
valid_task = {"task": to_dict(new_task)}

return jsonify(valid_task), 201


@tasks_bp.route("", methods=["GET"], strict_slashes=False)
def get_tasks():

tasks_response = []

sort_query = request.args.get("sort")

if sort_query == "asc":
tasks = Task.query.order_by(Task.title.asc())

elif sort_query == "desc":
tasks = Task.query.order_by(Task.title.desc())

else:
tasks = Task.query.all()
Comment on lines +44 to +51

Choose a reason for hiding this comment

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

Nice!


for task in tasks:
tasks_response.append(to_dict(task))

return jsonify(tasks_response), 200


@tasks_bp.route("/<task_id>", methods=["GET", "PUT", "DELETE"], strict_slashes=False)

Choose a reason for hiding this comment

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

In the "" route above (lines 17 & 37), the methods are split into separate functions, while here they're combined into the same method. Both ways are fine, but for readability, I recommend following the same patterns throughout your code.

def handle_task(task_id):

task = Task.query.get(task_id)

if request.method == "GET":
if task is None:
return make_response(f"404 Not Found", 404)

Choose a reason for hiding this comment

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

Having an error message is great, but I would suggest having a more meaningful error message, something like 'Task with ID X not found'. Also this test is something you are doing for all of the methods - it could be moved up before the checks for the type of method so it only needs to be done once.


else:
one_task = to_dict(task)

Choose a reason for hiding this comment

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

Here is where the last test fails - if the task has a valid goal_id, it should include the goal_id in the json output. One way to handle this is:

if task.goal_id:
    one_task = to_dict_goal(task)
else:
    one_task = to_dict(task)


return {"task": one_task}

Choose a reason for hiding this comment

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

For readability I recommend using the same format to create your responses throughout your project. Lines 56, 66 and 71 all use a different format. Flask handles all of these formats gracefully, but having multiple formats can make it seem that different things are happening when in fact they are all more or less the same. Also, I recommend explicitly setting a status code everywhere so that you can accurately predict what your API will do in each situation.



elif request.method == "PUT":
if task:
form_data = request.get_json()
task.title = form_data["title"]
task.description = form_data["description"]
task.is_complete = form_data["completed_at"]
db.session.commit()

updated_task = {
"id": task.task_id,
"title": task.title,
"description": task.description,
"is_complete": bool(task.completed_at)
}
Comment on lines +82 to +87

Choose a reason for hiding this comment

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

This would be a great place to use use the to_dict helper function.

else:
return make_response(f"", 404)

return {'task': updated_task}

elif request.method == "DELETE":
if task:
db.session.delete(task)
db.session.commit()

response = {"details": f"Task {task.task_id} \"{task.title}\" successfully deleted"}

return jsonify(response), 200

else:
return make_response(f"", 404)


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

task = Task.query.get(task_id)

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

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

slack_bot_notification("Did this work")

return jsonify({"task": to_dict(task)}), 200

def slack_bot_notification(message):
path = "https://slack.com/api/chat.postMessage"
SLACK_KEY = os.environ.get("SLACK_TOKEN")
headers = {"Authorization": f"Bearer {SLACK_KEY}"}
query_params = {"channel": "task-notifications", "text": message}
requests.post(path, params=query_params, headers=headers)


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

task = Task.query.get(task_id)

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

task.completed_at = None
db.session.commit()

return jsonify({"task": to_dict(task)}), 200


# goals

@goals_bp.route("", methods=["POST"], strict_slashes=False)
def create_goal():

request_body = request.get_json()

response = {"details": "Invalid data"}

if "title" not in request_body.keys():

return jsonify(response), 400

else:
new_goal = Goal(title = request_body["title"])
db.session.add(new_goal)
db.session.commit()
valid_goal = {"goal": to_json(new_goal)}

return jsonify(valid_goal), 201

@goals_bp.route("", methods=["GET"], strict_slashes=False)
def get_goals():

goals = Goal.query.all()
goals_response = []

if goals != None:

for goal in goals:
goals_response.append(to_json(goal))

return jsonify(goals_response), 200

return jsonify(goals_response), 200


@goals_bp.route("/<goal_id>", methods=["GET", "PUT", "DELETE"], strict_slashes=False)
def handle_goal(goal_id):

goal = Goal.query.get(goal_id)

if request.method == "GET":
if goal is None:
return make_response("", 404)

else:
valid_goal = {"goal": to_json(goal)}

return jsonify(valid_goal), 200

elif request.method == "PUT":
if goal:
form_data = request.get_json()
goal.title = form_data["title"]
db.session.commit()

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

else:
return make_response("", 404)

return {'goal': updated_goal}

elif request.method == "DELETE":
if goal:
db.session.delete(goal)
db.session.commit()

response = {"details": f"Goal {goal.goal_id} \"{goal.title}\" successfully deleted"}

return jsonify(response), 200

else:
return make_response(f"", 404)


@goals_bp.route("/<goal_id>/tasks", methods=["POST", "GET"], strict_slashes=False)
def goal_task_relationship(goal_id):

goal = Goal.query.get(goal_id)

request_body = request.get_json()

if request.method == "POST":

task_ids = request_body["task_ids"]

for task_id in task_ids:
task = Task.query.get(task_id)
task.goal_id = goal_id # or goal.tasks.append(task)

db.session.commit()

return {"id": int(goal_id), "task_ids": task_ids}, 200

elif request.method == "GET":

# tasks_list = []

if goal:
tasks = goal.tasks

# for task in tasks:
# tasks_list.append(to_dict_goal(task))

task_list = [to_dict_goal(task) for task in tasks]

Choose a reason for hiding this comment

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

Nice list comprehension!


return {
"id": goal.goal_id,
"title": goal.title,
"tasks": task_list
}, 200

else:
return make_response("", 404)


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