From 8839c8594d518dd35eb77e82085c818d675c62fc Mon Sep 17 00:00:00 2001 From: Florian Aucomte Date: Thu, 15 Feb 2024 16:51:50 +0000 Subject: [PATCH] fix: Scoreboard issue with new levels (#1584) * fix: Scoreboard issue with new levels * Improve test * Merge branch 'master' into fix_scoreboard_new_eps * Merge master * Merge branch 'master' into fix_scoreboard_new_eps * Merge branch 'master' into fix_scoreboard_new_eps --- game/forms.py | 5 +- .../0089_episodes_in_development.py | 30 +++ game/tests/test_scoreboard.py | 218 ++++++++++++++---- game/views/scoreboard.py | 133 ++++++++--- 4 files changed, 304 insertions(+), 82 deletions(-) create mode 100644 game/migrations/0089_episodes_in_development.py diff --git a/game/forms.py b/game/forms.py index ddb13049e..c995dafd6 100644 --- a/game/forms.py +++ b/game/forms.py @@ -18,7 +18,10 @@ 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 dropdown menu - episodes_choices = ((episode.id, episode.name) for episode in Episode.objects.all()) + episodes_choices = ( + (episode.id, episode.name) + for episode in Episode.objects.filter(in_development=False) + ) self.fields["episodes"] = forms.MultipleChoiceField( choices=itertools.chain(episodes_choices), widget=forms.CheckboxSelectMultiple(), diff --git a/game/migrations/0089_episodes_in_development.py b/game/migrations/0089_episodes_in_development.py new file mode 100644 index 000000000..ea5939f54 --- /dev/null +++ b/game/migrations/0089_episodes_in_development.py @@ -0,0 +1,30 @@ +from django.apps.registry import Apps +from django.db import migrations + + +def mark_episodes_in_development(apps: Apps, *args): + Episode = apps.get_model("game", "Episode") + + for i in range(13, 16): + episode = Episode.objects.get(pk=i) + episode.in_development = True + episode.save() + + +def unmark_episodes_in_development(apps: Apps, *args): + Episode = apps.get_model("game", "Episode") + + for i in range(13, 16): + episode = Episode.objects.get(pk=i) + episode.in_development = False + episode.save() + + +class Migration(migrations.Migration): + dependencies = [("game", "0088_rename_episodes")] + operations = [ + migrations.RunPython( + mark_episodes_in_development, + reverse_code=unmark_episodes_in_development, + ) + ] diff --git a/game/tests/test_scoreboard.py b/game/tests/test_scoreboard.py index a2288fb36..b509a8433 100644 --- a/game/tests/test_scoreboard.py +++ b/game/tests/test_scoreboard.py @@ -6,7 +6,10 @@ from common.models import Class, Teacher, Student from common.tests.utils.classes import create_class_directly from common.tests.utils.organisation import create_organisation_directly -from common.tests.utils.student import create_school_student_directly, create_independent_student_directly +from common.tests.utils.student import ( + create_school_student_directly, + create_independent_student_directly, +) from common.tests.utils.teacher import signup_teacher_directly from django.test import Client, TestCase from django.urls import reverse @@ -22,7 +25,11 @@ shared_levels_data, SharedHeaders, ) -from game.views.scoreboard_csv import scoreboard_csv, Headers as CSVHeaders, SharedHeaders as CSVSharedHeaders +from game.views.scoreboard_csv import ( + scoreboard_csv, + Headers as CSVHeaders, + SharedHeaders as CSVSharedHeaders, +) class ScoreboardTestCase(TestCase): @@ -31,7 +38,10 @@ def test_teacher_multiple_students_multiple_levels(self): episode_ids = [1, 2] episode1 = Episode.objects.get(id=1) episode2 = Episode.objects.get(id=2) - level_ids = [f"{x}" for x in range(1, len(episode1.levels) + len(episode2.levels) + 1)] + level_ids = [ + f"{x}" + for x in range(1, len(episode1.levels) + len(episode2.levels) + 1) + ] level1 = Level.objects.get(name="1") level13 = Level.objects.get(name="13") @@ -42,7 +52,9 @@ def test_teacher_multiple_students_multiple_levels(self): create_attempt(student2, level13, 16) # Setup custom levels data - shared_level = create_save_level(student, "custom_level1", shared_with=[student2.new_user]) + shared_level = create_save_level( + student, "custom_level1", shared_with=[student2.new_user] + ) create_attempt(student2, shared_level, 10) @@ -50,9 +62,9 @@ def test_teacher_multiple_students_multiple_levels(self): all_shared_levels = [shared_level] attempts_per_student = { - student: Attempt.objects.filter(level__in=all_levels, student=student, is_best_attempt=True).select_related( - "level" - ), + student: Attempt.objects.filter( + level__in=all_levels, student=student, is_best_attempt=True + ).select_related("level"), student2: Attempt.objects.filter( level__in=all_levels, student=student2, is_best_attempt=True ).select_related("level"), @@ -60,13 +72,21 @@ def test_teacher_multiple_students_multiple_levels(self): shared_attempts_per_student = { student2: Attempt.objects.filter( - level__in=all_shared_levels, student=student2, is_best_attempt=True + 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_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 ) @@ -99,7 +119,9 @@ def test_teacher_multiple_students_multiple_levels(self): # Check data for custom levels matches assert shared_headers == SharedHeaders - assert shared_level_headers == [f"{shared_level.name} ({shared_level.owner})"] + assert shared_level_headers == [ + f"{shared_level.name} ({shared_level.owner})" + ] assert len(shared_student_data) == 1 @@ -130,14 +152,23 @@ def test_scoreboard_loads(self): data = {"classes": [klass.id], "view": [""]} response = c.post(url, data) + + active_levels = Level.objects.filter(episode__in_development=False) + assert response.status_code == 200 - assert len(response.context["level_headers"]) == 109 + assert len(response.context["level_headers"]) == active_levels.count() def test_student_can_see_classes(self): """A student should be able to see the classes they are in""" - mr_teacher = Teacher.objects.factory("Normal", "Teacher", "normal@school.edu", "secretpa$sword") - klass, name1, _ = create_class_directly(mr_teacher.user.user.email, class_name="Class 1") - _, name2, _ = create_class_directly(mr_teacher.user.user.email, class_name="Class 2") + mr_teacher = Teacher.objects.factory( + "Normal", "Teacher", "normal@school.edu", "secretpa$sword" + ) + klass, name1, _ = create_class_directly( + mr_teacher.user.user.email, class_name="Class 1" + ) + _, name2, _ = create_class_directly( + mr_teacher.user.user.email, class_name="Class 2" + ) student = Student.objects.schoolFactory(klass, "some student", "secret") c = Client() @@ -146,30 +177,46 @@ def test_student_can_see_classes(self): url = reverse("scoreboard") response = c.get(url) - choices_in_form = [v for (k, v) in response.context["form"]["classes"].field.choices] + choices_in_form = [ + v for (k, v) in response.context["form"]["classes"].field.choices + ] assert name1 in choices_in_form assert name2 not in choices_in_form assert len(choices_in_form) == 1 def test_admin_teacher_can_see_all_classes(self): """An admin should be able to see all classes, not just the ones they teach""" - normal_teacher = Teacher.objects.factory("Normal", "Teacher", "normal@school.edu", "secretpa$sword") - admin_teacher = Teacher.objects.factory("Admin", "Admin", "admin@school.edu", "secretpa$sword2") + normal_teacher = Teacher.objects.factory( + "Normal", "Teacher", "normal@school.edu", "secretpa$sword" + ) + admin_teacher = Teacher.objects.factory( + "Admin", "Admin", "admin@school.edu", "secretpa$sword2" + ) admin_teacher.is_admin = True admin_teacher.save() - _, name1, _ = create_class_directly(admin_teacher.user.user.email, class_name="Class 1") - _, name2, _ = create_class_directly(admin_teacher.user.user.email, class_name="Class 2") - _, name3, _ = create_class_directly(normal_teacher.user.user.email, class_name="Class 3") + _, name1, _ = create_class_directly( + admin_teacher.user.user.email, class_name="Class 1" + ) + _, name2, _ = create_class_directly( + admin_teacher.user.user.email, class_name="Class 2" + ) + _, name3, _ = create_class_directly( + normal_teacher.user.user.email, class_name="Class 3" + ) c = Client() - c.login(username=admin_teacher.user.user.email, password="secretpa$sword2") + c.login( + username=admin_teacher.user.user.email, password="secretpa$sword2" + ) url = reverse("scoreboard") response = c.get(url) - choices_in_form = [v for (k, v) in response.context["form"]["classes"].field.choices] + choices_in_form = [ + v for (k, v) in response.context["form"]["classes"].field.choices + ] assert name1 in choices_in_form assert name2 in choices_in_form @@ -177,12 +224,22 @@ def test_admin_teacher_can_see_all_classes(self): def test_non_admin_teacher_can_only_see_their_own_classes(self): """A teacher who is not an admin should only be able to see their classes, not ones taught by others""" - teacher1 = Teacher.objects.factory("First", "Teacher", "normal@school.edu", "secretpa$sword") - teacher2 = Teacher.objects.factory("Second", "Teacher", "admin@school.edu", "secretpa$sword2") + teacher1 = Teacher.objects.factory( + "First", "Teacher", "normal@school.edu", "secretpa$sword" + ) + teacher2 = Teacher.objects.factory( + "Second", "Teacher", "admin@school.edu", "secretpa$sword2" + ) - _, name1, _ = create_class_directly(teacher2.user.user.email, class_name="Class 1") - _, name2, _ = create_class_directly(teacher2.user.user.email, class_name="Class 2") - _, name3, _ = create_class_directly(teacher1.user.user.email, class_name="Class 3") + _, name1, _ = create_class_directly( + teacher2.user.user.email, class_name="Class 1" + ) + _, name2, _ = create_class_directly( + teacher2.user.user.email, class_name="Class 2" + ) + _, name3, _ = create_class_directly( + teacher1.user.user.email, class_name="Class 3" + ) c = Client() # First teacher logs in. Should see only Class 3 @@ -191,7 +248,9 @@ def test_non_admin_teacher_can_only_see_their_own_classes(self): url = reverse("scoreboard") response = c.get(url) - choices_in_form = [v for (k, v) in response.context["form"]["classes"].field.choices] + choices_in_form = [ + v for (k, v) in response.context["form"]["classes"].field.choices + ] assert name3 in choices_in_form assert name1 not in choices_in_form @@ -202,7 +261,9 @@ def test_non_admin_teacher_can_only_see_their_own_classes(self): c.login(username="admin@school.edu", password="secretpa$sword2") response = c.get(url) - choices_in_form = [v for (k, v) in response.context["form"]["classes"].field.choices] + choices_in_form = [ + v for (k, v) in response.context["form"]["classes"].field.choices + ] assert name3 not in choices_in_form assert name1 in choices_in_form @@ -218,7 +279,10 @@ def test_independent_student_cannot_see_scoreboard(self): url = reverse("scoreboard") response = c.get(url) - assert "Scoreboard is only visible to school students and teachers" in str(response.content) + assert ( + "Scoreboard is only visible to school students and teachers" + in str(response.content) + ) class ScoreboardCsvTestCase(TestCase): @@ -234,14 +298,25 @@ def test_scoreboard_csv(self): # Create 2 custom levels and create the associated student data shared_level_rows = [None, None] - shared_level1 = create_save_level(students[0], "level1", shared_with=[students[1].new_user]) + shared_level1 = create_save_level( + students[0], "level1", shared_with=[students[1].new_user] + ) shared_level2 = create_save_level(students[1], "level2") shared_levels = [shared_level1, shared_level2] - shared_levels_headers = list([shared_level_to_name(level, level.owner) for level in shared_levels]) + shared_levels_headers = list( + [ + shared_level_to_name(level, level.owner) + for level in shared_levels + ] + ) - shared_level_rows[0] = self.shared_student_row(students[0], shared_levels) - shared_level_rows[1] = self.shared_student_row(students[1], shared_levels) + shared_level_rows[0] = self.shared_student_row( + students[0], shared_levels + ) + shared_level_rows[1] = self.shared_student_row( + students[1], shared_levels + ) # Create students' improvement table data improvement_data = [] @@ -250,7 +325,13 @@ def test_scoreboard_csv(self): improvement_data.append(stud) # Generate the CSV - response = scoreboard_csv(student_rows, levels, improvement_data, shared_levels_headers, shared_level_rows) + response = scoreboard_csv( + student_rows, + levels, + improvement_data, + shared_levels_headers, + shared_level_rows, + ) # Gather the data from the CSV ( @@ -261,15 +342,27 @@ def test_scoreboard_csv(self): ) = 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) + assert actual_scoreboard_header == self.expected_scoreboard_header( + levels + ) assert len(actual_scoreboard_rows) == len(student_rows) - assert actual_shared_levels_header == self.expected_shared_levels_header(shared_levels) + assert ( + actual_shared_levels_header + == self.expected_shared_levels_header(shared_levels) + ) assert len(actual_shared_levels_rows) == len(shared_level_rows) # check first scoreboard row - (class_name, name, completed_levels, total_time, total_scores, l1, l2, improvement) = actual_scoreboard_rows[ - 0 - ].split(",") + ( + class_name, + name, + completed_levels, + total_time, + total_scores, + l1, + l2, + improvement, + ) = actual_scoreboard_rows[0].split(",") assert student_rows[0].class_field.name == class_name assert student_rows[0].name == name assert student_rows[0].level_scores[0]["score"] == int(l1) @@ -278,9 +371,16 @@ def test_scoreboard_csv(self): # check last scoreboard row last = len(actual_scoreboard_rows) - 1 - (class_name, name, completed_levels, total_time, total_scores, l1, l2, improvement) = actual_scoreboard_rows[ - last - ].split(",") + ( + class_name, + name, + completed_levels, + total_time, + total_scores, + l1, + l2, + improvement, + ) = actual_scoreboard_rows[last].split(",") assert student_rows[last].class_field.name == class_name assert student_rows[last].name == name assert str(student_rows[last].total_time) == total_time @@ -364,12 +464,19 @@ def shared_student_row(self, student, shared_levels): def expected_scoreboard_header(self, levels): level_strings = list(map(str, levels)) - all_header_strings = CSVHeaders + level_strings + ["Areas for improvement"] + all_header_strings = ( + CSVHeaders + level_strings + ["Areas for improvement"] + ) joined = ",".join(all_header_strings) return joined def expected_shared_levels_header(self, shared_levels): - level_strings = list([shared_level_to_name(level, level.owner) for level in shared_levels]) + level_strings = list( + [ + shared_level_to_name(level, level.owner) + for level in shared_levels + ] + ) all_header_strings = CSVSharedHeaders + level_strings joined = ",".join(all_header_strings) return joined @@ -390,14 +497,25 @@ def actual_data(self, content, number_of_students): scoreboard_header = split[scoreboard_header_row] scoreboard_rows = split[scoreboard_rows_start:scoreboard_rows_end] shared_levels_header = split[shared_levels_header_row] - shared_levels_rows = split[shared_levels_rows_start:shared_levels_rows_end] - - return scoreboard_header, scoreboard_rows, shared_levels_header, shared_levels_rows + shared_levels_rows = split[ + shared_levels_rows_start:shared_levels_rows_end + ] + + return ( + scoreboard_header, + scoreboard_rows, + shared_levels_header, + shared_levels_rows, + ) def create_attempt(student, level, score): attempt = Attempt.objects.create( - finish_time=datetime.fromtimestamp(1435305072), level=level, student=student, score=score, is_best_attempt=True + finish_time=datetime.fromtimestamp(1435305072), + level=level, + student=student, + score=score, + is_best_attempt=True, ) attempt.start_time = datetime.fromtimestamp(1435305072) attempt.save() diff --git a/game/views/scoreboard.py b/game/views/scoreboard.py index d33c34df5..38797a8fa 100644 --- a/game/views/scoreboard.py +++ b/game/views/scoreboard.py @@ -51,14 +51,20 @@ def student_row(levels_sorted, student, best_attempts): level_scores[level.id] = {} level_scores[level.id]["score"] = "" - if level.episode is None and student.new_user not in level.shared_with.all(): + if ( + level.episode is None + and student.new_user not in level.shared_with.all() + ): level_scores[level.id]["score"] = "Not shared" if level.owner == student.user: level_scores[level.id]["score"] = "Owner" if best_attempts: - attempts_dict = {best_attempt.level.id: best_attempt for best_attempt in best_attempts} + attempts_dict = { + best_attempt.level.id: best_attempt + for best_attempt in best_attempts + } for level in levels_sorted: attempt = attempts_dict.get(level.id) @@ -96,16 +102,24 @@ def student_row(levels_sorted, student, best_attempts): times.append(chop_miliseconds(elapsed_time)) # '-' is used to show that the student has started the level but has not submitted any attempts - level_scores[level.id]["score"] = int(attempt.score) if attempt.score is not None else "-" - level_scores[level.id]["full_score"] = attempt.score == max_score - level_scores[level.id]["is_low_attempt"] = attempt.score == 0 or max_score / attempt.score < threshold + level_scores[level.id]["score"] = ( + int(attempt.score) if attempt.score is not None else "-" + ) + level_scores[level.id]["full_score"] = ( + attempt.score == max_score + ) + level_scores[level.id]["is_low_attempt"] = ( + attempt.score == 0 or max_score / attempt.score < threshold + ) else: times.append(timedelta(0)) total_time = sum(times, timedelta()) success_rate = ( - total_score / total_possible_score * 100 if total_possible_score > 0 else 0 + total_score / total_possible_score * 100 + if total_possible_score > 0 + else 0 ) row = StudentRow( @@ -124,7 +138,11 @@ def to_name(level): def shared_level_to_name(level, user): - return f"{level.name} (you)" if user == level.owner else f"{level.name} ({level.owner})" + return ( + f"{level.name} (you)" + if user == level.owner + else f"{level.name} ({level.owner})" + ) def scoreboard_data(episode_ids, attempts_per_students): @@ -136,16 +154,20 @@ def scoreboard_data(episode_ids, attempts_per_students): level_headers = list(map(to_name, levels_sorted)) student_data = [ - student_row(levels_sorted, student, best_attempts) for student, best_attempts in attempts_per_students.items() + student_row(levels_sorted, student, best_attempts) + for student, best_attempts in attempts_per_students.items() ] return student_data, Headers, level_headers, levels_sorted def shared_levels_data(user, shared_levels, attempts_per_students): - shared_level_headers = list(shared_level_to_name(level, user) for level in shared_levels) + shared_level_headers = list( + shared_level_to_name(level, user) for level in shared_levels + ) shared_student_data = [ - student_row(shared_levels, student, best_attempts) for student, best_attempts in attempts_per_students.items() + student_row(shared_levels, student, best_attempts) + for student, best_attempts in attempts_per_students.items() ] return SharedHeaders, shared_level_headers, shared_student_data @@ -169,14 +191,20 @@ def _check_attempts(best_attempts): total_score = 0 total_possible_score = 0 # Get the best attempts for the specific Episode - attempts = [best_attempt for best_attempt in best_attempts if best_attempt.level.episode.id == episode_id] + attempts = [ + best_attempt + for best_attempt in best_attempts + if best_attempt.level.episode.id == episode_id + ] for attempt in attempts: max_score = 10 if attempt.level.disable_route_score else 20 total_score += attempt.score if attempt.score is not None else 0 total_possible_score += max_score - is_low_attempt = attempt.score == 0 or max_score / attempt.score < threshold + is_low_attempt = ( + attempt.score == 0 or max_score / attempt.score < threshold + ) if is_low_attempt: low_episode_ids.add(episode_id) @@ -189,9 +217,14 @@ def get_improvement_data(attempts_per_student): for student, best_attempts in attempts_per_student.items(): episodes_of_concern = _check_attempts(best_attempts) if episodes_of_concern: - areas = [messages.get_episode_title(ep_id) for ep_id in episodes_of_concern] + areas = [ + messages.get_episode_title(ep_id) + for ep_id in episodes_of_concern + ] areas_summary = ", ".join(areas) - the_students.append(StudentInTrouble(student=student, areas=areas_summary)) + the_students.append( + StudentInTrouble(student=student, areas=areas_summary) + ) return the_students @@ -238,7 +271,9 @@ def scoreboard(request): user = User(request.user.userprofile) users_classes = classes_for(user) - all_episode_ids = list(range(1, 12)) + all_episode_ids = [ + episode.id for episode in Episode.objects.filter(in_development=False) + ] if user.is_independent_student(): return render_no_permission_error(request) @@ -247,7 +282,9 @@ def scoreboard(request): class_ids = set(map(int, request.POST.getlist("classes"))) # Show all levels if the teacher doesn't select any episode_ids = ( - set(all_episode_ids) if "episodes" not in request.POST else set(map(int, request.POST.getlist("episodes"))) + set(all_episode_ids) + if "episodes" not in request.POST + else set(map(int, request.POST.getlist("episodes"))) ) else: # Show no data on page load by default (if teacher) @@ -290,7 +327,9 @@ def scoreboard(request): if user.is_teacher(): if user.teacher.is_admin: # Get all custom levels owned by non-admin teachers - standard_teachers = Teacher.objects.filter(school=user.teacher.school, is_admin=False) + standard_teachers = Teacher.objects.filter( + school=user.teacher.school, is_admin=False + ) for standard_teacher in standard_teachers: shared_levels += levels_owned_by(standard_teacher.new_user) else: @@ -298,7 +337,9 @@ def scoreboard(request): shared_levels += levels_owned_by(request.user) # In all cases, get all admins' custom levels - school_admins = Teacher.objects.filter(school=user.teacher.school, is_admin=True) + school_admins = Teacher.objects.filter( + school=user.teacher.school, is_admin=True + ) for school_admin in school_admins: shared_levels += levels_owned_by(school_admin.new_user) @@ -314,18 +355,34 @@ def scoreboard(request): best_attempts_shared_levels = Attempt.objects.filter( level__in=shared_levels, student=student, is_best_attempt=True ).select_related("level") - attempts_per_student_shared_levels[student] = best_attempts_shared_levels + attempts_per_student_shared_levels[ + student + ] = best_attempts_shared_levels - (student_data, headers, level_headers, levels_sorted) = scoreboard_data(episode_ids, attempts_per_student) + (student_data, headers, level_headers, levels_sorted) = scoreboard_data( + episode_ids, attempts_per_student + ) improvement_data = get_improvement_data(attempts_per_student) - shared_headers, shared_level_headers, shared_student_data = shared_levels_data( - request.user.userprofile, shared_levels, attempts_per_student_shared_levels + ( + shared_headers, + shared_level_headers, + shared_student_data, + ) = shared_levels_data( + request.user.userprofile, + shared_levels, + attempts_per_student_shared_levels, ) csv_export = "export" in request.POST if csv_export: - return scoreboard_csv(student_data, levels_sorted, improvement_data, shared_level_headers, shared_student_data) + return scoreboard_csv( + student_data, + levels_sorted, + improvement_data, + shared_level_headers, + shared_student_data, + ) else: return scoreboard_view( request, @@ -341,7 +398,11 @@ def scoreboard(request): def render_no_permission_error(request): - return renderError(request, messages.no_permission_title(), messages.no_permission_scoreboard()) + return renderError( + request, + messages.no_permission_title(), + messages.no_permission_scoreboard(), + ) def is_teacher_with_no_classes_assigned(user, users_classes): @@ -371,7 +432,9 @@ def sorted_levels_by(level_ids): def are_classes_viewable_by_teacher(class_ids, user): teachers = Teacher.objects.filter(school=user.teacher.school) - classes_in_teachers_school = Class.objects.filter(teacher__in=teachers).values_list("id", flat=True) + classes_in_teachers_school = Class.objects.filter( + teacher__in=teachers + ).values_list("id", flat=True) for class_id in class_ids: is_authorised = class_id in classes_in_teachers_school if not is_authorised: @@ -386,7 +449,9 @@ def authorised_student_access(class_, class_ids): def students_visible_to_student(student): class_ = student.class_field if is_viewable(class_): - return class_.students.filter(new_user__is_active=True).select_related("class_field", "user__user") + return class_.students.filter(new_user__is_active=True).select_related( + "class_field", "user__user" + ) else: return [student] @@ -400,9 +465,9 @@ def students_visible_to_user(user, classes): def students_of_classes(classes): - return Student.objects.filter(class_field__in=classes, new_user__is_active=True).select_related( - "class_field", "user__user" - ) + return Student.objects.filter( + class_field__in=classes, new_user__is_active=True + ).select_related("class_field", "user__user") def is_valid_request(user, class_ids): @@ -434,10 +499,16 @@ def __init__(self, profile): self.student = profile.student def is_student(self): - return hasattr(self.profile, "student") and not self.profile.student.is_independent() + return ( + hasattr(self.profile, "student") + and not self.profile.student.is_independent() + ) def is_teacher(self): return hasattr(self.profile, "teacher") def is_independent_student(self): - return hasattr(self.profile, "student") and self.profile.student.is_independent() + return ( + hasattr(self.profile, "student") + and self.profile.student.is_independent() + )