Merged in feature/VBV-464-excel-importer (pull request #160)
Feature/VBV-464 excel importer Approved-by: Daniel Egger
This commit is contained in:
commit
a1403f8a54
|
|
@ -122,6 +122,7 @@ LOCAL_APPS = [
|
|||
"vbv_lernwelt.notify",
|
||||
"vbv_lernwelt.assignment",
|
||||
"vbv_lernwelt.duedate",
|
||||
"vbv_lernwelt.importer",
|
||||
]
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
|
||||
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
||||
|
|
|
|||
|
|
@ -42,6 +42,11 @@ from vbv_lernwelt.feedback.views import (
|
|||
get_expert_feedbacks_for_course,
|
||||
get_feedback_for_circle,
|
||||
)
|
||||
from vbv_lernwelt.importer.views import (
|
||||
coursesessions_students_import,
|
||||
coursesessions_trainers_import,
|
||||
t2l_sync,
|
||||
)
|
||||
from vbv_lernwelt.notify.views import email_notification_settings
|
||||
from wagtail import urls as wagtail_urls
|
||||
from wagtail.admin import urls as wagtailadmin_urls
|
||||
|
|
@ -143,6 +148,23 @@ urlpatterns = [
|
|||
# edoniq test
|
||||
path(r'api/core/edoniq-test/export-users/', export_students, name='edoniq_export_students'),
|
||||
|
||||
# importer
|
||||
path(
|
||||
r"server/importer/coursesession-trainer-import/",
|
||||
coursesessions_trainers_import,
|
||||
name="coursesessions_trainers_import",
|
||||
),
|
||||
path(
|
||||
r"server/importer/coursesession-students-import/",
|
||||
coursesessions_students_import,
|
||||
name="coursesessions_students_import",
|
||||
),
|
||||
path(
|
||||
r"server/importer/t2l-sync/",
|
||||
t2l_sync,
|
||||
name="t2l_sync",
|
||||
),
|
||||
|
||||
# testing and debug
|
||||
path('server/raise_error/',
|
||||
user_passes_test(lambda u: u.is_superuser, login_url='/login/')(
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ class UserAdmin(auth_admin.UserAdmin):
|
|||
},
|
||||
),
|
||||
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
|
||||
(_("Additional data"), {"fields": ("additional_json_data",)}),
|
||||
)
|
||||
list_display = [
|
||||
"username",
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ from vbv_lernwelt.feedback.creators.create_demo_feedback import create_feedback
|
|||
from vbv_lernwelt.importer.services import (
|
||||
import_course_sessions_from_excel,
|
||||
import_students_from_excel,
|
||||
import_trainers_from_excel,
|
||||
import_trainers_from_excel_for_training,
|
||||
)
|
||||
from vbv_lernwelt.learnpath.create_vv_new_learning_path import (
|
||||
create_vv_new_learning_path,
|
||||
|
|
@ -561,12 +561,13 @@ def create_course_training_de():
|
|||
print(current_dir)
|
||||
course = Course.objects.get(id=COURSE_UK_TRAINING)
|
||||
import_course_sessions_from_excel(
|
||||
course,
|
||||
f"{current_dir}/../../../importer/tests/Schulungen_Durchfuehrung_Trainer.xlsx",
|
||||
course=course,
|
||||
restrict_language="de",
|
||||
)
|
||||
import_trainers_from_excel(
|
||||
course,
|
||||
import_trainers_from_excel_for_training(
|
||||
f"{current_dir}/../../../importer/tests/Schulungen_Durchfuehrung_Trainer.xlsx",
|
||||
course=course,
|
||||
)
|
||||
import_students_from_excel(
|
||||
f"{current_dir}/../../../importer/tests/Schulungen_Teilnehmende.xlsx",
|
||||
|
|
@ -650,13 +651,13 @@ def create_course_training_fr():
|
|||
print(current_dir)
|
||||
course = Course.objects.get(id=COURSE_UK_TRAINING_FR)
|
||||
import_course_sessions_from_excel(
|
||||
course,
|
||||
f"{current_dir}/../../../importer/tests/Schulungen_Durchfuehrung_Trainer.xlsx",
|
||||
language="fr",
|
||||
restrict_language="fr",
|
||||
course=course,
|
||||
)
|
||||
import_trainers_from_excel(
|
||||
course,
|
||||
import_trainers_from_excel_for_training(
|
||||
f"{current_dir}/../../../importer/tests/Schulungen_Durchfuehrung_Trainer.xlsx",
|
||||
course=course,
|
||||
language="fr",
|
||||
)
|
||||
import_students_from_excel(
|
||||
|
|
@ -737,13 +738,13 @@ def create_course_training_it():
|
|||
print(current_dir)
|
||||
course = Course.objects.get(id=COURSE_UK_TRAINING_IT)
|
||||
import_course_sessions_from_excel(
|
||||
course,
|
||||
f"{current_dir}/../../../importer/tests/Schulungen_Durchfuehrung_Trainer.xlsx",
|
||||
language="it",
|
||||
restrict_language="it",
|
||||
course=course,
|
||||
)
|
||||
import_trainers_from_excel(
|
||||
course,
|
||||
import_trainers_from_excel_for_training(
|
||||
f"{current_dir}/../../../importer/tests/Schulungen_Durchfuehrung_Trainer.xlsx",
|
||||
course=course,
|
||||
language="it",
|
||||
)
|
||||
import_students_from_excel(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
from typing import Any, Dict
|
||||
from datetime import date, datetime, time
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import structlog
|
||||
from openpyxl.reader.excel import load_workbook
|
||||
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.consts import COURSE_UK, COURSE_UK_FR, COURSE_UK_IT
|
||||
from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser
|
||||
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
||||
from vbv_lernwelt.importer.utils import (
|
||||
|
|
@ -15,9 +17,27 @@ from vbv_lernwelt.learnpath.models import Circle, LearningContentAttendanceCours
|
|||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
CIRCLE_NAMES = {
|
||||
"Kickoff": {"de": "Kickoff", "fr": "Lancement", "it": "Introduzione"},
|
||||
"Basis": {"de": "Basis", "fr": "Base", "it": "Base"},
|
||||
"Fahrzeug": {"de": "Fahrzeug", "fr": "Véhicule", "it": "Veicolo"},
|
||||
"Haushalt Teil 1": {"de": "Haushalt Teil 1", "fr": "Habitat partie 1", "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):
|
||||
pass
|
||||
|
||||
|
||||
def create_or_update_user(
|
||||
email: str, first_name: str = "", last_name: str = "", sso_id: str = None
|
||||
email: str,
|
||||
first_name: str = "",
|
||||
last_name: str = "",
|
||||
sso_id: str = None,
|
||||
contract_number: str = "",
|
||||
):
|
||||
logger.debug(
|
||||
"create_or_update_user",
|
||||
|
|
@ -33,6 +53,13 @@ def create_or_update_user(
|
|||
if user_qs.exists():
|
||||
user = user_qs.first()
|
||||
|
||||
if not user and contract_number:
|
||||
user_qs = User.objects.filter(
|
||||
additional_json_data__Lehrvertragsnummer=contract_number
|
||||
)
|
||||
if user_qs.exists():
|
||||
user = user_qs.first()
|
||||
|
||||
if not user:
|
||||
user_qs = User.objects.filter(email=email)
|
||||
if user_qs.exists():
|
||||
|
|
@ -53,25 +80,41 @@ def create_or_update_user(
|
|||
return user
|
||||
|
||||
|
||||
def import_course_sessions_from_excel(course: Course, filename: str, language="de"):
|
||||
def import_course_sessions_from_excel(
|
||||
filename: str, course: Course = None, restrict_language=None
|
||||
):
|
||||
workbook = load_workbook(filename=filename)
|
||||
sheet = workbook["Schulungen Durchführung"]
|
||||
|
||||
tuple_list = calc_header_tuple_list_from_pyxl_sheet(sheet)
|
||||
|
||||
for row in tuple_list:
|
||||
data = dict(row)
|
||||
validate_row_data(data, ["Klasse", "ID", "Generation", "Region", "Sprache"])
|
||||
language = data["Sprache"].strip()
|
||||
|
||||
# this can be removed when the training import (create_course_training_xx) is no longer used
|
||||
if restrict_language and language != restrict_language:
|
||||
continue
|
||||
|
||||
if not course:
|
||||
course = get_uk_course(language)
|
||||
|
||||
create_or_update_course_session(
|
||||
course, dict(row), language=language, circles=["Fahrzeug"]
|
||||
course, data, language, circle_keys=["Kickoff", "Basis", "Fahrzeug"]
|
||||
)
|
||||
|
||||
|
||||
def create_or_update_course_session(
|
||||
course: Course, data: Dict[str, Any], language="de", circles=None
|
||||
course: Course,
|
||||
data: Dict[str, Any],
|
||||
language: str,
|
||||
circle_keys=None,
|
||||
):
|
||||
"""
|
||||
:param data: the following keys are required to process the data: Generation, Region, Klasse
|
||||
:return:
|
||||
"""
|
||||
|
||||
logger.debug(
|
||||
"create_or_update_course_session",
|
||||
course=course.title,
|
||||
|
|
@ -79,10 +122,9 @@ def create_or_update_course_session(
|
|||
label="import",
|
||||
)
|
||||
|
||||
if circles is None:
|
||||
circles = []
|
||||
if not circle_keys:
|
||||
circle_keys = []
|
||||
|
||||
# TODO: validation
|
||||
group = data["Klasse"].strip()
|
||||
import_id = data["ID"].strip()
|
||||
|
||||
|
|
@ -91,12 +133,8 @@ def create_or_update_course_session(
|
|||
|
||||
title = f"{region} {generation} {group}"
|
||||
|
||||
if not import_id.lower().startswith(language.lower()):
|
||||
# FIXME: language check depends on import_id format for now...
|
||||
return None
|
||||
|
||||
cs, _created = CourseSession.objects.get_or_create(
|
||||
import_id=import_id, group=group, course=course
|
||||
title=title, course=course, import_id=import_id
|
||||
)
|
||||
|
||||
cs.additional_json_data["import_data"] = data
|
||||
|
|
@ -109,31 +147,23 @@ def create_or_update_course_session(
|
|||
cs.import_id = import_id
|
||||
|
||||
cs.save()
|
||||
for circle in circles:
|
||||
attendance_course_lp_qs = None
|
||||
if language == "de":
|
||||
attendance_course_lp_qs = LearningContentAttendanceCourse.objects.filter(
|
||||
slug=f"{course.slug}-lp-circle-{circle.lower()}-lc-präsenzkurs-{circle.lower()}"
|
||||
for circle in circle_keys:
|
||||
circle_name = CIRCLE_NAMES[circle][language]
|
||||
|
||||
attendance_course_lc = LearningContentAttendanceCourse.objects.filter(
|
||||
slug=f"{course.slug}-lp-circle-{circle_name.lower()}-lc-präsenzkurs-{circle_name.lower()}"
|
||||
).first()
|
||||
|
||||
if attendance_course_lc:
|
||||
# update existing data
|
||||
csa, _created = CourseSessionAttendanceCourse.objects.get_or_create(
|
||||
course_session=cs, learning_content=attendance_course_lc
|
||||
)
|
||||
|
||||
elif language == "fr":
|
||||
# todo: this is a hack remove me
|
||||
attendance_course_lp_qs = LearningContentAttendanceCourse.objects.filter(
|
||||
slug=f"{course.slug}-lp-circle-véhicule-lc-cours-de-présence-véhicule-à-moteur"
|
||||
)
|
||||
elif language == "it":
|
||||
# todo: this is a hack remove me
|
||||
attendance_course_lp_qs = LearningContentAttendanceCourse.objects.filter(
|
||||
slug=f"{course.slug}-lp-circle-veicolo-lc-corso-di-presenza-veicolo"
|
||||
)
|
||||
location = f"{data[f'{circle} Raum']}, {data[f'{circle} Standort']}, {data[f'{circle} Adresse']}"
|
||||
csa.location = location
|
||||
|
||||
if attendance_course_lp_qs and attendance_course_lp_qs.exists():
|
||||
csa = CourseSessionAttendanceCourse.objects.create(
|
||||
course_session=cs,
|
||||
learning_content=attendance_course_lp_qs.first(),
|
||||
location=data[f"{circle} Raum"],
|
||||
trainer="",
|
||||
)
|
||||
csa.trainer = ""
|
||||
csa.due_date.start = try_parse_datetime(data[f"{circle} Start"])[1]
|
||||
csa.due_date.end = try_parse_datetime(data[f"{circle} Ende"])[1]
|
||||
csa.due_date.save()
|
||||
|
|
@ -141,19 +171,50 @@ def create_or_update_course_session(
|
|||
return cs
|
||||
|
||||
|
||||
def import_trainers_from_excel(course: Course, filename: str, language="de"):
|
||||
def validate_row_data(data: Dict[str, any], required_headers: List[str]):
|
||||
for header in required_headers:
|
||||
some = str(data.get(header, "")).strip()
|
||||
if str(data.get(header, "")).strip() in ["", "None"]:
|
||||
logger.debug(
|
||||
"validate_row_data_missing_header",
|
||||
data={"data": data, "header": header},
|
||||
label="import",
|
||||
)
|
||||
raise DataImportError(f"Missing or empty value for header {header}")
|
||||
|
||||
|
||||
def get_uk_course(language: str) -> Course:
|
||||
if language == "fr":
|
||||
course_id = COURSE_UK_FR
|
||||
elif language == "it":
|
||||
course_id = COURSE_UK_IT
|
||||
else:
|
||||
course_id = COURSE_UK
|
||||
|
||||
return Course.objects.get(id=course_id)
|
||||
|
||||
|
||||
def import_trainers_from_excel_for_training(
|
||||
filename: str, language="de", course: Course = None
|
||||
):
|
||||
workbook = load_workbook(filename=filename)
|
||||
sheet = workbook["Schulungen Trainer"]
|
||||
|
||||
tuple_list = calc_header_tuple_list_from_pyxl_sheet(sheet)
|
||||
for row in tuple_list:
|
||||
create_or_update_trainer(course, dict(row), language=language)
|
||||
data = dict(row)
|
||||
validate_row_data(
|
||||
data, ["Email", "Vorname", "Name", "Sprache", "Klasse", "Generation"]
|
||||
)
|
||||
create_or_update_trainer(course, data, language=language)
|
||||
|
||||
|
||||
def create_or_update_trainer(course: Course, data: Dict[str, Any], language="de"):
|
||||
course_title = course.title if course else "None"
|
||||
|
||||
logger.debug(
|
||||
"create_or_update_trainer",
|
||||
course=course.title,
|
||||
course=course_title,
|
||||
data=data,
|
||||
label="import",
|
||||
)
|
||||
|
|
@ -163,62 +224,64 @@ def create_or_update_trainer(course: Course, data: Dict[str, Any], language="de"
|
|||
first_name=data["Vorname"],
|
||||
last_name=data["Name"],
|
||||
)
|
||||
user.language = data["Sprache"]
|
||||
user.save()
|
||||
|
||||
groups = [g.strip() for g in data["Klasse"].strip().split(",")]
|
||||
group = data["Klasse"].strip()
|
||||
|
||||
# general expert handling
|
||||
for group in groups:
|
||||
import_id = f"{data['Generation'].strip()} {group}"
|
||||
course_session = CourseSession.objects.filter(
|
||||
import_id = f"{data['Generation'].strip()} {group}"
|
||||
course_session = CourseSession.objects.filter(
|
||||
import_id=import_id,
|
||||
group=group,
|
||||
).first()
|
||||
if course_session:
|
||||
csu, _created = CourseSessionUser.objects.get_or_create(
|
||||
course_session_id=course_session.id, user_id=user.id
|
||||
)
|
||||
csu.role = CourseSessionUser.Role.EXPERT
|
||||
csu.save()
|
||||
else:
|
||||
logger.warning(
|
||||
"create_or_update_trainer: course_session not found",
|
||||
import_id=import_id,
|
||||
group=group,
|
||||
).first()
|
||||
if course_session:
|
||||
csu, _created = CourseSessionUser.objects.get_or_create(
|
||||
course_session_id=course_session.id, user_id=user.id
|
||||
)
|
||||
csu.role = CourseSessionUser.Role.EXPERT
|
||||
csu.save()
|
||||
else:
|
||||
logger.warning(
|
||||
"create_or_update_trainer: course_session not found",
|
||||
import_id=import_id,
|
||||
group=group,
|
||||
label="import",
|
||||
)
|
||||
label="import",
|
||||
)
|
||||
|
||||
if not course and not course_session:
|
||||
logger.warning(
|
||||
"create_or_update_trainer: course_session and course are None",
|
||||
import_id=import_id,
|
||||
group=group,
|
||||
label="import",
|
||||
)
|
||||
return
|
||||
|
||||
if not course:
|
||||
return
|
||||
course = course_session.course
|
||||
|
||||
# circle expert handling
|
||||
circle_data = parse_circle_group_string(data["Circles"])
|
||||
for circle_string in circle_data:
|
||||
parts = circle_string.split("(", 1)
|
||||
circle_name = parts[0].strip()
|
||||
|
||||
groups = [g.strip() for g in parts[1].rstrip(")").strip().split(",")]
|
||||
|
||||
# FIXME: hardcoded translation
|
||||
if language == "fr" and circle_name == "Fahrzeug":
|
||||
circle_name = "Véhicule"
|
||||
for circle_key in circle_data:
|
||||
circle_name = CIRCLE_NAMES[circle_key][language]
|
||||
|
||||
# print(circle_name, groups)
|
||||
for group in groups:
|
||||
import_id = f"{data['Generation'].strip()} {group}"
|
||||
course_session = CourseSession.objects.filter(
|
||||
import_id=import_id, group=group
|
||||
).first()
|
||||
circle = Circle.objects.filter(
|
||||
slug=f"{course.slug}-lp-circle-{circle_name.lower()}"
|
||||
).first()
|
||||
import_id = f"{data['Generation'].strip()} {group}"
|
||||
course_session = CourseSession.objects.filter(
|
||||
import_id=import_id, group=group
|
||||
).first()
|
||||
circle = Circle.objects.filter(
|
||||
slug=f"{course.slug}-lp-circle-{circle_name.lower()}"
|
||||
).first()
|
||||
|
||||
if course_session and circle:
|
||||
csu = CourseSessionUser.objects.filter(
|
||||
course_session_id=course_session.id, user_id=user.id
|
||||
).first()
|
||||
if csu:
|
||||
csu.expert.add(circle)
|
||||
csu.save()
|
||||
if course_session and circle:
|
||||
csu = CourseSessionUser.objects.filter(
|
||||
course_session_id=course_session.id, user_id=user.id
|
||||
).first()
|
||||
if csu:
|
||||
csu.expert.add(circle)
|
||||
csu.save()
|
||||
|
||||
|
||||
def import_students_from_excel(filename: str):
|
||||
|
|
@ -227,7 +290,18 @@ def import_students_from_excel(filename: str):
|
|||
|
||||
tuple_list = calc_header_tuple_list_from_pyxl_sheet(sheet)
|
||||
for row in tuple_list:
|
||||
create_or_update_student(dict(row))
|
||||
data = dict(row)
|
||||
validate_row_data(
|
||||
data,
|
||||
[
|
||||
"Email",
|
||||
"Vorname",
|
||||
"Name",
|
||||
"Sprache",
|
||||
"Durchführungen",
|
||||
],
|
||||
)
|
||||
create_or_update_student(data)
|
||||
|
||||
|
||||
def create_or_update_student(data: Dict[str, Any]):
|
||||
|
|
@ -241,16 +315,71 @@ def create_or_update_student(data: Dict[str, Any]):
|
|||
email=data["Email"].lower(),
|
||||
first_name=data["Vorname"],
|
||||
last_name=data["Name"],
|
||||
contract_number=data.get("Lehrvertragsnummer", ""),
|
||||
)
|
||||
|
||||
# TODO: handle language
|
||||
user.language = data["Sprache"]
|
||||
update_user_json_data(user, data)
|
||||
user.save()
|
||||
|
||||
# general expert handling
|
||||
import_ids = [i.strip() for i in data["Durchführungen"].split(",")]
|
||||
for import_id in import_ids:
|
||||
course_session = CourseSession.objects.filter(import_id=import_id).first()
|
||||
if course_session:
|
||||
csu, _created = CourseSessionUser.objects.get_or_create(
|
||||
course_session_id=course_session.id, user_id=user.id
|
||||
)
|
||||
csu.save()
|
||||
import_id = data["Durchführungen"]
|
||||
course_session = CourseSession.objects.filter(import_id=import_id).first()
|
||||
if course_session:
|
||||
csu, _created = CourseSessionUser.objects.get_or_create(
|
||||
course_session_id=course_session.id, user_id=user.id
|
||||
)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
{% extends "admin/index.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div id="content-main">
|
||||
<div class="content">
|
||||
<h1>Die Daten konnten nicht importiert werden</h1>
|
||||
<p>Folgender Fehler ist aufgetreten:</p>
|
||||
<pre>{{ error }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -6,7 +6,11 @@ from openpyxl.reader.excel import load_workbook
|
|||
from vbv_lernwelt.course.creators.test_course import create_test_course
|
||||
from vbv_lernwelt.course.models import CourseSession
|
||||
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
||||
from vbv_lernwelt.importer.services import create_or_update_course_session
|
||||
from vbv_lernwelt.importer.services import (
|
||||
create_or_update_course_session,
|
||||
DataImportError,
|
||||
validate_row_data,
|
||||
)
|
||||
from vbv_lernwelt.importer.utils import calc_header_tuple_list_from_pyxl_sheet
|
||||
|
||||
test_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
|
@ -26,10 +30,10 @@ class ImportCourseSessionTestCase(TestCase):
|
|||
for row in tuple_list:
|
||||
print(row)
|
||||
create_or_update_course_session(
|
||||
self.course, dict(row), language="de", circles=["Fahrzeug"]
|
||||
self.course, dict(row), language="de", circle_keys=["Fahrzeug"]
|
||||
)
|
||||
|
||||
self.assertEqual(CourseSession.objects.count(), 3)
|
||||
self.assertEqual(CourseSession.objects.count(), 6)
|
||||
|
||||
|
||||
class CreateOrUpdateCourseSessionTestCase(TestCase):
|
||||
|
|
@ -41,6 +45,7 @@ class CreateOrUpdateCourseSessionTestCase(TestCase):
|
|||
("ID", "DE 2023 A"),
|
||||
("Generation", 2023),
|
||||
("Region", "Deutschschweiz"),
|
||||
("Sprache", "de"),
|
||||
("Klasse", "A"),
|
||||
("Fahrzeug Start", "06.06.2023, 13:30"),
|
||||
("Fahrzeug Ende", "06.06.2023, 15:00"),
|
||||
|
|
@ -54,7 +59,9 @@ class CreateOrUpdateCourseSessionTestCase(TestCase):
|
|||
|
||||
data = dict(row)
|
||||
|
||||
cs = create_or_update_course_session(self.course, data, circles=["Fahrzeug"])
|
||||
cs = create_or_update_course_session(
|
||||
self.course, data, language="de", circle_keys=["Fahrzeug"]
|
||||
)
|
||||
|
||||
self.assertEqual(cs.import_id, "DE 2023 A")
|
||||
self.assertEqual(cs.title, "Deutschschweiz 2023 A")
|
||||
|
|
@ -82,6 +89,7 @@ class CreateOrUpdateCourseSessionTestCase(TestCase):
|
|||
("ID", "DE 2023"),
|
||||
("Generation", 2023),
|
||||
("Region", "Deutschschweiz"),
|
||||
("Sprache", "de"),
|
||||
("Klasse", "A"),
|
||||
("Fahrzeug Start", "06.06.2023, 13:30"),
|
||||
("Fahrzeug Ende", "06.06.2023, 15:00"),
|
||||
|
|
@ -95,7 +103,9 @@ class CreateOrUpdateCourseSessionTestCase(TestCase):
|
|||
|
||||
data = dict(row)
|
||||
|
||||
cs = create_or_update_course_session(self.course, data, circles=["Fahrzeug"])
|
||||
cs = create_or_update_course_session(
|
||||
self.course, data, language="de", circle_keys=["Fahrzeug"]
|
||||
)
|
||||
|
||||
self.assertEqual(1, CourseSession.objects.count())
|
||||
|
||||
|
|
@ -112,3 +122,83 @@ class CreateOrUpdateCourseSessionTestCase(TestCase):
|
|||
self.assertEqual(
|
||||
attendance_course.due_date.end.isoformat(), "2023-06-06T13:00:00+00:00"
|
||||
)
|
||||
|
||||
def test_import_course_session_twice(self):
|
||||
"""
|
||||
importing the course session twice should create the
|
||||
`CourseSessionAttendanceCourse` only once
|
||||
"""
|
||||
row = [
|
||||
("ID", "DE 2023 A"),
|
||||
("Generation", 2023),
|
||||
("Region", "Deutschschweiz"),
|
||||
("Sprache", "de"),
|
||||
("Klasse", "A"),
|
||||
("Fahrzeug Start", "06.06.2023, 13:30"),
|
||||
("Fahrzeug Ende", "06.06.2023, 15:00"),
|
||||
(
|
||||
"Fahrzeug Raum",
|
||||
"https://teams.microsoft.com/l/meetup-join/19%3ameeting_N2I5YzViZTQtYTM2Ny00OTYwLTgzNzAtYWI4OTQzODcxNTlj%40thread.v2/0?context=%7b%22Tid%22%3a%22fedd03c8-a756-4803-8f27-0db8f7c488f2%22%2c%22Oid%22%3a%22f92e6382-3884-4e71-a2fd-b305a75d9812%22%7d",
|
||||
),
|
||||
("Fahrzeug Standort", None),
|
||||
("Fahrzeug Adresse", None),
|
||||
]
|
||||
|
||||
data = dict(row)
|
||||
|
||||
cs1 = create_or_update_course_session(
|
||||
self.course, data, language="de", circle_keys=["Fahrzeug"]
|
||||
)
|
||||
attendance_course1 = CourseSessionAttendanceCourse.objects.first()
|
||||
self.assertEqual(
|
||||
attendance_course1.due_date.start.isoformat(), "2023-06-06T11:30:00+00:00"
|
||||
)
|
||||
self.assertEqual(
|
||||
attendance_course1.due_date.end.isoformat(), "2023-06-06T13:00:00+00:00"
|
||||
)
|
||||
|
||||
# import a second time
|
||||
data["Fahrzeug Start"] = "06.06.2023, 13:15"
|
||||
data["Fahrzeug Ende"] = "06.06.2023, 14:45"
|
||||
cs2 = create_or_update_course_session(
|
||||
self.course, data, language="de", circle_keys=["Fahrzeug"]
|
||||
)
|
||||
|
||||
self.assertEqual(cs1.id, cs2.id)
|
||||
attendance_course2 = CourseSessionAttendanceCourse.objects.first()
|
||||
self.assertEqual(
|
||||
attendance_course2.due_date.start.isoformat(), "2023-06-06T11:15:00+00:00"
|
||||
)
|
||||
self.assertEqual(
|
||||
attendance_course2.due_date.end.isoformat(), "2023-06-06T12:45:00+00:00"
|
||||
)
|
||||
self.assertEqual(attendance_course1.id, attendance_course2.id)
|
||||
|
||||
def test_raise_exception_if_header_is_missing(self):
|
||||
data = [
|
||||
("ID", "DE 2023"),
|
||||
]
|
||||
|
||||
with self.assertRaises(DataImportError):
|
||||
validate_row_data(
|
||||
dict(data),
|
||||
[
|
||||
"ID",
|
||||
"Generation",
|
||||
],
|
||||
)
|
||||
|
||||
def test_raise_exception_if_required_field_is_empty(self):
|
||||
data = [
|
||||
("ID", "DE 2023"),
|
||||
("Generation", ""),
|
||||
]
|
||||
|
||||
with self.assertRaises(DataImportError):
|
||||
validate_row_data(
|
||||
dict(data),
|
||||
[
|
||||
"ID",
|
||||
"Generation",
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from django.test import TestCase
|
||||
from openpyxl.reader.excel import load_workbook
|
||||
|
|
@ -44,33 +43,58 @@ class CreateOrUpdateStudentTestCase(TestCase):
|
|||
group="A",
|
||||
)
|
||||
|
||||
def test_create_student(self):
|
||||
row = [
|
||||
("Name", "Rascher"),
|
||||
("Vorname", "Barbara"),
|
||||
("Email", "barbara.rascher@vbv-afa.ch"),
|
||||
("Sprache", "de"),
|
||||
("Durchführungen", "DE 2023 A"),
|
||||
("Datum", datetime(2023, 9, 6, 0, 0)),
|
||||
(None, "VBV"),
|
||||
(None, None),
|
||||
(None, None),
|
||||
(None, None),
|
||||
(None, None),
|
||||
]
|
||||
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(dict(row))
|
||||
def test_create_student(self):
|
||||
create_or_update_student(self.user_dict)
|
||||
|
||||
self.assertEqual(
|
||||
CourseSessionUser.objects.filter(
|
||||
user__email=self.user_dict["Email"]
|
||||
).count(),
|
||||
1,
|
||||
)
|
||||
|
||||
csu = CourseSessionUser.objects.filter(
|
||||
course_session=self.course_session_a,
|
||||
).first()
|
||||
|
||||
self.assertEqual(csu.role, CourseSessionUser.Role.MEMBER)
|
||||
self.assertEqual(csu.user.email, self.user_dict["Email"])
|
||||
self.assertEqual(csu.user.additional_json_data, self.user_dict)
|
||||
|
||||
def test_update_student(self):
|
||||
create_or_update_student(self.user_dict)
|
||||
|
||||
self.user_dict["Email"] = "br@vbv.ch"
|
||||
create_or_update_student(self.user_dict)
|
||||
|
||||
self.assertEqual(
|
||||
CourseSessionUser.objects.filter(
|
||||
user__email="barbara.rascher@vbv-afa.ch"
|
||||
).count(),
|
||||
0,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
CourseSessionUser.objects.filter(
|
||||
user__additional_json_data__Lehrvertragsnummer=self.user_dict[
|
||||
"Lehrvertragsnummer"
|
||||
]
|
||||
).count(),
|
||||
1,
|
||||
)
|
||||
|
||||
csu = CourseSessionUser.objects.get(
|
||||
csu = CourseSessionUser.objects.filter(
|
||||
course_session=self.course_session_a,
|
||||
)
|
||||
).first()
|
||||
|
||||
self.assertEqual(csu.role, CourseSessionUser.Role.MEMBER)
|
||||
self.assertEqual(csu.user.email, "barbara.rascher@vbv-afa.ch")
|
||||
self.assertEqual(csu.user.additional_json_data, self.user_dict)
|
||||
|
|
|
|||
|
|
@ -58,19 +58,29 @@ class CreateOrUpdateTrainerTestCase(TestCase):
|
|||
)
|
||||
|
||||
def test_create_trainer(self):
|
||||
row = [
|
||||
("Name", "Hänni"),
|
||||
("Vorname", "Fabienne"),
|
||||
("Email", "fabienne.haenni@vbv-afa.ch"),
|
||||
("Sprache", "de"),
|
||||
("Generation", "DE 2023"),
|
||||
("Klasse", "A, B"),
|
||||
("Circles", "Fahrzeug (A, B), Reisen (A), KMU (B)"),
|
||||
("Status Referenten", "ok"),
|
||||
(None, "Schulung D"),
|
||||
rows = [
|
||||
[
|
||||
("Name", "Hänni"),
|
||||
("Vorname", "Fabienne"),
|
||||
("Email", "fabienne.haenni@vbv-afa.ch"),
|
||||
("Sprache", "de"),
|
||||
("Generation", "DE 2023"),
|
||||
("Klasse", "A"),
|
||||
("Circles", "Fahrzeug, Haushalt Teil 1"),
|
||||
],
|
||||
[
|
||||
("Name", "Hänni"),
|
||||
("Vorname", "Fabienne"),
|
||||
("Email", "fabienne.haenni@vbv-afa.ch"),
|
||||
("Sprache", "de"),
|
||||
("Generation", "DE 2023"),
|
||||
("Klasse", "B"),
|
||||
("Circles", "Fahrzeug, Haushalt Teil 2"),
|
||||
],
|
||||
]
|
||||
|
||||
create_or_update_trainer(self.course, dict(row))
|
||||
for row in rows:
|
||||
create_or_update_trainer(self.course, dict(row))
|
||||
|
||||
self.assertEqual(
|
||||
CourseSessionUser.objects.filter(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import traceback
|
||||
from typing import Callable
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.shortcuts import redirect, render
|
||||
|
||||
from vbv_lernwelt.importer.services import (
|
||||
import_course_sessions_from_excel,
|
||||
import_students_from_excel,
|
||||
import_trainers_from_excel_for_training,
|
||||
sync_students_from_t2l_excel,
|
||||
)
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def coursesessions_trainers_import(request):
|
||||
return handle_import(
|
||||
request,
|
||||
"Die Durchführungen und Trainer wurden erfolgreich importiert!",
|
||||
import_training_and_trainer,
|
||||
)
|
||||
|
||||
|
||||
def import_training_and_trainer(excel_file: str):
|
||||
import_course_sessions_from_excel(
|
||||
excel_file,
|
||||
)
|
||||
import_trainers_from_excel_for_training(excel_file)
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def coursesessions_students_import(request):
|
||||
return handle_import(
|
||||
request,
|
||||
"Die Teilnehmer wurden erflogreich importiert!",
|
||||
import_students_from_excel,
|
||||
)
|
||||
|
||||
|
||||
@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]):
|
||||
if request.method == "POST" and request.FILES["excel_file"]:
|
||||
excel_file = request.FILES["excel_file"]
|
||||
try:
|
||||
importer(excel_file)
|
||||
except Exception as e:
|
||||
return render(
|
||||
# it is a "power" feature, so we will output the traceback on error
|
||||
request,
|
||||
"admin/importer/error.html",
|
||||
{"error": traceback.format_exc()},
|
||||
)
|
||||
|
||||
messages.info(request, success_msg)
|
||||
return redirect("admin:index")
|
||||
|
|
@ -5,12 +5,33 @@
|
|||
{% include "admin/app_list.html" with app_list=app_list show_changelinks=True %}
|
||||
|
||||
<div class="content">
|
||||
<h1>Export üK</h1>
|
||||
<h2>Edoniq Teilnehmer</h2>
|
||||
<h1>üK</h1>
|
||||
<h1>Excel Import üK</h1>
|
||||
<h2>Durchführungen und Trainer</h2>
|
||||
<form method="post" enctype="multipart/form-data" action="/server/importer/coursesession-trainer-import/">
|
||||
{% csrf_token %}
|
||||
<input type="file" name="excel_file" accept=".xlsx, .xls">
|
||||
<input type="submit" value="Durchführungen und Trainer importieren">
|
||||
</form>
|
||||
|
||||
<h2>Teilnehmer</h2>
|
||||
<form method="post" enctype="multipart/form-data" action="/server/importer/coursesession-students-import/">
|
||||
{% csrf_token %}
|
||||
<input type="file" name="excel_file" accept=".xlsx, .xls">
|
||||
<input type="submit" value="Teilnehmer importieren">
|
||||
</form>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
<h2>Reset</h2>
|
||||
<h1>Reset</h1>
|
||||
<form action="/api/core/cypressreset/" method="post">
|
||||
{% csrf_token %}
|
||||
<button class="btn" name="">Testdaten zurück setzen</button>
|
||||
|
|
|
|||
Loading…
Reference in New Issue