From d9a8c68e742c6a6a77d22b1a181f167f60169a24 Mon Sep 17 00:00:00 2001 From: Florian Aucomte <33633200+faucomte97@users.noreply.github.com> Date: Tue, 21 Feb 2023 14:13:20 +0000 Subject: [PATCH] fix: Custom levels always have max score of 10 (#1422) --- Pipfile.lock | 30 ++++++------ game/forms.py | 6 +-- game/templates/game/scoreboard.html | 4 +- game/tests/test_scoreboard.py | 73 +++++++++++++++++++++++------ game/views/scoreboard.py | 21 ++++++--- 5 files changed, 93 insertions(+), 41 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 5aeffdcac..953edc30d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -342,11 +342,11 @@ }, "setuptools": { "hashes": [ - "sha256:95f00380ef2ffa41d9bba85d95b27689d923c93dfbafed4aecd7cf988a25e012", - "sha256:bb6d8e508de562768f2027902929f8523932fcd1fb784e6d573d2cafac995a48" + "sha256:9d3de8591bd6f6522594406fa46a6418eabd0562dacb267f8556675762801514", + "sha256:ed4e75fafe103c79b692f217158ba87edf38d31004b9dbc1913debb48793c828" ], "markers": "python_version >= '3.7'", - "version": "==67.3.2" + "version": "==67.3.3" }, "sqlparse": { "hashes": [ @@ -412,10 +412,10 @@ "develop": { "aimmo": { "hashes": [ - "sha256:87785b3474b4374c2bf392122447c02c63c8cf2650749d84f2e9d02d1fc42f52", - "sha256:f4c6c55fcfc2656f7962524f99bdcc4a0380e54d7d8eeef257052cf92820d501" + "sha256:3aea2cdb7c314161dbb4c37228543bafb9f79dbb88dca925713a6bf549227e04", + "sha256:f263588ac4a3ead1176a7144f3d7a87e614c15b6a0ed14ad1811c14d13cff8e6" ], - "version": "==2.5.13" + "version": "==2.5.15" }, "asgiref": { "hashes": [ @@ -666,11 +666,11 @@ }, "google-auth": { "hashes": [ - "sha256:5045648c821fb72384cdc0e82cc326df195f113a33049d9b62b74589243d2acc", - "sha256:ed7057a101af1146f0554a769930ac9de506aeca4fd5af6543ebe791851a9fbd" + "sha256:5fd170986bce6bfd7bb5c845c4b8362edb1e0cba901e062196e83f8bb5d5d32c", + "sha256:75d76ea857df65938e1f71dcbcd7d0cd48e3f80b34b8870ba229c9292081f7ef" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==2.16.0" + "version": "==2.16.1" }, "greenlet": { "hashes": [ @@ -1149,11 +1149,11 @@ }, "setuptools": { "hashes": [ - "sha256:95f00380ef2ffa41d9bba85d95b27689d923c93dfbafed4aecd7cf988a25e012", - "sha256:bb6d8e508de562768f2027902929f8523932fcd1fb784e6d573d2cafac995a48" + "sha256:9d3de8591bd6f6522594406fa46a6418eabd0562dacb267f8556675762801514", + "sha256:ed4e75fafe103c79b692f217158ba87edf38d31004b9dbc1913debb48793c828" ], "markers": "python_version >= '3.7'", - "version": "==67.3.2" + "version": "==67.3.3" }, "six": { "hashes": [ @@ -1241,11 +1241,11 @@ }, "zipp": { "hashes": [ - "sha256:23f70e964bc11a34cef175bc90ba2914e1e4545ea1e3e2f67c079671883f9cb6", - "sha256:e8b2a36ea17df80ffe9e2c4fda3f693c3dad6df1697d3cd3af232db680950b0b" + "sha256:188834565033387710d046e3fe96acfc9b5e86cbca7f39ff69cf21a4128198b7", + "sha256:9e5421e176ef5ab4c0ad896624e87a7b2f07aca746c9b2aa305952800cb8eecb" ], "markers": "python_version >= '3.7'", - "version": "==3.13.0" + "version": "==3.14.0" } } } diff --git a/game/forms.py b/game/forms.py index e3ed145ef..ddb13049e 100644 --- a/game/forms.py +++ b/game/forms.py @@ -17,10 +17,8 @@ def __init__(self, *args, **kwargs): ) # Each tuple in choices has two elements, id and name of each level # First element is the actual value set on the model - # Second element is the string displayed on the drop down menu - episodes_choices = ( - (episode.id, episode.name) for episode in Episode.objects.all() - ) + # Second element is the string displayed on the dropdown menu + episodes_choices = ((episode.id, episode.name) for episode in Episode.objects.all()) self.fields["episodes"] = forms.MultipleChoiceField( choices=itertools.chain(episodes_choices), widget=forms.CheckboxSelectMultiple(), diff --git a/game/templates/game/scoreboard.html b/game/templates/game/scoreboard.html index f0b05f4e7..f42acb98f 100644 --- a/game/templates/game/scoreboard.html +++ b/game/templates/game/scoreboard.html @@ -181,9 +181,9 @@

Scoreboard

{% endif %} {% endfor %} {% if user|is_logged_in_as_teacher %} - {% if student.percentage_complete > 75 %} + {% if student.success_rate > 75 %} - {% elif student.percentage_complete < 33 %} + {% elif student.success_rate < 33 %} {% else %} diff --git a/game/tests/test_scoreboard.py b/game/tests/test_scoreboard.py index 1f8a56198..a2288fb36 100644 --- a/game/tests/test_scoreboard.py +++ b/game/tests/test_scoreboard.py @@ -13,25 +13,41 @@ from game.models import Attempt, Level, Episode from game.tests.utils.level import create_save_level -from game.views.scoreboard import StudentRow, scoreboard_data, Headers, StudentInTrouble, shared_level_to_name +from game.views.scoreboard import ( + StudentRow, + scoreboard_data, + Headers, + StudentInTrouble, + shared_level_to_name, + shared_levels_data, + SharedHeaders, +) from game.views.scoreboard_csv import scoreboard_csv, Headers as CSVHeaders, SharedHeaders as CSVSharedHeaders class ScoreboardTestCase(TestCase): def test_teacher_multiple_students_multiple_levels(self): - episode_ids = [1] + # Setup official levels data + episode_ids = [1, 2] episode1 = Episode.objects.get(id=1) - level_ids = [f"{x}" for x in range(1, len(episode1.levels) + 1)] + episode2 = Episode.objects.get(id=2) + level_ids = [f"{x}" for x in range(1, len(episode1.levels) + len(episode2.levels) + 1)] level1 = Level.objects.get(name="1") - level2 = Level.objects.get(name="2") + level13 = Level.objects.get(name="13") clas, student, student2 = set_up_data() create_attempt(student, level1, 20) create_attempt(student2, level1, 2) - create_attempt(student2, level2, 16) + create_attempt(student2, level13, 16) - all_levels = [level1, level2] + # Setup custom levels data + shared_level = create_save_level(student, "custom_level1", shared_with=[student2.new_user]) + + create_attempt(student2, shared_level, 10) + + all_levels = [level1, level13] + all_shared_levels = [shared_level] attempts_per_student = { student: Attempt.objects.filter(level__in=all_levels, student=student, is_best_attempt=True).select_related( @@ -42,8 +58,19 @@ def test_teacher_multiple_students_multiple_levels(self): ).select_related("level"), } + shared_attempts_per_student = { + student2: Attempt.objects.filter( + level__in=all_shared_levels, student=student2, is_best_attempt=True + ).select_related("level"), + } + + # Generate results student_data, headers, level_headers, levels_sorted = scoreboard_data(episode_ids, attempts_per_student) + shared_headers, shared_level_headers, shared_student_data = shared_levels_data( + student.new_user, all_shared_levels, shared_attempts_per_student + ) + # Check data for official levels matches assert headers == Headers assert level_headers == [f"L{id}" for id in level_ids] @@ -54,7 +81,7 @@ def test_teacher_multiple_students_multiple_levels(self): assert student_row.name == student.user.user.first_name assert student_row.total_score == 20 assert student_row.total_time == timedelta(0) - assert student_row.level_scores[1]["score"] == 20 + assert student_row.level_scores[all_levels[0].id]["score"] == 20 assert student_row.completed == 1 assert student_row.success_rate == 100.0 @@ -63,13 +90,28 @@ def test_teacher_multiple_students_multiple_levels(self): assert student_row.name == student2.user.user.first_name assert student_row.total_score == 18 assert student_row.total_time == timedelta(0) - assert student_row.level_scores[1]["score"] == 2 - assert student_row.level_scores[2]["score"] == 16 + assert student_row.level_scores[all_levels[0].id]["score"] == 2 + assert student_row.level_scores[all_levels[1].id]["score"] == 16 assert student_row.completed == 1 assert ( student_row.success_rate == 45.0 ) ## the scores, (2 + 16 = 18), divided by the total possible, (2 * 20 = 40), 18/40 = 45% + # Check data for custom levels matches + assert shared_headers == SharedHeaders + assert shared_level_headers == [f"{shared_level.name} ({shared_level.owner})"] + + assert len(shared_student_data) == 1 + + student_row = shared_student_data[0] + assert student_row.class_field.name == clas.name + assert student_row.name == student2.user.user.first_name + assert student_row.total_score == 10 + assert student_row.total_time == timedelta(0) + assert student_row.level_scores[shared_level.id]["score"] == 10 + assert student_row.completed == 1 + assert student_row.success_rate == 100.0 + def test_scoreboard_loads(self): email, password = signup_teacher_directly() create_organisation_directly(email) @@ -211,9 +253,12 @@ def test_scoreboard_csv(self): response = scoreboard_csv(student_rows, levels, improvement_data, shared_levels_headers, shared_level_rows) # Gather the data from the CSV - actual_scoreboard_header, actual_scoreboard_rows, actual_shared_levels_header, actual_shared_levels_rows = self.actual_data( - response.content.decode("utf-8"), len(students) - ) + ( + actual_scoreboard_header, + actual_scoreboard_rows, + actual_shared_levels_header, + actual_shared_levels_rows, + ) = self.actual_data(response.content.decode("utf-8"), len(students)) # Check the headers and the number or rows match expectations assert actual_scoreboard_header == self.expected_scoreboard_header(levels) @@ -281,7 +326,7 @@ def student_row(self, class_name=None): total_score=total_score, level_scores=level_scores, completed=2, - percentage_complete=45, + success_rate=45, ) return row, student @@ -312,7 +357,7 @@ def shared_student_row(self, student, shared_levels): total_score=0, level_scores=level_scores, completed=0, - percentage_complete=0, + success_rate=0, ) return row diff --git a/game/views/scoreboard.py b/game/views/scoreboard.py index f8910c976..d33c34df5 100644 --- a/game/views/scoreboard.py +++ b/game/views/scoreboard.py @@ -65,12 +65,21 @@ def student_row(levels_sorted, student, best_attempts): if attempt: max_score = 0 - if not attempt.level.disable_route_score: - max_score += 10 - if not attempt.level.disable_algorithm_score: - max_score += 10 - if int(attempt.level.name) < 13: - max_score = 20 + + # Max score logic as follows: + # - All custom levels have a max score of 10 + # - All official levels have a max score of 20 if both route score and algorithm scores are enabled + # except for levels 1-12 which have a max score of 20 even though the algorithm score is disabled + if attempt.level.episode is None: + max_score = 10 + else: + if not attempt.level.disable_route_score: + max_score += 10 + if not attempt.level.disable_algorithm_score: + max_score += 10 + if int(attempt.level.name) < 13: + max_score = 20 + num_all += 1 if attempt.score: if attempt.score / max_score >= threshold: