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

Raina Campbell Completed Task List #125

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 5 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,9 @@ def create_app(test_config=None):
migrate.init_app(app, db)

# Register Blueprints here
from .task_routes import task_bp
app.register_blueprint(task_bp)
from .goal_routes import goal_bp
app.register_blueprint(goal_bp)

return app
117 changes: 117 additions & 0 deletions app/goal_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from app import db
from app.models.task import Task
from app.models.goal import Goal
from .task_routes import validate_model
from flask import Blueprint, jsonify, request, make_response, abort
from datetime import datetime
import os
import requests
goal_bp = Blueprint("goal_bp", __name__, url_prefix="/goals")

# ----------------------------- ROUTES FOR GOAL MODEL -------------------------

# create a new goal
@goal_bp.route("",methods=["POST"])
def create_goal():
request_body = request.get_json()

try:
new_goal = Goal.from_dict(request_body)
except:
return {"details": "Invalid data"}, 400
Comment on lines +18 to +21

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.


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

return {
"goal":{
"id": new_goal.goal_id,
"title": new_goal.title
}
}, 201

# One (goal) to many (tasks) relationship
@goal_bp.route("/<goal_id>/tasks", methods=["POST"])
def update_goal_id_for_task(goal_id):
goal_from_id = validate_model(Goal, goal_id)

request_body = request.get_json()

for i in request_body["task_ids"]:

Choose a reason for hiding this comment

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

Love this!

task_to_update = validate_model(Task, i)
task_to_update.goal = goal_from_id
db.session.commit()

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


@goal_bp.route("", methods=["GET"])
def read_all_goals():
goals = Goal.query.all()
goals_response = []

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

Choose a reason for hiding this comment

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

How would your code look different if you were to implement this into a class function?


return jsonify(goals_response), 200


@goal_bp.route("/<goal_id>", methods=["GET"])
def read_one_task(goal_id):
goal = validate_model(Goal, goal_id)

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

@goal_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"]

Choose a reason for hiding this comment

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

Right now, if a user sends a request without the key title your server would crash. There's a couple of ways to handle this, you could call the validate_request function before you access the keys in request_body or you could implement a try/except block.


db.session.commit()

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



@goal_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)


# get tasks for a speciic goal
@goal_bp.route("/<goal_id>/tasks", methods=["GET"])
def get_tasks_for_specific_goal(goal_id):
goal = validate_model(Goal, goal_id)

tasks_response = []

for task in goal.tasks:
tasks_response.append({
"id": task.task_id,
"goal_id": task.goal_id,
"title": task.title,
"description": task.description,
"is_complete": (task.completed_at != None)
})

return {
"id": goal.goal_id,
"title": goal.title,
"tasks": tasks_response
}
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)
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):
goal_as_dict = {}
goal_as_dict["id"] = self.goal_id
goal_as_dict["title"] = self.title

return goal_as_dict

@classmethod
def from_dict(cls, goal_data):
new_goal = Goal(title=goal_data["title"])
return new_goal


27 changes: 25 additions & 2 deletions app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
from app import db


# create Task model
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):
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"] = (self.completed_at != None)

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.

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


# register task bp
task_bp = Blueprint("task_bp", __name__, url_prefix="/tasks")
SLACK_BOT_TOKEN = os.environ.get('SLACK_BOT_TOKEN')

# validate task by id!
def validate_model(cls, model_id):
try:
model_id = int(model_id)
except:
abort(make_response({"message":f"{cls.__name__} {model_id} invalid"}, 400))

model = cls.query.get(model_id)

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

return model

# create task as a post request
@task_bp.route("",methods=["POST"])
def create_task():
request_body = request.get_json()

#try except
try:
task = Task.from_dict(request_body)
except:
return {"details": "Invalid data"}, 400

db.session.add(task)
db.session.commit()

return {"task":{
"id": task.task_id,
"description": task.description,
"is_complete": (task.completed_at != None),
"title": task.title}}, 201


# read all tasks
@task_bp.route("",methods=["GET"])
def read_all_tasks():
tasks = Task.query.all()

# ascending sort
sorting_query = request.args.get("sort")
if sorting_query == "asc":
tasks = Task.query.order_by(Task.title.asc()).all()
elif sorting_query == "desc":
tasks = Task.query.order_by(Task.title.desc()).all()
else:
tasks = Task.query.all()
Comment on lines +56 to +61

Choose a reason for hiding this comment

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

Great work on making the database do the heavy lifting when it comes to sorting the tasks!


tasks_response = []
for task in tasks:
tasks_response.append(task.to_dict())

return jsonify(tasks_response), 200


# read one task
@task_bp.route("/<task_id>", methods=["GET"])
def read_one_task(task_id):
task = validate_model(Task, task_id)
if task.goal_id:
return {
"task":{
"id": task.task_id,
"title": task.title,
"description": task.description,
"goal_id": task.goal_id,
"is_complete": (task.completed_at != None)
}
}
else:

Choose a reason for hiding this comment

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

Another way you could implement this by changing your to_dict method for Task to include a goal_id if one is present.

return {
"task":{
"id": task.task_id,
"title": task.title,
"description": task.description,
"is_complete": (task.completed_at != None)
}
}


# if task.goal_id != None:
# result["task"]["goal_id"] = task.goal_id
# return result, 200


# update a task
@task_bp.route("/<task_id>", methods=["PUT"])
def update_task(task_id):
task_to_update = validate_model(Task, task_id)

request_body = request.get_json()

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

db.session.commit()

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


# patch request to mark complete on incompleted task
@task_bp.route("/<task_id>/mark_complete", methods=["PATCH"])
def update_task_to_completed(task_id):
task = validate_model(Task, task_id)

if task.completed_at is None:
task.completed_at = datetime.utcnow()

slack_message = f"Someone just completed the task {task.title}"
# add header
header = {"Authorization": f"Bearer {SLACK_BOT_TOKEN}"}
# add response
json_body = {
"channel": "C057EA9H4G7",
"text": slack_message
}
response = requests.post("https://slack.com/api/chat.postMessage", headers=header, json=json_body)

Choose a reason for hiding this comment

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

Our code could possibly fail while trying to commit the change to our database and therefore sending the message to Slack could be a false positive. We should send alerts like these at the very end of our logic just in case something goes wrong.


# if the status code from response does not indicate successful action
# if response.status_code != 200:
# # send failure message
# abort(make_response({"details": "Failed to send message to Slack"}, 500))

db.session.commit()

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

@task_bp.route("/<task_id>/mark_incomplete", methods=["PATCH"])
def update_task_incomplete(task_id):
task = validate_model(Task, task_id)

if task.completed_at != None:
task.completed_at = None

db.session.commit()

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

# delete a task
@task_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 abort(make_response({"details":f"Task {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, 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.
Loading