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

Whales - Gaby Webb #110

Open
wants to merge 16 commits into
base: master
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
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: gunicorn 'app:create_app()'
9 changes: 6 additions & 3 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
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()
migrate = Migrate()
Expand All @@ -30,5 +29,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
12 changes: 11 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,14 @@


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', backref='goal', lazy=True)

def create_goal_dict(self):

Choose a reason for hiding this comment

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

Style: Since this is a method on the Goal class having goal in the name is redundant:

Suggested change
def create_goal_dict(self):
def create_dict(self):

goal_dict = {
"id": self.goal_id,
"title": self.title,
}
return goal_dict
Comment on lines +10 to +14

Choose a reason for hiding this comment

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

Since you immediately return this you don't need to assign it to a variable:

Suggested change
goal_dict = {
"id": self.goal_id,
"title": self.title,
}
return goal_dict
return {
"id": self.goal_id,
"title": self.title,
}


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


class Task(db.Model):
task_id = db.Column(db.Integer, primary_key=True)
task_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
goal_id = db.Column(db.Integer, db.ForeignKey('goal.goal_id'))
title = db.Column(db.String)
description = db.Column(db.String)
completed_at = db.Column(db.DateTime)

def create_task_dict(self):
task_dict = {
"id": self.task_id,
"title": self.title,
"description": self.description
}
if self.completed_at is None:
task_dict["is_complete"] = False
else:
task_dict["is_complete"] = True
Comment on lines +11 to +19

Choose a reason for hiding this comment

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

This can be simplified to:

Suggested change
task_dict = {
"id": self.task_id,
"title": self.title,
"description": self.description
}
if self.completed_at is None:
task_dict["is_complete"] = False
else:
task_dict["is_complete"] = True
task_dict = {
"id": self.task_id,
"title": self.title,
"description": self.description,
"is_complete": bool(self.completed_at)
}


if self.goal_id:
task_dict["goal_id"] = self.goal_id

return task_dict

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

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

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

if "title" not in request_body or "description" not in request_body:
return {"details": "Invalid data"}, 400

if "completed_at" in request_body:
new_task = Task(title=request_body["title"],
description=request_body["description"],
completed_at=request_body["completed_at"])
else:
new_task = Task(title=request_body["title"],
description=request_body["description"])

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

response = {
"task": new_task.create_task_dict()
}
return response, 201

@tasks_bp.route("", methods=["GET"])
def get_all_tasks():
title_sort_query = request.args.get("sort")
if title_sort_query == "asc":
tasks = Task.query.order_by(Task.title.asc())
elif title_sort_query == "desc":
tasks = Task.query.order_by(Task.title.desc())
else:
tasks = Task.query.all()

response = []
for task in tasks:
response.append(task.create_task_dict())

return jsonify(response)

@tasks_bp.route("/<task_id>", methods=["GET"])
def get_one_task(task_id):
task = get_one_task_or_abort(task_id)
response = {
"task": task.create_task_dict()
}
return response

def get_one_task_or_abort(task_id):

Choose a reason for hiding this comment

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

I like this helper function. It's really clearly named and clean. 😃

(You even have great error messages!)

try:
task_id = int(task_id)
except ValueError:
response = {"msg":f"Invalid task id: {task_id}. ID must be an integer."}
abort(make_response(jsonify(response), 400))

requested_task = Task.query.get(task_id)
if requested_task is None:
response = {"msg":f"Could not find task with id: {task_id}"}
abort(make_response(jsonify(response), 404))

return requested_task

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

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

db.session.commit()
response = {
"task": task.create_task_dict()
}
return response

@tasks_bp.route("/<task_id>", methods=["DELETE"])
def delete_task(task_id):
task = get_one_task_or_abort(task_id)
db.session.delete(task)
db.session.commit()
response = {"details": f'Task {task.task_id} "{task.title}" successfully deleted'}
return response

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

db.session.commit()
response = {
"task": task.create_task_dict()
}

path = "https://slack.com/api/chat.postMessage"
data = {
"channel": "task-notifications",
"text": f"Someone just completed the task {task.title}"
}
headers = {
"authorization": "Bearer " + os.environ.get("SLACKBOT_API_KEY")
}

post_message = requests.post(path, data=data, headers=headers)

Choose a reason for hiding this comment

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

Style: Since you don't do anything with this variable it's a best practice not to store it:

Suggested change
post_message = requests.post(path, data=data, headers=headers)
requests.post(path, data=data, headers=headers)


return response

@tasks_bp.route("/<task_id>/mark_incomplete", methods=["PATCH"])
def mark_task_incomplete(task_id):
task = get_one_task_or_abort(task_id)
task.completed_at = None
db.session.commit()
response = {
"task": task.create_task_dict()
}
return response

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

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

if "title" not in request_body:
return {"details": "Invalid data"}, 400

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

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

response = {
"goal": new_goal.create_goal_dict()
}
return response, 201

@goals_bp.route("", methods=["GET"])
def get_all_goals():
goals = Goal.query.all()
response = []
for goal in goals:
response.append(
goal.create_goal_dict()
)

return jsonify(response)

@goals_bp.route("/<goal_id>", methods=["GET"])
def get_one_goal(goal_id):
goal = get_one_goal_or_abort(goal_id)
response = {
"goal": goal.create_goal_dict()
}
return response

def get_one_goal_or_abort(goal_id):
try:
goal_id = int(goal_id)
except ValueError:
response = {"msg":f"Invalid goal id: {goal_id}. ID must be an integer."}
abort(make_response(jsonify(response), 400))

requested_goal = Goal.query.get(goal_id)
if not requested_goal:
response = {"msg":f"Could not find goal with id: {goal_id}"}
abort(make_response(jsonify(response), 404))

return requested_goal

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

goal.title = request_body["title"]
db.session.commit()
response = {
"goal": goal.create_goal_dict()
}
return response

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

db.session.delete(goal)
db.session.commit()
response = {"details": f'Goal {goal.goal_id} "{goal.title}" successfully deleted'}

return response

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

try:
task_ids = request_body["task_ids"]
except KeyError:
return {"msg": "Missing task_ids in request body"}, 400

if not isinstance(task_ids, list):
return {"msg": "Expected list of task ids"}, 400

tasks = []
for id in task_ids:
tasks.append(get_one_task_or_abort(id))

for task in tasks:
task.goal = goal

db.session.commit()

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

@goals_bp.route("/<goal_id>/tasks", methods=["GET"])
def get_tasks_assoc_goal(goal_id):
goal = get_one_goal_or_abort(goal_id)
request_body = request.get_json()

goal_tasks = []
for task in goal.tasks:
goal_tasks.append(task)

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

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

return 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