From 8dfa7ef08734b4a92b3060bb0b4df2efd5b1faa0 Mon Sep 17 00:00:00 2001 From: Luke Emery-Fertitta Date: Sun, 24 Dec 2023 11:11:00 -0800 Subject: [PATCH] Re-organize flask app --- Procfile | 3 +- climbdex/__init__.py | 11 + climbdex/api.py | 36 ++++ climbdex/db.py | 191 ++++++++++++++++++ .../static}/js/boardSelection.js | 0 {static => climbdex/static}/js/common.js | 0 .../static}/js/filterSelection.js | 0 {static => climbdex/static}/js/results.js | 10 +- {static => climbdex/static}/media/icon.svg | 0 .../templates}/boardSelection.html.j2 | 0 .../templates}/filterSelection.html.j2 | 2 +- .../templates}/footer.html.j2 | 0 .../templates}/head.html.j2 | 0 .../templates}/heading.html.j2 | 0 .../templates}/results.html.j2 | 0 climbdex/views.py | 80 ++++++++ 16 files changed, 327 insertions(+), 6 deletions(-) create mode 100644 climbdex/__init__.py create mode 100644 climbdex/api.py create mode 100644 climbdex/db.py rename {static => climbdex/static}/js/boardSelection.js (100%) rename {static => climbdex/static}/js/common.js (100%) rename {static => climbdex/static}/js/filterSelection.js (100%) rename {static => climbdex/static}/js/results.js (92%) rename {static => climbdex/static}/media/icon.svg (100%) rename {templates => climbdex/templates}/boardSelection.html.j2 (100%) rename {templates => climbdex/templates}/filterSelection.html.j2 (98%) rename {templates => climbdex/templates}/footer.html.j2 (100%) rename {templates => climbdex/templates}/head.html.j2 (100%) rename {templates => climbdex/templates}/heading.html.j2 (100%) rename {templates => climbdex/templates}/results.html.j2 (100%) create mode 100644 climbdex/views.py diff --git a/Procfile b/Procfile index 8c67bbb..546db75 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1 @@ -# TODO: Modify this Procfile to fit your needs -web: gunicorn app:app +web: gunicorn climbdex:create_app() diff --git a/climbdex/__init__.py b/climbdex/__init__.py new file mode 100644 index 0000000..d09ed92 --- /dev/null +++ b/climbdex/__init__.py @@ -0,0 +1,11 @@ +import flask + +import climbdex.api +import climbdex.views + + +def create_app(): + app = flask.Flask(__name__, instance_relative_config=True) + app.register_blueprint(climbdex.api.blueprint) + app.register_blueprint(climbdex.views.blueprint) + return app diff --git a/climbdex/api.py b/climbdex/api.py new file mode 100644 index 0000000..d303228 --- /dev/null +++ b/climbdex/api.py @@ -0,0 +1,36 @@ +import flask + +import climbdex.db + +blueprint = flask.Blueprint("api", __name__) + + +@blueprint.route("/api/v1//layouts") +def layouts(board_name): + return flask.jsonify(climbdex.db.get_data(board_name, "layouts")) + + +@blueprint.route("/api/v1//layouts//sizes") +def sizes(board_name, layout_id): + return flask.jsonify( + climbdex.db.get_data(board_name, "sizes", {"layout_id": layout_id}) + ) + + +@blueprint.route("/api/v1//layouts//sizes//sets") +def sets(board_name, layout_id, size_id): + return flask.jsonify( + climbdex.db.get_data( + board_name, "sets", {"layout_id": layout_id, "size_id": size_id} + ) + ) + + +@blueprint.route("/api/v1/search/count") +def resultsCount(): + return flask.jsonify(climbdex.db.get_search_count(flask.request.args)) + + +@blueprint.route("/api/v1/search") +def search(): + return flask.jsonify(climbdex.db.get_search_results(flask.request.args)) diff --git a/climbdex/db.py b/climbdex/db.py new file mode 100644 index 0000000..9227d74 --- /dev/null +++ b/climbdex/db.py @@ -0,0 +1,191 @@ +import flask +import sqlite3 + +QUERIES = { + "angles": """ + SELECT angle + FROM products_angles + JOIN layouts + ON layouts.product_id = products_angles.product_id + WHERE layouts.id = $layout_id + ORDER BY angle ASC""", + "colors": """ + SELECT + placement_roles.id, + '#' || placement_roles.screen_color + FROM placement_roles + JOIN layouts + ON layouts.product_id = placement_roles.product_id + WHERE layouts.id = $layout_id""", + "grades": """ + SELECT + difficulty, + boulder_name + FROM difficulty_grades + WHERE is_listed = 1 + ORDER BY difficulty ASC""", + "holds": """ + SELECT + placements.id, + holes.x, + holes.y + FROM placements + INNER JOIN holes + ON placements.hole_id=holes.id + WHERE placements.layout_id = $layout_id + AND placements.set_id = $set_id""", + "layouts": """ + SELECT id, name + FROM layouts + WHERE is_listed=1 + AND password IS NULL""", + "layout_name": """ + SELECT name + FROM layouts + WHERE id = $layout_id""", + "image_filename": """ + SELECT + image_filename + FROM product_sizes_layouts_sets + WHERE layout_id = $layout_id + AND product_size_id = $size_id + AND set_id = $set_id""", + "search": """ + SELECT + climbs.uuid, + climbs.setter_username, + climbs.name, + climbs.description, + climbs.frames, + climb_stats.angle, + climb_stats.ascensionist_count, + (SELECT boulder_name FROM difficulty_grades WHERE difficulty = ROUND(climb_stats.display_difficulty)) AS difficulty, + climb_stats.quality_average + FROM climbs + LEFT JOIN climb_stats + ON climb_stats.climb_uuid = climbs.uuid + INNER JOIN product_sizes + ON product_sizes.id = $size_id + WHERE climbs.frames_count = 1 + AND climbs.is_draft = 0 + AND climbs.is_listed = 1 + AND climbs.layout_id = $layout_id + AND climbs.edge_left >= product_sizes.edge_left + AND climbs.edge_right <= product_sizes.edge_right + AND climbs.edge_bottom >= product_sizes.edge_bottom + AND climbs.edge_top <= product_sizes.edge_top + AND climb_stats.ascensionist_count >= $min_ascents + AND climb_stats.display_difficulty BETWEEN $min_grade AND $max_grade + AND climb_stats.quality_average >= $min_rating + """, + "sets": """ + SELECT + sets.id, + sets.name + FROM sets + INNER JOIN product_sizes_layouts_sets psls on sets.id = psls.set_id + WHERE psls.product_size_id = $size_id + AND psls.layout_id = $layout_id""", + "size_name": """ + SELECT + product_sizes.name + FROM product_sizes + INNER JOIN layouts + ON product_sizes.product_id = layouts.product_id + WHERE layouts.id = $layout_id + AND product_sizes.id = $size_id""", + "sizes": """ + SELECT + product_sizes.id, + product_sizes.name, + product_sizes.description + FROM product_sizes + INNER JOIN layouts + ON product_sizes.product_id = layouts.product_id + WHERE layouts.id = $layout_id""", + "size_dimensions": """ + SELECT + edge_left, + edge_right, + edge_bottom, + edge_top + FROM product_sizes + WHERE id = $size_id""", +} + + +def get_board_database(board_name): + try: + return flask.g.database + except AttributeError: + flask.g.database = sqlite3.connect(f"data/{board_name}/db.sqlite3") + return flask.g.database + + +def get_data(board_name, query_name, binds={}): + database = get_board_database(board_name) + cursor = database.cursor() + cursor.execute(QUERIES[query_name], binds) + return cursor.fetchall() + + +def get_search_count(args): + base_sql, binds = get_search_base_sql_and_binds(args) + database = get_board_database(args.get("board")) + cursor = database.cursor() + cursor.execute(f"SELECT COUNT(*) FROM ({base_sql})", binds) + return cursor.fetchall()[0][0] + + +def get_search_results(args): + base_sql, binds = get_search_base_sql_and_binds(args) + order_by_sql_name = { + "ascents": "climb_stats.ascensionist_count", + "difficulty": "climb_stats.display_difficulty", + "name": "climbs.name", + "quality": "climb_stats.quality_average", + }[flask.request.args.get("sortBy")] + sort_order = "ASC" if flask.request.args.get("sortOrder") == "asc" else "DESC" + ordered_sql = f"{base_sql} ORDER BY {order_by_sql_name} {sort_order}" + + limited_sql = f"{ordered_sql} LIMIT $limit OFFSET $offset" + binds["limit"] = int(flask.request.args.get("pageSize", 10)) + binds["offset"] = int(flask.request.args.get("page", 0)) * int(binds["limit"]) + + database = get_board_database(flask.request.args.get("board")) + cursor = database.cursor() + results = cursor.execute(limited_sql, binds).fetchall() + print(results) + return results + + +def get_search_base_sql_and_binds(args): + sql = QUERIES["search"] + binds = { + "layout_id": args.get("layout"), + "size_id": args.get("size"), + "min_ascents": args.get("minAscents"), + "min_grade": args.get("minGrade"), + "max_grade": args.get("maxGrade"), + "min_rating": args.get("minRating"), + } + + angle = args.get("angle") + if angle and angle != "any": + sql += " AND climb_stats.angle = $angle" + binds["angle"] = angle + + holds = args.get("holds") + if holds: + sql += " AND climbs.frames LIKE $like_string" + like_string_center = "%".join( + sorted( + f"p{hold_string}" + for hold_string in holds.split("p") + if len(hold_string) > 0 + ) + ) + like_string = f"%{like_string_center}%" + binds["like_string"] = like_string + + return sql, binds diff --git a/static/js/boardSelection.js b/climbdex/static/js/boardSelection.js similarity index 100% rename from static/js/boardSelection.js rename to climbdex/static/js/boardSelection.js diff --git a/static/js/common.js b/climbdex/static/js/common.js similarity index 100% rename from static/js/common.js rename to climbdex/static/js/common.js diff --git a/static/js/filterSelection.js b/climbdex/static/js/filterSelection.js similarity index 100% rename from static/js/filterSelection.js rename to climbdex/static/js/filterSelection.js diff --git a/static/js/results.js b/climbdex/static/js/results.js similarity index 92% rename from static/js/results.js rename to climbdex/static/js/results.js index 0034de6..b1e377f 100644 --- a/static/js/results.js +++ b/climbdex/static/js/results.js @@ -83,14 +83,18 @@ function drawResultsPage(pageNumber, pageSize) { rating, ] = result; - const difficultyAngleText = `${difficulty} at ${angle}\u00B0`; + const difficultyAngleText = + difficulty && angle ? `${difficulty} at ${angle}\u00B0` : ""; listButton.addEventListener("click", function () { drawClimb(uuid, name, frames, setter, difficultyAngleText); }); const nameText = document.createElement("p"); - nameText.textContent = `${name} (${difficultyAngleText})`; + nameText.textContent = `${name} ${difficultyAngleText}`; const statsText = document.createElement("p"); - statsText.textContent = `${ascents} ascents, ${rating.toFixed(2)}\u2605`; + statsText.textContent = + ascents && rating + ? `${ascents} ascents, ${rating.toFixed(2)}\u2605` + : ""; statsText.classList.add("fw-light"); listButton.appendChild(nameText); listButton.appendChild(statsText); diff --git a/static/media/icon.svg b/climbdex/static/media/icon.svg similarity index 100% rename from static/media/icon.svg rename to climbdex/static/media/icon.svg diff --git a/templates/boardSelection.html.j2 b/climbdex/templates/boardSelection.html.j2 similarity index 100% rename from templates/boardSelection.html.j2 rename to climbdex/templates/boardSelection.html.j2 diff --git a/templates/filterSelection.html.j2 b/climbdex/templates/filterSelection.html.j2 similarity index 98% rename from templates/filterSelection.html.j2 rename to climbdex/templates/filterSelection.html.j2 index 6755a62..a4fdf90 100644 --- a/templates/filterSelection.html.j2 +++ b/climbdex/templates/filterSelection.html.j2 @@ -29,7 +29,7 @@
Min Ascents - +
Min Rating diff --git a/templates/footer.html.j2 b/climbdex/templates/footer.html.j2 similarity index 100% rename from templates/footer.html.j2 rename to climbdex/templates/footer.html.j2 diff --git a/templates/head.html.j2 b/climbdex/templates/head.html.j2 similarity index 100% rename from templates/head.html.j2 rename to climbdex/templates/head.html.j2 diff --git a/templates/heading.html.j2 b/climbdex/templates/heading.html.j2 similarity index 100% rename from templates/heading.html.j2 rename to climbdex/templates/heading.html.j2 diff --git a/templates/results.html.j2 b/climbdex/templates/results.html.j2 similarity index 100% rename from templates/results.html.j2 rename to climbdex/templates/results.html.j2 diff --git a/climbdex/views.py b/climbdex/views.py new file mode 100644 index 0000000..1cf90dc --- /dev/null +++ b/climbdex/views.py @@ -0,0 +1,80 @@ +import boardlib.api.aurora +import flask + +import climbdex.db + +blueprint = flask.Blueprint("view", __name__) + + +@blueprint.route("/") +def index(): + return flask.render_template( + "boardSelection.html.j2", + ) + + +@blueprint.route("/filter") +def filter(): + board_name = flask.request.args.get("board") + layout_id = flask.request.args.get("layout") + size_id = flask.request.args.get("size") + set_ids = flask.request.args.getlist("set") + return flask.render_template( + "filterSelection.html.j2", + params=flask.request.args, + board_name=board_name, + layout_name=climbdex.db.get_data( + board_name, "layout_name", {"layout_id": layout_id} + )[0][0], + size_name=climbdex.db.get_data( + board_name, "size_name", {"layout_id": layout_id, "size_id": size_id} + )[0][0], + angles=climbdex.db.get_data(board_name, "angles", {"layout_id": layout_id}), + grades=climbdex.db.get_data(board_name, "grades"), + colors=climbdex.db.get_data(board_name, "colors", {"layout_id": layout_id}), + **get_draw_board_kwargs(board_name, layout_id, size_id, set_ids), + ) + + +@blueprint.route("/results") +def results(): + board_name = flask.request.args.get("board") + layout_id = flask.request.args.get("layout") + size_id = flask.request.args.get("size") + set_ids = flask.request.args.getlist("set") + return flask.render_template( + "results.html.j2", + app_url=boardlib.api.aurora.WEB_HOSTS[board_name], + colors=climbdex.db.get_data(board_name, "colors", {"layout_id": layout_id}), + **get_draw_board_kwargs( + board_name, + layout_id, + size_id, + set_ids, + ), + ) + + +def get_draw_board_kwargs(board_name, layout_id, size_id, set_ids): + images_to_holds = {} + for set_id in set_ids: + image_filename = climbdex.db.get_data( + board_name, + "image_filename", + {"layout_id": layout_id, "size_id": size_id, "set_id": set_id}, + )[0][0] + image_url = f"{boardlib.api.aurora.API_HOSTS[board_name]}/img/{image_filename}" + images_to_holds[image_url] = climbdex.db.get_data( + board_name, "holds", {"layout_id": layout_id, "set_id": set_id} + ) + + size_dimensions = climbdex.db.get_data( + board_name, "size_dimensions", {"size_id": size_id} + )[0] + return { + "images_to_holds": images_to_holds, + "edge_left": size_dimensions[0], + "edge_right": size_dimensions[1], + "edge_bottom": size_dimensions[2], + "edge_top": size_dimensions[3], + }