-
Notifications
You must be signed in to change notification settings - Fork 128
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
Zoisite - Sabs Ford #123
base: main
Are you sure you want to change the base?
Zoisite - Sabs Ford #123
Changes from all commits
7f75d27
c4c2e0e
5d718b4
7cc6389
cb5e47c
04f3a46
e19404a
cd8f7cd
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 |
---|---|---|
|
@@ -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", backref="goal", lazy=True) | ||
|
||
|
||
@classmethod | ||
def from_dict(cls, goal_data): | ||
new_goal = Goal(title=goal_data["title"]) | ||
return new_goal | ||
|
||
def make_dict(self): | ||
return dict( | ||
id = self.goal_id, | ||
title = self.title) | ||
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 looks good! |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,4 +2,32 @@ | |
|
||
|
||
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, default=None) | ||
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 default for nullable is True, so we do not need to explicitly set the attribute to True here! |
||
goal_id = db.Column(db.Integer, db.ForeignKey('goal.goal_id'), nullable=True) | ||
# goal = db.relationship("Goal", back_populates="tasks") | ||
|
||
@classmethod | ||
def from_dict(cls, task_data): | ||
new_task = Task(title=task_data["title"], | ||
description=task_data["description"]) | ||
return new_task | ||
|
||
def make_dict(self): | ||
if self.goal_id: | ||
return dict( | ||
id = self.task_id, | ||
goal_id = self.goal_id, | ||
title = self.title, | ||
description = self.description, | ||
is_complete = bool(self.completed_at)) | ||
else: | ||
return dict( | ||
id = self.task_id, | ||
title = self.title, | ||
description = self.description, | ||
is_complete = bool(self.completed_at)) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,212 @@ | ||
from flask import Blueprint | ||
from app import db | ||
from app.models.task import Task | ||
from app.models.goal import Goal | ||
from sqlalchemy import asc, desc | ||
from flask import Blueprint, jsonify, make_response, abort, request | ||
import requests | ||
from datetime import date | ||
import os | ||
from dotenv import load_dotenv | ||
|
||
load_dotenv() | ||
|
||
tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") | ||
goals_bp = Blueprint("goals", __name__, url_prefix="/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. Overall, it's ok to have all of our routes here in one file because this is a pretty small project, but as you start building more and larger scale APIs, it's a good idea to separate your routes into their own files. For example, you could have a task_routes.py and a goal_routes.py as well. If you wanted to take it a step further, you can also have a helper_functions.py file that could be used to house any helper functions that apply to all routes (validate_model for example). |
||
|
||
def validate_task(task_id): | ||
try: | ||
task_id = int(task_id) | ||
except: | ||
message = f"task {task_id} not found" | ||
abort(make_response({"message": message}, 400)) | ||
|
||
task = Task.query.get(task_id) | ||
|
||
if not task: | ||
abort(make_response({"message":f"task {task_id} not found"}, 404)) | ||
return task | ||
|
||
def validate_goal(goal_id): | ||
try: | ||
goal_id = int(goal_id) | ||
except: | ||
message = f"task {goal_id} not found" | ||
abort(make_response({"message": message}, 400)) | ||
|
||
goal = Goal.query.get(goal_id) | ||
|
||
if not goal: | ||
abort(make_response({"message":f"goal {goal_id} not found"}, 404)) | ||
return 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 is a great start! As you refactor, make note of any places where you have code that looks really similar. For example, your validate_task and your validate_goal methods are so similar. You could refactor them into a single validate_model that looks something like this:
|
||
|
||
|
||
@tasks_bp.route("", methods =["POST"]) | ||
def create_task(): | ||
request_body = request.get_json() | ||
|
||
if "description" not in request_body or "title" not in request_body: | ||
abort(make_response({"details": "Invalid data"}, 400)) | ||
|
||
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. While this exception handling can work, a more appropriate structure would be to use a try/catch block here. This allows you to catch a variety of errors as opposed to having to specify each thing that might be an issue. For example, we could replace the conditional here with:
This will attempt to make the new_task and if for whatever reason it can't, it will throw the error. While this functions the same way as your conditional, it's just a little more streamlined and easy to read! |
||
new_task = Task.from_dict(request_body) | ||
|
||
db.session.add(new_task) | ||
db.session.commit() | ||
|
||
return make_response({"task": new_task.make_dict()}, 201) | ||
|
||
@tasks_bp.route("", methods =["GET"]) | ||
def get_tasks_data(): | ||
sort_query = request.args.get("sort") | ||
|
||
if sort_query == "asc": | ||
tasks = Task.query.order_by(asc(Task.title)) | ||
elif sort_query == "desc": | ||
tasks = Task.query.order_by(desc(Task.title)) | ||
else: | ||
tasks = Task.query.all() | ||
|
||
tasks_response = [] | ||
for task in tasks: | ||
tasks_response.append(task.make_dict()) | ||
return jsonify(tasks_response), 200 | ||
|
||
|
||
@tasks_bp.route("/<task_id>", methods =["GET"]) | ||
def get_task_data(task_id): | ||
task = validate_task(task_id) | ||
return make_response({"task": task.make_dict()}, 200) | ||
|
||
@tasks_bp.route("/<task_id>", methods =["PUT"]) | ||
def update_task(task_id): | ||
task = validate_task(task_id) | ||
request_body = request.get_json() | ||
|
||
task.title = request_body["title"] | ||
task.description = request_body["description"] | ||
|
||
db.session.commit() | ||
|
||
return make_response({"task": task.make_dict()}, 200) | ||
|
||
@tasks_bp.route("/<task_id>", methods =["DELETE"]) | ||
def delete_task(task_id): | ||
task = validate_task(task_id) | ||
|
||
db.session.delete(task) | ||
db.session.commit() | ||
|
||
message = {f"details": 'Task 1 "Go on my daily walk 🏞" successfully deleted'} | ||
return make_response(jsonify(message)), 200 | ||
|
||
@tasks_bp.route("/<task_id>/mark_complete", methods =["PATCH"]) | ||
def mark_complete(task_id): | ||
task = validate_task(task_id) | ||
task.completed_at = date.today() | ||
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. Your patch methods and really any method where you plan on including any information in the request body is a good candidate for error handling! This allows you to make sure the user is attaching the correct information to the route's request body! |
||
|
||
db.session.commit() | ||
notify_slack(task) | ||
|
||
return make_response({"task": task.make_dict()}, 200) | ||
|
||
def notify_slack(task): | ||
url = "https://slack.com/api/chat.postMessage" | ||
body = { | ||
"channel": "task-notification", | ||
"text": f"Someone just completed the task {task.title}" | ||
} | ||
header = {"Authorization": os.environ.get('SLACK_TOKEN'), | ||
"Content-Type": "application/json"} | ||
|
||
requests.post(url, json=body, headers=header) | ||
|
||
@tasks_bp.route("/<task_id>/mark_incomplete", methods =["PATCH"]) | ||
def mark_incomplete(task_id): | ||
|
||
task = validate_task(task_id) | ||
|
||
task.completed_at = None | ||
|
||
db.session.commit() | ||
|
||
return make_response({"task": task.make_dict()}, 200) | ||
|
||
|
||
@goals_bp.route("", methods =["POST"]) | ||
def create_goals(): | ||
request_body = request.get_json() | ||
|
||
if "title" not in request_body: | ||
abort(make_response({"details": "Invalid data"}, 400)) | ||
|
||
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. Same as before. Let's look at using a try/except block here for the error handling! |
||
new_goal = Goal.from_dict(request_body) | ||
|
||
db.session.add(new_goal) | ||
db.session.commit() | ||
|
||
|
||
return make_response({"goal": new_goal.make_dict()}, 201) | ||
|
||
@goals_bp.route("", methods =["GET"]) | ||
def get_goals_data(): | ||
goals = Goal.query.all() | ||
|
||
goals_response = [] | ||
for goal in goals: | ||
goals_response.append(goal.make_dict()) | ||
return jsonify(goals_response), 200 | ||
|
||
@goals_bp.route("/<goal_id>", methods =["GET"]) | ||
def get_goal_data(goal_id): | ||
goal = validate_goal(goal_id) | ||
return make_response({"goal": goal.make_dict()}, 200) | ||
|
||
|
||
@goals_bp.route("/<goal_id>", methods =["PUT"]) | ||
def update_goal(goal_id): | ||
goal = validate_goal(goal_id) | ||
request_body = request.get_json() | ||
|
||
goal.title = request_body["title"] | ||
|
||
db.session.commit() | ||
|
||
return make_response({"goal": goal.make_dict()}, 200) | ||
|
||
@goals_bp.route("/<goal_id>", methods =["DELETE"]) | ||
def delete_goal(goal_id): | ||
goal = validate_goal(goal_id) | ||
|
||
db.session.delete(goal) | ||
db.session.commit() | ||
|
||
message = {f"details": 'Goal 1 \"Build a habit of going outside daily\" successfully deleted'} | ||
return make_response(jsonify(message)), 200 | ||
|
||
|
||
@goals_bp.route("/<goal_id>/tasks", methods =["POST"]) | ||
def sending_list_of_task_to_goal(goal_id): | ||
goal = validate_goal(goal_id) | ||
request_body = request.get_json() | ||
|
||
for task_id in request_body["task_ids"]: | ||
task = validate_task(task_id) | ||
goal.tasks.append(task) | ||
|
||
db.session.commit() | ||
|
||
return make_response(jsonify(id=goal.goal_id, task_ids=request_body["task_ids"])) | ||
|
||
@goals_bp.route("/<goal_id>/tasks", methods =["GET"]) | ||
def task_of_one_goal(goal_id): | ||
goal = validate_goal(goal_id) | ||
response = {"id": goal.goal_id, | ||
"title": goal.title, | ||
"tasks": []} | ||
|
||
for task in goal.tasks: | ||
response["tasks"].append(task.make_dict()) | ||
|
||
return make_response(response) | ||
|
||
|
||
|
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.
Great job with configuration here!