diff --git a/CHANGELOG.md b/CHANGELOG.md index 27830586..6dc95162 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ -# Change log +# Change Log -## Version 1.3 (upcoming) +## Version 1.4 (2024-02-29) +- cleanup cross-task corrector assignments created by wrong imports +- improve corrector assignments export and import + - only login is used to identify writers and correctors + - create writer or corrector on the fly if login exists + +## Version 1.3 (2024-02-24) - unnumbered headlines schemes with one or three levels - switch to allow browser spellcheck for writing a task - fixed 0040291: Imagemagick error in edutiek on ubuntu 22.04 diff --git a/classes/CorrectorAdmin/class.CorrectorAdminGUI.php b/classes/CorrectorAdmin/class.CorrectorAdminGUI.php index 41dabf13..583fca46 100644 --- a/classes/CorrectorAdmin/class.CorrectorAdminGUI.php +++ b/classes/CorrectorAdmin/class.CorrectorAdminGUI.php @@ -30,17 +30,15 @@ */ class CorrectorAdminGUI extends BaseGUI { - - /** @var CorrectorAdminService */ - protected $service; - - /** @var CorrectionSettings */ - protected $settings; + protected CorrectorAdminService $service; + protected CorrectorAssignmentsService $assignment_service; + protected CorrectionSettings $settings; public function __construct(\ilObjLongEssayAssessmentGUI $objectGUI) { parent::__construct($objectGUI); $this->service = $this->localDI->getCorrectorAdminService($this->object->getId()); + $this->assignment_service = $this->localDI->getCorrectorAssignmentService($this->object->getId()); $this->settings = $this->localDI->getTaskRepo()->getCorrectionSettingsById($this->object->getId()); } @@ -696,8 +694,6 @@ protected function confirmRemoveAuthorizationsAsync() public function correctorAssignmentSpreadsheetImport() { - $corrector_repo = $this->localDI->getCorrectorRepo(); - $task_repo = $this->localDI->getTaskRepo(); $tempfile = new ilLongEssayAssessmentUploadTempFile($this->storage, $this->dic->filesystem(), $this->dic->upload()); $form = $this->uiFactory->input()->container()->form()->standard( @@ -707,7 +703,8 @@ public function correctorAssignmentSpreadsheetImport() $this->storage, $tempfile ), - $this->plugin->txt("assignment_excel_import") + $this->plugin->txt("assignment_excel_import"), + $this->plugin->txt("assignment_excel_import_info") )] ); @@ -716,172 +713,36 @@ public function correctorAssignmentSpreadsheetImport() if($data = $form->getData()) { $filename = $data['excel'][0]; - $needed_corr = $task_repo->getCorrectionSettingsById($this->object->getId())->getRequiredCorrectors(); try { - $spreadsheet = new CorrectorAssignmentExcel(); - $spreadsheet->loadFromFile(\ilUtil::getDataDir() . '/temp/' . $filename); - - $spreadsheet->setActiveSheet(1); - $corrector = []; - $r = 2; - while (($id = $spreadsheet->getCell($r, 0)) != null) { - $login = $spreadsheet->getCell($r, 1); - $corrector[$login] = $id; - $r++; - } - $spreadsheet->setActiveSheet(0); - $corrector_assignments = []; - $r = 2; - while (($id = $spreadsheet->getCell($r, 0)) != null) { - $assigned = []; - foreach(range(0, $needed_corr-1) as $pos) { - $login = $spreadsheet->getCell($r, 8+$pos); - $assigned[$pos] = (int) $corrector[$login] ?? CorrectorAdminService::BLANK_CORRECTOR_ASSIGNMENT; - } - - $corrector_assignments[$id] = $assigned; - $r++; - } - - foreach($corrector_assignments as $writer_id => $as) { - $fa = $as[0]; - $sa = $needed_corr == 2 ? $as[1] : CorrectorAdminService::UNCHANGED_CORRECTOR_ASSIGNMENT; - - $this->service->assignMultipleCorrector($fa, $sa, [$writer_id]); - } + $this->assignment_service->importAssignments(\ilUtil::getDataDir() . '/temp/' . $filename); + ilUtil::sendSuccess($this->plugin->txt("corrector_assignment_change_file_success"), true); $tempfile->removeTempFile($filename); - ilUtil::sendSuccess($this->plugin->txt("corrector_assignment_changed"), true); $this->ctrl->redirect($this); - } catch (\Exception $exception) { + } + catch (CorrectorAssignmentsException $exception) { + $tempfile->removeTempFile($filename); + ilUtil::sendFailure($this->plugin->txt("corrector_assignment_change_file_failure") + . '
' . nl2br($exception->getMessage()), true) . '
'; + } + catch (\Exception $exception) { $tempfile->removeTempFile($filename); - ilUtil::sendFailure($this->plugin->txt("corrector_assignment_change_file_failure"), true); + ilUtil::sendFailure($this->plugin->txt("corrector_assignment_change_file_failure")); } } } - ilUtil::sendInfo($this->plugin->txt("change_corrector_info")); + $this->tpl->setContent($this->renderer->render($form)); } public function correctorAssignmentSpreadsheetExport() { - $corrector_repo = $this->localDI->getCorrectorRepo(); - $writer_repo = $this->localDI->getWriterRepo(); - $task_repo = $this->localDI->getTaskRepo(); - $essay_repo = $this->localDI->getEssayRepo(); - $needed_corr = $task_repo->getCorrectionSettingsById($this->object->getId())->getRequiredCorrectors(); - $participant_title = "Writer"; - $corrector_title = "Corrector"; - $locations = []; - - foreach($task_repo->getLocationsByTaskId($this->object->getId()) as $location) { - $locations[$location->getId()] = $location; + try { + $this->assignment_service->exportAssignments($this->spreadsheetAssignmentToggle()); } - - $essays = []; - - foreach($essay_repo->getEssaysByTaskId($this->object->getId()) as $essay) { - $essays[$essay->getWriterId()] = $essay; + catch (Exception $e) { + // maybe logging } - $corrector = []; - foreach($corrector_repo->getCorrectorsByTaskId($this->object->getId()) as $r) { - $corrector[$r->getId()] = $r; - } - - $writer = $writer_repo->getWritersByTaskId($this->object->getId()); - - if($this->spreadsheetAssignmentToggle()) { - - $authorized_user = []; - foreach($essays as $writer_id => $essay) { - if($essay->getWritingAuthorized() !== null) { - $authorized_user[] = $essay->getWriterId(); - } - } - $writer = array_filter($writer, fn ($x) => in_array($x->getId(), $authorized_user)); - } - - $users = []; - - foreach(\ilObjUser::_getUserData(array_merge( - array_map(fn (Corrector $x) => $x->getUserId(), $corrector), - array_map(fn (Writer $x) => $x->getUserId(), $writer), - )) as $u) { - $users[(int)$u['usr_id']] = $u; - } - - $assignments = []; - - foreach($corrector_repo->getAssignmentsByTaskId($this->object->getId()) as $assignment) { - $assignments[$assignment->getWriterId()][$assignment->getPosition()] = $assignment; - } - $r = 2; - $spreadsheet = new CorrectorAssignmentExcel(); - $writer_sheet = $spreadsheet->addSheet($participant_title, true); - $corrector_sheet = $spreadsheet->addSheet($corrector_title, false); - $spreadsheet->setCell(1, 0, 'Id'); - $spreadsheet->setCell(1, 1, 'Login'); - $spreadsheet->setCell(1, 2, 'Firstname'); - $spreadsheet->setCell(1, 3, 'Lastname'); - $spreadsheet->setCell(1, 4, 'Email'); - $spreadsheet->setCell(1, 5, 'Pseudonym'); - $spreadsheet->setCell(1, 6, 'Location'); - $spreadsheet->setCell(1, 7, 'Words'); - foreach(range(0, $needed_corr-1) as $pos) { - $spreadsheet->setCell(1, 8+$pos, 'Corrector ' . ($pos+1)); - } - - foreach($writer as $w) { - $data = $users[$w->getUserId()] ?? []; - $ass = $assignments[$w->getId()] ?? []; - ksort($ass); - $essay = $essays[$w->getId()] ?? null; - $location = $essay !== null ? $locations[$essay->getLocation()] ?? null : null; - $written_text = $essay !== null ? $essay->getWrittenText() : ""; - $location_text = $location !== null ? $location->getTitle() : ""; - - $spreadsheet->setCell($r, 0, $w->getId()); - $spreadsheet->setCell($r, 1, $data['login'] ?? ""); - $spreadsheet->setCell($r, 2, $data['firstname'] ?? ""); - $spreadsheet->setCell($r, 3, $data['lastname'] ?? ""); - $spreadsheet->setCell($r, 4, $data['email'] ?? ""); - $spreadsheet->setCell($r, 5, $w->getPseudonym()); - $spreadsheet->setCell($r, 6, $location_text); - $spreadsheet->setCell($r, 7, str_word_count($written_text)); - - foreach(range(0, $needed_corr-1) as $pos) { - $spreadsheet->addDropdownCol($r, 8+$pos, '=\''.$corrector_title.'\'!$B$2:$B$'.(count($corrector)+1)); - } - - foreach ($ass as $a) { - $c = $corrector[$a->getCorrectorId()] ?? null; - $login = $c !== null && isset($users[$c->getUserId()]) ? $users[$c->getUserId()]['login'] ?? '' : ''; - $spreadsheet->setCell($r, 8+$a->getPosition(), $login); - } - $r++; - } - $r = 2; - $spreadsheet->setActiveSheet($corrector_sheet); - $spreadsheet->setCell(1, 0, "Id"); - $spreadsheet->setCell(1, 1, 'Login'); - $spreadsheet->setCell(1, 2, 'Firstname'); - $spreadsheet->setCell(1, 3, 'Lastname'); - $spreadsheet->setCell(1, 4, 'Email'); - - foreach($corrector as $c) { - $data = $users[$c->getUserId()] ?? []; - $ass = $assignments[$c->getId()] ?? []; - ksort($ass); - - $spreadsheet->setCell($r, 0, $c->getId()); - $spreadsheet->setCell($r, 1, $data['login'] ?? ""); - $spreadsheet->setCell($r, 2, $data['firstname'] ?? ""); - $spreadsheet->setCell($r, 3, $data['lastname'] ?? ""); - $spreadsheet->setCell($r, 4, $data['email'] ?? ""); - $r++; - } - $spreadsheet->setActiveSheet($corrector_sheet); - $spreadsheet->sendToClient("corrector_assignment.xlsx"); $this->ctrl->redirect($this); } diff --git a/classes/CorrectorAdmin/class.CorrectorAdminService.php b/classes/CorrectorAdmin/class.CorrectorAdminService.php index 554790ac..1313bca9 100644 --- a/classes/CorrectorAdmin/class.CorrectorAdminService.php +++ b/classes/CorrectorAdmin/class.CorrectorAdminService.php @@ -78,16 +78,27 @@ public function getSettings() : CorrectionSettings /** * Get or create a writer object for an ILIAS user - * @param int $user_id - * @return Corrector + * A new corrector object is not yet saved */ - public function getOrCreateCorrectorFromUserId(int $user_id) : Corrector + public function getCorrectorFromUserId(int $user_id) : Corrector { $corrector = $this->correctorRepo->getCorrectorByUserId($user_id, $this->settings->getTaskId()); if (!isset($corrector)) { $corrector = Corrector::model(); $corrector->setUserId($user_id); $corrector->setTaskId($this->settings->getTaskId()); + } + return $corrector; + } + + /** + * Get or create a writer object for an ILIAS user + * A new corrector object is already saved + */ + public function getOrCreateCorrectorFromUserId(int $user_id) : Corrector + { + $corrector = $this->getCorrectorFromUserId($user_id); + if (empty($corrector->getId())) { $this->correctorRepo->save($corrector); } return $corrector; diff --git a/classes/CorrectorAdmin/class.CorrectorAssignmentExcel.php b/classes/CorrectorAdmin/class.CorrectorAssignmentExcel.php index 864a7b0b..2098783e 100644 --- a/classes/CorrectorAdmin/class.CorrectorAssignmentExcel.php +++ b/classes/CorrectorAdmin/class.CorrectorAssignmentExcel.php @@ -23,4 +23,39 @@ public function addDropdownCol($a_row, $a_col, $a_target_formula) $objValidation->setFormula1($a_target_formula); $this->workbook->getActiveSheet()->getCellByColumnAndRow($a_row, $a_col)->setDataValidation(clone $objValidation); } + + /** + * Get the column titles in the forts row of the sheet + */ + public function getColumnTitlesFromActiveSheet() : array + { + $data = $this->getSheetAsArray(); + return $data[0] ?? []; + } + + /** + * Get an array of records below the first line + * Each record is an associative array with column title as key + * @return array[][] + */ + public function getAssocDataFromActiveSheet() : array + { + $data = $this->getSheetAsArray(); + $columns = $this->getColumnTitlesFromActiveSheet(); + $assoc = []; + + for ($row = 1; $row <= count($data); $row++) { + $record = []; + foreach ($columns as $col => $label) { + if (!empty($label)) { + $record[$label] = $data[$row][$col] ?? ''; + } + } + if (!empty($record)) { + $assoc[] = $record; + } + } + return $assoc; + } + } diff --git a/classes/CorrectorAdmin/class.CorrectorAssignmentsException.php b/classes/CorrectorAdmin/class.CorrectorAssignmentsException.php new file mode 100644 index 00000000..92b7a034 --- /dev/null +++ b/classes/CorrectorAdmin/class.CorrectorAssignmentsException.php @@ -0,0 +1,9 @@ +writer_repo = $this->localDI->getWriterRepo(); + $this->corrector_repo = $this->localDI->getCorrectorRepo(); + $this->essay_repo = $this->localDI->getEssayRepo(); + $this->task_repo = $this->localDI->getTaskRepo(); + $this->writer_admin_service = $this->localDI->getWriterAdminService($this->task_id); + $this->corrector_admin_service = $this->localDI->getCorrectorAdminService($this->task_id); + $this->settings = $this->task_repo->getCorrectionSettingsById($this->task_id); + } + + /** + * Export assignment to an excel file + * @throws \PhpOffice\PhpSpreadsheet\Exception + */ + public function exportAssignments(bool $only_authorized): void + { + $participant_title = "Writer"; + $corrector_title = "Corrector"; + $locations = []; + + foreach($this->task_repo->getLocationsByTaskId($this->task_id) as $location) { + $locations[$location->getId()] = $location; + } + + $essays = []; + + foreach($this->essay_repo->getEssaysByTaskId($this->task_id) as $essay) { + $essays[$essay->getWriterId()] = $essay; + } + $corrector = []; + foreach($this->corrector_repo->getCorrectorsByTaskId($this->task_id) as $r) { + $corrector[$r->getId()] = $r; + } + + $writer = $this->writer_repo->getWritersByTaskId($this->task_id); + + if ($only_authorized) { + $authorized_user = []; + foreach($essays as $writer_id => $essay) { + if($essay->getWritingAuthorized() !== null) { + $authorized_user[] = $essay->getWriterId(); + } + } + $writer = array_filter($writer, fn ($x) => in_array($x->getId(), $authorized_user)); + } + + $users = []; + + foreach(\ilObjUser::_getUserData(array_merge( + array_map(fn (Corrector $x) => $x->getUserId(), $corrector), + array_map(fn (Writer $x) => $x->getUserId(), $writer), + )) as $u) { + $users[(int)$u['usr_id']] = $u; + } + + $assignments = []; + + foreach($this->corrector_repo->getAssignmentsByTaskId($this->task_id) as $assignment) { + $assignments[$assignment->getWriterId()][$assignment->getPosition()] = $assignment; + } + $r = 2; + $spreadsheet = new CorrectorAssignmentExcel(); + $writer_sheet = $spreadsheet->addSheet($participant_title, true); + $corrector_sheet = $spreadsheet->addSheet($corrector_title, false); + $spreadsheet->setCell(1, 0, 'Login'); + $spreadsheet->setCell(1, 1, 'Firstname'); + $spreadsheet->setCell(1, 2, 'Lastname'); + $spreadsheet->setCell(1, 3, 'Email'); + $spreadsheet->setCell(1, 4, 'Pseudonym'); + $spreadsheet->setCell(1, 5, 'Location'); + $spreadsheet->setCell(1, 6, 'Words'); + foreach(range(0, $this->settings->getRequiredCorrectors() -1 ) as $pos) { + $spreadsheet->setCell(1, 7 + $pos, 'Corrector ' . ($pos + 1)); + } + + foreach($writer as $w) { + $data = $users[$w->getUserId()] ?? []; + $ass = $assignments[$w->getId()] ?? []; + ksort($ass); + $essay = $essays[$w->getId()] ?? null; + $location = $essay !== null ? $locations[$essay->getLocation()] ?? null : null; + $written_text = $essay !== null ? $essay->getWrittenText() : ""; + $location_text = $location !== null ? $location->getTitle() : ""; + + $spreadsheet->setCell($r, 0, $data['login'] ?? ""); + $spreadsheet->setCell($r, 1, $data['firstname'] ?? ""); + $spreadsheet->setCell($r, 2, $data['lastname'] ?? ""); + $spreadsheet->setCell($r, 3, $data['email'] ?? ""); + $spreadsheet->setCell($r, 4, $w->getPseudonym()); + $spreadsheet->setCell($r, 5, $location_text); + $spreadsheet->setCell($r, 6, str_word_count($written_text)); + + foreach(range(0, $this->settings->getRequiredCorrectors()-1) as $pos) { + $spreadsheet->addDropdownCol($r, 7 + $pos, '=\''.$corrector_title.'\'!$A$2:$A$'.(count($corrector)+1)); + } + + foreach ($ass as $a) { + $c = $corrector[$a->getCorrectorId()] ?? null; + $login = $c !== null && isset($users[$c->getUserId()]) ? $users[$c->getUserId()]['login'] ?? '' : ''; + $spreadsheet->setCell($r, 7+$a->getPosition(), $login); + } + $r++; + } + $r = 2; + $spreadsheet->setActiveSheet($corrector_sheet); + $spreadsheet->setCell(1, 0, 'Login'); + $spreadsheet->setCell(1, 1, 'Firstname'); + $spreadsheet->setCell(1, 2, 'Lastname'); + $spreadsheet->setCell(1, 3, 'Email'); + + foreach($corrector as $c) { + $data = $users[$c->getUserId()] ?? []; + $ass = $assignments[$c->getId()] ?? []; + ksort($ass); + + $spreadsheet->setCell($r, 0, $data['login'] ?? ""); + $spreadsheet->setCell($r, 1, $data['firstname'] ?? ""); + $spreadsheet->setCell($r, 2, $data['lastname'] ?? ""); + $spreadsheet->setCell($r, 3, $data['email'] ?? ""); + $r++; + } + $spreadsheet->setActiveSheet($corrector_sheet); + $spreadsheet->sendToClient("corrector_assignment.xlsx"); + + } + + /** + * Import assignments from an exel file + * Writers and correctors will be created by login on the fly + * @throws CorrectorAssignmentsException + */ + public function importAssignments(string $file) + { + $excel = new CorrectorAssignmentExcel(); + $excel->loadFromFile($file); + $excel->setActiveSheet(0); + $errors = []; + + // check if the required column labels are present + $columns = $excel->getColumnTitlesFromActiveSheet(); + $required_labels = ['Login']; + for ($pos = 0; $pos < $this->settings->getRequiredCorrectors(); $pos++) { + $required_labels[] = 'Corrector ' . ($pos + 1); + } + foreach ($required_labels as $label) { + if (!in_array($label, $columns)) { + $errors[] = sprintf($this->plugin->txt('import_missing_column'), $label); + } + } + + if (!empty($errors)) { + throw new CorrectorAssignmentsException(implode("\n", $errors)); + } + + // collect writers, correctors and assignments (saved later) + $writers = []; // login => Writer + $correctors = []; // login => Corrector + $to_assign = []; // login => position (0-x) => login + + foreach ($excel->getAssocDataFromActiveSheet() as $line => $record) { + + if (!empty($writer_login = $record['Login'] ?? '')) { + $to_assign[$writer_login] = []; + + if (empty($writer_user_id = \ilObjUser::_lookupId($writer_login))) { + $errors[] = sprintf($this->plugin->txt('import_line_user_not_found'), $line + 1, $writer_login); + continue; // next writer + } + elseif (!empty($writers[$writer_login])) { + $errors[] = sprintf($this->plugin->txt('import_line_writer_repeated'), $line + 1, $writer_login); + continue; // next writer + } + $writers[$writer_login] = $this->writer_admin_service->getWriterFromUserId($writer_user_id); + + for ($pos = 0; $pos < $this->settings->getRequiredCorrectors(); $pos++) { + + if (!empty($corrector_login = $record['Corrector ' . ($pos + 1)] ?? '')) { + + if (in_array($corrector_login, $to_assign[$writer_login])) { + $errors[] = sprintf($this->plugin->txt('import_line_corrector_repeated'), $line + 1, $corrector_login); + continue; // next corrector + } + elseif (empty($corrector_user_id = \ilObjUser::_lookupId($corrector_login))) { + $errors[] = sprintf($this->plugin->txt('import_line_user_not_found'),$line + 1, $corrector_login); + continue; // next corrector + } + + if (empty($correctors[$corrector_login])) { + $correctors[$corrector_login] = $this->corrector_admin_service->getCorrectorFromUserId($corrector_user_id); + } + + $to_assign[$writer_login][$pos] = $corrector_login; + } + } + } + } + + if (!empty($errors)) { + throw new CorrectorAssignmentsException(implode("\n", $errors)); + } + + + /** @var Writer[] $writers */ + foreach ($writers as $writer) { + if (empty($writer->getId())) { + $this->writer_repo->save($writer); + } + } + /** @var Corrector[] $correctors */ + foreach ($correctors as $corrector) { + if (empty($corrector->getId())) + $this->corrector_repo->save($corrector); + } + + foreach ($to_assign as $writer_login => $corrector_logins) { + $writer_id = $writers[$writer_login]->getId(); + + $corrector1_id = empty($corrector_logins[0]) + ? CorrectorAdminService::BLANK_CORRECTOR_ASSIGNMENT + : $correctors[$corrector_logins[0]]->getId(); + + $corrector2_id = empty($corrector_logins[1]) + ? CorrectorAdminService::BLANK_CORRECTOR_ASSIGNMENT + : $correctors[$corrector_logins[1]]->getId(); + + $this->corrector_admin_service->assignMultipleCorrector( + $corrector1_id, + $corrector2_id, + [$writer_id] + ); + } + } +} \ No newline at end of file diff --git a/classes/WriterAdmin/class.WriterAdminService.php b/classes/WriterAdmin/class.WriterAdminService.php index 963ed593..98ebaf20 100644 --- a/classes/WriterAdmin/class.WriterAdminService.php +++ b/classes/WriterAdmin/class.WriterAdminService.php @@ -54,21 +54,31 @@ public function __construct(int $task_id) } /** - * Get or create a writer object for an ILIAS user + * Get a writer object for an ILIAS user + * A new writer object is not yet saved * @param int $user_id * @return Writer */ - public function getOrCreateWriterFromUserId(int $user_id) : Writer + public function getWriterFromUserId(int $user_id) : Writer { $writer = $this->writerRepo->getWriterByUserIdAndTaskId($user_id, $this->task_id); if (!isset($writer)) { $writer = new Writer(); $writer->setUserId($user_id) - ->setTaskId($this->task_id) - ->setPseudonym($this->plugin->txt('participant') . ' ' . $user_id); - $this->writerRepo->save($writer); - // change the pseudonym to the writer id - $writer->setPseudonym($this->plugin->txt('participant') . ' ' .$writer->getId()); + ->setTaskId($this->task_id) + ->setPseudonym($this->plugin->txt('participant') . ' ' .$writer->getId()); + } + return $writer; + } + + /** + * Get or create a writer object for an ILIAS user + * A new writer object is already saved + */ + public function getOrCreateWriterFromUserId(int $user_id) : Writer + { + $writer = $this->getWriterFromUserId($user_id); + if (empty($writer->getId())) { $this->writerRepo->save($writer); } return $writer; diff --git a/classes/class.LongEssayAssessmentDI.php b/classes/class.LongEssayAssessmentDI.php index 82cb0a51..e924ec31 100644 --- a/classes/class.LongEssayAssessmentDI.php +++ b/classes/class.LongEssayAssessmentDI.php @@ -18,6 +18,7 @@ use ILIAS\Plugin\LongEssayAssessment\UI\UIService; use ILIAS\Plugin\LongEssayAssessment\WriterAdmin\WriterAdminService; use ILIAS\Plugin\LongEssayAssessment\Task\LoggingService; +use ILIAS\Plugin\LongEssayAssessment\CorrectorAdmin\CorrectorAssignmentsService; /** * @author Fabian Wolf