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
|
import math
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from vbv_lernwelt.core.models import User
|
from vbv_lernwelt.core.models import User
|
||||||
from vbv_lernwelt.course.models import CourseSessionUser
|
from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
|
|
@ -38,7 +38,7 @@ def attendance_day_presences(
|
||||||
course_session__course_id=course_id,
|
course_session__course_id=course_id,
|
||||||
course_session__coursesessionuser__user=user,
|
course_session__coursesessionuser__user=user,
|
||||||
course_session__coursesessionuser__role=CourseSessionUser.Role.SESSION_SUPERVISOR,
|
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")
|
).order_by("-due_date__end")
|
||||||
|
|
||||||
records = []
|
records = []
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
import graphene
|
import graphene
|
||||||
|
|
||||||
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
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 (
|
from vbv_lernwelt.dashboard.graphql.types.attendance import (
|
||||||
attendance_day_presences,
|
attendance_day_presences,
|
||||||
AttendanceDayPresences,
|
AttendanceDayPresences,
|
||||||
|
|
@ -36,6 +40,7 @@ class CourseDashboardType(graphene.ObjectType):
|
||||||
course_session_properties = graphene.Field(CourseSessionProperties)
|
course_session_properties = graphene.Field(CourseSessionProperties)
|
||||||
attendance_day_presences = graphene.Field(AttendanceDayPresences)
|
attendance_day_presences = graphene.Field(AttendanceDayPresences)
|
||||||
feedback_responses = graphene.Field(FeedbackResponses)
|
feedback_responses = graphene.Field(FeedbackResponses)
|
||||||
|
assignments = graphene.Field(Assignments)
|
||||||
competences = graphene.Field(Competences)
|
competences = graphene.Field(Competences)
|
||||||
|
|
||||||
def resolve_attendance_day_presences(root, info) -> AttendanceDayPresences:
|
def resolve_attendance_day_presences(root, info) -> AttendanceDayPresences:
|
||||||
|
|
@ -47,6 +52,9 @@ class CourseDashboardType(graphene.ObjectType):
|
||||||
def resolve_competences(root, info) -> Competences:
|
def resolve_competences(root, info) -> Competences:
|
||||||
return competences(root.course_id, info.context.user)
|
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):
|
def resolve_course_session_properties(root, info):
|
||||||
course_session_data = []
|
course_session_data = []
|
||||||
circle_data = []
|
circle_data = []
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,11 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import NamedTuple, List
|
from typing import Tuple
|
||||||
|
|
||||||
from graphene_django.utils import GraphQLTestCase
|
from graphene_django.utils import GraphQLTestCase
|
||||||
|
|
||||||
from vbv_lernwelt.assignment.models import (
|
from vbv_lernwelt.assignment.models import (
|
||||||
AssignmentType,
|
AssignmentType,
|
||||||
AssignmentCompletion,
|
|
||||||
Assignment,
|
Assignment,
|
||||||
AssignmentCompletionStatus,
|
|
||||||
)
|
|
||||||
from vbv_lernwelt.assignment.tests.assignment_factories import (
|
|
||||||
AssignmentFactory,
|
|
||||||
AssignmentListPageFactory,
|
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.course.models import CourseSessionUser, CourseSession
|
from vbv_lernwelt.course.models import CourseSessionUser, CourseSession
|
||||||
from vbv_lernwelt.course_session.models import (
|
from vbv_lernwelt.course_session.models import (
|
||||||
|
|
@ -24,257 +18,349 @@ from vbv_lernwelt.dashboard.tests.graphql.utils import (
|
||||||
create_course,
|
create_course,
|
||||||
create_course_session,
|
create_course_session,
|
||||||
create_user,
|
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 (
|
from vbv_lernwelt.learnpath.models import Circle
|
||||||
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):
|
class AssignmentTestCase(GraphQLTestCase):
|
||||||
GRAPHQL_URL = "/server/graphql/"
|
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):
|
def setUp(self):
|
||||||
# GIVEN
|
self.course, self.course_page = create_course("Test Course")
|
||||||
course, course_page = create_course("Test Course")
|
self.course_session = create_course_session(course=self.course, title=":)")
|
||||||
course_session = create_course_session(course=course, title="Test Bern 2022 a")
|
self.circle, _ = create_circle(title="Circle", course_page=self.course_page)
|
||||||
circle, _ = create_circle(title="Test Circle", course_page=course_page)
|
|
||||||
|
|
||||||
supervisor = create_user("supervisor")
|
self.supervisor = create_user("supervisor")
|
||||||
add_course_session_user(
|
add_course_session_user(
|
||||||
course_session=course_session,
|
course_session=self.course_session,
|
||||||
user=supervisor,
|
user=self.supervisor,
|
||||||
role=CourseSessionUser.Role.SESSION_SUPERVISOR,
|
role=CourseSessionUser.Role.SESSION_SUPERVISOR,
|
||||||
)
|
)
|
||||||
|
|
||||||
m1 = create_user("member_1")
|
self.m1 = create_user("member_1")
|
||||||
add_course_session_user(
|
add_course_session_user(
|
||||||
course_session=course_session,
|
course_session=self.course_session,
|
||||||
user=m1,
|
user=self.m1,
|
||||||
role=CourseSessionUser.Role.MEMBER,
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
)
|
)
|
||||||
|
|
||||||
m2 = create_user("member_2")
|
self.m2 = create_user("member_2")
|
||||||
add_course_session_user(
|
add_course_session_user(
|
||||||
course_session=course_session,
|
course_session=self.course_session,
|
||||||
user=m2,
|
user=self.m2,
|
||||||
role=CourseSessionUser.Role.MEMBER,
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
)
|
)
|
||||||
|
|
||||||
m3 = create_user("member_3")
|
self.m3 = create_user("member_3")
|
||||||
add_course_session_user(
|
add_course_session_user(
|
||||||
course_session=course_session,
|
course_session=self.course_session,
|
||||||
user=m3,
|
user=self.m3,
|
||||||
role=CourseSessionUser.Role.MEMBER,
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
)
|
)
|
||||||
|
|
||||||
e1 = create_user("expert_1")
|
self.e1 = create_user("expert_1")
|
||||||
add_course_session_user(
|
add_course_session_user(
|
||||||
course_session=course_session,
|
course_session=self.course_session,
|
||||||
user=e1,
|
user=self.e1,
|
||||||
role=CourseSessionUser.Role.EXPERT,
|
role=CourseSessionUser.Role.EXPERT,
|
||||||
)
|
)
|
||||||
|
|
||||||
assignment = AssignmentFactory(
|
self.client.force_login(self.supervisor)
|
||||||
parent=AssignmentListPageFactory(
|
|
||||||
parent=course.coursepage,
|
def test_dashboard_contains_casework(self):
|
||||||
),
|
self._test_assignment_type_dashboard_details(
|
||||||
assignment_type=AssignmentType.CASEWORK.name,
|
assignment_type=AssignmentType.CASEWORK
|
||||||
title="Test Assignment",
|
|
||||||
effort_required="However long it takes",
|
|
||||||
intro_text="Assignment Intro Text",
|
|
||||||
performance_objectives=[],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
edoniq_test = AssignmentFactory(
|
def test_dashboard_contains_prep_assignments(self):
|
||||||
parent=AssignmentListPageFactory(
|
self._test_assignment_type_dashboard_details(
|
||||||
parent=course.coursepage,
|
assignment_type=AssignmentType.PREP_ASSIGNMENT
|
||||||
),
|
|
||||||
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(
|
def test_dashboard_contains_edoniq_tests(self):
|
||||||
assignment_user=m1,
|
self._test_assignment_type_dashboard_details(
|
||||||
assignment=edoniq_test,
|
assignment_type=AssignmentType.EDONIQ_TEST
|
||||||
evaluation_passed=True,
|
|
||||||
course_session=course_session,
|
|
||||||
completion_data={},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
AssignmentCompletion.objects.create(
|
def test_dashboard_not_contains_unsupported_types(self):
|
||||||
assignment_user=m1,
|
"""
|
||||||
|
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,
|
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,
|
course_session=course_session,
|
||||||
completion_data={},
|
learning_content_edoniq_test=create_assignment_learning_content(
|
||||||
|
circle=circle,
|
||||||
|
assignment=assignment,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
return assignment, cset
|
||||||
learning_content_assignment = LearningContentAssignmentFactory(
|
else:
|
||||||
title="Learning Content Assignment Title",
|
csa = create_course_session_assignment(
|
||||||
parent=circle,
|
deadline_at=deadline_at,
|
||||||
content_assignment=assignment,
|
course_session=course_session,
|
||||||
|
learning_content_assignment=create_assignment_learning_content(
|
||||||
|
circle=circle,
|
||||||
|
assignment=assignment,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
return assignment, csa
|
||||||
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)
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,16 @@ from typing import List, Tuple
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
from django.utils import timezone
|
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 (
|
from vbv_lernwelt.competence.factories import (
|
||||||
ActionCompetenceFactory,
|
ActionCompetenceFactory,
|
||||||
ActionCompetenceListPageFactory,
|
ActionCompetenceListPageFactory,
|
||||||
|
|
@ -21,15 +31,26 @@ from vbv_lernwelt.course.models import (
|
||||||
CourseSessionUser,
|
CourseSessionUser,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.course.utils import get_wagtail_default_site
|
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.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 (
|
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
||||||
CircleFactory,
|
CircleFactory,
|
||||||
LearningContentAttendanceCourseFactory,
|
LearningContentAttendanceCourseFactory,
|
||||||
LearningPathFactory,
|
LearningPathFactory,
|
||||||
LearningUnitFactory,
|
LearningUnitFactory,
|
||||||
TopicFactory,
|
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(
|
def create_performance_criteria_page(
|
||||||
course: Course, course_page: CoursePage, circle: Circle
|
course: Course, course_page: CoursePage, circle: Circle
|
||||||
) -> PerformanceCriteria:
|
) -> PerformanceCriteria:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue