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

Tick Climbs and Show Stats #72

Merged
merged 33 commits into from
Aug 29, 2024
Merged
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
12dd4c5
Mark Attempts + Logout Button
gardaholm Jun 27, 2024
2507028
minor styling
gardaholm Jun 27, 2024
d7a1b92
updated bids
gardaholm Jun 30, 2024
d254fd0
updated views.py
gardaholm Jul 3, 2024
da4a5d0
Fix it
Jul 3, 2024
f8e7d5b
removed testing files
gardaholm Jul 3, 2024
4124ef3
Combined Menu on Results
gardaholm Jul 17, 2024
3869a2c
Added some Features
Jul 18, 2024
531419b
updated radio buttons
gardaholm Jul 18, 2024
52b55a1
Addded Save buttons
Jul 18, 2024
4e5d72d
Revert "updated radio buttons"
gardaholm Jul 18, 2024
d3c6d5e
Merge branch 'new-branch-name'
gardaholm Jul 18, 2024
9d7f553
updated fly + requirements
gardaholm Jul 18, 2024
c32723c
Your commit message here
Jul 18, 2024
c9417e5
Merge branch 'alerts'
gardaholm Jul 18, 2024
60dbe88
form updates
gardaholm Jul 18, 2024
8d99168
Remove from Logging Attempts
gardaholm Jul 21, 2024
9aaf434
Fixed CSS Problem on Menu Dropdown
gardaholm Aug 10, 2024
3d7aa68
minor cleanups
gardaholm Aug 12, 2024
c34e747
cleanup
gardaholm Aug 12, 2024
e732edf
Reverted fly.toml for climbdex
gardaholm Aug 12, 2024
b7be947
diffForSave camelCase
gardaholm Aug 19, 2024
a687ce7
Added some things
Aug 19, 2024
6a6e155
Added some things
Aug 19, 2024
1a35c2b
Added import request and chaged view.py
Aug 20, 2024
582f5bb
Back to start
Aug 22, 2024
9854444
Angle missmatch resolved
Aug 22, 2024
3656539
bid_count instead of attempt_id
gardaholm Aug 26, 2024
f150117
fixed overflow
gardaholm Aug 27, 2024
cf3a7ee
Your Attempts instead of Statistics
gardaholm Aug 27, 2024
bdc521c
mistake in overflow commit
gardaholm Aug 27, 2024
f4c7b51
Style and nomenclature updates
lemeryfertitta Aug 29, 2024
cb94194
Remove div-climb styling
lemeryfertitta Aug 29, 2024
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
76 changes: 59 additions & 17 deletions climbdex/api.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
from flask_parameter_validation import ValidateParameters, Query

import boardlib.api.aurora
import flask
import requests
import json
from flask_parameter_validation import ValidateParameters, Json, Query
import boardlib.api.aurora
import logging

import climbdex.db
import requests

blueprint = flask.Blueprint("api", __name__)

def parameter_error(e):
code = 400
name = str(type(e).__name__)
description = f"Parameters were missing and/or misconfigured. If the issue persists, please <a href=\"https://github.com/lemeryfertitta/Climbdex/issues/new?title={str(type(e).__name__)}: {str(e)} ({code})\" target='_blank'>report it</a> (code: {code})"
description = (
f"Parameters were missing and/or misconfigured. If the issue persists, please "
f"<a href=\"https://github.com/lemeryfertitta/Climbdex/issues/new?title={str(type(e).__name__)}: {str(e)} ({code})\" target='_blank'>report it</a> (code: {code})"
)

response = {
response = {
"error": True,
"code": code,
"name": name,
Expand All @@ -27,31 +27,32 @@ def parameter_error(e):

@blueprint.errorhandler(Exception)
def handle_exception(e):
logging.error(f"Unhandled exception: {str(e)}", exc_info=True)
response = e.get_response()
response.data = json.dumps({
response.data = flask.json.dumps({
"error": True,
"code": e.code,
"name": e.name,
"description": f"There was a problem while getting results from the server. If the issue persists, please <a href=\"https://github.com/lemeryfertitta/Climbdex/issues/new?title={e.name} ({e.code})&body={e.description}\" target='_blank'>report it</a> (code: {e.code})",
"description": (
f"There was a problem while getting results from the server. If the issue persists, "
f"please <a href=\"https://github.com/lemeryfertitta/Climbdex/issues/new?title={e.name} ({e.code})&body={e.description}\" "
f"target='_blank'>report it</a> (code: {e.code})"
),
})
response.content_type = "application/json"
logging.error(response.data)
return response



@blueprint.route("/api/v1/<board_name>/layouts")
def layouts(board_name):
return flask.jsonify(climbdex.db.get_data(board_name, "layouts"))


@blueprint.route("/api/v1/<board_name>/layouts/<layout_id>/sizes")
def sizes(board_name, layout_id):
return flask.jsonify(
climbdex.db.get_data(board_name, "sizes", {"layout_id": int(layout_id)})
)


@blueprint.route("/api/v1/<board_name>/layouts/<layout_id>/sizes/<size_id>/sets")
def sets(board_name, layout_id, size_id):
return flask.jsonify(
Expand All @@ -60,7 +61,6 @@ def sets(board_name, layout_id, size_id):
)
)


@blueprint.route("/api/v1/search/count")
@ValidateParameters(parameter_error)
def resultsCount(
Expand All @@ -87,12 +87,10 @@ def search(
):
return flask.jsonify(climbdex.db.get_search_results(flask.request.args))


@blueprint.route("/api/v1/<board_name>/beta/<uuid>")
def beta(board_name, uuid):
return flask.jsonify(climbdex.db.get_data(board_name, "beta", {"uuid": uuid}))


@blueprint.route("/api/v1/login/", methods=["POST"])
def login():
try:
Expand All @@ -112,3 +110,47 @@ def login():
)
else:
return flask.jsonify({"error": str(e)}), e.response.status_code

@blueprint.route("/api/v1/save_ascent", methods=["POST"])
@ValidateParameters(parameter_error)
def api_save_ascent(
board: str = Json(),
climb_uuid: str = Json(),
angle: int = Json(),
is_mirror: bool = Json(),
attempt_id: int = Json(),
bid_count: int = Json(),
quality: int = Json(),
difficulty: int = Json(),
is_benchmark: bool = Json(),
comment: str = Json(),
climbed_at: str = Json(),
):
try:
login_cookie = flask.request.cookies.get(f"{board}_login")
if not login_cookie:
return flask.jsonify({"error": "Login required"}), 401

login_info = flask.json.loads(login_cookie)
token = login_info["token"]
user_id = login_info["user_id"]

result = boardlib.api.aurora.save_ascent(
board=board,
token=token,
user_id=user_id,
climb_uuid=climb_uuid,
angle=angle,
is_mirror=is_mirror,
attempt_id=attempt_id,
gardaholm marked this conversation as resolved.
Show resolved Hide resolved
bid_count=bid_count,
quality=quality,
difficulty=difficulty,
is_benchmark=is_benchmark,
comment=comment,
climbed_at=climbed_at
)
return flask.jsonify(result)
except Exception as e:
logging.error(f"Error in save_ascent: {str(e)}", exc_info=True)
return flask.jsonify({"error": str(e)}), 500
1 change: 1 addition & 0 deletions climbdex/static/js/boardSelection.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ loginForm.addEventListener("submit", function (event) {
const modal = bootstrap.Modal.getInstance("#div-modal");
modal.hide();
populateLoginForm(boardName);
location.reload();
}
});
});
Expand Down
167 changes: 156 additions & 11 deletions climbdex/static/js/results.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ function drawClimb(
frames,
Copy link
Owner

@lemeryfertitta lemeryfertitta Aug 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried updating boardlib to 0.7.1 and running this PR locally. There were a number of things that were broken for me:

  • Upon successful login there is no indication of success (the login text doesn't change) and climbs don't appear to be ticked. Seemed like logging in was generally broken, but I didn't see any error messages in the console or logs.
  • Board image is overflowing, at least for Kilter 8x12
image

Let me know if you are experiencing the same or if I'm doing something wrong!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strange, didn't happen on our tests here, but I will spin up our test version on fly.io with the official boardlib 0.7.1 and will check it. will probably be able to do it tomorrow morning. sorry for that!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure it installed the new requirements?
I've spinned a fly.io version here https://climbdex-wandering-moon-444.fly.dev/ (linked to boardlib 0.7.1)
and it seems to work for me …

  • tested kilter + tension board with my aurora credentials
  • I could login, and get the different status message (You're logged in!)
  • can see my ticked and tried climbs (green + yellow)
  • could log my climb (logged climbs only get displayed after a full refresh, when the logbook is renewed), also get the Successfull Tick Message.
  • the problem with the kilter and tension board classic layout can be fixed via css (will add a commit, seems to be a overflow problemon certain screen dimensions)

Copy link
Owner

@lemeryfertitta lemeryfertitta Aug 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I found the issue. The local deployment bind I was running on 0.0.0.0:8000 (was doing it for some mobile testing) didn't allow the cookie to be stored, so it just silently was trying and failing to set the cookie. Created #83 for that, as it's pretty confusing why the login doesn't work if your cookies aren't enabled, but it is unrelated to your PR here. As an aside, I figured out how to close the modal without refreshing the whole page (your location.reload change), but I'll just update that in a separate PR.

I think the only thing that is a regression from this PR is the overflow issue. I think its just some issue with the bootstrap containers that were changed to add the new buttons and such.

Also, I just played with the tick feature a little bit more and had a few additional thoughts:

  1. Ticking repeats is a little bit weird, it looks identical to trying the climb over multiple sessions and only sending once vs. sending in multiple sessions. I wonder if we shouldn't be trying to aggregate here, and we should just show a list of sessions for each climb. So basically this would just be the list of all of the logbook entries from boardlib for a specific climb

  2. Maybe we can rename "Statistics" to "Your Attempts" or something similar. I feel like when you click statistics you expect to get the same information as the info button on the official apps, which shows other peoples ticks and grade distribution chart and such.

We can leave 1. as is for now and change that in a future PR, I think it's worth just getting the current functionality in. Let's get the overflow issue fixed and rename the menu item for this PR.

Copy link
Contributor Author

@gardaholm gardaholm Aug 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the only thing that is a regression from this PR is the overflow issue. I think its just some issue with the bootstrap containers that were changed to add the new buttons and such.

should be fixed. I think the occurence is pretty rare >> probably 13'' notebooks + board images which are very long.

Also, I just played with the tick feature a little bit more and had a few additional thoughts:

  1. Ticking repeats is a little bit weird, it looks identical to trying the climb over multiple sessions and only sending once vs. sending in multiple sessions. I wonder if we shouldn't be trying to aggregate here, and we should just show a list of sessions for each climb. So basically this would just be the list of all of the logbook entries from boardlib for a specific climb

true. I was thinking about the same: if you open the log item a small indication about the sessions would be great and if there are already sessions logged then the flash button can be hidden (I already ran into this issue a few times and wondering why the app don't show the flash icon …). Information is already there so I guess it shouldn't be too much work for a future PR.

What this PR is also missing is a indication that the current climb is logged (besided the success message). It would be nice if the entry in the result list gets the background color and the tick icon after saving the modal. Shouldn't be too hard, maybe I can do this in another PR, it's more a cosmetical thing I guess …

  1. Maybe we can rename "Statistics" to "Your Attempts" or something similar. I feel like when you click statistics you expect to get the same information as the info button on the official apps, which shows other peoples ticks and grade distribution chart and such.

also fine, renamed the entries (Menu + Modal-Title).
Would love to have the stats. grad distribution and comment also somewhere in climbdex. but probably something for later.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be fixed. I think the occurence is pretty rare >> probably 13'' notebooks + board images which are very long.

Didn't really like the overflow approach so I changed it back to the original behavior to scale based on viewport. Also made a few other minor changes to some of the nomenclature but will get this merged and deployed now! Lots of other improvements to make but its a huge start 😄

setter,
difficultyAngleSpan,
description
description,
attempts_infotext,
difficulty
) {
document
.getElementById("svg-climb")
Expand All @@ -33,6 +35,11 @@ function drawClimb(
anchor.target = "_blank";
anchor.rel = "noopener noreferrer";

const diffForSave = document.getElementById("difficulty");
diffForSave.value = difficulty;
const event = new Event("change");
diffForSave.dispatchEvent(event);

const climbNameHeader = document.getElementById("header-climb-name");
climbNameHeader.innerHTML = "";
climbNameHeader.appendChild(anchor);
Expand All @@ -41,7 +48,6 @@ function drawClimb(

const climbSetterHeader = document.getElementById("header-climb-setter");
climbSetterHeader.textContent = `by ${setter}`;

const climbStatsParagraph = document.getElementById("paragraph-climb-stats");
climbStatsParagraph.innerHTML = difficultyAngleSpan.outerHTML;

Expand All @@ -56,6 +62,15 @@ function drawClimb(
climbDescriptionParagraph.innerHTML = `Description: ${trimmedDescription.italics()}`;
}

const climbedAttempts = document.getElementById("paragraph-climb-attempts");

if (attempts_infotext === undefined) {
climbedAttempts.classList.add("d-none");
} else {
climbedAttempts.classList.remove("d-none");
climbedAttempts.innerHTML = `${attempts_infotext}`;
}

const urlParams = new URLSearchParams(window.location.search);
const board = urlParams.get("board");
fetchBetaCount(board, uuid).then((betaCount) => {
Expand All @@ -74,7 +89,109 @@ function drawClimb(
);
illuminateClimb(board, bluetoothPacket);
};

const modalclimbNameHeader = document.getElementById("modal-climb-name");
modalclimbNameHeader.innerHTML = name;

const modalclimbStatsParagraph = document.getElementById("modal-climb-stats");
modalclimbStatsParagraph.innerHTML = difficultyAngleSpan.outerHTML;
}
const gradeMappingObject = gradeMapping.reduce((acc, [difficulty, grade]) => {
acc[grade] = difficulty;
return acc;
}, {});

document
.getElementById("button-log-ascent")
.addEventListener("click", function () {
const urlParams = new URLSearchParams(window.location.search);
const board = urlParams.get("board");
const climb_uuid = document
.querySelector("#header-climb-name a")
.href.split("/")
.pop();
const angle = parseInt(
document
.querySelector("#modal-climb-stats span")
.textContent.match(/\d+°/)[0]
);
const is_mirror = false;
const attempt_id = 0;
const bid_count =
document.querySelector('input[name="attemptType"]:checked').id === "flash"
? 1
: parseInt(document.getElementById("attempts").value);
const quality =
parseInt(document.querySelector(".star-rating input:checked")?.value) ||
0;
const selectedAttemptType = document.querySelector(
'input[name="attemptType"]:checked'
).id;
const difficultyValue = document.getElementById("difficulty").value;
const convertedDifficulty = gradeMappingObject[difficultyValue];

const finalDifficulty = ["flash", "send"].includes(selectedAttemptType)
? parseInt(convertedDifficulty)
: 0;

const is_benchmark = document
.querySelector("#paragraph-climb-stats span")
.textContent.includes("©")
? true
: false;
const climbed_at = new Date().toISOString().split("T")[0] + " 00:00:00";
const comment = document.getElementById("comment").value;

const data = {
board: board,
climb_uuid: climb_uuid,
angle: angle,
is_mirror: is_mirror,
attempt_id: attempt_id,
bid_count: bid_count,
quality: quality,
difficulty: finalDifficulty,
is_benchmark: is_benchmark,
comment: comment,
climbed_at: climbed_at,
};

fetch("/api/v1/save_ascent", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
})
.then((response) => {
if (!response.ok) {
throw new Error("Network response was not ok " + response.statusText);
}
return response.json();
})
.then((data) => {
const successAlert = document.querySelector(".alert-success");
successAlert.style.display = "block";

setTimeout(() => {
successAlert.style.display = "none";
const logModal = document.getElementById("div-log-modal");
const modalInstance = bootstrap.Modal.getInstance(logModal);
if (modalInstance) {
modalInstance.hide();
}
}, 3000);
})
.catch((error) => {
console.error("Error:", error);
const errorAlert = document.querySelector(".alert-danger");
errorAlert.style.display = "block";

setTimeout(() => {
errorAlert.style.display = "none";
}, 3000);
});
});

async function fetchBetaCount(board, uuid) {
const response = await fetch(`/api/v1/${board}/beta/${uuid}`);
Expand All @@ -87,9 +204,10 @@ async function fetchResultsCount() {
const response = await fetch("/api/v1/search/count?" + urlParams);
const resultsCount = await response.json();

if (resultsCount['error'] == true) {
alert.querySelector('.alert-content').innerHTML = resultsCount['description']
alert.classList.add('show-alert')
if (resultsCount["error"] == true) {
alert.querySelector(".alert-content").innerHTML =
resultsCount["description"];
alert.classList.add("show-alert");
} else {
return resultsCount;
}
Expand All @@ -102,9 +220,9 @@ async function fetchResults(pageNumber, pageSize) {
const response = await fetch("/api/v1/search?" + urlParams);
const results = await response.json();

if (results['error'] == true) {
alert.querySelector('.alert-content').innerHTML = resultsCount['description']
alert.classList.add('show-alert')
if (results["error"] == true) {
alert.querySelector(".alert-content").innerHTML = results["description"];
alert.classList.add("show-alert");
} else {
return results;
}
Expand Down Expand Up @@ -182,7 +300,7 @@ function drawResultsPage(results, pageNumber, pageSize, resultsCount) {
difficulty,
rating,
difficultyError,
classic
classic,
] = result;

const classicSymbol = classic !== null ? "\u00A9" : "";
Expand All @@ -198,12 +316,30 @@ function drawResultsPage(results, pageNumber, pageSize, resultsCount) {
difficultyAngleSpan.appendChild(
document.createTextNode(difficultyAngleText)
);

const show_attempts = attemptedClimbs[`${uuid}-${angle}`];
let attempts_infotext;
if (show_attempts !== undefined) {
listButton.classList.add("bg-warning-subtle");
attempts_infotext =
"You had " +
show_attempts["total_tries"] +
(show_attempts["total_tries"] === 1 ? " try in " : " tries in ") +
show_attempts["total_sessions"] +
" session. <br> The last session was: " +
show_attempts["last_try"];
} else {
attempts_infotext = "You had no tries so far.";
}

const tickType = tickedClimbs[`${uuid}-${angle}`];
if (tickType !== undefined) {
listButton.classList.add("bg-secondary-subtle");
listButton.classList.add("bg-success-subtle");
listButton.classList.remove("bg-warning-subtle"); //remove class if a climb used to be a attemped but was ticked later
difficultyAngleSpan.appendChild(document.createTextNode(" "));
difficultyAngleSpan.appendChild(getTickSvg(tickType));
}

listButton.addEventListener("click", function (event) {
const index = Number(event.currentTarget.getAttribute("data-index"));
const prevButton = document.getElementById("button-prev");
Expand All @@ -216,7 +352,16 @@ function drawResultsPage(results, pageNumber, pageSize, resultsCount) {
nextButton.onclick = function () {
clickClimbButton(index + 1, pageSize, resultsCount);
};
drawClimb(uuid, name, frames, setter, difficultyAngleSpan, description);
drawClimb(
uuid,
name,
frames,
setter,
difficultyAngleSpan,
description,
attempts_infotext,
difficulty
);
});
const nameText = document.createElement("p");
nameText.innerHTML = `${name} ${difficultyAngleSpan.outerHTML}`;
Expand Down
4 changes: 4 additions & 0 deletions climbdex/templates/climbCreation.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
max-height: 800px;
overflow-y: scroll;
}

#div-climb {
overflow: auto;
}
</style>
</head>

Expand Down
Loading