Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ef1dd60
adds Task model
sundbean May 6, 2021
b724489
creates task_bp and registers blueprint
sundbean May 6, 2021
b7a3f5a
creates /tasks endpoint that accepts POST method
sundbean May 6, 2021
0b7d59e
extends /tasks endpoint to accommodate GET method
sundbean May 6, 2021
07fd376
creates /tasks/<task_id> endpoint that accommodates GET method
sundbean May 6, 2021
91931bd
creates a 404 response if no matching task at /tasks/<task_id> endpoint
sundbean May 6, 2021
e6a3816
extends /tasks/<task_id> to accommodate PUT method
sundbean May 6, 2021
a7b311e
extends /tasks/<task_id> endpoint to accommodate DELETE method
sundbean May 6, 2021
fbc87ef
returns 400 response if POST to /tasks endpoint contains missing data
sundbean May 6, 2021
1ef0ca6
fixes typo to pass all wave 1 tests
sundbean May 6, 2021
b0449e4
accommodates sort query param for GET method to /tasks endpoint
sundbean May 6, 2021
69ce6de
adds routes for mark_incomplete and mark_complete and updates depende…
sundbean May 6, 2021
479ba43
refactors /tasks/<task_id>/mark_complete endpoint to send message to …
sundbean May 6, 2021
f0cdea2
refactors send_message_to_slack() function to use the requests packag…
sundbean May 6, 2021
d45f4c3
creates Goal model, goals blueprint, and registers blueprint in __ini…
sundbean May 6, 2021
1c515b5
creates /goals endpoint that accommodates POST method
sundbean May 6, 2021
a8d04e1
extends /goals endpoint to accommodate GET method, makes POST method …
sundbean May 6, 2021
e0570bc
adds /goals/<goal_id> endpoint and accommodates GET method
sundbean May 6, 2021
18cc1ea
creates a 404 response at /<goal_id> endpoint if none found
sundbean May 6, 2021
0d5945a
extends /<goal_id> endpoint to accommodate PUT method
sundbean May 6, 2021
30126f1
extends /<goal_id> endpoint to accommodate DELETE method
sundbean May 6, 2021
42e7774
creates a try/except to handle invlaid data in POST method to /goals …
sundbean May 6, 2021
e02c18b
edits Task and Goal models to create one-to-many relationship
sundbean May 6, 2021
db980dc
adds /<goal_id>/tasks endpoint that accommodates GET and POST methods…
sundbean May 7, 2021
fc3efd6
refactors /tasks/<task_id> endpoint to make goal_id presence conditio…
sundbean May 7, 2021
84bc061
adds is_task_complete() and get_task_info() methods to Task model, re…
sundbean May 7, 2021
784773f
removes unnecessary import from routes.py
sundbean May 7, 2021
8e5404d
deletes an unnecessary import from goal.py
sundbean May 10, 2021
be930e8
adds Procfile for Heroku deployment
sundbean May 10, 2021
3ef4d7d
reorganizes routes.py to separate each HTTP method into its own route…
sundbean May 11, 2021
47c647d
adds from_json() method to Task model and updates PUT method to /task…
sundbean May 13, 2021
e34eb52
adds a to_json() method to Goal model, updates routes.py to utilize m…
sundbean May 13, 2021
d5ccb01
refactors POST /goals route to utilize .to_json() method of Goal model
sundbean May 13, 2021
7685c3a
refactors /<goal_id>/tasks endpoint GET method to utilize Goal's to_j…
sundbean May 13, 2021
96632bb
refactors routes.py to use list comprehension
sundbean May 13, 2021
dd3ad40
adds option to sort tasks by id and adds tests for these additions
sundbean May 13, 2021
bca49fc
adds option to filter tasks by title and adds test for this option
sundbean May 13, 2021
1921b7a
adds query param options to /goals endpoint, adds corresponding tests…
sundbean May 13, 2021
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()'
3 changes: 3 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,8 @@ def create_app(test_config=None):
migrate.init_app(app, db)

# Register Blueprints here
from .routes import tasks_bp, 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
@@ -1,6 +1,16 @@
from flask import current_app
from flask import Flask, current_app
from flask_sqlalchemy import SQLAlchemy
from app import db
from app.models.task import Task # is this necessary?


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)

def to_json(self):

Choose a reason for hiding this comment

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

Great helper method!

return {
"id": self.goal_id,
"title": self.title
}
32 changes: 29 additions & 3 deletions app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
from flask import current_app
from flask import Flask, current_app
from flask_sqlalchemy import SQLAlchemy
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)
title = db.Column(db.String)
description = db.Column(db.String)
completed_at = db.Column(db.DateTime)
goal_id = db.Column(db.Integer, db.ForeignKey('goal.goal_id'), nullable=True)

def is_task_complete(self):
if not self.completed_at:
return False
return True

def get_task_info(self):
Comment on lines +12 to +17

Choose a reason for hiding this comment

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

Great helper methods

task_info = {
"id": self.task_id,
"title": self.title,
"description": self.description,
"is_complete": self.is_task_complete(),
}
if self.goal_id:
task_info['goal_id'] = self.goal_id
return task_info

def from_json(self, request_body):

Choose a reason for hiding this comment

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

Nice!

self.title = request_body['title']
self.description = request_body['description']
self.completed_at = request_body['completed_at']
return self
281 changes: 280 additions & 1 deletion app/routes.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,281 @@
from flask import Blueprint
from app import db
from app.models.task import Task
from app.models.goal import Goal
from flask import Blueprint, make_response, request, jsonify
from sqlalchemy import desc
from datetime import datetime
import requests
import os

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

##########################
#### /tasks Endpoints ####
##########################

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

try:
new_task = Task(title=request_body['title'],
description=request_body['description'],
completed_at=request_body['completed_at'])
except KeyError:
return make_response({
"details": "Invalid data"
}, 400)

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

response = {
"task": new_task.get_task_info()
}

return make_response(jsonify(response), 201)


@tasks_bp.route("", methods=["GET"])
def get_tasks():
sort_query = request.args.get("sort")
sort_by_id_query = request.args.get("sort_by_id")
filter_by_query = request.args.get("filter_by_title")
Comment on lines +42 to +44

Choose a reason for hiding this comment

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

Doing the optionals! 😀


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

return jsonify([task.get_task_info() for task in tasks])

Choose a reason for hiding this comment

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

Nice list comprehension



@tasks_bp.route("/<task_id>", methods=["GET"])
def get_single_task(task_id):
task = Task.query.get(task_id)
if task is None:
return make_response("", 404)

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


@tasks_bp.route("/<task_id>", methods=["PUT"])
def edit_task(task_id):
task = Task.query.get(task_id)
if task is None:
return make_response("", 404)

request_body = request.get_json()

task = task.from_json(request_body)

db.session.commit()

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


@tasks_bp.route("/<task_id>", methods=["DELETE"])
def delete_task(task_id):
task = Task.query.get(task_id)
if task is None:
return make_response("", 404)

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

return {
"details": f"Task {task.task_id} \"{task.title}\" successfully deleted"
}


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

if task is None:
return make_response("", 404)

task.completed_at = None

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


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

if task is None:
return make_response("", 404)

task.completed_at = datetime.now()

post_to_slack(f"Someone just completed the task {task.title}")

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



##########################
#### /goals Endpoints ####
##########################

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

try:
new_goal = Goal(title=request_body['title'])
except KeyError:
return make_response({
"details": "Invalid data"
}, 400)
Comment on lines +143 to +148

Choose a reason for hiding this comment

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

Handling exceptions 👍


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

response = {
"goal": new_goal.to_json()
}

return make_response(jsonify(response), 201)


@goals_bp.route("", methods=['GET'])
def get_goals():
sort_query = request.args.get("sort")
sort_by_id_query = request.args.get("sort_by_id")
filter_by_title_query = request.args.get("filter_by_title")

if sort_query == "asc":
goals = Goal.query.order_by("title")
elif sort_query == "desc":
goals = Goal.query.order_by(desc("title"))
elif sort_by_id_query == "asc":
goals = Goal.query.order_by("goal_id")
elif sort_by_id_query == "desc":
goals = Goal.query.order_by(desc("goal_id"))
elif filter_by_title_query:
goals = Goal.query.filter_by(title=filter_by_title_query)
else:
goals = Goal.query.all()

return jsonify([goal.to_json() for goal in goals])


@goals_bp.route("/<goal_id>", methods=['GET'])
def get_single_goal(goal_id):
goal = Goal.query.get(goal_id)
if goal is None:
return make_response("", 404)

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


@goals_bp.route("/<goal_id>", methods=['PUT'])
def edit_goal(goal_id):
goal = Goal.query.get(goal_id)
if goal is None:
return make_response("", 404)

request_body = request.get_json()

goal.title = request_body["title"]

db.session.commit()

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


@goals_bp.route("/<goal_id>", methods=["DELETE"])
def delete_goal(goal_id):
goal = Goal.query.get(goal_id)
if goal is None:
return make_response("", 404)

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

return {
"details": f"Goal {goal.goal_id} \"{goal.title}\" successfully deleted"
}


@goals_bp.route("/<goal_id>/tasks", methods=["POST"])
def post_tasks_for_goal(goal_id):
goal = Goal.query.get(goal_id)
if goal is None:
return make_response("", 404)

request_body = request.get_json()

for task_id in request_body['task_ids']:
task = Task.query.get(task_id)
task.goal_id = int(goal_id)

db.session.commit()

return {
"id": int(goal_id),
"task_ids": request_body['task_ids']
}


@goals_bp.route("/<goal_id>/tasks", methods=["GET"])
def get_tasks_for_goal(goal_id):
goal = Goal.query.get(goal_id)
if goal is None:
return make_response("", 404)

associated_tasks = Task.query.filter_by(goal_id=int(goal_id))

response = goal.to_json()
response['tasks'] = [task.get_task_info() for task in associated_tasks]

return response



##########################
#### Helper Functions ####
##########################

def post_to_slack(message):

Choose a reason for hiding this comment

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

Great helper function

"""
Posts a given message to the task-notifications channel in my Task Manager Slack workspace.
"""
path = "https://slack.com/api/chat.postMessage"

SLACK_API_KEY = os.environ.get("SLACK_API_KEY")

auth_header = {
"Authorization": f"Bearer {SLACK_API_KEY}"
}

query_params = {
"channel": "task-notifications",
"text": message
}

requests.post(path, params=query_params, headers=auth_header)

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