From 5e736f212f83cf52108b0debd4bab15657079091 Mon Sep 17 00:00:00 2001 From: Rachael Date: Sun, 19 Dec 2021 09:33:14 -0500 Subject: [PATCH 01/16] Test to make sure I can push to github --- app/models/card.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/models/card.py b/app/models/card.py index 147eb748..ca6b61be 100644 --- a/app/models/card.py +++ b/app/models/card.py @@ -1 +1,4 @@ from app import db + + +# THIS IS JUST A TEST TO MAKE SURE I CAN PUSH TO GITHUB. \ No newline at end of file From 4eb067a6f8a7a9fea8040c37a57c66815a243747 Mon Sep 17 00:00:00 2001 From: starseed2021 Date: Sun, 19 Dec 2021 10:05:33 -0500 Subject: [PATCH 02/16] Test --- app/models/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/__init__.py b/app/models/__init__.py index e69de29b..e65a8e52 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -0,0 +1 @@ +# TEST TO PUSH TO GITHUB From a904949d292d7d37f9afda1a8479050a3cc7b2cd Mon Sep 17 00:00:00 2001 From: Rachael Date: Sun, 19 Dec 2021 11:18:30 -0500 Subject: [PATCH 03/16] Adds board and card model and sets up database connection --- app/__init__.py | 15 +++-- app/models/board.py | 8 +++ app/models/card.py | 6 +- migrations/README | 1 + migrations/alembic.ini | 45 +++++++++++++ migrations/env.py | 96 ++++++++++++++++++++++++++++ migrations/script.py.mako | 24 +++++++ migrations/versions/791c7c9986c7_.py | 42 ++++++++++++ 8 files changed, 231 insertions(+), 6 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/791c7c9986c7_.py diff --git a/app/__init__.py b/app/__init__.py index 1c821436..219aa2e6 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -10,15 +10,22 @@ load_dotenv() -def create_app(): +def create_app(test_config=None): app = Flask(__name__) + app.url_map.strict_slashes = False app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False - app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get( - "SQLALCHEMY_DATABASE_URI") + if test_config is None: + app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get( + "SQLALCHEMY_DATABASE_URI") + else: + app.config["TESTING"] = True + 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) diff --git a/app/models/board.py b/app/models/board.py index 147eb748..fde0f74c 100644 --- a/app/models/board.py +++ b/app/models/board.py @@ -1 +1,9 @@ from app import db +from sqlalchemy.orm import backref + +class Board(db.Model): + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + owner_name = db.Column(db.String) + title = db.Column(db.String) + card_id = db.Column(db.Integer, db.ForeignKey("card.id"), nullable=False) + card = db.relationship("Card", backref=backref("cards", cascade="delete")) \ No newline at end of file diff --git a/app/models/card.py b/app/models/card.py index ca6b61be..e69114f8 100644 --- a/app/models/card.py +++ b/app/models/card.py @@ -1,4 +1,6 @@ from app import db - -# THIS IS JUST A TEST TO MAKE SURE I CAN PUSH TO GITHUB. \ No newline at end of file +class Card(db.Model): + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + message = db.Column(db.String) + likes_count = db.Column(db.Integer) \ 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"} diff --git a/migrations/versions/791c7c9986c7_.py b/migrations/versions/791c7c9986c7_.py new file mode 100644 index 00000000..27352d15 --- /dev/null +++ b/migrations/versions/791c7c9986c7_.py @@ -0,0 +1,42 @@ +"""empty message + +Revision ID: 791c7c9986c7 +Revises: +Create Date: 2021-12-19 11:15:47.230923 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '791c7c9986c7' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('card', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('message', sa.String(), nullable=True), + sa.Column('likes_count', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('board', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('owner_name', sa.String(), nullable=True), + sa.Column('title', sa.String(), nullable=True), + sa.Column('card_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['card_id'], ['card.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('board') + op.drop_table('card') + # ### end Alembic commands ### From b62cd7440ab1b5b34cb14c60fb4fe7486b646988 Mon Sep 17 00:00:00 2001 From: starseed2021 Date: Sun, 19 Dec 2021 11:40:36 -0500 Subject: [PATCH 04/16] db migrate and db upgrade; now columns show in database --- app/models/board.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/board.py b/app/models/board.py index fde0f74c..fec8ad12 100644 --- a/app/models/board.py +++ b/app/models/board.py @@ -6,4 +6,4 @@ class Board(db.Model): owner_name = db.Column(db.String) title = db.Column(db.String) card_id = db.Column(db.Integer, db.ForeignKey("card.id"), nullable=False) - card = db.relationship("Card", backref=backref("cards", cascade="delete")) \ No newline at end of file + card = db.relationship("Card", backref=backref("cards", cascade="delete")) From 7faaa96feb839ba50d6f54a398d50cd1bc87335d Mon Sep 17 00:00:00 2001 From: starseed2021 Date: Sun, 19 Dec 2021 11:42:01 -0500 Subject: [PATCH 05/16] adjusted board.py --- app/models/board.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/models/board.py b/app/models/board.py index fec8ad12..7ad6b3e7 100644 --- a/app/models/board.py +++ b/app/models/board.py @@ -7,3 +7,6 @@ class Board(db.Model): title = db.Column(db.String) card_id = db.Column(db.Integer, db.ForeignKey("card.id"), nullable=False) card = db.relationship("Card", backref=backref("cards", cascade="delete")) + + + From e99c2bc8811343ff619df8c9b8a092c1c1c6d4ac Mon Sep 17 00:00:00 2001 From: starseed2021 Date: Sun, 19 Dec 2021 21:17:20 -0500 Subject: [PATCH 06/16] Added routes for Board model; registered blueprint in __init__ file --- app/__init__.py | 4 ++- app/models/board.py | 8 +++++ app/routes.py | 83 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 219aa2e6..8828f97a 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -24,7 +24,7 @@ def create_app(test_config=None): "SQLALCHEMY_TEST_DATABASE_URI") # Import models here for Alembic setup - from app.models.board import Board + from app.models.board import Board from app.models.card import Card db.init_app(app) @@ -33,6 +33,8 @@ def create_app(test_config=None): # Register Blueprints here # from .routes import example_bp # app.register_blueprint(example_bp) + from .routes import boards_bp + app.register_blueprint(boards_bp) CORS(app) return app diff --git a/app/models/board.py b/app/models/board.py index 7ad6b3e7..7f868d91 100644 --- a/app/models/board.py +++ b/app/models/board.py @@ -10,3 +10,11 @@ class Board(db.Model): + # RETURNS RESPONSE BODY + def get_board_response(self): + return { + "id": self.id, + "owner_name": self.owner_name, + "title": self.title, + "card_id": self.card_id + } \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index 480b8c4b..5329e53a 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,4 +1,83 @@ -from flask import Blueprint, request, jsonify, make_response +from flask import Blueprint, request, jsonify from app import db +from app.models.board import Board + + +boards_bp = Blueprint("boards_bp", __name__, url_prefix="/boards") + +# BOARD ROUTES +@boards_bp.route("", methods=["POST", "GET"]) +def handle_boards(): + # POST REQUEST + if request.method == "POST": + board_request_body = request.get_json() + + new_board = Board( + owner_name=board_request_body["owner_name"], + title=board_request_body["title"], + card_id=board_request_body["card"] + ) + + db.session.add(new_board) + db.session.commit() + + new_board_response = new_board.get_board_response() + + return jsonify(new_board_response), 201 + + # GET REQUEST + elif request.method == "GET": + board_title_query = request.args.get("title") + board_name_query = request.args.get("owner_name") + if board_title_query or board_name_query: + boards = Board.query.filter(Board.title.contains(board_title_query)) + boards = Board.query.filter(Board.owner_name.contains(board_name_query)) + else: + boards = Board.query.all() + + board_response = [board.get_board_response() for board in boards] + + if board_response == []: + return jsonify(board_response), 200 + + return jsonify(board_response), 200 + +# GET, PUT, DELETE ONE BOARD AT A TIME +@boards_bp.route("/", methods=["GET", "PUT", "DELETE"]) +def handle_one_board(board_id): + if not board_id.isnumeric(): + return jsonify(None), 400 + + board = Board.query.get(board_id) + + if board is None: + return jsonify({"Message": f"Board {board_id} was not found"}), 404 + + if request.method == "GET": + return jsonify(board.get_board_resonse()), 200 + + elif request.method == "PUT": + board_update_request_body = request.get_json() + + if "owner_name" not in board_update_request_body or "title" not in board_update_request_body: + return jsonify(None), 400 + + board.owner_name=board_update_request_body["owner_name"], + board.title=board_update_request_body["title"], + board.card_id=board_update_request_body["card_id"] + + db.session.commit() + + updated_board_response = board.get_board_response() + + return jsonify(updated_board_response), 200 + + elif request.method == "DELETE": + + db.session.delete(board) + db.session.commit() + + board_delete_response = board.get_board_response() + + return jsonify(board_delete_response), 200 -# example_bp = Blueprint('example_bp', __name__) From 7d413cb91f5464591022d1f576da16e1dc37cb0d Mon Sep 17 00:00:00 2001 From: Rachael Date: Mon, 20 Dec 2021 10:31:31 -0500 Subject: [PATCH 07/16] Adds card routes --- app/__init__.py | 4 ++-- app/models/board.py | 2 +- app/models/card.py | 9 +++++++- app/routes.py | 56 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 4 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 219aa2e6..7ab60b28 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -31,8 +31,8 @@ def create_app(test_config=None): migrate.init_app(app, db) # Register Blueprints here - # from .routes import example_bp - # app.register_blueprint(example_bp) + from app.routes import cards_bp + app.register_blueprint(cards_bp) CORS(app) return app diff --git a/app/models/board.py b/app/models/board.py index fde0f74c..00121357 100644 --- a/app/models/board.py +++ b/app/models/board.py @@ -5,5 +5,5 @@ class Board(db.Model): id = db.Column(db.Integer, primary_key=True, autoincrement=True) owner_name = db.Column(db.String) title = db.Column(db.String) - card_id = db.Column(db.Integer, db.ForeignKey("card.id"), nullable=False) + card_id = db.Column(db.Integer, db.ForeignKey("card.id"), nullable=True) card = db.relationship("Card", backref=backref("cards", cascade="delete")) \ No newline at end of file diff --git a/app/models/card.py b/app/models/card.py index e69114f8..83e631d6 100644 --- a/app/models/card.py +++ b/app/models/card.py @@ -3,4 +3,11 @@ class Card(db.Model): id = db.Column(db.Integer, primary_key=True, autoincrement=True) message = db.Column(db.String) - likes_count = db.Column(db.Integer) \ No newline at end of file + likes_count = db.Column(db.Integer) + + def to_json(self): + return { + "id" : self.id, + "message" : self.message, + "likes_count" : self.likes_count, + } \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index 480b8c4b..2bb64eaa 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,4 +1,60 @@ from flask import Blueprint, request, jsonify, make_response +from app.models.card import Card from app import db # example_bp = Blueprint('example_bp', __name__) +# Note: the url_prefix should maybe be removed later, but I'll keep it for now +# for the sake of being able to test with Postman. +cards_bp = Blueprint("cards", __name__, url_prefix="/cards") + +# --- Card routes --- # + +# Get all cards +# Note: I think don't think we'll actually need this b/c what we'll have instead +# is a route that'll allow us to get all cards BY BOARD. +# i.e. GET - /boards//cards +@cards_bp.route("", methods=["GET"]) +def cards(): + cards = Card.query.all() + cards_response = [card.to_json() for card in cards] + return jsonify(cards_response), 200 + +# Add a card +# Note: Again, I think this should actually be a route undder board since we'd +# only had a card to a PARTICULAR BOARD. +# i.e. POST - /boards//cards +@cards_bp.route("", methods=["POST"]) +def add_card(): + request_data = request.get_json() + card = Card( + message = request_data['message'], + likes_count = 0 + ) + db.session.add(card) + db.session.commit() + + return jsonify(card.to_json()), 201 + +# Update a card's likes_count +# ######## FINISH THIS ONE ######### +# @cards_bp.route("//add_like", methods=["PUT"]) +# def increment_card_likes_count(card_id): +# card = Card.query.get(card_id) + +# # add one to the count for this card +# # do here + +# # commit it to DB +# db.session.commit() + + return jsonify(card.to_json()),200 + +# Delete a card +@cards_bp.route("/", methods=["DELETE"]) +def increment_card_likes_count(card_id): + card = Card.query.get(card_id) + + db.session.delete(card) + db.session.commit() + + return f"Card {card_id} has been deleted.", 200 \ No newline at end of file From 8cb964d9ddbd322c24d87356657c597f5c6fbde4 Mon Sep 17 00:00:00 2001 From: starseed2021 Date: Mon, 20 Dec 2021 10:32:27 -0500 Subject: [PATCH 08/16] updated routes --- app/models/board.py | 4 ++-- app/routes.py | 4 ++-- migrations/versions/8b5b93ec9a0f_.py | 32 ++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 migrations/versions/8b5b93ec9a0f_.py diff --git a/app/models/board.py b/app/models/board.py index 7f868d91..a1f319fc 100644 --- a/app/models/board.py +++ b/app/models/board.py @@ -5,7 +5,7 @@ class Board(db.Model): id = db.Column(db.Integer, primary_key=True, autoincrement=True) owner_name = db.Column(db.String) title = db.Column(db.String) - card_id = db.Column(db.Integer, db.ForeignKey("card.id"), nullable=False) + card_id = db.Column(db.Integer, db.ForeignKey("card.id"), nullable=True) card = db.relationship("Card", backref=backref("cards", cascade="delete")) @@ -16,5 +16,5 @@ def get_board_response(self): "id": self.id, "owner_name": self.owner_name, "title": self.title, - "card_id": self.card_id + # "card_id": self.card_id } \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index 5329e53a..001d5e62 100644 --- a/app/routes.py +++ b/app/routes.py @@ -15,7 +15,7 @@ def handle_boards(): new_board = Board( owner_name=board_request_body["owner_name"], title=board_request_body["title"], - card_id=board_request_body["card"] + # card_id=board_request_body["card"] ) db.session.add(new_board) @@ -64,7 +64,7 @@ def handle_one_board(board_id): board.owner_name=board_update_request_body["owner_name"], board.title=board_update_request_body["title"], - board.card_id=board_update_request_body["card_id"] + # board.card_id=board_update_request_body["card_id"] db.session.commit() diff --git a/migrations/versions/8b5b93ec9a0f_.py b/migrations/versions/8b5b93ec9a0f_.py new file mode 100644 index 00000000..55a16a90 --- /dev/null +++ b/migrations/versions/8b5b93ec9a0f_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: 8b5b93ec9a0f +Revises: 791c7c9986c7 +Create Date: 2021-12-20 10:01:17.796577 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '8b5b93ec9a0f' +down_revision = '791c7c9986c7' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('board', 'card_id', + existing_type=sa.INTEGER(), + nullable=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('board', 'card_id', + existing_type=sa.INTEGER(), + nullable=False) + # ### end Alembic commands ### From 5cde94fabf4b20eac6df01c4b1c6a7d7e8f73dc6 Mon Sep 17 00:00:00 2001 From: Rachael Date: Mon, 20 Dec 2021 10:54:46 -0500 Subject: [PATCH 09/16] Updates models; updates routes --- app/models/board.py | 2 - app/models/card.py | 3 ++ app/routes.py | 47 ++++++++----------- .../{791c7c9986c7_.py => 05a0bb191fcf_.py} | 24 +++++----- migrations/versions/8b5b93ec9a0f_.py | 32 ------------- 5 files changed, 35 insertions(+), 73 deletions(-) rename migrations/versions/{791c7c9986c7_.py => 05a0bb191fcf_.py} (82%) delete mode 100644 migrations/versions/8b5b93ec9a0f_.py diff --git a/app/models/board.py b/app/models/board.py index 80ddca60..2e0a8096 100644 --- a/app/models/board.py +++ b/app/models/board.py @@ -5,8 +5,6 @@ class Board(db.Model): id = db.Column(db.Integer, primary_key=True, autoincrement=True) owner_name = db.Column(db.String) title = db.Column(db.String) - card_id = db.Column(db.Integer, db.ForeignKey("card.id"), nullable=True) - card = db.relationship("Card", backref=backref("cards", cascade="delete")) diff --git a/app/models/card.py b/app/models/card.py index 83e631d6..427b5bae 100644 --- a/app/models/card.py +++ b/app/models/card.py @@ -1,9 +1,12 @@ from app import db +from sqlalchemy.orm import backref class Card(db.Model): id = db.Column(db.Integer, primary_key=True, autoincrement=True) message = db.Column(db.String) likes_count = db.Column(db.Integer) + board_id = db.Column(db.Integer, db.ForeignKey("board.id"), nullable=False) + boards = db.relationship("Board", backref=backref("boards", cascade="delete")) def to_json(self): return { diff --git a/app/routes.py b/app/routes.py index 7ef938d2..259940d2 100644 --- a/app/routes.py +++ b/app/routes.py @@ -5,6 +5,7 @@ boards_bp = Blueprint("boards_bp", __name__, url_prefix="/boards") +cards_bp = Blueprint("cards", __name__, url_prefix="/cards") # BOARD ROUTES @boards_bp.route("", methods=["POST", "GET"]) @@ -82,39 +83,31 @@ def handle_one_board(board_id): return jsonify(board_delete_response), 200 -# example_bp = Blueprint('example_bp', __name__) -# Note: the url_prefix should maybe be removed later, but I'll keep it for now -# for the sake of being able to test with Postman. -cards_bp = Blueprint("cards", __name__, url_prefix="/cards") - # --- Card routes --- # -# Get all cards -# Note: I think don't think we'll actually need this b/c what we'll have instead -# is a route that'll allow us to get all cards BY BOARD. -# i.e. GET - /boards//cards -@cards_bp.route("", methods=["GET"]) -def cards(): - cards = Card.query.all() +# Get all cards by board id +# FINISH THIS ONE AND TEST IT! +@boards_bp.route("//cards", methods=["GET"]) +def cards_by_board(board_id): + cards = Card.query.get(board_id) cards_response = [card.to_json() for card in cards] return jsonify(cards_response), 200 -# Add a card -# Note: Again, I think this should actually be a route undder board since we'd -# only had a card to a PARTICULAR BOARD. -# i.e. POST - /boards//cards -@cards_bp.route("", methods=["POST"]) +# Add a card to a particular board (will be based on front-end event!) +@boards_bp.route("//cards", methods=["POST"]) def add_card(): - request_data = request.get_json() - card = Card( - message = request_data['message'], - likes_count = 0 - ) - db.session.add(card) - db.session.commit() - - return jsonify(card.to_json()), 201 - + pass + # request_data = request.get_json() + # card = Card( + # message = request_data['message'], + # likes_count = 0 + # ) + # db.session.add(card) + # db.session.commit() + + # return jsonify(card.to_json()), 201 + +# CARDS ROUTES # Update a card's likes_count # ######## FINISH THIS ONE ######### # @cards_bp.route("//add_like", methods=["PUT"]) diff --git a/migrations/versions/791c7c9986c7_.py b/migrations/versions/05a0bb191fcf_.py similarity index 82% rename from migrations/versions/791c7c9986c7_.py rename to migrations/versions/05a0bb191fcf_.py index 27352d15..902b9e67 100644 --- a/migrations/versions/791c7c9986c7_.py +++ b/migrations/versions/05a0bb191fcf_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: 791c7c9986c7 +Revision ID: 05a0bb191fcf Revises: -Create Date: 2021-12-19 11:15:47.230923 +Create Date: 2021-12-20 10:52:58.117771 """ from alembic import op @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. -revision = '791c7c9986c7' +revision = '05a0bb191fcf' down_revision = None branch_labels = None depends_on = None @@ -18,18 +18,18 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('card', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('message', sa.String(), nullable=True), - sa.Column('likes_count', sa.Integer(), nullable=True), - sa.PrimaryKeyConstraint('id') - ) op.create_table('board', sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('owner_name', sa.String(), nullable=True), sa.Column('title', sa.String(), nullable=True), - sa.Column('card_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['card_id'], ['card.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('card', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('message', sa.String(), nullable=True), + sa.Column('likes_count', sa.Integer(), nullable=True), + sa.Column('board_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['board_id'], ['board.id'], ), sa.PrimaryKeyConstraint('id') ) # ### end Alembic commands ### @@ -37,6 +37,6 @@ def upgrade(): def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('board') op.drop_table('card') + op.drop_table('board') # ### end Alembic commands ### diff --git a/migrations/versions/8b5b93ec9a0f_.py b/migrations/versions/8b5b93ec9a0f_.py deleted file mode 100644 index 55a16a90..00000000 --- a/migrations/versions/8b5b93ec9a0f_.py +++ /dev/null @@ -1,32 +0,0 @@ -"""empty message - -Revision ID: 8b5b93ec9a0f -Revises: 791c7c9986c7 -Create Date: 2021-12-20 10:01:17.796577 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '8b5b93ec9a0f' -down_revision = '791c7c9986c7' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('board', 'card_id', - existing_type=sa.INTEGER(), - nullable=True) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('board', 'card_id', - existing_type=sa.INTEGER(), - nullable=False) - # ### end Alembic commands ### From 0892cdb3addbd6b0a3162965490c8a08fa47bf34 Mon Sep 17 00:00:00 2001 From: starseed2021 Date: Mon, 20 Dec 2021 10:59:11 -0500 Subject: [PATCH 10/16] updated database --- migrations/versions/{05a0bb191fcf_.py => e98b2a07a53b_.py} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename migrations/versions/{05a0bb191fcf_.py => e98b2a07a53b_.py} (92%) diff --git a/migrations/versions/05a0bb191fcf_.py b/migrations/versions/e98b2a07a53b_.py similarity index 92% rename from migrations/versions/05a0bb191fcf_.py rename to migrations/versions/e98b2a07a53b_.py index 902b9e67..747ec13b 100644 --- a/migrations/versions/05a0bb191fcf_.py +++ b/migrations/versions/e98b2a07a53b_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: 05a0bb191fcf +Revision ID: e98b2a07a53b Revises: -Create Date: 2021-12-20 10:52:58.117771 +Create Date: 2021-12-20 10:57:32.635579 """ from alembic import op @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. -revision = '05a0bb191fcf' +revision = 'e98b2a07a53b' down_revision = None branch_labels = None depends_on = None From c3c19cb6b8fdf19869707807ef79456df29cb92e Mon Sep 17 00:00:00 2001 From: Rachael Date: Mon, 20 Dec 2021 12:06:19 -0500 Subject: [PATCH 11/16] Adds routes for cards; all routes are now functional --- app/models/board.py | 1 - app/models/card.py | 1 + app/routes.py | 80 +++++++++++++++++++++++++-------------------- 3 files changed, 46 insertions(+), 36 deletions(-) diff --git a/app/models/board.py b/app/models/board.py index 2e0a8096..960f869c 100644 --- a/app/models/board.py +++ b/app/models/board.py @@ -14,5 +14,4 @@ def get_board_response(self): "id": self.id, "owner_name": self.owner_name, "title": self.title, - # "card_id": self.card_id } diff --git a/app/models/card.py b/app/models/card.py index 427b5bae..08bd31a6 100644 --- a/app/models/card.py +++ b/app/models/card.py @@ -13,4 +13,5 @@ def to_json(self): "id" : self.id, "message" : self.message, "likes_count" : self.likes_count, + "board_id" : self.board_id } \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index 259940d2..a8bd693a 100644 --- a/app/routes.py +++ b/app/routes.py @@ -17,7 +17,6 @@ def handle_boards(): new_board = Board( owner_name=board_request_body["owner_name"], title=board_request_body["title"], - # card_id=board_request_body["card"] ) db.session.add(new_board) @@ -83,51 +82,62 @@ def handle_one_board(board_id): return jsonify(board_delete_response), 200 -# --- Card routes --- # - # Get all cards by board id -# FINISH THIS ONE AND TEST IT! @boards_bp.route("//cards", methods=["GET"]) def cards_by_board(board_id): - cards = Card.query.get(board_id) + cards = Card.query.filter_by(board_id=board_id) cards_response = [card.to_json() for card in cards] return jsonify(cards_response), 200 -# Add a card to a particular board (will be based on front-end event!) +# Add a card to a particular board @boards_bp.route("//cards", methods=["POST"]) -def add_card(): - pass - # request_data = request.get_json() - # card = Card( - # message = request_data['message'], - # likes_count = 0 - # ) - # db.session.add(card) - # db.session.commit() - - # return jsonify(card.to_json()), 201 - -# CARDS ROUTES -# Update a card's likes_count -# ######## FINISH THIS ONE ######### -# @cards_bp.route("//add_like", methods=["PUT"]) -# def increment_card_likes_count(card_id): -# card = Card.query.get(card_id) - -# # add one to the count for this card -# # do here - -# # commit it to DB -# db.session.commit() - - return jsonify(card.to_json()),200 - -# Delete a card +def add_card(board_id): + request_data = request.get_json() + card = Card( + message = request_data['message'], + likes_count = 0, + board_id = board_id + ) + db.session.add(card) + db.session.commit() + + return card.to_json(), 200 + +# CARD ROUTES + +# Get all cards +# Note: this is only for testing purposes in Postman; the FE will never call it. +@cards_bp.route("", methods=["GET"]) +def cards(): + cards = Card.query.all() + cards_response = [card.to_json() for card in cards] + return jsonify(cards_response), 200 + +# Get one card +# Note: this is also just for testing purposes in Postman; the FE will never call it. +@cards_bp.route("/", methods=["GET"]) +def card(card_id): + card = Card.query.get(card_id) + return card.to_json(), 200 + +# Delete one card (some FE event handler should use this) @cards_bp.route("/", methods=["DELETE"]) -def increment_card_likes_count(card_id): +def delete_card(card_id): card = Card.query.get(card_id) db.session.delete(card) db.session.commit() return f"Card {card_id} has been deleted.", 200 + +# Update a card's likes_count (some FE event handler should use this) +@cards_bp.route("//add_like", methods=["PUT"]) +def increment_likes(card_id): + card = Card.query.get(card_id) + if card is None: + return make_response("", 400) + + card.likes_count += 1 + db.session.commit() + + return card.to_json(), 200 \ No newline at end of file From d23cfebcb597b37c9053f66ba7a4a28ad76b18bc Mon Sep 17 00:00:00 2001 From: Rachael Date: Mon, 20 Dec 2021 12:09:20 -0500 Subject: [PATCH 12/16] Adds comments to routes for front-end team --- app/routes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/routes.py b/app/routes.py index a8bd693a..15e5276a 100644 --- a/app/routes.py +++ b/app/routes.py @@ -82,14 +82,14 @@ def handle_one_board(board_id): return jsonify(board_delete_response), 200 -# Get all cards by board id +# Get all cards by board id (some FE event handler should use this) @boards_bp.route("//cards", methods=["GET"]) def cards_by_board(board_id): cards = Card.query.filter_by(board_id=board_id) cards_response = [card.to_json() for card in cards] return jsonify(cards_response), 200 -# Add a card to a particular board +# Add a card to a particular board (some FE event handler should use this) @boards_bp.route("//cards", methods=["POST"]) def add_card(board_id): request_data = request.get_json() From d94154c1eab0ff5bca547896e63265ecf28d2a72 Mon Sep 17 00:00:00 2001 From: starseed2021 Date: Mon, 20 Dec 2021 17:15:44 -0500 Subject: [PATCH 13/16] removed card_id from board routes --- app/routes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/routes.py b/app/routes.py index 15e5276a..f366fe23 100644 --- a/app/routes.py +++ b/app/routes.py @@ -64,8 +64,7 @@ def handle_one_board(board_id): return jsonify(None), 400 board.owner_name=board_update_request_body["owner_name"], - board.title=board_update_request_body["title"], - # board.card_id=board_update_request_body["card_id"] + board.title=board_update_request_body["title"] db.session.commit() From e6be6614dd755cbd0ba3d1af96990f7727982c55 Mon Sep 17 00:00:00 2001 From: Rachael Date: Tue, 21 Dec 2021 08:29:25 -0500 Subject: [PATCH 14/16] Fixes add_card POST route to ensure only a message between 1-40 characters is accepted --- app/routes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/routes.py b/app/routes.py index f366fe23..10ad4d01 100644 --- a/app/routes.py +++ b/app/routes.py @@ -92,6 +92,9 @@ def cards_by_board(board_id): @boards_bp.route("//cards", methods=["POST"]) def add_card(board_id): request_data = request.get_json() + if len(request_data['message']) > 40 or len(request_data['message']) == 0: + return make_response("Your message must be between 1 and 40 characters.", 400) + card = Card( message = request_data['message'], likes_count = 0, From 859cdcb5996dc69cbdac415ee17f6f7b4fda246c Mon Sep 17 00:00:00 2001 From: Rachael Date: Tue, 21 Dec 2021 10:17:06 -0500 Subject: [PATCH 15/16] Adds 10 tests to test routes --- app/models/board.py | 2 - app/routes.py | 8 +- tests/conftest.py | 40 ++++++++++ tests/test_routes.py | 183 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 229 insertions(+), 4 deletions(-) diff --git a/app/models/board.py b/app/models/board.py index 960f869c..9d246c67 100644 --- a/app/models/board.py +++ b/app/models/board.py @@ -6,8 +6,6 @@ class Board(db.Model): owner_name = db.Column(db.String) title = db.Column(db.String) - - # RETURNS RESPONSE BODY def get_board_response(self): return { diff --git a/app/routes.py b/app/routes.py index 10ad4d01..1cd1ed0f 100644 --- a/app/routes.py +++ b/app/routes.py @@ -55,7 +55,7 @@ def handle_one_board(board_id): return jsonify({"Message": f"Board {board_id} was not found"}), 404 if request.method == "GET": - return jsonify(board.get_board_resonse()), 200 + return jsonify(board.get_board_response()), 200 elif request.method == "PUT": board_update_request_body = request.get_json() @@ -120,17 +120,21 @@ def cards(): @cards_bp.route("/", methods=["GET"]) def card(card_id): card = Card.query.get(card_id) + if not card: + return make_response("", 404) return card.to_json(), 200 # Delete one card (some FE event handler should use this) @cards_bp.route("/", methods=["DELETE"]) def delete_card(card_id): card = Card.query.get(card_id) + if not card: + return make_response("", 404) db.session.delete(card) db.session.commit() - return f"Card {card_id} has been deleted.", 200 + return card.to_json(), 200 # Update a card's likes_count (some FE event handler should use this) @cards_bp.route("//add_like", methods=["PUT"]) diff --git a/tests/conftest.py b/tests/conftest.py index 2b7296d5..1d0e09ab 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,8 @@ import pytest from app import create_app from app import db +from app.models.board import Board +from app.models.card import Card @pytest.fixture @@ -20,3 +22,41 @@ def app(): @pytest.fixture def client(app): return app.test_client() + + +# This fixture gets called in every test that references "one_board". +# This fixture creates a board and saves it in the database. +@pytest.fixture +def one_board(app): + new_board = Board( + owner_name="Ansel", title="Sloth board") + db.session.add(new_board) + db.session.commit() + + +# This fixture gets called in every test that references "three_boards". +# This fixture creates three boardes and saves them in the database. +@pytest.fixture +def three_boards(app): + db.session.add_all([ + Board( + owner_name="Piglet", title="Nice notes 🌷"), + Board( + owner_name="Pooh", title="Best spots to find honey"), + Board( + owner_name="Eeyore", title="Sadboi board 😭") + ]) + db.session.commit() + + +# This fixture gets called in every test that references "one_card". +# This fixture creates a card and saves it in the database. +@pytest.fixture +def one_card(app, one_board): + new_card = Card( + message="This is our last project before Capstone!", + likes_count=0, + board_id=1 + ) + db.session.add(new_card) + db.session.commit() diff --git a/tests/test_routes.py b/tests/test_routes.py index e69de29b..31c10203 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -0,0 +1,183 @@ +from app.models.board import Board +from app.models.card import Card + +def test_get_board_no_saved_boards(client): + # Act + response = client.get("/boards") + response_body = response.get_json() + + # Assert + assert response.status_code == 200 + assert response_body == [] + + +def test_get_boards_one_saved_board(client, one_board): + # Act + response = client.get("/boards/1") + response_body = response.get_json() + + # Assert + assert response.status_code == 200 + assert response_body == { + "id": 1, + "owner_name": "Ansel", + "title": "Sloth board" + } + + +def test_get_boards_three_saved_boards(client, three_boards): + # Act + response = client.get("/boards") + response_body = response.get_json() + + # Assert + assert response.status_code == 200 + assert len(response_body) == 3 + assert response_body[0]['id'] == 1 + assert response_body[1]['id'] == 2 + assert response_body[2]['id'] == 3 + assert response_body[0] == { + 'id': 1, + 'owner_name': 'Piglet', + 'title': 'Nice notes 🌷' + } + + +def test_create_board_no_saved_cards(client): + # Act + response = client.post("/boards", json={ + "owner_name": "Example owner", + "title": "Example title" + }) + response_body = response.get_json() + + # Assert + assert response.status_code == 201 + new_board = Board.query.get(1) + assert new_board + assert new_board.owner_name == "Example owner" + assert new_board.title == "Example title" + + +def test_delete_board(client, three_boards): + # Act + response = client.delete("/boards/1") + response_body = response.get_json() + + # Assert + assert response.status_code == 200 + assert response_body == { + 'id': 1, + 'owner_name': 'Piglet', + 'title': 'Nice notes 🌷' + } + assert len(Board.query.all()) == 2 + + +def test_get_cards_no_saved_cards(client): + # Act + response = client.get("/boards/1/cards") + response_body = response.get_json() + + # Assert + assert response.status_code == 200 + assert response_body == [] + + +def test_add_one_card_and_get_one_card(client, one_board): + # Act - add one card + card_response = client.post("/boards/1/cards", json={ + "board_id" : "1", + "message" : "Example message--hello world!", + "likes_count" : "0" + }) + + # Act - get one card + board_response = client.get("/boards/1/cards") + board_response_body = board_response.get_json() + + # Assert + assert card_response.status_code == 200 + assert board_response.status_code == 200 + assert len(board_response_body) == 1 + assert board_response_body == [{ + 'board_id': 1, + 'id': 1, + 'likes_count': 0, + 'message': 'Example message--hello world!' + }] + + +def test_add_two_cards_and_get_two_cards(client, one_board): + # Act - add two cards + card_response = client.post("/boards/1/cards", json={ + "board_id" : "1", + "message" : "A message", + "likes_count" : "0" + }) + + second_card_response = client.post("/boards/1/cards", json={ + "board_id" : "1", + "message" : "Another message", + "likes_count" : "0" + }) + + # Act - get two cards + board_response = client.get("/boards/1/cards") + board_response_body = board_response.get_json() + + # Assert + assert card_response.status_code == 200 + assert second_card_response.status_code == 200 + assert board_response.status_code == 200 + assert len(board_response_body) == 2 + assert board_response_body == [ + { + 'board_id': 1, + 'id': 1, + 'likes_count': 0, + 'message': 'A message' + }, + { + 'board_id': 1, + 'id': 2, + 'likes_count': 0, + 'message': 'Another message' + } + ] + + +def test_delete_card(client, one_card): + # Act + response = client.delete("/cards/1") + response_body = response.get_json() + + # Assert + assert response.status_code == 200 + assert response_body == { + 'board_id': 1, + 'id': 1, + 'likes_count': 0, + 'message': 'This is our last project before Capstone!' + } + + # Check that the card was deleted + response = client.get("/cards/1") + assert response.status_code == 404 + + +def test_delete_card_not_found(client): + # Act + response = client.delete("/cards/1") + response_body = response.get_json() + + # Assert + assert response.status_code == 404 + assert response_body == None + assert Card.query.all() == [] + + +# Need to be done by Rachael or Tiffany at some point +def test_create_card_must_contain_message(client): + pass + From 0a970e7624437f341a89f97f62792cf80c6f45d9 Mon Sep 17 00:00:00 2001 From: Rachael Date: Tue, 21 Dec 2021 10:33:40 -0500 Subject: [PATCH 16/16] Updates test_create_board --- tests/test_routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_routes.py b/tests/test_routes.py index 31c10203..fdaeb782 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -43,7 +43,7 @@ def test_get_boards_three_saved_boards(client, three_boards): } -def test_create_board_no_saved_cards(client): +def test_create_board(client): # Act response = client.post("/boards", json={ "owner_name": "Example owner",