diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 000000000..e69de29bb
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 000000000..105ce2da2
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 000000000..a8a99e530
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 000000000..dbcb90b51
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/task-list-api.iml b/.idea/task-list-api.iml
new file mode 100644
index 000000000..519512493
--- /dev/null
+++ b/.idea/task-list-api.iml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 000000000..94a25f7f4
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 000000000..a9556e977
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1667959287816
+
+
+ 1667959287816
+
+
+
+
\ No newline at end of file
diff --git a/app/__init__.py b/app/__init__.py
index 2764c4cc8..4dcce36af 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -22,13 +22,17 @@ def create_app(test_config=None):
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
"SQLALCHEMY_TEST_DATABASE_URI")
- # Import models here for Alembic setup
from app.models.task import Task
from app.models.goal import Goal
db.init_app(app)
migrate.init_app(app, db)
- # Register Blueprints here
+ # Register Blueprints
+ from . import task_routes
+ from . import goal_routes
- return app
+ app.register_blueprint(task_routes.tasks_bp)
+ app.register_blueprint(goal_routes.goals_bp)
+
+ return app
\ No newline at end of file
diff --git a/app/goal_routes.py b/app/goal_routes.py
new file mode 100644
index 000000000..5816f0b95
--- /dev/null
+++ b/app/goal_routes.py
@@ -0,0 +1,147 @@
+import json
+import datetime
+from os import abort
+
+from flask import Blueprint, abort, jsonify, make_response, request
+from sqlalchemy import asc, desc
+
+from app import db
+from app.models.goal import Goal
+from app.models.task import Task
+
+goals_bp = Blueprint('goals', __name__, url_prefix="/goals")
+
+
+def validate_goal_id(cls, goal_id):
+ try:
+ goal_id = int(goal_id)
+ except:
+ abort(make_response({"message":f"{cls.__name__} {goal_id} invalid"},400))
+
+ goal = cls.query.get(goal_id)
+
+ if not goal:
+ abort(make_response({"message":f"{cls.__name__} {goal_id} not found"}, 404))
+
+ return goal
+
+
+# @goals_bp.route('/', methods=['GET'])
+# def no_saved_goal(goal_id):
+# goal_validate = validate_goal_id(goal_id)
+
+# goal_response = []
+# # goal.todict() for goal in Goal
+
+# if goal_response is None:
+# return None
+# else:
+# return jsonify({goal_validate.goal_dict()}),200
+
+# @goals_bp.route('/', methods=['GET'])
+# def one_saved_goal(goal_id):
+# goal_validate = validate_goal_id(goal_id)
+
+# goal_response = [goal.todict() for goal in Goal]
+
+# if goal_id == None:
+# return None
+# else:
+# return jsonify({goal_validate.goal_dict()}),
+
+@goals_bp.route('/', methods=['GET'])
+def get_goal(goal_id):
+ goal_validate = validate_goal_id(Goal,goal_id)
+
+
+ return jsonify({"goal": goal_validate.goal_dict()}), 200
+
+@goals_bp.route("", methods=['POST'])
+def create_goal():
+ response_body = request.get_json()
+
+ if "title" not in response_body:
+ return jsonify({"details": "Invalid data"}), 400
+
+ created_goal = Goal(title=response_body["title"])
+
+ # new_goal.goal = Goal
+
+ db.session.add(created_goal)
+ db.session.commit()
+
+ return jsonify({"goal": created_goal.goal_dict()}), 201
+
+
+@goals_bp.route('', methods=['GET'])
+def query_all():
+
+ sort_query = request.args.get("sort")
+
+ query_lists = []
+
+ if sort_query== "desc":
+ query_goals = Goal.query.order_by(Goal.title.desc())
+
+
+ elif sort_query == "asc":
+ query_goals = Goal.query.order_by(Goal.title.asc())
+
+ else:
+ query_goals = Goal.query.all()
+
+ for query in query_goals:
+ query_lists.append(query.goal_dict())
+
+ return jsonify(query_lists), 200
+
+
+
+@goals_bp.route('/', methods=['PUT'])
+def update_goals(goal_id):
+
+ goal_object = validate_goal_id(Goal,goal_id)
+
+ response_body = request.get_json()
+
+ if "title" in response_body:
+ goal_object.title = response_body["title"]
+
+ db.session.commit()
+
+ return jsonify(goal_object.goal_dict()), 200
+
+
+@goals_bp.route('/', methods=['DELETE'])
+def delete_goals(goal_id):
+ test_goal = validate_goal_id(Goal,goal_id)
+ result_notice = {"details": f'Goal {goal_id} "{test_goal.title}" successfully deleted'}
+
+ db.session.delete(test_goal)
+ db.session.commit()
+
+ return make_response(result_notice, 200)
+
+@goals_bp.route('//tasks', methods=['POST'])
+def task_ids_to_goal(goal_id):
+ goal_object = validate_goal_id(Goal,goal_id)
+
+ request_body = request.get_json()
+
+ goal_object.tasks = [Task.query.get(task_id) for task_id in request_body["task_ids"]]
+ db.session.commit()
+
+ return {"id":goal_object.goal_id,
+ "task_ids":[task.task_id for task in goal_object.tasks]}, 200
+
+
+@goals_bp.route('//tasks', methods=['GET'])
+def goal_with_no_task(goal_id):
+ goal_object = validate_goal_id(Goal,goal_id)
+
+ # request_body = request.get_json()
+
+ tasks = [task.build_task_dict() for task in goal_object.tasks]
+
+ return {"id": goal_object.goal_id, "title": goal_object.title,
+ "tasks": tasks}, 200
diff --git a/app/models/goal.py b/app/models/goal.py
index b0ed11dd8..525692147 100644
--- a/app/models/goal.py
+++ b/app/models/goal.py
@@ -2,4 +2,18 @@
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(80))
+ tasks = db.relationship("Task", back_populates="goal", lazy=True)
+
+ def goal_dict(self):
+ return {
+ "id": self.goal_id,
+ "title": self.title}
+
+
+ @classmethod
+ def from_dict(cls, goal_data):
+ goal_class = cls(title=goal_data["title"])
+ return goal_class
+
\ No newline at end of file
diff --git a/app/models/task.py b/app/models/task.py
index c91ab281f..54b13e95f 100644
--- a/app/models/task.py
+++ b/app/models/task.py
@@ -1,5 +1,27 @@
from app import db
-
+from datetime import datetime
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(80))
+ description = db.Column(db.String(80))
+ completed_at = db.Column(db.DateTime, nullable=True)
+ goal = db.relationship("Goal", back_populates="tasks")
+ goal_id = db.Column(db.Integer, db.ForeignKey('goal.goal_id'),nullable=True)
+
+ def build_task_dict(self):
+ return {
+ "id": self.task_id,
+ "title": self.title,
+ "description": self.description,
+ "is_complete": bool(self.completed_at),
+ "goal_id": self.goal_id
+ }
+
+ @classmethod
+ def from_dict(cls, task_data):
+ new_Task = cls(title=task_data["title"],
+ description=task_data["description"],
+ is_complete=task_data["completed_at"],
+ completed_at=task_data.get("is_complete", None),)
+ return new_Task
\ No newline at end of file
diff --git a/app/routes.py b/app/routes.py
deleted file mode 100644
index 3aae38d49..000000000
--- a/app/routes.py
+++ /dev/null
@@ -1 +0,0 @@
-from flask import Blueprint
\ No newline at end of file
diff --git a/app/task_routes.py b/app/task_routes.py
new file mode 100644
index 000000000..43bc88312
--- /dev/null
+++ b/app/task_routes.py
@@ -0,0 +1,127 @@
+from flask import Blueprint
+from app.models.task import Task
+from app import db
+from flask import Blueprint, jsonify, abort, make_response, request
+from datetime import datetime
+
+
+tasks_bp = Blueprint('tasks', __name__, url_prefix="/tasks")
+
+
+@tasks_bp.route("", methods=['POST'])
+def created_task():
+ response_body = request.get_json()
+
+ if "title" not in response_body or "description" not in response_body:
+ return {"details": "Invalid data"}, 400
+
+ created_task = Task(title=response_body["title"],
+ description=response_body["description"])
+
+
+ db.session.add(created_task)
+ db.session.commit()
+
+ return make_response(jsonify({"task": created_task.build_task_dict()})), 201
+
+
+def validate_task_id(task_id):
+ try:
+ task_id = int(task_id)
+ except:
+ abort(make_response({"message":f"Task {task_id} invalid"}, 400))
+
+ task = Task.query.get(task_id)
+
+ if not task:
+ return abort(make_response({"message":f"Task {task_id} not found"}, 404))
+ # abort(make_response(None))
+ return task
+
+@tasks_bp.route('', methods=['GET'])
+def query_all():
+
+ sort_query = request.args.get("sort")
+
+ query_lists = []
+
+ if sort_query== "desc":
+ query_tasks = Task.query.order_by(Task.title.desc())
+
+
+ elif sort_query == "asc":
+ query_tasks = Task.query.order_by(Task.title.asc())
+
+ else:
+ query_tasks = Task.query.all()
+
+ for query in query_tasks:
+ query_lists.append(query.build_task_dict())
+
+ return jsonify(query_lists), 200
+
+
+
+@tasks_bp.route('/', methods=['GET'])
+def one_saved_task(task_id):
+ task_validate = validate_task_id(task_id)
+
+ # task = Task.query.get(task_id)
+ if task_id == None:
+ return "The task ID submitted, does not exist: error code 404"
+ else:
+ return {"task": task_validate.build_task_dict()}
+
+
+@tasks_bp.route('/', methods=['PUT'])
+def update_tasks(task_id):
+
+ validate_id = validate_task_id(task_id)
+
+ response_body = request.get_json()
+
+ validate_id.title = response_body["title"]
+ validate_id.description = response_body["description"]
+ # validate_id.completed_at = response_body["completed_at"]
+
+ db.session.commit()
+
+ return jsonify({"task": validate_id.build_task_dict()}),200
+
+
+@tasks_bp.route('/', methods=['DELETE'])
+def delete_tasks(task_id):
+ test_task = validate_task_id(task_id)
+ result_notice = {"details": f'Task {task_id} "{test_task.title}" successfully deleted'}
+
+ db.session.delete(test_task)
+ db.session.commit()
+
+ return make_response(result_notice, 200)
+
+@tasks_bp.route('//mark_complete', methods=['PATCH'])
+def mark_complete_on_incomplete_task(task_id):
+ test_task = validate_task_id(task_id)
+ test_task.completed_at = datetime.today()
+ test_task.is_complete = True
+ db.session.commit()
+ print(test_task.completed_at)
+
+ return make_response({"task": {
+ "id": test_task.task_id,
+ "title": test_task.title,
+ "description":test_task.description,
+ "is_complete": test_task.is_complete}}), 200
+
+@tasks_bp.route('//mark_incomplete', methods=['PATCH'])
+def mark_incomplete_on_complete_task(task_id):
+ test_task = validate_task_id(task_id)
+ test_task.completed_at = None
+ test_task.is_complete = False
+ db.session.commit()
+
+ return make_response({"task": {
+ "id": test_task.task_id,
+ "title": test_task.title,
+ "description":test_task.description,
+ "is_complete": test_task.is_complete}}), 200
\ 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/08d90f10bf26_added_more_variables_to_the_class.py b/migrations/versions/08d90f10bf26_added_more_variables_to_the_class.py
new file mode 100644
index 000000000..b9acc7b3c
--- /dev/null
+++ b/migrations/versions/08d90f10bf26_added_more_variables_to_the_class.py
@@ -0,0 +1,32 @@
+"""added more variables to the class
+
+Revision ID: 08d90f10bf26
+Revises: bddf6ef63637
+Create Date: 2022-11-16 23:05:26.972666
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '08d90f10bf26'
+down_revision = 'bddf6ef63637'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column('goal', sa.Column('completed_at', sa.DateTime(), nullable=True))
+ op.add_column('goal', sa.Column('description', sa.String(length=80), nullable=True))
+ op.add_column('goal', sa.Column('goal_title', sa.String(length=80), nullable=True))
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_column('goal', 'goal_title')
+ op.drop_column('goal', 'description')
+ op.drop_column('goal', 'completed_at')
+ # ### end Alembic commands ###
diff --git a/migrations/versions/17069dd1dc5c_.py b/migrations/versions/17069dd1dc5c_.py
new file mode 100644
index 000000000..2591e373e
--- /dev/null
+++ b/migrations/versions/17069dd1dc5c_.py
@@ -0,0 +1,34 @@
+"""empty message
+
+Revision ID: 17069dd1dc5c
+Revises: 08d90f10bf26
+Create Date: 2022-11-21 09:59:35.100729
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision = '17069dd1dc5c'
+down_revision = '08d90f10bf26'
+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=80), nullable=True))
+ op.drop_column('goal', 'goal_title')
+ op.drop_column('goal', 'completed_at')
+ op.drop_column('goal', 'description')
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column('goal', sa.Column('description', sa.VARCHAR(length=80), autoincrement=False, nullable=True))
+ op.add_column('goal', sa.Column('completed_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True))
+ op.add_column('goal', sa.Column('goal_title', sa.VARCHAR(length=80), autoincrement=False, nullable=True))
+ op.drop_column('goal', 'title')
+ # ### end Alembic commands ###
diff --git a/migrations/versions/7039fbae1b28_.py b/migrations/versions/7039fbae1b28_.py
new file mode 100644
index 000000000..43bc2cb3f
--- /dev/null
+++ b/migrations/versions/7039fbae1b28_.py
@@ -0,0 +1,30 @@
+"""empty message
+
+Revision ID: 7039fbae1b28
+Revises: f81eec31fc3f
+Create Date: 2022-11-12 14:12:36.993387
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision = '7039fbae1b28'
+down_revision = 'f81eec31fc3f'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column('task', sa.Column('is_complete', sa.DateTime(), nullable=True))
+ op.drop_column('task', 'completed_at')
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column('task', sa.Column('completed_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True))
+ op.drop_column('task', 'is_complete')
+ # ### end Alembic commands ###
diff --git a/migrations/versions/bddf6ef63637_.py b/migrations/versions/bddf6ef63637_.py
new file mode 100644
index 000000000..17d2b47a0
--- /dev/null
+++ b/migrations/versions/bddf6ef63637_.py
@@ -0,0 +1,30 @@
+"""empty message
+
+Revision ID: bddf6ef63637
+Revises: 7039fbae1b28
+Create Date: 2022-11-12 14:40:26.752660
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision = 'bddf6ef63637'
+down_revision = '7039fbae1b28'
+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.drop_column('task', 'is_complete')
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column('task', sa.Column('is_complete', postgresql.TIMESTAMP(), autoincrement=False, nullable=True))
+ op.drop_column('task', 'completed_at')
+ # ### end Alembic commands ###
diff --git a/migrations/versions/cf1d8b0de5b9_.py b/migrations/versions/cf1d8b0de5b9_.py
new file mode 100644
index 000000000..3f11917b9
--- /dev/null
+++ b/migrations/versions/cf1d8b0de5b9_.py
@@ -0,0 +1,45 @@
+"""empty message
+
+Revision ID: cf1d8b0de5b9
+Revises:
+Create Date: 2022-11-11 18:24:46.976383
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'cf1d8b0de5b9'
+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=80), nullable=True),
+ sa.Column('description', sa.String(length=80), nullable=True),
+ sa.Column('completed_at', sa.DateTime(), nullable=True),
+ sa.PrimaryKeyConstraint('task_id')
+ )
+ op.create_index(op.f('ix_task_completed_at'), 'task', ['completed_at'], unique=False)
+ op.create_index(op.f('ix_task_description'), 'task', ['description'], unique=False)
+ op.create_index(op.f('ix_task_title'), 'task', ['title'], unique=False)
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_index(op.f('ix_task_title'), table_name='task')
+ op.drop_index(op.f('ix_task_description'), table_name='task')
+ op.drop_index(op.f('ix_task_completed_at'), table_name='task')
+ op.drop_table('task')
+ op.drop_table('goal')
+ # ### end Alembic commands ###
diff --git a/migrations/versions/e650b09bff6e_.py b/migrations/versions/e650b09bff6e_.py
new file mode 100644
index 000000000..a14423aac
--- /dev/null
+++ b/migrations/versions/e650b09bff6e_.py
@@ -0,0 +1,30 @@
+"""empty message
+
+Revision ID: e650b09bff6e
+Revises: 17069dd1dc5c
+Create Date: 2023-01-20 18:00:37.997559
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'e650b09bff6e'
+down_revision = '17069dd1dc5c'
+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/migrations/versions/f81eec31fc3f_.py b/migrations/versions/f81eec31fc3f_.py
new file mode 100644
index 000000000..1d057803d
--- /dev/null
+++ b/migrations/versions/f81eec31fc3f_.py
@@ -0,0 +1,32 @@
+"""empty message
+
+Revision ID: f81eec31fc3f
+Revises: cf1d8b0de5b9
+Create Date: 2022-11-11 19:07:54.318699
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'f81eec31fc3f'
+down_revision = 'cf1d8b0de5b9'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_index('ix_task_completed_at', table_name='task')
+ op.drop_index('ix_task_description', table_name='task')
+ op.drop_index('ix_task_title', table_name='task')
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_index('ix_task_title', 'task', ['title'], unique=False)
+ op.create_index('ix_task_description', 'task', ['description'], unique=False)
+ op.create_index('ix_task_completed_at', 'task', ['completed_at'], unique=False)
+ # ### end Alembic commands ###
diff --git a/requirements.txt b/requirements.txt
index cacdbc36e..e8da56e54 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,7 +5,9 @@ blinker==1.4
certifi==2020.12.5
chardet==4.0.0
click==7.1.2
+coverage==6.3.3
Flask==1.1.2
+Flask-Cors==3.0.10
Flask-Migrate==2.6.0
Flask-SQLAlchemy==2.4.4
gunicorn==20.1.0
@@ -15,6 +17,8 @@ itsdangerous==1.1.0
Jinja2==2.11.3
Mako==1.1.4
MarkupSafe==1.1.1
+mypy @ git+https://github.com/python/mypy.git@abc9d155ffbd9ea160eec0b57c450cdf7e53ce39
+mypy-extensions==0.4.3
packaging==20.9
pluggy==0.13.1
psycopg2-binary==2.9.4
diff --git a/tests/test_wave_01.py b/tests/test_wave_01.py
index dca626d78..295905f65 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")
+ assert response_body == {'message': 'Task 1 not found'}
+
+ # raise Exception("Complete test with assertion about response body")
# *****************************************************************
# **Complete test with assertion about response body***************
# *****************************************************************
-@pytest.mark.skip(reason="No way to test this feature yet")
+# # @pytest.mark.skip(reason="No way to test this feature yet")
def test_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={
@@ -130,14 +132,14 @@ def test_update_task_not_found(client):
# Assert
assert response.status_code == 404
-
- raise Exception("Complete test with assertion about response body")
+ assert Task.query.get(1) == None
+ # raise Exception("Complete test with assertion about response body")
# *****************************************************************
# **Complete test with assertion about response body***************
# *****************************************************************
-@pytest.mark.skip(reason="No way to test this feature yet")
+# # @pytest.mark.skip(reason="No way to test this feature yet")
def test_delete_task(client, one_task):
# Act
response = client.delete("/tasks/1")
@@ -152,7 +154,7 @@ def test_delete_task(client, one_task):
assert Task.query.get(1) == None
-@pytest.mark.skip(reason="No way to test this feature yet")
+# # @pytest.mark.skip(reason="No way to test this feature yet")
def test_delete_task_not_found(client):
# Act
response = client.delete("/tasks/1")
@@ -160,16 +162,18 @@ def test_delete_task_not_found(client):
# Assert
assert response.status_code == 404
+ assert Task.query.all() == []
+ assert response_body == {'message': 'Task 1 not found'}
- raise Exception("Complete test with assertion about response body")
+ # raise Exception("Complete test with assertion about response body")
# *****************************************************************
# **Complete test with assertion about response body***************
# *****************************************************************
- assert Task.query.all() == []
-@pytest.mark.skip(reason="No way to test this feature yet")
+
+# # @pytest.mark.skip(reason="No way to test this feature yet")
def test_create_task_must_contain_title(client):
# Act
response = client.post("/tasks", json={
@@ -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")
diff --git a/tests/test_wave_03.py b/tests/test_wave_03.py
index 32d379822..b435ca98b 100644
--- a/tests/test_wave_03.py
+++ b/tests/test_wave_03.py
@@ -5,21 +5,21 @@
import pytest
-@pytest.mark.skip(reason="No way to test this feature yet")
+# @pytest.mark.skip(reason="No way to test this feature yet")
def test_mark_complete_on_incomplete_task(client, one_task):
# Arrange
- """
- The future Wave 4 adds special functionality to this route,
- so for this test, we need to set-up "mocking."
+ # """
+ # 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!
+ # 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).
+ # 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.
- """
+ # 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
@@ -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,21 @@ 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."
+ # """
+ # 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!
+ # 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).
+ # 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.
- """
+ # 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 +99,7 @@ def test_mark_complete_on_completed_task(client, completed_task):
assert Task.query.get(1).completed_at
-@pytest.mark.skip(reason="No way to test this feature yet")
+# @pytest.mark.skip(reason="No way to test this feature yet")
def test_mark_incomplete_on_incomplete_task(client, one_task):
# Act
response = client.patch("/tasks/1/mark_incomplete")
@@ -119,7 +119,7 @@ def test_mark_incomplete_on_incomplete_task(client, one_task):
assert Task.query.get(1).completed_at == None
-@pytest.mark.skip(reason="No way to test this feature yet")
+# @pytest.mark.skip(reason="No way to test this feature yet")
def test_mark_complete_missing_task(client):
# Act
response = client.patch("/tasks/1/mark_complete")
@@ -128,13 +128,14 @@ 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")
+ assert response_body == {'message': 'Task 1 not found'}
# *****************************************************************
# **Complete test with assertion about response body***************
# *****************************************************************
-@pytest.mark.skip(reason="No way to test this feature yet")
+# @pytest.mark.skip(reason="No way to test this feature yet")
def test_mark_incomplete_missing_task(client):
# Act
response = client.patch("/tasks/1/mark_incomplete")
@@ -143,7 +144,9 @@ 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")
+
+ assert response_body == {'message': 'Task 1 not found'}
# *****************************************************************
# **Complete test with assertion about response body***************
# *****************************************************************
diff --git a/tests/test_wave_05.py b/tests/test_wave_05.py
index aee7c52a1..6832f7e65 100644
--- a/tests/test_wave_05.py
+++ b/tests/test_wave_05.py
@@ -1,7 +1,8 @@
+from app.models.goal import Goal
import pytest
-@pytest.mark.skip(reason="No way to test this feature yet")
+# @pytest.mark.skip(reason="No way to test this feature yet")
def test_get_goals_no_saved_goals(client):
# Act
response = client.get("/goals")
@@ -12,7 +13,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 +30,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,14 +47,17 @@ 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 response.status_code == 404
+ assert "message" in response_body
+ assert response_body == {"message":"Goal 1 not found"}
+
+ # raise Exception("Complete test")
# Assert
# ---- Complete Test ----
# assertion 1 goes here
@@ -61,7 +65,7 @@ def test_get_goal_not_found(client):
# ---- Complete Test ----
-@pytest.mark.skip(reason="No way to test this feature yet")
+# @pytest.mark.skip(reason="No way to test this feature yet")
def test_create_goal(client):
# Act
response = client.post("/goals", json={
@@ -80,34 +84,38 @@ 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")
# Act
# ---- Complete Act Here ----
+ response = client.put("/goals/1", json={
+ "title": "My New Goal"
+ })
+ response_body = response.get_json()
+
+ # raise Exception("Complete test")
+ assert response.status_code == 200
+ assert "title" in response_body
+ assert response_body["title"] == "My New Goal"
- # Assert
- # ---- Complete Assertions Here ----
- # assertion 1 goes here
- # assertion 2 goes here
- # assertion 3 goes here
- # ---- Complete Assertions Here ----
-@pytest.mark.skip(reason="test to be completed by student")
+# @pytest.mark.skip(reason="test to be completed by student")
def test_update_goal_not_found(client):
- raise Exception("Complete test")
- # Act
+ # Act
# ---- Complete Act Here ----
+ response = client.put("/goals/1", json={
+ "title": "My New Goal"
+ })
+ response_body = response.get_json()
- # Assert
- # ---- Complete Assertions Here ----
- # assertion 1 goes here
- # assertion 2 goes here
- # ---- Complete Assertions Here ----
+ # raise Exception("Complete test")
+ assert response.status_code == 404
+ assert "message" in 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_delete_goal(client, one_goal):
# Act
response = client.delete("/goals/1")
@@ -124,27 +132,28 @@ 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")
+ # raise Exception("Complete test with assertion about response body")
# *****************************************************************
# **Complete test with assertion about response body***************
# *****************************************************************
-@pytest.mark.skip(reason="test to be completed by student")
+# @pytest.mark.skip(reason="test to be completed by student")
def test_delete_goal_not_found(client):
- raise Exception("Complete test")
-
+ # raise Exception("Complete test")
# Act
- # ---- Complete Act Here ----
+ response = client.post("/goals", json={})
+ response_body = response.get_json()
# Assert
- # ---- Complete Assertions Here ----
- # assertion 1 goes here
- # assertion 2 goes here
- # ---- Complete Assertions Here ----
+ assert response.status_code == 400
+ assert response_body == {
+ "details": "Invalid data"
+ }
+
-@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={})
diff --git a/tests/test_wave_06.py b/tests/test_wave_06.py
index 8afa4325e..6cb44ee8f 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,22 +42,24 @@ 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")
response_body = response.get_json()
# Assert
+ assert "message" in response_body
+ assert response_body == {"message":"Goal 1 not found"}
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***************
# *****************************************************************
-@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 +76,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 +101,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()