diff --git a/server/vbv_lernwelt/dashboard/graphql/types/assignment.py b/server/vbv_lernwelt/dashboard/graphql/types/assignment.py new file mode 100644 index 00000000..07ad0ead --- /dev/null +++ b/server/vbv_lernwelt/dashboard/graphql/types/assignment.py @@ -0,0 +1,156 @@ +import math +from typing import List + +import graphene + +import vbv_lernwelt.assignment.models +from vbv_lernwelt.assignment.models import ( + AssignmentCompletion, + AssignmentCompletionStatus, + AssignmentType, +) +from vbv_lernwelt.course.models import CourseSession, CourseSessionUser +from vbv_lernwelt.course_session.models import ( + CourseSessionAssignment, + CourseSessionEdoniqTest, +) + + +class AssignmentCompletionMetrics(graphene.ObjectType): + passed_count = graphene.Int(required=True) + failed_count = graphene.Int(required=True) + unranked_count = graphene.Int(required=True) + ranking_completed = graphene.Boolean(required=True) + average_passed = graphene.Float(required=True) + + +class AssignmentRecord(graphene.ObjectType): + course_session_id = graphene.ID(required=True) + course_session_assignment_id = graphene.ID(required=True) + circle_id = graphene.ID(required=True) + generation = graphene.String(required=True) + assignment_type_translation_key = graphene.String(required=True) + assignment_title = graphene.String(required=True) + deadline = graphene.DateTime(required=True) + metrics = graphene.Field(AssignmentCompletionMetrics, required=True) + details_url = graphene.String(required=True) + + +class AssignmentSummary(graphene.ObjectType): + completed_count = graphene.Int(required=True) + average_passed = graphene.Float(required=True) + + +class Assignments(graphene.ObjectType): + records = graphene.List(AssignmentRecord, required=True) + summary = graphene.Field(AssignmentSummary, required=True) + + +def create_assignment_summary(metrics) -> AssignmentSummary: + completed_metrics = [m for m in metrics if m.ranking_completed] + + if not completed_metrics: + return AssignmentSummary(completed_count=0, average_passed=0) # noqa + + completed_count = len(completed_metrics) + + average_passed_completed = ( + sum([m.average_passed for m in completed_metrics]) / completed_count + ) + + return AssignmentSummary( + completed_count=completed_count, average_passed=average_passed_completed # noqa + ) + + +def get_assignment_completion_metrics( + course_session: CourseSession, assignment: vbv_lernwelt.assignment.models.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.value, + assignment_user__in=course_session_users, + course_session=course_session, + assignment=assignment, + ).values_list("evaluation_passed", flat=True) + + passed_count = len([passed for passed in evaluation_results if passed]) + failed_count = len(evaluation_results) - passed_count + + participants_count = len(course_session_users) + unranked_count = participants_count - passed_count - failed_count + + if participants_count == 0: + average_passed = 0 + else: + average_passed = math.ceil(passed_count / participants_count * 100) + + return AssignmentCompletionMetrics( + passed_count=passed_count, # noqa + failed_count=failed_count, # noqa + unranked_count=unranked_count, # noqa + ranking_completed=unranked_count == 0, # noqa + average_passed=average_passed, # noqa + ) + + +def create_record( + course_session_assignment: CourseSessionAssignment | CourseSessionEdoniqTest, +) -> AssignmentRecord: + 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 AssignmentRecord( + course_session_id=str(course_session_assignment.course_session.id), # noqa + circle_id=learning_content.get_circle().id, # noqa + course_session_assignment_id=str(course_session_assignment.id), # noqa + generation=course_session_assignment.course_session.generation, # noqa + assignment_type_translation_key=due_date.assignment_type_translation_key, # noqa + assignment_title=learning_content.content_assignment.title, # noqa + metrics=get_assignment_completion_metrics( # noqa + course_session=course_session_assignment.course_session, # noqa + assignment=learning_content.content_assignment, # noqa + ), + details_url=due_date.url_expert, # noqa + deadline=due_date.start, # noqa + ) + + +def assignments(course_id, user) -> Assignments: + course_sessions = CourseSession.objects.filter( + coursesessionuser__user=user, + coursesessionuser__role=CourseSessionUser.Role.SESSION_SUPERVISOR, + course_id=course_id, + ) + + records: List[AssignmentRecord] = [] + + for course_session in course_sessions: + for csa in CourseSessionAssignment.objects.filter( + course_session=course_session, + learning_content__assignment_type__in=[ + AssignmentType.CASEWORK.value, + AssignmentType.PREP_ASSIGNMENT.value, + ], + ): + record = create_record(csa) + records.append(record) + + for cset in CourseSessionEdoniqTest.objects.filter( + course_session=course_session + ): + record = create_record(cset) + records.append(record) + + return Assignments( + records=sorted(records, key=lambda r: r.deadline), # noqa + summary=create_assignment_summary([r.metrics for r in records]), # noqa + ) diff --git a/server/vbv_lernwelt/dashboard/graphql/types/attendance.py b/server/vbv_lernwelt/dashboard/graphql/types/attendance.py index 7a37378f..3db5fa69 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/attendance.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/attendance.py @@ -1,8 +1,8 @@ -import datetime import math from typing import List import graphene +from django.utils import timezone from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import CourseSessionUser @@ -38,7 +38,7 @@ def attendance_day_presences( course_session__course_id=course_id, course_session__coursesessionuser__user=user, course_session__coursesessionuser__role=CourseSessionUser.Role.SESSION_SUPERVISOR, - due_date__end__lt=datetime.datetime.now(), + due_date__end__lt=timezone.now(), ).order_by("-due_date__end") records = [] diff --git a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py index 3c342c81..2d1435c9 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py @@ -1,6 +1,10 @@ import graphene from vbv_lernwelt.course.models import CourseSession, CourseSessionUser +from vbv_lernwelt.dashboard.graphql.types.assignment import ( + Assignments, + assignments, +) from vbv_lernwelt.dashboard.graphql.types.attendance import ( attendance_day_presences, AttendanceDayPresences, @@ -36,6 +40,7 @@ class CourseDashboardType(graphene.ObjectType): course_session_properties = graphene.Field(CourseSessionProperties) attendance_day_presences = graphene.Field(AttendanceDayPresences) feedback_responses = graphene.Field(FeedbackResponses) + assignments = graphene.Field(Assignments) competences = graphene.Field(Competences) def resolve_attendance_day_presences(root, info) -> AttendanceDayPresences: @@ -47,6 +52,9 @@ class CourseDashboardType(graphene.ObjectType): def resolve_competences(root, info) -> Competences: return competences(root.course_id, info.context.user) + def resolve_assignments(root, info): + return assignments(root.course_id, info.context.user) + def resolve_course_session_properties(root, info): course_session_data = [] circle_data = [] diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py index db9ae805..2df40247 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py @@ -1,17 +1,11 @@ from datetime import datetime -from typing import NamedTuple, List +from typing import Tuple 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 ( @@ -24,257 +18,349 @@ from vbv_lernwelt.dashboard.tests.graphql.utils import ( create_course, create_course_session, create_user, + create_assignment, + create_assignment_completion, + create_assignment_learning_content, + create_course_session_edoniq_test, + create_course_session_assignment, ) -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) +from vbv_lernwelt.learnpath.models import Circle class AssignmentTestCase(GraphQLTestCase): GRAPHQL_URL = "/server/graphql/" + GRAPHQL_QUERY = f"""query($course_id: ID) {{ + course_dashboard(course_id: $course_id) {{ + assignments{{ + summary{{ + completed_count + average_passed + }} + records{{ + course_session_id + course_session_assignment_id + circle_id + generation + assignment_title + assignment_type_translation_key + details_url + deadline + metrics {{ + passed_count + failed_count + unranked_count + ranking_completed + average_passed + }} + }} + }} + }} + }}""" - 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) + def setUp(self): + self.course, self.course_page = create_course("Test Course") + self.course_session = create_course_session(course=self.course, title=":)") + self.circle, _ = create_circle(title="Circle", course_page=self.course_page) - supervisor = create_user("supervisor") + self.supervisor = create_user("supervisor") add_course_session_user( - course_session=course_session, - user=supervisor, + course_session=self.course_session, + user=self.supervisor, role=CourseSessionUser.Role.SESSION_SUPERVISOR, ) - m1 = create_user("member_1") + self.m1 = create_user("member_1") add_course_session_user( - course_session=course_session, - user=m1, + course_session=self.course_session, + user=self.m1, role=CourseSessionUser.Role.MEMBER, ) - m2 = create_user("member_2") + self.m2 = create_user("member_2") add_course_session_user( - course_session=course_session, - user=m2, + course_session=self.course_session, + user=self.m2, role=CourseSessionUser.Role.MEMBER, ) - m3 = create_user("member_3") + self.m3 = create_user("member_3") add_course_session_user( - course_session=course_session, - user=m3, + course_session=self.course_session, + user=self.m3, role=CourseSessionUser.Role.MEMBER, ) - e1 = create_user("expert_1") + self.e1 = create_user("expert_1") add_course_session_user( - course_session=course_session, - user=e1, + course_session=self.course_session, + user=self.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=[], + self.client.force_login(self.supervisor) + + def test_dashboard_contains_casework(self): + self._test_assignment_type_dashboard_details( + assignment_type=AssignmentType.CASEWORK ) - 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=[], + def test_dashboard_contains_prep_assignments(self): + self._test_assignment_type_dashboard_details( + assignment_type=AssignmentType.PREP_ASSIGNMENT ) - AssignmentCompletion.objects.create( - assignment_user=m1, - assignment=edoniq_test, - evaluation_passed=True, - course_session=course_session, - completion_data={}, + def test_dashboard_contains_edoniq_tests(self): + self._test_assignment_type_dashboard_details( + assignment_type=AssignmentType.EDONIQ_TEST ) - AssignmentCompletion.objects.create( - assignment_user=m1, + def test_dashboard_not_contains_unsupported_types(self): + """ + Since everything is mixed in the same table, we need to make sure + that the dashboard only contains the supported types does not + get confused by the unsupported ones. + """ + + irrelevant_types_for_dashboard = set(AssignmentType) - { + AssignmentType.CASEWORK, + AssignmentType.PREP_ASSIGNMENT, + AssignmentType.EDONIQ_TEST, + } + + for assignment_type in irrelevant_types_for_dashboard: + self._test_assignment_type_not_in_dashboard(assignment_type=assignment_type) + + def _test_assignment_type_dashboard_details(self, assignment_type: AssignmentType): + # GIVEN + assignment, csa = mix_assignment_cocktail( + assignment_type=assignment_type, + deadline_at=datetime(2000, 4, 1), + course_session=self.course_session, + circle=self.circle, + ) + + create_assignment_completion( + user=self.m1, assignment=assignment, - evaluation_passed=True, + course_session=self.course_session, + ) + + # WHEN + response = self.query( + self.GRAPHQL_QUERY, variables={"course_id": str(self.course.id)} + ) + + # THEN + self.assertResponseNoErrors(response) + dashboard = response.json()["data"]["course_dashboard"] + + records = dashboard[0]["assignments"]["records"] + self.assertEqual(len(records), 1) + + record = records[0] + + if isinstance(csa, CourseSessionAssignment): + due_date = csa.submission_deadline + else: + due_date = csa.deadline + + self.assertEqual(record["course_session_id"], str(self.course_session.id)) + self.assertEqual(record["course_session_assignment_id"], str(csa.id)) + self.assertEqual(record["generation"], str(self.course_session.generation)) + self.assertEqual(record["circle_id"], str(self.circle.id)) + self.assertEqual(record["details_url"], due_date.url_expert) + self.assertEqual(datetime.fromisoformat(record["deadline"]), due_date.start) + + self.assertEqual( + record["assignment_title"], + csa.learning_content.content_assignment.title, + ) + self.assertEqual( + record["assignment_type_translation_key"], + due_date.assignment_type_translation_key, + ) + + def _test_assignment_type_not_in_dashboard(self, assignment_type: AssignmentType): + _, csa = mix_assignment_cocktail( + assignment_type=assignment_type, + course_session=self.course_session, + circle=self.circle, + ) + + # WHEN + response = self.query( + self.GRAPHQL_QUERY, variables={"course_id": str(self.course.id)} + ) + + # THEN + self.assertResponseNoErrors(response) + dashboard = response.json()["data"]["course_dashboard"] + + records = dashboard[0]["assignments"]["records"] + self.assertEqual(len(records), 0) + + def test_metrics_summary(self): + # GIVEN + assignment_1, _ = mix_assignment_cocktail( + deadline_at=datetime(1990, 4, 1), + assignment_type=AssignmentType.CASEWORK, + course_session=self.course_session, + circle=self.circle, + ) + + assignment_2, _ = mix_assignment_cocktail( + deadline_at=datetime(2000, 4, 1), + assignment_type=AssignmentType.EDONIQ_TEST, + course_session=self.course_session, + circle=self.circle, + ) + + assignment_3, _ = mix_assignment_cocktail( + deadline_at=datetime(2010, 4, 1), + assignment_type=AssignmentType.PREP_ASSIGNMENT, + course_session=self.course_session, + circle=self.circle, + ) + + # no completions for this assignment yet + assignment_4, _ = mix_assignment_cocktail( + deadline_at=datetime(2020, 4, 1), + assignment_type=AssignmentType.EDONIQ_TEST, + course_session=self.course_session, + circle=self.circle, + ) + + # assignment 1 + assigment_1_results = [ + (self.m1, True), # passed + (self.m2, False), # failed + (self.m3, None), # unranked + ] + + for user, has_passed in assigment_1_results: + if has_passed is None: + continue + create_assignment_completion( + user=user, + assignment=assignment_1, + course_session=self.course_session, + has_passed=has_passed, + ) + + # assignment 2 + assignment_2_results = [ + (self.m1, True), # passed + (self.m2, True), # passed + (self.m3, False), # failed + ] + + for user, has_passed in assignment_2_results: + create_assignment_completion( + user=user, + assignment=assignment_2, + course_session=self.course_session, + has_passed=has_passed, + ) + + # assignment 3 + assignment_3_results = [ + (self.m1, True), # passed + (self.m2, True), # passed + (self.m3, True), # passed + ] + for user, has_passed in assignment_3_results: + create_assignment_completion( + user=user, + assignment=assignment_3, + course_session=self.course_session, + has_passed=has_passed, + ) + + # WHEN + response = self.query( + self.GRAPHQL_QUERY, variables={"course_id": str(self.course.id)} + ) + + # THEN + self.assertResponseNoErrors(response) + dashboard = response.json()["data"]["course_dashboard"] + + # 1 -> incomplete (not counted for average) + # 2 -> complete 66% passed ... + # 3 -> complete 100% passed --> 83.5% + # 4 -> incomplete (not counted for average) + summary = dashboard[0]["assignments"]["summary"] + self.assertEqual(summary["completed_count"], 2) + self.assertEqual(summary["average_passed"], 83.5) + + records = dashboard[0]["assignments"]["records"] + self.assertEqual(len(records), 4) + + # 1 -> assigment_1_results (oldest) + assignment_1_metrics = records[0]["metrics"] + self.assertEqual(assignment_1_metrics["passed_count"], 1) + self.assertEqual(assignment_1_metrics["failed_count"], 1) + self.assertEqual(assignment_1_metrics["unranked_count"], 1) + self.assertEqual(assignment_1_metrics["ranking_completed"], False) + self.assertEqual(assignment_1_metrics["average_passed"], 34) + + # 2 -> assignment_2_results + assignment_2_metrics = records[1]["metrics"] + self.assertEqual(assignment_2_metrics["passed_count"], 2) + self.assertEqual(assignment_2_metrics["failed_count"], 1) + self.assertEqual(assignment_2_metrics["unranked_count"], 0) + self.assertEqual(assignment_2_metrics["ranking_completed"], True) + self.assertEqual(assignment_2_metrics["average_passed"], 67) + + # 3 -> assignment_3_results + assignment_3_metrics = records[2]["metrics"] + self.assertEqual(assignment_3_metrics["passed_count"], 3) + self.assertEqual(assignment_3_metrics["failed_count"], 0) + self.assertEqual(assignment_3_metrics["unranked_count"], 0) + self.assertEqual(assignment_3_metrics["ranking_completed"], True) + self.assertEqual(assignment_3_metrics["average_passed"], 100) + + # 4 -> no completions (newest) + assignment_4_metrics = records[3]["metrics"] + self.assertEqual(assignment_4_metrics["passed_count"], 0) + self.assertEqual(assignment_4_metrics["failed_count"], 0) + self.assertEqual(assignment_4_metrics["unranked_count"], 3) + self.assertEqual(assignment_4_metrics["ranking_completed"], False) + self.assertEqual(assignment_4_metrics["average_passed"], 0) + + +def mix_assignment_cocktail( + assignment_type: AssignmentType, + course_session: CourseSession, + circle: Circle, + deadline_at: datetime | None = None, +) -> Tuple[Assignment, CourseSessionAssignment | CourseSessionEdoniqTest]: + """ + Little test helper to create a course session assignment or edoniq test based + on the given assignment type. + """ + + assignment = create_assignment( + course=course_session.course, assignment_type=assignment_type + ) + + if assignment_type == AssignmentType.EDONIQ_TEST: + cset = create_course_session_edoniq_test( + deadline_at=deadline_at, course_session=course_session, - completion_data={}, + learning_content_edoniq_test=create_assignment_learning_content( + circle=circle, + assignment=assignment, + ), ) - - learning_content_assignment = LearningContentAssignmentFactory( - title="Learning Content Assignment Title", - parent=circle, - content_assignment=assignment, + return assignment, cset + else: + csa = create_course_session_assignment( + deadline_at=deadline_at, + course_session=course_session, + learning_content_assignment=create_assignment_learning_content( + circle=circle, + 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) + return assignment, csa diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/utils.py b/server/vbv_lernwelt/dashboard/tests/graphql/utils.py index 374d513f..e67621e5 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/utils.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/utils.py @@ -4,6 +4,16 @@ from typing import List, Tuple from django.contrib.auth.hashers import make_password from django.utils import timezone +from vbv_lernwelt.assignment.models import ( + AssignmentType, + AssignmentCompletion, + Assignment, + AssignmentCompletionStatus, +) +from vbv_lernwelt.assignment.tests.assignment_factories import ( + AssignmentFactory, + AssignmentListPageFactory, +) from vbv_lernwelt.competence.factories import ( ActionCompetenceFactory, ActionCompetenceListPageFactory, @@ -21,15 +31,26 @@ from vbv_lernwelt.course.models import ( CourseSessionUser, ) from vbv_lernwelt.course.utils import get_wagtail_default_site -from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse +from vbv_lernwelt.course_session.models import ( + CourseSessionAttendanceCourse, + CourseSessionAssignment, + CourseSessionEdoniqTest, +) from vbv_lernwelt.duedate.models import DueDate -from vbv_lernwelt.learnpath.models import Circle, LearningPath +from vbv_lernwelt.learnpath.models import ( + Circle, + LearningPath, + LearningContentAssignment, + LearningContentEdoniqTest, +) from vbv_lernwelt.learnpath.tests.learning_path_factories import ( CircleFactory, LearningContentAttendanceCourseFactory, LearningPathFactory, LearningUnitFactory, TopicFactory, + LearningContentAssignmentFactory, + LearningContentEdoniqTestFactory, ) @@ -116,6 +137,92 @@ def create_attendance_course( ) +def create_assignment( + course: Course, + assignment_type: AssignmentType, +) -> Assignment: + return AssignmentFactory( + parent=AssignmentListPageFactory( + parent=course.coursepage, + ), + assignment_type=assignment_type.name, + title=f"Dummy Assignment ({assignment_type.name})", + effort_required=":)", + intro_text=":)", + performance_objectives=[], + ) + + +def create_assignment_completion( + user: User, + assignment: Assignment, + course_session: CourseSession, + has_passed: bool | None = None, +) -> AssignmentCompletion: + return AssignmentCompletion.objects.create( + completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value, + assignment_user=user, + assignment=assignment, + evaluation_passed=has_passed, + course_session=course_session, + completion_data={}, + ) + + +def create_assignment_learning_content( + circle: Circle, + assignment: Assignment, +) -> LearningContentAssignment | LearningContentEdoniqTest: + if AssignmentType(assignment.assignment_type) == AssignmentType.EDONIQ_TEST: + return LearningContentEdoniqTestFactory( + title="Learning Content (EDONIQ_TEST)", + parent=circle, + content_assignment=assignment, + ) + + return LearningContentAssignmentFactory( + title=f"Learning Content ({assignment.assignment_type})", + parent=circle, + content_assignment=assignment, + ) + + +def create_course_session_assignment( + course_session: CourseSession, + learning_content_assignment: LearningContentAssignment, + deadline_at: datetime | None = None, +) -> CourseSessionAssignment: + cas = CourseSessionAssignment.objects.create( + course_session=course_session, + learning_content=learning_content_assignment, + ) + + if deadline_at: + # the save on the course_session_assignment already sets a lot + # of due date fields, so it's easier to just overwrite the this + cas.submission_deadline.start = timezone.make_aware(deadline_at) + cas.submission_deadline.save() + + return cas + + +def create_course_session_edoniq_test( + course_session: CourseSession, + learning_content_edoniq_test: LearningContentEdoniqTest, + deadline_at: datetime, +) -> CourseSessionEdoniqTest: + cset = CourseSessionEdoniqTest.objects.create( + course_session=course_session, + learning_content=learning_content_edoniq_test, + ) + + # same as above (see create_course_session_assignment) + cset.deadline.start = timezone.make_aware(deadline_at) + cset.deadline.save() + + return cset + + def create_performance_criteria_page( course: Course, course_page: CoursePage, circle: Circle ) -> PerformanceCriteria: