Skip to content

ivy-task-list-api #133

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 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 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 @@ -5,6 +5,7 @@
from dotenv import load_dotenv



db = SQLAlchemy()
migrate = Migrate()
load_dotenv()
Expand All @@ -30,5 +31,9 @@ def create_app(test_config=None):
migrate.init_app(app, db)

# Register Blueprints here
from .task_routes import task_bp
app.register_blueprint(task_bp)
from .goal_routes import goal_bp
app.register_blueprint(goal_bp)

return app
122 changes: 122 additions & 0 deletions app/goal_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from app import db
from .models.task import Task
from .models.goal import Goal
from flask import Blueprint, request, make_response, jsonify, abort
import sqlalchemy
from .route_helpers import validate_model_id
import random



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

# all goal methods
# Create
@goal_bp.route("", methods=["POST"])
def create_goal():

request_body = request.get_json()
new_goal = Goal.from_dict(request_body)

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

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

# Read
@goal_bp.route("", methods=["GET"])
def get_all_goals():

sort_query = request.args.get("sort")
if sort_query:
sort_function = getattr(sqlalchemy, sort_query)
goal_list = Goal.query.order_by(sort_function(Goal.title))
else:
goal_list = Goal.query.all()
Comment on lines +31 to +35

Choose a reason for hiding this comment

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

clever, nice!


response = []
for goal in goal_list:
response.append(goal.to_dict())

return jsonify(response), 200

@goal_bp.route("random", methods=["GET"])
def get_random_goal():
Comment on lines +43 to +44

Choose a reason for hiding this comment

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

Interesting route!

goal_list = Goal.query.all()
max_index = len(goal_list) - 1
rand_goal = goal_list[random.randint(0,max_index)]

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



# Individual goal methods

# Read
@goal_bp.route("/<goal_id>", methods=["GET"])
def get_specific_goal(goal_id):

goal = validate_model_id(Goal, goal_id)

return {"goal" : goal.to_dict()}, 200

# Update
@goal_bp.route ("/<goal_id>", methods=["PUT"])
def update_goal(goal_id):
goal = validate_model_id(Goal, goal_id)

request_body = request.get_json()

goal.title = request_body["title"]

db.session.commit()

return {"goal" : goal.to_dict()}, 200

@goal_bp.route("<goal_id>/<marker>", methods=["PATCH"])
def mark_goal_status(goal_id, marker):
goal = validate_model_id(Goal, goal_id)
eval("goal." + marker + "()")

db.session.commit()

return {"goal" : goal.to_dict()}, 200

# Delete
@goal_bp.route("/<goal_id>", methods=["DELETE"])
def delete_goal(goal_id):
goal = validate_model_id(Goal, goal_id)

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

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


# Nested route for task assigned to one goal

@goal_bp.route("/<goal_id>/tasks", methods=["POST"])
def post_task_ids_to_goal(goal_id):
goal = validate_model_id(Goal, goal_id)

request_body = request.get_json()


for task_id in request_body["task_ids"]:
new_task = validate_model_id(Task, task_id)
new_task.goal_id = goal_id

db.session.commit()

return make_response({
"id" : goal.id,
"task_ids" : goal.get_task_ids()
}, 200)

@goal_bp.route("/<goal_id>/tasks", methods=["GET"])
def get_tasks_from_goal(goal_id):
goal = validate_model_id(Goal, goal_id)
response_body = goal.to_dict()
response_body.update({"tasks" : goal.get_tasks()})

return make_response(response_body, 200)
36 changes: 35 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,39 @@
from app import db
from flask import abort, make_response


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

def to_dict(self):
goal_dict = {
"id" : self.id,
"title" : self.title,
}
return goal_dict

def get_tasks(self):
response = []
for task in self.tasks:
response.append(task.to_dict())
return response

def get_task_ids(self):
response = []
for task in self.tasks:
response.append(task.id)
return response



@classmethod
def from_dict(cls, request_body):
Comment on lines +31 to +32

Choose a reason for hiding this comment

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

Great helper method

try:
goal = Goal(title = request_body["title"])
return goal
except:
abort(make_response({"details" : "Invalid data"}, 400))


49 changes: 48 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,52 @@
from app import db
from flask import abort, make_response
import datetime


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, nullable = True)
goal = db.relationship("Goal", back_populates="tasks")
goal_id = db.Column(db.Integer, db.ForeignKey('goal.id'), nullable = True)



def is_complete(self):
if self.completed_at == None:
return False
else:
return True

def to_dict(self):
task_dict = {
"id" :self.id,
"title" : self.title,
"description" : self.description,
"is_complete" : self.is_complete()
}
if self.goal_id != None:
task_dict.update({"goal_id" : self.goal_id})
return task_dict

def mark_complete(self):
if self.is_complete():
pass
else:
self.completed_at = datetime.datetime.now()

def mark_incomplete(self):
if not self.is_complete():
pass
else:
self.completed_at = None

@classmethod
def from_dict(cls, request_body):
try:
task = Task(title = request_body["title"],
description = request_body["description"])
return task
except:
abort(make_response({"details" : "Invalid data"}, 400))
19 changes: 19 additions & 0 deletions app/route_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

from flask import make_response, abort



def validate_model_id(cls, id):

Choose a reason for hiding this comment

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

Nice helper function. It would be good to have another to validate required fields in the request body.

cls_name = cls.__name__

try:
id = int(id)
except:
abort(make_response({"message" : f"{cls_name.lower()} id: {id} is invalid"}, 400))

model = cls.query.get(id)

if not model:
abort(make_response({"message" : f"{cls_name.lower()} {id} not found"}, 404))

return model
1 change: 0 additions & 1 deletion app/routes.py

This file was deleted.

86 changes: 86 additions & 0 deletions app/task_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from app import db
from .models.task import Task
from .models.goal import Goal
from flask import Blueprint, request, make_response, jsonify, abort
import sqlalchemy
from .route_helpers import validate_model_id



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


# all task methods
# Create
@task_bp.route("", methods=["POST"])
def create_task():

request_body = request.get_json()
new_task = Task.from_dict(request_body)
Comment on lines +18 to +19

Choose a reason for hiding this comment

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

There doesn't seem to be any validation to check to see if the request body has a title or description.


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

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

# Read
@task_bp.route("", methods=["GET"])
def get_all_task():

sort_query = request.args.get("sort")
if sort_query:
sort_function = getattr(sqlalchemy, sort_query)
task_list = Task.query.order_by(sort_function(Task.title))
else:
task_list = Task.query.all()

response = []
for task in task_list:
response.append(task.to_dict())

return jsonify(response), 200



# Individual task methods

# Read
@task_bp.route("/<task_id>", methods=["GET"])
def get_one_task(task_id):

task = validate_model_id(Task, task_id)

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

# Update
@task_bp.route ("/<task_id>", methods=["PUT"])
def update_task(task_id):
task = validate_model_id(Task, task_id)

request_body = request.get_json()

task.title = request_body["title"]
task.description = request_body["description"]
Comment on lines +62 to +63

Choose a reason for hiding this comment

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

No data validation here?


db.session.commit()

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

@task_bp.route("<task_id>/<marker>", methods=["PATCH"])
def mark_task_status(task_id, marker):
task = validate_model_id(Task, task_id)
eval("task." + marker + "()")

db.session.commit()

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

# Delete
@task_bp.route("/<task_id>", methods=["DELETE"])
def delete_task(task_id):
task = validate_model_id(Task, task_id)

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

return make_response({"details" : f"Task {task_id} \"{task.title}\" successfully deleted"}, 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