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/models.py CHANGED
@@ -1,8 +1,10 @@
1
+ import typing as t
1
2
  from builtins import str
2
3
 
3
4
  from common.models import Class, Student, UserProfile
4
5
  from django.contrib.auth.models import User
5
6
  from django.db import models
7
+ from django.db.models.query import QuerySet
6
8
 
7
9
 
8
10
  def theme_choices():
@@ -41,9 +43,11 @@ class Block(models.Model):
41
43
  class Episode(models.Model):
42
44
  """Variables prefixed with r_ signify they are parameters for random level generation"""
43
45
 
46
+ worksheets: QuerySet["Worksheet"]
47
+
44
48
  name = models.CharField(max_length=200)
45
49
  next_episode = models.ForeignKey(
46
- "self", null=True, default=None, on_delete=models.SET_NULL
50
+ "self", null=True, blank=True, default=None, on_delete=models.SET_NULL
47
51
  )
48
52
  in_development = models.BooleanField(default=False)
49
53
 
@@ -53,9 +57,9 @@ class Episode(models.Model):
53
57
  r_curviness = models.FloatField(default=0, null=True)
54
58
  r_num_tiles = models.IntegerField(default=5, null=True)
55
59
  r_blocks = models.ManyToManyField(Block, related_name="episodes")
56
- r_blocklyEnabled = models.BooleanField(default=True)
57
- r_pythonEnabled = models.BooleanField(default=False)
58
- r_trafficLights = models.BooleanField(default=False)
60
+ r_blockly_enabled = models.BooleanField(default=True)
61
+ r_python_enabled = models.BooleanField(default=False)
62
+ r_traffic_lights = models.BooleanField(default=False)
59
63
  r_cows = models.BooleanField(default=False)
60
64
 
61
65
  @property
@@ -84,12 +88,23 @@ class Episode(models.Model):
84
88
  7: "medium-hard",
85
89
  8: "medium-hard",
86
90
  9: "brainteasers",
87
- 10: "hard",
88
- 11: "advanced",
89
- 12: "loops",
90
- 13: "loops",
91
- 14: "loops",
92
- 15: "loops",
91
+ 10: "early-python",
92
+ 11: "early-python",
93
+ 12: "late-python",
94
+ 13: "late-python",
95
+ 14: "late-python",
96
+ 15: "late-python",
97
+ 16: "early-python",
98
+ 17: "early-python",
99
+ 18: "early-python",
100
+ 19: "early-python",
101
+ 20: "late-python",
102
+ 21: "late-python",
103
+ 22: "late-python",
104
+ 23: "late-python",
105
+ 24: "late-python",
106
+ 25: "late-python",
107
+ 26: "late-python",
93
108
  }
94
109
 
95
110
  return difficulty_map.get(self.id, "easy")
@@ -115,6 +130,8 @@ def sort_levels(levels):
115
130
 
116
131
 
117
132
  class Level(models.Model):
133
+ after_worksheet: t.Optional["Worksheet"]
134
+
118
135
  name = models.CharField(max_length=100)
119
136
  episode = models.ForeignKey(
120
137
  Episode, blank=True, null=True, default=None, on_delete=models.PROTECT
@@ -134,10 +151,10 @@ class Level(models.Model):
134
151
  )
135
152
  fuel_gauge = models.BooleanField(default=True)
136
153
  max_fuel = models.IntegerField(default=50)
137
- direct_drive = models.BooleanField(default=False)
138
154
  next_level = models.ForeignKey(
139
155
  "self",
140
156
  null=True,
157
+ blank=True,
141
158
  default=None,
142
159
  on_delete=models.SET_NULL,
143
160
  related_name="prev_level",
@@ -147,21 +164,81 @@ class Level(models.Model):
147
164
  disable_route_score = models.BooleanField(default=False)
148
165
  disable_algorithm_score = models.BooleanField(default=False)
149
166
  threads = models.IntegerField(blank=False, default=1)
150
- blocklyEnabled = models.BooleanField(default=True)
151
- pythonEnabled = models.BooleanField(default=True)
152
- pythonViewEnabled = models.BooleanField(default=False)
167
+ blockly_enabled = models.BooleanField(default=True)
168
+ python_enabled = models.BooleanField(default=True)
169
+ python_view_enabled = models.BooleanField(default=False)
153
170
  theme_name = models.CharField(
154
- max_length=10, choices=theme_choices(), blank=True, null=True, default=None
171
+ max_length=10,
172
+ choices=theme_choices(),
173
+ blank=True,
174
+ null=True,
175
+ default=None,
155
176
  )
156
177
  character_name = models.CharField(
157
- max_length=20, choices=character_choices(), blank=True, null=True, default=None
178
+ max_length=20,
179
+ choices=character_choices(),
180
+ blank=True,
181
+ null=True,
182
+ default=None,
183
+ )
184
+ subtitle = models.TextField(max_length=100, blank=True, null=True)
185
+ lesson = models.TextField(
186
+ max_length=10000, default="Can you find the shortest route?"
187
+ )
188
+ hint = models.TextField(
189
+ max_length=10000,
190
+ default="Think back to earlier levels. What did you learn?",
191
+ )
192
+ commands = models.TextField(
193
+ max_length=10000,
194
+ default='<div class="row">'
195
+ + '<div class="large-4 columns">'
196
+ + "<p><b>Movement</b>"
197
+ + "<br>my_van.move_forwards()"
198
+ + "<br>my_van.turn_around()"
199
+ + "<br>my_van.turn_left()"
200
+ + "<br>my_van.turn_right()"
201
+ + "<br>my_van.wait()<p></div>"
202
+ + '<div class="large-4 columns">'
203
+ + "<p><b>Position</b>"
204
+ + "<br>my_van.at_dead_end()"
205
+ + "<br>my_van.at_destination()"
206
+ + "<br>my_van.at_red_traffic_light()"
207
+ + "<br>my_van.at_green_traffic_light()"
208
+ + "<br>my_van.at_traffic_light(c)"
209
+ + "<br><i>where c is 'RED' or 'GREEN'</i></p></div>"
210
+ + '<div class="large-4 columns">'
211
+ + "<p><br>my_van.is_road_right()"
212
+ + "<br>my_van.is_road_left()"
213
+ + "<br>my_van.is_road_forward()"
214
+ + "<br>my_van.is_road(d)"
215
+ + "<br><i>where d is 'FORWARD', 'LEFT', or 'RIGHT'</i></p></div>"
216
+ + "</div>"
217
+ + '<div class="row">'
218
+ + '<div class="large-4 columns">'
219
+ + "<p><b>Animals</b>"
220
+ + "<br>my_van.is_animal_crossing()"
221
+ + "<br>my_van.sound_horn()</div>"
222
+ + "</div>",
158
223
  )
159
224
  anonymous = models.BooleanField(default=False)
160
225
  locked_for_class = models.ManyToManyField(
161
226
  Class, blank=True, related_name="locked_levels"
162
227
  )
228
+ needs_approval = models.BooleanField(default=True)
163
229
  objects = LevelManager()
164
230
 
231
+ class Meta:
232
+ constraints = [
233
+ models.CheckConstraint(
234
+ check=~models.Q(
235
+ default=True,
236
+ needs_approval=True,
237
+ ),
238
+ name="level__default_does_not_need_approval",
239
+ ),
240
+ ]
241
+
165
242
  def __str__(self):
166
243
  return f"Level {self.name}"
167
244
 
@@ -210,7 +287,7 @@ class LevelDecor(models.Model):
210
287
  x = models.IntegerField()
211
288
  y = models.IntegerField()
212
289
  level = models.ForeignKey(Level, on_delete=models.CASCADE)
213
- decorName = models.CharField(max_length=100, default="tree1")
290
+ decor_name = models.CharField(max_length=100, default="tree1")
214
291
 
215
292
 
216
293
  class Workspace(models.Model):
@@ -251,3 +328,36 @@ class Attempt(models.Model):
251
328
 
252
329
  def elapsed_time(self):
253
330
  return self.finish_time - self.start_time
331
+
332
+
333
+ class Worksheet(models.Model):
334
+ episode = models.ForeignKey(
335
+ Episode,
336
+ blank=True,
337
+ null=True,
338
+ default=None,
339
+ on_delete=models.PROTECT,
340
+ related_name="worksheets",
341
+ )
342
+ before_level = models.OneToOneField(
343
+ Level,
344
+ blank=True,
345
+ null=True,
346
+ default=None,
347
+ on_delete=models.PROTECT,
348
+ related_name="after_worksheet",
349
+ )
350
+ lesson_plan_link = models.CharField(
351
+ max_length=500, null=True, blank=True, default=None
352
+ )
353
+ slides_link = models.CharField(max_length=500, null=True, blank=True, default=None)
354
+ student_worksheet_link = models.CharField(
355
+ max_length=500, null=True, blank=True, default=None
356
+ )
357
+ indy_worksheet_link = models.CharField(
358
+ max_length=500, null=True, blank=True, default=None
359
+ )
360
+ video_link = models.CharField(max_length=500, null=True, blank=True, default=None)
361
+ locked_classes = models.ManyToManyField(
362
+ Class, blank=True, related_name="locked_worksheets"
363
+ )
game/permissions.py CHANGED
@@ -1,9 +1,5 @@
1
- import logging
2
-
3
1
  from rest_framework import permissions
4
2
 
5
- LOGGER = logging.getLogger(__name__)
6
-
7
3
 
8
4
  def _get_userprofile_school(userprofile):
9
5
  if hasattr(userprofile, "teacher"):
@@ -11,7 +7,6 @@ def _get_userprofile_school(userprofile):
11
7
  elif hasattr(userprofile, "student"):
12
8
  return userprofile.student.class_field.teacher.school
13
9
  else:
14
- LOGGER.error(f"Userprofile ID {userprofile.id} has no teacher or student attribute")
15
10
  return None
16
11
 
17
12
 
@@ -49,15 +44,32 @@ def can_play_or_delete_level(user, level):
49
44
  # If the teacher is an admin, they can play any student's level in the school, otherwise only student levels
50
45
  # from their own classes
51
46
  if user.userprofile.teacher.is_admin and hasattr(level.owner, "student"):
52
- return user.userprofile.teacher.school == level.owner.student.class_field.teacher.school
47
+ return (
48
+ user.userprofile.teacher.school
49
+ == level.owner.student.class_field.teacher.school
50
+ )
53
51
  else:
54
52
  return user.userprofile.teacher.teaches(level.owner)
55
53
 
56
54
 
55
+ def can_approve_level(user, level):
56
+ return (
57
+ hasattr(user.userprofile, "teacher")
58
+ and level.shared_with.filter(id=user.id).exists()
59
+ )
60
+
61
+
57
62
  def can_play_level(user, level, early_access):
58
- if not user.is_anonymous and hasattr(user.userprofile, "student") and user.userprofile.student.class_field:
63
+ if (
64
+ not user.is_anonymous
65
+ and hasattr(user.userprofile, "student")
66
+ and user.userprofile.student.class_field
67
+ ):
59
68
  # If the user is a student, check that the level isn't locked for their class
60
- return user.userprofile.student.class_field not in level.locked_for_class.all()
69
+ return user.userprofile.id == level.owner_id or (
70
+ user.userprofile.student.class_field not in level.locked_for_class.all()
71
+ and not level.needs_approval
72
+ )
61
73
  elif level.default and not level.episode.in_development:
62
74
  return True
63
75
  elif level.anonymous:
@@ -86,7 +98,9 @@ def can_load_level(user, level):
86
98
  owner_school = _get_userprofile_school(level.owner)
87
99
  return user_school is not None and user_school == owner_school
88
100
  else:
89
- return hasattr(user.userprofile, "teacher") and user.userprofile.teacher.teaches(level.owner)
101
+ return hasattr(
102
+ user.userprofile, "teacher"
103
+ ) and user.userprofile.teacher.teaches(level.owner)
90
104
 
91
105
 
92
106
  def can_save_level(user, level):
@@ -112,8 +126,8 @@ def can_delete_level(user, level):
112
126
  class CanShareLevel(permissions.BasePermission):
113
127
  """
114
128
  Used to verify that an incoming request is made by a user who is authorised to share
115
- the level - that is, that they are the owner of the level as a student, or if they're a teacher that the level was
116
- shared with them.
129
+ the level - that is, that they are the owner of the level as a student and the level has been approved by a teacher,
130
+ or if they're a teacher that the level was shared with them.
117
131
  """
118
132
 
119
133
  def has_permission(self, request, view):
@@ -122,13 +136,19 @@ class CanShareLevel(permissions.BasePermission):
122
136
  def has_object_permission(self, request, view, obj):
123
137
  if request.user.is_anonymous:
124
138
  return False
125
- elif hasattr(request.user.userprofile, "student") and request.user.userprofile.student.is_independent():
139
+ elif (
140
+ hasattr(request.user.userprofile, "student")
141
+ and request.user.userprofile.student.is_independent()
142
+ ):
126
143
  return False
127
144
  # if the user is a teacher and the level is shared with them
128
- elif hasattr(request.user.userprofile, "teacher") and obj.shared_with.filter(id=request.user.id).exists():
145
+ elif (
146
+ hasattr(request.user.userprofile, "teacher")
147
+ and obj.shared_with.filter(id=request.user.id).exists()
148
+ ):
129
149
  return True
130
150
  else:
131
- return obj.owner == request.user.userprofile
151
+ return obj.owner == request.user.userprofile and not obj.needs_approval
132
152
 
133
153
 
134
154
  class CanShareLevelWith(permissions.BasePermission):
@@ -164,18 +184,30 @@ class CanShareLevelWith(permissions.BasePermission):
164
184
  and not (recipient_profile.student.is_independent())
165
185
  ):
166
186
  # Are they in the same class?
167
- return sharer_profile.student.class_field == recipient_profile.student.class_field
168
- elif hasattr(sharer_profile, "teacher") and sharer_profile.teacher.teaches(recipient_profile):
187
+ return (
188
+ sharer_profile.student.class_field
189
+ == recipient_profile.student.class_field
190
+ )
191
+ elif hasattr(sharer_profile, "teacher") and sharer_profile.teacher.teaches(
192
+ recipient_profile
193
+ ):
169
194
  # Is the recipient taught by the sharer?
170
195
  return True
171
- elif hasattr(recipient_profile, "teacher") and recipient_profile.teacher.teaches(sharer_profile):
196
+ elif hasattr(
197
+ recipient_profile, "teacher"
198
+ ) and recipient_profile.teacher.teaches(sharer_profile):
172
199
  # Is the sharer taught by the recipient?
173
200
  return True
174
- elif hasattr(sharer_profile, "teacher") and hasattr(recipient_profile, "teacher"):
201
+ elif hasattr(sharer_profile, "teacher") and hasattr(
202
+ recipient_profile, "teacher"
203
+ ):
175
204
  # Are they in the same organisation?
176
205
  return recipient_profile.teacher.school == sharer_profile.teacher.school
177
206
  elif hasattr(sharer_profile, "teacher") and sharer_profile.teacher.is_admin:
178
- return recipient_profile.student.class_field.teacher.school == sharer_profile.teacher.school
207
+ return (
208
+ recipient_profile.student.class_field.teacher.school
209
+ == sharer_profile.teacher.school
210
+ )
179
211
  else:
180
212
  return False
181
213
 
@@ -0,0 +1,26 @@
1
+ from django.urls import re_path
2
+
3
+ from game.views.level import play_default_python_level, start_python_episode
4
+ from game.views.level_selection import python_levels
5
+ from game.views.scoreboard import python_scoreboard
6
+ from game.views.worksheet import worksheet
7
+
8
+ urlpatterns = [
9
+ re_path(r"^$", python_levels, name="python_levels"),
10
+ re_path(
11
+ r"^(?P<level_name>[A-Z0-9]+)/$",
12
+ play_default_python_level,
13
+ name="play_python_default_level",
14
+ ),
15
+ re_path(
16
+ r"^episode/(?P<episodeId>[0-9]+)/$",
17
+ start_python_episode,
18
+ name="start_python_episode",
19
+ ),
20
+ re_path(
21
+ r"^worksheet/(?P<worksheetId>[0-9]+)/$",
22
+ worksheet,
23
+ name="worksheet",
24
+ ),
25
+ re_path(r"^scoreboard/$", python_scoreboard, name="python_scoreboard"),
26
+ ]
game/random_road.py CHANGED
@@ -58,7 +58,7 @@ def create(episode=None):
58
58
  loopiness = episode.r_loopiness if episode else DEFAULT_LOOPINESS
59
59
  curviness = episode.r_curviness if episode else DEFAULT_CURVINESS
60
60
  blocks = episode.r_blocks.all() if episode else Block.objects.all()
61
- traffic_lights = episode.r_trafficLights if episode else DEFAULT_TRAFFIC_LIGHTS
61
+ traffic_lights = episode.r_traffic_lights if episode else DEFAULT_TRAFFIC_LIGHTS
62
62
  cows = episode.r_cows if episode else DEFAULT_TRAFFIC_LIGHTS
63
63
  decor = DEFAULT_DECOR
64
64
 
@@ -68,8 +68,8 @@ def create(episode=None):
68
68
  level_data["theme"] = 1
69
69
  level_data["name"] = ("Random level for " + episode.name) if episode else "Default random level"
70
70
  level_data["character"] = 1
71
- level_data["blocklyEnabled"] = episode.r_blocklyEnabled if episode else True
72
- level_data["pythonEnabled"] = episode.r_pythonEnabled if episode else False
71
+ level_data["blockly_enabled"] = episode.r_blockly_enabled if episode else True
72
+ level_data["python_enabled"] = episode.r_python_enabled if episode else False
73
73
  level_data["blocks"] = [{"type": block.type} for block in blocks]
74
74
 
75
75
  level = Level(default=False, anonymous=True)
@@ -276,7 +276,7 @@ def generate_traffic_lights(path):
276
276
  random.shuffle(candidateNodes)
277
277
  nodesSelected = candidateNodes[:numberOfJunctions]
278
278
 
279
- trafficLights = []
279
+ traffic_lights = []
280
280
  for node in nodesSelected:
281
281
 
282
282
  controlledNeighbours = []
@@ -291,7 +291,7 @@ def generate_traffic_lights(path):
291
291
 
292
292
  direction = get_direction(node, neighbour)
293
293
 
294
- trafficLights.append(
294
+ traffic_lights.append(
295
295
  {
296
296
  "sourceCoordinate": {"x": neighbour["coordinate"].x, "y": neighbour["coordinate"].y},
297
297
  "direction": direction,
@@ -303,7 +303,7 @@ def generate_traffic_lights(path):
303
303
  )
304
304
  counter += 1
305
305
 
306
- return trafficLights
306
+ return traffic_lights
307
307
 
308
308
 
309
309
  def generate_cows(path):
@@ -346,7 +346,7 @@ def generate_decor(path, num_tiles):
346
346
  elem == "pond"
347
347
  and (coord["x"] // GRID_SIZE == x + 1 and coord["y"] // GRID_SIZE == y or x + 1 < WIDTH)
348
348
  )
349
- or (dec["decorName"] == "pond" and coord["x"] // GRID_SIZE + 1 == x and coord["y"] // GRID_SIZE == y)
349
+ or (dec["decor_name"] == "pond" and coord["x"] // GRID_SIZE + 1 == x and coord["y"] // GRID_SIZE == y)
350
350
  ):
351
351
  return True
352
352
 
@@ -368,7 +368,7 @@ def generate_decor(path, num_tiles):
368
368
  x = x * GRID_SIZE + int((GRID_SIZE - decor_object.width) * 0.5 * (1 - dx))
369
369
  y = y * GRID_SIZE + int((GRID_SIZE - decor_object.height) * 0.5 * (1 - dy))
370
370
 
371
- decor.append({"coordinate": {"x": x, "y": y}, "decorName": dec, "height": decor_object.height})
371
+ decor.append({"coordinate": {"x": x, "y": y}, "decor_name": dec, "height": decor_object.height})
372
372
 
373
373
  def place_near_road(elem, decor, path):
374
374
  for i in range(1, len(path) - 1):
@@ -389,7 +389,7 @@ def generate_decor(path, num_tiles):
389
389
  def place_bush(elem, decor, nodes):
390
390
  bush_exists = False
391
391
  for dec in decor:
392
- if dec["decorName"] == elem:
392
+ if dec["decor_name"] == elem:
393
393
  bush_exists = True
394
394
  for (dx, dy) in DIRECTIONS:
395
395
  x = dec["coordinate"]["x"] // GRID_SIZE + dx
game/serializers.py CHANGED
@@ -5,7 +5,6 @@ from builtins import object
5
5
  from rest_framework import serializers
6
6
 
7
7
  from game import messages
8
- from game.messages import description_level_default, hint_level_default
9
8
  from game.theme import get_theme, get_themes_url
10
9
  from .models import Workspace, Level, Episode, LevelDecor, LevelBlock, Block
11
10
 
@@ -27,8 +26,8 @@ class LevelListSerializer(serializers.HyperlinkedModelSerializer):
27
26
  "name",
28
27
  "title",
29
28
  "default",
30
- "blocklyEnabled",
31
- "pythonEnabled",
29
+ "blockly_enabled",
30
+ "python_enabled",
32
31
  )
33
32
 
34
33
  def get_title(self, obj):
@@ -43,6 +42,7 @@ class LevelDetailSerializer(serializers.HyperlinkedModelSerializer):
43
42
  title = serializers.SerializerMethodField()
44
43
  description = serializers.SerializerMethodField()
45
44
  hint = serializers.SerializerMethodField()
45
+ commands = serializers.SerializerMethodField()
46
46
  levelblock_set = serializers.HyperlinkedIdentityField(
47
47
  view_name="levelblock-for-level", read_only=True
48
48
  )
@@ -67,9 +67,9 @@ class LevelDetailSerializer(serializers.HyperlinkedModelSerializer):
67
67
  "levelblock_set",
68
68
  "map",
69
69
  "mode",
70
- "blocklyEnabled",
71
- "pythonEnabled",
72
- "pythonViewEnabled",
70
+ "blockly_enabled",
71
+ "python_enabled",
72
+ "python_view_enabled",
73
73
  )
74
74
 
75
75
  def get_title(self, obj):
@@ -80,18 +80,13 @@ class LevelDetailSerializer(serializers.HyperlinkedModelSerializer):
80
80
  return "Custom Level"
81
81
 
82
82
  def get_description(self, obj):
83
- if obj.default:
84
- description = getattr(messages, "description_level" + obj.name)()
85
- return description
86
- else:
87
- return description_level_default()
83
+ return getattr(messages, "description_level" + obj.name)() if obj.default else obj.description
88
84
 
89
85
  def get_hint(self, obj):
90
- if obj.default:
91
- hint = getattr(messages, "hint_level" + obj.name)()
92
- return hint
93
- else:
94
- return hint_level_default()
86
+ return getattr(messages, "hint_level" + obj.name)() if obj.default else obj.hint
87
+
88
+ def get_commands(self, obj):
89
+ return getattr(messages, "commands_level" + obj.name)() if obj.default else obj.commands
95
90
 
96
91
  def get_leveldecor_set(self, obj):
97
92
  leveldecors = LevelDecor.objects.filter(level__id=obj.id)
@@ -106,7 +101,7 @@ class LevelDetailSerializer(serializers.HyperlinkedModelSerializer):
106
101
  class LevelModeSerializer(serializers.HyperlinkedModelSerializer):
107
102
  class Meta(object):
108
103
  model = Level
109
- fields = ("blocklyEnabled", "pythonEnabled", "pythonViewEnabled")
104
+ fields = ("blockly_enabled", "python_enabled", "python_view_enabled")
110
105
 
111
106
 
112
107
  class LevelMapListSerializer(serializers.HyperlinkedModelSerializer):