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

Paper - hiphip array - Manu Whit Nandita Maite #20

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6e49a63
set up and created models board and card
MaiteArp Jun 29, 2021
c5e3ec9
creates blueprints and registers blueprints
sundbean Jun 29, 2021
8a7504b
adds models to init.py
MaiteArp Jun 29, 2021
ce8db15
Merge branch 'skeleton'
sundbean Jun 29, 2021
1f403e7
adds skeleton code for routes
sundbean Jun 29, 2021
a572ff7
created board routes
MaiteArp Jun 29, 2021
ae6cb15
adds GET and DELETE endpoints to card
MaiteArp Jun 29, 2021
453a582
Merge pull request #1 from MaiteArp/maite1
sundbean Jun 29, 2021
470b03d
adds POST method to /boards/id/cards route, adds a default value to l…
sundbean Jun 29, 2021
14d492e
adds PATCH method to /cards/id/like route
sundbean Jun 29, 2021
d76df9d
Merge pull request #2 from MaiteArp/whit-routes
MaiteArp Jun 29, 2021
b38578b
adds some imports to testconfig and edits __init__py to accomodate te…
sundbean Jun 30, 2021
6b58496
adds fixtures to conftest
sundbean Jun 30, 2021
636fc4b
adds tests for /boards endpoint
sundbean Jun 30, 2021
3a5e396
fixes DELETE /boards endpoint to pass test
sundbean Jun 30, 2021
30e8b13
Merge pull request #3 from MaiteArp/writing-tests
MaiteArp Jun 30, 2021
c53cb0d
adds query parameters and docstring to GET /boards
sundbean Jun 30, 2021
45c3462
adds test skeletons for GET /boards query parameters
sundbean Jun 30, 2021
711bdbf
adds query parameters to DELETE /boards, adds docstrings, adds test s…
sundbean Jun 30, 2021
dce32ac
adds query parameters to GET /boards/id/cards to sort result by likes…
sundbean Jun 30, 2021
aa8f554
Merge pull request #4 from MaiteArp/query-params
MaiteArp Jun 30, 2021
8b525ec
adds tests for cards endpoints
MaiteArp Jun 30, 2021
fc666e9
Merge pull request #5 from MaiteArp/maite2
sundbean Jun 30, 2021
aff6178
changes GET /boards/id/cards route to return a vanilla list of cards
sundbean Jul 1, 2021
92bb68a
Merge pull request #6 from MaiteArp/whit-edits
sundbean Jul 1, 2021
d3e0e8e
fixes database migration bug
MaiteArp Jul 1, 2021
49d336c
Merge pull request #7 from MaiteArp/migration_fix
pancakes4lyfe Jul 1, 2021
73a1739
Revert "fixes database migration bug"
MaiteArp Jul 1, 2021
5765cb0
adds tests for card over characters and conditional to return error
MaiteArp Jul 1, 2021
42fcc87
Merge pull request #8 from MaiteArp/card_character
sundbean Jul 1, 2021
15728e5
adds sorting capability to /boards/id/cards endopoint
sundbean Jul 1, 2021
405f0d9
fixes typo
sundbean Jul 1, 2021
0bf4d28
minor changes to sort cards params, adds necessary import
sundbean Jul 1, 2021
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
20 changes: 15 additions & 5 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,32 @@
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")
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

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

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

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


CORS(app)
return app
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)
title = db.Column(db.String)
owner = db.Column(db.String)
cards = db.relationship('Card', backref='board', lazy=True)


def to_json(self):
return {
"board_id": self.board_id,
"title": self.title,
"owner": self.owner,
}
15 changes: 15 additions & 0 deletions app/models/card.py
Original file line number Diff line number Diff line change
@@ -1 +1,16 @@
from app import db

class Card(db.Model):
card_id = db.Column(db.Integer, primary_key=True)
message = db.Column(db.String)
likes_count = db.Column(db.Integer, default=0)
board_id = db.Column(db.Integer, db.ForeignKey('board.board_id'))


def to_json(self):
return {
"card_id": self.card_id,
"message": self.message,
"likes_count": self.likes_count,
"board_id": self.board_id,
}
252 changes: 251 additions & 1 deletion app/routes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,254 @@
from flask import Blueprint, request, jsonify, make_response
from app import db
from app.models.board import Board
from app.models.card import Card
from sqlalchemy import desc

# example_bp = Blueprint('example_bp', __name__)

boards_bp = Blueprint('boards', __name__, url_prefix="/boards")
cards_bp = Blueprint('cards', __name__, url_prefix="/cards")


############################################## BOARDS CRUD ##########################################################


@boards_bp.route("", methods=["GET"])
def get_all_boards():
"""
Request body: None. Accommodates query parameters in route path to sort by title or owner, or filter results by title or owner.
Action: Gets all boards within query parameters, otherwise gets all boards in database.
Response: A JSON list of dictionaries, each dictionary representing a board. These dictionaries contain keys "id", "title", and "owner"
"""
sort_by_title_query = request.args.get("sort_by_title")
sort_by_owner_query = request.args.get("sort_by_owner")
filter_by_title_query = request.args.get("filter_by_title")
filter_by_owner_query = request.args.get("filter_by_owner")

if sort_by_title_query == "asc":
boards = Board.query.order_by("title")
elif sort_by_title_query == "desc":
boards = Board.query.order_by(desc("title"))
elif sort_by_owner_query == "asc":
boards = Board.query.order_by("owner")
elif sort_by_owner_query == "desc":
boards = Board.query.order_by(desc("owner"))
elif filter_by_title_query:
boards = Board.query.filter_by(title=filter_by_title_query)
elif filter_by_owner_query:
boards = Board.query.filter_by(owner=filter_by_owner_query)
else:
boards = Board.query.all()

return jsonify([board.to_json() for board in boards])


@boards_bp.route("", methods=["POST"])
def create_new_board():
"""
Request body: A JSON dictionary with "title" and "owner"
Action: Creates a new board with specified title and owner. If either of these details is missing from request body, throws a 400 error.
Response: 201 Created. Returns a JSON dictionary with key "board", whose value is another dictionary detailing new board's info ("board_id", "title", "owner")
"""
request_body = request.get_json()
try:
new_board = Board(title=request_body['title'],
owner=request_body['owner'])
except KeyError:
return make_response({
"details": "invalid data"
}, 400)
db.session.add(new_board)
db.session.commit()
response = {
"board": new_board.to_json()
}
return make_response(jsonify(response), 201)


@boards_bp.route("/<id>", methods=["GET"])
def get_single_board(id):
"""
Request body: None; requested board id specified in route path.
Action: Gets baord with specified ID; returns 404 response if no board found.
Response: 200 OK, returns JSON dictionary with key "board" whose value is another dictionary detailing the board's info ("board_id", "title", "owner")
"""
board = Board.query.get(id)
if board is None:
return make_response("", 404)

return {
"board": board.to_json()
}



@boards_bp.route("/<id>", methods=["DELETE"])
def delete_single_board(id):
"""
Request body: None. Requested board id is specified in route path.
Action: Deletes board at specified ID if board exists.
Response: 404 if not found; otherwise, 200 OK. Returns dictionary with message that the selected board was deleted.
"""
board = Board.query.get(id)
if board is None:
return make_response("", 404)
db.session.delete(board)
db.session.commit()
return {
"details": f"Board {board.board_id} \"{board.title}\" successfully deleted"
} ### Also not in the hints doc


@boards_bp.route("", methods=["DELETE"])
def delete_all_boards():
"""
Request body: None. Query parameters optionally provided in route path to delete boards by title or owner.
Action: Deletes all boards if no query parameters. Query parameters accommodates deleting all boards that match specific title or owner.
Response: 404 if empty database or invalid owner/title query param. Otherwise, 200 ok. Returns message confirming board deletion.
"""
filter_by_title_query = request.args.get("filter_by_title")
filter_by_owner_query = request.args.get("filter_by_owner")

if filter_by_title_query:
boards = Board.query.filter_by(title=filter_by_title_query)
elif filter_by_owner_query:
boards = Board.query.filter_by(owner=filter_by_owner_query)
else:
boards = Board.query.all()

if len(boards) == 0:
return make_response("", 404)

for board in boards:
db.session.delete(board)
db.session.commit()
return {
"details": "Boards successfully deleted"
}


@boards_bp.route("/<id>/cards", methods=["GET"])
def get_cards_for_specific_board(id):
"""
Request body: None. Optional query parameter to sort by like count, ascending or descending.
Action: Gets all cards associated with the baord id provided in route path.
Response: 200 OK. 404 if board not found. Returns JSON list of dictionaries representing resulting cards.
"""
sort_query = request.args.get("sort")

board = Board.query.get(id)
if board is None:
return make_response("", 404)

if sort_query == "likes":
associated_cards = Card.query.filter_by(board_id=int(id)).order_by(desc("likes_count"))
elif sort_query == "id":
associated_cards = Card.query.filter_by(board_id=int(id)).order_by("card_id")
elif sort_query == "alphabetical":
associated_cards = Card.query.filter_by(board_id=int(id)).order_by("message")
else:
associated_cards = Card.query.filter_by(board_id=int(id))

return jsonify([card.to_json() for card in associated_cards])


@boards_bp.route("/<id>/cards", methods=["POST"])
def create_new_card(id):
"""
Request body: A JSON dictionary with a single key-value pair ("message": <message_body>)
Action: Creates a new entry in cards table with default (0) like_count, message provided in request body,
and board id provided in route.
Response: 201 Created; response body is JSON dictionary with keys "card_id", "message", "likes_count", and "board_id"
"""
board = Board.query.get(id)
if board is None:
return make_response("", 404)

request_body = request.get_json()

try:
note = request_body['message']
if len(note) > 40:
return make_response ({
"details": "message exceeds 40 characters"
}, 400)
new_card = Card(message=request_body['message'],
board_id=id)
except KeyError:
return make_response({
"details": "invalid data"
}, 400)

db.session.add(new_card)
db.session.commit()

response = {
"card": new_card.to_json()
}

return make_response(jsonify(response), 201)



############################################## CARDS CRUD ##########################################################
#GET /cards/ all
@cards_bp.route("", methods=["GET"])
def get_all_cards():
cards = Card.query.all()
return jsonify([card.to_json() for card in cards])


# GET /cards/{id}
@cards_bp.route("/<id>", methods=["GET"])
def get_single_card(id):
card = Card.query.get(id)
if card is None:
return make_response("", 404)

return {
"card": card.to_json()
} #### not in the hints doc


# PATCH /card/{id}/like
@cards_bp.route("/<id>/like", methods=["PATCH"]) #*** simon used a PUT for this
def like_card(id):
"""
Request body: none
Action: Increases likes_count by 1 if card exists and updates database
Response: A JSON dictionary with key "card", whose value is another dictionary detailing updated card info,
with keys "card_id", "message", "likes_count", and "board_id"
"""
card = Card.query.get(id)
if card is None:
return make_response("", 404)

card.likes_count += 1

db.session.commit()

return {
"card": card.to_json()
}


# DELETE /cards/{id}
@cards_bp.route("/<id>", methods=["DELETE"])
def delete_card(id):
card = Card.query.get(id)
if card is None:
return make_response("", 404)
db.session.delete(card)
db.session.commit()
return {
"details": f"Card {card.card_id} \"{card.message}\" successfully deleted"
}



######### EXTRAS ##########
# PUT /board/{id} < unnecessary, extra feature?
# PUT /cards/{id} < unnecessary

# GET /boards/<owner> (get boards by owner) or query param
# DELETE /boards/<owner> (delete boards that have a specific owner) or query param
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