From bace2d4a2a9ddc213b2d1f83b62568cf38afdd68 Mon Sep 17 00:00:00 2001 From: Alie Ibarra Date: Thu, 28 Oct 2021 15:54:49 -0700 Subject: [PATCH 01/14] Create POST Route --- app/__init__.py | 2 + app/models/task.py | 13 +++- app/routes.py | 42 +++++++++++- migrations/README | 1 + migrations/alembic.ini | 45 +++++++++++++ migrations/env.py | 96 ++++++++++++++++++++++++++++ migrations/script.py.mako | 24 +++++++ migrations/versions/79c4f18fa59f_.py | 39 +++++++++++ 8 files changed, 260 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/79c4f18fa59f_.py diff --git a/app/__init__.py b/app/__init__.py index 2764c4cc8..9ceda0f1f 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -30,5 +30,7 @@ def create_app(test_config=None): migrate.init_app(app, db) # Register Blueprints here + from .routes import tasks_list_bp + app.register_blueprint(tasks_list_bp) return app diff --git a/app/models/task.py b/app/models/task.py index 39c89cd16..287b51095 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -3,4 +3,15 @@ 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(50)) + description = db.Column(db.String) + completed_at = db.Column(db.DateTime, nullable=True, default=None) + + def to_dict(self): + return { + "id": self.task_id, + "title": self.title, + "description": self.description, + "is_complete": self.completed_at, + } \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index 8e9dfe684..6d837f2b1 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,2 +1,42 @@ -from flask import Blueprint +from app import db +from flask import Blueprint, request, make_response, jsonify +from app.models.task import Task +# DEFINE BLUEPRINT +tasks_list_bp = Blueprint("tasks", __name__, url_prefix="/tasks") + +#CREATE +@tasks_list_bp.route("", methods=["POST"]) +def create_task(): + request_body = request.get_json() + 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 make_response(f"New task {new_task.title} successfully created!", 201) + +#READ +@tasks_list_bp.route("", methods=["GET"]) +def read_all_tasks(): + tasks = Task.query.all() + tasks_repsonse = [] + for task in tasks: + tasks.response.append(task.to_dict()) + return jsonify(tasks_repsonse) + + +#UPDATE +@tasks_list_bp.route("", methods=["PATCH"]) +def update_task(): + pass + + +#DELETE +@tasks_list_bp.route("", methods=["DELETE"]) +def delete_task(): + pass \ No newline at end of file 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/79c4f18fa59f_.py b/migrations/versions/79c4f18fa59f_.py new file mode 100644 index 000000000..c3f8abeca --- /dev/null +++ b/migrations/versions/79c4f18fa59f_.py @@ -0,0 +1,39 @@ +"""empty message + +Revision ID: 79c4f18fa59f +Revises: +Create Date: 2021-10-28 14:17:53.400104 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '79c4f18fa59f' +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(), autoincrement=True, nullable=False), + sa.Column('title', sa.String(length=50), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.Column('completed_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('task_id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('task') + op.drop_table('goal') + # ### end Alembic commands ### From b1cf90cbc52f46fac7b9f8e2c25e8205702bf89b Mon Sep 17 00:00:00 2001 From: Alie Ibarra Date: Thu, 28 Oct 2021 15:58:20 -0700 Subject: [PATCH 02/14] Create GET Route for read all tasks --- app/routes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/routes.py b/app/routes.py index 6d837f2b1..9d00d950f 100644 --- a/app/routes.py +++ b/app/routes.py @@ -24,10 +24,10 @@ def create_task(): @tasks_list_bp.route("", methods=["GET"]) def read_all_tasks(): tasks = Task.query.all() - tasks_repsonse = [] + tasks_response = [] for task in tasks: - tasks.response.append(task.to_dict()) - return jsonify(tasks_repsonse) + tasks_response.append(task.to_dict()) + return jsonify(tasks_response) #UPDATE From 5e94984daca66fc026262bf57f882a96f4bb3591 Mon Sep 17 00:00:00 2001 From: Alie Ibarra Date: Thu, 28 Oct 2021 16:07:30 -0700 Subject: [PATCH 03/14] Create GET Route for read one task --- app/routes.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/app/routes.py b/app/routes.py index 9d00d950f..c65e395b3 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,10 +1,21 @@ from app import db -from flask import Blueprint, request, make_response, jsonify +from flask import Blueprint, request, make_response, jsonify, abort from app.models.task import Task # DEFINE BLUEPRINT tasks_list_bp = Blueprint("tasks", __name__, url_prefix="/tasks") +#----------------- +#HELPER FUNCTIONS +def get_task_from_id(id): + try: + id = int(id) + except: + abort(400, {"error": "invalid id"}) + return Task.query.get_or_404(id) + + +#----------------- #CREATE @tasks_list_bp.route("", methods=["POST"]) def create_task(): @@ -20,6 +31,8 @@ def create_task(): return make_response(f"New task {new_task.title} successfully created!", 201) + +#----------------- #READ @tasks_list_bp.route("", methods=["GET"]) def read_all_tasks(): @@ -29,6 +42,11 @@ def read_all_tasks(): tasks_response.append(task.to_dict()) return jsonify(tasks_response) +@tasks_list_bp.route("/", methods=["GET"]) +def read_one_task(id): + task = get_task_from_id(id) + return task.to_dict() + #UPDATE @tasks_list_bp.route("", methods=["PATCH"]) From 7ddd46475edf2e87eee63a774ff8376edc41db17 Mon Sep 17 00:00:00 2001 From: Alie Ibarra Date: Fri, 29 Oct 2021 10:00:49 -0700 Subject: [PATCH 04/14] Create PATCH and DELETE task routes --- app/routes.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/app/routes.py b/app/routes.py index c65e395b3..d0d590ce9 100644 --- a/app/routes.py +++ b/app/routes.py @@ -49,12 +49,24 @@ def read_one_task(id): #UPDATE -@tasks_list_bp.route("", methods=["PATCH"]) -def update_task(): - pass +@tasks_list_bp.route("/", methods=["PATCH"]) +def update_task(id): + task = get_task_from_id(id) + request_body = request.get_json() + if "title" in request_body: + task.title = request_body["title"] + if "description" in request_body: + task.description = request_body["description"] + if "completed_at" in request_body: + task.completed_at = request_body["completed_at"] + db.session.commit() + return jsonify([task.to_dict(), "Task Updated Successful"]) #DELETE -@tasks_list_bp.route("", methods=["DELETE"]) -def delete_task(): - pass \ No newline at end of file +@tasks_list_bp.route("/", methods=["DELETE"]) +def delete_task(id): + task = get_task_from_id(id) + db.session.delete(task) + db.session.commit() + return make_response("Task has been deleted from your Task List", 200) \ No newline at end of file From 2afc26c54dc7a8936325f9e14245e5f66813faf7 Mon Sep 17 00:00:00 2001 From: Alie Ibarra Date: Fri, 29 Oct 2021 13:57:47 -0700 Subject: [PATCH 05/14] Pass 10/12 Tests in Wave 01 --- app/models/task.py | 12 +++++++++--- app/routes.py | 29 +++++++++++++++++++++-------- tests/test_wave_04.py | 2 +- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/app/models/task.py b/app/models/task.py index 287b51095..233e1b2f8 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -4,14 +4,20 @@ class Task(db.Model): task_id = db.Column(db.Integer, primary_key=True, autoincrement= True) - title = db.Column(db.String(50)) + title = db.Column(db.String) description = db.Column(db.String) - completed_at = db.Column(db.DateTime, nullable=True, default=None) + completed_at = db.Column(db.DateTime, nullable=True) def to_dict(self): + if not self.completed_at: + is_complete = False + else: + is_complete = True + #automatically makes "is_complete = False" + return { "id": self.task_id, "title": self.title, "description": self.description, - "is_complete": self.completed_at, + "is_complete": is_complete, } \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index d0d590ce9..cdab8c735 100644 --- a/app/routes.py +++ b/app/routes.py @@ -20,6 +20,13 @@ def get_task_from_id(id): @tasks_list_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 make_response({"details" : "Invalid data"}, 400) + + if request_body["completed_at"] == None: + pass + new_task = Task( title=request_body["title"], description=request_body["description"], @@ -29,8 +36,8 @@ def create_task(): db.session.add(new_task) db.session.commit() - return make_response(f"New task {new_task.title} successfully created!", 201) - + + return make_response("201 CREATED", 201) #----------------- #READ @@ -45,28 +52,34 @@ def read_all_tasks(): @tasks_list_bp.route("/", methods=["GET"]) def read_one_task(id): task = get_task_from_id(id) - return task.to_dict() + return make_response({"task" : task.to_dict()},200) + #UPDATE -@tasks_list_bp.route("/", methods=["PATCH"]) +@tasks_list_bp.route("/", methods=["PUT"]) def update_task(id): task = get_task_from_id(id) request_body = request.get_json() + + #if "title" or "description" not in request body return 400 if "title" in request_body: task.title = request_body["title"] if "description" in request_body: task.description = request_body["description"] - if "completed_at" in request_body: - task.completed_at = request_body["completed_at"] + + db.session.commit() - return jsonify([task.to_dict(), "Task Updated Successful"]) + + return make_response({"task" : task.to_dict()},200) #DELETE @tasks_list_bp.route("/", methods=["DELETE"]) def delete_task(id): task = get_task_from_id(id) + db.session.delete(task) db.session.commit() - return make_response("Task has been deleted from your Task List", 200) \ No newline at end of file + + return make_response({"details": f"Task {id} {task.description} successfully deleted"},200) \ No newline at end of file diff --git a/tests/test_wave_04.py b/tests/test_wave_04.py index d0b26b2d1..f367f24c5 100644 --- a/tests/test_wave_04.py +++ b/tests/test_wave_04.py @@ -1 +1 @@ -# There are no tests for wave 4. +# There are no tests for wave 4. \ No newline at end of file From 09be772e7d824574e95044aa3f6f843318e7cf31 Mon Sep 17 00:00:00 2001 From: Alie Ibarra Date: Mon, 1 Nov 2021 15:25:06 -0700 Subject: [PATCH 06/14] Pass 2/2 Wave 2 Tests --- app/models/task.py | 2 +- app/routes.py | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/app/models/task.py b/app/models/task.py index 233e1b2f8..9513bb8bc 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -13,7 +13,7 @@ def to_dict(self): is_complete = False else: is_complete = True - #automatically makes "is_complete = False" + return { "id": self.task_id, diff --git a/app/routes.py b/app/routes.py index cdab8c735..6366719af 100644 --- a/app/routes.py +++ b/app/routes.py @@ -25,7 +25,7 @@ def create_task(): return make_response({"details" : "Invalid data"}, 400) if request_body["completed_at"] == None: - pass + pass new_task = Task( title=request_body["title"], @@ -43,7 +43,18 @@ def create_task(): #READ @tasks_list_bp.route("", methods=["GET"]) def read_all_tasks(): - tasks = Task.query.all() + sort_query = request.args.get("sort") + + if sort_query == "asc": + tasks = Task.query.order_by(Task.title.asc()) + + elif sort_query == "desc": + tasks = Task.query.order_by(Task.title.desc()) + + else: + tasks = Task.query.all() + + tasks_response = [] for task in tasks: tasks_response.append(task.to_dict()) @@ -55,7 +66,7 @@ def read_one_task(id): return make_response({"task" : task.to_dict()},200) - +#----------------- #UPDATE @tasks_list_bp.route("/", methods=["PUT"]) def update_task(id): @@ -73,7 +84,7 @@ def update_task(id): return make_response({"task" : task.to_dict()},200) - +#----------------- #DELETE @tasks_list_bp.route("/", methods=["DELETE"]) def delete_task(id): @@ -82,4 +93,4 @@ def delete_task(id): db.session.delete(task) db.session.commit() - return make_response({"details": f"Task {id} {task.description} successfully deleted"},200) \ No newline at end of file + return make_response({'details': f'Task {task.task_id} "{task.title}" successfully deleted'},200) \ No newline at end of file From a2d82264a2856c2b398c53107c5767febea4c0c7 Mon Sep 17 00:00:00 2001 From: Alie Ibarra Date: Mon, 1 Nov 2021 16:21:39 -0700 Subject: [PATCH 07/14] Pass all tests Wave 1 and Wave 2 --- app/routes.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/routes.py b/app/routes.py index 6366719af..fd4ed19f5 100644 --- a/app/routes.py +++ b/app/routes.py @@ -24,9 +24,6 @@ def create_task(): if "title" not in request_body or "description" not in request_body or "completed_at" not in request_body: return make_response({"details" : "Invalid data"}, 400) - if request_body["completed_at"] == None: - pass - new_task = Task( title=request_body["title"], description=request_body["description"], @@ -37,7 +34,7 @@ def create_task(): db.session.commit() - return make_response("201 CREATED", 201) + return make_response(jsonify({"task": new_task.to_dict()}), 201) #----------------- #READ @@ -84,6 +81,13 @@ def update_task(id): return make_response({"task" : task.to_dict()},200) +#----------------- +#UPDATE +@tasks_list_bp.route("/", methods=["PATCH"]) +def mark_complete(id): + pass + + #----------------- #DELETE @tasks_list_bp.route("/", methods=["DELETE"]) @@ -93,4 +97,5 @@ def delete_task(id): db.session.delete(task) db.session.commit() - return make_response({'details': f'Task {task.task_id} "{task.title}" successfully deleted'},200) \ No newline at end of file + return make_response({'details': f'Task {task.task_id} "{task.title}" successfully deleted'},200) + From 496b55b11b0bf137622cb04ef2c7eb88379e6edd Mon Sep 17 00:00:00 2001 From: Alie Ibarra Date: Tue, 2 Nov 2021 11:27:14 -0700 Subject: [PATCH 08/14] Pass all Wave 03 Tests --- app/routes.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/app/routes.py b/app/routes.py index fd4ed19f5..4ff53d46a 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,6 +1,7 @@ from app import db from flask import Blueprint, request, make_response, jsonify, abort from app.models.task import Task +from datetime import datetime # DEFINE BLUEPRINT tasks_list_bp = Blueprint("tasks", __name__, url_prefix="/tasks") @@ -22,7 +23,7 @@ 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 make_response({"details" : "Invalid data"}, 400) + return make_response(jsonify({"details" : "Invalid data"}), 400) new_task = Task( title=request_body["title"], @@ -60,7 +61,7 @@ def read_all_tasks(): @tasks_list_bp.route("/", methods=["GET"]) def read_one_task(id): task = get_task_from_id(id) - return make_response({"task" : task.to_dict()},200) + return make_response(jsonify({"task" : task.to_dict()}),200) #----------------- @@ -79,13 +80,23 @@ def update_task(id): db.session.commit() - return make_response({"task" : task.to_dict()},200) + return make_response(jsonify({"task" : task.to_dict()}),200) #----------------- -#UPDATE -@tasks_list_bp.route("/", methods=["PATCH"]) -def mark_complete(id): - pass +#UPDATE COMPLETION STATUS +@tasks_list_bp.route("//", methods=["PATCH"]) +def mark_complete(id, completion_status): + task = get_task_from_id(id) + #task_dict = {} + + if completion_status == "mark_complete": + task.completed_at = datetime.date + if completion_status == "mark_incomplete": + task.completed_at = None + + + #task_dict["task"] = task.to_dict() + return jsonify({"task" : task.to_dict()}) #----------------- @@ -97,5 +108,5 @@ def delete_task(id): db.session.delete(task) db.session.commit() - return make_response({'details': f'Task {task.task_id} "{task.title}" successfully deleted'},200) + return make_response(jsonify({'details': f'Task {task.task_id} "{task.title}" successfully deleted'}),200) From 71a2fe817624e52be2c261bc20048a8e1ba8f1b6 Mon Sep 17 00:00:00 2001 From: Alie Ibarra Date: Thu, 4 Nov 2021 15:26:06 -0700 Subject: [PATCH 09/14] Pass Provided Tests for Wave 05 --- app/__init__.py | 7 ++- app/models/goal.py | 10 +++- app/models/task.py | 1 - app/routes.py | 77 ++++++++++++++++++++++------ migrations/versions/9213ae7a89e8_.py | 28 ++++++++++ requirements.txt | 9 ++++ 6 files changed, 111 insertions(+), 21 deletions(-) create mode 100644 migrations/versions/9213ae7a89e8_.py diff --git a/app/__init__.py b/app/__init__.py index 9ceda0f1f..088fc5129 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -30,7 +30,10 @@ def create_app(test_config=None): migrate.init_app(app, db) # Register Blueprints here - from .routes import tasks_list_bp - app.register_blueprint(tasks_list_bp) + from .routes import tasks_bp + app.register_blueprint(tasks_bp) + + from .routes import goals_bp + app.register_blueprint(goals_bp) return app diff --git a/app/models/goal.py b/app/models/goal.py index 8cad278f8..12291f33c 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -3,4 +3,12 @@ class Goal(db.Model): - goal_id = db.Column(db.Integer, primary_key=True) + goal_id = db.Column(db.Integer, primary_key=True, autoincrement= True) + title = db.Column(db.String) + + def to_dict(self): + + return { + "id": self.goal_id, + "title": self.title, + } diff --git a/app/models/task.py b/app/models/task.py index 9513bb8bc..34c606443 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -14,7 +14,6 @@ def to_dict(self): else: is_complete = True - return { "id": self.task_id, "title": self.title, diff --git a/app/routes.py b/app/routes.py index 4ff53d46a..4342271b2 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,10 +1,14 @@ +from re import match from app import db from flask import Blueprint, request, make_response, jsonify, abort from app.models.task import Task +from app.models.goal import Goal from datetime import datetime + # DEFINE BLUEPRINT -tasks_list_bp = Blueprint("tasks", __name__, url_prefix="/tasks") +tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") +goals_bp = Blueprint("goals", __name__, url_prefix="/goals") #----------------- #HELPER FUNCTIONS @@ -15,10 +19,16 @@ def get_task_from_id(id): abort(400, {"error": "invalid id"}) return Task.query.get_or_404(id) +def get_goal_from_id(id): + try: + id = int(id) + except: + abort(400, {"error": "invalid id"}) + return Goal.query.get_or_404(id) #----------------- -#CREATE -@tasks_list_bp.route("", methods=["POST"]) +#CREATE (aka POST) +@tasks_bp.route("", methods=["POST"]) def create_task(): request_body = request.get_json() @@ -34,12 +44,27 @@ def create_task(): db.session.add(new_task) db.session.commit() - return make_response(jsonify({"task": new_task.to_dict()}), 201) +@goals_bp.route("", methods=["POST"]) +def create_goal(): + request_body = request.get_json() + + if "title" not in request_body: + return make_response(jsonify({"details" : "Invalid data"}), 400) + + new_goal = Goal( + title=request_body["title"] + ) + + db.session.add(new_goal) + db.session.commit() + + return make_response(jsonify({"goal": new_goal.to_dict()}), 201) + #----------------- -#READ -@tasks_list_bp.route("", methods=["GET"]) +#READ (aka GET) +@tasks_bp.route("", methods=["GET"]) def read_all_tasks(): sort_query = request.args.get("sort") @@ -52,21 +77,33 @@ def read_all_tasks(): else: tasks = Task.query.all() - tasks_response = [] for task in tasks: tasks_response.append(task.to_dict()) return jsonify(tasks_response) -@tasks_list_bp.route("/", methods=["GET"]) +@tasks_bp.route("/", methods=["GET"]) def read_one_task(id): task = get_task_from_id(id) return make_response(jsonify({"task" : task.to_dict()}),200) - +@goals_bp.route("", methods=["GET"]) +def read_all_goals(): + goals = Goal.query.all() + + goals_response = [] + for goal in goals: + goals_response.append(goal.to_dict()) + return jsonify(goals_response) + +@goals_bp.route("/", methods=["GET"]) +def read_one_task(id): + goal = get_goal_from_id(id) + return make_response(jsonify({"goal" : goal.to_dict()}),200) + #----------------- -#UPDATE -@tasks_list_bp.route("/", methods=["PUT"]) +#UPDATE +@tasks_bp.route("/", methods=["PUT"]) def update_task(id): task = get_task_from_id(id) request_body = request.get_json() @@ -77,14 +114,13 @@ def update_task(id): if "description" in request_body: task.description = request_body["description"] - db.session.commit() return make_response(jsonify({"task" : task.to_dict()}),200) #----------------- -#UPDATE COMPLETION STATUS -@tasks_list_bp.route("//", methods=["PATCH"]) +#UPDATE --TASK-- COMPLETION STATUS +@tasks_bp.route("//", methods=["PATCH"]) def mark_complete(id, completion_status): task = get_task_from_id(id) #task_dict = {} @@ -94,14 +130,12 @@ def mark_complete(id, completion_status): if completion_status == "mark_incomplete": task.completed_at = None - #task_dict["task"] = task.to_dict() return jsonify({"task" : task.to_dict()}) - #----------------- #DELETE -@tasks_list_bp.route("/", methods=["DELETE"]) +@tasks_bp.route("/", methods=["DELETE"]) def delete_task(id): task = get_task_from_id(id) @@ -110,3 +144,12 @@ def delete_task(id): return make_response(jsonify({'details': f'Task {task.task_id} "{task.title}" successfully deleted'}),200) +@goals_bp.route("/", methods=["DELETE"]) +def delete_goal(id): + goal = get_goal_from_id(id) + + db.session.delete(goal) + db.session.commit() + + return make_response(jsonify({'details': f'Goal {goal.goal_id} "{goal.title}" successfully deleted'}),200) + diff --git a/migrations/versions/9213ae7a89e8_.py b/migrations/versions/9213ae7a89e8_.py new file mode 100644 index 000000000..2660e093a --- /dev/null +++ b/migrations/versions/9213ae7a89e8_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 9213ae7a89e8 +Revises: 79c4f18fa59f +Create Date: 2021-11-04 14:46:45.970447 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9213ae7a89e8' +down_revision = '79c4f18fa59f' +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/requirements.txt b/requirements.txt index cfdf74050..aba519a51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,17 @@ +aiohttp==3.8.0 +aiosignal==1.2.0 alembic==1.5.4 +async-timeout==4.0.0 attrs==20.3.0 autopep8==1.5.5 certifi==2020.12.5 chardet==4.0.0 +charset-normalizer==2.0.7 click==7.1.2 Flask==1.1.2 Flask-Migrate==2.6.0 Flask-SQLAlchemy==2.4.4 +frozenlist==1.2.0 gunicorn==20.1.0 idna==2.10 iniconfig==1.1.1 @@ -14,6 +19,7 @@ itsdangerous==1.1.0 Jinja2==2.11.3 Mako==1.1.4 MarkupSafe==1.1.1 +multidict==5.2.0 packaging==20.9 pluggy==0.13.1 psycopg2-binary==2.8.6 @@ -26,7 +32,10 @@ python-dotenv==0.15.0 python-editor==1.0.4 requests==2.25.1 six==1.15.0 +slackclient==2.9.3 SQLAlchemy==1.3.23 toml==0.10.2 +typing-extensions==3.10.0.2 urllib3==1.26.4 Werkzeug==1.0.1 +yarl==1.7.2 From 509b809a892d6acac57d61ab663c05629e643f9a Mon Sep 17 00:00:00 2001 From: Alie Ibarra Date: Thu, 4 Nov 2021 15:53:47 -0700 Subject: [PATCH 10/14] Wrote and passed last 4 tests of Wave 05 --- app/routes.py | 12 +++++++++++- tests/test_wave_05.py | 35 +++++++++++++++++++---------------- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/app/routes.py b/app/routes.py index 4342271b2..e68253028 100644 --- a/app/routes.py +++ b/app/routes.py @@ -108,7 +108,6 @@ def update_task(id): task = get_task_from_id(id) request_body = request.get_json() - #if "title" or "description" not in request body return 400 if "title" in request_body: task.title = request_body["title"] if "description" in request_body: @@ -118,6 +117,17 @@ def update_task(id): return make_response(jsonify({"task" : task.to_dict()}),200) +@goals_bp.route("/", methods=["PUT"]) +def update_goal(id): + goal = get_goal_from_id(id) + request_body = request.get_json() + + goal.title = request_body["title"] + + db.session.commit() + + return make_response(jsonify({"goal" : goal.to_dict()}),200) + #----------------- #UPDATE --TASK-- COMPLETION STATUS @tasks_bp.route("//", methods=["PATCH"]) diff --git a/tests/test_wave_05.py b/tests/test_wave_05.py index 6ba60c6fa..409e0b7f1 100644 --- a/tests/test_wave_05.py +++ b/tests/test_wave_05.py @@ -41,7 +41,7 @@ def test_get_goal(client, one_goal): } } -@pytest.mark.skip(reason="test to be completed by student") +#@pytest.mark.skip(reason="test to be completed by student") def test_get_goal_not_found(client): pass # Act @@ -50,8 +50,8 @@ def test_get_goal_not_found(client): # Assert # ---- Complete Test ---- - # assertion 1 goes here - # assertion 2 goes here + assert response.status_code == 404 + assert response_body == None # ---- Complete Test ---- def test_create_goal(client): @@ -71,29 +71,31 @@ 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"}) + response_body = response.get_json() # Assert # ---- Complete Assertions Here ---- - # assertion 1 goes here - # assertion 2 goes here - # assertion 3 goes here + assert response.status_code == 200 + assert "goal" in response_body + assert response_body == {"goal": {"id": 1, "title":"Updated Goal"}} # ---- 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 - # ---- Complete Act Here ---- + response = client.put("/goals/1") + response_body = response.get_json() # Assert # ---- Complete Assertions Here ---- - # assertion 1 goes here - # assertion 2 goes here + assert response.status_code == 404 + assert response_body == None # ---- Complete Assertions Here ---- @@ -113,17 +115,18 @@ 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.get("goal/1") + response_body = response.get_json() # Assert # ---- Complete Assertions Here ---- - # assertion 1 goes here - # assertion 2 goes here + assert response.status_code == 404 + assert response_body == None # ---- Complete Assertions Here ---- From 9ca28fe4af4bc3c022c367229b9455a174690206 Mon Sep 17 00:00:00 2001 From: Alie Ibarra Date: Sun, 7 Nov 2021 21:03:04 -0800 Subject: [PATCH 11/14] Pass all 6 Wave Tests WITHOUT Slack Messaging --- app/models/goal.py | 1 + app/models/task.py | 16 ++++-- app/routes.py | 81 +++++++++++++++++++++++++--- migrations/versions/60e34370deef_.py | 30 +++++++++++ requirements.txt | 1 - 5 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 migrations/versions/60e34370deef_.py diff --git a/app/models/goal.py b/app/models/goal.py index 12291f33c..f18ec2253 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -5,6 +5,7 @@ class Goal(db.Model): goal_id = db.Column(db.Integer, primary_key=True, autoincrement= True) title = db.Column(db.String) + tasks = db.relationship("Task", back_populates="goal") #NEW def to_dict(self): diff --git a/app/models/task.py b/app/models/task.py index 34c606443..65365db9d 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -1,4 +1,5 @@ from flask import current_app +from sqlalchemy.orm import backref from app import db @@ -7,16 +8,21 @@ 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.goal_id')) #NEW + goal = db.relationship("Goal", back_populates= "tasks") #NEW + + def is_complete(self): + return bool(self.completed_at) def to_dict(self): - if not self.completed_at: - is_complete = False - else: - is_complete = True + # if not self.completed_at: + # is_complete = False + # else: + # is_complete = True return { "id": self.task_id, "title": self.title, "description": self.description, - "is_complete": is_complete, + "is_complete": self.is_complete(), } \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index e68253028..9139d74c9 100644 --- a/app/routes.py +++ b/app/routes.py @@ -4,7 +4,8 @@ from app.models.task import Task from app.models.goal import Goal from datetime import datetime - +import requests +import os # DEFINE BLUEPRINT tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") @@ -61,9 +62,29 @@ def create_goal(): db.session.commit() return make_response(jsonify({"goal": new_goal.to_dict()}), 201) + +@goals_bp.route("//tasks", methods=["POST"]) +def create_goal_tasks(goal_id): + request_body = request.get_json() + + goal = Goal.query.get(goal_id) + if goal is None: + return make_response("Invalid Goal ID", 404) + + goal_task_ids = [] + + for task_id in request_body["task_ids"]: + task = Task.query.get(task_id) + task.goal = goal + goal_task_ids.append(task.task_id) + db.session.add(task) + db.session.commit + + return jsonify({"id": int(goal_id), "task_ids": goal_task_ids}) + #----------------- -#READ (aka GET) +#READ ALL (aka GET) @tasks_bp.route("", methods=["GET"]) def read_all_tasks(): sort_query = request.args.get("sort") @@ -82,11 +103,6 @@ def read_all_tasks(): tasks_response.append(task.to_dict()) return jsonify(tasks_response) -@tasks_bp.route("/", methods=["GET"]) -def read_one_task(id): - task = get_task_from_id(id) - return make_response(jsonify({"task" : task.to_dict()}),200) - @goals_bp.route("", methods=["GET"]) def read_all_goals(): goals = Goal.query.all() @@ -96,10 +112,47 @@ def read_all_goals(): goals_response.append(goal.to_dict()) return jsonify(goals_response) +#----------------- +#READ ONE (aka GET) +@tasks_bp.route("/", methods=["GET"]) +def read_one_task(id): + task = get_task_from_id(id) + request_body = request.get_json() + + if not task.goal_id: + return make_response(jsonify({"task" : task.to_dict()}),200) + + else: + task_goal_response = { + "id": task.task_id, + "goal_id": task.goal_id, + "title": task.title, + "description": task.description, + "is_complete": task.is_complete(), + } + return jsonify({"task" : task_goal_response}) + @goals_bp.route("/", methods=["GET"]) def read_one_task(id): goal = get_goal_from_id(id) return make_response(jsonify({"goal" : goal.to_dict()}),200) + +@goals_bp.route("//tasks", methods=["GET"]) +def read_goal_tasks(goal_id): + goal = get_goal_from_id(goal_id) + goal_tasks_response = [] + + for task in goal.tasks: + goal_tasks_response.append({ + "id": task.task_id, + "goal_id": task.goal_id, + "title": task.title, + "description": task.description, + "is_complete": task.is_complete(), + }) + + + return (jsonify({"id": int(goal_id), "title": goal.title, "tasks": goal_tasks_response}), 200) #----------------- #UPDATE @@ -137,6 +190,20 @@ def mark_complete(id, completion_status): if completion_status == "mark_complete": task.completed_at = datetime.date + + #SLACK MESSAGING + # SLACK_MSG_URL = 'https//slack.com/api/com.postMessage' + # SLACK_MSG_CHANNEL = 'task-notifications' + # SLACK_BOT_USERNAME = 'AliesBot' + # SLACK_TOKEN = 'xoxb21582911322942688710391650efd5ToZNAmN0gXxyri2orNvT' + # slack_msg = "Someone did a thing!" + # slack_response = request.post(SLACK_MSG_URL,{ + # "token": SLACK_TOKEN, + # "channel": SLACK_MSG_CHANNEL, + # "text": slack_msg, + # "username": SLACK_BOT_USERNAME + # }) + if completion_status == "mark_incomplete": task.completed_at = None diff --git a/migrations/versions/60e34370deef_.py b/migrations/versions/60e34370deef_.py new file mode 100644 index 000000000..5d61880d2 --- /dev/null +++ b/migrations/versions/60e34370deef_.py @@ -0,0 +1,30 @@ +"""empty message + +Revision ID: 60e34370deef +Revises: 9213ae7a89e8 +Create Date: 2021-11-07 13:33:43.791439 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '60e34370deef' +down_revision = '9213ae7a89e8' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('task', sa.Column('goal_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'task', 'goal', ['goal_id'], ['goal_id']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'task', type_='foreignkey') + op.drop_column('task', 'goal_id') + # ### end Alembic commands ### diff --git a/requirements.txt b/requirements.txt index aba519a51..b657cac9d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,6 @@ python-dotenv==0.15.0 python-editor==1.0.4 requests==2.25.1 six==1.15.0 -slackclient==2.9.3 SQLAlchemy==1.3.23 toml==0.10.2 typing-extensions==3.10.0.2 From eaf1cc1db110d35a61be34d541bdcb43977cb2ee Mon Sep 17 00:00:00 2001 From: Alie Ibarra Date: Mon, 8 Nov 2021 11:27:39 -0800 Subject: [PATCH 12/14] Pass all Test Waves AND Slack Token secured --- app/__init__.py | 1 - app/routes.py | 26 ++++++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 088fc5129..8129a9a08 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -9,7 +9,6 @@ migrate = Migrate() load_dotenv() - def create_app(test_config=None): app = Flask(__name__) app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False diff --git a/app/routes.py b/app/routes.py index 9139d74c9..9856bcd39 100644 --- a/app/routes.py +++ b/app/routes.py @@ -7,6 +7,8 @@ import requests import os + + # DEFINE BLUEPRINT tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") goals_bp = Blueprint("goals", __name__, url_prefix="/goals") @@ -151,7 +153,6 @@ def read_goal_tasks(goal_id): "is_complete": task.is_complete(), }) - return (jsonify({"id": int(goal_id), "title": goal.title, "tasks": goal_tasks_response}), 200) #----------------- @@ -192,17 +193,18 @@ def mark_complete(id, completion_status): task.completed_at = datetime.date #SLACK MESSAGING - # SLACK_MSG_URL = 'https//slack.com/api/com.postMessage' - # SLACK_MSG_CHANNEL = 'task-notifications' - # SLACK_BOT_USERNAME = 'AliesBot' - # SLACK_TOKEN = 'xoxb21582911322942688710391650efd5ToZNAmN0gXxyri2orNvT' - # slack_msg = "Someone did a thing!" - # slack_response = request.post(SLACK_MSG_URL,{ - # "token": SLACK_TOKEN, - # "channel": SLACK_MSG_CHANNEL, - # "text": slack_msg, - # "username": SLACK_BOT_USERNAME - # }) + SLACK_MSG_URL = 'https//slack.com/api/com.postMessage' + SLACK_MSG_CHANNEL = 'task-notifications' + SLACK_BOT_USERNAME = 'AliesBot' + SLACK_TOKEN = os.environ.get("SLACK_TOKEN") + slack_msg = "Someone did a thing!" + + requests.post(SLACK_MSG_URL,{ + "token": SLACK_TOKEN, + "channel": SLACK_MSG_CHANNEL, + "text": slack_msg, + "username": SLACK_BOT_USERNAME + }) if completion_status == "mark_incomplete": task.completed_at = None From ca9a55b16ea3a4fb8ab25302ae1c116808742064 Mon Sep 17 00:00:00 2001 From: Alie Ibarra Date: Mon, 8 Nov 2021 11:38:14 -0800 Subject: [PATCH 13/14] Create 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 6a87bbc4f2019a795c1d28bf1159540219271f44 Mon Sep 17 00:00:00 2001 From: Alie Ibarra Date: Mon, 8 Nov 2021 14:27:52 -0800 Subject: [PATCH 14/14] Debug 500 Heroku Error --- app/models/task.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/models/task.py b/app/models/task.py index 65365db9d..bb93131fb 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -12,6 +12,11 @@ class Task(db.Model): goal = db.relationship("Goal", back_populates= "tasks") #NEW def is_complete(self): + if not self.completed_at: + is_complete = False + else: + is_complete = True + return bool(self.completed_at) def to_dict(self):