aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarcus Eibrink-Lunzenauer <lunzenauer@elan-ev.de>2025-07-10 09:14:44 +0000
committerRon Lucke <lucke@elan-ev.de>2025-07-10 11:14:44 +0200
commit1f5b6ce81987ed5a03279265ca881ecb4bac0832 (patch)
treeaff162afb291f206a490c0eb2d4ec98d29e3e140
parent7f557a0d69924597be5ca6b3aa8495ed26460429 (diff)
STEP 3263: Block- und Abschnittstypen standortspezifisch deaktivieren. (zweite Version)
Closes #3263 Merge request studip/studip!4206
-rw-r--r--app/controllers/admin/courseware.php139
-rw-r--r--app/views/admin/courseware/elements.php101
-rw-r--r--db/migrations/6.1.11_create_block_type_states.php30
-rw-r--r--db/migrations/6.1.12_create_container_type_states.php30
-rw-r--r--lib/classes/JsonApi/Schemas/Courseware/Instance.php2
-rw-r--r--lib/models/Courseware/BlockTypes/BlockType.php64
-rw-r--r--lib/models/Courseware/BlockTypes/BlockTypeState.php58
-rw-r--r--lib/models/Courseware/ContainerTypes/ContainerType.php56
-rw-r--r--lib/models/Courseware/ContainerTypes/ContainerTypeState.php60
-rw-r--r--lib/models/Courseware/Instance.php8
-rw-r--r--lib/navigation/AdminNavigation.php12
-rw-r--r--resources/assets/stylesheets/scss/courseware/blocks/default-block.scss33
-rw-r--r--resources/assets/stylesheets/scss/courseware/containers/default-container.scss21
-rw-r--r--resources/assets/stylesheets/scss/courseware/layouts/radioset.scss6
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareBlockActions.vue18
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue26
-rw-r--r--resources/vue/components/courseware/containers/CoursewareContainerActions.vue7
-rw-r--r--resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue20
-rw-r--r--resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue2
-rw-r--r--resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue17
-rw-r--r--resources/vue/components/courseware/toolbar/CoursewareToolbarContainers.vue6
-rw-r--r--resources/vue/components/courseware/widgets/CoursewareAdminViewWidget.vue16
22 files changed, 687 insertions, 45 deletions
diff --git a/app/controllers/admin/courseware.php b/app/controllers/admin/courseware.php
index 964ff4d..1af8e8c 100644
--- a/app/controllers/admin/courseware.php
+++ b/app/controllers/admin/courseware.php
@@ -1,28 +1,146 @@
<?php
+use Courseware\BlockTypes\BlockType;
+use Courseware\ContainerTypes\ContainerType;
+
+/**
+ * @SuppressWarnings(PHPMD.CamelCaseClassName)
+ * @SuppressWarnings(PHPMD.CamelCaseMethodName)
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ */
class Admin_CoursewareController extends AuthenticatedController
{
+ /**
+ * @SuppressWarnings(PHPMD.Superglobals)
+ */
public function before_filter(&$action, &$args)
{
parent::before_filter($action, $args);
$GLOBALS['perm']->check('root');
- PageLayout::setTitle(_('Coursewareverwaltung'));
- Navigation::activateItem('/admin/locations/courseware');
}
public function index_action()
{
- $this->setSidebar();
+ PageLayout::setTitle(_('Courseware Vorlagen'));
+ Navigation::activateItem('/admin/locations/courseware_templates');
+ $this->setIndexSidebar();
}
- private function setSidebar()
+ public function elements_action(): void
{
- $sidebar = Sidebar::Get();
- $views = new TemplateWidget(
- _('Ansichten'),
- $this->get_template_factory()->open('admin/courseware/admin_view_widget')
+ PageLayout::setTitle(_('Courseware Inhaltselemente'));
+ Navigation::activateItem('/admin/locations/courseware_elements');
+
+ $this->blockTypes = BlockType::getBlockTypes();
+ usort($this->blockTypes, fn($blockTypeA, $blockTypeB) => $blockTypeA::getTitle() <=> $blockTypeB::getTitle());
+
+ $this->containerTypes = ContainerType::getContainerTypes();
+
+ usort(
+ $this->containerTypes,
+ fn($containerTypeA, $containerTypeB) => $containerTypeA::getTitle() <=> $containerTypeB::getTitle()
+ );
+ }
+
+ public function activate_block_types_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $requestedBlockTypes = $this->validateBlockTypes();
+ $changed = array_sum(array_map(fn($blockType) => $blockType::activate() ? 1 : 0, $requestedBlockTypes));
+
+ PageLayout::postSuccess(
+ sprintf(
+ ngettext('Block-Typ erfolgreich aktiviert.', '%d Block-Typen erfolgreich aktiviert.', $changed),
+ $changed
+ )
);
- $sidebar->addWidget($views)->addLayoutCSSClass('courseware-admin-view-widget');
+ $this->redirect($this->action_url('elements'));
+ }
+
+ public function deactivate_block_types_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $requestedBlockTypes = $this->validateBlockTypes();
+ $changed = array_sum(array_map(fn($blockType) => $blockType::deactivate() ? 1 : 0, $requestedBlockTypes));
+
+ PageLayout::postSuccess(
+ sprintf(
+ ngettext('Block-Typ erfolgreich deaktiviert.', '%d Block-Typen erfolgreich deaktiviert.', $changed),
+ $changed
+ )
+ );
+ $this->redirect($this->action_url('elements'));
+ }
+
+ private function validateBlockTypes(): iterable
+ {
+ $requestedBlockTypes = Request::getArray('block_types');
+ $diff = array_diff($requestedBlockTypes, BlockType::getBlockTypes());
+ if (count($diff)) {
+ throw new Trails\Exception(400);
+ }
+
+ return $requestedBlockTypes;
+ }
+
+ public function activate_container_types_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $requestedContainerTypes = $this->validateContainerTypes();
+ $changed = array_sum(
+ array_map(fn($containerType) => $containerType::activate() ? 1 : 0, $requestedContainerTypes)
+ );
+
+ PageLayout::postSuccess(
+ sprintf(
+ ngettext('Container-Typ erfolgreich aktiviert.', '%d Container-Typen erfolgreich aktiviert.', $changed),
+ $changed
+ )
+ );
+ $this->redirect($this->action_url('elements'));
+ }
+
+ /**
+ */
+ public function deactivate_container_types_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $requestedContainerTypes = $this->validateContainerTypes();
+ $changed = array_sum(
+ array_map(fn($containerType) => $containerType::deactivate() ? 1 : 0, $requestedContainerTypes)
+ );
+
+ PageLayout::postSuccess(
+ sprintf(
+ ngettext(
+ 'Container-Typ erfolgreich deaktiviert.',
+ '%d Container-Typen erfolgreich deaktiviert.',
+ $changed
+ ),
+ $changed
+ )
+ );
+ $this->redirect($this->action_url('elements'));
+ }
+
+ private function validateContainerTypes(): iterable
+ {
+ $requestedContainerTypes = Request::getArray('container_types');
+ $diff = array_diff($requestedContainerTypes, ContainerType::getContainerTypes());
+ if (count($diff)) {
+ throw new Trails\Exception(400);
+ }
+
+ return $requestedContainerTypes;
+ }
+
+ private function setIndexSidebar()
+ {
+ $sidebar = Sidebar::Get();
$views = new TemplateWidget(
_('Aktionen'),
@@ -30,5 +148,4 @@ class Admin_CoursewareController extends AuthenticatedController
);
$sidebar->addWidget($views)->addLayoutCSSClass('courseware-admin-action-widget');
}
-
-} \ No newline at end of file
+}
diff --git a/app/views/admin/courseware/elements.php b/app/views/admin/courseware/elements.php
new file mode 100644
index 0000000..59a1c0f
--- /dev/null
+++ b/app/views/admin/courseware/elements.php
@@ -0,0 +1,101 @@
+<form method="post" action="<?= URLHelper::getLink('dispatch.php/admin/courseware/container_types') ?>">
+ <?= CSRFProtection::tokenTag() ?>
+ <table class="default sortable-table cw-admin-container-types">
+ <caption>
+ <?= _('Abschnittstypen') ?>
+ </caption>
+ <colgroup>
+ <col style="width: 5%">
+ <col style="width: 35%">
+ <col style="width: 60%">
+ </colgroup>
+ <thead>
+ <tr>
+ <th><?= _('Aktiv') ?></th>
+ <th data-sort="text"><?= _('Container-Typ') ?></th>
+ <th data-sort="text"><?= _('Beschreibung') ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <? foreach ($containerTypes as $containerType) { ?>
+ <? $isActivated = $containerType::isActivated(); ?>
+ <tr>
+ <td>
+ <label>
+ <? $formaction = $isActivated ? URLHelper::getURL(
+ 'dispatch.php/admin/courseware/deactivate_container_types',
+ ['container_types' => [$containerType]]
+ ) :
+ URLHelper::getURL('dispatch.php/admin/courseware/activate_container_types', [
+ 'container_types' => [$containerType],
+ ])
+ ?>
+ <span
+ class="sr-only"><? printf(_('Abschnittstyp "%s" %s'), $containerType::getTitle(), $isActivated ? _('deaktivieren') : _('aktivieren')) ?></span>
+ <button class="undecorated"
+ formaction="<?= $formaction ?>"><?= Icon::create($isActivated ? 'checkbox-checked' : 'checkbox-unchecked') ?></button>
+ </label>
+ </td>
+ <td data-sort-value="<?= htmlReady(strtolower($containerType::getType())) ?>">
+ <span><?= htmlReady($containerType::getTitle()) ?></span>
+ <span>(<?= htmlReady($containerType::getType()) ?>)</span>
+ </td>
+ <td data-sort-value="<?= htmlReady(strtolower($containerType::getDescription())) ?>">
+ <p>
+ <?= htmlReady($containerType::getDescription()) ?>
+ </p>
+ </td>
+ </tr>
+ <? } ?>
+ </tbody>
+ </table>
+</form>
+<form method="post" action="<?= URLHelper::getLink('dispatch.php/admin/courseware/block_types') ?>">
+ <?= CSRFProtection::tokenTag() ?>
+ <table class="default sortable-table cw-admin-block-types">
+ <caption><?= _('Blocktypen') ?></caption>
+ <colgroup>
+ <col style="width: 5%">
+ <col style="width: 35%">
+ <col style="width: 60%">
+ </colgroup>
+ <thead>
+ <tr>
+ <th><?= _('Aktiv') ?></th>
+ <th data-sort="text"><?= _('Block-Typ') ?></th>
+ <th data-sort="text"><?= _('Beschreibung') ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <? foreach ($blockTypes as $blockType) { ?>
+ <? $isActivated = $blockType::isActivated(); ?>
+ <tr>
+ <td>
+ <label>
+ <? $formaction = $isActivated ? URLHelper::getURL(
+ 'dispatch.php/admin/courseware/deactivate_block_types',
+ ['block_types' => [$blockType]]
+ ) : URLHelper::getURL(
+ 'dispatch.php/admin/courseware/activate_block_types',
+ ['block_types' => [$blockType]]
+ ) ?>
+ <span
+ class="sr-only"><? printf(_('Blocktyp "%s" %s'), $blockType::getTitle(), $isActivated ? _('deaktivieren') : _('aktivieren')) ?></span>
+ <button class="undecorated"
+ formaction="<?= $formaction ?>"><?= Icon::create($isActivated ? 'checkbox-checked' : 'checkbox-unchecked') ?></button>
+ </label>
+ </td>
+ <td data-sort-value="<?= htmlReady(strtolower($blockType::getType())) ?>">
+ <span><?= htmlReady($blockType::getTitle()) ?></span>
+ <span>(<?= htmlReady($blockType::getType()) ?>)</span>
+ </td>
+ <td data-sort-value="<?= htmlReady(strtolower($blockType::getDescription())) ?>">
+ <p>
+ <?= htmlReady($blockType::getDescription()) ?>
+ </p>
+ </td>
+ </tr>
+ <? } ?>
+ </tbody>
+ </table>
+</form> \ No newline at end of file
diff --git a/db/migrations/6.1.11_create_block_type_states.php b/db/migrations/6.1.11_create_block_type_states.php
new file mode 100644
index 0000000..51eea92
--- /dev/null
+++ b/db/migrations/6.1.11_create_block_type_states.php
@@ -0,0 +1,30 @@
+<?php
+
+class CreateBlockTypeStates extends Migration
+{
+ public function description()
+ {
+ return 'create table for block type states';
+ }
+
+ public function up()
+ {
+ $db = DBManager::get();
+ $query =
+ "CREATE TABLE IF NOT EXISTS `cw_block_type_states` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `block_type` VARCHAR(255) NOT NULL,
+ `activated` TINYINT(1) NOT NULL DEFAULT 0,
+ `mkdate` int(11) UNSIGNED NOT NULL,
+ `chdate` int(11) UNSIGNED NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `block_type` (`block_type`))";
+ $db->exec($query);
+ }
+
+ public function down()
+ {
+ $db = DBManager::get();
+ $db->exec('DROP TABLE IF EXISTS `cw_block_type_states`');
+ }
+}
diff --git a/db/migrations/6.1.12_create_container_type_states.php b/db/migrations/6.1.12_create_container_type_states.php
new file mode 100644
index 0000000..2b05aec
--- /dev/null
+++ b/db/migrations/6.1.12_create_container_type_states.php
@@ -0,0 +1,30 @@
+<?php
+
+class CreateContainerTypeStates extends Migration
+{
+ public function description()
+ {
+ return 'create table for container type states';
+ }
+
+ public function up()
+ {
+ $db = DBManager::get();
+ $query =
+ "CREATE TABLE IF NOT EXISTS `cw_container_type_states` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `container_type` VARCHAR(255) NOT NULL,
+ `activated` TINYINT(1) NOT NULL DEFAULT 0,
+ `mkdate` int(11) UNSIGNED NOT NULL,
+ `chdate` int(11) UNSIGNED NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `container_type` (`container_type`))";
+ $db->exec($query);
+ }
+
+ public function down()
+ {
+ $db = DBManager::get();
+ $db->exec('DROP TABLE IF EXISTS `cw_container_type_states`');
+ }
+}
diff --git a/lib/classes/JsonApi/Schemas/Courseware/Instance.php b/lib/classes/JsonApi/Schemas/Courseware/Instance.php
index c5683bc..dd8f4cb 100644
--- a/lib/classes/JsonApi/Schemas/Courseware/Instance.php
+++ b/lib/classes/JsonApi/Schemas/Courseware/Instance.php
@@ -68,6 +68,7 @@ class Instance extends SchemaProvider
'content_types' => $typeClass::getContentTypes(),
'file_types' => $typeClass::getFileTypes(),
'tags' => $typeClass::getTags(),
+ 'is-activated' => $typeClass::isActivated(),
];
}
@@ -80,6 +81,7 @@ class Instance extends SchemaProvider
'type' => $typeClass::getType(),
'title' => $typeClass::getTitle(),
'description' => $typeClass::getDescription(),
+ 'is-activated' => $typeClass::isActivated(),
];
}
diff --git a/lib/models/Courseware/BlockTypes/BlockType.php b/lib/models/Courseware/BlockTypes/BlockType.php
index 3e301eb..29ef06d 100644
--- a/lib/models/Courseware/BlockTypes/BlockType.php
+++ b/lib/models/Courseware/BlockTypes/BlockType.php
@@ -16,6 +16,7 @@ use Opis\JsonSchema\Validator;
* @since Stud.IP 5.0
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * @SuppressWarnings(PHPMD.StaticAccess)
*/
abstract class BlockType
{
@@ -78,7 +79,7 @@ abstract class BlockType
/**
* Returns a list of tags to which this type of block is associated.
- *
+ *
* @return array the list of tags
*/
public static function getTags(): array
@@ -127,9 +128,9 @@ abstract class BlockType
];
// try {
- foreach (\PluginEngine::getPlugins(CoursewarePlugin::class) as $plugin) {
- $blockTypes = $plugin->registerBlockTypes($blockTypes);
- }
+ foreach (\PluginEngine::getPlugins(CoursewarePlugin::class) as $plugin) {
+ $blockTypes = $plugin->registerBlockTypes($blockTypes);
+ }
// } catch (\Exception $e) {
// // there is nothing we can do here other than absorbing exceptions
// }
@@ -138,6 +139,16 @@ abstract class BlockType
}
/**
+ * Return all block types that are activated in this Stud.IP installation.
+ *
+ * @return iterable<string> an iterable of all activated `BlockType` classes
+ */
+ public static function getActivatedBlockTypes(): iterable
+ {
+ return BlockTypeState::getActivatedBlockTypes();
+ }
+
+ /**
* @param string $blockType a short string describing a type of block; see `getType`
*
* @return bool true, if the given type of block is valid; false otherwise
@@ -184,6 +195,47 @@ abstract class BlockType
}
/**
+ * @return bool `true`, if this `BlockType` is activated, otherwise `false`
+ */
+ public static function isActivated(): bool
+ {
+ return in_array(static::class, BlockTypeState::getActivatedBlockTypes());
+ }
+
+ /**
+ * Activates a `BlockType`.
+ *
+ * @return `true`, if this `BlockType` was activated, otherwise `false`
+ */
+ public static function activate(): bool
+ {
+ $state = static::findBlockTypeStateOrNew();
+ return $state->activate();
+ }
+
+ /**
+ * Deactivates a `BlockType`.
+ *
+ * @return `true`, if this `BlockType` was deactivated, otherwise `false`
+ */
+ public static function deactivate(): bool
+ {
+ $state = static::findBlockTypeStateOrNew();
+ return $state->deactivate();
+ }
+
+ private static function findBlockTypeStateOrNew(): BlockTypeState
+ {
+ $state = BlockTypeState::findOneBySql('block_type = ?', [static::class]);
+ if (!$state) {
+ $state = new BlockTypeState();
+ $state->block_type = static::class;
+ }
+
+ return $state;
+ }
+
+ /**
* Validates a given payload according to the JSON schema of this type of block.
*
* @param mixed $payload the payload to be validated
@@ -443,6 +495,10 @@ abstract class BlockType
*/
public function getPdfHtmlTemplate(): ?\Flexi\Template
{
+ if (!$this->isActivated()) {
+ return null;
+ }
+
$template = null;
try {
$template_name = strtosnakecase((new \ReflectionClass($this))->getShortName());
diff --git a/lib/models/Courseware/BlockTypes/BlockTypeState.php b/lib/models/Courseware/BlockTypes/BlockTypeState.php
new file mode 100644
index 0000000..b72a9f4
--- /dev/null
+++ b/lib/models/Courseware/BlockTypes/BlockTypeState.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Courseware\BlockTypes;
+
+use DBManager;
+use SimpleORMap;
+
+/**
+ * This class represents the activation state of a block type.
+ *
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ */
+class BlockTypeState extends SimpleORMap
+{
+ protected static function configure($config = [])
+ {
+ $config['db_table'] = 'cw_block_type_states';
+
+ parent::configure($config);
+ }
+
+ /**
+ * Returns all `BlockType`s that are activated in this Stud.IP installation.
+ *
+ * @return iterable<string> an iterable of all `BlockType` classes that are activated.
+ */
+ public static function getActivatedBlockTypes(): iterable
+ {
+ $args = [
+ BlockType::getBlockTypes(),
+ DBManager::get()->fetchFirst('SELECT `block_type` FROM `cw_block_type_states` WHERE `activated` = 0'),
+ ];
+
+ return [...array_diff(...$args)];
+ }
+
+ /**
+ * Activate the `BlockType` this `BlockTypeState` is relating to.
+ *
+ * @return `true` on success, `false` otherwise
+ */
+ public function activate(): bool
+ {
+ $this->activated = 1;
+ return (bool) $this->store();
+ }
+
+ /**
+ * Deactivate the `BlockType` this `BlockTypeState` is relating to.
+ *
+ * @return `true` on success, `false` otherwise
+ */
+ public function deactivate(): bool
+ {
+ $this->activated = 0;
+ return (bool) $this->store();
+ }
+}
diff --git a/lib/models/Courseware/ContainerTypes/ContainerType.php b/lib/models/Courseware/ContainerTypes/ContainerType.php
index 5639286..1081eec 100644
--- a/lib/models/Courseware/ContainerTypes/ContainerType.php
+++ b/lib/models/Courseware/ContainerTypes/ContainerType.php
@@ -12,6 +12,8 @@ use Opis\JsonSchema\Validator;
* @author Ron Lucke <lucke@elan-ev.de>
* @license GPL2 or any later version
*
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ *
* @since Stud.IP 5.0
*/
abstract class ContainerType
@@ -81,6 +83,15 @@ abstract class ContainerType
}
/**
+ * Return all container types that are activated in this Stud.IP installation.
+ *
+ * @return iterable<string> an iterable of all activated `ContainerType` classes
+ */
+ public static function getActivatedContainerTypes(): iterable
+ {
+ return ContainerTypeState::getActivatedContainerTypes();
+ }
+ /**
* @param string $containerType a short string describing a type of container; see `getType`
*
* @return bool true, if the given type of container is valid; false otherwise
@@ -141,6 +152,47 @@ abstract class ContainerType
}
/**
+ * @return bool `true`, if this `ContainerType` is activated, otherwise `false`
+ */
+ public static function isActivated(): bool
+ {
+ return in_array(static::class, ContainerTypeState::getActivatedContainerTypes());
+ }
+
+ /**
+ * Activates a `ContainerType`.
+ *
+ * @return `true`, if this `ContainerType` was activated, otherwise `false`
+ */
+ public static function activate(): bool
+ {
+ $state = static::findContainerTypeStateOrNew();
+ return $state->activate();
+ }
+
+ /**
+ * Deactivates a `ContainerType`.
+ *
+ * @return `true`, if this `ContainerType` was deactivated, otherwise `false`
+ */
+ public static function deactivate(): bool
+ {
+ $state = static::findContainerTypeStateOrNew();
+ return $state->deactivate();
+ }
+
+ private static function findContainerTypeStateOrNew(): ContainerTypeState
+ {
+ $state = ContainerTypeState::findOneBySql('container_type = ?', [static::class]);
+ if (!$state) {
+ $state = new ContainerTypeState();
+ $state->container_type = static::class;
+ }
+
+ return $state;
+ }
+
+ /**
* Validates a given payload according to the JSON schema of this type of container.
*
* @param mixed $payload the payload to be validated
@@ -253,6 +305,10 @@ abstract class ContainerType
*/
public function getPdfHtmlTemplate(): ?\Flexi\Template
{
+ if (!$this->isActivated()) {
+ return null;
+ }
+
$template = null;
try {
$template_name = strtosnakecase((new \ReflectionClass($this))->getShortName());
diff --git a/lib/models/Courseware/ContainerTypes/ContainerTypeState.php b/lib/models/Courseware/ContainerTypes/ContainerTypeState.php
new file mode 100644
index 0000000..c9b77f8
--- /dev/null
+++ b/lib/models/Courseware/ContainerTypes/ContainerTypeState.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Courseware\ContainerTypes;
+
+use DBManager;
+use SimpleORMap;
+
+/**
+ * This class represents the activation state of a container type.
+ *
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ */
+class ContainerTypeState extends SimpleORMap
+{
+ protected static function configure($config = [])
+ {
+ $config['db_table'] = 'cw_container_type_states';
+
+ parent::configure($config);
+ }
+
+ /**
+ * Returns all `ContainerType`s that are activated in this Stud.IP installation.
+ *
+ * @return iterable<string> an iterable of all `ContainerType` classes that are activated.
+ */
+ public static function getActivatedContainerTypes(): iterable
+ {
+ $args = [
+ ContainerType::getContainerTypes(),
+ DBManager::get()->fetchFirst(
+ 'SELECT `container_type` FROM `cw_container_type_states` WHERE `activated` = 0'
+ ),
+ ];
+
+ return [...array_diff(...$args)];
+ }
+
+ /**
+ * Activate the `ContainerType` this `ContainerTypeState` is relating to.
+ *
+ * @return `true` on success, `false` otherwise
+ */
+ public function activate(): bool
+ {
+ $this->activated = 1;
+ return (bool) $this->store();
+ }
+
+ /**
+ * Deactivate the `ContainerType` this `ContainerTypeState` is relating to.
+ *
+ * @return `true` on success, `false` otherwise
+ */
+ public function deactivate(): bool
+ {
+ $this->activated = 0;
+ return (bool) $this->store();
+ }
+}
diff --git a/lib/models/Courseware/Instance.php b/lib/models/Courseware/Instance.php
index c9005de..3579427 100644
--- a/lib/models/Courseware/Instance.php
+++ b/lib/models/Courseware/Instance.php
@@ -176,7 +176,7 @@ class Instance
- /*
+ /*
*
* GENERAL SETTINGS
*
@@ -295,7 +295,7 @@ class Instance
}
- /*
+ /*
*
* FEEDBACK
*
@@ -312,7 +312,7 @@ class Instance
{
$this->unit->config['show_feedback_popup'] = $showFeedbackPopup ? 1 : 0;
}
-
+
public function getShowFeedbackInContentbar(): bool
{
$showFeedbackInContentbar = $this->unit->config['show_feedback__in_contentbar'] ?? false;
@@ -325,7 +325,7 @@ class Instance
$this->unit->config['show_feedback__in_contentbar'] = $showFeedbackInContentbar ? 1 : 0;
}
- /*
+ /*
*
* CERTIFICATE
*
diff --git a/lib/navigation/AdminNavigation.php b/lib/navigation/AdminNavigation.php
index 3c92b62..7e115b2 100644
--- a/lib/navigation/AdminNavigation.php
+++ b/lib/navigation/AdminNavigation.php
@@ -129,12 +129,20 @@ class AdminNavigation extends Navigation
if (PluginManager::getInstance()->getPlugin(CoursewareModule::class)) {
$navigation->addSubNavigation(
- 'courseware',
+ 'courseware_elements',
new Navigation(
- _('Courseware'),
+ _('Courseware Inhaltselemente'),
+ 'dispatch.php/admin/courseware/elements'
+ )
+ );
+ $navigation->addSubNavigation(
+ 'courseware_templates',
+ new Navigation(
+ _('Courseware Vorlagen'),
'dispatch.php/admin/courseware/index'
)
);
+
}
if (Config::get()->OERCAMPUS_ENABLED) {
diff --git a/resources/assets/stylesheets/scss/courseware/blocks/default-block.scss b/resources/assets/stylesheets/scss/courseware/blocks/default-block.scss
index e5a9999..4b032ae 100644
--- a/resources/assets/stylesheets/scss/courseware/blocks/default-block.scss
+++ b/resources/assets/stylesheets/scss/courseware/blocks/default-block.scss
@@ -24,6 +24,7 @@
font-weight: 700;
line-height: 2em;
font-size: 1.1em;
+ margin-right: 0.25em;
&.cw-default-block-invisible-info,
&.cw-default-block-blocker-warning {
@@ -111,3 +112,35 @@
padding-top: 106px;
}
}
+
+
+.cw-block-item:not(.cw-block-item-sortable):has(.cw-default-block-deactivated) {
+ display: none;
+}
+
+.cw-default-block-deactivated {
+ .cw-block-header {
+ background-color: var(--color--attention);
+ }
+
+ .cw-block-content {
+ opacity: 0.3;
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: repeating-linear-gradient(
+ 45deg,
+ rgba(0, 0, 0, 0.4),
+ rgba(0, 0, 0, 0.4) 2px,
+ transparent 2px,
+ transparent 10px
+ );
+ pointer-events: none;
+ }
+ }
+} \ No newline at end of file
diff --git a/resources/assets/stylesheets/scss/courseware/containers/default-container.scss b/resources/assets/stylesheets/scss/courseware/containers/default-container.scss
index 8821935..02317f9 100644
--- a/resources/assets/stylesheets/scss/courseware/containers/default-container.scss
+++ b/resources/assets/stylesheets/scss/courseware/containers/default-container.scss
@@ -105,3 +105,24 @@ form.cw-container-dialog-edit-form {
padding-top: 10px;
}
}
+
+
+.cw-container-item.cw-container-deactivated {
+ display: none;
+}
+
+.cw-container-item-sortable .cw-container-item.cw-container-deactivated {
+ display: block;
+ .cw-container-header {
+ background-color: var(--color--attention);
+ }
+ .cw-block-wrapper {
+ background: repeating-linear-gradient(
+ 45deg,
+ rgba(0, 0, 0, 0.05),
+ rgba(0, 0, 0, 0.05) 2px,
+ transparent 2px,
+ transparent 10px
+ );
+ }
+} \ No newline at end of file
diff --git a/resources/assets/stylesheets/scss/courseware/layouts/radioset.scss b/resources/assets/stylesheets/scss/courseware/layouts/radioset.scss
index 9569da4..673ed2b 100644
--- a/resources/assets/stylesheets/scss/courseware/layouts/radioset.scss
+++ b/resources/assets/stylesheets/scss/courseware/layouts/radioset.scss
@@ -81,6 +81,12 @@ form.default .cw-radioset {
}
}
}
+ &.disabled {
+ opacity: 0.5;
+ label {
+ cursor: default;
+ }
+ }
&:last-child {
margin-right: 0;
}
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue b/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue
index 4688090..db9e775 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue
@@ -84,7 +84,7 @@ export default {
label: this.block.attributes.visible
? this.$gettext('unsichtbar setzen')
: this.$gettext('sichtbar setzen'),
- icon: this.block.attributes.visible ? 'visibility-visible' : 'visibility-invisible', // do we change the icons ?
+ icon: this.block.attributes.visible ? 'visibility-visible' : 'visibility-invisible',
emit: 'setVisibility',
});
if (this.userIsTeacher) {
@@ -106,14 +106,6 @@ export default {
emit: 'removeLock',
});
}
- if (!this.blocked || this.blockedByThisUser) {
- menuItems.push({
- id: 9,
- label: this.$gettext('Block löschen'),
- icon: 'trash',
- emit: 'deleteBlock'
- });
- }
menuItems.push({
id: 2,
label: this.$gettext('Block merken'),
@@ -127,6 +119,14 @@ export default {
emit: 'showInfo',
});
}
+ if (!this.blocked || this.blockedByThisUser) {
+ menuItems.push({
+ id: 9,
+ label: this.$gettext('Block löschen'),
+ icon: 'trash',
+ emit: 'deleteBlock'
+ });
+ }
}
menuItems.sort((a, b) => {
return a.id > b.id ? 1 : b.id > a.id ? -1 : 0;
diff --git a/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue b/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue
index 02e56ac..6aee29b 100644
--- a/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue
@@ -1,5 +1,5 @@
<template>
- <div v-if="block.attributes.visible || canEdit" class="cw-default-block" :class="[showEditMode ? 'cw-default-block-active' : '']">
+ <div v-if="block.attributes.visible || canEdit" class="cw-default-block" :class="[showEditMode ? 'cw-default-block-active' : '', isActivated ? '' : 'cw-default-block-deactivated']">
<div class="cw-content-wrapper" :class="[showEditMode ? 'cw-content-wrapper-active' : '']">
<header v-if="showEditMode" class="cw-block-header">
<a href="#" class="cw-block-header-toggle" :aria-expanded="isOpen" @click.prevent="isOpen = !isOpen">
@@ -13,11 +13,14 @@
<span v-if="!block.attributes.visible" class="cw-default-block-invisible-info">
{{ $gettext('Unsichtbar für Nutzende ohne Schreibrecht') }}
</span>
+ <span v-if="!isActivated">
+ - {{ $gettext('Block-Typ ist deaktiviert') }}
+ </span>
</a>
<courseware-block-actions
:block="block"
:canEdit="canEdit"
- :deleteOnly="deleteOnly"
+ :deleteOnly="deleteOnly || !isActivated"
@editBlock="displayFeature('Edit')"
@showInfo="displayFeature('Info')"
@showExportOptions="displayFeature('ExportOptions')"
@@ -97,7 +100,6 @@ import StudipIcon from '../../StudipIcon.vue';
import blockMixin from '@/vue/mixins/courseware/block.js';
import { mapActions, mapGetters } from 'vuex';
-
export default {
name: 'courseware-default-block',
mixins: [blockMixin],
@@ -185,9 +187,7 @@ export default {
return this.blockingUser ? this.blockingUser.attributes['formatted-name'] : '';
},
blockTitle() {
- const type = this.block.attributes['block-type'];
-
- return this.blockTypes.find((blockType) => blockType.type === type)?.title || this.$gettext('Fehler');
+ return this.blockTypes.find((blockType) => blockType.type === this.blockTypeName)?.title || this.$gettext('Fehler');
},
public() {
return this.context.type === 'public';
@@ -195,6 +195,20 @@ export default {
commentable() {
return this.block?.attributes?.commentable ?? false;
},
+ blockTypeName() {
+ return this.block.attributes['block-type'];
+ },
+ blockType(){
+ return this.blockTypes.find((blockType) => blockType.type === this.blockTypeName);
+ },
+
+ isActivated() {
+ if (!this.blockType) {
+ return false;
+ }
+
+ return this.blockType['is-activated'];
+ }
},
mounted() {
if (this.blocked) {
diff --git a/resources/vue/components/courseware/containers/CoursewareContainerActions.vue b/resources/vue/components/courseware/containers/CoursewareContainerActions.vue
index ce7c545..4b9656e 100644
--- a/resources/vue/components/courseware/containers/CoursewareContainerActions.vue
+++ b/resources/vue/components/courseware/containers/CoursewareContainerActions.vue
@@ -20,6 +20,7 @@ export default {
props: {
canEdit: Boolean,
container: Object,
+ isActivated: Boolean,
},
emits: [
'changeContainer',
@@ -48,11 +49,13 @@ export default {
menuItems() {
let menuItems = [];
if (!this.blockedByAnotherUser) {
- if (this.container.attributes["container-type"] !== 'list') {
+ if (this.container.attributes["container-type"] !== 'list' && this.isActivated) {
menuItems.push({ id: 1, label: this.$gettext('Abschnitt bearbeiten'), icon: 'edit', emit: 'editContainer' });
}
menuItems.push({ id: 2, label: this.$gettext('Abschnitt verändern'), icon: 'settings', emit: 'changeContainer' });
- menuItems.push({ id: 3, label: this.$gettext('Abschnitt merken'), icon: 'clipboard', emit: 'copyToClipboard' });
+ if (this.isActivated) {
+ menuItems.push({ id: 3, label: this.$gettext('Abschnitt merken'), icon: 'clipboard', emit: 'copyToClipboard' });
+ }
menuItems.push({ id: 5, label: this.$gettext('Abschnitt löschen'), icon: 'trash', emit: 'deleteContainer' });
}
diff --git a/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue b/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue
index b2eb624..6f1373b 100644
--- a/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue
+++ b/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue
@@ -2,7 +2,7 @@
<div
:id="'cw_container_' + container.id"
class="cw-container"
- :class="['cw-container-colspan-' + colSpan, showEditMode && canEdit ? 'cw-container-active' : '', containerClass]"
+ :class="['cw-container-colspan-' + colSpan, showEditMode && canEdit ? 'cw-container-active' : '', containerClass, isActivated ? '' : 'cw-container-deactivated']"
>
<div class="cw-container-content">
<header v-if="showEditMode" class="cw-container-header" :class="{ 'cw-container-header-open': isOpen }">
@@ -13,10 +13,14 @@
<span v-if="blockedByAnotherUser" class="cw-default-container-blocker-warning">
{{ $gettext('Wird im Moment von %{ userName } bearbeitet', { userName: this.blockingUserName }) }}
</span>
+ <span v-if="!isActivated">
+ - {{ $gettext('Abschnitts-Typ ist deaktiviert') }}
+ </span>
</a>
<courseware-container-actions
:canEdit="canEdit"
:container="container"
+ :isActivated="isActivated"
@editContainer="displayEditDialog"
@changeContainer="displayChangeDialog"
@deleteContainer="displayDeleteDialog"
@@ -76,9 +80,9 @@
v-for="(container, index) in containerTypes"
:key="index"
class="cw-radioset-box"
- :class="[container.type === changeType ? 'selected' : '']"
+ :class="[container.type === changeType ? 'selected' : '', container['is-activated'] ? '' : 'disabled']"
>
- <input type="radio" :id="'type-' + container.type" :value="container.type" v-model="changeType" name="container-type"/>
+ <input type="radio" :id="'type-' + container.type" :value="container.type" v-model="changeType" name="container-type" :disabled="!container['is-activated']"/>
<label :for="'type-' + container.type" >
<div class="label-icon" :class="[container.type, container.type === changeType ? 'selected' : '']"></div>
<div class="label-text"><span>{{ container.title }}</span></div>
@@ -220,6 +224,16 @@ export default {
},
type() {
return this.container.attributes['container-type'];
+ },
+ containerType() {
+ return this.containerTypes.find((containerType) => containerType.type === this.type);
+ },
+ isActivated() {
+ if (!this.containerType) {
+ return false;
+ }
+
+ return this.containerType['is-activated'];
}
},
methods: {
diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue
index df98bb4..ab42e4b 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue
@@ -137,7 +137,7 @@ export default {
favoriteBlockTypes: 'favoriteBlockTypes',
}),
blockTypes() {
- return _.sortBy(JSON.parse(JSON.stringify(this.unorderedBlockTypes)), ['title']);
+ return _.sortBy(JSON.parse(JSON.stringify(this.unorderedBlockTypes)), ['title']).filter(blockType => blockType['is-activated']);
},
blockCategories() {
return [
diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue
index f8f392a..de9e7aa 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue
@@ -126,15 +126,28 @@ export default {
containerById: 'courseware-containers/byId',
usersClipboards: 'courseware-clipboards/all',
userId: 'userId',
+ blockTypes: 'blockTypes',
+ containerTypes: 'containerTypes'
}),
clipboardBlocks() {
return this.usersClipboards
- .filter((clipboard) => clipboard.attributes['object-type'] === 'courseware-blocks')
+ .filter((clipboard) => {
+ const isBlock = clipboard.attributes['object-type'] === 'courseware-blocks';
+ const blockType = this.blockTypes.find((blockType) => blockType.type === clipboard.attributes['object-kind']);
+
+ return isBlock && blockType['is-activated'];
+ })
.sort((a, b) => b.attributes.mkdate - a.attributes.mkdate);
},
clipboardContainers() {
return this.usersClipboards
- .filter((clipboard) => clipboard.attributes['object-type'] === 'courseware-containers')
+ .filter((clipboard) => {
+ const isContainer = clipboard.attributes['object-type'] === 'courseware-containers';
+
+ const containerType = this.containerTypes.find((containerType) => containerType.type === clipboard.attributes['object-kind']);
+
+ return isContainer && containerType['is-activated'];
+ })
.sort((a, b) => b.attributes.mkdate < a.attributes.mkdate);
},
textDeleteClipboardTitle() {
diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarContainers.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarContainers.vue
index acc30b7..39ca582 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareToolbarContainers.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarContainers.vue
@@ -55,6 +55,7 @@
<script>
import CoursewareContainerAdderItem from './CoursewareContainerAdderItem.vue';
import containerMixin from '@/vue/mixins/courseware/container.js';
+import * as _ from 'lodash';
import draggable from 'vuedraggable';
import { mapGetters } from 'vuex';
@@ -73,11 +74,14 @@ export default {
},
computed: {
...mapGetters({
- containerTypes: 'containerTypes',
+ unfilteredContainerTypes: 'containerTypes',
structuralElementById: 'courseware-structural-elements/byId',
relatedContainers: 'courseware-containers/related',
}),
+ containerTypes() {
+ return _.sortBy(JSON.parse(JSON.stringify(this.unfilteredContainerTypes)), ['title']).filter(containerType => containerType['is-activated']);
+ },
containerStyles() {
return [
{ key: 0, title: this.$gettext('Volle Breite'), colspan: 'full' },
diff --git a/resources/vue/components/courseware/widgets/CoursewareAdminViewWidget.vue b/resources/vue/components/courseware/widgets/CoursewareAdminViewWidget.vue
index aad266d..d0c29c0 100644
--- a/resources/vue/components/courseware/widgets/CoursewareAdminViewWidget.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareAdminViewWidget.vue
@@ -5,6 +5,16 @@
{{ $gettext('Vorlagen') }}
</a>
</li>
+ <li>
+ <a :href="blockTypeUrl">
+ {{ $gettext("Block-Typen") }}
+ </a>
+ </li>
+ <li>
+ <a :href="containerTypeUrl">
+ {{ $gettext("Container-Typen") }}
+ </a>
+ </li>
</ul>
</template>
@@ -17,6 +27,12 @@ export default {
...mapGetters({
adminViewMode: 'adminViewMode'
}),
+ blockTypeUrl() {
+ return window.STUDIP.URLHelper.getURL('dispatch.php/admin/courseware/block_types');
+ },
+ containerTypeUrl() {
+ return window.STUDIP.URLHelper.getURL('dispatch.php/admin/courseware/container_types');
+ },
templatesView() {
return this.adminViewMode === 'templates';
},