vbv/server/vbv_lernwelt/importer/services.py

1041 lines
34 KiB
Python

from datetime import date, datetime
from typing import Any, Dict, List
import structlog
from django.utils import timezone
from vbv_lernwelt.assignment.models import AssignmentType
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 (
CourseSessionAssignment,
CourseSessionAttendanceCourse,
CourseSessionEdoniqTest,
)
from vbv_lernwelt.importer.utils import (
calc_header_tuple_list_from_pyxl_sheet,
parse_circle_group_string,
try_parse_datetime,
)
from vbv_lernwelt.learnpath.models import (
Circle,
LearningContentAssignment,
LearningContentAttendanceCourse,
LearningContentEdoniqTest,
)
from vbv_lernwelt.notify.models import NotificationCategory
from vbv_lernwelt.sso.role_sync.services import create_and_update_user, create_user
logger = structlog.get_logger(__name__)
# it's easier to define the data here, constructing slugs is error-prone and there are some exceptions
LP_DATA = {
"Kickoff": {
"de": {
"title": "Kickoff",
"slug": "kickoff",
"presence_course": "kickoff-lc-präsenzkurs-kickoff",
"assignments": [
"kickoff-lc-vorbereitungsauftrag",
"kickoff-lc-redlichkeitserklärung",
"kickoff-lc-reflexion",
],
"edoniq_tests": ["kickoff-lc-wissens-und-verständnisfragen"],
},
"fr": {
"title": "Lancement",
"slug": "lancement",
"presence_course": "lancement-lc-cours-de-présence-lancement",
"assignments": [
"lancement-lc-mandat-de-préparation",
"lancement-lc-déclaration-de-probité",
"lancement-lc-réflexion",
],
"edoniq_tests": [
"lancement-lc-questions-de-connaissances-et-de-compréhension"
],
},
"it": {
"title": "Introduzione",
"slug": "introduzione",
"presence_course": "introduzione-lc-corso-di-presenza-introduzione",
"assignments": [
"introduzione-lc-incarico-di-preparazione",
"introduzione-lc-dichiarazione-di-onestà",
"introduzione-lc-riflessione",
],
"edoniq_tests": ["introduzione-lc-domande-di-conoscenza-e-di-comprensione"],
},
},
"Basis": {
"de": {
"title": "Basis",
"slug": "basis",
"presence_course": "basis-lc-präsenzkurs-basis",
"assignments": [
"basis-lc-vorbereitungsauftrag-circle-basis",
],
"edoniq_tests": ["basis-lc-wissens-und-verständnisfragen"],
},
"fr": {
"title": "Base",
"slug": "base",
"presence_course": "base-lc-cours-de-présence-base",
"assignments": [
"base-lc-mandat-de-préparation",
],
"edoniq_tests": ["base-lc-questions-de-connaissances-et-de-compréhension"],
},
"it": {
"title": "Base",
"slug": "base",
"presence_course": "base-lc-corso-di-presenza-base",
"assignments": [
"base-lc-incarico-di-preparazione",
],
"edoniq_tests": ["base-lc-domande-di-conoscenza-e-di-comprensione"],
},
},
"Fahrzeug": {
"de": {
"title": "Fahrzeug",
"slug": "fahrzeug",
"presence_course": "fahrzeug-lc-präsenzkurs-fahrzeug",
"assignments": [
"fahrzeug-lc-geleitete-fallarbeit-überprüfen-einer-versicherungspolice",
"fahrzeug-lc-fahrzeug-mein-erstes-auto",
],
"edoniq_tests": [],
},
"fr": {
"title": "Véhicule",
"slug": "véhicule",
"presence_course": "véhicule-lc-cours-de-présence-véhicule",
"assignments": [
"véhicule-lc-mandat-de-préparation",
"véhicule-lc-étude-de-cas-dirigée-vérification-dune-police-dassurance",
],
"edoniq_tests": [],
},
"it": {
"title": "Veicolo",
"slug": "veicolo",
"presence_course": "veicolo-lc-corso-di-presenza",
"assignments": [
"veicolo-lc-caso-di-studio-guidato-verifica-di-una-polizza-di-assicurazione",
"veicolo-lc-incarico-di-preparazione",
],
"edoniq_tests": [],
},
},
"Haushalt Teil 1": {
"de": {
"title": "Haushalt Teil 1",
"slug": "haushalt-teil-1",
"presence_course": "haushalt-teil-1-lc-präsenzkurs-haushalt-1",
"assignments": ["haushalt-teil-1-lc-vorbereitungsauftrag"],
"edoniq_tests": ["haushalt-teil-1-lc-wissens-und-verständnisfragen"],
},
"fr": {
"title": "Ménage partie 1",
"slug": "ménage-partie-1",
"presence_course": "ménage-partie-1-lc-cours-de-présence-ménage-partie-1",
"assignments": ["ménage-partie-1-lc-mandat-de-préparation"],
"edoniq_tests": [
"ménage-partie-1-lc-questions-de-connaissances-et-de-compréhension"
],
},
"it": {
"title": "Economica domestica parte 1",
"slug": "economica-domestica-parte-1",
"presence_course": "economica-domestica-parte-1-lc-corso-di-presenza-economica-domestica-parte-1",
"assignments": ["economica-domestica-parte-1-lc-incarico-di-preparazione"],
"edoniq_tests": [
"economica-domestica-parte-1-lc-domande-di-conoscenza-e-di-comprensione"
],
},
},
"Haushalt Teil 2": {
"de": {
"title": "Haushalt Teil 2",
"slug": "haushalt-teil-2",
"presence_course": "haushalt-teil-2-lc-präsenzkurs-haushalt-2",
"assignments": [
"haushalt-teil-2-lc-vorbereitungsauftrag",
"haushalt-teil-2-lc-geleitete-fallarbeit-schadenfall-managen",
],
"edoniq_tests": [],
},
"fr": {
"title": "Ménage partie 2",
"slug": "ménage-partie-2",
"presence_course": "ménage-partie-2-lc-cours-de-présence-ménage-partie-2",
"assignments": [
"ménage-partie-2-lc-mandat-de-préparation",
"ménage-partie-2-lc-étude-de-cas-dirigée-gérer-un-cas-de-sinistre",
],
"edoniq_tests": [],
},
"it": {
"title": "Economica domestica parte 2",
"slug": "economica-domestica-parte-2",
"presence_course": "economica-domestica-parte-2-lc-corso-di-presenza-economica-domestica-parte-2",
"assignments": [
"economica-domestica-parte-2-lc-incarico-di-preparazione",
"economica-domestica-parte-2-lc-caso-di-studio-guidato-gestione-di-un-sinistro",
],
"edoniq_tests": [],
},
},
"Reisen & Rechtsstreitigkeiten": {
"de": {
"title": "Reisen & Rechtsstreitigkeiten",
"slug": "reisen-rechtsstreitigkeiten",
"presence_course": "reisen-rechtsstreitigkeiten-lc-präsenzkurs-reisen-rechtsstreitigkeiten",
"assignments": [
"reisen-rechtsstreitigkeiten-lc-vorbereitungsauftrag",
],
"edoniq_tests": [
"reisen-rechtsstreitigkeiten-lc-wissens-und-verständnisfragen"
],
},
"fr": {
"title": "Voyages / Litiges juridiques",
"slug": "voyages-litiges-juridiques",
"presence_course": "voyages-litiges-juridiques-lc-cours-de-présence-voyages-litiges-juridiques",
"assignments": [
"voyages-litiges-juridiques-lc-mandat-de-préparation",
],
"edoniq_tests": [
"voyages-litiges-juridiques-lc-questions-de-connaissances-et-de-compréhension"
],
},
"it": {
"title": "Viaggi e controversie giuridiche",
"slug": "viaggi-e-controversie-giuridiche",
"presence_course": "viaggi-e-controversie-giuridiche-lc-corso-di-presenza-viaggi-e-controversie-giuridiche",
"assignments": [
"viaggi-e-controversie-giuridiche-lc-incarico-di-preparazione",
],
"edoniq_tests": [
"viaggi-e-controversie-giuridiche-lc-domande-di-conoscenza-e-di-comprensione"
],
},
},
"Wohneigentum": {
"de": {
"title": "Wohneigentum",
"slug": "wohneigentum",
"presence_course": "wohneigentum-lc-präsenzkurs-wohneigentum",
"assignments": [
"wohneigentum-lc-vorbereitungsauftrag",
"wohneigentum-lc-geleitete-fallarbeit-von-der-baustelle-zum-eigenheim",
],
"edoniq_tests": [],
},
"fr": {
"title": "Propriété du logement",
"slug": "propriété-du-logement",
"presence_course": "propriété-du-logement-lc-cours-de-présence-propriété-du-logement",
"assignments": [
"propriété-du-logement-lc-mandat-de-préparation",
"propriété-du-logement-lc-etude-de-cas-dirigée",
],
"edoniq_tests": [],
},
"it": {
"title": "Casa di proprietà",
"slug": "casa-di-proprietà",
"presence_course": "casa-di-proprietà-lc-corso-di-presenza-casa-di-proprietà",
"assignments": [
"casa-di-proprietà-lc-incarico-di-preparazione",
"casa-di-proprietà-lc-caso-di-studio-guidato-dal-cantiere-alla-propria-casa",
],
"edoniq_tests": [],
},
},
"KMU Teil 1": {
"de": {
"title": "KMU Teil 1",
"slug": "kmu-teil-1",
"presence_course": "kmu-teil-1-lc-präsenzkurs-kmu-teil-1",
"assignments": [
"kmu-teil-1-lc-vorbereitungsauftrag",
],
"edoniq_tests": ["kmu-teil-1-lc-wissens-und-verständnisfragen"],
},
"fr": {
"title": "PME, Partie 1",
"slug": "pme-partie-1",
"presence_course": "pme-partie-1-lc-cours-de-présence-pme-partie-1",
"assignments": [
"pme-partie-1-lc-mandat-de-préparation",
],
"edoniq_tests": [
"pme-partie-1-lc-questions-de-connaissances-et-de-compréhension-placeholder"
],
},
"it": {
"title": "PMI parte 1",
"slug": "PMI parte 1",
"presence_course": "pmi-parte-1-lc-corso-di-presenza-pmi-parte-1",
"assignments": [
"pmi-parte-1-lc-incarico-di-preparazione",
],
"edoniq_tests": ["pmi-parte-1-lc-domande-di-conoscenza-e-di-comprensione"],
},
},
"KMU Teil 2": {
"de": {
"title": "KMU Teil 2",
"slug": "kmu-teil-2",
"presence_course": "kmu-teil-2-lc-präsenzkurs-kmu-teil-2",
"assignments": [
"kmu-teil-2-lc-vorbereitungsauftrag",
"kmu-teil-2-lc-geleitete-fallarbeit-kmu-betrieb-besuchen",
],
"edoniq_tests": [],
},
"fr": {
"title": "PME, Partie 2",
"slug": "pme-partie-2",
"presence_course": "pme-partie-2-lc-cours-de-présence-pme-partie-2",
"assignments": [
"pme-partie-2-lc-mandat-de-préparation",
"pme-partie-2-lc-etude-de-cas-dirigée",
],
"edoniq_tests": [],
},
"it": {
"title": "PMI parte 2",
"slug": "PMI parte 2",
"presence_course": "pmi-parte-2-lc-corso-di-presenza-pmi-parte-2",
"assignments": [
"pmi-parte-2-lc-incarico-di-preparazione",
"pmi-parte-2-lc-caso-di-studio-guidato-visitare-unazienda-pmi",
],
"edoniq_tests": [],
},
},
"3-Säulenkonzept": {
"de": {
"title": "3-säulenkonzept",
"slug": "3-säulenkonzept",
"presence_course": "3-säulensystem-lc-präsenzkurs-3-säulenkonzept",
"assignments": [
"3-säulensystem-lc-vorbereitungsauftrag",
],
"edoniq_tests": ["3-säulensystem-lc-wissens-und-verständnisfragen"],
},
"fr": {
"title": "Concept des 3 piliers",
"slug": "concept-des-3-piliers",
"presence_course": "concept-des-3-piliers-lc-cours-de-présence-concept-des-3-piliers",
"assignments": [
"concept-des-3-piliers-lc-mandat-de-préparation",
],
"edoniq_tests": [
"concept-des-3-piliers-lc-questions-de-connaissances-et-de-compréhension"
],
},
"it": {
"title": "Concetto dei 3 pilastri",
"slug": "concetto-dei-3-pilastri",
"presence_course": "concetto-dei-3-pilastri-lc-corso-di-presenza-concetto-dei-3-pilastri",
"assignments": [
"concetto-dei-3-pilastri-lc-incarico-di-preparazione",
],
"edoniq_tests": [
"concetto-dei-3-pilastri-lc-domande-di-conoscenza-e-di-comprensione"
],
},
},
"Einkommenssicherung (Invalidität)": {
"de": {
"title": "Einkommenssicherung (Invalidität)",
"slug": "einkommenssicherung-invalidität",
"presence_course": "einkommenssicherung-invalidität-lc-präsenzkurs-einkommenssicherung-invalidität",
"assignments": [
"einkommenssicherung-invalidität-lc-vorbereitungsauftrag",
],
"edoniq_tests": [
"einkommenssicherung-invalidität-lc-wissens-und-verständnisfragen"
],
},
"fr": {
"title": "Garantie des revenus, Partie 1",
"slug": "garantie-des-revenus-partie-1",
"presence_course": "garantie-des-revenus-partie-1-lc-cours-de-présence-garantie-des-revenus-partie-1",
"assignments": [
"garantie-des-revenus-partie-1-lc-mandat-de-préparation",
],
"edoniq_tests": [
"garantie-des-revenus-partie-1-lc-questions-de-connaissances-et-de-compréhension"
],
},
"it": {
"title": "Protezione del reddito parte 1",
"slug": "protezione-del-reddito-parte-1",
"presence_course": "protezione-del-reddito-parte-1-lc-corso-di-presenza-protezione-del-reddito-parte-1",
"assignments": [
"protezione-del-reddito-parte-1-lc-incarico-di-preparazione",
],
"edoniq_tests": [
"protezione-del-reddito-parte-1-lc-domande-di-conoscenza-e-di-comprensione"
],
},
},
"Einkommenssicherung (Todesfall)": {
"de": {
"title": "Einkommenssicherung (Todesfall)",
"slug": "einkommenssicherung-todesfall",
"presence_course": "einkommenssicherung-todesfall-lc-präsenzkurs-einkommenssicherung-todesfall",
"assignments": [
"einkommenssicherung-todesfall-lc-vorbereitungsauftrag",
],
"edoniq_tests": [
"einkommenssicherung-todesfall-lc-wissens-und-verständnisfragen"
],
},
"fr": {
"title": "Garantie des revenus, Partie 2",
"slug": "garantie-des-revenus-partie-2",
"presence_course": "garantie-des-revenus-partie-2-lc-cours-de-présence-garantie-des-revenus-partie-2",
"assignments": [
"garantie-des-revenus-partie-2-lc-mandat-de-préparation",
],
"edoniq_tests": [
"garantie-des-revenus-partie-2-lc-questions-de-connaissances-et-de-compréhension"
],
},
"it": {
"title": "Protezione del reddito parte 2",
"slug": "protezione-del-reddito-parte-2",
"presence_course": "protezione-del-reddito-parte-2-lc-corso-di-presenza-protezione-del-reddito-parte-2",
"assignments": [
"protezione-del-reddito-parte-2-lc-incarico-di-preparazione",
],
"edoniq_tests": [
"protezione-del-reddito-parte-2-lc-domande-di-conoscenza-e-di-comprensione"
],
},
},
"Pensionierung": {
"de": {
"title": "Penionierung",
"slug": "pensionierung",
"presence_course": "pensionierung-lc-präsenzkurs-pensionierung",
"assignments": [
"pensionierung-lc-vorbereitungsauftrag",
"pensionierung-lc-geleitete-fallarbeit",
],
"edoniq_tests": [],
},
"fr": {
"title": "Retraite",
"slug": "retraite",
"presence_course": "retraite-lc-cours-de-présence-retraite",
"assignments": [
"retraite-lc-mandat-de-préparation",
"retraite-lc-etude-de-cas-dirigée",
],
"edoniq_tests": [],
},
"it": {
"title": "Pensionamento",
"slug": "pensionamento",
"presence_course": "pensionamento-lc-corso-di-presenza-pensionamento",
"assignments": [
"pensionamento-lc-incarico-di-preparazione",
"pensionamento-lc-caso-di-studio-guidato",
],
"edoniq_tests": [],
},
},
}
# the request is always, so we cannot rely on the language in the request
TRANSLATIONS = {
"de": {
"raum": "Raum",
"standort": "Standort",
"adresse": "Adresse",
"start": "Start",
"ende": "Ende",
},
"fr": {
"raum": "Raum",
"standort": "Standort",
"adresse": "Adresse",
"start": "Start",
"ende": "Ende",
},
"it": {
"raum": "Raum",
"standort": "Standort",
"adresse": "Adresse",
"start": "Start",
"ende": "Ende",
},
}
T2L_IGNORE_FIELDS = ["Vorname", "Name", "Email", "Sprache", "Durchführungen"]
EDONIQ_TEST_PERIOD = 14
class DataImportError(Exception):
pass
def create_or_update_user(
email: str,
first_name: str = "",
last_name: str = "",
sso_id: str = None,
contract_number: str = "",
date_of_birth: str = "",
intermediate_sso_id: str = "", # from keycloak
) -> User:
logger.debug(
"create_or_update_user",
email=email,
first_name=first_name,
last_name=last_name,
sso_id=sso_id,
label="import",
)
user = None
if sso_id:
user_qs = User.objects.filter(sso_id=sso_id)
if user_qs.exists():
user = user_qs.first()
# use the ID from DBPLAP2 (Lehrvertragsnummer, firstname, lastname, date of birth)
if not user and contract_number:
user_qs = User.objects.filter(
first_name=first_name,
last_name=last_name,
additional_json_data__Lehrvertragsnummer=contract_number,
additional_json_data__Geburtsdatum=date_of_birth,
)
if user_qs.exists():
user = user_qs.first()
if not user:
user_qs = User.objects.filter(email=email)
if user_qs.exists():
user = user_qs.first()
if not user:
# create user
user = User(
sso_id=sso_id,
email=email,
username=email,
)
user.email = email
user.sso_id = user.sso_id or sso_id
user.first_name = first_name or user.first_name
user.last_name = last_name or user.last_name
user.username = email
user.update_additional_json_data({"intermediate_sso_id": intermediate_sso_id})
init_notification_settings(user)
user.set_unusable_password()
user.save()
return user
def import_course_sessions_from_excel(
filename: str, course: Course = None, restrict_language=None, circle_keys=None
):
if circle_keys is None:
circle_keys = [
"Kickoff",
"Basis",
"Fahrzeug",
]
from openpyxl.reader.excel import load_workbook
workbook = load_workbook(filename=filename)
sheet = workbook["Schulungen Durchführung"]
no_course = course is None
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 no_course:
course = get_uk_course(language)
create_or_update_course_session(
course,
data,
language,
circle_keys=circle_keys,
)
def create_or_update_course_session(
course: Course,
data: Dict[str, Any],
language: str,
circle_keys=None,
lp_data=LP_DATA,
):
"""
: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,
data=data,
label="import",
)
if not circle_keys:
circle_keys = []
group = data["Klasse"].strip()
import_id = data["ID"].strip()
generation = str(data["Generation"]).strip()
region = data["Region"].strip()
title = f"{region} {generation} {group}"
cs, _created = CourseSession.objects.get_or_create(
course=course, import_id=import_id
)
cs.additional_json_data["import_data"] = data
cs.save()
cs.title = title
cs.generation = generation
cs.region = region
cs.group = group
cs.import_id = import_id
cs.save()
for circle in circle_keys:
circle_data = lp_data[circle][language]
logger.debug("import", data=circle_data)
attendance_course_lc = LearningContentAttendanceCourse.objects.filter(
slug=f"{course.slug}-lp-circle-{circle_data['presence_course']}"
).first()
try:
room = data[f"{circle} {TRANSLATIONS[language]['raum']}"]
place = data[f"{circle} {TRANSLATIONS[language]['standort']}"]
address = data[f"{circle} {TRANSLATIONS[language]['adresse']}"]
location = f"{room}, {place}, {address}"
presence_day_start = try_parse_datetime(
data[f"{circle} {TRANSLATIONS[language]['start']}"]
)[1]
presence_day_end = try_parse_datetime(
data[f"{circle} {TRANSLATIONS[language]['ende']}"]
)[1]
except KeyError as e:
logger.debug("import", type="presence_data", key_error=e)
continue
if attendance_course_lc:
create_or_update_course_session_attendance(
cs,
attendance_course_lc,
course.slug,
circle_data["slug"],
location,
presence_day_start,
presence_day_end,
)
else:
logger.debug(
"import",
type="course_session_attendance",
slug=f"{course.slug}-lp-circle-{circle_data['presence_course']}",
error="Does not exist",
)
for assignment_slug in circle_data["assignments"]:
create_or_update_course_session_assignment(
cs, course.slug, assignment_slug, presence_day_start
)
for test_slug in circle_data["edoniq_tests"]:
create_or_update_course_session_edoniq_test(
cs, course.slug, test_slug, presence_day_start
)
return cs
def create_or_update_course_session_attendance(
cs: CourseSession,
attendance_course_lc: LearningContentAttendanceCourse,
course_slug: str,
circle_slug: str,
location: str,
start: datetime,
end: datetime,
):
logger.debug(
"create_or_update_course_session_attendance",
slug=f"{course_slug}-lp-circle-{circle_slug}",
start=start,
end=end,
)
csa, _created = CourseSessionAttendanceCourse.objects.get_or_create(
course_session=cs, learning_content=attendance_course_lc
)
# trigger save to update due date
csa.save()
csa.location = location
expert = CourseSessionUser.objects.filter(
course_session_id=cs.id,
expert__slug=f"{course_slug}-lp-circle-{circle_slug}",
role=CourseSessionUser.Role.EXPERT,
).first()
if expert:
csa.trainer = f"{expert.user.first_name} {expert.user.last_name}"
if start:
csa.due_date.start = timezone.make_aware(start)
if end:
csa.due_date.end = timezone.make_aware(end)
csa.due_date.save()
csa.save()
def create_or_update_course_session_assignment(
cs: CourseSession,
course_slug: str,
assignment_slug: str,
start: datetime,
):
logger.debug("import", slug=f"{course_slug}-lp-circle-{assignment_slug}")
learning_content = LearningContentAssignment.objects.filter(
slug=f"{course_slug}-lp-circle-{assignment_slug}"
).first()
if learning_content:
csa, _created = CourseSessionAssignment.objects.get_or_create(
course_session=cs,
learning_content=LearningContentAssignment.objects.get(
slug=f"{course_slug}-lp-circle-{assignment_slug}"
),
)
# trigger save to update due date
csa.save()
if (
csa.learning_content.assignment_type == AssignmentType.PREP_ASSIGNMENT.value
and start
):
csa.submission_deadline.start = timezone.make_aware(start)
csa.submission_deadline.end = None
csa.submission_deadline.save()
elif (
csa.learning_content.assignment_type == AssignmentType.CASEWORK.value
and start
):
csa.submission_deadline.start = timezone.make_aware(
start
) + timezone.timedelta(days=30)
csa.submission_deadline.end = None
csa.submission_deadline.save()
csa.evaluation_deadline.start = timezone.make_aware(
start
) + timezone.timedelta(days=60)
csa.evaluation_deadline.end = None
csa.evaluation_deadline.save()
else:
logger.debug(
"import",
type="course_session_assignment",
slug=f"{course_slug}-lp-circle-{assignment_slug}",
error="Does not exist",
)
def create_or_update_course_session_edoniq_test(
cs: CourseSession, course_slug: str, test_slug: str, start: datetime
):
learning_content = LearningContentEdoniqTest.objects.filter(
slug=f"{course_slug}-lp-circle-{test_slug}"
).first()
if learning_content:
cset, _created = CourseSessionEdoniqTest.objects.get_or_create(
course_session=cs, learning_content=learning_content
)
# trigger save to update due date
cset.save()
cset.deadline.start = timezone.make_aware(start) + timezone.timedelta(
days=EDONIQ_TEST_PERIOD
)
cset.deadline.end = None
cset.deadline.save()
else:
logger.debug(
"import",
type="course_session_edoniq_test",
slug=f"{course_slug}-lp-circle-{test_slug}",
error="Does not exist",
)
def validate_row_data(data: Dict[str, any], required_headers: List[str]):
logger.debug(
"validate_row_data_missing_header",
data={"data": data},
label="import",
)
for header in required_headers:
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
):
from openpyxl.reader.excel import load_workbook
workbook = load_workbook(filename=filename)
sheet = workbook["Schulungen Trainer"]
tuple_list = calc_header_tuple_list_from_pyxl_sheet(sheet)
for row in tuple_list:
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,
data=data,
label="import",
)
user = create_or_update_user(
email=data["Email"].lower(),
first_name=data["Vorname"],
last_name=data["Name"],
)
user.language = data["Sprache"]
# create user in intermediate sso i.e. Keycloak
create_and_update_user(user)
init_notification_settings(user)
user.save()
# As the is never set this is the only way to determine the correct course
if user.language != language:
language = user.language
group = data["Klasse"].strip()
# general expert handling
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,
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:
course = course_session.course
# circle expert handling
circle_data = parse_circle_group_string(data["Circles"])
for circle_key in circle_data:
circle_slug = LP_DATA[circle_key][language]["slug"]
# print(circle_name, 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_slug}"
).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()
def import_students_from_excel(filename: str):
from openpyxl.reader.excel import load_workbook
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)
validate_row_data(
data,
[
"Email",
"Vorname",
"Name",
"Sprache",
"Durchführungen",
"Lehrvertragsnummer",
],
)
create_or_update_student(data)
def create_or_update_student(data: Dict[str, Any]):
logger.debug(
"create_or_update_student",
data=data,
label="import",
)
date_of_birth = _get_date_of_birth(data)
user = create_or_update_user(
email=data["Email"].lower(),
first_name=data["Vorname"],
last_name=data["Name"],
contract_number=data.get("Lehrvertragsnummer", ""),
date_of_birth=date_of_birth,
)
user.language = data["Sprache"]
data["intermediate_sso_id"] = create_user(user)
user.update_additional_json_data(data)
user.save()
# general expert handling
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 _get_date_of_birth(data: Dict[str, Any]) -> str:
date_of_birth = data.get("Geburtsdatum", None)
if date_of_birth is None:
return ""
elif date_of_birth is date or date_of_birth is datetime:
return date_of_birth.strftime("%d.%m.%Y")
elif isinstance(date_of_birth, str):
return date_of_birth
def sync_students_from_t2l_excel(filename: str):
from openpyxl.reader.excel import load_workbook
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):
date_of_birth = _get_date_of_birth(data)
try:
user = User.objects.get(
first_name=data["Vorname"],
last_name=data["Name"],
additional_json_data__Lehrvertragsnummer=data["Lehrvertragsnummer"],
additional_json_data__Geburtsdatum=date_of_birth,
)
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
user.update_additional_json_data(data)
user.save()
def init_notification_settings(user: User):
data = {
"email_notification_categories": [str(NotificationCategory.INFORMATION)],
}
user.update_additional_json_data(data)