Skip to content

Whales - Alexa Coffman #107

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 31 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
44f1def
created Task model, initialized test and development dbs
AlexaCoffman May 5, 2022
0cbb311
registered blueprints, added create_task POST route:
AlexaCoffman May 5, 2022
e7feabb
created get_tasks route for all tasks
AlexaCoffman May 6, 2022
8de8960
added get_one_task route, refactored validate_task
AlexaCoffman May 6, 2022
4150fb8
refactored validate_task AGAIN, refactored get_one_task
AlexaCoffman May 6, 2022
c73290a
created update_task route
AlexaCoffman May 6, 2022
1d027f2
updated create_task to validata description and title are in request …
AlexaCoffman May 6, 2022
0e9fabf
created format_response helper function to display task details consi…
AlexaCoffman May 6, 2022
03f2610
added sort param to get_tasks
AlexaCoffman May 7, 2022
185595b
created check_if_task_complete function
AlexaCoffman May 7, 2022
f2d05de
added mark_complete route
AlexaCoffman May 7, 2022
278593c
created mark_complete and mark_incomplete routes
AlexaCoffman May 7, 2022
ee148b3
added option to mark task complete when created or updates
AlexaCoffman May 7, 2022
190b1d1
added option to mark task complete when created or updated
AlexaCoffman May 7, 2022
c709e41
refactored to check boolean value of completed_at, removed redundant …
AlexaCoffman May 7, 2022
c93bcac
removed asc order_by conditional as it is asc by default
AlexaCoffman May 10, 2022
0cb2473
added push_complete_task_to_slack for when a task is marked complete
AlexaCoffman May 10, 2022
418921f
created goal model, added create_goal and get_goals routes
AlexaCoffman May 10, 2022
2b4fef2
added get_one_goal route, validate_goal helper
AlexaCoffman May 11, 2022
623514d
added update_goal route, completed related tests
AlexaCoffman May 11, 2022
0ca6a41
created delete_goal route
AlexaCoffman May 12, 2022
f9b0fea
created model between task and goal
AlexaCoffman May 12, 2022
7d16549
created assign_task_to_goal route
AlexaCoffman May 12, 2022
5ce2e7d
updated get_tasks_of_goal to account for missing goal, added asserts …
AlexaCoffman May 12, 2022
27a54a3
separated goal and task route files
AlexaCoffman May 12, 2022
ecdb5ac
removed unused import
AlexaCoffman May 12, 2022
8cbfd02
updated requirements
AlexaCoffman May 13, 2022
9229915
added Procfile for Heroku
AlexaCoffman May 13, 2022
dfa21d1
fixed migrations
AlexaCoffman May 13, 2022
c7f0225
updated migrations
AlexaCoffman May 13, 2022
511cf14
fixed migrations AGAIN
AlexaCoffman May 13, 2022
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: 7 additions & 2 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import os
from dotenv import load_dotenv


db = SQLAlchemy()
migrate = Migrate()
load_dotenv()
Expand All @@ -29,6 +28,12 @@ def create_app(test_config=None):
db.init_app(app)
migrate.init_app(app, db)

# Register Blueprints here
# Register blueprints

from .task_routes import tasks_bp
app.register_blueprint(tasks_bp)

from .goal_routes import goals_bp
app.register_blueprint(goals_bp)

return app
130 changes: 130 additions & 0 deletions app/goal_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
from flask import Blueprint, jsonify, request
from app import db
from app.models.goal import Goal
from sqlalchemy import asc
from app.task_routes import validate_task

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

def validate_goal(goal_id):
try:
goal_id = int(goal_id)
except ValueError:
return jsonify({"msg" : f"'{goal_id}' is invalid"}), 400

Choose a reason for hiding this comment

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

Nice job including the invalid goal_id! 😃


goal = Goal.query.get(goal_id)
if not goal:
return jsonify({"message" : f"Could not find '{goal_id}'"}), 404
return goal

def format_goal(goal):
return {
"goal": {
"id" : goal.goal_id,
"title" : goal.title
}
}

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

return format_goal(new_goal), 201

@goals_bp.route('', methods=['GET'])
def get_goals():
goals = Goal.query.order_by(asc(Goal.title)).all()
goals_response = []

for goal in goals:
goals_response.append(
{
"id" : goal.goal_id,
"title" : goal.title
}
)
return jsonify(goals_response), 200

@goals_bp.route('/<goal_id>', methods=['GET'])
def get_one_goal(goal_id):
goal = validate_goal(goal_id)

if isinstance(goal, Goal):
return format_goal(goal), 200
return goal

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

if isinstance(goal, Goal):
goal.title = request_body["title"]
db.session.commit()

return format_goal(goal), 200
return goal

@goals_bp.route('/<goal_id>', methods=['DELETE'])
def delete_goal(goal_id):
goal = validate_goal(goal_id)
if isinstance(goal, Goal):
db.session.delete(goal)
db.session.commit()
return {
"details" : f'Goal {goal_id} "{goal.title}" successfully deleted'
}
return goal

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

if isinstance(goal, Goal):
tasks = []
for task_id in request_body['task_ids']:
task = validate_task(task_id)
task.goal_id = goal_id
tasks.append(task.task_id)
db.session.commit()

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

@goals_bp.route('/<goal_id>/tasks', methods=['GET'])
def get_tasks_of_goal(goal_id):
goal = validate_goal(goal_id)

if isinstance(goal, Goal):
tasks = []
for task in goal.tasks:
tasks.append({
"id" : task.task_id,
"goal_id" : task.goal_id,
"title" : task.title,
"description" : task.description,
"is_complete" : bool(task.completed_at)
})

return {
"id" : goal.goal_id,
"title" : goal.title,
"tasks" : tasks
}
return goal
2 changes: 2 additions & 0 deletions app/models/goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@

class Goal(db.Model):
goal_id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String)
tasks = db.relationship("Task", back_populates="goal", lazy=True)
7 changes: 6 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,9 @@


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")
1 change: 0 additions & 1 deletion app/routes.py

This file was deleted.

148 changes: 148 additions & 0 deletions app/task_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
from flask import Blueprint, jsonify, request
from app import db
from app.models.task import Task
from sqlalchemy import desc, asc
from datetime import datetime
import requests
import os

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


def validate_task(task_id):
try:
task_id = int(task_id)
except ValueError:
return jsonify({"msg" : f"'{task_id}' is invalid"}), 400

task = Task.query.get(task_id)
if not task:
return jsonify({"message" : f"Could not find '{task_id}'"}), 404
return task

def format_task(task):
return {
"task" :
{
"is_complete" : bool(task.completed_at),
"description" : task.description,
"title" : task.title,
"id" : task.task_id,
"goal_id" : task.goal_id
}
}

def push_complete_to_slack(task):
bot_token = os.environ.get("SLACK_BOT_TOKEN")
params = {
"channel" : "task-notifications",
"text" : f"Someone just completed the task {task.title}"
}
headers = {"authorization" : "Bearer " + bot_token}

Choose a reason for hiding this comment

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

The reason your tests fail on Learn is because you try to use the token without first verifying that it is not None.

A check at the beginning of this that does an early return if there's no token is probably the correct way to solve this (since if Slack/not configured is down you'd like your API to still work).

requests.post("https://slack.com/api/chat.postMessage", params=params, headers=headers)

@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:
return {
"details" : "Invalid data"
}, 400

new_task = Task(
description = request_body['description'],
title = request_body['title']
)

if "completed_at" in request_body:
new_task.completed_at = request_body['completed_at']

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

return format_task(new_task), 201

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

tasks_response = []

for task in tasks:
tasks_response.append(
{
"is_complete" : bool(task.completed_at),
"description" : task.description,
"title" : task.title,
"id" : task.task_id
}
)

return jsonify(tasks_response), 200

@tasks_bp.route('/<task_id>', methods=['GET'])
def get_one_task(task_id):
task = validate_task(task_id)

if isinstance(task, Task):
return format_task(task), 200
return task

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

if isinstance(task, Task):
task.title = request_body["title"]
task.description = request_body["description"]
db.session.commit()

if "completed_at" in request_body:
task.completed_at = request_body['completed_at']

return format_task(task), 200
return task

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

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

if isinstance(task, Task):

task.completed_at = datetime.utcnow()
db.session.commit()
push_complete_to_slack(task)
return format_task(task), 200
return task

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

if isinstance(task, Task):
task.completed_at = None
db.session.commit()
return format_task(task), 200
return task





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