From 65a5354a4a0e51470201c0e74f91f34a84bd623a Mon Sep 17 00:00:00 2001
From: Thomas Hackl <hackl@data-quest.de>
Date: Mon, 13 Feb 2023 15:57:15 +0000
Subject: [PATCH] =?UTF-8?q?Resolve=20"Zertifikatsversand=20muss=20f=C3=BCr?=
 =?UTF-8?q?=20neue=20Coursewarefunktionen=20angepasst=20werden"?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Closes #2027

Merge request studip/studip!1320
---
 db/migrations/5.3.18_cw_unit_adjustments.php  | 108 ++++++
 .../Courseware/CoursewareInstancesUpdate.php  |   3 +
 lib/cronjobs/courseware.php                   | 364 +++++++++---------
 lib/models/Courseware/Instance.php            | 135 ++++---
 lib/models/Courseware/Unit.php                |   9 +-
 5 files changed, 356 insertions(+), 263 deletions(-)
 create mode 100644 db/migrations/5.3.18_cw_unit_adjustments.php

diff --git a/db/migrations/5.3.18_cw_unit_adjustments.php b/db/migrations/5.3.18_cw_unit_adjustments.php
new file mode 100644
index 00000000000..131c81c7558
--- /dev/null
+++ b/db/migrations/5.3.18_cw_unit_adjustments.php
@@ -0,0 +1,108 @@
+<?php
+
+class CwUnitAdjustments extends Migration
+{
+    public function description()
+    {
+        return 'adjust courseware config to units';
+    }
+
+    public function up()
+    {
+        // Add column for storing per-unit configuration.
+        DBManager::get()->exec(
+            "ALTER TABLE `cw_units` ADD `config` TEXT NOT NULL DEFAULT '' AFTER `withdraw_date`"
+        );
+
+        // Which fields in config are relevant for this migration?
+        $fields = [
+            'COURSEWARE_SEQUENTIAL_PROGRESSION',
+            'COURSEWARE_EDITING_PERMISSION',
+            'COURSEWARE_CERTIFICATE_SETTINGS',
+            'COURSEWARE_REMINDER_SETTINGS',
+            'COURSEWARE_RESET_PROGRESS_SETTINGS',
+            'COURSEWARE_LAST_REMINDER',
+            'COURSEWARE_LAST_PROGRESS_RESET'
+        ];
+
+        // Which courses do have custom courseware settings and need to be migrated?
+        $ranges = DBManager::get()->fetchFirst(
+            "SELECT DISTINCT `range_id` FROM `config_values` WHERE `field` IN (:fields)",
+            ['fields' => $fields]
+        );
+
+        $update = DBManager::get()->prepare("UPDATE `cw_units` SET `config` = :config WHERE `id` = :unit");
+
+        // Get courseware settings per course as stored in config_values,
+        foreach ($ranges as $course) {
+            $global = DBManager::get()->fetchAll(
+                "SELECT `field`, `value` FROM `config_values` WHERE `range_id` = :range AND `field` IN (:fields)",
+                ['range' => $course, 'fields' => $fields]
+            );
+
+            // Build configuration per unit.
+            $config = [];
+            // Convert values.
+            foreach ($global as $one) {
+
+                $decoded = json_decode($one['value'], true);
+
+                foreach ($decoded as $unit_id => $settings) {
+                    switch ($one['field']) {
+                        case 'COURSEWARE_SEQUENTIAL_PROGRESSION':
+                            $config[$unit_id]['sequential_progression'] = $settings;
+                            break;
+                        case 'COURSEWARE_EDITING_PERMISSION':
+                            $config[$unit_id]['editing_permission'] = $settings;
+                            break;
+                        case 'COURSEWARE_CERTIFICATE_SETTINGS':
+                            $config[$unit_id]['certificate'] = $settings;
+                            break;
+                        case 'COURSEWARE_REMINDER_SETTINGS':
+                            $config[$unit_id]['reminder'] = $settings;
+                            break;
+                        case 'COURSEWARE_RESET_PROGRESS_SETTINGS':
+                            $config[$unit_id]['reset_progress'] = $settings;
+                            break;
+                        case 'COURSEWARE_LAST_REMINDER':
+                            $config[$unit_id]['last_reminder'] = $settings;
+                            break;
+                        case 'COURSEWARE_LAST_PROGRESS_RESET':
+                            $config[$unit_id]['last_progress_reset'] = $settings;
+                            break;
+                    }
+                }
+            }
+
+            // Now write per-unit configurations to database.
+            foreach ($config as $unit => $config) {
+                $update->execute(['config' => json_encode($config), 'unit' => $unit]);
+            }
+
+        }
+
+        // Drop old values from global config.
+        DBManager::get()->execute(
+            "DELETE FROM `config` WHERE `field` IN (:fields)",
+            ['fields' => $fields]
+        );
+        DBManager::get()->execute(
+            "DELETE FROM `config_values` WHERE `field` IN (:fields)",
+            ['fields' => $fields]
+        );
+
+        // Add column for storing unit_id with certificate date.
+        DBManager::get()->exec(
+            "ALTER TABLE `cw_certificates` ADD `unit_id` INT NOT NULL AFTER `course_id`"
+        );
+        DBManager::get()->exec("ALTER TABLE `cw_certificates` DROP INDEX `index_course_id`, DROP INDEX `index_user_ourse`");
+        DBManager::get()->exec("ALTER TABLE `cw_certificates` ADD INDEX index_unit_id (`unit_id`)");
+    }
+
+    public function down()
+    {
+        // Drop columns for storing per-unit configuration.
+        DBManager::get()->exec("ALTER TABLE `cw_units` DROP `config`");
+        DBManager::get()->exec("ALTER TABLE `cw_certificates` DROP `unit_id`");
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesUpdate.php b/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesUpdate.php
index e48588413cf..fff4608cdeb 100644
--- a/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesUpdate.php
+++ b/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesUpdate.php
@@ -133,6 +133,9 @@ class CoursewareInstancesUpdate extends JsonApiController
         $resetProgressSettings = $get('data.attributes.reset-progress-settings');
         $instance->setResetProgressSettings($resetProgressSettings);
 
+        // Store changes in unit configuration.
+        $instance->getUnit()->store();
+
         return $instance;
     }
 }
diff --git a/lib/cronjobs/courseware.php b/lib/cronjobs/courseware.php
index 16e0ad96475..37259c6b309 100644
--- a/lib/cronjobs/courseware.php
+++ b/lib/cronjobs/courseware.php
@@ -41,200 +41,176 @@ class CoursewareCronjob extends CronJob
         $verbose = $parameters['verbose'];
 
         /*
-         * Fetch all courses that have some relevant settings.
+         * Fetch all units that have some relevant settings.
          */
-        $todo = DBManager::get()->fetchAll(
-            "SELECT c.`range_id`, c. `field`, c.`value`
-            FROM `config_values` c
-                JOIN `seminare` s ON (s.`Seminar_id` = c.`range_id`)
-            WHERE c.`field` IN (:fields)",
-            ['fields' => [
-                // Send certificate when this progress is reached
-                'COURSEWARE_CERTIFICATE_SETTINGS',
-                // Remind all users about courseware
-                'COURSEWARE_REMINDER_SETTINGS',
-                // Reset user progress to 0
-                'COURSEWARE_RESET_PROGRESS_SETTINGS'
-            ]
-            ]
+        $todo = Courseware\Unit::findBySQL(
+            "`range_type` = 'course' AND (`config` LIKE (:cert) OR `config` LIKE (:reminder) OR `config` LIKE (:reset))",
+            ['cert' => '%"certificate":%', 'reminder' => '%"reminder":%', 'reset' => '%"reset_progress":%']
         );
 
         if (count($todo) > 0) {
 
             if ($verbose) {
-                echo sprintf("Found %u courses to process.\n", count($todo));
+                printf("Found %u units to process.\n", count($todo));
             }
 
             $timezone = Config::get()->DEFAULT_TIMEZONE;
 
             // Process all found entries...
-            foreach ($todo as $one) {
-
-                // Fetch all courseware blocks belonging to the current course.
-                $blocks = DBManager::get()->fetchFirst(
-                    "SELECT DISTINCT b.`id`
-                            FROM `cw_blocks` b
-                                JOIN `cw_containers` c ON (c.`id` = b.`container_id`)
-                                JOIN `cw_structural_elements` e ON (e.`id` = c.`structural_element_id`)
-                            WHERE e.`range_id` = :course",
-                    ['course' => $one['range_id']]
-                );
-
-                // extract details from JSON
-                $settings = json_decode($one['value'], true);
-
-                // differentiate by setting type
-                switch ($one['field']) {
-                    // Send certificates to those who have progressed far enough and have not yet gotten one.
-                    case 'COURSEWARE_CERTIFICATE_SETTINGS':
-
-                        if ($verbose) {
-                            echo sprintf("Generating certificates for course %s.\n",
-                                $one['range_id']);
-                        }
-
-                        // Fetch accumulated progress values for all users in this course.
-                        $progresses = DBManager::get()->fetchAll(
-                            "SELECT DISTINCT p.`user_id`, SUM(p.`grade`) AS progress
-                            FROM `cw_user_progresses` p
-                            WHERE `block_id` IN (:blocks)
-                                AND NOT EXISTS (
-                                    SELECT `id` FROM `cw_certificates` WHERE `user_id` = p.`user_id` AND `course_id` = :course
-                                )
-                            GROUP BY `user_id`",
-                            ['blocks' => $blocks, 'course' => $one['range_id']]
-                        );
-
-                        // Calculate percentual progress and send certificates if necessary.
-                        foreach ($progresses as $progress) {
-                            $percent = ($progress['progress'] / count($blocks)) * 100;
-                            if ($percent >= $settings['threshold']) {
-                                if ($verbose) {
-                                    echo sprintf("User %s will get a certificate for course %s.\n",
-                                        $progress['user_id'], $one['range_id']);
-                                }
-
-                                $this->sendCertificate($one['range_id'], $progress['user_id'],
-                                    $percent, $settings);
-
-                                /*
-                                 * Insert a new entry into database for tracking who already got a certificate.
-                                 * This can be useful if certificates get a validity time or such.
-                                 */
-                                $entry = new Courseware\Certificate();
-                                $entry->user_id = $progress['user_id'];
-                                $entry->course_id = $one['range_id'];
-                                $entry->store();
-                            }
-                        }
-
-                        break;
-
-                    // Send reminders to all course participants.
-                    case 'COURSEWARE_REMINDER_SETTINGS':
-
-                        // Check when the last reminder was sent...
-                        $now = new DateTime('', new DateTimeZone($timezone));
-
-                        // What would be the minimum date for the last reminder?
-                        $minReminder = clone $now;
-
-                        // The last reminder has been sent at?
-                        $lastReminder = new DateTime('', new DateTimeZone($timezone));
-                        $lastReminder->setTimestamp(
-                            UserConfig::get($one['range_id'])->COURSEWARE_LAST_REMINDER ?: 0
-                        );
-
-                        // Check if the settings specify a start and/or end date for reminders
-                        $start = new DateTime($settings['startDate'] ?: '1970-01-01',
-                            new DateTimeZone($timezone));
-                        $end = new DateTime($settings['endDate'] ?: '2199-12-31',
-                            new DateTimeZone($timezone));
-
-                        $interval = new DateInterval('P1D');
-                        switch ($settings['interval']) {
-                            case 7:
-                                $interval = new DateInterval('P7D');
-                                break;
-                            case 14:
-                                $interval = new DateInterval('P14D');
-                                break;
-                            case 30:
-                                $interval = new DateInterval('P1M');
-                                break;
-                            case 90:
-                                $interval = new DateInterval('P3M');
-                                break;
-                            case 180:
-                                $interval = new DateInterval('P6M');
-                                break;
-                            case 365:
-                                $interval = new DateInterval('P1Y');
-                                break;
-                        }
-                        $minReminder->sub($interval);
-
-                        // ... and send a new one if necessary.
-                        if ($lastReminder <= $minReminder && $now >= $start && $now <= $end) {
+            foreach ($todo as $unit) {
+
+                // Fetch all courseware block IDs belonging to the current unit.
+                $instance = new Courseware\Instance($unit->structural_element);
+                $blocks = array_column($instance->findAllBlocks(), 'id');
+
+                // Send certificates to those who have progressed far enough and have not yet gotten one.
+                if (isset($unit->config['certificate'])) {
+                    if ($verbose) {
+                        printf("Generating certificates for course %s, unit %u.\n",
+                            $unit->range_id, $unit->id);
+                    }
+
+                    // Fetch accumulated progress values for all users in this course.
+                    $progresses = DBManager::get()->fetchAll(
+                        "SELECT DISTINCT p.`user_id`, SUM(p.`grade`) AS progress
+                        FROM `cw_user_progresses` p
+                        WHERE `block_id` IN (:blocks)
+                            AND NOT EXISTS (
+                                SELECT `id` FROM `cw_certificates` WHERE `user_id` = p.`user_id` AND `unit_id` = :unit
+                            )
+                        GROUP BY `user_id`",
+                        ['blocks' => $blocks, 'unit' => $unit->id]
+                    );
+
+                    // Calculate percentual progress and send certificates if necessary.
+                    foreach ($progresses as $progress) {
+                        $percent = ($progress['progress'] / count($blocks)) * 100;
+                        printf("User %s has progress %u.\n", $progress['user_id'], $percent);
+                        if ($percent >= $unit->config['certificate']['threshold']) {
                             if ($verbose) {
-                                echo sprintf("Sending reminders for course %s.\n",
-                                    $one['range_id']);
+                                printf("User %s will get a certificate for course %s and unit %u.\n",
+                                    $progress['user_id'], $unit->range_id, $unit->id);
                             }
 
-                            if ($this->sendReminders($one['range_id'], $settings)) {
-                                UserConfig::get($one['range_id'])->store('COURSEWARE_LAST_REMINDER',
-                                    $now->getTimestamp()
-                                );
+                            if (!$this->sendCertificate($unit, $progress['user_id'], $percent,
+                                $unit->config['certificate']['image'])) {
+                                printf("Could not send certificate for course %s and unit %u to user %s.\n",
+                                    $unit->range_id, $unit->id, $progress['user_id']);
                             }
                         }
+                    }
+                }
 
-                        break;
-
-                    // Reset courseware progress to 0 for all course participants.
-                    case 'COURSEWARE_RESET_PROGRESS_SETTINGS':
-
-                        // Check when the last reset was performed...
-                        $now = new DateTime('', new DateTimeZone($timezone));
-                        $checkLast = clone $now;
-                        $lastReset = new DateTime('', new DateTimeZone($timezone));
-                        $lastReset->setTimestamp(
-                            UserConfig::get($one['range_id'])->COURSEWARE_LAST_PROGRESS_RESET ?: 0
-                        );
-
-                        $interval = new DateInterval('P1D');
-                        switch ($one['value']) {
-                            case 14:
-                                $interval = new DateInterval('P14D');
-                                break;
-                            case 30:
-                                $interval = new DateInterval('P1M');
-                                break;
-                            case 90:
-                                $interval = new DateInterval('P3M');
-                                break;
-                            case 180:
-                                $interval = new DateInterval('P6M');
-                                break;
-                            case 365:
-                                $interval = new DateInterval('P1Y');
-                                break;
+                // Send reminder messages to participants if necessary.
+                if (isset($unit->config['reminder'])) {
+                    // Check when the last reminder was sent...
+                    $now = new DateTime('', new DateTimeZone($timezone));
+
+                    // What would be the minimum date for the last reminder?
+                    $minReminder = clone $now;
+
+                    // The last reminder has been sent at?
+                    $lastReminder = new DateTime('', new DateTimeZone($timezone));
+                    $lastReminder->setTimestamp($unit->config['last_reminder'] ?? 0);
+
+                    // Check if the settings specify a start and/or end date for reminders
+                    $start = new DateTime($unit->config['reminder']['startDate'] ?? '1970-01-01',
+                        new DateTimeZone($timezone));
+                    $end = new DateTime($unit->config['reminder']['endDate'] ?? '2199-12-31',
+                        new DateTimeZone($timezone));
+
+                    $interval = new DateInterval('P1D');
+                    switch ($unit->config['reminder']['interval']) {
+                        case 7:
+                            $interval = new DateInterval('P7D');
+                            break;
+                        case 14:
+                            $interval = new DateInterval('P14D');
+                            break;
+                        case 30:
+                            $interval = new DateInterval('P1M');
+                            break;
+                        case 90:
+                            $interval = new DateInterval('P3M');
+                            break;
+                        case 180:
+                            $interval = new DateInterval('P6M');
+                            break;
+                        case 365:
+                            $interval = new DateInterval('P1Y');
+                            break;
+                    }
+                    $minReminder->sub($interval);
+
+                    // ... and send a new one if necessary.
+                    if ($lastReminder <= $minReminder && $now >= $start && $now <= $end) {
+                        if ($verbose) {
+                            printf("Sending reminders for course %s and unit %u.\n",
+                                $unit->range_id, $unit->id);
                         }
 
-                        // ... and reset again if necessary.
-                        if ($lastReset <= $checkLast->sub($interval)) {
-                            if ($verbose) {
-                                echo sprintf("Resetting all progress for courseware in course %s.\n",
-                                    $one['range_id']);
-                            }
+                        if ($this->sendReminders($unit)) {
+                            $unit->config['last_reminder'] = time();
+                        }
+                    }
+                }
 
-                            // Remove all progress in the given blocks.
-                            $this->resetProgress($one['range_id'], $blocks, $settings);
+                // Reset progress if necessary.
+                if (isset($unit->config['reset_progress'])) {
+                    // Check when the last rest took place...
+                    $now = new DateTime('', new DateTimeZone($timezone));
+
+                    // What would be the minimum date for the last reset?
+                    $minReset = clone $now;
+
+                    // The last reset was done at:
+                    $lastReset = new DateTime('', new DateTimeZone($timezone));
+                    $lastReset->setTimestamp($unit->config['last_progress_reset'] ?? 0);
+
+                    // Check if the settings specify a start and/or end date for reminders
+                    $start = new DateTime($unit->config['reset_progress']['startDate'] ?? '1970-01-01',
+                        new DateTimeZone($timezone));
+                    $end = new DateTime($unit->config['reset_progress']['endDate'] ?? '2199-12-31',
+                        new DateTimeZone($timezone));
+
+                    $interval = new DateInterval('P1D');
+                    switch ($unit->config['reset_progress']['interval']) {
+                        case 7:
+                            $interval = new DateInterval('P7D');
+                            break;
+                        case 14:
+                            $interval = new DateInterval('P14D');
+                            break;
+                        case 30:
+                            $interval = new DateInterval('P1M');
+                            break;
+                        case 90:
+                            $interval = new DateInterval('P3M');
+                            break;
+                        case 180:
+                            $interval = new DateInterval('P6M');
+                            break;
+                        case 365:
+                            $interval = new DateInterval('P1Y');
+                            break;
+                    }
+                    $minReset->sub($interval);
+
+                    // ... and send a new one if necessary.
+                    if ($lastReset <= $minReset && $now >= $start && $now <= $end) {
+                        if ($verbose) {
+                            printf("Resetting progress for course %s and unit %u.\n",
+                                $unit->range_id, $unit->id);
+                        }
 
-                            UserConfig::get($one['range_id'])->store('COURSEWARE_LAST_PROGRESS_RESET',
-                                $now->getTimestamp()
-                            );
+                        if ($this->resetProgress($unit, $blocks, $unit->config['reset_progress']['mailText'])) {
+                            $unit->config['last_progress_reset'] = time();
                         }
+                    }
                 }
+
+                // Store config back, saving timestamps for reminders and progress reset.
+                $unit->store();
             }
 
         } else if ($verbose) {
@@ -242,10 +218,10 @@ class CoursewareCronjob extends CronJob
         }
     }
 
-    private function sendCertificate($course_id, $user_id, $progress, $settings)
+    private function sendCertificate($unit, $user_id, $progress, $image = '')
     {
         $user = User::find($user_id);
-        $course = Course::find($course_id);
+        $course = Course::find($unit->range_id);
 
         $template = $GLOBALS['template_factory']->open('courseware/mails/certificate');
         $html = $template->render(
@@ -253,7 +229,7 @@ class CoursewareCronjob extends CronJob
         );
 
         // Generate the PDF.
-        $pdf = new CoursewarePDFCertificate($settings['image']);
+        $pdf = new CoursewarePDFCertificate($image);
         $pdf->AddPage();
         $pdf->writeHTML($html, true, false, true, false, '');
         $pdf_file_name = $user->nachname . '_' . $course->name . '_' . _('Zertifikat') . '.pdf';
@@ -265,12 +241,12 @@ class CoursewareCronjob extends CronJob
 
         $message = sprintf(
             _('Anbei erhalten Sie Ihr Courseware-Zertifikat zur Veranstaltung %1$s, in der Sie einen Fortschritt ' .
-                'von %2$u %% erreicht haben.'), $course->getFullname(), $progress);
+                'von %2$u %% im Lernmaterial "%s" erreicht haben.'),
+            $course->getFullname(), $progress, $unit->structural_element->title);
         $message .= "\n\n" . _('Ãœber folgenden Link kommen Sie direkt zur Courseware') . ': ' .
-            URLHelper::getURL('seminar_main.php', ['auswahl' => $course->id,
-                'redirect_to' => 'dispatch.php/course/courseware']);
+            URLHelper::getURL('dispatch.php/course/courseware/courseware/' . $unit->id, ['cid' => $course->id]);
 
-        $mail->addRecipient($user->email, $user->getFullname())
+        $sent = $mail->addRecipient($user->email, $user->getFullname())
             ->setSubject(_('Courseware: Zertifikat') . ' - ' . $course->getFullname())
             ->setBodyText($message)
             ->addFileAttachment($filename, $pdf_file_name)
@@ -279,12 +255,20 @@ class CoursewareCronjob extends CronJob
         @unlink($filename);
 
         // Add database entry for the certificate.
-
+        if ($sent) {
+            $cert = new Courseware\Certificate();
+            $cert->user_id = $user_id;
+            $cert->course_id = $course->id;
+            $cert->unit_id = $unit->id;
+            return $cert->store();
+        } else {
+            return false;
+        }
     }
 
-    private function sendReminders($course_id, $settings)
+    private function sendReminders($unit)
     {
-        $course = Course::find($course_id);
+        $course = Course::find($unit->range_id);
 
         $recipients = $course->getMembersWithStatus('autor', true);
 
@@ -298,19 +282,19 @@ class CoursewareCronjob extends CronJob
             );
         }
 
-        $message = $settings['mailText'] . "\n\n" . _('Ãœber folgenden Link kommen Sie direkt zur Courseware') . ': ' .
-            URLHelper::getURL('seminar_main.php', ['auswahl' => $course->id,
-                'redirect_to' => 'dispatch.php/course/courseware']);
+        $message = $unit->config['reminder']['mailText'] . "\n\n" . _('Ãœber folgenden Link kommen Sie direkt zur Courseware') . ': ' .
+            URLHelper::getURL('dispatch.php/course/courseware/courseware/' . $unit->id, ['cid' => $course->id]);
 
-        $mail->setSubject(_('Courseware: Erinnerung') . ' - ' . $course->getFullname())
+        $mail->setSubject(_('Courseware: Erinnerung') . ' - ' . $course->getFullname() .
+                ', ' . $unit->structural_element->title)
             ->setBodyText($message);
 
         return $mail->send();
     }
 
-    private function resetProgress($course_id, $block_ids, $settings)
+    private function resetProgress($unit, $block_ids)
     {
-        $course = Course::find($course_id);
+        $course = Course::find($unit->range_id);
 
         DBManager::get()->execute(
             "DELETE FROM `cw_user_progresses` WHERE `block_id` IN (:blocks)",
@@ -329,9 +313,9 @@ class CoursewareCronjob extends CronJob
             );
         }
 
-        $message = $settings['mailText'] . "\n\n" . _('Ãœber folgenden Link kommen Sie direkt zur Courseware') . ': ' .
-            URLHelper::getURL('seminar_main.php', ['auswahl' => $course->id,
-                'redirect_to' => 'dispatch.php/course/courseware']);
+        $message = $unit->config['reset_progress']['mailText'] . "\n\n" .
+            _('Ãœber folgenden Link kommen Sie direkt zur Courseware') . ': ' .
+            URLHelper::getURL('dispatch.php/course/courseware/courseware/' . $unit->id, ['cid' => $course->id]);
 
         $mail->setSubject(_('Courseware: Fortschritt zurückgesetzt') . ' - ' . $course->getFullname())
             ->setBodyText($message);
diff --git a/lib/models/Courseware/Instance.php b/lib/models/Courseware/Instance.php
index 63bd34bd8aa..bee37016af2 100644
--- a/lib/models/Courseware/Instance.php
+++ b/lib/models/Courseware/Instance.php
@@ -34,9 +34,6 @@ class Instance
 
         $instance = new self($root);
 
-        $range->getConfiguration()->delete('COURSEWARE_SEQUENTIAL_PROGRESSION');
-        $range->getConfiguration()->delete('COURSEWARE_EDITING_PERMISSION');
-
         $last_element_configs = \ConfigValue::findBySQL('field = ? AND value LIKE ?', [
             'COURSEWARE_LAST_ELEMENT',
             '%' . $range->getRangeId() . '%',
@@ -62,6 +59,11 @@ class Instance
      */
     private $root;
 
+    /**
+     * @var Unit
+     */
+    private $unit;
+
     /**
      * Create a new representation of a a courseware instance.
      *
@@ -73,6 +75,7 @@ class Instance
     public function __construct(StructuralElement $root)
     {
         $this->root = $root;
+        $this->unit = $root->findUnit();
     }
 
     /**
@@ -85,6 +88,16 @@ class Instance
         return $this->root;
     }
 
+    /**
+     * Returns the unit belonging to this courseware instance.
+     *
+     * @return Unit the unit belonging this courseware instance
+     */
+    public function getUnit(): Unit
+    {
+        return $this->unit;
+    }
+
     /**
      * Returns the range this courseware instance belongs to.
      *
@@ -164,9 +177,7 @@ class Instance
      */
     public function getSequentialProgression(): bool
     {
-        $range = $this->getRange();
-        $root = $this->getRoot();
-        $sequentialProgression = $range->getConfiguration()->COURSEWARE_SEQUENTIAL_PROGRESSION[$root->id];
+        $sequentialProgression = $this->unit->config['sequential_progression'] ?? false;
 
         return (bool) $sequentialProgression;
     }
@@ -178,11 +189,7 @@ class Instance
      */
     public function setSequentialProgression(bool $isSequentialProgression): void
     {
-        $range = $this->getRange();
-        $root = $this->getRoot();
-        $progressions = $range->getConfiguration()->getValue('COURSEWARE_SEQUENTIAL_PROGRESSION');
-        $progressions[$root->id] = $isSequentialProgression ? 1 : 0;
-        $range->getConfiguration()->store('COURSEWARE_SEQUENTIAL_PROGRESSION', $progressions);
+        $this->unit->config['sequential_progression'] = $isSequentialProgression ? 1 : 0;
     }
 
     const EDITING_PERMISSION_DOZENT = 'dozent';
@@ -195,10 +202,8 @@ class Instance
      */
     public function getEditingPermissionLevel(): string
     {
-        $range = $this->getRange();
-        $root = $this->getRoot();
         /** @var string $editingPermissionLevel */
-        $editingPermissionLevel = $range->getConfiguration()->COURSEWARE_EDITING_PERMISSION[$root->id];
+        $editingPermissionLevel = $this->unit->config['editing_permission'];
         if ($editingPermissionLevel) {
             $this->validateEditingPermissionLevel($editingPermissionLevel);
             return $editingPermissionLevel;
@@ -216,11 +221,7 @@ class Instance
     public function setEditingPermissionLevel(string $editingPermissionLevel): void
     {
         $this->validateEditingPermissionLevel($editingPermissionLevel);
-        $range = $this->getRange();
-        $root = $this->getRoot();
-        $permissions = $range->getConfiguration()->getValue('COURSEWARE_EDITING_PERMISSION');
-        $permissions[$root->id] = $editingPermissionLevel;
-        $range->getConfiguration()->store('COURSEWARE_EDITING_PERMISSION', $permissions);
+        $this->unit->config['editing_permission'] = $editingPermissionLevel;
     }
 
     /**
@@ -250,13 +251,10 @@ class Instance
      */
     public function getCertificateSettings(): array
     {
-        $range = $this->getRange();
-        $root = $this->getRoot();
         /** @var array $certificateSettings */
-        $certificateSettings = json_decode(
-            $range->getConfiguration()->COURSEWARE_CERTIFICATE_SETTINGS[$root->id],
-            true
-        )?: [];
+        $certificateSettings = isset($this->unit->config['certificate'])
+            ? $this->unit->config['certificate']->getArrayCopy()
+            : [];
         $this->validateCertificateSettings($certificateSettings);
 
         return $certificateSettings;
@@ -269,27 +267,27 @@ class Instance
      */
     public function setCertificateSettings(array $certificateSettings): void
     {
-        $this->validateCertificateSettings($certificateSettings);
-        $range = $this->getRange();
-        $root = $this->getRoot();
-        $settings = $range->getConfiguration()->getValue('COURSEWARE_CERTIFICATE_SETTINGS');
-        $settings[$root->id] = count($certificateSettings) > 0 ? json_encode($certificateSettings) : null;
-        $range->getConfiguration()->store('COURSEWARE_CERTIFICATE_SETTINGS', $settings);
+        if (count($certificateSettings) > 0) {
+            $this->validateCertificateSettings($certificateSettings);
+            $this->unit->config['certificate'] = $certificateSettings;
+        } else {
+            unset($this->unit->config['certificate']);
+        }
     }
 
     /**
      * Validates certificate settings.
      *
-     * @param array $certificateSettings settings for certificate creation
+     * @param \JSONArrayObject $certificateSettings settings for certificate creation
      *
      * @return bool true if all given values are valid, false otherwise
      */
-    public function isValidCertificateSettings(array $certificateSettings): bool
+    public function isValidCertificateSettings($certificateSettings): bool
     {
         return (int) $certificateSettings['threshold'] >= 0 && (int) $certificateSettings['threshold'] <= 100;
     }
 
-    private function validateCertificateSettings(array $certificateSettings): void
+    private function validateCertificateSettings($certificateSettings): void
     {
         if (!$this->isValidCertificateSettings($certificateSettings)) {
             throw new \InvalidArgumentException('Invalid certificate settings given.');
@@ -303,13 +301,10 @@ class Instance
      */
     public function getReminderSettings(): array
     {
-        $range = $this->getRange();
-        $root = $this->getRoot();
-        /** @var int $reminderInterval */
-        $reminderSettings = json_decode(
-            $range->getConfiguration()->COURSEWARE_REMINDER_SETTINGS[$root->id],
-            true
-        )?: [];
+        /** @var array $reminderSettings */
+        $reminderSettings = isset($this->unit->config['reminder'])
+            ? $this->unit->config['reminder']->getArrayCopy()
+            : [];
         $this->validateReminderSettings($reminderSettings);
 
         return $reminderSettings;
@@ -318,33 +313,34 @@ class Instance
     /**
      * Sets the reminder message settings this courseware instance.
      *
-     * @param array $reminderSettings an array of parameters
+     * @param \JSONArrayObject $reminderSettings an array of parameters
      */
-    public function setReminderSettings(array $reminderSettings): void
+    public function setReminderSettings($reminderSettings): void
     {
-        $this->validateReminderSettings($reminderSettings);
-        $range = $this->getRange();
-        $root = $this->getRoot();
-        $settings = $range->getConfiguration()->getValue('COURSEWARE_REMINDER_SETTINGS');
-        $settings[$root->id] = count($reminderSettings) > 0 ? json_encode($reminderSettings) : null;
-        $range->getConfiguration()->store('COURSEWARE_REMINDER_SETTINGS', $settings);
+        if (count($reminderSettings) > 0) {
+            $this->validateReminderSettings($reminderSettings);
+            $this->unit->config['reminder'] = $reminderSettings;
+        } else {
+            unset($this->unit->config['reminder']);
+            unset($this->unit->config['last_reminder']);
+        }
     }
 
     /**
      * Validates reminder message settings.
      *
-     * @param array $reminderSettings settings for reminder mail sending
+     * @param \JSONArrayObject $reminderSettings settings for reminder mail sending
      *
      * @return bool true if all given values are valid, false otherwise
      */
-    public function isValidReminderSettings(array $reminderSettings): bool
+    public function isValidReminderSettings($reminderSettings): bool
     {
         $valid = in_array($reminderSettings['interval'], [0, 7, 14, 30, 90, 180, 365]);
 
         return $valid;
     }
 
-    private function validateReminderSettings(array $reminderSettings): void
+    private function validateReminderSettings($reminderSettings): void
     {
         if (!$this->isValidReminderSettings($reminderSettings)) {
             throw new \InvalidArgumentException('Invalid reminder settings given.');
@@ -358,13 +354,10 @@ class Instance
      */
     public function getResetProgressSettings(): array
     {
-        $range = $this->getRange();
-        $root = $this->getRoot();
-        /** @var int $reminderInterval */
-        $resetProgressSettings = json_decode(
-            $range->getConfiguration()->COURSEWARE_RESET_PROGRESS_SETTINGS[$root->id],
-            true
-        )?: [];
+        /** @var array $resetProgressSettings */
+        $resetProgressSettings = isset($this->unit->config['reset_progress'])
+            ? $this->unit->config['reset_progress']->getArrayCopy()
+            : [];
         $this->validateResetProgressSettings($resetProgressSettings);
 
         return $resetProgressSettings;
@@ -373,33 +366,34 @@ class Instance
     /**
      * Sets the progress resetting settings this courseware instance.
      *
-     * @param array $reminderSettings an array of parameters
+     * @param \JSONArrayObject $resetProgressSettings an array of parameters
      */
-    public function setResetProgressSettings(array $resetProgressSettings): void
+    public function setResetProgressSettings($resetProgressSettings): void
     {
-        $this->validateResetProgressSettings($resetProgressSettings);
-        $range = $this->getRange();
-        $root = $this->getRoot();
-        $settings = $range->getConfiguration()->getValue('COURSEWARE_RESET_PROGRESS_SETTINGS');
-        $settings[$root->id] = count($resetProgressSettings) > 0 ? json_encode($resetProgressSettings) : null;
-        $range->getConfiguration()->store('COURSEWARE_RESET_PROGRESS_SETTINGS', $settings);
+        if (count($resetProgressSettings) > 0) {
+            $this->validateResetProgressSettings($resetProgressSettings);
+            $this->unit->config['reset_progress'] = $resetProgressSettings;
+        } else {
+            unset($this->unit->config['reset_progress']);
+            unset($this->unit->config['last_progress_reset']);
+        }
     }
 
     /**
      * Validates progress resetting settings.
      *
-     * @param array $resetProgressSettings settings for progress resetting
+     * @param \JSONArrayObject $resetProgressSettings settings for progress resetting
      *
      * @return bool true if all given values are valid, false otherwise
      */
-    public function isValidResetProgressSettings(array $resetProgressSettings): bool
+    public function isValidResetProgressSettings($resetProgressSettings): bool
     {
         $valid = in_array($resetProgressSettings['interval'], [0, 14, 30, 90, 180, 365]);
 
         return $valid;
     }
 
-    private function validateResetProgressSettings(array $resetProgressSettings): void
+    private function validateResetProgressSettings($resetProgressSettings): void
     {
         if (!$this->isValidResetProgressSettings($resetProgressSettings)) {
             throw new \InvalidArgumentException('Invalid progress resetting settings given.');
@@ -491,4 +485,5 @@ class Instance
 
         return $data;
     }
+
 }
diff --git a/lib/models/Courseware/Unit.php b/lib/models/Courseware/Unit.php
index 5782d11c063..5c470c901ff 100644
--- a/lib/models/Courseware/Unit.php
+++ b/lib/models/Courseware/Unit.php
@@ -11,7 +11,7 @@ use User;
  * @license GPL2 or any later version
  *
  * @since   Stud.IP 5.3
- * 
+ *
  * @property int                            $id                     database column
  * @property string                         $range_id               database column
  * @property string                         $range_type             database column
@@ -21,11 +21,12 @@ use User;
  * @property string                         $creator_id             database column
  * @property int                            $release_date           database column
  * @property int                            $withdraw_date          database column
+ * @property \JSONArrayObject               $config                 database column
  * @property int                            $mkdate                 database column
  * @property int                            $chdate                 database column
  * @property \User                          $creator                belongs_to User
  * @property \Courseware\StructuralElement  $structural_element     belongs_to Courseware\StructuralElement
- * 
+ *
  * @SuppressWarnings(PHPMD.TooManyPublicMethods)
  * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
  */
@@ -36,6 +37,8 @@ class Unit extends \SimpleORMap
     {
         $config['db_table'] = 'cw_units';
 
+        $config['serialized_fields']['config'] = 'JSONArrayObject';
+
         $config['has_one']['structural_element'] = [
             'class_name' => StructuralElement::class,
             'foreign_key' => 'structural_element_id',
@@ -102,7 +105,7 @@ class Unit extends \SimpleORMap
             'release_date' => null,
             'withdraw_date' => null,
         ]);
-        
+
         $newUnit->store();
 
         return $newUnit;
-- 
GitLab