codeforlife-portal 6.46.1__py2.py3-none-any.whl → 7.0.0__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.
Potentially problematic release.
This version of codeforlife-portal might be problematic. Click here for more details.
- cfl_common/common/csp_config.py +0 -2
- cfl_common/common/migrations/0005_add_worksheets.py +1 -5
- cfl_common/common/migrations/0007_add_pdf_names_to_first_two_worksheets.py +1 -5
- cfl_common/common/migrations/0054_delete_aimmo_models.py +20 -0
- cfl_common/common/models.py +0 -25
- {codeforlife_portal-6.46.1.dist-info → codeforlife_portal-7.0.0.dist-info}/METADATA +3 -4
- {codeforlife_portal-6.46.1.dist-info → codeforlife_portal-7.0.0.dist-info}/RECORD +42 -66
- example_project/portal_test_settings.py +0 -1
- example_project/settings.py +0 -1
- example_project/urls.py +0 -2
- portal/__init__.py +1 -1
- portal/static/portal/sass/partials/_banners.scss +0 -177
- portal/static/portal/sass/partials/_buttons.scss +0 -12
- portal/static/portal/sass/partials/_grids.scss +0 -53
- portal/static/portal/sass/partials/_text.scss +1 -10
- portal/static/portal/sass/styles.scss +0 -1
- portal/strings/play.py +1 -2
- portal/strings/teacher_resources.py +0 -10
- portal/templates/portal/about.html +91 -60
- portal/templates/portal/contribute.html +45 -49
- portal/templates/portal/partials/header.html +0 -12
- portal/templates/portal/play/independent_student_dashboard.html +12 -25
- portal/templates/portal/play/student_dashboard.html +16 -34
- portal/templates/portal/play.html +36 -49
- portal/templates/portal/register.html +1 -1
- portal/templates/portal/teach.html +37 -55
- portal/templates/portal/ten_year_map.html +9 -9
- portal/templatetags/app_tags.py +13 -28
- portal/tests/conftest.py +4 -16
- portal/tests/pageObjects/portal/base_page.py +20 -20
- portal/tests/snapshots/snap_test_partials.py +0 -452
- portal/tests/test_class.py +213 -45
- portal/tests/test_independent_student.py +0 -9
- portal/tests/test_partials.py +6 -56
- portal/tests/test_teacher.py +221 -285
- portal/tests/test_views.py +156 -73
- portal/urls.py +23 -20
- portal/views/student/play.py +36 -25
- portal/views/teacher/teach.py +0 -5
- cfl_common/common/tests/test_migration_aimmo_characters.py +0 -29
- portal/forms/add_game.py +0 -29
- portal/static/portal/img/kurono_hero.jpg +0 -0
- portal/static/portal/img/kurono_landing_hero.png +0 -0
- portal/static/portal/img/kurono_logo.svg +0 -1
- portal/static/portal/img/kurono_logo_grey_background.svg +0 -1
- portal/static/portal/img/kurono_logo_mark.svg +0 -1
- portal/static/portal/img/kurono_resources_hero.jpg +0 -0
- portal/static/portal/img/kurono_story.png +0 -0
- portal/static/portal/img/thumbnail_educate_kurono.png +0 -0
- portal/static/portal/img/thumbnail_kurono_resources.png +0 -0
- portal/static/portal/img/thumbnail_play_kurono.png +0 -0
- portal/static/portal/js/aimmoGame.js +0 -106
- portal/static/portal/sass/partials/_videos.scss +0 -10
- portal/static/portal/video/aimmo_play_now_background_video.mp4 +0 -0
- portal/strings/student_aimmo_dashboard.py +0 -6
- portal/templates/portal/partials/aimmo_games_table.html +0 -89
- portal/templates/portal/play/student_aimmo_dashboard.html +0 -46
- portal/templates/portal/teach/teacher_aimmo_dashboard.html +0 -95
- portal/templatetags/character_list_tags.py +0 -16
- portal/tests/pageObjects/portal/kurono_teacher_dashboard_page.py +0 -49
- portal/tests/test_aimmo_dashboards.py +0 -206
- portal/tests/utils/aimmo_games.py +0 -30
- portal/views/aimmo/__init__.py +0 -0
- portal/views/aimmo/dashboard.py +0 -105
- {codeforlife_portal-6.46.1.dist-info → codeforlife_portal-7.0.0.dist-info}/LICENSE.md +0 -0
- {codeforlife_portal-6.46.1.dist-info → codeforlife_portal-7.0.0.dist-info}/WHEEL +0 -0
- {codeforlife_portal-6.46.1.dist-info → codeforlife_portal-7.0.0.dist-info}/top_level.txt +0 -0
portal/views/student/play.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from typing import Any, Dict, List, Optional
|
|
2
2
|
|
|
3
|
-
from common.helpers.emails import NOTIFICATION_EMAIL, send_email
|
|
4
3
|
from common.mail import campaign_ids, send_dotdigital_email
|
|
5
4
|
from common.models import Student
|
|
6
5
|
from common.permissions import (
|
|
@@ -22,7 +21,9 @@ from game.models import Attempt, Level
|
|
|
22
21
|
from portal.forms.play import StudentJoinOrganisationForm
|
|
23
22
|
|
|
24
23
|
|
|
25
|
-
class SchoolStudentDashboard(
|
|
24
|
+
class SchoolStudentDashboard(
|
|
25
|
+
LoginRequiredNoErrorMixin, UserPassesTestMixin, TemplateView
|
|
26
|
+
):
|
|
26
27
|
template_name = "portal/play/student_dashboard.html"
|
|
27
28
|
login_url = reverse_lazy("student_login_access_code")
|
|
28
29
|
|
|
@@ -31,10 +32,9 @@ class SchoolStudentDashboard(LoginRequiredNoErrorMixin, UserPassesTestMixin, Tem
|
|
|
31
32
|
|
|
32
33
|
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
|
33
34
|
"""
|
|
34
|
-
Gathers the context data required by the template. First, the student's
|
|
35
|
-
for the original Rapid Router levels is gathered, second,
|
|
36
|
-
for any levels shared with them by their teacher
|
|
37
|
-
Kurono game information if they have one.
|
|
35
|
+
Gathers the context data required by the template. First, the student's
|
|
36
|
+
scores for the original Rapid Router levels is gathered, second,
|
|
37
|
+
the student's scores for any levels shared with them by their teacher.
|
|
38
38
|
"""
|
|
39
39
|
# Get score data for all original levels
|
|
40
40
|
levels = Level.objects.sorted_levels()
|
|
@@ -42,29 +42,30 @@ class SchoolStudentDashboard(LoginRequiredNoErrorMixin, UserPassesTestMixin, Tem
|
|
|
42
42
|
|
|
43
43
|
context_data = _compute_rapid_router_scores(student, levels)
|
|
44
44
|
|
|
45
|
-
# Find any custom levels created by the teacher and shared with the
|
|
45
|
+
# Find any custom levels created by the teacher and shared with the
|
|
46
|
+
# student
|
|
46
47
|
klass = student.class_field
|
|
47
48
|
teacher = klass.teacher.user
|
|
48
49
|
custom_levels = student.new_user.shared.filter(owner=teacher)
|
|
49
50
|
|
|
50
51
|
if custom_levels:
|
|
51
|
-
custom_levels_data = _compute_rapid_router_scores(
|
|
52
|
+
custom_levels_data = _compute_rapid_router_scores(
|
|
53
|
+
student, custom_levels
|
|
54
|
+
)
|
|
52
55
|
|
|
53
|
-
context_data["total_custom_score"] = custom_levels_data[
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
active_worksheet = aimmo_game.worksheet
|
|
60
|
-
|
|
61
|
-
context_data["worksheet_id"] = active_worksheet.id
|
|
62
|
-
context_data["worksheet_image"] = active_worksheet.image_path
|
|
56
|
+
context_data["total_custom_score"] = custom_levels_data[
|
|
57
|
+
"total_score"
|
|
58
|
+
]
|
|
59
|
+
context_data["total_custom_available_score"] = custom_levels_data[
|
|
60
|
+
"total_available_score"
|
|
61
|
+
]
|
|
63
62
|
|
|
64
63
|
return context_data
|
|
65
64
|
|
|
66
65
|
|
|
67
|
-
class IndependentStudentDashboard(
|
|
66
|
+
class IndependentStudentDashboard(
|
|
67
|
+
LoginRequiredNoErrorMixin, UserPassesTestMixin, TemplateView, FormView
|
|
68
|
+
):
|
|
68
69
|
template_name = "portal/play/independent_student_dashboard.html"
|
|
69
70
|
login_url = reverse_lazy("independent_student_login")
|
|
70
71
|
|
|
@@ -81,7 +82,9 @@ class IndependentStudentDashboard(LoginRequiredNoErrorMixin, UserPassesTestMixin
|
|
|
81
82
|
)
|
|
82
83
|
|
|
83
84
|
|
|
84
|
-
def _compute_rapid_router_scores(
|
|
85
|
+
def _compute_rapid_router_scores(
|
|
86
|
+
student: Student, levels: List[Level] or QuerySet
|
|
87
|
+
) -> Dict[str, int]:
|
|
85
88
|
"""
|
|
86
89
|
Finds Rapid Router progress and score data for a specific student and a specific
|
|
87
90
|
set of levels. This is used to show quick score data to the student on their
|
|
@@ -100,9 +103,9 @@ def _compute_rapid_router_scores(student: Student, levels: List[Level] or QueryS
|
|
|
100
103
|
num_completed = num_top_scores = total_available_score = 0
|
|
101
104
|
total_score = 0.0
|
|
102
105
|
# Get a QuerySet of best attempts for each level
|
|
103
|
-
best_attempts = Attempt.objects.filter(
|
|
104
|
-
|
|
105
|
-
)
|
|
106
|
+
best_attempts = Attempt.objects.filter(
|
|
107
|
+
level__in=levels, student=student, is_best_attempt=True
|
|
108
|
+
).select_related("level")
|
|
106
109
|
|
|
107
110
|
for level in levels:
|
|
108
111
|
total_available_score += _get_max_score_for_level(level)
|
|
@@ -110,7 +113,10 @@ def _compute_rapid_router_scores(student: Student, levels: List[Level] or QueryS
|
|
|
110
113
|
# For each level, compare best attempt's score with level's max score and
|
|
111
114
|
# increment variables as needed
|
|
112
115
|
if best_attempts:
|
|
113
|
-
attempts_dict = {
|
|
116
|
+
attempts_dict = {
|
|
117
|
+
best_attempt.level.id: best_attempt
|
|
118
|
+
for best_attempt in best_attempts
|
|
119
|
+
}
|
|
114
120
|
for level in levels:
|
|
115
121
|
attempt = attempts_dict.get(level.id)
|
|
116
122
|
|
|
@@ -140,7 +146,12 @@ def _get_max_score_for_level(level: Level) -> int:
|
|
|
140
146
|
"""
|
|
141
147
|
return (
|
|
142
148
|
10
|
|
143
|
-
if level.id > 12
|
|
149
|
+
if level.id > 12
|
|
150
|
+
and (
|
|
151
|
+
level.disable_route_score
|
|
152
|
+
or level.disable_algorithm_score
|
|
153
|
+
or not level.episode
|
|
154
|
+
)
|
|
144
155
|
else 20
|
|
145
156
|
)
|
|
146
157
|
|
portal/views/teacher/teach.py
CHANGED
|
@@ -6,7 +6,6 @@ from enum import Enum
|
|
|
6
6
|
from functools import partial, wraps
|
|
7
7
|
from uuid import uuid4
|
|
8
8
|
|
|
9
|
-
from aimmo.models import Game
|
|
10
9
|
from common.helpers.emails import send_verification_email
|
|
11
10
|
from common.helpers.generators import generate_access_code, generate_login_id, generate_password, get_hashed_login_id
|
|
12
11
|
from common.models import Class, DailyActivity, JoinReleaseStudent, Student, Teacher, TotalActivity
|
|
@@ -188,7 +187,6 @@ def teacher_view_class(request, access_code):
|
|
|
188
187
|
@user_passes_test(logged_in_as_teacher, login_url=reverse_lazy("teacher_login"))
|
|
189
188
|
def teacher_delete_class(request, access_code):
|
|
190
189
|
klass = get_object_or_404(Class, access_code=access_code)
|
|
191
|
-
games = Game.objects.filter(game_class=klass)
|
|
192
190
|
|
|
193
191
|
# check user authorised to see class
|
|
194
192
|
check_teacher_authorised(request, klass.teacher)
|
|
@@ -199,9 +197,6 @@ def teacher_delete_class(request, access_code):
|
|
|
199
197
|
)
|
|
200
198
|
return HttpResponseRedirect(reverse_lazy("view_class", kwargs={"access_code": access_code}))
|
|
201
199
|
|
|
202
|
-
for game in games:
|
|
203
|
-
game.is_archived = True
|
|
204
|
-
game.save()
|
|
205
200
|
klass.anonymise()
|
|
206
201
|
|
|
207
202
|
return HttpResponseRedirect(reverse_lazy("dashboard") + "#classes")
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from django.db.models.query import QuerySet
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
@pytest.mark.django_db
|
|
6
|
-
def test_characters_added(migrator):
|
|
7
|
-
migrator.apply_initial_migration(("common", "0002_emailverification"))
|
|
8
|
-
new_state = migrator.apply_tested_migration(("common", "0004_add_aimmocharacters"))
|
|
9
|
-
|
|
10
|
-
model_names = [model._meta.db_table for model in new_state.apps.get_models()]
|
|
11
|
-
|
|
12
|
-
assert "common_aimmocharacter" in model_names
|
|
13
|
-
|
|
14
|
-
AimmoCharacter = new_state.apps.get_model("common", "aimmocharacter")
|
|
15
|
-
all_characters: QuerySet = AimmoCharacter.objects.all()
|
|
16
|
-
|
|
17
|
-
assert all_characters.count() == 3
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@pytest.mark.django_db
|
|
21
|
-
def test_image_paths_updated(migrator):
|
|
22
|
-
migrator.apply_initial_migration(("common", "0005_add_worksheets"))
|
|
23
|
-
new_state = migrator.apply_tested_migration(("common", "0006_update_aimmo_character_image_path"))
|
|
24
|
-
|
|
25
|
-
AimmoCharacter = new_state.apps.get_model("common", "aimmocharacter")
|
|
26
|
-
all_characters: QuerySet = AimmoCharacter.objects.all()
|
|
27
|
-
|
|
28
|
-
for character in all_characters:
|
|
29
|
-
assert character.image_path.startswith("images/aimmo_characters/")
|
portal/forms/add_game.py
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
from aimmo.models import Game
|
|
2
|
-
from common.models import Class
|
|
3
|
-
from django.core.exceptions import ValidationError
|
|
4
|
-
from django.db.models.query import QuerySet
|
|
5
|
-
from django.forms import ModelChoiceField, ModelForm, Select
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class AddGameForm(ModelForm):
|
|
9
|
-
def __init__(self, classes: QuerySet, *args, **kwargs):
|
|
10
|
-
super(AddGameForm, self).__init__(*args, **kwargs)
|
|
11
|
-
self.fields["game_class"].queryset = classes
|
|
12
|
-
|
|
13
|
-
game_class = ModelChoiceField(queryset=None, widget=Select, label="Class", required=True)
|
|
14
|
-
|
|
15
|
-
class Meta:
|
|
16
|
-
model = Game
|
|
17
|
-
fields = ["game_class"]
|
|
18
|
-
|
|
19
|
-
def clean(self):
|
|
20
|
-
super(AddGameForm, self).clean()
|
|
21
|
-
game_class: Class = self.cleaned_data.get("game_class")
|
|
22
|
-
|
|
23
|
-
if not game_class:
|
|
24
|
-
raise ValidationError("An invalid class was entered")
|
|
25
|
-
|
|
26
|
-
if Game.objects.filter(game_class=game_class, is_archived=False).exists():
|
|
27
|
-
raise ValidationError("An active game already exists for this class")
|
|
28
|
-
|
|
29
|
-
return self.cleaned_data
|
|
Binary file
|
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 781.61 255.82"><defs><style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:#fff;}</style><linearGradient id="linear-gradient" x1="390.81" x2="390.81" y2="255.82" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#00cea7"/><stop offset="0.2" stop-color="#00c3ac"/><stop offset="0.68" stop-color="#00afb5"/><stop offset="1" stop-color="#00a8b8"/></linearGradient></defs><title>K_simple_logo_full_colour</title><path class="cls-1" d="M697.33.34A84.18,84.18,0,0,0,630.4,33.48,71.21,71.21,0,0,0,621,21.9C607,8,588,.41,567.25.41S527.48,8,513.54,21.9a72,72,0,0,0-9.45,11.59,84.09,84.09,0,0,0-115.1-18A35.89,35.89,0,0,0,359.73.41c-21.56,0-41.4,8-55.87,22.43-.52.53-1,1.07-1.52,1.61a37.13,37.13,0,0,0-65.33-8,9.77,9.77,0,0,1-16.12-.1A37.16,37.16,0,0,0,188.05.48a36.22,36.22,0,0,0-20.39,7.87A37.45,37.45,0,0,0,144.19,0a37,37,0,0,0-22.25,7.39L74.47,42.77V37.24A37.24,37.24,0,1,0,0,37.24V131.3A37.18,37.18,0,0,0,13.53,160a37,37,0,0,0-8.08,47.6,99.5,99.5,0,0,0,170.54,0A37,37,0,0,0,167.9,160a36.51,36.51,0,0,0,6-6.27A38.17,38.17,0,0,0,177,149c13.77,12.81,32.12,19.86,52,19.86,20.69,0,39.77-7.63,53.71-21.49.37-.37.73-.76,1.09-1.14a37.16,37.16,0,0,0,71.36-14.53V103.94a84.2,84.2,0,0,0,139.53,42.21,36.93,36.93,0,0,0,64.5,6.69,9.78,9.78,0,0,1,16.17,0,36.92,36.92,0,0,0,64.49-6.69A84.25,84.25,0,1,0,697.33.34Z"/><path class="cls-2" d="M284.47,37.56V91.85c0,34-24.37,56.71-55.49,56.71s-55.5-22.69-55.5-56.71V37.56a16.89,16.89,0,0,1,33.78,0V93.05c0,15.69,9.66,24.14,21.72,24.14s21.71-8.45,21.71-24.14V37.56a16.89,16.89,0,0,1,33.78,0Z"/><path class="cls-2" d="M375.42,36.36a15.66,15.66,0,0,1-15.69,15.69c-13.75,0-24.85,9.65-24.85,29.67v49.95a16.9,16.9,0,0,1-33.79,0V81.72c0-37.39,26.07-61,58.64-61A15.66,15.66,0,0,1,375.42,36.36Z"/><path class="cls-2" d="M622.74,77.39v54.29a16.89,16.89,0,0,1-33.77,0V76.19c0-15.69-9.65-24.14-21.72-24.14s-21.71,8.45-21.71,24.14v55.49a16.9,16.9,0,0,1-33.79,0V77.39c0-34,24.37-56.71,55.5-56.71S622.74,43.37,622.74,77.39Z"/><path class="cls-2" d="M761.28,84.62a64,64,0,1,1-63.95-63.95A63.95,63.95,0,0,1,761.28,84.62ZM697.33,52a32.58,32.58,0,1,0,32.58,32.58A32.58,32.58,0,0,0,697.33,52Z"/><path class="cls-2" d="M501.11,84.62a64,64,0,1,1-63.95-63.95A63.95,63.95,0,0,1,501.11,84.62ZM437.16,52a32.58,32.58,0,1,0,32.58,32.58A32.58,32.58,0,0,0,437.16,52Z"/><path class="cls-2" d="M37.24,148.21A16.91,16.91,0,0,1,20.33,131.3V37.24a16.91,16.91,0,1,1,33.81,0V131.3A16.91,16.91,0,0,1,37.24,148.21Z"/><path class="cls-2" d="M144.19,148.21A16.8,16.8,0,0,1,134,144.8L71.27,97.5a16.9,16.9,0,0,1,.08-27l62.74-46.77a16.91,16.91,0,1,1,20.22,27.1l-44.69,33.3,44.76,33.72a16.91,16.91,0,0,1-10.19,30.4Z"/><path class="cls-2" d="M90.72,235.49a78.54,78.54,0,0,1-67.85-38.37A16.76,16.76,0,0,1,51.6,179.85a45.65,45.65,0,0,0,78.24,0,16.76,16.76,0,1,1,28.72,17.27A78.52,78.52,0,0,1,90.72,235.49Z"/></svg>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1330.02 596.66"><defs><style>.a{fill:#f2f2f2;}.b{fill:url(#a);}.c{fill:#fff;}</style><linearGradient id="a" x1="665.01" y1="510.52" x2="665.01" y2="287.55" gradientTransform="matrix(1, 0, 0, -1, 0, 724.17)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#00cea7"/><stop offset="0.2" stop-color="#00c3ac"/><stop offset="0.68" stop-color="#00afb5"/><stop offset="1" stop-color="#00a8b8"/></linearGradient></defs><rect class="a" width="1330.02" height="596.66"/><path class="b" d="M932.17,214a73.34,73.34,0,0,0-58.33,28.88,62.06,62.06,0,0,0-8.2-10.09C853.44,220.62,836.88,214,818.8,214s-34.67,6.61-46.82,18.73a62.32,62.32,0,0,0-8.23,10.1,73.3,73.3,0,0,0-100.32-15.69A31.27,31.27,0,0,0,637.93,214c-18.8,0-36.09,7-48.7,19.55-.45.46-.87.93-1.32,1.4A32.36,32.36,0,0,0,531,228a8.53,8.53,0,0,1-11.86,2.14,8.44,8.44,0,0,1-2.19-2.23,32.39,32.39,0,0,0-28.63-13.83,31.57,31.57,0,0,0-17.77,6.86,32.64,32.64,0,0,0-20.45-7.28,32.29,32.29,0,0,0-19.4,6.44L389.3,250.93v-4.82A32.46,32.46,0,1,0,324.39,245v83a32.39,32.39,0,0,0,11.79,25,32.25,32.25,0,0,0-7,41.49,86.72,86.72,0,0,0,148.64,0,32.25,32.25,0,0,0-7-41.49,31.82,31.82,0,0,0,5.23-5.46,34.75,34.75,0,0,0,2.7-4.12c12,11.16,28,17.31,45.32,17.31,18,0,34.67-6.65,46.82-18.73l1-1a32.38,32.38,0,0,0,62.19-12.66v-24.2A73.39,73.39,0,0,0,755.55,341a32.19,32.19,0,0,0,56.22,5.83,8.52,8.52,0,0,1,14.09,0A32.17,32.17,0,0,0,882.07,341,73.43,73.43,0,1,0,932.17,214Z"/><path class="c" d="M572.33,246.39v47.32c0,29.63-21.24,49.42-48.36,49.42s-48.38-19.77-48.38-49.42V246.39a14.73,14.73,0,0,1,29.45,0v48.36c0,13.68,8.42,21,18.93,21s18.92-7.36,18.92-21V246.39a14.72,14.72,0,0,1,29.44,0Z"/><path class="c" d="M651.6,245.34A13.65,13.65,0,0,1,638,259h-.05c-12,0-21.66,8.41-21.66,25.86v43.53a14.73,14.73,0,0,1-29.45,0V284.88c0-32.59,22.72-53.17,51.11-53.17a13.65,13.65,0,0,1,13.67,13.62Z"/><path class="c" d="M867.16,281.1v47.32a14.72,14.72,0,0,1-29.43,0V280.06c0-13.68-8.41-21-18.93-21s-18.93,7.36-18.93,21v48.36a14.73,14.73,0,0,1-29.45,0V281.1c0-29.63,21.24-49.43,48.38-49.43S867.16,251.45,867.16,281.1Z"/><path class="c" d="M987.91,287.4a55.78,55.78,0,1,1-55.83-55.73h.09a55.74,55.74,0,0,1,55.74,55.73ZM932.17,259a28.4,28.4,0,1,0,28.4,28.4,28.41,28.41,0,0,0-28.4-28.4Z"/><path class="c" d="M761.15,287.4a55.78,55.78,0,1,1-55.83-55.73h.09a55.74,55.74,0,0,1,55.74,55.73ZM705.41,259a28.4,28.4,0,1,0,28.4,28.4h0A28.41,28.41,0,0,0,705.41,259Z"/><path class="c" d="M356.85,342.83a14.74,14.74,0,0,1-14.74-14.74h0v-82a14.74,14.74,0,0,1,29.47-.72q0,.36,0,.72v82A14.74,14.74,0,0,1,356.85,342.83Z"/><path class="c" d="M450.07,342.83a14.7,14.7,0,0,1-8.89-3l-54.67-41.23a14.73,14.73,0,0,1,.07-23.53l54.68-40.77a14.74,14.74,0,0,1,18.14,23.24l-.51.38-39,29,39,29.39a14.74,14.74,0,0,1-8.88,26.49Z"/><path class="c" d="M403.46,418.9a68.44,68.44,0,0,1-59.13-33.44,14.61,14.61,0,0,1,25-15.06,39.79,39.79,0,0,0,68.19,0,14.61,14.61,0,0,1,25.35,14.53c-.1.18-.21.35-.32.53A68.45,68.45,0,0,1,403.46,418.9Z"/></svg>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 230.45 324.92"><defs><style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:#fff;}</style><linearGradient id="linear-gradient" x1="107.38" y1="84.32" x2="127.22" y2="408.64" gradientTransform="translate(13.19 -90.72) rotate(3.5)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#00cea7"/><stop offset="0.2" stop-color="#00c3ac"/><stop offset="0.68" stop-color="#00afb5"/><stop offset="1" stop-color="#00a8b8"/></linearGradient></defs><title>K_mark_full_colour</title><path class="cls-1" d="M220.9,195.26A47.28,47.28,0,0,0,211.62,129l-29.32-22.1L211.42,85.2A47.28,47.28,0,0,0,221.06,19,47.55,47.55,0,0,0,183.13,0a46.93,46.93,0,0,0-28.25,9.38L94.58,54.33v-7A47.29,47.29,0,0,0,0,47.29V166.77a47.24,47.24,0,0,0,17.19,36.45A47,47,0,0,0,6.92,263.67a126.37,126.37,0,0,0,216.6,0,47,47,0,0,0-10.27-60.45A47.3,47.3,0,0,0,220.9,195.26Z"/><path class="cls-2" d="M47.29,188.24a21.47,21.47,0,0,1-21.47-21.47V47.29a21.47,21.47,0,0,1,42.94,0V166.77A21.47,21.47,0,0,1,47.29,188.24Z"/><path class="cls-2" d="M183.13,188.24a21.4,21.4,0,0,1-12.92-4.33L90.52,123.84a21.47,21.47,0,0,1,.1-34.35l79.69-59.41A21.48,21.48,0,0,1,196,64.51L139.23,106.8l56.85,42.83a21.47,21.47,0,0,1-13,38.61Z"/><path class="cls-2" d="M115.22,299.1a99.75,99.75,0,0,1-86.17-48.74,21.29,21.29,0,0,1,36.49-21.94,58,58,0,0,0,99.36,0,21.29,21.29,0,0,1,36.49,21.94A99.75,99.75,0,0,1,115.22,299.1Z"/></svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/* global showPopupConfirmation */
|
|
2
|
-
/* global hidePopupConfirmation */
|
|
3
|
-
|
|
4
|
-
function classesText(classes) {
|
|
5
|
-
return classes
|
|
6
|
-
.map(
|
|
7
|
-
(name, index) =>
|
|
8
|
-
`${
|
|
9
|
-
index === 0
|
|
10
|
-
? ""
|
|
11
|
-
: index === classes.length - 1
|
|
12
|
-
? " and "
|
|
13
|
-
: ", "
|
|
14
|
-
}<strong>${$("<div>").text(name).html()}</strong>`
|
|
15
|
-
)
|
|
16
|
-
.join("");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function clickDeleteGames() {
|
|
20
|
-
let selectedGameIds = [];
|
|
21
|
-
let selectedClasses = [];
|
|
22
|
-
$("input[name='game_ids']:checked").each(function () {
|
|
23
|
-
selectedGameIds.push($(this).val());
|
|
24
|
-
selectedClasses.push($(this).data("className"));
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
if (!selectedGameIds.length) {
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
let title = "Delete class games";
|
|
32
|
-
let text = `
|
|
33
|
-
<div class='popup-text'>
|
|
34
|
-
<p>Are you sure that you want to delete the game${
|
|
35
|
-
selectedClasses.length > 1 ? "s" : ""
|
|
36
|
-
} for ${classesText(selectedClasses)}?</p>
|
|
37
|
-
<p>This action will delete any progress ${
|
|
38
|
-
selectedClasses.length > 1 ? "those classes have" : "that class has"
|
|
39
|
-
} made.</p>
|
|
40
|
-
</div>`;
|
|
41
|
-
let confirmHandler = "deleteGames()";
|
|
42
|
-
|
|
43
|
-
showPopupConfirmation(title, text, confirmHandler);
|
|
44
|
-
let popup = $(".popup-wrapper");
|
|
45
|
-
popup.data("gameIds", selectedGameIds);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function deleteGames() {
|
|
49
|
-
let gameIds = $("#popup").data("gameIds");
|
|
50
|
-
|
|
51
|
-
$.ajax({
|
|
52
|
-
url: "/kurono/api/games/delete_games/",
|
|
53
|
-
type: "POST",
|
|
54
|
-
data: { game_ids: gameIds },
|
|
55
|
-
traditional: true,
|
|
56
|
-
headers: {
|
|
57
|
-
"X-CSRFToken": $("input[name=csrfmiddlewaretoken]").val(),
|
|
58
|
-
},
|
|
59
|
-
success: function (data) {
|
|
60
|
-
hidePopupConfirmation();
|
|
61
|
-
document.location.reload(true);
|
|
62
|
-
},
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function changeWorksheetConfirmation(gameID, className, worksheetID) {
|
|
67
|
-
let title = "Change Challenge";
|
|
68
|
-
let text =
|
|
69
|
-
"<div class='popup-text'><p>Please confirm that you would like to change the challenge for class: " +
|
|
70
|
-
"<strong class='popup__class-name'></strong>. This will change the level for the students when they rejoin " +
|
|
71
|
-
"the game.</p></div>";
|
|
72
|
-
let confirmHandler = "changeWorksheet()";
|
|
73
|
-
|
|
74
|
-
showPopupConfirmation(title, text, confirmHandler);
|
|
75
|
-
let popup = $(".popup-wrapper");
|
|
76
|
-
popup.data("gameId", gameID);
|
|
77
|
-
popup.data("worksheetId", worksheetID);
|
|
78
|
-
$(".popup__class-name").text(className);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function changeWorksheet() {
|
|
82
|
-
let gameID = $("#popup").data("gameId");
|
|
83
|
-
let worksheetID = $("#popup").data("worksheetId");
|
|
84
|
-
|
|
85
|
-
$.ajax({
|
|
86
|
-
url: "/kurono/api/games/" + gameID + "/",
|
|
87
|
-
type: "PUT",
|
|
88
|
-
data: { worksheet_id: worksheetID },
|
|
89
|
-
success: function (data) {
|
|
90
|
-
hidePopupConfirmation();
|
|
91
|
-
document.location.reload(true);
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
$(document).ready(function () {
|
|
97
|
-
// Handlers for the games checklist and select all checklist
|
|
98
|
-
$('[id^="game_"]').on("click", () => {
|
|
99
|
-
$("#gamesListToggle").prop("checked",
|
|
100
|
-
$('[id^="game_"]:checked').length === $('[id^="game_"]').length);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
$("#gamesListToggle").on("click", () => {
|
|
104
|
-
$('[id^="game_"]').prop("checked", $("#gamesListToggle").is(":checked"));
|
|
105
|
-
});
|
|
106
|
-
});
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
@import 'base';
|
|
2
|
-
|
|
3
|
-
.video--aimmo--play-online {
|
|
4
|
-
margin-top: 4 * $spacing;
|
|
5
|
-
padding-left: 0px;
|
|
6
|
-
padding-right: 0px;
|
|
7
|
-
-webkit-box-shadow: 0px 4px 18px 0px $color-ui-widget-overlay;
|
|
8
|
-
-moz-box-shadow: 0px 4px 18px 0px $color-ui-widget-overlay;
|
|
9
|
-
box-shadow: 0px 4px 18px 0px $color-ui-widget-overlay;
|
|
10
|
-
}
|
|
Binary file
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
{% load static %}
|
|
2
|
-
{% load app_tags %}
|
|
3
|
-
{% block scripts %}
|
|
4
|
-
<script type="text/javascript" src="{% static 'portal/js/aimmoGame.js' %}"></script>
|
|
5
|
-
{% endblock scripts %}
|
|
6
|
-
|
|
7
|
-
{% if open_play_games %}
|
|
8
|
-
{% include "portal/partials/popup.html" %}
|
|
9
|
-
<table id="games-table" class="games-table header-primary data-primary">
|
|
10
|
-
<tr class="games-table__header">
|
|
11
|
-
<th class="cell-left">
|
|
12
|
-
<p>Class</p>
|
|
13
|
-
</th>
|
|
14
|
-
<th class="cell-left col-xs-6">
|
|
15
|
-
<p>Challenge</p>
|
|
16
|
-
</th>
|
|
17
|
-
<th class="cell-center">
|
|
18
|
-
<p>Action</p>
|
|
19
|
-
</th>
|
|
20
|
-
<th class="cell-center">
|
|
21
|
-
<input id="gamesListToggle" name="gamesListToggle" type="checkbox" value="">
|
|
22
|
-
</th>
|
|
23
|
-
</tr>
|
|
24
|
-
{% for game in open_play_games %}
|
|
25
|
-
<tr>
|
|
26
|
-
<td>
|
|
27
|
-
<div class="games-table__cell">
|
|
28
|
-
<p>{{ game.game_class.name }}
|
|
29
|
-
{% if user.userprofile.teacher == game.game_class.teacher %}
|
|
30
|
-
(You)
|
|
31
|
-
{% else %}
|
|
32
|
-
({{ game.game_class.teacher }})
|
|
33
|
-
{% endif %}
|
|
34
|
-
</p>
|
|
35
|
-
</div>
|
|
36
|
-
</td>
|
|
37
|
-
<td>
|
|
38
|
-
<div class="games-table__cell">
|
|
39
|
-
<div class="dropdown">
|
|
40
|
-
<button id="worksheets_dropdown" class="button--secondary button--secondary--dark button--small button--dropdown"
|
|
41
|
-
data-toggle="dropdown" aria-expanded="false" type="button">
|
|
42
|
-
<div class="dropdown__text">{{ game.worksheet.id }} - {{ game.worksheet.name }}</div>
|
|
43
|
-
</button>
|
|
44
|
-
<ul id="worksheets_dropdown_menu" class="dropdown-menu">
|
|
45
|
-
{% for worksheet in complete_worksheets %}
|
|
46
|
-
<li class="dropdown-menu__option">
|
|
47
|
-
{% if worksheet.name == game.worksheet.name %}
|
|
48
|
-
<a class="button button--small disabled">
|
|
49
|
-
<p class="dropdown-menu__option__text">{{ worksheet.id }} - {{ worksheet.name }}</p>
|
|
50
|
-
<span class="material-icons-outlined">check</span>
|
|
51
|
-
</a>
|
|
52
|
-
{% else %}
|
|
53
|
-
<a class="button button--small" id="worksheet_{{ worksheet.id }}"
|
|
54
|
-
onclick="changeWorksheetConfirmation('{{ game.id|escapejs }}',
|
|
55
|
-
'{{ game.game_class.name|escapejs }}',
|
|
56
|
-
'{{ worksheet.id|escapejs }}')">
|
|
57
|
-
<p class="dropdown-menu__option__text">{{ worksheet.id }} - {{ worksheet.name }}</p>
|
|
58
|
-
</a>
|
|
59
|
-
{% endif %}
|
|
60
|
-
</li>
|
|
61
|
-
{% endfor %}
|
|
62
|
-
{% for worksheet in incomplete_worksheets %}
|
|
63
|
-
<li class="dropdown-menu__option">
|
|
64
|
-
<a class="button button--small disabled">
|
|
65
|
-
<p class="dropdown-menu__option__text">{{ worksheet.id }} - {{ worksheet.name }}</p>
|
|
66
|
-
</a>
|
|
67
|
-
</li>
|
|
68
|
-
{% endfor %}
|
|
69
|
-
</ul>
|
|
70
|
-
</div>
|
|
71
|
-
</div>
|
|
72
|
-
</td>
|
|
73
|
-
<td>
|
|
74
|
-
<div class="games-table__buttons">
|
|
75
|
-
<a class="button button--small button--primary" href="{% url base_url id=game.id %}">Play</a>
|
|
76
|
-
</div>
|
|
77
|
-
</td>
|
|
78
|
-
<td class="cell-center">
|
|
79
|
-
<input type="checkbox" name="game_ids" id="game_{{ game.id }}" value="{{ game.id }}" data-class-name="{{ game.game_class.name }}">
|
|
80
|
-
</td>
|
|
81
|
-
</tr>
|
|
82
|
-
{% endfor %}
|
|
83
|
-
</table>
|
|
84
|
-
<button onclick="clickDeleteGames()" class="button button--primary button--primary--danger button--icon pull-right" id="deleteGamesButton">
|
|
85
|
-
Delete<span class="iconify" data-icon="mdi:delete-outline"></span>
|
|
86
|
-
</button>
|
|
87
|
-
{% else %}
|
|
88
|
-
<p>It doesn't look like you have any games created. To create a game, use the 'Select class' button above.</p>
|
|
89
|
-
{% endif %}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
{% extends 'portal/base.html' %}
|
|
2
|
-
{% load static %}
|
|
3
|
-
{% load app_tags banner_tags hero_card_tags card_list_tags character_list_tags %}
|
|
4
|
-
|
|
5
|
-
{% block subNav %}
|
|
6
|
-
<section class="banner banner--aimmo">
|
|
7
|
-
<img title="Kurono logo" alt="Kurono logo" src="{% static 'portal/img/kurono_logo.svg' %}">
|
|
8
|
-
</section>
|
|
9
|
-
{% endblock subNav %}
|
|
10
|
-
|
|
11
|
-
{% block content %}
|
|
12
|
-
{% if not user|is_independent_student %}
|
|
13
|
-
{% if HERO_CARD %}
|
|
14
|
-
<div class="background container">
|
|
15
|
-
{% hero_card hero_card_name="HERO_CARD" %}
|
|
16
|
-
</div>
|
|
17
|
-
|
|
18
|
-
<div class="container carousel-cards-container">
|
|
19
|
-
{% card_list %}
|
|
20
|
-
</div>
|
|
21
|
-
{% else %}
|
|
22
|
-
<div class="background text-center container">
|
|
23
|
-
<h4>My Games</h4>
|
|
24
|
-
<p>
|
|
25
|
-
Oh no! It doesn't look like you have access to any games, yet. You'll need to ask your teacher to set
|
|
26
|
-
some up.
|
|
27
|
-
</p>
|
|
28
|
-
</div>
|
|
29
|
-
{% endif %}
|
|
30
|
-
|
|
31
|
-
<div class="background background--primary">
|
|
32
|
-
<div class="container">
|
|
33
|
-
{% character_list %}
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
{% else %}
|
|
37
|
-
<div class="background text-center container">
|
|
38
|
-
<h4>My Games</h4>
|
|
39
|
-
<p>
|
|
40
|
-
Hi there! 👋 We are sorry, but Kurono is designed to be used in a classroom, and therefore it is not
|
|
41
|
-
available to independent students at this time.
|
|
42
|
-
</p>
|
|
43
|
-
</div>
|
|
44
|
-
{% endif %}
|
|
45
|
-
|
|
46
|
-
{% endblock content %}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
{% extends 'portal/base.html' %}
|
|
2
|
-
{% load static %}
|
|
3
|
-
{% load app_tags %}
|
|
4
|
-
|
|
5
|
-
{% block scripts %}
|
|
6
|
-
{{block.super}}
|
|
7
|
-
<script type="text/javascript" src="{% static 'portal/js/join_create_game_toggle.js' %}"></script>
|
|
8
|
-
{% endblock scripts %}
|
|
9
|
-
|
|
10
|
-
{% block subNav %}
|
|
11
|
-
<section class="banner banner--aimmo">
|
|
12
|
-
<img title="Kurono logo" alt="Kurono logo" src="{% static 'portal/img/kurono_logo.svg' %}">
|
|
13
|
-
</section>
|
|
14
|
-
<div class="sub-nav sub-nav--teacher">
|
|
15
|
-
<div class="container">
|
|
16
|
-
<div class="sub-nav__content">
|
|
17
|
-
<p>Select a class from the dropdown menu to add a new game below</p>
|
|
18
|
-
<div class="dropdown">
|
|
19
|
-
<form autocomplete="off" id="create-game-form" method="post" class="hidden">
|
|
20
|
-
{% csrf_token %}
|
|
21
|
-
<input type="hidden" name="game_class" id="id_game_class">
|
|
22
|
-
</form>
|
|
23
|
-
<button class="button--dropdown" data-toggle="dropdown" aria-expanded="false" id="add_class_dropdown">
|
|
24
|
-
Select class
|
|
25
|
-
</button>
|
|
26
|
-
<ul id="add-class-dropdown-menu" class="dropdown-menu">
|
|
27
|
-
{% for game_class in form.game_class.field.queryset %}
|
|
28
|
-
<li class="dropdown-menu__option">
|
|
29
|
-
{% if game_class.active_game %}
|
|
30
|
-
<a class="button button--regular disabled" data-class-id="{{ game_class.id }}">
|
|
31
|
-
{% else %}
|
|
32
|
-
<a class="button button--regular" id="class_{{ game_class.id }}"
|
|
33
|
-
data-class-id="{{ game_class.id }}"
|
|
34
|
-
onclick='send_event("Kurono", "Clicked", "Create game button");'>
|
|
35
|
-
{% endif %}
|
|
36
|
-
<p class="dropdown-menu__option__text">{{ game_class.name }} {% if user.userprofile.teacher == game_class.teacher %}(You) {% else %}({{ game_class.teacher }}) {% endif %}</p>
|
|
37
|
-
</a>
|
|
38
|
-
</li>
|
|
39
|
-
{% endfor %}
|
|
40
|
-
</ul>
|
|
41
|
-
</div>
|
|
42
|
-
</div>
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
{% endblock subNav %}
|
|
46
|
-
|
|
47
|
-
{% block content %}
|
|
48
|
-
<div class="background container">
|
|
49
|
-
<section><h4>My games</h4></section>
|
|
50
|
-
{% games_table 'kurono/play' %}
|
|
51
|
-
</div>
|
|
52
|
-
|
|
53
|
-
<div id="kurono_teacher_dashboard_page"></div>
|
|
54
|
-
<div class="background background--primary">
|
|
55
|
-
<div class="container">
|
|
56
|
-
<div class="row d-flex">
|
|
57
|
-
<div class="col-sm-6 d-flex flex-column">
|
|
58
|
-
<div class="flex-grow-1">
|
|
59
|
-
<h5 class="mt-0">Kurono Resources</h5>
|
|
60
|
-
<p>We have a set of individual and collaborative worksheets that keep the students engaged and having fun whilst
|
|
61
|
-
embedding important Python skills, supported by lesson guides and resource sheets.</p>
|
|
62
|
-
<p>Please visit our dedicated Code for Life Space to find everything you need from lesson plans to solutions.</p>
|
|
63
|
-
<p>This space is only available to teachers.</p>
|
|
64
|
-
</div>
|
|
65
|
-
<div>
|
|
66
|
-
<a href="https://code-for-life.gitbook.io/teaching-resources/v/kurono-teaching-resources/"
|
|
67
|
-
class="button button--primary button--icon col-sm-6" target="_blank">
|
|
68
|
-
Open Kurono resources<span class="iconify" data-icon="mdi:open-in-new"></span>
|
|
69
|
-
</a>
|
|
70
|
-
</div>
|
|
71
|
-
</div>
|
|
72
|
-
<div class="col-sm-6">
|
|
73
|
-
<img alt="Kurono Resources" title="Kurono Resources" src="{% static 'portal/img/thumbnail_kurono_resources.png' %}">
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
<div>
|
|
79
|
-
<div class="background container">
|
|
80
|
-
<h5>Tell us what you think of Kurono...</h5>
|
|
81
|
-
<p>Your testing and feedback will help Code for Life deliver an enjoyable game, and will allow us to consult
|
|
82
|
-
with you on resources that will be relevant to teaching computing classes in secondary schools
|
|
83
|
-
(13 — 18 year olds).</p>
|
|
84
|
-
<div class="row background">
|
|
85
|
-
<div class="col-sm-6">
|
|
86
|
-
<a href="https://docs.google.com/forms/d/e/1FAIpQLSdNGbf-oLanNhIqCQ-Yz7mbiTBBjX-8rpdXQUB8XIgBvwwuJg/viewform?usp=sf_link"
|
|
87
|
-
class="button button--primary button--icon col-sm-6" target="_blank">
|
|
88
|
-
Give feedback<span class="iconify" data-icon="mdi:open-in-new"></span>
|
|
89
|
-
</a>
|
|
90
|
-
</div>
|
|
91
|
-
</div>
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
94
|
-
|
|
95
|
-
{% endblock content %}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
from django import template
|
|
2
|
-
from common.models import AimmoCharacter
|
|
3
|
-
|
|
4
|
-
register = template.Library()
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@register.inclusion_tag("portal/partials/character_list.html")
|
|
8
|
-
def character_list():
|
|
9
|
-
"""
|
|
10
|
-
Registers the inclusion tag for the character card list partial.
|
|
11
|
-
The template currently expects a list of elements which each contain the following:
|
|
12
|
-
- name: the heading of the card
|
|
13
|
-
- image_path: the path to the card's image
|
|
14
|
-
- description: the text paragraph of the card
|
|
15
|
-
"""
|
|
16
|
-
return {"characters": AimmoCharacter.objects.sorted()}
|