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

Project complete and deployed #105

Open
wants to merge 9 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()'
8 changes: 8 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,15 @@ def create_app(test_config=None):

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


# Register Blueprints here

from .routes import tasks_bp
app.register_blueprint(tasks_bp)

from .goal_routes import goal_bp
app.register_blueprint(goal_bp)


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

Choose a reason for hiding this comment

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

Deleting comments like this can keep your code clean


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

Choose a reason for hiding this comment

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

To keep your project organized, you can create a routes directory under the app directory. In the routes directory, you can add goal_routes.py and you can rename routes.py to task_routes.py



def validate_goal(id_):

Choose a reason for hiding this comment

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

Curious about id_, is the underscore intentional here after id?

try:
id_ = int(id_)
except:
abort(make_response({"details": "Invalid data"}, 400))



goal = Goal.query.get(id_)
# gets the whole row in db with that particular id

if not goal:
abort(make_response({"message": f"Goal {id_} not found"}, 404))

return goal

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

request_body = request.get_json()

try:
new_goal = Goal.create(request_body)

except KeyError:
return abort(make_response({"details": "Invalid data"}, 400))
Comment on lines +33 to +37

Choose a reason for hiding this comment

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

Nice error handling here! We can't assume that clients will send valid requests to our API so we have to anticipate a scenario where the req doesn't have the keys your method will need to access.



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

return make_response(jsonify(new_goal.to_dict()), 201)



@goal_bp.route("", methods=["GET"])
def get_goals():
goals = Goal.query.all()
goals_response = []
for goal in goals:
goals_response.append(
{
"id": goal.id,
"title": goal.title
}
)
Comment on lines +51 to +57

Choose a reason for hiding this comment

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

If you use a to_json() method on each goal, then you could use list comprehension here to keep your route method concise. Something like:

goals_response = [goal.to_json() for goal in goals]


return jsonify(goals_response), 200

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

tasks_response = []
for task in goal.tasks:
tasks_response.append(Task.to_json(task))

Choose a reason for hiding this comment

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

Quick thought here about Task.to_json(task).

to_json() is an instance method of the Task class (since it's not decorated with @classmethod so instead of calling to_json() on the Task class on line 67, you can write this as:

tasks_response.append(task.to_json())

# goal["tasks"] = tasks_response

return jsonify({"id": goal.id, "title": goal.title, "tasks": tasks_response}), 200

Choose a reason for hiding this comment

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

Consider using your Goal instance method to_dict() which already sets id and title.

You could add conditional logic in to_dict() to set tasks too. Something like:

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

# return jsonify(goal), 200

@goal_bp.route("/<id>", methods=["GET"])
def get_one_goal(id):
goal = validate_goal(id)

return jsonify(goal.to_dict()), 200


@goal_bp.route("/<id>", methods=["PUT"])
def update_goal(id):
goal = validate_goal(id)

request_body = request.get_json()

goal.update(request_body)

Choose a reason for hiding this comment

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

You can add error handling here like you did in your POST method above to handle a scenario where the request_body might not have "title"


db.session.commit()

return jsonify(goal.to_dict()), 200


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

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

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

@goal_bp.route("/<goal_id>/tasks", methods=["POST"])
def create_task_with_goal(goal_id):
goal = validate_goal(goal_id)
request_body = request.get_json()
# new_task = Task.create(request_body)
task_ids = request_body["task_ids"]

Choose a reason for hiding this comment

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

You can add error handling here to ensure that request_body has "task_ids" and implement some logic to return an error if task_ids aren't there.


for task_id in task_ids:
task = Task.query.get(task_id)

Choose a reason for hiding this comment

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

You could use your validate_task() method here

task.goal_id = goal_id

db.session.commit()

goal_response = {
"id": goal.id,
"task_ids": task_ids
}

# return make_response(jsonify({"id": goal.id, "task_ids": task_ids}), 200)
return make_response(jsonify(goal_response), 200)
Comment on lines +115 to +121

Choose a reason for hiding this comment

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

You can use your to_dict function in the Goal class directly in line 121, this would enable you to delete lines 115-118.

Then line 121 would look like:

return make_response(jsonify(goal.to_json()), 200)



25 changes: 24 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
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, autoincrement=True)
title = db.Column(db.String, nullable=False)

Choose a reason for hiding this comment

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

Nice work using nullable=False

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


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


def update(self, req_body):
self.title = req_body["title"]


@classmethod
def create(cls, req_body):
new_goal = cls(
title=req_body["title"]
)
return new_goal
46 changes: 46 additions & 0 deletions app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,51 @@

from app import db
# import datetime


class Task(db.Model):
task_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.

Consider adding nullable=False here so your tasks have titles

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)
goal = db.relationship("Goal", back_populates="tasks")

def to_json(self):

Choose a reason for hiding this comment

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

👍

# change name to_dict(self):
is_complete = False
# if (self.completed_at is not None):
if (self.completed_at is not None):
is_complete = True
task = {
# "task_id": self.task_id,
"id": self.task_id,
"title": self.title,
"description": self.description,
"is_complete": is_complete
}
if self.goal:
# task["goal"] = self.goal.title
task["goal_id"] = self.goal.id
return task

def update(self, req_body):
self.title = req_body["title"]
self.description = req_body["description"]
# self.completed_at = req_body["is_complete"]
# check to see if this needs to be here or not

@classmethod
def create(cls, req_body):
if "completed_at" in req_body:
new_task = cls(
title=req_body["title"],
description=req_body["description"],
completed_at=req_body["completed_at"]
)
else:
new_task = cls(
title=req_body["title"],
description=req_body["description"]
)
return new_task
147 changes: 146 additions & 1 deletion app/routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,146 @@
from flask import Blueprint
from flask import Blueprint, make_response, request, jsonify, abort
from app.models.task import Task
from app import db
import requests
from datetime import datetime
import os
from dotenv import load_dotenv
load_dotenv()

# datetime.datetime.utcnow()

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


def validate_task(id_):

Choose a reason for hiding this comment

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

Same q here about id_. Maybe this is a convention I'm not aware of! Feel free to respond to this comment to let me know 😄

try:
id_ = int(id_)
except:
abort(make_response({"details": "Invalid data"}, 400))



task = Task.query.get(id_)
# gets the whole row in db with that particular id

if not task:
abort(make_response({"message": f"Task {id_} not found"}, 404))

return task


@tasks_bp.route("", methods=["POST"])
def create_task():

request_body = request.get_json()
print(request_body)
try:
new_task = Task.create(request_body)
# new_task = Task(title=request_body["title"],
# description=request_body["description"])
except KeyError:
return abort(make_response({"details": "Invalid data"}, 400))


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

# return make_response(jsonify(f"Task {new_task.title} successfully created"), 201)
return make_response(jsonify({"task": new_task.to_json()}), 201)


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

# request_body = request.get_json()
# task.completed_at = request_body["completed_at"]
task.completed_at = datetime.utcnow()

# request_body = request.get_json()
# task.completed_at = response_body["completed_at"]
# task.update(response_body)
Comment on lines +56 to +62

Choose a reason for hiding this comment

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

Remember to remove comments to keep your code clean. It also helps us to avoid the sitch where something gets accidentally uncommented and the code executes when it shouldn't.


db.session.commit()

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

query_params = {
"channel": "test-channel",
"text": f"Someone just completed the task {task.title}"
}

headers = {"Authorization": os.environ.get("SLACK_BOT_KEY")}

response_bot = requests.post(SLACK_BOT_POST_PATH, params=query_params, headers=headers)
Comment on lines +66 to +75

Choose a reason for hiding this comment

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

Consider putting this into a helper method and then calling it here to keep the route concise. Using a helper method would also apply the SRP (single responsibility principle) to the route function.


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

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

task.completed_at = None

# request_body = request.get_json()
# task.completed_at = response_body["completed_at"]
# task.update(response_body)

db.session.commit()

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


@tasks_bp.route("", methods=["GET"])
def get_tasks():
title_param = request.args.get("title")

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

if title_param:
tasks = Task.query.filter_by(title=title_param)
else:
if sort_param == "desc":
tasks = Task.query.order_by(Task.title.desc()).all()
else:
# tasks = Task.query.all()
tasks = Task.query.order_by(Task.title.asc()).all()
Comment on lines +103 to +107

Choose a reason for hiding this comment

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

with order_by() you don't need to use chain .all() to the end of this statement.



task_response_body = []
for task in tasks:
task_response_body.append(task.to_json())
Comment on lines +111 to +112

Choose a reason for hiding this comment

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

You can use list comprehension here if you'd like.


# [task.tojson() for task in tasks]

return jsonify(task_response_body), 200



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

return jsonify({"task": task.to_json()}), 200

@tasks_bp.route("/<task_id>", methods=["PUT"])
def update_planet(task_id):
task = validate_task(task_id)

request_body = request.get_json()

task.update(request_body)

Choose a reason for hiding this comment

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

Consider adding some error handling here if request_body doesn't have "title" or "description"


db.session.commit()

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

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

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

return make_response(jsonify({"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.
Loading