Skip to content
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()'
8 changes: 8 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,12 @@ def create_app(test_config=None):

# Register Blueprints here

#Task Blueprint
from.routes import task_bp
app.register_blueprint(task_bp)

#Goal Blueprint
from.routes import goal_bp
app.register_blueprint(goal_bp)

return app
9 changes: 9 additions & 0 deletions app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
from flask import current_app
from app import db
from sqlalchemy.orm import backref

Choose a reason for hiding this comment

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

I think we don't need this import statement. The backref on line 9 is just a named parameter for the relationship function instead of an object we need to import.



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_dict(self):
return {
"id": self.goal_id,
"title": self.title
}
28 changes: 28 additions & 0 deletions app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,34 @@
from flask import current_app
from app import db
from app.models.goal import Goal


class Task(db.Model):
task_id = db.Column(db.Integer, primary_key=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)

Choose a reason for hiding this comment

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

Nice foreign key relationship!


def to_dict(self):
if self.goal_id == None:
if not self.completed_at:
self.completed_at = None
else:
self.completed_at = True
return {
"id": self.task_id,
"title": self.title,
"description": self.description,
"is_complete": True if self.completed_at else False

Choose a reason for hiding this comment

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

Nice use of truthiness and the ternary operator!

}
else:
task_dict = {
"id": self.task_id,
"goal_id": self.goal_id,
"title": self.title,
"description": self.description,
"is_complete": self.completed_at is not None
}
return task_dict
Comment on lines +14 to +33

Choose a reason for hiding this comment

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

This definitely works! For an extra challenge, see if you can think of how to re-write this with less duplication of code. It might be helpful to start off with a dictionary with all the shared values, and then only add the additional values you need in the conditionals.


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

load_dotenv()

task_bp = Blueprint("task_bp", __name__, url_prefix="/tasks")
goal_bp = Blueprint("goal_bp", __name__, url_prefix="/goals")

@task_bp.route("", methods=["GET", "POST"])
def handle_all_tasks():
tasks_response = []

#WV2: Get Tasks Asc, Get Tasks Desc
if request.method == "GET":
if request.args.get('sort') == 'asc':
tasks = Task.query.order_by(Task.title.asc()).all()
elif request.args.get('sort') == 'desc':
tasks = Task.query.order_by(Task.title.desc()).all()

#WV1: No Saved Tasks, One Saved Tasks
else:
tasks = Task.query.all()
for task in tasks:
tasks_response.append(task.to_dict())
Comment on lines +21 to +30

Choose a reason for hiding this comment

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

Awesome condensed code here!

return jsonify(tasks_response), 200

#WV1: Create Task With None Completed_At, Create Task - Title, Create Task - Description, Create Task - Completed_At
#WV3: Create Task Wtih Valid Completed_At
if request.method == "POST":
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 ({'details': 'Invalid data'}, 400)
Comment on lines +37 to +42

Choose a reason for hiding this comment

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

Cool use of try and except! I especially like that you specify that KeyError is the only type of exception that should be caught.

db.session.add(new_task)
db.session.commit()
return jsonify({"task":new_task.to_dict()}), 201

Choose a reason for hiding this comment

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

Nice compact return statement.


@task_bp.route("/<task_id>", methods=["GET", "PUT", "DELETE"])
def handle_single_task(task_id):
task_id = int(task_id)
task = Task.query.get(task_id)

Choose a reason for hiding this comment

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

Consider using get_or_404 here to avoid the repeated if task is None later in this function.


#WV1: Get Task, Get Task Not Found
if request.method == "GET":
if task is None:
return make_response("", 404)
return jsonify({"task": task.to_dict()}), 200

#WV1: Update Task, Update Task Not Found
#WV3: Update Task With Valid Completed At
elif request.method == "PUT":
input_data = request.get_json()
if task is None:
return make_response("", 404)
task.title = input_data["title"]
task.description = input_data["description"]
db.session.commit()
return ({"task":task.to_dict()}), 200

#WV1: Delete Task, Delete Task Not Found
elif request.method == "DELETE":
if task is None:
return make_response("", 404)
else:
db.session.delete(task)
db.session.commit()
return ({'details': f'Task {task_id} "{task.title}" successfully deleted'}), 200

#WV3: Mark Comp on Incom, Mark Incom on Comp, Mark Comp on Comp, Marck Comp on Missing, Mark Incom on Missing
@task_bp.route("/<task_id>/<completion_status>", methods=["PATCH"])

Choose a reason for hiding this comment

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

Very cool idea making the completion status a dynamic part of the route! I hadn't considered doing it that way!

def mark_complete(task_id, completion_status):

task = Task.query.get(task_id)
PATH="https://slack.com/api/chat.postMessage"
if task is None:
return make_response("", 404)

if completion_status == "mark_complete":
task.completed_at = date.today()

#WV4: Send Slack Message
token=os.environ.get("TOKEN")

Choose a reason for hiding this comment

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

Nice job getting the token from the environment variables! You might consider fetching the variable from the OS once, and then keeping it saved so that your code does not need to do os.environ.get every time a task is completed. This could be achieved in a way similar to how the environment variables are handled in app/__init__.py. You might also consider a bit more descriptive of a environment variable name, perhaps something like SLACK_API_TOKEN.

query_params = {
"token": token,
"channel": "task-notifications",
"text": f"Somone just completed task {task.title}"
}
requests.post(PATH, data=query_params)

if completion_status == "mark_incomplete":
task.completed_at = None
db.session.commit()

Choose a reason for hiding this comment

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

Consider adding logic here to send back an error code if the user attempts to pass in an invalid completion_status

return jsonify({"task": task.to_dict()})

@goal_bp.route("", methods=["GET", "POST"])
def handle_all_goals():
goal_response = []

#WV5: No Saved Goals, One Saved Goals
if request.method == "GET":
goals = Goal.query.all()
for goal in goals:
goal_response.append(goal.to_dict())
Comment on lines +112 to +113

Choose a reason for hiding this comment

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

This looks great! As an additional challenge, can you think of how you would do this in a list comprehension? Either way is fine, it's just a matter of personal style.

return jsonify(goal_response), 200

#WV5: Create Goal, Create Goal Missing Title
if request.method == "POST":
request_body = request.get_json()
try:
new_goal = Goal(title = request_body["title"])
except KeyError:
return ({'details': 'Invalid data'}, 400)
db.session.add(new_goal)
db.session.commit()

return jsonify({"goal": new_goal.to_dict()}), 201

@goal_bp.route("/<goal_id>", methods=["GET", "DELETE", "PUT", "PATCH"])
def handle_single_goal(goal_id):
goal_id = int(goal_id)

Choose a reason for hiding this comment

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

Consider adding validation here so an error response is made if the user passes in a non-integer goal_id

goal = Goal.query.get(goal_id)

#WV5: Get Goal, Get Goal Not Found
if request.method == "GET":
if goal is None:
return make_response("", 404)
return jsonify({"goal": goal.to_dict()}), 200

#WV5: Update Goal, Update Goal Not Found
elif request.method == "PUT":
input_data = request.get_json()
if goal is None:
return make_response("", 404)
goal.title = input_data["title"]
db.session.commit()
return ({"goal": goal.to_dict()}), 200

#WV5: Delete Goal Not Found, Delete Goal
elif request.method == "DELETE":
if goal is None:
return make_response("", 404)
else:
db.session.delete(goal)
db.session.commit()
return ({'details': f'Goal {goal_id} "{goal.title}" successfully deleted'}), 200

@goal_bp.route("/<goal_id>/tasks", methods=["GET", "POST"])
def handle_goal_and_tasks(goal_id):
goal = Goal.query.get(goal_id)
request_body = request.get_json()

#W5: Get Tasks, Get Tasks No Goal, Get Tasks No Tasks, Task Include Goal ID
if request.method == "GET":
if goal is None:
return make_response("", 404)
task_list = []
for task in goal.tasks:
task_list.append(task.to_dict())
return {
"id": goal.goal_id,
"title": goal.title,
"tasks": task_list
}, 200
Comment on lines +167 to +173

Choose a reason for hiding this comment

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

👍🏻


#W5: Post Tasks IDs to Goal, Post Tasks to Goal with Goals
if request.method == "POST":
if goal is None:
return make_response("", 404)
task_ids = request_body["task_ids"]
for id in task_ids:
task = Task.query.get(id)
task.goal_id = goal_id
db.session.commit()
return make_response({"id": int(goal_id), "task_ids": task_ids}), 200
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
96 changes: 96 additions & 0 deletions migrations/env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from __future__ import with_statement

import logging
from logging.config import fileConfig

from sqlalchemy import engine_from_config
from sqlalchemy import pool
from flask import current_app

from alembic import context

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
config.set_main_option(
'sqlalchemy.url',
str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%'))
target_metadata = current_app.extensions['migrate'].db.metadata

# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.


def run_migrations_offline():
"""Run migrations in 'offline' mode.

This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.

Calls to context.execute() here emit the given string to the
script output.

"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True
)

with context.begin_transaction():
context.run_migrations()


def run_migrations_online():
"""Run migrations in 'online' mode.

In this scenario we need to create an Engine
and associate a connection with the context.

"""

# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')

connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)

with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args
)

with context.begin_transaction():
context.run_migrations()


if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
Loading