diff --git a/src/__tests__/rubric-defaults.spec.ts b/src/__tests__/rubric-defaults.spec.ts deleted file mode 100644 index 409a45c..0000000 --- a/src/__tests__/rubric-defaults.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { addRubric } from '../rubric-defaults'; -import { PieContent } from '../interface'; - -let pieContent: PieContent = { - id: '1', - elements: { - 'pie-rubric': '@pie-element/rubric@latest' - }, - models: [ - { - id: 'x', - element: 'pie-rubric' - } - ], - markup: 'markup here' -} - -describe('rubric defaults', () => { - - it('adds rubric to markup', () => { - const handled = addRubric(pieContent); - expect(handled.markup).toMatch(/pie-rubric/) - }); - - it('do nothing if in markup', () => { - const content = {...pieContent, markup: ''}; - const handled = addRubric(content); - expect(handled).toEqual(content); - }); - - - it('do nothing if no rubric', () => { - const content = { - id: '1', - elements: { - 'pie-rubric': '@pie-element/something-else@latest' - }, - models: [ - { - id: 'x', - element: 'pie-rubric' - } - ], - markup: 'markup here' - }; - const handled = addRubric(content); - expect(handled).toEqual(content); - }); -}); \ No newline at end of file diff --git a/src/__tests__/rubric-utils.spec.ts b/src/__tests__/rubric-utils.spec.ts new file mode 100644 index 0000000..70a9947 --- /dev/null +++ b/src/__tests__/rubric-utils.spec.ts @@ -0,0 +1,95 @@ +import { addRubric, addPackageToContent } from '../rubric-utils'; +import { PieContent, PieModel } from '../interface'; +import cloneDeep from 'lodash/cloneDeep' + + + +describe('rubric utils', () => { + + let pieContent:PieContent, noRubric:PieContent; + let rubricModel: PieModel; + beforeEach(() => { + pieContent = { + id: '1', + elements: { + 'pie-rubric': '@pie-element/rubric@latest' + }, + models: [ + { + id: 'x', + element: 'pie-rubric' + } + ], + markup: 'markup here' + }; + + noRubric = { + id: '1', + elements: { + 'something-else': '@pie-element/something-else@latest' + }, + models: [ + { + id: 'x', + element: 'something-else' + } + ], + markup: 'markup here' + }; + + rubricModel = { + "id": "rubric", + "element": "pie-rubric", + "points": [ + "level 1", + "level 2", + "level 3", + "level 4" + ], + "maxPoints": 4, + "excludeZero": false + } + + + }); + + describe('add rubric to markup', () => { + it('adds rubric to markup', () => { + const handled = addRubric(pieContent); + expect(handled.markup).toMatch(/pie-rubric/); + }); + + it('do nothing if in markup', () => { + const content = { + ...pieContent, + markup: '' + }; + const handled = addRubric(content); + expect(handled).toEqual(content); + }); + + it('do nothing if no rubric', () => { + const handled = addRubric(noRubric); + expect(handled).toEqual(noRubric); + }); + }); + + describe('add rubric to model', () => { + it('adds a rubric to a model', () => { + addPackageToContent(noRubric, '@pie-element/rubric', rubricModel); + expect(Object.values(noRubric.elements)).toContain('@pie-element/rubric') + }); + + it('does not modify if rubric already there', () => { + const copy = cloneDeep(pieContent); + addPackageToContent(pieContent, '@pie-element/rubric', rubricModel); + expect(copy).toEqual(pieContent); + }); + + it('works with semver', () => { + addPackageToContent(noRubric, '@pie-element/rubric@latest', rubricModel); + expect(Object.values(noRubric.elements)).toContain('@pie-element/rubric@latest') + }); + }); + +}); diff --git a/src/components.d.ts b/src/components.d.ts index 83137df..67ba1e4 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -24,6 +24,14 @@ export namespace Components { */ 'addPreview': boolean; /** + * If set the player will add a rubric authoring interaction to the config + */ + 'addRubric': boolean; + /** + * Utility method to add a `@pie-element/rubric` section to an item config when creating an item should be used before setting the config. + */ + 'addRubricToConfig': (config: ItemConfig, rubricModel?: any) => Promise; + /** * The Pie config model. */ 'config': ItemConfig; @@ -38,6 +46,10 @@ export namespace Components { */ 'addPreview'?: boolean; /** + * If set the player will add a rubric authoring interaction to the config + */ + 'addRubric'?: boolean; + /** * The Pie config model. */ 'config'?: ItemConfig; diff --git a/src/components/__tests__/mockPieCloudResponse.js b/src/components/__tests__/mockPieCloudResponse.js index 8d61561..917c6f5 100644 --- a/src/components/__tests__/mockPieCloudResponse.js +++ b/src/components/__tests__/mockPieCloudResponse.js @@ -61,6 +61,8 @@ class InlineChoiceConfig extends MockConfig {} class MathInline extends MockElement {} class MathInlineConfig extends MockConfig {} class Passage extends MockElement {} +class Rubric extends MockElement {} +class RubricConfig extends MockConfig {} @@ -81,6 +83,11 @@ window['pie'] = { Configure: MathInlineConfig, controller }, + '@pie-element/rubric': { + Element: Rubric, + Configure: RubricConfig, + controller + }, '@pie-element/passage': { Element: Passage } diff --git a/src/components/pie-author/__tests__/pie-author.e2e.ts b/src/components/pie-author/__tests__/pie-author.e2e.ts index 430aac6..1646ffc 100644 --- a/src/components/pie-author/__tests__/pie-author.e2e.ts +++ b/src/components/pie-author/__tests__/pie-author.e2e.ts @@ -1,13 +1,17 @@ import { newE2EPage, E2EElement, E2EPage } from '@stencil/core/testing'; import { setupInterceptPieCloud } from '../../__tests__/util'; import { simplePieMock, multipleChoiceItem, inlineChoiceItem } from '../../__mock__/config'; +import cloneDeep from 'lodash/cloneDeep'; describe('pie-author', () => { let pie; let page: E2EPage, pieAuthor: E2EElement; + let pieMock beforeEach(async () => { pie = '@pie-element/multiple-choice'; - page = await newE2EPage() + page = await newE2EPage(); + pieMock = cloneDeep(simplePieMock); + }); it('renders', async () => { @@ -21,7 +25,7 @@ describe('pie-author', () => { await page.setContent(''); pieAuthor = await page.find('pie-author'); await setupInterceptPieCloud(page, pie); - pieAuthor.setProperty('config', simplePieMock) + pieAuthor.setProperty('config', pieMock) await page.waitForChanges(); const el = await page.waitForSelector('pie-author'); expect(el).toBeDefined(); @@ -35,7 +39,7 @@ describe('pie-author', () => { await page.setContent(''); pieAuthor = await page.find('pie-author'); await setupInterceptPieCloud(page, pie); - pieAuthor.setProperty('config', simplePieMock) + pieAuthor.setProperty('config', pieMock) const spy = await page.spyOnEvent('modelLoaded'); await page.waitForChanges(); expect(spy).toHaveReceivedEvent(); @@ -46,7 +50,7 @@ describe('pie-author', () => { await page.setContent(''); pieAuthor = await page.find('pie-author'); await setupInterceptPieCloud(page, pie); - const emptyItem = simplePieMock; + const emptyItem = pieMock; emptyItem.models = null; await page.$eval('pie-author', (elm: any, prop) => { @@ -74,7 +78,7 @@ describe('pie-author', () => { return elm; }, { - config: simplePieMock, + config: pieMock, configSettings: {'@pie-element/multiple-choice': { "foo": "bar"} } } ); @@ -88,6 +92,26 @@ describe('pie-author', () => { expect(configProp.foo).toEqual("bar"); }); + it('add a rubric before adding config', async () => { + await page.setContent(''); + pieAuthor = await page.find('pie-author'); + await setupInterceptPieCloud(page, pie); + const rubricAdded = await pieAuthor.callMethod('addRubricToConfig', pieMock, {foo: 'bar'}); + expect(Object.values(rubricAdded.elements)).toContain('@pie-element/rubric'); + + const tagName = Object.keys(rubricAdded.elements)[1]; + + pieAuthor.setProperty('config', rubricAdded); + await page.waitForChanges(); + await page.waitForSelector(`pie-author ${tagName}-config:defined`); + const rubricModel = await page.$eval( + `pie-author ${tagName}-config`, + el => (el as any).model + ); + expect(rubricModel.foo).toEqual('bar'); + + }); + // TODO request intercetpion needs to be updated for this to work it.skip('can switch items', async() => { await page.setContent(''); @@ -102,7 +126,6 @@ describe('pie-author', () => { ); expect(pieModel.element).toEqual('pie-multiple-choice'); - console.log(`got past first item`) await setupInterceptPieCloud(page, `@pie-element/inline-choice`); pieAuthor.setProperty('config', inlineChoiceItem); await page.waitForChanges(); diff --git a/src/components/pie-author/pie-author.tsx b/src/components/pie-author/pie-author.tsx index 9547f39..a184fea 100644 --- a/src/components/pie-author/pie-author.tsx +++ b/src/components/pie-author/pie-author.tsx @@ -1,10 +1,11 @@ -import { Component, Element, Prop, State, Watch, Event, EventEmitter } from '@stencil/core'; -import { PieContent, ItemConfig, PieElement } from '../../interface'; +import { Component, Element, Prop, State, Watch, Event, EventEmitter, Method } from '@stencil/core'; +import { PieContent, ItemConfig, PieElement, PieModel } from '../../interface'; import { PieLoader } from '../../pie-loader'; import { pieContentFromConfig } from '../../utils/utils'; import parseNpm from 'parse-package-name'; import { ModelUpdatedEvent } from '@pie-framework/pie-configure-events'; import _isEqual from 'lodash/isEqual'; +import { addPackageToContent, addRubric } from '../../rubric-utils'; /** * Pie Author will load a Pie Content model for authoring. @@ -18,6 +19,13 @@ import _isEqual from 'lodash/isEqual'; export class Author { @Prop({ context: 'document' }) doc!: Document; + + /** + * If set the player will add a rubric authoring interaction to the config + */ + @Prop() addRubric: boolean; + + /** * Adds a preview view which will render the content in another tab as it may appear to a student or instructor. */ @@ -161,7 +169,6 @@ export class Author { await this.afterRender(); await this.updateModels(); this.el.addEventListener(ModelUpdatedEvent.TYPE, (e:ModelUpdatedEvent) => { - console.log(`got event ${e.detail}`) // set the internal model // emit a content-item level event with the model if (this.pieContentModel && e.update) { @@ -197,6 +204,36 @@ export class Author { } } + /** + * Utility method to add a `@pie-element/rubric` section to an item config when creating an item should be used before setting the config. + * + * @deprecated this method is for temporary use, will be removed at next major release + * + * @param config the item config to mutate + * @param rubricModel + */ + @Method() + async addRubricToConfig(config: ItemConfig, rubricModel?) { + if (!rubricModel) { + rubricModel = { + "id": "rubric", + "element": "pie-rubric", + "points": [ + "", + "", + "", + "" + ], + "maxPoints": 4, + "excludeZero": false + }; + } + const configPieContent = pieContentFromConfig(config); + addPackageToContent(configPieContent, '@pie-element/rubric', rubricModel as PieModel); + addRubric(configPieContent); + return config; + } + render() { if (this.pieContentModel && this.pieContentModel.markup) { if (this.addPreview) { diff --git a/src/components/pie-author/readme.md b/src/components/pie-author/readme.md index 451c5f9..27ba258 100644 --- a/src/components/pie-author/readme.md +++ b/src/components/pie-author/readme.md @@ -16,6 +16,7 @@ The class `pie-loading` will be added to the element while assets are being load | Property | Attribute | Description | Type | Default | | ---------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ----------- | | `addPreview` | `add-preview` | Adds a preview view which will render the content in another tab as it may appear to a student or instructor. | `boolean` | `false` | +| `addRubric` | `add-rubric` | If set the player will add a rubric authoring interaction to the config | `boolean` | `undefined` | | `config` | -- | The Pie config model. | `AdvancedItemConfig \| PieContent` | `undefined` | | `configSettings` | -- | To customize the standard behaviour provided by interaction configuration views you can provide settings key-ed by the package name. e.g. `{ '@pie-element/inline-choice': { promptLabel: 'Item Stem' } }` The settings that are configurable for each authoring view are documented in the `@package-name/docs` folder for each package. | `{ [packageName: string]: Object; }` | `undefined` | @@ -28,6 +29,26 @@ The class `pie-loading` will be added to the element while assets are being load | `modelUpdated` | Emmitted when the model for the content has been updated within the ui due to user action. | `CustomEvent` | +## Methods + +### `addRubricToConfig(config: ItemConfig, rubricModel?: any) => Promise` + +Utility method to add a `@pie-element/rubric` section to an item config when creating an item should be used before setting the config. + +#### Parameters + +| Name | Type | Description | +| ------------- | ---------------------------------- | ------------------------- | +| `config` | `AdvancedItemConfig \| PieContent` | the item config to mutate | +| `rubricModel` | `any` | | + +#### Returns + +Type: `Promise` + + + + ---------------------------------------------- *Built with [StencilJS](https://stenciljs.com/)* diff --git a/src/components/pie-player/pie-player.tsx b/src/components/pie-player/pie-player.tsx index ddc9587..363bd80 100644 --- a/src/components/pie-player/pie-player.tsx +++ b/src/components/pie-player/pie-player.tsx @@ -10,7 +10,7 @@ import { } from '@stencil/core'; import { PieContent, ItemConfig, ItemSession, PieElement, PieController, AdvancedItemConfig, PieModel} from '../../interface'; import { PieLoader } from '../../pie-loader'; -import { addRubric } from '../../rubric-defaults'; +import { addRubric } from '../../rubric-utils'; const controllerErrorMessage: string = 'Error processing question configuration, verify the question model?'; diff --git a/src/demo/config-preview.html b/src/demo/config-preview.html index 34c7c33..81e880d 100644 --- a/src/demo/config-preview.html +++ b/src/demo/config-preview.html @@ -84,8 +84,11 @@ markup: "" }; - - author.config = config; + author.componentOnReady().then(() => { + author.addRubricToConfig(config).then(newConf => { + author.config = newConf; + }) + }); diff --git a/src/demo/item-author-add-rubric.html b/src/demo/item-author-add-rubric.html new file mode 100644 index 0000000..2c0a10c --- /dev/null +++ b/src/demo/item-author-add-rubric.html @@ -0,0 +1,88 @@ + + + + + + Item + + + + + + + + diff --git a/src/rubric-defaults.ts b/src/rubric-utils.ts similarity index 54% rename from src/rubric-defaults.ts rename to src/rubric-utils.ts index 21b165a..d845c79 100644 --- a/src/rubric-defaults.ts +++ b/src/rubric-utils.ts @@ -1,14 +1,17 @@ -import { PieContent } from './interface'; +import { PieContent, PieModel } from './interface'; import { modelsForPackage, - elementForPackage + elementForPackage, + pieShortIdGenerator, + elementsHasPackage } from './utils/utils'; + /** * Allows you to modify the markup for a package that is present in elements/model but * missing from markup. * @param content the pie content - * @param npmPackage the npm pacakge + * @param npmPackage the npm package * @param template a callback function for modifing the markup */ export const addMarkupForPackage = ( @@ -47,3 +50,25 @@ export const addRubric = (content: PieContent): PieContent => { } ); }; + +/** + * Adds the provided package to the provided PieContent Object's `elements` and `models` properties. + * + * @param content the PieContent for rendering + * @param packageToAdd the NPM Package to add to the content config + * @param model optional the PieModel to add, `id` and `element` properties will be replaced by this function if present + */ +export const addPackageToContent = (content: PieContent, packageToAdd, model?:PieModel) => { + + if (packageToAdd && !elementsHasPackage(content.elements, packageToAdd)) { + model = model ? model : {} as any; + // add package + model.id = pieShortIdGenerator(); + const elementName = pieShortIdGenerator(); + model.element = elementName; + content.models && content.models.push(model); + content.elements && (content.elements[elementName] = packageToAdd); + return content; + } + +}; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 050734e..0cf047c 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -118,3 +118,15 @@ export const patchMDCSwitchFocus = element => { { passive: true } ); }; + +/** + * Creates short id for use within pie models. + * Can be used to create random element tag or model id. + */ +export const pieShortIdGenerator = () => { + // Not an advanced algorighm, but only need to be unique within the current model. + var S4 = function() { + return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); + }; + return `p-${S4() + S4()}`; +} \ No newline at end of file