feat(report_quiz): add a leaderboard#119
Conversation
In saving the quiz data, we will be able to retrieve the submitted answers and build the leaderboard
This commit introduces the following changes: - Added a function `get_user_answers` and `fetch_quiz_data` to be able to get the user quiz answers and update the quiz_data database - Restricted the quiz answers to one answer submission per user per quiz
3280748 to
48e548f
Compare
48e548f to
d3cc3da
Compare
When a quiz_data is created, the user_answers field is not present and the addition can't proceed. As change, we will use an empty value in this case
- added the possibility to generate and send the leaderboard
Stop all the active quiz, submit the result and archive the quiz data
Added a friendly announcement message. Additonnal change - moved the price calculation from `generate_leaderboard` to `compute_total_score`. - fixed issue when a quiz data contains no user answer submission during the score computation
- Added the start new competition action - Added the fetching of the quiz user answers - Added the backup of the data using git - Added the ending of a current competition
|
@Sanix-Darker , @elhmn , this pr is ready for review. |
The close competition fail during the closing of a already closed quiz. Since the status of this process is not important, we will ignore it. Like that the closing of the competition can proceed.
This commit handles the case when there is no results during the closing of the competition.
Sanix-Darker
left a comment
There was a problem hiding this comment.
Hey there, thanks for this contribution, can you provide a video demo or workflow on your PR description please ?
| @@ -0,0 +1,9 @@ | |||
| # compute the user total score from more than one quiz_data | |||
| compute_total_score(){ | |||
| jq -s --argjson total_price 30000 'map((.correct_answers | map(.)) as $answers | (.user_answers // []) | map(.score=(.options_ids | map(select($answers[.] == "true")) | length)-(.options_ids | map(select($answers[.] == "false")) | length) | del(.options_ids))) | add | group_by(.first_name) | map((map(.score) | add) as $total_score | .[0] | del(.score) | .total_score=$total_score) | group_by(-.total_score) | to_entries | map(.price=$total_price/((.key+1)*2))' $@ | |||
There was a problem hiding this comment.
This is too compact, even tho it's understandable, make it digest please, not in a single line
There was a problem hiding this comment.
Yes, but the fact that it's a set of sequential instructions, don't affect his readability.
After a look at the source JSON, it becomes easier to understand.
https://github.com/pythonbrad/reportquiz_data/blob/main/archive/1727937431/6050905932701893008.json
Between, I haven't succeeded to split it while keeping it more readable.
I can't do shell piping since it will affect the performance.
Note that I use functional programming.
There was a problem hiding this comment.
Add in the comment that is up the method a simple use case représentation of what can be an input and an output, to help us see what the method is capable of if the JQ filter can not be on multi lines.
There was a problem hiding this comment.
This jq command reflects the function name and his comment.
There was a problem hiding this comment.
i agree with @Sanix-Darker , this is not easy to maintain. I am pretty sure you can split it into smaller sequential and logical chunks so that other people can understand it quicker. Performance is not an issue here, since this run in a runner on a cron, no one is waiting for this answer to be sent in less than 10 milliseconds. I am more for privileging readability/maintainability over speed here.
You also could write it in multiple lines for example, add some single line comments to your script, use re-usable variables etc which I think are supported by jq.
There was a problem hiding this comment.
@rakici @Sanix-Darker
Is it this splitting enough?
map(
(.correct_answers | map(.)) as $answers
| (.user_answers // [])
| map(
.score=(
.options_ids
| map(select($answers[.] == "true"))
| length
) - (
.options_ids
| map(select($answers[.] == "false"))
| length
)
| del(.options_ids)
)
)
| add
| group_by(.first_name)
| map(
(map(.score) | add) as $total_score
| .[0]
| del(.score)
| .total_score=$total_score
)
| group_by(-.total_score)
| to_entries
| map(.price=$total_price/((.key+1)*2))
| source "${BASE_DIR}/leaderboard/functions.sh" | ||
|
|
||
| # load environement variables | ||
| source "${BASE_DIR}/env.sh" |
There was a problem hiding this comment.
Add a -f check for this file
And exit 1 if it's not present before continuing
There was a problem hiding this comment.
Fail fast is enabled at the top of the main.sh
There was a problem hiding this comment.
I prefer handling personally exeptions and explicit errors messages to have full control when am building blocks in a codebase, but okay... Let's keep it like that.
To go deeper, the source will fails with something like "env.sh No such file or directory" (I guess)... But in an another composes scenario, that error is not going to reflect a user error meaningful enought to fix without relooking what the script does
| @@ -0,0 +1,84 @@ | |||
| MAX_LENGTH=100 | |||
|
🗳️ To have at the end a human-readable data, at the level of the archiving should I convert the JSON to YAML using yq ?
|
|
@Sanix-Darker, feel free to apply your updates in this pr |
There was a problem hiding this comment.
Thanks a lot for submitting this PR.
I went half way through the submission, and I decided to stop because I think that the complexity of this solution, does no go well with the programming languages that chosen (bash).
This is how I think we can do this leaderboard with bash or python using a simpler approach.
1- We will move to an unanonymous poll
2- We will add a new cron job that will request, answers from our users, and update 2 leaderboards.
- An all time leaderboard that aggregates scores for each user since the beginning of time
- A monthly leaderboard that gives us who leads and won for a single month. And is reinitialised each months.
3- That 2nd cronjob could detect that we are at the end of the month, reinitialise the monthly leaderboard and send a message of who is the leader. This could also be done by a 3rd job instead.
This solution seem way simpler and more practical. If we want to do something more complex I would want us to create an telegram app, but I don't think that's needed as our little quizz can work well, with a very small set of features.
| git init | ||
| git remote add origin https://${{ secrets.REPORTQUIZ_DATA_REPO_TOKEN }}@github.com/osscameroon/reportquiz_data.git | ||
| git pull origin main | ||
| git switch -c main |
There was a problem hiding this comment.
This is going to cause a bunch of issues, as the global-github-actions main branch could conflict and create some confusion with the reportquiz_data repo. I recommend using actions/checkout@v4 and clone that repo on a separate path https://github.com/actions/checkout?tab=readme-ov-file#checkout-multiple-repos-side-by-side
There was a problem hiding this comment.
Another question, does the reportquiz_data repo already exist ? If not can we create it, before this PR is merged ?
There was a problem hiding this comment.
This is going to cause a bunch of issues, as the
global-github-actionsmain branch could conflict and create some confusion with thereportquiz_datarepo.
The repo is initialized at the level of the bot folder. I don't think that it can create confusions. 🤔
There was a problem hiding this comment.
Another question, does the
reportquiz_datarepo already exist ? If not can we create it, before this PR is merged ?
It require a dedicated repo for data submissions.
The idea behind is to make the quiz data open source and available for anyone interested to play with it.
And there is also the fact that, I prefer the database and the code stay separate.
| source main.sh | ||
| # update the quiz database | ||
| fetch_quiz_user_answers |
There was a problem hiding this comment.
We seem to be using main.sh as a library here and in several other places. Can we a separate script for each of the function here, or rename main.sh lib.sh, I don't like that last approach but if it that's what you prefer go for it.
| 5. Setting up your bash terminal | ||
|
|
||
| ```bash | ||
| source main.sh |
There was a problem hiding this comment.
It would be nice to avoid loading this in people's local terminal. That's why I recommend having a script for each of the function you want to use.
| . start_quiz_competition | ||
| # Fetch user answers of the qizzes. | ||
| . fetch_quiz_users_answers | ||
| # Stop the current quiz competition and submit resutls. | ||
| . stop_quiz_competition |
There was a problem hiding this comment.
Again that doesn't look like a great way to run scripts, on a local terminal session.
There was a problem hiding this comment.
Can we a separate script for each of the function here, or rename main.sh lib.sh
@rakici
It's to be used in a script.
I used it directly in the github action, because I don't see the utility to create a script to run one command
Should I rename it lib.sh ?
| @@ -0,0 +1,9 @@ | |||
| # compute the user total score from more than one quiz_data | |||
| compute_total_score(){ | |||
| jq -s --argjson total_price 30000 'map((.correct_answers | map(.)) as $answers | (.user_answers // []) | map(.score=(.options_ids | map(select($answers[.] == "true")) | length)-(.options_ids | map(select($answers[.] == "false")) | length) | del(.options_ids))) | add | group_by(.first_name) | map((map(.score) | add) as $total_score | .[0] | del(.score) | .total_score=$total_score) | group_by(-.total_score) | to_entries | map(.price=$total_price/((.key+1)*2))' $@ | |||
There was a problem hiding this comment.
i agree with @Sanix-Darker , this is not easy to maintain. I am pretty sure you can split it into smaller sequential and logical chunks so that other people can understand it quicker. Performance is not an issue here, since this run in a runner on a cron, no one is waiting for this answer to be sent in less than 10 milliseconds. I am more for privileging readability/maintainability over speed here.
You also could write it in multiple lines for example, add some single line comments to your script, use re-usable variables etc which I think are supported by jq.
|
|
||
| # genarate a textual leaderboard with price | ||
| generate_leaderboard(){ | ||
| jq -r '["1st", "2nd", "3rd", "4th", "5th"] as $rangs | ["🥇", "🥈", "🥉", "🎖️"] as $badges | map(.key as $key | [([$rangs[.key], "PLACE", "-", .price, "yotas"] | join(" ")), (.value | map([$badges[$key], .first_name, "("+"@"+.username+")"] | join(" ")) | sort)] | flatten | join("\\n")) | join("\\n\\n")' $1 |
There was a problem hiding this comment.
| jq -r '["1st", "2nd", "3rd", "4th", "5th"] as $rangs | ["🥇", "🥈", "🥉", "🎖️"] as $badges | map(.key as $key | [([$rangs[.key], "PLACE", "-", .price, "yotas"] | join(" ")), (.value | map([$badges[$key], .first_name, "("+"@"+.username+")"] | join(" ")) | sort)] | flatten | join("\\n")) | join("\\n\\n")' $1 | |
| jq -r '["1st", "2nd", "3rd", "4th", "5th"] as $ranks | ["🥇", "🥈", "🥉", "🎖️"] as $badges | map(.key as $key | [([$ranks[.key], "PLACE", "-", .price, "yotas"] | join(" ")), (.value | map([$badges[$key], .first_name, "("+"@"+.username+")"] | join(" ")) | sort)] | flatten | join("\\n")) | join("\\n\\n")' $1 |
| - name: Check the status of the competition | ||
| shell: bash | ||
| run: | | ||
| source main.sh | ||
| # start a new competition each monday morning | ||
| if [ $(date +%a_%p) == Mon_AM ]; then start_quiz_competition; fi | ||
| # end the competition each sunday evening | ||
| if [ $(date +%a_%p) == Sun_PM ]; then | ||
| stop_quiz_competition | ||
| touch /tmp/skip_send_quiz | ||
| fi |
There was a problem hiding this comment.
The competition thing is nice, but is not necessary for this initial version, and I think it makes the bot a little more complicated than it should have been.
There was a problem hiding this comment.
it makes the bot a little more complicated than it should have been.
I don't understand, could you explain?
| # update quiz data based on user answers submission | ||
| fetch_quiz_user_answers(){ | ||
| # require a ongoing competition | ||
| has_ongoing_competition || return 0 |
There was a problem hiding this comment.
I don't understand, you means has_ongoing_competition || return 0 ?
|
Based on your reviews, i begin to think that my code looks complex and not easy to read and maintain. In brief, I think Maybe another scripting programming language like Since, i won't maintain it anymore, i will simplify close the pr. Feel free to take inspiration from it for your next proposal bot. 🙂 |
@elhmn, it's what the bot is doing. Please, Can you check the pr again? Or clarify what is missing? |
eba596d to
1ef97c0
Compare
Related issues
Change
TODO
stderrescpinutils/function.shNotice
I plan to use a git repo as a database. Hence, the report_quiz action will hold a private repo.
git commit -m "Added a new quiz"git commit -m "Added new user answers to quiz:<quiz_id>"git commit -m "Closed quiz:<quiz_id> and generated result"Useful link