From 4ac18c57505043f448d95b11f246b512151d064d Mon Sep 17 00:00:00 2001 From: trish Date: Mon, 12 Jul 2021 21:55:48 -0700 Subject: [PATCH 01/16] inital set up of basic models and migrations --- app/__init__.py | 13 ++++-- app/models/board.py | 9 ++++ app/models/card.py | 8 ++++ migrations/README | 1 + migrations/alembic.ini | 45 ++++++++++++++++++ migrations/env.py | 96 +++++++++++++++++++++++++++++++++++++++ migrations/script.py.mako | 24 ++++++++++ 7 files changed, 192 insertions(+), 4 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 diff --git a/app/__init__.py b/app/__init__.py index 1c821436..67a00825 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -9,13 +9,18 @@ migrate = Migrate() load_dotenv() - -def create_app(): +def create_app(test_config=None): app = Flask(__name__) - app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False - app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get( + if not test_config: + app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get( "SQLALCHEMY_DATABASE_URI") + else: + app.config["TESTING"] = True + app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get( + "SQLALCHEMY_TEST_DATABASE_URI") # Import models here for Alembic setup # from app.models.ExampleModel import ExampleModel diff --git a/app/models/board.py b/app/models/board.py index 147eb748..2325db21 100644 --- a/app/models/board.py +++ b/app/models/board.py @@ -1 +1,10 @@ +from sqlalchemy.orm import backref from app import db + +class Board(db.model): + board_id = db.Column(db.integer, primary_key=True) + title = db.Column(db.String) + owner = db.Column(db.String) + cards = db.Relationship("Card", backref="board", lazy=True) + # not sure if we actually need cards here. + # sort of like we had tasks. \ No newline at end of file diff --git a/app/models/card.py b/app/models/card.py index 147eb748..5c2aa55e 100644 --- a/app/models/card.py +++ b/app/models/card.py @@ -1 +1,9 @@ from app import db + +class Card(db.model): + card_id = db.Column(db.Integer, primary_key=True) + message = db.Column(db.String) + likes_count = db.Column(db.Integer) + board_id = db.Column(db.Integer, db.ForeignKey("board.board_id")) + # would we ever have a case where it would be ok + # for this to be null ? \ No newline at end of file diff --git a/migrations/README b/migrations/README new file mode 100644 index 00000000..98e4f9c4 --- /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 00000000..f8ed4801 --- /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 00000000..8b3fb335 --- /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 00000000..2c015630 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} From e6fb9637b5a4829412ca9ce6df2a9212adc7484d Mon Sep 17 00:00:00 2001 From: trish Date: Mon, 12 Jul 2021 22:21:58 -0700 Subject: [PATCH 02/16] fix db models. Return empty array on first get --- app/__init__.py | 12 ++++++-- app/models/board.py | 6 ++-- app/models/card.py | 4 +-- app/routes.py | 23 +++++++++++++++ migrations/versions/d92fb1e3635d_.py | 42 ++++++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 migrations/versions/d92fb1e3635d_.py diff --git a/app/__init__.py b/app/__init__.py index 67a00825..f8eddaba 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -11,26 +11,32 @@ def create_app(test_config=None): app = Flask(__name__) + app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False - if not test_config: - app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + if test_config is None: app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get( "SQLALCHEMY_DATABASE_URI") else: app.config["TESTING"] = True - app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get( "SQLALCHEMY_TEST_DATABASE_URI") # Import models here for Alembic setup # from app.models.ExampleModel import ExampleModel + from app.models.board import Board + from app.models.card import Card db.init_app(app) migrate.init_app(app, db) # Register Blueprints here # from .routes import example_bp + from .routes import boards_bp + from .routes import cards_bp + # app.register_blueprint(example_bp) + app.register_blueprint(boards_bp) + app.register_blueprint(cards_bp) CORS(app) return app diff --git a/app/models/board.py b/app/models/board.py index 2325db21..be341157 100644 --- a/app/models/board.py +++ b/app/models/board.py @@ -1,10 +1,10 @@ from sqlalchemy.orm import backref from app import db -class Board(db.model): - board_id = db.Column(db.integer, primary_key=True) +class Board(db.Model): + board_id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String) owner = db.Column(db.String) - cards = db.Relationship("Card", backref="board", lazy=True) + cards = db.relationship("Card", backref="board", lazy=True) # not sure if we actually need cards here. # sort of like we had tasks. \ No newline at end of file diff --git a/app/models/card.py b/app/models/card.py index 5c2aa55e..991d3510 100644 --- a/app/models/card.py +++ b/app/models/card.py @@ -1,9 +1,9 @@ from app import db -class Card(db.model): +class Card(db.Model): card_id = db.Column(db.Integer, primary_key=True) message = db.Column(db.String) likes_count = db.Column(db.Integer) - board_id = db.Column(db.Integer, db.ForeignKey("board.board_id")) + board_id = db.Column(db.Integer, db.ForeignKey("board.board_id"), nullable=True) # would we ever have a case where it would be ok # for this to be null ? \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index 480b8c4b..63789d2a 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,4 +1,27 @@ from flask import Blueprint, request, jsonify, make_response from app import db +from app.models.board import Board +from app.models.card import Card +from dotenv import load_dotenv +import requests +import os + +load_dotenv() +boards_bp = Blueprint("boards", __name__, url_prefix="/boards") +cards_bp = Blueprint("cards", __name__, url_prefix="/cards") # example_bp = Blueprint('example_bp', __name__) +@boards_bp.route("", methods=["GET", "POST"]) +def handle_boards(): + if request.method == "GET": + boards = Board.query.all() + boards_response = [] + + for board in boards: + boards_response.append({ + "board_id": board.board_id, + "title": board.title, + "owner": board.owner + }) + print('we are in the get request') + return make_response(jsonify(boards_response), 200) diff --git a/migrations/versions/d92fb1e3635d_.py b/migrations/versions/d92fb1e3635d_.py new file mode 100644 index 00000000..4fc6c7f4 --- /dev/null +++ b/migrations/versions/d92fb1e3635d_.py @@ -0,0 +1,42 @@ +"""empty message + +Revision ID: d92fb1e3635d +Revises: +Create Date: 2021-07-12 22:18:46.720776 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd92fb1e3635d' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('board', + sa.Column('board_id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(), nullable=True), + sa.Column('owner', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('board_id') + ) + op.create_table('card', + sa.Column('card_id', sa.Integer(), nullable=False), + sa.Column('message', sa.String(), nullable=True), + sa.Column('likes_count', sa.Integer(), nullable=True), + sa.Column('board_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['board_id'], ['board.board_id'], ), + sa.PrimaryKeyConstraint('card_id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('card') + op.drop_table('board') + # ### end Alembic commands ### From 9cc20d1e4bf97532d4810f14ef8744599e41b75a Mon Sep 17 00:00:00 2001 From: trish Date: Tue, 13 Jul 2021 09:55:46 -0700 Subject: [PATCH 03/16] start of crud plans --- crud_plans.md | 163 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 crud_plans.md diff --git a/crud_plans.md b/crud_plans.md new file mode 100644 index 00000000..c52b3a8d --- /dev/null +++ b/crud_plans.md @@ -0,0 +1,163 @@ +# Must Ensure the following CRUD paths work: +~~- [ ] GET /boards~~ +~~- [ ] POST /boards~~ + +- [ ] GET /boards//cards +- [ ] POST /boards//cards +- [ ] DELETE /cards/ +- [ ] PUT /cards//like + + +### 1. Get Boards: Getting saved Boards +- [ ] GET /boards + +As a client, I want to be able to make a `GET` request to `/boards` when there is at least one but possibly many saved boards and get this response: +`200 ok` + +```json +[ + { + "board_id": 1, + "title": "Literary Quotes", + "owner": "Octavia Butler" + }, + { + "board_id": 2, + "title": "Supreme Court Quotes", + "owner": "Ruth Bader Ginsburg" + }, + { + "board_id": 3, + "title": "Jokes", + "owner": "Really Funny Person" + } +] +``` + +### 2. Create a Board: need to decide what could be valid.. +- [ ] POST /boards + +As a client, I want to be able to make a `POST` request to `/boards` with the following HTTP request body + + +```json +{ + "title": "Brand New Board", + "owner": "Marry Poppins" +} +``` + +and get this response: + +`201 CREATED` + +```json +{ + "board": { + "board_id": 1, + "title": "Brand New Board", + "owner": "Marry Poppins" + } +} +``` +So that I know I succesfully created a Board that is saved in the databse. + +******this might need to change its shape. Since it seems possible to have a many to many relationship here**** + +Question: There could be many boards with many cards...? If so, changes my models a bit. I think. + +### Get Boards: No saved Boards + +### Get one Board: one saved Board +As a client, I want to be able to make a `GET` request to `/boards/1` when there is at least one saved board and get this response: + +`200 ok` +```json +{ + "board": { + "board_id": 1, + "title": "New Board for me", + "owner": "Spice Girls" + } +} +``` + +### Get One Board: No matching Boards +As a client, I want to be able to make a `GET` request to `/boards/1` when there are no matching boards and get this response: + +`404 Not Found` + +No response body. + +### Update Board +As a client, I want to be able to make a `PUT` request to `/boards/1` when there is at least one saved board with this request body: + +```json +{ + "title": "New name for fancy board", + "owner": "Morty" +} +``` + +and get this response: +`200 ok` +```json +{ + "board": { + "board_id": 1, + "title": "New name for fancy board", + "owner": "Morty" + } +} +``` + +### Update Board: No matching Board +As a client, I want to be able to make a `PUT` request to `/boards/2` when there are no matching tasks with this request body: + +```json +{ + "title": "New title for board", + "owner": "Trish" +} +``` +and get this response: +`404 Not Found` +No response body + +### Delete Board: Deleting a board + +### Delete Board: No Matching Board + +*** +Need to decide on what circumstance will we not allow a database creation to begin. +- example +-- when no title is provided? +-- when no owner is provided? + +### Create a Board: Invalid Board With Missing Data ** + +
+ +# Need to go through crud for Cards like I did for board above. + +### Create a Card: valid card + +### Get Cards: Getting saved cards + +### Get Cards: No saved Cards + +### Get One Card: One saved Card + +### Get One Card: No matching Card + +### Update Card: + +### Update Card: No matching Card + +### Delete Card: Deleting a Card + +### Delete Card: No matching Card + +### Create a Card: Invalid Card with missing data + + From 8518fa841c0557bd50594c5628742f81f3b502ac Mon Sep 17 00:00:00 2001 From: trish Date: Tue, 13 Jul 2021 10:38:30 -0700 Subject: [PATCH 04/16] adjust card model and migrate/upgrade change --- app/models/card.py | 2 +- migrations/versions/5914cb95c7e9_.py | 32 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 migrations/versions/5914cb95c7e9_.py diff --git a/app/models/card.py b/app/models/card.py index 991d3510..53fde38f 100644 --- a/app/models/card.py +++ b/app/models/card.py @@ -4,6 +4,6 @@ class Card(db.Model): card_id = db.Column(db.Integer, primary_key=True) message = db.Column(db.String) likes_count = db.Column(db.Integer) - board_id = db.Column(db.Integer, db.ForeignKey("board.board_id"), nullable=True) + board_id = db.Column(db.Integer, db.ForeignKey("board.board_id"), nullable=False) # would we ever have a case where it would be ok # for this to be null ? \ No newline at end of file diff --git a/migrations/versions/5914cb95c7e9_.py b/migrations/versions/5914cb95c7e9_.py new file mode 100644 index 00000000..a76b1d06 --- /dev/null +++ b/migrations/versions/5914cb95c7e9_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: 5914cb95c7e9 +Revises: d92fb1e3635d +Create Date: 2021-07-13 10:37:44.277295 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '5914cb95c7e9' +down_revision = 'd92fb1e3635d' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('card', 'board_id', + existing_type=sa.INTEGER(), + nullable=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('card', 'board_id', + existing_type=sa.INTEGER(), + nullable=True) + # ### end Alembic commands ### From 276b3c57852efab2521cd87fa9dcd1c98f126ea0 Mon Sep 17 00:00:00 2001 From: trish Date: Tue, 13 Jul 2021 10:39:51 -0700 Subject: [PATCH 05/16] create GET/POST abilities for viewing and creating boards --- app/routes.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/routes.py b/app/routes.py index 63789d2a..07f73b12 100644 --- a/app/routes.py +++ b/app/routes.py @@ -25,3 +25,22 @@ def handle_boards(): }) print('we are in the get request') return make_response(jsonify(boards_response), 200) + elif request.method == "POST": + request_body = request.get_json() + if "title" not in request_body or "owner" not in request_body: + return { + "details": f"Invalid data" + }, 400 + new_board = Board( + title=request_body["title"], + owner=request_body["owner"] + ) + db.session.add(new_board) + db.session.commit() + + return make_response({ + "board": { + "id": new_board.board_id, + "title": new_board.title, + } + }, 201) \ No newline at end of file From 3311346d9e76c4adca99a5d85625854b90bad7d9 Mon Sep 17 00:00:00 2001 From: trish Date: Tue, 13 Jul 2021 11:15:51 -0700 Subject: [PATCH 06/16] add messages and ability to delete boards --- app/routes.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/app/routes.py b/app/routes.py index 07f73b12..07f3b2eb 100644 --- a/app/routes.py +++ b/app/routes.py @@ -10,7 +10,6 @@ boards_bp = Blueprint("boards", __name__, url_prefix="/boards") cards_bp = Blueprint("cards", __name__, url_prefix="/cards") -# example_bp = Blueprint('example_bp', __name__) @boards_bp.route("", methods=["GET", "POST"]) def handle_boards(): if request.method == "GET": @@ -43,4 +42,25 @@ def handle_boards(): "id": new_board.board_id, "title": new_board.title, } - }, 201) \ No newline at end of file + }, 201) + +# needed to delete boards to test functionality. +# We don't need to keep this production.s +@boards_bp.route("/", methods=["DELETE"]) +def handle_board(board_id): + board = Board.query.get(board_id) + print('board ---> ', board) + + if request.method == "DELETE": + if board is None: + return make_response(f"Board {board_id} not found. ", 404) + + db.session.delete(board) + db.session.commit() + + return make_response( + { + "details": + f"Board: {board.board_id} with title: {board.title}. Successfully deleted" + } + ) From e53698dcda49cc6c0afacf6e6fb33fce8fcdf306 Mon Sep 17 00:00:00 2001 From: trish Date: Tue, 13 Jul 2021 12:18:00 -0700 Subject: [PATCH 07/16] update crud plans --- crud_plans.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/crud_plans.md b/crud_plans.md index c52b3a8d..58815ac5 100644 --- a/crud_plans.md +++ b/crud_plans.md @@ -1,5 +1,6 @@ # Must Ensure the following CRUD paths work: ~~- [ ] GET /boards~~ + ~~- [ ] POST /boards~~ - [ ] GET /boards//cards @@ -34,7 +35,7 @@ As a client, I want to be able to make a `GET` request to `/boards` when there i ] ``` -### 2. Create a Board: need to decide what could be valid.. +### 2. Create a Board: (invalid if title or owner not included) - [ ] POST /boards As a client, I want to be able to make a `POST` request to `/boards` with the following HTTP request body @@ -62,11 +63,15 @@ and get this response: ``` So that I know I succesfully created a Board that is saved in the databse. -******this might need to change its shape. Since it seems possible to have a many to many relationship here**** +### 3. Get Boards: No saved Boards +- [ ] GET /boards -Question: There could be many boards with many cards...? If so, changes my models a bit. I think. +As a client, I want to be able to make a `POST` request to `/boards` when there are zero saved tasks and get this response: +`200 ok` +```json +[] +``` -### Get Boards: No saved Boards ### Get one Board: one saved Board As a client, I want to be able to make a `GET` request to `/boards/1` when there is at least one saved board and get this response: From 1fbc41b8a50d8cfc1c1674be5adab36217299f71 Mon Sep 17 00:00:00 2001 From: trish Date: Tue, 13 Jul 2021 14:24:58 -0700 Subject: [PATCH 08/16] update Card model to accept when card.board_id is null --- app/models/card.py | 2 +- migrations/versions/698776f3fbcf_.py | 32 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 migrations/versions/698776f3fbcf_.py diff --git a/app/models/card.py b/app/models/card.py index 53fde38f..eded3f07 100644 --- a/app/models/card.py +++ b/app/models/card.py @@ -4,6 +4,6 @@ class Card(db.Model): card_id = db.Column(db.Integer, primary_key=True) message = db.Column(db.String) likes_count = db.Column(db.Integer) - board_id = db.Column(db.Integer, db.ForeignKey("board.board_id"), nullable=False) + board_id = db.Column(db.Integer, db.ForeignKey("board.board_id")) # would we ever have a case where it would be ok # for this to be null ? \ No newline at end of file diff --git a/migrations/versions/698776f3fbcf_.py b/migrations/versions/698776f3fbcf_.py new file mode 100644 index 00000000..bc59764e --- /dev/null +++ b/migrations/versions/698776f3fbcf_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: 698776f3fbcf +Revises: 5914cb95c7e9 +Create Date: 2021-07-13 14:24:11.948379 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '698776f3fbcf' +down_revision = '5914cb95c7e9' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('card', 'board_id', + existing_type=sa.INTEGER(), + nullable=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('card', 'board_id', + existing_type=sa.INTEGER(), + nullable=False) + # ### end Alembic commands ### From 2db0bb4abc2f9618da91167d8c8dde203699a0c9 Mon Sep 17 00:00:00 2001 From: trish Date: Tue, 13 Jul 2021 16:18:04 -0700 Subject: [PATCH 09/16] create GET and POST for /boards//cards --- app/routes.py | 77 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/app/routes.py b/app/routes.py index 07f3b2eb..b3b6d794 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,10 +1,10 @@ +import re from flask import Blueprint, request, jsonify, make_response from app import db from app.models.board import Board from app.models.card import Card from dotenv import load_dotenv import requests -import os load_dotenv() boards_bp = Blueprint("boards", __name__, url_prefix="/boards") @@ -12,6 +12,7 @@ @boards_bp.route("", methods=["GET", "POST"]) def handle_boards(): + if request.method == "GET": boards = Board.query.all() boards_response = [] @@ -22,14 +23,20 @@ def handle_boards(): "title": board.title, "owner": board.owner }) - print('we are in the get request') + return make_response(jsonify(boards_response), 200) + elif request.method == "POST": request_body = request.get_json() - if "title" not in request_body or "owner" not in request_body: + if "title" not in request_body: return { - "details": f"Invalid data" + "details": f"Invalid data. Must include title." }, 400 + if "owner" not in request_body: + return { + "details": f"Invalid data. Must include owner." + }, 400 + new_board = Board( title=request_body["title"], owner=request_body["owner"] @@ -41,15 +48,14 @@ def handle_boards(): "board": { "id": new_board.board_id, "title": new_board.title, + "owner": new_board.owner } }, 201) -# needed to delete boards to test functionality. -# We don't need to keep this production.s +# Deleting a single board instance @boards_bp.route("/", methods=["DELETE"]) def handle_board(board_id): board = Board.query.get(board_id) - print('board ---> ', board) if request.method == "DELETE": if board is None: @@ -64,3 +70,60 @@ def handle_board(board_id): f"Board: {board.board_id} with title: {board.title}. Successfully deleted" } ) + +# Getting board and associated cards or creating cards and associating a board +@boards_bp.route("//cards", methods=["GET", "POST"]) +def handle_board_cards(board_id): + if request.method == "POST": + board = Board.query.get(board_id) + + if not board: + return make_response({ + f"Board #{board_id} not found." + }, 404) + + request_body = request.get_json() + if "message" not in request_body: + return { + "details": f"Invalid, must include message." + } + + new_card = Card( + message=request_body["message"], + likes_count=0, + board_id=board.board_id + ) + db.session.add(new_card) + db.session.commit() + + return make_response({ + "card": { + "id": new_card.card_id, + "message": new_card.message, + "likes_count": new_card.likes_count, + "board_id": new_card.board_id + } + }, 201) + + elif request.method == "GET": + board = Board.query.get(board_id) + if not board: + return make_response(f"Board {board_id} not found", 404) + cards = board.cards + + list_of_cards = [] + + for card in cards: + individual_card = { + "id": card.card_id, + "board_id": board.board_id, + "message": card.message, + "likes_count": card.likes_count + } + list_of_cards.append(individual_card) + return make_response({ + "id": board.board_id, + "title": board.title, + "owner": board.owner, + "cards": list_of_cards + }) \ No newline at end of file From e19061e4b7dd9799d7ef5f69118352a9e4aa339f Mon Sep 17 00:00:00 2001 From: trish Date: Tue, 13 Jul 2021 16:18:58 -0700 Subject: [PATCH 10/16] remove commments --- app/models/card.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/models/card.py b/app/models/card.py index eded3f07..5aa7b33c 100644 --- a/app/models/card.py +++ b/app/models/card.py @@ -4,6 +4,4 @@ class Card(db.Model): card_id = db.Column(db.Integer, primary_key=True) message = db.Column(db.String) likes_count = db.Column(db.Integer) - board_id = db.Column(db.Integer, db.ForeignKey("board.board_id")) - # would we ever have a case where it would be ok - # for this to be null ? \ No newline at end of file + board_id = db.Column(db.Integer, db.ForeignKey("board.board_id")) \ No newline at end of file From 9afa99acc334bf1769afab3fb5e206539ebd1f4b Mon Sep 17 00:00:00 2001 From: trish Date: Tue, 13 Jul 2021 16:30:56 -0700 Subject: [PATCH 11/16] add ability to delete individual cards --- app/routes.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/routes.py b/app/routes.py index b3b6d794..2e344422 100644 --- a/app/routes.py +++ b/app/routes.py @@ -126,4 +126,23 @@ def handle_board_cards(board_id): "title": board.title, "owner": board.owner, "cards": list_of_cards - }) \ No newline at end of file + }) + +# deleting DELETE /cards/ + +@cards_bp.route("/", methods=["DELETE"]) +def handle_card(card_id): + card = Card.query.get(card_id) + + if card is None: + return make_response(f"Card #{card_id} not found.", 404) + + if request.method == "DELETE": + db.session.delete(card) + db.session.commit() + + return make_response( + { + "details": f"Card at card_id: {card.card_id}. Successfully deleted" + } + ) \ No newline at end of file From cfd835b5f2a1219b30a7a767dc7dd7900164515d Mon Sep 17 00:00:00 2001 From: trish Date: Tue, 13 Jul 2021 23:05:50 -0700 Subject: [PATCH 12/16] add ability to get, edit board by ID --- app/routes.py | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/app/routes.py b/app/routes.py index 2e344422..07d9d413 100644 --- a/app/routes.py +++ b/app/routes.py @@ -10,6 +10,9 @@ boards_bp = Blueprint("boards", __name__, url_prefix="/boards") cards_bp = Blueprint("cards", __name__, url_prefix="/cards") +# ----------------- Board Endpoints ----------------- # +# Get all boards --> done +# Create a board --> done @boards_bp.route("", methods=["GET", "POST"]) def handle_boards(): @@ -52,15 +55,16 @@ def handle_boards(): } }, 201) -# Deleting a single board instance -@boards_bp.route("/", methods=["DELETE"]) +# Delete a board by ID --> done +# Get a board by ID --> done +# Edit a board by ID --> done +@boards_bp.route("/", methods=["DELETE", "GET", "PUT"]) def handle_board(board_id): board = Board.query.get(board_id) - - if request.method == "DELETE": - if board is None: + if board is None: return make_response(f"Board {board_id} not found. ", 404) + if request.method == "DELETE": db.session.delete(board) db.session.commit() @@ -71,7 +75,29 @@ def handle_board(board_id): } ) -# Getting board and associated cards or creating cards and associating a board + elif request.method == "GET": + return { + "id": board.board_id, + "title": board.title, + "owner": board.owner + } + + elif request.method == "PUT": + form_data = request.get_json() + board.title = form_data["title"] + board.owner = form_data["owner"] + + db.session.commit() + + return make_response({ + "board": { + "id": board.board_id, + "title": board.title, + "owner": board.owner, + } + }) + + @boards_bp.route("//cards", methods=["GET", "POST"]) def handle_board_cards(board_id): if request.method == "POST": From 4b4ccb888948efa91345e4a86840b7d943850cd2 Mon Sep 17 00:00:00 2001 From: trish Date: Tue, 13 Jul 2021 23:08:09 -0700 Subject: [PATCH 13/16] add ability to delete, edit, get card by ID --- app/routes.py | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/app/routes.py b/app/routes.py index 07d9d413..75f40f5c 100644 --- a/app/routes.py +++ b/app/routes.py @@ -98,8 +98,12 @@ def handle_board(board_id): }) +# ----------------- Card Endpoints ----------------- # +# Getting cards by board_id --> done +# Creating a card & associating it with a specific board --> done @boards_bp.route("//cards", methods=["GET", "POST"]) def handle_board_cards(board_id): + if request.method == "POST": board = Board.query.get(board_id) @@ -154,9 +158,10 @@ def handle_board_cards(board_id): "cards": list_of_cards }) -# deleting DELETE /cards/ - -@cards_bp.route("/", methods=["DELETE"]) +# Delete a card by ID --> done +# Get card by ID --> done +# Edit card by ID --> done +@cards_bp.route("/", methods=["DELETE", "GET", "PUT"]) def handle_card(card_id): card = Card.query.get(card_id) @@ -171,4 +176,28 @@ def handle_card(card_id): { "details": f"Card at card_id: {card.card_id}. Successfully deleted" } - ) \ No newline at end of file + ) + + elif request.method == "GET": + return make_response({ + "id": card.card_id, + "message": card.message, + "likes_count": card.likes_count, + "board_id": card.board_id + }) + + elif request.method == "PUT": + form_data = request.get_json() + card.message = form_data["message"] + + db.session.commit() + + return make_response({ + "card": { + "id": card.card_id, + "message": card.message, + "likes_count": card.likes_count, + "board_id": card.board_id + } + }) + From 634c2c209f1287132dc7563d403c8882ad7f0174 Mon Sep 17 00:00:00 2001 From: trish Date: Tue, 13 Jul 2021 23:08:51 -0700 Subject: [PATCH 14/16] Add ability to increase likes by card_id --- app/routes.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/routes.py b/app/routes.py index 75f40f5c..29586aed 100644 --- a/app/routes.py +++ b/app/routes.py @@ -201,3 +201,21 @@ def handle_card(card_id): } }) +# ----------------- Increase Card Likes ----------------- # +@cards_bp.route("//like", methods=["PUT"]) +def handle_likes(card_id): + card = Card.query.get(card_id) + + if card is None: + return make_response({ + f"Card #{card_id} not found." + }, 404) + + if request.method == "PUT": + + card.likes_count += 1 + db.session.commit() + + return make_response( + {"likes_count": card.likes_count} + ), 200 From 05f33a209654235b3b04293b9af284a83a8446f2 Mon Sep 17 00:00:00 2001 From: trish Date: Tue, 13 Jul 2021 23:14:16 -0700 Subject: [PATCH 15/16] remove random string --- app/routes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/routes.py b/app/routes.py index 29586aed..85c8856a 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,4 +1,3 @@ -import re from flask import Blueprint, request, jsonify, make_response from app import db from app.models.board import Board From 1ff0edc4ec93662a53ddc34c6a5ad7964db68961 Mon Sep 17 00:00:00 2001 From: trish Date: Wed, 14 Jul 2021 14:34:20 -0700 Subject: [PATCH 16/16] Created markdown file to display all possibly endpoints --- crud_plans.md | 195 ++++++++++++++++---------------------------------- 1 file changed, 60 insertions(+), 135 deletions(-) diff --git a/crud_plans.md b/crud_plans.md index 58815ac5..253bc089 100644 --- a/crud_plans.md +++ b/crud_plans.md @@ -1,168 +1,93 @@ -# Must Ensure the following CRUD paths work: -~~- [ ] GET /boards~~ - -~~- [ ] POST /boards~~ - -- [ ] GET /boards//cards -- [ ] POST /boards//cards -- [ ] DELETE /cards/ -- [ ] PUT /cards//like - - -### 1. Get Boards: Getting saved Boards -- [ ] GET /boards - -As a client, I want to be able to make a `GET` request to `/boards` when there is at least one but possibly many saved boards and get this response: -`200 ok` - -```json -[ - { - "board_id": 1, - "title": "Literary Quotes", - "owner": "Octavia Butler" - }, - { - "board_id": 2, - "title": "Supreme Court Quotes", - "owner": "Ruth Bader Ginsburg" - }, - { - "board_id": 3, - "title": "Jokes", - "owner": "Really Funny Person" - } -] -``` - -### 2. Create a Board: (invalid if title or owner not included) -- [ ] POST /boards - -As a client, I want to be able to make a `POST` request to `/boards` with the following HTTP request body +# CRUD Operations for the Inspo Board backend API +- You will need to replace the s and s for your own usecases. +# Handling Boards +### Post --> Create a new Board +- `/boards` +- `https://inspo--board.herokuapp.com/boards` +Example Payload body: ```json { - "title": "Brand New Board", - "owner": "Marry Poppins" + "title": "Arizona", + "owner": "TrishTheDish" } ``` +Endpoint will not let you create a board without a title or owner. It will give you an error message alerting you to this fact. -and get this response: - -`201 CREATED` +*** +### PUT --> Edit a board by ID +- `/boards/` +- `https://inspo--board.herokuapp.com/boards/2` +Example Payload body: ```json { - "board": { - "board_id": 1, - "title": "Brand New Board", - "owner": "Marry Poppins" - } + "owner": "Rudy G", + "title": "Lumber is a board." } ``` -So that I know I succesfully created a Board that is saved in the databse. +Endpoint will not let you edit a board that doesn't exist. Will give you helpful message if the board doesn't exist. -### 3. Get Boards: No saved Boards -- [ ] GET /boards - -As a client, I want to be able to make a `POST` request to `/boards` when there are zero saved tasks and get this response: -`200 ok` -```json -[] -``` +*** +### DEL --> Delete a board by ID +- `/boards/` +- `https://inspo--board.herokuapp.com/boards/3` +Endpoint will not let you delete a board that doesn't exist. Will give you a helpful message if board doesn't exist. -### Get one Board: one saved Board -As a client, I want to be able to make a `GET` request to `/boards/1` when there is at least one saved board and get this response: +*** +### GET --> Get a board by ID +- `/boards/` +- `https://inspo--board.herokuapp.com/boards/2` -`200 ok` -```json -{ - "board": { - "board_id": 1, - "title": "New Board for me", - "owner": "Spice Girls" - } -} -``` +Endpoint will not let you get board that doesn't exist. Will give you helpful message if board doesn't exist. -### Get One Board: No matching Boards -As a client, I want to be able to make a `GET` request to `/boards/1` when there are no matching boards and get this response: +*** +### GET --> Get all The Boards if any exist +- `/boards` +- `https://inspo--board.herokuapp.com/boards` -`404 Not Found` +If no boards exist then it will return an empty array. -No response body. -### Update Board -As a client, I want to be able to make a `PUT` request to `/boards/1` when there is at least one saved board with this request body: +# Handling Cards +### Post --> creating new card for a particular board. +- `boards//cards` +- `https://inspo--board.herokuapp.com/boards/2/cards` +Example Payload body: ```json { - "title": "New name for fancy board", - "owner": "Morty" + "message": "here is a lovely quote for you to post" } ``` +*** +### GET --> Get All Cards associated with a particular board. +- `/boards//cards` +- `https://inspo--board.herokuapp.com/boards/2/cards` -and get this response: -`200 ok` -```json -{ - "board": { - "board_id": 1, - "title": "New name for fancy board", - "owner": "Morty" - } -} -``` +*** +### Get --> Get card by ID +- `/cards/` +- `https://inspo--board.herokuapp.com/cards/9` +*** -### Update Board: No matching Board -As a client, I want to be able to make a `PUT` request to `/boards/2` when there are no matching tasks with this request body: +### DEL --> Delete a card ny ID +- `/cards/` +- `https://inspo--board.herokuapp.com/cards/9` +*** +### PUT --> Edit card by ID +- `/cards/` +- `http://127.0.0.1:5000/cards/8` ```json { - "title": "New title for board", - "owner": "Trish" + "message": "editing it one more time" } ``` -and get this response: -`404 Not Found` -No response body - -### Delete Board: Deleting a board - -### Delete Board: No Matching Board - -*** -Need to decide on what circumstance will we not allow a database creation to begin. -- example --- when no title is provided? --- when no owner is provided? - -### Create a Board: Invalid Board With Missing Data ** - -
- -# Need to go through crud for Cards like I did for board above. - -### Create a Card: valid card - -### Get Cards: Getting saved cards - -### Get Cards: No saved Cards - -### Get One Card: One saved Card - -### Get One Card: No matching Card - -### Update Card: - -### Update Card: No matching Card - -### Delete Card: Deleting a Card - -### Delete Card: No matching Card - -### Create a Card: Invalid Card with missing data - +# Handle Likes +### Put --> Increase Likes Count by 1 +- `/cards//like` +- `https://inspo--board.herokuapp.com/cards/9/like` \ No newline at end of file