From b41197dd2b36644528a76b515b46d8b2b91d1851 Mon Sep 17 00:00:00 2001 From: ivystrayed Date: Thu, 3 Nov 2022 12:54:35 -0700 Subject: [PATCH 01/16] Set up task model and created function to get tasks from database --- app/__init__.py | 3 + app/models/task.py | 5 +- app/routes.py | 41 +++++++++++- migrations/README | 1 + migrations/alembic.ini | 45 +++++++++++++ migrations/env.py | 96 ++++++++++++++++++++++++++++ migrations/script.py.mako | 24 +++++++ migrations/versions/5dd236d26034_.py | 39 +++++++++++ 8 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 migrations/README create mode 100644 migrations/alembic.ini create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 migrations/versions/5dd236d26034_.py diff --git a/app/__init__.py b/app/__init__.py index 2764c4cc8..91cc49cf7 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -5,6 +5,7 @@ from dotenv import load_dotenv + db = SQLAlchemy() migrate = Migrate() load_dotenv() @@ -30,5 +31,7 @@ def create_app(test_config=None): migrate.init_app(app, db) # Register Blueprints here + from .routes import task_bp + app.register_blueprint(task_bp) return app diff --git a/app/models/task.py b/app/models/task.py index c91ab281f..459bbae3c 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -2,4 +2,7 @@ class Task(db.Model): - task_id = db.Column(db.Integer, primary_key=True) + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String) + description = db.Column(db.String) + completed_at = db.Column(db.DateTime, nullable = True) diff --git a/app/routes.py b/app/routes.py index 3aae38d49..c468f1194 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1 +1,40 @@ -from flask import Blueprint \ No newline at end of file +from app import db +from .models.task import Task +from flask import Blueprint, request, make_response, jsonify + + +task_bp = Blueprint("task_bp", __name__, url_prefix="/tasks") + + +@task_bp.route("", methods=["GET"]) +def get_task(): + task_list = Task.query.all() + response = [] + + for task in task_list: + task_dict = { + "id" :task.id, + "title" : task.title, + "description" : task.description, + "is_complete" : False + } + response.append(task_dict) + + return jsonify(response), 200 + + + + + +@task_bp.route("", methods = ["POST"]) +def add_task(): + request_body = request.get_json() + + new_task = { + "title" : request_body["title"], + "description" : request_body["description"], + } + + db.session.add(new_task) + db.session.commit() + diff --git a/migrations/README b/migrations/README new file mode 100644 index 000000000..98e4f9c44 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 000000000..f8ed4801f --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,45 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 000000000..8b3fb3353 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,96 @@ +from __future__ import with_statement + +import logging +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option( + 'sqlalchemy.url', + str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%')) +target_metadata = current_app.extensions['migrate'].db.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=target_metadata, literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix='sqlalchemy.', + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 000000000..2c0156303 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/5dd236d26034_.py b/migrations/versions/5dd236d26034_.py new file mode 100644 index 000000000..cf0968eee --- /dev/null +++ b/migrations/versions/5dd236d26034_.py @@ -0,0 +1,39 @@ +"""empty message + +Revision ID: 5dd236d26034 +Revises: +Create Date: 2022-11-03 11:37:47.776478 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '5dd236d26034' +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('comepleted_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 bed1cf2f01737e6c8b3acd57f65ba9e5bdbaac81 Mon Sep 17 00:00:00 2001 From: ivystrayed Date: Mon, 7 Nov 2022 10:39:09 -0800 Subject: [PATCH 02/16] created post method and to dict helper function in the model init --- app/models/task.py | 18 ++++++++++++++ app/routes.py | 37 ++++++++++++++-------------- migrations/versions/70c88c16985a_.py | 34 +++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 18 deletions(-) create mode 100644 migrations/versions/70c88c16985a_.py diff --git a/app/models/task.py b/app/models/task.py index 459bbae3c..9b0dd9ac1 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -1,4 +1,5 @@ from app import db +from flask import jsonify class Task(db.Model): @@ -6,3 +7,20 @@ class Task(db.Model): title = db.Column(db.String) description = db.Column(db.String) completed_at = db.Column(db.DateTime, nullable = True) + + + + def is_complete(self): + if self.completed_at == None: + return False + else: + return True + + def to_dict(self): + task_dict = { + "id" :self.id, + "title" : self.title, + "description" : self.description, + "is_complete" : self.is_complete() + } + return task_dict diff --git a/app/routes.py b/app/routes.py index c468f1194..1679f75fc 100644 --- a/app/routes.py +++ b/app/routes.py @@ -7,34 +7,35 @@ @task_bp.route("", methods=["GET"]) -def get_task(): - task_list = Task.query.all() +def get_all_task(): response = [] - + task_list = Task.query.all() for task in task_list: - task_dict = { - "id" :task.id, - "title" : task.title, - "description" : task.description, - "is_complete" : False - } - response.append(task_dict) - + response.append(task.to_dict()) return jsonify(response), 200 +@task_bp.route("/", methods=["GET"]) +def get_one_task(id): + try: + task = Task.query.get(id) + response = {"task" : task.to_dict()} + except: + return jsonify("Task Not Found"), 404 + return jsonify(response), 200 - - -@task_bp.route("", methods = ["POST"]) +@task_bp.route("", methods=["POST"]) def add_task(): request_body = request.get_json() - new_task = { - "title" : request_body["title"], - "description" : request_body["description"], - } + new_task = Task (title = request_body["title"], + description = request_body["description"]) db.session.add(new_task) db.session.commit() + return make_response({"task" : new_task.to_dict()}, 201) + + + + diff --git a/migrations/versions/70c88c16985a_.py b/migrations/versions/70c88c16985a_.py new file mode 100644 index 000000000..86869964f --- /dev/null +++ b/migrations/versions/70c88c16985a_.py @@ -0,0 +1,34 @@ +"""empty message + +Revision ID: 70c88c16985a +Revises: 5dd236d26034 +Create Date: 2022-11-03 16:47:55.009381 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '70c88c16985a' +down_revision = '5dd236d26034' +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('id', sa.Integer(), nullable=False)) + op.drop_column('task', 'task_id') + op.drop_column('task', 'comepleted_at') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('task', sa.Column('comepleted_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True)) + op.add_column('task', sa.Column('task_id', sa.INTEGER(), autoincrement=True, nullable=False)) + op.drop_column('task', 'id') + op.drop_column('task', 'completed_at') + # ### end Alembic commands ### From c98b41b7d5a913c318db23dee1f635e50ccdadf3 Mon Sep 17 00:00:00 2001 From: ivystrayed Date: Wed, 9 Nov 2022 13:07:03 -0800 Subject: [PATCH 03/16] Added update and Delete functions and created the validate task helper function to eliminate redundancy --- app/routes.py | 52 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/app/routes.py b/app/routes.py index 1679f75fc..71ef770ab 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,11 +1,25 @@ from app import db from .models.task import Task -from flask import Blueprint, request, make_response, jsonify +from flask import Blueprint, request, make_response, jsonify, abort task_bp = Blueprint("task_bp", __name__, url_prefix="/tasks") +def validate_task(task_id): + try: + task_id = int(task_id) + except: + abort(make_response({"message" : f"task id: {task_id} is invalid"}, 400)) + + task = Task.query.get(task_id) + + if not task: + abort(make_response({"message" : f"task {task_id} not found"}, 404)) + + return task + + @task_bp.route("", methods=["GET"]) def get_all_task(): response = [] @@ -14,14 +28,7 @@ def get_all_task(): response.append(task.to_dict()) return jsonify(response), 200 -@task_bp.route("/", methods=["GET"]) -def get_one_task(id): - try: - task = Task.query.get(id) - response = {"task" : task.to_dict()} - except: - return jsonify("Task Not Found"), 404 - return jsonify(response), 200 + @task_bp.route("", methods=["POST"]) @@ -36,6 +43,33 @@ def add_task(): return make_response({"task" : new_task.to_dict()}, 201) +@task_bp.route("/", methods=["GET"]) +def get_one_task(task_id): + + task = validate_task(task_id) + + return {"task" : task.to_dict()}, 200 + +@task_bp.route ("/", methods=["PUT"]) +def update_task(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()}, 200 + +@task_bp.route("/", methods=["DELETE"]) +def delete_task(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"}, 200) From 4d24c10c94cb6fe07f2f03d104331b375e536cce Mon Sep 17 00:00:00 2001 From: ivystrayed Date: Wed, 9 Nov 2022 15:21:15 -0800 Subject: [PATCH 04/16] WAVE 03 DONE, added query parameter which returns tasks sorted by title in ascending and descending order --- app/routes.py | 34 ++++++++++++++++++++++++---------- tests/test_wave_01.py | 34 +++++++++++++++++++--------------- tests/test_wave_02.py | 4 ++-- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/app/routes.py b/app/routes.py index 71ef770ab..d34092767 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,12 +1,13 @@ from app import db from .models.task import Task from flask import Blueprint, request, make_response, jsonify, abort +import sqlalchemy task_bp = Blueprint("task_bp", __name__, url_prefix="/tasks") -def validate_task(task_id): +def validate_task_id(task_id): try: task_id = int(task_id) except: @@ -22,37 +23,49 @@ def validate_task(task_id): @task_bp.route("", methods=["GET"]) def get_all_task(): - response = [] - task_list = Task.query.all() + + sort_query = request.args.get("sort") + if sort_query: + sort_function = getattr(sqlalchemy, sort_query) + task_list = Task.query.order_by(sort_function(Task.title)) + + else: + task_list = Task.query.all() + + response = [] for task in task_list: response.append(task.to_dict()) + return jsonify(response), 200 @task_bp.route("", methods=["POST"]) -def add_task(): +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() return make_response({"task" : new_task.to_dict()}, 201) + @task_bp.route("/", methods=["GET"]) def get_one_task(task_id): - task = validate_task(task_id) + task = validate_task_id(task_id) return {"task" : task.to_dict()}, 200 @task_bp.route ("/", methods=["PUT"]) def update_task(task_id): - task = validate_task(task_id) + task = validate_task_id(task_id) request_body = request.get_json() @@ -63,9 +76,10 @@ def update_task(task_id): return {"task" : task.to_dict()}, 200 + @task_bp.route("/", methods=["DELETE"]) def delete_task(task_id): - task = validate_task(task_id) + task = validate_task_id(task_id) db.session.delete(task) db.session.commit() diff --git a/tests/test_wave_01.py b/tests/test_wave_01.py index dca626d78..3c44ce88d 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") @@ -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") @@ -60,13 +60,15 @@ def test_get_task_not_found(client): # Assert assert response.status_code == 404 - 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*************** # ***************************************************************** + assert response_body == {"message" : "task 1 not found"} -@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={ @@ -93,7 +95,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={ @@ -119,7 +121,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={ @@ -131,13 +133,14 @@ def test_update_task_not_found(client): # Assert assert response.status_code == 404 - raise Exception("Complete test with assertion about response body") + # ***************************************************************** # **Complete test with assertion about response body*************** # ***************************************************************** + assert response_body == {"message" : "task 1 not found"} + - -@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") @@ -152,7 +155,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") @@ -161,15 +164,16 @@ def test_delete_task_not_found(client): # Assert assert response.status_code == 404 - 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*************** # ***************************************************************** assert Task.query.all() == [] + assert response_body == {"message" : "task 1 not found"} -@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={ @@ -186,7 +190,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={ diff --git a/tests/test_wave_02.py b/tests/test_wave_02.py index a087e0909..651e3aebd 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") @@ -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 4a66f09a3e015792993ed44b28fc76fdb3f6f54e Mon Sep 17 00:00:00 2001 From: ivystrayed Date: Wed, 9 Nov 2022 16:00:58 -0800 Subject: [PATCH 05/16] refactored create task and validate_task_id methods into class methods --- app/models/task.py | 26 ++++++++++++++++++++- app/routes.py | 58 +++++++++++++++++++++------------------------- 2 files changed, 51 insertions(+), 33 deletions(-) diff --git a/app/models/task.py b/app/models/task.py index 9b0dd9ac1..be429d5eb 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -1,5 +1,5 @@ from app import db -from flask import jsonify +from flask import jsonify, abort, make_response class Task(db.Model): @@ -24,3 +24,27 @@ def to_dict(self): "is_complete" : self.is_complete() } return task_dict + + @classmethod + def from_dict(cls, request_body): + try: + task = Task(title = request_body["title"], + description = request_body["description"]) + return task + except: + abort(make_response({"details" : "Invalid data"}, 400)) + + @classmethod + def validate_task_id(cls, task_id): + try: + task_id = int(task_id) + except: + abort(make_response({"message" : f"task id: {task_id} is invalid"}, 400)) + + task = Task.query.get(task_id) + + if not task: + abort(make_response({"message" : f"task {task_id} not found"}, 404)) + + return task + \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index d34092767..8370b53cb 100644 --- a/app/routes.py +++ b/app/routes.py @@ -7,28 +7,27 @@ task_bp = Blueprint("task_bp", __name__, url_prefix="/tasks") -def validate_task_id(task_id): - try: - task_id = int(task_id) - except: - abort(make_response({"message" : f"task id: {task_id} is invalid"}, 400)) - - task = Task.query.get(task_id) - - if not task: - abort(make_response({"message" : f"task {task_id} not found"}, 404)) +# all task methods + # Create +@task_bp.route("", methods=["POST"]) +def create_task(): - return task + request_body = request.get_json() + new_task = Task.from_dict(request_body) + db.session.add(new_task) + db.session.commit() + return make_response({"task" : new_task.to_dict()}, 201) + + # Read @task_bp.route("", methods=["GET"]) def get_all_task(): - + sort_query = request.args.get("sort") if sort_query: sort_function = getattr(sqlalchemy, sort_query) task_list = Task.query.order_by(sort_function(Task.title)) - else: task_list = Task.query.all() @@ -36,36 +35,24 @@ def get_all_task(): for task in task_list: response.append(task.to_dict()) - return jsonify(response), 200 + return jsonify(response), 200 - - -@task_bp.route("", methods=["POST"]) -def create_task(): - request_body = request.get_json() - 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() - - return make_response({"task" : new_task.to_dict()}, 201) +# Individual task methods + # Read @task_bp.route("/", methods=["GET"]) def get_one_task(task_id): - task = validate_task_id(task_id) + task = Task.validate_task_id(task_id) return {"task" : task.to_dict()}, 200 + # Update @task_bp.route ("/", methods=["PUT"]) def update_task(task_id): - task = validate_task_id(task_id) + task = Task.validate_task_id(task_id) request_body = request.get_json() @@ -76,10 +63,17 @@ def update_task(task_id): return {"task" : task.to_dict()}, 200 +@task_bp.route("/", methods=["PATCH"]) +def mark_task_status(task_id, status): + pass + + + + # Delete @task_bp.route("/", methods=["DELETE"]) def delete_task(task_id): - task = validate_task_id(task_id) + task = Task.validate_task_id(task_id) db.session.delete(task) db.session.commit() From 2819eb59c660670fee6f1f7bf44a8afa9d5090fe Mon Sep 17 00:00:00 2001 From: ivystrayed Date: Wed, 9 Nov 2022 16:39:43 -0800 Subject: [PATCH 06/16] WAVE3 DONE, added helper functions in model to mark a task complete and incomplete, added a function in routes which takes in both task number and complete/incomplete as variable and routes to helper functions --- app/models/task.py | 15 ++++++++++++++- app/routes.py | 15 ++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/app/models/task.py b/app/models/task.py index be429d5eb..26db2ea05 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -1,5 +1,6 @@ from app import db -from flask import jsonify, abort, make_response +from flask import abort, make_response +import datetime class Task(db.Model): @@ -24,6 +25,18 @@ def to_dict(self): "is_complete" : self.is_complete() } return task_dict + + def mark_complete(self): + if self.is_complete(): + pass + else: + self.completed_at = datetime.datetime.now() + + def mark_incomplete(self): + if not self.is_complete(): + pass + else: + self.completed_at = None @classmethod def from_dict(cls, request_body): diff --git a/app/routes.py b/app/routes.py index 8370b53cb..b74d8d6f8 100644 --- a/app/routes.py +++ b/app/routes.py @@ -2,6 +2,7 @@ from .models.task import Task from flask import Blueprint, request, make_response, jsonify, abort import sqlalchemy +import datetime task_bp = Blueprint("task_bp", __name__, url_prefix="/tasks") @@ -63,9 +64,17 @@ def update_task(task_id): return {"task" : task.to_dict()}, 200 -@task_bp.route("/", methods=["PATCH"]) -def mark_task_status(task_id, status): - pass +@task_bp.route("/", methods=["PATCH"]) +def mark_task_status(task_id, marker): + task = Task.validate_task_id(task_id) + eval("task." + marker + "()") + + + db.session.commit() + + return {"task" : task.to_dict()}, 200 + + From 2ecdc37885234ea177de6cb51d47bea2a1e4d035 Mon Sep 17 00:00:00 2001 From: ivystrayed Date: Wed, 9 Nov 2022 16:40:30 -0800 Subject: [PATCH 07/16] WAVE 3 DONE --- tests/test_wave_03.py | 43 ++++++++++++++++--------------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/tests/test_wave_03.py b/tests/test_wave_03.py index 32d379822..85986da51 100644 --- a/tests/test_wave_03.py +++ b/tests/test_wave_03.py @@ -3,12 +3,7 @@ from datetime import datetime from app.models.task import Task import pytest - - -@pytest.mark.skip(reason="No way to test this feature yet") -def test_mark_complete_on_incomplete_task(client, one_task): - # Arrange - """ +""" The future Wave 4 adds special functionality to this route, so for this test, we need to set-up "mocking." @@ -19,7 +14,12 @@ def test_mark_complete_on_incomplete_task(client, one_task): test (due to Wave 4). There is no action needed here, the tests should work as-is. - """ +""" + +# @pytest.mark.skip(reason="No way to test this feature yet") +def test_mark_complete_on_incomplete_task(client, one_task): + # Arrange + with patch("requests.post") as mock_get: mock_get.return_value.status_code = 200 @@ -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") @@ -62,21 +62,9 @@ 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 - """ - The future Wave 4 adds special functionality to this route, - so for this test, we need to set-up "mocking." - - Mocking will help our tests work in isolation, which is a - good thing! - - We need to mock any POST requests that may occur during this - test (due to Wave 4). - - There is no action needed here, the tests should work as-is. - """ with patch("requests.post") as mock_get: mock_get.return_value.status_code = 200 @@ -99,7 +87,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 +107,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") @@ -128,13 +116,13 @@ def test_mark_complete_missing_task(client): # Assert assert response.status_code == 404 - 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*************** # ***************************************************************** + assert response_body == {"message" : "task 1 not found"} - -@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") @@ -143,7 +131,8 @@ def test_mark_incomplete_missing_task(client): # Assert assert response.status_code == 404 - 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*************** # ***************************************************************** + assert response_body == {"message" : "task 1 not found"} \ No newline at end of file From 5169e25cc25385e83abbe9dfa332be6458f06a67 Mon Sep 17 00:00:00 2001 From: ivystrayed Date: Wed, 9 Nov 2022 17:43:32 -0800 Subject: [PATCH 08/16] created Goal model, blueprint, goal_db and copied task methods. still need to DRY and finish up the conversion from task to goal --- app/__init__.py | 3 +- app/models/goal.py | 34 ++++++++++- app/routes.py | 88 +++++++++++++++++++++++++--- migrations/versions/62b67d156b2c_.py | 28 +++++++++ 4 files changed, 144 insertions(+), 9 deletions(-) create mode 100644 migrations/versions/62b67d156b2c_.py diff --git a/app/__init__.py b/app/__init__.py index 91cc49cf7..0304a5bb2 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -31,7 +31,8 @@ def create_app(test_config=None): migrate.init_app(app, db) # Register Blueprints here - from .routes import task_bp + from .routes import task_bp, goal_bp app.register_blueprint(task_bp) + app.register_blueprint(goal_bp) return app diff --git a/app/models/goal.py b/app/models/goal.py index b0ed11dd8..493e4e9e6 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -1,5 +1,37 @@ from app import db +from flask import abort, make_response 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): + goal_dict = { + "id" : self.id, + "title" : self.title, + } + return goal_dict + + @classmethod + def from_dict(cls, request_body): + try: + goal = Goal(title = request_body["title"]) + return goal + except: + abort(make_response({"details" : "Invalid data"}, 400)) + + @classmethod + def validate_goal_id(cls, goal_id): + try: + goal_id = int(goal_id) + except: + abort(make_response({"message" : f"task id: {goal_id} is invalid"}, 400)) + + goal = Goal.query.get(goal_id) + + if not goal: + abort(make_response({"message" : f"task {goal_id} not found"}, 404)) + + return goal + \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index b74d8d6f8..9f64e515e 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,8 +1,9 @@ from app import db from .models.task import Task +from .models.goal import Goal from flask import Blueprint, request, make_response, jsonify, abort import sqlalchemy -import datetime + task_bp = Blueprint("task_bp", __name__, url_prefix="/tasks") @@ -69,15 +70,9 @@ def mark_task_status(task_id, marker): task = Task.validate_task_id(task_id) eval("task." + marker + "()") - db.session.commit() return {"task" : task.to_dict()}, 200 - - - - - # Delete @task_bp.route("/", methods=["DELETE"]) @@ -90,3 +85,82 @@ def delete_task(task_id): return make_response({"details" : f"Task {task_id} \"{task.title}\" successfully deleted"}, 200) +goal_bp = Blueprint("goal_bp", __name__, url_prefix="/goals") + +# all goal methods + # Create +@goal_bp.route("", methods=["POST"]) +def create_goal(): + + request_body = request.get_json() + new_goal = Goal.from_dict(request_body) + + db.session.add(new_goal) + db.session.commit() + + return make_response({"goal" : new_goal.to_dict()}, 201) + + # Read +@goal_bp.route("", methods=["GET"]) +def get_all_goals(): + + sort_query = request.args.get("sort") + if sort_query: + sort_function = getattr(sqlalchemy, sort_query) + goal_list = Goal.query.order_by(sort_function(Goal.title)) + else: + goal_list = Goal.query.all() + + response = [] + for goal in goal_list: + response.append(goal.to_dict()) + + return jsonify(response), 200 + + + +# Individual goal methods + + # Read +@goal_bp.route("/", methods=["GET"]) +def get_specific_goal(goal_id): + + goal = Goal.validate_goal_id(goal_id) + + return {"goal" : goal.to_dict()}, 200 +@goal_bp.route("", methods = ["GET"]) +def get_any_goal(): + goal = Goal.query.get(1) + return{"goal" : goal.to_dict()}, 200 + + # Update +@goal_bp.route ("/", methods=["PUT"]) +def update_goal(goal_id): + goal = Goal.validate_task_id(goal_id) + + request_body = request.get_json() + + goal.title = request_body["title"] + + db.session.commit() + + return {"goal" : goal.to_dict()}, 200 + +# @goal_bp.route("/", methods=["PATCH"]) +# def mark_goal_status(goal_id, marker): +# goal = Goal.validate_goal_id(goal_id) +# eval("goal." + marker + "()") + +# db.session.commit() + +# return {"goal" : goal.to_dict()}, 200 + + # Delete +# @goal_bp.route("/", methods=["DELETE"]) +# def delete_goal(goal_id): +# goal = Goal.validate_goal_id(goal_id) + +# db.session.delete(goal) +# db.session.commit() + +# return make_response({"details" : f"Task {goal_id} \"{goal.title}\" successfully deleted"}, 200) diff --git a/migrations/versions/62b67d156b2c_.py b/migrations/versions/62b67d156b2c_.py new file mode 100644 index 000000000..6d2a518d3 --- /dev/null +++ b/migrations/versions/62b67d156b2c_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 62b67d156b2c +Revises: 70c88c16985a +Create Date: 2022-11-09 17:06:42.471730 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '62b67d156b2c' +down_revision = '70c88c16985a' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('goal', sa.Column('title', sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('goal', 'title') + # ### end Alembic commands ### From 3a83758cf174e0f58e2b94e922c2885074fb324a Mon Sep 17 00:00:00 2001 From: ivystrayed Date: Sat, 12 Nov 2022 13:55:27 -0800 Subject: [PATCH 09/16] WAVE 05 COMPLETE, finished goal model and routes --- app/models/goal.py | 4 +- app/routes.py | 26 ++++++------- tests/test_wave_05.py | 85 ++++++++++++++++++++++--------------------- 3 files changed, 59 insertions(+), 56 deletions(-) diff --git a/app/models/goal.py b/app/models/goal.py index 493e4e9e6..7bb60cae2 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -26,12 +26,12 @@ def validate_goal_id(cls, goal_id): try: goal_id = int(goal_id) except: - abort(make_response({"message" : f"task id: {goal_id} is invalid"}, 400)) + abort(make_response({"message" : f"goal id: {goal_id} is invalid"}, 400)) goal = Goal.query.get(goal_id) if not goal: - abort(make_response({"message" : f"task {goal_id} not found"}, 404)) + abort(make_response({"message" : f"goal {goal_id} not found"}, 404)) return goal \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index 9f64e515e..d27e53972 100644 --- a/app/routes.py +++ b/app/routes.py @@ -136,7 +136,7 @@ def get_any_goal(): # Update @goal_bp.route ("/", methods=["PUT"]) def update_goal(goal_id): - goal = Goal.validate_task_id(goal_id) + goal = Goal.validate_goal_id(goal_id) request_body = request.get_json() @@ -146,21 +146,21 @@ def update_goal(goal_id): return {"goal" : goal.to_dict()}, 200 -# @goal_bp.route("/", methods=["PATCH"]) -# def mark_goal_status(goal_id, marker): -# goal = Goal.validate_goal_id(goal_id) -# eval("goal." + marker + "()") +@goal_bp.route("/", methods=["PATCH"]) +def mark_goal_status(goal_id, marker): + goal = Goal.validate_goal_id(goal_id) + eval("goal." + marker + "()") -# db.session.commit() + db.session.commit() -# return {"goal" : goal.to_dict()}, 200 + return {"goal" : goal.to_dict()}, 200 # Delete -# @goal_bp.route("/", methods=["DELETE"]) -# def delete_goal(goal_id): -# goal = Goal.validate_goal_id(goal_id) +@goal_bp.route("/", methods=["DELETE"]) +def delete_goal(goal_id): + goal = Goal.validate_goal_id(goal_id) -# db.session.delete(goal) -# db.session.commit() + db.session.delete(goal) + db.session.commit() -# return make_response({"details" : f"Task {goal_id} \"{goal.title}\" successfully deleted"}, 200) + return make_response({"details" : f"Goal {goal_id} \"{goal.title}\" successfully deleted"}, 200) diff --git a/tests/test_wave_05.py b/tests/test_wave_05.py index aee7c52a1..5c61993f9 100644 --- a/tests/test_wave_05.py +++ b/tests/test_wave_05.py @@ -1,7 +1,9 @@ import pytest +from app.models.goal import 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_goals_no_saved_goals(client): # Act response = client.get("/goals") @@ -12,7 +14,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") @@ -29,7 +31,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,22 +48,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") - # Assert - # ---- Complete Test ---- - # assertion 1 goes here - # assertion 2 goes here - # ---- Complete Test ---- + assert response_body == {"message" : f"goal 1 not found"} + assert response.status_code == 404 -@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={ @@ -80,34 +79,44 @@ 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": "An updated, more-acheivable, easier goal" + }) + + response_body = response.get_json() + # Assert - # ---- Complete Assertions Here ---- - # assertion 1 goes here - # assertion 2 goes here - # assertion 3 goes here - # ---- Complete Assertions Here ---- + goal = Goal.query.get(1) + assert response_body == {"goal" :{ + "id" : 1, + "title" : "An updated, more-acheivable, easier goal" + }} + assert response.status_code == 200 + assert goal.title == "An updated, more-acheivable, easier goal" + -@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") # Act - # ---- Complete Act Here ---- - + response = client.put("/goals/1", json={ + "title": "An updated, more-acheivable, easier goal" + }) + response_body = response.get_json() # Assert - # ---- Complete Assertions Here ---- - # assertion 1 goes here - # assertion 2 goes here - # ---- Complete Assertions Here ---- + + assert response_body == {"message" : f"goal 1 not found"} + assert response.status_code == 404 + -@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") @@ -124,27 +133,21 @@ def test_delete_goal(client, one_goal): response = client.get("/goals/1") assert response.status_code == 404 - raise Exception("Complete test with assertion about response body") - # ***************************************************************** - # **Complete test with assertion about response body*************** - # ***************************************************************** - + response_body = response.get_json() + assert response_body == {"message" : "goal 1 not found"} -@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") # 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 - # ---- Complete Assertions Here ---- + assert response.status_code == 404 + assert response_body == {"message" : "goal 1 not found"} -@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 14144efb983749f3fc08daead557f9fae87d9841 Mon Sep 17 00:00:00 2001 From: ivystrayed Date: Sat, 12 Nov 2022 15:47:33 -0800 Subject: [PATCH 10/16] WAVE 06 COMPLETE, created connected database and adjusted to dictionary functions to deal with new situaitons --- app/models/goal.py | 15 +++++++++++++++ app/models/task.py | 4 ++++ app/routes.py | 29 +++++++++++++++++++++++++++++ tests/test_wave_06.py | 16 ++++++++-------- 4 files changed, 56 insertions(+), 8 deletions(-) diff --git a/app/models/goal.py b/app/models/goal.py index 7bb60cae2..bfcaf2bda 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") def to_dict(self): goal_dict = { @@ -13,6 +14,20 @@ def to_dict(self): } return goal_dict + def get_tasks(self): + response = [] + for task in self.tasks: + response.append(task.to_dict()) + return response + + def get_task_ids(self): + response = [] + for task in self.tasks: + response.append(task.id) + return response + + + @classmethod def from_dict(cls, request_body): try: diff --git a/app/models/task.py b/app/models/task.py index 26db2ea05..b882661f8 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -8,6 +8,8 @@ class Task(db.Model): title = db.Column(db.String) description = db.Column(db.String) completed_at = db.Column(db.DateTime, nullable = True) + goal_id = db.Column(db.Integer, db.ForeignKey("goal.id"), nullable = True) + @@ -24,6 +26,8 @@ def to_dict(self): "description" : self.description, "is_complete" : self.is_complete() } + if self.goal_id != None: + task_dict.update({"goal_id" : self.goal_id}) return task_dict def mark_complete(self): diff --git a/app/routes.py b/app/routes.py index d27e53972..7ee983b3d 100644 --- a/app/routes.py +++ b/app/routes.py @@ -164,3 +164,32 @@ def delete_goal(goal_id): db.session.commit() return make_response({"details" : f"Goal {goal_id} \"{goal.title}\" successfully deleted"}, 200) + + +# Nested route for task assigned to one goal + +@goal_bp.route("//tasks", methods=["POST"]) +def post_task_ids_to_goal(goal_id): + goal = Goal.validate_goal_id(goal_id) + + request_body = request.get_json() + + + for task in request_body["task_ids"]: + new_task = Task.validate_task_id(task) + new_task.goal_id = goal_id + + db.session.commit() + + return make_response({ + "id" : goal.id, + "task_ids" : goal.get_task_ids() + }, 200) + +@goal_bp.route("//tasks", methods=["GET"]) +def get_tasks_from_goal(goal_id): + goal = Goal.validate_goal_id(goal_id) + response_body = goal.to_dict() + response_body.update({"tasks" : goal.get_tasks()}) + + return make_response(response_body, 200) diff --git a/tests/test_wave_06.py b/tests/test_wave_06.py index 8afa4325e..c81ab58f9 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={ @@ -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={ @@ -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") @@ -51,13 +51,13 @@ def test_get_tasks_for_specific_goal_no_goal(client): # Assert assert response.status_code == 404 - 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*************** # ***************************************************************** + assert response_body == {"message" : "goal 1 not found"} - -@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") @@ -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") @@ -99,7 +99,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 d92bdb2889d77f739b3c19a1bacc2522d8e387a8 Mon Sep 17 00:00:00 2001 From: ivystrayed Date: Sat, 12 Nov 2022 16:03:57 -0800 Subject: [PATCH 11/16] added 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 6c00ab60ee5c2bce12f5a46d75a785a363199ddb Mon Sep 17 00:00:00 2001 From: ivystrayed Date: Mon, 14 Nov 2022 12:44:29 -0800 Subject: [PATCH 12/16] refactored so that goal routes and task routes exist in their own python files and created a helper function for routes to validate the models to eliminate redundency in task and goal models --- app/__init__.py | 3 +- app/{routes.py => goal_routes.py} | 94 +++---------------------------- app/models/goal.py | 13 ----- app/models/task.py | 15 ----- app/route_helpers.py | 19 +++++++ app/task_routes.py | 86 ++++++++++++++++++++++++++++ 6 files changed, 115 insertions(+), 115 deletions(-) rename app/{routes.py => goal_routes.py} (54%) create mode 100644 app/route_helpers.py create mode 100644 app/task_routes.py diff --git a/app/__init__.py b/app/__init__.py index 0304a5bb2..327c78ccd 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -31,8 +31,9 @@ def create_app(test_config=None): migrate.init_app(app, db) # Register Blueprints here - from .routes import task_bp, goal_bp + from .task_routes import task_bp app.register_blueprint(task_bp) + from .goal_routes import goal_bp app.register_blueprint(goal_bp) return app diff --git a/app/routes.py b/app/goal_routes.py similarity index 54% rename from app/routes.py rename to app/goal_routes.py index 7ee983b3d..e4de5fcd2 100644 --- a/app/routes.py +++ b/app/goal_routes.py @@ -2,89 +2,11 @@ from .models.task import Task from .models.goal import Goal from flask import Blueprint, request, make_response, jsonify, abort -import sqlalchemy +import sqlalchemy +from .route_helpers import validate_model_id -task_bp = Blueprint("task_bp", __name__, url_prefix="/tasks") - - -# all task methods - # Create -@task_bp.route("", methods=["POST"]) -def create_task(): - - request_body = request.get_json() - new_task = Task.from_dict(request_body) - - db.session.add(new_task) - db.session.commit() - - return make_response({"task" : new_task.to_dict()}, 201) - - # Read -@task_bp.route("", methods=["GET"]) -def get_all_task(): - - sort_query = request.args.get("sort") - if sort_query: - sort_function = getattr(sqlalchemy, sort_query) - task_list = Task.query.order_by(sort_function(Task.title)) - else: - task_list = Task.query.all() - - response = [] - for task in task_list: - response.append(task.to_dict()) - - return jsonify(response), 200 - - - -# Individual task methods - - # Read -@task_bp.route("/", methods=["GET"]) -def get_one_task(task_id): - - task = Task.validate_task_id(task_id) - - return {"task" : task.to_dict()}, 200 - - # Update -@task_bp.route ("/", methods=["PUT"]) -def update_task(task_id): - task = Task.validate_task_id(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()}, 200 - -@task_bp.route("/", methods=["PATCH"]) -def mark_task_status(task_id, marker): - task = Task.validate_task_id(task_id) - eval("task." + marker + "()") - - db.session.commit() - - return {"task" : task.to_dict()}, 200 - - # Delete -@task_bp.route("/", methods=["DELETE"]) -def delete_task(task_id): - task = Task.validate_task_id(task_id) - - db.session.delete(task) - db.session.commit() - - return make_response({"details" : f"Task {task_id} \"{task.title}\" successfully deleted"}, 200) - - goal_bp = Blueprint("goal_bp", __name__, url_prefix="/goals") # all goal methods @@ -125,7 +47,7 @@ def get_all_goals(): @goal_bp.route("/", methods=["GET"]) def get_specific_goal(goal_id): - goal = Goal.validate_goal_id(goal_id) + goal = validate_model_id(Goal, goal_id) return {"goal" : goal.to_dict()}, 200 @goal_bp.route("", methods = ["GET"]) @@ -136,7 +58,7 @@ def get_any_goal(): # Update @goal_bp.route ("/", methods=["PUT"]) def update_goal(goal_id): - goal = Goal.validate_goal_id(goal_id) + goal = validate_model_id(Goal, goal_id) request_body = request.get_json() @@ -148,7 +70,7 @@ def update_goal(goal_id): @goal_bp.route("/", methods=["PATCH"]) def mark_goal_status(goal_id, marker): - goal = Goal.validate_goal_id(goal_id) + goal = validate_model_id(Goal, goal_id) eval("goal." + marker + "()") db.session.commit() @@ -158,7 +80,7 @@ def mark_goal_status(goal_id, marker): # Delete @goal_bp.route("/", methods=["DELETE"]) def delete_goal(goal_id): - goal = Goal.validate_goal_id(goal_id) + goal = validate_model_id(Goal, goal_id) db.session.delete(goal) db.session.commit() @@ -170,7 +92,7 @@ def delete_goal(goal_id): @goal_bp.route("//tasks", methods=["POST"]) def post_task_ids_to_goal(goal_id): - goal = Goal.validate_goal_id(goal_id) + goal = validate_model_id(Goal, goal_id) request_body = request.get_json() @@ -188,7 +110,7 @@ def post_task_ids_to_goal(goal_id): @goal_bp.route("//tasks", methods=["GET"]) def get_tasks_from_goal(goal_id): - goal = Goal.validate_goal_id(goal_id) + goal = validate_model_id(Goal, goal_id) response_body = goal.to_dict() response_body.update({"tasks" : goal.get_tasks()}) diff --git a/app/models/goal.py b/app/models/goal.py index bfcaf2bda..df910f9e3 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -36,17 +36,4 @@ def from_dict(cls, request_body): except: abort(make_response({"details" : "Invalid data"}, 400)) - @classmethod - def validate_goal_id(cls, goal_id): - try: - goal_id = int(goal_id) - except: - abort(make_response({"message" : f"goal id: {goal_id} is invalid"}, 400)) - - goal = Goal.query.get(goal_id) - - if not goal: - abort(make_response({"message" : f"goal {goal_id} not found"}, 404)) - - return goal \ No newline at end of file diff --git a/app/models/task.py b/app/models/task.py index b882661f8..078d39729 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -50,18 +50,3 @@ def from_dict(cls, request_body): return task except: abort(make_response({"details" : "Invalid data"}, 400)) - - @classmethod - def validate_task_id(cls, task_id): - try: - task_id = int(task_id) - except: - abort(make_response({"message" : f"task id: {task_id} is invalid"}, 400)) - - task = Task.query.get(task_id) - - if not task: - abort(make_response({"message" : f"task {task_id} not found"}, 404)) - - return task - \ No newline at end of file diff --git a/app/route_helpers.py b/app/route_helpers.py new file mode 100644 index 000000000..dfc460475 --- /dev/null +++ b/app/route_helpers.py @@ -0,0 +1,19 @@ + +from flask import make_response, abort + + + +def validate_model_id(cls, id): + cls_name = cls.__name__ + + try: + id = int(id) + except: + abort(make_response({"message" : f"{cls_name.lower()} id: {id} is invalid"}, 400)) + + model = cls.query.get(id) + + if not model: + abort(make_response({"message" : f"{cls_name.lower()} {id} not found"}, 404)) + + return model \ No newline at end of file diff --git a/app/task_routes.py b/app/task_routes.py new file mode 100644 index 000000000..2695252d1 --- /dev/null +++ b/app/task_routes.py @@ -0,0 +1,86 @@ +from app import db +from .models.task import Task +from .models.goal import Goal +from flask import Blueprint, request, make_response, jsonify, abort +import sqlalchemy +from .route_helpers import validate_model_id + + + +task_bp = Blueprint("task_bp", __name__, url_prefix="/tasks") + + +# all task methods + # Create +@task_bp.route("", methods=["POST"]) +def create_task(): + + request_body = request.get_json() + new_task = Task.from_dict(request_body) + + db.session.add(new_task) + db.session.commit() + + return make_response({"task" : new_task.to_dict()}, 201) + + # Read +@task_bp.route("", methods=["GET"]) +def get_all_task(): + + sort_query = request.args.get("sort") + if sort_query: + sort_function = getattr(sqlalchemy, sort_query) + task_list = Task.query.order_by(sort_function(Task.title)) + else: + task_list = Task.query.all() + + response = [] + for task in task_list: + response.append(task.to_dict()) + + return jsonify(response), 200 + + + +# Individual task methods + + # Read +@task_bp.route("/", methods=["GET"]) +def get_one_task(task_id): + + task = validate_model_id(Task, task_id) + + return {"task" : task.to_dict()}, 200 + + # Update +@task_bp.route ("/", methods=["PUT"]) +def update_task(task_id): + task = validate_model_id(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()}, 200 + +@task_bp.route("/", methods=["PATCH"]) +def mark_task_status(task_id, marker): + task = validate_model_id(Task, task_id) + eval("task." + marker + "()") + + db.session.commit() + + return {"task" : task.to_dict()}, 200 + + # Delete +@task_bp.route("/", methods=["DELETE"]) +def delete_task(task_id): + task = validate_model_id(Task, task_id) + + db.session.delete(task) + db.session.commit() + + return make_response({"details" : f"Task {task_id} \"{task.title}\" successfully deleted"}, 200) From 30211b1b521c8b0b1170ff850d15c3d118aac4d0 Mon Sep 17 00:00:00 2001 From: ivystrayed Date: Mon, 14 Nov 2022 13:36:34 -0800 Subject: [PATCH 13/16] added tests and methods to get a single random goal --- app/goal_routes.py | 18 +++++++++++------- tests/conftest.py | 9 +++++++++ tests/test_wave_05.py | 9 +++++++++ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/app/goal_routes.py b/app/goal_routes.py index e4de5fcd2..f46a5fbab 100644 --- a/app/goal_routes.py +++ b/app/goal_routes.py @@ -4,6 +4,7 @@ from flask import Blueprint, request, make_response, jsonify, abort import sqlalchemy from .route_helpers import validate_model_id +import random @@ -32,12 +33,19 @@ def get_all_goals(): goal_list = Goal.query.order_by(sort_function(Goal.title)) else: goal_list = Goal.query.all() - + response = [] for goal in goal_list: response.append(goal.to_dict()) return jsonify(response), 200 +@goal_bp.route("random", methods=["GET"]) +def get_random_goal(): + goal_list = Goal.query.all() + max_index = len(goal_list) - 1 + rand_goal = goal_list[random.randint(0,max_index)] + + return jsonify({"goal" : rand_goal.to_dict()}), 200 @@ -50,10 +58,6 @@ def get_specific_goal(goal_id): goal = validate_model_id(Goal, goal_id) return {"goal" : goal.to_dict()}, 200 -@goal_bp.route("", methods = ["GET"]) -def get_any_goal(): - goal = Goal.query.get(1) - return{"goal" : goal.to_dict()}, 200 # Update @goal_bp.route ("/", methods=["PUT"]) @@ -97,8 +101,8 @@ def post_task_ids_to_goal(goal_id): request_body = request.get_json() - for task in request_body["task_ids"]: - new_task = Task.validate_task_id(task) + for task_id in request_body["task_ids"]: + new_task = validate_model_id(Task, task_id) new_task.goal_id = goal_id db.session.commit() diff --git a/tests/conftest.py b/tests/conftest.py index 6639378e6..d5f062618 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -79,6 +79,15 @@ def one_goal(app): db.session.add(new_goal) db.session.commit() +@pytest.fixture +def three_goals(app): + db.session.add_all([ + Goal(title="Eat Three Meals a Day"), + Goal(title="Do 3 tattoos a week"), + Goal(title="Get Better at Texting Friends"), + ]) + db.session.commit() + # This fixture gets called in every test that # references "one_task_belongs_to_one_goal" diff --git a/tests/test_wave_05.py b/tests/test_wave_05.py index 5c61993f9..3e6cd9c28 100644 --- a/tests/test_wave_05.py +++ b/tests/test_wave_05.py @@ -30,6 +30,15 @@ def test_get_goals_one_saved_goal(client, one_goal): } ] +def test_get_one_random_goal(client, three_goals): + response = client.get("/goals/random") + response_body = response.get_json() + + assert response.status_code == 200 + assert "goal" in response_body + assert len(response_body) == 1 + + # @pytest.mark.skip(reason="No way to test this feature yet") def test_get_goal(client, one_goal): From e99db1766db0d65c84bb2f5d0b5760bee807411a Mon Sep 17 00:00:00 2001 From: ivystrayed Date: Mon, 14 Nov 2022 14:14:02 -0800 Subject: [PATCH 14/16] commited final changes before deploying --- app/goal_routes.py | 1 + migrations/versions/8524b4ed10c8_.py | 30 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 migrations/versions/8524b4ed10c8_.py diff --git a/app/goal_routes.py b/app/goal_routes.py index f46a5fbab..b2d32b6c7 100644 --- a/app/goal_routes.py +++ b/app/goal_routes.py @@ -39,6 +39,7 @@ def get_all_goals(): response.append(goal.to_dict()) return jsonify(response), 200 + @goal_bp.route("random", methods=["GET"]) def get_random_goal(): goal_list = Goal.query.all() diff --git a/migrations/versions/8524b4ed10c8_.py b/migrations/versions/8524b4ed10c8_.py new file mode 100644 index 000000000..76e8513f9 --- /dev/null +++ b/migrations/versions/8524b4ed10c8_.py @@ -0,0 +1,30 @@ +"""empty message + +Revision ID: 8524b4ed10c8 +Revises: 62b67d156b2c +Create Date: 2022-11-12 13:47:31.855945 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '8524b4ed10c8' +down_revision = '62b67d156b2c' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('goal', sa.Column('id', sa.Integer(), nullable=False)) + op.drop_column('goal', 'goal_id') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('goal', sa.Column('goal_id', sa.INTEGER(), autoincrement=True, nullable=False)) + op.drop_column('goal', 'id') + # ### end Alembic commands ### From f1ab9b270423064342697e2879c3a13a964f1fe6 Mon Sep 17 00:00:00 2001 From: ivystrayed Date: Mon, 14 Nov 2022 15:40:24 -0800 Subject: [PATCH 15/16] chnaged name of column in goal table from goal_id to id because it was causing error --- app/models/goal.py | 4 +-- app/models/task.py | 6 ++--- migrations/versions/5dd236d26034_.py | 39 ---------------------------- migrations/versions/62b67d156b2c_.py | 28 -------------------- migrations/versions/70c88c16985a_.py | 34 ------------------------ migrations/versions/8524b4ed10c8_.py | 30 --------------------- 6 files changed, 5 insertions(+), 136 deletions(-) delete mode 100644 migrations/versions/5dd236d26034_.py delete mode 100644 migrations/versions/62b67d156b2c_.py delete mode 100644 migrations/versions/70c88c16985a_.py delete mode 100644 migrations/versions/8524b4ed10c8_.py diff --git a/app/models/goal.py b/app/models/goal.py index df910f9e3..668e89ea3 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -3,9 +3,9 @@ class Goal(db.Model): - id = db.Column(db.Integer, primary_key=True) + id = db.Column(db.Integer, primary_key=True, autoincrement=True) title = db.Column(db.String) - tasks = db.relationship("Task") + 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 078d39729..3478f65af 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -4,12 +4,12 @@ class Task(db.Model): - id = db.Column(db.Integer, primary_key=True) + id = db.Column(db.Integer, primary_key=True, autoincrement=True) title = db.Column(db.String) description = db.Column(db.String) completed_at = db.Column(db.DateTime, nullable = True) - goal_id = db.Column(db.Integer, db.ForeignKey("goal.id"), nullable = True) - + goal = db.relationship("Goal", back_populates="tasks") + goal_id = db.Column(db.Integer, db.ForeignKey('goal.id'), nullable = True) diff --git a/migrations/versions/5dd236d26034_.py b/migrations/versions/5dd236d26034_.py deleted file mode 100644 index cf0968eee..000000000 --- a/migrations/versions/5dd236d26034_.py +++ /dev/null @@ -1,39 +0,0 @@ -"""empty message - -Revision ID: 5dd236d26034 -Revises: -Create Date: 2022-11-03 11:37:47.776478 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '5dd236d26034' -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('comepleted_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 ### diff --git a/migrations/versions/62b67d156b2c_.py b/migrations/versions/62b67d156b2c_.py deleted file mode 100644 index 6d2a518d3..000000000 --- a/migrations/versions/62b67d156b2c_.py +++ /dev/null @@ -1,28 +0,0 @@ -"""empty message - -Revision ID: 62b67d156b2c -Revises: 70c88c16985a -Create Date: 2022-11-09 17:06:42.471730 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '62b67d156b2c' -down_revision = '70c88c16985a' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('goal', sa.Column('title', sa.String(), nullable=True)) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('goal', 'title') - # ### end Alembic commands ### diff --git a/migrations/versions/70c88c16985a_.py b/migrations/versions/70c88c16985a_.py deleted file mode 100644 index 86869964f..000000000 --- a/migrations/versions/70c88c16985a_.py +++ /dev/null @@ -1,34 +0,0 @@ -"""empty message - -Revision ID: 70c88c16985a -Revises: 5dd236d26034 -Create Date: 2022-11-03 16:47:55.009381 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision = '70c88c16985a' -down_revision = '5dd236d26034' -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('id', sa.Integer(), nullable=False)) - op.drop_column('task', 'task_id') - op.drop_column('task', 'comepleted_at') - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('task', sa.Column('comepleted_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True)) - op.add_column('task', sa.Column('task_id', sa.INTEGER(), autoincrement=True, nullable=False)) - op.drop_column('task', 'id') - op.drop_column('task', 'completed_at') - # ### end Alembic commands ### diff --git a/migrations/versions/8524b4ed10c8_.py b/migrations/versions/8524b4ed10c8_.py deleted file mode 100644 index 76e8513f9..000000000 --- a/migrations/versions/8524b4ed10c8_.py +++ /dev/null @@ -1,30 +0,0 @@ -"""empty message - -Revision ID: 8524b4ed10c8 -Revises: 62b67d156b2c -Create Date: 2022-11-12 13:47:31.855945 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '8524b4ed10c8' -down_revision = '62b67d156b2c' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('goal', sa.Column('id', sa.Integer(), nullable=False)) - op.drop_column('goal', 'goal_id') - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('goal', sa.Column('goal_id', sa.INTEGER(), autoincrement=True, nullable=False)) - op.drop_column('goal', 'id') - # ### end Alembic commands ### From 031944b88c9adaff0fc320ac055737a204ec8de0 Mon Sep 17 00:00:00 2001 From: ivystrayed Date: Thu, 17 Nov 2022 15:15:35 -0800 Subject: [PATCH 16/16] fixed database migration issue --- migrations/versions/169c9ca46d4d_.py | 42 ++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 migrations/versions/169c9ca46d4d_.py diff --git a/migrations/versions/169c9ca46d4d_.py b/migrations/versions/169c9ca46d4d_.py new file mode 100644 index 000000000..fc1829c25 --- /dev/null +++ b/migrations/versions/169c9ca46d4d_.py @@ -0,0 +1,42 @@ +"""empty message + +Revision ID: 169c9ca46d4d +Revises: +Create Date: 2022-11-17 15:13:06.677176 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '169c9ca46d4d' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('goal', + sa.Column('id', sa.Integer(), autoincrement=True, 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=True), + 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 ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('task') + op.drop_table('goal') + # ### end Alembic commands ###