-
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
Amethyst - Megan G #112
base: main
Are you sure you want to change the base?
Amethyst - Megan G #112
Changes from 46 commits
058b9b3
c14aa16
10c8536
015f3f5
5c92162
259ce1e
8ece6ac
33c7dd4
6e3acaf
7666b42
53e7f4f
586c405
b762abe
18e61a3
f51a2ac
1ddaf52
db10823
07b3a81
986b5cd
d509d0b
dcf7a2c
7f07064
342c5af
26090f7
756bb54
8c42aab
315a44a
219e8a1
530e082
4a023ec
f2d010c
bf06968
d0cdaa9
7d310b4
0c39ebb
fb78260
0250f48
96633d9
9e5c1d6
17fdc66
6377c0a
24c223a
d808524
6130e9d
15d02ec
82f1005
cd04b45
8cd7817
2f70169
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 |
---|---|---|
@@ -0,0 +1,106 @@ | ||
from app import db | ||
from app.models.goal import Goal | ||
from app.models.task import Task | ||
from flask import Blueprint, jsonify, make_response, abort, request | ||
import requests | ||
from app.task_routes import validate_model | ||
|
||
goals_bp = Blueprint("goals'", __name__, url_prefix = "/goals") | ||
|
||
@goals_bp.route("", methods=["POST"]) | ||
def create_goal(): | ||
request_body = request.get_json() | ||
|
||
if not request_body.get("title"): | ||
abort(make_response( | ||
{ | ||
"details": "Invalid data" | ||
}, 400 | ||
)) | ||
|
||
new_goal = Goal( | ||
title = request_body["title"], | ||
) | ||
|
||
db.session.add(new_goal) | ||
db.session.commit() | ||
|
||
return make_response({"goal": new_goal.to_dict()}, 201) | ||
|
||
@goals_bp.route("/<goal_id>/tasks", methods=["POST"]) | ||
def add_task_to_goal(goal_id): | ||
goal = validate_model(Goal, goal_id) | ||
|
||
request_body = request.get_json() | ||
|
||
for task_id in request_body["task_ids"]: | ||
task = validate_model(Task, task_id) | ||
task.goal_id = goal.goal_id | ||
|
||
db.session.commit() | ||
|
||
return make_response( | ||
{ | ||
"id": goal.goal_id, | ||
"task_ids": request_body["task_ids"] | ||
} | ||
), 200 | ||
|
||
|
||
@goals_bp.route("", methods=["GET"]) | ||
def read_goals(): | ||
goals = Goal.query.all() | ||
|
||
goals_response = [] | ||
for goal in goals: | ||
goals_response.append(goal.to_dict()) | ||
|
||
return jsonify(goals_response), 200 | ||
|
||
@goals_bp.route("/<goal_id>", methods=["GET"]) | ||
def read_one_saved_goal(goal_id): | ||
goal = validate_model(Goal, goal_id) | ||
|
||
return { | ||
"goal": goal.to_dict() | ||
} | ||
|
||
@goals_bp.route("/<goal_id>/tasks", methods=["GET"]) | ||
def get_tasks_of_one_goal(goal_id): | ||
goal = validate_model(Goal, goal_id) | ||
|
||
tasks_response = [] | ||
for task in goal.tasks: | ||
tasks_response.append(task.to_dict()) | ||
Comment on lines
+72
to
+74
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. How do you think your code would change if you made this logic into a class function? |
||
|
||
return jsonify({ | ||
"id": goal.goal_id, | ||
"title": goal.title, | ||
"tasks": tasks_response | ||
}), 200 | ||
|
||
@goals_bp.route("/<goal_id>", methods=["PUT"]) | ||
def update_goal(goal_id): | ||
goal = validate_model(Goal, goal_id) | ||
|
||
request_body = request.get_json() | ||
|
||
goal.title = request_body["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. Refer to my comment about error handling, on line |
||
|
||
db.session.commit() | ||
|
||
return make_response({"goal": goal.to_dict()}), 200 | ||
|
||
|
||
@goals_bp.route("/<goal_id>", methods=["DELETE"]) | ||
def delete_goal(goal_id): | ||
goal = validate_model(Goal, goal_id) | ||
|
||
db.session.delete(goal) | ||
db.session.commit() | ||
|
||
return make_response( | ||
{ | ||
"details": f"Goal {goal.goal_id} \"{goal.title}\" successfully deleted" | ||
}, 200 | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,4 +2,20 @@ | |
|
||
|
||
class Goal(db.Model): | ||
goal_id = db.Column(db.Integer, primary_key=True) | ||
goal_id = db.Column(db.Integer, primary_key=True, autoincrement=True) | ||
title = db.Column(db.String) | ||
tasks = db.relationship("Task", back_populates="goal", lazy = True) | ||
|
||
def to_dict(self): | ||
|
||
return { | ||
"id": self.goal_id, | ||
"title": self.title, | ||
} | ||
|
||
@classmethod | ||
def from_dict(cls, goal_data): | ||
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 class function! |
||
new_goal = Goal( | ||
title = goal_data["title"], | ||
) | ||
return new_goal |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,4 +2,36 @@ | |
|
||
|
||
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) | ||
goal_id = db.Column(db.Integer, db.ForeignKey("goal.goal_id"), nullable=True) | ||
goal = db.relationship("Goal", back_populates ="tasks") | ||
|
||
def to_dict(self): | ||
if self.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. You always are so good at implementing guard clauses! |
||
return { | ||
"id": self.task_id, | ||
"title": self.title, | ||
"goal_id": self.goal_id, | ||
"description": self.description, | ||
"is_complete": bool(self.completed_at) | ||
} | ||
|
||
else: | ||
return { | ||
"id": self.task_id, | ||
"title": self.title, | ||
"description": self.description, | ||
"is_complete": bool(self.completed_at) | ||
} | ||
|
||
@classmethod | ||
def from_dict(cls, task_data): | ||
new_task = Task( | ||
title = task_data["title"], | ||
description = task_data["description"], | ||
) | ||
return new_task | ||
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
from app import db | ||
from app.models.task import Task | ||
from app.models.goal import Goal | ||
from flask import Blueprint, jsonify, make_response, abort, request | ||
import datetime | ||
import requests | ||
import os | ||
|
||
tasks_bp = Blueprint("tasks", __name__, url_prefix = "/tasks") | ||
|
||
###### Create Route ###### | ||
@tasks_bp.route("", methods=["POST"]) | ||
def create_task(): | ||
request_body = request.get_json() | ||
|
||
if not request_body.get("title") or not request_body.get("description"): | ||
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. Like we validated models, could we also make a general function that validates request bodies? Here's an example: def validate_request_body(request_body, keys):
for key in keys:
if not request_body.get(key):
abort(make_response({
'Invalid Data': f'missing key: {key}'
}, 400))
return True We can pass in the |
||
abort(make_response( | ||
{ | ||
"details": "Invalid data" | ||
}, 400 | ||
)) | ||
|
||
task = Task.from_dict(request_body) | ||
|
||
db.session.add(task) | ||
db.session.commit() | ||
|
||
return jsonify({"task": task.to_dict()}), 201 | ||
|
||
###### Read Route ###### | ||
|
||
@tasks_bp.route("", methods=["GET"]) | ||
def read_tasks(): | ||
|
||
title_query = request.args.get("sort") | ||
if title_query == "asc": | ||
tasks = Task.query.order_by(Task.title).all() | ||
elif title_query == "desc": | ||
tasks = Task.query.order_by(Task.title.desc()).all() | ||
else: | ||
tasks = Task.query.all() | ||
Comment on lines
+35
to
+41
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 work on having the database sort the tasks for you! |
||
|
||
tasks_response = [] | ||
for task in tasks: | ||
tasks_response.append(task.to_dict()), 200 | ||
|
||
return jsonify(tasks_response) | ||
|
||
@tasks_bp.route("/<task_id>", methods=["GET"]) | ||
def read_one_saved_task(task_id): | ||
task = validate_model(Task, task_id) | ||
|
||
return {"task": task.to_dict()} | ||
|
||
###### Update Route ###### | ||
@tasks_bp.route("/<task_id>", methods=["PUT"]) | ||
def update_task(task_id): | ||
task = validate_model(Task, task_id) | ||
|
||
request_body = request.get_json() | ||
|
||
task.title = request_body["title"] | ||
task.description = request_body["description"] | ||
Comment on lines
+62
to
+63
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. Right now, if a user sends a request without the keys title or description your server would crash. There's a couple of ways to handle this, you could call the |
||
|
||
db.session.commit() | ||
|
||
return make_response({"task": task.to_dict()},200) | ||
|
||
###### Patch Route ###### | ||
@tasks_bp.route("/<task_id>/mark_complete", methods=["PATCH"]) | ||
def mark_task_complete(task_id): | ||
task = validate_model(Task, task_id) | ||
|
||
request_body = request.get_json() | ||
|
||
task.completed_at = datetime.datetime.now() | ||
|
||
db.session.commit() | ||
|
||
slack_channel_id = "C0581Q2TDGQ" | ||
slack_url = "https://slack.com/api/chat.postMessage" | ||
|
||
slack_data = { | ||
"channel": slack_channel_id, | ||
"text": f"Someone completed the {task.title} task!" | ||
} | ||
|
||
headers= { | ||
"Authorization": f"Bearer {os.environ.get('SLACK_KEY')}" | ||
} | ||
|
||
slack_request = requests.post(slack_url, headers=headers, json=slack_data) | ||
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 work on having your Slack post sent after the logic of marking a task complete! We don't want to send out any false positive alerts just in case our logic fails during the update! |
||
print(slack_request.text) | ||
return make_response({"task": task.to_dict()}), 200 | ||
|
||
@tasks_bp.route("/<task_id>/mark_incomplete", methods=["PATCH"]) | ||
def mark_task_incomplete(task_id): | ||
task = validate_model(Task, task_id) | ||
|
||
request_body = request.get_json() | ||
|
||
task.completed_at = None | ||
|
||
db.session.commit() | ||
|
||
return make_response({"task": task.to_dict()}, 200) | ||
|
||
###### Delete Route ###### | ||
|
||
@tasks_bp.route("/<task_id>", methods=["DELETE"]) | ||
def delete_task(task_id): | ||
task = validate_model(Task, task_id) | ||
|
||
db.session.delete(task) | ||
db.session.commit() | ||
|
||
return make_response( | ||
{ | ||
"details": f"Task {task.task_id} \"{task.title}\" successfully deleted" | ||
}, 200 | ||
) | ||
|
||
def validate_model(cls, model_id): | ||
try: | ||
model_id = int(model_id) | ||
except: | ||
abort(make_response({"details":f"Invalid data"}, 400)) | ||
|
||
model = cls.query.get(model_id) | ||
|
||
if not model: | ||
abort(make_response({"details":f"{cls.__name__} ID not found"}, 404)) | ||
|
||
return model | ||
Comment on lines
+123
to
+134
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. Make sure that your helper functions are in a place that is easy to find, I typically put them in the top of the file or in another folder somewhere else. 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. Well done on this project, I didn't have much to comment on and that is a good thing! Keep up the good work! Really looking forward to what you create in the frontend! Please feel free to reach out if you have any questions about the feedback that I left! ✨💫🤭 |
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.
Very nice
for loop
, Megan! You ate this! 🤭