Skip to content

Amethyst-PhalesaPatton- #122

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

Open
wants to merge 13 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
9 changes: 7 additions & 2 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from dotenv import load_dotenv




db = SQLAlchemy()
migrate = Migrate()
load_dotenv()
Expand All @@ -15,8 +17,7 @@ def create_app(test_config=None):
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

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

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

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

class Goal(db.Model):
goal_id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String, nullable=False)
tasks = db.relationship("Task", back_populates="goal")


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

return new_goal

def to_dict_goals(self):
return {
"id": self.goal_id,
"title": self.title
}
35 changes: 34 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,37 @@


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, nullable=False)
description = db.Column(db.String, nullable=False)
# is_complete = db.Column(db.Boolean, default=False)
completed_at = db.Column(db.DateTime, default=None)
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 to_dict(self):
return {
"id": self.task_id,
"title": self.title,
"description": self.description,
"is_complete": bool(self.completed_at)
}

def to_dict_with_goal(self):
return {
"id": self.task_id,
"goal_id":self.goal_id,
"title": self.title,
"description": self.description,
"is_complete": bool(self.completed_at)

Choose a reason for hiding this comment

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

Love how you used the boolean constructor here!

}
275 changes: 274 additions & 1 deletion app/routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,274 @@
from flask import Blueprint
from os import abort
from app import db
from app.models.task import Task
from flask import Blueprint, jsonify, abort, make_response, request
from sqlalchemy import asc, desc
from datetime import datetime
# from slack_sdk import WebClient
# from slack_sdk.errors import SlackApiError
import os
import requests
from app.models.goal import Goal
import json




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

def validate_model(cls, model_id):
try:
model_id = int(model_id)
except:
abort(make_response({"message":f"task {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


@tasks_bp.route("", methods=["POST"])
def create_task():
request_body = request.get_json()
try:
new_task = Task.from_dict(request_body)

except:
Comment on lines +37 to +40

Choose a reason for hiding this comment

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

Love how you implemented a try/except block, here! 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.

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

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


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

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

def read_all_tasks():
title_query = request.args.get("title")
sort_filter = request.args.get("sort")

if title_query:
tasks = Task.query.filter_by(title=title_query)
else:
tasks = Task.query.all()

if sort_filter:
if sort_filter == "asc":
tasks = Task.query.order_by(Task.title.asc())
elif sort_filter == "desc":
tasks = Task.query.order_by(Task.title.desc())
else:
tasks = Task.query.all()
Comment on lines +60 to +66

Choose a reason for hiding this comment

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

Love this implementation! You did well with making the database do the heavy lifting when it came to sorting the tasks!


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

return jsonify(tasks_response)

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

def read_one_task(task_id):
task = validate_model(Task, task_id)
if task.goal_id:

Choose a reason for hiding this comment

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

⭐️

return {"task": task.to_dict_with_goal()}
else:
return {"task": task.to_dict()}

@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 +89 to +90

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 keys title or description 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 {"task": task.to_dict()}

@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'})

def slack_notification(task):
token = os.environ.get("slack_token")
slack_url = "https://slack.com/api/chat.postMessage"
headers = {"Authorization":token}
body = {
"channel": "task-notifications",
"text": f"Someone just completed the task {task.title}",
}

requests.post(slack_url, headers=headers, json=body)
return
Comment on lines +106 to +116

Choose a reason for hiding this comment

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

Love how you made this into a helper function! Make sure you are putting them a place that is easy to find, I typically put mine a separate folder or at the top of the file.



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

task = validate_model(Task, task_id)

task.completed_at = datetime.utcnow()



#use requests instead of WebClient and add bearer... add token to .env look up requests.post
# client = WebClient(token=os.environ.get("slack_token"))
# client.chat_postMessage(channel="C0570RZGHDL", text=f"Someone just completed the task {task.title}")










db.session.commit()

slack_notification(task)

Choose a reason for hiding this comment

The 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!



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



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

task = validate_model(Task, task_id)

if task.completed_at:
task.completed_at = None


db.session.commit()

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


@goals_bp.route("", methods=["POST"])
def create_goal():
request_body = request.get_json()
try:
new_goal = Goal.from_dict_goals(request_body)

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

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


return make_response(jsonify({"goal": new_goal.to_dict_goals()}),201)

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

def read_all_goals():
title_query = request.args.get("title")
sort_filter = request.args.get("sort")

if title_query:
goals = Goal.query.filter_by(title=title_query)
else:
goals = Goal.query.all()

if sort_filter:
if sort_filter == "asc":
goals = Goal.query.order_by(Goal.title.asc())
elif sort_filter == "desc":
goals = Goal.query.order_by(Goal.title.desc())
else:
goals = Goal.query.all()

goals_response = []
for goal in goals:
goals_response.append(goal.to_dict_goals())

return jsonify(goals_response)

Choose a reason for hiding this comment

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

💫


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

def read_one_goal(goal_id):
goal = validate_model(Goal, goal_id)
return {"goal": goal.to_dict_goals()}

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


db.session.commit()

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

@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'})

@goals_bp.route("/<goal_id>/tasks", methods=["POST"])
def create_goal_by_id(goal_id):
goal = validate_model(Goal, goal_id)
request_body = request.get_json()

for task in request_body["task_ids"]:
task = validate_model(Task, task)
goal.tasks.append(task)

db.session.commit()
return {
"id": goal.goal_id,
"task_ids": request_body["task_ids"]
}


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

# return {"goal": goal.to_dict_goals_tasks()}

goals_tasks = {
"id": goal.goal_id,
"title": goal.title,
"tasks": []

}

for task in goal.tasks:
goals_tasks["tasks"].append(task.to_dict_with_goal
())

return jsonify(goals_tasks), 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 Phalesa, 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