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

Sea Turtles - Shannon Bellemore - Task List API #116

Open
wants to merge 12 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()'
7 changes: 6 additions & 1 deletion 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 .task_routes import tasks_bp
app.register_blueprint(tasks_bp)

return app
from .goal_routes import goals_bp
app.register_blueprint(goals_bp)
Comment on lines +33 to +37
Copy link

Choose a reason for hiding this comment

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

I like that you organized tasks and goals into two separate files. You can also move them to a directory called routes to make it even more organized.


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

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

# helper functions
def validate_goal(goal_id):
Copy link

Choose a reason for hiding this comment

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

great validation and error handling. You can also move this function and other helper functions into a file helper_functions.py

try:
goal_id = int(goal_id)
except:
abort(make_response({"error": f"Goal id invalid"}, 400))

goal = Goal.query.get(goal_id)
if not goal:
abort(make_response({"error":f"Goal not found"}, 404))
return goal


# ----------------- END POINTS -------------------

# creates new goal to the database
@goals_bp.route("", methods=["POST"])
def create_goal():
request_body = request.get_json()
if "title" not in request_body:
return make_response(jsonify({"details": "Invalid data"}), 400)
Copy link

Choose a reason for hiding this comment

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

you can add more specific details Invalid Data: the title is missing just like you did in your validate helper function


new_goal = Goal(
title=request_body["title"])

db.session.add(new_goal)
db.session.commit()
response_body = {
"goal": new_goal.make_dict()}
return make_response(jsonify(response_body), 201)


# get all saved goals
@goals_bp.route("", methods=["GET"])
def get_all_goals():
goals = Goal.query.all()
response_body = [goal.make_dict() for goal in goals]
return make_response(jsonify(response_body), 200)


# get one goal by task id
@goals_bp.route("/<goal_id>", methods=["GET"])
def get_one_goal(goal_id):
goal = validate_goal(goal_id)
response_body = {"goal": goal.make_dict()}
return make_response(jsonify(response_body), 200)


# update goal
@goals_bp.route("/<goal_id>", methods=["PUT"])
def update_goal(goal_id):
request_body = request.get_json()
goal = validate_goal(goal_id)
goal.title = request_body["title"]
db.session.commit()
response_body = {"goal": goal.make_dict()}
return make_response(jsonify(response_body), 200)


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


# adds tasks to a goal
@goals_bp.route("/<goal_id>/tasks", methods=["POST"])
def add_task_to_goal(goal_id):
goal = validate_goal(goal_id)
request_body = request.get_json()
task_ids = request_body["task_ids"]
for task_id in task_ids:
task = validate_task(task_id)
task.goal_id = goal_id
db.session.commit()

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


# get tasks of a goal
@goals_bp.route("/<goal_id>/tasks", methods=["GET"])
def get_tasks_of_goal(goal_id):
goal = validate_goal(goal_id)
tasks = goal.tasks
list_of_tasks = []
for task in tasks:
list_of_tasks.append(task.make_dict())

response_body = {"id": goal.goal_id, "title": goal.title, "tasks": list_of_tasks}
return make_response(jsonify(response_body), 200)
10 changes: 9 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,12 @@


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)
tasks = db.relationship("Task", back_populates="goal", lazy=True)
Copy link

Choose a reason for hiding this comment

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

Because the default value for lazy is True you could technically leave it off. More info here https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html


def make_dict(self):
return {
"id": self.goal_id,
"title": self.title,
}
24 changes: 23 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,26 @@


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)
Copy link

Choose a reason for hiding this comment

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

you could add nullable=True here because completed_at doesn't have to exist

goal_id = db.Column(db.Integer, db.ForeignKey("goal.goal_id"))
goal = db.relationship("Goal", back_populates="tasks")

def make_dict(self):
Copy link

Choose a reason for hiding this comment

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

Because this is repetitive you could create a response dictionary to check for goal_id something like

response_dict = dict(
    id=self.task_id,
    title=self.title,
    description=self.description,
    is_complete= bool(self.completed_at)
)
        
if self.goal_id:
    response_dict["goal_id"] = self.goal_id
            
return response_dict

if self.goal_id:
return {
"id": self.task_id,
"goal_id": self.goal_id,
"title": self.title,
"description": self.description,
"is_complete": True if self.completed_at else False
}
else:
return {
"id": self.task_id,
"title": self.title,
"description": self.description,
"is_complete": True if self.completed_at else False
}
1 change: 0 additions & 1 deletion app/routes.py

This file was deleted.

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

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

# helper functions
def validate_task(task_id):
try:
task_id = int(task_id)
except:
abort(make_response({"error": f"Task id invalid"}, 400))

task = Task.query.get(task_id)
if not task:
abort(make_response({"error":f"Task not found"}, 404))
return task

def slack_bot(task):
Copy link

Choose a reason for hiding this comment

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

🙌🏽

load_dotenv()
message = f"Someone just completed the task {task.title}"
slack_url = "https://slack.com/api/chat.postMessage"
header = {'Authorization': f'Bearer {os.environ.get("SLACK_API_TOKEN")}'}
param = {"channel": "task-notifications",
"text": message}

return requests.post(url=slack_url, params=param, headers=header)


# ----------------- END POINTS -------------------

# creates new task to the database
@tasks_bp.route("", methods=["POST"])
def create_task():
request_body = request.get_json()

if "title" not in request_body or "description" not in request_body:
return make_response(jsonify({"details": "Invalid data"}), 400)

if "completed_at" in request_body:
new_task = Task(
title=request_body["title"],
description=request_body["description"],
completed_at=request_body["completed_at"])
else:
new_task = Task(
title=request_body["title"],
description=request_body["description"])
Comment on lines +43 to +51
Copy link

Choose a reason for hiding this comment

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

How could you use something like a ternary or conditional on line 47 instead of an if/else?


db.session.add(new_task)
db.session.commit()
response_body = {"task": new_task.make_dict()}
return make_response(jsonify(response_body), 201)


# get all saved tasks
@tasks_bp.route("", methods=["GET"])
def get_all_tasks():
title_query = request.args.get("title")
description_query = request.args.get("description")
sort_query = request.args.get("sort")

if sort_query == "desc":
tasks = Task.query.order_by(Task.title.desc())
elif sort_query == "asc":
tasks = Task.query.order_by(Task.title.asc())
elif title_query:
tasks = Task.query.filter_by(title=title_query)
elif description_query:
tasks = Task.query.filter_by(description=description_query)
else:
tasks = Task.query.all()

response_body = [task.make_dict() for task in tasks]
return make_response(jsonify(response_body), 200)


# get one task by task id
@tasks_bp.route("/<task_id>", methods=["GET"])
def get_one_task(task_id):
task = validate_task(task_id)
response_body = {
"task": task.make_dict()}
return make_response(jsonify(response_body), 200)


# update a task
@tasks_bp.route("/<task_id>", methods=["PUT"])
def update_task(task_id):
task = validate_task(task_id)
request_body = request.get_json()
task.title = request_body["title"]
task.description = request_body["description"]

db.session.commit()
response_body = {"task": task.make_dict()}
return make_response(jsonify(response_body), 200)

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

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

response_body = {
"details":f'Task {task.task_id} "{task.title}" successfully deleted'}
return make_response(jsonify(response_body), 200)


# patches a task to mark as complete
@tasks_bp.route("/<task_id>/mark_complete", methods=["PATCH"])
def mark_task_as_complete(task_id):
task = validate_task(task_id)
task.completed_at = datetime.utcnow()

db.session.commit()
slack_bot(task)
response_body = {"task": task.make_dict()}
return make_response(jsonify(response_body), 200)


# patches a task to mark as incomplete
@tasks_bp.route("/<task_id>/mark_incomplete", methods=["PATCH"])
def mark_task_as_incomplete(task_id):
task = validate_task(task_id)
task.completed_at = None

db.session.commit()
response_body = {"task": task.make_dict()}
return make_response(jsonify(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