diff options
| author | Marcus Eibrink-Lunzenauer <lunzenauer@elan-ev.de> | 2025-07-10 09:14:44 +0000 |
|---|---|---|
| committer | Ron Lucke <lucke@elan-ev.de> | 2025-07-10 11:14:44 +0200 |
| commit | 1f5b6ce81987ed5a03279265ca881ecb4bac0832 (patch) | |
| tree | aff162afb291f206a490c0eb2d4ec98d29e3e140 | |
| parent | 7f557a0d69924597be5ca6b3aa8495ed26460429 (diff) | |
STEP 3263: Block- und Abschnittstypen standortspezifisch deaktivieren. (zweite Version)
Closes #3263
Merge request studip/studip!4206
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'; }, |
