Skip to content

Commit

Permalink
Merge pull request #124 from edx/aakbar/PROD-1637
Browse files Browse the repository at this point in the history
feat: Add create new enrollment button
  • Loading branch information
Ali-D-Akbar authored Jun 18, 2021
2 parents ca3d25b + 1eea0ce commit a3e2de5
Show file tree
Hide file tree
Showing 14 changed files with 630 additions and 225 deletions.
42 changes: 39 additions & 3 deletions src/users/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,16 +304,52 @@ export async function postEntitlement({
}
}

export async function postEnrollmentChange({
export async function postEnrollment({
user,
courseID,
mode,
reason,
}) {
try {
const { data } = await getAuthenticatedHttpClient().post(
AppUrls.getEnrollmentsUrl(user),
{
course_id: courseID,
mode,
reason,
},
);
return data;
} catch (error) {
if (error.customAttributes.httpErrorStatus === 400) {
// eslint-disable-next-line no-console
console.log(JSON.parse(error.customAttributes.httpErrorResponseData));
}
return {
errors: [
{
code: null,
dismissible: true,
text:
'There was an error creating the enrollment. Check the JavaScript console for detailed errors.',
type: 'danger',
topic: 'enrollments',
},
],
};
}
}

export async function patchEnrollment({
user,
courseID,
newMode,
oldMode,
reason,
}) {
try {
const { data } = await getAuthenticatedHttpClient().post(
AppUrls.getEnrollmentChangeUrl(user),
const { data } = await getAuthenticatedHttpClient().patch(
AppUrls.getEnrollmentsUrl(user),
{
course_id: courseID,
new_mode: newMode,
Expand Down
51 changes: 45 additions & 6 deletions src/users/data/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

import enrollmentsData from './test/enrollments';
import { enrollmentsData } from './test/enrollments';
import * as api from './api';

describe('API', () => {
Expand Down Expand Up @@ -504,7 +504,46 @@ describe('API', () => {
});
});

describe('Post Enrollment Change', () => {
describe('Post Enrollment', () => {
const apiCallData = {
user: testUsername,
courseID: 'course-v1:testX',
mode: 'audit',
reason: 'test enrollment create',
};

const requestData = {
course_id: 'course-v1:testX',
mode: 'audit',
reason: 'test enrollment create',
};

it('Unsuccessful enrollment create', async () => {
const expectedError = {
code: null,
dismissible: true,
text:
'There was an error creating the enrollment. Check the JavaScript console for detailed errors.',
type: 'danger',
topic: 'enrollments',
};
mockAdapter.onPost(enrollmentsApiUrl, requestData).reply(() => throwError(400, ''));
const response = await api.postEnrollment({ ...apiCallData });
expect(...response.errors).toEqual(expectedError);
});

it('Successful enrollment create', async () => {
const expectedSuccessResponse = {
topic: 'enrollments',
message: 'enrollment created',
};
mockAdapter.onPost(enrollmentsApiUrl, requestData).reply(200, expectedSuccessResponse);
const response = await api.postEnrollment({ ...apiCallData });
expect(response).toEqual(expectedSuccessResponse);
});
});

describe('Patch Enrollment Change', () => {
const apiCallData = {
user: testUsername,
courseID: 'course-v1:testX',
Expand All @@ -529,8 +568,8 @@ describe('API', () => {
type: 'danger',
topic: 'enrollments',
};
mockAdapter.onPost(enrollmentsApiUrl, requestData).reply(() => throwError(400, ''));
const response = await api.postEnrollmentChange({ ...apiCallData });
mockAdapter.onPatch(enrollmentsApiUrl, requestData).reply(() => throwError(400, ''));
const response = await api.patchEnrollment({ ...apiCallData });
expect(...response.errors).toEqual(expectedError);
});

Expand All @@ -539,8 +578,8 @@ describe('API', () => {
topic: 'enrollments',
message: 'enrollment mode changed',
};
mockAdapter.onPost(enrollmentsApiUrl, requestData).reply(200, expectedSuccessResponse);
const response = await api.postEnrollmentChange({ ...apiCallData });
mockAdapter.onPatch(enrollmentsApiUrl, requestData).reply(200, expectedSuccessResponse);
const response = await api.patchEnrollment({ ...apiCallData });
expect(response).toEqual(expectedSuccessResponse);
});

Expand Down
26 changes: 0 additions & 26 deletions src/users/data/test/enrollmentForm.js

This file was deleted.

34 changes: 31 additions & 3 deletions src/users/data/test/enrollments.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,34 @@
const enrollmentsData = {
export const changeEnrollmentFormData = {
user: 'edX',
enrollment: {
courseId: 'course-v1:testX+test123+2030',
courseStart: Date().toLocaleString(),
verifiedUpgradeDeadline: Date().toLocaleString(),
courseEnd: Date().toLocaleString(),
created: Date().toLocaleString(),
courseModes: [
{
slug: 'verified',
},
],
isActive: true,
mode: 'audit',
manualEnrollment: {
reason: 'Test Enrollment',
enrolledBy: 'edX',
timestamp: Date().toLocaleString(),
},
},
changeHandler: jest.fn(() => {}),
closeHandler: jest.fn(() => {}),
};

export const createEnrollmentFormData = {
user: 'edX',
closeHandler: jest.fn(() => {}),
};

export const enrollmentsData = {
data: [{
courseId: 'course-v1:testX+test123+2030',
courseStart: Date().toLocaleString(),
Expand Down Expand Up @@ -42,5 +72,3 @@ const enrollmentsData = {
changeHandler: jest.fn(() => {}),
expanded: true,
};

export default enrollmentsData;
4 changes: 0 additions & 4 deletions src/users/data/urls.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,6 @@ export const getEntitlementUrl = (uuid = null) => {
return `${LMS_BASE_URL}/api/entitlements/v1/entitlements/${postfix}`;
};

export const getEnrollmentChangeUrl = user => `${
LMS_BASE_URL
}/support/enrollment/${user}`;

export const getTogglePasswordStatusUrl = user => `${
LMS_BASE_URL
}/support/manage_user/${user}`;
Expand Down
138 changes: 138 additions & 0 deletions src/users/enrollments/ChangeEnrollmentForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React, { useCallback, useState, useContext } from 'react';
import PropTypes from 'prop-types';
import {
Button, Input, InputSelect,
} from '@edx/paragon';

import AlertList from '../../userMessages/AlertList';
import { patchEnrollment } from '../data/api';
import UserMessagesContext from '../../userMessages/UserMessagesContext';
import { reasons } from './constants';

const getModes = function getModes(enrollment) {
const modeList = [];
enrollment.courseModes.map(mode => (
modeList.push(mode.slug)
));
if (!modeList.some(mode => mode === enrollment.mode)) {
modeList.push(enrollment.mode);
}
return modeList.sort();
};

export default function ChangeEnrollmentForm({
user,
enrollment,
changeHandler,
closeHandler,
forwardedRef,
}) {
const [mode, setMode] = useState(enrollment.mode);
const [reason, setReason] = useState('');
const [comments, setComments] = useState('');
const { add } = useContext(UserMessagesContext);

const submit = useCallback(() => {
const sendReason = (reason === 'other') ? comments : reason;
patchEnrollment({
user,
courseID: enrollment.courseId,
oldMode: enrollment.mode,
newMode: mode,
reason: sendReason,
}).then((result) => {
if (result.errors !== undefined) {
result.errors.forEach(error => add(error));
} else {
changeHandler();
}
});
});

return (
<section className="card mb-3">
<form className="card-body">
<AlertList topic="enrollments" className="mb-3" />
<h4 className="card-title">Change Enrollment</h4>
<div className="form-group">
<h5>Current Enrollment Data</h5>
<div className="mb-1"><strong>Course Run ID:</strong> {enrollment.courseId}</div>
<div className="mb-1"><strong>Mode:</strong> {enrollment.mode}</div>
<div className="mb-1"><strong>Active:</strong> {enrollment.isActive.toString()}</div>
</div>
<hr />
<div className="form-group">
<h5 className="card-subtitle">All fields are required</h5>
<InputSelect
label="New Mode"
type="select"
options={getModes(enrollment)}
id="mode"
name="mode"
value={enrollment.mode}
onChange={(event) => setMode(event)}
/>
</div>
<div className="form-group">
<InputSelect
label="Reason for change"
type="select"
options={reasons}
id="reason"
name="reason"
value=""
onChange={(event) => setReason(event)}
/>
</div>
<div className="form-group">
<label htmlFor="comments">Explain if other</label>
<Input
type="textarea"
id="comments"
name="comments"
defaultValue=""
onChange={(event) => setComments(event.target.value)}
ref={forwardedRef}
/>
</div>
<div>
<Button
variant="primary"
disabled={!(mode && reason)}
className="mr-3"
onClick={submit}
>
Submit
</Button>
<Button
variant="outline-secondary"
onClick={closeHandler}
>
Cancel
</Button>
</div>
</form>
</section>
);
}

ChangeEnrollmentForm.propTypes = {
enrollment: PropTypes.shape({
courseId: PropTypes.string.isRequired,
mode: PropTypes.string.isRequired,
isActive: PropTypes.bool.isRequired,
}),
user: PropTypes.string.isRequired,
changeHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
forwardedRef: PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
};

ChangeEnrollmentForm.defaultProps = {
enrollment: {
courseId: '',
mode: '',
isActive: false,
},
forwardedRef: null,
};
Loading

0 comments on commit a3e2de5

Please sign in to comment.