From b2e3e83781d0b3ceb7c739ee8ece42853871592e Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 5 May 2022 16:30:22 -0500 Subject: [PATCH 01/49] Completed setup --- migrations/README | 1 + migrations/alembic.ini | 45 ++++++++++++++++++ migrations/env.py | 96 +++++++++++++++++++++++++++++++++++++++ migrations/script.py.mako | 24 ++++++++++ 4 files changed, 166 insertions(+) create mode 100644 migrations/README create mode 100644 migrations/alembic.ini create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako 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"} From 9cf350e26cf084b5a76d8e4ae34d53f0e38bb72d Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 5 May 2022 16:47:37 -0500 Subject: [PATCH 02/49] Created Task model --- app/models/task.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/models/task.py b/app/models/task.py index c91ab281f..af7519a27 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -3,3 +3,6 @@ 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, default=None) From f01c1f8723fd308ce379c51e67730cedd13178df Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 5 May 2022 17:03:09 -0500 Subject: [PATCH 03/49] Registered tasks_bp in __init__ file --- app/__init__.py | 2 ++ migrations/versions/4f06f64382a7_.py | 39 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 migrations/versions/4f06f64382a7_.py diff --git a/app/__init__.py b/app/__init__.py index 2764c4cc8..30052751d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -30,5 +30,7 @@ def create_app(test_config=None): migrate.init_app(app, db) # Register Blueprints here + from .routes import tasks_bp + app.register_blueprint(tasks_bp) return app diff --git a/migrations/versions/4f06f64382a7_.py b/migrations/versions/4f06f64382a7_.py new file mode 100644 index 000000000..9ae31e048 --- /dev/null +++ b/migrations/versions/4f06f64382a7_.py @@ -0,0 +1,39 @@ +"""empty message + +Revision ID: 4f06f64382a7 +Revises: +Create Date: 2022-05-05 16:59:55.390214 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '4f06f64382a7' +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.create_table('task', + sa.Column('task_id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.Column('completed_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('task_id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('task') + op.drop_table('goal') + # ### end Alembic commands ### From 04dcaeab0408ec222f81865fc1dd78137888b5d6 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 5 May 2022 18:42:46 -0500 Subject: [PATCH 04/49] Created create_task POST endpoint --- app/routes.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/routes.py b/app/routes.py index 3aae38d49..41f40e98f 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1 +1,18 @@ -from flask import Blueprint \ No newline at end of file +from flask import Blueprint, request, jsonify, make_response +from app import db +from app.models.task import Task + +tasks_bp = Blueprint("tasks_bp", __name__, url_prefix="/tasks") + +# create a new task +@tasks_bp.route("", methods=["POST"]) +def create_task(): + request_body = request.get_json() + new_task = Task(title=request_body["title"], description=request_body["description"]) + + db.session.add(new_task) + db.session.commit() + + response_body = {"task": new_task.to_dict()} + + return make_response(jsonify(response_body), 201) \ No newline at end of file From 484a5760db19dec5eb2fdf332871779b216aa553 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 5 May 2022 18:43:26 -0500 Subject: [PATCH 05/49] Added to_dict method to Task model, passed wave 01 --- app/models/task.py | 13 ++++++++++++- tests/test_wave_01.py | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/models/task.py b/app/models/task.py index af7519a27..b61d703bd 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -5,4 +5,15 @@ 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, default=None) + completed_at = db.Column(db.DateTime, nullable=True, default=None) + + def to_dict(self): + if self.completed_at == None: + self.completed_at = False + + return dict( + id=self.task_id, + title=self.title, + description=self.description, + is_complete=self.completed_at + ) \ No newline at end of file diff --git a/tests/test_wave_01.py b/tests/test_wave_01.py index dca626d78..292d2a5c4 100644 --- a/tests/test_wave_01.py +++ b/tests/test_wave_01.py @@ -66,7 +66,7 @@ def test_get_task_not_found(client): # ***************************************************************** -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_create_task(client): # Act response = client.post("/tasks", json={ From 0007e841e132c50f23a2504f20c998362f0361c4 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 5 May 2022 20:08:06 -0500 Subject: [PATCH 06/49] Created get_all_tasks GET endpoint --- app/routes.py | 13 ++++++++++++- tests/test_wave_01.py | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/routes.py b/app/routes.py index 41f40e98f..1c1ae4078 100644 --- a/app/routes.py +++ b/app/routes.py @@ -15,4 +15,15 @@ def create_task(): response_body = {"task": new_task.to_dict()} - return make_response(jsonify(response_body), 201) \ No newline at end of file + return make_response(jsonify(response_body), 201) + +# retrieve all tasks +@tasks_bp.route("", methods=["GET"]) +def get_all_tasks(): + tasks_response = [] + tasks = Task.query.all() + + for task in tasks: + tasks_response.append(task.to_dict()) + + return jsonify(tasks_response) \ No newline at end of file diff --git a/tests/test_wave_01.py b/tests/test_wave_01.py index 292d2a5c4..c059f3244 100644 --- a/tests/test_wave_01.py +++ b/tests/test_wave_01.py @@ -2,7 +2,7 @@ import pytest -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_tasks_no_saved_tasks(client): # Act response = client.get("/tasks") @@ -13,7 +13,7 @@ def test_get_tasks_no_saved_tasks(client): assert response_body == [] -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_tasks_one_saved_tasks(client, one_task): # Act response = client.get("/tasks") From b632bf4ba5dffbbe0d20891d5fde8b9279576488 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 5 May 2022 20:20:46 -0500 Subject: [PATCH 07/49] Created validate_task helper function --- app/routes.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/app/routes.py b/app/routes.py index 1c1ae4078..f0b07452e 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,4 +1,4 @@ -from flask import Blueprint, request, jsonify, make_response +from flask import Blueprint, request, jsonify, make_response, abort from app import db from app.models.task import Task @@ -26,4 +26,23 @@ def get_all_tasks(): for task in tasks: tasks_response.append(task.to_dict()) - return jsonify(tasks_response) \ No newline at end of file + return jsonify(tasks_response) + +# retrieve one task by id +@tasks_bp.route("/", methods=["GET"]) +def get_one_task_by_id(task_id): + pass + +# check for valid task using id +def validate_task(task_id): + try: + task_id = int(task_id) + except: + abort(make_response({"message": f"Task #{task_id} invalid"}, 400)) + + task = Task.query.get(task_id) + + if not task: + abort(make_response({"message": f"Task #{task_id} not found"}, 404)) + + return task From b88cb51d47a4aaebcfbbbbcd9070960752635ba4 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 5 May 2022 20:35:48 -0500 Subject: [PATCH 08/49] Created get_one_task_by_id GET endpoint --- app/routes.py | 4 +++- tests/test_wave_01.py | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/routes.py b/app/routes.py index f0b07452e..e11ef9b12 100644 --- a/app/routes.py +++ b/app/routes.py @@ -31,7 +31,9 @@ def get_all_tasks(): # retrieve one task by id @tasks_bp.route("/", methods=["GET"]) def get_one_task_by_id(task_id): - pass + task = validate_task(task_id) + + return {"task": task.to_dict()} # check for valid task using id def validate_task(task_id): diff --git a/tests/test_wave_01.py b/tests/test_wave_01.py index c059f3244..c4bcc5252 100644 --- a/tests/test_wave_01.py +++ b/tests/test_wave_01.py @@ -32,7 +32,7 @@ def test_get_tasks_one_saved_tasks(client, one_task): ] -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_task(client, one_task): # Act response = client.get("/tasks/1") @@ -51,7 +51,7 @@ def test_get_task(client, one_task): } -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_task_not_found(client): # Act response = client.get("/tasks/1") @@ -59,8 +59,9 @@ def test_get_task_not_found(client): # Assert assert response.status_code == 404 + assert response_body == {"message": f"Task #1 not found"} - raise Exception("Complete test with assertion about response body") + # raise Exception("Complete test with assertion about response body") # ***************************************************************** # **Complete test with assertion about response body*************** # ***************************************************************** From bd1ec2f478862ee22e40e43115e042cea607957e Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 5 May 2022 20:48:55 -0500 Subject: [PATCH 09/49] Created update_one_task_by_id PUT endpoint --- app/routes.py | 13 +++++++++++++ tests/test_wave_01.py | 5 +++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/routes.py b/app/routes.py index e11ef9b12..c1091eb1c 100644 --- a/app/routes.py +++ b/app/routes.py @@ -35,6 +35,19 @@ def get_one_task_by_id(task_id): return {"task": task.to_dict()} +# update one task by id +@tasks_bp.route("/", methods=["PUT"]) +def update_one_task_by_id(task_id): + task = validate_task(task_id) + request_body = request.get_json() + + task.title = request_body["title"] + task.description = request_body["description"] + + db.session.commit() + + return {"task": task.to_dict()} + # check for valid task using id def validate_task(task_id): try: diff --git a/tests/test_wave_01.py b/tests/test_wave_01.py index c4bcc5252..ac3befd47 100644 --- a/tests/test_wave_01.py +++ b/tests/test_wave_01.py @@ -94,7 +94,7 @@ def test_create_task(client): assert new_task.completed_at == None -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_update_task(client, one_task): # Act response = client.put("/tasks/1", json={ @@ -131,8 +131,9 @@ def test_update_task_not_found(client): # Assert assert response.status_code == 404 + assert response_body == {"message": f"Task #1 not found"} - raise Exception("Complete test with assertion about response body") + # raise Exception("Complete test with assertion about response body") # ***************************************************************** # **Complete test with assertion about response body*************** # ***************************************************************** From faa3165827ac73929f10592b6db9e2cd7791f8ce Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 5 May 2022 20:57:13 -0500 Subject: [PATCH 10/49] Created delete_task_by_id DELETE endpoint --- app/routes.py | 10 ++++++++++ tests/test_wave_01.py | 9 +++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/routes.py b/app/routes.py index c1091eb1c..edb77861d 100644 --- a/app/routes.py +++ b/app/routes.py @@ -48,6 +48,16 @@ def update_one_task_by_id(task_id): return {"task": task.to_dict()} +# delete one task by id +@tasks_bp.route("/", methods=["DELETE"]) +def delete_task_by_id(task_id): + task = validate_task(task_id) + + db.session.delete(task) + db.session.commit() + + return make_response({"details": f'Task {task_id} "{task.title}" successfully deleted'}) + # check for valid task using id def validate_task(task_id): try: diff --git a/tests/test_wave_01.py b/tests/test_wave_01.py index ac3befd47..e1d44414c 100644 --- a/tests/test_wave_01.py +++ b/tests/test_wave_01.py @@ -120,7 +120,7 @@ def test_update_task(client, one_task): assert task.completed_at == None -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_update_task_not_found(client): # Act response = client.put("/tasks/1", json={ @@ -139,7 +139,7 @@ def test_update_task_not_found(client): # ***************************************************************** -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_delete_task(client, one_task): # Act response = client.delete("/tasks/1") @@ -154,7 +154,7 @@ def test_delete_task(client, one_task): assert Task.query.get(1) == None -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_delete_task_not_found(client): # Act response = client.delete("/tasks/1") @@ -162,8 +162,9 @@ def test_delete_task_not_found(client): # Assert assert response.status_code == 404 + assert response_body == {"message": f"Task #1 not found"} - raise Exception("Complete test with assertion about response body") + # raise Exception("Complete test with assertion about response body") # ***************************************************************** # **Complete test with assertion about response body*************** # ***************************************************************** From cb32cc1d4016c6e8808483ede421e2690396ead3 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 5 May 2022 21:02:51 -0500 Subject: [PATCH 11/49] Refactored create_task endpoint to handle missing title or description --- app/routes.py | 6 +++++- tests/test_wave_01.py | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/routes.py b/app/routes.py index edb77861d..11e10f16c 100644 --- a/app/routes.py +++ b/app/routes.py @@ -8,7 +8,11 @@ @tasks_bp.route("", methods=["POST"]) def create_task(): request_body = request.get_json() - new_task = Task(title=request_body["title"], description=request_body["description"]) + + try: + new_task = Task(title=request_body["title"], description=request_body["description"]) + except: + abort(make_response({"details": "Invalid data"}, 400)) db.session.add(new_task) db.session.commit() diff --git a/tests/test_wave_01.py b/tests/test_wave_01.py index e1d44414c..117b4e7f3 100644 --- a/tests/test_wave_01.py +++ b/tests/test_wave_01.py @@ -172,7 +172,7 @@ def test_delete_task_not_found(client): assert Task.query.all() == [] -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_create_task_must_contain_title(client): # Act response = client.post("/tasks", json={ @@ -189,7 +189,7 @@ def test_create_task_must_contain_title(client): assert Task.query.all() == [] -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_create_task_must_contain_description(client): # Act response = client.post("/tasks", json={ From c84d03cc61265280779ca1731972b9f549cd0e33 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 5 May 2022 21:04:31 -0500 Subject: [PATCH 12/49] Refactored validate_task helper function for consistent error messaging --- app/routes.py | 4 ++-- tests/test_wave_01.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/routes.py b/app/routes.py index 11e10f16c..905608ede 100644 --- a/app/routes.py +++ b/app/routes.py @@ -67,11 +67,11 @@ def validate_task(task_id): try: task_id = int(task_id) except: - abort(make_response({"message": f"Task #{task_id} invalid"}, 400)) + abort(make_response({"details": f"Task #{task_id} invalid"}, 400)) task = Task.query.get(task_id) if not task: - abort(make_response({"message": f"Task #{task_id} not found"}, 404)) + abort(make_response({"details": f"Task #{task_id} not found"}, 404)) return task diff --git a/tests/test_wave_01.py b/tests/test_wave_01.py index 117b4e7f3..c94404da5 100644 --- a/tests/test_wave_01.py +++ b/tests/test_wave_01.py @@ -59,7 +59,7 @@ def test_get_task_not_found(client): # Assert assert response.status_code == 404 - assert response_body == {"message": f"Task #1 not found"} + assert response_body == {"details": f"Task #1 not found"} # raise Exception("Complete test with assertion about response body") # ***************************************************************** @@ -131,7 +131,7 @@ def test_update_task_not_found(client): # Assert assert response.status_code == 404 - assert response_body == {"message": f"Task #1 not found"} + assert response_body == {"details": f"Task #1 not found"} # raise Exception("Complete test with assertion about response body") # ***************************************************************** @@ -162,7 +162,7 @@ def test_delete_task_not_found(client): # Assert assert response.status_code == 404 - assert response_body == {"message": f"Task #1 not found"} + assert response_body == {"details": f"Task #1 not found"} # raise Exception("Complete test with assertion about response body") # ***************************************************************** From 0a6acc23d145a201d67e67cb3d419167a3c3f52a Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Wed, 11 May 2022 18:29:38 -0500 Subject: [PATCH 13/49] Adds ascending alphabetical sorting to get_all_tasks endpoint. --- app/routes.py | 13 +++++++++---- tests/test_wave_02.py | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/routes.py b/app/routes.py index 905608ede..c9121946e 100644 --- a/app/routes.py +++ b/app/routes.py @@ -24,11 +24,16 @@ def create_task(): # retrieve all tasks @tasks_bp.route("", methods=["GET"]) def get_all_tasks(): - tasks_response = [] - tasks = Task.query.all() + sort_param = request.args.get("sort") - for task in tasks: - tasks_response.append(task.to_dict()) + if sort_param == "asc": + tasks = Task.query.order_by(Task.title).all() + elif sort_param == "desc": + tasks = Task.query.order_by(Task.title).all() + else: + tasks = Task.query.all() + + tasks_response = [task.to_dict() for task in tasks] return jsonify(tasks_response) diff --git a/tests/test_wave_02.py b/tests/test_wave_02.py index a087e0909..544ab5a00 100644 --- a/tests/test_wave_02.py +++ b/tests/test_wave_02.py @@ -1,7 +1,7 @@ import pytest -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_tasks_sorted_asc(client, three_tasks): # Act response = client.get("/tasks?sort=asc") From 3d219949906a3b566c063fee903dbc6701eed79f Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Wed, 11 May 2022 18:31:59 -0500 Subject: [PATCH 14/49] Adds descending alphabetical sorting to get_all_tasks endpoint, passes wave 02 --- app/routes.py | 3 ++- tests/test_wave_02.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/routes.py b/app/routes.py index c9121946e..f72fcb2fe 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,4 +1,5 @@ from flask import Blueprint, request, jsonify, make_response, abort +from sqlalchemy import desc from app import db from app.models.task import Task @@ -29,7 +30,7 @@ def get_all_tasks(): if sort_param == "asc": tasks = Task.query.order_by(Task.title).all() elif sort_param == "desc": - tasks = Task.query.order_by(Task.title).all() + tasks = Task.query.order_by(desc(Task.title)).all() else: tasks = Task.query.all() diff --git a/tests/test_wave_02.py b/tests/test_wave_02.py index 544ab5a00..651e3aebd 100644 --- a/tests/test_wave_02.py +++ b/tests/test_wave_02.py @@ -29,7 +29,7 @@ def test_get_tasks_sorted_asc(client, three_tasks): ] -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_tasks_sorted_desc(client, three_tasks): # Act response = client.get("/tasks?sort=desc") From a43cd6025cdfbac5750201c9c7cc52623096878d Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Wed, 11 May 2022 19:36:38 -0500 Subject: [PATCH 15/49] Refactors Task model to_dict() method --- app/models/task.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/app/models/task.py b/app/models/task.py index b61d703bd..72542cf1d 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -8,12 +8,15 @@ class Task(db.Model): completed_at = db.Column(db.DateTime, nullable=True, default=None) def to_dict(self): - if self.completed_at == None: - self.completed_at = False + task_dict = { + "id": self.task_id, + "title": self.title, + "description": self.description, + } - return dict( - id=self.task_id, - title=self.title, - description=self.description, - is_complete=self.completed_at - ) \ No newline at end of file + if not self.completed_at: + task_dict["is_complete"] = False + else: + task_dict["is_complete"] = True + + return task_dict \ No newline at end of file From 97b9776b9a5fd21efa71948a0d4b3230e4e354e5 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Wed, 11 May 2022 20:01:48 -0500 Subject: [PATCH 16/49] Creates mark_task_complete endpoint --- app/routes.py | 12 ++++++++++++ tests/test_wave_03.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/routes.py b/app/routes.py index f72fcb2fe..ec56dda44 100644 --- a/app/routes.py +++ b/app/routes.py @@ -2,6 +2,7 @@ from sqlalchemy import desc from app import db from app.models.task import Task +from datetime import date tasks_bp = Blueprint("tasks_bp", __name__, url_prefix="/tasks") @@ -68,6 +69,17 @@ def delete_task_by_id(task_id): return make_response({"details": f'Task {task_id} "{task.title}" successfully deleted'}) +# mark one task complete by id +@tasks_bp.route("//mark_complete", methods=["PATCH"]) +def mark_task_complete(task_id): + task = validate_task(task_id) + + task.completed_at = date.today() + response_body = {"task": task.to_dict()} + + db.session.commit() + return make_response(jsonify(response_body), 200) + # check for valid task using id def validate_task(task_id): try: diff --git a/tests/test_wave_03.py b/tests/test_wave_03.py index 959176ceb..ffc49a5b4 100644 --- a/tests/test_wave_03.py +++ b/tests/test_wave_03.py @@ -5,7 +5,7 @@ import pytest -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_mark_complete_on_incomplete_task(client, one_task): # Arrange """ From 3ce71c25ca21252c946062f358d47ee5dabca44c Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Wed, 11 May 2022 20:08:31 -0500 Subject: [PATCH 17/49] Creates mark_task_incomplete endpoint --- app/routes.py | 11 +++++++++++ tests/test_wave_03.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/routes.py b/app/routes.py index ec56dda44..c4d9893a4 100644 --- a/app/routes.py +++ b/app/routes.py @@ -80,6 +80,17 @@ def mark_task_complete(task_id): db.session.commit() return make_response(jsonify(response_body), 200) +# mark one task incomplete by id +@tasks_bp.route("//mark_incomplete", methods=["PATCH"]) +def mark_task_incomplete(task_id): + task = validate_task(task_id) + + task.completed_at = None + response_body = {"task": task.to_dict()} + + db.session.commit() + return make_response(jsonify(response_body), 200) + # check for valid task using id def validate_task(task_id): try: diff --git a/tests/test_wave_03.py b/tests/test_wave_03.py index ffc49a5b4..befec2230 100644 --- a/tests/test_wave_03.py +++ b/tests/test_wave_03.py @@ -42,7 +42,7 @@ def test_mark_complete_on_incomplete_task(client, one_task): assert Task.query.get(1).completed_at -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_mark_incomplete_on_complete_task(client, completed_task): # Act response = client.patch("/tasks/1/mark_incomplete") From 9054dc1afc2c43eb37a72c0fe6c1e99b5d887641 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Wed, 11 May 2022 20:18:01 -0500 Subject: [PATCH 18/49] Refactors create_task endpoint to allow for completion functionality --- app/routes.py | 6 +++++- tests/test_wave_03.py | 16 +++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/app/routes.py b/app/routes.py index c4d9893a4..a73ac60e0 100644 --- a/app/routes.py +++ b/app/routes.py @@ -12,7 +12,11 @@ def create_task(): request_body = request.get_json() try: - new_task = Task(title=request_body["title"], description=request_body["description"]) + new_task = Task( + title=request_body["title"], + description=request_body["description"], + completed_at=request_body["completed_at"] + ) except: abort(make_response({"details": "Invalid data"}, 400)) diff --git a/tests/test_wave_03.py b/tests/test_wave_03.py index befec2230..78205520c 100644 --- a/tests/test_wave_03.py +++ b/tests/test_wave_03.py @@ -62,7 +62,7 @@ def test_mark_incomplete_on_complete_task(client, completed_task): assert Task.query.get(1).completed_at == None -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_mark_complete_on_completed_task(client, completed_task): # Arrange """ @@ -99,7 +99,7 @@ def test_mark_complete_on_completed_task(client, completed_task): assert Task.query.get(1).completed_at -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_mark_incomplete_on_incomplete_task(client, one_task): # Act response = client.patch("/tasks/1/mark_incomplete") @@ -119,7 +119,7 @@ def test_mark_incomplete_on_incomplete_task(client, one_task): assert Task.query.get(1).completed_at == None -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_mark_complete_missing_task(client): # Act response = client.patch("/tasks/1/mark_complete") @@ -127,14 +127,15 @@ def test_mark_complete_missing_task(client): # Assert assert response.status_code == 404 + assert response_body == {"details": f"Task #1 not found"} - raise Exception("Complete test with assertion about response body") + # raise Exception("Complete test with assertion about response body") # ***************************************************************** # **Complete test with assertion about response body*************** # ***************************************************************** -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_mark_incomplete_missing_task(client): # Act response = client.patch("/tasks/1/mark_incomplete") @@ -142,8 +143,9 @@ def test_mark_incomplete_missing_task(client): # Assert assert response.status_code == 404 + assert response_body == {"details": f"Task #1 not found"} - raise Exception("Complete test with assertion about response body") + # raise Exception("Complete test with assertion about response body") # ***************************************************************** # **Complete test with assertion about response body*************** # ***************************************************************** @@ -151,7 +153,7 @@ def test_mark_incomplete_missing_task(client): # Let's add this test for creating tasks, now that # the completion functionality has been implemented -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_create_task_with_valid_completed_at(client): # Act response = client.post("/tasks", json={ From f3844c683dff3ec231c14cae430a277a85518f32 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Wed, 11 May 2022 20:19:07 -0500 Subject: [PATCH 19/49] Refactors update_one_task_by_id endpoint to allow for completion functionality, passes wave 03 --- app/routes.py | 1 + tests/test_wave_03.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/routes.py b/app/routes.py index a73ac60e0..bff056efc 100644 --- a/app/routes.py +++ b/app/routes.py @@ -58,6 +58,7 @@ def update_one_task_by_id(task_id): task.title = request_body["title"] task.description = request_body["description"] + task.completed_at = request_body["completed_at"] db.session.commit() diff --git a/tests/test_wave_03.py b/tests/test_wave_03.py index 78205520c..5ded30417 100644 --- a/tests/test_wave_03.py +++ b/tests/test_wave_03.py @@ -183,7 +183,7 @@ def test_create_task_with_valid_completed_at(client): # Let's add this test for updating tasks, now that # the completion functionality has been implemented -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_update_task_with_completed_at_date(client, completed_task): # Act response = client.put("/tasks/1", json={ From bd3256fb197abde90d608ed529c7fcaf28b17e7c Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Wed, 11 May 2022 21:18:14 -0500 Subject: [PATCH 20/49] Refactors create_task and update_one_task_by_id endpoints (in progress) --- app/routes.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/routes.py b/app/routes.py index bff056efc..7b2703cdc 100644 --- a/app/routes.py +++ b/app/routes.py @@ -15,10 +15,12 @@ def create_task(): new_task = Task( title=request_body["title"], description=request_body["description"], - completed_at=request_body["completed_at"] ) except: abort(make_response({"details": "Invalid data"}, 400)) + + if request_body["completed_at"]: + new_task.completed_at = request_body["completed_at"] db.session.add(new_task) db.session.commit() @@ -58,7 +60,9 @@ def update_one_task_by_id(task_id): task.title = request_body["title"] task.description = request_body["description"] - task.completed_at = request_body["completed_at"] + + if request_body["completed_at"]: + task.completed_at = request_body["completed_at"] db.session.commit() @@ -83,6 +87,7 @@ def mark_task_complete(task_id): response_body = {"task": task.to_dict()} db.session.commit() + return make_response(jsonify(response_body), 200) # mark one task incomplete by id From 58d75957ba1c3f00e0847ce7c7dc92ae9b28bab1 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 12 May 2022 12:51:17 -0500 Subject: [PATCH 21/49] Refactors create_task endpoint to check for completed_at value, passes waves 1-3 --- app/routes.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/routes.py b/app/routes.py index 7b2703cdc..4a4f014bd 100644 --- a/app/routes.py +++ b/app/routes.py @@ -3,6 +3,7 @@ from app import db from app.models.task import Task from datetime import date +import os tasks_bp = Blueprint("tasks_bp", __name__, url_prefix="/tasks") @@ -14,13 +15,13 @@ def create_task(): try: new_task = Task( title=request_body["title"], - description=request_body["description"], + description=request_body["description"] ) except: abort(make_response({"details": "Invalid data"}, 400)) - if request_body["completed_at"]: - new_task.completed_at = request_body["completed_at"] + if request_body.get("completed_at"): + new_task.completed_at = request_body.get("completed_at") db.session.add(new_task) db.session.commit() @@ -61,9 +62,6 @@ def update_one_task_by_id(task_id): task.title = request_body["title"] task.description = request_body["description"] - if request_body["completed_at"]: - task.completed_at = request_body["completed_at"] - db.session.commit() return {"task": task.to_dict()} From 8765a46b0edace25e0fed6f60b9d2575048d649a Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 12 May 2022 14:37:37 -0500 Subject: [PATCH 22/49] Creates post_slack_completion_message helper function, passes wave 04 --- app/routes.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/app/routes.py b/app/routes.py index 4a4f014bd..f578bacae 100644 --- a/app/routes.py +++ b/app/routes.py @@ -3,7 +3,12 @@ from app import db from app.models.task import Task from datetime import date + +# imports for slackbot import os +import logging +from slack_sdk import WebClient +from slack_sdk.errors import SlackApiError tasks_bp = Blueprint("tasks_bp", __name__, url_prefix="/tasks") @@ -85,6 +90,7 @@ def mark_task_complete(task_id): response_body = {"task": task.to_dict()} db.session.commit() + post_slack_completion_message(task_id) return make_response(jsonify(response_body), 200) @@ -112,3 +118,21 @@ def validate_task(task_id): abort(make_response({"details": f"Task #{task_id} not found"}, 404)) return task + +# post completion message to slack +def post_slack_completion_message(task_id): + client = WebClient(token=os.environ.get("SLACK_BOT_TOKEN")) + logger = logging.getLogger(__name__) + channel_id = "C03EP2Q0WK1" + + task = validate_task(task_id) + + try: + result = client.chat_postMessage( + channel=channel_id, + text=f"Someone just completed the task {task.title}" + ) + logger.info(result) + + except SlackApiError as e: + logger.error(f"Error posting message: {e}") \ No newline at end of file From 9fc8fa9f93225b65d7c810caaab702b6241f281f Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 12 May 2022 14:58:14 -0500 Subject: [PATCH 23/49] Create Goal model --- app/models/goal.py | 1 + .../versions/c568882e1c90_add_goal_model.py | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 migrations/versions/c568882e1c90_add_goal_model.py diff --git a/app/models/goal.py b/app/models/goal.py index b0ed11dd8..4d7f174eb 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -3,3 +3,4 @@ class Goal(db.Model): goal_id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String, nullable=False) diff --git a/migrations/versions/c568882e1c90_add_goal_model.py b/migrations/versions/c568882e1c90_add_goal_model.py new file mode 100644 index 000000000..9c1aacf86 --- /dev/null +++ b/migrations/versions/c568882e1c90_add_goal_model.py @@ -0,0 +1,28 @@ +"""Add Goal model + +Revision ID: c568882e1c90 +Revises: 4f06f64382a7 +Create Date: 2022-05-12 14:56:34.152688 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'c568882e1c90' +down_revision = '4f06f64382a7' +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=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('goal', 'title') + # ### end Alembic commands ### From 27ccc02bafd90ac72275fd4ae1995b79adcc4975 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 12 May 2022 16:05:46 -0500 Subject: [PATCH 24/49] Separate route files into goal_routes.py and task_routes.py --- app/goal_routes.py | 0 app/{routes.py => task_routes.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/goal_routes.py rename app/{routes.py => task_routes.py} (100%) diff --git a/app/goal_routes.py b/app/goal_routes.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/routes.py b/app/task_routes.py similarity index 100% rename from app/routes.py rename to app/task_routes.py From f831a72aa95cf9b33697c7e233371b6c9175791f Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 12 May 2022 16:08:57 -0500 Subject: [PATCH 25/49] Create goals bp, register in __init__ file --- app/__init__.py | 5 ++++- app/goal_routes.py | 6 ++++++ app/models/goal.py | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 30052751d..4d510c552 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -30,7 +30,10 @@ def create_app(test_config=None): migrate.init_app(app, db) # Register Blueprints here - from .routes import tasks_bp + from .task_routes import tasks_bp app.register_blueprint(tasks_bp) + from .goal_routes import goals_bp + app.register_blueprint(goals_bp) + return app diff --git a/app/goal_routes.py b/app/goal_routes.py index e69de29bb..08b6ddd6e 100644 --- a/app/goal_routes.py +++ b/app/goal_routes.py @@ -0,0 +1,6 @@ +from flask import Blueprint, request, jsonify, make_response, abort +from app import db +from app.models.goal import Goal + +goals_bp = Blueprint("goals_bp", __name__, url_prefix="/goals") + diff --git a/app/models/goal.py b/app/models/goal.py index 4d7f174eb..259212684 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -3,4 +3,4 @@ class Goal(db.Model): goal_id = db.Column(db.Integer, primary_key=True) - title = db.Column(db.String, nullable=False) + title = db.Column(db.String, nullable=False) \ No newline at end of file From e166d4a3d3643b5bc9c57e72d532bf97f708c310 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 12 May 2022 16:13:08 -0500 Subject: [PATCH 26/49] Create to_dict() method for Goal model --- app/models/goal.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/models/goal.py b/app/models/goal.py index 259212684..3e53b89cb 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -3,4 +3,12 @@ class Goal(db.Model): goal_id = db.Column(db.Integer, primary_key=True) - title = db.Column(db.String, nullable=False) \ No newline at end of file + title = db.Column(db.String, nullable=False) + + def to_dict(self): + goal_dict = { + "id": self.goal_id, + "title": self.title + } + + return goal_dict \ No newline at end of file From 3ea0767bcf6a3f7880c67ad9fc6051310baf7531 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 12 May 2022 19:30:19 -0500 Subject: [PATCH 27/49] Add create_goal endpoint --- app/goal_routes.py | 11 +++++++++++ tests/test_wave_05.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/goal_routes.py b/app/goal_routes.py index 08b6ddd6e..cfc8c7c17 100644 --- a/app/goal_routes.py +++ b/app/goal_routes.py @@ -4,3 +4,14 @@ goals_bp = Blueprint("goals_bp", __name__, url_prefix="/goals") +@goals_bp.route("", methods=["POST"]) +def create_goal(): + request_body = request.get_json() + new_goal = Goal(title=request_body["title"]) + + db.session.add(new_goal) + db.session.commit() + + response_body = {"goal": new_goal.to_dict()} + + return make_response(jsonify(response_body), 201) \ No newline at end of file diff --git a/tests/test_wave_05.py b/tests/test_wave_05.py index aee7c52a1..33fe33fde 100644 --- a/tests/test_wave_05.py +++ b/tests/test_wave_05.py @@ -61,7 +61,7 @@ def test_get_goal_not_found(client): # ---- Complete Test ---- -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_create_goal(client): # Act response = client.post("/goals", json={ From 0309358529df5e3ab38b8a560eb3e6e3d82b75d7 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 12 May 2022 19:52:43 -0500 Subject: [PATCH 28/49] Add get_all_goals() endpoint --- app/goal_routes.py | 11 ++++++++++- tests/test_wave_05.py | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/goal_routes.py b/app/goal_routes.py index cfc8c7c17..ab0abda6e 100644 --- a/app/goal_routes.py +++ b/app/goal_routes.py @@ -4,6 +4,7 @@ goals_bp = Blueprint("goals_bp", __name__, url_prefix="/goals") +# create a new goal @goals_bp.route("", methods=["POST"]) def create_goal(): request_body = request.get_json() @@ -14,4 +15,12 @@ def create_goal(): response_body = {"goal": new_goal.to_dict()} - return make_response(jsonify(response_body), 201) \ No newline at end of file + return make_response(jsonify(response_body), 201) + +# get all goals +@goals_bp.route("", methods=["GET"]) +def get_all_goals(): + goals = Goal.query.all() + goals_response = [goal.to_dict() for goal in goals] + + return jsonify(goals_response) \ No newline at end of file diff --git a/tests/test_wave_05.py b/tests/test_wave_05.py index 33fe33fde..19b29115e 100644 --- a/tests/test_wave_05.py +++ b/tests/test_wave_05.py @@ -1,7 +1,7 @@ import pytest -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_goals_no_saved_goals(client): # Act response = client.get("/goals") @@ -12,7 +12,7 @@ def test_get_goals_no_saved_goals(client): assert response_body == [] -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_goals_one_saved_goal(client, one_goal): # Act response = client.get("/goals") From ed07c6ef58c349f5b5bc3d1c0e97e8977bf8fb3a Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 12 May 2022 19:59:36 -0500 Subject: [PATCH 29/49] Create validate_goal helper function --- app/goal_routes.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/app/goal_routes.py b/app/goal_routes.py index ab0abda6e..90c422d7f 100644 --- a/app/goal_routes.py +++ b/app/goal_routes.py @@ -23,4 +23,22 @@ def get_all_goals(): goals = Goal.query.all() goals_response = [goal.to_dict() for goal in goals] - return jsonify(goals_response) \ No newline at end of file + return jsonify(goals_response) + +# get goal by id +@goals_bp.route("/", methods=["GET"]) +def get_goal_by_id(goal_id): + pass + +def validate_goal(goal_id): + try: + goal_id = int(goal_id) + except: + abort(make_response({"details": f"Goal '{goal_id}' invalid"})) + + goal = Goal.query.get(goal_id) + + if not goal: + abort(make_response({"details": f"Goal '{goal_id}' not found"})) + + return goal \ No newline at end of file From 98512a163543b19ad2b44bd80e1d78aff8f296b0 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 12 May 2022 20:15:49 -0500 Subject: [PATCH 30/49] Create get_goal_by_id endpoint --- app/goal_routes.py | 9 ++++++--- tests/test_wave_05.py | 11 ++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/goal_routes.py b/app/goal_routes.py index 90c422d7f..f28b004f7 100644 --- a/app/goal_routes.py +++ b/app/goal_routes.py @@ -28,17 +28,20 @@ def get_all_goals(): # get goal by id @goals_bp.route("/", methods=["GET"]) def get_goal_by_id(goal_id): - pass + goal = validate_goal(goal_id) + return {"goal": goal.to_dict()} + + def validate_goal(goal_id): try: goal_id = int(goal_id) except: - abort(make_response({"details": f"Goal '{goal_id}' invalid"})) + abort(make_response({"details": f"Goal #{goal_id} invalid"}, 400)) goal = Goal.query.get(goal_id) if not goal: - abort(make_response({"details": f"Goal '{goal_id}' not found"})) + abort(make_response({"details": f"Goal #{goal_id} not found"}, 404)) return goal \ No newline at end of file diff --git a/tests/test_wave_05.py b/tests/test_wave_05.py index 19b29115e..b6ae720f0 100644 --- a/tests/test_wave_05.py +++ b/tests/test_wave_05.py @@ -29,7 +29,7 @@ def test_get_goals_one_saved_goal(client, one_goal): ] -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_goal(client, one_goal): # Act response = client.get("/goals/1") @@ -46,18 +46,19 @@ def test_get_goal(client, one_goal): } -@pytest.mark.skip(reason="test to be completed by student") +# @pytest.mark.skip(reason="test to be completed by student") def test_get_goal_not_found(client): pass # Act response = client.get("/goals/1") response_body = response.get_json() - raise Exception("Complete test") + # raise Exception("Complete test") + # Assert # ---- Complete Test ---- - # assertion 1 goes here - # assertion 2 goes here + assert response_body == {"details": f"Goal #1 not found"} + assert response.status_code == 404 # ---- Complete Test ---- From 51753f6d63843e23fc5edf6b2905809d821ae749 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 12 May 2022 21:32:28 -0500 Subject: [PATCH 31/49] Create update_goal endpoint --- app/goal_routes.py | 10 ++++++++++ tests/test_wave_05.py | 21 ++++++++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/app/goal_routes.py b/app/goal_routes.py index f28b004f7..64634f51f 100644 --- a/app/goal_routes.py +++ b/app/goal_routes.py @@ -31,8 +31,18 @@ def get_goal_by_id(goal_id): goal = validate_goal(goal_id) return {"goal": goal.to_dict()} +# update goal by id +@goals_bp.route("/", methods=["PUT"]) +def update_goal(goal_id): + goal = validate_goal(goal_id) + + request_body = request.get_json() + goal.title = request_body["title"] + + return {"goal": goal.to_dict()} +# helper function to validate goal by id def validate_goal(goal_id): try: goal_id = int(goal_id) diff --git a/tests/test_wave_05.py b/tests/test_wave_05.py index b6ae720f0..02e3af6fc 100644 --- a/tests/test_wave_05.py +++ b/tests/test_wave_05.py @@ -1,4 +1,5 @@ import pytest +from app.models.goal import Goal # @pytest.mark.skip(reason="No way to test this feature yet") @@ -81,17 +82,27 @@ def test_create_goal(client): } -@pytest.mark.skip(reason="test to be completed by student") +# @pytest.mark.skip(reason="test to be completed by student") def test_update_goal(client, one_goal): - raise Exception("Complete test") + # raise Exception("Complete test") # Act # ---- Complete Act Here ---- + response = client.put("/goals/1", json={ + "title": "Updated Goal Title" + }) + response_body = response.get_json() # Assert # ---- Complete Assertions Here ---- - # assertion 1 goes here - # assertion 2 goes here - # assertion 3 goes here + assert response.status_code == 200 + assert "goal" in response_body + assert response_body == { + "goal": { + "id": 1, + "title": "Updated Goal Title" + } + } + # ---- Complete Assertions Here ---- From 8e084943e9058872799f39fc0a4d9df7bf4d059e Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 12 May 2022 21:35:21 -0500 Subject: [PATCH 32/49] Refactor update_goal endpoint to upgrade db --- app/goal_routes.py | 7 +++++++ tests/test_wave_05.py | 2 ++ 2 files changed, 9 insertions(+) diff --git a/app/goal_routes.py b/app/goal_routes.py index 64634f51f..08697b078 100644 --- a/app/goal_routes.py +++ b/app/goal_routes.py @@ -39,8 +39,15 @@ def update_goal(goal_id): request_body = request.get_json() goal.title = request_body["title"] + db.session.commit() return {"goal": goal.to_dict()} +# delete goal by id +@goals_bp.route("/", methods=["DELETE"]) +def delete_goal(goal_id): + goal = validate_goal(goal_id) + + # helper function to validate goal by id def validate_goal(goal_id): diff --git a/tests/test_wave_05.py b/tests/test_wave_05.py index 02e3af6fc..25b547f72 100644 --- a/tests/test_wave_05.py +++ b/tests/test_wave_05.py @@ -91,6 +91,7 @@ def test_update_goal(client, one_goal): "title": "Updated Goal Title" }) response_body = response.get_json() + goal = Goal.query.get(1) # Assert # ---- Complete Assertions Here ---- @@ -102,6 +103,7 @@ def test_update_goal(client, one_goal): "title": "Updated Goal Title" } } + assert goal.title == "Updated Goal Title" # ---- Complete Assertions Here ---- From 8fe168fa025f498fe770ff182bdf923a9a975f95 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 12 May 2022 21:48:27 -0500 Subject: [PATCH 33/49] Create delete_goal() endpoint --- app/goal_routes.py | 3 +++ tests/test_wave_05.py | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/goal_routes.py b/app/goal_routes.py index 08697b078..638c69984 100644 --- a/app/goal_routes.py +++ b/app/goal_routes.py @@ -47,7 +47,10 @@ def update_goal(goal_id): def delete_goal(goal_id): goal = validate_goal(goal_id) + db.session.delete(goal) + db.session.commit() + return make_response({"details": f'Goal {goal_id} \"{goal.title}\" successfully deleted'}) # helper function to validate goal by id def validate_goal(goal_id): diff --git a/tests/test_wave_05.py b/tests/test_wave_05.py index 25b547f72..9a6428fc1 100644 --- a/tests/test_wave_05.py +++ b/tests/test_wave_05.py @@ -121,7 +121,7 @@ def test_update_goal_not_found(client): # ---- Complete Assertions Here ---- -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_delete_goal(client, one_goal): # Act response = client.delete("/goals/1") @@ -137,8 +137,9 @@ def test_delete_goal(client, one_goal): # Check that the goal was deleted response = client.get("/goals/1") assert response.status_code == 404 + assert response.get_json() == {"details": "Goal #1 not found"} - raise Exception("Complete test with assertion about response body") + # raise Exception("Complete test with assertion about response body") # ***************************************************************** # **Complete test with assertion about response body*************** # ***************************************************************** From b2cafb66c31b77778fd208bf751e4b4a15bb7a66 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 12 May 2022 21:56:25 -0500 Subject: [PATCH 34/49] Refactor create_goal() endpoint to handle empty request body, pass wave 05 --- app/goal_routes.py | 3 +++ tests/test_wave_05.py | 24 +++++++++++++++--------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/app/goal_routes.py b/app/goal_routes.py index 638c69984..df53cb95b 100644 --- a/app/goal_routes.py +++ b/app/goal_routes.py @@ -7,6 +7,9 @@ # create a new goal @goals_bp.route("", methods=["POST"]) def create_goal(): + if not request.get_json(): + abort(make_response({"details": "Invalid data"}, 400)) + request_body = request.get_json() new_goal = Goal(title=request_body["title"]) diff --git a/tests/test_wave_05.py b/tests/test_wave_05.py index 9a6428fc1..237640075 100644 --- a/tests/test_wave_05.py +++ b/tests/test_wave_05.py @@ -108,16 +108,20 @@ def test_update_goal(client, one_goal): # ---- Complete Assertions Here ---- -@pytest.mark.skip(reason="test to be completed by student") +# @pytest.mark.skip(reason="test to be completed by student") def test_update_goal_not_found(client): - raise Exception("Complete test") + # raise Exception("Complete test") # Act # ---- Complete Act Here ---- + response = client.put("/goals/1", json={ + "title": "Updated Goal Title", + }) + response_body = response.get_json() # Assert # ---- Complete Assertions Here ---- - # assertion 1 goes here - # assertion 2 goes here + assert response.status_code == 404 + assert response_body == {"details": f"Goal #1 not found"} # ---- Complete Assertions Here ---- @@ -145,21 +149,23 @@ def test_delete_goal(client, one_goal): # ***************************************************************** -@pytest.mark.skip(reason="test to be completed by student") +# @pytest.mark.skip(reason="test to be completed by student") def test_delete_goal_not_found(client): - raise Exception("Complete test") + # raise Exception("Complete test") # Act # ---- Complete Act Here ---- + response = client.delete("/goals/1") + response_body = response.get_json() # Assert # ---- Complete Assertions Here ---- - # assertion 1 goes here - # assertion 2 goes here + assert response.status_code == 404 + assert response_body == {"details": f"Goal #1 not found"} # ---- Complete Assertions Here ---- -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_create_goal_missing_title(client): # Act response = client.post("/goals", json={}) From d4bac3f80eae464ff9bd167d8896881ad0a1cb77 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 12 May 2022 22:04:00 -0500 Subject: [PATCH 35/49] Create one-to-many relationship between goals and tasks --- app/models/goal.py | 1 + app/models/task.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/models/goal.py b/app/models/goal.py index 3e53b89cb..0fb996990 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -4,6 +4,7 @@ class Goal(db.Model): goal_id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String, nullable=False) + tasks = db.relationship("Task", back_populates="goal") def to_dict(self): goal_dict = { diff --git a/app/models/task.py b/app/models/task.py index 72542cf1d..bcc6ddf96 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -6,6 +6,8 @@ class Task(db.Model): title = db.Column(db.String) description = db.Column(db.String) completed_at = db.Column(db.DateTime, nullable=True, default=None) + goal_id = db.Column(db.Integer, db.ForeignKey("goal.id")) + goal = db.relationship("Goal", back_populates="tasks") def to_dict(self): task_dict = { From 7c6e7e4a0839d089f6fcb2c1bc85a090e7a2e938 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 12 May 2022 22:05:20 -0500 Subject: [PATCH 36/49] Fix foreign key attribute error in Task model --- app/models/task.py | 2 +- ...reate_one_to_many_relationship_between_.py | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 migrations/versions/00eaa6151070_create_one_to_many_relationship_between_.py diff --git a/app/models/task.py b/app/models/task.py index bcc6ddf96..37280837d 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -6,7 +6,7 @@ class Task(db.Model): title = db.Column(db.String) description = db.Column(db.String) completed_at = db.Column(db.DateTime, nullable=True, default=None) - goal_id = db.Column(db.Integer, db.ForeignKey("goal.id")) + goal_id = db.Column(db.Integer, db.ForeignKey("goal.goal_id")) goal = db.relationship("Goal", back_populates="tasks") def to_dict(self): diff --git a/migrations/versions/00eaa6151070_create_one_to_many_relationship_between_.py b/migrations/versions/00eaa6151070_create_one_to_many_relationship_between_.py new file mode 100644 index 000000000..97126854c --- /dev/null +++ b/migrations/versions/00eaa6151070_create_one_to_many_relationship_between_.py @@ -0,0 +1,30 @@ +"""Create one-to-many relationship between goals and tasks + +Revision ID: 00eaa6151070 +Revises: c568882e1c90 +Create Date: 2022-05-12 22:04:45.113669 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '00eaa6151070' +down_revision = 'c568882e1c90' +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.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') + op.drop_column('task', 'goal_id') + # ### end Alembic commands ### From ab086d22bbedf97db6279daae9c9b69de7be2c0a Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 12 May 2022 22:22:03 -0500 Subject: [PATCH 37/49] Create get_tasks_for_goal endpoint --- app/goal_routes.py | 11 +++++++++++ app/models/goal.py | 3 +++ app/models/task.py | 3 +++ tests/test_wave_06.py | 2 +- 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/goal_routes.py b/app/goal_routes.py index df53cb95b..e0a7dd284 100644 --- a/app/goal_routes.py +++ b/app/goal_routes.py @@ -55,6 +55,17 @@ def delete_goal(goal_id): return make_response({"details": f'Goal {goal_id} \"{goal.title}\" successfully deleted'}) +# get tasks by goal id +@goals_bp.route("//tasks", methods=["GET"]) +def get_tasks_for_goal(goal_id): + goal = validate_goal(goal_id) + task_list = [task.to_dict() for task in goal.tasks] + + goal_dict = goal.to_dict() + goal_dict["tasks"] = task_list + + return jsonify(goal_dict) + # helper function to validate goal by id def validate_goal(goal_id): try: diff --git a/app/models/goal.py b/app/models/goal.py index 0fb996990..96355ce51 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -12,4 +12,7 @@ def to_dict(self): "title": self.title } + if self.tasks: + goal_dict["tasks"] = [] + return goal_dict \ No newline at end of file diff --git a/app/models/task.py b/app/models/task.py index 37280837d..266cee6ec 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -20,5 +20,8 @@ def to_dict(self): task_dict["is_complete"] = False else: task_dict["is_complete"] = True + + if self.goal_id: + task_dict["goal_id"] = self.goal_id return task_dict \ No newline at end of file diff --git a/tests/test_wave_06.py b/tests/test_wave_06.py index 8afa4325e..96995e3b8 100644 --- a/tests/test_wave_06.py +++ b/tests/test_wave_06.py @@ -74,7 +74,7 @@ def test_get_tasks_for_specific_goal_no_tasks(client, one_goal): } -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_tasks_for_specific_goal(client, one_task_belongs_to_one_goal): # Act response = client.get("/goals/1/tasks") From b2a26e4e1158b4538603ceabc60d1de832a84c64 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 12 May 2022 22:53:25 -0500 Subject: [PATCH 38/49] Create post_tasks_to_goal endpoint --- app/goal_routes.py | 20 ++++++++++++++++++++ tests/test_wave_06.py | 11 ++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/app/goal_routes.py b/app/goal_routes.py index e0a7dd284..2bd62939b 100644 --- a/app/goal_routes.py +++ b/app/goal_routes.py @@ -1,6 +1,8 @@ from flask import Blueprint, request, jsonify, make_response, abort from app import db from app.models.goal import Goal +from app.models.task import Task +from app.task_routes import validate_task goals_bp = Blueprint("goals_bp", __name__, url_prefix="/goals") @@ -66,6 +68,24 @@ def get_tasks_for_goal(goal_id): return jsonify(goal_dict) +# post tasks to goal by goal id +@goals_bp.route("//tasks", methods=["POST"]) +def post_tasks_to_goal(goal_id): + goal = validate_goal(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_id + task.goal = goal + + db.session.commit() + + return make_response({ + "id": goal.goal_id, + "task_ids": request_body["task_ids"] + }) + # helper function to validate goal by id def validate_goal(goal_id): try: diff --git a/tests/test_wave_06.py b/tests/test_wave_06.py index 96995e3b8..71c85dc29 100644 --- a/tests/test_wave_06.py +++ b/tests/test_wave_06.py @@ -2,7 +2,7 @@ import pytest -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_post_task_ids_to_goal(client, one_goal, three_tasks): # Act response = client.post("/goals/1/tasks", json={ @@ -42,7 +42,7 @@ def test_post_task_ids_to_goal_already_with_goals(client, one_task_belongs_to_on assert len(Goal.query.get(1).tasks) == 2 -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_tasks_for_specific_goal_no_goal(client): # Act response = client.get("/goals/1/tasks") @@ -50,14 +50,15 @@ def test_get_tasks_for_specific_goal_no_goal(client): # Assert assert response.status_code == 404 + assert response_body == {"details": f"Goal #1 not found"} - raise Exception("Complete test with assertion about response body") + # raise Exception("Complete test with assertion about response body") # ***************************************************************** # **Complete test with assertion about response body*************** # ***************************************************************** -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_tasks_for_specific_goal_no_tasks(client, one_goal): # Act response = client.get("/goals/1/tasks") @@ -99,7 +100,7 @@ def test_get_tasks_for_specific_goal(client, one_task_belongs_to_one_goal): } -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_task_includes_goal_id(client, one_task_belongs_to_one_goal): response = client.get("/tasks/1") response_body = response.get_json() From f58e31bed24a79fbde24cd680aa9e89959abbafe Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 12 May 2022 22:54:35 -0500 Subject: [PATCH 39/49] Pass all tests --- tests/test_wave_06.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_wave_06.py b/tests/test_wave_06.py index 71c85dc29..c42d70c18 100644 --- a/tests/test_wave_06.py +++ b/tests/test_wave_06.py @@ -23,7 +23,7 @@ def test_post_task_ids_to_goal(client, one_goal, three_tasks): assert len(Goal.query.get(1).tasks) == 3 -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_post_task_ids_to_goal_already_with_goals(client, one_task_belongs_to_one_goal, three_tasks): # Act response = client.post("/goals/1/tasks", json={ From d91c67a296e2118b624e4a90cb9e88934162aa09 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 12 May 2022 22:58:06 -0500 Subject: [PATCH 40/49] Add Procfile for Heroku deployment --- Procfile | 1 + 1 file changed, 1 insertion(+) create mode 100644 Procfile diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..62e430aca --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: gunicorn 'app:create_app()' \ No newline at end of file From 0a6677e3cb9e68dfd2d69987de0885bf9c8d9a7d Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Thu, 12 May 2022 23:11:32 -0500 Subject: [PATCH 41/49] Add slack-sdk to project requirements --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 30ff414fe..4e3b88829 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ blinker==1.4 certifi==2020.12.5 chardet==4.0.0 click==7.1.2 +coverage==6.3.2 Flask==1.1.2 Flask-Migrate==2.6.0 Flask-SQLAlchemy==2.4.4 @@ -28,6 +29,7 @@ python-dotenv==0.15.0 python-editor==1.0.4 requests==2.25.1 six==1.15.0 +slack-sdk==3.16.1 SQLAlchemy==1.3.23 toml==0.10.2 urllib3==1.26.5 From bc3e7a15edfe9b8fe03f53a6dc9e824ba165ec23 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Fri, 13 May 2022 10:30:45 -0500 Subject: [PATCH 42/49] Add error_message helper function in route_helpers file --- app/goal_routes.py | 10 ++++++---- app/route_helpers.py | 5 +++++ 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 app/route_helpers.py diff --git a/app/goal_routes.py b/app/goal_routes.py index 2bd62939b..4db326b7d 100644 --- a/app/goal_routes.py +++ b/app/goal_routes.py @@ -2,19 +2,21 @@ from app import db from app.models.goal import Goal from app.models.task import Task -from app.task_routes import validate_task +from app.route_helpers import error_message goals_bp = Blueprint("goals_bp", __name__, url_prefix="/goals") # create a new goal @goals_bp.route("", methods=["POST"]) def create_goal(): + # check for request body if not request.get_json(): abort(make_response({"details": "Invalid data"}, 400)) request_body = request.get_json() new_goal = Goal(title=request_body["title"]) + # update database db.session.add(new_goal) db.session.commit() @@ -28,7 +30,7 @@ def get_all_goals(): goals = Goal.query.all() goals_response = [goal.to_dict() for goal in goals] - return jsonify(goals_response) + return make_response(jsonify(goals_response)) # get goal by id @goals_bp.route("/", methods=["GET"]) @@ -91,11 +93,11 @@ def validate_goal(goal_id): try: goal_id = int(goal_id) except: - abort(make_response({"details": f"Goal #{goal_id} invalid"}, 400)) + error_message(f"Goal #{goal_id} invalid", 400) goal = Goal.query.get(goal_id) if not goal: - abort(make_response({"details": f"Goal #{goal_id} not found"}, 404)) + error_message(f"Goal #{goal_id} not found", 404) return goal \ No newline at end of file diff --git a/app/route_helpers.py b/app/route_helpers.py new file mode 100644 index 000000000..c570b136b --- /dev/null +++ b/app/route_helpers.py @@ -0,0 +1,5 @@ +from flask import jsonify, make_response, abort + +# helper function to generate error message +def error_message(message, status_code): + abort(make_response(jsonify(dict(details=message)), status_code)) \ No newline at end of file From 864a25a9c4f926af876736340cc6325fef91b258 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Fri, 13 May 2022 11:11:57 -0500 Subject: [PATCH 43/49] Refactor endpoints to use error_message() helper function --- app/goal_routes.py | 11 ++++++----- app/task_routes.py | 7 ++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/goal_routes.py b/app/goal_routes.py index 4db326b7d..7523f35b3 100644 --- a/app/goal_routes.py +++ b/app/goal_routes.py @@ -9,12 +9,13 @@ # create a new goal @goals_bp.route("", methods=["POST"]) def create_goal(): - # check for request body - if not request.get_json(): - abort(make_response({"details": "Invalid data"}, 400)) - request_body = request.get_json() - new_goal = Goal(title=request_body["title"]) + + # check for request body + try: + new_goal = Goal(title=request_body["title"]) + except: + error_message("Invalid data", 400) # update database db.session.add(new_goal) diff --git a/app/task_routes.py b/app/task_routes.py index f578bacae..c1587a85d 100644 --- a/app/task_routes.py +++ b/app/task_routes.py @@ -3,6 +3,7 @@ from app import db from app.models.task import Task from datetime import date +from app.route_helpers import error_message # imports for slackbot import os @@ -23,7 +24,7 @@ def create_task(): description=request_body["description"] ) except: - abort(make_response({"details": "Invalid data"}, 400)) + error_message("Invalid data", 400) if request_body.get("completed_at"): new_task.completed_at = request_body.get("completed_at") @@ -110,12 +111,12 @@ def validate_task(task_id): try: task_id = int(task_id) except: - abort(make_response({"details": f"Task #{task_id} invalid"}, 400)) + error_message(f"Task #{task_id} invalid", 400) task = Task.query.get(task_id) if not task: - abort(make_response({"details": f"Task #{task_id} not found"}, 404)) + error_message(f"Task #{task_id} not found", 404) return task From 147170aeb617a0de43e53fe3828f2b607173f2b8 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Fri, 13 May 2022 18:22:05 -0500 Subject: [PATCH 44/49] Create validate_model_instance() helper function --- app/route_helpers.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/app/route_helpers.py b/app/route_helpers.py index c570b136b..fe5f162fd 100644 --- a/app/route_helpers.py +++ b/app/route_helpers.py @@ -1,5 +1,26 @@ from flask import jsonify, make_response, abort +from app.models.goal import Goal +from app.models.task import Task # helper function to generate error message def error_message(message, status_code): - abort(make_response(jsonify(dict(details=message)), status_code)) \ No newline at end of file + abort(make_response(jsonify(dict(details=message)), status_code)) + +# class/model-agnostic helper function to validate model instances +def validate_model_instance(model, id): + if model == Task: + model_name = "Task" + elif model == Goal: + model_name = "Goal" + + try: + id = int(id) + except: + error_message(f"{model_name} #{id} invalid", 400) + + model_instance = model.query.get(id) + + if not model_instance: + error_message(f"{model_name} #{id} not found", 404) + + return model_instance From 4fe80da4e7352e93d926da4744e85663149784fc Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Fri, 13 May 2022 18:23:47 -0500 Subject: [PATCH 45/49] Refactor task_routes.py to use helper functions --- app/task_routes.py | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/app/task_routes.py b/app/task_routes.py index c1587a85d..e722aa4a6 100644 --- a/app/task_routes.py +++ b/app/task_routes.py @@ -3,7 +3,7 @@ from app import db from app.models.task import Task from datetime import date -from app.route_helpers import error_message +from app.route_helpers import error_message, validate_model_instance # imports for slackbot import os @@ -55,14 +55,14 @@ def get_all_tasks(): # retrieve one task by id @tasks_bp.route("/", methods=["GET"]) def get_one_task_by_id(task_id): - task = validate_task(task_id) + task = validate_model_instance(Task, task_id) return {"task": task.to_dict()} # update one task by id @tasks_bp.route("/", methods=["PUT"]) def update_one_task_by_id(task_id): - task = validate_task(task_id) + task = validate_model_instance(Task, task_id) request_body = request.get_json() task.title = request_body["title"] @@ -75,7 +75,7 @@ def update_one_task_by_id(task_id): # delete one task by id @tasks_bp.route("/", methods=["DELETE"]) def delete_task_by_id(task_id): - task = validate_task(task_id) + task = validate_model_instance(Task, task_id) db.session.delete(task) db.session.commit() @@ -85,7 +85,7 @@ def delete_task_by_id(task_id): # mark one task complete by id @tasks_bp.route("//mark_complete", methods=["PATCH"]) def mark_task_complete(task_id): - task = validate_task(task_id) + task = validate_model_instance(Task, task_id) task.completed_at = date.today() response_body = {"task": task.to_dict()} @@ -98,7 +98,7 @@ def mark_task_complete(task_id): # mark one task incomplete by id @tasks_bp.route("//mark_incomplete", methods=["PATCH"]) def mark_task_incomplete(task_id): - task = validate_task(task_id) + task = validate_model_instance(Task, task_id) task.completed_at = None response_body = {"task": task.to_dict()} @@ -106,27 +106,14 @@ def mark_task_incomplete(task_id): db.session.commit() return make_response(jsonify(response_body), 200) -# check for valid task using id -def validate_task(task_id): - try: - task_id = int(task_id) - except: - error_message(f"Task #{task_id} invalid", 400) - - task = Task.query.get(task_id) - - if not task: - error_message(f"Task #{task_id} not found", 404) - - return task - # post completion message to slack def post_slack_completion_message(task_id): client = WebClient(token=os.environ.get("SLACK_BOT_TOKEN")) logger = logging.getLogger(__name__) channel_id = "C03EP2Q0WK1" - task = validate_task(task_id) + # task = validate_task(task_id) + task = validate_model_instance(Task, task_id) try: result = client.chat_postMessage( From 16ba613eac511aa4133ebeb70d8cd8a2e507bb5c Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Fri, 13 May 2022 18:26:47 -0500 Subject: [PATCH 46/49] Refactor goal_routes.py to use helper function --- app/goal_routes.py | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/app/goal_routes.py b/app/goal_routes.py index 7523f35b3..99e736af4 100644 --- a/app/goal_routes.py +++ b/app/goal_routes.py @@ -2,7 +2,7 @@ from app import db from app.models.goal import Goal from app.models.task import Task -from app.route_helpers import error_message +from app.route_helpers import error_message, validate_model_instance goals_bp = Blueprint("goals_bp", __name__, url_prefix="/goals") @@ -36,13 +36,13 @@ def get_all_goals(): # get goal by id @goals_bp.route("/", methods=["GET"]) def get_goal_by_id(goal_id): - goal = validate_goal(goal_id) + goal = validate_model_instance(Goal, goal_id) return {"goal": goal.to_dict()} # update goal by id @goals_bp.route("/", methods=["PUT"]) def update_goal(goal_id): - goal = validate_goal(goal_id) + goal = validate_model_instance(Goal, goal_id) request_body = request.get_json() goal.title = request_body["title"] @@ -53,7 +53,7 @@ def update_goal(goal_id): # delete goal by id @goals_bp.route("/", methods=["DELETE"]) def delete_goal(goal_id): - goal = validate_goal(goal_id) + goal = validate_model_instance(Goal, goal_id) db.session.delete(goal) db.session.commit() @@ -63,7 +63,7 @@ def delete_goal(goal_id): # get tasks by goal id @goals_bp.route("//tasks", methods=["GET"]) def get_tasks_for_goal(goal_id): - goal = validate_goal(goal_id) + goal = validate_model_instance(Goal, goal_id) task_list = [task.to_dict() for task in goal.tasks] goal_dict = goal.to_dict() @@ -74,7 +74,7 @@ def get_tasks_for_goal(goal_id): # post tasks to goal by goal id @goals_bp.route("//tasks", methods=["POST"]) def post_tasks_to_goal(goal_id): - goal = validate_goal(goal_id) + goal = validate_model_instance(Goal, goal_id) request_body = request.get_json() for task_id in request_body["task_ids"]: @@ -87,18 +87,4 @@ def post_tasks_to_goal(goal_id): return make_response({ "id": goal.goal_id, "task_ids": request_body["task_ids"] - }) - -# helper function to validate goal by id -def validate_goal(goal_id): - try: - goal_id = int(goal_id) - except: - error_message(f"Goal #{goal_id} invalid", 400) - - goal = Goal.query.get(goal_id) - - if not goal: - error_message(f"Goal #{goal_id} not found", 404) - - return goal \ No newline at end of file + }) \ No newline at end of file From bac6ee6e193a83cc6616b14f3cd537cc2c04b5f4 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Fri, 13 May 2022 19:36:35 -0500 Subject: [PATCH 47/49] Refactor post_slack_completion_message helper function --- app/route_helpers.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/route_helpers.py b/app/route_helpers.py index fe5f162fd..1811c26e6 100644 --- a/app/route_helpers.py +++ b/app/route_helpers.py @@ -1,6 +1,7 @@ from flask import jsonify, make_response, abort from app.models.goal import Goal from app.models.task import Task +import requests, os # helper function to generate error message def error_message(message, status_code): @@ -24,3 +25,17 @@ def validate_model_instance(model, id): error_message(f"{model_name} #{id} not found", 404) return model_instance + +# helper function to post completion message to slack +def post_slack_completion_message(task_id): + task = validate_model_instance(Task, task_id) + path = "https://slack.com/api/chat.postMessage" + SLACK_API_KEY = os.environ.get("SLACK_BOT_TOKEN") + + request_headers = {"Authorization": f"Bearer {SLACK_API_KEY}"} + request_body = { + "channel": "C03EP2Q0WK1", + "text": f"Someone just completed the task {task.title}" + } + + requests.post(path, headers=request_headers, json=request_body) \ No newline at end of file From 06401c4e6ad7ee5f2e80b6f2c461b60ea173b925 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Fri, 13 May 2022 19:37:02 -0500 Subject: [PATCH 48/49] Remove unused imports --- app/task_routes.py | 29 ++--------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/app/task_routes.py b/app/task_routes.py index e722aa4a6..93685f659 100644 --- a/app/task_routes.py +++ b/app/task_routes.py @@ -3,13 +3,7 @@ from app import db from app.models.task import Task from datetime import date -from app.route_helpers import error_message, validate_model_instance - -# imports for slackbot -import os -import logging -from slack_sdk import WebClient -from slack_sdk.errors import SlackApiError +from app.route_helpers import error_message, validate_model_instance, post_slack_completion_message tasks_bp = Blueprint("tasks_bp", __name__, url_prefix="/tasks") @@ -104,23 +98,4 @@ def mark_task_incomplete(task_id): response_body = {"task": task.to_dict()} db.session.commit() - return make_response(jsonify(response_body), 200) - -# post completion message to slack -def post_slack_completion_message(task_id): - client = WebClient(token=os.environ.get("SLACK_BOT_TOKEN")) - logger = logging.getLogger(__name__) - channel_id = "C03EP2Q0WK1" - - # task = validate_task(task_id) - task = validate_model_instance(Task, task_id) - - try: - result = client.chat_postMessage( - channel=channel_id, - text=f"Someone just completed the task {task.title}" - ) - logger.info(result) - - except SlackApiError as e: - logger.error(f"Error posting message: {e}") \ No newline at end of file + return make_response(jsonify(response_body), 200) \ No newline at end of file From 3d942c0c7bea38292d22e1b68e2842c6310c07a1 Mon Sep 17 00:00:00 2001 From: Tiffini Hyatt Date: Fri, 13 May 2022 20:39:17 -0500 Subject: [PATCH 49/49] Refactor Goal to_dict method to include associated task ids --- app/models/goal.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/goal.py b/app/models/goal.py index 96355ce51..5af9115b3 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -13,6 +13,7 @@ def to_dict(self): } if self.tasks: - goal_dict["tasks"] = [] + goal_dict["tasks"] = [task.task_id for task in self.tasks] + # goal_dict["tasks"] = [] return goal_dict \ No newline at end of file