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 = '
{% 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 "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 "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),