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

Olive - Task List - Sea Turtles #94

Open
wants to merge 20 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()'
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 .routes.task_routes import tasks_bp
app.register_blueprint(tasks_bp)
from .routes.goal_routes import goals_bp
app.register_blueprint(goals_bp)
Comment on lines +33 to +36

Choose a reason for hiding this comment

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

Nice, I like the choice to divide the routes into their own files!


return app
29 changes: 28 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,31 @@


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

Choose a reason for hiding this comment

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

If we check out the SQLAlchemy docs for loading items: https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html the docs say:

"The default value of the relationship.lazy argument is "select", which indicates lazy loading."

Doing some more digging about the possible lazy parameter values we might find further info like in https://medium.com/@ns2586/sqlalchemys-relationship-and-lazy-parameter-4a553257d9ef where we see the line:

"lazy = ‘select’ (or True)".

Putting these together we can see that the default value for lazy in a db relationship is select which is the same as True. Since we aren't changing the default value for lazy, we could leave this parameter off and our code would behave the same:

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



def to_dict(self):
return dict(
id = self.id,
title = self.title
)

def to_dict_with_tasks(self):
tasks_info = [task.to_dict() for task in self.tasks]
return dict(
id = self.id,
title = self.title,
tasks = tasks_info
)
Comment on lines +10 to +22

Choose a reason for hiding this comment

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

If we added a new attribute to Goal that needed to be included in every response, we'd have to update 2 functions. This redundancy creates places for potential bugs. If both formats are necessary, I'd think about creating a single function with boolean parameters for the attributes that aren't sent every time. (That could get tedious over time, another option would be to have a single parameter that is a list of all the attributes we want added to the response).

    def to_dict(self, include_tasks):
        goal_dict =  dict(
            id = self.id,
            title = self.title
            )
        
        if include_tasks:
            tasks_info = [task.to_dict() for task in self.tasks]
            goal_dict["tasks"] = tasks_info
            
        return goal_dict


@classmethod
def from_dict(cls, data_dict):
return cls(
title = data_dict["title"],
)

def replace_details(self, data_dict):
self.title = data_dict["title"]

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


class Task(db.Model):
task_id = db.Column(db.Integer, primary_key=True)
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.id"))


def to_dict(self):
if self.goal_id:
return dict(
id = self.id,
goal_id = self.goal_id,
title = self.title,
description = self.description,
is_complete = bool(self.completed_at)
)
else:
return dict(
id = self.id,
title = self.title,
description = self.description,
is_complete = bool(self.completed_at)
)
Comment on lines +12 to +27

Choose a reason for hiding this comment

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

The feedback on to_dict in Goal applies here too.

@classmethod
def from_dict(cls, data_dict):
completed_time = data_dict["completed_at"] if "completed_at" in data_dict else None

Choose a reason for hiding this comment

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

Nice handling for the optional parameter!

return cls(
title = data_dict["title"],
description = data_dict["description"],
completed_at = completed_time
)

def replace_details(self, data_dict):
self.title = data_dict["title"]
self.description = data_dict["description"]
1 change: 0 additions & 1 deletion app/routes.py

This file was deleted.

83 changes: 83 additions & 0 deletions app/routes/goal_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from app.models.goal import Goal
from app.models.task import Task
from flask import Blueprint, jsonify, request
from app import db
from .routes_helper import get_record_by_id, make_goal_safely, replace_goal_safely

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

# POST /goals
@goals_bp.route("", methods = ["POST"])
def create_goal():
request_body = request.get_json()
new_goal = make_goal_safely(request_body)

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

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

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

result_list = [goal.to_dict() for goal in goals]

return jsonify(result_list)

# GET /goals/<id>
@goals_bp.route("/<id>", methods=["Get"])
def read_goal_by_id(id):
goal = get_record_by_id(Goal, id)
return jsonify({"goal":goal.to_dict()})

# PUT /goals/<id>
@goals_bp.route("/<id>", methods=["PUT"])
def replace_goal_by_id(id):
request_body = request.get_json()
goal = get_record_by_id(Goal, id)

replace_goal_safely(goal, request_body)

db.session.add(goal)
db.session.commit()

return jsonify({"goal":goal.to_dict()})

# DELETE /goals/<id>
@goals_bp.route("/<id>", methods=["DELETE"])
def delete_goal_by_id(id):
goal = get_record_by_id(Goal, id)

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

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

Choose a reason for hiding this comment

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

Nice, I appreciate the informative messages 👍


# POST /goals/<id>/tasks
@goals_bp.route("/<id>/tasks", methods=["POST"])
def post_tasks_to_goal(id):
request_body = request.get_json()
goal = get_record_by_id(Goal, id)

task_ids = request_body["task_ids"]

for id in task_ids:
task = get_record_by_id(Task, id)
task.goal = goal

db.session.commit()

task_list = [task.id for task in goal.tasks ]

return(jsonify({"id":goal.id, "task_ids": task_list}))

# GET /goals/<id>/tasks
@goals_bp.route("/<id>/tasks", methods=["GET"])
def read_tasks_from_goal(id):
goal = get_record_by_id(Goal, id)

return jsonify(goal.to_dict_with_tasks())


42 changes: 42 additions & 0 deletions app/routes/routes_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from flask import jsonify, abort, make_response
from app.models.goal import Goal
from app.models.task import Task

def error_message(message, status_code):
abort(make_response(jsonify(dict(details=message)), status_code))

def get_record_by_id(cls, id):

Choose a reason for hiding this comment

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

Nice use of a more generic, flexible function!

try:
id = int(id)
except ValueError:
error_message(f"Invalid id {id}", 400)

model = cls.query.get(id)
if model:
return model

error_message(f"No model of type {cls} with id {id} found", 404)

def make_task_safely(data_dict):
try:
return Task.from_dict(data_dict)
except KeyError as err:
error_message(f"Missing key: {err}", 400)

def replace_task_safely(task, data_dict):
try:
task.replace_details(data_dict)
except KeyError as err:
error_message(f"Missing key: {err}", 400)

def make_goal_safely(data_dict):
try:
return Goal.from_dict(data_dict)
except KeyError as err:
error_message(f"Missing key: {err}", 400)

def replace_goal_safely(goal, data_dict):
try:
goal.replace_details(data_dict)
except KeyError as err:
error_message(f"Missing key: {err}", 400)
Comment on lines +20 to +42

Choose a reason for hiding this comment

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

Since make_task_safely & replace_task_safely are only used by task_routes.py, I would consider placing them in that file (same applies to the goal functions and goal route file). Another option would be to try making more generic create and update functions which could work with both models.

101 changes: 101 additions & 0 deletions app/routes/task_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from datetime import datetime
from flask import Blueprint, jsonify, request
from app.models.task import Task
from app import db
from .routes_helper import get_record_by_id, make_task_safely, replace_task_safely
import os
import requests
from dotenv import load_dotenv

load_dotenv()

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

def post_completed_task_to_slack(task):

Choose a reason for hiding this comment

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

Nice helper function for the Slack API call.

API_KEY = os.environ.get('SLACKBOT_API_KEY')
url = "https://slack.com/api/chat.postMessage"
data = {"channel": "task-notifications", "text": f"Someone just completed the task {task.title}"}
headers = {'Authorization' : f"Bearer {API_KEY}" }

requests.post(url, data=data, headers=headers)

# POST /tasks
@tasks_bp.route("", methods = ["POST"])
def create_task():
request_body = request.get_json()
new_task = make_task_safely(request_body)

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

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

# GET /tasks
@tasks_bp.route("", methods=["GET"])
def read_all_tasks():
sort_param = request.args.get("sort")

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

result_list = [task.to_dict() for task in tasks]

Choose a reason for hiding this comment

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

Great use of list comprehensions across the project.


return jsonify(result_list)

# GET /tasks/<id>
@tasks_bp.route("/<id>", methods=["Get"])
def read_task_by_id(id):
task = get_record_by_id(Task, id)
return jsonify({"task":task.to_dict()})

# PUT /tasks/<id>
@tasks_bp.route("/<id>", methods=["PUT"])
def replace_task_by_id(id):
request_body = request.get_json()
task = get_record_by_id(Task, id)

replace_task_safely(task, request_body)

db.session.add(task)
db.session.commit()

return jsonify({"task":task.to_dict()})

# DELETE /tasks/<id>
@tasks_bp.route("/<id>", methods=["DELETE"])
def delete_task_by_id(id):
task = get_record_by_id(Task, id)

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

return jsonify({"details": f'Task {task.id} "{task.title}" successfully deleted'})

# PATCH /tasks/<id>/mark_complete
@tasks_bp.route("/<id>/mark_complete", methods=["PATCH"])
def update_task_to_complete(id):
task = get_record_by_id(Task, id)

task.completed_at = datetime.now()

db.session.commit()

post_completed_task_to_slack(task)

return jsonify({"task":task.to_dict()})

# PATCH /tasks/<id>/mark_incomplete
@tasks_bp.route("/<id>/mark_incomplete", methods=["PATCH"])
def update_task_to_incomplete(id):
task = get_record_by_id(Task, id)
task.completed_at = None

db.session.commit()

return jsonify({"task":task.to_dict()})


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