VBV-488: Verarbeite Edoniq Test-Resultate
This commit is contained in:
parent
4123e15f22
commit
7d291b3e3f
|
|
@ -4,5 +4,8 @@
|
||||||
# Run every 6 hours
|
# Run every 6 hours
|
||||||
0 */6 * * * /usr/local/bin/python /app/manage.py simple_dummy_job
|
0 */6 * * * /usr/local/bin/python /app/manage.py simple_dummy_job
|
||||||
|
|
||||||
|
# Run every hour
|
||||||
|
0 */1 * * * /usr/local/bin/python /app/manage.py edoniq_import_results
|
||||||
|
|
||||||
# every day at 19:30
|
# every day at 19:30
|
||||||
30 19 * * * /usr/local/bin/python /app/manage.py send_attendance_course_reminders
|
30 19 * * * /usr/local/bin/python /app/manage.py send_attendance_course_reminders
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,7 @@ LOCAL_APPS = [
|
||||||
"vbv_lernwelt.assignment",
|
"vbv_lernwelt.assignment",
|
||||||
"vbv_lernwelt.duedate",
|
"vbv_lernwelt.duedate",
|
||||||
"vbv_lernwelt.importer",
|
"vbv_lernwelt.importer",
|
||||||
|
"vbv_lernwelt.edoniq_test",
|
||||||
]
|
]
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
|
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
|
||||||
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ class AssignmentCompletionMutation(graphene.Mutation):
|
||||||
"evaluation_passed": evaluation_passed,
|
"evaluation_passed": evaluation_passed,
|
||||||
}
|
}
|
||||||
|
|
||||||
ac = update_assignment_completion(
|
ac, created = update_assignment_completion(
|
||||||
copy_task_data=False,
|
copy_task_data=False,
|
||||||
initialize_completion=initialize_completion,
|
initialize_completion=initialize_completion,
|
||||||
**assignment_data,
|
**assignment_data,
|
||||||
|
|
@ -116,6 +116,7 @@ class AssignmentCompletionMutation(graphene.Mutation):
|
||||||
assignment_user_id=assignment_user_id,
|
assignment_user_id=assignment_user_id,
|
||||||
course_session_id=course_session_id,
|
course_session_id=course_session_id,
|
||||||
completion_status=completion_status,
|
completion_status=completion_status,
|
||||||
|
created=created,
|
||||||
)
|
)
|
||||||
|
|
||||||
return AssignmentCompletionMutation(assignment_completion=ac)
|
return AssignmentCompletionMutation(assignment_completion=ac)
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,13 @@ def update_assignment_completion(
|
||||||
evaluation_user: User | None = None,
|
evaluation_user: User | None = None,
|
||||||
evaluation_points: float | None = None,
|
evaluation_points: float | None = None,
|
||||||
evaluation_passed: bool | None = None,
|
evaluation_passed: bool | None = None,
|
||||||
|
evaluation_max_points: float | None = None,
|
||||||
validate_completion_status_change: bool = True,
|
validate_completion_status_change: bool = True,
|
||||||
copy_task_data: bool = False,
|
copy_task_data: bool = False,
|
||||||
initialize_completion: bool = False,
|
initialize_completion: bool = False,
|
||||||
) -> AssignmentCompletion:
|
additional_json_data: dict | None = None,
|
||||||
|
validate_submission_update: bool = True,
|
||||||
|
) -> tuple[AssignmentCompletion, bool]:
|
||||||
"""
|
"""
|
||||||
:param completion_data: should have the following structure:
|
:param completion_data: should have the following structure:
|
||||||
{
|
{
|
||||||
|
|
@ -56,6 +59,9 @@ def update_assignment_completion(
|
||||||
if completion_data is None:
|
if completion_data is None:
|
||||||
completion_data = {}
|
completion_data = {}
|
||||||
|
|
||||||
|
if additional_json_data is None:
|
||||||
|
additional_json_data = {}
|
||||||
|
|
||||||
ac, created = AssignmentCompletion.objects.get_or_create(
|
ac, created = AssignmentCompletion.objects.get_or_create(
|
||||||
assignment_user_id=assignment_user.id,
|
assignment_user_id=assignment_user.id,
|
||||||
assignment_id=assignment.id,
|
assignment_id=assignment.id,
|
||||||
|
|
@ -78,17 +84,22 @@ def update_assignment_completion(
|
||||||
|
|
||||||
if validate_completion_status_change:
|
if validate_completion_status_change:
|
||||||
if completion_status == AssignmentCompletionStatus.SUBMITTED:
|
if completion_status == AssignmentCompletionStatus.SUBMITTED:
|
||||||
if ac.completion_status in [
|
completion_status_to_check = [
|
||||||
"SUBMITTED",
|
|
||||||
"EVALUATION_IN_PROGRESS",
|
"EVALUATION_IN_PROGRESS",
|
||||||
"EVALUATION_SUBMITTED",
|
"EVALUATION_SUBMITTED",
|
||||||
]:
|
]
|
||||||
|
if validate_submission_update:
|
||||||
|
completion_status_to_check.append("SUBMITTED")
|
||||||
|
if ac.completion_status in completion_status_to_check:
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
{
|
{
|
||||||
"completion_status": f"Cannot update completion status from {ac.completion_status} to SUBMITTED"
|
"completion_status": f"Cannot update completion status from {ac.completion_status} to SUBMITTED"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED:
|
elif (
|
||||||
|
completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED
|
||||||
|
and validate_submission_update
|
||||||
|
):
|
||||||
if ac.completion_status == "EVALUATION_SUBMITTED":
|
if ac.completion_status == "EVALUATION_SUBMITTED":
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
{
|
{
|
||||||
|
|
@ -126,14 +137,20 @@ def update_assignment_completion(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
ac.evaluation_max_points = assignment.get_max_points()
|
if evaluation_max_points is None:
|
||||||
|
ac.evaluation_max_points = assignment.get_max_points()
|
||||||
|
else:
|
||||||
|
ac.evaluation_max_points = evaluation_max_points
|
||||||
ac.evaluation_points = evaluation_points
|
ac.evaluation_points = evaluation_points
|
||||||
|
|
||||||
# if no evaluation_passed is provided, we calculate it from the points
|
# if no evaluation_passed is provided, we calculate it from the points
|
||||||
if evaluation_passed is None and evaluation_points is not None:
|
if evaluation_passed is None:
|
||||||
# if more or equal than 60% of the points are reached, the assignment is passed
|
if evaluation_points is not None and ac.evaluation_max_points is not None:
|
||||||
# TODO: check with VBV if this is correct
|
# if more or equal than 60% of the points are reached, the assignment is passed
|
||||||
ac.evaluation_passed = (evaluation_points / ac.evaluation_max_points) >= 0.6
|
# TODO: check with VBV if this is correct
|
||||||
|
ac.evaluation_passed = (evaluation_points / ac.evaluation_max_points) >= 0.6
|
||||||
|
else:
|
||||||
|
ac.evaluation_passed = evaluation_passed
|
||||||
|
|
||||||
if completion_status == AssignmentCompletionStatus.SUBMITTED:
|
if completion_status == AssignmentCompletionStatus.SUBMITTED:
|
||||||
ac.submitted_at = timezone.now()
|
ac.submitted_at = timezone.now()
|
||||||
|
|
@ -160,6 +177,7 @@ def update_assignment_completion(
|
||||||
)
|
)
|
||||||
|
|
||||||
ac.completion_status = completion_status.value
|
ac.completion_status = completion_status.value
|
||||||
|
ac.additional_json_data = ac.additional_json_data | additional_json_data
|
||||||
|
|
||||||
# TODO: make more validation of the provided input -> maybe with graphql
|
# TODO: make more validation of the provided input -> maybe with graphql
|
||||||
completion_data = _remove_unknown_entries(assignment, completion_data)
|
completion_data = _remove_unknown_entries(assignment, completion_data)
|
||||||
|
|
@ -225,7 +243,7 @@ def update_assignment_completion(
|
||||||
completion_status=CourseCompletionStatus.SUCCESS.value,
|
completion_status=CourseCompletionStatus.SUCCESS.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ac
|
return (ac, created)
|
||||||
|
|
||||||
|
|
||||||
def _remove_unknown_entries(assignment, completion_data):
|
def _remove_unknown_entries(assignment, completion_data):
|
||||||
|
|
|
||||||
|
|
@ -471,7 +471,11 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.
|
||||||
),
|
),
|
||||||
checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden und die Redlichkeitserklärung akzeptiert habe.",
|
checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden und die Redlichkeitserklärung akzeptiert habe.",
|
||||||
test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
|
test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
|
||||||
|
edoniq_course_release_id="1689096897473",
|
||||||
|
edoniq_sequence_id="1688059890497",
|
||||||
extended_time_test_url="https://exam2.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
|
extended_time_test_url="https://exam2.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
|
||||||
|
edoniq_extended_course_release_id="1689096897473",
|
||||||
|
edoniq_extended_sequence_id="1691151920116",
|
||||||
)
|
)
|
||||||
|
|
||||||
LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end")
|
LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end")
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class LearnpathConfig(AppConfig):
|
class EdoniqTestConfig(AppConfig):
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
name = "vbv_lernwelt.edoniq_test"
|
name = "vbv_lernwelt.edoniq_test"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
import structlog
|
||||||
|
|
||||||
|
from vbv_lernwelt.core.base import LoggedCommand
|
||||||
|
from vbv_lernwelt.edoniq_test.result_import.edoniq_sftp_connection import (
|
||||||
|
load_edoniq_test_results_csv_data,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.edoniq_test.result_import.services import (
|
||||||
|
create_edoniq_csv_test_result,
|
||||||
|
filter_relevant_rows,
|
||||||
|
upsert_edoniq_test_result,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(LoggedCommand):
|
||||||
|
help = "Imports Test Results from Edoniq"
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
logger.info(
|
||||||
|
"edoniq_import_results job started",
|
||||||
|
label="edoniq_import_results",
|
||||||
|
)
|
||||||
|
|
||||||
|
stat_results = {
|
||||||
|
"num_created": 0,
|
||||||
|
"num_updated": 0,
|
||||||
|
"num_error": 0,
|
||||||
|
"num_passed": 0,
|
||||||
|
"num_failed": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
csv_data = load_edoniq_test_results_csv_data()
|
||||||
|
relevant_rows = filter_relevant_rows(csv_data)
|
||||||
|
stat_results["num_relevant_edoniq_csv_rows"] = len(relevant_rows)
|
||||||
|
|
||||||
|
for row in relevant_rows:
|
||||||
|
result_row = create_edoniq_csv_test_result(row)
|
||||||
|
assignment_completion, created = upsert_edoniq_test_result(
|
||||||
|
result_row, fail_silently=True
|
||||||
|
)
|
||||||
|
if assignment_completion:
|
||||||
|
if created:
|
||||||
|
stat_results["num_created"] += 1
|
||||||
|
else:
|
||||||
|
stat_results["num_updated"] += 1
|
||||||
|
if assignment_completion.evaluation_passed:
|
||||||
|
stat_results["num_passed"] += 1
|
||||||
|
else:
|
||||||
|
stat_results["num_failed"] += 1
|
||||||
|
else:
|
||||||
|
stat_results["num_error"] += 1
|
||||||
|
|
||||||
|
self.job_log.json_data = stat_results
|
||||||
|
self.job_log.save()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"edoniq_import_results job finished",
|
||||||
|
label="edoniq_import_results",
|
||||||
|
stat_results=stat_results,
|
||||||
|
)
|
||||||
|
|
@ -19,7 +19,7 @@ def _create_edoniq_sftp_client():
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"Environment variable not set or empty",
|
"Environment variable not set or empty",
|
||||||
env_var=private_key_env_var_name,
|
env_var=private_key_env_var_name,
|
||||||
label="edoniq_import",
|
label="edoniq_import_results",
|
||||||
)
|
)
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Environment variable {private_key_env_var_name} not set or empty."
|
f"Environment variable {private_key_env_var_name} not set or empty."
|
||||||
|
|
@ -59,5 +59,7 @@ if __name__ == "__main__":
|
||||||
csv_data = load_edoniq_test_results_csv_data()
|
csv_data = load_edoniq_test_results_csv_data()
|
||||||
csv_reader = csv.reader(StringIO(csv_data), delimiter=";")
|
csv_reader = csv.reader(StringIO(csv_data), delimiter=";")
|
||||||
for i, row in enumerate(csv_reader):
|
for i, row in enumerate(csv_reader):
|
||||||
if len(row) > 4 and row[4] == "AG 2023 A" and row[5] == "de_üK1_BA_KN":
|
# if len(row) > 4 and row[4] == "AG 2023 A" and row[5] == "de_üK1_BA_KN":
|
||||||
|
# print(row)
|
||||||
|
if i < 10:
|
||||||
print(row)
|
print(row)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
import csv
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
import structlog
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from vbv_lernwelt.assignment.models import AssignmentCompletionStatus
|
||||||
|
from vbv_lernwelt.assignment.services import update_assignment_completion
|
||||||
|
from vbv_lernwelt.core.models import User
|
||||||
|
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
||||||
|
from vbv_lernwelt.learnpath.models import LearningContentEdoniqTest
|
||||||
|
|
||||||
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_distinct_sequence_ids():
|
||||||
|
distinct_sequence_ids = LearningContentEdoniqTest.objects.values_list(
|
||||||
|
"edoniq_sequence_id", flat=True
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
distinct_sequence_extended_ids = LearningContentEdoniqTest.objects.values_list(
|
||||||
|
"edoniq_extended_sequence_id", flat=True
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
# filter out empty values
|
||||||
|
return {
|
||||||
|
seq_id
|
||||||
|
for seq_id in list(distinct_sequence_ids) + list(distinct_sequence_extended_ids)
|
||||||
|
if seq_id
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def filter_relevant_rows(csv_data):
|
||||||
|
sequence_ids = get_distinct_sequence_ids()
|
||||||
|
csv_reader = csv.reader(StringIO(csv_data), delimiter=";")
|
||||||
|
|
||||||
|
# read headers
|
||||||
|
headers = next(csv_reader)
|
||||||
|
headers = [header.strip() for header in headers]
|
||||||
|
|
||||||
|
# Read the rest of the lines and create a list of dictionaries
|
||||||
|
records = []
|
||||||
|
for row in csv_reader:
|
||||||
|
# strip all whitespace around the column entries
|
||||||
|
row = [item.strip() for item in row]
|
||||||
|
records.append(dict(zip(headers, row)))
|
||||||
|
|
||||||
|
relevant_rows = [row for row in records if row.get("Sequenz ID") in sequence_ids]
|
||||||
|
|
||||||
|
return relevant_rows
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EdoniqCsvTestResult:
|
||||||
|
user_id: str
|
||||||
|
user_points: float
|
||||||
|
max_points: float
|
||||||
|
passed: bool
|
||||||
|
edoniq_sequence_id: str
|
||||||
|
unfinished: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
def create_edoniq_csv_test_result(edoniq_csv_row_dict) -> EdoniqCsvTestResult:
|
||||||
|
user_id = edoniq_csv_row_dict["Benutzer Login"]
|
||||||
|
user_points = float(edoniq_csv_row_dict["erreichte Punkte absolut"])
|
||||||
|
max_points = float(edoniq_csv_row_dict["erreichbares Punktetotal absolut"])
|
||||||
|
passed = edoniq_csv_row_dict["Bestanden-/Nicht-Bestanden Status"] == "Bestanden"
|
||||||
|
unfinished = edoniq_csv_row_dict["Bestanden-/Nicht-Bestanden Status"] == "Begonnen"
|
||||||
|
edoniq_sequence_id = edoniq_csv_row_dict["Sequenz ID"]
|
||||||
|
|
||||||
|
return EdoniqCsvTestResult(
|
||||||
|
user_id=user_id,
|
||||||
|
user_points=user_points,
|
||||||
|
max_points=max_points,
|
||||||
|
passed=passed,
|
||||||
|
edoniq_sequence_id=edoniq_sequence_id,
|
||||||
|
unfinished=unfinished,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def upsert_edoniq_test_result(
|
||||||
|
edoniq_csv_test_result: EdoniqCsvTestResult, fail_silently=False
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
u = User.objects.get(id=edoniq_csv_test_result.user_id)
|
||||||
|
|
||||||
|
# because the same edoniq test is used in multiple languages,
|
||||||
|
# we have to consider multiple learning contents
|
||||||
|
learning_content_qs = LearningContentEdoniqTest.objects.filter(
|
||||||
|
Q(edoniq_sequence_id=edoniq_csv_test_result.edoniq_sequence_id)
|
||||||
|
| Q(edoniq_extended_sequence_id=edoniq_csv_test_result.edoniq_sequence_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
user_course_session_ids = set(
|
||||||
|
CourseSessionUser.objects.filter(
|
||||||
|
user_id=u.id,
|
||||||
|
course_session__course_id__in=[
|
||||||
|
lc.get_course().id for lc in learning_content_qs
|
||||||
|
],
|
||||||
|
).values_list("course_session__id", flat=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
for course_session_id in user_course_session_ids:
|
||||||
|
course_session = CourseSession.objects.get(id=course_session_id)
|
||||||
|
|
||||||
|
# find the assignment which is attached to this language course
|
||||||
|
assignment = (
|
||||||
|
learning_content_qs.descendant_of(course_session.course.coursepage)
|
||||||
|
.first()
|
||||||
|
.content_assignment
|
||||||
|
)
|
||||||
|
|
||||||
|
completion_status = AssignmentCompletionStatus.EVALUATION_SUBMITTED
|
||||||
|
if edoniq_csv_test_result.unfinished:
|
||||||
|
completion_status = AssignmentCompletionStatus.SUBMITTED
|
||||||
|
|
||||||
|
return update_assignment_completion(
|
||||||
|
assignment_user=u,
|
||||||
|
assignment=assignment,
|
||||||
|
course_session=course_session,
|
||||||
|
learning_content_page=assignment.find_attached_learning_content(),
|
||||||
|
completion_data={},
|
||||||
|
completion_status=completion_status,
|
||||||
|
evaluation_user=User.objects.get(username="admin"),
|
||||||
|
evaluation_points=edoniq_csv_test_result.user_points,
|
||||||
|
evaluation_passed=edoniq_csv_test_result.passed,
|
||||||
|
evaluation_max_points=edoniq_csv_test_result.max_points,
|
||||||
|
additional_json_data={
|
||||||
|
"edoniq_sequence_id": edoniq_csv_test_result.edoniq_sequence_id,
|
||||||
|
},
|
||||||
|
validate_submission_update=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
"edoniq_test_result_import_fail",
|
||||||
|
label="edoniq_import_results",
|
||||||
|
error="user or learning_content not found",
|
||||||
|
user_id=edoniq_csv_test_result.user_id,
|
||||||
|
sequence_id=edoniq_csv_test_result.edoniq_sequence_id,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
if not fail_silently:
|
||||||
|
raise e
|
||||||
|
return None, False
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
Benutzer Login;Benutzer Name;Benutzer Vorname;Korrespondenzsprache;OE;Sequenztitel;Sequenz ID;letztes Datum der Durchführung des Kandidaten;Zeitstempel Beginn;Bearbeitungsdauer;erreichte Punkte absolut ; erreichbares Punktetotal absolut;erreichte Punkte;Bestehensgrenze prozentual;Bestanden-/Nicht-Bestanden Status;Note;prozentualer Durchschnitt der Versuche dieses Kandidaten;prozentualer Durchschnitt aller Kandidaten
|
Benutzer Login;Benutzer Name;Benutzer Vorname;Korrespondenzsprache;OE;Sequenztitel;Sequenz ID;letztes Datum der Durchführung des Kandidaten;Zeitstempel Beginn;Bearbeitungsdauer;erreichte Punkte absolut ; erreichbares Punktetotal absolut;erreichte Punkte;Bestehensgrenze prozentual;Bestanden-/Nicht-Bestanden Status;Note;prozentualer Durchschnitt der Versuche dieses Kandidaten;prozentualer Durchschnitt aller Kandidaten
|
||||||
;
|
;
|
||||||
cc36ad95-d09c-4187-b1d4-689ada3ef497;Sommer;Diego;de;AG 2023 A;de_üK1_BA_KN;1688059890497;06.09.2023;06.09.2023 18:48;00:17:10.4650;21.25 ; 29;73.28;60;Bestanden;;73.28;65.91
|
65c73ad0-6d53-43a9-a4a4-64143f27b03a;Student1;Test;de;Test Bern 2022 a;de_üK1_BA_KN;1688059890497;06.09.2023;06.09.2023 18:48;00:17:10.4650;21.25 ; 29;73.28;60;Bestanden;;73.28;65.91
|
||||||
58fdb8e2-881d-48fd-8df9-05f31586e909;Demirel;Beyzanur;de;AG 2023 A;de_üK1_BA_KN;1688059890497;07.09.2023;07.09.2023 19:00;00:17:49.8550;12.23 ; 29;42.16;60;Nicht bestanden;;42.16;65.91
|
19c40d94-15cc-4198-aaad-ef707c4b0900;Student2;Test;de;Test Bern 2022 a;de_üK1_BA_KN;1688059890497;07.09.2023;07.09.2023 19:00;00:17:49.8550;12.23 ; 29;42.16;60;Nicht bestanden;;42.16;65.91
|
||||||
19d4e374-7300-45cc-a420-bebe184c5c4e;Appert;Mascha;de;AG 2023 A;de_üK1_BA_KN;1688059890497;10.09.2023;10.09.2023 10:48;00:18:22.9710;17.62 ; 29;60.76;60;Bestanden;;60.76;65.91
|
bcf94dba-53bc-474b-a22d-e4af39aa042b;Student3;Test;de;Test Bern 2022 a;de_üK1_BA_KN;1691151920116;10.09.2023;10.09.2023 10:48;00:18:22.9710;0.00 ; 29;60.76;60;Begonnen;;60.76;65.91
|
||||||
29610d7c-ed48-43b8-ab71-ce0e726aada4;Gianformaggio;Ylaria;de;AG 2023 A;de_üK1_KO_Testlauf;1686325621487;10.09.2023;10.09.2023 16:32;00:00:00.0000;0.00 ; 15;0.00;60;Begonnen;;38.11;55.30
|
bcf94dba-53bc-474b-a22d-e4af39aa042b;Student3;Test;de;Test Bern 2022 a;AG 2023 A;de_üK1_KO_Testlauf;1686325621487;10.09.2023;10.09.2023 16:32;00:00:00.0000;0.00 ; 15;0.00;60;Begonnen;;38.11;55.30
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from vbv_lernwelt.assignment.models import AssignmentCompletion
|
||||||
|
from vbv_lernwelt.core.constants import (
|
||||||
|
TEST_STUDENT1_USER_ID,
|
||||||
|
TEST_STUDENT2_USER_ID,
|
||||||
|
TEST_STUDENT3_USER_ID,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.core.create_default_users import create_default_users
|
||||||
|
from vbv_lernwelt.course.creators.test_course import create_test_course
|
||||||
|
from vbv_lernwelt.edoniq_test.result_import.services import (
|
||||||
|
create_edoniq_csv_test_result,
|
||||||
|
EdoniqCsvTestResult,
|
||||||
|
filter_relevant_rows,
|
||||||
|
get_distinct_sequence_ids,
|
||||||
|
upsert_edoniq_test_result,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EdoniqResultImportTestCase(TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
create_default_users()
|
||||||
|
create_test_course(with_sessions=True)
|
||||||
|
|
||||||
|
def test_get_distinct_sequence_ids(self):
|
||||||
|
sequence_ids = get_distinct_sequence_ids()
|
||||||
|
self.assertEqual(len(sequence_ids), 2)
|
||||||
|
self.assertSetEqual(sequence_ids, {"1691151920116", "1688059890497"})
|
||||||
|
|
||||||
|
def test_filter_relevant_rows(self):
|
||||||
|
csv_file_path = os.path.dirname(__file__) + "/edoniq_pruefung_resultate.csv"
|
||||||
|
with open(
|
||||||
|
csv_file_path,
|
||||||
|
"r",
|
||||||
|
encoding="utf-8",
|
||||||
|
) as file:
|
||||||
|
csv_data = file.read()
|
||||||
|
|
||||||
|
relevant_rows = filter_relevant_rows(csv_data)
|
||||||
|
self.assertEqual(len(relevant_rows), 3)
|
||||||
|
print(relevant_rows)
|
||||||
|
|
||||||
|
self.assertEqual(relevant_rows[0]["Benutzer Login"], TEST_STUDENT1_USER_ID)
|
||||||
|
self.assertEqual(relevant_rows[1]["Benutzer Login"], TEST_STUDENT2_USER_ID)
|
||||||
|
self.assertEqual(relevant_rows[2]["Benutzer Login"], TEST_STUDENT3_USER_ID)
|
||||||
|
|
||||||
|
self.assertEqual(relevant_rows[0]["erreichte Punkte absolut"], "21.25")
|
||||||
|
self.assertEqual(relevant_rows[0]["erreichbares Punktetotal absolut"], "29")
|
||||||
|
self.assertEqual(
|
||||||
|
relevant_rows[0]["Bestanden-/Nicht-Bestanden Status"], "Bestanden"
|
||||||
|
)
|
||||||
|
self.assertEqual(relevant_rows[0]["Zeitstempel Beginn"], "06.09.2023 18:48")
|
||||||
|
|
||||||
|
self.assertEqual(relevant_rows[2]["erreichte Punkte absolut"], "0.00")
|
||||||
|
self.assertEqual(relevant_rows[2]["erreichbares Punktetotal absolut"], "29")
|
||||||
|
self.assertEqual(
|
||||||
|
relevant_rows[2]["Bestanden-/Nicht-Bestanden Status"], "Begonnen"
|
||||||
|
)
|
||||||
|
|
||||||
|
edoniq_test_result_row = create_edoniq_csv_test_result(relevant_rows[0])
|
||||||
|
self.assertEqual(edoniq_test_result_row.user_id, TEST_STUDENT1_USER_ID)
|
||||||
|
self.assertEqual(edoniq_test_result_row.user_points, 21.25)
|
||||||
|
self.assertEqual(edoniq_test_result_row.max_points, 29)
|
||||||
|
self.assertEqual(edoniq_test_result_row.passed, True)
|
||||||
|
self.assertEqual(edoniq_test_result_row.edoniq_sequence_id, "1688059890497")
|
||||||
|
|
||||||
|
def test_upsert_edoniq_test_results(self):
|
||||||
|
row = EdoniqCsvTestResult(
|
||||||
|
user_id=TEST_STUDENT1_USER_ID,
|
||||||
|
edoniq_sequence_id="1688059890497",
|
||||||
|
user_points=21.25,
|
||||||
|
max_points=29,
|
||||||
|
passed=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
_ac, created = upsert_edoniq_test_result(row)
|
||||||
|
self.assertTrue(created)
|
||||||
|
|
||||||
|
_ac, created = upsert_edoniq_test_result(row)
|
||||||
|
self.assertFalse(created)
|
||||||
|
|
||||||
|
ac = AssignmentCompletion.objects.get(
|
||||||
|
assignment_user_id=TEST_STUDENT1_USER_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(ac.evaluation_points, 21.25)
|
||||||
|
self.assertEqual(ac.evaluation_max_points, 29)
|
||||||
|
self.assertTrue(ac.evaluation_passed)
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Generated by Django 3.2.20 on 2023-09-29 12:55
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"learnpath",
|
||||||
|
"0006_alter_learningcontentfeedback_can_user_self_toggle_course_completion",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontentedoniqtest",
|
||||||
|
name="edoniq_extended_course_release_id",
|
||||||
|
field=models.CharField(blank=True, default="", max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontentedoniqtest",
|
||||||
|
name="edoniq_course_release_id",
|
||||||
|
field=models.CharField(blank=True, default="", max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontentedoniqtest",
|
||||||
|
name="edoniq_extended_sequence_id",
|
||||||
|
field=models.CharField(blank=True, default="", max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontentedoniqtest",
|
||||||
|
name="edoniq_sequence_id",
|
||||||
|
field=models.CharField(blank=True, default="", max_length=255),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Generated by Django 3.2.20 on 2023-09-29 14:02
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def update_edoniq_ids(apps, schema_editor):
|
||||||
|
LearningContentEdoniqTest = apps.get_model("learnpath", "LearningContentEdoniqTest")
|
||||||
|
LearningContentEdoniqTest.objects.filter(
|
||||||
|
test_url__icontains="1689096523730"
|
||||||
|
).update(edoniq_sequence_id="1688059890497")
|
||||||
|
LearningContentEdoniqTest.objects.filter(
|
||||||
|
test_url__icontains="1689096523730"
|
||||||
|
).update(
|
||||||
|
edoniq_sequence_id="1688059890497",
|
||||||
|
edoniq_course_release_id="1689096523730",
|
||||||
|
edoniq_extended_sequence_id="1691151920116",
|
||||||
|
edoniq_extended_course_release_id="1691157696911",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("learnpath", "0007_auto_20230929_1455"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(update_edoniq_ids),
|
||||||
|
]
|
||||||
|
|
@ -342,10 +342,26 @@ class LearningContentEdoniqTest(LearningContent):
|
||||||
test_url = models.TextField(blank=True)
|
test_url = models.TextField(blank=True)
|
||||||
extended_time_test_url = models.TextField(blank=True)
|
extended_time_test_url = models.TextField(blank=True)
|
||||||
|
|
||||||
|
# Sequenz ID von Edoniq
|
||||||
|
edoniq_sequence_id = models.CharField(max_length=255, blank=True, default="")
|
||||||
|
edoniq_extended_sequence_id = models.CharField(
|
||||||
|
max_length=255, blank=True, default=""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Kursfreigaben ID von Edoniq
|
||||||
|
edoniq_course_release_id = models.CharField(max_length=255, blank=True, default="")
|
||||||
|
edoniq_extended_course_release_id = models.CharField(
|
||||||
|
max_length=255, blank=True, default=""
|
||||||
|
)
|
||||||
|
|
||||||
content_panels = LearningContent.content_panels + [
|
content_panels = LearningContent.content_panels + [
|
||||||
FieldPanel("checkbox_text", classname="Text"),
|
FieldPanel("checkbox_text", classname="Text"),
|
||||||
FieldPanel("test_url", classname="Text"),
|
FieldPanel("test_url", classname="Text"),
|
||||||
|
FieldPanel("edoniq_course_release_id", classname="Text"),
|
||||||
|
FieldPanel("edoniq_sequence_id", classname="Text"),
|
||||||
FieldPanel("extended_time_test_url", classname="Text"),
|
FieldPanel("extended_time_test_url", classname="Text"),
|
||||||
|
FieldPanel("edoniq_extended_course_release_id", classname="Text"),
|
||||||
|
FieldPanel("edoniq_extended_sequence_id", classname="Text"),
|
||||||
PageChooserPanel("content_assignment", "assignment.Assignment"),
|
PageChooserPanel("content_assignment", "assignment.Assignment"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue