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.
Files changed (170) hide show
  1. example_project/rapid_router_test_settings.py +19 -7
  2. example_project/settings.py +21 -8
  3. example_project/urls.py +5 -6
  4. game/__init__.py +1 -1
  5. game/admin.py +7 -2
  6. game/character.py +8 -0
  7. game/decor.py +40 -0
  8. game/end_to_end_tests/base_game_test.py +34 -27
  9. game/end_to_end_tests/editor_page.py +15 -0
  10. game/end_to_end_tests/game_page.py +88 -20
  11. game/end_to_end_tests/selenium_test_case.py +1 -20
  12. game/end_to_end_tests/test_cow_crashes.py +3 -5
  13. game/end_to_end_tests/test_level_editor.py +273 -10
  14. game/end_to_end_tests/test_level_selection.py +25 -3
  15. game/end_to_end_tests/test_play_through.py +222 -127
  16. game/end_to_end_tests/test_python_levels.py +41 -7
  17. game/end_to_end_tests/test_saving_workspace.py +2 -1
  18. game/forms.py +7 -1
  19. game/level_management.py +26 -11
  20. game/messages.py +899 -337
  21. game/migrations/0001_squashed_0025_levels_ordering_pt1.py +19 -1
  22. game/migrations/0026_levels_pt2.py +13 -2
  23. game/migrations/0032_cannot_turn_left_level.py +13 -2
  24. game/migrations/0033_recursion_level.py +13 -2
  25. game/migrations/0034_joes_level.py +13 -2
  26. game/migrations/0035_disable_route_score_level_70.py +0 -2
  27. game/migrations/0036_level_score_73.py +0 -2
  28. game/migrations/0037_level_score_79.py +0 -2
  29. game/migrations/0038_level_score_40.py +0 -1
  30. game/migrations/0042_level_score_73.py +0 -2
  31. game/migrations/0048_add_cow_field_and_blocks.py +0 -2
  32. game/migrations/0049_level_score_34.py +0 -2
  33. game/migrations/0050_level_score_40.py +0 -2
  34. game/migrations/0051_level_score_49.py +0 -1
  35. game/migrations/0086_loop_levels.py +13 -2
  36. game/migrations/0092_disable_algo_score_in_custom_levels.py +28 -0
  37. game/migrations/0093_alter_level_character_name.py +18 -0
  38. game/migrations/0094_add_hint_lesson_subtitle_to_levels.py +28 -0
  39. game/migrations/0095_level_commands.py +18 -0
  40. game/migrations/0096_alter_level_commands.py +18 -0
  41. game/migrations/0097_add_python_den_levels.py +1515 -0
  42. game/migrations/0098_add_episode_link_fields.py +44 -0
  43. game/migrations/0099_python_episodes_links.py +103 -0
  44. game/migrations/0100_reorder_python_levels.py +179 -0
  45. game/migrations/0101_rename_episodes.py +45 -0
  46. game/migrations/0102_reoder_episodes_13_14.py +136 -0
  47. game/migrations/0103_level_1015_solution.py +26 -0
  48. game/migrations/0104_remove_level_direct_drive.py +17 -0
  49. game/migrations/0105_delete_invalid_attempts.py +18 -0
  50. game/migrations/0106_fields_to_snake_case.py +48 -0
  51. game/migrations/0107_rename_worksheet_link_episode_student_worksheet_link.py +18 -0
  52. game/migrations/0108_episode_indy_worksheet_link.py +18 -0
  53. game/migrations/0109_create_episodes_23_and_24.py +99 -0
  54. game/migrations/0110_remove_episode_indy_worksheet_link_and_more.py +100 -0
  55. game/migrations/0111_create_worksheets.py +149 -0
  56. game/migrations/0112_worksheet_locked_classes.py +21 -0
  57. game/migrations/0113_level_needs_approval.py +18 -0
  58. game/migrations/0114_default_and_non_student_levels_no_approval.py +31 -0
  59. game/migrations/0115_level_level__default_does_not_need_approval.py +22 -0
  60. game/migrations/0116_update_worksheet_video_links.py +68 -0
  61. game/migrations/0117_update_solutions_to_if_else.py +61 -0
  62. game/models.py +127 -17
  63. game/permissions.py +51 -19
  64. game/python_den_urls.py +26 -0
  65. game/random_road.py +9 -9
  66. game/serializers.py +12 -17
  67. game/static/django_reverse_js/js/reverse.js +171 -0
  68. game/static/game/css/LilitaOne-Regular.ttf +0 -0
  69. game/static/game/css/backgrounds.css +8 -12
  70. game/static/game/css/dataTables.custom.css +3 -2
  71. game/static/game/css/editor.css +47 -0
  72. game/static/game/css/game.css +37 -43
  73. game/static/game/css/game_screen.css +16 -0
  74. game/static/game/css/level_editor.css +5 -0
  75. game/static/game/css/level_selection.css +17 -2
  76. game/static/game/image/Python_Den_hero_student.png +0 -0
  77. game/static/game/image/Python_levels_page.svg +1954 -0
  78. game/static/game/image/characters/front_view/Electric_van.svg +448 -0
  79. game/static/game/image/characters/top_view/Electric_van.svg +448 -0
  80. game/static/game/image/decor/city/solar_panel.svg +1200 -0
  81. game/static/game/image/decor/farm/solar_panel.svg +86 -0
  82. game/static/game/image/decor/grass/solar_panel.svg +86 -0
  83. game/static/game/image/decor/snow/solar_panel.svg +173 -0
  84. game/static/game/image/electric_van.svg +448 -0
  85. game/static/game/image/icons/description.svg +1 -0
  86. game/static/game/image/icons/hint.svg +1 -0
  87. game/static/game/image/icons/python.svg +1 -1
  88. game/static/game/image/pigeon.svg +684 -0
  89. game/static/game/image/python_den_header.svg +19 -0
  90. game/static/game/js/animation.js +65 -24
  91. game/static/game/js/blockly/msg/js/bg.js +52 -1
  92. game/static/game/js/blockly/msg/js/ca.js +52 -1
  93. game/static/game/js/blockly/msg/js/en-gb.js +2 -0
  94. game/static/game/js/blockly/msg/js/en.js +2 -0
  95. game/static/game/js/blockly/msg/js/es.js +52 -1
  96. game/static/game/js/blockly/msg/js/fr.js +2 -0
  97. game/static/game/js/blockly/msg/js/hi.js +2 -0
  98. game/static/game/js/blockly/msg/js/it.js +52 -1
  99. game/static/game/js/blockly/msg/js/pl.js +52 -1
  100. game/static/game/js/blockly/msg/js/pt-br.js +52 -1
  101. game/static/game/js/blockly/msg/js/ru.js +52 -1
  102. game/static/game/js/blockly/msg/js/ur.js +52 -1
  103. game/static/game/js/blocklyCustomBlocks.js +93 -52
  104. game/static/game/js/button.js +12 -0
  105. game/static/game/js/cow.js +11 -7
  106. game/static/game/js/drawing.js +68 -29
  107. game/static/game/js/editor.js +23 -0
  108. game/static/game/js/game.js +74 -110
  109. game/static/game/js/level_editor.js +646 -274
  110. game/static/game/js/level_moderation.js +33 -2
  111. game/static/game/js/level_selection.js +1 -1
  112. game/static/game/js/loadLanguages.js +2 -2
  113. game/static/game/js/model.js +32 -2
  114. game/static/game/js/pythonControl.js +14 -1
  115. game/static/game/js/scoreboard.js +0 -37
  116. game/static/game/js/scoreboardSharedLevels.js +48 -0
  117. game/static/game/js/skulpt/skulpt-stdlib.js +1 -1
  118. game/static/game/js/sound.js +52 -5
  119. game/static/game/raphael_image/characters/top_view/Electric_van.svg +448 -0
  120. game/static/game/raphael_image/decor/city/solar_panel.svg +1200 -0
  121. game/static/game/raphael_image/decor/farm/solar_panel.svg +86 -0
  122. game/static/game/raphael_image/decor/grass/solar_panel.svg +86 -0
  123. game/static/game/raphael_image/decor/snow/solar_panel.svg +173 -0
  124. game/static/game/raphael_image/pigeon.svg +685 -0
  125. game/static/game/sass/game.scss +2 -2
  126. game/static/game/sound/clown_horn.mp3 +0 -0
  127. game/static/game/sound/clown_horn.ogg +0 -0
  128. game/static/game/sound/electric_van_starting.mp3 +0 -0
  129. game/static/game/sound/electric_van_starting.ogg +0 -0
  130. game/static/game/sound/pigeon.mp3 +0 -0
  131. game/static/game/sound/pigeon.ogg +0 -0
  132. game/static/game/sound/sleigh_bells.mp3 +0 -0
  133. game/static/game/sound/sleigh_bells.ogg +0 -0
  134. game/static/game/sound/sleigh_crash.mp3 +0 -0
  135. game/static/game/sound/sleigh_crash.ogg +0 -0
  136. game/templates/game/base.html +34 -14
  137. game/templates/game/basenonav.html +11 -5
  138. game/templates/game/game.html +142 -38
  139. game/templates/game/level_editor.html +340 -236
  140. game/templates/game/level_moderation.html +19 -6
  141. game/templates/game/level_selection.html +18 -110
  142. game/templates/game/python_den_level_selection.html +291 -0
  143. game/templates/game/python_den_worksheet.html +101 -0
  144. game/templates/game/scoreboard.html +83 -64
  145. game/tests/test_level_editor.py +94 -26
  146. game/tests/test_level_selection.py +149 -46
  147. game/tests/test_python_den_worksheet.py +85 -0
  148. game/tests/test_scoreboard.py +34 -7
  149. game/tests/utils/level.py +32 -26
  150. game/theme.py +5 -5
  151. game/urls.py +199 -61
  152. game/views/language_code_conversions.py +86 -86
  153. game/views/level.py +155 -63
  154. game/views/level_editor.py +88 -55
  155. game/views/level_moderation.py +23 -0
  156. game/views/level_selection.py +116 -47
  157. game/views/level_solutions.py +491 -106
  158. game/views/scoreboard.py +76 -51
  159. game/views/worksheet.py +25 -0
  160. rapid_router-7.6.8.dist-info/METADATA +174 -0
  161. {rapid_router-5.18.0.dist-info → rapid_router-7.6.8.dist-info}/RECORD +164 -104
  162. {rapid_router-5.18.0.dist-info → rapid_router-7.6.8.dist-info}/WHEEL +1 -1
  163. example_project/manage.py +0 -10
  164. game/static/game/image/actions/go.svg +0 -18
  165. game/static/game/js/js-reverse.js +0 -14
  166. game/static/game/js/pqselect.min.js +0 -9
  167. game/static/game/js/widget-scroller.js +0 -906
  168. rapid_router-5.18.0.dist-info/METADATA +0 -17
  169. {rapid_router-5.18.0.dist-info → rapid_router-7.6.8.dist-info/licenses}/LICENSE.md +0 -0
  170. {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 render, get_object_or_404
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 Level, Attempt, Workspace
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, levelName):
48
- level = cached_default_level(levelName)
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 _prev_level_url(level, user, night_mode):
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 it.
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` field in the Level model, but in the case
77
- where the user is a student and the teacher has locked certain levels, then loop until we find the next unlocked
78
- level (or we run out of levels).
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 int(next_level.name) < 109:
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
- return reverse("play_default_level", args=[level.name])
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 = False if not app_settings.NIGHT_MODE_FEATURE_ENABLED else "night" in request.GET
143
-
144
- if not permissions.can_play_level(request.user, level, app_settings.EARLY_ACCESS_FUNCTION(request)):
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
- lesson = mark_safe(lessonCall())
166
- hint = mark_safe(hintCall())
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(level=level, student=student, score=None, night_mode=night_mode)
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 = "level_editor" if from_editor else "levels"
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
- [temp_block_data.append(block) for block in block_data if block not in temp_block_data]
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(app_settings.NIGHT_MODE_FEATURE_ENABLED).lower(),
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(level, request.user, night_mode),
255
- "next_level_url": _next_level_url(level, request.user, night_mode),
256
- "flip_night_mode_url": _level_url(level, not night_mode),
257
- "available_language_dict": language_code_dict
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(level=attempt.level, student=attempt.student, night_mode=attempt.night_mode)
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(json.dumps({"success": success}), content_type="application/javascript")
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 not request.user.is_anonymous and request.method == "POST" and hasattr(request.user.userprofile, "student"):
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(level=level, student=student, finish_time__isnull=True).first()
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 = ["name", "contents", "python_contents", "blockly_enabled", "python_enabled", "pythonViewEnabled"]
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("Request missing the following required parameters", missing_params)
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("pythonViewEnabled"))
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.blocklyEnabled:
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, False)
520
+ return play_level(request, episode.first_level, from_python_den=True)
429
521
 
430
522
 
431
523
  @require_POST
@@ -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
- lesson = mark_safe(messages.description_level_default())
90
- hint = mark_safe(messages.hint_level_default())
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
- pattern = re.compile("^(\w?[ ]?)*$")
224
-
225
- if pattern.match(data["name"]):
226
- level_management.save_level(level, data)
227
-
228
- if levelId is None:
229
- teacher = None
230
-
231
- is_user_school_student = (
232
- hasattr(level.owner, "student")
233
- and not level.owner.student.is_independent()
234
- )
235
- is_user_independent = (
236
- hasattr(level.owner, "student") and level.owner.student.is_independent()
237
- )
238
- is_user_teacher = hasattr(level.owner, "teacher")
239
-
240
- # if level owner is a school student, share with teacher automatically if they aren't an admin
241
- if is_user_school_student:
242
- teacher = level.owner.student.class_field.teacher
243
- if not teacher.is_admin:
244
- level.shared_with.add(teacher.new_user)
245
-
246
- if not data["anonymous"]:
247
- level_management.email_new_custom_level(
248
- level.owner.student.class_field.teacher.new_user.email,
249
- request.build_absolute_uri(reverse("level_moderation")),
250
- request.build_absolute_uri(
251
- reverse("play_custom_level", kwargs={"levelId": level.id})
252
- ),
253
- request.build_absolute_uri(reverse("home")),
254
- str(level.owner.student),
255
- level.owner.student.class_field.name,
256
- )
257
- elif is_user_teacher:
258
- teacher = level.owner.teacher
259
-
260
- # share with all admins of the school if user is in a school
261
- if not is_user_independent:
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
- level.save()
271
- response = {"id": level.id}
272
- return HttpResponse(json.dumps(response), content_type="application/javascript")
273
- else:
274
- return HttpResponseUnauthorized()
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": f"{class_.name} ({app_tags.make_into_username(class_.teacher.new_user)})"
378
- if teacher.is_admin
379
- else class_.name,
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
  {
@@ -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
+ )