Skip to content

Commit fc383af

Browse files
committed
feat: implement schedule and details permissions
1 parent ad49a12 commit fc383af

21 files changed

Lines changed: 164 additions & 37 deletions

File tree

src/authz/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,8 @@ export const COURSE_PERMISSIONS = {
2020

2121
VIEW_GRADING_SETTINGS: 'courses.view_grading_settings',
2222
EDIT_GRADING_SETTINGS: 'courses.edit_grading_settings',
23+
24+
VIEW_SCHEDULE_AND_DETAILS: 'courses.view_schedule_and_details',
25+
EDIT_SCHEDULE: 'courses.edit_schedule',
26+
EDIT_DETAILS: 'courses.edit_details',
2327
};

src/authz/permissionHelpers.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
import { COURSE_PERMISSIONS } from './constants';
22

3+
export const getScheduleAndDetailsPermissions = (courseId: string) => ({
4+
canViewScheduleAndDetails: {
5+
action: COURSE_PERMISSIONS.VIEW_SCHEDULE_AND_DETAILS,
6+
scope: courseId,
7+
},
8+
canEditSchedule: {
9+
action: COURSE_PERMISSIONS.EDIT_SCHEDULE,
10+
scope: courseId,
11+
},
12+
canEditDetails: {
13+
action: COURSE_PERMISSIONS.EDIT_DETAILS,
14+
scope: courseId,
15+
},
16+
});
17+
318
export const getGradingPermissions = (courseId: string) => ({
419
canViewGradingSettings: {
520
action: COURSE_PERMISSIONS.VIEW_GRADING_SETTINGS,

src/generic/WysiwygEditor.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const SUPPORTED_TEXT_EDITORS = {
1010
};
1111

1212
export const WysiwygEditor = ({
13-
initialValue, editorType, onChange, minHeight,
13+
initialValue, editorType, onChange, minHeight, disabled,
1414
}) => {
1515
const { editorRef, refReady, setEditorRef } = prepareEditorRef();
1616
const { courseId } = useCourseAuthoringContext();
@@ -60,6 +60,7 @@ export const WysiwygEditor = ({
6060
images={{}}
6161
enableImageUpload={false}
6262
onEditorChange={() => ({})}
63+
disabled={disabled}
6364
/>
6465
);
6566
};
@@ -68,11 +69,13 @@ WysiwygEditor.defaultProps = {
6869
initialValue: '',
6970
editorType: SUPPORTED_TEXT_EDITORS.text,
7071
minHeight: 200,
72+
disabled: false,
7173
};
7274

7375
WysiwygEditor.propTypes = {
7476
initialValue: PropTypes.string,
7577
editorType: PropTypes.oneOf(Object.values(SUPPORTED_TEXT_EDITORS)),
7678
onChange: PropTypes.func.isRequired,
7779
minHeight: PropTypes.number,
80+
disabled: PropTypes.bool,
7881
};

src/generic/course-upload-image/index.jsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const CourseUploadImage = ({
2727
identifierFieldText,
2828
showImageBodyText,
2929
customInputPlaceholder,
30+
disabled,
3031
onChange,
3132
}) => {
3233
const { courseId } = useParams();
@@ -109,13 +110,16 @@ const CourseUploadImage = ({
109110
<Form.Label>{label}</Form.Label>
110111
<Card>
111112
<Card.Body className="image-body">
112-
<Dropzone
113-
onProcessUpload={handleProcessUpload}
114-
inputComponent={inputComponent}
115-
accept={{
116-
'image/*': ['.png', '.jpeg'],
117-
}}
118-
/>
113+
<div style={disabled ? { pointerEvents: 'none' } : undefined}>
114+
<Dropzone
115+
onProcessUpload={handleProcessUpload}
116+
inputComponent={inputComponent}
117+
accept={{
118+
'image/*': ['.png', '.jpeg'],
119+
}}
120+
disabled={disabled}
121+
/>
122+
</div>
119123
{showImageBodyText && cardImageTextBody}
120124
</Card.Body>
121125
<Card.Divider />
@@ -129,6 +133,7 @@ const CourseUploadImage = ({
129133
identifierFieldText,
130134
})
131135
}
136+
disabled={disabled}
132137
/>
133138
</Card.Footer>
134139
</Card>
@@ -150,6 +155,7 @@ CourseUploadImage.defaultProps = {
150155
showImageBodyText: false,
151156
identifierFieldText: '',
152157
customInputPlaceholder: '',
158+
disabled: false,
153159
};
154160

155161
CourseUploadImage.propTypes = {
@@ -161,6 +167,7 @@ CourseUploadImage.propTypes = {
161167
showImageBodyText: PropTypes.bool,
162168
identifierFieldText: PropTypes.string,
163169
customInputPlaceholder: PropTypes.string,
170+
disabled: PropTypes.bool,
164171
onChange: PropTypes.func.isRequired,
165172
};
166173

src/schedule-and-details/details-section/index.jsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import SectionSubHeader from '../../generic/section-sub-header';
77
import messages from './messages';
88

99
const DetailsSection = ({
10-
language, languageOptions, onChange,
10+
language, languageOptions, isEditable, onChange,
1111
}) => {
1212
const intl = useIntl();
1313
const formattedLanguage = () => {
@@ -24,14 +24,14 @@ const DetailsSection = ({
2424
<Form.Group className="form-group-custom dropdown-language">
2525
<Form.Label>{intl.formatMessage(messages.dropdownLabel)}</Form.Label>
2626
<Dropdown className="bg-white">
27-
<Dropdown.Toggle variant="outline-primary" id="languageDropdown">
27+
<Dropdown.Toggle variant="outline-primary" id="languageDropdown" disabled={!isEditable}>
2828
{formattedLanguage()}
2929
</Dropdown.Toggle>
3030
<Dropdown.Menu>
3131
{languageOptions.map((option) => (
3232
<Dropdown.Item
3333
key={option[0]}
34-
onClick={() => onChange(option[0], 'language')}
34+
onClick={isEditable ? () => onChange(option[0], 'language') : undefined}
3535
>
3636
{option[1]}
3737
</Dropdown.Item>
@@ -48,13 +48,15 @@ const DetailsSection = ({
4848

4949
DetailsSection.defaultProps = {
5050
language: '',
51+
isEditable: true,
5152
};
5253

5354
DetailsSection.propTypes = {
5455
language: PropTypes.string,
5556
languageOptions: PropTypes.arrayOf(
5657
PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
5758
).isRequired,
59+
isEditable: PropTypes.bool,
5860
onChange: PropTypes.func.isRequired,
5961
};
6062

src/schedule-and-details/index.jsx

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import { STATEFUL_BUTTON_STATES } from '@src/constants';
1717
import getPageHeadTitle from '@src/generic/utils';
1818
import { useScrollToHashElement } from '@src/hooks';
1919
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
20+
import { useUserPermissionsWithAuthzCourse } from '@src/authz/hooks';
21+
import { getScheduleAndDetailsPermissions } from '@src/authz/permissionHelpers';
22+
import PermissionDeniedAlert from '@src/generic/PermissionDeniedAlert';
2023

2124
import {
2225
fetchCourseSettingsQuery,
@@ -49,12 +52,19 @@ const ScheduleAndDetails = () => {
4952
const courseDetails = useSelector(getCourseDetails);
5053
const loadingDetailsStatus = useSelector(getLoadingDetailsStatus);
5154
const loadingSettingsStatus = useSelector(getLoadingSettingsStatus);
52-
const isLoading = loadingDetailsStatus === RequestStatus.IN_PROGRESS
53-
|| loadingSettingsStatus === RequestStatus.IN_PROGRESS;
5455

5556
const { courseId, courseDetails: course } = useCourseAuthoringContext();
5657
document.title = getPageHeadTitle(course?.name || '', intl.formatMessage(messages.headingTitle));
5758

59+
const {
60+
isLoading: isLoadingUserPermissions,
61+
permissions: userPermissions,
62+
} = useUserPermissionsWithAuthzCourse(courseId, getScheduleAndDetailsPermissions(courseId));
63+
64+
const isLoading = loadingDetailsStatus === RequestStatus.IN_PROGRESS
65+
|| loadingSettingsStatus === RequestStatus.IN_PROGRESS
66+
|| isLoadingUserPermissions;
67+
5868
const {
5969
platformName,
6070
isCreditCourse,
@@ -144,6 +154,10 @@ const ScheduleAndDetails = () => {
144154
const { overview: initialOverview } = courseDetails || {};
145155
const { aboutSidebarHtml: initialAboutSidebarHtml } = courseDetails || {};
146156

157+
if (!isLoadingUserPermissions && !userPermissions.canViewScheduleAndDetails) {
158+
return <PermissionDeniedAlert />;
159+
}
160+
147161
if (isLoading) {
148162
// eslint-disable-next-line react/jsx-no-useless-fragment
149163
return <></>;
@@ -157,6 +171,9 @@ const ScheduleAndDetails = () => {
157171
);
158172
}
159173

174+
const isScheduleEditable = !isLoadingUserPermissions && userPermissions.canEditSchedule;
175+
const isDetailsEditable = !isLoadingUserPermissions && userPermissions.canEditDetails;
176+
160177
const showCreditSection = creditEligibilityEnabled && isCreditCourse;
161178
const showRequirementsSection = aboutPageEditable || isPrerequisiteCoursesEnabled || isEntranceExamsEnabled;
162179
const hasErrors = !!Object.keys(errorFields).length;
@@ -255,6 +272,7 @@ const ScheduleAndDetails = () => {
255272
<PacingSection
256273
selfPaced={selfPaced}
257274
startDate={startDate}
275+
isEditable={isDetailsEditable}
258276
onChange={handleValuesChange}
259277
/>
260278
<ScheduleSection
@@ -269,12 +287,14 @@ const ScheduleAndDetails = () => {
269287
certificateAvailableDate={certificateAvailableDate}
270288
certificatesDisplayBehavior={certificatesDisplayBehavior}
271289
canShowCertificateAvailableDateField={canShowCertificateAvailableDateField}
290+
isEditable={isScheduleEditable}
272291
onChange={handleValuesChange}
273292
/>
274293
{aboutPageEditable && (
275294
<DetailsSection
276295
language={language}
277296
languageOptions={languageOptions}
297+
isEditable={isDetailsEditable}
278298
onChange={handleValuesChange}
279299
/>
280300
)}
@@ -295,16 +315,19 @@ const ScheduleAndDetails = () => {
295315
shortDescriptionEditable={shortDescriptionEditable}
296316
enableExtendedCourseDetails={enableExtendedCourseDetails}
297317
videoThumbnailImageAssetPath={videoThumbnailImageAssetPath}
318+
isEditable={isDetailsEditable}
298319
onChange={handleValuesChange}
299320
/>
300321
{enableExtendedCourseDetails && (
301322
<>
302323
<LearningOutcomesSection
303324
learningInfo={learningInfo}
325+
isEditable={isDetailsEditable}
304326
onChange={handleValuesChange}
305327
/>
306328
<InstructorsSection
307329
instructors={instructorInfo?.instructors}
330+
isEditable={isDetailsEditable}
308331
onChange={handleValuesChange}
309332
/>
310333
</>
@@ -322,12 +345,14 @@ const ScheduleAndDetails = () => {
322345
isPrerequisiteCoursesEnabled={
323346
isPrerequisiteCoursesEnabled
324347
}
348+
isEditable={isDetailsEditable}
325349
onChange={handleValuesChange}
326350
/>
327351
)}
328352
{licensingEnabled && (
329353
<LicenseSection
330354
license={license}
355+
isEditable={isDetailsEditable}
331356
onChange={handleValuesChange}
332357
/>
333358
)}
@@ -375,7 +400,7 @@ const ScheduleAndDetails = () => {
375400
<StatefulButton
376401
key="save-button"
377402
onClick={handleUpdateValues}
378-
disabled={hasErrors}
403+
disabled={hasErrors || (!isScheduleEditable && !isDetailsEditable)}
379404
state={
380405
isQueryPending
381406
? STATEFUL_BUTTON_STATES.pending

src/schedule-and-details/instructors-section/index.jsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import InstructorContainer from './instructor-container';
99
import SectionSubHeader from '../../generic/section-sub-header';
1010
import messages from './messages';
1111

12-
const InstructorsSection = ({ instructors, onChange }) => {
12+
const InstructorsSection = ({ instructors, isEditable, onChange }) => {
1313
const intl = useIntl();
1414
const newInstructor = {
1515
bio: '',
@@ -64,12 +64,13 @@ const InstructorsSection = ({ instructors, onChange }) => {
6464
instructor={instructor}
6565
key={uuid}
6666
idx={idx}
67+
isEditable={isEditable}
6768
onDelete={handleDelete}
6869
onChange={handleChange}
6970
/>
7071
))}
7172
</ul>
72-
<Button iconBefore={AddIcon} variant="primary" onClick={handleAdd}>
73+
<Button iconBefore={AddIcon} variant="primary" onClick={handleAdd} disabled={!isEditable}>
7374
{intl.formatMessage(messages.instructorAdd)}
7475
</Button>
7576
</section>
@@ -78,6 +79,7 @@ const InstructorsSection = ({ instructors, onChange }) => {
7879

7980
InstructorsSection.defaultProps = {
8081
instructors: [],
82+
isEditable: true,
8183
};
8284

8385
InstructorsSection.propTypes = {
@@ -90,6 +92,7 @@ InstructorsSection.propTypes = {
9092
title: PropTypes.string,
9193
}),
9294
),
95+
isEditable: PropTypes.bool,
9396
onChange: PropTypes.func.isRequired,
9497
};
9598

src/schedule-and-details/instructors-section/instructor-container/index.jsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import CourseUploadImage from '../../../generic/course-upload-image';
1010
import messages from './messages';
1111

1212
const InstructorContainer = ({
13-
instructor, idx, onDelete, onChange,
13+
instructor, idx, isEditable, onDelete, onChange,
1414
}) => {
1515
const intl = useIntl();
1616
return (
@@ -26,6 +26,7 @@ const InstructorContainer = ({
2626
value={instructor?.name}
2727
placeholder={intl.formatMessage(messages.instructorNameInputPlaceholder)}
2828
onChange={(e) => onChange(e.target.value, idx, 'name')}
29+
disabled={!isEditable}
2930
/>
3031
<Form.Text>
3132
{intl.formatMessage(messages.instructorNameHelpText)}
@@ -40,6 +41,7 @@ const InstructorContainer = ({
4041
value={instructor?.title}
4142
placeholder={intl.formatMessage(messages.instructorTitleInputPlaceholder)}
4243
onChange={(e) => onChange(e.target.value, idx, 'title')}
44+
disabled={!isEditable}
4345
/>
4446
<Form.Text>
4547
{intl.formatMessage(messages.instructorTitleHelpText)}
@@ -54,6 +56,7 @@ const InstructorContainer = ({
5456
value={instructor?.organization}
5557
placeholder={intl.formatMessage(messages.instructorOrganizationInputPlaceholder)}
5658
onChange={(e) => onChange(e.target.value, idx, 'organization')}
59+
disabled={!isEditable}
5760
/>
5861
<Form.Text>
5962
{intl.formatMessage(messages.instructorOrganizationHelpText)}
@@ -70,6 +73,7 @@ const InstructorContainer = ({
7073
value={instructor?.bio}
7174
placeholder={intl.formatMessage(messages.instructorBioInputPlaceholder)}
7275
onChange={(e) => onChange(e.target.value, idx, 'bio')}
76+
disabled={!isEditable}
7377
/>
7478
<Form.Text>
7579
{intl.formatMessage(messages.instructorBioHelpText)}
@@ -85,14 +89,15 @@ const InstructorContainer = ({
8589
messages.instructorPhotoInputPlaceholder,
8690
)}
8791
customHelpText={intl.formatMessage(messages.instructorPhotoHelpText)}
92+
disabled={!isEditable}
8893
onChange={(value, field) => onChange(value, idx, field)}
8994
/>
9095
</Form.Row>
9196
</Form>
9297
</Card.Body>
9398
<Card.Divider />
9499
<Card.Footer className="p-0 mt-2.5">
95-
<Button variant="outline-primary" onClick={() => onDelete(idx)}>
100+
<Button variant="outline-primary" onClick={() => onDelete(idx)} disabled={!isEditable}>
96101
{intl.formatMessage(messages.instructorDelete)}
97102
</Button>
98103
</Card.Footer>
@@ -102,6 +107,7 @@ const InstructorContainer = ({
102107

103108
InstructorContainer.defaultProps = {
104109
instructor: {},
110+
isEditable: true,
105111
};
106112

107113
InstructorContainer.propTypes = {
@@ -113,6 +119,7 @@ InstructorContainer.propTypes = {
113119
title: PropTypes.string,
114120
}),
115121
idx: PropTypes.number.isRequired,
122+
isEditable: PropTypes.bool,
116123
onDelete: PropTypes.func.isRequired,
117124
onChange: PropTypes.func.isRequired,
118125
};

0 commit comments

Comments
 (0)