-
Notifications
You must be signed in to change notification settings - Fork 15
Accelerate - Risha A. #13
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
base: master
Are you sure you want to change the base?
Changes from all commits
def3a08
083c255
1f1a276
9d24258
a586ca8
af0f34e
9dc49c2
8443b7e
ffa798e
8f4eb91
4c444ff
c7bdf78
65b3ba2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,6 +1,14 @@ | ||||||||||||||||||||||
from flask import current_app | ||||||||||||||||||||||
from app import db | ||||||||||||||||||||||
|
||||||||||||||||||||||
from app.models.task import Task | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need to import the Task class name here. We only use the string "Task" in the relationship declaration. We only need to import |
||||||||||||||||||||||
|
||||||||||||||||||||||
class Goal(db.Model): | ||||||||||||||||||||||
goal_id = db.Column(db.Integer, primary_key=True) | ||||||||||||||||||||||
title = db.Column(db.String) | ||||||||||||||||||||||
tasks = db.relationship("Task", backref="task", lazy=True) | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Then, if we have a The
Suggested change
|
||||||||||||||||||||||
|
||||||||||||||||||||||
def goal_json(self): | ||||||||||||||||||||||
return( { | ||||||||||||||||||||||
"id": self.goal_id, | ||||||||||||||||||||||
"title": self.title | ||||||||||||||||||||||
}, 200) | ||||||||||||||||||||||
Comment on lines
+10
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method is the main reason wave 5 has some failures. Your routes logic is trying to call a method called The following change would allow Wave 5 to pass!
Suggested change
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,6 +1,29 @@ | ||||||||||||||||||||
from flask import current_app | ||||||||||||||||||||
from app import db | ||||||||||||||||||||
|
||||||||||||||||||||
# from app.models.goal import Goal | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line is not necessary (it's commented, so you probably noticed that) 😄 We only need to import thee Additionally. since there was an import in |
||||||||||||||||||||
|
||||||||||||||||||||
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) | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By default, columns are nullable. We really ony need to specify nullable is we want it to be But, we should think about whether the other columns should be nullable. Does it make sense for a task to have a NULL (None) description? How about a title? We should think about what logically makes sense for our data, and as much as we can, use the database to help us protect the integrity of our data! |
||||||||||||||||||||
goal_id = db.Column(db.Integer, db.ForeignKey("goal.goal_id"), nullable=True) | ||||||||||||||||||||
__tablename__ = "tasks" | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What prompted you to change the default table name? |
||||||||||||||||||||
|
||||||||||||||||||||
def to_json(self): | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This helper doesn't seem to be used in your code. By adding this method, we would be able to avoid building the json dictionary literals for tasks throughout the routes file. By using this method (with the minor fixes mentioned below), most of wave 6 would pass. |
||||||||||||||||||||
if self.completed_at: | ||||||||||||||||||||
completed = True | ||||||||||||||||||||
else: | ||||||||||||||||||||
completed = False | ||||||||||||||||||||
|
||||||||||||||||||||
task_dict={ | ||||||||||||||||||||
"id": self.id, | ||||||||||||||||||||
"title": self.title, | ||||||||||||||||||||
"description": self.description, | ||||||||||||||||||||
"is_complete": completed, | ||||||||||||||||||||
"goal_id": self.goal_id # if the task belongs a goal add if not do not add | ||||||||||||||||||||
Comment on lines
+20
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a small typo in getting the task id (the field is called task_id). Also, we don't need to add the goal_id here, since it is selectively added just below.
Suggested change
|
||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
if self.goals_id: | ||||||||||||||||||||
task_dict["goal_id"]= self.goals_id | ||||||||||||||||||||
Comment on lines
+27
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Be careful of the field name. It should be
Suggested change
|
||||||||||||||||||||
return task_dict |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,2 +1,273 @@ | ||||||
from flask import Blueprint | ||||||
from app import db | ||||||
from flask import Blueprint, json | ||||||
from sqlalchemy import asc, desc | ||||||
from app.models import task | ||||||
from app.models.task import Task | ||||||
from app.models.goal import Goal | ||||||
from datetime import datetime | ||||||
from sqlalchemy.sql.functions import now | ||||||
from flask import request, make_response, jsonify | ||||||
from dotenv import load_dotenv | ||||||
import os | ||||||
import requests | ||||||
|
||||||
load_dotenv() | ||||||
|
||||||
task_bp = Blueprint("tasks", __name__, url_prefix="/tasks") | ||||||
goals_bp = Blueprint("goals", __name__, url_prefix="/goals") | ||||||
Comment on lines
+16
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could consider putting all the routes related to the tasks endpoint in their own file, and doing the same for the goals routes. |
||||||
|
||||||
|
||||||
# return all tasks | ||||||
@task_bp.route("/", methods=["GET", "POST"], strict_slashes = False) | ||||||
def handle_tasks(): | ||||||
if request.method == "GET": | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic for GET and POST doesn't share any code, so we could consider putting the logic for each in separate functions, maybe |
||||||
|
||||||
# tasks by asc and desc order | ||||||
sort_by_url = request.args.get("sort") # query parameters and replace the previous query | ||||||
if sort_by_url == "asc": # this is a list queried by title in asc order | ||||||
tasks = Task.query.order_by(Task.title.asc()).all() | ||||||
elif sort_by_url == "desc": | ||||||
tasks = Task.query.order_by(Task.title.desc()).all() | ||||||
else: | ||||||
tasks = Task.query.all() | ||||||
Comment on lines
+26
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 Nice handling of the sort parameter (great job pushing the sorting work to the database!), and a sensible fallback if the option is invalid (just return everything as usual). |
||||||
# end of the new code | ||||||
|
||||||
tasks_response = [] | ||||||
for task in tasks: | ||||||
if task.completed_at == None: | ||||||
completed_at = False | ||||||
else: | ||||||
completed_at = True | ||||||
|
||||||
tasks_response.append({ | ||||||
"id": task.task_id, | ||||||
"title": task.title, | ||||||
"description": task.description, | ||||||
"is_complete": completed_at, | ||||||
Comment on lines
+43
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Places like this are where we'd like to use that |
||||||
}) | ||||||
return make_response(jsonify(tasks_response), 200) | ||||||
|
||||||
|
||||||
#create a task with null completed at | ||||||
elif request.method == "POST": | ||||||
request_body = request.get_json() | ||||||
if "title" not in request_body or "description" not in request_body or "completed_at" not in request_body: | ||||||
return {"details": "Invalid data"}, 400 | ||||||
|
||||||
if not "completed_at" in request_body or not request_body["completed_at"]: | ||||||
completed_at = False | ||||||
else: | ||||||
completed_at = True | ||||||
|
||||||
Comment on lines
+54
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 Good job on handling all these cases for creating a new This can be created as a class method on @classmethod
def from_dict(values):
# create a new task, and set the model values from the values passed in
# be sure to validate that all required values are present, we could return `None` or raise an error if needed
return new_task |
||||||
new_task = Task( | ||||||
title=request_body["title"], | ||||||
description=request_body["description"], | ||||||
completed_at=request_body["completed_at"] | ||||||
) | ||||||
|
||||||
db.session.add(new_task) # "adds model to the db" | ||||||
db.session.commit() # commits the action above | ||||||
|
||||||
return make_response({ | ||||||
"task":{ | ||||||
"id": new_task.task_id, | ||||||
"title": new_task.title, | ||||||
"description": new_task.description, | ||||||
"is_complete": completed_at | ||||||
} | ||||||
}), 201 | ||||||
|
||||||
# return one task | ||||||
@task_bp.route("/<task_id>", methods=["GET", "PUT", "DELETE"], strict_slashes = False) | ||||||
def handle_task(task_id): | ||||||
task = Task.query.get(task_id) | ||||||
|
||||||
if request.method == "GET": | ||||||
if task is None: | ||||||
return make_response(f"Task #{task_id} not found"), 404 | ||||||
Comment on lines
+86
to
+87
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All of these verbs need to check that the Task was valid, so this could be moved up and out of the per-verb logic. We could think about using And if the shared lookup code is reduced to using |
||||||
if not task.completed_at: | ||||||
completed_at = False | ||||||
else: | ||||||
completed_at = task.completed_at["completed_at"] | ||||||
return { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One reason wave 6 was failing was because here, |
||||||
"task":{ | ||||||
"id": task.task_id, | ||||||
"title": task.title, | ||||||
"description": task.description, | ||||||
"is_complete": completed_at | ||||||
} | ||||||
}, 200 | ||||||
|
||||||
# Update a task | ||||||
elif request.method == "PUT": | ||||||
if task is None: | ||||||
return make_response(f"Task #{task_id} not found"), 404 | ||||||
form_data = request.get_json() | ||||||
|
||||||
if "title" not in form_data or "description" not in form_data or "completed_at" not in form_data: | ||||||
return {"details": "Invalid data"}, 400 | ||||||
|
||||||
if not "completed_at" in form_data or not form_data["completed_at"]: | ||||||
is_complete = False | ||||||
else: | ||||||
is_complete = True | ||||||
Comment on lines
+107
to
+113
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mentioned this above, but this and the equivalent POST code could benefit from being moved to a helper function. |
||||||
task.title = form_data["title"] | ||||||
task.description = form_data["description"] | ||||||
task.completed_at = form_data["completed_at"] | ||||||
|
||||||
db.session.commit() | ||||||
return { | ||||||
"task": { | ||||||
"id": task.task_id, | ||||||
"title": task.title, | ||||||
"description": task.description, | ||||||
"is_complete": is_complete | ||||||
} | ||||||
}, 200 | ||||||
|
||||||
# Delete a task | ||||||
elif request.method == "DELETE": | ||||||
if not task: | ||||||
return "", 404 | ||||||
db.session.delete(task) | ||||||
db.session.commit() | ||||||
return { | ||||||
"details": f"Task {task_id} \"{task.title}\" successfully deleted" | ||||||
}, 200 | ||||||
|
||||||
|
||||||
# modify complete task | ||||||
@task_bp.route("/<task_id>/mark_complete", methods=["PATCH"], strict_slashes = False) | ||||||
def mark_task_complete(task_id): | ||||||
task = Task.query.get(task_id) | ||||||
|
||||||
if not task: # this is the task check (wash my car) | ||||||
return make_response(f"Task #{task_id} not found"), 404 | ||||||
task.completed_at = datetime.utcnow() | ||||||
db.session.commit() | ||||||
|
||||||
base_path = "https://slack.com/api/chat.postMessage" | ||||||
slack_tocken = os.environ.get("SLACK_API_KEY") | ||||||
query_params = { | ||||||
"channel": "task-notifications", | ||||||
"text": f"Someone just completed the {task.title}" | ||||||
} | ||||||
headers = { | ||||||
"Authorization": f"Bearer {slack_tocken}" | ||||||
} | ||||||
requests.post(base_path, params=query_params, headers=headers) | ||||||
Comment on lines
+149
to
+158
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 Nice Slack integration. We could consider moving this to a helper function so that the main purpose of this function (marking the task complete) is easier to see. Also, since we're sending a post request, we would typically send the parameters as form data, rather than as query params. Query params do have a maximum length (as part of the HTTP standard), so when we have potentially large data (like a text message), we often send that data in the form-encoded body of a POST request (this stems from older web standards. Now, we might use JSON in the request body). With the requests.post(base_path, data=query_params, headers=headers) |
||||||
|
||||||
response_body = { | ||||||
"task": { | ||||||
"id": task.task_id, | ||||||
"title": task.title, | ||||||
"description": task.description, | ||||||
"is_complete": True # this is always true bc we are always setting completed at | ||||||
} | ||||||
} | ||||||
return jsonify(response_body), 200 | ||||||
|
||||||
# modify incomplete task | ||||||
@task_bp.route("/<task_id>/mark_incomplete", methods=["PATCH"]) | ||||||
def make_task_incomplete(task_id): | ||||||
task = Task.query.get(task_id) | ||||||
|
||||||
if task is None: | ||||||
return make_response(f"Task #{task_id} not found"), 404 | ||||||
else: | ||||||
task.completed_at = None | ||||||
db.session.commit() | ||||||
response_body = { | ||||||
"task": { | ||||||
"id": task.task_id, | ||||||
"title": task.title, | ||||||
"description": task.description, | ||||||
"is_complete": False | ||||||
} | ||||||
} | ||||||
return jsonify(response_body), 200 | ||||||
|
||||||
|
||||||
# goals route | ||||||
@goals_bp.route("", methods = ["POST", "GET"], strict_slashes = False) | ||||||
def handle_goals(): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar feedback about splitting these functions, and moving validation and dictionary-handling logic around that I made for |
||||||
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() | ||||||
response_body = { | ||||||
"goal": new_goal.goal_to_json() | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The main issue with wave 5 was that your equivalent "to json" method was defined with the name |
||||||
} | ||||||
return jsonify(response_body), 201 | ||||||
|
||||||
#get all goals | ||||||
if request.method == "GET": | ||||||
goals = Goal.query.all() | ||||||
response_body = [] | ||||||
for a_goal in goals: | ||||||
response_body.append(a_goal.goal_to_json()) | ||||||
return jsonify(response_body), 200 | ||||||
|
||||||
# get one goal | ||||||
@goals_bp.route("/<goal_id>", methods=["GET", "PUT", "DELETE"], strict_slashes = False) | ||||||
def handle_goal(goal_id): | ||||||
goal = Goal.query.get(goal_id) | ||||||
|
||||||
if request.method == "GET": | ||||||
|
||||||
if goal is None: | ||||||
return make_response("", 404) | ||||||
|
||||||
response_body = { | ||||||
"goal": goal.goal_to_json() | ||||||
} | ||||||
return jsonify(response_body), 200 | ||||||
|
||||||
# Update a goal | ||||||
elif request.method == "PUT": | ||||||
if goal is None: | ||||||
return make_response(f"Goal #{goal_id} not found"), 404 | ||||||
form_data = request.get_json() | ||||||
|
||||||
if "title" not in form_data: | ||||||
return {"details": "Invalid data"}, 400 | ||||||
|
||||||
goal.title = form_data["title"] | ||||||
db.session.commit() | ||||||
|
||||||
response_body = { | ||||||
"goal": goal.goal_to_json() | ||||||
} | ||||||
return jsonify(response_body), 200 | ||||||
|
||||||
# Delete a goal | ||||||
elif request.method == "DELETE": | ||||||
if not goal: | ||||||
return "", 404 | ||||||
db.session.delete(goal) | ||||||
db.session.commit() | ||||||
|
||||||
return { | ||||||
"details": f"Goal {goal.goal_id} \"{goal.title}\" successfully deleted" | ||||||
} | ||||||
# one-to-many relationship | ||||||
@goals_bp.route("/<goal_id>/tasks", methods = ["POST", "GET"], strict_slashes = False) | ||||||
def handle_task_to_goals(goal_id): | ||||||
goal = Goal.query.get(goal_id) | ||||||
if not goal: | ||||||
return "", 404 | ||||||
|
||||||
if request.method == "POST": | ||||||
request_body = request.get_json() | ||||||
task_ids = request_body["task_ids"] | ||||||
|
||||||
for task_id in task_ids: | ||||||
goal_id.task_id = goal_id | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For any of these cases, we really need to lookup each passed task id to get an instance, then either set the task = Task.query.get_or_404(task_id)
task.goal = goal This wouldn't remove any existing tasks from the goal (we should think about what to do for those tasks). Additionally, to ensure that the change to the relationships occur transactionally (either they all apply, or none of them do), we can move the |
||||||
|
||||||
db.session.commit() | ||||||
|
||||||
return {"id":(goal_id), "task_ids": task_ids},200 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The test is expecting a numeric version of the goal id, so we can get the version stored in the goal itself (or force the conversion with
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The last bit that would be needed for wave 6 would be to handle GET requests for the tasks of a goal. Something like the following would be a good start. else if request.method == "GET":
tasks = []
for task in goal.tasks:
tasks.append(task.to_json())
return {
"id": goal.goal_id,
"title": goal.title,
"tasks": tasks
}, 200 |
||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Generic single-database configuration. |
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
One thing we started to touch on in the video store live code was that we can split routes into multiple files. We can make a routes folder, and put routes for each endpoint into separate files, named for their model. Then we can use the name
bp
for the blueprint in each file since it would be the only blueprint in the file. Then these imports might look like: