Add sync
This commit is contained in:
parent
12977b01cc
commit
5d0f7b88b5
|
|
@ -45,6 +45,7 @@ from vbv_lernwelt.feedback.views import (
|
||||||
from vbv_lernwelt.importer.views import (
|
from vbv_lernwelt.importer.views import (
|
||||||
coursesessions_students_import,
|
coursesessions_students_import,
|
||||||
coursesessions_trainers_import,
|
coursesessions_trainers_import,
|
||||||
|
t2l_sync,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.notify.views import email_notification_settings
|
from vbv_lernwelt.notify.views import email_notification_settings
|
||||||
from wagtail import urls as wagtail_urls
|
from wagtail import urls as wagtail_urls
|
||||||
|
|
@ -158,6 +159,11 @@ urlpatterns = [
|
||||||
coursesessions_students_import,
|
coursesessions_students_import,
|
||||||
name="coursesessions_students_import",
|
name="coursesessions_students_import",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
r"server/importer/t2l-sync/",
|
||||||
|
t2l_sync,
|
||||||
|
name="t2l_sync",
|
||||||
|
),
|
||||||
|
|
||||||
# testing and debug
|
# testing and debug
|
||||||
path('server/raise_error/',
|
path('server/raise_error/',
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from datetime import date, datetime, time
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
|
|
@ -24,6 +25,8 @@ CIRCLE_NAMES = {
|
||||||
"Haushalt Teil 2": {"de": "Haushalt Teil 2", "fr": "Haushalt Teil 2", "it": ""},
|
"Haushalt Teil 2": {"de": "Haushalt Teil 2", "fr": "Haushalt Teil 2", "it": ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
T2L_IGNORE_FIELDS = ["Vorname", "Name", "Email", "Sprache", "Durchführungen"]
|
||||||
|
|
||||||
|
|
||||||
class DataImportError(Exception):
|
class DataImportError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
@ -322,7 +325,7 @@ def create_or_update_student(data: Dict[str, Any]):
|
||||||
)
|
)
|
||||||
|
|
||||||
user.language = data["Sprache"]
|
user.language = data["Sprache"]
|
||||||
user.additional_json_data = user.additional_json_data | data
|
update_user_json_data(user, data)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
# general expert handling
|
# general expert handling
|
||||||
|
|
@ -333,3 +336,56 @@ def create_or_update_student(data: Dict[str, Any]):
|
||||||
course_session_id=course_session.id, user_id=user.id
|
course_session_id=course_session.id, user_id=user.id
|
||||||
)
|
)
|
||||||
csu.save()
|
csu.save()
|
||||||
|
|
||||||
|
|
||||||
|
def sync_students_from_t2l_excel(filename: str):
|
||||||
|
workbook = load_workbook(filename=filename)
|
||||||
|
sheet = workbook.active
|
||||||
|
|
||||||
|
tuple_list = calc_header_tuple_list_from_pyxl_sheet(sheet)
|
||||||
|
for row in tuple_list:
|
||||||
|
data = dict(row)
|
||||||
|
sync_students_from_t2l(data | {})
|
||||||
|
|
||||||
|
|
||||||
|
def sync_students_from_t2l(data):
|
||||||
|
# ignore errors
|
||||||
|
try:
|
||||||
|
user = User.objects.get(
|
||||||
|
additional_json_data__Lehrvertragsnummer=data["Lehrvertragsnummer"]
|
||||||
|
)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return
|
||||||
|
|
||||||
|
# only sync data that is not in our user model
|
||||||
|
for field in T2L_IGNORE_FIELDS:
|
||||||
|
try:
|
||||||
|
del data[field]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
update_user_json_data(user, data)
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
|
def update_user_json_data(user: User, data: Dict[str, Any]):
|
||||||
|
user.additional_json_data = user.additional_json_data | sanitize_json_data_input(
|
||||||
|
data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_json_data_input(data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Saving additional_json_data fails if the data contains datetime objects.
|
||||||
|
This is a quick and dirty fix to convert datetime objects to iso strings.
|
||||||
|
"""
|
||||||
|
for key, value in data.items():
|
||||||
|
if isinstance(value, datetime):
|
||||||
|
data[key] = value.isoformat()
|
||||||
|
elif isinstance(value, date):
|
||||||
|
data[key] = value.isoformat()
|
||||||
|
elif isinstance(value, time):
|
||||||
|
data[key] = value.isoformat()
|
||||||
|
else:
|
||||||
|
data[key] = value
|
||||||
|
return data
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,9 @@ class CreateOrUpdateStudentTestCase(TestCase):
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
CourseSessionUser.objects.filter(
|
CourseSessionUser.objects.filter(
|
||||||
user__additional_json_data__Lehrvertragsnummer=self.user_dict["Lehrvertragsnummer"]
|
user__additional_json_data__Lehrvertragsnummer=self.user_dict[
|
||||||
|
"Lehrvertragsnummer"
|
||||||
|
]
|
||||||
).count(),
|
).count(),
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
import os
|
||||||
|
from datetime import date, datetime, time
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from vbv_lernwelt.course.creators.test_course import create_test_course
|
||||||
|
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
||||||
|
from vbv_lernwelt.importer.services import (
|
||||||
|
create_or_update_student,
|
||||||
|
sanitize_json_data_input,
|
||||||
|
sync_students_from_t2l,
|
||||||
|
)
|
||||||
|
|
||||||
|
test_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
class SyncT2lTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.course = create_test_course(include_vv=False)
|
||||||
|
self.course_session_a = CourseSession.objects.create(
|
||||||
|
course=self.course,
|
||||||
|
title="Deutschschweiz 2023 A",
|
||||||
|
import_id="DE 2023 A",
|
||||||
|
group="A",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.user_dict = {
|
||||||
|
"Name": "Rascher",
|
||||||
|
"Vorname": "Barbara",
|
||||||
|
"Email": "barbara.rascher@vbv-afa.ch",
|
||||||
|
"Sprache": "de",
|
||||||
|
"Durchführungen": "DE 2023 A",
|
||||||
|
"Lehrvertragsnummer": "1234",
|
||||||
|
"Tel. Privat": "079 593 83 43",
|
||||||
|
}
|
||||||
|
create_or_update_student(self.user_dict)
|
||||||
|
|
||||||
|
def test_updates_field(self):
|
||||||
|
user_dict = {
|
||||||
|
"Name": "Rascher",
|
||||||
|
"Vorname": "Barbara",
|
||||||
|
"Email": "barbara.rascher@vbv-afa.ch",
|
||||||
|
"Sprache": "de",
|
||||||
|
"Durchführungen": "DE 2023 A",
|
||||||
|
"Lehrvertragsnummer": "1234",
|
||||||
|
"Tel. Privat": "079 593 83 65",
|
||||||
|
}
|
||||||
|
|
||||||
|
sync_students_from_t2l(user_dict)
|
||||||
|
updated_user = CourseSessionUser.objects.get(
|
||||||
|
user__email=self.user_dict["Email"]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
updated_user.user.additional_json_data["Tel. Privat"],
|
||||||
|
user_dict["Tel. Privat"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_adds_field(self):
|
||||||
|
user_dict = {
|
||||||
|
"Name": "Rascher",
|
||||||
|
"Vorname": "Barbara",
|
||||||
|
"Email": "barbara.rascher@vbv-afa.ch",
|
||||||
|
"Sprache": "de",
|
||||||
|
"Durchführungen": "DE 2023 A",
|
||||||
|
"Lehrvertragsnummer": "1234",
|
||||||
|
"Tel. Privat": "079 593 83 43",
|
||||||
|
"Firma": "VBV",
|
||||||
|
}
|
||||||
|
|
||||||
|
sync_students_from_t2l(user_dict)
|
||||||
|
updated_user = CourseSessionUser.objects.get(
|
||||||
|
user__email=self.user_dict["Email"]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
updated_user.user.additional_json_data["Firma"], user_dict["Firma"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_ignors_defined_field(self):
|
||||||
|
user_dict = {
|
||||||
|
"Name": "Rascher2",
|
||||||
|
"Vorname": "Barbara2",
|
||||||
|
"Email": "barbara.rascher2@vbv-afa.ch",
|
||||||
|
"Sprache": "fr",
|
||||||
|
"Durchführungen": "DE 2023 B",
|
||||||
|
"Lehrvertragsnummer": "1234",
|
||||||
|
"Tel. Privat": "079 593 83 43",
|
||||||
|
}
|
||||||
|
|
||||||
|
sync_students_from_t2l(user_dict)
|
||||||
|
updated_user = CourseSessionUser.objects.get(
|
||||||
|
user__email=self.user_dict["Email"]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
updated_user.user.additional_json_data["Name"], self.user_dict["Name"]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
updated_user.user.additional_json_data["Vorname"], self.user_dict["Vorname"]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
updated_user.user.additional_json_data["Durchführungen"],
|
||||||
|
self.user_dict["Durchführungen"],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
updated_user.user.additional_json_data["Sprache"], self.user_dict["Sprache"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_ignors_missing_defined_field(self):
|
||||||
|
user_dict = {
|
||||||
|
"Name": "Rascher2",
|
||||||
|
"Vorname": "Barbara2",
|
||||||
|
"Email": "barbara.rascher2@vbv-afa.ch",
|
||||||
|
"Sprache": "fr",
|
||||||
|
"Lehrvertragsnummer": "1234",
|
||||||
|
"Tel. Privat": "079 593 83 43",
|
||||||
|
}
|
||||||
|
|
||||||
|
sync_students_from_t2l(user_dict)
|
||||||
|
updated_user = CourseSessionUser.objects.get(
|
||||||
|
user__email=self.user_dict["Email"]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
updated_user.user.additional_json_data["Name"], self.user_dict["Name"]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
updated_user.user.additional_json_data["Vorname"], self.user_dict["Vorname"]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
updated_user.user.additional_json_data["Sprache"], self.user_dict["Sprache"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_ignors_wrong_contract_nummer(self):
|
||||||
|
user_dict = {
|
||||||
|
"Name": "Rascher2",
|
||||||
|
"Vorname": "Barbara2",
|
||||||
|
"Email": "barbara.rascher2@vbv-afa.ch",
|
||||||
|
"Sprache": "fr",
|
||||||
|
"Lehrvertragsnummer": "12345",
|
||||||
|
"Tel. Privat": "079 593 83 43",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
sync_students_from_t2l(user_dict)
|
||||||
|
updated_user = CourseSessionUser.objects.get(
|
||||||
|
user__email=self.user_dict["Email"]
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.fail(
|
||||||
|
f"SyncT2lTestCase.test_ignors_wrong_contract_number: An exception was unexpectedly raised: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SanitizerTestCase(TestCase):
|
||||||
|
def test_date(self):
|
||||||
|
a_date = date(2021, 1, 1)
|
||||||
|
user_dict = {"Name": "Rascher", "Datum": a_date}
|
||||||
|
|
||||||
|
expected_sanitized_data = {"Name": "Rascher", "Datum": a_date.isoformat()}
|
||||||
|
|
||||||
|
sanitized_data = sanitize_json_data_input(user_dict)
|
||||||
|
self.assertEqual(sanitized_data, expected_sanitized_data)
|
||||||
|
|
||||||
|
def test_datetime(self):
|
||||||
|
a_date = datetime(2021, 1, 1)
|
||||||
|
user_dict = {"Name": "Rascher", "Datum": a_date}
|
||||||
|
|
||||||
|
expected_sanitized_data = {"Name": "Rascher", "Datum": a_date.isoformat()}
|
||||||
|
|
||||||
|
sanitized_data = sanitize_json_data_input(user_dict)
|
||||||
|
self.assertEqual(sanitized_data, expected_sanitized_data)
|
||||||
|
|
||||||
|
def test_time(self):
|
||||||
|
a_date = time(23, 59, 59)
|
||||||
|
user_dict = {"Name": "Rascher", "Datum": a_date}
|
||||||
|
|
||||||
|
expected_sanitized_data = {"Name": "Rascher", "Datum": a_date.isoformat()}
|
||||||
|
|
||||||
|
sanitized_data = sanitize_json_data_input(user_dict)
|
||||||
|
self.assertEqual(sanitized_data, expected_sanitized_data)
|
||||||
|
|
@ -8,6 +8,7 @@ from vbv_lernwelt.importer.services import (
|
||||||
import_course_sessions_from_excel,
|
import_course_sessions_from_excel,
|
||||||
import_students_from_excel,
|
import_students_from_excel,
|
||||||
import_trainers_from_excel_for_training,
|
import_trainers_from_excel_for_training,
|
||||||
|
sync_students_from_t2l_excel,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -36,6 +37,15 @@ def coursesessions_students_import(request):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@staff_member_required
|
||||||
|
def t2l_sync(request):
|
||||||
|
return handle_import(
|
||||||
|
request,
|
||||||
|
"Die Daten wurden erflogreich synchronisiert!",
|
||||||
|
sync_students_from_t2l_excel,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def handle_import(request, success_msg: str, importer: Callable[[str], None]):
|
def handle_import(request, success_msg: str, importer: Callable[[str], None]):
|
||||||
if request.method == "POST" and request.FILES["excel_file"]:
|
if request.method == "POST" and request.FILES["excel_file"]:
|
||||||
excel_file = request.FILES["excel_file"]
|
excel_file = request.FILES["excel_file"]
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
{% include "admin/app_list.html" with app_list=app_list show_changelinks=True %}
|
{% include "admin/app_list.html" with app_list=app_list show_changelinks=True %}
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h1>Export üK</h1>
|
<h1>üK</h1>
|
||||||
<h1>Excel Import üK</h1>
|
<h1>Excel Import üK</h1>
|
||||||
<h2>Durchführungen und Trainer</h2>
|
<h2>Durchführungen und Trainer</h2>
|
||||||
<form method="post" enctype="multipart/form-data" action="/server/importer/coursesession-trainer-import/">
|
<form method="post" enctype="multipart/form-data" action="/server/importer/coursesession-trainer-import/">
|
||||||
|
|
@ -21,11 +21,17 @@
|
||||||
<input type="submit" value="Teilnehmer importieren">
|
<input type="submit" value="Teilnehmer importieren">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<h2>Edoniq Teilnehmer</h2>
|
<h2>Sync mit T2L Daten</h2>
|
||||||
|
<form method="post" enctype="multipart/form-data" action="/server/importer/t2l-sync/">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="file" name="excel_file" accept=".xlsx, .xls">
|
||||||
|
<input type="submit" value="Teilnehmer mit T2L-Daten Synchronisiern">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h2>Export Edoniq Teilnehmer</h2>
|
||||||
<a href="{% url 'edoniq_export_students' %}" class="btn btn-primary">Teilnehmer exportieren</a>
|
<a href="{% url 'edoniq_export_students' %}" class="btn btn-primary">Teilnehmer exportieren</a>
|
||||||
|
|
||||||
|
<h1>Reset</h1>
|
||||||
<h2>Reset</h2>
|
|
||||||
<form action="/api/core/cypressreset/" method="post">
|
<form action="/api/core/cypressreset/" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="btn" name="">Testdaten zurück setzen</button>
|
<button class="btn" name="">Testdaten zurück setzen</button>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue