@windward/integrations 0.0.1

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 (54) hide show
  1. package/.editorconfig +13 -0
  2. package/.eslintrc.js +15 -0
  3. package/.prettierrc +4 -0
  4. package/README.md +19 -0
  5. package/babel.config.js +1 -0
  6. package/components/Integration/Driver/ManageAtutor.vue +143 -0
  7. package/components/Integration/Driver/ManageBase.vue +145 -0
  8. package/components/Integration/JobTable.vue +308 -0
  9. package/components/Integration/TestConnection.vue +45 -0
  10. package/config/integration.config.js +13 -0
  11. package/helpers/Driver/Atutor.ts +12 -0
  12. package/helpers/Driver/BaseDriver.ts +25 -0
  13. package/helpers/Driver/DriverInterface.ts +7 -0
  14. package/helpers/IntegrationHelper.ts +150 -0
  15. package/i18n/en-US/components/index.ts +7 -0
  16. package/i18n/en-US/components/integration/driver.ts +18 -0
  17. package/i18n/en-US/components/integration/index.ts +7 -0
  18. package/i18n/en-US/components/integration/job.ts +22 -0
  19. package/i18n/en-US/components/navigation/index.ts +5 -0
  20. package/i18n/en-US/components/navigation/integrations.ts +8 -0
  21. package/i18n/en-US/index.ts +16 -0
  22. package/i18n/en-US/modules/index.ts +5 -0
  23. package/i18n/en-US/pages/importContent.ts +3 -0
  24. package/i18n/en-US/pages/importCourse.ts +13 -0
  25. package/i18n/en-US/pages/index.ts +9 -0
  26. package/i18n/en-US/pages/vendor.ts +11 -0
  27. package/i18n/en-US/shared/error.ts +8 -0
  28. package/i18n/en-US/shared/index.ts +11 -0
  29. package/i18n/en-US/shared/menu.ts +3 -0
  30. package/i18n/en-US/shared/permission.ts +26 -0
  31. package/i18n/en-US/shared/settings.ts +1 -0
  32. package/jest.config.js +17 -0
  33. package/models/CourseSectionIntegration.ts +12 -0
  34. package/models/IntegrationJob.ts +12 -0
  35. package/models/Organization.ts +14 -0
  36. package/models/OrganizationIntegration.ts +17 -0
  37. package/models/RemoteContent.ts +12 -0
  38. package/models/RemoteCourse.ts +17 -0
  39. package/models/RemoteOrganization.ts +17 -0
  40. package/models/Vendor.ts +12 -0
  41. package/package.json +44 -0
  42. package/pages/admin/importCourse.vue +390 -0
  43. package/pages/admin/vendors.vue +241 -0
  44. package/pages/course/importContent.vue +25 -0
  45. package/plugin.js +110 -0
  46. package/test/Helpers/IntegrationHelper.spec.js +92 -0
  47. package/test/Pages/Admin/ImportCourse.spec.js +19 -0
  48. package/test/Pages/Admin/vendors.spec.js +19 -0
  49. package/test/Pages/Course/importContent.spec.js +19 -0
  50. package/test/__mocks__/lodashMock.js +31 -0
  51. package/test/__mocks__/modelMock.js +101 -0
  52. package/test/__mocks__/vuexMock.js +31 -0
  53. package/test/mocks.js +18 -0
  54. package/tsconfig.json +21 -0
@@ -0,0 +1,45 @@
1
+ <template>
2
+ <div>
3
+ <v-btn
4
+ :disabled="disabled"
5
+ :loading="loading"
6
+ outlined
7
+ color="primary"
8
+ @click="$emit('click')"
9
+ >{{
10
+ $t(
11
+ 'windward.integrations.components.integration.driver.test_connection'
12
+ )
13
+ }}
14
+ </v-btn>
15
+
16
+ <v-alert v-if="errors" class="mt-5" color="error" icon="mdi-connection">
17
+ <p>
18
+ <strong>
19
+ {{
20
+ $t(
21
+ 'windward.integrations.components.integration.driver.connection_error'
22
+ )
23
+ }}
24
+ </strong>
25
+ </p>
26
+ <p>
27
+ <code>
28
+ {{ errors }}
29
+ </code>
30
+ </p>
31
+ </v-alert>
32
+ </div>
33
+ </template>
34
+
35
+ <script>
36
+ export default {
37
+ name: 'IntegrationTestConnection',
38
+ props: {
39
+ loading: { type: Boolean, required: false, default: false },
40
+ disabled: { type: Boolean, required: false, default: false },
41
+ errors: { type: String, required: false, default: '' },
42
+ },
43
+ emits: ['click'],
44
+ }
45
+ </script>
@@ -0,0 +1,13 @@
1
+ import Atutor from '../helpers/Driver/Atutor'
2
+
3
+ export default {
4
+ /**
5
+ * Enabled Integrations
6
+ *
7
+ * Formatted {key}: DriverImport
8
+ * {key} is the question type name as it appears in the database's integration_vendors.product_code
9
+ */
10
+ integrationDrivers: {
11
+ atutor: { driver: Atutor },
12
+ },
13
+ }
@@ -0,0 +1,12 @@
1
+ // @ts-ignore
2
+ import Manage from '../../components/Integration/Driver/ManageAtutor.vue'
3
+ import DriverInterface, { IntegrationComponents } from './DriverInterface'
4
+ import BaseDriver from './BaseDriver'
5
+
6
+ export default class Atutor extends BaseDriver implements DriverInterface {
7
+ public components(): IntegrationComponents {
8
+ return {
9
+ Manage,
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,25 @@
1
+ import DriverInterface, { IntegrationComponents } from './DriverInterface'
2
+
3
+ export default class BaseDriver implements DriverInterface {
4
+ /**
5
+ * An object containing references to the various vue templates.
6
+ * This returns the typescript type IntegrationComponents { Manage }
7
+ *
8
+ * Manage: The template used by an editor to setup the integration
9
+ *
10
+ * @returns Object {Manage}
11
+ */
12
+ public components(): IntegrationComponents {
13
+ return {
14
+ Manage: {},
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Helper function to get the manage component
20
+ * @returns Object The manage component
21
+ */
22
+ public getManageComponent(): any {
23
+ return this.components().Manage
24
+ }
25
+ }
@@ -0,0 +1,7 @@
1
+ export type IntegrationComponents = {
2
+ Manage: object
3
+ }
4
+
5
+ export default interface DriverInterface {
6
+ components(): IntegrationComponents
7
+ }
@@ -0,0 +1,150 @@
1
+ import _ from 'lodash'
2
+ import Echo from 'laravel-echo'
3
+ import Pusher from 'pusher-js'
4
+
5
+ import Settings from '../config/integration.config'
6
+ import Vendor from '../models/Vendor'
7
+
8
+ export default class IntegrationHelper {
9
+ private _app: any
10
+ private _loaded: boolean
11
+ private _vendors: any = []
12
+ private _Pusher?: any
13
+ private _Echo?: Echo
14
+
15
+ constructor(app: any) {
16
+ this._app = app
17
+ this._loaded = false
18
+ }
19
+
20
+ /**
21
+ * Get the Laravel Echo socket
22
+ * This also initalizes a socket if not already connected and stores it as a singleton
23
+ *
24
+ * @returns {Echo} Singleton Laravel echo socket connection
25
+ */
26
+ public getSocket(): Echo {
27
+ // Only define _.Echo once
28
+ if (typeof this._Echo === 'undefined') {
29
+ this._Pusher = Pusher
30
+ // Set the API endpoint since laravel echo will attempt to use the current url aka ui for requests
31
+ // Also include the bearer token for auth
32
+ this._Echo = new Echo({
33
+ authEndpoint: process.env.BASE_URL + '/broadcasting/auth',
34
+ auth: {
35
+ headers: {
36
+ Authorization: this._app.$auth.strategy.token.get(),
37
+ },
38
+ },
39
+ broadcaster: process.env.BROADCAST_DRIVER,
40
+ key: process.env.BROADCAST_KEY,
41
+ cluster: process.env.BROADCAST_CLUSTER,
42
+ wsHost: process.env.BROADCAST_HOST,
43
+ wsPort: process.env.BROADCAST_PORT,
44
+ wssPort: process.env.BROADCAST_PORT,
45
+ forceTLS:
46
+ _.get(
47
+ process.env,
48
+ 'BROADCAST_FORCE_TLS',
49
+ ''
50
+ ).toLowerCase() === 'true',
51
+ disableStats: true,
52
+ })
53
+ }
54
+
55
+ return this._Echo
56
+ }
57
+
58
+ /**
59
+ * Load the vendors from the database
60
+ *
61
+ * @return {Promise<void>}
62
+ */
63
+ public async load(): Promise<void> {
64
+ if (!this._loaded) {
65
+ // Be sure to only make the below await request once
66
+ this._loaded = true
67
+
68
+ // @ts-ignore
69
+ const vendors = await Vendor.where('enabled', true).get()
70
+
71
+ // Only add question types that exist both in the DB and in the assessment.config.js
72
+ Object.keys(Settings.integrationDrivers).forEach((driverName) => {
73
+ const vendor = vendors.find(
74
+ (v: any) => v.product_code === driverName
75
+ )
76
+
77
+ if (vendor) {
78
+ this._vendors.push(vendor)
79
+ }
80
+ })
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Get all the enabled system vendors
86
+ *
87
+ * @returns {array} An array of the vendors
88
+ */
89
+ public getVendors() {
90
+ return _.cloneDeep(this._vendors)
91
+ }
92
+
93
+ /**
94
+ * The the vendor specific driver
95
+ *
96
+ * @param {string} _vendorId The vendor id from the Vendor model
97
+ * @returns {DriverInterface} Vendor specific instance of the DriverInterface
98
+ */
99
+ public getVendorDriver(_vendorId: String) {
100
+ const settings = this.getVendorConfig(_vendorId)
101
+
102
+ const Driver: any = settings.driver
103
+
104
+ if (Driver) {
105
+ return new Driver()
106
+ } else {
107
+ throw new TypeError(
108
+ `Could not determine assessment question driver for question type '${settings.vendor.product_code}' of id '${_vendorId}'`
109
+ )
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Get the integration.config values for a specific vendor id
115
+ *
116
+ * @param {string} _vendorId The vendor id from the Vendor model
117
+ * @returns {object} The vendor configuration or TypeError if not found
118
+ */
119
+ public getVendorConfig(_vendorId: String) {
120
+ const vendorModel = this.getVendors().find(
121
+ (v: any) => v.id === _vendorId
122
+ )
123
+
124
+ const settings: any = _.get(
125
+ Settings.integrationDrivers,
126
+ vendorModel.product_code,
127
+ null
128
+ )
129
+
130
+ if (settings) {
131
+ settings.vendor = vendorModel
132
+ return settings
133
+ } else {
134
+ throw new TypeError(
135
+ `Could not determine vendor driver for question type '${vendorModel.product_code}' of id '${_vendorId}'`
136
+ )
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Get the manage vue component for a specific vendor
142
+ *
143
+ * @param {string} _vendorId The vendor id from the Vendor model
144
+ * @returns {VueComponent} The drivers component
145
+ */
146
+ public getVendorManageComponent(_vendorId: String): any {
147
+ const driver = this.getVendorDriver(_vendorId)
148
+ return driver.getManageComponent()
149
+ }
150
+ }
@@ -0,0 +1,7 @@
1
+ import navigation from './navigation'
2
+ import integration from './integration'
3
+
4
+ export default {
5
+ navigation,
6
+ integration,
7
+ }
@@ -0,0 +1,18 @@
1
+ export default {
2
+ atutor: {
3
+ manage_dialog_title: 'Manage ATutor Driver',
4
+ url: 'ATutor API Url',
5
+ url_hint: 'Eg: https://atutor.mindedgecollege.com',
6
+ username: 'Username',
7
+ password: 'Password',
8
+ aws_secure_url: 'AWS CDN Url',
9
+ aws_secure_url_hint: 'Eg: https://cdn-d.mindedgecollege.com',
10
+ },
11
+ enabled: 'Integration Enabled',
12
+ disabled: 'Integration Disabled',
13
+ ssl_enabled: 'SSL Enabled (Should be enabled for production)',
14
+ connection_error: 'Connection Error',
15
+ test_connection: 'Test Connection',
16
+ id: 'Id',
17
+ base_url: 'Base Url',
18
+ }
@@ -0,0 +1,7 @@
1
+ import driver from './driver'
2
+ import job from './job'
3
+
4
+ export default {
5
+ driver,
6
+ job,
7
+ }
@@ -0,0 +1,22 @@
1
+ export default {
2
+ recent_jobs_title: 'Recent Import Jobs',
3
+ job_completed: 'Job Completed',
4
+ no_recent_jobs: 'No recent jobs',
5
+ job_id: 'Job Id',
6
+ vendor_name: 'Vendor Name',
7
+ status: 'Status',
8
+ progress: 'Progress',
9
+ created: 'Created',
10
+ details: 'Details',
11
+ job_details: {
12
+ none: 'No details available',
13
+ import_course: "Importing Course Id {0} '{1}'",
14
+ },
15
+ job_status: {
16
+ not_started: 'Not Started',
17
+ started: 'Started',
18
+ in_progress: 'In Progress',
19
+ failed: 'Failed',
20
+ completed: 'Completed',
21
+ },
22
+ }
@@ -0,0 +1,5 @@
1
+ import integrations from './integrations'
2
+
3
+ export default {
4
+ integrations,
5
+ }
@@ -0,0 +1,8 @@
1
+ export default {
2
+ title: 'Integrations',
3
+ manage_integrations: 'Manage Integration Vendors',
4
+ import_course: 'Import Courses',
5
+ import_content: 'Import Content & Files',
6
+ manage_lti: 'Manage LTI Connections',
7
+ manage_lti_links: 'Manage LTI Links',
8
+ }
@@ -0,0 +1,16 @@
1
+ import pages from './pages/index'
2
+ import components from './components/index'
3
+ import shared from './shared/index'
4
+ import modules from './modules/index'
5
+
6
+ export default {
7
+ windward: {
8
+ integrations: {
9
+ name: 'Windward Plugin Integrations',
10
+ pages,
11
+ components,
12
+ shared,
13
+ modules,
14
+ },
15
+ },
16
+ }
@@ -0,0 +1,5 @@
1
+ // import example from './example'
2
+
3
+ export default {
4
+ // example,
5
+ }
@@ -0,0 +1,3 @@
1
+ export default {
2
+ title: 'Import Content',
3
+ }
@@ -0,0 +1,13 @@
1
+ export default {
2
+ title: 'Import Course',
3
+ recent_import_title: 'Recent Imports',
4
+ select_vendor: 'Select a vendor',
5
+ select_remote_organization: 'Select a remote organization',
6
+ select_remote_course: 'Select a remote course',
7
+ select_remote_content: 'Select individual content pages',
8
+ start_import: 'Start Import',
9
+ import_started: 'Import Started',
10
+ notify: 'Notify me when completed',
11
+ no_vendors:
12
+ 'There are no configured vendors. Please go back and configure an integration vendor',
13
+ }
@@ -0,0 +1,9 @@
1
+ import vendor from './vendor'
2
+ import importCourse from './importCourse'
3
+ import importContent from './importContent'
4
+
5
+ export default {
6
+ vendor,
7
+ import_course: importCourse,
8
+ import_content: importContent,
9
+ }
@@ -0,0 +1,11 @@
1
+ export default {
2
+ title: 'Integrations',
3
+ vendor_disabled: 'This vendor is not available for integrations',
4
+ vendor_not_configured: 'This integration vendor is not configured',
5
+ organization_integration_disabled:
6
+ 'This organization integration is disabled',
7
+ vendor_name: 'Vendor Name',
8
+ configured: 'Integration Configured',
9
+ enabled: 'Integration Enabled',
10
+ manage: 'Manage Integration',
11
+ }
@@ -0,0 +1,8 @@
1
+ export default {
2
+ save_failed: 'Could not save this configuration',
3
+ load_remote_organization_failed: 'Could not load remote organizations',
4
+ load_remote_course_failed: 'Could not load remote courses',
5
+ load_remote_content_failed: 'Could not load remote content',
6
+ connect_success: 'Successfully connected',
7
+ connect_fail: 'Failed to connect',
8
+ }
@@ -0,0 +1,11 @@
1
+ import settings from './settings'
2
+ import menu from './menu'
3
+ import permission from './permission'
4
+ import error from './error'
5
+
6
+ export default {
7
+ settings,
8
+ menu,
9
+ permission,
10
+ error,
11
+ }
@@ -0,0 +1,3 @@
1
+ export default {
2
+ integrations: 'Integrations',
3
+ }
@@ -0,0 +1,26 @@
1
+ export default {
2
+ type_title: {
3
+ 'plugin->windward->integrations->vendor': 'Integration Vendors',
4
+ 'plugin->windward->integrations->jobs': 'Integration Jobs',
5
+ 'plugin->windward->integrations->organization->jobs':
6
+ 'Organization Integration Jobs',
7
+ 'plugin->windward->integrations->organization->integration':
8
+ 'Organization Integrations',
9
+ 'plugin->windward->integrations->course': 'Course Integrations',
10
+ 'plugin->windward->integrations->content': 'Content Integrations',
11
+ },
12
+
13
+ type_description: {
14
+ 'plugin->windward->integrations->vendor':
15
+ 'Access all integration vendors across the whole system',
16
+ 'plugin->windward->integrations->jobs': 'Access integration jobs',
17
+ 'plugin->windward->integrations->organization->jobs':
18
+ 'Access integration jobs in the current organization',
19
+ 'plugin->windward->integrations->organization->integration':
20
+ 'Access and manage integrations in the current organization',
21
+ 'plugin->windward->integrations->course':
22
+ 'Access and manage course imports',
23
+ 'plugin->windward->integrations->content':
24
+ 'Access and manage content imports',
25
+ },
26
+ }
@@ -0,0 +1 @@
1
+ export default {}
package/jest.config.js ADDED
@@ -0,0 +1,17 @@
1
+ module.exports = {
2
+ moduleNameMapper: {
3
+ '^@/(.*)$': '<rootDir>/$1',
4
+ '^vue$': 'vue/dist/vue.common.js',
5
+ '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
6
+ '<rootDir>/node_modules/@windward/core/test/__mocks__/fileMock.js',
7
+ '\\.(css|less)$':
8
+ '<rootDir>/node_modules/@windward/core/test/__mocks__/styleMock.js',
9
+ },
10
+ moduleFileExtensions: ['ts', 'js', 'vue', 'json'],
11
+ transformIgnorePatterns: ['node_modules/(?!@ngrx|(?!deck.gl)|ng-dynamic)'],
12
+ transform: {
13
+ '^.+\\.ts$': 'ts-jest',
14
+ '^.+\\.js$': 'babel-jest',
15
+ '.*\\.(vue)$': 'vue-jest',
16
+ },
17
+ }
@@ -0,0 +1,12 @@
1
+ import Model from '~/models/Model'
2
+
3
+ export default class CourseSectionIntegration extends Model {
4
+ get required(): string[] {
5
+ return []
6
+ }
7
+
8
+ // Set the resource route of the model
9
+ resource() {
10
+ return 'section-integrations'
11
+ }
12
+ }
@@ -0,0 +1,12 @@
1
+ import Model from '~/models/Model'
2
+
3
+ export default class IntegrationJob extends Model {
4
+ get required(): string[] {
5
+ return []
6
+ }
7
+
8
+ // Set the resource route of the model
9
+ resource() {
10
+ return 'integration-jobs'
11
+ }
12
+ }
@@ -0,0 +1,14 @@
1
+ import Model from '~/models/Model'
2
+ import BaseOrganization from '~/models/Organization'
3
+ import IntegrationJob from './IntegrationJob'
4
+ import OrganizationIntegration from './OrganizationIntegration'
5
+
6
+ export default class Organization extends BaseOrganization {
7
+ integrations() {
8
+ return this.hasMany(OrganizationIntegration)
9
+ }
10
+
11
+ integrationJobs() {
12
+ return this.hasMany(IntegrationJob)
13
+ }
14
+ }
@@ -0,0 +1,17 @@
1
+ import Model from '~/models/Model'
2
+ import RemoteOrganization from './RemoteOrganization'
3
+
4
+ export default class OrganizationIntegration extends Model {
5
+ get required(): string[] {
6
+ return []
7
+ }
8
+
9
+ // Set the resource route of the model
10
+ resource() {
11
+ return 'organization-integrations'
12
+ }
13
+
14
+ remoteOrganizations() {
15
+ return this.hasMany(RemoteOrganization)
16
+ }
17
+ }
@@ -0,0 +1,12 @@
1
+ import Model from '~/models/Model'
2
+
3
+ export default class RemoteContent extends Model {
4
+ get required(): string[] {
5
+ return []
6
+ }
7
+
8
+ // Set the resource route of the model
9
+ resource() {
10
+ return 'remote-content'
11
+ }
12
+ }
@@ -0,0 +1,17 @@
1
+ import Model from '~/models/Model'
2
+ import RemoteContent from './RemoteContent'
3
+
4
+ export default class RemoteCourse extends Model {
5
+ get required(): string[] {
6
+ return []
7
+ }
8
+
9
+ // Set the resource route of the model
10
+ resource() {
11
+ return 'remote-courses'
12
+ }
13
+
14
+ remoteContent() {
15
+ return this.hasMany(RemoteContent)
16
+ }
17
+ }
@@ -0,0 +1,17 @@
1
+ import Model from '~/models/Model'
2
+ import RemoteCourse from './RemoteCourse'
3
+
4
+ export default class RemoteOrganization extends Model {
5
+ get required(): string[] {
6
+ return []
7
+ }
8
+
9
+ // Set the resource route of the model
10
+ resource() {
11
+ return 'remote-organizations'
12
+ }
13
+
14
+ remoteCourses() {
15
+ return this.hasMany(RemoteCourse)
16
+ }
17
+ }
@@ -0,0 +1,12 @@
1
+ import Model from '~/models/Model'
2
+
3
+ export default class Vendor extends Model {
4
+ get required(): string[] {
5
+ return []
6
+ }
7
+
8
+ // Set the resource route of the model
9
+ resource() {
10
+ return 'integration-vendors'
11
+ }
12
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@windward/integrations",
3
+ "version": "0.0.1",
4
+ "description": "Windward UI Plugin Integrations for 3rd Party Systems",
5
+ "main": "plugin.js",
6
+ "scripts": {
7
+ "test": "jest",
8
+ "lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
9
+ "build": "tsc"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+ssh://git@bitbucket.org:mindedge/windward-ui-plugin-integrations.git"
14
+ },
15
+ "author": "Jacob Rogaishio",
16
+ "license": "MIT",
17
+ "homepage": "https://bitbucket.org/mindedge/windward-ui-plugin-integrations#readme",
18
+ "dependencies": {
19
+ "@windward/core": "^0.0.2",
20
+ "canvas-confetti": "^1.6.0",
21
+ "eslint": "^8.11.0",
22
+ "laravel-echo": "^1.15.0",
23
+ "prettier": "^2.6.0",
24
+ "pusher-js": "^8.0.1"
25
+ },
26
+ "devDependencies": {
27
+ "@babel/preset-env": "^7.16.11",
28
+ "@nuxtjs/axios": "^5.13.6",
29
+ "@nuxtjs/eslint-config-typescript": "^12.0.0",
30
+ "@types/lodash": "^4.14.180",
31
+ "@vue/test-utils": "^1.1.3",
32
+ "babel-core": "^7.0.0-bridge.0",
33
+ "babel-jest": "^26.6.3",
34
+ "eslint-config-prettier": "^8.5.0",
35
+ "eslint-plugin-nuxt": "^3.2.0",
36
+ "eslint-plugin-prettier": "^4.0.0",
37
+ "jest": "^26.6.3",
38
+ "ts-jest": "^26.5.6",
39
+ "typescript": "^4.6.3",
40
+ "vue-api-query": "^1.11.0",
41
+ "vue-jest": "^3.0.7",
42
+ "vuetify": "^2.6.4"
43
+ }
44
+ }