From 617c2091ab6805ec84b752394e481160760eab30 Mon Sep 17 00:00:00 2001 From: Victoria Duke Date: Fri, 5 Nov 2021 00:00:14 -0700 Subject: [PATCH 1/5] Adds sort_by for GET routes, adds PATCH route --- app/__init__.py | 4 ++ app/models/task.py | 19 +++++++++- app/routes.py | 93 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 114 insertions(+), 2 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 2764c4cc8..79aab230c 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -30,5 +30,9 @@ def create_app(test_config=None): migrate.init_app(app, db) # Register Blueprints here + #(unneeded? look above) from app.models.task import Task + from .routes import tasks_bp + app.register_blueprint(tasks_bp) + return app diff --git a/app/models/task.py b/app/models/task.py index 39c89cd16..f1d751deb 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -3,4 +3,21 @@ 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(100), nullable = False) + description = db.Column(db.String(200), nullable = False) + completed_at = db.Column(db.DateTime, nullable = True) + # I'm not sure about completed_at and is_completed. + # Is completed_at only for POST, and then is_completed only for responses? + + def to_dict(self): + if self.completed_at: + is_complete = True + else: + is_complete = False + return { + "id": self.task_id, + "title": self.title, + "description": self.description, + "is_complete": is_complete + } \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index 8e9dfe684..732d634f1 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,2 +1,93 @@ -from flask import Blueprint +from sqlalchemy.sql.expression import null +from app import db +from app.models.task import Task +from datetime import datetime +from flask import abort, Blueprint, jsonify, make_response, request +from sqlalchemy import desc +tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") + +# Posts new task +@tasks_bp.route("", methods=["POST"]) +def create_task(): + request_body = request.get_json() + if "title" not in request_body or "description" not in request_body or "completed_at" not in request_body: + return jsonify({"details": "Invalid data"}), 400 + new_task = Task(title=request_body["title"], + description=request_body["description"], + completed_at=request_body["completed_at"]) + + db.session.add(new_task) + db.session.commit() + + #return jsonify(tasks_response), 200 + new_task_response = {"task": new_task.to_dict()} + return jsonify(new_task_response), 201 + +# Handles all tasks +@tasks_bp.route("", methods=["GET"]) +def handle_tasks(): + tasks_response = [] + sort_by = request.args.get('sort') + if sort_by == "asc": + tasks = Task.query.order_by(Task.title).all() + elif sort_by == "desc": + tasks = Task.query.order_by(desc(Task.title)).all() + else: + tasks = Task.query.all() + for task in tasks: + tasks_response.append(task.to_dict()) + return jsonify(tasks_response), 200 + +# Handles one task +@tasks_bp.route("/", methods=["GET", "PUT"]) +def handle_task(task_id): + task_id = validate_id_int(task_id) + task = Task.query.get(task_id) + if not task: + return make_response("", 404) + if request.method == "GET": + return jsonify({"task": task.to_dict()}), 200 + elif request.method == "PUT": + request_body = request.get_json() + task.title=request_body["title"] + task.description=request_body["description"] + if "completed_at" in request_body: + task.completed_at=request_body["completed_at"] + + db.session.commit() + return jsonify({"task": task.to_dict()}), 200 + +@tasks_bp.route("/", methods=["PATCH"]) +def patch_task(task_id, patch_complete): + task_id = validate_id_int(task_id) + task = Task.query.get(task_id) + if not task: + return make_response("", 404) + if patch_complete == "mark_complete": + task.completed_at=datetime.now() + elif patch_complete == "mark_incomplete": + task.completed_at=None + db.session.commit() + return jsonify({"task": task.to_dict()}), 200 + +@tasks_bp.route("/", methods=["DELETE"]) +def delete_task(task_id): + print(task_id) + task_id=validate_id_int(task_id) + + task = Task.query.get(task_id) + + if task: + db.session.delete(task) + db.session.commit() + return jsonify({"details": f'Task {task_id} "{task.title}" successfully deleted'}), 200 + else: + return make_response("", 404) + +def validate_id_int(task_id): + try: + task_id = int(task_id) + return task_id + except: + abort(400, "Error: Task ID needs to be a number") \ No newline at end of file From 7622ae967111d45dd187b62ee651ed96bbabd780 Mon Sep 17 00:00:00 2001 From: Victoria Duke Date: Fri, 5 Nov 2021 01:39:07 -0700 Subject: [PATCH 2/5] Integrates the Slack web API --- app/routes.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/routes.py b/app/routes.py index 732d634f1..c8082472c 100644 --- a/app/routes.py +++ b/app/routes.py @@ -3,6 +3,8 @@ from app.models.task import Task from datetime import datetime from flask import abort, Blueprint, jsonify, make_response, request +import os +import requests from sqlalchemy import desc tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") @@ -66,6 +68,16 @@ def patch_task(task_id, patch_complete): return make_response("", 404) if patch_complete == "mark_complete": task.completed_at=datetime.now() + # ID of the channel you want to send the message to + channel_id = "C02LA52J4AW" + SLACK_KEY = os.environ.get("SLACK_API_KEY") + text=f"Someone just completed the task {task.title}" + data = { + 'channel': channel_id, + 'as_user': True, + 'text': text + } + requests.post("https://slack.com/api/chat.postMessage", headers={"Authorization": f"Bearer {SLACK_KEY}"}, data=data) elif patch_complete == "mark_incomplete": task.completed_at=None db.session.commit() From fa4740bc4520e0ba3d8364948b5c9fbab3f9a840 Mon Sep 17 00:00:00 2001 From: Victoria Duke Date: Fri, 26 Nov 2021 23:48:32 -0800 Subject: [PATCH 3/5] adds tests for test_wave_05 --- app/__init__.py | 3 +- app/models/goal.py | 9 ++ app/routes.py | 67 ++++++++++++- migrations/README | 1 + migrations/alembic.ini | 45 +++++++++ migrations/env.py | 96 +++++++++++++++++++ migrations/script.py.mako | 24 +++++ migrations/versions/436ce74f5bc1_.py | 28 ++++++ .../versions/9823bd254953_adds_task_model.py | 39 ++++++++ tests/test_wave_05.py | 43 +++++++-- 10 files changed, 345 insertions(+), 10 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/436ce74f5bc1_.py create mode 100644 migrations/versions/9823bd254953_adds_task_model.py diff --git a/app/__init__.py b/app/__init__.py index 79aab230c..b295f980d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -32,7 +32,8 @@ def create_app(test_config=None): # Register Blueprints here #(unneeded? look above) from app.models.task import Task from .routes import tasks_bp + from .routes import goals_bp app.register_blueprint(tasks_bp) - + app.register_blueprint(goals_bp) return app diff --git a/app/models/goal.py b/app/models/goal.py index 8cad278f8..14dd5019b 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -4,3 +4,12 @@ class Goal(db.Model): goal_id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(100), nullable = False) + # Make goal_id autoincrement? + + # Have to rename? or use the one from Task model? + def to_dict(self): + return { + "id": self.goal_id, + "title": self.title + } \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index c8082472c..b9666956f 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,5 +1,6 @@ from sqlalchemy.sql.expression import null from app import db +from app.models.goal import Goal from app.models.task import Task from datetime import datetime from flask import abort, Blueprint, jsonify, make_response, request @@ -8,7 +9,9 @@ from sqlalchemy import desc tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") +goals_bp = Blueprint("goals", __name__, url_prefix="/goals") +# Task routes # Posts new task @tasks_bp.route("", methods=["POST"]) def create_task(): @@ -102,4 +105,66 @@ def validate_id_int(task_id): task_id = int(task_id) return task_id except: - abort(400, "Error: Task ID needs to be a number") \ No newline at end of file + abort(400, "Error: Task ID needs to be a number") + +# Goal routes +# Posts new goal +@goals_bp.route("", methods=["POST"]) +def create_goal(): + request_body = request.get_json() + if "title" not in request_body: + return jsonify({"details": "Invalid data"}), 400 + new_goal = Goal(title=request_body["title"]) + + db.session.add(new_goal) + db.session.commit() + + #return jsonify(tasks_response), 200 + new_goal_response = {"goal": new_goal.to_dict()} + return jsonify(new_goal_response), 201 + +# Handles all goals +@goals_bp.route("", methods=["GET"]) +def handle_goals(): + goals_response = [] + sort_by = request.args.get('sort') + if sort_by == "asc": + goals = Goal.query.order_by(Goal.title).all() + elif sort_by == "desc": + goals = Goal.query.order_by(desc(Goal.title)).all() + else: + goals = Goal.query.all() + for goal in goals: + goals_response.append(goal.to_dict()) + return jsonify(goals_response), 200 + +# Handles one goal +@goals_bp.route("/", methods=["GET", "PUT"]) +def handle_goal(goal_id): + goal_id = validate_id_int(goal_id) + goal = Goal.query.get(goal_id) + if not goal: + return make_response("", 404) + if request.method == "GET": + return jsonify({"goal": goal.to_dict()}), 200 + elif request.method == "PUT": + request_body = request.get_json() + goal.title=request_body["title"] + # Remove, not needed? - goal.description=request_body["description"] + + db.session.commit() + return jsonify({"goal": goal.to_dict()}), 200 + +@goals_bp.route("/", methods=["DELETE"]) +def delete_goal(goal_id): + print(goal_id) + goal_id=validate_id_int(goal_id) + + goal = Goal.query.get(goal_id) + + if goal: + db.session.delete(goal) + db.session.commit() + return jsonify({"details": f'Goal {goal_id} "{goal.title}" successfully deleted'}), 200 + else: + return make_response("", 404) 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/436ce74f5bc1_.py b/migrations/versions/436ce74f5bc1_.py new file mode 100644 index 000000000..02c893f0d --- /dev/null +++ b/migrations/versions/436ce74f5bc1_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 436ce74f5bc1 +Revises: 9823bd254953 +Create Date: 2021-11-06 22:09:25.674361 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '436ce74f5bc1' +down_revision = '9823bd254953' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('goal', sa.Column('title', sa.String(length=100), nullable=False)) + # ### 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/9823bd254953_adds_task_model.py b/migrations/versions/9823bd254953_adds_task_model.py new file mode 100644 index 000000000..16e0d579d --- /dev/null +++ b/migrations/versions/9823bd254953_adds_task_model.py @@ -0,0 +1,39 @@ +"""adds Task model + +Revision ID: 9823bd254953 +Revises: +Create Date: 2021-10-29 11:51:26.504155 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9823bd254953' +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(length=100), nullable=False), + sa.Column('description', sa.String(length=200), nullable=False), + sa.Column('completed_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('task_id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('task') + op.drop_table('goal') + # ### end Alembic commands ### diff --git a/tests/test_wave_05.py b/tests/test_wave_05.py index 6ba60c6fa..36ad05bfe 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 @@ -41,7 +42,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 @@ -51,7 +52,9 @@ def test_get_goal_not_found(client): # Assert # ---- Complete Test ---- # assertion 1 goes here + assert response.status_code == 404 # assertion 2 goes here + assert response_body == None # ---- Complete Test ---- def test_create_goal(client): @@ -71,29 +74,48 @@ 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 + assert response.status_code == 200 # assertion 2 goes here + assert "goal" in response_body # assertion 3 goes here + assert response_body == { + "goal": { + "id": 1, + "title": "Updated Goal Title" + } + } + goal = Goal.query.get(1) + assert goal.title == "Updated Goal Title" # ---- Complete Assertions Here ---- -@pytest.mark.skip(reason="test to be completed by student") +#@pytest.mark.skip(reason="test to be completed by student") def test_update_goal_not_found(client): - pass # Act + response = client.put("/goals/1", json={ + "title": "Updated Goal Title" + }) + response_body = response.get_json() # ---- Complete Act Here ---- # Assert # ---- Complete Assertions Here ---- # assertion 1 goes here + assert response.status_code == 404 # assertion 2 goes here + assert response_body == None + # ---- Complete Assertions Here ---- @@ -113,17 +135,22 @@ 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("/goals/1") + response_body = response.get_json() + # Assert # ---- Complete Assertions Here ---- # assertion 1 goes here + assert response.status_code == 404 # assertion 2 goes here + assert response_body == None + assert Goal.query.all() == [] + # ---- Complete Assertions Here ---- From 7d37e2a54eb3e2530daa35224658c77c914ca7af Mon Sep 17 00:00:00 2001 From: Victoria Duke Date: Sat, 27 Nov 2021 01:01:02 -0800 Subject: [PATCH 4/5] creates tasks to goal relationship, creates route //tasks --- app/models/goal.py | 4 +- app/models/task.py | 2 + app/routes.py | 20 ++++++++++ migrations/versions/436ce74f5bc1_.py | 28 ------------- .../versions/9823bd254953_adds_task_model.py | 39 ------------------- 5 files changed, 24 insertions(+), 69 deletions(-) delete mode 100644 migrations/versions/436ce74f5bc1_.py delete mode 100644 migrations/versions/9823bd254953_adds_task_model.py diff --git a/app/models/goal.py b/app/models/goal.py index 14dd5019b..f7cf92e92 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -3,9 +3,9 @@ class Goal(db.Model): - goal_id = db.Column(db.Integer, primary_key=True) + goal_id = db.Column(db.Integer, primary_key=True, autoincrement=True) title = db.Column(db.String(100), nullable = False) - # Make goal_id autoincrement? + tasks = db.relationship("Task", back_populates="goal", lazy=True) # Have to rename? or use the one from Task model? def to_dict(self): diff --git a/app/models/task.py b/app/models/task.py index f1d751deb..61c3200ba 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -9,6 +9,8 @@ class Task(db.Model): completed_at = db.Column(db.DateTime, nullable = True) # I'm not sure about completed_at and is_completed. # Is completed_at only for POST, and then is_completed only for responses? + goal_id = db.Column(db.Integer, db.ForeignKey('goal.goal_id'), nullable = True) + goal = db.relationship("Goal", back_populates="tasks") def to_dict(self): if self.completed_at: diff --git a/app/routes.py b/app/routes.py index b9666956f..ce70d0b24 100644 --- a/app/routes.py +++ b/app/routes.py @@ -168,3 +168,23 @@ def delete_goal(goal_id): return jsonify({"details": f'Goal {goal_id} "{goal.title}" successfully deleted'}), 200 else: return make_response("", 404) + +@goals_bp.route("//tasks", methods=["GET", "POST"]) +def handle_goals_tasks(goal_id): + goal = Goal.query.get(goal_id) + if goal is None: + return make_response("Goal not found", 404) + + if request.method == "POST": + request_body = request.get_json() + goal.tasks = request_body["task_ids"] + db.session.add() + db.session.commit() + + return make_response(goal, 200) + + elif request.method == "GET": + tasks_response = [] + for task in goal.tasks: + tasks_response.append(task.to_dict()) + return jsonify(tasks_response), 200 diff --git a/migrations/versions/436ce74f5bc1_.py b/migrations/versions/436ce74f5bc1_.py deleted file mode 100644 index 02c893f0d..000000000 --- a/migrations/versions/436ce74f5bc1_.py +++ /dev/null @@ -1,28 +0,0 @@ -"""empty message - -Revision ID: 436ce74f5bc1 -Revises: 9823bd254953 -Create Date: 2021-11-06 22:09:25.674361 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '436ce74f5bc1' -down_revision = '9823bd254953' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('goal', sa.Column('title', sa.String(length=100), nullable=False)) - # ### 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/9823bd254953_adds_task_model.py b/migrations/versions/9823bd254953_adds_task_model.py deleted file mode 100644 index 16e0d579d..000000000 --- a/migrations/versions/9823bd254953_adds_task_model.py +++ /dev/null @@ -1,39 +0,0 @@ -"""adds Task model - -Revision ID: 9823bd254953 -Revises: -Create Date: 2021-10-29 11:51:26.504155 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '9823bd254953' -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(length=100), nullable=False), - sa.Column('description', sa.String(length=200), nullable=False), - sa.Column('completed_at', sa.DateTime(), nullable=True), - sa.PrimaryKeyConstraint('task_id') - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('task') - op.drop_table('goal') - # ### end Alembic commands ### From 2d8ca7f4990f394d200c4450dda64591c0241c4a Mon Sep 17 00:00:00 2001 From: Victoria Duke Date: Tue, 30 Nov 2021 15:58:56 -0800 Subject: [PATCH 5/5] fixes POST method for /goals//tasks --- app/models/goal.py | 9 ++++++ app/routes.py | 8 +++--- migrations/versions/80eb6df17276_.py | 42 ++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 migrations/versions/80eb6df17276_.py diff --git a/app/models/goal.py b/app/models/goal.py index f7cf92e92..f6a6965c7 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -12,4 +12,13 @@ def to_dict(self): return { "id": self.goal_id, "title": self.title + } + + def tasks_to_dict(self): + task_ids_list = [] + for task in self.tasks: + task_ids_list.append(task.task_id) + return { + "id": self.goal_id, + "task_ids": task_ids_list } \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index ce70d0b24..61b87314f 100644 --- a/app/routes.py +++ b/app/routes.py @@ -177,11 +177,11 @@ def handle_goals_tasks(goal_id): if request.method == "POST": request_body = request.get_json() - goal.tasks = request_body["task_ids"] - db.session.add() + for task_id in request_body["task_ids"]: + task = Task.query.get(task_id) + goal.tasks.append(task) db.session.commit() - - return make_response(goal, 200) + return jsonify(goal.tasks_to_dict()), 200 elif request.method == "GET": tasks_response = [] diff --git a/migrations/versions/80eb6df17276_.py b/migrations/versions/80eb6df17276_.py new file mode 100644 index 000000000..f12844307 --- /dev/null +++ b/migrations/versions/80eb6df17276_.py @@ -0,0 +1,42 @@ +"""empty message + +Revision ID: 80eb6df17276 +Revises: +Create Date: 2021-11-30 15:23:26.416634 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '80eb6df17276' +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(), autoincrement=True, nullable=False), + sa.Column('title', sa.String(length=100), nullable=False), + sa.PrimaryKeyConstraint('goal_id') + ) + op.create_table('task', + sa.Column('task_id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('title', sa.String(length=100), nullable=False), + sa.Column('description', sa.String(length=200), nullable=False), + sa.Column('completed_at', sa.DateTime(), nullable=True), + sa.Column('goal_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['goal_id'], ['goal.goal_id'], ), + 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 ###