Skip to content

Panthers - Kallie and Billie resubmission #140

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

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
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
@@ -1,7 +1,7 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
import os
import os
from dotenv import load_dotenv


Expand Down Expand Up @@ -30,5 +30,10 @@ def create_app(test_config=None):
migrate.init_app(app, db)

# Register Blueprints here
from .tasks_routes import tasks_bp
app.register_blueprint(tasks_bp)

from .goals_routes import goals_bp
app.register_blueprint(goals_bp)

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

goals_bp = Blueprint('goals_bp', __name__, url_prefix='/goals')
GOAL_ID_PREFIX = '/<goal_id>'

def validate_goal(goal_id):
try:
goal_id = int(goal_id)
except:
abort(make_response({"message":f"Goal {goal_id} invalid"}, 400))

goal = Goal.query.get(goal_id)

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

return goal
Comment on lines +9 to +20

Choose a reason for hiding this comment

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

👍 Great helper function!


@goals_bp.route("", methods=["POST"])
def create_goal():
request_body = request.get_json()

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

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

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

return make_response({"goal":new_goal.to_dict()}, 201)
Comment on lines +22 to +34
Copy link

@audreyandoy audreyandoy Dec 7, 2022

Choose a reason for hiding this comment

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

👍 Great work using to_dict() here! Because we're returning a dictionary, we can take advantage of Flask's default behavior of turning dictionaries into JSON format as according to their documentation:

If you return a dict from a view, it will be converted to a JSON response.

https://flask.palletsprojects.com/en/1.1.x/quickstart/#apis-with-json

We can apply this same comment to the rest of this project.


@goals_bp.route("", methods=["GET"])
def get_goals():
goal_query = Goal.query
goals = goal_query.all()

goals_response = []
for goal in goals:
goals_response.append({
"id": goal.goal_id,
"title": goal.title,
})

return make_response(jsonify(goals_response), 200)
Comment on lines +36 to +48

Choose a reason for hiding this comment

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

👍 Nice work! We can use list comprehension to build goal_response and use to_dict like so:

goals_response = [goal.to_dict() for goal in goals]
return jsonify(goals_response), 200

@goals_bp.route(GOAL_ID_PREFIX, methods=["GET"])
def get_one_goal(goal_id):
goal = validate_goal(goal_id)
goal = Goal.query.get(goal_id)

return {"goal": goal.to_dict()}
Comment on lines +49 to +54

Choose a reason for hiding this comment

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

👍


@goals_bp.route(GOAL_ID_PREFIX,methods=['PUT'])
def update_task(goal_id):
request_body = request.get_json()
if "title" not in request_body:
return make_response("Invalid Request, Goal Must Have Title", 400)

goal = validate_goal(goal_id)
goal = Goal.query.get(goal_id)

goal.title = request_body["title"]

db.session.commit()
return make_response({"goal":goal.to_dict()}, 200)
Comment on lines +56 to +68

Choose a reason for hiding this comment

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

👍


@goals_bp.route(GOAL_ID_PREFIX, methods=['DELETE'])
def delete_goal(goal_id):
goal = validate_goal(goal_id)
goal = Goal.query.get(goal_id)

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

return make_response({"details": f'Goal {goal_id} "{goal.title}" successfully deleted'}, 200)
Comment on lines +70 to +78

Choose a reason for hiding this comment

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

👍



@goals_bp.route(GOAL_ID_PREFIX + "/tasks", methods=['GET'])
def read_tasks (goal_id):
goal = validate_goal(goal_id)
goal = Goal.query.get(goal_id)

return_body = goal.to_dict()
return_body["tasks"] = [task.to_dict_in_goal() for task in goal.tasks]

return make_response(jsonify(return_body), 200)
Comment on lines +81 to +89

Choose a reason for hiding this comment

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

Nice work building the return_body dictionary!



@goals_bp.route(GOAL_ID_PREFIX + "/tasks", methods=['POST'])
def add_tasks_to_goal(goal_id):
goal = validate_goal(goal_id)
goal = Goal.query.get(goal_id)

request_body = request.get_json()
tasks_to_assign = request_body["task_ids"]

for task_id in tasks_to_assign:
task = Task.query.get(task_id)
task.goal_id = goal.goal_id

db.session.commit()

return_body = {}
return_body["id"] = goal.goal_id
return_body["task_ids"] = tasks_to_assign

return make_response(return_body, 200)
Comment on lines +92 to +110

Choose a reason for hiding this comment

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

Great work utilizing the relationship attribute goal_id for task id's!

Choose a reason for hiding this comment

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

We can DRY up our code by building the code directly:

return_body = {
    "id": goal.goal_id,
    "task_id": tasks_to_assign
}

return return_body, 200

9 changes: 8 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
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", back_populates="goal")

def to_dict(self):
return {
"id": self.goal_id,
"title": self.title
}
Comment on lines +5 to +12
Copy link

@audreyandoy audreyandoy Dec 7, 2022

Choose a reason for hiding this comment

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

👍 Good work setting up the Goal model.

It wasn't mentioned in the README but whenever we're designing models/tables we should also consider whether each column should allow nullable values. In this case, is storing a goal with an empty title useful to have in our database? Or should we require every goal to have a title? If the latter, we can add nullable=False to the arguments of db.Column. Otherwise, when that argument is omitted SQLAlchemy will set the column to nullable=True by default.

Also nice helper function!

25 changes: 24 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,27 @@


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)
completed_at = db.Column(db.DateTime, nullable=True)
is_completed = db.Column(db.Boolean, default=False)
goal_id = db.Column(db.Integer, db.ForeignKey('goal.goal_id'), nullable=True)
goal = db.relationship("Goal", back_populates="tasks")
Comment on lines +5 to +11

Choose a reason for hiding this comment

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

👍 Good work setting up the Task model. The same comments about using nullable=False in the Goal class can also apply here.


def to_dict(self):
return {
"id": self.task_id,
"title": self.title,
"description": self.description,
"is_complete": self.is_completed
}

def to_dict_in_goal(self):
return {
"id": self.task_id,
"goal_id": self.goal_id,
"title": self.title,
"description": self.description,
"is_complete": self.is_completed
}
Comment on lines +13 to +28

Choose a reason for hiding this comment

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

Nice helper functions! We can condense these two functions into one by building an initial dictionary and then checking if a goal_id exists for that particular task.

  def to_dict(self):
      task = {
            "id": self.task_id,
            "title": self.title,
            "description": self.description,
            "is_complete": self.is_completed
        }

      if self.goal_id:
             task['goal_id'] = self.goal_id

      return task

1 change: 0 additions & 1 deletion app/routes.py

This file was deleted.

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

tasks_bp = Blueprint('tasks_bp', __name__, url_prefix='/tasks')
TASK_ID_PREFIX = '/<task_id>'

def validate_task(task_id):
try:
task_id = int(task_id)
except:
abort(make_response({"message":f"Task {task_id} invalid"}, 400))

task = Task.query.get(task_id)

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

return task
Comment on lines +13 to +24

Choose a reason for hiding this comment

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

Nice helper function! Notice how goal_routes uses the exact same helper function? We can move this function into a new file and provide a cls parameter to make this function flexible enough to use on both task and goal.

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

    obj = cls.query.get(id)

    if not obj:
        abort(make_response({"message":f"{cls.__name__}{id} not found"}, 404))
    
    return obj


@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({"details": "Invalid data"}, 400)

new_task = Task(
title = request_body["title"],
description = request_body["description"],
)

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

return make_response({"task":new_task.to_dict()}, 201)
Comment on lines +26 to +41

Choose a reason for hiding this comment

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

👍


@tasks_bp.route(TASK_ID_PREFIX,methods=['PUT'])
def update_task(task_id):
request_body = request.get_json()
if "title" not in request_body or "description" not in request_body:
return make_response("Invalid Request, Title & Description Can't Be Empty", 400)

task = validate_task(task_id)
task = Task.query.get(task_id)

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

db.session.commit()
return make_response({"task":task.to_dict()}, 200)
Comment on lines +43 to +56

Choose a reason for hiding this comment

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

Nice use of validate_task. See earlier comment about making that helper more flexible.

I've mentioned it before, but we don't need to use make_response on dictionaries as Flask automatically turns dictionaries into JSON format.



@tasks_bp.route(TASK_ID_PREFIX, methods=["GET"])
# GET /task/id
def handle_task(task_id):
# Query our db to grab the task that has the id we want:
task = validate_task(task_id)
task = Task.query.get(task_id)

if task.goal_id is not None:
return{"task": task.to_dict_in_goal()}
else:
return {"task": task.to_dict()}
Comment on lines +59 to +69

Choose a reason for hiding this comment

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

👍 See comment in tasks class that combines to_dict_in_goal and to_dict.

with that refactor we can reduce these lines to:

return {"task": task.to_dict()}



@tasks_bp.route('',methods=['GET'])
def get_task():
task_query = Task.query

sort_query = request.args.get("sort")
if sort_query:
task_response = []
tasks = task_query.all()
for task in tasks:
task_response.append(task.to_dict())
Comment on lines +79 to +81

Choose a reason for hiding this comment

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

We can use list comprehension here

task_response = [ task.to_dict() for task in tasks]


task_titles = []

for task in task_response:
for key, value in task.items():
if key == "title":
task_titles.append(value)

response_body = []
if sort_query == "asc":
sorted_tasks = sorted(task_titles)
if sort_query == "desc":
sorted_tasks = sorted(task_titles, reverse=True)
while len(response_body) < len(task_titles):
for task in task_response:
if len(sorted_tasks) == 0:
break
if task["title"] == sorted_tasks[0]:
response_body.append(task)
sorted_tasks.pop(0)
return make_response(jsonify(response_body), 200)
Comment on lines +77 to +102

Choose a reason for hiding this comment

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

The goal here was to use SQLAlchemy's sort commands to do the sorting for us rather than using python's built-in sorted function. When we use a database tool, we should take advantages of it's own features such as returning a list of sorted objects in ascending or descending order.

We can do that by using :

tasks = task_query.order_by(Task.title.desc())
tasks = task_query.order_by(Task.title.asc())

Choose a reason for hiding this comment

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

Just that line should get rid of all extra the logic to sort


descripiton_query = request.args.get("description")
if descripiton_query:
task_query = task_query.filter_by(description = descripiton_query)

title_query = request.args.get("title")
if title_query:
task_query = task_query.filter_by(name = title_query)

is_complete_query = request.args.get("is_completed")
if is_complete_query:
task_query = task_query.filter_by(is_completed= is_complete_query)
Comment on lines +104 to +114

Choose a reason for hiding this comment

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

Nice work adding extra queries!


tasks = task_query.all()

tasks_response = []
for task in tasks:
tasks_response.append({
"id": task.task_id,
"title": task.title,
"is_complete": task.is_completed,
"description": task.description
})
Comment on lines +118 to +125

Choose a reason for hiding this comment

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

We can use the to_dict helper method here to build a list comprehension:

tasks_response = [tasks.to_dict() for task in tasks]


return jsonify(tasks_response)

@tasks_bp.route(TASK_ID_PREFIX,methods=['DELETE'])
def delete_task(task_id):
task = validate_task(task_id)
task = Task.query.get(task_id)

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

return make_response({"details": f'Task {task_id} "{task.title}" successfully deleted'}, 200)
Comment on lines +129 to +137

Choose a reason for hiding this comment

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

👍


@tasks_bp.route(TASK_ID_PREFIX + '/mark_complete', methods=['PATCH'])
def update_task_complete(task_id):
task = validate_task(task_id)
date_time_assign = datetime.now()

Choose a reason for hiding this comment

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

We didn't have to put this in a variable as datetime.now() is pretty easy syntax to undertand.

if task.is_completed == False:
task.completed_at = date_time_assign
task.is_completed = True
task_response = {"task": task.to_dict()}
db.session.commit()
load_dotenv()

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

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

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


requests.post(URL, data=payload, headers=headers)
Comment on lines +150 to +160

Choose a reason for hiding this comment

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

Nice work setting up the slackbot. We can move this logic into a helper function just to follow the single responsibility principle. Moving this logic into a helper function also allows the opportunity for other routes to make a slack post!

return jsonify(task_response),200

@tasks_bp.route(TASK_ID_PREFIX + '/mark_incomplete', methods=['PATCH'])
def update_task_incomplete(task_id):
task = validate_task(task_id)
task = Task.query.get(task_id)

task.is_completed= False
task.completed_at = None

db.session.commit()
return make_response({"task":task.to_dict()}, 200)
Comment on lines +163 to +172

Choose a reason for hiding this comment

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

👍

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