Skip to content
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
14 changes: 12 additions & 2 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@ def create_app(test_config=None):
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

if test_config is None:
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
"SQLALCHEMY_DATABASE_URI")
# app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
#"SQLALCHEMY_DATABASE_URI")

app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get("RENDER_DATABASE_URI")



else:
app.config["TESTING"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
Expand All @@ -28,7 +33,12 @@ def create_app(test_config=None):

db.init_app(app)
migrate.init_app(app, db)
from app.models.task import Task
from app.models.goal import Goal

# Register Blueprints here
from .routes import task_list_bp,goals_bp

Choose a reason for hiding this comment

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

Don't be afraid to separate your routes into task_routes and goal_routes!

app.register_blueprint(task_list_bp)
app.register_blueprint(goals_bp)

return app

Choose a reason for hiding this comment

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

Remember to make an empty init.py file in any package folder/subfolder. app has one, but we should have one here in the models folder as well.

13 changes: 13 additions & 0 deletions app/models/goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,16 @@

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
}

@classmethod
def from_dict(cls,request_data):
return cls(
title=request_data["title"])

Choose a reason for hiding this comment

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

This goal model looks great!

30 changes: 28 additions & 2 deletions app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
from app import db

from flask import make_response

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)

Choose a reason for hiding this comment

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

The default for a nullable constraint is True, so you can absolutely leave that out here if you would like!

goal_id = db.Column(db.Integer,db.ForeignKey("goal.goal_id"))
goal = db.relationship("Goal", back_populates="tasks")

#returning a dictionary from the database
def task_dict(self):
task_dict = {
"id": self.task_id,
"title": self.title,
"description": self.description,
"is_complete": self.completed_at != None}

Choose a reason for hiding this comment

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

Great job using the inclusion of the .completed_at to define the truthiness of is_complete! Just a slight nitpick, make sure to move the brace at the end to the next line!


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


#take data from user to make new task
@classmethod
def from_dict(cls,request_data):
return cls(
title=request_data["title"],
description=request_data["description"])

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

task_list_bp = Blueprint("tasks", __name__, url_prefix ="/tasks")
goals_bp = Blueprint("goals", __name__, url_prefix = "/goals")

@task_list_bp.route("", methods = ["POST"])
def create_tasks():
request_body = request.get_json()
print(request_body)
print("************")

if (not "title" in request_body) or (not "description" in request_body):
return{
"details":"Invalid data"
}, 400
try:
new_task = Task.from_dict(request_body)
db.session.add(new_task)
db.session.commit()

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

except KeyError as e:
abort(make_response("Invalid request. Missing required value: {e}"), 400)

Choose a reason for hiding this comment

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

Nice error handling when processing a user-created request body! We should assume that a user's request body may contain errors!


@task_list_bp.route("/<id>", methods = ["GET"])
def get_one_saved_task(id):
task = validate_task(id)
return jsonify({"task":task.task_dict()}), 200

@task_list_bp.route("", methods = ["GET"])
def get_all_saved_tasks():
sort_query=request.args.get("sort")
tasks_query=Task.query

if sort_query =="asc":
tasks_query = Task.query.order_by(Task.title.asc())
elif sort_query =="desc":
tasks_query = Task.query.order_by(Task.title.desc())

tasks = tasks_query.all()

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

Choose a reason for hiding this comment

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

Great use of list comprehension here!


return (jsonify(tasks_response)),200



def validate_task(task_id):

Choose a reason for hiding this comment

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

Not too big of an issue since you are only ever using validate_task to validate your tasks, but it's never a bad idea to make it a generic function in case you have other models that need to be validated later! Also, since it is a helper function, best practice would either have it placed before your routes or in its own helper file!

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


@task_list_bp.route("/<id>", methods = ["PUT"])
def update_task(id):
task = validate_task(id)
request_body = request.get_json()
task.title = request_body["title"]
task.description = request_body["description"]

db.session.commit()
response_body = {"task":task.task_dict()}


return make_response(jsonify(response_body), 200)

Choose a reason for hiding this comment

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

We need to have error handling for a request that uses the request body to create a response (like you have in the POST route). If request_body doesn't have a key "title" then line 75 would throw an unhandled exception.


@task_list_bp.route("/<id>", methods=["DELETE"])
def delete_one_task(id):
task= validate_task(id)

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

message = {"details":f'Task {task.task_id} "{task.title}" successfully deleted'}
return make_response(jsonify(message), 200)


@task_list_bp.route("/<id>/mark_complete", methods=["PATCH"])
def mark_task_complete(id):
task=validate_task(id)
task.completed_at=datetime.now()
db.session.commit()

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



@task_list_bp.route("/<id>/mark_incomplete", methods=["PATCH"])
def mark_task_incomplete(id):
task=validate_task(id)
task.completed_at=None
db.session.commit()

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

Choose a reason for hiding this comment

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

This looks great! 😊


#wave4
def slack_notification():
url = "https://slack.com/api/chat.postMessage"

payload = {
"channel": "api-test-channel",
"text": "Task completed"
}
headers = {
'Authorization':os.environ.get("SLACK_API_TOKEN")}

response = requests.post(url, headers=headers, data=payload)

return response.text

#wave 5
@goals_bp.route("",methods=["POST","GET"])
def handle_goal():
if request.method == "POST":
request_body = request.get_json()
if "title" not in request_body:
return{
"details": "Invalid data"
},400

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

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

return {
"goal": {
"id":new_goal.goal_id,
"title":new_goal.title
}
}, 201

elif request.method == "GET":
sorting_goals= request.args.get('sort')
list = None
if sorting_goals== "desc":
list = Goal.query.order_by(Goal.title.desc()) # descending method
elif sorting_goals == "asc":
list = Goal.query.order_by(Goal.title.asc()) # ascending method
else:
list = Goal.query.all()
goals_response = []
for goal in list:
goals_response.append({
"id": goal.goal_id,
"title": goal.title,
})

return jsonify(goals_response), 200

@goals_bp.route("/<goal_id>", methods=["GET","PUT","DELETE"])
def handle_goal_get(goal_id):
goal = Goal.query.get(goal_id)
if goal == None:
return ("", 404)

if request.method == "GET":
return {
"goal": {
"id":goal.goal_id,
"title":goal.title,
}
}
if request.method == "PUT":
form_data = request.get_json()

goal.title = form_data["title"]

db.session.commit()

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

elif request.method == "DELETE":
db.session.delete(goal)
db.session.commit()

if not goal in Goal.query:
abort(make_response({"message": f"Goal {goal_id} not found"}, 404))
return jsonify({
"details": f'Goal {goal.goal_id} "{goal.title}" successfully deleted'
}),200


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

goal = Goal.query.get(goal_id)

if goal == None:
return (""), 404

if request.method == "POST":
request_body = request.get_json()

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

goal.tasks = tasks_instances

db.session.commit()

task_ids = []
for task in goal.tasks:
task_ids.append(task.task_id)

response_body = {
"id": goal.goal_id,
"task_ids": task_ids
}

return jsonify(response_body), 200

if request.method == "GET":
tasks_response =[]
for task in goal.tasks:
tasks_response.append({
"id": task.task_id,
"goal_id": task.goal_id,
"title": task.title,
"description": task.description,
"is_complete": bool(task.completed_at)
})

Choose a reason for hiding this comment

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

This looks nearly identical to what you have in the instance method to_dict in the Task class. We should use the to_dict method on each task from goal.tasks instead of repeating code. You already have the logic to handle adding goal_id to each task dict too in to_dict!

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

Choose a reason for hiding this comment

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

While it is possible to attach multiple methods to a single route like you've done here, it starts to get a bit cluttered. It will absolutely depend on how your team wants to handle things, but overall, it's a good idea to separate each method out for readability!


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