diff --git a/app/__init__.py b/app/__init__.py index 1c821436..02b4b0e0 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -17,12 +17,18 @@ def create_app(): app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get( "SQLALCHEMY_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) + from .routes import hello_world_bp + app.register_blueprint(hello_world_bp) + + from .routes import boards_bp + app.register_blueprint(boards_bp) + # Register Blueprints here # from .routes import example_bp # app.register_blueprint(example_bp) diff --git a/app/models.card.py b/app/models.card.py new file mode 100644 index 00000000..e69de29b diff --git a/app/models/__init__.py b/app/models/__init__.py index e69de29b..fcd2caea 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -0,0 +1,26 @@ +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate +from dotenv import load_dotenv +import os + +db = SQLAlchemy() +migrate = Migrate() +load_dotenv() + + +def create_app(test_config=None): + app = Flask(__name__) + + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get( + "SQLALCHEMY_DATABASE_URI") + + db.init_app(app) + migrate.init_app(app, db) + + # from app.models.planet import Planet + # from .routes import planets_bp + # app.register_blueprint(planets_bp) + + return app diff --git a/app/models/board.py b/app/models/board.py index 147eb748..c0a2e9c6 100644 --- a/app/models/board.py +++ b/app/models/board.py @@ -1 +1,8 @@ from app import db + + +class Board(db.Model): + board_id = db.Column(db.Integer, primary_key=True, autoincrement=True) + title = db.Column(db.String) + owner = db.Column(db.String) + cards = db.relationship('Card', backref='cards', lazy=True) diff --git a/app/models/card.py b/app/models/card.py index 147eb748..82e137bd 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, autoincrement=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) diff --git a/app/routes.py b/app/routes.py index 480b8c4b..a89f4db6 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,4 +1,159 @@ from flask import Blueprint, request, jsonify, make_response from app import db +from app.models.board import Board +from app.models.card import Card +import os +from dotenv import load_dotenv +import requests +from slack_sdk.errors import SlackApiError +boards_bp = Blueprint("boards", __name__, url_prefix="/boards") +hello_world_bp = Blueprint("hello_world", __name__) + + +load_dotenv() + +# Basic route to test if the server is running + + +@hello_world_bp.route("/hello-world", methods=["GET"]) +def hello_world(): + my_beautiful_response_body = "Hello, World!" + return my_beautiful_response_body + +# method to post message to slack + + +def post_message_to_slack(text): + SLACK_TOKEN = os.environ.get('SLACKBOT_TOKEN') + slack_path = "https://slack.com/api/chat.postMessage" + query_params = { + 'channel': 'C0286U213J5', + 'text': text + } + headers = {'Authorization': f"Bearer {SLACK_TOKEN}"} + requests.post(slack_path, params=query_params, headers=headers) + +# routes for getting all boards and creating a new board + + +@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, + }) + return jsonify(boards_response) + elif request.method == "POST": + request_body = request.get_json() + title = request_body.get("title") + owner = request_body.get("owner") + new_board = Board(title=request_body["title"], + owner=request_body["owner"]) + db.session.add(new_board) + db.session.commit() + slack_message = f"Some duck just added a new board!" + post_message_to_slack(slack_message) + + return make_response(f"Board {new_board.title} successfully created", 201) + +# routes for getting a specific board, updating a board, and deleting a board + + +@boards_bp.route("/", methods=["GET", "PUT", "DELETE"]) +def handle_board(board_id): + board = Board.query.get_or_404(board_id) + if request.method == "GET": + cards = [] + for card in board.cards: + single_card = { + "message": card.message, + } + cards.append(single_card) + return make_response({ + "id": board.board_id, + "title": board.title, + "owner": board.owner, + "cards": cards + }) + elif request.method == "PUT": + if board == None: + return make_response("Board does not exist", 404) + form_data = request.get_json() + + board.title = form_data["title"] + board.owner = form_data["owner"] + + db.session.commit() + + return make_response(f"Board: {board.title} sucessfully updated.") + + elif request.method == "DELETE": + if board == None: + return make_response("Board does not exist", 404) + db.session.delete(board) + db.session.commit() + return make_response(f"Board: {board.title} sucessfully deleted.") # example_bp = Blueprint('example_bp', __name__) + +# route for getting all cards in a board and making a new card + + +@boards_bp.route("//cards", methods=["POST", "GET"]) +def handle_cards(board_id): + board = Board.query.get(board_id) + + if board is None: + return make_response("", 404) + + if request.method == "GET": + cards = Board.query.get(board_id).cards + cards_response = [] + for card in cards: + cards_response.append({ + "message": card.message, + "likes_count": card.likes_count, + }) + + return make_response( + { + "cards": cards_response + }, 200) + elif request.method == "POST": + request_body = request.get_json() + if 'message' not in request_body: + return {"details": "Invalid data"}, 400 + + new_card = Card(message=request_body["message"], + board_id=board_id) + + db.session.add(new_card) + db.session.commit() + slack_message = f"Some duck just added a new card!" + post_message_to_slack(slack_message) + + return { + "card": { + "id": new_card.card_id, + "message": new_card.message, + "likes_count": new_card.likes_count, + } + }, 201 + +# route for deleting a card + + +@boards_bp.route("//", methods=["DELETE"]) +def handle_card(board_id, card_id): + card = Card.query.get_or_404(card_id) + + db.session.delete(card) + db.session.commit() + + return make_response( + f"Card Message: {card.message} Card ID: {card.card_id} deleted successfully") diff --git a/logfile b/logfile new file mode 100644 index 00000000..3e4a1763 --- /dev/null +++ b/logfile @@ -0,0 +1,36 @@ +2021-07-13 06:07:34.941 PDT [96833] LOG: starting PostgreSQL 13.3 on x86_64-apple-darwin20.4.0, compiled by Apple clang version 12.0.5 (clang-1205.0.22.9), 64-bit +2021-07-13 06:07:34.942 PDT [96833] LOG: listening on IPv4 address "127.0.0.1", port 5432 +2021-07-13 06:07:34.942 PDT [96833] LOG: listening on IPv6 address "::1", port 5432 +2021-07-13 06:07:34.943 PDT [96833] LOG: listening on Unix socket "/tmp/.s.PGSQL.5432" +2021-07-13 06:07:34.947 PDT [96834] LOG: database system was shut down at 2021-07-13 06:07:20 PDT +2021-07-13 06:07:34.949 PDT [96833] LOG: database system is ready to accept connections +2021-07-13 06:07:46.358 PDT [96866] FATAL: role "postgres" does not exist +2021-07-13 06:08:40.481 PDT [96996] FATAL: database "du" does not exist +2021-07-13 06:09:23.143 PDT [97350] FATAL: role "postgres" does not exist +2021-07-13 06:10:40.468 PDT [97924] FATAL: role "postgres" does not exist +2021-07-13 06:13:27.223 PDT [98323] FATAL: role "postgres" does not exist +2021-07-13 06:19:38.338 PDT [310] FATAL: role "postgres" does not exist +2021-07-13 06:23:44.026 PDT [1012] FATAL: role "postgres" does not exist +2021-07-13 06:23:54.376 PDT [1035] FATAL: database "Xtina206" does not exist +2021-07-13 06:28:43.565 PDT [1965] FATAL: role "postgres" does not exist +2021-07-13 06:32:11.170 PDT [2642] FATAL: role "xtina" is not permitted to log in +2021-07-13 06:32:22.481 PDT [2666] FATAL: database "Xtina206" does not exist +2021-07-13 06:33:39.135 PDT [2911] FATAL: role "postgres" does not exist +2021-07-13 06:33:50.071 PDT [2934] FATAL: database "Xtina206" does not exist +2021-07-13 06:33:59.221 PDT [2957] FATAL: database "Xtina206" does not exist +2021-07-13 06:34:16.502 PDT [3001] FATAL: role "inspiration_board_development" does not exist +2021-07-13 06:34:26.178 PDT [3025] FATAL: role "postgres" does not exist +2021-07-13 06:34:35.779 PDT [3049] FATAL: database "Xtina206" does not exist +2021-07-13 06:34:57.264 PDT [3095] FATAL: role "postgres" does not exist +2021-07-13 07:30:28.182 PDT [12165] LOG: unexpected EOF on client connection with an open transaction +2021-07-14 08:49:51.995 PDT [64205] LOG: unexpected EOF on client connection with an open transaction +2021-07-14 08:53:28.067 PDT [64292] LOG: unexpected EOF on client connection with an open transaction +2021-07-14 09:05:12.011 PDT [64978] LOG: unexpected EOF on client connection with an open transaction +2021-07-14 09:06:21.462 PDT [66682] LOG: unexpected EOF on client connection with an open transaction +2021-07-14 09:07:36.303 PDT [66847] LOG: unexpected EOF on client connection with an open transaction +2021-07-14 09:08:03.094 PDT [67041] LOG: unexpected EOF on client connection with an open transaction +2021-07-14 09:09:05.336 PDT [67110] LOG: unexpected EOF on client connection with an open transaction +2021-07-14 09:09:13.806 PDT [67265] LOG: unexpected EOF on client connection with an open transaction +2021-07-14 09:11:00.888 PDT [67474] LOG: unexpected EOF on client connection with an open transaction +2021-07-14 09:14:08.700 PDT [68281] LOG: unexpected EOF on client connection with an open transaction +2021-07-14 09:18:04.828 PDT [68607] LOG: unexpected EOF on client connection with an open transaction 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/0998518428e5_adding_models.py b/migrations/versions/0998518428e5_adding_models.py new file mode 100644 index 00000000..70ad956d --- /dev/null +++ b/migrations/versions/0998518428e5_adding_models.py @@ -0,0 +1,87 @@ +"""adding models + +Revision ID: 0998518428e5 +Revises: +Create Date: 2021-07-12 15:09:07.865089 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '0998518428e5' +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(), autoincrement=True, 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(), autoincrement=True, nullable=False), + sa.Column('message', sa.String(), nullable=True), + sa.Column('likes_count', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('card_id') + ) + # op.drop_table('hotel_guests') + # op.drop_table('flowers') + # op.drop_table('reviews') + # op.drop_table('tags') + # op.drop_table('posts') + # op.drop_table('posts_tags') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + # op.create_table('posts_tags', + # sa.Column('post_id', sa.INTEGER(), autoincrement=False, nullable=False), + # sa.Column('tag_id', sa.INTEGER(), autoincrement=False, nullable=False), + # sa.ForeignKeyConstraint(['post_id'], ['posts.id'], name='posts_tags_post_id_fkey'), + # sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], name='posts_tags_tag_id_fkey'), + # sa.PrimaryKeyConstraint('post_id', 'tag_id', name='posts_tags_pkey') + # ) + # op.create_table('posts', + # sa.Column('id', sa.INTEGER(), autoincrement=False, nullable=False), + # sa.Column('title', sa.VARCHAR(length=64), autoincrement=False, nullable=True), + # sa.Column('body', sa.TEXT(), autoincrement=False, nullable=True), + # sa.PrimaryKeyConstraint('id', name='posts_pkey') + # ) + # op.create_table('tags', + # sa.Column('id', sa.INTEGER(), autoincrement=False, nullable=False), + # sa.Column('tagname', sa.VARCHAR(length=64), autoincrement=False, nullable=True), + # sa.PrimaryKeyConstraint('id', name='tags_pkey') + # ) + # op.create_table('reviews', + # sa.Column('id', sa.INTEGER(), autoincrement=False, nullable=False), + # sa.Column('title', sa.VARCHAR(length=32), autoincrement=False, nullable=True), + # sa.Column('product_id', sa.INTEGER(), autoincrement=False, nullable=True), + # sa.Column('body', sa.TEXT(), autoincrement=False, nullable=True), + # sa.Column('creator_id', sa.INTEGER(), autoincrement=False, nullable=True), + # sa.Column('stars', sa.INTEGER(), autoincrement=False, nullable=True), + # sa.PrimaryKeyConstraint('id', name='reviews_pkey') + # ) + # op.create_table('flowers', + # sa.Column('id', sa.INTEGER(), autoincrement=False, nullable=False), + # sa.Column('name', sa.VARCHAR(length=32), autoincrement=False, nullable=True), + # sa.Column('soil_type', sa.VARCHAR(length=32), autoincrement=False, nullable=True), + # sa.Column('light_level', sa.VARCHAR(length=32), autoincrement=False, nullable=True), + # sa.Column('season', sa.VARCHAR(length=32), autoincrement=False, nullable=True), + # sa.PrimaryKeyConstraint('id', name='flowers_pkey') + # ) + # op.create_table('hotel_guests', + # sa.Column('guest_id', sa.INTEGER(), autoincrement=False, nullable=False), + # sa.Column('guest_name', sa.VARCHAR(length=200), autoincrement=False, nullable=True), + # sa.Column('is_checked_in', sa.BOOLEAN(), autoincrement=False, nullable=True), + # sa.PrimaryKeyConstraint('guest_id', name='hotel_guests_pkey') + # ) + op.drop_table('card') + op.drop_table('board') + # ### end Alembic commands ### diff --git a/migrations/versions/456c575cd4a3_.py b/migrations/versions/456c575cd4a3_.py new file mode 100644 index 00000000..7c0f7030 --- /dev/null +++ b/migrations/versions/456c575cd4a3_.py @@ -0,0 +1,34 @@ +"""empty message + +Revision ID: 456c575cd4a3 +Revises: 0998518428e5 +Create Date: 2021-07-13 11:00:17.480900 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '456c575cd4a3' +down_revision = '0998518428e5' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('board', sa.Column('card_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'board', 'card', ['card_id'], ['card_id']) + op.add_column('card', sa.Column('board_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'card', 'board', ['board_id'], ['board_id']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'card', type_='foreignkey') + op.drop_column('card', 'board_id') + op.drop_constraint(None, 'board', type_='foreignkey') + op.drop_column('board', 'card_id') + # ### end Alembic commands ### diff --git a/migrations/versions/476dbf543b0b_.py b/migrations/versions/476dbf543b0b_.py new file mode 100644 index 00000000..5397bbf3 --- /dev/null +++ b/migrations/versions/476dbf543b0b_.py @@ -0,0 +1,30 @@ +"""empty message + +Revision ID: 476dbf543b0b +Revises: 456c575cd4a3 +Create Date: 2021-07-13 11:02:51.043402 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '476dbf543b0b' +down_revision = '456c575cd4a3' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint('board_card_id_fkey', 'board', type_='foreignkey') + op.drop_column('board', 'card_id') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('board', sa.Column('card_id', sa.INTEGER(), autoincrement=False, nullable=True)) + op.create_foreign_key('board_card_id_fkey', 'board', 'card', ['card_id'], ['card_id']) + # ### end Alembic commands ### diff --git a/requirements.txt b/requirements.txt index 97ca22a7..0eb5bc5b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,6 +27,7 @@ python-dotenv==0.15.0 python-editor==1.0.4 requests==2.25.1 six==1.15.0 +slack-sdk==3.7.0 SQLAlchemy==1.3.23 toml==0.10.2 urllib3==1.26.4 diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 00000000..fb57ccd1 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + +