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

Tigers - "I'm Horrible with Names" Group #18

Open
wants to merge 38 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f422954
adds board and card model files and flask migration files
neidacreates Jan 3, 2023
2ef8e76
CRUD for board
Tanil-Cadette Jan 4, 2023
e3592a3
board
Tanil-Cadette Jan 4, 2023
2062c25
testing both models
Tanil-Cadette Jan 4, 2023
d2d03c3
relationship successful
Tanil-Cadette Jan 4, 2023
2e1f24c
all routes and models working
Tanil-Cadette Jan 5, 2023
5cb3362
updates to routes
Tanil-Cadette Jan 5, 2023
b71e865
format board_routes.py
allcomputersarebad Jan 5, 2023
0236610
fix board creation return
allcomputersarebad Jan 6, 2023
7c15c09
Merge pull request #1 from Tanil-Cadette/fix-board-creation-route
allcomputersarebad Jan 6, 2023
9f6108c
modifies create_app to handle testing
neidacreates Jan 6, 2023
ba9a8d7
use board_id in card create
allcomputersarebad Jan 6, 2023
a489697
Merge pull request #2 from Tanil-Cadette/fix-card-create-method
allcomputersarebad Jan 6, 2023
a327dc0
adds pytest fixtures and some tests for card routes
neidacreates Jan 6, 2023
0203c44
fixed cors
lilianashley Jan 6, 2023
71fe813
removed empty test
neidacreates Jan 15, 2023
2c7c726
Merge branch 'main' of https://github.com/Tanil-Cadette/back-end-insp…
neidacreates Jan 15, 2023
c15bacc
adds tests for card routes
neidacreates Jan 18, 2023
7b3f98d
formatting
allcomputersarebad Jan 18, 2023
fde0282
adds tests for board routes
neidacreates Jan 18, 2023
7ada41d
deletes duplicate routes.py file
neidacreates Jan 18, 2023
17f6b72
rm routes.py
allcomputersarebad Jan 18, 2023
97e54f5
model fields nullable=False
allcomputersarebad Jan 18, 2023
661a031
add validation to board model, error handler route
allcomputersarebad Jan 18, 2023
b7524dc
Merge branch 'main' into fix-required-fields
allcomputersarebad Jan 18, 2023
342a5d6
adds board assignment to card test fixtures
neidacreates Jan 20, 2023
cce2547
adds test for card without board id
neidacreates Jan 20, 2023
e64063e
formatting
allcomputersarebad Jan 18, 2023
f1bfb06
card integrity error handler route
allcomputersarebad Jan 18, 2023
c94baf7
more board validation, eliminate routes
allcomputersarebad Jan 18, 2023
61e471d
card validation, card routes
allcomputersarebad Jan 18, 2023
3293f08
formatting
allcomputersarebad Jan 20, 2023
7f4e007
card validation with model and errorhandler
allcomputersarebad Jan 20, 2023
9a580a2
model, routes
allcomputersarebad Jan 20, 2023
20ac840
color migration
allcomputersarebad Jan 20, 2023
6c49616
add color option to_dict
allcomputersarebad Jan 20, 2023
4ffeb8c
finish color
allcomputersarebad Jan 20, 2023
afc26ce
force color on board
allcomputersarebad Jan 20, 2023
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
30 changes: 19 additions & 11 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,30 @@
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(
"SQLALCHEMY_DATABASE_URI")

# Import models here for Alembic setup
# from app.models.ExampleModel import ExampleModel
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")

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

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

from app.models.board import Board
from app.models.card import Card

from .board_routes import boards_bp
app.register_blueprint(boards_bp)

from .card_routes import cards_bp
app.register_blueprint(cards_bp)

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

from sqlalchemy.exc import IntegrityError

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


def validate_model(cls, model_id):
try:
model_id = int(model_id)
except:
abort(make_response({"message": f"{cls.__name__} {model_id} invalid"}, 400))

model = cls.query.get(model_id)

if not model:
abort(make_response({"message": f"{cls.__name__} {model_id} not found"}, 404))

return model


# ____________________________________________________________________________________________________________
# --------------------------------CREATE BOARD-----------------------------------------------------------------
# ____________________________________________________________________________________________________________
@boards_bp.route("", methods=["POST"])
def create_board():
request_body = request.get_json()

new_board = Board.from_dict(request_body)
db.session.add(new_board)
db.session.commit()

result = new_board.to_dict()
return make_response(jsonify(result), 201)


# __________________________________________________________________________________________________________
# -----------------------------------GET BOARD---------------------------------------------------------------
# __________________________________________________________________________________________________________
@boards_bp.route("", methods=["GET"])
def get_boards():
id_query = request.args.get("board_id")

if id_query: # TODO: this seems broken?
boards = Board.query.order_by(Board.board_id.asc()).all()
else:
boards = Board.query.all()

board_list = [board.to_dict() for board in boards]

return jsonify(board_list), 200


@boards_bp.route("/<board_id>", methods=["GET"])
def get_one_board(board_id):
board = validate_model(Board, board_id)
board_dict = board.to_dict()
return jsonify({"board": board_dict})


@boards_bp.route("/<board_id>/cards", methods=["GET"])
def read_board_cards(board_id):
board = validate_model(Board, board_id)
response = [card.to_dict() for card in board.cards]
return jsonify(response), 200


# __________________________________________________________________________________________________________
# --------------------------------COLOR ROUTES---------------------------------------------------------------
# __________________________________________________________________________________________________________
@boards_bp.route("/<board_id>/color", methods=["GET"])
def get_board_color(board_id):
board = validate_model(Board, board_id)
board_dict = board.to_dict(color=True)
db.session.commit()
return make_response(jsonify({"board": board_dict}))


@boards_bp.route("/<board_id>/color", methods=["POST"])
def set_board_color(board_id):
board = validate_model(Board, board_id)
request_body = request.get_json()
board.set_color(request_body["color"])
board_dict = board.to_dict(color=True)
db.session.commit()
return make_response(jsonify({"board": board_dict}))


# ____________________________________________________________________________________________________________
# --------------------------------DELETE BOARD----------------------------------------------------------------
# ____________________________________________________________________________________________________________
@boards_bp.route("/<board_id>", methods=["DELETE"])
def delete_board(board_id):
board = validate_model(Board, board_id)
board_dict = board.to_dict()

for card in board.cards:
db.session.delete(card)

db.session.delete(board)
db.session.commit()

return jsonify({"message": (f'Board {board_id} {board_dict["title"]} was deleted')})


@boards_bp.errorhandler(IntegrityError)
@boards_bp.errorhandler(ValueError)
def handle_invalid_data(e):
return make_response({"message": "invalid or incomplete board data"}, 400)
86 changes: 86 additions & 0 deletions app/card_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from flask import Blueprint, jsonify, abort, make_response, request
from app import db
from app.models.card import Card

from sqlalchemy.exc import IntegrityError

cards_bp = Blueprint("cards", __name__, url_prefix="/cards")

# ==================================
# Helper function to validate id
# ==================================
def validate_id(class_name, id):
try:
id = int(id)
except:
abort(make_response({"message": f"Id {id} is an invalid id"}, 400))

query_result = class_name.query.get(id)
if not query_result:
abort(make_response({"message": f"Id {id} not found"}, 404))

return query_result


# ==================================
# CREATE CARD
# ==================================
@cards_bp.route("", methods=["POST"])
def create_card():
request_body = request.get_json()

new_card = Card.from_dict(request_body)
db.session.add(new_card)
db.session.commit()

result = new_card.to_dict()
return make_response(jsonify({"card": result}), 201)


# ==================================
# GET CARD
# ==================================
@cards_bp.route("", methods=["GET"])
def get_cards():
cards = Card.query.all()
cards_response = [card.to_dict() for card in cards]

return jsonify(cards_response), 200


@cards_bp.route("/<card_id>", methods=["GET"])
def get_one_card(card_id):
card = validate_id(Card, card_id)
card_dict = card.to_dict()

return jsonify({"card": card_dict})


# ==================================
# DELETE one card by id
# ==================================
@cards_bp.route("/<card_id>", methods=["DELETE"])
def delete_one_card(card_id):
card = validate_id(Card, card_id)

db.session.delete(card)
db.session.commit()

return make_response({"message": f"Card {card.card_id} successfully deleted"}, 200)


# ==================================
# PATCH one card by id
# ==================================
@cards_bp.route("<card_id>", methods=["PATCH"])
def edit_card_likes(card_id):
card = validate_id(Card, card_id)
card.likes_count += 1
db.session.commit()
return jsonify(card.to_dict()), 200


@cards_bp.errorhandler(IntegrityError)
@cards_bp.errorhandler(KeyError)
def handle_invalid_data(e):
return make_response({"message": "Invalid or incomplete card data"}, 400)
61 changes: 61 additions & 0 deletions app/models/board.py
Original file line number Diff line number Diff line change
@@ -1 +1,62 @@
from app import db

from random import randint


class Board(db.Model):
board_id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String, nullable=False)
owner = db.Column(db.String, nullable=False)
color = db.Column(db.Integer, default=lambda: randint(0, 256**3), nullable=False)
cards = db.relationship("Card", back_populates="board")

@db.validates("title", "owner")
def no_empty_strings(self, key, value):
value = str(value).strip()
if not value:
raise ValueError(f"{key} must be a non-empty string.")
return value

@db.validates("color")
def color_24bit(self, key, value):
colorError = ValueError(f"{key} must represent a 24-bit color.")

if isinstance(value, str):
value = "".join(filter(lambda v: v in "0123456789abcdefABCDEF", value))
value = int(value, 16)

if value is None:
return randint(0, 256**3)

if isinstance(value, int):
if value < 0 or value > (256**3):
raise colorError
return value

raise colorError

def to_dict(self, color=False):
board_dict = {
"board_id": self.board_id,
"title": self.title,
"owner": self.owner,
}
if color:
board_dict["color"] = f"#{self.color:0>6X}"
return board_dict

@classmethod
def filter_data(cls, board_data):
accepted_data = ("title", "owner")
return {field: board_data[field] for field in accepted_data}

@classmethod
def from_dict(cls, board_data):
return Board(**Board.filter_data(board_data))

def update(self, board_data):
for field, value in Board.filter_data(board_data).items():
setattr(self, field, value)

def set_color(self, color):
self.color = color
32 changes: 32 additions & 0 deletions app/models/card.py
Original file line number Diff line number Diff line change
@@ -1 +1,33 @@
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)
board = db.relationship("Board", back_populates="cards")

@db.validates("message")
def no_empty_strings(self, key, value):
value = str(value).strip()
if not value:
raise ValueError(f"{key} must be a non-empty string.")
return value

def to_dict(self):
return {
"card_id": self.card_id,
"message": self.message,
"likes_count": self.likes_count,
"board_id": self.board_id,
}

@classmethod
def filter_data(cls, board_data):
accepted_data = ("message", "board_id")
return {field: board_data[field] for field in accepted_data}

@classmethod
def from_dict(cls, card_data):
return Card(**Card.filter_data(card_data))
4 changes: 0 additions & 4 deletions app/routes.py

This file was deleted.

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
Loading