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

Cedar - Maria O. #91

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
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 app.routes.task_routes import tasks_bp
app.register_blueprint(tasks_bp)
from app.routes.goal_routes import goals_bp
app.register_blueprint(goals_bp)

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


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

def to_dict(self):

Choose a reason for hiding this comment

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

Good work making these instance methods

return {
"id": self.goal_id,
"title": self.title
}

def add_tasks_to_goal_dict(self):
tasks_list = [task.task_id for task in self.tasks]
return {
"id": self.goal_id,
"task_ids": tasks_list
}

def get_tasks_from_goal_dict(self):
tasks = [task.to_dict_for_goal() for task in self.tasks]
return {
"id": self.goal_id,
"title": self.title,
"tasks": tasks
}
34 changes: 33 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
from flask import current_app
from app import db
import datetime


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)
# is_complete = db.Column(db.Boolean)
completed_at = db.Column(db.DateTime(timezone=True), nullable=True)
# relationship
goal_id = db.Column(db.Integer, db.ForeignKey('goal.goal_id'), nullable=True)

def to_dict(self):
return {
"id": self.task_id,
"title": self.title,
"description": self.description,
# "is_complete": True if self.completed_at == True else False
"is_complete": bool(self.completed_at)
}
Comment on lines +15 to +22

Choose a reason for hiding this comment

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

Consider how you could refactor this to include the goal_id when self.goal_id is not null (None), then look below for one possible solution.

dict =  {
            "id": self.task_id,
            "title": self.title,
            "description": self.description,
            # "is_complete": True if self.completed_at == True else False
            "is_complete": bool(self.completed_at)
        }
if self.goal_id:
    dict["goal_id"] = self.goal_id

return dict


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

def is_related_to_goal(self):

Choose a reason for hiding this comment

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

This is related to goal! Note how the suggested refactor above negates the need for this check.

if self.goal_id:
related_to_goal = True
else:
related_to_goal = False
return related_to_goal
2 changes: 0 additions & 2 deletions app/routes.py

This file was deleted.

105 changes: 105 additions & 0 deletions app/routes/goal_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from flask import Blueprint, request, make_response, jsonify, abort
from flask.wrappers import Response
from app.models.goal import Goal
from app.models.task import Task
from app import db
from datetime import date, datetime

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

# # Helper functions
def valid_int(id):

Choose a reason for hiding this comment

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

Note that you have this function in goal_routes and task_routes. Consider making a separate file with shared helper functions and importing them where you need them to DRY up your code.

try:
number = int(id)
return number
except:
response_body = 'Invalid Data'
abort(make_response(response_body,400))

def get_goal_from_id(goal_id):
id = valid_int(goal_id)
selected_goal = Goal.query.filter_by(goal_id=goal_id).one_or_none()
# Goal not found
if selected_goal is None:
abort(make_response("Not Found", 404))
return selected_goal

def valid_goal(request_body):
if "title" not in request_body:
abort(make_response({"details": "Invalid data"}, 400))
# #

# Create a goal
@goals_bp.route("", methods=["POST"], strict_slashes=False)
def create_goal():
request_body = request.get_json()
valid_goal(request_body)

Choose a reason for hiding this comment

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

Great use of a helper function. You might consider also including the creation of the goal in valid_goal such that the function either aborts because of Invalid data or returns the goal instance.

new_goal = Goal(title=request_body["title"])
db.session.add(new_goal)
db.session.commit()
response = {"goal": new_goal.to_dict()}
return make_response(response, 201)

# Get all goals
@goals_bp.route("", methods=["GET"], strict_slashes=False)
def get_all_goals():
response_list = []
goal_objects = Goal.query.all()
for goal in goal_objects:
response_list.append(goal.to_dict())
Comment on lines +48 to +49

Choose a reason for hiding this comment

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

This could be a nice place to use a list comprehension.

return make_response(jsonify(response_list), 200)

# Get one goal
@goals_bp.route("/<goal_id>", methods=["GET"], strict_slashes=False)
def get_goal(goal_id):
selected_goal = get_goal_from_id(goal_id)
response_body = {"goal": selected_goal.to_dict()}
return make_response(response_body, 200)

# Update goal
@goals_bp.route("/<goal_id>", methods=["PUT"], strict_slashes=False)
def update_goal(goal_id):
selected_goal = get_goal_from_id(goal_id)
request_body = request.get_json()
if "title" in request_body:
selected_goal.title = request_body["title"]
db.session.commit()
response_body = {"goal": selected_goal.to_dict()}
return make_response(response_body, 200)

# Delete goal
@goals_bp.route("/<goal_id>", methods=["DELETE"], strict_slashes=False)
def delete_goal(goal_id):
selected_goal = get_goal_from_id(goal_id)
db.session.delete(selected_goal)
db.session.commit()
response_body = {'details': f'Goal {goal_id} "{selected_goal.title}" successfully deleted'}
return make_response(response_body, 200)

# Post list of task_ids to a goal
@goals_bp.route("/<goal_id>/tasks", methods=["POST"], strict_slashes=False)
def post_tasks_to_goal(goal_id):
selected_goal = get_goal_from_id(goal_id)
selected_goal = Goal.query.get(goal_id)
request_body = request.get_json()
task_ids_list = request_body["task_ids"]

for task_id in task_ids_list:
task = Task.query.get(task_id)

Choose a reason for hiding this comment

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

We should probably double check that the task ids in the request body actually correspond to existing tasks. We can do that with a get_or_404 or a check on whether the task is None.

Also note that add_tasks_to_goal_dict does the work of creating a list of task ids. That list has already been handed in as part of the request body, so this extra work is not necessary.

selected_goal.tasks.append(task)

db.session.commit()
response_body = selected_goal.add_tasks_to_goal_dict()
return make_response(response_body, 200)

# Get tasks from goal
@goals_bp.route("/<goal_id>/tasks", methods=["GET"], strict_slashes=False)
def get_tasks_from_goal(goal_id):
selected_goal = get_goal_from_id(goal_id)
selected_goal = Goal.query.get(goal_id)



response_body = selected_goal.get_tasks_from_goal_dict()

return make_response(response_body, 200)
111 changes: 111 additions & 0 deletions app/routes/task_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from flask import Blueprint, request, make_response, jsonify, abort
from app.models.task import Task
from app import db
from datetime import date, datetime

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

# # Helper functions
def valid_int(id):
try:
number = int(id)
return number
except:
response_body = 'Invalid Data'
abort(make_response(response_body,400))

def get_task_from_id(task_id):
id = valid_int(task_id)
selected_task = Task.query.filter_by(task_id=task_id).one_or_none()
# Task not found
if selected_task is None:
abort(make_response("Not Found", 404))
return selected_task

def valid_task(request_body):
if "title" not in request_body or "description" not in request_body or "completed_at" not in request_body:
abort(make_response({"details": "Invalid data"}, 400))
# #

# Create one task with error handlers
@tasks_bp.route("", methods=["POST"], strict_slashes=False)
def create_task():
request_body = request.get_json()
valid_task(request_body)

Choose a reason for hiding this comment

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

Just like with goal, consider using valid_task to return a task instance.

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

# Get all tasks
@tasks_bp.route("", methods=["GET"], strict_slashes=False)
def get_all_tasks():
sort_tasks = request.args.get("sort")
response_list = []
# Sort task: by title, ascending
if sort_tasks == "asc":
task_objects = Task.query.order_by(Task.title.asc())
# Sort task: by title descending
elif sort_tasks == "desc":
task_objects = Task.query.order_by(Task.title.desc())
else:
task_objects = Task.query.all()
for task in task_objects:
response_list.append(task.to_dict())
return make_response(jsonify(response_list), 200)

# Get one task
@tasks_bp.route("/<task_id>", methods=["GET"], strict_slashes=False)
def get_task(task_id):
selected_task = get_task_from_id(task_id)
if selected_task.is_related_to_goal():
response_body = {"task": selected_task.to_dict_for_goal()}
else:
response_body = {"task": selected_task.to_dict()}
return make_response(response_body, 200)

# Update task
@tasks_bp.route("/<task_id>", methods=["PUT"], strict_slashes=False)
def update_task(task_id):
selected_task = get_task_from_id(task_id)
request_body = request.get_json()
if "title" in request_body:
selected_task.title = request_body["title"]
if "description" in request_body:
selected_task.description = request_body["description"]
db.session.commit()
response_body = {"task": selected_task.to_dict()}
return make_response(response_body, 200)

# Mark task complete
@tasks_bp.route("/<task_id>/mark_complete", methods=["PATCH"], strict_slashes=False)
def mark_task_complete(task_id):
selected_task = get_task_from_id(task_id)
# selected_task.is_complete = True
selected_task.completed_at = datetime.now()
db.session.commit()
response_body = {"task": selected_task.to_dict()}
return make_response(response_body, 200)

# Mark task incomplete
@tasks_bp.route("/<task_id>/mark_incomplete", methods=["PATCH"], strict_slashes=False)
def mark_task_incomplete(task_id):
selected_task = get_task_from_id(task_id)
selected_task.completed_at = None
db.session.commit()
response_body = {"task": selected_task.to_dict()}
return make_response(response_body, 200)

# Delete task
@tasks_bp.route("/<task_id>", methods=["DELETE"], strict_slashes=False)
def delete_task(task_id):
selected_task = get_task_from_id(task_id)
db.session.delete(selected_task)
db.session.commit()
response_body = {'details': f'Task {task_id} "{selected_task.title}" successfully deleted'}
return make_response(response_body, 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