diff --git a/portal/tests/test_views.py b/portal/tests/test_views.py index cfba21ea6..0de935fd5 100644 --- a/portal/tests/test_views.py +++ b/portal/tests/test_views.py @@ -8,9 +8,21 @@ import pytest from aimmo.models import Game from common.helpers.emails import NOTIFICATION_EMAIL -from common.models import Class, DailyActivity, School, Student, Teacher, TotalActivity, UserProfile, UserSession +from common.models import ( + Class, + DailyActivity, + School, + Student, + Teacher, + TotalActivity, + UserProfile, + UserSession, +) from common.tests.utils.classes import create_class_directly -from common.tests.utils.organisation import create_organisation_directly, join_teacher_to_organisation +from common.tests.utils.organisation import ( + create_organisation_directly, + join_teacher_to_organisation, +) from common.tests.utils.student import ( create_independent_student_directly, create_school_student_directly, @@ -43,7 +55,9 @@ class TestTeacherViews(TestCase): def setUpTestData(cls): cls.email, cls.password = signup_teacher_directly() _, _, cls.class_access_code = create_class_directly(cls.email) - _, _, cls.student = create_school_student_directly(cls.class_access_code) + _, _, cls.student = create_school_student_directly( + cls.class_access_code + ) def login(self): c = Client() @@ -52,7 +66,9 @@ def login(self): def test_reminder_cards(self): c = self.login() - url = reverse("teacher_print_reminder_cards", args=[self.class_access_code]) + url = reverse( + "teacher_print_reminder_cards", args=[self.class_access_code] + ) # First test with 2 dummy students NAME1 = "Test name" @@ -86,7 +102,9 @@ def test_reminder_cards(self): # page number students_per_page = REMINDER_CARDS_PDF_ROWS * REMINDER_CARDS_PDF_COLUMNS for _ in range(len(studentlist), students_per_page + 1): - studentlist.append({"name": NAME1, "password": PASSWORD1, "login_url": URL}) + studentlist.append( + {"name": NAME1, "password": PASSWORD1, "login_url": URL} + ) assert len(studentlist) == students_per_page + 1 @@ -125,7 +143,9 @@ def test_csv(self): reader = csv.reader(io.StringIO(content)) access_code = self.class_access_code - class_url = reverse("student_login", kwargs={"access_code": access_code}) + class_url = reverse( + "student_login", kwargs={"access_code": access_code} + ) row0 = next(reader) assert row0[0].strip() == access_code assert class_url in row0[1].strip() @@ -164,7 +184,9 @@ def test_organisation_kick_has_correct_permissions(self): def test_daily_activity_student_details(self): c = self.login() - url = reverse("teacher_print_reminder_cards", args=[self.class_access_code]) + url = reverse( + "teacher_print_reminder_cards", args=[self.class_access_code] + ) data = { "data": json.dumps( @@ -227,9 +249,17 @@ def _set_up_test_data(self): teacher_email, teacher_password = signup_teacher_directly() create_organisation_directly(teacher_email) _, _, class_access_code = create_class_directly(teacher_email) - student_name, student_password, _ = create_school_student_directly(class_access_code) + student_name, student_password, _ = create_school_student_directly( + class_access_code + ) - return teacher_email, teacher_password, student_name, student_password, class_access_code + return ( + teacher_email, + teacher_password, + student_name, + student_password, + class_access_code, + ) def _create_and_login_teacher(self, next_url=False): email, password, _, _, _ = self._set_up_test_data() @@ -241,7 +271,12 @@ def _create_and_login_teacher(self, next_url=False): c = Client() response = c.post( - url, {"auth-username": email, "auth-password": password, "teacher_login_view-current_step": "auth"} + url, + { + "auth-username": email, + "auth-password": password, + "teacher_login_view-current_step": "auth", + }, ) return response, c @@ -249,9 +284,16 @@ def _create_and_login_school_student(self, next_url=False): _, _, name, password, class_access_code = self._set_up_test_data() if next_url: - url = reverse("student_login", kwargs={"access_code": class_access_code}) + "?next=/" + url = ( + reverse( + "student_login", kwargs={"access_code": class_access_code} + ) + + "?next=/" + ) else: - url = reverse("student_login", kwargs={"access_code": class_access_code}) + url = reverse( + "student_login", kwargs={"access_code": class_access_code} + ) c = Client() response = c.post(url, {"username": name, "password": password}) @@ -270,7 +312,11 @@ def test_teacher_session(self): c = Client() c.post( reverse("teacher_login"), - {"auth-username": email, "auth-password": password, "teacher_login_view-current_step": "auth"}, + { + "auth-username": email, + "auth-password": password, + "teacher_login_view-current_step": "auth", + }, ) # check if there's a UserSession data within the last minute now = timezone.now() @@ -286,7 +332,9 @@ def test_teacher_session(self): def _get_user_class(self, name, class_access_code): klass = Class.objects.get(access_code=class_access_code) - students = Student.objects.filter(new_user__first_name__iexact=name, class_field=klass) + students = Student.objects.filter( + new_user__first_name__iexact=name, class_field=klass + ) assert len(students) == 1 user = students[0].new_user return user, klass @@ -296,10 +344,19 @@ def test_student_session_class_form(self): _, _, name, password, class_access_code = self._set_up_test_data() c = Client() - resp = c.post(reverse("student_login_access_code"), {"access_code": class_access_code}) + resp = c.post( + reverse("student_login_access_code"), + {"access_code": class_access_code}, + ) assert resp.status_code == 302 nexturl = resp.url - assert nexturl == reverse("student_login", kwargs={"access_code": class_access_code, "login_type": "classform"}) + assert nexturl == reverse( + "student_login", + kwargs={ + "access_code": class_access_code, + "login_type": "classform", + }, + ) c.post(nexturl, {"username": name, "password": password}) # check if there's a UserSession data within the last 10 secs @@ -319,7 +376,9 @@ def test_student_session_class_link(self): _, _, name, password, class_access_code = self._set_up_test_data() c = Client() - url = reverse("student_login", kwargs={"access_code": class_access_code}) + url = reverse( + "student_login", kwargs={"access_code": class_access_code} + ) c.post(url, {"username": name, "password": password}) # check if there's a UserSession data within the last 10 secs @@ -340,7 +399,9 @@ def test_student_login_failed(self): randomname = "randomname" c = Client() - url = reverse("student_login", kwargs={"access_code": class_access_code}) + url = reverse( + "student_login", kwargs={"access_code": class_access_code} + ) c.post(url, {"username": randomname, "password": "xx"}) # check if there's a UserSession data within the last 10 secs @@ -366,7 +427,9 @@ def test_indep_student_session(self): def test_student_direct_login(self): _, _, _, _, class_access_code = self._set_up_test_data() - student, login_id, _, _ = create_student_with_direct_login(class_access_code) + student, login_id, _, _ = create_student_with_direct_login( + class_access_code + ) c = Client() assert c.login(user_id=student.new_user.id, login_id=login_id) == True @@ -441,14 +504,18 @@ def test_student_dashboard_view(self): teacher_email, teacher_password = signup_teacher_directly() create_organisation_directly(teacher_email) klass, _, class_access_code = create_class_directly(teacher_email) - student_name, student_password, student = create_school_student_directly(class_access_code) + ( + student_name, + student_password, + student, + ) = create_school_student_directly(class_access_code) # Expected context data when a student hasn't played anything yet EXPECTED_DATA_FIRST_LOGIN = { "num_completed": 0, "num_top_scores": 0, "total_score": 0, - "total_available_score": 2320, + "total_available_score": 2040, } # Expected context data when a student has attempted some RR levels @@ -456,7 +523,7 @@ def test_student_dashboard_view(self): "num_completed": 2, "num_top_scores": 1, "total_score": 39, - "total_available_score": 2320, + "total_available_score": 2040, } # Expected context data when a student has also attempted some custom RR levels @@ -464,7 +531,7 @@ def test_student_dashboard_view(self): "num_completed": 2, "num_top_scores": 1, "total_score": 39, - "total_available_score": 2320, + "total_available_score": 2040, "total_custom_score": 10, "total_custom_available_score": 20, } @@ -474,7 +541,7 @@ def test_student_dashboard_view(self): "num_completed": 2, "num_top_scores": 1, "total_score": 39, - "total_available_score": 2320, + "total_available_score": 2040, "total_custom_score": 10, "total_custom_available_score": 20, "worksheet_id": 3, @@ -484,7 +551,9 @@ def test_student_dashboard_view(self): c = Client() # Login and check initial data - url = reverse("student_login", kwargs={"access_code": class_access_code}) + url = reverse( + "student_login", kwargs={"access_code": class_access_code} + ) c.post(url, {"username": student_name, "password": student_password}) student_dashboard_url = reverse("student_details") @@ -540,7 +609,14 @@ def test_delete_account(self): c = Client() url = reverse("teacher_login") - c.post(url, {"auth-username": email, "auth-password": password, "teacher_login_view-current_step": "auth"}) + c.post( + url, + { + "auth-username": email, + "auth-password": password, + "teacher_login_view-current_step": "auth", + }, + ) # fail to delete with incorrect password url = reverse("delete_account") @@ -555,7 +631,9 @@ def test_delete_account(self): # try again with the correct password url = reverse("delete_account") - response = c.post(url, {"password": password, "unsubscribe_newsletter": "on"}) + response = c.post( + url, {"password": password, "unsubscribe_newsletter": "on"} + ) assert response.status_code == 302 assert response.url == reverse("home") @@ -608,7 +686,14 @@ def test_delete_account_admin(self): c = Client() url = reverse("teacher_login") - c.post(url, {"auth-username": email1, "auth-password": password1, "teacher_login_view-current_step": "auth"}) + c.post( + url, + { + "auth-username": email1, + "auth-password": password1, + "teacher_login_view-current_step": "auth", + }, + ) # delete teacher1 account url = reverse("delete_account") @@ -625,7 +710,9 @@ def test_delete_account_admin(self): school_id = school.id school_name = school.name - teachers = Teacher.objects.filter(school=school).order_by("new_user__last_name", "new_user__first_name") + teachers = Teacher.objects.filter(school=school).order_by( + "new_user__last_name", "new_user__first_name" + ) assert len(teachers) == 3 # one of the remaining teachers should be admin (the second in our case, as it's alphabetical) @@ -641,14 +728,23 @@ def test_delete_account_admin(self): user3.new_teacher.save() url = reverse("teacher_login") - c.post(url, {"auth-username": email3, "auth-password": password3, "teacher_login_view-current_step": "auth"}) + c.post( + url, + { + "auth-username": email3, + "auth-password": password3, + "teacher_login_view-current_step": "auth", + }, + ) # now delete teacher3 account url = reverse("delete_account") c.post(url, {"password": password3}) # 2 teachers left - teachers = Teacher.objects.filter(school=school).order_by("new_user__last_name", "new_user__first_name") + teachers = Teacher.objects.filter(school=school).order_by( + "new_user__last_name", "new_user__first_name" + ) assert len(teachers) == 2 # teacher2 should still be admin, teacher4 is not passed admin role because there is teacher2 @@ -660,14 +756,23 @@ def test_delete_account_admin(self): # delete teacher4 anonymise(user4) - teachers = Teacher.objects.filter(school=school).order_by("new_user__last_name", "new_user__first_name") + teachers = Teacher.objects.filter(school=school).order_by( + "new_user__last_name", "new_user__first_name" + ) assert len(teachers) == 1 u = User.objects.get(id=usrid2) assert u.new_teacher.is_admin # delete teacher2 (the last one left) url = reverse("teacher_login") - c.post(url, {"auth-username": email2, "auth-password": password2, "teacher_login_view-current_step": "auth"}) + c.post( + url, + { + "auth-username": email2, + "auth-password": password2, + "teacher_login_view-current_step": "auth", + }, + ) url = reverse("delete_account") c.post(url, {"password": password2}) @@ -715,7 +820,9 @@ def test_registrations_increment_data(self): total_activity = TotalActivity.objects.get(id=1) teacher_registration_count = total_activity.teacher_registrations student_registration_count = total_activity.student_registrations - independent_registration_count = total_activity.independent_registrations + independent_registration_count = ( + total_activity.independent_registrations + ) response = c.post( reverse("register"), @@ -734,7 +841,10 @@ def test_registrations_increment_data(self): total_activity = TotalActivity.objects.get(id=1) - assert total_activity.teacher_registrations == teacher_registration_count + 1 + assert ( + total_activity.teacher_registrations + == teacher_registration_count + 1 + ) response = c.post( reverse("register"), @@ -755,20 +865,29 @@ def test_registrations_increment_data(self): total_activity = TotalActivity.objects.get(id=1) - assert total_activity.independent_registrations == independent_registration_count + 1 + assert ( + total_activity.independent_registrations + == independent_registration_count + 1 + ) teacher_email, teacher_password = signup_teacher_directly() create_organisation_directly(teacher_email) _, _, access_code = create_class_directly(teacher_email) c.login(username=teacher_email, password=teacher_password) - c.post(reverse("view_class", kwargs={"access_code": access_code}), {"names": "Student 1, Student 2, Student 3"}) + c.post( + reverse("view_class", kwargs={"access_code": access_code}), + {"names": "Student 1, Student 2, Student 3"}, + ) assert response.status_code == 302 total_activity = TotalActivity.objects.get(id=1) - assert total_activity.student_registrations == student_registration_count + 3 + assert ( + total_activity.student_registrations + == student_registration_count + 3 + ) # CRON view tests @@ -787,8 +906,12 @@ def generic( secure=False, **extra, ): - wsgi_response = super().generic(method, path, data, content_type, secure, **extra) - assert 200 <= wsgi_response.status_code < 300, f"Response has error status code: {wsgi_response.status_code}" + wsgi_response = super().generic( + method, path, data, content_type, secure, **extra + ) + assert ( + 200 <= wsgi_response.status_code < 300 + ), f"Response has error status code: {wsgi_response.status_code}" return wsgi_response @@ -807,7 +930,9 @@ def setUp(self): indy_email, _, _ = create_independent_student_directly() self.teacher_user = User.objects.get(email=teacher_email) - self.teacher_user_profile = UserProfile.objects.get(user=self.teacher_user) + self.teacher_user_profile = UserProfile.objects.get( + user=self.teacher_user + ) self.indy_user = User.objects.get(email=indy_email) self.indy_user_profile = UserProfile.objects.get(user=self.indy_user) @@ -822,11 +947,17 @@ def send_verify_email_reminder( send_email: Mock, assert_called: bool, ): - self.teacher_user.date_joined = timezone.now() - timedelta(days=days, hours=12) + self.teacher_user.date_joined = timezone.now() - timedelta( + days=days, hours=12 + ) self.teacher_user.save() - self.student_user.date_joined = timezone.now() - timedelta(days=days, hours=12) + self.student_user.date_joined = timezone.now() - timedelta( + days=days, hours=12 + ) self.student_user.save() - self.indy_user.date_joined = timezone.now() - timedelta(days=days, hours=12) + self.indy_user.date_joined = timezone.now() - timedelta( + days=days, hours=12 + ) self.indy_user.save() self.teacher_user_profile.is_verified = is_verified @@ -928,7 +1059,9 @@ def test_anonymise_unverified_accounts_view(self): now = timezone.now() for user in [self.teacher_user, self.indy_user, self.student_user]: - user.date_joined = now - timedelta(days=USER_DELETE_UNVERIFIED_ACCOUNT_DAYS + 1) + user.date_joined = now - timedelta( + days=USER_DELETE_UNVERIFIED_ACCOUNT_DAYS + 1 + ) user.save() for user_profile in [self.teacher_user_profile, self.indy_user_profile]: @@ -993,7 +1126,9 @@ def anonymise_unverified_users( new_user=indy_user, ) - activity_today = DailyActivity.objects.get_or_create(date=datetime.now().date())[0] + activity_today = DailyActivity.objects.get_or_create( + date=datetime.now().date() + )[0] daily_teacher_count = activity_today.anonymised_unverified_teachers daily_indy_count = activity_today.anonymised_unverified_independents @@ -1016,16 +1151,30 @@ def anonymise_unverified_users( assert indy_user_active == assert_active assert student_user_active - activity_today = DailyActivity.objects.get_or_create(date=datetime.now().date())[0] + activity_today = DailyActivity.objects.get_or_create( + date=datetime.now().date() + )[0] total_activity = TotalActivity.objects.get(id=1) if not teacher_user_active: - assert activity_today.anonymised_unverified_teachers == daily_teacher_count + 1 - assert total_activity.anonymised_unverified_teachers == total_teacher_count + 1 + assert ( + activity_today.anonymised_unverified_teachers + == daily_teacher_count + 1 + ) + assert ( + total_activity.anonymised_unverified_teachers + == total_teacher_count + 1 + ) if not indy_user_active: - assert activity_today.anonymised_unverified_independents == daily_indy_count + 1 - assert total_activity.anonymised_unverified_independents == total_indy_count + 1 + assert ( + activity_today.anonymised_unverified_independents + == daily_indy_count + 1 + ) + assert ( + total_activity.anonymised_unverified_independents + == total_indy_count + 1 + ) teacher_user.delete() indy_user.delete() diff --git a/portal/views/student/play.py b/portal/views/student/play.py index 6c24a5470..3137f6e82 100644 --- a/portal/views/student/play.py +++ b/portal/views/student/play.py @@ -116,6 +116,13 @@ def _compute_rapid_router_scores( level__in=levels, student=student, is_best_attempt=True ).select_related("level") + # Calculate total available score. A level has a max score of 20 by + # default unless its route score or algorithm score is disable or + # it is a custom level (not in an episode). Levels 1-12 have a + # max score of 20 even if the algo score is disabled. + for level in levels: + total_available_score += _get_max_score_for_level(level) + # For each level, compare best attempt's score with level's max score and # increment variables as needed if best_attempts: @@ -124,28 +131,11 @@ def _compute_rapid_router_scores( for best_attempt in best_attempts } for level in levels: - # Calculate total available score. A level has a max score of 20 by - # default unless its route score or algorithm score is disable or - # it is a custom level (not in an episode). Levels 1-12 have a - # max score of 20 even if the algo score is disabled. - max_score = ( - 10 - if ( - level.id > 12 - and ( - level.disable_route_score - or level.disable_algorithm_score - or not level.episode - ) - ) - else 20 - ) - total_available_score += max_score attempt = attempts_dict.get(level.id) if attempt and attempt.score: num_completed += 1 - if attempt.score == max_score: + if attempt.score == _get_max_score_for_level(level): num_top_scores += 1 total_score += attempt.score @@ -158,6 +148,19 @@ def _compute_rapid_router_scores( } +def _get_max_score_for_level(level: Level) -> int: + return ( + 10 + if level.id > 12 + and ( + level.disable_route_score + or level.disable_algorithm_score + or not level.episode + ) + else 20 + ) + + def username_labeller(request): return request.user.username