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 45 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
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,6 @@
"."
],
"python.testing.pytestEnabled": true,
"python.testing.unittestEnabled": false
"python.testing.unittestEnabled": false,
"editor.defaultFormatter": "ms-python.black-formatter"
}
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_description_tab(self):
self.browser.find_element(By.ID, "description_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()
157 changes: 136 additions & 21 deletions game/end_to_end_tests/test_level_editor.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import Select, 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 All @@ -13,7 +16,9 @@ def set_up_basic_map(self):
add_road_button = self.selenium.find_element(By.ID, "add_road")
add_road_button.click()

road_start = self.selenium.find_element(By.CSS_SELECTOR, "rect[x='130'][y='530']")
road_start = self.selenium.find_element(
By.CSS_SELECTOR, "rect[x='130'][y='530']"
)
road_end = self.selenium.find_element(By.CSS_SELECTOR, "rect[x='330'][y='530']")
ActionChains(self.selenium).drag_and_drop(road_start, road_end).perform()

Expand Down Expand Up @@ -45,59 +50,80 @@ def test_code_tab_blocks_load(self):
def test_multiple_houses(self):
[road_start, road_end] = self.set_up_basic_map()

road_middle = self.selenium.find_element(By.CSS_SELECTOR, "rect[x='230'][y='530']")
road_middle = self.selenium.find_element(
By.CSS_SELECTOR, "rect[x='230'][y='530']"
)

add_house_button = self.selenium.find_element(By.ID, "add_house")
add_house_button.click()
ActionChains(self.selenium).move_to_element(road_middle).click().perform()

added_houses = self.selenium.find_elements(By.CSS_SELECTOR, "rect[fill='#0000ff']")
added_houses = self.selenium.find_elements(
By.CSS_SELECTOR, "rect[fill='#0000ff']"
)
assert len(added_houses) == 2

delete_house_button = self.selenium.find_element(By.ID, "delete_house")
delete_house_button.click()
ActionChains(self.selenium).move_to_element(road_middle).click().perform()
ActionChains(self.selenium).move_to_element(road_start).perform()

houses_after_delete = self.selenium.find_elements(By.CSS_SELECTOR, "rect[fill='#0000ff']")
houses_after_delete = self.selenium.find_elements(
By.CSS_SELECTOR, "rect[fill='#0000ff']"
)
assert len(houses_after_delete) == 1

def test_cow_on_origin(self):
page = self.go_to_level_editor()
[road_start, road_end] = self.set_up_basic_map()

origin_space = self.selenium.find_elements(By.CSS_SELECTOR, "rect[fill='#ff0000']")
origin_space = self.selenium.find_elements(
By.CSS_SELECTOR, "rect[fill='#ff0000']"
)
assert len(origin_space) == 1

page.go_to_scenery_tab()

draggable_cow = self.selenium.find_element(By.ID, "cow")
ActionChains(self.selenium).click_and_hold(draggable_cow).move_to_element(road_start).perform()
start_space_warning = self.selenium.find_elements(By.CSS_SELECTOR, "rect[fill='#e35f4d'][fill-opacity='0.7'][x='130'][y='530']")
ActionChains(self.selenium).click_and_hold(draggable_cow).move_to_element(
road_start
).perform()
start_space_warning = self.selenium.find_elements(
By.CSS_SELECTOR,
"rect[fill='#e35f4d'][fill-opacity='0.7'][x='130'][y='530']",
)
assert len(start_space_warning) == 1

def test_cow_on_house(self):
page = self.go_to_level_editor()
[road_start, road_end] = self.set_up_basic_map()

house_space = self.selenium.find_elements(By.CSS_SELECTOR, "rect[fill='#0000ff']")
house_space = self.selenium.find_elements(
By.CSS_SELECTOR, "rect[fill='#0000ff']"
)
assert len(house_space) == 1
assert road_end == house_space[0]

page.go_to_scenery_tab()

draggable_cow = self.selenium.find_elements(By.ID, "cow")
assert len(draggable_cow) == 1
ActionChains(self.selenium).click_and_hold(draggable_cow[0]).move_to_element(road_end).perform()
allowed_space = self.selenium.find_elements(By.CSS_SELECTOR, "rect[fill='#87e34d']")
ActionChains(self.selenium).click_and_hold(draggable_cow[0]).move_to_element(
road_end
).perform()
allowed_space = self.selenium.find_elements(
By.CSS_SELECTOR, "rect[fill='#87e34d']"
)
assert len(allowed_space) == 0

def test_draggable_decor(self):
page = self.go_to_level_editor()
page.go_to_scenery_tab()

source_tree = self.selenium.find_element(By.ID, "tree2")
end_space = self.selenium.find_element(By.CSS_SELECTOR, "rect[x='130'][y='530']")
end_space = self.selenium.find_element(
By.CSS_SELECTOR, "rect[x='130'][y='530']"
)
ActionChains(self.selenium).drag_and_drop(source_tree, end_space).perform()

decor_tree = self.selenium.find_elements(By.CSS_SELECTOR, "image[x='0'][y='0']")
Expand All @@ -110,10 +136,14 @@ def test_draggable_cow(self):
page.go_to_scenery_tab()

source_cow = self.selenium.find_element(By.ID, "cow")
end_space = self.selenium.find_element(By.CSS_SELECTOR, "rect[x='130'][y='530']")
end_space = self.selenium.find_element(
By.CSS_SELECTOR, "rect[x='130'][y='530']"
)
ActionChains(self.selenium).drag_and_drop(source_cow, end_space).perform()

scenery_cow = self.selenium.find_elements(By.CSS_SELECTOR, "image[x='0'][y='0']")
scenery_cow = self.selenium.find_elements(
By.CSS_SELECTOR, "image[x='0'][y='0']"
)
cloned_source_cow = self.selenium.find_elements(By.ID, "cow")
assert len(scenery_cow) == 1
assert len(cloned_source_cow) == 1
Expand All @@ -123,23 +153,108 @@ def test_draggable_traffic_light(self):
page.go_to_scenery_tab()

source_light = self.selenium.find_element(By.ID, "trafficLightRed")
end_space = self.selenium.find_element(By.CSS_SELECTOR, "rect[x='130'][y='530']")
end_space = self.selenium.find_element(
By.CSS_SELECTOR, "rect[x='130'][y='530']"
)
ActionChains(self.selenium).drag_and_drop(source_light, end_space).perform()

scenery_light = self.selenium.find_elements(By.CSS_SELECTOR, "image[x='0'][y='0']")
scenery_light = self.selenium.find_elements(
By.CSS_SELECTOR, "image[x='0'][y='0']"
)
cloned_source_light = self.selenium.find_elements(By.ID, "trafficLightRed")
assert len(scenery_light) == 1
assert len(cloned_source_light) == 1

def test_custom_description_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 description and hint fields
page.go_to_description_tab()
self.selenium.find_element(By.ID, "subtitle").send_keys("test subtitle")
self.selenium.find_element(By.ID, "description").send_keys("test description")

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 description" 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_two = self.selenium.find_element(
By.ID, "myModal-mainText"
).get_attribute("innerHTML")
assert "test hint" in hint_modal_text_two

def test_solar_panels(self):
'''test that the solar panels appear as a scenery option when clicking on the scenery tab
and that they disappear as a scenery option when switching to an incompatible theme, i.e. snow'''
"""test that the solar panels appear as a scenery option when clicking on the scenery tab
and that they disappear as a scenery option when switching to an incompatible theme, i.e. snow
"""
page = self.go_to_level_editor()
page.go_to_scenery_tab()

solar_panel_style = self.selenium.find_element(By.ID, "solar_panel").get_attribute("style")
solar_panel_style = self.selenium.find_element(
By.ID, "solar_panel"
).get_attribute("style")
assert "inline" in solar_panel_style

Select(self.selenium.find_element(By.ID, "theme_select")).select_by_value("snow")
solar_panel_snow_style = self.selenium.find_element(By.ID, "solar_panel").get_attribute("style")
Select(self.selenium.find_element(By.ID, "theme_select")).select_by_value(
"snow"
)
solar_panel_snow_style = self.selenium.find_element(
By.ID, "solar_panel"
).get_attribute("style")
assert "none" in solar_panel_snow_style
7 changes: 7 additions & 0 deletions game/level_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ 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)
if data.get("subtitle") != None:
level.subtitle = data.get("subtitle")
if data.get("lesson") != None:
level.lesson = data.get("lesson")
if data.get("hint") != None:
level.hint = data.get("hint")

level.save()

set_decor(level, data["decor"])
Expand Down
12 changes: 0 additions & 12 deletions game/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,18 +101,6 @@ def build_description(title, message):
return f"<b>{title}</b><br><br>{message}"


def title_level_default():
return " "


def description_level_default():
return "Can you find the shortest route?"


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


def title_level1():
return "Can you help the van get to the house?"

Expand Down
28 changes: 28 additions & 0 deletions game/migrations/0094_add_description_hint__subtitle_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-19 14:43

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('game', '0093_alter_level_character_name'),
]

operations = [
migrations.AddField(
model_name='level',
name='hint',
field=models.TextField(default='Think back to earlier levels. What did you learn?', max_length=10000),
),
migrations.AddField(
model_name='level',
name='lesson',
field=models.TextField(default='Can you find the shortest route?', max_length=10000),
),
migrations.AddField(
model_name='level',
name='subtitle',
field=models.TextField(default='', max_length=10000),
),
]
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=100, blank=True)
lesson = models.TextField(max_length=10000, default="Can you find the shortest route?")
hint = models.TextField(max_length=10000, default="Think back to earlier levels. What did you learn?")
anonymous = models.BooleanField(default=False)
locked_for_class = models.ManyToManyField(
Class, blank=True, related_name="locked_levels"
Expand Down
5 changes: 2 additions & 3 deletions game/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from rest_framework import serializers

from game import messages
from game.messages import description_level_default, hint_level_default
from game.theme import get_theme, get_themes_url
from .models import Workspace, Level, Episode, LevelDecor, LevelBlock, Block

Expand Down Expand Up @@ -84,14 +83,14 @@ def get_description(self, obj):
description = getattr(messages, "description_level" + obj.name)()
return description
else:
return description_level_default()
return "Can you find the shortest route?"

def get_hint(self, obj):
if obj.default:
hint = getattr(messages, "hint_level" + obj.name)()
return hint
else:
return hint_level_default()
return "Think back to earlier levels. What did you learn?"

def get_leveldecor_set(self, obj):
leveldecors = LevelDecor.objects.filter(level__id=obj.id)
Expand Down
1 change: 1 addition & 0 deletions game/static/game/image/icons/description.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/hint.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion game/static/game/js/animation.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ 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>';
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>';
}
ocargo.Drawing.startPopup(title, leadMsg, otherMsg, true, buttons);
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
Loading
Loading