@windward/integrations 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/components/ExternalIntegration/Driver/Lti1p1/ManageConsumer.vue +13 -0
  2. package/components/ExternalIntegration/Driver/Lti1p1/ManageConsumers.vue +13 -0
  3. package/components/ExternalIntegration/Driver/Lti1p1/ManageProvider.vue +374 -0
  4. package/components/ExternalIntegration/Driver/Lti1p1/ManageProviders.vue +220 -0
  5. package/components/ExternalIntegration/Driver/ManageLti1p1.vue +45 -0
  6. package/components/Integration/Driver/ManageBase.vue +1 -1
  7. package/components/SecretField.vue +50 -0
  8. package/i18n/en-US/components/external_integration/driver/lti1p1.ts +26 -0
  9. package/i18n/en-US/components/external_integration/index.ts +5 -0
  10. package/i18n/en-US/components/index.ts +2 -0
  11. package/i18n/en-US/pages/course/external_integration/index.ts +6 -0
  12. package/i18n/en-US/pages/course/index.ts +5 -0
  13. package/i18n/en-US/pages/index.ts +4 -0
  14. package/i18n/en-US/pages/login/index.ts +5 -0
  15. package/i18n/en-US/pages/login/lti.ts +21 -0
  16. package/i18n/en-US/shared/permission.ts +4 -0
  17. package/models/ExternalIntegration/Lti1p1Consumer.ts +13 -0
  18. package/models/ExternalIntegration/Lti1p1Provider.ts +13 -0
  19. package/package.json +1 -1
  20. package/pages/admin/vendors.vue +1 -1
  21. package/pages/course/externalIntegration/index.vue +72 -0
  22. package/pages/login/lti/error.vue +98 -0
  23. package/plugin.js +28 -0
  24. package/test/Components/ExternalIntegration/Lti1p1/ManageConsumer.spec.js +19 -0
  25. package/test/Components/ExternalIntegration/Lti1p1/ManageConsumers.spec.js +19 -0
  26. package/test/Components/ExternalIntegration/Lti1p1/ManageProvider.spec.js +19 -0
  27. package/test/Components/ExternalIntegration/Lti1p1/ManageProviders.spec.js +19 -0
  28. package/test/Components/ExternalIntegration/ManageLti1p1.spec.js +19 -0
  29. package/test/Pages/Course/ExternalIntegration/index.spec.js +19 -0
  30. package/test/Pages/Login/Lti/error.spec.js +19 -0
  31. package/test/__mocks__/modelMock.js +4 -1
  32. package/test/mocks.js +1 -0
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <div>LTI 1.1 Consumers Not Implemented yet</div>
3
+ </template>
4
+
5
+ <script>
6
+ export default {
7
+ name: 'ManageLti1p1ConsumerDriver',
8
+ data() {
9
+ return {}
10
+ },
11
+ methods: {},
12
+ }
13
+ </script>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <div>LTI 1.1 Consumers Not Implemented yet</div>
3
+ </template>
4
+
5
+ <script>
6
+ export default {
7
+ name: 'ManageLti1p1ConsumersDriver',
8
+ data() {
9
+ return {}
10
+ },
11
+ methods: {},
12
+ }
13
+ </script>
@@ -0,0 +1,374 @@
1
+ <template>
2
+ <div>
3
+ <div v-if="!render" class="integration-loading">
4
+ <v-progress-circular size="128" indeterminate />
5
+ </div>
6
+ <div v-if="render">
7
+ <v-form v-model="formValid" @submit.prevent>
8
+ <v-row justify="center" align="center" class="mt-5">
9
+ <v-col cols="12">
10
+ <v-text-field
11
+ v-model="provider.target"
12
+ :label="
13
+ $t(
14
+ 'windward.integrations.components.external_integration.driver.lti1p1.target_url'
15
+ )
16
+ "
17
+ :hint="
18
+ $t(
19
+ 'windward.integrations.components.external_integration.driver.lti1p1.target_url'
20
+ )
21
+ "
22
+ ></v-text-field>
23
+
24
+ <v-switch
25
+ v-model="provider.enabled"
26
+ :label="
27
+ $t(
28
+ 'windward.integrations.components.external_integration.driver.lti1p1.enabled'
29
+ )
30
+ "
31
+ />
32
+
33
+ <v-text-field
34
+ v-model="provider.metadata.key"
35
+ :placeholder="
36
+ $t(
37
+ 'windward.integrations.components.external_integration.driver.lti1p1.auto_key'
38
+ )
39
+ "
40
+ :label="
41
+ $t(
42
+ 'windward.integrations.components.external_integration.driver.lti1p1.key'
43
+ )
44
+ "
45
+ :hint="
46
+ $t(
47
+ 'windward.integrations.components.external_integration.driver.lti1p1.key'
48
+ )
49
+ "
50
+ ></v-text-field>
51
+
52
+ <v-text-field
53
+ v-model="provider.metadata.secret"
54
+ :placeholder="
55
+ $t(
56
+ 'windward.integrations.components.external_integration.driver.lti1p1.auto_secret'
57
+ )
58
+ "
59
+ :label="
60
+ $t(
61
+ 'windward.integrations.components.external_integration.driver.lti1p1.secret'
62
+ )
63
+ "
64
+ :hint="
65
+ $t(
66
+ 'windward.integrations.components.external_integration.driver.lti1p1.secret'
67
+ )
68
+ "
69
+ >
70
+ </v-text-field>
71
+ </v-col>
72
+ </v-row>
73
+ <v-row>
74
+ <v-col cols="12">
75
+ <v-expansion-panels v-model="rolePanel">
76
+ <v-expansion-panel>
77
+ <v-expansion-panel-header>
78
+ {{
79
+ $t(
80
+ 'windward.integrations.components.external_integration.driver.lti1p1.role_map_panel'
81
+ )
82
+ }}
83
+ </v-expansion-panel-header>
84
+ <v-expansion-panel-content>
85
+ <p>
86
+ <strong>
87
+ {{
88
+ $t(
89
+ 'windward.integrations.components.external_integration.driver.lti1p1.role_map_instructions'
90
+ )
91
+ }}
92
+ </strong>
93
+ </p>
94
+ <p>
95
+ {{
96
+ $t(
97
+ 'windward.integrations.components.external_integration.driver.lti1p1.role_map_warning'
98
+ )
99
+ }}
100
+ </p>
101
+ <v-simple-table>
102
+ <template #default>
103
+ <thead>
104
+ <tr>
105
+ <th>
106
+ {{
107
+ $t(
108
+ 'windward.integrations.components.external_integration.driver.lti1p1.role_in_host'
109
+ )
110
+ }}
111
+ </th>
112
+ <th>
113
+ {{
114
+ $t(
115
+ 'windward.integrations.components.external_integration.driver.lti1p1.role_in_local'
116
+ )
117
+ }}
118
+ </th>
119
+ </tr>
120
+ </thead>
121
+ <tbody>
122
+ <tr
123
+ v-for="(
124
+ id, externalName
125
+ ) in provider.role_metadata"
126
+ :key="externalName"
127
+ >
128
+ <td>
129
+ {{
130
+ uppercase(
131
+ externalName
132
+ )
133
+ }}
134
+ </td>
135
+ <td>
136
+ <v-select
137
+ v-model="
138
+ provider
139
+ .role_metadata[
140
+ externalName
141
+ ]
142
+ "
143
+ :items="roles"
144
+ item-value="id"
145
+ item-text="name"
146
+ :label="
147
+ $t(
148
+ 'windward.integrations.components.external_integration.driver.lti1p1.map_to_role'
149
+ )
150
+ "
151
+ ></v-select>
152
+ </td>
153
+ <td>
154
+ <v-btn
155
+ icon
156
+ @click="
157
+ deleteMapItem(
158
+ externalName
159
+ )
160
+ "
161
+ >
162
+ <v-icon>
163
+ mdi-delete
164
+ </v-icon>
165
+ <span
166
+ class="sr-only"
167
+ >
168
+ {{
169
+ $t(
170
+ 'windward.integrations.components.external_integration.driver.lti1p1.delete_role_map_item'
171
+ )
172
+ }}
173
+ </span>
174
+ </v-btn>
175
+ </td>
176
+ </tr>
177
+ <tr>
178
+ <td>
179
+ <v-text-field
180
+ v-model="
181
+ newMap.name
182
+ "
183
+ :label="
184
+ $t(
185
+ 'windward.integrations.components.external_integration.driver.lti1p1.remote_role'
186
+ )
187
+ "
188
+ ></v-text-field>
189
+ </td>
190
+ <td>
191
+ <v-select
192
+ v-model="newMap.id"
193
+ :items="roles"
194
+ :label="
195
+ $t(
196
+ 'windward.integrations.components.external_integration.driver.lti1p1.map_to_role'
197
+ )
198
+ "
199
+ item-value="id"
200
+ item-text="name"
201
+ ></v-select>
202
+ </td>
203
+ <td>
204
+ <v-btn
205
+ icon
206
+ :disabled="
207
+ !newMap.id ||
208
+ !newMap.name
209
+ "
210
+ @click="addMapItem"
211
+ >
212
+ <v-icon>
213
+ mdi-plus
214
+ </v-icon>
215
+ <span
216
+ class="sr-only"
217
+ >
218
+ {{
219
+ $t(
220
+ 'windward.integrations.components.external_integration.driver.lti1p1.add_role_map_item'
221
+ )
222
+ }}
223
+ </span>
224
+ </v-btn>
225
+ </td>
226
+ </tr>
227
+ </tbody>
228
+ </template>
229
+ </v-simple-table>
230
+ </v-expansion-panel-content>
231
+ </v-expansion-panel>
232
+ </v-expansion-panels>
233
+ </v-col>
234
+ </v-row>
235
+ </v-form>
236
+ </div>
237
+ </div>
238
+ </template>
239
+
240
+ <script>
241
+ import _ from 'lodash'
242
+ import { mapGetters } from 'vuex'
243
+ import Lti1p1Provider from '../../../../models/ExternalIntegration/Lti1p1Provider'
244
+ import FormVue from '~/components/Form'
245
+ import Role from '~/models/Role'
246
+ import Organization from '~/models/Organization'
247
+ import Course from '~/models/Course'
248
+
249
+ export default {
250
+ name: 'ManageLti1p1ProviderDriver',
251
+ components: {},
252
+ extends: FormVue,
253
+ props: {
254
+ value: {
255
+ type: [Lti1p1Provider, null],
256
+ required: false,
257
+ default: null,
258
+ },
259
+ },
260
+ emits: ['update:provider'],
261
+ meta: {
262
+ privilege: {
263
+ '': {
264
+ writable: true,
265
+ },
266
+ },
267
+ },
268
+ data() {
269
+ return {
270
+ render: false,
271
+ provider: {
272
+ role_metadata: {},
273
+ metadata: {},
274
+ },
275
+ rolePanel: false,
276
+ roles: [],
277
+ newMap: {
278
+ name: null,
279
+ id: null,
280
+ },
281
+ }
282
+ },
283
+ async fetch() {
284
+ // Get available roles but filter out default high level roles since they'll be ignore anyways from the api
285
+ this.roles = await Role.whereIn('name,ne', [
286
+ 'admin',
287
+ 'editor',
288
+ 'instructor',
289
+ 'student',
290
+ ]).all()
291
+ },
292
+ computed: {
293
+ ...mapGetters({
294
+ organization: 'organization/get',
295
+ course: 'course/get',
296
+ }),
297
+ },
298
+ created() {
299
+ if (_.isEmpty(this.value)) {
300
+ this.provider = new Lti1p1Provider({
301
+ role_metadata: {},
302
+ metadata: {},
303
+ })
304
+ } else {
305
+ this.provider = _.cloneDeep(this.value)
306
+ }
307
+ },
308
+ mounted() {
309
+ if (
310
+ !this.$PermissionService.userHasAccessTo(
311
+ 'plugin.windward.integrations.course.externalIntegration',
312
+ 'writable'
313
+ )
314
+ ) {
315
+ // Display an angry error that they can't view this driver
316
+ this.$dialog.error(this.$t('shared.error.description_401'), {
317
+ duration: null,
318
+ action: {
319
+ text: this.$t('shared.forms.close'),
320
+ onClick: (e, toastObject) => {
321
+ toastObject.goAway(0)
322
+ },
323
+ },
324
+ })
325
+ console.error('You do not have access to this provider!')
326
+
327
+ // Return so we don't even attempt loading
328
+ return false
329
+ }
330
+
331
+ this.render = true
332
+ },
333
+ methods: {
334
+ uppercase(value) {
335
+ return _.startCase(value)
336
+ },
337
+ deleteMapItem(key) {
338
+ this.$delete(this.provider.role_metadata, key)
339
+ },
340
+ addMapItem() {
341
+ this.provider.role_metadata[this.newMap.name] = this.newMap.id
342
+ this.newMap.name = null
343
+ this.newMap.id = null
344
+ },
345
+ async save() {
346
+ let provider = new Lti1p1Provider(this.provider).for(
347
+ new Organization({ id: this.organization.id }),
348
+ new Course({ id: this.course.id })
349
+ )
350
+
351
+ try {
352
+ provider = await provider.save()
353
+ this.provider = provider
354
+
355
+ // Clone and delete the metadata since we don't need to share that around
356
+ // Also include the vendor going back for easier mapping
357
+ const providerEvent = _.cloneDeep(provider)
358
+
359
+ this.$dialog.success(this.$t('shared.forms.saved'))
360
+ this.$emit('update:provider', providerEvent)
361
+ } catch (e) {
362
+ this.$dialog.error(
363
+ this.$t('windward.integrations.shared.error.save_failed')
364
+ )
365
+ }
366
+ },
367
+ async onSave() {
368
+ if (this.formValid) {
369
+ await this.save()
370
+ }
371
+ },
372
+ },
373
+ }
374
+ </script>
@@ -0,0 +1,220 @@
1
+ <template>
2
+ <div>
3
+ <Dialog
4
+ color="primary"
5
+ action-save
6
+ action-save-new
7
+ @click:save="onSaved"
8
+ >
9
+ <template #title>{{
10
+ $t(
11
+ 'windward.integrations.components.external_integration.driver.lti1p1.new'
12
+ )
13
+ }}</template>
14
+ <template #trigger>{{ $t('shared.forms.new') }}</template>
15
+ <template #form="{ on, attrs }"
16
+ ><ManageProvider v-bind="attrs" v-on="on"></ManageProvider
17
+ ></template>
18
+ </Dialog>
19
+
20
+ <v-data-table
21
+ :headers="headers"
22
+ :items="providers"
23
+ :items-per-page="10"
24
+ class="elevation-1"
25
+ >
26
+ <template #item.metadata.key="{ item }">
27
+ <SecretField v-model="item.metadata.key"></SecretField>
28
+ </template>
29
+ <template #item.metadata.secret="{ item }">
30
+ <SecretField v-model="item.metadata.secret"></SecretField>
31
+ </template>
32
+ <template #item.url="{ item }">
33
+ <SecretField v-model="item.url" :hidden="false"></SecretField>
34
+ </template>
35
+
36
+ <template #item.enabled="{ item }">
37
+ <v-icon :color="item.enabled ? 'success' : 'error'"
38
+ >{{ item.enabled ? 'mdi-check' : 'mdi-close' }}
39
+ </v-icon>
40
+ <span v-if="!item.enabled" class="sr-only">{{
41
+ $t(
42
+ 'windward.integrations.components.external_integration.driver.lti1p1.enabled'
43
+ )
44
+ }}</span>
45
+ </template>
46
+
47
+ <template #item.created_at="{ item }">
48
+ {{ $d(new Date(item.created_at), 'long') }}
49
+ </template>
50
+ <template #[`item.actions`]="{ index, item }">
51
+ <Dialog color="primary" action-save @click:save="onSaved">
52
+ <template #title>{{
53
+ $t(
54
+ 'windward.integrations.components.external_integration.driver.lti1p1.edit'
55
+ )
56
+ }}</template>
57
+ <template #trigger>
58
+ <v-icon small>mdi-pencil</v-icon>
59
+ <span class="sr-only">{{
60
+ $t(
61
+ 'windward.integrations.components.external_integration.driver.lti1p1.edit'
62
+ )
63
+ }}</span>
64
+ </template>
65
+ <template #form="{ on, attrs }"
66
+ ><ManageProvider
67
+ v-model="providers[index]"
68
+ v-bind="attrs"
69
+ v-on="on"
70
+ ></ManageProvider
71
+ ></template>
72
+ </Dialog>
73
+
74
+ <v-btn icon>
75
+ <v-icon @click="onConfirmDelete(item)"> mdi-delete </v-icon>
76
+ <span class="sr-only">{{ $t('shared.forms.delete') }}</span>
77
+ </v-btn>
78
+ </template>
79
+ </v-data-table>
80
+ </div>
81
+ </template>
82
+
83
+ <script>
84
+ import _ from 'lodash'
85
+ import { mapGetters } from 'vuex'
86
+ import Lti1p1Provider from '../../../../models/ExternalIntegration/Lti1p1Provider'
87
+ import SecretField from '../../../SecretField.vue'
88
+ import ManageProvider from './ManageProvider.vue'
89
+ import Course from '~/models/Course'
90
+ import Organization from '~/models/Organization'
91
+ import Dialog from '~/components/Dialog.vue'
92
+
93
+ export default {
94
+ name: 'ManageLti1p1ProvidersDriver',
95
+ components: { SecretField, Dialog, ManageProvider },
96
+ data() {
97
+ return {
98
+ providers: [],
99
+ headers: [
100
+ {
101
+ text: this.$t(
102
+ 'windward.integrations.components.external_integration.driver.lti1p1.target_url'
103
+ ),
104
+ value: 'target',
105
+ },
106
+ {
107
+ text: this.$t(
108
+ 'windward.integrations.components.external_integration.driver.lti1p1.key'
109
+ ),
110
+ value: 'metadata.key',
111
+ },
112
+ {
113
+ text: this.$t(
114
+ 'windward.integrations.components.external_integration.driver.lti1p1.secret'
115
+ ),
116
+ value: 'metadata.secret',
117
+ },
118
+ {
119
+ text: this.$t(
120
+ 'windward.integrations.components.external_integration.driver.lti1p1.launch_url'
121
+ ),
122
+ value: 'url',
123
+ },
124
+ {
125
+ text: this.$t(
126
+ 'windward.integrations.components.external_integration.driver.lti1p1.enabled'
127
+ ),
128
+ value: 'enabled',
129
+ },
130
+ { text: this.$t('shared.forms.created'), value: 'created_at' },
131
+ {
132
+ text: this.$t('shared.forms.actions'),
133
+ value: 'actions',
134
+ sortable: false,
135
+ },
136
+ ],
137
+ }
138
+ },
139
+
140
+ async fetch() {
141
+ if (
142
+ !this.$PermissionService.userHasAccessTo(
143
+ 'plugin.windward.integrations.course.externalIntegration',
144
+ 'readable'
145
+ ) ||
146
+ _.isEmpty(this.organization.id) ||
147
+ _.isEmpty(this.course.id)
148
+ ) {
149
+ // Display an angry error that they can't view this driver
150
+ this.$dialog.error(this.$t('shared.error.description_401'), {
151
+ duration: null,
152
+ action: {
153
+ text: this.$t('shared.forms.close'),
154
+ onClick: (e, toastObject) => {
155
+ toastObject.goAway(0)
156
+ },
157
+ },
158
+ })
159
+ if (_.isEmpty(this.organization.id) || _.isEmpty(this.course.id)) {
160
+ console.error(
161
+ 'Cannot load external integrations because organization or course is not set!'
162
+ )
163
+ } else {
164
+ console.error(
165
+ 'You do not have access to this external integration!'
166
+ )
167
+ }
168
+
169
+ // Return so we don't even attempt loading
170
+ return false
171
+ }
172
+
173
+ await this.loadProviders()
174
+ },
175
+ computed: {
176
+ ...mapGetters({
177
+ organization: 'organization/get',
178
+ course: 'course/get',
179
+ }),
180
+ },
181
+ methods: {
182
+ onSaved(e) {
183
+ this.loadProviders()
184
+ },
185
+ async loadProviders() {
186
+ this.providers = await new Lti1p1Provider()
187
+ .for(
188
+ new Organization({ id: this.organization.id }),
189
+ new Course({ id: this.course.id })
190
+ )
191
+ .get()
192
+ },
193
+ onConfirmDelete(provider) {
194
+ this.$dialog.show(this.$t('shared.forms.confirm_delete_text'), {
195
+ icon: 'mdi-help',
196
+ duration: null,
197
+ action: [
198
+ {
199
+ text: this.$t('shared.forms.cancel'),
200
+ onClick: (e, toastObject) => {
201
+ toastObject.goAway(0)
202
+ },
203
+ },
204
+ {
205
+ text: this.$t('shared.forms.confirm'),
206
+ // router navigation
207
+ onClick: (e, toastObject) => {
208
+ this.deleteProvider(provider)
209
+ toastObject.goAway(0)
210
+ },
211
+ },
212
+ ],
213
+ })
214
+ },
215
+ deleteProvider(provider) {
216
+ provider.delete()
217
+ },
218
+ },
219
+ }
220
+ </script>
@@ -0,0 +1,45 @@
1
+ <template>
2
+ <div>
3
+ <v-expansion-panels v-model="panel">
4
+ <v-expansion-panel>
5
+ <v-expansion-panel-header>
6
+ {{
7
+ $t(
8
+ 'windward.integrations.components.external_integration.driver.lti1p1.provider_panel'
9
+ )
10
+ }}
11
+ </v-expansion-panel-header>
12
+ <v-expansion-panel-content>
13
+ <ListProviders></ListProviders>
14
+ </v-expansion-panel-content>
15
+ </v-expansion-panel>
16
+ <v-expansion-panel>
17
+ <v-expansion-panel-header>
18
+ {{
19
+ $t(
20
+ 'windward.integrations.components.external_integration.driver.lti1p1.consumer_panel'
21
+ )
22
+ }}
23
+ </v-expansion-panel-header>
24
+ <v-expansion-panel-content>
25
+ <ListConsumers></ListConsumers>
26
+ </v-expansion-panel-content>
27
+ </v-expansion-panel>
28
+ </v-expansion-panels>
29
+ </div>
30
+ </template>
31
+
32
+ <script>
33
+ import ListProviders from './Lti1p1/ManageProviders.vue'
34
+ import ListConsumers from './Lti1p1/ManageConsumers.vue'
35
+
36
+ export default {
37
+ name: 'ManageLti1p1Driver',
38
+ components: { ListProviders, ListConsumers },
39
+ data() {
40
+ return {
41
+ panel: 0,
42
+ }
43
+ },
44
+ }
45
+ </script>
@@ -29,7 +29,7 @@ export default {
29
29
  async fetch() {
30
30
  if (
31
31
  !this.$PermissionService.userHasAccessTo(
32
- 'plugin.integration.organization',
32
+ 'plugin.windward.integrations.organization.integration',
33
33
  'writable'
34
34
  )
35
35
  ) {
@@ -0,0 +1,50 @@
1
+ <template>
2
+ <v-text-field :value="value" readonly :type="fieldType">
3
+ <template #append>
4
+ <v-btn icon @click="toggleClear">
5
+ <v-icon>{{ showClear ? 'mdi-eye-off' : 'mdi-eye' }}</v-icon>
6
+ </v-btn>
7
+ <v-btn v-if="copy" icon @click="copyText(value)">
8
+ <v-icon>mdi-content-copy</v-icon>
9
+ </v-btn>
10
+ </template>
11
+ </v-text-field>
12
+ </template>
13
+
14
+ <script>
15
+ export default {
16
+ name: 'SecretField',
17
+ props: {
18
+ value: { type: String, required: false, default: '' },
19
+ hidden: { type: Boolean, required: false, default: true },
20
+ copy: { type: Boolean, required: false, default: true },
21
+ },
22
+ data() {
23
+ return {
24
+ showClear: false,
25
+ }
26
+ },
27
+
28
+ computed: {
29
+ fieldType() {
30
+ if (this.showClear) {
31
+ return 'text'
32
+ } else {
33
+ return 'password'
34
+ }
35
+ },
36
+ },
37
+ mounted() {
38
+ this.showClear = !this.hidden
39
+ },
40
+ methods: {
41
+ toggleClear() {
42
+ this.showClear = !this.showClear
43
+ },
44
+ copyText(text) {
45
+ navigator.clipboard.writeText(text || '')
46
+ this.$dialog.show(this.$t('shared.forms.copied'))
47
+ },
48
+ },
49
+ }
50
+ </script>
@@ -0,0 +1,26 @@
1
+ export default {
2
+ key: 'Key',
3
+ secret: 'Secret',
4
+ auto_key: 'Autogenerated Key',
5
+ auto_secret: 'Autogenerated Secret',
6
+ target_url: 'Target Url',
7
+ launch_url: 'Launch Url',
8
+ new: 'New LTI Link',
9
+ edit: 'Edit LTI Link',
10
+ enabled: 'Enabled',
11
+ provider: 'Provider',
12
+ consumer: 'Consumer',
13
+ provider_panel: 'Provider (Students incoming to Windward)',
14
+ consumer_panel: 'Consumer (Students outgoing from Windward)',
15
+ role_map_panel: 'Role map',
16
+ role_in_host: 'Role in Host LMS',
17
+ role_in_local: 'Role in Windward',
18
+ remote_role: 'Remote Role name',
19
+ map_to_role: 'Map to Windward Role',
20
+ delete_role_map_item: 'Delete role map item',
21
+ add_role_map_item: 'Add role map item',
22
+ role_map_instructions:
23
+ 'Only add roles to the below map if the host LMS uses different role names that you want to elevated permissions. Eg "Mentor" that you want to elevate to "External Instructor"',
24
+ role_map_warning:
25
+ 'Only add roles to the below map if the host LMS uses different role names that you want to elevated permissions. Eg "Mentor" that you want to elevate to "External Instructor"',
26
+ }
@@ -0,0 +1,5 @@
1
+ import lti1p1 from './driver/lti1p1'
2
+
3
+ export default {
4
+ driver: { lti1p1 },
5
+ }
@@ -1,7 +1,9 @@
1
1
  import navigation from './navigation'
2
2
  import integration from './integration'
3
+ import externalIntegration from './external_integration'
3
4
 
4
5
  export default {
5
6
  navigation,
6
7
  integration,
8
+ external_integration: externalIntegration,
7
9
  }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ title: 'External Integrations',
3
+ lti_1p1: 'LTI 1.1',
4
+ lti_1p3: 'LTI 1.3',
5
+ scorm_1p2: 'SCORM 1.2',
6
+ }
@@ -0,0 +1,5 @@
1
+ import externalIntegration from './external_integration'
2
+
3
+ export default {
4
+ external_integration: externalIntegration,
5
+ }
@@ -1,9 +1,13 @@
1
1
  import vendor from './vendor'
2
2
  import importCourse from './importCourse'
3
3
  import importContent from './importContent'
4
+ import login from './login'
5
+ import course from './course'
4
6
 
5
7
  export default {
6
8
  vendor,
7
9
  import_course: importCourse,
8
10
  import_content: importContent,
11
+ login,
12
+ course,
9
13
  }
@@ -0,0 +1,5 @@
1
+ import lti from './lti'
2
+
3
+ export default {
4
+ lti,
5
+ }
@@ -0,0 +1,21 @@
1
+ export default {
2
+ lti_error: 'LTI Errors',
3
+ launch_error: 'LTI link failed to launch',
4
+ missing_title: 'The below fields are missing:',
5
+ error_title: 'The below errors occurred:',
6
+ error: {
7
+ unknown: 'An unknown error occurred',
8
+ organization: 'Invalid organization',
9
+ tool: 'Invalid tool id',
10
+ tool_organization: 'This tool does not belong to this organization',
11
+ oauth: 'Invalid OAuth. Check your key and secret.',
12
+ },
13
+ missing: {
14
+ context_label:
15
+ 'The context label field `context_label` is missing or invalid.',
16
+ resource_link_title:
17
+ 'The resource link title `resource_link_title` is missing or invalid.',
18
+ lis_person_contact_email_primary:
19
+ 'The users email is missing and an anonymous email could not be generated.',
20
+ },
21
+ }
@@ -8,6 +8,8 @@ export default {
8
8
  'Organization Integrations',
9
9
  'plugin->windward->integrations->course': 'Course Integrations',
10
10
  'plugin->windward->integrations->content': 'Content Integrations',
11
+ 'plugin->windward->integrations->course->externalIntegration':
12
+ 'External Integrations (LTI / SCORM / etc)',
11
13
  },
12
14
 
13
15
  type_description: {
@@ -22,5 +24,7 @@ export default {
22
24
  'Access and manage course imports',
23
25
  'plugin->windward->integrations->content':
24
26
  'Access and manage content imports',
27
+ 'plugin->windward->integrations->course->externalIntegration':
28
+ 'Access and manage external integrations such as LTI and SCORM',
25
29
  },
26
30
  }
@@ -0,0 +1,13 @@
1
+ // @ts-ignore
2
+ import Model from '~/models/Model'
3
+
4
+ export default class Lti1p1Consumer extends Model {
5
+ get required(): string[] {
6
+ return []
7
+ }
8
+
9
+ // Set the resource route of the model
10
+ resource() {
11
+ return 'external-integrations/lti/1.1/consumers'
12
+ }
13
+ }
@@ -0,0 +1,13 @@
1
+ // @ts-ignore
2
+ import Model from '~/models/Model'
3
+
4
+ export default class Lti1p1Provider extends Model {
5
+ get required(): string[] {
6
+ return []
7
+ }
8
+
9
+ // Set the resource route of the model
10
+ resource() {
11
+ return 'external-integrations/lti/1.1/providers'
12
+ }
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windward/integrations",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Windward UI Plugin Integrations for 3rd Party Systems",
5
5
  "main": "plugin.js",
6
6
  "scripts": {
@@ -151,9 +151,9 @@
151
151
  import _ from 'lodash'
152
152
  import { mapGetters } from 'vuex'
153
153
 
154
- import Dialog from '~/components/Dialog.vue'
155
154
  import Organization from '../../models/Organization'
156
155
  import IntegrationJobTable from '../../components/Integration/JobTable.vue'
156
+ import Dialog from '~/components/Dialog.vue'
157
157
 
158
158
  export default {
159
159
  name: 'PluginIntegrationsAdminVendorsPage',
@@ -0,0 +1,72 @@
1
+ <template>
2
+ <div>
3
+ <h1>
4
+ {{
5
+ $t(
6
+ 'windward.integrations.pages.course.external_integration.title'
7
+ )
8
+ }}
9
+ </h1>
10
+ <v-tabs v-model="tab">
11
+ <v-tabs-slider></v-tabs-slider>
12
+
13
+ <v-tab>
14
+ {{
15
+ $t(
16
+ 'windward.integrations.pages.course.external_integration.lti_1p1'
17
+ )
18
+ }}
19
+ </v-tab>
20
+
21
+ <v-tab>
22
+ {{
23
+ $t(
24
+ 'windward.integrations.pages.course.external_integration.lti_1p3'
25
+ )
26
+ }}
27
+ </v-tab>
28
+
29
+ <v-tab>
30
+ {{
31
+ $t(
32
+ 'windward.integrations.pages.course.external_integration.scorm_1p2'
33
+ )
34
+ }}
35
+ </v-tab>
36
+ </v-tabs>
37
+
38
+ <v-tabs-items v-model="tab">
39
+ <v-tab-item> <ManageLti1p1></ManageLti1p1> </v-tab-item>
40
+ <v-tab-item>
41
+ <p class="ma-5">LTI 1.3 Not yet implemented</p>
42
+ </v-tab-item>
43
+ <v-tab-item>
44
+ <p class="ma-5">SCORM 1.2 Not yet implemented</p>
45
+ </v-tab-item>
46
+ </v-tabs-items>
47
+ </div>
48
+ </template>
49
+
50
+ <script>
51
+ import ManageLti1p1 from '../../../components/ExternalIntegration/Driver/ManageLti1p1.vue'
52
+
53
+ export default {
54
+ name: 'PluginIntegrationsExternalIntegrationIndexPage',
55
+ components: { ManageLti1p1 },
56
+ layout: 'authenticated',
57
+ meta: {
58
+ privilege: {
59
+ 'plugin.windward.integrations.course.externalIntegration': {
60
+ readable: true,
61
+ },
62
+ },
63
+ },
64
+ data() {
65
+ return {
66
+ tab: 0,
67
+ }
68
+ },
69
+ async fetch() {},
70
+ computed: {},
71
+ }
72
+ </script>
@@ -0,0 +1,98 @@
1
+ <template>
2
+ <v-container>
3
+ <v-divider />
4
+ <v-row>
5
+ <v-col cols="12" md="6" offset-md="3">
6
+ <h1 class="text-center mt-5">
7
+ {{
8
+ $t('windward.integrations.pages.login.lti.launch_error')
9
+ }}
10
+ </h1>
11
+ <v-alert
12
+ v-if="error.length === 0 && missing.length === 0"
13
+ type="error"
14
+ class="mt-5"
15
+ >
16
+ <strong>{{
17
+ $t(
18
+ 'windward.integrations.pages.login.lti.error.unknown'
19
+ )
20
+ }}</strong>
21
+ </v-alert>
22
+ <v-alert v-if="missing.length" type="warning" class="mt-5">
23
+ <strong>{{
24
+ $t(
25
+ 'windward.integrations.pages.login.lti.missing_title'
26
+ )
27
+ }}</strong>
28
+ <ul>
29
+ <li v-for="missingItem in missing" :key="missingItem">
30
+ {{
31
+ $t(
32
+ 'windward.integrations.pages.login.lti.missing.' +
33
+ missingItem
34
+ )
35
+ }}
36
+ </li>
37
+ </ul>
38
+ </v-alert>
39
+ <v-alert v-if="error.length" type="error" class="mt-5">
40
+ <strong>{{
41
+ $t('windward.integrations.pages.login.lti.error_title')
42
+ }}</strong>
43
+ <ul>
44
+ <li v-for="errorItem in error" :key="errorItem">
45
+ {{
46
+ $t(
47
+ 'windward.integrations.pages.login.lti.error.' +
48
+ errorItem
49
+ )
50
+ }}
51
+ </li>
52
+ </ul>
53
+ </v-alert>
54
+ </v-col>
55
+ </v-row>
56
+ </v-container>
57
+ </template>
58
+
59
+ <script>
60
+ import _ from 'lodash'
61
+ export default {
62
+ name: 'PluginIntegrationsLoginLtiErrorPage',
63
+ layout: 'default',
64
+ data() {
65
+ return {
66
+ missing: [],
67
+ error: [],
68
+ }
69
+ },
70
+ async fetch() {},
71
+ computed: {},
72
+ mounted() {
73
+ // Get the missing url parameters to display
74
+ if (!_.isEmpty(_.get(this.$route, 'query.missing', ''))) {
75
+ this.missing = _.get(this.$route, 'query.missing', '')
76
+ .split(',')
77
+ .map((v) => v.trim())
78
+ }
79
+
80
+ // Get the error url parameters to display
81
+ if (!_.isEmpty(_.get(this.$route, 'query.error', ''))) {
82
+ this.error = _.get(this.$route, 'query.error', '')
83
+ .split(',')
84
+ .map((v) => v.trim())
85
+ }
86
+
87
+ // Set the appropriate locale
88
+ if (!_.isEmpty(_.get(this.$route, 'query.locale', ''))) {
89
+ const locale = _.get(this.$route, 'query.locale', '')
90
+
91
+ if (this.$i18n.locales.includes(locale)) {
92
+ this.$i18n.setLocale(locale)
93
+ }
94
+ }
95
+ },
96
+ methods: {},
97
+ }
98
+ </script>
package/plugin.js CHANGED
@@ -2,6 +2,8 @@ import enUS from './i18n/en-US'
2
2
  import AdminVendorsPage from './pages/admin/vendors.vue'
3
3
  import AdminImportCoursePage from './pages/admin/importCourse.vue'
4
4
  import ImportContentPage from './pages/course/importContent.vue'
5
+ import ExternalIntegrationIndexPage from './pages/course/externalIntegration/index.vue'
6
+ import LoginLtiErrorPage from './pages/login/lti/error.vue'
5
7
  import IntegrationHelper from './helpers/IntegrationHelper'
6
8
 
7
9
  export default {
@@ -41,6 +43,21 @@ export default {
41
43
  name: 'PluginIntegrationsImportContentPage',
42
44
  template: ImportContentPage,
43
45
  },
46
+
47
+ {
48
+ page: 'admin-integrations',
49
+ path: '/course/:course/integrations/external',
50
+ i18n: 'windward.integrations.pages.course.external_integration.title',
51
+ name: 'PluginIntegrationsExternalIntegrationIndexPage',
52
+ template: ExternalIntegrationIndexPage,
53
+ },
54
+ {
55
+ page: 'lti-error',
56
+ path: '/login/lti/error',
57
+ i18n: 'windward.integrations.pages.login.lti.lti_error',
58
+ name: 'PluginIntegrationsLoginLtiErrorPage',
59
+ template: LoginLtiErrorPage,
60
+ },
44
61
  ],
45
62
  components: {
46
63
  menu: [
@@ -78,6 +95,17 @@ export default {
78
95
  },
79
96
  },
80
97
  },
98
+ {
99
+ i18n: 'windward.integrations.pages.course.external_integration.title',
100
+ path: '/course/{course.id}/integrations/external',
101
+ context: ['course'],
102
+ permissions: {
103
+ 'plugin.windward.integrations.course.externalIntegration':
104
+ {
105
+ readable: true,
106
+ },
107
+ },
108
+ },
81
109
  /*
82
110
  {
83
111
  i18n: 'windward.integrations.components.navigation.integrations.manage_lti',
@@ -0,0 +1,19 @@
1
+ import { shallowMount } from '@vue/test-utils'
2
+
3
+ import Vue from 'vue'
4
+ import Vuetify from 'vuetify'
5
+ import { defaultMocks } from '@/test/mocks'
6
+
7
+ import ManageConsumer from '@/components/ExternalIntegration/Driver/Lti1p1/ManageConsumer.vue'
8
+
9
+ Vue.use(Vuetify)
10
+
11
+ describe('ManageConsumer', () => {
12
+ test('ManageConsumer is a Vue instance', () => {
13
+ const wrapper = shallowMount(ManageConsumer, {
14
+ vuetify: new Vuetify(),
15
+ mocks: defaultMocks,
16
+ })
17
+ expect(wrapper.vm).toBeTruthy()
18
+ })
19
+ })
@@ -0,0 +1,19 @@
1
+ import { shallowMount } from '@vue/test-utils'
2
+
3
+ import Vue from 'vue'
4
+ import Vuetify from 'vuetify'
5
+ import { defaultMocks } from '@/test/mocks'
6
+
7
+ import ManageConsumers from '@/components/ExternalIntegration/Driver/Lti1p1/ManageConsumers.vue'
8
+
9
+ Vue.use(Vuetify)
10
+
11
+ describe('ManageConsumers', () => {
12
+ test('ManageConsumers is a Vue instance', () => {
13
+ const wrapper = shallowMount(ManageConsumers, {
14
+ vuetify: new Vuetify(),
15
+ mocks: defaultMocks,
16
+ })
17
+ expect(wrapper.vm).toBeTruthy()
18
+ })
19
+ })
@@ -0,0 +1,19 @@
1
+ import { shallowMount } from '@vue/test-utils'
2
+
3
+ import Vue from 'vue'
4
+ import Vuetify from 'vuetify'
5
+ import { defaultMocks } from '@/test/mocks'
6
+
7
+ import ManageProvider from '@/components/ExternalIntegration/Driver/Lti1p1/ManageProvider.vue'
8
+
9
+ Vue.use(Vuetify)
10
+
11
+ describe('ManageProvider', () => {
12
+ test('ManageProvider is a Vue instance', () => {
13
+ const wrapper = shallowMount(ManageProvider, {
14
+ vuetify: new Vuetify(),
15
+ mocks: defaultMocks,
16
+ })
17
+ expect(wrapper.vm).toBeTruthy()
18
+ })
19
+ })
@@ -0,0 +1,19 @@
1
+ import { shallowMount } from '@vue/test-utils'
2
+
3
+ import Vue from 'vue'
4
+ import Vuetify from 'vuetify'
5
+ import { defaultMocks } from '@/test/mocks'
6
+
7
+ import ManageProviders from '@/components/ExternalIntegration/Driver/Lti1p1/ManageProviders.vue'
8
+
9
+ Vue.use(Vuetify)
10
+
11
+ describe('ManageProviders', () => {
12
+ test('ManageProviders is a Vue instance', () => {
13
+ const wrapper = shallowMount(ManageProviders, {
14
+ vuetify: new Vuetify(),
15
+ mocks: defaultMocks,
16
+ })
17
+ expect(wrapper.vm).toBeTruthy()
18
+ })
19
+ })
@@ -0,0 +1,19 @@
1
+ import { shallowMount } from '@vue/test-utils'
2
+
3
+ import Vue from 'vue'
4
+ import Vuetify from 'vuetify'
5
+ import { defaultMocks } from '@/test/mocks'
6
+
7
+ import ManageLti1p1 from '@/components/ExternalIntegration/Driver/ManageLti1p1.vue'
8
+
9
+ Vue.use(Vuetify)
10
+
11
+ describe('ManageLti1p1', () => {
12
+ test('ManageLti1p1 is a Vue instance', () => {
13
+ const wrapper = shallowMount(ManageLti1p1, {
14
+ vuetify: new Vuetify(),
15
+ mocks: defaultMocks,
16
+ })
17
+ expect(wrapper.vm).toBeTruthy()
18
+ })
19
+ })
@@ -0,0 +1,19 @@
1
+ import { shallowMount } from '@vue/test-utils'
2
+
3
+ import Vue from 'vue'
4
+ import Vuetify from 'vuetify'
5
+ import { defaultMocks } from '@/test/mocks'
6
+
7
+ import ExternalIntegrationIndexPage from '@/pages/course/externalIntegration/index.vue'
8
+
9
+ Vue.use(Vuetify)
10
+
11
+ describe('ExternalIntegrationIndexPage', () => {
12
+ test('ExternalIntegrationIndexPage is a Vue instance', () => {
13
+ const wrapper = shallowMount(ExternalIntegrationIndexPage, {
14
+ vuetify: new Vuetify(),
15
+ mocks: defaultMocks,
16
+ })
17
+ expect(wrapper.vm).toBeTruthy()
18
+ })
19
+ })
@@ -0,0 +1,19 @@
1
+ import { shallowMount } from '@vue/test-utils'
2
+
3
+ import Vue from 'vue'
4
+ import Vuetify from 'vuetify'
5
+ import { defaultMocks } from '@/test/mocks'
6
+
7
+ import LtiErrorPage from '@/pages/login/lti/error.vue'
8
+
9
+ Vue.use(Vuetify)
10
+
11
+ describe('LtiErrorPage', () => {
12
+ test('LtiErrorPage is a Vue instance', () => {
13
+ const wrapper = shallowMount(LtiErrorPage, {
14
+ vuetify: new Vuetify(),
15
+ mocks: defaultMocks,
16
+ })
17
+ expect(wrapper.vm).toBeTruthy()
18
+ })
19
+ })
@@ -7,7 +7,10 @@ mockModel.$http = axios
7
7
  jest.mock('axios')
8
8
 
9
9
  // Define any new model mocks here. The imports / mocks will be auto-generated below
10
- const mockVirtualModels = []
10
+ const mockVirtualModels = [
11
+ { path: '~/models/Role', resource: 'roles' },
12
+ { path: '~/models/Organization', resource: 'organizations' },
13
+ ]
11
14
 
12
15
  const mockModels = [
13
16
  { path: '../../models/Organization', resource: 'organizations' },
package/test/mocks.js CHANGED
@@ -14,5 +14,6 @@ mocks.$Integration = {
14
14
  })
15
15
  },
16
16
  }
17
+ mocks.$PermissionService = { userHasAccessTo: () => true }
17
18
 
18
19
  export const defaultMocks = mocks