Skip to content

Commit fcce1df

Browse files
committed
feat: implement grading permissions and authorization hooks
1 parent b2a4b9c commit fcce1df

12 files changed

Lines changed: 165 additions & 6 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
@@ -12,7 +12,10 @@ import { Helmet } from 'react-helmet';
1212
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
1313
import { STATEFUL_BUTTON_STATES } from '@src/constants';
1414
import { useCourseSettings } from '@src/data/apiHooks';
15+
import { useUserPermissionsWithAuthzCourse } from '@src/authz/hooks';
16+
import { getGradingPermissions } from '@src/authz/permissionHelpers';
1517
import ConnectionErrorAlert from '@src/generic/ConnectionErrorAlert';
18+
import PermissionDeniedAlert from '@src/generic/PermissionDeniedAlert';
1619
import SectionSubHeader from '@src/generic/section-sub-header';
1720
import SubHeader from '@src/generic/sub-header/SubHeader';
1821
import AlertMessage from '@src/generic/alert-message';
@@ -34,6 +37,12 @@ import messages from './messages';
3437
const GradingSettings = () => {
3538
const intl = useIntl();
3639
const { courseId, courseDetails } = useCourseAuthoringContext();
40+
41+
const {
42+
isLoading: isLoadingUserPermissions,
43+
permissions: userPermissions,
44+
} = useUserPermissionsWithAuthzCourse(courseId, getGradingPermissions(courseId));
45+
3746
const {
3847
data: gradingSettings,
3948
isLoading: isGradingSettingsLoading,
@@ -55,7 +64,7 @@ const GradingSettings = () => {
5564
const courseGradingDetails = gradingSettings?.courseDetails;
5665
const isLoadingDenied = isGradingSettingsError || isCourseSettingsError;
5766
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
58-
const isLoading = isCourseSettingsLoading || isGradingSettingsLoading;
67+
const isLoading = isCourseSettingsLoading || isGradingSettingsLoading || isLoadingUserPermissions;
5968
const [isQueryPending, setIsQueryPending] = useState(false);
6069
const [showOverrideInternetConnectionAlert, setOverrideInternetConnectionAlert] = useState(false);
6170
const [eligibleGrade, setEligibleGrade] = useState(null);
@@ -93,6 +102,10 @@ const GradingSettings = () => {
93102
}
94103
}, [savePending]);
95104

105+
if (!isLoadingUserPermissions && !userPermissions.canViewGradingSettings) {
106+
return <PermissionDeniedAlert />;
107+
}
108+
96109
if (isLoadingDenied) {
97110
return (
98111
<Container size="xl" className="course-unit px-4 mt-4">
@@ -105,6 +118,8 @@ const GradingSettings = () => {
105118
return null;
106119
}
107120

121+
const isEditable = !isLoadingUserPermissions && userPermissions.canEditGradingSettings;
122+
108123
const handleQueryProcessing = () => {
109124
setShowSuccessAlert(false);
110125
updateGradingSettings(gradingData);
@@ -177,6 +192,7 @@ const GradingSettings = () => {
177192
setOverrideInternetConnectionAlert={setOverrideInternetConnectionAlert}
178193
setEligibleGrade={setEligibleGrade}
179194
defaultGradeDesignations={gradingSettings?.defaultGradeDesignations}
195+
isEditable={isEditable}
180196
/>
181197
</section>
182198
{courseSettingsData.creditEligibilityEnabled && courseSettingsData.isCreditCourse && (
@@ -191,6 +207,7 @@ const GradingSettings = () => {
191207
minimumGradeCredit={minimumGradeCredit}
192208
setGradingData={setGradingData}
193209
setShowSuccessAlert={setShowSuccessAlert}
210+
isEditable={isEditable}
194211
/>
195212
</section>
196213
)}
@@ -204,6 +221,7 @@ const GradingSettings = () => {
204221
gracePeriod={gracePeriod}
205222
setGradingData={setGradingData}
206223
setShowSuccessAlert={setShowSuccessAlert}
224+
isEditable={isEditable}
207225
/>
208226
</section>
209227
<section>
@@ -222,11 +240,13 @@ const GradingSettings = () => {
222240
setGradingData={setGradingData}
223241
courseAssignmentLists={courseAssignmentLists}
224242
setShowSuccessAlert={setShowSuccessAlert}
243+
isEditable={isEditable}
225244
/>
226245
<Button
227246
variant="primary"
228247
iconBefore={IconAdd}
229248
onClick={handleAddAssignment}
249+
disabled={!isEditable}
230250
>
231251
{intl.formatMessage(messages.addNewAssignmentTypeBtn)}
232252
</Button>
@@ -270,6 +290,7 @@ const GradingSettings = () => {
270290
key="statefulBtn"
271291
onClick={handleSendGradingSettingsData}
272292
state={isQueryPending ? STATEFUL_BUTTON_STATES.pending : STATEFUL_BUTTON_STATES.default}
293+
disabled={!isEditable}
273294
{...updateValuesButtonState}
274295
/>,
275296
].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
@@ -38,6 +39,7 @@ const AssignmentItem = ({
3839
value={value}
3940
isInvalid={errorEffort}
4041
trailingElement={trailingElement}
42+
disabled={disabled}
4143
/>
4244
<Form.Control.Feedback className="grading-description">
4345
{descriptions}
@@ -65,6 +67,7 @@ AssignmentItem.defaultProps = {
6567
errorEffort: false,
6668
gradeField: undefined,
6769
trailingElement: undefined,
70+
disabled: false,
6871
};
6972

7073
AssignmentItem.propTypes = {
@@ -82,6 +85,7 @@ AssignmentItem.propTypes = {
8285
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
8386
gradeField: PropTypes.shape(defaultAssignmentsPropTypes),
8487
trailingElement: PropTypes.string,
88+
disabled: PropTypes.bool,
8589
};
8690

8791
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);
@@ -32,6 +33,7 @@ const AssignmentTypeName = ({
3233
onChange={onChange}
3334
value={value}
3435
isInvalid={Boolean(errorEffort)}
36+
disabled={disabled}
3537
/>
3638
<Form.Control.Feedback className="grading-description">
3739
{intl.formatMessage(messages.assignmentTypeNameDescription)}
@@ -61,12 +63,14 @@ const AssignmentTypeName = ({
6163

6264
AssignmentTypeName.defaultProps = {
6365
errorEffort: false,
66+
disabled: false,
6467
};
6568

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

7276
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({});
@@ -87,6 +88,7 @@ const AssignmentSection = ({
8788
value={gradeField.type}
8889
errorEffort={errorList[`${type}-${gradeField.id}`]}
8990
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
91+
disabled={!isEditable}
9092
/>
9193
<AssignmentItem
9294
className="course-grading-assignment-abbreviation"
@@ -96,6 +98,7 @@ const AssignmentSection = ({
9698
name="shortLabel"
9799
value={gradeField.shortLabel}
98100
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
101+
disabled={!isEditable}
99102
/>
100103
<AssignmentItem
101104
className="course-grading-assignment-total-grade"
@@ -110,6 +113,7 @@ const AssignmentSection = ({
110113
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
111114
errorEffort={errorList[`${weight}-${gradeField.id}`]}
112115
trailingElement="%"
116+
disabled={!isEditable}
113117
/>
114118
<AssignmentItem
115119
className="course-grading-assignment-total-number"
@@ -122,6 +126,7 @@ const AssignmentSection = ({
122126
value={gradeField.minCount}
123127
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
124128
errorEffort={errorList[`${minCount}-${gradeField.id}`]}
129+
disabled={!isEditable}
125130
/>
126131
<AssignmentItem
127132
className="course-grading-assignment-number-droppable"
@@ -138,6 +143,7 @@ const AssignmentSection = ({
138143
type: gradeField.type,
139144
})}
140145
errorEffort={errorList[`${dropCount}-${gradeField.id}`]}
146+
disabled={!isEditable}
141147
/>
142148
</ol>
143149
{showDefinedCaseAlert && (
@@ -187,6 +193,7 @@ const AssignmentSection = ({
187193
variant="outline-primary"
188194
size="sm"
189195
onClick={() => handleRemoveAssignment(gradeField.id)}
196+
disabled={!isEditable}
190197
>
191198
{intl.formatMessage(messages.assignmentDeleteButton)}
192199
</Button>
@@ -200,6 +207,7 @@ const AssignmentSection = ({
200207
AssignmentSection.defaultProps = {
201208
courseAssignmentLists: undefined,
202209
graders: undefined,
210+
isEditable: true,
203211
};
204212

205213
AssignmentSection.propTypes = {
@@ -211,6 +219,7 @@ AssignmentSection.propTypes = {
211219
graders: PropTypes.arrayOf(
212220
PropTypes.shape(defaultAssignmentsPropTypes),
213221
),
222+
isEditable: PropTypes.bool,
214223
};
215224

216225
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)