From 4eaed31f4f248241261c51fd30f98eb6bd7f7e86 Mon Sep 17 00:00:00 2001 From: Cloudy Lopez Date: Thu, 10 Jun 2021 20:58:44 -0700 Subject: [PATCH 01/11] wave one - passing some tests - not complete --- app/__init__.py | 7 +- app/models/task.py | 5 +- app/routes.py | 84 ++++++++++++++++++++++++ migrations/README | 1 + migrations/alembic.ini | 45 +++++++++++++ migrations/env.py | 96 ++++++++++++++++++++++++++++ migrations/script.py.mako | 24 +++++++ migrations/versions/86487318a410_.py | 33 ++++++++++ 8 files changed, 290 insertions(+), 5 deletions(-) create mode 100644 migrations/README create mode 100644 migrations/alembic.ini create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 migrations/versions/86487318a410_.py diff --git a/app/__init__.py b/app/__init__.py index 2764c4cc8..bb1061a45 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -4,12 +4,10 @@ import os from dotenv import load_dotenv - db = SQLAlchemy() migrate = Migrate() load_dotenv() - def create_app(test_config=None): app = Flask(__name__) app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False @@ -28,7 +26,8 @@ def create_app(test_config=None): db.init_app(app) migrate.init_app(app, db) - - # Register Blueprints here + from app.models.task import Task + from .routes import tasks_bp + app.register_blueprint(tasks_bp) return app diff --git a/app/models/task.py b/app/models/task.py index 39c89cd16..73dfebda0 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -3,4 +3,7 @@ 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, default=None) \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index 8e9dfe684..9dd405fe8 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,2 +1,86 @@ from flask import Blueprint +from app.models.task import Task +from app import db +from flask import request, Blueprint, make_response, jsonify + +tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") #/tasks? + +@tasks_bp.route("", methods=["GET", "POST"]) +def handle_tasks(): + if request.method == "POST": + request_body = request.get_json() + title = request_body.get("title") + description = request_body.get("description") + + if not title or not description or "completed_at" not in request_body: + return jsonify({"details": "Invalid Data"}), 400 + + new_task = Task( + id=request_body["id"], + title=request_body["title"], + description=request_body["description"], + completed_at=request_body["completed_at"]) + db.session.add(new_task) + db.session.commit() + + if new_task.completed_at == None: + new_task.completed_at = False + else: + new_task.completed_at = True + return jsonify(f'Task {new_task.title} successfully created', 201) + + elif request.method == "GET": + url_title = request.args.get("title") + if url_title: + tasks = Task.query.filter_by(title=url_title) + else: + tasks = Task.query.order_by(Task.title).all() + tasks_response = [] + for task in tasks: + tasks_response.append({ + "id": task.id, + "title": task.title, + "description": task.description, + "is_complete": bool(task.completed_at) + }) + return jsonify(tasks_response) + + +@tasks_bp.route("/", methods=["GET", "PUT", "DELETE"]) +def handle_task(task_id): + task = Task.query.get(task_id) + if task is None: + return make_response("No matching task found", 404) + + if request.method == "GET": + selected_task = {"task": + {"id": task.id, + "title": task.title, + "description": task.description, + "is_complete": bool(task.completed_at) + }} + return jsonify(selected_task), 200 + + elif request.method == "PUT": + request_body = request.get_json() + + task.title = request_body["title"] + task.description = request_body["description"] + task.completed_at = request_body["completed_at"] + + updated_task = {"task": + {"id": task.id, + "title": task.title, + "description": task.description, + "is_complete": bool(task.completed_at) + }} + db.sessions.add(task) + db.session.commit() + return jsonify(updated_task), 200 + + elif request.method == "DELETE": + db.session.delete(task) + db.session.commit() + task_response_body = {"details": f'Task {task.id} "with title: {task.title}" has been successfully deleted'} + return jsonify(task_response_body), 200 diff --git a/migrations/README b/migrations/README new file mode 100644 index 000000000..98e4f9c44 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 000000000..f8ed4801f --- /dev/null +++ b/migrations/alembic.ini @@ -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 diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 000000000..8b3fb3353 --- /dev/null +++ b/migrations/env.py @@ -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() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 000000000..2c0156303 --- /dev/null +++ b/migrations/script.py.mako @@ -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"} diff --git a/migrations/versions/86487318a410_.py b/migrations/versions/86487318a410_.py new file mode 100644 index 000000000..6eb978e56 --- /dev/null +++ b/migrations/versions/86487318a410_.py @@ -0,0 +1,33 @@ +"""empty message + +Revision ID: 86487318a410 +Revises: +Create Date: 2021-06-09 12:18:41.790660 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '86487318a410' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('goal', + sa.Column('goal_id', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('goal_id') + ) + op.drop_column('task', 'is_complete') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('task', sa.Column('is_complete', sa.BOOLEAN(), autoincrement=False, nullable=True)) + op.drop_table('goal') + # ### end Alembic commands ### From ef7b36f52fdb7bfd24774b830c70164aa190ad5c Mon Sep 17 00:00:00 2001 From: Cloudy Lopez Date: Fri, 11 Jun 2021 16:03:58 -0700 Subject: [PATCH 02/11] wave one - completed - passing all tests --- app/routes.py | 45 +++++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/app/routes.py b/app/routes.py index 9dd405fe8..279b270b9 100644 --- a/app/routes.py +++ b/app/routes.py @@ -13,21 +13,30 @@ def handle_tasks(): description = request_body.get("description") if not title or not description or "completed_at" not in request_body: - return jsonify({"details": "Invalid Data"}), 400 + return make_response(jsonify({"details": "Invalid data"}), 400) new_task = Task( - id=request_body["id"], title=request_body["title"], description=request_body["description"], completed_at=request_body["completed_at"]) - db.session.add(new_task) - db.session.commit() - + if new_task.completed_at == None: - new_task.completed_at = False + completed_at = False else: - new_task.completed_at = True - return jsonify(f'Task {new_task.title} successfully created', 201) + completed_at = True + + # response_body = { + # } + db.session.add(new_task) + db.session.commit() + + return make_response({ + "task": { + "id": new_task.task_id, + "title": new_task.title, + "description": new_task.description, + "is_complete": completed_at + }}, 201) elif request.method == "GET": url_title = request.args.get("title") @@ -38,14 +47,13 @@ def handle_tasks(): tasks_response = [] for task in tasks: tasks_response.append({ - "id": task.id, + "id": task.task_id, "title": task.title, "description": task.description, "is_complete": bool(task.completed_at) }) return jsonify(tasks_response) - @tasks_bp.route("/", methods=["GET", "PUT", "DELETE"]) def handle_task(task_id): task = Task.query.get(task_id) @@ -54,14 +62,14 @@ def handle_task(task_id): if request.method == "GET": selected_task = {"task": - {"id": task.id, + {"id": task.task_id, "title": task.title, "description": task.description, "is_complete": bool(task.completed_at) }} return jsonify(selected_task), 200 - elif request.method == "PUT": + elif request.method == "PUT": request_body = request.get_json() task.title = request_body["title"] @@ -69,18 +77,23 @@ def handle_task(task_id): task.completed_at = request_body["completed_at"] updated_task = {"task": - {"id": task.id, + {"id": task.task_id, "title": task.title, "description": task.description, "is_complete": bool(task.completed_at) }} - db.sessions.add(task) + db.session.add(task) db.session.commit() - return jsonify(updated_task), 200 + return make_response(jsonify(updated_task)), 200 elif request.method == "DELETE": db.session.delete(task) db.session.commit() - task_response_body = {"details": f'Task {task.id} "with title: {task.title}" has been successfully deleted'} + task_response_body = { + "details": + f'Task {task.task_id} \"{task.title}\" successfully deleted' + } return jsonify(task_response_body), 200 +# https://github.com/OhCloud/task-list-api +# https://github.com/Ada-C15A/task-list-api/pull/3 \ No newline at end of file From 6d5d69cfdfa9b9376c56db284a51f96f290ae069 Mon Sep 17 00:00:00 2001 From: Cloudy Lopez Date: Mon, 14 Jun 2021 21:05:27 -0700 Subject: [PATCH 03/11] wave 2 completed - passing all tests --- app/routes.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/routes.py b/app/routes.py index 279b270b9..b6405ec06 100644 --- a/app/routes.py +++ b/app/routes.py @@ -25,8 +25,6 @@ def handle_tasks(): else: completed_at = True - # response_body = { - # } db.session.add(new_task) db.session.commit() @@ -44,6 +42,17 @@ def handle_tasks(): tasks = Task.query.filter_by(title=url_title) else: tasks = Task.query.order_by(Task.title).all() + + sort = request.args.get("sort") + if not sort: + tasks = Task.query.all() + elif sort == "asc": + tasks = Task.query.order_by(Task.title.asc()).all() + elif sort == "desc": + tasks = Task.query.order_by(Task.title.desc()).all() + else: + tasks = Task.query.all() + tasks_response = [] for task in tasks: tasks_response.append({ From ffe38cea282bfb5fe178ba3e9030dc3455417aba Mon Sep 17 00:00:00 2001 From: Cloudy Lopez Date: Mon, 14 Jun 2021 22:00:11 -0700 Subject: [PATCH 04/11] wave 3 completed - all tests passing --- app/routes.py | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/app/routes.py b/app/routes.py index b6405ec06..53c8d2f17 100644 --- a/app/routes.py +++ b/app/routes.py @@ -2,8 +2,9 @@ from app.models.task import Task from app import db from flask import request, Blueprint, make_response, jsonify +from datetime import datetime -tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") #/tasks? +tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") @tasks_bp.route("", methods=["GET", "POST"]) def handle_tasks(): @@ -52,7 +53,7 @@ def handle_tasks(): tasks = Task.query.order_by(Task.title.desc()).all() else: tasks = Task.query.all() - + tasks_response = [] for task in tasks: tasks_response.append({ @@ -63,7 +64,7 @@ def handle_tasks(): }) return jsonify(tasks_response) -@tasks_bp.route("/", methods=["GET", "PUT", "DELETE"]) +@tasks_bp.route("/", methods=["GET", "PUT", "DELETE", "PATCH"]) def handle_task(task_id): task = Task.query.get(task_id) if task is None: @@ -93,6 +94,7 @@ def handle_task(task_id): }} db.session.add(task) db.session.commit() + return make_response(jsonify(updated_task)), 200 elif request.method == "DELETE": @@ -104,5 +106,37 @@ def handle_task(task_id): } return jsonify(task_response_body), 200 +@tasks_bp.route("//mark_complete", methods=["PATCH"]) +def mark_task_complete(task_id): + task = Task.query.get_or_404(task_id) + + task.completed_at = datetime.now() + + db.session.commit() + completed_task = {"task": + + {"id": task.task_id, + "title": task.title, + "description": task.description, + "is_complete": bool(task.completed_at) + }} + return jsonify(completed_task), 200 + +@tasks_bp.route("//mark_incomplete", methods=["PATCH"]) +def mark_task_incomplete(task_id): + task = Task.query.get_or_404(task_id) + + task.completed_at = None + db.session.commit() + incompleted_task = {"task": + + {"id": task.task_id, + "title": task.title, + "description": task.description, + "is_complete": bool(task.completed_at) + }} + return jsonify(incompleted_task), 200 + + # https://github.com/OhCloud/task-list-api # https://github.com/Ada-C15A/task-list-api/pull/3 \ No newline at end of file From 1c9936112521f6a2364f181c5e0a3084f8fb93d9 Mon Sep 17 00:00:00 2001 From: Cloudy Lopez Date: Tue, 15 Jun 2021 10:31:34 -0700 Subject: [PATCH 05/11] slack bot up and running --- app/routes.py | 25 ++++++++++++++++++++++--- requirements.txt | 1 + 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/routes.py b/app/routes.py index 53c8d2f17..47d83d974 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,11 +1,26 @@ -from flask import Blueprint +import re from app.models.task import Task -from app import db -from flask import request, Blueprint, make_response, jsonify +from app import db +from flask import request, Blueprint, make_response, jsonify# from datetime import datetime +from dotenv import load_dotenv +import os +import json, requests tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") +load_dotenv() + +def post_message_to_slack(text): + SLACK_TOKEN = os.environ.get('SLACKBOT_TOKEN') + slack_path = "https://slack.com/api/chat/postMessage" + query_params = { + 'channel': 'task-notifications', + 'text': text + } + headers = {'Authorization': f"Bearer {SLACK_TOKEN}"} + request.post(slack_path, params=query_params, headers=headers) + @tasks_bp.route("", methods=["GET", "POST"]) def handle_tasks(): if request.method == "POST": @@ -113,6 +128,10 @@ def mark_task_complete(task_id): task.completed_at = datetime.now() db.session.commit() + + slack_message = f"A user just completed task: {task.title}" + post_message_to_slack(slack_message) + completed_task = {"task": {"id": task.task_id, diff --git a/requirements.txt b/requirements.txt index cfdf74050..5207bdfd4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,6 +26,7 @@ python-dotenv==0.15.0 python-editor==1.0.4 requests==2.25.1 six==1.15.0 +slackclient==2.9.3 SQLAlchemy==1.3.23 toml==0.10.2 urllib3==1.26.4 From 1ddb401ec3a592cfeeb3d7c021ea4f5ce8c03a3a Mon Sep 17 00:00:00 2001 From: Cloudy Lopez Date: Tue, 15 Jun 2021 11:54:58 -0700 Subject: [PATCH 06/11] wave 5 completed - all tests passing --- app/__init__.py | 5 +- app/models/goal.py | 1 + app/routes.py | 77 ++++++++++++++++++++++++++-- migrations/versions/29b91b61ddcf_.py | 28 ++++++++++ 4 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 migrations/versions/29b91b61ddcf_.py diff --git a/app/__init__.py b/app/__init__.py index bb1061a45..cac5d268b 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -26,8 +26,11 @@ def create_app(test_config=None): db.init_app(app) migrate.init_app(app, db) - from app.models.task import Task + from .routes import tasks_bp app.register_blueprint(tasks_bp) + from .routes import goals_bp + app.register_blueprint(goals_bp) + return app diff --git a/app/models/goal.py b/app/models/goal.py index 8cad278f8..653ac1865 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -4,3 +4,4 @@ class Goal(db.Model): goal_id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String) \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index 47d83d974..6c20ef4ed 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,14 +1,15 @@ import re from app.models.task import Task +from app.models.goal import Goal from app import db from flask import request, Blueprint, make_response, jsonify# from datetime import datetime -from dotenv import load_dotenv import os import json, requests +from dotenv import load_dotenv tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") - +goals_bp = Blueprint("goals", __name__, url_prefix="/goals") load_dotenv() def post_message_to_slack(text): @@ -21,6 +22,76 @@ def post_message_to_slack(text): headers = {'Authorization': f"Bearer {SLACK_TOKEN}"} request.post(slack_path, params=query_params, headers=headers) +@goals_bp.route("", methods=["GET", "POST"]) +def handle_goals(): + if request.method == "POST": + request_body = request.get_json() + title = request_body.get("title") + + if not title: + return jsonify({"details": "Invalid data"}), 400 + + new_goal = Goal(title=request_body["title"]) + + db.session.add(new_goal) + db.session.commit() + + committed_goal = {"goal": + {"id": new_goal.goal_id, + "title": new_goal.title + }} + + return jsonify(committed_goal), 201 + + elif request.method == "GET": + goals = Goal.query.all() + goals_response = [] + + for goal in goals: + goals_response.append({ + "title": goal.title, + "id": goal.goal_id + }) + + return jsonify(goals_response), 200 + +@goals_bp.route("/", methods=["GET", "PUT", "DELETE"]) +def handle_goal(goal_id): + goal = Goal.query.get_or_404(goal_id) + + if request.method =="GET": + if goal == None: + return make_response("No matching goal found"), 404 + + selected_goal = {"goal": + {"id": goal.goal_id, + "title": goal.title + }} + return selected_goal + + elif request.method == "PUT": + form_data = request.get_json() + goal.title = form_data["title"] + + db.session.commit() + + committed_goal = {"goal": + {"id": goal.goal_id, + "title": goal.title + }} + return jsonify(committed_goal), 200 + + elif request.method == "DELETE": + db.session.delete(goal) + db.session.commit() + goal_response_body = { + "details": + f'Goal {goal.goal_id} \"{goal.title}\" successfully deleted' + } + return jsonify(goal_response_body) + + + @tasks_bp.route("", methods=["GET", "POST"]) def handle_tasks(): if request.method == "POST": @@ -131,7 +202,7 @@ def mark_task_complete(task_id): slack_message = f"A user just completed task: {task.title}" post_message_to_slack(slack_message) - + completed_task = {"task": {"id": task.task_id, diff --git a/migrations/versions/29b91b61ddcf_.py b/migrations/versions/29b91b61ddcf_.py new file mode 100644 index 000000000..48be023de --- /dev/null +++ b/migrations/versions/29b91b61ddcf_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 29b91b61ddcf +Revises: 86487318a410 +Create Date: 2021-06-15 11:37:16.113615 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '29b91b61ddcf' +down_revision = '86487318a410' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('goal', sa.Column('title', sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('goal', 'title') + # ### end Alembic commands ### From ce1c4f35f7e5ecbad263f6c6188d5c9d9cbff513 Mon Sep 17 00:00:00 2001 From: Cloudy Lopez Date: Tue, 15 Jun 2021 12:55:34 -0700 Subject: [PATCH 07/11] wave 6 - passing one test - not complete --- app/models/goal.py | 6 ++- app/models/task.py | 6 ++- app/routes.py | 60 +++++++++++++++++++++++++--- migrations/versions/4d5d03b10019_.py | 28 +++++++++++++ migrations/versions/5d3ef5e8db55_.py | 30 ++++++++++++++ 5 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 migrations/versions/4d5d03b10019_.py create mode 100644 migrations/versions/5d3ef5e8db55_.py diff --git a/app/models/goal.py b/app/models/goal.py index 653ac1865..9e9e22154 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -1,7 +1,11 @@ from flask import current_app +from sqlalchemy.orm import backref from app import db +from sqlalchemy import ForeignKey, update +from sqlalchemy.orm import relationship class Goal(db.Model): goal_id = db.Column(db.Integer, primary_key=True) - title = db.Column(db.String) \ No newline at end of file + title = db.Column(db.String) + task_ids = db.relationship('Task', backref='goal', lazy=True) \ No newline at end of file diff --git a/app/models/task.py b/app/models/task.py index 73dfebda0..a299fdcd5 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -1,9 +1,13 @@ from flask import current_app from app import db +from sqlalchemy import ForeignKey, update +from sqlalchemy.orm import relationship class Task(db.Model): 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, default=None) \ No newline at end of file + completed_at = db.Column(db.DateTime, nullable=True, default=None) + is_complete = db.Column(db.Boolean, default=False) + goal_id = db.Column(db.Integer, db.ForeignKey('goal.goal_id')) \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index 6c20ef4ed..2c609b0c0 100644 --- a/app/routes.py +++ b/app/routes.py @@ -10,6 +10,7 @@ tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") goals_bp = Blueprint("goals", __name__, url_prefix="/goals") + load_dotenv() def post_message_to_slack(text): @@ -27,6 +28,7 @@ def handle_goals(): if request.method == "POST": request_body = request.get_json() title = request_body.get("title") + task_ids = request.body["task_ids"] if not title: return jsonify({"details": "Invalid data"}), 400 @@ -38,7 +40,8 @@ def handle_goals(): committed_goal = {"goal": {"id": new_goal.goal_id, - "title": new_goal.title + "title": new_goal.title, + "task_ids": new_goal.task_ids }} return jsonify(committed_goal), 201 @@ -50,7 +53,8 @@ def handle_goals(): for goal in goals: goals_response.append({ "title": goal.title, - "id": goal.goal_id + "id": goal.goal_id, + "tasks": goal.task_ids }) return jsonify(goals_response), 200 @@ -62,10 +66,16 @@ def handle_goal(goal_id): if request.method =="GET": if goal == None: return make_response("No matching goal found"), 404 + + tasks = [] + + for item in goal.task_ids: + tasks.append(item) selected_goal = {"goal": {"id": goal.goal_id, - "title": goal.title + "title": goal.title, + "tasks": tasks }} return selected_goal @@ -77,7 +87,8 @@ def handle_goal(goal_id): committed_goal = {"goal": {"id": goal.goal_id, - "title": goal.title + "title": goal.title, + "task_ids": goal.task_ids }} return jsonify(committed_goal), 200 @@ -86,11 +97,47 @@ def handle_goal(goal_id): db.session.commit() goal_response_body = { "details": - f'Goal {goal.goal_id} \"{goal.title}\" successfully deleted' + f'Goal {goal.goal_id} successfully deleted' } return jsonify(goal_response_body) +@goals_bp.route("//tasks", methods=["GET", "POST"]) +def handle_goal_tasks(goal_id): + goal = Goal.query.get_or_404(goal_id) + + if request.method == "GET": + task_list = [] + for task in goal.task_ids: + task_list.append(task) + + goal_and_tasks = {"goal": + {"id": goal.goal_id, + "title": goal.title, + "task_ids": task_list + }} + return jsonify(goal) + + if request.method == "POST": + goal_tasks = [] + form_data = request.get_json() + task_ids = form_data["task_ids"] + + for task_id in task_ids: + task = Task.query.get_or_404(task_id) + goal_tasks.append(task_id) + + db.session.commit() + + task_ids_response = [] + + for item in goal_tasks: + task_ids_response.append(item) + result_response_task_ids = ({ + "id": goal.goal_id, + "task_ids": task_ids_response + }) + return result_response_task_ids @tasks_bp.route("", methods=["GET", "POST"]) def handle_tasks(): @@ -146,7 +193,8 @@ def handle_tasks(): "id": task.task_id, "title": task.title, "description": task.description, - "is_complete": bool(task.completed_at) + "is_complete": bool(task.completed_at), + "goal": task.goal_id }) return jsonify(tasks_response) diff --git a/migrations/versions/4d5d03b10019_.py b/migrations/versions/4d5d03b10019_.py new file mode 100644 index 000000000..77f384c50 --- /dev/null +++ b/migrations/versions/4d5d03b10019_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 4d5d03b10019 +Revises: 5d3ef5e8db55 +Create Date: 2021-06-15 12:50:20.967617 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '4d5d03b10019' +down_revision = '5d3ef5e8db55' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_foreign_key(None, 'task', 'goal', ['goal_id'], ['goal_id']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'task', type_='foreignkey') + # ### end Alembic commands ### diff --git a/migrations/versions/5d3ef5e8db55_.py b/migrations/versions/5d3ef5e8db55_.py new file mode 100644 index 000000000..bca4af3cb --- /dev/null +++ b/migrations/versions/5d3ef5e8db55_.py @@ -0,0 +1,30 @@ +"""empty message + +Revision ID: 5d3ef5e8db55 +Revises: 29b91b61ddcf +Create Date: 2021-06-15 12:39:42.033091 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '5d3ef5e8db55' +down_revision = '29b91b61ddcf' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('task', sa.Column('goal_id', sa.Integer(), nullable=True)) + op.add_column('task', sa.Column('is_complete', sa.Boolean(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('task', 'is_complete') + op.drop_column('task', 'goal_id') + # ### end Alembic commands ### From e1a270361162d067cb12b579ce4c82f70684a68c Mon Sep 17 00:00:00 2001 From: Cloudy Lopez Date: Tue, 15 Jun 2021 16:16:00 -0700 Subject: [PATCH 08/11] created a one to many relationship. --- app/models/goal.py | 2 +- app/models/task.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/goal.py b/app/models/goal.py index 9e9e22154..640f5ea0a 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -8,4 +8,4 @@ class Goal(db.Model): goal_id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String) - task_ids = db.relationship('Task', backref='goal', lazy=True) \ No newline at end of file + tasks = db.relationship('Task', backref='goal', lazy=True) \ No newline at end of file diff --git a/app/models/task.py b/app/models/task.py index a299fdcd5..768e24345 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -10,4 +10,4 @@ class Task(db.Model): description = db.Column(db.String) completed_at = db.Column(db.DateTime, nullable=True, default=None) is_complete = db.Column(db.Boolean, default=False) - goal_id = db.Column(db.Integer, db.ForeignKey('goal.goal_id')) \ No newline at end of file + goal_id = db.Column(db.Integer, db.ForeignKey('goal.goal_id'), nullable=True) \ No newline at end of file From d13dcd4a85fa8f5851b399583956e5e8103e269d Mon Sep 17 00:00:00 2001 From: Cloudy Lopez Date: Tue, 15 Jun 2021 16:25:11 -0700 Subject: [PATCH 09/11] wave 6 completed - passing all tests --- app/routes.py | 110 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 67 insertions(+), 43 deletions(-) diff --git a/app/routes.py b/app/routes.py index 2c609b0c0..b958b7b31 100644 --- a/app/routes.py +++ b/app/routes.py @@ -28,9 +28,8 @@ def handle_goals(): if request.method == "POST": request_body = request.get_json() title = request_body.get("title") - task_ids = request.body["task_ids"] - if not title: + if "title" not in request_body: return jsonify({"details": "Invalid data"}), 400 new_goal = Goal(title=request_body["title"]) @@ -38,11 +37,12 @@ def handle_goals(): db.session.add(new_goal) db.session.commit() - committed_goal = {"goal": - {"id": new_goal.goal_id, - "title": new_goal.title, - "task_ids": new_goal.task_ids - }} + committed_goal = { + "goal": { + "id": new_goal.goal_id, + "title": new_goal.title + } + } return jsonify(committed_goal), 201 @@ -53,8 +53,7 @@ def handle_goals(): for goal in goals: goals_response.append({ "title": goal.title, - "id": goal.goal_id, - "tasks": goal.task_ids + "id": goal.goal_id }) return jsonify(goals_response), 200 @@ -69,7 +68,7 @@ def handle_goal(goal_id): tasks = [] - for item in goal.task_ids: + for item in goal.tasks: tasks.append(item) selected_goal = {"goal": @@ -78,7 +77,7 @@ def handle_goal(goal_id): "tasks": tasks }} return selected_goal - + elif request.method == "PUT": form_data = request.get_json() goal.title = form_data["title"] @@ -88,7 +87,6 @@ def handle_goal(goal_id): committed_goal = {"goal": {"id": goal.goal_id, "title": goal.title, - "task_ids": goal.task_ids }} return jsonify(committed_goal), 200 @@ -106,39 +104,45 @@ def handle_goal_tasks(goal_id): goal = Goal.query.get_or_404(goal_id) if request.method == "GET": - task_list = [] - for task in goal.task_ids: - task_list.append(task) + tasks = goal.tasks + list_of_tasks = [] - goal_and_tasks = {"goal": - {"id": goal.goal_id, + for task in tasks: + if task.completed_at == None: + completed_at = False + else: + completed_at = True + + individual_task = { + "id": task.task_id, + "title": task.title, + "description": task.description, + "is_complete": completed_at, + "goal_id": goal.goal_id + } + list_of_tasks.append(individual_task) + + return make_response({ + "id": goal.goal_id, "title": goal.title, - "task_ids": task_list - }} - return jsonify(goal) + "tasks": list_of_tasks + }) if request.method == "POST": - goal_tasks = [] - form_data = request.get_json() - task_ids = form_data["task_ids"] - - for task_id in task_ids: - task = Task.query.get_or_404(task_id) - goal_tasks.append(task_id) - - db.session.commit() + goal = Goal.query.get(goal_id) + request_body = request.get_json() - task_ids_response = [] + for ids_per_task in request_body["task_ids"]: + task = Task.query.get(ids_per_task) + goal.tasks.append(task) + db.session.add(goal) + db.session.commit() - for item in goal_tasks: - task_ids_response.append(item) - result_response_task_ids = ({ + return make_response({ "id": goal.goal_id, - "task_ids": task_ids_response + "task_ids": request_body["task_ids"] }) - return result_response_task_ids - @tasks_bp.route("", methods=["GET", "POST"]) def handle_tasks(): if request.method == "POST": @@ -205,13 +209,33 @@ def handle_task(task_id): return make_response("No matching task found", 404) if request.method == "GET": - selected_task = {"task": - {"id": task.task_id, - "title": task.title, - "description": task.description, - "is_complete": bool(task.completed_at) - }} - return jsonify(selected_task), 200 + + if task.completed_at == None: + completed_at = False + else: + completed_at = True + + selected_task = { + "task": { + "id": task.task_id, + "title": task.title, + "description": task.description, + "is_complete": completed_at + } + } + + if task.goal_id == None: + return make_response(selected_task) + else: + return make_response({ + "task": { + "id": task.task_id, + "title": task.title, + "description": task.description, + "is_complete": completed_at, + "goal_id": task.goal_id + } + }) elif request.method == "PUT": request_body = request.get_json() From 96a45097bd4c0a1f059df6e97c8b8cc84f2feee3 Mon Sep 17 00:00:00 2001 From: Cloudy Lopez Date: Tue, 15 Jun 2021 17:01:00 -0700 Subject: [PATCH 10/11] code clean up --- app/routes.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/app/routes.py b/app/routes.py index b958b7b31..d67e2d548 100644 --- a/app/routes.py +++ b/app/routes.py @@ -192,17 +192,22 @@ def handle_tasks(): tasks = Task.query.all() tasks_response = [] + for task in tasks: + if task.completed_at == None: + completed_at = False + else: + completed_at = True + tasks_response.append({ "id": task.task_id, "title": task.title, "description": task.description, - "is_complete": bool(task.completed_at), - "goal": task.goal_id + "is_complete": completed_at }) - return jsonify(tasks_response) + return jsonify(tasks_response), 200 -@tasks_bp.route("/", methods=["GET", "PUT", "DELETE", "PATCH"]) +@tasks_bp.route("/", methods=["GET", "PUT", "DELETE"]) def handle_task(task_id): task = Task.query.get(task_id) if task is None: @@ -275,12 +280,17 @@ def mark_task_complete(task_id): slack_message = f"A user just completed task: {task.title}" post_message_to_slack(slack_message) - completed_task = {"task": + if task.completed_at == None: + completed_at = False + else: + completed_at = True - {"id": task.task_id, + completed_task = { + "task": { + "id": task.task_id, "title": task.title, "description": task.description, - "is_complete": bool(task.completed_at) + "is_complete": completed_at }} return jsonify(completed_task), 200 From 05df0f36968e2fac5e738c432aa76045dcd9cf82 Mon Sep 17 00:00:00 2001 From: Cloudy Lopez Date: Wed, 16 Jun 2021 12:29:04 -0700 Subject: [PATCH 11/11] fixed an additional test --- app/routes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/routes.py b/app/routes.py index d67e2d548..864180562 100644 --- a/app/routes.py +++ b/app/routes.py @@ -5,7 +5,7 @@ from flask import request, Blueprint, make_response, jsonify# from datetime import datetime import os -import json, requests +import requests from dotenv import load_dotenv tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") @@ -21,7 +21,7 @@ def post_message_to_slack(text): 'text': text } headers = {'Authorization': f"Bearer {SLACK_TOKEN}"} - request.post(slack_path, params=query_params, headers=headers) + requests.post(slack_path, params=query_params, headers=headers) @goals_bp.route("", methods=["GET", "POST"]) def handle_goals():