Skip to content
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 - Mazzy #119

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
88a9a46
created Task model
mlweir98 May 4, 2023
7622bd3
set up app
mlweir98 May 11, 2023
86a87f7
created task model
mlweir98 May 11, 2023
5c6e3e7
created routes for tasks
mlweir98 May 11, 2023
72c00ef
finished tests for wave 1
mlweir98 May 11, 2023
94bc321
finished tests for wave 2
mlweir98 May 11, 2023
4751c94
finished tests for wave 3
mlweir98 May 11, 2023
31f6ac0
finished wave 3: added mark_complete and mark_incomplete endpoints
mlweir98 May 11, 2023
24d4469
migrations
mlweir98 May 11, 2023
7adc19d
changed routes to goal and task routes
mlweir98 May 12, 2023
1272799
changed name
mlweir98 May 12, 2023
d00d80b
added slack into requirements
mlweir98 May 12, 2023
56bc34a
created file for goal routes
mlweir98 May 12, 2023
5504056
changed name of routes to task_routes and added in slackbot info for …
mlweir98 May 12, 2023
f117305
registered goals blueprint
mlweir98 May 12, 2023
8184a20
added GET, POST, PUT, DELETE endpoints for goal
mlweir98 May 12, 2023
64b7bcc
added title column to goal model
mlweir98 May 12, 2023
9631d61
added slack api to mark_complete
mlweir98 May 12, 2023
818e073
completed and passed all tests
mlweir98 May 12, 2023
de9aead
finished wave 6, adding nested routes
mlweir98 May 12, 2023
3ec8f39
added tasks relationship
mlweir98 May 12, 2023
a4a9d2b
added goals relationship
mlweir98 May 12, 2023
79d2158
updated GET endpoint to include book_id
mlweir98 May 12, 2023
dedf22e
finished test wave 6 and passed all tests
mlweir98 May 12, 2023
e466074
updated tables
mlweir98 May 12, 2023
b91bab4
refactored to add to_dict method in task class
mlweir98 May 13, 2023
29b0b0f
added to_dict method to class goal
mlweir98 May 13, 2023
3418a8f
connected to render
mlweir98 May 13, 2023
0060bfb
connected to render
mlweir98 May 13, 2023
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
7 changes: 6 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def create_app(test_config=None):

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

# Register Blueprints here
from .task_routes import tasks_bp
app.register_blueprint(tasks_bp)

from .goal_routes import goals_bp
app.register_blueprint(goals_bp)

return app
122 changes: 122 additions & 0 deletions app/goal_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from flask import Blueprint, jsonify, abort, make_response, request
from app import db
from app.models.task import Task
from app.models.goal import Goal
from .task_routes import validate_model
import requests

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

@goals_bp.route("", methods=["POST"])
def create_goal():

request_body = request.get_json()

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

except:
abort(make_response({
"details": "Invalid data"
Comment on lines +15 to +20

Choose a reason for hiding this comment

The 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 request_body and a list of strings that are keys and then check to see if those keys are present.

Choose a reason for hiding this comment

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

Now we can have error handling and request body validation across all our routes more easily.

}, 400))

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

return make_response(jsonify({"goal":{
"id" : new_goal.goal_id,
"title" : new_goal.title,
}}),201)

@goals_bp.route("", methods=["GET"])
def get_goals():

goals = Goal.query.all()

goals_response = []

for goal in goals:
goals_response.append(
goal.to_dict()
)
Comment on lines +38 to +41

Choose a reason for hiding this comment

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

Nice utilization of your .to_dict() helper method! ⭐️


return jsonify(goals_response)

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

goal = validate_model(Goal, goal_id)

return {"goal":goal.to_dict()}, 200

@goals_bp.route("/<goal_id>", methods=["PUT"])
def update_one_goal(goal_id):

goal = validate_model(Goal, goal_id)

request_body = request.get_json()

goal.title = request_body["title"]

db.session.commit()

return make_response(jsonify({"goal":goal.to_dict()}),200)

@goals_bp.route("/<goal_id>", methods=["DELETE"])
def remove_one_goal(goal_id):

goal = validate_model(Goal, goal_id)

db.session.delete(goal)
db.session.commit()

return make_response({
"details": f'{Goal.__name__} {goal_id} "{goal.title}" successfully deleted'
}), 200

# nested routes
@goals_bp.route("/<goal_id>/tasks", methods=["POST"])
def create_tasks_for_goal(goal_id):

goal = validate_model(Goal, goal_id)

request_body = request.get_json()

tasks_list = []

for task in request_body["task_ids"]:
task = validate_model(Task, task)
tasks_list.append(task)
task.goal_id = goal.goal_id
Comment on lines +87 to +90

Choose a reason for hiding this comment

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

⭐️


goal.tasks = tasks_list

db.session.commit()

return make_response(jsonify({
"id" : goal.goal_id,
"task_ids" : request_body["task_ids"],
}),200)

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

goal = validate_model(Goal, goal_id)

tasks_list = []

for task in goal.tasks:
tasks_list.append({
"id" : task.task_id,
"goal_id" : task.goal_id,
"title" : task.title,
"description" : task.description,
"is_complete" : False
})
Comment on lines +108 to +115

Choose a reason for hiding this comment

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

Well done! I have a question for you, do you think that this loop and the logic within could be moved to into the Goal class?


return {
"id": goal.goal_id,
"title" : goal.title,
"tasks" : tasks_list
}, 200

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

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",lazy=True)

def to_dict(self):
goal_as_dict = {}
goal_as_dict["id"] = self.goal_id
goal_as_dict["title"] = self.title


return goal_as_dict
Comment on lines +9 to +15

Choose a reason for hiding this comment

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

Nice helper method function! ⭐️

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

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)
goal_id = db.Column(db.Integer, db.ForeignKey('goal.goal_id'),nullable=True)
goal = db.relationship("Goal", back_populates="tasks")

def to_dict(self):
task_as_dict = {}
task_as_dict["id"] = self.task_id
task_as_dict["title"] = self.title
task_as_dict["description"] = self.description
task_as_dict["is_complete"] = bool(self.completed_at)

return task_as_dict

@classmethod
def from_dict(cls, task_data):
new_task = Task(
title = task_data["title"],
description = task_data["description"]
)

return new_task

Choose a reason for hiding this comment

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

1 change: 0 additions & 1 deletion app/routes.py

This file was deleted.

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

tasks_bp = Blueprint("tasks",__name__,url_prefix="/tasks")

@tasks_bp.route("", methods=["POST"])
def create_task():

request_body = request.get_json()

try:
new_task = Task(title=request_body["title"],
description=request_body["description"])

except:
abort(make_response({
"details": "Invalid data"
}, 400))

db.session.add(new_task)
db.session.commit()

return make_response(jsonify({"task":{
"id" : new_task.task_id,
"title" : new_task.title,
"description" : new_task.description,
"is_complete" : False
}}),201)

@tasks_bp.route("", methods=["GET"])
def get_tasks():

sort_query = request.args.get("sort")

if sort_query == "asc":
tasks = Task.query.order_by(Task.title.asc()).all()
elif sort_query == "desc":
tasks = Task.query.order_by(Task.title.desc()).all()
else:
tasks = Task.query.all()
Comment on lines +40 to +45

Choose a reason for hiding this comment

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

Awesome! 💫


tasks_response = []

for task in tasks:
tasks_response.append(
task.to_dict()
)

return jsonify(tasks_response)

def validate_model(cls, model_id):

Choose a reason for hiding this comment

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

Make sure your helper functions are in a place that's easy to find

try:
model_id = int(model_id)
except:
abort(make_response({"message":f"{model_id} is not valid type ({type(model_id)}) invalid. Please use integer."}, 400))

model = cls.query.get(model_id)

if not model:
abort(make_response({"message":f"{cls.__name__} {model_id} does not exist"}, 404))

return model

@tasks_bp.route("/<task_id>", methods=["GET"])
def get_one_task(task_id):

task = validate_model(Task, task_id)

if task.goal_id:
return {
"task": {
"id": task.task_id,
"goal_id": task.goal_id,
"title": task.title,
"description": task.description,
"is_complete": False
}
}

return {"task":task.to_dict()}, 200

@tasks_bp.route("/<task_id>", methods=["PUT"])
def update_one_task(task_id):
task = validate_model(Task, task_id)

request_body = request.get_json()

task.title = request_body["title"]
task.description = request_body["description"]

db.session.commit()

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

def send_slack_message(task_title):

path = 'https://slack.com/api/chat.postMessage'

slack_key = os.environ.get("SLACK_API_KEY")

params = {
"channel":"general",
"text":f"Someone just completed the task {task_title}"
}
headers = {
"Authorization":slack_key
}

requests.get(path, headers=headers, params=params)

@tasks_bp.route("<task_id>/mark_complete", methods=["PATCH"])
def mark_complete(task_id):

task = validate_model(Task, task_id)

send_slack_message(task.title)

Choose a reason for hiding this comment

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

So, right now your route sends a message to slack saying that the task has been completed, before the task is marked complete. But what happens if your can't actually commit the change, your code will send out a false positive. You typically will want to send alerts like this after all your other logic has ran.


task.completed_at = datetime.utcnow()

db.session.commit()

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


@tasks_bp.route("<task_id>/mark_incomplete", methods=["PATCH"])
def mark_incomplete(task_id):

task = validate_model(Task, task_id)

task.completed_at = None

db.session.commit()

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

@tasks_bp.route("/<task_id>", methods=["DELETE"])
def remove_one_task(task_id):

task = validate_model(Task, task_id)

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

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

Choose a reason for hiding this comment

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

Well done on this project Maz, 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! ✨💫🤭

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