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 - Melissa Nguyen #62

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
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()'
6 changes: 6 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,10 @@ def create_app(test_config=None):

# Register Blueprints here

from .routes import tasks_bp
app.register_blueprint(tasks_bp)

from .routes import goals_bp
app.register_blueprint(goals_bp)

return app
4 changes: 4 additions & 0 deletions app/models/goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@

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)


11 changes: 11 additions & 0 deletions app/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,14 @@

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)
goal_id = db.Column(db.Integer, db.ForeignKey('goal.goal_id'), nullable=True)

def task_completed(self):

Choose a reason for hiding this comment

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

Nice work using this helper function.

if self.completed_at == None:
return False
else:
return True

275 changes: 274 additions & 1 deletion app/routes.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,275 @@
from flask import Blueprint
import requests
import os
from flask import request, Blueprint, make_response, jsonify

from app import db
from app.models.task import Task
from app.models.goal import Goal
from datetime import datetime


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

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

if request.method == "POST":
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
else:
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()

return make_response({
"task": {
"id": new_task.task_id,
"title": new_task.title,
"description": new_task.description,
"is_complete": new_task.task_completed()
}
Comment on lines +27 to +32

Choose a reason for hiding this comment

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

Notice that this code is repeated many places throughout your routes.py. Consider moving the logic to build a json response into a instance method on the task model that you could then call here with something like new_task.to_json().

}, 201)

elif request.method == "GET":
task_query = request.args.get("sort")
if task_query == "asc":
tasks = Task.query.order_by(Task.title.asc()).all()
elif task_query == "desc":
tasks = Task.query.order_by(Task.title.desc()).all()
else:
tasks = Task.query.all()

tasks_response = []
for task in tasks:
tasks_response.append({
"id": task.task_id,
"title": task.title,
"description": task.description,
"is_complete": task.task_completed()
})
return jsonify(tasks_response), 200

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

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

elif request.method == "GET":

if task.goal == None:
return jsonify({
"task": {
"id": task.task_id,
"title": task.title,
"description": task.description,
"is_complete": task.task_completed()
}
}), 200

else:
return jsonify({
"task": {
"id": task.task_id,
"goal_id": task.goal_id,
"title": task.title,
"description": task.description,
"is_complete": task.task_completed()
}
}), 200

elif request.method == "PUT":
form_data = request.get_json()

task.title = form_data["title"]
task.description = form_data["description"]
task.completed_at = form_data["completed_at"]
db.session.commit()

return jsonify({
"task": {
"id": task.task_id,
"title": task.title,
"description": task.description,
"is_complete": task.task_completed()
}
}), 200

elif request.method == "DELETE":
db.session.delete(task)
db.session.commit()
return jsonify({"details": f'Task {task.task_id} "{task.title}" successfully deleted'}), 200

def slack_message_bot(task):

Choose a reason for hiding this comment

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

Great work making a helper function.


params = {
"channel": "task-notifications",
"text": f"Someone just completed the task {task.task_id}"
}

headers = {
"Authorization": f"Bearer {os.environ.get('SLACK_API_KEY')}"
}

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


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

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

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

slack_message_bot(task)

return jsonify({
"task": {
"id": task.task_id,
"title": task.title,
"description": task.description,
"is_complete": task.task_completed()
}
}), 200

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

if task == None:
return make_response(), 404
Comment on lines +145 to +146

Choose a reason for hiding this comment

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

You have this code 3 places. Consider making a helper function, or even using get_or_404 https://flask-sqlalchemy.palletsprojects.com/en/2.x/api/#flask_sqlalchemy.BaseQuery.get_or_404.

Also, this is a very minor note, but I just learned this cohort that the python-y way to say if task == None is if not task


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

return jsonify({
"task": {
"id": task.task_id,
"title": task.title,
"description": task.description,
"is_complete": task.task_completed()
}
}), 200
Comment on lines +151 to +158

Choose a reason for hiding this comment

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

Sometimes your code uses make_response and sometimes it doesn't. Consistently using it (or not) can add readability to your code.


goals_bp = Blueprint("goals", __name__, url_prefix= "/goals")
@goals_bp.route("", methods=["POST", "GET"])
def create_goal():
if request.method == "POST":
request_body = request.get_json()
if "title" not in request_body:
return jsonify({"details": "Invalid data"}), 400
else:
new_goal = Goal(title=request_body["title"])

db.session.add(new_goal)
db.session.commit()
Comment on lines +167 to +171

Choose a reason for hiding this comment

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

Minor note: this does not need to be nested in an else. Reducing indentation can increase readability.


return make_response({
"goal": {
"id": new_goal.goal_id,
"title": new_goal.title,
}
}, 201)

elif request.method == "GET":
goals = Goal.query.all()
goals_response = []

for goal in goals:
goals_response.append({
"id": goal.goal_id,
"title": goal.title
})
Comment on lines +173 to +188

Choose a reason for hiding this comment

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

Just as with the task response body, note that the goal response body is used a few times. Consider moving this logic to an instance method on the goal model that returns the needed dictionary, and then calling it where needed with something like goal.to_json()


return jsonify(goals_response), 200

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

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

elif request.method == "GET":
return jsonify({
"goal": {
"id": goal.goal_id,
"title": goal.title
}
}), 200

elif request.method == "PUT":
form_data = request.get_json()

goal.title = form_data["title"]
db.session.commit()

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


elif request.method == "DELETE":
db.session.delete(goal)
db.session.commit()
return jsonify({"details": f'Goal {goal.goal_id} "{goal.title}" successfully deleted'}), 200

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

if goal is None:
return make_response("", 404)

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

for task_id in request_body["task_ids"]:
task = Task.query.get(task_id)
task.goal_id = goal.goal_id
Comment on lines +236 to +238

Choose a reason for hiding this comment

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

Nice work writing this logic to make the relationship between goals and tasks.


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


elif request.method == "GET":
tasks = Task.query.filter_by(goal_id=goal.goal_id)

connected_tasks = []
for task in tasks:
connected_tasks.append({
"id": task.task_id,
"goal_id": task.goal_id,
"title": task.title,
"description": task.description,
"is_complete": task.task_completed(),
})

return jsonify({
"id": goal.goal_id,
"title": goal.title,
"tasks": connected_tasks
}), 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