From 8302ea34fe359e95ac682dbd4cf1576979476628 Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Wed, 27 Oct 2021 13:44:39 -0700 Subject: [PATCH 01/29] Creates task_list_api_development and task_list_api_test db --- 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 2fa5202085f51a7deb78f325c49f8139c49f7f70 Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Wed, 27 Oct 2021 14:09:06 -0700 Subject: [PATCH 02/29] Creates Task class --- migrations/versions/bc0a256da763_.py | 36 ++++++++++++++++++++++++++++ models/task.py | 7 ++++++ 2 files changed, 43 insertions(+) create mode 100644 migrations/versions/bc0a256da763_.py create mode 100644 models/task.py diff --git a/migrations/versions/bc0a256da763_.py b/migrations/versions/bc0a256da763_.py new file mode 100644 index 000000000..da572625d --- /dev/null +++ b/migrations/versions/bc0a256da763_.py @@ -0,0 +1,36 @@ +"""empty message + +Revision ID: bc0a256da763 +Revises: +Create Date: 2021-10-27 13:56:32.027459 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'bc0a256da763' +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.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 ### diff --git a/models/task.py b/models/task.py new file mode 100644 index 000000000..fd30abda0 --- /dev/null +++ b/models/task.py @@ -0,0 +1,7 @@ +from app import db + +class Task(db.Model): + task_id = db.Column(db.Integer, primary_key=True, autoincrement=True) + title = db.Column(db.String, nullable=False) + description = db.Column(db.String) + completed_at = db.Column(db.DateTime, nullable=True) \ No newline at end of file From 3a0f0caba4a27d3c63444130d6230aa2dbfb8593 Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Wed, 27 Oct 2021 15:36:59 -0700 Subject: [PATCH 03/29] Moves Task into correct Models folder --- app/__init__.py | 2 ++ app/models/task.py | 5 ++++- app/routes.py | 5 ++++- migrations/versions/9d68d35372b3_.py | 32 ++++++++++++++++++++++++++++ models/task.py | 7 ------ 5 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 migrations/versions/9d68d35372b3_.py delete mode 100644 models/task.py diff --git a/app/__init__.py b/app/__init__.py index 2764c4cc8..59e2e2c44 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/app/models/task.py b/app/models/task.py index 39c89cd16..a98332bbe 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, nullable=False) + description = db.Column(db.String) + completed_at = db.Column(db.DateTime, nullable=True) \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index 8e9dfe684..b171e0d1c 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,2 +1,5 @@ -from flask import Blueprint +from flask import Blueprint, abort, jsonify, request +# from app.models.task import Task + +task_bp = Blueprint("tasks", __name__, url_prefix="/tasks") diff --git a/migrations/versions/9d68d35372b3_.py b/migrations/versions/9d68d35372b3_.py new file mode 100644 index 000000000..f67131804 --- /dev/null +++ b/migrations/versions/9d68d35372b3_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: 9d68d35372b3 +Revises: bc0a256da763 +Create Date: 2021-10-27 15:34:45.808468 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9d68d35372b3' +down_revision = 'bc0a256da763' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('task', sa.Column('completed_at', sa.DateTime(), nullable=True)) + op.add_column('task', sa.Column('description', sa.String(), nullable=True)) + op.add_column('task', sa.Column('title', sa.String(), nullable=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('task', 'title') + op.drop_column('task', 'description') + op.drop_column('task', 'completed_at') + # ### end Alembic commands ### diff --git a/models/task.py b/models/task.py deleted file mode 100644 index fd30abda0..000000000 --- a/models/task.py +++ /dev/null @@ -1,7 +0,0 @@ -from app import db - -class Task(db.Model): - task_id = db.Column(db.Integer, primary_key=True, autoincrement=True) - title = db.Column(db.String, nullable=False) - description = db.Column(db.String) - completed_at = db.Column(db.DateTime, nullable=True) \ No newline at end of file From 3ebaa2cf03994908a677dd158f42561329f6eb34 Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Wed, 27 Oct 2021 17:21:47 -0700 Subject: [PATCH 04/29] Updates get_tasks route with to_dict() helper. --- app/__init__.py | 4 ++-- app/models/task.py | 9 ++++++++- app/routes.py | 19 ++++++++++++++++--- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 59e2e2c44..30052751d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -30,7 +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) + 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 a98332bbe..3cd898212 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -6,4 +6,11 @@ class Task(db.Model): task_id = db.Column(db.Integer, primary_key=True, autoincrement=True) title = db.Column(db.String, nullable=False) description = db.Column(db.String) - completed_at = db.Column(db.DateTime, nullable=True) \ No newline at end of file + completed_at = db.Column(db.DateTime, nullable=True) + + def to_dict(self, data): + return { + self.title, + self.description, + self.completed_at + } \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index b171e0d1c..d15855dfd 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,5 +1,18 @@ -from flask import Blueprint, abort, jsonify, request -# from app.models.task import Task +from app import db +from flask import Blueprint, request, abort, jsonify +from app.models.task import Task -task_bp = Blueprint("tasks", __name__, url_prefix="/tasks") +tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") +@tasks_bp.route("", methods=["GET"]) +def get_tasks(): + tasks = Task.query.all() + + if not tasks: + return jsonify("No tasks found."), 404 + + task_response = [] + for task in tasks: + task_response.append(task.to_dict()) + + return jsonify(task_response), 200 \ No newline at end of file From aae9d696ba870165c6d5090475f670dff0b41685 Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Wed, 27 Oct 2021 18:03:44 -0700 Subject: [PATCH 05/29] Creates post_task endpoint --- app/models/task.py | 5 +++-- app/routes.py | 25 ++++++++++++++++++---- migrations/versions/3b77faa77808_.py | 32 ++++++++++++++++++++++++++++ migrations/versions/9e4957786f5b_.py | 28 ++++++++++++++++++++++++ migrations/versions/eaaee94825ea_.py | 30 ++++++++++++++++++++++++++ 5 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 migrations/versions/3b77faa77808_.py create mode 100644 migrations/versions/9e4957786f5b_.py create mode 100644 migrations/versions/eaaee94825ea_.py diff --git a/app/models/task.py b/app/models/task.py index 3cd898212..893d47b7a 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -3,12 +3,13 @@ class Task(db.Model): - task_id = db.Column(db.Integer, primary_key=True, autoincrement=True) + id = db.Column(db.Integer, primary_key=True, autoincrement=True) title = db.Column(db.String, nullable=False) description = db.Column(db.String) + is_complete = db.Column(db.Boolean, default=False) completed_at = db.Column(db.DateTime, nullable=True) - def to_dict(self, data): + def to_dict(self): return { self.title, self.description, diff --git a/app/routes.py b/app/routes.py index d15855dfd..6765f5a9e 100644 --- a/app/routes.py +++ b/app/routes.py @@ -8,11 +8,28 @@ def get_tasks(): tasks = Task.query.all() - if not tasks: - return jsonify("No tasks found."), 404 - task_response = [] for task in tasks: task_response.append(task.to_dict()) - return jsonify(task_response), 200 \ No newline at end of file + return jsonify(task_response), 200 + +@tasks_bp.route("/", methods=["GET"]) +def get_task(task_id): + task = Task.query.get(task_id) + return jsonify(task.to_dict()), 200 + +@tasks_bp.route("", methods=["POST"]) +def post_task(): + form_data = request.get_json() + + new_task = Task( + title=form_data["title"], + description=form_data["description"] + ) + + db.session.add(new_task) + db.session.commit() + + return jsonify(f"Task {new_task.title} created successfully."), 201 + diff --git a/migrations/versions/3b77faa77808_.py b/migrations/versions/3b77faa77808_.py new file mode 100644 index 000000000..92c754e06 --- /dev/null +++ b/migrations/versions/3b77faa77808_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: 3b77faa77808 +Revises: eaaee94825ea +Create Date: 2021-10-27 17:58:46.767196 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '3b77faa77808' +down_revision = 'eaaee94825ea' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('task', 'is_complete', + existing_type=sa.BOOLEAN(), + nullable=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('task', 'is_complete', + existing_type=sa.BOOLEAN(), + nullable=False) + # ### end Alembic commands ### diff --git a/migrations/versions/9e4957786f5b_.py b/migrations/versions/9e4957786f5b_.py new file mode 100644 index 000000000..7266ebd4a --- /dev/null +++ b/migrations/versions/9e4957786f5b_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 9e4957786f5b +Revises: 9d68d35372b3 +Create Date: 2021-10-27 17:45:21.170754 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9e4957786f5b' +down_revision = '9d68d35372b3' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('task', sa.Column('is_complete', sa.Boolean(), nullable=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('task', 'is_complete') + # ### end Alembic commands ### diff --git a/migrations/versions/eaaee94825ea_.py b/migrations/versions/eaaee94825ea_.py new file mode 100644 index 000000000..a2aeb98e7 --- /dev/null +++ b/migrations/versions/eaaee94825ea_.py @@ -0,0 +1,30 @@ +"""empty message + +Revision ID: eaaee94825ea +Revises: 9e4957786f5b +Create Date: 2021-10-27 17:48:14.084278 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'eaaee94825ea' +down_revision = '9e4957786f5b' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('task', sa.Column('id', sa.Integer(), autoincrement=True, nullable=False)) + op.drop_column('task', 'task_id') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('task', sa.Column('task_id', sa.INTEGER(), autoincrement=True, nullable=False)) + op.drop_column('task', 'id') + # ### end Alembic commands ### From b2ac6ec9fb645ceb3eb82b059956a3539689789b Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Thu, 28 Oct 2021 20:52:28 -0700 Subject: [PATCH 06/29] Debugs get & post endpoint and creates put_task endpoint --- app/models/task.py | 8 +-- app/routes.py | 64 +++++++++++++++++-- .../{9e4957786f5b_.py => 1451bf391041_.py} | 12 ++-- migrations/versions/3b77faa77808_.py | 32 ---------- migrations/versions/9d68d35372b3_.py | 32 ---------- .../{bc0a256da763_.py => e5357d30d71c_.py} | 13 ++-- migrations/versions/eaaee94825ea_.py | 30 --------- 7 files changed, 77 insertions(+), 114 deletions(-) rename migrations/versions/{9e4957786f5b_.py => 1451bf391041_.py} (75%) delete mode 100644 migrations/versions/3b77faa77808_.py delete mode 100644 migrations/versions/9d68d35372b3_.py rename migrations/versions/{bc0a256da763_.py => e5357d30d71c_.py} (63%) delete mode 100644 migrations/versions/eaaee94825ea_.py diff --git a/app/models/task.py b/app/models/task.py index 893d47b7a..9fed6e807 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -7,11 +7,11 @@ class Task(db.Model): title = db.Column(db.String, nullable=False) description = db.Column(db.String) is_complete = db.Column(db.Boolean, default=False) - completed_at = db.Column(db.DateTime, nullable=True) + completed_at = db.Column(db.DateTime) def to_dict(self): return { - self.title, - self.description, - self.completed_at + "title": self.title, + "description": self.description, + "is_complete": self.is_complete } \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index 6765f5a9e..fb2a73999 100644 --- a/app/routes.py +++ b/app/routes.py @@ -6,30 +6,84 @@ @tasks_bp.route("", methods=["GET"]) def get_tasks(): + """Retrieve all stored tasks.""" tasks = Task.query.all() task_response = [] for task in tasks: - task_response.append(task.to_dict()) + task_response.append({ + "id": task.id, + "title": task.title, + "description": task.description, + "is_complete": task.is_complete + }) + #TODO: Refactor to use to_dict() method return jsonify(task_response), 200 @tasks_bp.route("/", methods=["GET"]) def get_task(task_id): - task = Task.query.get(task_id) - return jsonify(task.to_dict()), 200 + """Retrieve one stored task by id.""" + task = Task.query.get_or_404(task_id) + return jsonify({"task": { + "id": task.id, + "title": task.title, + "description": task.description, + "is_complete": task.is_complete + }}), 200 + #TODO: Refactor to use to_dict() method @tasks_bp.route("", methods=["POST"]) def post_task(): + """Create a new task from JSON data.""" form_data = request.get_json() + #TODO: Refactor to validation decorator helper method + mandatory_fields = ["title", "description", "completed_at"] + for field in mandatory_fields: + if field not in form_data: + return jsonify({"details": "Invalid data"}), 400 + new_task = Task( title=form_data["title"], - description=form_data["description"] + description=form_data["description"], + completed_at=form_data["completed_at"] ) db.session.add(new_task) db.session.commit() - return jsonify(f"Task {new_task.title} created successfully."), 201 + print(new_task.to_dict()) + return {"task": { + "id": new_task.id, + "title": new_task.title, + "description": new_task.description, + "is_complete": new_task.is_complete + } + }, 201 + #TODO: Refactor to use to_dict() method + +@tasks_bp.route("/", methods=["PUT"]) +def put_task(task_id): + """Updates task by id.""" + # Search database for task by id + task = Task.query.get_or_404(task_id) + # Retrieve form data + form_data = request.get_json() + + # Loops through attributes provided by user + for key, value in form_data.items(): + # Restricts to attributes that are table columns + if key in Task.__table__.columns.keys(): + setattr(task, key, value) + + db.session.commit() + + return {"task": { + "id": task.id, + "title": task.title, + "description": task.description, + "is_complete": task.is_complete + }} + #TODO: Refactor to use to_dict() method diff --git a/migrations/versions/9e4957786f5b_.py b/migrations/versions/1451bf391041_.py similarity index 75% rename from migrations/versions/9e4957786f5b_.py rename to migrations/versions/1451bf391041_.py index 7266ebd4a..e62c1e08b 100644 --- a/migrations/versions/9e4957786f5b_.py +++ b/migrations/versions/1451bf391041_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: 9e4957786f5b -Revises: 9d68d35372b3 -Create Date: 2021-10-27 17:45:21.170754 +Revision ID: 1451bf391041 +Revises: e5357d30d71c +Create Date: 2021-10-28 18:45:15.089599 """ from alembic import op @@ -10,15 +10,15 @@ # revision identifiers, used by Alembic. -revision = '9e4957786f5b' -down_revision = '9d68d35372b3' +revision = '1451bf391041' +down_revision = 'e5357d30d71c' branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('task', sa.Column('is_complete', sa.Boolean(), nullable=False)) + op.add_column('task', sa.Column('is_complete', sa.Boolean(), nullable=True)) # ### end Alembic commands ### diff --git a/migrations/versions/3b77faa77808_.py b/migrations/versions/3b77faa77808_.py deleted file mode 100644 index 92c754e06..000000000 --- a/migrations/versions/3b77faa77808_.py +++ /dev/null @@ -1,32 +0,0 @@ -"""empty message - -Revision ID: 3b77faa77808 -Revises: eaaee94825ea -Create Date: 2021-10-27 17:58:46.767196 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '3b77faa77808' -down_revision = 'eaaee94825ea' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('task', 'is_complete', - existing_type=sa.BOOLEAN(), - nullable=True) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('task', 'is_complete', - existing_type=sa.BOOLEAN(), - nullable=False) - # ### end Alembic commands ### diff --git a/migrations/versions/9d68d35372b3_.py b/migrations/versions/9d68d35372b3_.py deleted file mode 100644 index f67131804..000000000 --- a/migrations/versions/9d68d35372b3_.py +++ /dev/null @@ -1,32 +0,0 @@ -"""empty message - -Revision ID: 9d68d35372b3 -Revises: bc0a256da763 -Create Date: 2021-10-27 15:34:45.808468 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '9d68d35372b3' -down_revision = 'bc0a256da763' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('task', sa.Column('completed_at', sa.DateTime(), nullable=True)) - op.add_column('task', sa.Column('description', sa.String(), nullable=True)) - op.add_column('task', sa.Column('title', sa.String(), nullable=False)) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('task', 'title') - op.drop_column('task', 'description') - op.drop_column('task', 'completed_at') - # ### end Alembic commands ### diff --git a/migrations/versions/bc0a256da763_.py b/migrations/versions/e5357d30d71c_.py similarity index 63% rename from migrations/versions/bc0a256da763_.py rename to migrations/versions/e5357d30d71c_.py index da572625d..cb2a50f69 100644 --- a/migrations/versions/bc0a256da763_.py +++ b/migrations/versions/e5357d30d71c_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: bc0a256da763 +Revision ID: e5357d30d71c Revises: -Create Date: 2021-10-27 13:56:32.027459 +Create Date: 2021-10-27 20:08:54.502438 """ from alembic import op @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. -revision = 'bc0a256da763' +revision = 'e5357d30d71c' down_revision = None branch_labels = None depends_on = None @@ -23,8 +23,11 @@ def upgrade(): sa.PrimaryKeyConstraint('goal_id') ) op.create_table('task', - sa.Column('task_id', sa.Integer(), nullable=False), - sa.PrimaryKeyConstraint('task_id') + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('title', sa.String(), nullable=False), + sa.Column('description', sa.String(), nullable=True), + sa.Column('completed_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id') ) # ### end Alembic commands ### diff --git a/migrations/versions/eaaee94825ea_.py b/migrations/versions/eaaee94825ea_.py deleted file mode 100644 index a2aeb98e7..000000000 --- a/migrations/versions/eaaee94825ea_.py +++ /dev/null @@ -1,30 +0,0 @@ -"""empty message - -Revision ID: eaaee94825ea -Revises: 9e4957786f5b -Create Date: 2021-10-27 17:48:14.084278 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'eaaee94825ea' -down_revision = '9e4957786f5b' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('task', sa.Column('id', sa.Integer(), autoincrement=True, nullable=False)) - op.drop_column('task', 'task_id') - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('task', sa.Column('task_id', sa.INTEGER(), autoincrement=True, nullable=False)) - op.drop_column('task', 'id') - # ### end Alembic commands ### From dd1c8c16225116d0b11a604819934108f20b9e67 Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Fri, 29 Oct 2021 08:09:15 -0700 Subject: [PATCH 07/29] Creates delete_task() endpoint --- app/routes.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/routes.py b/app/routes.py index fb2a73999..a1dbe5627 100644 --- a/app/routes.py +++ b/app/routes.py @@ -87,3 +87,15 @@ def put_task(task_id): }} #TODO: Refactor to use to_dict() method +@tasks_bp.route("/", methods=["DELETE"]) +def delete_task(task_id): + """Deletes task by id.""" + task = Task.query.get_or_404(task_id) + if task: + return_statement = f"Task {task.id} \"{task.title}\" successfully deleted" + db.session.delete(task) + db.session.commit() + + return { + "details": return_statement + }, 200 \ No newline at end of file From f24fbd0e3d511c3b1dcd5124a79e6bee1b4e493e Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Fri, 29 Oct 2021 09:42:22 -0700 Subject: [PATCH 08/29] Debug tasks/PUT route --- app/models/task.py | 1 + app/routes.py | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/models/task.py b/app/models/task.py index 9fed6e807..db9924fed 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -11,6 +11,7 @@ class Task(db.Model): def to_dict(self): return { + "id": self.id, "title": self.title, "description": self.description, "is_complete": self.is_complete diff --git a/app/routes.py b/app/routes.py index a1dbe5627..86bbde70c 100644 --- a/app/routes.py +++ b/app/routes.py @@ -25,6 +25,9 @@ def get_tasks(): def get_task(task_id): """Retrieve one stored task by id.""" task = Task.query.get_or_404(task_id) + print(task.__dict__) + + # return jsonify({"task": task.to_dict()}), 200 return jsonify({"task": { "id": task.id, "title": task.title, @@ -84,18 +87,17 @@ def put_task(task_id): "title": task.title, "description": task.description, "is_complete": task.is_complete - }} + }}, 200 #TODO: Refactor to use to_dict() method @tasks_bp.route("/", methods=["DELETE"]) def delete_task(task_id): """Deletes task by id.""" task = Task.query.get_or_404(task_id) - if task: - return_statement = f"Task {task.id} \"{task.title}\" successfully deleted" - db.session.delete(task) - db.session.commit() - - return { - "details": return_statement - }, 200 \ No newline at end of file + + db.session.delete(task) + db.session.commit() + + return { + "details": f"Task {task.id} \"{task.title}\" successfully deleted" + }, 200 \ No newline at end of file From 5666d175910ce2b9248547e370e01feea26c5a47 Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Fri, 29 Oct 2021 10:31:55 -0700 Subject: [PATCH 09/29] Added option for query param in task get_tasks() endpoint --- app/routes.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/routes.py b/app/routes.py index 86bbde70c..82f31263e 100644 --- a/app/routes.py +++ b/app/routes.py @@ -7,7 +7,13 @@ @tasks_bp.route("", methods=["GET"]) def get_tasks(): """Retrieve all stored tasks.""" - tasks = Task.query.all() + query = request.args.get("sort") + if query == "asc": + tasks = Task.query.order_by(Task.title) + elif query == "desc": + tasks = Task.query.order_by(Task.title.desc()) + else: + tasks = Task.query.all() task_response = [] for task in tasks: @@ -17,7 +23,7 @@ def get_tasks(): "description": task.description, "is_complete": task.is_complete }) - #TODO: Refactor to use to_dict() method + #TODO: Refactor to use to_dict() method return jsonify(task_response), 200 @@ -34,7 +40,7 @@ def get_task(task_id): "description": task.description, "is_complete": task.is_complete }}), 200 - #TODO: Refactor to use to_dict() method + #TODO: Refactor to use to_dict() method @tasks_bp.route("", methods=["POST"]) def post_task(): From 4dc8ada29718a82a0b8f2d08f1f00df1871d7d8e Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Fri, 29 Oct 2021 13:26:51 -0700 Subject: [PATCH 10/29] Debug Task to_dict method --- app/models/task.py | 8 +++- app/routes.py | 40 ++++--------------- .../0b2d3a36227b_remove_is_complete_column.py | 28 +++++++++++++ 3 files changed, 41 insertions(+), 35 deletions(-) create mode 100644 migrations/versions/0b2d3a36227b_remove_is_complete_column.py diff --git a/app/models/task.py b/app/models/task.py index db9924fed..0222b5d0b 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -6,13 +6,17 @@ class Task(db.Model): id = db.Column(db.Integer, primary_key=True, autoincrement=True) title = db.Column(db.String, nullable=False) description = db.Column(db.String) - is_complete = db.Column(db.Boolean, default=False) completed_at = db.Column(db.DateTime) + def check_if_completed(self): + if self.completed_at: + return True + return False + def to_dict(self): return { "id": self.id, "title": self.title, "description": self.description, - "is_complete": self.is_complete + "is_complete": self.check_if_completed() } \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index 82f31263e..bc4b86e92 100644 --- a/app/routes.py +++ b/app/routes.py @@ -15,32 +15,19 @@ def get_tasks(): else: tasks = Task.query.all() - task_response = [] + tasks_response = [] for task in tasks: - task_response.append({ - "id": task.id, - "title": task.title, - "description": task.description, - "is_complete": task.is_complete - }) - #TODO: Refactor to use to_dict() method + tasks_response.append(task.to_dict()) + print(tasks_response) - return jsonify(task_response), 200 + return jsonify(tasks_response) @tasks_bp.route("/", methods=["GET"]) def get_task(task_id): """Retrieve one stored task by id.""" task = Task.query.get_or_404(task_id) - print(task.__dict__) - # return jsonify({"task": task.to_dict()}), 200 - return jsonify({"task": { - "id": task.id, - "title": task.title, - "description": task.description, - "is_complete": task.is_complete - }}), 200 - #TODO: Refactor to use to_dict() method + return jsonify({"task": task.to_dict()}), 200 @tasks_bp.route("", methods=["POST"]) def post_task(): @@ -63,14 +50,7 @@ def post_task(): db.session.commit() print(new_task.to_dict()) - return {"task": { - "id": new_task.id, - "title": new_task.title, - "description": new_task.description, - "is_complete": new_task.is_complete - } - }, 201 - #TODO: Refactor to use to_dict() method + return {"task": new_task.to_dict()}, 201 @tasks_bp.route("/", methods=["PUT"]) def put_task(task_id): @@ -88,13 +68,7 @@ def put_task(task_id): db.session.commit() - return {"task": { - "id": task.id, - "title": task.title, - "description": task.description, - "is_complete": task.is_complete - }}, 200 - #TODO: Refactor to use to_dict() method + return {"task": task.to_dict()}, 200 @tasks_bp.route("/", methods=["DELETE"]) def delete_task(task_id): diff --git a/migrations/versions/0b2d3a36227b_remove_is_complete_column.py b/migrations/versions/0b2d3a36227b_remove_is_complete_column.py new file mode 100644 index 000000000..aec4f240f --- /dev/null +++ b/migrations/versions/0b2d3a36227b_remove_is_complete_column.py @@ -0,0 +1,28 @@ +"""Remove is_complete column + +Revision ID: 0b2d3a36227b +Revises: 1451bf391041 +Create Date: 2021-10-29 10:37:59.009652 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '0b2d3a36227b' +down_revision = '1451bf391041' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + 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)) + # ### end Alembic commands ### From 0297299feddc8aa9654f58c62d644c8b979f7c9b Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Fri, 29 Oct 2021 13:41:06 -0700 Subject: [PATCH 11/29] Creates Task PATCH /mark_complete and /mark_incomplete endpoints --- app/routes.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/routes.py b/app/routes.py index bc4b86e92..5334107b5 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,5 +1,6 @@ from app import db from flask import Blueprint, request, abort, jsonify +from datetime import datetime from app.models.task import Task tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") @@ -70,6 +71,25 @@ def put_task(task_id): return {"task": task.to_dict()}, 200 +@tasks_bp.route("//mark_complete", methods=["PATCH"]) +def update_task_to_complete(task_id): + """Updates task at particular id to completed.""" + task = Task.query.get_or_404(task_id) + + task.completed_at = datetime.now() + + db.session.commit() + return {"task": task.to_dict()}, 200 + +@tasks_bp.route("/mark_incomplete", methods=["PATCH"]) +def update_task_to_incomplete(task_id): + """ """ + task = Task.query.get_or_404(task_id) + + task.completed_at = None + db.session.commit() + return {"task": task.to_dict()}, 200 + @tasks_bp.route("/", methods=["DELETE"]) def delete_task(task_id): """Deletes task by id.""" From d149904b15bb6ebb4a054566bf758db2f2ff4778 Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Fri, 29 Oct 2021 20:33:14 -0700 Subject: [PATCH 12/29] Implements call to Slack API --- app/routes.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/app/routes.py b/app/routes.py index 5334107b5..b5be4d21a 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,6 +1,9 @@ from app import db from flask import Blueprint, request, abort, jsonify from datetime import datetime +from dotenv import load_dotenv +import os +import requests from app.models.task import Task tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") @@ -19,7 +22,6 @@ def get_tasks(): tasks_response = [] for task in tasks: tasks_response.append(task.to_dict()) - print(tasks_response) return jsonify(tasks_response) @@ -49,8 +51,6 @@ def post_task(): db.session.add(new_task) db.session.commit() - - print(new_task.to_dict()) return {"task": new_task.to_dict()}, 201 @tasks_bp.route("/", methods=["PUT"]) @@ -73,17 +73,33 @@ def put_task(task_id): @tasks_bp.route("//mark_complete", methods=["PATCH"]) def update_task_to_complete(task_id): - """Updates task at particular id to completed.""" + """Updates task at particular id to completed using PATCH.""" task = Task.query.get_or_404(task_id) - + + # Make call to Slack API if task newly completed + if not task.check_if_completed(): + slack_api_url = "https://slack.com/api/chat.postMessage" + headers = {"Authorization": "Bearer " + os.environ.get("SLACK_API_KEY")} + param_payload = { + "channel": "task-notifications", + "text": f"Someone has just completed the task {task.title}" + } + + try: + r = requests.post(slack_api_url, headers=headers, params=param_payload) + + except Exception as e: + return f"Error posting message: {e}" + + # Change task to completed in db task.completed_at = datetime.now() - db.session.commit() + return {"task": task.to_dict()}, 200 @tasks_bp.route("/mark_incomplete", methods=["PATCH"]) def update_task_to_incomplete(task_id): - """ """ + """Updates task at particular id to incomplete using PATCH.""" task = Task.query.get_or_404(task_id) task.completed_at = None From 6ed91ca3778b48ab17f9d97d175e788efc5dd2c7 Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Sat, 30 Oct 2021 17:08:15 -0700 Subject: [PATCH 13/29] Adds require_task decorator, needs debug for None json response --- app/routes.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/app/routes.py b/app/routes.py index b5be4d21a..7856b747b 100644 --- a/app/routes.py +++ b/app/routes.py @@ -2,12 +2,28 @@ from flask import Blueprint, request, abort, jsonify from datetime import datetime from dotenv import load_dotenv +from functools import wraps import os import requests from app.models.task import Task tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") +def require_task(endpoint): + """Decorator to validate input data.""" + @wraps(endpoint) # Makes fn look like func to return + def fn(*args, **kwargs): + task_id = kwargs.get("task_id", None) + task = Task.query.get(task_id) + + if not task: + return json("null", 404) # HOW TO RETURN AN EMPTY BODY... + + kwargs.pop("task_id") + return endpoint(*args, task=task, **kwargs) + + return fn + @tasks_bp.route("", methods=["GET"]) def get_tasks(): """Retrieve all stored tasks.""" @@ -26,6 +42,7 @@ def get_tasks(): return jsonify(tasks_response) @tasks_bp.route("/", methods=["GET"]) +# @require_task def get_task(task_id): """Retrieve one stored task by id.""" task = Task.query.get_or_404(task_id) @@ -38,6 +55,7 @@ def post_task(): form_data = request.get_json() #TODO: Refactor to validation decorator helper method + # All fields must be provided mandatory_fields = ["title", "description", "completed_at"] for field in mandatory_fields: if field not in form_data: @@ -56,9 +74,7 @@ def post_task(): @tasks_bp.route("/", methods=["PUT"]) def put_task(task_id): """Updates task by id.""" - # Search database for task by id task = Task.query.get_or_404(task_id) - # Retrieve form data form_data = request.get_json() # Loops through attributes provided by user @@ -86,10 +102,10 @@ def update_task_to_complete(task_id): } try: - r = requests.post(slack_api_url, headers=headers, params=param_payload) + requests.post(slack_api_url, headers=headers, params=param_payload) except Exception as e: - return f"Error posting message: {e}" + return f"Error posting message to Slack: {e}" # Change task to completed in db task.completed_at = datetime.now() From c849296d1b80f5043f264d13c4d7412110d2e8ec Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Sat, 30 Oct 2021 17:17:20 -0700 Subject: [PATCH 14/29] Refactors and comments get_tasks endpoint --- app/routes.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/app/routes.py b/app/routes.py index 7856b747b..4f672a5be 100644 --- a/app/routes.py +++ b/app/routes.py @@ -26,20 +26,22 @@ def fn(*args, **kwargs): @tasks_bp.route("", methods=["GET"]) def get_tasks(): - """Retrieve all stored tasks.""" - query = request.args.get("sort") - if query == "asc": - tasks = Task.query.order_by(Task.title) - elif query == "desc": - tasks = Task.query.order_by(Task.title.desc()) - else: - tasks = Task.query.all() - - tasks_response = [] - for task in tasks: - tasks_response.append(task.to_dict()) - - return jsonify(tasks_response) + """ + Retrieve all tasks. Allows for use of query parameters. + Returns JSON list of task dictionaries. """ + query = Task.query # Base query + + # Query params, adding to query where indicated + sort = request.args.get("sort") + if sort == "asc": + query = query.order_by(Task.title) + elif sort == "desc": + query = query.order_by(Task.title.desc()) + + query = query.all() # Final query + + # Returns jsonified list of task dicionaries + return jsonify([task.to_dict() for task in query]) @tasks_bp.route("/", methods=["GET"]) # @require_task From 83387e7a4a5b0828c17e8864ee3f3344417ab1ed Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Wed, 3 Nov 2021 22:07:08 -0700 Subject: [PATCH 15/29] Refactored using @require_task_or_404 --- app/models/goal.py | 16 +++++++++++++++- app/models/task.py | 9 ++++++++- app/routes.py | 44 ++++++++++++++++++------------------------- tests/test_wave_05.py | 28 +++++++++++++++------------ 4 files changed, 57 insertions(+), 40 deletions(-) diff --git a/app/models/goal.py b/app/models/goal.py index 8cad278f8..51ff43dd2 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -3,4 +3,18 @@ class Goal(db.Model): - goal_id = db.Column(db.Integer, primary_key=True) + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String) + + def to_dict(self): + return { + "id": self.id, + "title": self.title + } + + def update_from_dict(self, data): + # Loops through attributes provided by users + for key, value in data.items(): + # Restricts to attributes that are columns + if key in Goal.__table__.columns.keys(): + setattr(self, key, value) \ No newline at end of file diff --git a/app/models/task.py b/app/models/task.py index 0222b5d0b..dc964a052 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -19,4 +19,11 @@ def to_dict(self): "title": self.title, "description": self.description, "is_complete": self.check_if_completed() - } \ No newline at end of file + } + + def update_from_dict(self, data): + # Loops through attributes provided by user + for key, value in data.items(): + # Restricts to attributes that are table columns + if key in Task.__table__.columns.keys(): + setattr(self, key, value) \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index 4f672a5be..c0f42f097 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,5 +1,5 @@ from app import db -from flask import Blueprint, request, abort, jsonify +from flask import Blueprint, request, abort, jsonify, make_response from datetime import datetime from dotenv import load_dotenv from functools import wraps @@ -8,8 +8,8 @@ from app.models.task import Task tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") - -def require_task(endpoint): + +def require_task_or_404(endpoint): """Decorator to validate input data.""" @wraps(endpoint) # Makes fn look like func to return def fn(*args, **kwargs): @@ -17,7 +17,7 @@ def fn(*args, **kwargs): task = Task.query.get(task_id) if not task: - return json("null", 404) # HOW TO RETURN AN EMPTY BODY... + return jsonify(None), 404 ## null kwargs.pop("task_id") return endpoint(*args, task=task, **kwargs) @@ -41,14 +41,12 @@ def get_tasks(): query = query.all() # Final query # Returns jsonified list of task dicionaries - return jsonify([task.to_dict() for task in query]) + return jsonify([task.to_dict() for task in query]), 200 @tasks_bp.route("/", methods=["GET"]) -# @require_task -def get_task(task_id): +@require_task_or_404 +def get_task(task): """Retrieve one stored task by id.""" - task = Task.query.get_or_404(task_id) - return jsonify({"task": task.to_dict()}), 200 @tasks_bp.route("", methods=["POST"]) @@ -57,6 +55,7 @@ def post_task(): form_data = request.get_json() #TODO: Refactor to validation decorator helper method + # All fields must be provided mandatory_fields = ["title", "description", "completed_at"] for field in mandatory_fields: @@ -74,26 +73,21 @@ def post_task(): return {"task": new_task.to_dict()}, 201 @tasks_bp.route("/", methods=["PUT"]) -def put_task(task_id): +@require_task_or_404 +def put_task(task): """Updates task by id.""" - task = Task.query.get_or_404(task_id) form_data = request.get_json() - # Loops through attributes provided by user - for key, value in form_data.items(): - # Restricts to attributes that are table columns - if key in Task.__table__.columns.keys(): - setattr(task, key, value) - + # Updates object from form data + task.update_from_dict(form_data) db.session.commit() return {"task": task.to_dict()}, 200 @tasks_bp.route("//mark_complete", methods=["PATCH"]) -def update_task_to_complete(task_id): +@require_task_or_404 +def update_task_to_complete(task): """Updates task at particular id to completed using PATCH.""" - task = Task.query.get_or_404(task_id) - # Make call to Slack API if task newly completed if not task.check_if_completed(): slack_api_url = "https://slack.com/api/chat.postMessage" @@ -116,19 +110,17 @@ def update_task_to_complete(task_id): return {"task": task.to_dict()}, 200 @tasks_bp.route("/mark_incomplete", methods=["PATCH"]) -def update_task_to_incomplete(task_id): +@require_task_or_404 +def update_task_to_incomplete(task): """Updates task at particular id to incomplete using PATCH.""" - task = Task.query.get_or_404(task_id) - task.completed_at = None db.session.commit() return {"task": task.to_dict()}, 200 @tasks_bp.route("/", methods=["DELETE"]) -def delete_task(task_id): +@require_task_or_404 +def delete_task(task): """Deletes task by id.""" - task = Task.query.get_or_404(task_id) - db.session.delete(task) db.session.commit() diff --git a/tests/test_wave_05.py b/tests/test_wave_05.py index 6ba60c6fa..f19e947e4 100644 --- a/tests/test_wave_05.py +++ b/tests/test_wave_05.py @@ -41,7 +41,7 @@ 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 @@ -49,10 +49,8 @@ def test_get_goal_not_found(client): response_body = response.get_json() # Assert - # ---- Complete Test ---- - # assertion 1 goes here - # assertion 2 goes here - # ---- Complete Test ---- + assert response.status_code == 404 + assert response_body == None def test_create_goal(client): # Act @@ -71,18 +69,24 @@ 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): pass # 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 - # ---- Complete Assertions Here ---- + assert response.status_code == 200 + assert "title" in response_body + assert response_body == { + "goal": { + "id": 1, + "title": "Updated Goal Title" + } + } @pytest.mark.skip(reason="test to be completed by student") def test_update_goal_not_found(client): From c7369b02d830e8c3dfb16bda1242aab594cecab9 Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Thu, 4 Nov 2021 10:07:01 -0700 Subject: [PATCH 16/29] Adds get_goals method() --- app/__init__.py | 3 ++- app/routes.py | 19 +++++++++++++++++-- tests/test_wave_05.py | 1 - 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 30052751d..d88bb54c4 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -30,7 +30,8 @@ def create_app(test_config=None): migrate.init_app(app, db) # Register Blueprints here - from .routes import tasks_bp + from .routes import tasks_bp, goals_bp app.register_blueprint(tasks_bp) + app.register_blueprint(goals_bp) return app diff --git a/app/routes.py b/app/routes.py index c0f42f097..434e8a36e 100644 --- a/app/routes.py +++ b/app/routes.py @@ -6,9 +6,11 @@ import os import requests from app.models.task import Task +from app.models.goal import Goal tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") - +goals_bp = Blueprint("goals", __name__, url_prefix="/goals") + def require_task_or_404(endpoint): """Decorator to validate input data.""" @wraps(endpoint) # Makes fn look like func to return @@ -126,4 +128,17 @@ def delete_task(task): return { "details": f"Task {task.id} \"{task.title}\" successfully deleted" - }, 200 \ No newline at end of file + }, 200 + +@goals_bp.route("", methods=["GET"]) +def get_goals(): + """Retrieve all stored goals.""" + goals = Goal.query.all() + + return jsonify([goal.to_dict() for goal in goals]), 200 + +@goals_bp.route("/", methods=["GET"]) +def get_goal(goal_id): + """Retrieve one stored goal by id.""" + goal = Goal.get_or_404(goal_id) + diff --git a/tests/test_wave_05.py b/tests/test_wave_05.py index f19e947e4..ee48a8e42 100644 --- a/tests/test_wave_05.py +++ b/tests/test_wave_05.py @@ -25,7 +25,6 @@ def test_get_goals_one_saved_goal(client, one_goal): } ] - def test_get_goal(client, one_goal): # Act response = client.get("/goals/1") From 695f9c4fac787fb70a32cef359803bb030fa40e7 Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Thu, 4 Nov 2021 11:04:20 -0700 Subject: [PATCH 17/29] Adds update_goal and delete_goal methods --- app/routes.py | 44 ++++++++++++++++++++++++++++++++++++++++++- tests/test_wave_05.py | 33 +++++++++++++++++--------------- 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/app/routes.py b/app/routes.py index 434e8a36e..3dd77651c 100644 --- a/app/routes.py +++ b/app/routes.py @@ -11,6 +11,7 @@ tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") goals_bp = Blueprint("goals", __name__, url_prefix="/goals") +# Alternate --> util module/helper functions, route_wrappers.py def require_task_or_404(endpoint): """Decorator to validate input data.""" @wraps(endpoint) # Makes fn look like func to return @@ -140,5 +141,46 @@ def get_goals(): @goals_bp.route("/", methods=["GET"]) def get_goal(goal_id): """Retrieve one stored goal by id.""" - goal = Goal.get_or_404(goal_id) + goal = Goal.query.get_or_404(goal_id) + return jsonify({"goal": goal.to_dict()}), 200 + +# @goals_bp.route("", methods=["POST"]) +# def create_goal(): +# pass + +@goals_bp.route("", methods=["POST"]) +def create_goal(): + """Create a new goal from JSON data.""" + form_data = request.get_json() + + new_goal = Goal( + title=form_data["title"] + ) + db.session.add(new_goal) + db.session.commit() + + return jsonify({"goal": new_goal.to_dict()}), 201 + +@goals_bp.route("/", methods=["PATCH"]) +def update_goal(goal_id): + """Updates goal by id.""" + form_data = request.get_json() + + goal = Goal.query.get(goal_id) + new_goal = goal.update_from_dict(form_data) + db.session.commit() + + return jsonify({"goal": new_goal.to_dict()}), 200 + +@goals_bp.route("/", methods=["DELETE"]) +def delete_goal(goal_id): + """Deletes goal by id.""" + goal = Goal.query.get(goal_id) + + db.session.delete(goal_id) + db.session.commit() + + return { + "details": f"Goal {goal.id} \"{goal.title}\" successfully deleted" + }, 200 \ No newline at end of file diff --git a/tests/test_wave_05.py b/tests/test_wave_05.py index ee48a8e42..0de0b0c7d 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 def test_get_goals_no_saved_goals(client): # Act @@ -70,7 +71,6 @@ def test_create_goal(client): # @pytest.mark.skip(reason="test to be completed by student") def test_update_goal(client, one_goal): - pass # Act response = client.put("/goals/1", json={ "title": "Updated Goal Title" @@ -86,18 +86,23 @@ def test_update_goal(client, one_goal): "title": "Updated Goal Title" } } + goal = Goal.query.get(1) + assert goal.title == "Updated Goal Title" -@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): pass # Act - # ---- Complete Act Here ---- + response = client.put("/tasks/1", json={ + "title": "Updated Task Title", + "description": "Updated Test Description", + }) + + response_body = response.get_json() # Assert - # ---- Complete Assertions Here ---- - # assertion 1 goes here - # assertion 2 goes here - # ---- Complete Assertions Here ---- + assert response.status_code == 404 + assert response_body == None def test_delete_goal(client, one_goal): @@ -116,18 +121,16 @@ def test_delete_goal(client, one_goal): response = client.get("/goals/1") assert response.status_code == 404 -@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): - pass - # Act - # ---- Complete Act Here ---- + response = client.delete("/tasks/1") + response_body = response.get_json() # Assert - # ---- Complete Assertions Here ---- - # assertion 1 goes here - # assertion 2 goes here - # ---- Complete Assertions Here ---- + assert response.status_code == 404 + assert response_body == None + assert Goal.query.all() == [] def test_create_goal_missing_title(client): From b532840b0de0ff2216e864240dc39277de198fce Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Thu, 4 Nov 2021 11:26:04 -0700 Subject: [PATCH 18/29] Debugs goal_delete endpoint --- app/routes.py | 15 +++++++-------- tests/test_wave_05.py | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/routes.py b/app/routes.py index 3dd77651c..59f04c47c 100644 --- a/app/routes.py +++ b/app/routes.py @@ -145,15 +145,14 @@ def get_goal(goal_id): return jsonify({"goal": goal.to_dict()}), 200 -# @goals_bp.route("", methods=["POST"]) -# def create_goal(): -# pass - @goals_bp.route("", methods=["POST"]) def create_goal(): """Create a new goal from JSON data.""" form_data = request.get_json() + if "title" not in form_data: + return jsonify({"details": "Invalid data"}), 400 + new_goal = Goal( title=form_data["title"] ) @@ -162,23 +161,23 @@ def create_goal(): return jsonify({"goal": new_goal.to_dict()}), 201 -@goals_bp.route("/", methods=["PATCH"]) +@goals_bp.route("/", methods=["PUT"]) def update_goal(goal_id): """Updates goal by id.""" form_data = request.get_json() goal = Goal.query.get(goal_id) - new_goal = goal.update_from_dict(form_data) + goal.update_from_dict(form_data) db.session.commit() - return jsonify({"goal": new_goal.to_dict()}), 200 + return jsonify({"goal": goal.to_dict()}), 200 @goals_bp.route("/", methods=["DELETE"]) def delete_goal(goal_id): """Deletes goal by id.""" goal = Goal.query.get(goal_id) - db.session.delete(goal_id) + db.session.delete(goal) db.session.commit() return { diff --git a/tests/test_wave_05.py b/tests/test_wave_05.py index 0de0b0c7d..0aa8e9a70 100644 --- a/tests/test_wave_05.py +++ b/tests/test_wave_05.py @@ -79,7 +79,7 @@ def test_update_goal(client, one_goal): # Assert assert response.status_code == 200 - assert "title" in response_body + assert "goal" in response_body assert response_body == { "goal": { "id": 1, From bf6ed512e4adffdb411c80991718e6cab61f3997 Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Thu, 4 Nov 2021 20:50:39 -0700 Subject: [PATCH 19/29] Debug relationships --- 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 51ff43dd2..d49a06092 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -5,6 +5,7 @@ class Goal(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String) + tasks = db.relationship("Task", back_populates="goal", lazy=True) def to_dict(self): return { diff --git a/app/models/task.py b/app/models/task.py index dc964a052..be179c00a 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -7,6 +7,8 @@ class Task(db.Model): title = db.Column(db.String, nullable=False) description = db.Column(db.String) completed_at = db.Column(db.DateTime) + goal_id = db.Column(db.Integer, db.ForeignKey('goal.id'), nullable=True) + goal = db.relationship("Goal", back_populates="tasks") def check_if_completed(self): if self.completed_at: From d3c1eb229ca2c73160f09f330262a6312d21efba Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Thu, 4 Nov 2021 22:08:05 -0700 Subject: [PATCH 20/29] Refactor Goal to_dict() method --- app/models/goal.py | 4 ++- app/routes.py | 16 ++++++++++- .../0b2d3a36227b_remove_is_complete_column.py | 28 ------------------- migrations/versions/1451bf391041_.py | 28 ------------------- .../{e5357d30d71c_.py => b73adc7c9cf5_.py} | 13 +++++---- 5 files changed, 26 insertions(+), 63 deletions(-) delete mode 100644 migrations/versions/0b2d3a36227b_remove_is_complete_column.py delete mode 100644 migrations/versions/1451bf391041_.py rename migrations/versions/{e5357d30d71c_.py => b73adc7c9cf5_.py} (70%) diff --git a/app/models/goal.py b/app/models/goal.py index d49a06092..3b55d92eb 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -8,9 +8,11 @@ class Goal(db.Model): tasks = db.relationship("Task", back_populates="goal", lazy=True) def to_dict(self): + tasks = [task.name for task in self.tasks] return { "id": self.id, - "title": self.title + "title": self.title, + "tasks": tasks } def update_from_dict(self, data): diff --git a/app/routes.py b/app/routes.py index 59f04c47c..a5b3d8229 100644 --- a/app/routes.py +++ b/app/routes.py @@ -182,4 +182,18 @@ def delete_goal(goal_id): return { "details": f"Goal {goal.id} \"{goal.title}\" successfully deleted" - }, 200 \ No newline at end of file + }, 200 + +@goals_bp.route("//tasks", methods=["POST"]) +def get_tasks_related_to_goal(goal_id): + """Retrieves all tasks associated with goal id.""" + goal = Goal.query.get(goal_id) + if not goal: + return jsonify(None), 404 + + form_data = request.get_json() + + for task in form_data["tasks"]: + goal.tasks.append(Task.query.get(id)) + + return jsonify({"id": goal.id, "task_ids": [tasks]}), 200 \ No newline at end of file diff --git a/migrations/versions/0b2d3a36227b_remove_is_complete_column.py b/migrations/versions/0b2d3a36227b_remove_is_complete_column.py deleted file mode 100644 index aec4f240f..000000000 --- a/migrations/versions/0b2d3a36227b_remove_is_complete_column.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Remove is_complete column - -Revision ID: 0b2d3a36227b -Revises: 1451bf391041 -Create Date: 2021-10-29 10:37:59.009652 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '0b2d3a36227b' -down_revision = '1451bf391041' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - 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)) - # ### end Alembic commands ### diff --git a/migrations/versions/1451bf391041_.py b/migrations/versions/1451bf391041_.py deleted file mode 100644 index e62c1e08b..000000000 --- a/migrations/versions/1451bf391041_.py +++ /dev/null @@ -1,28 +0,0 @@ -"""empty message - -Revision ID: 1451bf391041 -Revises: e5357d30d71c -Create Date: 2021-10-28 18:45:15.089599 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '1451bf391041' -down_revision = 'e5357d30d71c' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - 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') - # ### end Alembic commands ### diff --git a/migrations/versions/e5357d30d71c_.py b/migrations/versions/b73adc7c9cf5_.py similarity index 70% rename from migrations/versions/e5357d30d71c_.py rename to migrations/versions/b73adc7c9cf5_.py index cb2a50f69..2a357b3e2 100644 --- a/migrations/versions/e5357d30d71c_.py +++ b/migrations/versions/b73adc7c9cf5_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: e5357d30d71c +Revision ID: b73adc7c9cf5 Revises: -Create Date: 2021-10-27 20:08:54.502438 +Create Date: 2021-11-04 20:49:26.638587 """ from alembic import op @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. -revision = 'e5357d30d71c' +revision = 'b73adc7c9cf5' down_revision = None branch_labels = None depends_on = None @@ -19,14 +19,17 @@ 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') + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id') ) op.create_table('task', sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('title', sa.String(), nullable=False), sa.Column('description', sa.String(), nullable=True), sa.Column('completed_at', sa.DateTime(), nullable=True), + sa.Column('goal_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['goal_id'], ['goal.id'], ), sa.PrimaryKeyConstraint('id') ) # ### end Alembic commands ### From 5e22b5ad0ad3383fdc488f6749d99e501c344936 Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Thu, 4 Nov 2021 22:20:25 -0700 Subject: [PATCH 21/29] Debugs Goal.to_dict() --- app/models/goal.py | 4 ++-- app/routes.py | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/models/goal.py b/app/models/goal.py index 3b55d92eb..65024f038 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -8,11 +8,11 @@ class Goal(db.Model): tasks = db.relationship("Task", back_populates="goal", lazy=True) def to_dict(self): - tasks = [task.name for task in self.tasks] + tasks = [task.id for task in self.tasks] return { "id": self.id, "title": self.title, - "tasks": tasks + "task_ids": tasks } def update_from_dict(self, data): diff --git a/app/routes.py b/app/routes.py index a5b3d8229..30a0cfeae 100644 --- a/app/routes.py +++ b/app/routes.py @@ -192,8 +192,11 @@ def get_tasks_related_to_goal(goal_id): return jsonify(None), 404 form_data = request.get_json() + print(form_data) - for task in form_data["tasks"]: - goal.tasks.append(Task.query.get(id)) + for task_id in form_data["task_ids"]: + goal.tasks.append(Task.query.get(task_id)) - return jsonify({"id": goal.id, "task_ids": [tasks]}), 200 \ No newline at end of file + db.session.commit() + + return jsonify(goal.to_dict()), 200 \ No newline at end of file From f2e28663ce1fac16964cd2dc6794a307007c9b25 Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Thu, 4 Nov 2021 22:29:06 -0700 Subject: [PATCH 22/29] Debugs /goals/1/tasks GET --- app/models/goal.py | 4 ++-- app/models/task.py | 1 + app/routes.py | 14 +++++++++++--- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/models/goal.py b/app/models/goal.py index 65024f038..3b6d53a6a 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -8,11 +8,11 @@ class Goal(db.Model): tasks = db.relationship("Task", back_populates="goal", lazy=True) def to_dict(self): - tasks = [task.id for task in self.tasks] + tasks = [task.to_dict() for task in self.tasks] return { "id": self.id, "title": self.title, - "task_ids": tasks + "tasks": tasks } def update_from_dict(self, data): diff --git a/app/models/task.py b/app/models/task.py index be179c00a..28f92b2dc 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -18,6 +18,7 @@ def check_if_completed(self): def to_dict(self): return { "id": self.id, + "goal_id": self.goal_id, "title": self.title, "description": self.description, "is_complete": self.check_if_completed() diff --git a/app/routes.py b/app/routes.py index 30a0cfeae..0d15a6ca5 100644 --- a/app/routes.py +++ b/app/routes.py @@ -185,18 +185,26 @@ def delete_goal(goal_id): }, 200 @goals_bp.route("//tasks", methods=["POST"]) -def get_tasks_related_to_goal(goal_id): - """Retrieves all tasks associated with goal id.""" +def post_tasks_related_to_goal(goal_id): + """Adds tasks to goal wiht id.""" goal = Goal.query.get(goal_id) if not goal: return jsonify(None), 404 form_data = request.get_json() - print(form_data) for task_id in form_data["task_ids"]: goal.tasks.append(Task.query.get(task_id)) db.session.commit() + return jsonify(goal.to_dict()), 200 + +@goals_bp.route("//tasks", methods=["GET"]) +def get_tasks_related_to_goal(goal_id): + """Retrieves all tasks associated with goal id.""" + goal = Goal.query.get(goal_id) + if not goal: + return jsonify(None), 404 + return jsonify(goal.to_dict()), 200 \ No newline at end of file From 6f8e8f3dbe1f946d1834a7da2043a5da5da84a8f Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Thu, 4 Nov 2021 22:41:15 -0700 Subject: [PATCH 23/29] Creates goals_to_dict methods for Task and Goal --- app/models/goal.py | 8 +++++++- app/models/task.py | 8 ++++++++ app/routes.py | 7 +++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/app/models/goal.py b/app/models/goal.py index 3b6d53a6a..c6d5a3fb7 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -8,7 +8,13 @@ class Goal(db.Model): tasks = db.relationship("Task", back_populates="goal", lazy=True) def to_dict(self): - tasks = [task.to_dict() for task in self.tasks] + return { + "id": self.id, + "title": self.title + } + + def tasks_to_dict(self): + tasks = [task.goals_to_dict() for task in self.tasks] return { "id": self.id, "title": self.title, diff --git a/app/models/task.py b/app/models/task.py index 28f92b2dc..80355392b 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -16,6 +16,14 @@ def check_if_completed(self): return False def to_dict(self): + return { + "id": self.id, + "title": self.title, + "description": self.description, + "is_complete": self.check_if_completed() + } + + def goals_to_dict(self): return { "id": self.id, "goal_id": self.goal_id, diff --git a/app/routes.py b/app/routes.py index 0d15a6ca5..a162ff842 100644 --- a/app/routes.py +++ b/app/routes.py @@ -198,7 +198,10 @@ def post_tasks_related_to_goal(goal_id): db.session.commit() - return jsonify(goal.to_dict()), 200 + return jsonify({ + "id": goal.id, + "task_ids": [task.id for task in goal.tasks] + }), 200 @goals_bp.route("//tasks", methods=["GET"]) def get_tasks_related_to_goal(goal_id): @@ -207,4 +210,4 @@ def get_tasks_related_to_goal(goal_id): if not goal: return jsonify(None), 404 - return jsonify(goal.to_dict()), 200 \ No newline at end of file + return jsonify(goal.tasks_to_dict()), 200 \ No newline at end of file From b7161712d7bc80756a8e0078063a107a6f313cae Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Thu, 4 Nov 2021 22:48:02 -0700 Subject: [PATCH 24/29] All tests pass --- app/routes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/routes.py b/app/routes.py index a162ff842..b317f5c4a 100644 --- a/app/routes.py +++ b/app/routes.py @@ -50,7 +50,10 @@ def get_tasks(): @require_task_or_404 def get_task(task): """Retrieve one stored task by id.""" - return jsonify({"task": task.to_dict()}), 200 + if task.goal_id: + return jsonify({"task": task.goals_to_dict()}), 200 + else: + return jsonify({"task": task.to_dict()}), 200 @tasks_bp.route("", methods=["POST"]) def post_task(): From dae4290ce753850006bc20864f940d759862b2a2 Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Thu, 4 Nov 2021 23:04:10 -0700 Subject: [PATCH 25/29] Refactor @require_instance_or_404 decorator --- app/routes.py | 64 +++++++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/app/routes.py b/app/routes.py index b317f5c4a..6281b6d8b 100644 --- a/app/routes.py +++ b/app/routes.py @@ -12,21 +12,33 @@ goals_bp = Blueprint("goals", __name__, url_prefix="/goals") # Alternate --> util module/helper functions, route_wrappers.py -def require_task_or_404(endpoint): +def require_instance_or_404(endpoint): """Decorator to validate input data.""" @wraps(endpoint) # Makes fn look like func to return def fn(*args, **kwargs): - task_id = kwargs.get("task_id", None) - task = Task.query.get(task_id) + if "task_id" in kwargs: + task_id = kwargs.get("task_id", None) + task = Task.query.get(task_id) - if not task: - return jsonify(None), 404 ## null + if not task: + return jsonify(None), 404 # null - kwargs.pop("task_id") - return endpoint(*args, task=task, **kwargs) + kwargs.pop("task_id") + return endpoint(*args, task=task, **kwargs) + + elif "goal_id" in kwargs: + goal_id = kwargs.get("goal_id", None) + goal = Goal.query.get(goal_id) + + if not goal: + return jsonify(None), 404 + + kwargs.pop("goal_id") + return endpoint(*args, goal=goal, **kwargs) return fn + @tasks_bp.route("", methods=["GET"]) def get_tasks(): """ @@ -47,7 +59,7 @@ def get_tasks(): return jsonify([task.to_dict() for task in query]), 200 @tasks_bp.route("/", methods=["GET"]) -@require_task_or_404 +@require_instance_or_404 def get_task(task): """Retrieve one stored task by id.""" if task.goal_id: @@ -79,7 +91,7 @@ def post_task(): return {"task": new_task.to_dict()}, 201 @tasks_bp.route("/", methods=["PUT"]) -@require_task_or_404 +@require_instance_or_404 def put_task(task): """Updates task by id.""" form_data = request.get_json() @@ -91,7 +103,7 @@ def put_task(task): return {"task": task.to_dict()}, 200 @tasks_bp.route("//mark_complete", methods=["PATCH"]) -@require_task_or_404 +@require_instance_or_404 def update_task_to_complete(task): """Updates task at particular id to completed using PATCH.""" # Make call to Slack API if task newly completed @@ -116,7 +128,7 @@ def update_task_to_complete(task): return {"task": task.to_dict()}, 200 @tasks_bp.route("/mark_incomplete", methods=["PATCH"]) -@require_task_or_404 +@require_instance_or_404 def update_task_to_incomplete(task): """Updates task at particular id to incomplete using PATCH.""" task.completed_at = None @@ -124,7 +136,7 @@ def update_task_to_incomplete(task): return {"task": task.to_dict()}, 200 @tasks_bp.route("/", methods=["DELETE"]) -@require_task_or_404 +@require_instance_or_404 def delete_task(task): """Deletes task by id.""" db.session.delete(task) @@ -142,10 +154,9 @@ def get_goals(): return jsonify([goal.to_dict() for goal in goals]), 200 @goals_bp.route("/", methods=["GET"]) -def get_goal(goal_id): +@require_instance_or_404 +def get_goal(goal): """Retrieve one stored goal by id.""" - goal = Goal.query.get_or_404(goal_id) - return jsonify({"goal": goal.to_dict()}), 200 @goals_bp.route("", methods=["POST"]) @@ -165,21 +176,20 @@ def create_goal(): return jsonify({"goal": new_goal.to_dict()}), 201 @goals_bp.route("/", methods=["PUT"]) -def update_goal(goal_id): +@require_instance_or_404 +def update_goal(goal): """Updates goal by id.""" form_data = request.get_json() - goal = Goal.query.get(goal_id) goal.update_from_dict(form_data) db.session.commit() return jsonify({"goal": goal.to_dict()}), 200 @goals_bp.route("/", methods=["DELETE"]) -def delete_goal(goal_id): +@require_instance_or_404 +def delete_goal(goal): """Deletes goal by id.""" - goal = Goal.query.get(goal_id) - db.session.delete(goal) db.session.commit() @@ -188,12 +198,9 @@ def delete_goal(goal_id): }, 200 @goals_bp.route("//tasks", methods=["POST"]) -def post_tasks_related_to_goal(goal_id): +@require_instance_or_404 +def post_tasks_related_to_goal(goal): """Adds tasks to goal wiht id.""" - goal = Goal.query.get(goal_id) - if not goal: - return jsonify(None), 404 - form_data = request.get_json() for task_id in form_data["task_ids"]: @@ -207,10 +214,7 @@ def post_tasks_related_to_goal(goal_id): }), 200 @goals_bp.route("//tasks", methods=["GET"]) -def get_tasks_related_to_goal(goal_id): +@require_instance_or_404 +def get_tasks_related_to_goal(goal): """Retrieves all tasks associated with goal id.""" - goal = Goal.query.get(goal_id) - if not goal: - return jsonify(None), 404 - return jsonify(goal.tasks_to_dict()), 200 \ No newline at end of file From 6e5c832337df15679db16dd4572e557abc1cca1b Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Thu, 4 Nov 2021 23:09:03 -0700 Subject: [PATCH 26/29] OMG my utils module works q --- app/routes.py | 43 +++++++++++++++++++------------------ app/utils/__init__.py | 0 app/utils/route_wrappers.py | 32 +++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 21 deletions(-) create mode 100644 app/utils/__init__.py create mode 100644 app/utils/route_wrappers.py diff --git a/app/routes.py b/app/routes.py index 6281b6d8b..22f495448 100644 --- a/app/routes.py +++ b/app/routes.py @@ -2,41 +2,42 @@ from flask import Blueprint, request, abort, jsonify, make_response from datetime import datetime from dotenv import load_dotenv -from functools import wraps +# from functools import wraps import os import requests from app.models.task import Task from app.models.goal import Goal +from app.utils.route_wrappers import require_instance_or_404 tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") goals_bp = Blueprint("goals", __name__, url_prefix="/goals") -# Alternate --> util module/helper functions, route_wrappers.py -def require_instance_or_404(endpoint): - """Decorator to validate input data.""" - @wraps(endpoint) # Makes fn look like func to return - def fn(*args, **kwargs): - if "task_id" in kwargs: - task_id = kwargs.get("task_id", None) - task = Task.query.get(task_id) +# # Alternate --> util module/helper functions, route_wrappers.py +# def require_instance_or_404(endpoint): +# """Decorator to validate input data.""" +# @wraps(endpoint) # Makes fn look like func to return +# def fn(*args, **kwargs): +# if "task_id" in kwargs: +# task_id = kwargs.get("task_id", None) +# task = Task.query.get(task_id) - if not task: - return jsonify(None), 404 # null +# if not task: +# return jsonify(None), 404 # null - kwargs.pop("task_id") - return endpoint(*args, task=task, **kwargs) +# kwargs.pop("task_id") +# return endpoint(*args, task=task, **kwargs) - elif "goal_id" in kwargs: - goal_id = kwargs.get("goal_id", None) - goal = Goal.query.get(goal_id) +# elif "goal_id" in kwargs: +# goal_id = kwargs.get("goal_id", None) +# goal = Goal.query.get(goal_id) - if not goal: - return jsonify(None), 404 +# if not goal: +# return jsonify(None), 404 - kwargs.pop("goal_id") - return endpoint(*args, goal=goal, **kwargs) +# kwargs.pop("goal_id") +# return endpoint(*args, goal=goal, **kwargs) - return fn +# return fn @tasks_bp.route("", methods=["GET"]) diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/utils/route_wrappers.py b/app/utils/route_wrappers.py new file mode 100644 index 000000000..a8dcb0a2c --- /dev/null +++ b/app/utils/route_wrappers.py @@ -0,0 +1,32 @@ +from functools import wraps +from flask import jsonify +from app.models.task import Task +from app.models.goal import Goal + +def require_instance_or_404(endpoint): + """ + Decorator to validate that a requested id of input data exists. + Returns JSON and 404 if not found.""" + @wraps(endpoint) # Makes fn look like func to return + def fn(*args, **kwargs): + if "task_id" in kwargs: + task_id = kwargs.get("task_id", None) + task = Task.query.get(task_id) + + if not task: + return jsonify(None), 404 # null + + kwargs.pop("task_id") + return endpoint(*args, task=task, **kwargs) + + elif "goal_id" in kwargs: + goal_id = kwargs.get("goal_id", None) + goal = Goal.query.get(goal_id) + + if not goal: + return jsonify(None), 404 + + kwargs.pop("goal_id") + return endpoint(*args, goal=goal, **kwargs) + + return fn From b3ac445d26f161ea8ea5ff45ee9a20ff13efffc2 Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Thu, 4 Nov 2021 23:10:45 -0700 Subject: [PATCH 27/29] Final commit, hopefully --- app/routes.py | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/app/routes.py b/app/routes.py index 22f495448..6dc614636 100644 --- a/app/routes.py +++ b/app/routes.py @@ -2,7 +2,6 @@ from flask import Blueprint, request, abort, jsonify, make_response from datetime import datetime from dotenv import load_dotenv -# from functools import wraps import os import requests from app.models.task import Task @@ -12,34 +11,6 @@ tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") goals_bp = Blueprint("goals", __name__, url_prefix="/goals") -# # Alternate --> util module/helper functions, route_wrappers.py -# def require_instance_or_404(endpoint): -# """Decorator to validate input data.""" -# @wraps(endpoint) # Makes fn look like func to return -# def fn(*args, **kwargs): -# if "task_id" in kwargs: -# task_id = kwargs.get("task_id", None) -# task = Task.query.get(task_id) - -# if not task: -# return jsonify(None), 404 # null - -# kwargs.pop("task_id") -# return endpoint(*args, task=task, **kwargs) - -# elif "goal_id" in kwargs: -# goal_id = kwargs.get("goal_id", None) -# goal = Goal.query.get(goal_id) - -# if not goal: -# return jsonify(None), 404 - -# kwargs.pop("goal_id") -# return endpoint(*args, goal=goal, **kwargs) - -# return fn - - @tasks_bp.route("", methods=["GET"]) def get_tasks(): """ @@ -73,8 +44,6 @@ def post_task(): """Create a new task from JSON data.""" form_data = request.get_json() - #TODO: Refactor to validation decorator helper method - # All fields must be provided mandatory_fields = ["title", "description", "completed_at"] for field in mandatory_fields: From 1d2d3ad8ccbdb7a8f519ff5de0681a247c2c43bd Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Thu, 4 Nov 2021 23:18:36 -0700 Subject: [PATCH 28/29] Adds Procfile --- 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 49ea494803925ddd3f6e48303a057502384cf69a Mon Sep 17 00:00:00 2001 From: Lia Gaetano Date: Thu, 4 Nov 2021 23:48:56 -0700 Subject: [PATCH 29/29] Debug goals/1/tasks POST --- app/routes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/routes.py b/app/routes.py index 6dc614636..2a202dbf4 100644 --- a/app/routes.py +++ b/app/routes.py @@ -174,7 +174,10 @@ def post_tasks_related_to_goal(goal): form_data = request.get_json() for task_id in form_data["task_ids"]: - goal.tasks.append(Task.query.get(task_id)) + query = Task.query.get(task_id) + if not query: + continue + goal.tasks.append(query) db.session.commit()