diff --git a/.vscode/settings.json b/.vscode/settings.json index c6a4edd52..8b3e3ab54 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -67,5 +67,6 @@ "." ], "python.testing.pytestEnabled": true, - "python.testing.unittestEnabled": false + "python.testing.unittestEnabled": false, + "editor.defaultFormatter": "ms-python.black-formatter" } \ No newline at end of file diff --git a/game/end_to_end_tests/editor_page.py b/game/end_to_end_tests/editor_page.py index 634c56ff7..e45f7c54d 100644 --- a/game/end_to_end_tests/editor_page.py +++ b/game/end_to_end_tests/editor_page.py @@ -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() diff --git a/game/end_to_end_tests/test_level_editor.py b/game/end_to_end_tests/test_level_editor.py index 293b0ddab..c26e46309 100644 --- a/game/end_to_end_tests/test_level_editor.py +++ b/game/end_to_end_tests/test_level_editor.py @@ -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): @@ -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() @@ -45,13 +50,17 @@ 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") @@ -59,28 +68,39 @@ def test_multiple_houses(self): 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] @@ -88,8 +108,12 @@ def test_cow_on_house(self): 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): @@ -97,7 +121,9 @@ def test_draggable_decor(self): 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']") @@ -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 @@ -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 diff --git a/game/level_management.py b/game/level_management.py index cb61a8f6f..ebf0fb924 100644 --- a/game/level_management.py +++ b/game/level_management.py @@ -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"]) diff --git a/game/messages.py b/game/messages.py index 323af5bb4..d51e9c098 100644 --- a/game/messages.py +++ b/game/messages.py @@ -101,18 +101,6 @@ def build_description(title, message): return f"{title}

{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?" diff --git a/game/migrations/0094_add_hint_lesson_subtitle_to_levels.py b/game/migrations/0094_add_hint_lesson_subtitle_to_levels.py new file mode 100644 index 000000000..bcf9066fa --- /dev/null +++ b/game/migrations/0094_add_hint_lesson_subtitle_to_levels.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.25 on 2024-06-24 12:50 + +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(blank=True, max_length=100, null=True), + ), + ] diff --git a/game/models.py b/game/models.py index 23d1f5ea9..8e047dfe3 100644 --- a/game/models.py +++ b/game/models.py @@ -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, null=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" diff --git a/game/serializers.py b/game/serializers.py index 743110f70..2b5e2d75e 100644 --- a/game/serializers.py +++ b/game/serializers.py @@ -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 @@ -80,18 +79,10 @@ def get_title(self, obj): return "Custom Level" def get_description(self, obj): - if obj.default: - description = getattr(messages, "description_level" + obj.name)() - return description - else: - return description_level_default() + return getattr(messages, "description_level" + obj.name)() if obj.default else obj.description def get_hint(self, obj): - if obj.default: - hint = getattr(messages, "hint_level" + obj.name)() - return hint - else: - return hint_level_default() + return getattr(messages, "hint_level" + obj.name)() if obj.default else obj.hint def get_leveldecor_set(self, obj): leveldecors = LevelDecor.objects.filter(level__id=obj.id) diff --git a/game/static/game/image/icons/description.svg b/game/static/game/image/icons/description.svg new file mode 100644 index 000000000..16fbd608a --- /dev/null +++ b/game/static/game/image/icons/description.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/game/static/game/image/icons/hint.svg b/game/static/game/image/icons/hint.svg new file mode 100644 index 000000000..0c69700bc --- /dev/null +++ b/game/static/game/image/icons/hint.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/game/static/game/js/animation.js b/game/static/game/js/animation.js index 31c137c3f..209e3ecc2 100644 --- a/game/static/game/js/animation.js +++ b/game/static/game/js/animation.js @@ -350,7 +350,7 @@ ocargo.Animation.prototype.performAnimation = function(animation) { } var otherMsg = ""; if (animation.popupHint) { - buttons += ''; + buttons += ''; otherMsg = '
' + '
' + HINT + '
'; } ocargo.Drawing.startPopup(title, leadMsg, otherMsg, true, buttons); diff --git a/game/static/game/js/drawing.js b/game/static/game/js/drawing.js index bd4ad68e0..aab2fb359 100644 --- a/game/static/game/js/drawing.js +++ b/game/static/game/js/drawing.js @@ -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( diff --git a/game/static/game/js/game.js b/game/static/game/js/game.js index 28682e081..7d99c87ae 100644 --- a/game/static/game/js/game.js +++ b/game/static/game/js/game.js @@ -129,9 +129,10 @@ ocargo.Game.prototype.setup = function () { const showMascot = BLOCKLY_ENABLED && !PYTHON_VIEW_ENABLED && LEVEL_NAME <= 80; // show mascot on Blockly-only levels that are not above 80 + const subtitle = SUBTITLE == "None" ? "" : SUBTITLE; ocargo.Drawing.startPopup( title, - LESSON, + "" + subtitle + "

" + LESSON, message, showMascot, [ @@ -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) { diff --git a/game/static/game/js/level_editor.js b/game/static/game/js/level_editor.js index 122cc1728..f146daa9e 100644 --- a/game/static/game/js/level_editor.js +++ b/game/static/game/js/level_editor.js @@ -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.description = new ocargo.Tab($('#description_radio'), $('#description_radio + label'), $('#description_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')); @@ -204,6 +206,8 @@ ocargo.LevelEditor = function(levelId) { setupCharacterTab(); setupBlocksTab(); setupRandomTab(); + setupDescriptionTab(); + setupHintTab(); setupLoadTab(); setupSaveTab(); setupShareTab(); @@ -477,6 +481,18 @@ ocargo.LevelEditor = function(levelId) { }); } + function setupDescriptionTab() { + tabs.description.setOnChange(function() { + transitionTab(tabs.description); + }); + } + + function setupHintTab() { + tabs.hint.setOnChange(function() { + transitionTab(tabs.hint); + }); + } + function goToMapTab() { tabs.map.select(); } @@ -2489,6 +2505,19 @@ ocargo.LevelEditor = function(levelId) { state.pythonViewEnabled = language === 'blocklyWithPythonView'; state.pythonEnabled = language === 'python' || language === 'both'; + // Description and hint data + if ($('#subtitle').val().length > 0) { + state.subtitle = $('#subtitle').val(); + } + + if ($('#description').val().length > 0) { + state.lesson = $('#description').val(); + } + + if ($('#hint').val().length > 0) { + state.hint = $('#hint').val(); + } + // Other data state.theme = currentTheme.id; state.character = $('#character_select').val(); @@ -2617,6 +2646,11 @@ ocargo.LevelEditor = function(levelId) { } languageSelect.change(); + // Load in description and hint data + $('#subtitle').val(state.subtitle); + $('#description').val(state.lesson); + $('#hint').val(state.hint); + // Other data if(state.max_fuel) { $('#max_fuel').val(state.max_fuel); diff --git a/game/templates/game/game.html b/game/templates/game/game.html index ace7b34d0..59f0e5c65 100644 --- a/game/templates/game/game.html +++ b/game/templates/game/game.html @@ -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}} diff --git a/game/templates/game/level_editor.html b/game/templates/game/level_editor.html index 90bf0967f..5ba7c09c5 100644 --- a/game/templates/game/level_editor.html +++ b/game/templates/game/level_editor.html @@ -195,6 +195,22 @@ +
+ + +
+ +
+ + +
+
@@ -482,6 +498,33 @@

{% trans "Description" %}

+

{% trans "Give this level a subtitle and a description of what to do within this level for your students." %}

+

{% trans "Students will see this subtitle and description when starting this level so make sure they are useful to the student." %}

+
+
+

{% trans "Subtitle
What is the subtitle of this level?" %}

+ + +

{% trans "Description
What do players have to do to complete this level?" %}

+ +
+
+
+ +
+

{% trans "Hint" %}

+

{% trans "Help out your players by adding hints! Players will have the option to view a hint when they have made an unsuccessful attempt." %}

+

{% trans "Players can also access hints by clicking the hint button whilst playing." %}

+
+
+

{% trans "Hint" %}

+ +
+
+
+

{% trans "Load" %}

{% trans "Here you can load in levels created by you and your friends! Select a level in the table and press load." %}

diff --git a/game/views/level.py b/game/views/level.py index d6808f99a..4350f962c 100644 --- a/game/views/level.py +++ b/game/views/level.py @@ -144,26 +144,15 @@ def play_level(request, level, from_editor=False): if not permissions.can_play_level(request.user, level, app_settings.EARLY_ACCESS_FUNCTION(request)): return renderError(request, messages.no_permission_title(), messages.not_shared_level()) - # Set default level description/hint lookups - lesson = "description_level_default" - hint = "hint_level_default" - # If it's one of our levels, set level description/hint lookups # to point to what they should be if level.default: lesson = "description_level" + str(level.name) hint = "hint_level" + str(level.name) - # Try to get the relevant message, and fall back on defaults - try: - lessonCall = getattr(messages, lesson) - hintCall = getattr(messages, hint) - except AttributeError: - lessonCall = messages.description_level_default - hintCall = messages.hint_level_default - - lesson = mark_safe(lessonCall()) - hint = mark_safe(hintCall()) + subtitle = level.subtitle + lesson = level.lesson + hint = level.hint character = level.character character_url = character.top_down @@ -233,14 +222,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), diff --git a/game/views/level_editor.py b/game/views/level_editor.py index 09a644a25..6d2ae66e1 100644 --- a/game/views/level_editor.py +++ b/game/views/level_editor.py @@ -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 + lesson = level.lesson + hint = level.hint attempt = None house = get_decor_element("house", level.theme).url @@ -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),