Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pine: Inspirandwich Board (Tirhas, Melinda, Roslyn, Mac) #8

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c43ba98
initial setup
roslynm Dec 22, 2021
7a525bb
psuedo code for routes.py
roslynm Dec 30, 2021
4f9a3ca
Adds to_dict function to board model and starts get endpoint for boards
mac6551 Dec 30, 2021
f10a938
Adds endpoint to retrieve all boards
mac6551 Dec 30, 2021
0bd6ff9
Adds endpoint to retrieve one board by ID.
mac6551 Dec 30, 2021
2d36768
Creates relationship between card and board.
mac6551 Dec 30, 2021
8ea2455
Fixes helper_function import and other mindor typos
mac6551 Dec 30, 2021
f7628d9
Updates get_one_board to get return all cards associated with board
mac6551 Dec 30, 2021
c4d6d96
Adds helper functoin, valid_input, to validate input for PUT requests.
mac6551 Dec 30, 2021
8bbc658
Adds create_new_board endpoint.
mac6551 Dec 30, 2021
2939693
Adds to_dict function to card model
mac6551 Dec 30, 2021
12cc91d
Adds doc string to delete boards route
mac6551 Dec 30, 2021
fb9a89f
Adds @ to beggining of delete and post requests
mac6551 Dec 31, 2021
91fefdf
Adds endpoints for adding and updating a card
mac6551 Jan 1, 2022
3f320a4
Adds endpoint for deleting one card.
mac6551 Jan 1, 2022
794353e
Adds docstrings to card endpoints.
mac6551 Jan 1, 2022
f2fbd85
Changes board_bp url_prefix to empty
mac6551 Jan 3, 2022
de13f71
Changes board_bp back to have a boards url_prefix
mac6551 Jan 3, 2022
f398265
adds slash before boards
mac6551 Jan 3, 2022
b5e387b
edits end point response messages to be more concise
mac6551 Jan 4, 2022
3907621
Edits update card to add one to likes_count
mac6551 Jan 4, 2022
5d5d0ae
changes update card likes logic
mac6551 Jan 4, 2022
74b3283
changes required input for card
mac6551 Jan 5, 2022
baa1d81
changes default board id
mac6551 Jan 5, 2022
aa2d663
cleans up code
mac6551 Jan 6, 2022
7dbf453
stuff
mac6551 Jan 6, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os
from flask_cors import CORS


db = SQLAlchemy()
migrate = Migrate()
load_dotenv()
Expand All @@ -18,14 +19,21 @@ def create_app():
"SQLALCHEMY_DATABASE_URI")

# Import models here for Alembic setup
from app.models.board import Board
from app.models.card import Card
# from app.models.ExampleModel import ExampleModel

db.init_app(app)
migrate.init_app(app, db)

# Register Blueprints here
# from .routes import example_bp
# app.register_blueprint(example_bp)
# ************************ #
from .routes import board_bp
app.register_blueprint(board_bp)
# from .routes import card_bp
# app.register_blueprint(card_bp)
# ************************ #


CORS(app)
return app
45 changes: 45 additions & 0 deletions app/helper_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from app import db
from flask import make_response
from flask_sqlalchemy import abort
from app.models.board import Board
from app.models.card import Card

def valid_id(model, id):
"""Parameters: Model type and id of model.
Returns instance of model with matching ID.
Returns 404 and custom message if model with given ID does not exist."""
try:
id = int(id)
except:
abort(400, {"error": "invalid id"})

model = model.query.get(id)

if not model:
abort(make_response({"message": "not found"}, 404))

return model

def valid_input(request_body, model):
"""Input: JSON version of request and type of model.
Checks request body for required input per model.
Returns 400 with needed input if missing."""
if model == Board:
required_input = ["title", "owner"]

if model == Card:
required_input = ["message"]

for input in required_input:
if input not in request_body:
abort(make_response({"details": f"Request body must include {input}."}, 400))

def add_to_database(model_instance):
"""Adds valid instance of a model to database"""
db.session.add(model_instance)
db.session.commit()

def delete_from_database(model_instance):
"""Deletes valid instance of a model from database"""
db.session.delete(model_instance)
db.session.commit()
15 changes: 15 additions & 0 deletions app/models/board.py
Original file line number Diff line number Diff line change
@@ -1 +1,16 @@
from app import db

class Board(db.Model):

board_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
title = db.Column(db.String, nullable=False)
owner = db.Column(db.String, nullable=False)
cards = db.relationship('Card', backref='board', lazy=True)

def to_dict(self):
"""Returns model info as a dictionary."""
return {
"id": self.board_id,
"title": self.title,
"owner": self.owner
}
17 changes: 17 additions & 0 deletions app/models/card.py
Original file line number Diff line number Diff line change
@@ -1 +1,18 @@
from sqlalchemy.orm import backref
from app import db

class Card(db.Model):

card_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
message = db.Column(db.String, nullable=False)
likes_count = db.Column(db.Integer, default=0)
board_id = db.Column(db.Integer, db.ForeignKey("board.board_id"), nullable=False)

def to_dict(self):
"""Returns model info as a dictionary."""
return {
"id": self.card_id,
"message": self.message,
"likes_count": self.likes_count,
"board_id": self.board_id
}
109 changes: 107 additions & 2 deletions app/routes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,109 @@
from flask import Blueprint, request, jsonify, make_response
from flask import Blueprint, request, jsonify
from app import db
from app.models.board import Board
from app.models.card import Card
from .helper_functions import *

board_bp = Blueprint("boards", __name__, url_prefix="/boards")

####---------------------------------------------------####
####----------------- BOARD ENDPOINTS -----------------####
####---------------------------------------------------####

@board_bp.route("", methods = ["GET"])
def get_boards():
"""Returns list of dictionaires with board information"""
boards = Board.query.all()
boards_response = [board.to_dict() for board in boards]

return jsonify(boards_response), 200

@board_bp.route("/<board_id>", methods = ["GET"])
def get_cards_from_one_board(board_id):
"""Input: ID of board.
Returns list of dictionaries with card info for
cards associated with this board or
404 if it doesn't exist."""
board = valid_id(Board, board_id)
cards = board.cards
cards_response = [card.to_dict() for card in cards]

return jsonify(cards_response), 200

@board_bp.route("", methods = ["POST"])
def create_new_board():
"""Adds new board to db and returns new board ID and 201 code or
400 error if request body is missing title or owner."""
request_body = request.get_json()

valid_input(request_body, Board)

new_board = Board(title = request_body["title"],
owner = request_body["owner"])

add_to_database(new_board)

return {"id": new_board.board_id}, 201

@board_bp.route("", methods = ["DELETE"])
def delete_all_boards_but_default():
"""Deletes all boards and cards associated with boards from database,
except for the default board."""
boards = Board.query.filter(Board.board_id!=2).all()

for board in boards:
if board.cards:
for card in board.cards:
delete_from_database(card)

delete_from_database(board)

return {"id": 2}, 200

####---------------------------------------------------####
####------------------ CARD ENDPOINTS -----------------####
####---------------------------------------------------####
@board_bp.route("/<board_id>/<card_id>", methods=["PUT"])
def update_card_likes(board_id, card_id):
"""Input: Board ID and Card ID
Updates card in database and returns success message with card ID.
Returns 400 if invalid ID or 404 if card or board don't exist."""
card = valid_id(Card, card_id)
board = valid_id(Board, board_id)

card.likes_count += 1
db.session.commit()

return {"likes_count": card.likes_count}, 200

@board_bp.route("/<board_id>/<card_id>", methods=["DELETE"])
def delete_card(board_id, card_id):
"""Input: Board ID and Card ID
Deletes card from database and returns success message.
Returns 400 if ID isn't valid or 404 if card or board don't exist.
"""
card = valid_id(Card, card_id)
board = valid_id(Board, board_id)

delete_from_database(card)

return {"id": card.card_id}, 200

@board_bp.route("/<board_id>/cards", methods = ["POST"])
def create_new_card(board_id):
"""Input: Board ID
Adds card to database and returns card ID with success message.
Returns 400 if invalid ID or 404 if board doesn't exist."""
request_body = request.get_json()
valid_id(Board, board_id)
valid_input(request_body, Card)

new_card = Card(message = request_body["message"],
likes_count = 0,
board_id = board_id)

add_to_database(new_card)

return {"id": new_card.card_id}, 201


# example_bp = Blueprint('example_bp', __name__)
1 change: 1 addition & 0 deletions migrations/README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Generic single-database configuration.
45 changes: 45 additions & 0 deletions migrations/alembic.ini
Original file line number Diff line number Diff line change
@@ -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
96 changes: 96 additions & 0 deletions migrations/env.py
Original file line number Diff line number Diff line change
@@ -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()
24 changes: 24 additions & 0 deletions migrations/script.py.mako
Original file line number Diff line number Diff line change
@@ -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"}
Loading