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

Marie Keefer Task List API #117

Open
wants to merge 7 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()'
16 changes: 9 additions & 7 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
import os
from dotenv import load_dotenv


from flask import Flask
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
migrate = Migrate()
load_dotenv()
Expand All @@ -21,14 +19,18 @@ def create_app(test_config=None):
app.config["TESTING"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
"SQLALCHEMY_TEST_DATABASE_URI")

# Import models here for Alembic setup
from app.models.task import Task
from app.models.goal import Goal
from app.models.task import Task

db.init_app(app)
migrate.init_app(app, db)

# Register Blueprints here
from app.task_routes import tasks_bp
app.register_blueprint(tasks_bp)
from app.goal_routes import goals_bp
app.register_blueprint(goals_bp)

return app
89 changes: 89 additions & 0 deletions app/goal_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from app import db
from app.models.goal import Goal
from app.models.task import Task
from flask import Blueprint,jsonify, make_response, request
from app.helper_functions import validate_model
from sqlalchemy import asc, desc


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

@goals_bp.route("", methods=["POST"])
def create_goal():
request_body = request.get_json()
if not "title" in request_body:
return make_response({"details": "Invalid data"}, 400)

new_goal = Goal.from_dict(request_body)

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

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

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

task_list = []
for task_id in request_body["task_ids"]:
task = validate_model(Task, task_id)
task.goal = goal
task_list.append(task_id)

db.session.commit()

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

@goals_bp.route("", methods=["GET"])
def read_all_goals():
sort_query = request.args.get("sort")
if sort_query:
if sort_query == "asc":
goals = Goal.query.order_by(Goal.title.asc()).all()
elif sort_query == "desc":
goals = Goal.query.order_by(Goal.title.desc()).all()
else:
goals = Goal.query.all()

goal_response = []
for goal in goals:
goal_response.append(goal.to_dict())
return make_response(jsonify(goal_response), 200)

@goals_bp.route("/<goal_id>", methods=["GET"])
def read_one_goal(goal_id):
goal = validate_model(Goal, goal_id)

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

@goals_bp.route("/<goal_id>/tasks", methods=["GET"])
def read_tasks_of_one_goal(goal_id):
goal = validate_model(Goal, goal_id)

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

return make_response({"id": goal.goal_id, "title": goal.title, "tasks": task_list})

Choose a reason for hiding this comment

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

whoops! forgot your status code!

    return make_response({"id": goal.goal_id, "title": goal.title, "tasks": task_list}, 200)


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

goal.title = request_body["title"]

db.session.commit()

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

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

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

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

Choose a reason for hiding this comment

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

This works! We could also do this:

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

We can take advantage of the two different sets of quotes

29 changes: 29 additions & 0 deletions app/helper_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from app import db
from flask import abort, make_response
import os, requests

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

model = cls.query.get(model_id)

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

return model


def slack_bot_message(message):
slack_api_key = os.environ.get("SLACK_BOT_TOKEN")
slack_url = "https://slack.com/api/chat.postMessage"
header = {"Authorization": slack_api_key}

query_params = {
"channel": "task-notifications",
"text": message
}
print(slack_api_key)
requests.post(url=slack_url, data=query_params, headers=header)
15 changes: 15 additions & 0 deletions app/models/goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,18 @@

class Goal(db.Model):
goal_id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String)

Choose a reason for hiding this comment

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

See the Task model for more information about required attributes being given nullable=True

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

def to_dict(self):

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


@classmethod
def from_dict(cls, goal_data):
new_goal = Goal(title=goal_data["title"])

Choose a reason for hiding this comment

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

This works! But what if we changed the name of the model? Now this would continue creating Goal which no longer exists, so let's take advantage of the cls parameter. This will represent the current model, even if we change its name.

        new_goal = cls(title=goal_data["title"])

return new_goal
40 changes: 39 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,42 @@


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)
description = db.Column(db.String)
Comment on lines +6 to +7

Choose a reason for hiding this comment

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

It turns out that nullable=True is the default value for nullable. So all of the columns for Task here are currently marked as nullable. But should title or description be allowed to be NULL? (Does that make sense from a data standpoint?) Consider adding nullable=False to those columns.

The way the project emphasized that completed_at needs to accept NULL values may make it seem like we needed to explicitly call out that nullable should be True, but it turns out this is the default for nullable. Instead, we should think about the other data in our model and consider whether it makes sense for any of it to be NULL. If not, we can have the database help us protect against that happening!

completed_at = db.Column(db.DateTime, default=None)
goal_id = db.Column(db.Integer, db.ForeignKey("goal.goal_id"), nullable=True)
goal = db.relationship("Goal", back_populates="tasks")

def to_dict(self):
if self.completed_at:

return {
"id": self.task_id,
"title": self.title,
"description": self.description,
"is_complete": True
}
if self.goal_id and not self.completed_at:
return {
"id": self.task_id,
"goal_id": self.goal_id,
"title": self.title,
"description": self.description,
"is_complete": False
}

else:
return {
"id": self.task_id,
"title": self.title,
"description": self.description,
"is_complete": False
}


@classmethod
def from_dict(cls, task_data):
new_task = Task(title=task_data["title"],

Choose a reason for hiding this comment

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

        new_task = cls(title=task_data["title"],

description=task_data["description"])
return new_task
1 change: 0 additions & 1 deletion app/routes.py

This file was deleted.

94 changes: 94 additions & 0 deletions app/task_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from app import db
from app.helper_functions import validate_model, slack_bot_message
from app.models.task import Task
from datetime import datetime
from flask import Blueprint, abort, jsonify, make_response, request
from sqlalchemy import asc, desc
from dotenv import load_dotenv
load_dotenv()
Comment on lines +7 to +8

Choose a reason for hiding this comment

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

We can get rid of this since we aren't access the environmental variables in this file!

Suggested change
from dotenv import load_dotenv
load_dotenv()


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


# -------Routes-------
@tasks_bp.route("", methods=["POST"])
def create_task():
request_body = request.get_json()
if not "title" in request_body or not "description" in request_body:
return make_response({"details": "Invalid data"}, 400)

new_task = Task.from_dict(request_body)

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

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

@tasks_bp.route("", methods=["GET"])
def read_all_tasks():
sort_query = request.args.get("sort")
if sort_query:
if sort_query == "asc":
tasks = Task.query.order_by(Task.title.asc()).all()
elif sort_query == "desc":
tasks = Task.query.order_by(Task.title.desc()).all()
else:
tasks = Task.query.all()

task_response = []
for task in tasks:
task_response.append(task.to_dict())
return make_response(jsonify(task_response), 200)


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

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

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

task.title = request_body["title"]
task.description = request_body["description"]
Comment on lines +55 to +56

Choose a reason for hiding this comment

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

We could turn this into a helper method in Task model


db.session.commit()

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

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

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

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

Choose a reason for hiding this comment

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

This works! We could also do this:

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

We can take advantage of the two different sets of quotes


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

task.completed_at = datetime.utcnow()

db.session.commit()

slack_bot_message(f"Someone just completed the task {task.title}")

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

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

task.completed_at = None
db.session.commit()

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