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 - Megan G #112

Open
wants to merge 49 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
058b9b3
created Task model
scrambledmegs May 9, 2023
c14aa16
adds autoincrement to task_id
scrambledmegs May 9, 2023
10c8536
task model migration
scrambledmegs May 9, 2023
015f3f5
created and registered task blueprint
scrambledmegs May 9, 2023
5c92162
adds read task route
scrambledmegs May 10, 2023
259ce1e
adds create task route
scrambledmegs May 10, 2023
8ece6ac
adds read one saved task
scrambledmegs May 10, 2023
33c7dd4
adds update task route
scrambledmegs May 10, 2023
6e3acaf
adds delete task route
scrambledmegs May 10, 2023
7666b42
adds validate_tasks function to routes
scrambledmegs May 10, 2023
53e7f4f
updated validate_task_id function
scrambledmegs May 10, 2023
586c405
renamed validate_task function and adds validation in POST route
scrambledmegs May 10, 2023
b762abe
adds assertions to tests
scrambledmegs May 10, 2023
18e61a3
deletes app/routes.py
scrambledmegs May 10, 2023
f51a2ac
adds query param sort asc and desc by title
scrambledmegs May 11, 2023
1ddaf52
adds mark task complete
scrambledmegs May 11, 2023
db10823
adds mark incomplete task
scrambledmegs May 11, 2023
07b3a81
adds assertions to test wave 3
scrambledmegs May 11, 2023
986b5cd
uncomments tests
scrambledmegs May 11, 2023
d509d0b
adds request to slack api when task marked complete
scrambledmegs May 11, 2023
dcf7a2c
creates Goal model
scrambledmegs May 11, 2023
7f07064
updates with Goal model
scrambledmegs May 11, 2023
342c5af
adds create goal route
scrambledmegs May 11, 2023
26090f7
adds read goals route
scrambledmegs May 11, 2023
756bb54
adds read one saved goal
scrambledmegs May 11, 2023
8c42aab
adds validate_goal function
scrambledmegs May 11, 2023
315a44a
adds update goal route
scrambledmegs May 11, 2023
219e8a1
adds delete goal route
scrambledmegs May 11, 2023
530e082
created and registered goal blueprint
scrambledmegs May 11, 2023
4a023ec
adds testing functionality test wave 5
scrambledmegs May 11, 2023
f2d010c
creates one to many relationship Task model
scrambledmegs May 11, 2023
bf06968
creates one to many relationship Goal model
scrambledmegs May 11, 2023
d0cdaa9
updated migration for one to many relationship db
scrambledmegs May 11, 2023
7d310b4
updated goal model
scrambledmegs May 12, 2023
0c39ebb
updated task model
scrambledmegs May 12, 2023
fb78260
adds task to goal route
scrambledmegs May 12, 2023
0250f48
adds assertion test wave 6
scrambledmegs May 12, 2023
96633d9
updated comments
scrambledmegs May 12, 2023
9e5c1d6
adds get tasks of one goal route
scrambledmegs May 12, 2023
17fdc66
adds validate_model helper function
scrambledmegs May 12, 2023
6377c0a
deletes validate_goal function and updates routes with validate_model…
scrambledmegs May 12, 2023
24c223a
refactored goal routes
scrambledmegs May 12, 2023
d808524
adds two class methods
scrambledmegs May 12, 2023
6130e9d
adds two class methods
scrambledmegs May 12, 2023
15d02ec
refactored task routes
scrambledmegs May 12, 2023
82f1005
connecting render db
scrambledmegs May 15, 2023
cd04b45
adds flask_cors
scrambledmegs Jun 20, 2023
8cd7817
update
scrambledmegs Jun 20, 2023
2f70169
update requirements
scrambledmegs Jun 20, 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
12 changes: 9 additions & 3 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
import os
from dotenv import load_dotenv
import os



db = SQLAlchemy()
Expand All @@ -15,8 +16,9 @@ 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(
# "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 +32,9 @@ 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
106 changes: 106 additions & 0 deletions app/goal_routes.py
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
Comment on lines +36 to +38

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


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

Choose a reason for hiding this comment

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

Choose a reason for hiding this comment

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

Refer to my comment about error handling, on line 62 in task_routes.py


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
)
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):

return {
"id": self.goal_id,
"title": self.title,
}

@classmethod
def from_dict(cls, goal_data):

Choose a reason for hiding this comment

The 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
34 changes: 33 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Choose a reason for hiding this comment

The 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

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

This file was deleted.

134 changes: 134 additions & 0 deletions app/task_routes.py
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"):

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.

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

Choose a reason for hiding this comment

The 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

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

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!

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

Choose a reason for hiding this comment

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

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.
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