feat: adds graphql assignment dashboard
This commit is contained in:
parent
d16bb59392
commit
dc706e7ece
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue