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/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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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: "
|
|
88
|
-
11: "
|
|
89
|
-
12: "
|
|
90
|
-
13: "
|
|
91
|
-
14: "
|
|
92
|
-
15: "
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
168
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
game/python_den_urls.py
ADDED
|
@@ -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.
|
|
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["
|
|
72
|
-
level_data["
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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["
|
|
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}, "
|
|
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["
|
|
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
|
-
"
|
|
31
|
-
"
|
|
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
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
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
|
-
|
|
92
|
-
|
|
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 = ("
|
|
104
|
+
fields = ("blockly_enabled", "python_enabled", "python_view_enabled")
|
|
110
105
|
|
|
111
106
|
|
|
112
107
|
class LevelMapListSerializer(serializers.HyperlinkedModelSerializer):
|