From 9a720a69d9052a865163054c133b916782a8c529 Mon Sep 17 00:00:00 2001 From: Dennis Benz <dennis.benz@uni-osnabrueck.de> Date: Thu, 29 Jun 2023 08:37:50 +0000 Subject: [PATCH] New Courseware Block: LTI, refs #2326 Merge request studip/studip!1545 --- app/controllers/courseware/lti.php | 99 +++++++ lib/classes/JsonApi/RouteMap.php | 7 + lib/classes/JsonApi/Routes/Lti/Authority.php | 25 ++ .../JsonApi/Routes/Lti/LtiToolsIndex.php | 27 ++ .../JsonApi/Routes/Lti/LtiToolsShow.php | 32 ++ lib/classes/JsonApi/SchemaMap.php | 1 + lib/classes/JsonApi/Schemas/LtiTool.php | 30 ++ .../Courseware/BlockTypes/BlockType.php | 1 + lib/models/Courseware/BlockTypes/Lti.json | 37 +++ lib/models/Courseware/BlockTypes/Lti.php | 86 ++++++ .../assets/stylesheets/scss/courseware.scss | 25 ++ .../courseware/CoursewareLtiBlock.vue | 279 ++++++++++++++++++ .../courseware/container-components.js | 2 + resources/vue/courseware-index-app.js | 1 + 14 files changed, 652 insertions(+) create mode 100644 app/controllers/courseware/lti.php create mode 100644 lib/classes/JsonApi/Routes/Lti/Authority.php create mode 100644 lib/classes/JsonApi/Routes/Lti/LtiToolsIndex.php create mode 100644 lib/classes/JsonApi/Routes/Lti/LtiToolsShow.php create mode 100644 lib/classes/JsonApi/Schemas/LtiTool.php create mode 100644 lib/models/Courseware/BlockTypes/Lti.json create mode 100644 lib/models/Courseware/BlockTypes/Lti.php create mode 100644 resources/vue/components/courseware/CoursewareLtiBlock.vue diff --git a/app/controllers/courseware/lti.php b/app/controllers/courseware/lti.php new file mode 100644 index 00000000000..7792051d953 --- /dev/null +++ b/app/controllers/courseware/lti.php @@ -0,0 +1,99 @@ +<?php + +class Courseware_LtiController extends AuthenticatedController +{ + + /** + * Display the launch form for a tool as an iframe in a courseware LTI block. + * + * @param int $block_id courseware block id + */ + public function iframe_action($block_id) + { + $cw_block = \Courseware\Block::find($block_id); + if (!$cw_block->container->structural_element->canRead($GLOBALS['user']->id)) { + throw new AccessDeniedException(); + } + + $cw_block = \Courseware\Block::find($block_id); + + $lti_link = $this->getLtiLink($cw_block); + + $this->launch_url = $lti_link->getLaunchURL(); + $this->launch_data = $lti_link->getBasicLaunchData(); + $this->signature = $lti_link->getLaunchSignature($this->launch_data); + + $this->set_layout(null); + $this->render_template('course/lti/iframe'); + } + + /** + * Return an LtiLink object for the passed courseware LTI block. + * + * @param \Courseware\Block $cw_block courseware LTI block + * + * @return LtiLink LTI link representation + */ + public function getLtiLink($cw_block) + { + $block_payload = json_decode($cw_block->payload, true); + + // Collect LTI Data from courseware block payload + $id = $cw_block->id; + $context_id = Context::getId(); + $range_id = $cw_block->getStructuralElement()->range_id; + $title = trim($block_payload['title']); + $tool_id = $block_payload['tool_id']; + $launch_url = trim($block_payload['launch_url']); + $custom_parameters = trim($block_payload['custom_parameters']); + $document_target = 'iframe'; + + if ($tool_id) { + $tool = LtiTool::find($tool_id); + + // Prefer custom url + if (!$tool->allow_custom_url && !$tool->deep_linking || !$launch_url) { + $launch_url = $tool->launch_url; + } + + $consumer_key = $tool->consumer_key; + $consumer_secret = $tool->consumer_secret; + $send_lis_person = $tool->send_lis_person; + $oauth_signature_method = $tool->oauth_signature_method; + $custom_parameters = $tool->custom_parameters . "\n" . $custom_parameters; + } else { + $consumer_key = trim($block_payload['consumer_key']); + $consumer_secret = trim($block_payload['consumer_secret']); + $send_lis_person = $block_payload['send_lis_person']; + $oauth_signature_method = $block_payload['oauth_signature_method'] ?? 'sha1'; + } + + if ($context_id) { + // Role in course + $roles = $GLOBALS['perm']->have_studip_perm('tutor', $context_id) ? 'Instructor' : 'Learner'; + } else { + // Role in workspace + $roles = $range_id === $GLOBALS['user']->id ? 'Instructor' : 'Learner'; + } + + // Create LTI Link for setting up launch request + $lti_link = new LtiLink($launch_url, $consumer_key, $consumer_secret, $oauth_signature_method); + $lti_link->setResource($id, $title); + $lti_link->setUser($GLOBALS['user']->id, $roles, $send_lis_person); + $lti_link->setCourse($range_id); + $lti_link->addLaunchParameters([ + 'launch_presentation_locale' => str_replace('_', '-', $_SESSION['_language']), + 'launch_presentation_document_target' => $document_target, + ]); + + $custom_parameters = explode("\n", $custom_parameters); + foreach ($custom_parameters as $param) { + if (strpos($param, '=') !== false) { + list($key, $value) = explode('=', $param, 2); + $lti_link->addCustomParameter(trim($key), trim($value)); + } + } + + return $lti_link; + } +} diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php index 52d4402e54b..f01a0965170 100644 --- a/lib/classes/JsonApi/RouteMap.php +++ b/lib/classes/JsonApi/RouteMap.php @@ -129,6 +129,7 @@ class RouteMap $this->addAuthenticatedFilesRoutes($group); $this->addAuthenticatedForumRoutes($group); $this->addAuthenticatedInstitutesRoutes($group); + $this->addAuthenticatedLtiRoutes($group); $this->addAuthenticatedMessagesRoutes($group); $this->addAuthenticatedNewsRoutes($group); $this->addAuthenticatedStudyAreasRoutes($group); @@ -251,6 +252,12 @@ class RouteMap $group->get('/institutes/{id}/status-groups', Routes\Institutes\StatusGroupsOfInstitutes::class); } + private function addAuthenticatedLtiRoutes(RouteCollectorProxy $group): void + { + $group->get('/lti-tools/{id}', Routes\Lti\LtiToolsShow::class); + $group->get('/lti-tools', Routes\Lti\LtiToolsIndex::class); + } + private function addAuthenticatedNewsRoutes(RouteCollectorProxy $group): void { $group->post('/courses/{id}/news', Routes\News\CourseNewsCreate::class); diff --git a/lib/classes/JsonApi/Routes/Lti/Authority.php b/lib/classes/JsonApi/Routes/Lti/Authority.php new file mode 100644 index 00000000000..99d438b0ec3 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Lti/Authority.php @@ -0,0 +1,25 @@ +<?php + +namespace JsonApi\Routes\Lti; + +use LtiTool; +use User; + +class Authority +{ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public static function canShowLtiTool(User $user, LtiTool $tool): bool + { + return true; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public static function canIndexLtiTools(User $user): bool + { + return true; + } +} diff --git a/lib/classes/JsonApi/Routes/Lti/LtiToolsIndex.php b/lib/classes/JsonApi/Routes/Lti/LtiToolsIndex.php new file mode 100644 index 00000000000..d701f16fade --- /dev/null +++ b/lib/classes/JsonApi/Routes/Lti/LtiToolsIndex.php @@ -0,0 +1,27 @@ +<?php + +namespace JsonApi\Routes\Lti; + +use JsonApi\Errors\AuthorizationFailedException; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use JsonApi\JsonApiController; + +class LtiToolsIndex extends JsonApiController +{ + protected $allowedPagingParameters = ['offset', 'limit']; + + public function __invoke(Request $request, Response $response, $args) + { + if (!Authority::canIndexLtiTools($this->getUser($request))) { + throw new AuthorizationFailedException(); + } + + list($offset, $limit) = $this->getOffsetAndLimit(); + + $total = \LtiTool::countBySql('1'); + $tools = \LtiTool::findBySQL("1 ORDER BY `name` LIMIT ?, ?", [$offset, $limit]); + + return $this->getPaginatedContentResponse($tools, $total); + } +} diff --git a/lib/classes/JsonApi/Routes/Lti/LtiToolsShow.php b/lib/classes/JsonApi/Routes/Lti/LtiToolsShow.php new file mode 100644 index 00000000000..40924f91b10 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Lti/LtiToolsShow.php @@ -0,0 +1,32 @@ +<?php + +namespace JsonApi\Routes\Lti; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; + +/** + * Displays a certain lti tool. + */ +class LtiToolsShow extends JsonApiController +{ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + if (!$resource = \LtiTool::find($args['id'])) { + throw new RecordNotFoundException(); + } + + if (!Authority::canShowLtiTool($this->getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + + + return $this->getContentResponse($resource); + } +} diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php index f4ac20796ed..e5a2f678f5f 100644 --- a/lib/classes/JsonApi/SchemaMap.php +++ b/lib/classes/JsonApi/SchemaMap.php @@ -33,6 +33,7 @@ class SchemaMap \JsonApi\Models\ForumEntry::class => Schemas\ForumEntry::class, \Institute::class => Schemas\Institute::class, \InstituteMember::class => Schemas\InstituteMember::class, + \LtiTool::class => Schemas\LtiTool::class, \Message::class => Schemas\Message::class, \SemClass::class => Schemas\SemClass::class, \Semester::class => Schemas\Semester::class, diff --git a/lib/classes/JsonApi/Schemas/LtiTool.php b/lib/classes/JsonApi/Schemas/LtiTool.php new file mode 100644 index 00000000000..58aa24cd164 --- /dev/null +++ b/lib/classes/JsonApi/Schemas/LtiTool.php @@ -0,0 +1,30 @@ +<?php + +namespace JsonApi\Schemas; + +use Neomerx\JsonApi\Contracts\Schema\ContextInterface; + +class LtiTool extends SchemaProvider +{ + const TYPE = 'lti-tools'; + + public function getId($resource): ?string + { + return $resource->id; + } + + public function getAttributes($resource, ContextInterface $context): iterable + { + return [ + 'name' => $resource->name, + 'launch-url' => $resource->launch_url, + 'allow-custom-url' => (bool) $resource->allow_custom_url, + 'deep-linking' => (bool) $resource->deep_linking, + ]; + } + + public function getRelationships($resource, ContextInterface $context): iterable + { + return []; + } +} diff --git a/lib/models/Courseware/BlockTypes/BlockType.php b/lib/models/Courseware/BlockTypes/BlockType.php index 8c03c880832..e5526f5866a 100644 --- a/lib/models/Courseware/BlockTypes/BlockType.php +++ b/lib/models/Courseware/BlockTypes/BlockType.php @@ -118,6 +118,7 @@ abstract class BlockType ImageMap::class, KeyPoint::class, Link::class, + Lti::class, TableOfContents::class, Text::class, Timeline::class, diff --git a/lib/models/Courseware/BlockTypes/Lti.json b/lib/models/Courseware/BlockTypes/Lti.json new file mode 100644 index 00000000000..596eb002e84 --- /dev/null +++ b/lib/models/Courseware/BlockTypes/Lti.json @@ -0,0 +1,37 @@ +{ + "title": "Payload schema of Courseware\\BlockType\\LTI", + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "height": { + "type": "string" + }, + "tool_id": { + "type": "string" + }, + "launch_url": { + "type": "string" + }, + "consumer_key": { + "type": "string" + }, + "consumer_secret": { + "type": "string" + }, + "oauth_signature_method": { + "type": "string" + }, + "send_lis_person": { + "type": "boolean" + }, + "custom_parameters": { + "type": "string" + } + }, + "required": [ + "tool_id" + ], + "additionalProperties": true +} diff --git a/lib/models/Courseware/BlockTypes/Lti.php b/lib/models/Courseware/BlockTypes/Lti.php new file mode 100644 index 00000000000..8b527c193df --- /dev/null +++ b/lib/models/Courseware/BlockTypes/Lti.php @@ -0,0 +1,86 @@ +<?php + +namespace Courseware\BlockTypes; + +use Opis\JsonSchema\Schema; + +/** + * This class represents the content of a Courseware LTI block. + * + * @author Dennis Benz <debenz@uos.de> + * @license GPL2 or any later version + * + * @since Stud.IP 5.3 + */ +class Lti extends BlockType +{ + public static function getType(): string + { + return 'lti'; + } + + public static function getTitle(): string + { + return _('LTI'); + } + + public static function getDescription(): string + { + return _('Einbinden eines externen Tools.'); + } + + public function initialPayload(): array + { + return [ + 'title' => '', + 'height' => '640', + 'tool_id' => '', + 'launch_url' => '', + 'consumer_key' => '', + 'consumer_secret' => '', + 'oauth_signature_method' => 'sha1', + 'send_lis_person' => false, + 'custom_parameters' => '', + ]; + } + + public static function getJsonSchema(): Schema + { + $schemaFile = __DIR__.'/Lti.json'; + + return Schema::fromJsonString(file_get_contents($schemaFile)); + } + + public function getPayload() + { + $payload = $this->decodePayloadString($this->block['payload']); + $user = \User::findCurrent(); + + // Remove sensitive lti parameters if user has no edit permission + if (!$this->block->getStructuralElement()->canEdit($user)) { + unset($payload['launch_url']); + unset($payload['consumer_key']); + unset($payload['consumer_secret']); + unset($payload['oauth_signature_method']); + unset($payload['send_lis_person']); + unset($payload['custom_parameters']); + } + + return $payload; + } + + public static function getCategories(): array + { + return ['external']; + } + + public static function getContentTypes(): array + { + return ['rich']; + } + + public static function getFileTypes(): array + { + return []; + } +} diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss index 08e8811d604..525f7b4a000 100644 --- a/resources/assets/stylesheets/scss/courseware.scss +++ b/resources/assets/stylesheets/scss/courseware.scss @@ -85,6 +85,7 @@ $blockadder-items: ( folder: folder-full, headline: block-eyecatcher, iframe: door-enter, + lti: plugin, key-point: exclaim-circle, link: link-extern, table-of-contents: table-of-contents, @@ -3679,6 +3680,30 @@ i f r a m e b l o c k i f r a m e b l o c k e n d * * * * * * * * * * * * * */ +/* * * * * * * * + +l t i b l o c k +* * * * * * * * */ +.cw-block-lti { + .cw-block-content { + .cw-block-lti-content { + border: solid thin $content-color-40; + box-sizing: border-box; + } + .cw-block-lti-icon-tool { + @include background-icon(plugin, info, 24); + background-repeat: no-repeat; + + display: block; + padding: 16px 16px 16px 40px; + background-position: 10px center; + overflow: hidden; + text-overflow: ellipsis; + } + } +} +/* * * * * * * * * * * * +l t i b l o c k e n d +* * * * * * * * * * * */ /* * * * * * * * * * * * f o l d e r b l o c k diff --git a/resources/vue/components/courseware/CoursewareLtiBlock.vue b/resources/vue/components/courseware/CoursewareLtiBlock.vue new file mode 100644 index 00000000000..e9679625050 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareLtiBlock.vue @@ -0,0 +1,279 @@ +<template> + <div class="cw-block cw-block-lti"> + <courseware-default-block + :block="block" + :canEdit="canEdit" + :isTeacher="isTeacher" + :preview="false" + @showEdit="initCurrentData" + @storeEdit="storeBlock" + @closeEdit="initCurrentData" + > + <template #content> + <div v-if="currentTitle !== ''" class="cw-block-title">{{ currentTitle }}</div> + <iframe + v-if="toolId !== ''" + class="cw-block-lti-content" + :src="iframeUrl" + :height="currentHeight" + width="100%" + allowfullscreen + sandbox="allow-downloads allow-forms allow-popups allow-pointer-lock allow-same-origin allow-scripts" + /> + <div v-else class="cw-block-lti-content"> + <span class="cw-block-lti-icon-tool"> + {{ $gettext('Kein LTI-Tool konfiguriert') }} + </span> + </div> + </template> + <template v-if="canEdit" #edit> + <courseware-tabs> + <courseware-tab + :index="0" + :name="$gettext('Grunddaten')" + :selected="true" + > + <form class="default" @submit.prevent=""> + <label> + {{ $gettext('Titel') }} + <input type="text" v-model="currentTitle" /> + </label> + <label> + {{ $gettext('Auswahl des externen Tools') }} + <select v-model="currentToolId"> + <option v-for="tool in tools" :key="tool.id" :value="tool.id"> + {{ tool.name }} + </option> + <option value="0">{{ $gettext('Zugangsdaten selbst eingeben...') }}</option> + </select> + </label> + <label v-show="allowCustomUrl"> + {{ $gettext('URL der Anwendung (optional)') }} + <studip-tooltip-icon :text="$gettext('Sie können direkt auf eine URL in der Anwendung verlinken.')"/> + <input type="text" v-model="currentLaunchUrl" :placeholder="currentTool?.launch_url" /> + </label> + + <div v-show="customToolSelected"> + <label class="studiprequired"> + {{ $gettext('URL der Anwendung') }} + <span class="asterisk" :title="$gettext('Dies ist ein Pflichtfeld')" aria-hidden="true">*</span> + <studip-tooltip-icon :text="$gettext('Die Betreiber dieses Tools müssen Ihnen eine URL und Zugangsdaten (Consumer-Key und Consumer-Secret) mitteilen.')"/> + <input type="text" v-model="currentLaunchUrl" required> + </label> + <label class="studiprequired"> + {{ $gettext('Consumer-Key des LTI-Tools') }} + <span class="asterisk" :title="$gettext('Dies ist ein Pflichtfeld')" aria-hidden="true">*</span> + <input type="text" v-model="currentConsumerKey" required> + </label> + <label class="studiprequired"> + {{ $gettext('Consumer-Secret des LTI-Tools') }} + <span class="asterisk" :title="$gettext('Dies ist ein Pflichtfeld')" aria-hidden="true">*</span> + <input type="text" v-model="currentConsumerSecret" required> + </label> + <label> + {{ $gettext('OAuth Signatur Methode des LTI-Tools') }} + <select v-model="currentOauthSignatureMethod"> + <option value="sha1">HMAC-SHA1</option> + <option value="sha256">HMAC-SHA256</option> + </select> + </label> + <label> + <input type="checkbox" v-model="currentSendLisPerson" /> + {{ $gettext('Nutzerdaten an LTI-Tool senden') }} + <studip-tooltip-icon :text="$gettext('Nutzerdaten dürfen nur an das externe Tool gesendet werden, wenn es keine Datenschutzbedenken gibt. Mit Setzen des Hakens bestätigen Sie, dass die Übermittlung der Daten zulässig ist.')"/> + </label> + </div> + </form> + </courseware-tab> + <courseware-tab + :index="1" + :name="$gettext('Zusätzliche Einstellungen')" + > + <form class="default" @submit.prevent=""> + <label> + {{ $gettext('Höhe') }} + <input type="number" v-model="currentHeight" min="0" /> + </label> + <label> + {{ $gettext('Zusätzliche LTI-Parameter') }} + <studip-tooltip-icon :text="$gettext('Ein Wert pro Zeile, Beispiel: Review:Chapter=1.2.56')"/> + <textarea v-model="currentCustomParameters" /> + </label> + </form> + </courseware-tab> + </courseware-tabs> + </template> + <template #info> + <p>{{ $gettext('Informationen zum LTI-Block') }}</p> + </template> + </courseware-default-block> + </div> +</template> + +<script> +import CoursewareDefaultBlock from "./CoursewareDefaultBlock.vue"; +import {blockMixin} from "./block-mixin"; +import {mapActions, mapGetters} from "vuex"; +import CoursewareTabs from "./CoursewareTabs.vue"; +import CoursewareTab from "./CoursewareTab.vue"; + +export default { + name: 'courseware-lti-block', + mixins: [blockMixin], + components: { + CoursewareTab, + CoursewareTabs, + CoursewareDefaultBlock + }, + props: { + block: Object, + canEdit: Boolean, + isTeacher: Boolean, + }, + data() { + return { + currentTitle: '', + currentHeight: '', + currentToolId: '', + currentLaunchUrl: '', + currentConsumerKey: '', + currentConsumerSecret: '', + currentOauthSignatureMethod: '', + currentSendLisPerson: false, + currentCustomParameters: '', + } + }, + computed: { + ...mapGetters({ + urlHelper: 'urlHelper', + ltiTools: 'lti-tools/all', + }), + title() { + return this.block?.attributes?.payload?.title; + }, + height() { + return this.block?.attributes?.payload?.height; + }, + tools() { + return this.ltiTools.map(tool => ({ + id: tool.id, + name: tool.attributes.name, + launch_url: tool.attributes['launch-url'], + allow_custom_url: tool.attributes['allow-custom-url'], + })); + }, + toolId() { + return this.block?.attributes?.payload?.tool_id; + }, + currentTool() { + return this.tools.find(tool => tool.id === this.currentToolId); + }, + allowCustomUrl() { + return this.currentTool?.allow_custom_url; + }, + customToolSelected() { + return this.currentToolId === '0'; + }, + launchUrl() { + return this.block?.attributes?.payload?.launch_url; + }, + consumerKey() { + return this.block?.attributes?.payload?.consumer_key; + }, + consumerSecret() { + return this.block?.attributes?.payload?.consumer_secret; + }, + oauthSignatureMethod() { + return this.block?.attributes?.payload?.oauth_signature_method ?? 'sha1'; + }, + sendLisPerson() { + return this.block?.attributes?.payload?.send_lis_person; + }, + customParameters() { + return this.block?.attributes?.payload?.custom_parameters; + }, + iframeUrl() { + return this.urlHelper.getURL('dispatch.php/courseware/lti/iframe/' + this.block.id); + }, + }, + async mounted() { + await this.loadLtiTools(); + this.initCurrentData(); + }, + methods: { + ...mapActions({ + updateBlock: 'updateBlockInContainer', + loadLtiTools: 'lti-tools/loadAll', + companionWarning: 'companionWarning', + }), + initCurrentData() { + this.currentTitle = this.title; + this.currentHeight = this.height; + this.currentToolId = this.toolId !== '' ? this.toolId : this.currentToolId; // keep preselected tool + this.currentLaunchUrl = this.launchUrl; + this.currentConsumerKey = this.consumerKey; + this.currentConsumerSecret = this.consumerSecret; + this.currentOauthSignatureMethod = this.oauthSignatureMethod; + this.currentSendLisPerson = Boolean(this.sendLisPerson); // prevent undefined value + this.currentCustomParameters = this.customParameters; + }, + storeBlock() { + // require url, key and secret if custom tool is selected + if (this.currentToolId === '0') { + if (!this.currentLaunchUrl) { + this.companionWarning({ + info: this.$gettext('Bitte geben Sie eine URL der Anwendung an.') + }); + return false; + } + if (!this.currentConsumerKey) { + this.companionWarning({ + info: this.$gettext('Bitte geben Sie den Consumer-Key des LTI-Tools an.') + }); + return false; + } + if (!this.currentConsumerSecret) { + this.companionWarning({ + info: this.$gettext('Bitte geben Sie den Consumer-Secret des LTI-Tools an.') + }); + return false; + } + } + + let attributes = {}; + attributes.payload = {}; + attributes.payload.title = this.currentTitle; + attributes.payload.height = this.currentHeight; + attributes.payload.tool_id = this.currentToolId; + attributes.payload.launch_url = this.currentLaunchUrl; + if (this.currentToolId === '0') { + attributes.payload.consumer_key = this.currentConsumerKey; + attributes.payload.consumer_secret = this.currentConsumerSecret; + attributes.payload.oauth_signature_method = this.currentOauthSignatureMethod; + attributes.payload.send_lis_person = this.currentSendLisPerson; + } + attributes.payload.custom_parameters = this.currentCustomParameters; + + this.updateBlock({ + attributes: attributes, + blockId: this.block.id, + containerId: this.block.relationships.container.data.id, + }); + }, + }, + watch: { + tools(value) { + // Preselect tool + if (this.currentToolId === '') { + if (value.length > 0) { + // Preselect first tool + this.currentToolId = value[0].id; + } else { + // Preselect custom tool + this.currentToolId = '0'; + } + } + } + }, +}; +</script> diff --git a/resources/vue/components/courseware/container-components.js b/resources/vue/components/courseware/container-components.js index 2926a186e25..63e665887bd 100644 --- a/resources/vue/components/courseware/container-components.js +++ b/resources/vue/components/courseware/container-components.js @@ -21,6 +21,7 @@ import CoursewareIframeBlock from './CoursewareIframeBlock.vue'; import CoursewareImageMapBlock from './CoursewareImageMapBlock.vue'; import CoursewareKeyPointBlock from './CoursewareKeyPointBlock.vue'; import CoursewareLinkBlock from './CoursewareLinkBlock.vue'; +import CoursewareLtiBlock from "./CoursewareLtiBlock.vue"; import CoursewareTableOfContentsBlock from './CoursewareTableOfContentsBlock.vue'; import CoursewareTextBlock from './CoursewareTextBlock.vue'; import CoursewareTimelineBlock from './CoursewareTimelineBlock.vue'; @@ -56,6 +57,7 @@ const ContainerComponents = { CoursewareImageMapBlock, CoursewareKeyPointBlock, CoursewareLinkBlock, + CoursewareLtiBlock, CoursewareTableOfContentsBlock, CoursewareTextBlock, CoursewareTimelineBlock, diff --git a/resources/vue/courseware-index-app.js b/resources/vue/courseware-index-app.js index 4741cd46974..073948565fe 100644 --- a/resources/vue/courseware-index-app.js +++ b/resources/vue/courseware-index-app.js @@ -109,6 +109,7 @@ const mountApp = async (STUDIP, createApp, element) => { 'files', 'file-refs', 'folders', + 'lti-tools', 'status-groups', 'users', 'institutes', -- GitLab