Skip to content
Open
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()'
11 changes: 11 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ def create_app(test_config=None):
app = Flask(__name__)
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

# import models here




if test_config is None:
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
"SQLALCHEMY_DATABASE_URI")
Expand All @@ -30,5 +35,11 @@ def create_app(test_config=None):
migrate.init_app(app, db)

# Register Blueprints here

from .routes import tasks_bp
app.register_blueprint(tasks_bp)

from .routes import goals_bp
app.register_blueprint(goals_bp)

return app
34 changes: 33 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
from flask import current_app
from sqlalchemy.orm import backref
from app import db


class Goal(db.Model):
goal_id = db.Column(db.Integer, primary_key=True)
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String)
# genres = db.relationship("Genre", secondary="books_genres", backref="books")
tasks = db.relationship("Task", backref="goal", lazy=True)

def to_dict(self):
if self.list_of_task_ids():

Choose a reason for hiding this comment

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

Storing this result of function call here in a variable (ie task_ids = self.list_of_task_ids()) and then testing could create a slight optimization, because then the variable could be used on line 19 ("task_ids": task_ids) instead of calling the function again.


return{

"id": self.id,
"title": self.title,
"tasks_ids": self.list_of_task_ids()
}
else:
return{
"id": self.id,
"title": self.title
}

def list_of_task_ids(self):
task_ids = [task.id for task in self.tasks]
return task_ids

def task_list(self):
list = []
for task in self.tasks:
list.append(task.to_dict)
Comment on lines +32 to +34

Choose a reason for hiding this comment

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

I think there's a small bug here:

Suggested change
list = []
for task in self.tasks:
list.append(task.to_dict)
list = []
for task in self.tasks:
list.append(task.to_dict())

This loop is also a great candidate for a list comprehension similar to line 28:

Suggested change
list = []
for task in self.tasks:
list.append(task.to_dict)
list = [task.to_dict() for task in self.tasks]


return list


36 changes: 35 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,40 @@
from flask import current_app
from app import db

# task_id: a primary key for each task
# title: text to name the task
# description: text to describe the task
# completed_at: a datetime that has the date that a task is completed on. Can be nullable, and contain a null value. A task with a null value for completed_at has not been completed.

class Task(db.Model):
task_id = db.Column(db.Integer, primary_key=True)
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String)
description = db.Column(db.String)
completed_at = db.Column(db.DateTime,nullable=True)
goal_id = db.Column(db.Integer, db.ForeignKey('goal.id'), nullable=True)

def to_dict(self):
if self.goal_id is None:
return {
"id": self.id,
"title": self.title,
"description": self.description,
"is_complete": self.check_for_complete_task(),
}
else:

return{

"id": self.id,
"title": self.title,
"description": self.description,
"is_complete": self.check_for_complete_task(),
"goal_id": self.goal_id

Choose a reason for hiding this comment

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

👍


}

def check_for_complete_task(self):

Choose a reason for hiding this comment

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

👍

if self.completed_at:
return True
return False

272 changes: 271 additions & 1 deletion app/routes.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,272 @@
from flask import Blueprint
from flask.wrappers import Response
from app.models.task import Task
from app import db
from flask import Blueprint, jsonify, make_response, request, abort
from datetime import date
import os
import requests
from dotenv import load_dotenv
from app.models.goal import Goal

# handle_tasks handles GET and POST requests for the /tasks endpoint


tasks_bp = Blueprint("tasks_bp", __name__, url_prefix="/tasks")
def valid_int(number,parameter_type):

Choose a reason for hiding this comment

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

Great helper!

try:
int(number)
except:
abort(make_response({"error":f"{parameter_type} must be an int"},400))

@tasks_bp.route("", methods=["GET", "POST"])
def handle_tasks():
# Wave 1: Get Tasks: Getting Saved Tasks
if request.method == "GET":
sort = request.args.get("sort")
if sort == "asc":
tasks = Task.query.order_by(Task.title)
elif sort == "desc":
tasks = Task.query.order_by(Task.title.desc())
else:
tasks = Task.query.all()
#Wave 1: Get Tasks: No Saved Tasks
tasks_response = []
for task in tasks:
has_complete = task.completed_at
tasks_response.append(
{
"description": task.description,
"id": task.id,
"is_complete": False if has_complete == None else has_complete,
"title": task.title,
}
)
Comment on lines +36 to +43

Choose a reason for hiding this comment

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

This works, I recommend using the to_dict helper function to DRY this section:

Suggested change
tasks_response.append(
{
"description": task.description,
"id": task.id,
"is_complete": False if has_complete == None else has_complete,
"title": task.title,
}
)
tasks_response.append(task.to_dict())

return jsonify(tasks_response)
# Wave 1: Create a Task: Valid Task With null completed_at
elif request.method == "POST":
request_body = request.get_json()
#Wave 1: Create A Task: Missing Title
if "title" not in request_body or "description" not in request_body or "completed_at" not in request_body:
return jsonify ({
"details": "Invalid data"
}), 400


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()

#Wave 1: Create A Task: Valid Task with null completed_at 201 CREATED


return jsonify({"task":new_task.to_dict()}),201
# handle_one_task handles GET,PUT and DELETE requests for the tasks/task_id endpoint
@tasks_bp.route("/<task_id>", methods=["GET", "PUT", "DELETE"])
def handle_one_task(task_id):
valid_int(task_id,"task_id")
task = Task.query.get_or_404(task_id)

Choose a reason for hiding this comment

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

Great use of get_or_404!

# Wave 1: Get One Task: One Saved Task
if request.method == "GET":
has_complete = task.completed_at
if task.goal_id:
task_response={
"task": {
"id": task.id,
"goal_id": task.goal_id,
"title": task.title,
"description": task.description,
"is_complete": False if has_complete == None else has_complete,

}
}
else:
task_response={
"task": {
"id": task.id,
"title": task.title,
"description": task.description,
"is_complete": False if has_complete == None else has_complete,

}
}
Comment on lines +75 to +95

Choose a reason for hiding this comment

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

Task to_dict handles the existence of goal_id.

Suggested change
if task.goal_id:
task_response={
"task": {
"id": task.id,
"goal_id": task.goal_id,
"title": task.title,
"description": task.description,
"is_complete": False if has_complete == None else has_complete,
}
}
else:
task_response={
"task": {
"id": task.id,
"title": task.title,
"description": task.description,
"is_complete": False if has_complete == None else has_complete,
}
}
task_response = {"task":task.to_dict()}


return jsonify(task_response)
#Wave 1: Update Task, #Wave 1 Update Task: No Matching Task, Update Task 200 OK
elif request.method == "PUT":
form_data = request.get_json()

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


db.session.commit()
return jsonify({"task":task.to_dict()}),200

#Wave 1 Delete Task: Deleting A Task, #Wave 1: Delete Task: No Matching Task
elif request.method == "DELETE":
db.session.delete(task)
db.session.commit()
response = {
"details": f'Task {task.id} "{task.title}" successfully deleted'
}
json_response = jsonify(response)
return make_response(json_response, 200)

def slack_bot(title):

Choose a reason for hiding this comment

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

Great helper!

query_path = {
"channel": "melinda-bot",
"text": f"Someone completed the task {title}"
}
header = {
"Authorization": f"Bearer {os.environ.get('BOT')}"
}
response = requests.post("https://slack.com/api/chat.postMessage",params = query_path, headers = header)
return response.json()


#Wave 3
@tasks_bp.route("/<task_id>/mark_complete", methods=["PATCH"])
def handle_completed_task(task_id):

Choose a reason for hiding this comment

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

Great work, the use of helper functions and get_or_404 in this function really streamline the code!

valid_int(task_id,"task_id")
task = Task.query.get_or_404(task_id)
task.completed_at = date.today()
db.session.commit()
slack_bot(task.title)
return jsonify ({"task":task.to_dict()}),200


@tasks_bp.route("/<task_id>/mark_incomplete", methods=["PATCH"])
def handle_incompleted_task(task_id):
valid_int(task_id,"task_id")
task = Task.query.get_or_404(task_id)
task.completed_at = None
db.session.commit()
return jsonify ({"task":task.to_dict()}),200


# Wave 5 Creating a Goal Model Blueprint
goals_bp = Blueprint("goals_bp", __name__, url_prefix="/goals")

# Wave 5 Create A Goal: Valid Goal
@goals_bp.route("", methods=["POST"])
def handle_post_goals():

request_body = request.get_json()

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

new_goal = Goal(
title = request_body["title"]
)
db.session.add(new_goal)
db.session.commit()


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

# Wave 5 Get Goals: Getting Saved Goals
@goals_bp.route("", methods=["GET"])
def handle_goals():
goals = Goal.query.all()
goals_response = []
for goal in goals:
goals_response.append(goal.to_dict())
return jsonify(goals_response), 200


# Wave 5 Update Goal: Update Goal/No Matching Goal
@goals_bp.route("/<goal_id>", methods=["PUT", "GET"])
def handle_update_one_goal(goal_id):
goal = Goal.query.get_or_404(goal_id)
if request.method == "GET":
return jsonify({"goal":goal.to_dict()}),200
elif request.method == "PUT":
form_data = request.get_json()
if "title" not in form_data:
return jsonify( {
"details": "title required"
}), 400

goal.title = form_data["title"]

db.session.commit()
return jsonify({"goal":{"goal_id":goal.id, "title":goal.title}}),200

Choose a reason for hiding this comment

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

Suggested change
return jsonify({"goal":{"goal_id":goal.id, "title":goal.title}}),200
return jsonify({"goal":goal.to_dict()}),200


# Wave 5 Deleting A Goal: Deleting A Goal/No Matching Goal
@goals_bp.route("/<goal_id>", methods=["DELETE"])
def handle_delete_one_goal(goal_id):
goal = Goal.query.get_or_404(goal_id)
db.session.delete(goal)
db.session.commit()
return jsonify( {
"details": f"Goal {goal_id} \"{goal.title}\" successfully deleted"
}),200
# json_response = jsonify(response)
# return make_response(json_response), 200

#Wave # 6
@goals_bp.route("/<goal_id>/tasks", methods=["POST"])
def post_task_ids_to_goal(goal_id):

Choose a reason for hiding this comment

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

👍

valid_int(goal_id,"goal_id")
request_body = request.get_json()
goal = Goal.query.get_or_404(goal_id)
task_ids = request_body["task_ids"]
for task_id in task_ids:
task = Task.query.get(task_id)
goal.tasks.append(task)
db.session.commit()
return jsonify({"id":goal.id, "task_ids": [task.id for task in goal.tasks]}),200

@goals_bp.route("/<goal_id>/tasks", methods=["GET"])
def get_tasks_for_goal(goal_id):
valid_int(goal_id,"goal_id")
goal = Goal.query.get_or_404(goal_id)

tasks = goal.tasks
tasks_list = []
for task in tasks:
tasks_list.append(task.to_dict())
response_body = {"id":goal.id,
"title":goal.title,
"tasks": tasks_list
}


return 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.
Loading