Skip to content

nadia sarkissian - zoisite #108

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

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 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
6 changes: 5 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def create_app(test_config=None):

if test_config is None:
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
"SQLALCHEMY_DATABASE_URI")
"RENDER_DATABASE_URI")
else:
app.config["TESTING"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
Expand All @@ -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 choice to split up the routes into files that are more specific to the resources they work with!


return app
14 changes: 14 additions & 0 deletions app/helpers.py

Choose a reason for hiding this comment

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

I recommend placing this file in the routes directory to keep it close to the code that uses it.

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from flask import jsonify, abort, make_response

def validate_model(cls, model_id):
try:
model_id = int(model_id)
except:
abort(make_response(jsonify({"message":f"{cls.__name__} {model_id} invalid"}), 400))

Choose a reason for hiding this comment

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

To keep code shorter and easier to read, I'd suggest splitting this line up.


model = cls.query.get(model_id)

if not model:
abort(make_response(jsonify({"message":f"{cls.__name__} {model_id} not found"}), 404))

return model
19 changes: 18 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
from app import db


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

def to_dict(self, tasks=False):
goal_as_dict = {
"id": self.goal_id,
"title": self.title,
}
if tasks:

Choose a reason for hiding this comment

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

Nice use of an extra parameter to make the function more flexible!

goal_as_dict["tasks"] = [task.to_dict() for task in self.tasks]

return goal_as_dict

@classmethod
def from_dict(cls, goal_data):
return Goal(
title=goal_data["title"],
)
30 changes: 29 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,32 @@


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, nullable=False)
description = db.Column(db.String, nullable=False)
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")

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

Choose a reason for hiding this comment

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

Great choice to derive the value of is_complete from self.completed_at!

}

if self.goal_id:
task_as_dict["goal_id"] = self.goal_id

return task_as_dict
@classmethod

Choose a reason for hiding this comment

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

The PEP8 best practice for white space between functions is to use a single blank line to separate them

def from_dict(cls, task_data):
if "completed_at" not in task_data:
task_data["completed_at"] = None
Comment on lines +26 to +27

Choose a reason for hiding this comment

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

Nice conditional to ensure you avoid a KeyError if the user didn't send a completed_at value ^_^


return Task(
title=task_data["title"],
description=task_data["description"],
completed_at=task_data["completed_at"]
)
1 change: 0 additions & 1 deletion app/routes.py

This file was deleted.

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

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

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

return jsonify(results)

@goals_bp.route("/<goal_id>", methods=["GET"])
def get_one_goal(goal_id):
goal = validate_model(Goal, goal_id)
response = {"goal": goal.to_dict()}

Choose a reason for hiding this comment

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

Great way to add what you need for the response while getting to reuse the to_dict function!


return jsonify(response)

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

new_goal_is_valid = "title" in request_body
if not new_goal_is_valid:
abort(make_response(jsonify({"details":"Invalid data"}), 400))

new_goal = Goal.from_dict(request_body)
db.session.add(new_goal)
db.session.commit()

response = {"goal": new_goal.to_dict()}

return make_response((jsonify(response)), 201)

@goals_bp.route("<goal_id>", methods=["DELETE"])
def delete_one_goal(goal_id):
goal_to_delete = validate_model(Goal, goal_id)

db.session.delete(goal_to_delete)
db.session.commit()

message = {"details":f"Goal {goal_id} \"{goal_to_delete.title}\" successfully deleted"}
return make_response(message, 200)

@goals_bp.route("/<goal_id>", methods=["PUT"])
def update_one_goal(goal_id):
goal = validate_model(Goal, goal_id)
updated_data = request.get_json()

goal.title = updated_data["title"]

Choose a reason for hiding this comment

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

There's some error handling in create_goal that we could use here to alert the user if the request was missing a title key. How could we share the behavior with both functions?


db.session.commit()

response = {"goal": goal.to_dict()}

return make_response(response, 200)

@goals_bp.route("/<goal_id>/tasks", methods=["POST"])
def create_tasks_for_one_goal(goal_id):
goal = validate_model(Goal, goal_id)
request_body = request.get_json()

for task_id in request_body["task_ids"]:
task = validate_model(Task, task_id)
task.goal_id = goal.goal_id
db.session.commit()

task_id_list = [task.task_id for task in goal.tasks]

response_body = {
"id":goal.goal_id,
"task_ids": task_id_list
}
return jsonify(response_body)

@goals_bp.route("/<goal_id>/tasks", methods=["GET"])
def get_all_tasks_of_one_goal(goal_id):
goal = validate_model(Goal, goal_id)
goal = goal.to_dict(tasks=True)
return jsonify(goal)
103 changes: 103 additions & 0 deletions app/routes/task_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from app import db
from app.models.task import Task
from flask import Blueprint, jsonify, abort, make_response, request
from datetime import datetime
from app.helpers import validate_model
import os
import requests

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

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

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

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

results = [task.to_dict() for task in tasks]
Comment on lines +14 to +23

Choose a reason for hiding this comment

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

We could simplify the if/else tree a little if we get a reference to the Task.query object first:

task_query = Task.query
sort_direction = request.args.get("sort")

if sort_direction == "asc":
    task_query = task_query.order_by(Task.title.asc())
elif sort_direction == "desc":
    task_query = task_query.order_by(Task.title.desc())

results = [task.to_dict() for task in task_query.all()]


return jsonify(results)

@tasks_bp.route("/<task_id>", methods=["GET"])
def get_one_task(task_id):
task = validate_model(Task, task_id)
response = {"task": task.to_dict()}

return jsonify(response)

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

new_task_is_valid = "title" in request_body and "description" in request_body
if not new_task_is_valid:
abort(make_response(jsonify({"details":"Invalid data"}), 400))

new_task = Task.from_dict(request_body)
db.session.add(new_task)
db.session.commit()

response = {"task": new_task.to_dict()}

return make_response((jsonify(response)), 201)

@tasks_bp.route("/<task_id>", methods=["PUT"])
def update_one_task(task_id):
task = validate_model(Task, task_id)
updated_data = request.get_json()

task.title = updated_data["title"]
task.description = updated_data["description"]

db.session.commit()

response = {"task": task.to_dict()}

return make_response(response, 200)

@tasks_bp.route("<task_id>", methods=["DELETE"])
def delete_one_task(task_id):
task_to_delete = validate_model(Task, task_id)

db.session.delete(task_to_delete)
db.session.commit()

message = {"details":f"Task {task_id} \"{task_to_delete.title}\" successfully deleted"}
return make_response(message, 200)

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

if not task.completed_at:
task.completed_at = datetime.now()

db.session.commit()

url = "https://slack.com/api/chat.postMessage"
SLACK_API_TOKEN = os.environ.get("SLACK_API_TOKEN")

Choose a reason for hiding this comment

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

SLACK_API_TOKEN is a local variable of the complete_one_task function, so we'd typically see their name lowercased here. We use all caps for constant values, typically when declared where they're accessible across a file or project.

headers = {"Authorization": f"Bearer {SLACK_API_TOKEN}"}
message = f"Someone just completed the task {task.title}"
data = {"channel": "random", "text": message}

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

response = {"task": task.to_dict()}
return make_response(response, 200)

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

if task.completed_at:
task.completed_at = None

db.session.commit()
response = {"task": task.to_dict()}
return make_response(response, 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