Skip to content
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
8 changes: 8 additions & 0 deletions app/models/goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,11 @@

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,
}
26 changes: 26 additions & 0 deletions app/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,29 @@

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'))

def is_complete(self):
if self.completed_at:
return True
return False
Comment on lines +12 to +15

Choose a reason for hiding this comment

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

You can simplify this as:

Suggested change
def is_complete(self):
if self.completed_at:
return True
return False
def is_complete(self):
return bool(self.completed_at)


def to_dict(self):
if self.goal_id:
return {
"id": self.task_id,
"goal_id": self.goal_id,
"title": self.title,
"description": self.description,
"is_complete": self.is_complete()
}
return {
"id": self.task_id,
"title": self.title,
"description": self.description,
"is_complete": self.is_complete()
}

165 changes: 164 additions & 1 deletion app/routes.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,165 @@
from flask import Blueprint
from app import db
from flask import Blueprint, request, make_response, jsonify
from app.models.task import Task
from app.models.goal import Goal
from sqlalchemy import desc, asc
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_bp.route("", methods=["GET"])
def get_tasks():
order_query = request.args.get("sort")
if order_query == "asc":
tasks = Task.query.order_by(asc(Task.title))
elif order_query == "desc":
tasks = Task.query.order_by(desc(Task.title))
else:
tasks = Task.query.all()
tasks_response = [task.to_dict() 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 use of a list comprehension here. 😄

return make_response(jsonify(tasks_response), 200)

@tasks_bp.route("", methods=["POST"])
def create_task():
request_body = request.get_json()
if "title" in request_body and "description" in request_body and "completed_at" in request_body:
new_task = Task(title = request_body["title"],
description = request_body["description"],
completed_at=request_body["completed_at"])
db.session.add(new_task)
db.session.commit()
return make_response({"task": new_task.to_dict()}, 201)
return make_response({"details": "Invalid data"}, 400)

@tasks_bp.route("/<task_id>", methods=["GET"])
def get_one_task(task_id):
task = Task.query.get(task_id)
if task:
return {"task": task.to_dict()}, 200
return make_response("", 404)

@tasks_bp.route("/<task_id>", methods=["PUT"])
def update_task(task_id):
task = Task.query.get(task_id)
if task:
form_data = request.get_json()
task.title = form_data["title"]
task.description = form_data["description"]
task.completed_at = form_data["completed_at"]
db.session.commit()
return make_response({"task": task.to_dict()}, 200)
return make_response("", 404)

@tasks_bp.route("/<task_id>", methods=["DELETE"])
def delete_task(task_id):
task = Task.query.get(task_id)
if task:
db.session.delete(task)
db.session.commit()
return make_response({"details": f"Task {task.task_id} \"{task.title}\" successfully deleted"}, 200)
return make_response("", 404)

@tasks_bp.route("/<task_id>/<mark_status>", methods=["PATCH"])
def handle_task_completion(task_id, mark_status):
task = Task.query.get(task_id)
if task:
if mark_status == "mark_incomplete":
task.completed_at = None
db.session.commit()
return make_response({"task": task.to_dict()}, 200)
elif mark_status == "mark_complete":
task.completed_at = datetime.utcnow()
db.session.commit()
send_slack_message(task.title)
return make_response({"task": task.to_dict()}, 200)
return make_response("", 404)


#WAVE 4
# consider moving to a "utilities" folder in refactoring
PATH = "https://slack.com/api/chat.postMessage"

def send_slack_message(task_title):
query_params = {
"channel": "task-notifications",
"text": f"Someone just completed the task {task_title}"
}
slackbot_token = os.environ.get('SLACK_API_KEY')
header = {
"Authorization": f"Bearer {slackbot_token}"
}
requests.post(PATH, params=query_params, headers=header)


#WAVE 5
@goals_bp.route("", methods=["GET"])
def get_goals():
goals = Goal.query.all()
goals_response = [goal.to_dict() for goal in goals]
return make_response(jsonify(goals_response), 200)

@goals_bp.route("", methods=["POST"])
def create_goal():
request_body = request.get_json()
if "title" in request_body:
new_goal = Goal(title = request_body["title"])
db.session.add(new_goal)
db.session.commit()
return make_response({"goal": new_goal.to_dict()}, 201)
return make_response({"details": "Invalid data"}, 400)

@goals_bp.route("/<goal_id>", methods=["GET"])
def get_one_goal(goal_id):
goal = Goal.query.get(goal_id)
if goal:
return {"goal": goal.to_dict()}, 200
return make_response("", 404)

@goals_bp.route("/<goal_id>", methods=["PUT"])
def update_goal(goal_id):
goal = Goal.query.get(goal_id)
if goal:
form_data = request.get_json()
goal.title = form_data["title"]
db.session.commit()
return make_response({"goal": goal.to_dict()}, 200)
return make_response("", 404)

@goals_bp.route("/<goal_id>", methods=["DELETE"])
def delete_goal(goal_id):
goal = Goal.query.get(goal_id)
if goal:
db.session.delete(goal)
db.session.commit()
return make_response({"details": f"Goal {goal.goal_id} \"{goal.title}\" successfully deleted"}, 200)
return make_response("", 404)

# WAVE 06
@goals_bp.route("/<goal_id>/tasks", methods=["POST"])
def assign_tasks_to_goal(goal_id):
request_body = request.get_json()
tasks_for_goal = request_body["task_ids"]
for task_id in tasks_for_goal:
task = Task.query.get(task_id)
task.goal_id = int(goal_id)
return make_response({
"id": int(goal_id),
"task_ids": tasks_for_goal
}, 200)

@goals_bp.route("/<goal_id>/tasks", methods=["GET"])
def get_tasks_of_one_goal(goal_id):
goal = Goal.query.get(goal_id)
if goal:
tasks_of_goal = Task.query.filter_by(goal_id=goal_id)
if tasks_of_goal:
tasks_response = [task.to_dict() for task in tasks_of_goal]
return make_response({
"id": int(goal_id),
"title": goal.title,
"tasks": tasks_response
}, 200)
return make_response("", 404)
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()
24 changes: 24 additions & 0 deletions migrations/script.py.mako
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""${message}

Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}

"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}

# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}


def upgrade():
${upgrades if upgrades else "pass"}


def downgrade():
${downgrades if downgrades else "pass"}
Loading