rapid-router 5.18.0__py2.py3-none-any.whl → 7.6.8__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- example_project/rapid_router_test_settings.py +19 -7
- example_project/settings.py +21 -8
- example_project/urls.py +5 -6
- game/__init__.py +1 -1
- game/admin.py +7 -2
- game/character.py +8 -0
- game/decor.py +40 -0
- game/end_to_end_tests/base_game_test.py +34 -27
- game/end_to_end_tests/editor_page.py +15 -0
- game/end_to_end_tests/game_page.py +88 -20
- game/end_to_end_tests/selenium_test_case.py +1 -20
- game/end_to_end_tests/test_cow_crashes.py +3 -5
- game/end_to_end_tests/test_level_editor.py +273 -10
- game/end_to_end_tests/test_level_selection.py +25 -3
- game/end_to_end_tests/test_play_through.py +222 -127
- game/end_to_end_tests/test_python_levels.py +41 -7
- game/end_to_end_tests/test_saving_workspace.py +2 -1
- game/forms.py +7 -1
- game/level_management.py +26 -11
- game/messages.py +899 -337
- game/migrations/0001_squashed_0025_levels_ordering_pt1.py +19 -1
- game/migrations/0026_levels_pt2.py +13 -2
- game/migrations/0032_cannot_turn_left_level.py +13 -2
- game/migrations/0033_recursion_level.py +13 -2
- game/migrations/0034_joes_level.py +13 -2
- game/migrations/0035_disable_route_score_level_70.py +0 -2
- game/migrations/0036_level_score_73.py +0 -2
- game/migrations/0037_level_score_79.py +0 -2
- game/migrations/0038_level_score_40.py +0 -1
- game/migrations/0042_level_score_73.py +0 -2
- game/migrations/0048_add_cow_field_and_blocks.py +0 -2
- game/migrations/0049_level_score_34.py +0 -2
- game/migrations/0050_level_score_40.py +0 -2
- game/migrations/0051_level_score_49.py +0 -1
- game/migrations/0086_loop_levels.py +13 -2
- game/migrations/0092_disable_algo_score_in_custom_levels.py +28 -0
- game/migrations/0093_alter_level_character_name.py +18 -0
- game/migrations/0094_add_hint_lesson_subtitle_to_levels.py +28 -0
- game/migrations/0095_level_commands.py +18 -0
- game/migrations/0096_alter_level_commands.py +18 -0
- game/migrations/0097_add_python_den_levels.py +1515 -0
- game/migrations/0098_add_episode_link_fields.py +44 -0
- game/migrations/0099_python_episodes_links.py +103 -0
- game/migrations/0100_reorder_python_levels.py +179 -0
- game/migrations/0101_rename_episodes.py +45 -0
- game/migrations/0102_reoder_episodes_13_14.py +136 -0
- game/migrations/0103_level_1015_solution.py +26 -0
- game/migrations/0104_remove_level_direct_drive.py +17 -0
- game/migrations/0105_delete_invalid_attempts.py +18 -0
- game/migrations/0106_fields_to_snake_case.py +48 -0
- game/migrations/0107_rename_worksheet_link_episode_student_worksheet_link.py +18 -0
- game/migrations/0108_episode_indy_worksheet_link.py +18 -0
- game/migrations/0109_create_episodes_23_and_24.py +99 -0
- game/migrations/0110_remove_episode_indy_worksheet_link_and_more.py +100 -0
- game/migrations/0111_create_worksheets.py +149 -0
- game/migrations/0112_worksheet_locked_classes.py +21 -0
- game/migrations/0113_level_needs_approval.py +18 -0
- game/migrations/0114_default_and_non_student_levels_no_approval.py +31 -0
- game/migrations/0115_level_level__default_does_not_need_approval.py +22 -0
- game/migrations/0116_update_worksheet_video_links.py +68 -0
- game/migrations/0117_update_solutions_to_if_else.py +61 -0
- game/models.py +127 -17
- game/permissions.py +51 -19
- game/python_den_urls.py +26 -0
- game/random_road.py +9 -9
- game/serializers.py +12 -17
- game/static/django_reverse_js/js/reverse.js +171 -0
- game/static/game/css/LilitaOne-Regular.ttf +0 -0
- game/static/game/css/backgrounds.css +8 -12
- game/static/game/css/dataTables.custom.css +3 -2
- game/static/game/css/editor.css +47 -0
- game/static/game/css/game.css +37 -43
- game/static/game/css/game_screen.css +16 -0
- game/static/game/css/level_editor.css +5 -0
- game/static/game/css/level_selection.css +17 -2
- game/static/game/image/Python_Den_hero_student.png +0 -0
- game/static/game/image/Python_levels_page.svg +1954 -0
- game/static/game/image/characters/front_view/Electric_van.svg +448 -0
- game/static/game/image/characters/top_view/Electric_van.svg +448 -0
- game/static/game/image/decor/city/solar_panel.svg +1200 -0
- game/static/game/image/decor/farm/solar_panel.svg +86 -0
- game/static/game/image/decor/grass/solar_panel.svg +86 -0
- game/static/game/image/decor/snow/solar_panel.svg +173 -0
- game/static/game/image/electric_van.svg +448 -0
- game/static/game/image/icons/description.svg +1 -0
- game/static/game/image/icons/hint.svg +1 -0
- game/static/game/image/icons/python.svg +1 -1
- game/static/game/image/pigeon.svg +684 -0
- game/static/game/image/python_den_header.svg +19 -0
- game/static/game/js/animation.js +65 -24
- game/static/game/js/blockly/msg/js/bg.js +52 -1
- game/static/game/js/blockly/msg/js/ca.js +52 -1
- game/static/game/js/blockly/msg/js/en-gb.js +2 -0
- game/static/game/js/blockly/msg/js/en.js +2 -0
- game/static/game/js/blockly/msg/js/es.js +52 -1
- game/static/game/js/blockly/msg/js/fr.js +2 -0
- game/static/game/js/blockly/msg/js/hi.js +2 -0
- game/static/game/js/blockly/msg/js/it.js +52 -1
- game/static/game/js/blockly/msg/js/pl.js +52 -1
- game/static/game/js/blockly/msg/js/pt-br.js +52 -1
- game/static/game/js/blockly/msg/js/ru.js +52 -1
- game/static/game/js/blockly/msg/js/ur.js +52 -1
- game/static/game/js/blocklyCustomBlocks.js +93 -52
- game/static/game/js/button.js +12 -0
- game/static/game/js/cow.js +11 -7
- game/static/game/js/drawing.js +68 -29
- game/static/game/js/editor.js +23 -0
- game/static/game/js/game.js +74 -110
- game/static/game/js/level_editor.js +646 -274
- game/static/game/js/level_moderation.js +33 -2
- game/static/game/js/level_selection.js +1 -1
- game/static/game/js/loadLanguages.js +2 -2
- game/static/game/js/model.js +32 -2
- game/static/game/js/pythonControl.js +14 -1
- game/static/game/js/scoreboard.js +0 -37
- game/static/game/js/scoreboardSharedLevels.js +48 -0
- game/static/game/js/skulpt/skulpt-stdlib.js +1 -1
- game/static/game/js/sound.js +52 -5
- game/static/game/raphael_image/characters/top_view/Electric_van.svg +448 -0
- game/static/game/raphael_image/decor/city/solar_panel.svg +1200 -0
- game/static/game/raphael_image/decor/farm/solar_panel.svg +86 -0
- game/static/game/raphael_image/decor/grass/solar_panel.svg +86 -0
- game/static/game/raphael_image/decor/snow/solar_panel.svg +173 -0
- game/static/game/raphael_image/pigeon.svg +685 -0
- game/static/game/sass/game.scss +2 -2
- game/static/game/sound/clown_horn.mp3 +0 -0
- game/static/game/sound/clown_horn.ogg +0 -0
- game/static/game/sound/electric_van_starting.mp3 +0 -0
- game/static/game/sound/electric_van_starting.ogg +0 -0
- game/static/game/sound/pigeon.mp3 +0 -0
- game/static/game/sound/pigeon.ogg +0 -0
- game/static/game/sound/sleigh_bells.mp3 +0 -0
- game/static/game/sound/sleigh_bells.ogg +0 -0
- game/static/game/sound/sleigh_crash.mp3 +0 -0
- game/static/game/sound/sleigh_crash.ogg +0 -0
- game/templates/game/base.html +34 -14
- game/templates/game/basenonav.html +11 -5
- game/templates/game/game.html +142 -38
- game/templates/game/level_editor.html +340 -236
- game/templates/game/level_moderation.html +19 -6
- game/templates/game/level_selection.html +18 -110
- game/templates/game/python_den_level_selection.html +291 -0
- game/templates/game/python_den_worksheet.html +101 -0
- game/templates/game/scoreboard.html +83 -64
- game/tests/test_level_editor.py +94 -26
- game/tests/test_level_selection.py +149 -46
- game/tests/test_python_den_worksheet.py +85 -0
- game/tests/test_scoreboard.py +34 -7
- game/tests/utils/level.py +32 -26
- game/theme.py +5 -5
- game/urls.py +199 -61
- game/views/language_code_conversions.py +86 -86
- game/views/level.py +155 -63
- game/views/level_editor.py +88 -55
- game/views/level_moderation.py +23 -0
- game/views/level_selection.py +116 -47
- game/views/level_solutions.py +491 -106
- game/views/scoreboard.py +76 -51
- game/views/worksheet.py +25 -0
- rapid_router-7.6.8.dist-info/METADATA +174 -0
- {rapid_router-5.18.0.dist-info → rapid_router-7.6.8.dist-info}/RECORD +164 -104
- {rapid_router-5.18.0.dist-info → rapid_router-7.6.8.dist-info}/WHEEL +1 -1
- example_project/manage.py +0 -10
- game/static/game/image/actions/go.svg +0 -18
- game/static/game/js/js-reverse.js +0 -14
- game/static/game/js/pqselect.min.js +0 -9
- game/static/game/js/widget-scroller.js +0 -906
- rapid_router-5.18.0.dist-info/METADATA +0 -17
- {rapid_router-5.18.0.dist-info → rapid_router-7.6.8.dist-info/licenses}/LICENSE.md +0 -0
- {rapid_router-5.18.0.dist-info → rapid_router-7.6.8.dist-info}/top_level.txt +0 -0
game/views/level.py
CHANGED
|
@@ -5,10 +5,9 @@ from builtins import object, str
|
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
|
|
7
7
|
from django.http import Http404, HttpResponse
|
|
8
|
-
from django.shortcuts import
|
|
8
|
+
from django.shortcuts import get_object_or_404, redirect, render
|
|
9
9
|
from django.urls import reverse
|
|
10
10
|
from django.utils import timezone
|
|
11
|
-
from django.utils.safestring import mark_safe
|
|
12
11
|
from django.views.decorators.http import require_POST
|
|
13
12
|
from rest_framework import serializers
|
|
14
13
|
|
|
@@ -17,18 +16,19 @@ import game.messages as messages
|
|
|
17
16
|
import game.permissions as permissions
|
|
18
17
|
from game import app_settings
|
|
19
18
|
from game.cache import (
|
|
19
|
+
cached_custom_level,
|
|
20
20
|
cached_default_level,
|
|
21
21
|
cached_episode,
|
|
22
|
-
cached_custom_level,
|
|
23
|
-
cached_level_decor,
|
|
24
22
|
cached_level_blocks,
|
|
23
|
+
cached_level_decor,
|
|
25
24
|
)
|
|
26
25
|
from game.character import get_character
|
|
27
26
|
from game.decor import get_decor_element
|
|
28
|
-
from game.models import
|
|
27
|
+
from game.models import Attempt, Level, Workspace
|
|
29
28
|
from game.theme import get_theme
|
|
30
29
|
from game.views.language_code_conversions import language_code_dict
|
|
31
30
|
from game.views.level_solutions import solutions
|
|
31
|
+
|
|
32
32
|
from .helper import renderError
|
|
33
33
|
|
|
34
34
|
|
|
@@ -44,14 +44,42 @@ def play_custom_level(request, levelId, from_editor=False):
|
|
|
44
44
|
return play_level(request, level, from_editor)
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
def play_default_level(request,
|
|
48
|
-
|
|
47
|
+
def play_default_level(request, level_name):
|
|
48
|
+
level_index = int(level_name)
|
|
49
|
+
if level_index > 79:
|
|
50
|
+
raise Http404
|
|
51
|
+
if (
|
|
52
|
+
level_index > 19
|
|
53
|
+
and not level_index in [29, 33, 44, 51, 61, 68]
|
|
54
|
+
and not request.user.is_authenticated
|
|
55
|
+
):
|
|
56
|
+
return redirect(reverse("levels"))
|
|
57
|
+
|
|
58
|
+
level = cached_default_level(level_name)
|
|
49
59
|
return play_level(request, level)
|
|
50
60
|
|
|
51
61
|
|
|
52
|
-
def
|
|
62
|
+
def play_default_python_level(request, level_name):
|
|
63
|
+
level_index = int(level_name)
|
|
64
|
+
if level_index > 49:
|
|
65
|
+
raise Http404
|
|
66
|
+
if (
|
|
67
|
+
level_index > 26
|
|
68
|
+
and not level_index in [41]
|
|
69
|
+
and not request.user.is_authenticated
|
|
70
|
+
):
|
|
71
|
+
return redirect(reverse("python_levels"))
|
|
72
|
+
|
|
73
|
+
levelId = int(level_name) + 1000
|
|
74
|
+
|
|
75
|
+
level = cached_default_level(levelId)
|
|
76
|
+
return play_level(request, level, from_python_den=True)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _prev_level_url(level, user, night_mode, from_python_den):
|
|
53
80
|
"""
|
|
54
|
-
Find the previous available level. Check if level is available if so, go to
|
|
81
|
+
Find the previous available level. Check if level is available if so, go to
|
|
82
|
+
it.
|
|
55
83
|
"""
|
|
56
84
|
|
|
57
85
|
if not level.prev_level.all():
|
|
@@ -68,16 +96,24 @@ def _prev_level_url(level, user, night_mode):
|
|
|
68
96
|
prev_level = prev_level.prev_level.all()[0]
|
|
69
97
|
is_prev_level_locked = klass in prev_level.locked_for_class.all()
|
|
70
98
|
|
|
71
|
-
return _level_url(prev_level, night_mode)
|
|
99
|
+
return _level_url(prev_level, night_mode, from_python_den)
|
|
72
100
|
|
|
73
101
|
|
|
74
|
-
def _next_level_url(level, user, night_mode):
|
|
102
|
+
def _next_level_url(level, user, night_mode, from_python_den):
|
|
75
103
|
"""
|
|
76
|
-
Find the next available level. By default, this will be the `next_level`
|
|
77
|
-
|
|
78
|
-
|
|
104
|
+
Find the next available level. By default, this will be the `next_level`
|
|
105
|
+
field in the Level model, but in the case where the user is a student and
|
|
106
|
+
the teacher has locked certain levels, then loop until we find the next
|
|
107
|
+
unlocked level (or we run out of levels).
|
|
79
108
|
"""
|
|
109
|
+
|
|
80
110
|
if not level.next_level:
|
|
111
|
+
if (
|
|
112
|
+
level.episode
|
|
113
|
+
and level.episode.next_episode
|
|
114
|
+
and len(level.episode.next_episode.levels) == 0
|
|
115
|
+
):
|
|
116
|
+
return reverse("python_levels")
|
|
81
117
|
return ""
|
|
82
118
|
|
|
83
119
|
next_level = level.next_level
|
|
@@ -89,11 +125,13 @@ def _next_level_url(level, user, night_mode):
|
|
|
89
125
|
is_next_level_locked = klass in next_level.locked_for_class.all()
|
|
90
126
|
|
|
91
127
|
if is_next_level_locked:
|
|
92
|
-
while is_next_level_locked and
|
|
128
|
+
while is_next_level_locked and (
|
|
129
|
+
int(next_level.name) < 1050 if from_python_den else 80
|
|
130
|
+
):
|
|
93
131
|
next_level = next_level.next_level
|
|
94
132
|
is_next_level_locked = klass in next_level.locked_for_class.all()
|
|
95
133
|
|
|
96
|
-
return _level_url(next_level, night_mode)
|
|
134
|
+
return _level_url(next_level, night_mode, from_python_den)
|
|
97
135
|
|
|
98
136
|
|
|
99
137
|
def add_night(url, night_mode):
|
|
@@ -102,24 +140,28 @@ def add_night(url, night_mode):
|
|
|
102
140
|
return url
|
|
103
141
|
|
|
104
142
|
|
|
105
|
-
def _level_url(level, night_mode):
|
|
143
|
+
def _level_url(level, night_mode, from_python_den):
|
|
106
144
|
if level.default:
|
|
107
|
-
result = _default_level_url(level)
|
|
145
|
+
result = _default_level_url(level, from_python_den)
|
|
108
146
|
else:
|
|
109
147
|
result = _custom_level_url(level)
|
|
110
148
|
|
|
111
149
|
return add_night(result, night_mode)
|
|
112
150
|
|
|
113
151
|
|
|
114
|
-
def _default_level_url(level):
|
|
115
|
-
|
|
152
|
+
def _default_level_url(level, from_python_den):
|
|
153
|
+
viewname = "play_python_default_level" if from_python_den else "play_default_level"
|
|
154
|
+
|
|
155
|
+
level_name = int(level.name) - 1000 if from_python_den else level.name
|
|
156
|
+
|
|
157
|
+
return reverse(viewname, args=[level_name])
|
|
116
158
|
|
|
117
159
|
|
|
118
160
|
def _custom_level_url(level):
|
|
119
161
|
return reverse("play_custom_level", args=[level.id])
|
|
120
162
|
|
|
121
163
|
|
|
122
|
-
def play_level(request, level, from_editor=False):
|
|
164
|
+
def play_level(request, level, from_editor=False, from_python_den=False):
|
|
123
165
|
"""Loads a level for rendering in the game.
|
|
124
166
|
|
|
125
167
|
**Context**
|
|
@@ -139,32 +181,32 @@ def play_level(request, level, from_editor=False):
|
|
|
139
181
|
:template:`game/game.html`
|
|
140
182
|
"""
|
|
141
183
|
|
|
142
|
-
night_mode =
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return renderError(request, messages.no_permission_title(), messages.not_shared_level())
|
|
146
|
-
|
|
147
|
-
# Set default level description/hint lookups
|
|
148
|
-
lesson = "description_level_default"
|
|
149
|
-
hint = "hint_level_default"
|
|
150
|
-
|
|
151
|
-
# If it's one of our levels, set level description/hint lookups
|
|
152
|
-
# to point to what they should be
|
|
153
|
-
if level.default:
|
|
154
|
-
lesson = "description_level" + str(level.name)
|
|
155
|
-
hint = "hint_level" + str(level.name)
|
|
156
|
-
|
|
157
|
-
# Try to get the relevant message, and fall back on defaults
|
|
158
|
-
try:
|
|
159
|
-
lessonCall = getattr(messages, lesson)
|
|
160
|
-
hintCall = getattr(messages, hint)
|
|
161
|
-
except AttributeError:
|
|
162
|
-
lessonCall = messages.description_level_default
|
|
163
|
-
hintCall = messages.hint_level_default
|
|
184
|
+
night_mode = (
|
|
185
|
+
False if not app_settings.NIGHT_MODE_FEATURE_ENABLED else "night" in request.GET
|
|
186
|
+
)
|
|
164
187
|
|
|
165
|
-
|
|
166
|
-
|
|
188
|
+
if not permissions.can_play_level(
|
|
189
|
+
request.user, level, app_settings.EARLY_ACCESS_FUNCTION(request)
|
|
190
|
+
):
|
|
191
|
+
return renderError(
|
|
192
|
+
request, messages.no_permission_title(), messages.not_shared_level()
|
|
193
|
+
)
|
|
167
194
|
|
|
195
|
+
subtitle = level.subtitle
|
|
196
|
+
lesson = (
|
|
197
|
+
getattr(messages, "description_level" + str(level.name))
|
|
198
|
+
if level.default
|
|
199
|
+
else level.lesson
|
|
200
|
+
)
|
|
201
|
+
hint = (
|
|
202
|
+
getattr(messages, "hint_level" + str(level.name))
|
|
203
|
+
if level.default
|
|
204
|
+
else level.hint
|
|
205
|
+
)
|
|
206
|
+
commands_attr = "commands_level" + str(level.name)
|
|
207
|
+
commands = (
|
|
208
|
+
getattr(messages, commands_attr, None) if level.default else level.commands
|
|
209
|
+
)
|
|
168
210
|
character = level.character
|
|
169
211
|
character_url = character.top_down
|
|
170
212
|
wreckage_url = "van_wreckage.svg"
|
|
@@ -200,7 +242,9 @@ def play_level(request, level, from_editor=False):
|
|
|
200
242
|
.first()
|
|
201
243
|
)
|
|
202
244
|
if not attempt:
|
|
203
|
-
attempt = Attempt(
|
|
245
|
+
attempt = Attempt(
|
|
246
|
+
level=level, student=student, score=None, night_mode=night_mode
|
|
247
|
+
)
|
|
204
248
|
fetch_workspace_from_last_attempt(attempt)
|
|
205
249
|
attempt.save()
|
|
206
250
|
else:
|
|
@@ -221,10 +265,18 @@ def play_level(request, level, from_editor=False):
|
|
|
221
265
|
night_mode_javascript = "false"
|
|
222
266
|
model_solution = level.model_solution
|
|
223
267
|
|
|
224
|
-
return_view =
|
|
268
|
+
return_view = (
|
|
269
|
+
"level_editor"
|
|
270
|
+
if from_editor
|
|
271
|
+
else "python_levels" if from_python_den else "levels"
|
|
272
|
+
)
|
|
225
273
|
|
|
226
274
|
temp_block_data = []
|
|
227
|
-
[
|
|
275
|
+
[
|
|
276
|
+
temp_block_data.append(block)
|
|
277
|
+
for block in block_data
|
|
278
|
+
if block not in temp_block_data
|
|
279
|
+
]
|
|
228
280
|
|
|
229
281
|
block_data = temp_block_data
|
|
230
282
|
|
|
@@ -233,14 +285,16 @@ def play_level(request, level, from_editor=False):
|
|
|
233
285
|
"game/game.html",
|
|
234
286
|
context={
|
|
235
287
|
"level": level,
|
|
288
|
+
"subtitle": subtitle,
|
|
236
289
|
"lesson": lesson,
|
|
290
|
+
"hint": hint,
|
|
291
|
+
"commands": commands if commands is not None else level.commands,
|
|
237
292
|
"blocks": block_data,
|
|
238
293
|
"decor": decor_data,
|
|
239
294
|
"character": character,
|
|
240
295
|
"background": background,
|
|
241
296
|
"house": house,
|
|
242
297
|
"cfc": cfc,
|
|
243
|
-
"hint": hint,
|
|
244
298
|
"workspace": workspace,
|
|
245
299
|
"python_workspace": python_workspace,
|
|
246
300
|
"return_url": reverse(return_view),
|
|
@@ -249,19 +303,29 @@ def play_level(request, level, from_editor=False):
|
|
|
249
303
|
"character_height": character_height,
|
|
250
304
|
"wreckage_url": wreckage_url,
|
|
251
305
|
"night_mode": night_mode_javascript,
|
|
252
|
-
"night_mode_feature_enabled": str(
|
|
306
|
+
"night_mode_feature_enabled": str(
|
|
307
|
+
app_settings.NIGHT_MODE_FEATURE_ENABLED
|
|
308
|
+
).lower(),
|
|
253
309
|
"model_solution": model_solution,
|
|
254
|
-
"prev_level_url": _prev_level_url(
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
"
|
|
310
|
+
"prev_level_url": _prev_level_url(
|
|
311
|
+
level, request.user, night_mode, from_python_den
|
|
312
|
+
),
|
|
313
|
+
"next_level_url": _next_level_url(
|
|
314
|
+
level, request.user, night_mode, from_python_den
|
|
315
|
+
),
|
|
316
|
+
"flip_night_mode_url": _level_url(level, not night_mode, from_python_den),
|
|
317
|
+
"available_language_dict": language_code_dict,
|
|
258
318
|
},
|
|
259
319
|
)
|
|
260
320
|
|
|
261
321
|
|
|
262
322
|
def fetch_workspace_from_last_attempt(attempt):
|
|
263
323
|
latest_attempt = (
|
|
264
|
-
Attempt.objects.filter(
|
|
324
|
+
Attempt.objects.filter(
|
|
325
|
+
level=attempt.level,
|
|
326
|
+
student=attempt.student,
|
|
327
|
+
night_mode=attempt.night_mode,
|
|
328
|
+
)
|
|
265
329
|
.order_by("-start_time")
|
|
266
330
|
.first()
|
|
267
331
|
)
|
|
@@ -277,15 +341,23 @@ def delete_level(request, levelID):
|
|
|
277
341
|
level_management.delete_level(level)
|
|
278
342
|
success = True
|
|
279
343
|
|
|
280
|
-
return HttpResponse(
|
|
344
|
+
return HttpResponse(
|
|
345
|
+
json.dumps({"success": success}), content_type="application/javascript"
|
|
346
|
+
)
|
|
281
347
|
|
|
282
348
|
|
|
283
349
|
def submit_attempt(request):
|
|
284
350
|
"""Processes a request on submission of the program solving the current level."""
|
|
285
|
-
if
|
|
351
|
+
if (
|
|
352
|
+
not request.user.is_anonymous
|
|
353
|
+
and request.method == "POST"
|
|
354
|
+
and hasattr(request.user.userprofile, "student")
|
|
355
|
+
):
|
|
286
356
|
level = get_object_or_404(Level, id=request.POST.get("level", 1))
|
|
287
357
|
student = request.user.userprofile.student
|
|
288
|
-
attempt = Attempt.objects.filter(
|
|
358
|
+
attempt = Attempt.objects.filter(
|
|
359
|
+
level=level, student=student, finish_time__isnull=True
|
|
360
|
+
).first()
|
|
289
361
|
if attempt:
|
|
290
362
|
attempt.score = float(request.POST.get("score"))
|
|
291
363
|
attempt.workspace = request.POST.get("workspace")
|
|
@@ -362,16 +434,25 @@ def load_workspace(request, workspaceID):
|
|
|
362
434
|
|
|
363
435
|
|
|
364
436
|
def save_workspace(request, workspaceID=None):
|
|
365
|
-
request_params = [
|
|
437
|
+
request_params = [
|
|
438
|
+
"name",
|
|
439
|
+
"contents",
|
|
440
|
+
"python_contents",
|
|
441
|
+
"blockly_enabled",
|
|
442
|
+
"python_enabled",
|
|
443
|
+
"python_view_enabled",
|
|
444
|
+
]
|
|
366
445
|
missing_params = [param for param in request_params if param not in request.POST]
|
|
367
446
|
if missing_params != []:
|
|
368
|
-
raise Exception(
|
|
447
|
+
raise Exception(
|
|
448
|
+
"Request missing the following required parameters", missing_params
|
|
449
|
+
)
|
|
369
450
|
name = request.POST.get("name")
|
|
370
451
|
contents = request.POST.get("contents")
|
|
371
452
|
python_contents = request.POST.get("python_contents")
|
|
372
453
|
blockly_enabled = json.loads(request.POST.get("blockly_enabled"))
|
|
373
454
|
python_enabled = json.loads(request.POST.get("python_enabled"))
|
|
374
|
-
python_view_enabled = json.loads(request.POST.get("
|
|
455
|
+
python_view_enabled = json.loads(request.POST.get("python_view_enabled"))
|
|
375
456
|
|
|
376
457
|
workspace = None
|
|
377
458
|
if workspaceID:
|
|
@@ -401,7 +482,7 @@ def load_workspace_solution(request, level_name):
|
|
|
401
482
|
|
|
402
483
|
level = Level.objects.get(name=level_name, default=True)
|
|
403
484
|
|
|
404
|
-
if level.
|
|
485
|
+
if level.blockly_enabled:
|
|
405
486
|
workspace.contents = solutions[level_name]
|
|
406
487
|
workspace.blockly_enabled = True
|
|
407
488
|
workspace.python_enabled = False
|
|
@@ -424,8 +505,19 @@ def load_workspace_solution(request, level_name):
|
|
|
424
505
|
|
|
425
506
|
|
|
426
507
|
def start_episode(request, episodeId):
|
|
508
|
+
if int(episodeId) > 9:
|
|
509
|
+
raise Http404
|
|
510
|
+
|
|
511
|
+
episode = cached_episode(episodeId)
|
|
512
|
+
return play_level(request, episode.first_level)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def start_python_episode(request, episodeId):
|
|
516
|
+
if int(episodeId) < 12:
|
|
517
|
+
raise Http404
|
|
518
|
+
|
|
427
519
|
episode = cached_episode(episodeId)
|
|
428
|
-
return play_level(request, episode.first_level,
|
|
520
|
+
return play_level(request, episode.first_level, from_python_den=True)
|
|
429
521
|
|
|
430
522
|
|
|
431
523
|
@require_POST
|
game/views/level_editor.py
CHANGED
|
@@ -2,8 +2,7 @@ from __future__ import division
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import re
|
|
5
|
-
from builtins import map
|
|
6
|
-
from builtins import str
|
|
5
|
+
from builtins import map, str
|
|
7
6
|
|
|
8
7
|
from common.models import Student, Teacher
|
|
9
8
|
from django.contrib.auth.models import User
|
|
@@ -11,7 +10,6 @@ from django.db import transaction
|
|
|
11
10
|
from django.http import HttpResponse
|
|
12
11
|
from django.shortcuts import get_object_or_404, render, redirect
|
|
13
12
|
from django.urls import reverse
|
|
14
|
-
from django.utils.safestring import mark_safe
|
|
15
13
|
from django.views.decorators.http import require_POST
|
|
16
14
|
from portal.templatetags import app_tags
|
|
17
15
|
from rest_framework.authentication import SessionAuthentication
|
|
@@ -20,8 +18,7 @@ from rest_framework.views import APIView
|
|
|
20
18
|
import game.level_management as level_management
|
|
21
19
|
import game.messages as messages
|
|
22
20
|
import game.permissions as permissions
|
|
23
|
-
from game import app_settings
|
|
24
|
-
from game import random_road
|
|
21
|
+
from game import app_settings, random_road
|
|
25
22
|
from game.cache import cached_level_decor, cached_level_blocks
|
|
26
23
|
from game.character import get_all_character
|
|
27
24
|
from game.decor import get_all_decor, get_decor_element
|
|
@@ -86,8 +83,9 @@ def play_anonymous_level(request, levelId, from_level_editor=True, random_level=
|
|
|
86
83
|
if not level.anonymous:
|
|
87
84
|
return redirect(reverse("level_editor"), permanent=True)
|
|
88
85
|
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
subtitle = level.subtitle
|
|
87
|
+
lesson = level.lesson
|
|
88
|
+
hint = level.hint
|
|
91
89
|
|
|
92
90
|
attempt = None
|
|
93
91
|
house = get_decor_element("house", level.theme).url
|
|
@@ -123,12 +121,13 @@ def play_anonymous_level(request, levelId, from_level_editor=True, random_level=
|
|
|
123
121
|
"level": level,
|
|
124
122
|
"decor": decor_data,
|
|
125
123
|
"blocks": block_data,
|
|
124
|
+
"subtitle": subtitle,
|
|
126
125
|
"lesson": lesson,
|
|
126
|
+
"hint": hint,
|
|
127
127
|
"character": character,
|
|
128
128
|
"background": background,
|
|
129
129
|
"house": house,
|
|
130
130
|
"cfc": cfc,
|
|
131
|
-
"hint": hint,
|
|
132
131
|
"attempt": attempt,
|
|
133
132
|
"random_level": random_level,
|
|
134
133
|
"return_url": reverse(return_view_name),
|
|
@@ -208,6 +207,8 @@ def load_level_for_editor(request, levelID):
|
|
|
208
207
|
def save_level_for_editor(request, levelId=None):
|
|
209
208
|
"""Processes a request on creation of the map in the level editor"""
|
|
210
209
|
data = json.loads(request.POST["data"])
|
|
210
|
+
data["disable_algorithm_score"] = True
|
|
211
|
+
|
|
211
212
|
if ("character" not in data) or (not data["character"]):
|
|
212
213
|
# Set a default, to deal with issue #1158 "Cannot save custom level"
|
|
213
214
|
data["character"] = 1
|
|
@@ -220,45 +221,61 @@ def save_level_for_editor(request, levelId=None):
|
|
|
220
221
|
if not permissions.can_save_level(request.user, level):
|
|
221
222
|
return HttpResponseUnauthorized()
|
|
222
223
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
224
|
+
name_pattern = re.compile("^(\w?[ ]?)*$")
|
|
225
|
+
fields_pattern = re.compile("^[\w.?!', ]*$")
|
|
226
|
+
|
|
227
|
+
name_is_safe = name_pattern.match(data["name"])
|
|
228
|
+
fields_are_safe = all(
|
|
229
|
+
[
|
|
230
|
+
field not in data or fields_pattern.match(data[field])
|
|
231
|
+
for field in ["subtitle", "lesson", "hint"]
|
|
232
|
+
]
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if not (name_is_safe and fields_are_safe):
|
|
236
|
+
return HttpResponseUnauthorized()
|
|
237
|
+
|
|
238
|
+
level_management.save_level(level, data)
|
|
239
|
+
|
|
240
|
+
is_user_school_student = (
|
|
241
|
+
hasattr(level.owner, "student") and not level.owner.student.is_independent()
|
|
242
|
+
)
|
|
243
|
+
is_user_independent = (
|
|
244
|
+
hasattr(level.owner, "student") and level.owner.student.is_independent()
|
|
245
|
+
)
|
|
246
|
+
is_user_teacher = hasattr(level.owner, "teacher")
|
|
247
|
+
|
|
248
|
+
# when level is created
|
|
249
|
+
if levelId is None:
|
|
250
|
+
teacher = None
|
|
251
|
+
|
|
252
|
+
# if level owner is a school student, share with teacher automatically if they aren't an admin
|
|
253
|
+
if is_user_school_student:
|
|
254
|
+
teacher = level.owner.student.class_field.teacher
|
|
255
|
+
if not teacher.is_admin:
|
|
256
|
+
level.shared_with.add(teacher.new_user)
|
|
257
|
+
|
|
258
|
+
if not data["anonymous"]:
|
|
259
|
+
level_management.email_new_custom_level(
|
|
260
|
+
teacher.new_user.email,
|
|
261
|
+
request.build_absolute_uri(reverse("level_moderation")),
|
|
262
|
+
request.build_absolute_uri(
|
|
263
|
+
reverse("play_custom_level", kwargs={"levelId": level.id})
|
|
264
|
+
),
|
|
265
|
+
str(level.owner.student),
|
|
266
|
+
level.owner.student.class_field.name,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
elif is_user_teacher:
|
|
270
|
+
teacher = level.owner.teacher
|
|
271
|
+
|
|
272
|
+
# if level owner is a teacher or an indy user, approval isn't needed
|
|
273
|
+
if not is_user_school_student:
|
|
274
|
+
level.needs_approval = False
|
|
275
|
+
|
|
276
|
+
# share with all admins of the school if user is in a school
|
|
277
|
+
if not is_user_independent:
|
|
278
|
+
if not teacher.school is None:
|
|
262
279
|
school_admins = teacher.school.admins()
|
|
263
280
|
|
|
264
281
|
[
|
|
@@ -267,11 +284,25 @@ def save_level_for_editor(request, levelId=None):
|
|
|
267
284
|
if school_admin.new_user != request.user
|
|
268
285
|
]
|
|
269
286
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
287
|
+
# anytime a student edits their level
|
|
288
|
+
if is_user_school_student:
|
|
289
|
+
if not level.needs_approval:
|
|
290
|
+
level.needs_approval = True
|
|
291
|
+
|
|
292
|
+
if not data["anonymous"]:
|
|
293
|
+
level_management.email_new_custom_level(
|
|
294
|
+
level.owner.student.class_field.teacher.new_user.email,
|
|
295
|
+
request.build_absolute_uri(reverse("level_moderation")),
|
|
296
|
+
request.build_absolute_uri(
|
|
297
|
+
reverse("play_custom_level", kwargs={"levelId": level.id})
|
|
298
|
+
),
|
|
299
|
+
str(level.owner.student),
|
|
300
|
+
level.owner.student.class_field.name,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
level.save()
|
|
304
|
+
response = {"id": level.id}
|
|
305
|
+
return HttpResponse(json.dumps(response), content_type="application/json")
|
|
275
306
|
|
|
276
307
|
|
|
277
308
|
@transaction.atomic
|
|
@@ -374,9 +405,11 @@ class SharingInformationForEditor(APIView):
|
|
|
374
405
|
)
|
|
375
406
|
valid_recipients["classes"].append(
|
|
376
407
|
{
|
|
377
|
-
"name":
|
|
378
|
-
|
|
379
|
-
|
|
408
|
+
"name": (
|
|
409
|
+
f"{class_.name} ({app_tags.make_into_username(class_.teacher.new_user)})"
|
|
410
|
+
if teacher.is_admin
|
|
411
|
+
else class_.name
|
|
412
|
+
),
|
|
380
413
|
"id": class_.id,
|
|
381
414
|
"students": [
|
|
382
415
|
{
|
game/views/level_moderation.py
CHANGED
|
@@ -2,7 +2,10 @@ from __future__ import absolute_import
|
|
|
2
2
|
from __future__ import division
|
|
3
3
|
|
|
4
4
|
from common.models import Student, Class
|
|
5
|
+
from django.http import HttpResponse, HttpResponseRedirect
|
|
5
6
|
from django.shortcuts import render
|
|
7
|
+
from django.urls import reverse_lazy
|
|
8
|
+
from django.views.decorators.http import require_POST
|
|
6
9
|
from portal.templatetags import app_tags
|
|
7
10
|
|
|
8
11
|
import game.messages as messages
|
|
@@ -132,6 +135,7 @@ def level_moderation(request):
|
|
|
132
135
|
"id": level.id,
|
|
133
136
|
"name": level.name,
|
|
134
137
|
"shared_with": shared_str,
|
|
138
|
+
"needs_approval": level.needs_approval,
|
|
135
139
|
}
|
|
136
140
|
)
|
|
137
141
|
|
|
@@ -144,3 +148,22 @@ def level_moderation(request):
|
|
|
144
148
|
"thead": table_headers,
|
|
145
149
|
},
|
|
146
150
|
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@require_POST
|
|
154
|
+
def approve_level(request, levelID):
|
|
155
|
+
level = Level.objects.get(id=levelID)
|
|
156
|
+
if permissions.can_approve_level(request.user, level):
|
|
157
|
+
level.needs_approval = False
|
|
158
|
+
level.save()
|
|
159
|
+
|
|
160
|
+
return HttpResponseRedirect(reverse_lazy("level_moderation"))
|
|
161
|
+
else:
|
|
162
|
+
return HttpResponseUnauthorized()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class HttpResponseUnauthorized(HttpResponse):
|
|
166
|
+
def __init__(self):
|
|
167
|
+
super(HttpResponseUnauthorized, self).__init__(
|
|
168
|
+
content="Unauthorized", status=401
|
|
169
|
+
)
|