Skip to content

Commit

Permalink
💻 hide all quizes/parsons and show tabs in levels (#5156)
Browse files Browse the repository at this point in the history
Fixes #5141 

**Key points**
- a quize/parson is added as a tab in the customize-class page. This makes it closer to how it'd look like for students.
- the current "hide quize/puzzle" switches will behave as "toggle all quizes/puzzles". Why? Consider the following:
  - the teacher decides to hide all quizes. 
  - they are removed from all levels,
  - they can of course still choose to show a quize in certain levels, while keeping the rest hidden.
  - however, what if they (for whatever reason) want to show all quizes but hide one or two?
  - this scenario is likely because until now the "hide quize" behaves as toggling all quizes, so it should still behave similarly.
  - thus it could be considered as resetting all quizes/puzzles.
- the order of puzzle and quiz tabs are always fixed to be always the last two tabs, respectively.

Something went missing when i added the key translations for #5026 (check 6ee2c4f#diff-852046eb47f474139974edf5e69ed9758f9fde82e1432c49d772b89aef354b37), so I'm adding them here.

**How to test?**

- go the customize-class page
- add/remove a quiz/puzzle
- reset all puzzles/quizes to test that functionality.
   - _note_ that if you disable all quizes and you add one quiz in level 2, for instance, the disable checkbox will be updated since not all quizes are disabled now. (it needs a refresh for now for that to be reflected in the DOM, but this should be dealt with in #4888)
- log in as a student or preview as teacher to see  your changes from the perspective of students.
- verify that everything works as expected.
  • Loading branch information
hasan-sh authored Mar 7, 2024
1 parent 69ca645 commit 0297631
Show file tree
Hide file tree
Showing 59 changed files with 1,927 additions and 1,081 deletions.
82 changes: 63 additions & 19 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1355,6 +1355,7 @@ def hour_of_code(level, program_id=None):
@app.route('/ontrack', methods=['GET'], defaults={'level': '1', 'program_id': None})
@app.route('/onlinemasters', methods=['GET'], defaults={'level': '1', 'program_id': None})
@app.route('/onlinemasters/<int:level>', methods=['GET'], defaults={'program_id': None})
@app.route('/hedy', methods=['GET'], defaults={'program_id': None, 'level': '1'})
@app.route('/hedy/<int:level>', methods=['GET'], defaults={'program_id': None})
@app.route('/hedy/<int:level>/<program_id>', methods=['GET'])
def index(level, program_id):
Expand Down Expand Up @@ -1404,39 +1405,80 @@ def index(level, program_id):
# - The level is allowed and available
# - But, if there is a quiz threshold we have to check again if the user has reached it

parsons_in_level = True
quiz_in_level = True
if customizations.get("sorted_adventures") and len(customizations["sorted_adventures"]) > 2:
parsons_in_level = [adv for adv in customizations["sorted_adventures"][str(level)][-2:]
if adv.get("name") == "parsons"]
quiz_in_level = [adv for adv in customizations["sorted_adventures"][str(level)][-2:]
if adv.get("name") == "quiz"]

if 'level_thresholds' in customizations:
show_quiz = 'other_settings' in customizations and 'hide_quiz' not in customizations['other_settings']
if show_quiz and 'quiz' in customizations.get('level_thresholds'):
# If quiz in level and in some of the previous levels, then we check the threshold level.
check_threshold = 'other_settings' in customizations and 'hide_quiz' not in customizations['other_settings']

if check_threshold and 'quiz' in customizations.get('level_thresholds'):

# Temporary store the threshold
threshold = customizations.get('level_thresholds').get('quiz')
level_quiz_data = QUIZZES[g.lang].get_quiz_data_for_level(level)
# Get the max quiz score of the user in the previous level
# A bit out-of-scope, but we want to enable the next level button directly after finishing the quiz
# Todo: How can we fix this without a re-load?
quiz_stats = DATABASE.get_quiz_stats([current_user()['username']])

previous_quiz_level = level
for _prev_level in range(level - 1, 0, -1):
if _prev_level in available_levels and \
customizations["sorted_adventures"][str(_prev_level)][-1].get("name") == "quiz" and \
not any(x.get("scores") for x in quiz_stats if x.get("level") == _prev_level):
previous_quiz_level = _prev_level
break

# Not current leve-quiz's data because some levels may have no data for quizes,
# but we still need to check for the threshold.
if level - 1 in available_levels and level > 1 and \
(not level_quiz_data or QUIZZES[g.lang].get_quiz_data_for_level(level - 1)):
scores = [x.get('scores', []) for x in quiz_stats if x.get('level') == level - 1]
scores = [score for week_scores in scores for score in week_scores]
max_score = 0 if len(scores) < 1 else max(scores)
if max_score < threshold:
return utils.error_page(
error=403, ui_message=gettext('quiz_threshold_not_reached'))

# Only if we have found a quiz in previous levels with quiz data, we check the threshold.
if previous_quiz_level < level:
# scores = [x.get('scores', []) for x in quiz_stats if x.get('level') == level - 1]
scores = [x.get('scores', []) for x in quiz_stats if x.get('level') == previous_quiz_level]
scores = [score for week_scores in scores for score in week_scores]
max_score = 0 if len(scores) < 1 else max(scores)
if max_score < threshold:
# Instead of sending this level isn't available, we could send them to the right level?!
# return redirect(f"/hedy/{previous_quiz_level}")
return utils.error_page(
error=403, ui_message=gettext('quiz_threshold_not_reached'))

# We also have to check if the next level should be removed from the available_levels
# Only check the quiz threshold if there is a quiz to obtain a score on the current level
if level <= hedy.HEDY_MAX_LEVEL and level_quiz_data:
scores = [x.get('scores', []) for x in quiz_stats if x.get('level') == level]
scores = [score for week_scores in scores for score in week_scores]
max_score = 0 if len(scores) < 1 else max(scores)
# We don't have the score yet for the next level -> remove all upcoming
# levels from 'available_levels'
if max_score < threshold:
# if this level is currently available, but score is below max score
customizations["below_threshold"] = (level + 1 in available_levels)
available_levels = available_levels[:available_levels.index(level) + 1]
next_level_with_quiz = level - 1
for _next_level in range(level, hedy.HEDY_MAX_LEVEL):
# find the next level whose quiz isn't answered.
if _next_level in available_levels and \
customizations["sorted_adventures"][str(_next_level)][-1].get("name") == "quiz" and \
not any(x.get("scores") for x in quiz_stats if x.get("level") == _next_level):
next_level_with_quiz = _next_level
break

# If the next quiz is in the current or upcoming level,
# we attempt to adjust available levels beginning from that level.
# e.g., student2 completed quiz 2, levels 3,4 and 5 have not quizes, 6 does.
# We should start from that level. If next_level_with_quiz >= level,
# meaning we don't need to adjust available levels ~ all available/quizes done!
if next_level_with_quiz >= level:
scores = [x.get('scores', []) for x in quiz_stats if x.get('level') == next_level_with_quiz]
scores = [score for week_scores in scores for score in week_scores]
max_score = 0 if len(scores) < 1 else max(scores)
# We don't have the score yet for the next level -> remove all upcoming
# levels from 'available_levels'
if max_score < threshold:
# if this level is currently available, but score is below max score
customizations["below_threshold"] = (next_level_with_quiz + 1 in available_levels)
available_levels = available_levels[:available_levels.index(next_level_with_quiz) + 1]

# Add the available levels to the customizations dict -> simplify
# implementation on the front-end
Expand Down Expand Up @@ -1485,9 +1527,11 @@ def index(level, program_id):
if parsons:
parson_exercises = len(PARSONS[g.lang].get_parsons_data_for_level(level))

if 'other_settings' in customizations and 'hide_parsons' in customizations['other_settings']:
if not parsons_in_level or 'other_settings' in customizations and \
'hide_parsons' in customizations['other_settings']:
parsons = False
if 'other_settings' in customizations and 'hide_quiz' in customizations['other_settings']:
if not quiz_in_level or 'other_settings' in customizations and \
'hide_quiz' in customizations['other_settings']:
quiz = False

max_level = hedy.HEDY_MAX_LEVEL
Expand Down
63 changes: 46 additions & 17 deletions hedy_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@
'turtle_draw_it',
'restaurant',
'fortune',
'debugging'
'debugging',
'parsons',
'quiz',
],
2: [
'default',
Expand All @@ -63,7 +65,9 @@
'restaurant',
'turtle',
'turtle_draw_it',
'debugging'
'debugging',
'parsons',
'quiz',
],
3: [
'default',
Expand All @@ -80,7 +84,9 @@
'haunted',
'turtle',
'turtle_draw_it',
'debugging'
'debugging',
'parsons',
'quiz',
],
4: [
'default',
Expand All @@ -97,7 +103,9 @@
'haunted',
'fortune',
'restaurant',
'debugging'
'debugging',
'parsons',
'quiz',
],
5: [
'default',
Expand All @@ -116,7 +124,9 @@
'pressit',
'turtle',
'turtle_draw_it',
'debugging'
'debugging',
'parsons',
'quiz',
],
6: [
'default',
Expand All @@ -131,7 +141,9 @@
'calculator',
'fortune',
'restaurant',
'debugging'
'debugging',
'parsons',
'quiz',
],
7: [
'default',
Expand All @@ -146,7 +158,9 @@
'restaurant',
'pressit',
'turtle_draw_it',
'debugging'
'debugging',
'parsons',
'quiz',
],
8: [
'default',
Expand All @@ -161,7 +175,9 @@
'restaurant',
'turtle',
'turtle_draw_it',
'debugging'
'debugging',
'parsons',
'quiz',
],
9: [
'default',
Expand All @@ -176,7 +192,9 @@
'pressit',
'turtle',
'turtle_draw_it',
'debugging'
'debugging',
'parsons',
'quiz',
],
10: [
'default',
Expand All @@ -192,7 +210,9 @@
'rock',
'calculator',
'restaurant',
'debugging'
'debugging',
'parsons',
'quiz',
],
11: [
'default',
Expand All @@ -203,7 +223,9 @@
'restaurant',
'haunted',
'turtle_draw_it',
'debugging'
'debugging',
'parsons',
'quiz',
],
12: [
'default',
Expand All @@ -221,7 +243,9 @@
'piggybank',
'secret',
'turtle_draw_it',
'debugging'
'debugging',
'parsons',
'quiz',
],
13: [
'default',
Expand All @@ -235,7 +259,8 @@
'restaurant',
'calculator',
'tic',
'debugging'
'debugging',
'quiz',
],
14: [
'default',
Expand All @@ -251,7 +276,8 @@
'piggybank',
'quizmaster',
'tic',
'debugging'
'debugging',
'quiz',
],
15: [
'default',
Expand All @@ -263,23 +289,26 @@
'rock',
'calculator',
'tic',
'debugging'
'debugging',
'quiz',
],
16: [
'default',
'random_command',
'haunted',
'songs',
'language',
'debugging'
'debugging',
'quiz',
],
17: [
'default',
'for_command',
'elif_command',
'tic',
'blackjack',
'debugging'
'debugging',
'quiz',
],
18: [
'default',
Expand Down
20 changes: 10 additions & 10 deletions messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-02-13 11:36+0100\n"
"POT-Creation-Date: 2024-02-28 13:55+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
Expand Down Expand Up @@ -71,6 +71,9 @@ msgstr ""
msgid "Missing Inner Command"
msgstr ""

msgid "Missing Square Brackets"
msgstr ""

msgid "Missing Variable"
msgstr ""

Expand Down Expand Up @@ -200,9 +203,6 @@ msgstr ""
msgid "adventure_updated"
msgstr ""

msgid "adventures"
msgstr ""

msgid "adventures_info"
msgstr ""

Expand Down Expand Up @@ -500,6 +500,12 @@ msgstr ""
msgid "disable"
msgstr ""

msgid "disable_parsons"
msgstr ""

msgid "disable_quizes"
msgstr ""

msgid "disabled"
msgstr ""

Expand Down Expand Up @@ -707,12 +713,6 @@ msgstr ""
msgid "hide_keyword_switcher"
msgstr ""

msgid "hide_parsons"
msgstr ""

msgid "hide_quiz"
msgstr ""

msgid "highest_level_reached"
msgstr ""

Expand Down
4 changes: 2 additions & 2 deletions templates/customize-class.html
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,14 @@ <h3 class="px-4"><u>{{_('other_settings')}}</u></h3>
</td>
</tr>
<tr>
<td class="text-left border-t border-r border-gray-400">{{_('hide_quiz')}}</td>
<td class="text-left border-t border-r border-gray-400">{{_('disable_quizes')}}</td>
<td class="border-t border-gray-400">
<input class="other_settings_checkbox" id="hide_quiz" type="checkbox" {%
if "hide_quiz" in customizations['other_settings'] %}checked{% endif %}>
</td>
</tr>
<tr>
<td class="text-left border-t border-r border-gray-400">{{_('hide_parsons')}}</td>
<td class="text-left border-t border-r border-gray-400">{{_('disable_parsons')}}</td>
<td class="border-t border-gray-400">
<input class="other_settings_checkbox" id="hide_parsons" type="checkbox" {%
if "hide_parsons" in customizations['other_settings'] %}checked{% endif %}>
Expand Down
5 changes: 4 additions & 1 deletion templates/customize-class/partial-sortable-adventures.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ <h3 class="flex-1">{{_('select_adventures')}}</h3>
hx-include="[name='adventure']"
hx-indicator="#indicator">
{% for adventure in adventures[level|int] %}
<div class="tab {% if adventure.is_teacher_adventure %}teacher_tab{% elif adventure.is_command_adventure %}command_tab{% endif %} z-10 whitespace-nowrap flex items-center justify-left relative" data-cy="{{ adventure.short_name }}">
<div class="tab
{% if adventure.is_teacher_adventure %}teacher_tab{% elif adventure.is_command_adventure %}command_tab{% endif %}
{% if adventure.short_name in ['quiz', 'parsons'] %}special_tab{% endif %}
z-10 whitespace-nowrap flex items-center justify-left relative" data-cy="{{ adventure.short_name }}">
<span class="absolute top-0.5 right-0.5 text-gray-600 hover:text-red-400 fa-regular fa-circle-xmark" data-cy="hide"
hx-post="/for-teachers/remove-adventure?adventure_id={{ adventure.short_name }}&level={{ level }}"
hx-target="#adventure-dragger"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ describe('customize class page', () => {
.should('have.length', startLength);

// the adventure should now be last
cy.get('[data-cy="level-2"] div:last input')
.should('have.value', `${adventure}`)
cy.get(`[data-cy="level-2"] div[data-cy="${adventure}"`)
.should("exist")
});
});
});
Expand Down
Loading

0 comments on commit 0297631

Please sign in to comment.