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()'
12 changes: 10 additions & 2 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,20 @@ def create_app(test_config=None):
"SQLALCHEMY_TEST_DATABASE_URI")

# Import models here for Alembic setup
from app.models.task import Task
from app.models.goal import Goal


db.init_app(app)
migrate.init_app(app, db)

from app.models.task import Task
from app.models.goal import Goal

from .routes import tasks_bp
app.register_blueprint(tasks_bp)

from .routes import goals_bp
app.register_blueprint(goals_bp)

# Register Blueprints here

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 relationship


class Goal(db.Model):
goal_id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String)
tasks = db.relationship("Task", lazy=True)

def goal_json(self):
return {"goal": {
"id" : self.goal_id,
"title" : self.title
}}
23 changes: 23 additions & 0 deletions app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
from flask import current_app
from app import db
from sqlalchemy.orm import relationship


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)

def task_completed(self):
if self.completed_at:
return True
else:
return False

def return_task_json(self):
task_dict = {
Comment on lines +13 to +20

Choose a reason for hiding this comment

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

Love the helper methods!

"id" : self.task_id,
"title" : self.title,
"description": self.description,
"is_complete":self.task_completed()
}
if self.goal_id:
task_dict["goal_id"] = self.goal_id

return task_dict
168 changes: 168 additions & 0 deletions app/routes.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,170 @@
from app import db
from flask import Blueprint
from flask import request
from flask import jsonify, make_response
from .models.task import Task
from .models.goal import Goal
from datetime import datetime
import os
import requests

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


@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 or "completed_at" not in request_body:
return make_response({"details": "Invalid data"}, 400)
task = Task(title=request_body["title"],
description=request_body["description"],
completed_at=request_body["completed_at"])


db.session.add(task)
db.session.commit()

return make_response({"task": task.return_task_json()}, 201)


@tasks_bp.route("", methods=["GET"], strict_slashes=False)
def get_tasks():
tasks = Task.query.all()
sort_query = request.args.get("sort")
if sort_query:
if 'asc' in sort_query:
tasks = Task.query.order_by(Task.title.asc())
elif 'desc' in sort_query:
tasks = Task.query.order_by(Task.title.desc())
tasks_response = []
for task in tasks:
tasks_response.append({
"id" : task.task_id,
"title" : task.title,
"description": task.description,
"is_complete":task.task_completed()
})
return jsonify(tasks_response), 200

@tasks_bp.route("/<task_id>", methods=["GET", "PUT", "DELETE"])
def get_single_task(task_id):
task = Task.query.get(task_id)
form_data = request.get_json()
if task is None:
return make_response("", 404)
if request.method == "GET":
return make_response({"task":task.return_task_json()})
elif request.method == "PUT":
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.return_task_json()})
elif request.method == "DELETE":
db.session.delete(task)
db.session.commit()
return make_response({"details": f'Task {task.task_id} "{task.title}" successfully deleted'}, 200)



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

path = 'https://slack.com/api/chat.postMessage'

query_dictionary= {
"token" : os.environ.get("SLACK_BOT_TOKEN"),
"channel" : "task-notifications",
"text" : f"Someone just completed the task {task.title}"
}
requests.post(path, data = query_dictionary)

db.session.commit()
return make_response({"task": task.return_task_json()}, 200)

@tasks_bp.route("/<task_id>/mark_incomplete", methods=["PATCH"])
def task_incomplete(task_id):
task = Task.query.get(task_id)
if task is None:
return make_response("", 404)
task.completed_at = None
return make_response({"task": task.return_task_json()}, 200)

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

request_body = request.get_json()
if "title" not in request_body:
return make_response({"details": "Invalid data"}, 400)

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

db.session.add(goal)
db.session.commit()

return make_response(goal.goal_json(), 201)

@goals_bp.route("", methods=["GET"], strict_slashes=False)
def get_tasks():
goals = Goal.query.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", "PUT", "DELETE"])
def get_single_goal(goal_id):
goal = Goal.query.get(goal_id)
form_data = request.get_json()
if goal is None:
return make_response("", 404)
if request.method == "GET":
return make_response(goal.goal_json())
elif request.method == "PUT":
form_data = request.get_json()
goal.title = form_data["title"]
db.session.commit()
return make_response(goal.goal_json())
elif request.method == "DELETE":
db.session.delete(goal)
db.session.commit()
return make_response({"details": f'Goal {goal.goal_id} "{goal.title}" successfully deleted'}, 200)

@goals_bp.route("/<goal_id>/tasks", methods=["POST"])
def goals_and_tasks(goal_id):
goal = Goal.query.get(goal_id)

request_body = request.get_json()
for task_id in request_body["task_ids"]:
task = Task.query.get(task_id)
task.goal_id = goal.goal_id

db.session.commit()
return make_response({"id": goal.goal_id, "task_ids": request_body["task_ids"]}), 200

Choose a reason for hiding this comment

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

This change would have made the above-mentioned bug more obvious. This would also allow the response to include ALL tasks associated with the goal, not just the new ones.
This isn't super intuitive so I'd definitely be down to talk through this more in our next 1:1

Suggested change
return make_response({"id": goal.goal_id, "task_ids": request_body["task_ids"]}), 200
full_task_ids = []
for task in goal.tasks:
full_task_ids.append(task.task_id)
return make_response({"id": goal.goal_id, "task_ids": full_task_ids}, 200)


@goals_bp.route("/<goal_id>/tasks", methods=["GET"])
def get_goals(goal_id):
goal = Goal.query.get(goal_id)

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

tasks = Task.query.filter_by(goal_id=goal_id)
task_list = []
for task in tasks:
task_list.append(task.return_task_json())

return make_response({"id": goal.goal_id, "title": goal.title, "tasks": task_list }, 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