Skip to content

Commit

Permalink
Support for all boards, minor UI improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
lemeryfertitta committed Dec 23, 2023
1 parent f2aa0fb commit 4cf93c1
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 147 deletions.
120 changes: 70 additions & 50 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import boardlib.api.aurora
import flask
import sqlite3

app = flask.Flask(__name__)

DATABASE = "data/kilter/db.sqlite3"

ANGLES_SQL = """
SELECT
angle
Expand Down Expand Up @@ -136,66 +135,50 @@
"""


def get_db():
def get_db(board_name):
db = getattr(flask.g, "_database", None)
if db is None:
db = flask.g._database = sqlite3.connect(DATABASE)
db = flask.g._database = sqlite3.connect(f"data/{board_name}/db.sqlite3")
return db


@app.route("/api/v1/layouts")
def layouts():
database = get_db()
@app.route("/api/v1/<board_name>/layouts")
def layouts(board_name):
database = get_db(board_name)
cursor = database.cursor()
cursor.execute(LAYOUTS_SQL)
return flask.jsonify(cursor.fetchall())


@app.route("/api/v1/layouts/<layout_id>/sizes")
def sizes(layout_id):
database = get_db()
@app.route("/api/v1/<board_name>/layouts/<layout_id>/sizes")
def sizes(board_name, layout_id):
database = get_db(board_name)
cursor = database.cursor()
cursor.execute(SIZES_SQL, {"layout_id": layout_id})
return flask.jsonify(cursor.fetchall())


@app.route("/api/v1/layouts/<layout_id>/sizes/<size_id>/sets")
def sets(layout_id, size_id):
database = get_db()
@app.route("/api/v1/<board_name>/layouts/<layout_id>/sizes/<size_id>/sets")
def sets(board_name, layout_id, size_id):
database = get_db(board_name)
cursor = database.cursor()
cursor.execute(SETS_SQL, {"layout_id": layout_id, "size_id": size_id})
return flask.jsonify(cursor.fetchall())


@app.route("/api/v1/search")
def search():
sql = SEARCH_SQL
binds = {
"layout_id": flask.request.args.get("layout"),
"size_id": flask.request.args.get("size"),
"min_ascents": flask.request.args.get("minAscents"),
"min_grade": flask.request.args.get("minGrade"),
"max_grade": flask.request.args.get("maxGrade"),
"min_rating": flask.request.args.get("minRating"),
}
@app.route("/api/v1/search/count")
def resultsCount():
base_sql, binds = get_search_base_sql_and_binds()
database = get_db(flask.request.args.get("board"))
cursor = database.cursor()
cursor.execute(f"SELECT COUNT(*) FROM ({base_sql})", binds)
results = cursor.fetchall()
return flask.jsonify(results[0][0])

angle = flask.request.args.get("angle")
if angle and angle != "any":
sql += " AND climb_stats.angle = $angle"
binds["angle"] = angle

holds = flask.request.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
@app.route("/api/v1/search")
def search():
base_sql, binds = get_search_base_sql_and_binds()

order_by_sql_name = {
"ascents": "climb_stats.ascensionist_count",
Expand All @@ -204,14 +187,15 @@ def search():
"quality": "climb_stats.quality_average",
}[flask.request.args.get("sortBy")]
sort_order = "ASC" if flask.request.args.get("sortOrder") == "asc" else "DESC"
sql += f" ORDER BY {order_by_sql_name} {sort_order}"
sql += " LIMIT $limit OFFSET $offset"
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_db()

database = get_db(flask.request.args.get("board"))
cursor = database.cursor()
cursor.connection.set_trace_callback(print)
cursor.execute(sql, binds)
cursor.execute(limited_sql, binds)
results = cursor.fetchall()
return flask.jsonify(results)

Expand All @@ -229,7 +213,7 @@ def filter():
layout_id = flask.request.args.get("layout")
size_id = flask.request.args.get("size")
set_ids = flask.request.args.getlist("set")
database = get_db()
database = get_db(board_name)
cursor = database.cursor()
cursor.execute(ANGLES_SQL, {"layout_id": layout_id})
angles = cursor.fetchall()
Expand All @@ -244,7 +228,7 @@ def filter():
angles=angles,
grades=grades,
colors=colors,
**get_draw_board_args(cursor, layout_id, size_id, set_ids),
**get_draw_board_args(cursor, board_name, layout_id, size_id, set_ids),
)


Expand All @@ -254,30 +238,34 @@ def results():
layout_id = flask.request.args.get("layout")
size_id = flask.request.args.get("size")
set_ids = flask.request.args.getlist("set")
database = get_db()
database = get_db(board_name)
cursor = database.cursor()
cursor.execute(COLORS_SQL, {"layout_id": layout_id})
colors = cursor.fetchall()
return flask.render_template(
"results.html.j2",
app_url=boardlib.api.aurora.WEB_HOSTS[board_name],
colors=colors,
**get_draw_board_args(
cursor,
board_name,
layout_id,
size_id,
set_ids,
),
)


def get_draw_board_args(cursor, layout_id, size_id, set_ids):
def get_draw_board_args(cursor, board_name, layout_id, size_id, set_ids):
images_to_holds = {}
for set_id in set_ids:
cursor.execute(
IMAGE_FILENAME_SQL,
{"layout_id": layout_id, "size_id": size_id, "set_id": set_id},
)
image_filename = f"http://api.kilterboardapp.com/img/{cursor.fetchone()[0]}"
image_filename = (
f"{boardlib.api.aurora.API_HOSTS[board_name]}/img/{cursor.fetchone()[0]}"
)
cursor.execute(HOLDS_SQL, {"layout_id": layout_id, "set_id": set_id})
holds = cursor.fetchall()
images_to_holds[image_filename] = holds
Expand All @@ -291,3 +279,35 @@ def get_draw_board_args(cursor, layout_id, size_id, set_ids):
"edge_bottom": size_dimensions[2],
"edge_top": size_dimensions[3],
}


def get_search_base_sql_and_binds():
sql = SEARCH_SQL
binds = {
"layout_id": flask.request.args.get("layout"),
"size_id": flask.request.args.get("size"),
"min_ascents": flask.request.args.get("minAscents"),
"min_grade": flask.request.args.get("minGrade"),
"max_grade": flask.request.args.get("maxGrade"),
"min_rating": flask.request.args.get("minRating"),
}

angle = flask.request.args.get("angle")
if angle and angle != "any":
sql += " AND climb_stats.angle = $angle"
binds["angle"] = angle

holds = flask.request.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
2 changes: 1 addition & 1 deletion scripts/db_sync.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
for BOARD in kilter tension
for BOARD in decoy grasshopper kilter tension touchstone
do
mkdir -p data/$BOARD
boardlib database $BOARD data/$BOARD/db.sqlite3
Expand Down
91 changes: 49 additions & 42 deletions static/js/boardSelection.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
function populateLayouts() {
fetch("/api/v1/layouts").then((response) => {
function populateLayouts(boardName) {
fetch(`/api/v1/${boardName}/layouts`).then((response) => {
response.json().then((layouts) => {
const layoutSelect = document.getElementById("select-layout");
layoutSelect.innerHTML = "";
for (const [layoutId, layoutName] of layouts) {
let option = document.createElement("option");
option.text = layoutName;
option.value = layoutId;
layoutSelect.appendChild(option);
}
layoutSelect.addEventListener("change", function (event) {
populateSizes(event.target.value);
populateSizes(boardName, event.target.value);
});
populateSizes(layoutSelect.value);
populateSizes(boardName, layoutSelect.value);
});
});
}

function populateSizes(layoutId) {
fetch(`/api/v1/layouts/${layoutId}/sizes`).then((response) => {
function populateSizes(boardName, layoutId) {
fetch(`/api/v1/${boardName}/layouts/${layoutId}/sizes`).then((response) => {
response.json().then((sizes) => {
const sizeSelect = document.getElementById("select-size");
sizeSelect.innerHTML = "";
Expand All @@ -28,50 +29,51 @@ function populateSizes(layoutId) {
sizeSelect.appendChild(option);
}
sizeSelect.addEventListener("change", function (event) {
populateSets(layoutId, event.target.value);
populateSets(boardName, layoutId, event.target.value);
});
populateSets(layoutId, sizeSelect.value);
populateSets(boardName, layoutId, sizeSelect.value);
});
});
}

function populateSets(layoutId, productSizeId) {
fetch(`/api/v1/layouts/${layoutId}/sizes/${productSizeId}/sets`).then(
(response) => {
response.json().then((sets) => {
const setsDiv = document.getElementById("div-sets");
setsDiv.innerHTML = "";
for (const [setId, setName] of sets) {
const inputGroupDiv = document.createElement("div");
inputGroupDiv.className = "input-group mb-3";
setsDiv.appendChild(inputGroupDiv);
function populateSets(boardName, layoutId, productSizeId) {
fetch(
`/api/v1/${boardName}/layouts/${layoutId}/sizes/${productSizeId}/sets`
).then((response) => {
response.json().then((sets) => {
const setsDiv = document.getElementById("div-sets");
setsDiv.innerHTML = "";
for (const [setId, setName] of sets) {
const inputGroupDiv = document.createElement("div");
inputGroupDiv.className = "input-group mb-3";
setsDiv.appendChild(inputGroupDiv);

const span = document.createElement("span");
span.className = "input-group-text";
span.textContent = setName;
inputGroupDiv.appendChild(span);
const span = document.createElement("span");
span.className = "input-group-text";
span.textContent = setName;
inputGroupDiv.appendChild(span);

const select = document.createElement("select");
select.className = "form-select";
select.setAttribute("data-set-id", setId);
select.addEventListener("change", updateSetsInput);
inputGroupDiv.appendChild(select);
const select = document.createElement("select");
select.className = "form-select";
select.setAttribute("data-set-id", setId);
select.addEventListener("change", updateSetsInput);
inputGroupDiv.appendChild(select);

const optionEnabled = document.createElement("option");
optionEnabled.text = "Enabled";
optionEnabled.value = true;
optionEnabled.selected = true;
select.appendChild(optionEnabled);
const optionEnabled = document.createElement("option");
optionEnabled.text = "Enabled";
optionEnabled.value = true;
optionEnabled.selected = true;
select.appendChild(optionEnabled);

const optionDisabled = document.createElement("option");
optionDisabled.text = "Disabled";
optionDisabled.value = false;
select.appendChild(optionDisabled);
}
updateSetsInput();
});
}
);
const optionDisabled = document.createElement("option");
optionDisabled.text = "Disabled (coming soon)";
optionDisabled.value = false;
optionDisabled.disabled = true;
select.appendChild(optionDisabled);
}
updateSetsInput();
});
});
}

function updateSetsInput() {
Expand All @@ -92,4 +94,9 @@ function updateSetsInput() {
document.getElementById("button-next").disabled = !isOneSetEnabled;
}

populateLayouts();
const boardSelect = document.getElementById("select-board");
boardSelect.addEventListener("change", function (event) {
populateLayouts(event.target.value);
});

populateLayouts(boardSelect.value);
Loading

0 comments on commit 4cf93c1

Please sign in to comment.