Skip to content

feat(report_quiz): add a leaderboard#119

Open
pythonbrad wants to merge 31 commits into
osscameroon:mainfrom
pythonbrad:reportquiz_leaderboard
Open

feat(report_quiz): add a leaderboard#119
pythonbrad wants to merge 31 commits into
osscameroon:mainfrom
pythonbrad:reportquiz_leaderboard

Conversation

@pythonbrad

@pythonbrad pythonbrad commented Aug 23, 2024

Copy link
Copy Markdown
Member

Related issues

Change

  • removed the anonymized to be able to get user answers
  • restructured the code to apply the DRY principle
  • saved the quiz data in order to be able to get poll results

TODO

  • close the quiz and get user answers
  • fetch quiz user answers in a database
  • generate a leaderboard
  • sanction / mention user who retract their answers
  • log to stderr
  • put reusable functions like escp in utils/function.sh
  • announce the result (message)
  • Review the workflow file

Notice

I plan to use a git repo as a database. Hence, the report_quiz action will hold a private repo.

  • The created quiz information will be committed in the git repo
    git commit -m "Added a new quiz"
  • The user answers will be committed in the git repo
    git commit -m "Added new user answers to quiz:<quiz_id>"
  • The final quiz stats will be committed in the git repo
    git commit -m "Closed quiz:<quiz_id> and generated result"

Useful link

pythonbrad and others added 3 commits August 22, 2024 21:03
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
@pythonbrad
pythonbrad force-pushed the reportquiz_leaderboard branch 2 times, most recently from 3280748 to 48e548f Compare August 24, 2024 10:34
@pythonbrad
pythonbrad force-pushed the reportquiz_leaderboard branch from 48e548f to d3cc3da Compare August 24, 2024 10:53
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
@pythonbrad
pythonbrad marked this pull request as ready for review August 26, 2024 19:32
@pythonbrad

Copy link
Copy Markdown
Member Author

@Sanix-Darker , @elhmn , this pr is ready for review.

HanslettTheDev and others added 10 commits October 1, 2024 12:52
Added a documentation for the report quiz bot
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 Sanix-Darker left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hey there, thanks for this contribution, can you provide a video demo or workflow on your PR description please ?

Comment thread .github/workflows/report_quiz/README.md
Comment thread .github/workflows/report_quiz/README.md
Comment thread .github/workflows/report_quiz/common/utils.sh
Comment thread .github/workflows/report_quiz/leaderboard/functions.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))' $@

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is too compact, even tho it's understandable, make it digest please, not in a single line

@pythonbrad pythonbrad Oct 6, 2024

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This jq command reflects the function name and his comment.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@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"

@Sanix-Darker Sanix-Darker Oct 6, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Add a -f check for this file
And exit 1 if it's not present before continuing

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fail fast is enabled at the top of the main.sh

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Comment thread .github/workflows/report_quiz/main.sh
@@ -0,0 +1,84 @@
MAX_LENGTH=100

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

MAX_LENGTH_OF... ???

@pythonbrad

pythonbrad commented Oct 8, 2024

Copy link
Copy Markdown
Member Author

🗳️ To have at the end a human-readable data, at the level of the archiving should I convert the JSON to YAML using yq ?
Note:

  • yq is able to read the JSON as input.
  • yq is not fully jq query compatible.

@pythonbrad

Copy link
Copy Markdown
Member Author

@Sanix-Darker, feel free to apply your updates in this pr

@pythonbrad pythonbrad changed the title feat(report_bot): add a leaderboard feat(report_quiz): add a leaderboard Nov 7, 2024

@rakici rakici left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +28 to +31
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Another question, does the reportquiz_data repo already exist ? If not can we create it, before this PR is merged ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

The repo is initialized at the level of the bot folder. I don't think that it can create confusions. 🤔

@pythonbrad pythonbrad Nov 11, 2024

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Another question, does the reportquiz_data repo 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.

Comment on lines +36 to +38
source main.sh
# update the quiz database
fetch_quiz_user_answers

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +50 to +54
. start_quiz_competition
# Fetch user answers of the qizzes.
. fetch_quiz_users_answers
# Stop the current quiz competition and submit resutls.
. stop_quiz_competition

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Again that doesn't look like a great way to run scripts, on a local terminal session.

@pythonbrad pythonbrad Nov 11, 2024

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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))' $@

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
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

Comment thread .github/workflows/report_a_quiz.yaml Outdated
Comment on lines +40 to +50
- 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@pythonbrad pythonbrad Nov 11, 2024

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We don't need that.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't understand, you means has_ongoing_competition || return 0 ?

@pythonbrad

Copy link
Copy Markdown
Member Author

Based on your reviews, i begin to think that my code looks complex and not easy to read and maintain.

In brief, I think bash was not a good choice for this project.

Maybe another scripting programming language like python will be great. 🤔

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. 🙂

@pythonbrad pythonbrad closed this Nov 11, 2024
@pythonbrad

Copy link
Copy Markdown
Member Author

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.

@elhmn, it's what the bot is doing.

Please, Can you check the pr again? Or clarify what is missing?

@pythonbrad pythonbrad reopened this Jul 19, 2026
@pythonbrad
pythonbrad force-pushed the reportquiz_leaderboard branch from eba596d to 1ef97c0 Compare July 20, 2026 12:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants