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

Zoisite - Sabs Ford #123

Open
wants to merge 8 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
10 changes: 8 additions & 2 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,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")

Choose a reason for hiding this comment

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

Great job with configuration here!

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

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

from .routes import goals_bp
app.register_blueprint(goals_bp)

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

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


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

def make_dict(self):
return dict(
id = self.goal_id,
title = self.title)

Choose a reason for hiding this comment

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

This looks good!

30 changes: 29 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,32 @@


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, default=None)

Choose a reason for hiding this comment

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

The default for nullable is True, so we do not need to explicitly set the attribute to True here!

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 make_dict(self):
if self.goal_id:
return dict(
id = self.task_id,
goal_id = self.goal_id,
title = self.title,
description = self.description,
is_complete = bool(self.completed_at))
else:
return dict(
id = self.task_id,
title = self.title,
description = self.description,
is_complete = bool(self.completed_at))


213 changes: 212 additions & 1 deletion app/routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,212 @@
from flask import Blueprint
from app import db
from app.models.task import Task
from app.models.goal import Goal
from sqlalchemy import asc, desc
from flask import Blueprint, jsonify, make_response, abort, request
import requests
from datetime import date
import os
from dotenv import load_dotenv

load_dotenv()

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

Choose a reason for hiding this comment

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

Overall, it's ok to have all of our routes here in one file because this is a pretty small project, but as you start building more and larger scale APIs, it's a good idea to separate your routes into their own files. For example, you could have a task_routes.py and a goal_routes.py as well. If you wanted to take it a step further, you can also have a helper_functions.py file that could be used to house any helper functions that apply to all routes (validate_model for example).


def validate_task(task_id):
try:
task_id = int(task_id)
except:
message = f"task {task_id} not found"
abort(make_response({"message": message}, 400))

task = Task.query.get(task_id)

if not task:
abort(make_response({"message":f"task {task_id} not found"}, 404))
return task

def validate_goal(goal_id):
try:
goal_id = int(goal_id)
except:
message = f"task {goal_id} not found"
abort(make_response({"message": message}, 400))

goal = Goal.query.get(goal_id)

if not goal:
abort(make_response({"message":f"goal {goal_id} not found"}, 404))
return goal

Choose a reason for hiding this comment

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

This is a great start! As you refactor, make note of any places where you have code that looks really similar. For example, your validate_task and your validate_goal methods are so similar. You could refactor them into a single validate_model that looks something like this:

 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



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

if "description" not in request_body or "title" not in request_body:
abort(make_response({"details": "Invalid data"}, 400))

Choose a reason for hiding this comment

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

While this exception handling can work, a more appropriate structure would be to use a try/catch block here. This allows you to catch a variety of errors as opposed to having to specify each thing that might be an issue. For example, we could replace the conditional here with:

 try:
      new_task = Task.from_dict(request_body)
 except KeyError as error:
      abort(make_response({"details": "Invalid data"}, 400))

This will attempt to make the new_task and if for whatever reason it can't, it will throw the error. While this functions the same way as your conditional, it's just a little more streamlined and easy to read!

new_task = Task.from_dict(request_body)

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

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

@tasks_bp.route("", methods =["GET"])
def get_tasks_data():
sort_query = request.args.get("sort")

if sort_query == "asc":
tasks = Task.query.order_by(asc(Task.title))
elif sort_query == "desc":
tasks = Task.query.order_by(desc(Task.title))
else:
tasks = Task.query.all()

tasks_response = []
for task in tasks:
tasks_response.append(task.make_dict())
return jsonify(tasks_response), 200


@tasks_bp.route("/<task_id>", methods =["GET"])
def get_task_data(task_id):
task = validate_task(task_id)
return make_response({"task": task.make_dict()}, 200)

@tasks_bp.route("/<task_id>", methods =["PUT"])
def update_task(task_id):
task = validate_task(task_id)
request_body = request.get_json()

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

db.session.commit()

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

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

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

message = {f"details": 'Task 1 "Go on my daily walk 🏞" successfully deleted'}
return make_response(jsonify(message)), 200

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

Choose a reason for hiding this comment

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

Your patch methods and really any method where you plan on including any information in the request body is a good candidate for error handling! This allows you to make sure the user is attaching the correct information to the route's request body!


db.session.commit()
notify_slack(task)

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

def notify_slack(task):
url = "https://slack.com/api/chat.postMessage"
body = {
"channel": "task-notification",
"text": f"Someone just completed the task {task.title}"
}
header = {"Authorization": os.environ.get('SLACK_TOKEN'),
"Content-Type": "application/json"}

requests.post(url, json=body, headers=header)

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

task = validate_task(task_id)

task.completed_at = None

db.session.commit()

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


@goals_bp.route("", methods =["POST"])
def create_goals():
request_body = request.get_json()

if "title" not in request_body:
abort(make_response({"details": "Invalid data"}, 400))

Choose a reason for hiding this comment

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

Same as before. Let's look at using a try/except block here for the error handling!

new_goal = Goal.from_dict(request_body)

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


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

@goals_bp.route("", methods =["GET"])
def get_goals_data():
goals = Goal.query.all()

goals_response = []
for goal in goals:
goals_response.append(goal.make_dict())
return jsonify(goals_response), 200

@goals_bp.route("/<goal_id>", methods =["GET"])
def get_goal_data(goal_id):
goal = validate_goal(goal_id)
return make_response({"goal": goal.make_dict()}, 200)


@goals_bp.route("/<goal_id>", methods =["PUT"])
def update_goal(goal_id):
goal = validate_goal(goal_id)
request_body = request.get_json()

goal.title = request_body["title"]

db.session.commit()

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

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

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

message = {f"details": 'Goal 1 \"Build a habit of going outside daily\" successfully deleted'}
return make_response(jsonify(message)), 200


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

for task_id in request_body["task_ids"]:
task = validate_task(task_id)
goal.tasks.append(task)

db.session.commit()

return make_response(jsonify(id=goal.goal_id, task_ids=request_body["task_ids"]))

@goals_bp.route("/<goal_id>/tasks", methods =["GET"])
def task_of_one_goal(goal_id):
goal = validate_goal(goal_id)
response = {"id": goal.goal_id,
"title": goal.title,
"tasks": []}

for task in goal.tasks:
response["tasks"].append(task.make_dict())

return make_response(response)



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