Skip to content

Nested routes #137

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 35 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5c102ca
set up .env file
theLP100 Nov 2, 2022
c696890
made a working endpoint for getting all tasks. registered blueprints
theLP100 Nov 2, 2022
86c1c27
adding routes file to commit
theLP100 Nov 2, 2022
77d641e
added a column is_complete to the model that defaults to false. get a…
theLP100 Nov 3, 2022
b98d68a
added get route to get one task with a given id
theLP100 Nov 3, 2022
688dd46
wrote a test for 404 not found for get request with id of 1 and empty db
theLP100 Nov 3, 2022
cce9342
tried to add a post method but it broke everything
theLP100 Nov 3, 2022
1c134e7
making changes to get post working
theLP100 Nov 3, 2022
4d0b632
post method is now working
theLP100 Nov 3, 2022
08adb8a
Merge branch 'master' into implementing-post
theLP100 Nov 3, 2022
b3a0ae6
implemented put successfully
theLP100 Nov 3, 2022
715c469
added assert statement to test_update_task_not_found and checked that…
theLP100 Nov 3, 2022
39da99c
added delete route successfully
theLP100 Nov 3, 2022
7766313
if title or description missing from post, no entry posted. Deleted …
theLP100 Nov 4, 2022
5635280
all tests of wave1 passed: CRUD endpoints set up
theLP100 Nov 4, 2022
a9fdc69
refactored completed_at to accept info expected from wave 1. all CRU…
theLP100 Nov 7, 2022
4d3b714
implemented sort query correctly
theLP100 Nov 8, 2022
a7ad15b
endpoint with mark complete marks a given task complete
theLP100 Nov 14, 2022
950f7c5
mark_incomplete endpoint marks a task incomplete
theLP100 Nov 14, 2022
6709dcb
tests checking for task not found 404 respond correctly.
theLP100 Nov 14, 2022
b415889
trying to get integrating with slacking working. no success yet, but…
theLP100 Nov 14, 2022
ab234b6
updated put and patch route so that the slack bot posts a message to …
theLP100 Nov 15, 2022
98cfdaa
beginning to add goal_bp. made a blueprint and migrate to the db
theLP100 Nov 15, 2022
2aa5222
post route successfully added for goal. exclamationpoint
theLP100 Nov 16, 2022
beada76
get all goals route is working
theLP100 Nov 16, 2022
a74795b
get one task working for one task
theLP100 Nov 16, 2022
efff68e
goal get one goal with no goal returns 404
theLP100 Nov 16, 2022
eef38e5
put routes in place for goal, working for goal not found and goal found
theLP100 Nov 16, 2022
4bfb38e
delete one task working
theLP100 Nov 16, 2022
c3deccd
all endpoints for Goals have been set up. exclamation point.
theLP100 Nov 16, 2022
200a922
setting up relationship between task and goal
theLP100 Nov 16, 2022
a7a5d8a
got nested route for adding a goalwith tasks working
theLP100 Nov 16, 2022
1651a08
the get route for goals is working
theLP100 Nov 16, 2022
20f5ecc
more get functionality working. updated task.make_dict to influde goa…
theLP100 Nov 16, 2022
7ad720c
nested routes totally working
theLP100 Nov 16, 2022
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
6 changes: 6 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ def create_app(test_config=None):
app = Flask(__name__)
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

#app.config["SLACK_API_KEY"] = os.environ.get("SLACK_API_KEY")

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

# Register Blueprints here
from app.routes.task import task_bp
app.register_blueprint(task_bp)
from app.routes.goal import goal_bp
app.register_blueprint(goal_bp)

return app
18 changes: 17 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,20 @@


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

def make_dict(self):
"""return a ditionary with all attributes of a goal"""
goal_dict = {
"id": self.id,
"title": self.title
}
return goal_dict

@classmethod
def from_dict(cls, data_dict):
new_object = cls(
title = data_dict["title"])
return new_object
43 changes: 42 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,46 @@
from app import db
import os


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, default = None)
is_complete = db.Column(db.Boolean, default = False)
goal_id = db.Column(db.Integer, db.ForeignKey('goal.id'), nullable = True)
goal = db.relationship("Goal", back_populates="tasks" )
#project instructions are recommending setting nullable to True.
#where do we do this?

def make_dict(self):
"""given a task, return a dictionary
with all the attibutes of that task."""
#if goal_id is empty, don't include it in dictionary.
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

@classmethod
def from_dict(cls, data_dict):
#make the following a helper function:
if "completed_at" in data_dict:
completed_at = data_dict["completed_at"]
if not completed_at:
is_complete = False
else:
is_complete = True
else: is_complete = False
#end helper function.

new_object = cls(
title = data_dict["title"],
description = data_dict["description"],
is_complete = is_complete)
return new_object
1 change: 0 additions & 1 deletion app/routes.py

This file was deleted.

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

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

@goal_bp.route("", methods = ["POST"])
def post_new_goal():
request_body = request.get_json()
if "title" not in request_body:
response_str = "Invalid data"
abort(make_response({"details":response_str}, 400))

new_goal = Goal.from_dict(request_body)
db.session.add(new_goal)
db.session.commit()
goal_dict = new_goal.make_dict()
response = {"goal": goal_dict}
return make_response(response, 201)

@goal_bp.route("", methods = ["GET"])
def get_all_goals():
goals = Goal.query.all()
response = []
for goal in goals:
goal_dict = goal.make_dict()
response.append(goal_dict)
return jsonify(response), 200

@goal_bp.route("/<goal_id>", methods = ["GET"])
def get_one_goal(goal_id):
goal = validate_goal(goal_id)
goal_dict = goal.make_dict()
return make_response({"goal": goal_dict}, 200)

@goal_bp.route("/<goal_id>", methods = ["PUT", "PATCH"])
def update_goal(goal_id):
goal = validate_goal(goal_id)
request_body = request.get_json()
#This could be a helper function----#
if "title" in request_body:
goal.title = request_body["title"]
else:
response_str = f"Invalid data"
abort(make_response({"details": response_str}, 400))
#end a helper function#
db.session.commit()
response = {"goal": goal.make_dict()}
return make_response(response, 200)

@goal_bp.route("/<goal_id>", methods = ["DELETE"])
def delete_goal(goal_id):
goal = validate_goal(goal_id)
db.session.delete(goal)
db.session.commit()
response_str = f'Goal {goal_id} "{goal.title}" successfully deleted'
response_body = {"details": response_str}
return make_response(response_body, 200)

#nested routes:
@goal_bp.route("/<goal_id>/tasks", methods = ["POST"])
def create_a_goal_with_tasks(goal_id):
new_goal = validate_goal(goal_id)
request_body = request.get_json()
task_id_list = request_body["task_ids"]
#begin helper function
#for those tasks, assign them to the goal,
for task_id in task_id_list:
task = validate_task(task_id)
task.goal_id = goal_id
#end helper function
db.session.add(new_goal)
db.session.commit()
response_body = {
"id": int(goal_id), #there should be a better way to deal with this.
"task_ids": task_id_list}
return make_response(response_body, 200)

@goal_bp.route("/<goal_id>/tasks", methods = ["GET"])
def get_one_goal_with_tasks(goal_id):
goal = validate_goal(goal_id)
task_list = []
for task in goal.tasks:
task_list.append(task.make_dict())
response = {
"id": int(goal_id), #there should be a better way to do this;
"title": goal.title,
"tasks": task_list
}
return jsonify(response), 200


#ideally, combine this with validate task, passing in the class as well.
def validate_goal(goal_id):
try:
goal_id = int(goal_id)
except ValueError:
response_str = f"Goal {goal_id} must be an integer"
abort(make_response({"message": response_str}, 400))
goal = Goal.query.get(goal_id)
if not goal:
response_str = f"Goal {goal_id} not found"
abort(make_response({"message": response_str}, 404))
return goal

143 changes: 143 additions & 0 deletions app/routes/task.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
from flask import Blueprint, jsonify, abort, make_response, request
from app.models.task import Task
from app import db
from sqlalchemy import desc, asc
from datetime import date
import os
import requests

#make a blueprint
task_bp = Blueprint("task_bp", __name__, url_prefix = "/tasks")

#constants: (refactor to make this from the database later.)
COL_NAMES = ["title", "description", "is_complete"] #later, add completed ad
COL_DEFAULTS = [None, "", False]
COL_NAME_DEFAULT_DICT = dict(zip(COL_NAMES, COL_DEFAULTS))

@task_bp.route("", methods = ["GET"])
def get_all_tasks():
#refactor to helper function
sort_order = request.args.get("sort")
if sort_order == "desc":
tasks = Task.query.order_by(Task.title.desc()).all() #Task.title is just a string??
elif sort_order == "asc":
tasks = Task.query.order_by(Task.title.asc()).all() #look up doc for asc.
else:
tasks = Task.query.all()
#refactor to helper function end here
response = []
for task in tasks:
task_dict = task.make_dict()
response.append(task_dict)
return jsonify(response), 200


@task_bp.route("/<task_id>", methods = ["GET"])
def get_one_task(task_id):
task = validate_task(task_id)
task_dict = task.make_dict()
return make_response({"task": task_dict}, 200)

@task_bp.route("", methods = ["POST"])
def post_new_task():
request_body = request.get_json()
#dict_of_field_vals = fill_empties_with_defaults(request_body)
if "title" not in request_body or "description" not in request_body:
response_str = "Invalid data"
abort(make_response({"details":response_str}, 400))
if "completed_at" not in request_body:
request_body["is_complete"] = False

new_task = Task.from_dict(request_body)
db.session.add(new_task)
db.session.commit()
task_dict = new_task.make_dict()
response = {"task": task_dict}
return make_response(response, 201)

@task_bp.route("/<task_id>", methods = ["PUT", "PATCH"])
def update_task(task_id):
task = validate_task(task_id)
request_body = request.get_json()
task = update_given_values(task, request_body)
db.session.commit()
response = {"task": task.make_dict()} #refactor this line and line 37 above to helper function? or method on class?
return make_response(response, 200)

#can the following be combined with the route above?
@task_bp.route("/<task_id>/<complete_tag>", methods = ["PUT", "PATCH"])
def update_task_completion(task_id, complete_tag):
task = validate_task(task_id)
if complete_tag == "mark_complete":
task.completed_at = date.today().strftime("%B %d, %Y")
task.is_complete = True #not sure if this line is redundant

#post a message to slack to say the task is complete.
#----------Make the following into a helper function later-----#
path = "https://slack.com/api/chat.postMessage"
query_params = {
"channel" : "task-notifications",
"text": "Someone just completed the task " + task.title
}
headers = {
"Authorization" : "Bearer " + os.environ.get("SLACK_API_KEY")

Choose a reason for hiding this comment

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

The reason Learn is failing is that the Environment variable used by Learn is not named SLACK_API_KEY and so os.environ.get("SLACK_API_KEY") returns None.

}

response = requests.post(path, params = query_params, headers = headers)
#----------end helper function--------------#

elif complete_tag == "mark_incomplete":
task.completed_at = None
task.is_complete = False #not sure if this line is redundant
db.session.commit()
response = {"task": task.make_dict()}
return make_response(response, 200)


@task_bp.route("/<task_id>", methods = ["DELETE"])
def delete_task(task_id):
task = validate_task(task_id)
db.session.delete(task)
db.session.commit()
response_body = {"details": f'Task {task_id} "{task.title}" successfully deleted'}
return make_response(response_body, 200)

#ideally, combine this with validate goal, passing in the class as well.
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.

Nice helper function!

try:
task_id = int(task_id)
except ValueError:
response_str = f"Task {task_id} must be an integer"
abort(make_response({"message": response_str}, 400))
task = Task.query.get(task_id)
if not task:
response_str = f"Task {task_id} not found"
abort(make_response({"message": response_str}, 404))
return task

#can I make this a method for Tasks?
def update_given_values(task, request_body):
Comment on lines +118 to +119

Choose a reason for hiding this comment

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

You can indeed make this a method in the tasks class. It would make a great helper method.

if "title" in request_body:
task.title = request_body["title"]
if "description" in request_body:
task.description = request_body["description"]
if "is_complete" in request_body:
task.is_complete = request_body["is_complete"]
#can add completed_at when you put that in.
return task

# I used this in my journal.py. This isn't want the tests are asking for, so I'll delete it.
# def fill_empties_with_defaults(request_body):
# """Go through entered fields.
# If it has an entry, use that, if not, use the default."""
# task_dict = {}
# for field, default in COL_NAME_DEFAULT_DICT.items():

# if field not in request_body:
# task_dict[field] = default
# else:
# task_dict[field] = request_body[field]

# return task_dict


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