From 3d3eaee568091a6fbed6e635d7d386481c06a135 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Noack?= <noack@data-quest.de>
Date: Thu, 28 Dec 2023 11:12:49 +0000
Subject: [PATCH] Resolve #3344 "Testergebnisse aus ILIAS in das Stud.IP
 Gradebook importieren"

Closes #3344

Merge request studip/studip!2271
---
 .../course/gradebook/lecturers.php            | 164 ++++++++++++++++++
 app/controllers/course/gradebook/students.php |  23 +++
 .../course/gradebook/template_helpers.php     |  19 ++
 .../lecturers/custom_definitions.php          |  21 ++-
 .../lecturers/edit_custom_definitions.php     |   7 -
 .../lecturers/edit_ilias_definition.php       |  26 +++
 .../lecturers/edit_ilias_definitions.php      |  75 ++++++++
 .../course/gradebook/lecturers/index.php      |  14 +-
 .../lecturers/new_ilias_definition.php        |  32 ++++
 app/views/course/gradebook/students/index.php |  21 ++-
 .../5.5.15_step3344_ilias_results.php         |  32 ++++
 lib/cronjobs/import_ilias_testresults.php     |  51 ++++++
 lib/ilias_interface/ConnectedIlias.class.php  |  15 ++
 .../IliasObjectConnections.class.php          |  37 +++-
 lib/ilias_interface/IliasSoap.class.php       |  37 ++++
 lib/modules/GradebookModule.class.php         |   6 +
 16 files changed, 559 insertions(+), 21 deletions(-)
 create mode 100644 app/views/course/gradebook/lecturers/edit_ilias_definition.php
 create mode 100644 app/views/course/gradebook/lecturers/edit_ilias_definitions.php
 create mode 100644 app/views/course/gradebook/lecturers/new_ilias_definition.php
 create mode 100644 db/migrations/5.5.15_step3344_ilias_results.php
 create mode 100644 lib/cronjobs/import_ilias_testresults.php

diff --git a/app/controllers/course/gradebook/lecturers.php b/app/controllers/course/gradebook/lecturers.php
index 2e98856e7fc..2cba5f3bfc0 100644
--- a/app/controllers/course/gradebook/lecturers.php
+++ b/app/controllers/course/gradebook/lecturers.php
@@ -43,6 +43,7 @@ class Course_Gradebook_LecturersController extends AuthenticatedController
         $this->groupedInstances = $this->groupedInstances($course);
         $this->sumOfWeights = $this->getSumOfWeights($gradingDefinitions);
         $this->totalSums = $this->sumOfWeights ? $this->getTotalSums($gradingDefinitions) : 0;
+        $this->totalPassed = $this->getTotalPassed($gradingDefinitions);
     }
 
     /**
@@ -71,6 +72,7 @@ class Course_Gradebook_LecturersController extends AuthenticatedController
             $categoryName = Definition::CUSTOM_DEFINITIONS_CATEGORY === $category ? _('Manuell eingetragen') : $category;
             foreach ($this->groupedDefinitions[$category] as $definition) {
                 $headerLine[] = $categoryName.': '.$definition->name;
+                $headerLine[] = _('bestanden') . '(' . $categoryName.': '.$definition->name . ')';
             }
         }
         $studentLines = [];
@@ -81,6 +83,9 @@ class Course_Gradebook_LecturersController extends AuthenticatedController
                     $studentLine[] = isset($this->groupedInstances[$user->user_id][$definition->id])
                                    ? $this->groupedInstances[$user->user_id][$definition->id]->rawgrade
                                    : 0;
+                    $studentLine[] = isset($this->groupedInstances[$user->user_id][$definition->id])
+                        ? $this->groupedInstances[$user->user_id][$definition->id]->passed
+                        : 0;
                 }
             }
             $studentLines[] = $studentLine;
@@ -166,6 +171,8 @@ class Course_Gradebook_LecturersController extends AuthenticatedController
         )->pluck('id');
 
         $grades = \Request::getArray('grades');
+        $passed = \Request::getArray('passed');
+        $feedback = \Request::getArray('feedback');
         foreach ($grades as $studentId => $studentGrades) {
             if (!in_array($studentId, $studentIds)) {
                 continue;
@@ -177,6 +184,8 @@ class Course_Gradebook_LecturersController extends AuthenticatedController
 
                 $instance = new Instance([$definitionId, $studentId]);
                 $instance->rawgrade = ((int) $strGrade) / 100.0;
+                $instance->passed = $passed[$studentId][$definitionId] ?? 0;
+                $instance->feedback = $feedback[$studentId][$definitionId] ?? '';
                 $instance->store();
             }
         }
@@ -195,6 +204,9 @@ class Course_Gradebook_LecturersController extends AuthenticatedController
         $gradingDefinitions = Definition::findByCourse($course);
         $this->groupedDefinitions = $this->getGroupedDefinitions($gradingDefinitions);
         $this->customDefinitions = $this->groupedDefinitions[Definition::CUSTOM_DEFINITIONS_CATEGORY] ?? [];
+        if (!count($this->customDefinitions )) {
+            PageLayout::postInfo(_('Es sind keine manuellen Leistungen definiert.'));
+        }
     }
 
     /**
@@ -294,6 +306,131 @@ class Course_Gradebook_LecturersController extends AuthenticatedController
         $this->redirect('course/gradebook/lecturers/edit_custom_definitions');
     }
 
+    public function edit_ilias_definitions_action()
+    {
+        if (Navigation::hasItem('/course/gradebook/edit_ilias_definitions')) {
+            Navigation::activateItem('/course/gradebook/edit_ilias_definitions');
+        }
+
+        $course = \Context::get();
+        $gradingDefinitions = Definition::findByCourse($course);
+        $this->groupedDefinitions = $this->getGroupedDefinitions($gradingDefinitions);
+        $this->customDefinitions = $this->groupedDefinitions['ILIAS'] ?? [];
+        $this->setupIliasSidebar(count($this->customDefinitions));
+        if (!count($this->customDefinitions )) {
+            PageLayout::postInfo(_('Es sind keine ILIAS-Tests als Leistungen definiert.'));
+        }
+    }
+
+    public function new_ilias_definition_action()
+    {
+        $this->ilias_modules = [];
+        $course = Course::findCurrent();
+        $already_defined = new SimpleCollection(Definition::findBySQL("course_id = ? AND category='ILIAS'", [$course->id]));
+        foreach (Config::get()->ILIAS_INTERFACE_SETTINGS as $ilias_index => $ilias_config) {
+            if ($ilias_config['is_active']) {
+                $ilias = new ConnectedIlias($ilias_index);
+                $this->ilias_modules[$ilias_index] = array_filter(
+                    DBManager::get()->fetchFirst(
+                        "SELECT module_id FROM object_contentmodules WHERE object_id=? AND system_type=? AND module_type='tst'", [$course->id, $ilias_index],
+                        function ($module_id) use ($ilias, $already_defined) {
+                            $item = $ilias->index . '-' . $module_id;
+                            if (!$already_defined->findOneBy('item', $item)) {
+                                return $ilias->getModule($module_id);
+                            }
+                            return null;
+                        }
+                    )
+                );
+            }
+        }
+    }
+
+    public function delete_ilias_definition_action($definitionId)
+    {
+        CSRFProtection::verifyUnsafeRequest();
+        if (!$definition = Definition::findOneBySQL(
+            'id = ? AND course_id = ?',
+            [$definitionId, \Context::getId()]
+        )
+        ) {
+            \PageLayout::postError(_('Die Leistung konnte nicht gelöscht werden.'));
+        } else {
+            if (Definition::deleteBySQL('id = ?', [$definition->id])) {
+                \PageLayout::postSuccess(_('Die Leistung wurde gelöscht.'));
+            } else {
+                \PageLayout::postError(_('Die Leistung konnte nicht gelöscht werden.'));
+            }
+        }
+
+        $this->redirect('course/gradebook/lecturers/edit_ilias_definitions');
+    }
+
+    public function create_ilias_definition_action()
+    {
+        CSRFProtection::verifyUnsafeRequest();
+        $ilias_module = Request::get('ilias_module');
+        $module_import = Request::int('result') + Request::int('passed');
+        if (!$module_import) {
+            $module_import = 3;
+        }
+        if ($ilias_module) {
+            [$index, $module_id] = explode('-', $ilias_module);
+            $ilias = new ConnectedIlias($index);
+            $module = $ilias->getModule($module_id);
+            if ($module) {
+                $definition = Definition::create(
+                    [
+                        'course_id' => \Context::getId(),
+                        'item'      => $ilias_module . '-' . $module_import,
+                        'name'      => $module->getTitle(),
+                        'tool'      => 'ILIAS',
+                        'category'  => 'ILIAS',
+                        'position'  => 0,
+                        'weight'    => 0.0,
+                    ]
+                );
+
+                if (!$definition) {
+                    \PageLayout::postError(_('Die Leistung konnte nicht definiert werden.'));
+                } else {
+                    \PageLayout::postSuccess(_('Die Leistung wurde erfolgreich definiert.'));
+                }
+            }
+        }
+        $this->redirect('course/gradebook/lecturers/edit_ilias_definitions');
+    }
+
+    public function edit_ilias_definition_action($definition_id)
+    {
+        $this->definition = Definition::find($definition_id);
+        if ($this->definition && Request::submitted('test_name')) {
+            CSRFProtection::verifyUnsafeRequest();
+            $module_import = Request::int('result') + Request::int('passed');
+            [$index, $module_id] = explode('-', $this->definition->item );
+            $this->definition->name = Request::get('test_name');
+            $this->definition->item = $index . '-' . $module_id . '-' . $module_import;
+            if ($this->definition->store()) {
+                \PageLayout::postSuccess(_('Die Leistung wurde erfolgreich aktualisiert.'));
+            }
+
+            $this->redirect('course/gradebook/lecturers/edit_ilias_definitions');
+        }
+    }
+
+    public function import_ilias_results_action()
+    {
+        $num = IliasObjectConnections::importIliasResultsForCourse(Course::findCurrent());
+        PageLayout::postInfo(sprintf(
+            ngettext(
+                '%s Resultat wurde importiert.',
+                '%s Resultate wurden importiert.',
+                $num),
+            $num)
+        );
+        $this->redirect('course/gradebook/lecturers/edit_ilias_definitions');
+    }
+
     public function getInstanceForUser(Definition $definition, \CourseMember $user)
     {
         if (!isset($this->groupedInstances[$user->user_id])) {
@@ -342,4 +479,31 @@ class Course_Gradebook_LecturersController extends AuthenticatedController
 
         return $totalSums;
     }
+
+    private function getTotalPassed($gradingDefinitions)
+    {
+        $gradingDefinitions = \SimpleCollection::createFromArray($gradingDefinitions);
+        $totalPassed = [];
+        foreach ($this->students as $student) {
+            if (!isset($totalPassed[$student->user_id])) {
+                $totalPassed[$student->user_id] = 0;
+            }
+
+            if (!isset($this->groupedInstances[$student->user_id])) {
+                continue;
+            }
+
+            foreach ($this->groupedInstances[$student->user_id] as $definitionId => $instance) {
+                if ($gradingDefinitions->findOneBy('id', $definitionId)) {
+                    $totalPassed[$student->user_id] += $instance->passed;
+                }
+            }
+        }
+        $count = $gradingDefinitions->count();
+        $totalPassed = array_map(
+            function($p) use ($count) {
+                return $p == $count ? $p : 0;
+                }, $totalPassed);
+        return $totalPassed;
+    }
 }
diff --git a/app/controllers/course/gradebook/students.php b/app/controllers/course/gradebook/students.php
index 81a1f7fc9e0..b00c22d3860 100644
--- a/app/controllers/course/gradebook/students.php
+++ b/app/controllers/course/gradebook/students.php
@@ -45,6 +45,8 @@ class Course_Gradebook_StudentsController extends AuthenticatedController
         $this->sumOfWeights = $this->getSumOfWeights($this->gradingDefinitions);
         $this->subtotals = $this->getSubtotalGrades();
         $this->total = $this->getTotalGrade();
+        $this->subpassed = $this->getSubpassed();
+        $this->passed = array_sum($this->subpassed);
     }
 
     /**
@@ -75,6 +77,7 @@ class Course_Gradebook_StudentsController extends AuthenticatedController
                     $definition->name,
                     $definition->tool,
                     $instance ? $instance->rawgrade : 0,
+                    $instance ? $instance->passed : 0,
                     $instance ? $instance->feedback : null,
                 ];
             }
@@ -85,6 +88,7 @@ class Course_Gradebook_StudentsController extends AuthenticatedController
             _('Leistung'),
             _('Werkzeug'),
             _('Fortschritt'),
+            _('Bestanden'),
             _('Feedback'),
         ];
         $data = array_merge([$headerLine], $lines);
@@ -139,4 +143,23 @@ class Course_Gradebook_StudentsController extends AuthenticatedController
 
         return $sumOfWeights ? $sumOfWeightedGrades / $sumOfWeights : 0;
     }
+
+    private function getSubpassed()
+    {
+        $subpassed = [];
+
+        foreach ($this->groupedDefinitions as $category => $definitions) {
+            $passed = 0;
+
+            foreach ($definitions as $definition) {
+                if (isset($this->groupedInstances[$definition->id])) {
+                    $instance = $this->groupedInstances[$definition->id];
+                    $passed += $instance->passed;
+                }
+            }
+            $subpassed[$category] = $passed == count($definitions) ? $passed : 0;
+        }
+
+        return $subpassed;
+    }
 }
diff --git a/app/controllers/course/gradebook/template_helpers.php b/app/controllers/course/gradebook/template_helpers.php
index b77822e9635..3cbd8b42a63 100644
--- a/app/controllers/course/gradebook/template_helpers.php
+++ b/app/controllers/course/gradebook/template_helpers.php
@@ -87,4 +87,23 @@ trait GradebookTemplateHelpers
         \PageLayout::setTitle(Context::getHeaderLine().' - Gradebook');
         \PageLayout::setHelpKeyword("Basis.Gradebook");
     }
+
+    protected function setupIliasSidebar($num_definitions = 0)
+    {
+        $ilias = new \LinksWidget();
+        $ilias->setTitle(_('ILIAS'));
+        $ilias->addLink(
+            _('Test als Leistung hinzufügen'),
+            $this->url_for('course/gradebook/lecturers/new_ilias_definition'),
+            Icon::create('learnmodule+add')
+        )->asDialog();
+        if ($num_definitions) {
+            $ilias->addLink(
+                _('Ergebnisse aus ILIAS importieren'),
+                $this->url_for('course/gradebook/lecturers/import_ilias_results'),
+                Icon::create('refresh')
+            );
+        }
+        \Sidebar::Get()->addWidget($ilias);
+    }
 }
diff --git a/app/views/course/gradebook/lecturers/custom_definitions.php b/app/views/course/gradebook/lecturers/custom_definitions.php
index 864aa0d3c7f..6b906981456 100644
--- a/app/views/course/gradebook/lecturers/custom_definitions.php
+++ b/app/views/course/gradebook/lecturers/custom_definitions.php
@@ -1,14 +1,14 @@
 <form class="default" action="<?= $controller->link_for('course/gradebook/lecturers/store_grades') ?>" method="POST">
     <?= CSRFProtection::tokenTag()?>
     <div style="overflow-x:auto;">
-        <table class="default gradebook-lecturer-custom-definitions">
+        <table class="default gradebook-lecturer-custom-definitions sortable-table" data-sortlist="[[0, 0]]">
             <caption>
                 <?= _('Noten manuell erfassen') ?>
             </caption>
 
             <thead>
-                <tr class="tablesorter-ignoreRow">
-                    <th><?= _('Name') ?></th>
+                <tr class="sortable">
+                    <th data-sort="text"><?= _('Name') ?></th>
                     <? if (count($customDefinitions)) { ?>
                         <? foreach ($customDefinitions as $definition) { ?>
                             <th>
@@ -35,13 +35,26 @@
                                 <td class="gradebook-grade-input">
                                     <? $instance = $controller->getInstanceForUser($definition, $student) ?>
                                     <? $rawgrade = $instance ? $instance->rawgrade : 0 ?>
+                                    <? $passed = $instance ? $instance->passed : 0 ?>
+                                    <? $feedback = $instance ? $instance->feedback : '' ?>
                                     <label class="undecorated">
                                         <input type="number"
                                                name="grades[<?= htmlReady($student->user_id) ?>][<?= htmlReady($definition->id) ?>]"
                                                value="<?= $controller->formatAsPercent($rawgrade) ?>"
                                                min="0"> %
                                     </label>
-
+                                    <label>
+                                        <?=_('Bestanden')?>
+                                        <input type="checkbox"
+                                               name="passed[<?= htmlReady($student->user_id) ?>][<?= htmlReady($definition->id) ?>]"
+                                               value="1"
+                                               <?= $passed ? 'checked' : ''?>>
+                                    </label>
+                                    <label>
+                                        <input type="text"
+                                               name="feedback[<?= htmlReady($student->user_id) ?>][<?= htmlReady($definition->id) ?>]"
+                                               value="<?=htmlReady($feedback)?>" placeholder="<?=_('Feedback')?>">
+                                    </label>
                                 </td>
                             <? } ?>
                     <? } elseif ($index === 0) { ?>
diff --git a/app/views/course/gradebook/lecturers/edit_custom_definitions.php b/app/views/course/gradebook/lecturers/edit_custom_definitions.php
index dbb3454344f..19012045ada 100644
--- a/app/views/course/gradebook/lecturers/edit_custom_definitions.php
+++ b/app/views/course/gradebook/lecturers/edit_custom_definitions.php
@@ -42,13 +42,6 @@
                 </tr>
             <? } ?>
         </tbody>
-    <? } else { ?>
-        <tbody>
-            <tr>
-                <td colspan="2">
-                    <?= \MessageBox::info(_('Es sind keine manuellen Leistungen definiert.')) ?>
-                </td>
-        </tbody>
     <? } ?>
 
 
diff --git a/app/views/course/gradebook/lecturers/edit_ilias_definition.php b/app/views/course/gradebook/lecturers/edit_ilias_definition.php
new file mode 100644
index 00000000000..36389071fc6
--- /dev/null
+++ b/app/views/course/gradebook/lecturers/edit_ilias_definition.php
@@ -0,0 +1,26 @@
+<?php
+/** @var StudipController $controller */
+/** @var \Grading\Definition $definition */
+?>
+<form class="default" action="<?=$controller->link_for('course/gradebook/lecturers/edit_ilias_definition/' . $definition->id) ?>" method="POST">
+    <?= CSRFProtection::tokenTag()?>
+    <fieldset>
+        <label>
+            <?= _('Name der Leistung') ?>
+            <input type="text" value="<?=htmlReady($definition->name)?>" name="test_name">
+        </label>
+        <label>
+            <?=_('Prozentwert übertragen')?>
+            <input type="checkbox" value="1" <?=substr($definition->item, -1) & 1 ? 'checked' : ''?> name="result">
+        </label>
+        <label>
+            <?=_('Bestanden/nicht bestanden übertragen')?>
+            <input type="checkbox" value="2" <?=substr($definition->item, -1) & 2 ? 'checked' : ''?> name="passed">
+        </label>
+    </fieldset>
+
+    <footer data-dialog-button>
+        <?= \Studip\Button::createAccept(_('Speichern')) ?>
+        <?= \Studip\LinkButton::createCancel(_('Abbrechen'), $controller->url_for('course/gradebook/lecturers/edit_ilias_definitions')) ?>
+    </footer>
+</form>
diff --git a/app/views/course/gradebook/lecturers/edit_ilias_definitions.php b/app/views/course/gradebook/lecturers/edit_ilias_definitions.php
new file mode 100644
index 00000000000..1fd7a17cf29
--- /dev/null
+++ b/app/views/course/gradebook/lecturers/edit_ilias_definitions.php
@@ -0,0 +1,75 @@
+<?php
+/** @var Grading\Definition[] $customDefinitions */
+/** @var StudipController $controller */
+?>
+<table class="default">
+    <caption>
+        <?= _('ILIAS Leistungen definieren') ?>
+    </caption>
+
+    <thead>
+    <tr class="tablesorter-ignoreRow">
+        <th><?= _('Name') ?></th>
+        <th><?= _('ID') ?></th>
+        <th><?= _('Prozentwert') ?></th>
+        <th><?= _('Bestanden') ?></th>
+        <th class="actions"><?= _('Aktionen') ?></th>
+    </tr>
+    </thead>
+
+    <? if (count($customDefinitions)) { ?>
+        <tbody>
+        <? foreach ($customDefinitions as $definition) { ?>
+            <tr>
+                <td>
+                    <?= htmlReady($definition->name) ?>
+                </td>
+                <td>
+                    <?= htmlReady($definition->item) ?>
+                </td>
+                <td>
+                    <?= substr($definition->item, -1) & 1 ? 'x' : '' ?>
+                </td>
+                <td>
+                    <?= substr($definition->item, -1) & 2 ? 'x' : '' ?>
+                <td class="actions">
+                    <?=
+                    \ActionMenu::get()
+                        ->addLink(
+                            $controller->url_for(
+                                'course/gradebook/lecturers/edit_ilias_definition',
+                                $definition->id
+                            ),
+                            _('Ändern'),
+                            Icon::create('edit'),
+                            ['data-dialog' => 'size=fit']
+                        )
+                        ->addLink(
+                            $controller->url_for(
+                                'course/gradebook/lecturers/delete_ilias_definition',
+                                $definition->id
+                            ),
+                            _('Löschen'),
+                            Icon::create('trash'),
+                            ['onclick' => "return STUDIP.Dialog.confirmAsPost('" . _('Wollen Sie die Leistungsdefinition wirklich löschen?') . "', this.href);"]
+                        ) ?>
+                </td>
+            </tr>
+        <? } ?>
+        </tbody>
+    <? } ?>
+
+
+    <tfoot class="gradebook-lecturer-custom-definitions-actions">
+    <tr>
+        <td colspan="5">
+            <?= \Studip\LinkButton::createAdd(
+                count($customDefinitions) ? _('Weiteren Test als Leistung definieren') : _('Test als Leistung definieren'),
+                $controller->url_for('course/gradebook/lecturers/new_ilias_definition'),
+                ['data-dialog' => 'size=fit']
+            ) ?>
+        </td>
+    </tr>
+    </tfoot>
+</table>
+<?php
diff --git a/app/views/course/gradebook/lecturers/index.php b/app/views/course/gradebook/lecturers/index.php
index 62ebfccd387..7e5523c0daf 100644
--- a/app/views/course/gradebook/lecturers/index.php
+++ b/app/views/course/gradebook/lecturers/index.php
@@ -45,17 +45,21 @@
                             <?= $studentName ?>
                         </a>
                     </td>
-                    <? $totalSum = isset($totalSums[$student->user_id]) ? $totalSums[$student->user_id] : 0 ?>
-                    <td data-sort-value="<?= $totalSum?>">
-                        <?= $controller->formatAsPercent($totalSum) ?> %
+                    <? $totalSum = $totalSums[$student->user_id] ?? 0 ?>
+                    <? $totalPassed = $totalPassed[$student->user_id] ?? 0 ?>
+                    <td data-sort-value="<?= $totalSum + $totalPassed?>">
+                        <?= $totalSum > 0 ? $controller->formatAsPercent($totalSum) . ' %' : ''?>
+                        <?= $totalPassed ? _('bestanden') : ''?>
                     </td>
 
                     <? foreach ($categories as $category) { ?>
                         <? foreach ($groupedDefinitions[$category] as $definition) { ?>
                             <? $instance = $controller->getInstanceForUser($definition, $student) ?>
                             <? $rawgrade = $instance ? $instance->rawgrade : 0 ?>
-                            <td data-sort-value="<? $rawgrade ?>">
-                                <?= $controller->formatAsPercent($rawgrade) ?> %
+                            <? $passed = $instance ? $instance->passed : 0 ?>
+                            <td data-sort-value="<?= $rawgrade + $passed ?>">
+                                <?= $rawgrade > 0 ? $controller->formatAsPercent($rawgrade) . ' %' : ''?>
+                                <?= $passed ? _('bestanden') : ''?>
                             </td>
                         <? } ?>
                     <? } ?>
diff --git a/app/views/course/gradebook/lecturers/new_ilias_definition.php b/app/views/course/gradebook/lecturers/new_ilias_definition.php
new file mode 100644
index 00000000000..7a65c79fae0
--- /dev/null
+++ b/app/views/course/gradebook/lecturers/new_ilias_definition.php
@@ -0,0 +1,32 @@
+<?php
+/** @var StudipController $controller */
+/** @var array $ilias_modules */
+?>
+<form class="default" action="<?=$controller->link_for('course/gradebook/lecturers/create_ilias_definition') ?>" method="POST">
+    <?= CSRFProtection::tokenTag()?>
+    <fieldset>
+        <label>
+            <?= _('Bitte wählen Sie einen Test aus') ?>
+            <select name="ilias_module">
+            <? foreach ($ilias_modules as $key => $modules) : ?>
+                <? foreach ($modules as $module) : ?>
+                <option value="<?=$key . '-' . $module->getId()?>"><?=htmlReady($module->getTitle())?></option>
+                <? endforeach;?>
+            <? endforeach;?>
+            </select>
+        </label>
+        <label>
+            <?=_('Prozentwert übertragen')?>
+            <input type="checkbox" value="1" checked name="result">
+        </label>
+        <label>
+            <?=_('Bestanden/nicht bestanden übertragen')?>
+            <input type="checkbox" value="2" checked name="passed">
+        </label>
+    </fieldset>
+
+    <footer data-dialog-button>
+        <?= \Studip\Button::createAccept(_('Speichern')) ?>
+        <?= \Studip\LinkButton::createCancel(_('Abbrechen'), $controller->url_for('course/gradebook/lecturers/edit_ilias_definitions')) ?>
+    </footer>
+</form>
diff --git a/app/views/course/gradebook/students/index.php b/app/views/course/gradebook/students/index.php
index fb39eed1c6c..572067aceb8 100644
--- a/app/views/course/gradebook/students/index.php
+++ b/app/views/course/gradebook/students/index.php
@@ -10,16 +10,25 @@
 <article class="gradebook-student">
     <header>
         <h1><?= _("Gesamt") ?></h1>
-        <?= $this->render_partial("course/gradebook/_progress", ['value' => $controller->formatAsPercent($total)])?>
+        <? if ($total > 0) : ?>
+            <?= $this->render_partial("course/gradebook/_progress", ['value' => $controller->formatAsPercent($total)])?>
+        <? endif ?>
+        <? if ($passed) : ?>
+            <div><?= _('bestanden')?></div>
+        <? endif ?>
     </header>
 
     <? foreach ($categories as $category) { ?>
         <section class="gradebook-student-category">
             <header>
                 <h2><?= $controller->formatCategory($category) ?></h2>
-                <?= $this->render_partial("course/gradebook/_progress", ['value' => $controller->formatAsPercent($subtotals[$category])])?>
             </header>
-
+            <? if ($subtotals[$category] > 0) : ?>
+                <?= $this->render_partial("course/gradebook/_progress", ['value' => $controller->formatAsPercent($subtotals[$category])])?>
+            <? endif ?>
+            <? if ($subpassed[$category]) : ?>
+                <div><?= _('bestanden')?></div>
+            <? endif ?>
             <table class="default">
                 <colgroup>
                     <col width="200px" />
@@ -43,11 +52,15 @@
                         $instance = $groupedInstances[$definition->id] ?? null;
                         $grade = $controller->formatAsPercent($instance ? $instance->rawgrade : 0);
                         $feedback = $instance ? $instance->feedback : '';
+                        $passed = $instance ? $instance->passed : 0;
                     ?>
                         <tr>
                             <td>
                                 <span class="gradebook-definition-name"><?= htmlReady($definition->name) ?></span>
-                                <?= $this->render_partial("course/gradebook/_progress", ['value' => (int) $grade])?>
+                                <? if ($grade > 0) : ?>
+                                    <?= $this->render_partial("course/gradebook/_progress", ['value' => (int) $grade])?>
+                                <? endif ?>
+                                <div><?= $passed ? _('bestanden') : '' ?></div>
                             </td>
                             <td>
                                 <?= htmlReady($definition->tool) ?>
diff --git a/db/migrations/5.5.15_step3344_ilias_results.php b/db/migrations/5.5.15_step3344_ilias_results.php
new file mode 100644
index 00000000000..20bb33ad71f
--- /dev/null
+++ b/db/migrations/5.5.15_step3344_ilias_results.php
@@ -0,0 +1,32 @@
+<?php
+require_once 'lib/cronjobs/import_ilias_testresults.php';
+final class Step3344IliasResults extends Migration
+{
+
+    use DatabaseMigrationTrait;
+
+    public function description()
+    {
+        return 'adds column passed to table `grading_instances`, add cronjob';
+    }
+
+    protected function up()
+    {
+        if (!$this->columnExists('grading_instances', 'passed')) {
+            DBManager::get()->exec("ALTER TABLE `grading_instances` ADD
+                `passed` TINYINT NOT NULL DEFAULT 0 AFTER `feedback`");
+
+        }
+        ImportIliasTestresults::register()->schedulePeriodic(45, 1);
+    }
+
+    protected function down()
+    {
+        if ($this->columnExists('grading_instances', 'passed')) {
+            DBManager::get()->exec("ALTER TABLE `grading_instances` DROP
+                `passed`");
+        }
+        ImportIliasTestresults::unregister();
+    }
+
+}
diff --git a/lib/cronjobs/import_ilias_testresults.php b/lib/cronjobs/import_ilias_testresults.php
new file mode 100644
index 00000000000..c8c12250fc5
--- /dev/null
+++ b/lib/cronjobs/import_ilias_testresults.php
@@ -0,0 +1,51 @@
+<?php
+/**
+ * ImportIliasTestresults
+ *
+ * @author André Noack <noack@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
+ */
+
+class ImportIliasTestresults extends CronJob
+{
+    public static function getName()
+    {
+        return _('Testergebnisse aus ILIAS importieren');
+    }
+
+    public static function getDescription()
+    {
+        return _('Importiert Testergebnisse in das Gradebook');
+    }
+
+    public static function getParameters()
+    {
+        return [
+            'verbose' => [
+                'type'        => 'boolean',
+                'default'     => false,
+                'status'      => 'optional',
+                'description' => _('Sollen Ausgaben erzeugt werden (sind später im Log des Cronjobs sichtbar)'),
+            ]
+        ];
+    }
+
+    public function execute($last_result, $parameters = [])
+    {
+        $verbose = $parameters['verbose'];
+        $db = DBManager::get();
+        if (Config::get()->ILIAS_INTERFACE_ENABLE) {
+            $courses = $db->fetchFirst("SELECT DISTINCT course_id FROM grading_definitions WHERE tool='ILIAS'");
+            foreach ($courses as $course_id) {
+                $course = Course::find($course_id);
+                if ($course && $course->isToolActive('IliasInterfaceModule')) {
+                    $num = IliasObjectConnections::importIliasResultsForCourse($course);
+                    if ($verbose) {
+                        echo 'Veranstaltung: ' . $course->name . ' '. $course->id . ': ' . $num . ' Ergebnisse übertragen.' . "\n";
+                    }
+                }
+            }
+        } else {
+            echo 'ILIAS_INTERFACE is not enabled';
+        }
+    }
+}
diff --git a/lib/ilias_interface/ConnectedIlias.class.php b/lib/ilias_interface/ConnectedIlias.class.php
index dff41c892f3..6b827839a68 100644
--- a/lib/ilias_interface/ConnectedIlias.class.php
+++ b/lib/ilias_interface/ConnectedIlias.class.php
@@ -1346,4 +1346,19 @@ class ConnectedIlias
     public function deleteConnectedModules($object_id){
         return IliasObjectConnections::DeleteAllConnections($object_id, $this->index);
     }
+
+    /**
+     * @param string $ilias_user_id
+     * @return IliasUser|void
+     */
+    public function getConnectedUser(string $ilias_user_id)
+    {
+        $user_id = DBManager::get()->fetchColumn(
+            "SELECT studip_user_id FROM auth_extern WHERE external_user_id = ? AND external_user_system_type = ?",
+            [$ilias_user_id, $this->index]
+        );
+        if ($user_id) {
+            return new IliasUser($this->index, $this->ilias_config['version'], $user_id);
+        }
+    }
 }
diff --git a/lib/ilias_interface/IliasObjectConnections.class.php b/lib/ilias_interface/IliasObjectConnections.class.php
index 87468e99615..0c6a8a9721f 100644
--- a/lib/ilias_interface/IliasObjectConnections.class.php
+++ b/lib/ilias_interface/IliasObjectConnections.class.php
@@ -135,7 +135,7 @@ class IliasObjectConnections
             return false;
         }
     }
-    
+
     /**
     * get module-id
     *
@@ -273,4 +273,39 @@ class IliasObjectConnections
         $statement->execute([$object_id, $cms_type]);
         return $statement->rowCount();
     }
+
+    /**
+     * @param Course $course
+     * @return int
+     */
+    public static function importIliasResultsForCourse(Course $course): int
+    {
+        $connected_ilias = [];
+        $students = new SimpleCollection($course->getMembersWithStatus('autor'));
+        $num = 0;
+        foreach (Grading\Definition::findBySQL("course_id = ? AND tool='ILIAS'", [$course->id]) as $definition) {
+            [$index, $module_id, $import_type] = explode('-', $definition->item);
+            if (!isset($connected_ilias[$index])) {
+                $connected_ilias[$index] = new ConnectedIlias($index);
+            }
+            $test_result = $connected_ilias[$index]->soap_client->getTestResults($module_id);
+            foreach ($test_result as $result) {
+                $ilias_user = $connected_ilias[$index]->getConnectedUser($result['user_id']);
+                if ($ilias_user) {
+                    $member = $students->findOneBy('user_id', $ilias_user->getStudipId());
+                    if ($member) {
+                        $grade = Grading\Instance::import([
+                                'definition_id' => $definition->id,
+                                'user_id'       => $member->user_id,
+                                'rawgrade'      => $import_type & 1 && $result['maximum_points'] ? $result['received_points'] / $result['maximum_points'] : 0,
+                                'passed'        => $import_type & 2 ? $result['passed'] : 0
+                            ]
+                        );
+                        $num += $grade->store();
+                    }
+                }
+            }
+        }
+        return $num;
+    }
 }
diff --git a/lib/ilias_interface/IliasSoap.class.php b/lib/ilias_interface/IliasSoap.class.php
index d5647f4d73e..37d7c621aa1 100644
--- a/lib/ilias_interface/IliasSoap.class.php
+++ b/lib/ilias_interface/IliasSoap.class.php
@@ -1672,4 +1672,41 @@ class IliasSoap extends StudipSoapClient
         }
         return false;
     }
+
+    /**
+     * @param string $ref_id
+     * @param bool $sum_only
+     * @return array|false
+     * @throws Exception
+     */
+    public function getTestResults($ref_id, $sum_only = true)
+    {
+        $param = [
+            'sid'    => $this->getSID(),
+            'ref_id' => $ref_id,
+            'sum_only' => $sum_only
+        ];
+        $result = $this->call('getTestResults', $param);
+        if ($result !== false) {
+            $columns = [];
+            $data = [];
+            $xml = simplexml_load_string($result);
+            foreach ($xml->colspecs->colspec as $colspec) {
+                $columns[] = (string)$colspec['name'];
+            }
+            foreach ($xml->rows->row as $row) {
+                $data_row = [];
+                $i = 0;
+                foreach ($row->column as $column) {
+                    $data_row[$columns[$i++]] = (string)$column;
+                }
+                if (isset($data_row['user_id'])) {
+
+                }
+                $data[] = $data_row;
+            }
+            return $data;
+        }
+        return false;
+    }
 }
diff --git a/lib/modules/GradebookModule.class.php b/lib/modules/GradebookModule.class.php
index 6c000a74971..0423622a99a 100644
--- a/lib/modules/GradebookModule.class.php
+++ b/lib/modules/GradebookModule.class.php
@@ -112,6 +112,12 @@ class GradebookModule extends CorePlugin implements SystemPlugin, StudipModule
             'custom_definitions',
             new Navigation(_('Noten manuell erfassen'), 'dispatch.php/course/gradebook/lecturers/custom_definitions')
         );
+        if (Config::get()->ILIAS_INTERFACE_ENABLE && $cid && Course::findCurrent()->isToolActive('IliasInterfaceModule')) {
+            $navigation->addSubNavigation(
+                'edit_ilias_definitions',
+                new Navigation(_('ILIAS-Test als Leistung definieren'), 'dispatch.php/course/gradebook/lecturers/edit_ilias_definitions')
+            );
+        }
     }
 
     /**
-- 
GitLab