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

Sharks - Morgan B. #101

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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 .routes.task_routes import tasks_bp
app.register_blueprint(tasks_bp)

from .routes.goal_routes import goals_bp
app.register_blueprint(goals_bp)

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

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

Choose a reason for hiding this comment

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

Consider adding nullable=False to ensure every goal requires a title.

tasks = db.relationship("Task", back_populates="goal", lazy=True)

@classmethod
def create(cls, req_body):
new_goal = cls(
title=req_body["title"]
)
return new_goal

def to_json(self):
return {
"id": self.goal_id,
"title": self.title
}

def update(self, req_body):
self.title = req_body["title"]
33 changes: 33 additions & 0 deletions app/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,36 @@

class Task(db.Model):
task_id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String)

Choose a reason for hiding this comment

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

Consider adding nullable=False to ensure every task requires a title.

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", back_populates="tasks")

@classmethod
def create(cls, req_body):
new_task = cls(
title=req_body["title"],
description=req_body["description"],
completed_at = req_body.get("completed_at")
)
return new_task

def to_json(self):
task_dict = {
"id": self.task_id,
"title": self.title,
"description": self.description,
"is_complete": True if self.completed_at else False
}

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

return task_dict
Comment on lines +27 to +33

Choose a reason for hiding this comment

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

👍


def update(self, req_body):
self.title = req_body["title"]
self.description = req_body["description"]
self.completed_at = req_body.get("completed_at")
1 change: 0 additions & 1 deletion app/routes.py

This file was deleted.

Empty file added app/routes/__init__.py
Empty file.
90 changes: 90 additions & 0 deletions app/routes/goal_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from flask import Blueprint, jsonify, make_response, request, abort
from app import db
from app.models.goal import Goal
from .helpers import validate_goal, validate_task

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

Choose a reason for hiding this comment

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

Nice work creating two separate route files for each model and adding them to a routes directory


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

if request_body.get("title"):
new_goal = Goal.create(request_body)
else:
abort(make_response({"details": "Invalid data"}, 400))
Comment on lines +12 to +15

Choose a reason for hiding this comment

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

Nice error handling


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

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

@goals_bp.route("", methods=["GET"])
def get_all_goals():
goals = Goal.query.all()

goal_response_body = []
for goal in goals:
goal_response_body.append(goal.to_json())
Comment on lines +27 to +28

Choose a reason for hiding this comment

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

This works well and is readable, but if you'd like to incorporate list comprehensions into your code, you could write it like this:

goals_response = [goal.to_json() for goal in goals]


return jsonify(goal_response_body), 200

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

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

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

request_body = request.get_json()

if request_body.get("title"):
goal.update(request_body)
else:
abort(make_response({"details": "Invalid data"}, 400))
Comment on lines +44 to +47

Choose a reason for hiding this comment

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

👍 we can't assume that the client will always pass us a correctly formatted request so good job adding error handling here too


db.session.commit()

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

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

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"])
def assign_tasks(goal_id):
goal = validate_goal(goal_id)

request_body = request.get_json()

for task_id in request_body["task_ids"]:
task = validate_task(task_id)

task.goal = goal

db.session.commit()

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

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

tasks = []
for task in goal.tasks:
tasks.append(task.to_json())

goal_response = goal.to_json()
goal_response["tasks"] = tasks

Choose a reason for hiding this comment

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

Wonder if you could refactor to_json() so that the logic on line 88 gets moved into the method


return jsonify(goal_response), 200
29 changes: 29 additions & 0 deletions app/routes/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from flask import abort, make_response
from ..models.task import Task
from ..models.goal import Goal

def validate_task(id):
try:
id = int(id)
except:
abort(make_response({"message": f"{id} is not a valid id"}, 400))

task = Task.query.get(id)

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

return task

def validate_goal(id):
try:
id = int(id)
except:
abort(make_response({"message": f"{id} is not a valid id"}, 400))

goal = Goal.query.get(id)

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

return goal
107 changes: 107 additions & 0 deletions app/routes/task_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from flask import Blueprint, jsonify, make_response, request, abort
from app import db
from app.models.task import Task
from .helpers import validate_task
from datetime import datetime, timezone
import requests
import os

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

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

if request_body.get("title") and request_body.get("description"):
new_task = Task.create(request_body)
else:
abort(make_response({"details": "Invalid data"}, 400))

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

return make_response(jsonify({"task": new_task.to_json()}), 201)

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

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

else:
tasks = Task.query.all()

task_response_body = []
for task in tasks:
task_response_body.append(task.to_json())

return jsonify(task_response_body), 200

@tasks_bp.route("/<task_id>", methods=["GET"])
def get_one_task(task_id):
task = validate_task(task_id)

return jsonify({"task": task.to_json()}), 200

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

request_body = request.get_json()

if request_body.get("title") and request_body.get("description"):
task.update(request_body)
else:
abort(make_response({"details": "Invalid data"}, 400))

db.session.commit()

return jsonify({"task": task.to_json()}), 200

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

db.session.delete(task)
db.session.commit()

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

@tasks_bp.route("/<task_id>/mark_complete", methods=["PATCH"])
def mark_complete(task_id):
task = validate_task(task_id)
already_completed = bool(task.completed_at)

time = datetime.now(timezone.utc)

task.completed_at = time

db.session.commit()

if not already_completed:
key = os.environ.get("SLACK_API")
payload = {
"channel": "task-notifications",
"text": f"Someone just completed the task {task.title}"
}
header = {"Authorization": f"Bearer {key}"}
requests.post("https://slack.com/api/chat.postMessage", params=payload,
headers=header)
Comment on lines +86 to +94

Choose a reason for hiding this comment

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

Consider putting this in a helper function and calling it here to make mark_complete() a bit more concise.


return jsonify({"task": task.to_json()}), 200

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

if task.completed_at:
task.completed_at = None

db.session.commit()

return jsonify({"task": task.to_json()}), 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