Skip to content

Commit b60df73

Browse files
committed
feat: implement grading permissions and authorization hooks
1 parent 2930265 commit b60df73

12 files changed

Lines changed: 165 additions & 8 deletions

File tree

src/authz/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,7 @@ export const CONTENT_LIBRARY_PERMISSIONS = {
1717

1818
export const COURSE_PERMISSIONS = {
1919
MANAGE_ADVANCED_SETTINGS: 'courses.manage_advanced_settings',
20+
21+
VIEW_GRADING_SETTINGS: 'courses.view_grading_settings',
22+
EDIT_GRADING_SETTINGS: 'courses.edit_grading_settings',
2023
};

src/authz/hooks.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { useWaffleFlags } from '@src/data/apiHooks';
2+
import { useUserPermissions } from '@src/authz/data/apiHooks';
3+
import { PermissionValidationQuery, PermissionValidationAnswer } from '@src/authz/types';
4+
5+
/**
6+
* Return type for the useUserPermissionsWithAuthzCourse hook
7+
*/
8+
interface UseUserPermissionsWithAuthzCourseReturn {
9+
/** Whether permissions are currently loading */
10+
isLoading: boolean;
11+
/** Object containing permission results with boolean values */
12+
permissions: PermissionValidationAnswer;
13+
/** Whether authorization is enabled for the course */
14+
isAuthzEnabled: boolean;
15+
}
16+
17+
/**
18+
* Custom hook to handle user permissions with course authorization waffle flag
19+
*
20+
* This hook abstracts the common pattern of:
21+
* 1. Checking if authz is enabled via waffle flag
22+
* 2. Fetching user permissions when authz is enabled
23+
* 3. Defaulting all permissions to true when authz is disabled
24+
* 4. Providing fallback values for undefined permissions
25+
*
26+
* @param courseId - The course ID to check permissions for
27+
* @param permissions - Object mapping permission names to their action/scope definitions
28+
* @returns Object containing loading state, permissions results, and authz status
29+
*
30+
* @example
31+
* ```tsx
32+
* const { isLoading, permissions, isAuthzEnabled } = useUserPermissionsWithAuthzCourse(
33+
* courseId,
34+
* {
35+
* canViewGradingSettings: {
36+
* action: COURSE_PERMISSIONS.VIEW_GRADING_SETTINGS,
37+
* scope: courseId,
38+
* },
39+
* canEditGradingSettings: {
40+
* action: COURSE_PERMISSIONS.EDIT_GRADING_SETTINGS,
41+
* scope: courseId,
42+
* },
43+
* }
44+
* );
45+
*
46+
* const { canViewGradingSettings, canEditGradingSettings } = permissions;
47+
* ```
48+
*/
49+
export const useUserPermissionsWithAuthzCourse = (
50+
courseId: string,
51+
permissions: PermissionValidationQuery,
52+
): UseUserPermissionsWithAuthzCourseReturn => {
53+
const waffleFlags = useWaffleFlags(courseId);
54+
const isAuthzEnabled: boolean = waffleFlags?.enableAuthzCourseAuthoring ?? false;
55+
56+
const {
57+
isLoading: isLoadingUserPermissions,
58+
data: userPermissions,
59+
} = useUserPermissions(permissions, isAuthzEnabled);
60+
61+
// Build permission results object
62+
const permissionResults: PermissionValidationAnswer = {};
63+
64+
if (isAuthzEnabled && !isLoadingUserPermissions) {
65+
// Authz is enabled and permissions loaded, use actual permission values with fallback to false
66+
Object.keys(permissions).forEach((permissionKey: string) => {
67+
permissionResults[permissionKey] = userPermissions?.[permissionKey] ?? false;
68+
});
69+
} else if (!isLoadingUserPermissions) {
70+
// Authz is disabled, default all to true
71+
Object.keys(permissions).forEach((permissionKey: string) => {
72+
permissionResults[permissionKey] = true;
73+
});
74+
}
75+
76+
return {
77+
isLoading: isAuthzEnabled ? isLoadingUserPermissions : false,
78+
permissions: permissionResults,
79+
isAuthzEnabled,
80+
};
81+
};

src/authz/permissionHelpers.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { COURSE_PERMISSIONS } from './constants';
2+
3+
export const getGradingPermissions = (courseId: string) => ({
4+
canViewGradingSettings: {
5+
action: COURSE_PERMISSIONS.VIEW_GRADING_SETTINGS,
6+
scope: courseId,
7+
},
8+
canEditGradingSettings: {
9+
action: COURSE_PERMISSIONS.EDIT_GRADING_SETTINGS,
10+
scope: courseId,
11+
},
12+
});

src/grading-settings/GradingSettings.jsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import { Helmet } from 'react-helmet';
99
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
1010
import { STATEFUL_BUTTON_STATES } from '@src/constants';
1111
import { useCourseSettings } from '@src/data/apiHooks';
12+
import { useUserPermissionsWithAuthzCourse } from '@src/authz/hooks';
13+
import { getGradingPermissions } from '@src/authz/permissionHelpers';
1214
import ConnectionErrorAlert from '@src/generic/ConnectionErrorAlert';
15+
import PermissionDeniedAlert from '@src/generic/PermissionDeniedAlert';
1316
import SectionSubHeader from '@src/generic/section-sub-header';
1417
import SubHeader from '@src/generic/sub-header/SubHeader';
1518
import AlertMessage from '@src/generic/alert-message';
@@ -31,6 +34,12 @@ import messages from './messages';
3134
const GradingSettings = () => {
3235
const intl = useIntl();
3336
const { courseId, courseDetails } = useCourseAuthoringContext();
37+
38+
const {
39+
isLoading: isLoadingUserPermissions,
40+
permissions: userPermissions,
41+
} = useUserPermissionsWithAuthzCourse(courseId, getGradingPermissions(courseId));
42+
3443
const {
3544
data: gradingSettings,
3645
isLoading: isGradingSettingsLoading,
@@ -52,7 +61,7 @@ const GradingSettings = () => {
5261
const courseGradingDetails = gradingSettings?.courseDetails;
5362
const isLoadingDenied = isGradingSettingsError || isCourseSettingsError;
5463
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
55-
const isLoading = isCourseSettingsLoading || isGradingSettingsLoading;
64+
const isLoading = isCourseSettingsLoading || isGradingSettingsLoading || isLoadingUserPermissions;
5665
const [isQueryPending, setIsQueryPending] = useState(false);
5766
const [showOverrideInternetConnectionAlert, setOverrideInternetConnectionAlert] = useState(false);
5867
const [eligibleGrade, setEligibleGrade] = useState(null);
@@ -90,6 +99,10 @@ const GradingSettings = () => {
9099
}
91100
}, [savePending]);
92101

102+
if (!isLoadingUserPermissions && !userPermissions.canViewGradingSettings) {
103+
return <PermissionDeniedAlert />;
104+
}
105+
93106
if (isLoadingDenied) {
94107
return (
95108
<Container size="xl" className="course-unit px-4 mt-4">
@@ -102,6 +115,8 @@ const GradingSettings = () => {
102115
return null;
103116
}
104117

118+
const isEditable = !isLoadingUserPermissions && userPermissions.canEditGradingSettings;
119+
105120
const handleQueryProcessing = () => {
106121
setShowSuccessAlert(false);
107122
updateGradingSettings(gradingData);
@@ -174,6 +189,7 @@ const GradingSettings = () => {
174189
setOverrideInternetConnectionAlert={setOverrideInternetConnectionAlert}
175190
setEligibleGrade={setEligibleGrade}
176191
defaultGradeDesignations={gradingSettings?.defaultGradeDesignations}
192+
isEditable={isEditable}
177193
/>
178194
</section>
179195
{courseSettingsData.creditEligibilityEnabled && courseSettingsData.isCreditCourse && (
@@ -188,6 +204,7 @@ const GradingSettings = () => {
188204
minimumGradeCredit={minimumGradeCredit}
189205
setGradingData={setGradingData}
190206
setShowSuccessAlert={setShowSuccessAlert}
207+
isEditable={isEditable}
191208
/>
192209
</section>
193210
)}
@@ -201,6 +218,7 @@ const GradingSettings = () => {
201218
gracePeriod={gracePeriod}
202219
setGradingData={setGradingData}
203220
setShowSuccessAlert={setShowSuccessAlert}
221+
isEditable={isEditable}
204222
/>
205223
</section>
206224
<section>
@@ -219,11 +237,13 @@ const GradingSettings = () => {
219237
setGradingData={setGradingData}
220238
courseAssignmentLists={courseAssignmentLists}
221239
setShowSuccessAlert={setShowSuccessAlert}
240+
isEditable={isEditable}
222241
/>
223242
<Button
224243
variant="primary"
225244
iconBefore={IconAdd}
226245
onClick={handleAddAssignment}
246+
disabled={!isEditable}
227247
>
228248
{intl.formatMessage(messages.addNewAssignmentTypeBtn)}
229249
</Button>
@@ -267,6 +287,7 @@ const GradingSettings = () => {
267287
key="statefulBtn"
268288
onClick={handleSendGradingSettingsData}
269289
state={isQueryPending ? STATEFUL_BUTTON_STATES.pending : STATEFUL_BUTTON_STATES.default}
290+
disabled={!isEditable}
270291
{...updateValuesButtonState}
271292
/>,
272293
].filter(Boolean)}

src/grading-settings/assignment-section/assignments/AssignmentItem.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const AssignmentItem = ({
2020
secondErrorMsg,
2121
gradeField,
2222
trailingElement,
23+
disabled,
2324
}) => (
2425
<li className={className}>
2526
<Form.Group className={classNames('form-group-custom', {
@@ -37,6 +38,7 @@ const AssignmentItem = ({
3738
value={value}
3839
isInvalid={errorEffort}
3940
trailingElement={trailingElement}
41+
disabled={disabled}
4042
/>
4143
<Form.Control.Feedback className="grading-description">
4244
{descriptions}
@@ -64,6 +66,7 @@ AssignmentItem.defaultProps = {
6466
errorEffort: false,
6567
gradeField: undefined,
6668
trailingElement: undefined,
69+
disabled: false,
6770
};
6871

6972
AssignmentItem.propTypes = {
@@ -81,6 +84,7 @@ AssignmentItem.propTypes = {
8184
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
8285
gradeField: PropTypes.shape(defaultAssignmentsPropTypes),
8386
trailingElement: PropTypes.string,
87+
disabled: PropTypes.bool,
8488
};
8589

8690
export default AssignmentItem;

src/grading-settings/assignment-section/assignments/AssignmentTypeName.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const AssignmentTypeName = ({
1111
value,
1212
errorEffort,
1313
onChange,
14+
disabled,
1415
}) => {
1516
const intl = useIntl();
1617
const initialAssignmentName = useRef(value);
@@ -31,6 +32,7 @@ const AssignmentTypeName = ({
3132
onChange={onChange}
3233
value={value}
3334
isInvalid={Boolean(errorEffort)}
35+
disabled={disabled}
3436
/>
3537
<Form.Control.Feedback className="grading-description">
3638
{intl.formatMessage(messages.assignmentTypeNameDescription)}
@@ -60,12 +62,14 @@ const AssignmentTypeName = ({
6062

6163
AssignmentTypeName.defaultProps = {
6264
errorEffort: false,
65+
disabled: false,
6366
};
6467

6568
AssignmentTypeName.propTypes = {
6669
onChange: PropTypes.func.isRequired,
6770
errorEffort: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
6871
value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).isRequired,
72+
disabled: PropTypes.bool,
6973
};
7074

7175
export default AssignmentTypeName;

src/grading-settings/assignment-section/index.jsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const AssignmentSection = ({
2222
setGradingData,
2323
courseAssignmentLists,
2424
setShowSuccessAlert,
25+
isEditable,
2526
}) => {
2627
const intl = useIntl();
2728
const [errorList, setErrorList] = useState({});
@@ -84,6 +85,7 @@ const AssignmentSection = ({
8485
value={gradeField.type}
8586
errorEffort={errorList[`${type}-${gradeField.id}`]}
8687
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
88+
disabled={!isEditable}
8789
/>
8890
<AssignmentItem
8991
className="course-grading-assignment-abbreviation"
@@ -93,6 +95,7 @@ const AssignmentSection = ({
9395
name="shortLabel"
9496
value={gradeField.shortLabel}
9597
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
98+
disabled={!isEditable}
9699
/>
97100
<AssignmentItem
98101
className="course-grading-assignment-total-grade"
@@ -107,6 +110,7 @@ const AssignmentSection = ({
107110
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
108111
errorEffort={errorList[`${weight}-${gradeField.id}`]}
109112
trailingElement="%"
113+
disabled={!isEditable}
110114
/>
111115
<AssignmentItem
112116
className="course-grading-assignment-total-number"
@@ -119,6 +123,7 @@ const AssignmentSection = ({
119123
value={gradeField.minCount}
120124
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
121125
errorEffort={errorList[`${minCount}-${gradeField.id}`]}
126+
disabled={!isEditable}
122127
/>
123128
<AssignmentItem
124129
className="course-grading-assignment-number-droppable"
@@ -135,6 +140,7 @@ const AssignmentSection = ({
135140
type: gradeField.type,
136141
})}
137142
errorEffort={errorList[`${dropCount}-${gradeField.id}`]}
143+
disabled={!isEditable}
138144
/>
139145
</ol>
140146
{showDefinedCaseAlert && (
@@ -186,6 +192,7 @@ const AssignmentSection = ({
186192
variant="outline-primary"
187193
size="sm"
188194
onClick={() => handleRemoveAssignment(gradeField.id)}
195+
disabled={!isEditable}
189196
>
190197
{intl.formatMessage(messages.assignmentDeleteButton)}
191198
</Button>
@@ -199,6 +206,7 @@ const AssignmentSection = ({
199206
AssignmentSection.defaultProps = {
200207
courseAssignmentLists: undefined,
201208
graders: undefined,
209+
isEditable: true,
202210
};
203211

204212
AssignmentSection.propTypes = {
@@ -210,6 +218,7 @@ AssignmentSection.propTypes = {
210218
graders: PropTypes.arrayOf(
211219
PropTypes.shape(defaultAssignmentsPropTypes),
212220
),
221+
isEditable: PropTypes.bool,
213222
};
214223

215224
export default AssignmentSection;

src/grading-settings/credit-section/index.jsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const CreditSection = ({
1212
minimumGradeCredit,
1313
setGradingData,
1414
setShowSuccessAlert,
15+
isEditable,
1516
}) => {
1617
const intl = useIntl();
1718
const [errorEffort, setErrorEffort] = useState(false);
@@ -51,6 +52,7 @@ const CreditSection = ({
5152
value={Math.round(parseFloat(minimumGradeCredit) * 100) || ''}
5253
name="minimum_grade_credit"
5354
onChange={handleCreditChange}
55+
disabled={!isEditable}
5456
/>
5557
<Form.Control.Feedback className="grading-description">
5658
{intl.formatMessage(messages.creditEligibilityDescription)}
@@ -64,12 +66,17 @@ const CreditSection = ({
6466
);
6567
};
6668

69+
CreditSection.defaultProps = {
70+
isEditable: true,
71+
};
72+
6773
CreditSection.propTypes = {
6874
eligibleGrade: PropTypes.number.isRequired,
6975
setShowSavePrompt: PropTypes.func.isRequired,
7076
setGradingData: PropTypes.func.isRequired,
7177
setShowSuccessAlert: PropTypes.func.isRequired,
7278
minimumGradeCredit: PropTypes.number.isRequired,
79+
isEditable: PropTypes.bool,
7380
};
7481

7582
export default CreditSection;

0 commit comments

Comments
 (0)