WIP noten/pruefung import

This commit is contained in:
Johann Hoffmann
2025-08-01 09:48:46 +02:00
parent 1f2f866c61
commit e58bf3a8cf
6 changed files with 264 additions and 13 deletions
@@ -31,9 +31,17 @@ class Benotungstool extends Auth_Controller
// TODO: check if related CIS config is also loaded when being routed in Cis4 by vuerouter
// TODO: check if new benotungstool should be configurable the exact same way?
$viewData = array(
'uid'=>getAuthUID(),
'CIS_GESAMTNOTE_UEBERSCHREIBEN' => CIS_GESAMTNOTE_UEBERSCHREIBEN
'CIS_GESAMTNOTE_UEBERSCHREIBEN' => CIS_GESAMTNOTE_UEBERSCHREIBEN,
'CIS_GESAMTNOTE_PRUEFUNG_KOMMPRUEF' => CIS_GESAMTNOTE_PRUEFUNG_KOMMPRUEF,
'CIS_GESAMTNOTE_PRUEFUNG_TERMIN3' => CIS_GESAMTNOTE_PRUEFUNG_TERMIN3,
'CIS_GESAMTNOTE_PRUEFUNG_TERMIN2' => CIS_GESAMTNOTE_PRUEFUNG_TERMIN2,
'CIS_GESAMTNOTE_PRUEFUNG_MOODLE_LE_NOTE' => CIS_GESAMTNOTE_PRUEFUNG_MOODLE_LE_NOTE,
'CIS_GESAMTNOTE_PUNKTE' => CIS_GESAMTNOTE_PUNKTE,
'CIS_GESAMTNOTE_GEWICHTUNG' => CIS_GESAMTNOTE_GEWICHTUNG
);
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'Benotungstool']);
@@ -35,7 +35,8 @@ class Noten extends FHCAPI_Controller
'getNotenvorschlagStudent' => self::PERM_LOGGED,
'saveNotenvorschlag' => self::PERM_LOGGED,
'saveStudentPruefung' => self::PERM_LOGGED,
'createPruefungen' => self::PERM_LOGGED
'createPruefungen' => self::PERM_LOGGED,
'saveNotenvorschlagBulk' => self::PERM_LOGGED
]);
$this->load->library('AuthLib', null, 'AuthLib');
@@ -108,6 +109,8 @@ class Noten extends FHCAPI_Controller
}
// send $grades reference to moodle addon
// TODO: event getExterneNoten
Events::trigger(
'moodleGrades',
function & () use (&$grades)
@@ -556,6 +559,72 @@ class Noten extends FHCAPI_Controller
$this->terminateWithSuccess(array($ret, $lvgesamtnote));
}
public function saveNotenvorschlagBulk() {
$result = $this->getPostJSON();
if(!property_exists($result, 'lv_id') || !property_exists($result, 'sem_kurzbz') ||
!property_exists($result, 'noten')) {
$this->terminateWithError($this->p->t('global', 'missingParameters'), 'general');
}
$lv_id = $result->lv_id;
$sem_kurzbz = $result->sem_kurzbz;
$noten = $result->noten;
$retLvNoten = [];
$responseMsgs = [];
foreach($noten as $note)
{
$lvgesamtnote = new lvgesamtnote();
if (!$lvgesamtnote->load($lv_id, $note->uid, $sem_kurzbz))
{
$lvgesamtnote->student_uid = $note->uid;
$lvgesamtnote->lehrveranstaltung_id = $lv_id;
$lvgesamtnote->studiensemester_kurzbz = $sem_kurzbz;
$lvgesamtnote->note = trim($note->note);
$lvgesamtnote->mitarbeiter_uid = getAuthUID();
$lvgesamtnote->benotungsdatum = date("Y-m-d H:i:s");
$lvgesamtnote->freigabedatum = null;
$lvgesamtnote->freigabevon_uid = null;
$lvgesamtnote->bemerkung = null;
$lvgesamtnote->updateamum = null;
$lvgesamtnote->updatevon = null;
$lvgesamtnote->insertamum = date("Y-m-d H:i:s");
$lvgesamtnote->insertvon = getAuthUID();
$lvgesamtnote->punkte =// TODO: deprecated?
$new = true;
$response = "neu";
}
else
{
$lvgesamtnote->note = trim($note->note);
$lvgesamtnote->punkte = null; // TODO: deprecated?
$lvgesamtnote->benotungsdatum = date("Y-m-d H:i:s");
$lvgesamtnote->updateamum = date("Y-m-d H:i:s");
$lvgesamtnote->updatevon = getAuthUID();
$new = false;
if ($lvgesamtnote->freigabedatum)
$response = "update_f";
else
$response = "update";
}
if (!$lvgesamtnote->save($new))
$responseMsgs[] = $lvgesamtnote->errormsg;
else
$responseMsgs[] = $response;
$lvgesamtnote->load($lv_id, $note->uid, $sem_kurzbz);
$retLvNoten[] = $lvgesamtnote;
}
$this->terminateWithSuccess(array($retLvNoten, $responseMsgs));
}
public function createPruefungen() {
$result = $this->getPostJSON();
@@ -26,7 +26,8 @@ $includesArray = array(
'vendor/npm-asset/primevue/accordion/accordion.js',
'vendor/npm-asset/primevue/accordiontab/accordiontab.js',
'vendor/npm-asset/primevue/password/password.js',
'vendor/npm-asset/primevue/multiselect/multiselect.js'
'vendor/npm-asset/primevue/multiselect/multiselect.js',
'vendor/npm-asset/primevue/textarea/textarea.js'
),
'customJSModules' => array(
'public/js/apps/Dashboard/Fhc.js'
+10
View File
@@ -57,4 +57,14 @@ export default {
params: { uids, datum, lva_id, sem_kurzbz }
};
},
saveNotenvorschlagBulk(lv_id, sem_kurzbz, noten) {
return {
method: 'post',
url: '/api/frontend/v1/Noten/saveNotenvorschlagBulk',
params: { lv_id, sem_kurzbz, noten }
};
},
saveStudentPruefungBulk(lv_id, sem_kurzbz, pruefungen) {
}
};
@@ -12,6 +12,7 @@ export const Benotungstool = {
CoreFilterCmpt,
Dropdown: primevue.dropdown,
Password: primevue.password,
Textarea: primevue.textarea,
Datepicker: VueDatePicker,
Multiselect: primevue.multiselect
},
@@ -49,6 +50,7 @@ export const Benotungstool = {
changedNotenCounter: 0,
tabulatorUuid: Vue.ref(0),
domain: '',
importString: '',
teilnoten: null,
lv: null,
studenten: null,
@@ -107,6 +109,102 @@ export const Benotungstool = {
]};
},
methods: {
parseNote(rowParts, notenbulk) {
const uid = rowParts[0]
const student = this.studenten.find(s => s.uid === uid)
if(!student) return
const note = rowParts[1]
// find notenoption and check if its allowed to use in lehre
const notenOption = this.notenOptions.find(n => n.note == note)
if(!notenOption.lehre) return
notenbulk.push({uid, note})
},
parsePruefung(rowParts, notenbulk) {
const uid = rowParts[0]
const student = this.studenten.find(s => s.uid === uid)
if(!student) return
const datum = rowParts[1] // should be in 'YYYY.MM.DD'
const datumObj = datum
const year = datumObj.getFullYear();
const month = String(datumObj.getMonth() + 1).padStart(2, '0'); // Months are 0-based
const day = String(datumObj.getDate()).padStart(2, '0');
const dateStr = `${year}-${month}-${day}`
const note = rowParts[2]
// find notenoption and check if its allowed to use in lehre
const notenOption = this.notenOptions.find(n => n.note == note)
if(!notenOption.lehre) return
},
saveNotenBulk(notenbulk) {
this.$api.call(ApiNoten.saveNotenvorschlagBulk(this.lv_id, this.sem_kurzbz, notenbulk)).then(res => {
console.log(res)
if(res.meta.status === 'success') {
const lvNoten = res.data[0]
lvNoten.forEach(lvn => {
// 1.) get relevant student row by uid
const s = this.studenten.find(s => s.uid === lvn.uid)
s.note_vorschlag = lvn.note // TODO: check if note_vorschlag should be changed by import
this.teilnoten[s.uid].note_lv = lvn.note
s.freigabedatum = this.parseDate(lvn['freigabedatum'])
s.benotungsdatum = this.parseDate(lvn['benotungsdatum'])
s.freigegeben = this.checkFreigabe(s.freigabedatum, s.benotungsdatum, s.uid);
})
}
// 2.) set note_vorschlag field
// 4.) update rows with note_lv = note_vorschlag & recalculate freigabestatus
})
},
savePruefungBulk(pruefungenbulk) {
this.$api.call(ApiNoten.saveStudentPruefungBulk(this.lv_id, this.sem_kurzbz, pruefungenbulk))
.then((res)=> {
if(res.meta.status === 'success') {
this.$fhcAlert.alertInfo('Prüfungen gespeichert') // TODO: phrase
}
})
},
importNoten() {
console.log('importNoten', this.importString)
// TODO: check for signs of notenimport or pruefung import
const rows = this.importString.split('\n')
const bulk = []
let mode = ''
// read the lines
rows.forEach(r => {
const rowParts = r.split('\t')
if(rowParts.length === 3) {
this.parsePruefung(rowParts, bulk)
mode = 'pruefung' // if line parts are not uniform we are in trouble
} else if(rowParts.length === 2) {
this.parseNote(rowParts, bulk)
mode = 'note'
}
})
if(mode === 'note') this.saveNotenBulk(bulk)
else if (mode === 'pruefung') this.savePruefungBulk(bulk)
this.$refs.modalContainerNotenImport.hide()
},
selectionArraysAreEqual(arr1, arr2) {
if(arr1.length !== arr2.length) return false
@@ -408,7 +506,7 @@ export const Benotungstool = {
const date = `${dateParts[2]}.${dateParts[1]}.${dateParts[0]}`
// First column (date)
rowDiv.appendChild(createCol(date, 'col-4 d-flex align-items-center'));
rowDiv.appendChild(createCol(date, 'col-4 d-flex justify-content-center align-items-center'));
const noteDefEntry = data.note ? this.notenOptions.find(n => n.note == data[field].note) : null
@@ -418,7 +516,7 @@ export const Benotungstool = {
// no actions on kommPruef allowed
// no actions on termin1 aka pruefung 0 aka ursprüngliche note erlaubt
if(field === 'kommPruef' || colDef.originalNote) {
rowDiv.appendChild(createCol('', 'col-4 d-flex justify-content-center align-items-center')); // append empty col4 to have formatting similar
// rowDiv.appendChild(createCol('', 'col-4 d-flex justify-content-center align-items-center')); // append empty col4 to have formatting similar
return rowDiv
}
@@ -450,17 +548,22 @@ export const Benotungstool = {
openPruefungModal(student, pruefung = null, field) {
this.pruefungStudent = student
this.pruefung = pruefung
const dateStr = this.pruefung?.datum ?? field
const pruefungDateParts = dateStr.split('-')
// does not work correctly
// new date obj so datepicker picks ob the change by ref
const newDate = new Date()
newDate.setFullYear(pruefungDateParts[0])
newDate.setMonth(pruefungDateParts[1])
newDate.setMonth(newDate.getMonth() - 1) // acount for js date month offset
newDate.setDate(pruefungDateParts[2])
// const newDate = new Date()
// newDate.setFullYear(+pruefungDateParts[0])
// newDate.setMonth(+pruefungDateParts[1])
// // newDate.setMonth(newDate.getMonth() - 1) // acount for js date month offset
// newDate.setDate(+pruefungDateParts[2])
// works correctly
const newDate = new Date(+pruefungDateParts[0], +pruefungDateParts[1], +pruefungDateParts[2])
newDate.setMonth(newDate.getMonth() - 1)
this.selectedPruefungDate = newDate
@@ -1037,6 +1140,9 @@ export const Benotungstool = {
openNewPruefungsdatumModal() {
this.$refs.modalContainerNeuesPruefungsdatum.show()
},
openNotenImportModal() {
this.$refs.modalContainerNotenImport.show()
},
getOptionLabelNotePruefung(option) {
return option.bezeichnung
},
@@ -1233,12 +1339,13 @@ export const Benotungstool = {
return counter
},
getSaveBtnClass() {
// return "btn btn-primary ml-2"
return this.changedNoten?.length ? "btn btn-primary ml-2" : "btn btn-secondary ml-2"
},
getNewBtnClass() {
return "btn btn-primary ml-2"
// return !this.changedData.length ? "btn btn-secondary ml-2" : "btn btn-primary ml-2"
},
getNotenImportBtnClass() {
return "btn btn-primary ml-2"
},
changedNoten() {
const v = this.changedNotenCounter // hack to trigger computed
@@ -1259,6 +1366,19 @@ export const Benotungstool = {
this.setupMounted()
},
template: `
<bs-modal ref="modalContainerNotenImport" class="bootstrap-prompt" dialogClass="modal-lg">
<template v-slot:title>{{$p.t('benotungstool/c4notenImportieren')}}</template>
<template v-slot:default>
<div class="row mt-4 justify-content-center">
<Textarea v-model="importString" rows="5"></Textarea>
</div>
</template>
<template v-slot:footer>
<button type="button" class="btn btn-primary" @click="importNoten">{{ $p.t('benotungstool/c4import') }}</button>
</template>
</bs-modal>
<bs-modal ref="modalContainerNeuesPruefungsdatum" class="bootstrap-prompt" dialogClass="modal-lg">
<template v-slot:title>{{$p.t('benotungstool/c4addNewPruefung')}}</template>
@@ -1408,6 +1528,9 @@ export const Benotungstool = {
<button @click="openNewPruefungsdatumModal" role="button" :class="getNewBtnClass">
{{$p.t('benotungstool/c4addNewPruefung')}} <i class="fa fa-plus"></i>
</button>
<button @click="openNotenImportModal" role="button" :class="getNotenImportBtnClass">
{{$p.t('benotungstool/c4notenImportieren')}} <i class="fa fa-file-import"></i>
</button>
</template>
</core-filter-cmpt>
</div>
+40
View File
@@ -42036,6 +42036,46 @@ and represent the current state of research on the topic. The prescribed citatio
)
)
),
array(
'app' => 'core',
'category' => 'benotungstool',
'phrase' => 'c4notenImportieren',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Noten Import',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Import Grades',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'benotungstool',
'phrase' => 'c4import',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Importieren',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Import',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'benotungstool',