1041 lines
34 KiB
Python
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)
|