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

TASK LIST API #109

Open
wants to merge 3 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()'
10 changes: 8 additions & 2 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@
import os
from dotenv import load_dotenv


db = SQLAlchemy()
migrate = Migrate()
load_dotenv()


def create_app(test_config=None):
app = Flask(__name__)
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
Expand All @@ -31,4 +29,12 @@ def create_app(test_config=None):

# Register Blueprints here

from .models.goal import Goal
from .models.task import Task
from .routes import task_bp
from .routes import goal_bp
Comment on lines +34 to +35
Copy link

Choose a reason for hiding this comment

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

you could seperate the routes for tasks and goals into two separate files. So you can have task_routes.py and goal_routes.py


app.register_blueprint(task_bp)
app.register_blueprint(goal_bp)

return app
21 changes: 20 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
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", lazy = True, backref="goal")
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 to_dict(self):
Copy link

Choose a reason for hiding this comment

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

these instance methods look good !

return dict(goal=({
"id" : self.goal_id,
"title": self.title
}))

def task_ids(self):
task_ids = []
for task in self.tasks:
task_ids.append(task.task_id)
return task_ids

def goal_to_task(self):
goal_description = self.to_dict()["goal"]
task_description = [task.dict()["task"] for task in self.tasks]
goal_description["tasks"] = task_description
return goal_description
23 changes: 22 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
from app import db


class Task(db.Model):
task_id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String, nullable=False)
description = db.Column(db.String, nullable=False)
completed_at = db.Column(db.DateTime, nullable=True)
Copy link

Choose a reason for hiding this comment

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

good job adding nullable=True here because completed_at doesn't have to exist

goal_value = db.Column(db.Integer, db.ForeignKey('goal.goal_id'), nullable=True)

def dict(self):
return (dict(task={
"id" : self.task_id,
"title": self.title,
"description" : self.description,
"is_complete" : bool(self.completed_at),
"goal_id" : self.goal_value

}))

def to_dict(self):
return (dict(task={
"id" : self.task_id,
"title": self.title,
"description" : self.description,
"is_complete" : bool(self.completed_at)
}))
Comment on lines +10 to +26
Copy link

Choose a reason for hiding this comment

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

you can refactor this to one instance method

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

244 changes: 243 additions & 1 deletion app/routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,243 @@
from flask import Blueprint
from app import db
from app.models.goal import Goal
from app.models.task import Task
from flask import Blueprint, jsonify, make_response, request, abort
from datetime import datetime
from sqlalchemy import desc, asc
import requests, os


#instia Goals & Task Blueprint

task_bp = Blueprint("tasks", __name__, url_prefix="/tasks")
goal_bp = Blueprint("goals", __name__, url_prefix="/goals")
SLACK_AUTH = os.environ.get("SLACK_API_AUTH")

#-------------------------- SLACK --------------------------#

def post_to_slack(task_identity):
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 pulled this into a helper function. You could move this function and other helper functions into a separate file helper_functions.py to remove some code from this file.



url = "https://slack.com/api/chat.postMessage"

headers = {"Authorization" : f"Bearer {SLACK_AUTH}"}

data = {
"channel" : "task-notifications",
"text" : f"Someone just completed the task {task_identity.title}"
}

response = requests.post(url, headers=headers, data=data)
response_body = response.json()

return response_body

#-------------------------- GOAL HELPER --------------------------#

def validate_goal(goal_id):
try:
goal_id = int(goal_id)
except:
abort(make_response(dict(message=f"Goal {goal_id} is not valid."), 400))

goal = Goal.query.get(goal_id)
if not goal:
return abort(make_response(dict(message=f"Goal {goal_id} is invalid."), 404))
else:
return goal

#-------------------------- GOAL --------------------------#
Copy link

Choose a reason for hiding this comment

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

I do like that you added these comments to organize this file


@goal_bp.route("", methods=["POST"])
def create_goal():
request_body = request.get_json()
try:
new_goal = Goal(title = request_body['title']
)
except:
abort(make_response({"details":f"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.

think about how you can add more details to your error message Invalid Data: Title is missing. I think this helps the user understand what's causing the error.


db.session.add(new_goal)
db.session.commit()
return {
"goal": {
"id": new_goal.goal_id,
"title": new_goal.title,
}},201

@goal_bp.route("/<goal_id>/tasks", methods=["POST"])
def create_goal_list(goal_id):
Copy link

Choose a reason for hiding this comment

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

Very clean!

from app.models.task import Task

goal = validate_goal(goal_id)
request_body = request.get_json()

for task_id in request_body["task_ids"]:
task = Task.query.get(task_id)
goal.tasks.append(task)

db.session.add(goal)
db.session.commit()

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

@goal_bp.route("", methods=["GET"])
def read_all_goals():
goals_response = []

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

if order_goals == "asc":
goals = Goal.query.order_by(Goal.title.asc())
elif order_goals == "desc":
goals = Goal.query.order_by(Goal.title.desc())
else:
goals = Goal.query.all()

for goal in goals:
goal_object = goal.to_dict()
goals_response.append(goal_object["goal"])

return jsonify(goals_response), 200


@goal_bp.route("/<goal_id>", methods=["GET"])
def get_one_goal_by_id(goal_id):
goal = validate_goal(goal_id)
return jsonify(goal.to_dict()), 200


@goal_bp.route("/<goal_id>/tasks", methods=["GET"])
def get_goal(goal_id):
goal = validate_goal(goal_id)
return goal.goal_to_task(), 200

@goal_bp.route("/<goal_id>", methods=["PUT"])
def replace_goal_by_id(goal_id):
goal = validate_goal(goal_id)
request_body = request.get_json()
goal.title = request_body["title"]
db.session.commit()
return jsonify(goal.to_dict()), 200

@goal_bp.route("/<goal_id>", methods=["DELETE"])
def remove_goal(goal_id):
goal = validate_goal(goal_id)

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

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

#-------------------------- TASK HELPER --------------------------#

def validate_task(task_id):
try:
task_id = int(task_id)
except:
abort(make_response(dict(message=f"Task {task_id} is not an interger."), 400))

task = Task.query.get(task_id)

if not task:
return abort(make_response(dict(message=f"Task {task_id} is invalid."), 404))
else:
return task

#-------------------------- TASK --------------------------#

@task_bp.route("", methods=["POST"])
def create_task():
request_body = request.get_json()
try:
new_task = Task(title = request_body['title'], description = request_body['description'])
except:
abort(make_response({"details":f"Invalid data"}, 400))

if "completed_at" in request_body: new_task.completed_at = request_body["completed_at"]

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

return {
"task": {
Copy link

Choose a reason for hiding this comment

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

didn't you create an instance method you could use here?

"id": new_task.task_id,
"title": new_task.title,
"description": new_task.description,
"is_complete": bool(new_task.completed_at)}},201

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

task_response = []
for task in tasks:
task_response.append(
{
"id":task.task_id,
"title":task.title,
"description":task.description,
"is_complete": False
}
Comment on lines +182 to +187
Copy link

Choose a reason for hiding this comment

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

use your instance method you created

)
return jsonify(task_response)


@task_bp.route("/<task_identity>", methods=["GET"])
def get_one_task_by_id(task_identity):
task = validate_task(task_identity)
if task.goal_value:
return jsonify(task.dict()), 200

else:
return jsonify(task.to_dict()), 200

@task_bp.route("/<task_identity>", methods=["DELETE"])
def delete_task_by_id(task_identity):
task = validate_task(task_identity)

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

return make_response(dict(details=f'Task {task.task_id} "{task.description}" successfully deleted'), 200)

@task_bp.route("/<task_identity>", methods=["PUT"])
def replace_task_by_id(task_identity):
task = validate_task(task_identity)
request_body = request.get_json()

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

db.session.commit()

return jsonify(task.to_dict()), 200

@task_bp.route("/<task_identity>/mark_complete", methods=["PATCH"])
def check_complete_by_id(task_identity):

task = validate_task(task_identity)
task.completed_at = datetime.now()

db.session.commit()

post_to_slack(task)
return jsonify(task.to_dict()), 200

@task_bp.route("/<task_identity>/mark_incomplete", methods=["PATCH"])
def check_incomplete_by_id(task_identity):

task = validate_task(task_identity)
task.completed_at = None

db.session.commit()

return jsonify(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