From d16bb59392179bfd419ef29ab31ede8c189cca1b Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Thu, 19 Oct 2023 14:56:08 +0200 Subject: [PATCH] wip: assignment resolving experimental --- .../tests/graphql/test_assignment.py | 280 ++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py new file mode 100644 index 00000000..db9ae805 --- /dev/null +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py @@ -0,0 +1,280 @@ +from datetime import datetime +from typing import NamedTuple, List + +from graphene_django.utils import GraphQLTestCase + +from vbv_lernwelt.assignment.models import ( + AssignmentType, + AssignmentCompletion, + Assignment, + AssignmentCompletionStatus, +) +from vbv_lernwelt.assignment.tests.assignment_factories import ( + AssignmentFactory, + AssignmentListPageFactory, +) +from vbv_lernwelt.course.models import CourseSessionUser, CourseSession +from vbv_lernwelt.course_session.models import ( + CourseSessionAssignment, + CourseSessionEdoniqTest, +) +from vbv_lernwelt.dashboard.tests.graphql.utils import ( + add_course_session_user, + create_circle, + create_course, + create_course_session, + create_user, +) +from vbv_lernwelt.learnpath.tests.learning_path_factories import ( + LearningContentAssignmentFactory, + LearningContentEdoniqTestFactory, +) +from vbv_lernwelt.notify.email.email_services import format_swiss_datetime + + +class AssignmentCompletionMetrics(NamedTuple): + passed: int + failed: int + unranked: int + + @property + def ranking_completed(self) -> bool: + """ + Assumption: Completed means all users have been ranked + (passed or failed) -> intermediate states are irrelevant + for the course session supervisor. + """ + return self.unranked == 0 + + @property + def average_passed(self) -> float: + total = self.passed + self.failed + self.unranked + + if total == 0: + return 0 + + return self.passed / total + + +class AssignmentCompletionsStatistics(NamedTuple): + count_completed: int + average_passed: float + + +def calculate_assignment_completions_statistics( + metrics: List[AssignmentCompletionMetrics], +) -> AssignmentCompletionsStatistics: + completed_metrics = [m for m in metrics if m.ranking_completed] + + if not completed_metrics: + return AssignmentCompletionsStatistics(count_completed=0, average_passed=0) + + count_completed = len(completed_metrics) + + average_passed_completed = ( + sum([m.average_passed for m in completed_metrics]) / count_completed + ) + + return AssignmentCompletionsStatistics( + count_completed=count_completed, average_passed=average_passed_completed + ) + + +def get_assignment_completion_metrics( + course_session: CourseSession, assignment: Assignment +) -> AssignmentCompletionMetrics: + course_session_users = CourseSessionUser.objects.filter( + course_session=course_session, + role=CourseSessionUser.Role.MEMBER, + ).values_list("user", flat=True) + + evaluation_results = AssignmentCompletion.objects.filter( + completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED, + assignment_user__in=course_session_users, + course_session=course_session, + assignment=assignment, + ).values_list("evaluation_passed", flat=True) + + count_passed = len([passed for passed in evaluation_results if passed]) + count_failed = len(evaluation_results) - count_passed + count_unranked = len(course_session_users) - count_passed - count_failed + + return AssignmentCompletionMetrics( + passed=count_passed, failed=count_failed, unranked=count_unranked + ) + + +class DashboardAssignmentRecord(NamedTuple): + course_session_id: str + circle_id: str + + # type of assignment (translated client-side) + assignment_type_translation_key: str + assignment_title: str + + deadline: datetime + metrics: AssignmentCompletionMetrics + + details_url: str + + @property + def deadline_formatted(self) -> str: + return format_swiss_datetime(self.deadline) + + +def create_dashboard_record( + course_session_assignment: CourseSessionAssignment | CourseSessionEdoniqTest, +) -> DashboardAssignmentRecord: + if isinstance(course_session_assignment, CourseSessionAssignment): + due_date = course_session_assignment.submission_deadline + else: + due_date = course_session_assignment.deadline + + learning_content = course_session_assignment.learning_content + + return DashboardAssignmentRecord( + course_session_id=str(course_session_assignment.course_session.id), + circle_id=learning_content.get_circle().id, + assignment_type_translation_key=due_date.assignment_type_translation_key, + assignment_title=learning_content.content_assignment.title, + metrics=get_assignment_completion_metrics( + course_session=course_session_assignment.course_session, + assignment=learning_content.content_assignment, + ), + details_url=due_date.url_expert, + deadline=due_date.start, + ) + + +def trybetter(course_session): + dashboard_records: List[DashboardAssignmentRecord] = [] + + for course_session_assignment in CourseSessionAssignment.objects.filter( + course_session=course_session, + learning_content__assignment_type__in=[ + AssignmentType.CASEWORK.value, + AssignmentType.PREP_ASSIGNMENT.value, + ], + ): + dashboard_record = create_dashboard_record(course_session_assignment) + dashboard_records.append(dashboard_record) + + for course_session_edoniq_test in CourseSessionEdoniqTest.objects.filter( + course_session=course_session + ): + dashboard_record = create_dashboard_record(course_session_edoniq_test) + dashboard_records.append(dashboard_record) + + return sorted(dashboard_records, key=lambda r: r.deadline) + + +class AssignmentTestCase(GraphQLTestCase): + GRAPHQL_URL = "/server/graphql/" + + def test_assignment_something_not_yet_sure_what_exactly_who_knows(self): + # GIVEN + course, course_page = create_course("Test Course") + course_session = create_course_session(course=course, title="Test Bern 2022 a") + circle, _ = create_circle(title="Test Circle", course_page=course_page) + + supervisor = create_user("supervisor") + add_course_session_user( + course_session=course_session, + user=supervisor, + role=CourseSessionUser.Role.SESSION_SUPERVISOR, + ) + + m1 = create_user("member_1") + add_course_session_user( + course_session=course_session, + user=m1, + role=CourseSessionUser.Role.MEMBER, + ) + + m2 = create_user("member_2") + add_course_session_user( + course_session=course_session, + user=m2, + role=CourseSessionUser.Role.MEMBER, + ) + + m3 = create_user("member_3") + add_course_session_user( + course_session=course_session, + user=m3, + role=CourseSessionUser.Role.MEMBER, + ) + + e1 = create_user("expert_1") + add_course_session_user( + course_session=course_session, + user=e1, + role=CourseSessionUser.Role.EXPERT, + ) + + assignment = AssignmentFactory( + parent=AssignmentListPageFactory( + parent=course.coursepage, + ), + assignment_type=AssignmentType.CASEWORK.name, + title="Test Assignment", + effort_required="However long it takes", + intro_text="Assignment Intro Text", + performance_objectives=[], + ) + + edoniq_test = AssignmentFactory( + parent=AssignmentListPageFactory( + parent=course.coursepage, + ), + assignment_type=AssignmentType.EDONIQ_TEST.name, + title="Edoniq Test Assignment", + effort_required="However long it takes", + intro_text="Edoniq Test Assigment Intro Text", + performance_objectives=[], + ) + + AssignmentCompletion.objects.create( + assignment_user=m1, + assignment=edoniq_test, + evaluation_passed=True, + course_session=course_session, + completion_data={}, + ) + + AssignmentCompletion.objects.create( + assignment_user=m1, + assignment=assignment, + evaluation_passed=True, + course_session=course_session, + completion_data={}, + ) + + learning_content_assignment = LearningContentAssignmentFactory( + title="Learning Content Assignment Title", + parent=circle, + content_assignment=assignment, + ) + + learning_content_edoniq_test = LearningContentEdoniqTestFactory( + title="Learning Content Edoniq Test Title", + parent=circle, + content_assignment=edoniq_test, + ) + + CourseSessionAssignment.objects.create( + course_session=course_session, learning_content=learning_content_assignment + ) + + CourseSessionEdoniqTest.objects.create( + course_session=course_session, learning_content=learning_content_edoniq_test + ) + + # --------------------------------------------------------------------- + # --------------------------------------------------------------------- + # --------------------------------------------------------------------- + # --------------------------------------------------------------------- + + trybetter(course_session) + + self.client.force_login(supervisor)