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

feat: custom instruction and hint UI #1646

Merged
merged 48 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
073cc58
add instruction and hint tabs
evemartin Jun 6, 2024
b0019bd
begin work on custom instruction and hint
evemartin Jun 10, 2024
eeeb016
fix unusual error
evemartin Jun 10, 2024
8f15ea6
display hint after certain number of attempts
evemartin Jun 10, 2024
ebac712
add timed hint, remove redundant code
evemartin Jun 10, 2024
8dcbc0f
implement new fields
evemartin Jun 10, 2024
aa29c96
add fields
evemartin Jun 10, 2024
f4b5929
Merge branch 'custom-level-introduction-and-hint' into custom-level-i…
evemartin Jun 10, 2024
ab96bc0
load values in properly
evemartin Jun 10, 2024
4bc3f34
make small change to the way the lesson and hint are saved
evemartin Jun 10, 2024
5fa49f4
Merge branch 'custom-level-introduction-and-hint' into custom-level-i…
evemartin Jun 10, 2024
15c7905
debug tests
evemartin Jun 10, 2024
a9d1616
Merge branch 'custom-level-introduction-and-hint' into custom-level-i…
evemartin Jun 10, 2024
4fa3b48
write end to end test
evemartin Jun 10, 2024
14f2d16
merge in master and backend
evemartin Jun 10, 2024
a22554c
Merge branch 'master' into custom-instruction-and-hint-ui
evemartin Jun 10, 2024
f3e11ac
debug test
evemartin Jun 10, 2024
648a92f
debug test
evemartin Jun 10, 2024
1f782c3
debug test
evemartin Jun 10, 2024
0731847
debug test
evemartin Jun 10, 2024
9de747e
debug test
evemartin Jun 10, 2024
05d6248
debug test
evemartin Jun 10, 2024
965f4f2
fix containment checks in test
evemartin Jun 11, 2024
6725898
test out a change to the test
evemartin Jun 11, 2024
75668ac
test out another change to the test
evemartin Jun 11, 2024
a1242ec
debug test
evemartin Jun 11, 2024
be63ac0
add locator instead of element
evemartin Jun 11, 2024
be8ad84
add additional check
evemartin Jun 11, 2024
d00a829
change clicked element
evemartin Jun 11, 2024
9214a33
check for visibility not presence
evemartin Jun 11, 2024
032d784
check visibility not presence pt. 2
evemartin Jun 11, 2024
a6a97da
fix typo, make code consistent
evemartin Jun 11, 2024
ad08e23
make small changes to test
evemartin Jun 11, 2024
7c30e3f
change aim to subtitle, reformat initial modal
evemartin Jun 14, 2024
a72848a
get rid of configurable timer and trigger fields
evemartin Jun 14, 2024
a5cd859
add hint button to failure modal
evemartin Jun 17, 2024
4ce21c5
update text and test
evemartin Jun 17, 2024
415a9ff
debug test
evemartin Jun 17, 2024
9c7ba6c
add extra click to test
evemartin Jun 17, 2024
003f97b
fix hint tab text
evemartin Jun 18, 2024
83bd2a4
fix text
evemartin Jun 18, 2024
67fce64
Merge branch 'master' into custom-instruction-and-hint-ui
evemartin Jun 18, 2024
490e75f
fix migrations
evemartin Jun 18, 2024
ac6be36
address PR comments
evemartin Jun 19, 2024
c2a58b5
address PR comments
evemartin Jun 20, 2024
11fd20f
address PR comments
evemartin Jun 24, 2024
045cd22
addressed PR comment
evemartin Jun 24, 2024
8ecccff
address PR comments
evemartin Jun 24, 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
9 changes: 9 additions & 0 deletions game/end_to_end_tests/editor_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,12 @@ def go_to_code_tab(self):

def go_to_scenery_tab(self):
self.browser.find_element(By.ID, "scenery_tab").click()

def go_to_instruction_tab(self):
self.browser.find_element(By.ID, "instruction_tab").click()

def go_to_hint_tab(self):
self.browser.find_element(By.ID, "hint_tab").click()

def go_to_save_tab(self):
self.browser.find_element(By.ID, "save_tab").click()
69 changes: 69 additions & 0 deletions game/end_to_end_tests/test_level_editor.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import Select
from selenium.webdriver.support.ui import WebDriverWait

from game.end_to_end_tests.base_game_test import BaseGameTest
from game.views.level_editor import available_blocks

DELAY_TIME = 10


class TestLevelEditor(BaseGameTest):
def set_up_basic_map(self):
Expand Down Expand Up @@ -129,3 +134,67 @@ def test_draggable_traffic_light(self):
cloned_source_light = self.selenium.find_elements(By.ID, "trafficLightRed")
assert len(scenery_light) == 1
assert len(cloned_source_light) == 1

def test_custom_instruction_and_hint(self):
# login
self.login_once()

# go to level editor and set up basic map
page = self.go_to_level_editor()
[road_start, road_end] = self.set_up_basic_map()

# fill in custom instruction and hint fields
page.go_to_instruction_tab()
self.selenium.find_element(By.ID, "subtitle").send_keys("test subtitle")
self.selenium.find_element(By.ID, "instruction").send_keys("test lesson")

page.go_to_hint_tab()
self.selenium.find_element(By.ID, "hint").send_keys("test hint")

# save level and choose to play it
page.go_to_save_tab()
self.selenium.find_element(By.ID, "levelNameInput").send_keys("test level")
self.selenium.find_element(By.ID, "saveLevel").click()
assert WebDriverWait(self.selenium, DELAY_TIME).until(
EC.presence_of_element_located((By.ID, "play_button"))
)
self.selenium.find_element(By.ID, "play_button").click()

# check to see if custom subtitle and lesson appear on the initial popup
assert WebDriverWait(self.selenium, DELAY_TIME).until(
EC.presence_of_element_located((By.ID, "myModal-lead"))
)
modal_text = self.selenium.find_element(By.ID, "myModal-lead").get_attribute("innerHTML")
assert "test subtitle" in modal_text
assert "test lesson" in modal_text
self.selenium.find_element(By.ID, "close-modal").click()

# wait for modal to disappear
assert WebDriverWait(self.selenium, DELAY_TIME).until(
EC.none_of(EC.visibility_of_all_elements_located((By.ID, "myModal-mainText")))
)

# check to see if the custom hint appears on failure modal
fast_tab = self.selenium.find_element(By.ID, "fast_tab")
fast_tab.click()
assert WebDriverWait(self.selenium, DELAY_TIME).until(
EC.visibility_of_element_located((By.ID, "hintPopupBtn"))
)
hint_button = self.selenium.find_element(By.ID, "hintPopupBtn")
hint_button.click()

hint_modal_text = self.selenium.find_element(By.ID, "hintText").get_attribute("innerHTML")
hint_modal_style = self.selenium.find_element(By.ID, "hintText").get_attribute("style")
assert "display: none" in hint_button.get_attribute("style")
assert "display: block" in hint_modal_style
assert "test hint" in hint_modal_text

self.selenium.find_element(By.ID, "try_again_button").click()

# check to see if the custom hint appears on the hint popup
self.selenium.find_element(By.ID, "help_tab").click()
assert WebDriverWait(self.selenium, DELAY_TIME).until(
EC.visibility_of_element_located((By.ID, "myModal-mainText"))
)
hint_modal_text_three = self.selenium.find_element(By.ID, "myModal-mainText").get_attribute("innerHTML")
assert "test hint" in hint_modal_text_three
3 changes: 3 additions & 0 deletions game/level_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ def save_level(level, data):
level.theme = get_theme_by_pk(pk=data["theme"])
level.character = get_character_by_pk(pk=data["character"])
level.disable_algorithm_score = data.get("disable_algorithm_score", False)
level.subtitle = data.get("subtitle")
level.lesson = data.get("lesson")
level.hint = data.get("hint")
level.save()

set_decor(level, data["decor"])
Expand Down
4 changes: 4 additions & 0 deletions game/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ def description_level_default():
return "Can you find the shortest route?"


def subtitle_level_default():
return ""


def hint_level_default():
return "Think back to earlier levels. What did you learn?"

Expand Down
28 changes: 28 additions & 0 deletions game/migrations/0093_add_instruction_and_hint_to_levels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 3.2.25 on 2024-06-10 09:41

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('game', '0092_disable_algo_score_in_custom_levels'),
]

operations = [
migrations.AddField(
model_name='level',
name='subtitle',
field=models.TextField(max_length=10000, null=True),
),
migrations.AddField(
model_name='level',
name='hint',
field=models.TextField(max_length=10000, null=True),
),
migrations.AddField(
model_name='level',
name='lesson',
field=models.TextField(max_length=10000, null=True),
),
]
3 changes: 3 additions & 0 deletions game/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ class Level(models.Model):
character_name = models.CharField(
max_length=20, choices=character_choices(), blank=True, null=True, default=None
)
subtitle = models.TextField(max_length=10000, null=True)
lesson = models.TextField(max_length=10000, null=True)
hint = models.TextField(max_length=10000, null=True)
anonymous = models.BooleanField(default=False)
locked_for_class = models.ManyToManyField(
Class, blank=True, related_name="locked_levels"
Expand Down
1 change: 1 addition & 0 deletions game/static/game/image/icons/hint.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions game/static/game/image/icons/instruction.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion game/static/game/js/animation.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,9 +350,11 @@ ocargo.Animation.prototype.performAnimation = function(animation) {
}
var otherMsg = "";
if (animation.popupHint) {
buttons += '<button class="navigation_button long_button" id="hintPopupBtn"><span>' + gettext('Are you stuck? Do you need help?') + '</span></button>';
console.log("popuphint");
buttons += '<button class="navigation_button long_button" id="hintPopupBtn"><span>' + gettext('Show hint') + '</span></button>';
otherMsg = '<div id="hintBtnPara">' + '</div><div id="hintText">' + HINT + '</div>';
}
console.log(buttons);
ocargo.Drawing.startPopup(title, leadMsg, otherMsg, true, buttons);
if (animation.popupHint) {
$("#hintPopupBtn").click( function(){
Expand Down
4 changes: 4 additions & 0 deletions game/static/game/js/drawing.js
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,10 @@ ocargo.Drawing.startPopup = function (
playButton.append(icons[2])
buttonDiv.append(playButton)

let hintPopupButton = $("#hintPopupBtn")
hintPopupButton.removeClass().addClass("navigation_button_portal long_button")
buttonDiv.append(hintPopupButton)

$("#modal-buttons").html(buttonDiv)
} else {
$('#modal-buttons').html(
Expand Down
5 changes: 3 additions & 2 deletions game/static/game/js/game.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ ocargo.Game = function () {
this.failures = 0
this.currentlySelectedTab = null
this.isMuted = Cookies.get('muted') === 'true'
this.hasTimedHintAppeared = false
}

ocargo.Game.prototype.setup = function () {
Expand Down Expand Up @@ -131,7 +132,7 @@ ocargo.Game.prototype.setup = function () {

ocargo.Drawing.startPopup(
title,
LESSON,
"<b>" + SUBTITLE + "</b> <br> <br> " + LESSON,
message,
showMascot,
[
Expand Down Expand Up @@ -285,7 +286,7 @@ ocargo.Game.prototype.sendAttempt = function (score) {

ocargo.Game.prototype.registerFailure = function () {
this.failures += 1
return this.failures >= 3
return this.failures >= 1
}

ocargo.Game.prototype._setupFuelGauge = function (nodes, blocks) {
Expand Down
26 changes: 26 additions & 0 deletions game/static/game/js/level_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ ocargo.LevelEditor = function(levelId) {
tabs.character = new ocargo.Tab($('#character_radio'), $('#character_radio + label'), $('#character_pane'));
tabs.blocks = new ocargo.Tab($('#blocks_radio'), $('#blocks_radio + label'), $('#blocks_pane'));
tabs.random = new ocargo.Tab($('#random_radio'), $('#random_radio + label'), $('#random_pane'));
tabs.instruction = new ocargo.Tab($('#instruction_radio'), $('#instruction_radio + label'), $('#instruction_pane'));
tabs.hint = new ocargo.Tab($('#hint_radio'), $('#hint_radio + label'), $('#hint_pane'));
tabs.load = new ocargo.Tab($('#load_radio'), $('#load_radio + label'), $('#load_pane'));
tabs.save = new ocargo.Tab($('#save_radio'), $('#save_radio + label'), $('#save_pane'));
tabs.share = new ocargo.Tab($('#share_radio'), $('#share_radio + label'), $('#share_pane'));
Expand All @@ -204,6 +206,8 @@ ocargo.LevelEditor = function(levelId) {
setupCharacterTab();
setupBlocksTab();
setupRandomTab();
setupInstructionTab();
setupHintTab();
setupLoadTab();
setupSaveTab();
setupShareTab();
Expand Down Expand Up @@ -477,6 +481,18 @@ ocargo.LevelEditor = function(levelId) {
});
}

function setupInstructionTab() {
tabs.instruction.setOnChange(function() {
transitionTab(tabs.instruction);
});
}

function setupHintTab() {
tabs.hint.setOnChange(function() {
transitionTab(tabs.hint);
});
}

function goToMapTab() {
tabs.map.select();
}
Expand Down Expand Up @@ -2482,6 +2498,11 @@ ocargo.LevelEditor = function(levelId) {
state.pythonViewEnabled = language === 'blocklyWithPythonView';
state.pythonEnabled = language === 'python' || language === 'both';

// Instruction and hint data
state.subtitle = $('#subtitle').val();
state.lesson = $('#instruction').val();
state.hint = $('#hint').val();

// Other data
state.theme = currentTheme.id;
state.character = $('#character_select').val();
Expand Down Expand Up @@ -2610,6 +2631,11 @@ ocargo.LevelEditor = function(levelId) {
}
languageSelect.change();

// Load in instruction and hint data
$('#subtitle').val(state.subtitle);
$('#instruction').val(state.lesson);
$('#hint').val(state.hint);

// Other data
if(state.max_fuel) {
$('#max_fuel').val(state.max_fuel);
Expand Down
1 change: 1 addition & 0 deletions game/templates/game/game.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
var MODEL_SOLUTION = {{model_solution|default:"[]"}}
var DISABLE_ROUTE_SCORE = {{level.disable_route_score|booltojs}}
var DISABLE_ALGORITHM_SCORE = {{level.disable_algorithm_score|booltojs}}
var SUBTITLE = "{{subtitle|escapejs}}"
var LESSON = "{{lesson|escapejs}}"
var HINT = "{{hint|escapejs}}"
var DEFAULT_LEVEL = {{level.default|booltojs}}
Expand Down
43 changes: 43 additions & 0 deletions game/templates/game/level_editor.html
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,22 @@
</label>
</div>

<div id="instruction_tab" class="tab selectable">
<input type="radio" name="tabs" id="instruction_radio">
<label for="instruction_radio">
<img src='{% static "game/image/icons/instruction.svg" %}'>
<span>{% trans "Instruction" %}</span>
</label>
</div>

<div id="hint_tab" class="tab selectable">
<input type="radio" name="tabs" id="hint_radio">
<label for="hint_radio">
<img src='{% static "game/image/icons/hint.svg" %}'>
<span>{% trans "Hint" %}</span>
</label>
</div>

<div class="tab_break"></div>

<div id="load_tab" class="tab selectable">
Expand Down Expand Up @@ -477,6 +493,33 @@ <h2 class="title"><img class="modal_image" src='{% static "game/image/icons/rand
</div>
</div>

<div id="instruction_pane" class="tab_pane">
<h2 class="title"><img class="modal_image" src='{% static "game/image/icons/instruction.svg" %}'>{% trans "Instruction" %}</h2>
<p> {% trans "Give this level a subtitle and some instructions on what to do within this level for your students." %} </p>
<p> {% trans "Students will see this subtitle and the instructions when <b>starting this level</b> so make sure they are <b>useful to the student</b>." %} </p>
<div class="tab_pane_content_holder">
<div class="tab_pane_content">
<p>{% trans "<b>Subtitle</b><br>What is the subtitle of this level?" %}</p>
<textarea id="subtitle" rows="4" cols="50"></textarea>

<p>{% trans "<b>Instruction</b><br>What do players have to do to complete this level?" %}</p>
<textarea id="instruction" rows="4" cols="50"></textarea>
</div>
</div>
</div>

<div id="hint_pane" class="tab_pane">
<h2 class="title"><img class="modal_image" src='{% static "game/image/icons/hint.svg" %}'>{% trans "Hint" %}</h2>
<p> {% trans "Help out your players by adding hints! Players will have the option to view a hint when they have made an unsuccessful attempt." %} </p>
<p> {% trans "Players can also access hints by clicking the hint button whilst playing." %} </p>
<div class="tab_pane_content_holder">
<div class="tab_pane_content">
<p>{% trans "Hint" %}</p>
<textarea id="hint" rows="4" cols="50"></textarea>
</div>
</div>
</div>

<div id="load_pane" class="tab_pane">
<h2 class="title"><img class="modal_image" src='{% static "game/image/icons/load.svg" %}'>{% trans "Load" %}</h2>
<p>{% trans "Here you can load in levels created by you and your friends! Select a level in the table and press load." %}</p>
Expand Down
8 changes: 5 additions & 3 deletions game/views/level.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,9 @@ def play_level(request, level, from_editor=False):
lessonCall = messages.description_level_default
hintCall = messages.hint_level_default

lesson = mark_safe(lessonCall())
hint = mark_safe(hintCall())
subtitle = level.subtitle or messages.subtitle_level_default
lesson = level.lesson or mark_safe(lessonCall())
hint = level.hint or mark_safe(hintCall())

character = level.character
character_url = character.top_down
Expand Down Expand Up @@ -233,14 +234,15 @@ def play_level(request, level, from_editor=False):
"game/game.html",
context={
"level": level,
"subtitle": subtitle,
"lesson": lesson,
"hint": hint,
"blocks": block_data,
"decor": decor_data,
"character": character,
"background": background,
"house": house,
"cfc": cfc,
"hint": hint,
"workspace": workspace,
"python_workspace": python_workspace,
"return_url": reverse(return_view),
Expand Down
8 changes: 5 additions & 3 deletions game/views/level_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ def play_anonymous_level(request, levelId, from_level_editor=True, random_level=
if not level.anonymous:
return redirect(reverse("level_editor"), permanent=True)

lesson = mark_safe(messages.description_level_default())
hint = mark_safe(messages.hint_level_default())
subtitle = level.subtitle or mark_safe(messages.subtitle_level_default())
lesson = level.lesson or mark_safe(messages.description_level_default())
hint = level.hint or mark_safe(messages.hint_level_default())

attempt = None
house = get_decor_element("house", level.theme).url
Expand Down Expand Up @@ -123,12 +124,13 @@ def play_anonymous_level(request, levelId, from_level_editor=True, random_level=
"level": level,
"decor": decor_data,
"blocks": block_data,
"subtitle": subtitle,
"lesson": lesson,
"hint": hint,
"character": character,
"background": background,
"house": house,
"cfc": cfc,
"hint": hint,
"attempt": attempt,
"random_level": random_level,
"return_url": reverse(return_view_name),
Expand Down
Loading