Skip to content

Philomena Kelly - Sharks #103

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 1 commit 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()'
2 changes: 1 addition & 1 deletion ada-project-docs/wave_01.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Tasks are entities that describe a task a user wants to complete. They contain a

- title to name the task
- description to hold details about the task
- an optional datetime that the task is completed on
- an optional datetime that the task is completed on

Our goal for this wave is to be able to create, read, update, and delete different tasks. We will create RESTful routes for this different operations.

Expand Down
7 changes: 7 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,11 @@ def create_app(test_config=None):

# 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
30 changes: 30 additions & 0 deletions app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
from app import db
from datetime import datetime


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_json(self, category=None):

goal_dict = {
"id": self.goal_id,
"title": self.title,
}

if category == "tasks":
goal_dict["tasks"] = [
task.to_json() for task in self.tasks
]

return goal_dict


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
41 changes: 41 additions & 0 deletions app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,46 @@
from app import db
from datetime import datetime


class Task(db.Model):
task_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)
is_complete = False
goal_id = db.Column(db.Integer, db.ForeignKey('goal.goal_id'))
goal = db.relationship("Goal", back_populates="tasks")

Choose a reason for hiding this comment

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

great job! If you are curious, check out backref attribute in SQLAlchemy to save you a line of code here!

https://docs.sqlalchemy.org/en/14/orm/backref.html



def to_json(self):
task_dict = {
"id": self.task_id,
"title": self.title,
"description": self.description,
"is_complete" : self.is_complete
}

if self.goal_id:
task_dict["goal_id"] = self.goal_id

return task_dict

def written_out(cls):
return "task"
Comment on lines +28 to +29

Choose a reason for hiding this comment

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

I think this would make for a good @staticmethod over a @classmethod

Suggested change
def written_out(cls):
return "task"
@staticmethod
def written_out():
return "task"

Choose a reason for hiding this comment

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

also is this used anywhere? I couldn't find any files where this method was called? If that's the case, let's get rid of it


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

if req_body.get("completed_at"):
self.is_complete = True
self.completed_at = datetime.now()


@classmethod
def create(cls,req_body):
new_task = cls(
title=req_body['title'],
description=req_body['description'],
)
return new_task
256 changes: 255 additions & 1 deletion app/routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,255 @@
from flask import Blueprint
from os import abort
from app import db
from app.models.task import Task
from flask import Blueprint, jsonify, abort, make_response, request
from sqlalchemy.sql import text
from datetime import datetime
import requests
import os
from dotenv import load_dotenv
from app.models.goal import Goal

tasks_bp = Blueprint("tasks_bp", __name__, url_prefix="/tasks")
goals_bp = Blueprint("goals_bp", __name__, url_prefix="/goals")
Comment on lines +12 to +13

Choose a reason for hiding this comment

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

often in industry we will separate out routes based on models into their own file to help balance the workload on files, as well as making sure teammates don't step on each other's toes when coding



@goals_bp.route("/<goal_id>/tasks", methods=["POST"])

Choose a reason for hiding this comment

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

👍

def create_task(goal_id):

goal = validate_object("goal", goal_id)

request_body = request.get_json()

for task in request_body["task_ids"]:
task = validate_object("task", task)
task.goal_id = goal_id
Comment on lines +23 to +25

Choose a reason for hiding this comment

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

this would make a good instance method


db.session.commit()

tasks = []

for task in goal.tasks:
tasks.append(task.task_id)
Comment on lines +29 to +32

Choose a reason for hiding this comment

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

same here, we can make this into an instance method and take it out of the route entirely


response = {
"id" : goal.goal_id,
"task_ids" : tasks
}

return make_response(jsonify(response), 200)



@goals_bp.route("/<goal_id>/tasks", methods=["GET"])

Choose a reason for hiding this comment

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

👍

def get_tasks_by_goals(goal_id):

goal = validate_object("goal", goal_id)

response = goal.to_json(category="tasks")

return make_response(jsonify(response), 200)





@goals_bp.route("", methods=["POST"])
def create_one_goal():

request_body = request.get_json()

try:
new_goal = Goal.create(request_body)
except KeyError:
return make_response(jsonify({"details" : "Invalid data"}), 400)

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

response = { "goal": new_goal.to_json()}

return make_response(response, 201)

Choose a reason for hiding this comment

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

You are staying super consistent with your return statements with make_response(jsonify(), so let's bring in jsonify here as well




@goals_bp.route("", methods=["GET"])
def read_all_goals():
title_query = request.args.get("sort")

if title_query:
goals = Goal.query.order_by(text(f'title {title_query}'))

Choose a reason for hiding this comment

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

out of curiosity, what does the text function do here?

else:
goals = Goal.query.all()

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

@goals_bp.route("/<goal_id>", methods=["GET"])

Choose a reason for hiding this comment

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

👍

def get_one_goal(goal_id):
goal = validate_object("goal", goal_id)

response = { "goal": goal.to_json()}

return make_response(response, 200)

Choose a reason for hiding this comment

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

Suggested change
return make_response(response, 200)
return make_response(jsonify(response), 200)



@goals_bp.route("/<goal_id>", methods=["PUT"])

Choose a reason for hiding this comment

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

👍

def update_one_goal(goal_id):

goal = validate_object("goal", goal_id)

request_body = request.get_json()

goal.update(request_body)

db.session.commit()

response = { "goal": goal.to_json()}
return make_response(response, 200)


@goals_bp.route("/<goal_id>", methods=["DELETE"])

Choose a reason for hiding this comment

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

👍

def delete_one_goal(goal_id):

goal = validate_object("goal", goal_id)

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

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


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

request_body = request.get_json()
try:
new_task = Task.create(request_body)
except KeyError:
return make_response(jsonify({"details" : "Invalid data"}), 400)

if request_body.get("completed_at"):
new_task.is_complete = True
new_task.completed_at = datetime.now()
Comment on lines +135 to +137

Choose a reason for hiding this comment

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

here's a good place to turn this into an class method when creating a new task


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

response = { "task": new_task.to_json()}

return make_response(response, 201)

Choose a reason for hiding this comment

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

Suggested change
return make_response(response, 201)
return make_response(jsonify(response), 201)




@tasks_bp.route("", methods=["GET"])

Choose a reason for hiding this comment

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

👍

def read_all_tasks():
title_query = request.args.get("sort")

if title_query:
tasks = Task.query.order_by(text(f'title {title_query}'))
else:
tasks = Task.query.all()

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


@tasks_bp.route("/<task_id>", methods=["GET"])

Choose a reason for hiding this comment

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

👍

def get_one_task(task_id):
task = validate_object("task", task_id)

response = { "task": task.to_json()}

return make_response(response, 200)


@tasks_bp.route("/<task_id>", methods=["PUT"])

Choose a reason for hiding this comment

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

👍

def update_one_task(task_id):

task = validate_object("task", task_id)

request_body = request.get_json()

task.update(request_body)

db.session.commit()

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



@tasks_bp.route("/<task_id>", methods=["DELETE"])

Choose a reason for hiding this comment

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

👍

def delete_one_task(task_id):

task = validate_object("task", task_id)

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

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


@tasks_bp.route("<task_id>/mark_complete", methods=["PATCH"])

Choose a reason for hiding this comment

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

👍

def mark_task_completed(task_id):
task = validate_object("task", task_id)

task.is_complete = True
task.completed_at = datetime.now()

db.session.commit()

URL = "https://slack.com/api/chat.postMessage"
PARAMS = {'text' : f"Someone just completed the task {task.title}",
'channel' : 'task-notifications'}
HEADERS = {'Authorization' : f'Bearer {os.environ.get("TASKLIST_BOT_KEY")}'}
r = requests.get(url = URL, params = PARAMS, headers = HEADERS)
Comment on lines +210 to +214

Choose a reason for hiding this comment

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

this would make a good helper function


response = { "task": task.to_json()}

return response, 200


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

task.is_complete = False
task.completed_at = None
Comment on lines +225 to +226

Choose a reason for hiding this comment

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

we could make this into an instance method


db.session.commit()

response = { "task": task.to_json()}

return response, 200

Choose a reason for hiding this comment

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

keep consistency with your return statements here, so that users can use your API in a predictable fashion

Suggested change
return response, 200
return make_response(jsonify(response), 200)





Comment on lines +233 to +236

Choose a reason for hiding this comment

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

Suggested change


def validate_object(object_type, object_id):

try:
object_id = int(object_id)
except:
abort(make_response({"message":f"{object_type} {object_id} invalid"}, 400))

if object_type == "task":
item = Task.query.get(object_id)
elif object_type == "goal":
item = Goal.query.get(object_id)

if not item:
abort(make_response({"message":f"{object_type} {object_id} not found"}, 404))

return item
Comment on lines +238 to +253

Choose a reason for hiding this comment

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

Let's move this into its own file for helper functions



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