check for schlagwoerter length in zusatzdaten frontend/backend - likely the cause of finished projects with missing data

This commit is contained in:
Johann Hoffmann
2026-08-06 14:11:44 +02:00
parent ef3015dff2
commit 6a67eb750e
4 changed files with 240 additions and 39 deletions
@@ -22,6 +22,10 @@ use CI3_Events as Events;
class Abgabe extends FHCAPI_Controller
{
// col limits of lehre.tbl_projektarbeit, exceeding them aborts the zusatzdaten update and lead to unpublishable enduploads
const MAX_LEN_SCHLAGWOERTER = 150; // varchar(150)
const MAX_LEN_ABSTRACT = 5000; // text, capped to match the ui counter
const MAX_SEITENANZAHL = 32767; // smallint
/**
* Object initialization
@@ -326,6 +330,10 @@ class Abgabe extends FHCAPI_Controller
// block uploads for quality gate termine that are already graded
$this->checkPaabgabeForGradedStatus($paabgabe_id);
// the posted typ is client state only - verify it against the row so a stale ui cannot push an
// endabgabe through here, which would mark the abgabe without ever writing the zusatzdaten
$this->checkPaabgabeTypForZwischenabgabe($paabgabe_id, $paabgabetyp_kurzbz);
// load the $student_uid by $projektarbeit_id so we dont need any post params
$this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel');
$res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id);
@@ -403,8 +411,10 @@ class Abgabe extends FHCAPI_Controller
$this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general');
}
$this->checkZusatzdatenLengths($schlagwoerter, $schlagwoerter_en, $abstract, $abstract_en, $seitenanzahl);
$this->checkPaabgabeDeadline($paabgabe_id);
$this->checkProjektarbeitForFinishedStatus($projektarbeit_id);
// load the $student_uid by $projektarbeit_id so we dont need any post params
@@ -453,17 +463,25 @@ class Abgabe extends FHCAPI_Controller
$this->checkAbgabeSignatur($paabgabe, $projektarbeit->student_uid);
$signaturstatus = $paabgabe->signatur;
// update projektarbeit cols with zusatzdaten AND abgabedatum!
$this->ProjektarbeitModel->update($projektarbeit->projektarbeit_id, array(
'sprache' => $sprache,
'seitenanzahl' => $seitenanzahl,
'abgabedatum' => date('Y-m-d'),
'schlagwoerter_en' => $schlagwoerter_en,
'schlagwoerter' => $schlagwoerter,
'abstract' => $abstract,
'abstract_en' => $abstract_en
));
// update projektarbeit cols with zusatzdaten AND abgabedatum!
$updateRes = $this->ProjektarbeitModel->update($projektarbeit->projektarbeit_id, array(
'sprache' => $sprache,
'seitenanzahl' => $seitenanzahl,
'abgabedatum' => date('Y-m-d'),
'schlagwoerter_en' => $schlagwoerter_en,
'schlagwoerter' => $schlagwoerter,
'abstract' => $abstract,
'abstract_en' => $abstract_en
));
// abort before marking the abgabe - a failed update used to leave the projektarbeit without
// abgabedatum and zusatzdaten while the endupload counted as done, blocking the publication
if(isError($updateRes)) {
$this->logLib->logInfoDB(array('endupload projektarbeit update failed', getError($updateRes),
$projektarbeit_id, $paabgabe_id, getAuthUID(), getAuthPersonId()));
$this->terminateWithError($this->p->t('abgabetool', 'c4enduploadZusatzdatenSpeichernFehlgeschlagen'), 'general');
}
// update paabgabe datum
$res = $this->PaabgabeModel->update($paabgabe_id, array(
@@ -1070,6 +1088,22 @@ class Abgabe extends FHCAPI_Controller
$this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general');
}
// a termin with an upload or a note must keep its typ and datum.
// the table only hides those rows client side, so a student upload
// after the table was loaded would otherwise still let a zwischenabgabe be retyped to 'end'
$existingPaabgabe = $paabgabeArr[0];
if($existingPaabgabe->note !== null || $existingPaabgabe->abgabedatum !== null) {
if(isset($updateFields['paabgabetyp_kurzbz'])
&& $updateFields['paabgabetyp_kurzbz'] !== $existingPaabgabe->paabgabetyp_kurzbz) {
$this->terminateWithError($this->p->t('abgabetool', 'c4abgabetypAendernNichtErlaubt'));
}
if(isset($updateFields['datum']) && $updateFields['datum'] !== $existingPaabgabe->datum) {
$this->terminateWithError($this->p->t('abgabetool', 'c4datumAendernNichtErlaubt'));
}
}
$result = $this->PaabgabeModel->update(
$paabgabe_id,
array_merge($updateFields, [
@@ -1583,7 +1617,9 @@ class Abgabe extends FHCAPI_Controller
$this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general');
}
$this->checkZusatzdatenLengths($schlagwoerter, $schlagwoerter_en, $abstract, $abstract_en, $seitenanzahl);
$this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel');
@@ -1618,7 +1654,7 @@ class Abgabe extends FHCAPI_Controller
}
// update projektarbeit cols with zusatzdaten only
$this->ProjektarbeitModel->update($projektarbeit_id, array(
$updateRes = $this->ProjektarbeitModel->update($projektarbeit_id, array(
'sprache' => $sprache,
'seitenanzahl' => $seitenanzahl,
'schlagwoerter_en' => $schlagwoerter_en,
@@ -1627,6 +1663,9 @@ class Abgabe extends FHCAPI_Controller
'abstract_en' => $abstract_en
));
// dont report success on a failed update, otherwise a repair attempt silently changes nothing
$this->getDataOrTerminateWithError($updateRes, 'general');
$this->logLib->logInfoDB(array('zusatzdatenEditMitarbeiter', array(
'updatevon' => getAuthUID(),
'updateamum' => date('Y-m-d H:i:s')
@@ -2009,16 +2048,48 @@ class Abgabe extends FHCAPI_Controller
$data = getData($res)[0];
if($data->note !== NULL) {
// hardcode this error msg cause phrasen arent always being updated
$message = $this->p->t('abgabetool','c4fehlerAktualitaetProjektarbeitv3');
if(strpos($message, "<<") === 0) { // phrase could not be loaded
$this->terminateWithError('Die Projektarbeit wurde bereits benotet, Sie dürfen deshalb keine weiteren Termine anlegen oder bearbeiten.', 'general');
} else {
$this->terminateWithError($message);
}
$this->terminateWithError($this->p->t('abgabetool','c4fehlerAktualitaetProjektarbeitv3'));
}
}
// validates the zusatzdaten against the column limits of lehre.tbl_projektarbeit.
private function checkZusatzdatenLengths($schlagwoerter, $schlagwoerter_en, $abstract, $abstract_en, $seitenanzahl) {
if(mb_strlen((string)$schlagwoerter) > self::MAX_LEN_SCHLAGWOERTER
|| mb_strlen((string)$schlagwoerter_en) > self::MAX_LEN_SCHLAGWOERTER) {
$this->terminateWithError($this->p->t('abgabetool', 'c4schlagwoerterZuLang', array(self::MAX_LEN_SCHLAGWOERTER)), 'general');
}
if(mb_strlen((string)$abstract) > self::MAX_LEN_ABSTRACT
|| mb_strlen((string)$abstract_en) > self::MAX_LEN_ABSTRACT) {
$this->terminateWithError($this->p->t('abgabetool', 'c4abstractZuLang', array(self::MAX_LEN_ABSTRACT)), 'general');
}
if(!is_numeric($seitenanzahl) || (int)$seitenanzahl < 1 || (int)$seitenanzahl > self::MAX_SEITENANZAHL) {
$this->terminateWithError($this->p->t('abgabetool', 'c4seitenanzahlUngueltig', array(self::MAX_SEITENANZAHL)), 'general');
}
}
// zwischenabgabe uploads must never touch an endabgabe termin, to avoid empty zusatzdaten
private function checkPaabgabeTypForZwischenabgabe($paabgabe_id, $posted_paabgabetyp_kurzbz) {
$this->load->model('education/Paabgabe_model', 'PaabgabeModel');
$res = $this->PaabgabeModel->load($paabgabe_id);
if(isError($res) || !hasData($res)) {
$this->terminateWithError($this->p->t('abgabetool', 'c4projektabgabeNichtGefunden'), 'general');
}
$paabgabe = getData($res)[0];
if($paabgabe->paabgabetyp_kurzbz === 'end') {
$this->terminateWithError($this->p->t('abgabetool', 'c4enduploadUeberZwischenabgabeNichtErlaubt'), 'general');
}
// typ changed between page load and upload -> the client would use the wrong upload path
if($paabgabe->paabgabetyp_kurzbz !== $posted_paabgabetyp_kurzbz) {
$this->terminateWithError($this->p->t('abgabetool', 'c4abgabeterminGeaendertNeuLaden'), 'general');
}
}
private function checkPaabgabeForGradedStatus($paabgabe_id) {
$this->load->model('education/Paabgabe_model', 'PaabgabeModel');
$res = $this->PaabgabeModel->load($paabgabe_id);
@@ -2029,13 +2100,7 @@ class Abgabe extends FHCAPI_Controller
$paabgabe = getData($res)[0];
if($paabgabe->note !== NULL) {
// hardcode a fallback cause phrasen arent reliable
$message = $this->p->t('abgabetool', 'c4studentAbgabeNotAllowedRegularv3');
if(strpos($message, "<<") === 0) { // phrase could not be loaded
$this->terminateWithError('Uploads sind für bereits benotete Quality Gates gesperrt. Sollten Sie trotzdem etwas hochladen wollen, wenden Sie sich bitte an Ihre Studiengangsassistenz.', 'general');
} else {
$this->terminateWithError($message);
}
$this->terminateWithError($this->p->t('abgabetool', 'c4studentAbgabeNotAllowedRegularv3'));
}
}
@@ -442,7 +442,12 @@ export const AbgabeMitarbeiterDetail = {
return true
},
allowedToSaveZusatzdaten() {
return this.form.schlagwoerter.length > 0 && this.form.schlagwoerter_en.length > 0 && this.form.abstract.length > 0 && this.form.abstract_en.length > 0 && this.form.seitenanzahl > 0
// upper bounds mirror the column limits of lehre.tbl_projektarbeit, see Abgabe.php
return this.form.schlagwoerter.length > 0 && this.form.schlagwoerter.length <= 150
&& this.form.schlagwoerter_en.length > 0 && this.form.schlagwoerter_en.length <= 150
&& this.form.abstract.length > 0 && this.form.abstract.length <= 5000
&& this.form.abstract_en.length > 0 && this.form.abstract_en.length <= 5000
&& this.form.seitenanzahl > 0 && this.form.seitenanzahl <= 32767
},
getAllowedAbgabeTypeOptions() {
if(this.assistenzMode) {
@@ -1003,14 +1008,16 @@ export const AbgabeMitarbeiterDetail = {
<div class="row mb-3 align-items-start">
<div class="row">{{$capitalize( $p.t('abgabetool/c4schlagwoerterGer') )}}</div>
<div class="row">
<Textarea v-model="form.schlagwoerter" class="w-100"></Textarea>
<Textarea v-model="form.schlagwoerter" maxlength="150" class="w-100"></Textarea>
<p>{{ form.schlagwoerter?.length ? form.schlagwoerter.length : 0 }} / 150 characters</p>
</div>
</div>
<div class="row mb-3 align-items-start">
<div class="row">{{$capitalize( $p.t('abgabetool/c4schlagwoerterEng') )}}</div>
<div class="row">
<Textarea v-model="form.schlagwoerter_en" class="w-100"></Textarea>
<Textarea v-model="form.schlagwoerter_en" maxlength="150" class="w-100"></Textarea>
<p>{{ form.schlagwoerter_en?.length ? form.schlagwoerter_en.length : 0 }} / 150 characters</p>
</div>
</div>
@@ -1035,7 +1042,7 @@ export const AbgabeMitarbeiterDetail = {
<div class="row">
<InputNumber
v-model="form.seitenanzahl"
inputId="seitenanzahlInput" :min="1" :max="100000">
inputId="seitenanzahlInput" :min="1" :max="32767">
</InputNumber>
</div>
</div>
@@ -258,7 +258,9 @@ export const AbgabeStudentDetail = {
return false;
}
if(termin.bezeichnung?.paabgabetyp_kurzbz === 'end') {
// use the field from the backend, not the client side mapped bezeichnung object - a failed
// lookup there would silently route an endupload through the zwischenabgabe endpoint
if(termin.paabgabetyp_kurzbz === 'end') {
this.enduploadTermin = termin
this.$refs.modalContainerEnduploadZusatzdaten.show()
} else {
@@ -365,7 +367,12 @@ export const AbgabeStudentDetail = {
return this.$capitalize(this.$p.t('abgabetool/c4eidesstattlicheErklaerung'))
},
allowedToSaveZusatzdaten() {
return this.form.schlagwoerter.length > 0 && this.form.schlagwoerter_en.length > 0 && this.form.abstract.length > 0 && this.form.abstract_en.length > 0 && this.form.seitenanzahl > 0
// upper bounds mirror the column limits of lehre.tbl_projektarbeit, see Abgabe.php
return this.form.schlagwoerter.length > 0 && this.form.schlagwoerter.length <= 150
&& this.form.schlagwoerter_en.length > 0 && this.form.schlagwoerter_en.length <= 150
&& this.form.abstract.length > 0 && this.form.abstract.length <= 5000
&& this.form.abstract_en.length > 0 && this.form.abstract_en.length <= 5000
&& this.form.seitenanzahl > 0 && this.form.seitenanzahl <= 32767
},
getAllowedToSendEndupload() {
return this.eidAkzeptiert && this.allowedToSaveZusatzdaten
@@ -733,14 +740,16 @@ export const AbgabeStudentDetail = {
<div class="row mb-3 align-items-start">
<div class="row">{{$capitalize( $p.t('abgabetool/c4schlagwoerterGer') )}}</div>
<div class="row">
<Textarea v-model="form.schlagwoerter" class="w-100"></Textarea>
<Textarea v-model="form.schlagwoerter" maxlength="150" class="w-100"></Textarea>
<p>{{ form.schlagwoerter?.length ? form.schlagwoerter.length : 0 }} / 150 characters</p>
</div>
</div>
<div class="row mb-3 align-items-start">
<div class="row">{{$capitalize( $p.t('abgabetool/c4schlagwoerterEng') )}}</div>
<div class="row">
<Textarea v-model="form.schlagwoerter_en" class="w-100"></Textarea>
<Textarea v-model="form.schlagwoerter_en" maxlength="150" class="w-100"></Textarea>
<p>{{ form.schlagwoerter_en?.length ? form.schlagwoerter_en.length : 0 }} / 150 characters</p>
</div>
</div>
@@ -765,7 +774,7 @@ export const AbgabeStudentDetail = {
<div class="row">
<InputNumber
v-model="form.seitenanzahl"
inputId="seitenanzahlInput" :min="1" :max="100000">
inputId="seitenanzahlInput" :min="1" :max="32767">
</InputNumber>
</div>
</div>
+120
View File
@@ -48056,6 +48056,126 @@ array(
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4schlagwoerterZuLang',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Die Schlagwörter dürfen maximal {0} Zeichen lang sein.',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Keywords may not exceed {0} characters.',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4abstractZuLang',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Der Abstract darf maximal {0} Zeichen lang sein.',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'The abstract may not exceed {0} characters.',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4seitenanzahlUngueltig',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Die Seitenanzahl muss zwischen 1 und {0} liegen.',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'The number of pages must be between 1 and {0}.',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4enduploadZusatzdatenSpeichernFehlgeschlagen',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Die Zusatzdaten konnten nicht gespeichert werden, der Upload wurde daher nicht abgeschlossen. Bitte wenden Sie sich an Ihre Studiengangsassistenz.',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'The additional data could not be saved, therefore the upload was not completed. Please contact your degree programme assistant.',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4enduploadUeberZwischenabgabeNichtErlaubt',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Dieser Termin ist eine Endabgabe und kann nur über den Endupload inklusive Zusatzdaten hochgeladen werden. Bitte laden Sie die Seite neu.',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'This deadline is a final submission and can only be uploaded via the final upload including the additional data. Please reload the page.',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4abgabeterminGeaendertNeuLaden',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Der Abgabetermin wurde zwischenzeitlich geändert. Bitte laden Sie die Seite neu.',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'The submission deadline has been changed in the meantime. Please reload the page.',
'description' => '',
'insertvon' => 'system'
)
)
),
// ABGABETOOL PHRASEN END
array(
'app' => 'core',