Skip to content

Commit

Permalink
Use the new createAssignmentAPI if configured by the backend
Browse files Browse the repository at this point in the history
Create and edit assignments by first calling the API endpoint and then
submit the assignment to the LMS.
  • Loading branch information
marcospri committed Nov 2, 2021
1 parent cc6bb91 commit 30f7773
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 6 deletions.
40 changes: 37 additions & 3 deletions lms/static/scripts/frontend_apps/components/FilePickerApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from 'preact/hooks';

import { Config } from '../config';
import { apiCall } from '../utils/api';
import { truncateURL } from '../utils/format';

import ContentSelector from './ContentSelector';
Expand Down Expand Up @@ -64,14 +65,17 @@ function contentDescription(content) {
export default function FilePickerApp({ onSubmit }) {
const submitButton = useRef(/** @type {HTMLInputElement|null} */ (null));
const {
api: { authToken },
filePicker: {
formAction,
formFields,
createAssignmentAPI: createAssignmentAPI,
canvas: { groupsEnabled: enableGroupConfig, ltiLaunchUrl },
},
} = useContext(Config);

const [content, setContent] = useState(/** @type {Content|null} */ (null));
const [extLTIAssignmentId, setExtLTIAssignmentId] = useState(null);

const [groupConfig, setGroupConfig] = useState(
/** @type {GroupConfig} */ ({
Expand All @@ -89,7 +93,36 @@ export default function FilePickerApp({ onSubmit }) {
* render.
*/
const [shouldSubmit, setShouldSubmit] = useState(false);
const submit = useCallback(() => setShouldSubmit(true), []);
const submit = useCallback(
async (/** @type {Content} */ content) => {
async function createAssignment() {
const data = {
...createAssignmentAPI.data,
content,
groupset: groupConfig.groupSet,
};
const assignment = await apiCall({
authToken,
path: createAssignmentAPI.path,
data,
});
setExtLTIAssignmentId(assignment.ext_lti_assignment_id);
}
if (content && createAssignmentAPI && !extLTIAssignmentId) {
try {
await createAssignment();
} catch (error) {
setErrorInfo({
message: 'Creating or editing an assignment',
error: error,
});
return;
}
}
setShouldSubmit(true);
},
[authToken, createAssignmentAPI, extLTIAssignmentId, groupConfig.groupSet]
);

// Submit the form after a selection is made via one of the available
// methods.
Expand All @@ -107,7 +140,7 @@ export default function FilePickerApp({ onSubmit }) {
content => {
setContent(content);
if (!enableGroupConfig) {
submit();
submit(content);
}
},
[enableGroupConfig, submit]
Expand Down Expand Up @@ -152,7 +185,7 @@ export default function FilePickerApp({ onSubmit }) {
<LabeledButton
disabled={groupConfig.useGroupSet && !groupConfig.groupSet}
variant="primary"
onClick={submit}
onClick={() => submit(content)}
>
Continue
</LabeledButton>
Expand All @@ -164,6 +197,7 @@ export default function FilePickerApp({ onSubmit }) {
ltiLaunchURL={ltiLaunchUrl}
content={content}
formFields={formFields}
extLTIAssignmentId={extLTIAssignmentId}
groupSet={groupConfig.useGroupSet ? groupConfig.groupSet : null}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { contentItemForContent } from '../utils/content-item';
* @prop {Record<string,string>} formFields - Form fields provided by the backend
* that should be included in the response without any changes
* @prop {string|null} groupSet
* @prop {string|null} extLTIAssignmentId
*/

/**
Expand All @@ -34,9 +35,13 @@ export default function FilePickerFormFields({
formFields,
groupSet,
ltiLaunchURL,
extLTIAssignmentId,
}) {
/** @type {Record<string,string>} */
const extraParams = groupSet ? { group_set: groupSet } : {};
if (extLTIAssignmentId) {
extraParams.ext_lti_assignment_id = extLTIAssignmentId;
}
const contentItem = JSON.stringify(
contentItemForContent(ltiLaunchURL, content, extraParams)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Config } from '../../config';
import FilePickerApp, { $imports } from '../FilePickerApp';
import { checkAccessibility } from '../../../test-util/accessibility';
import mockImportedComponents from '../../../test-util/mock-imported-components';
import { waitFor } from '../../../test-util/wait';

function interact(wrapper, callback) {
act(callback);
Expand All @@ -31,6 +32,9 @@ describe('FilePickerApp', () => {

beforeEach(() => {
fakeConfig = {
api: {
authToken: 'dummyAuthToken',
},
filePicker: {
formAction: 'https://www.shinylms.com/',
formFields: { hidden_field: 'hidden_value' },
Expand All @@ -55,12 +59,18 @@ describe('FilePickerApp', () => {
/**
* Check that the expected hidden form fields were set.
*/
function checkFormFields(wrapper, expectedContent, expectedGroupSet) {
function checkFormFields(
wrapper,
expectedContent,
expectedGroupSet,
expectedExtLTIAssignmentId
) {
const formFields = wrapper.find('FilePickerFormFields');
assert.deepEqual(formFields.props(), {
children: [],
content: expectedContent,
formFields: fakeConfig.filePicker.formFields,
extLTIAssignmentId: expectedExtLTIAssignmentId,
groupSet: expectedGroupSet,
ltiLaunchURL: fakeConfig.filePicker.canvas.ltiLaunchUrl,
});
Expand Down Expand Up @@ -104,6 +114,79 @@ describe('FilePickerApp', () => {
});
}

context('when create assignment configuration is enabled', () => {
const authURL = 'https://testlms.hypothes.is/authorize';
const createAssignmentPath = '/api/canvas/assignments';
let fakeAPICall;
let fakeNewAssignment;

beforeEach(() => {
fakeConfig.filePicker.createAssignmentAPI = {
authURL,
path: createAssignmentPath,
};

fakeAPICall = sinon.stub();
fakeNewAssignment = { ext_lti_assignment_id: 10 };

fakeAPICall
.withArgs(sinon.match({ path: createAssignmentPath }))
.resolves(fakeNewAssignment);

$imports.$mock({
'../utils/api': { apiCall: fakeAPICall },
});
});

it('calls backend api when content is selected', async () => {
const onSubmit = sinon.stub().callsFake(e => e.preventDefault());
const wrapper = renderFilePicker({ onSubmit });

selectContent(wrapper, 'https://example.com');

await waitFor(() => fakeAPICall.called);
wrapper.update();

assert.calledWith(fakeAPICall, {
authToken: 'dummyAuthToken',
path: createAssignmentPath,
data: {
content: { type: 'url', url: 'https://example.com' },
groupset: null,
},
});

assert.called(onSubmit);
checkFormFields(
wrapper,
{
type: 'url',
url: 'https://example.com',
},
null /* groupSet */,
fakeNewAssignment.ext_lti_assignment_id
);
});

it('shows an error if creating the assignment fails', async () => {
const error = new Error('Something happened');
const onSubmit = sinon.stub().callsFake(e => e.preventDefault());
fakeAPICall
.withArgs(sinon.match({ path: createAssignmentPath }))
.rejects(error);

const wrapper = renderFilePicker({ onSubmit });

selectContent(wrapper, 'https://example.com');

await waitFor(() => fakeAPICall.called);
wrapper.update();

const errDialog = wrapper.find('ErrorDialog');
assert.equal(errDialog.length, 1);
assert.equal(errDialog.prop('error'), error);
});
});
context('when groups are not enabled', () => {
it('submits form when content is selected', () => {
const onSubmit = sinon.stub().callsFake(e => e.preventDefault());
Expand All @@ -118,7 +201,8 @@ describe('FilePickerApp', () => {
type: 'url',
url: 'https://example.com',
},
null /* groupSet */
null /* groupSet */,
null /* extLTIAssignmentId */
);
});

Expand Down Expand Up @@ -217,7 +301,8 @@ describe('FilePickerApp', () => {
type: 'url',
url: 'https://example.com',
},
useGroupSet ? 'groupSet1' : null
useGroupSet ? 'groupSet1' : null,
null /* extLTIAssignmentId */
);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,23 @@ describe('FilePickerFormFields', () => {
);
});

it('adds `ext_lti_assignment_id` query param to LTI launch URL if `extLTIAssignmentId` prop is specified', () => {
const content = { type: 'url', url: 'https://example.com/' };
const formFields = createComponent({
content,
extLTIAssignmentId: 'EXT_LTI_ASSIGNMENT_ID',
});
const contentItems = JSON.parse(
formFields.find('input[name="content_items"]').prop('value')
);
assert.deepEqual(
contentItems,
contentItemForContent(launchURL, content, {
ext_lti_assignment_id: 'EXT_LTI_ASSIGNMENT_ID',
})
);
});

it('renders `document_url` field for URL content', () => {
const formFields = createComponent({
content: { type: 'url', url: 'https://example.com/' },
Expand Down

0 comments on commit 30f7773

Please sign in to comment.