Skip to content

Task - advanced and certificates #316

Closed
openedx/frontend-app-authoring
#3206

Description

@MaferMazu
No description provided.

Activity

  1. bra-i-am commented on Jun 5, 2026

    @bra-i-am

    Refinement

    Caution

    The Authoring Roles and Permissions spec only grants manage_advanced_settings and manage_certificates to Course Staff and Course Admin. No view-only variant exists for either permission. As a result, Course Auditor and Course Editor receive a 403 from both APIs and cannot access the data at all — the pages fall back to <Placeholder /> ("Under Construction / Coming Soon"). The frontend designs require a view-only permission to be defined and shipped in the backend first; without it, the frontend changes below cannot be implemented.

    Here are previous backend PRs that attempted to add view_advanced_settings. This requires broader discussion:

    cc. @MaferMazu


    Role matrix:

    • Course Auditor (manage advanced settings ❌, manage certificates ❌)
    • Course Editor (manage advanced settings ❌, manage certificates ❌).

    Advanced Settings

    • Permission used:

      • courses.manage_advanced_settings

      Already in src/authz/constants.ts as COURSE_PERMISSIONS.MANAGE_ADVANCED_SETTINGS and in src/authz/permissionHelpers.ts as getAdvancedSettingsPermissions(courseId).

      Already resolved in advanced-settings/AdvancedSettings.tsx via useUserPermissions as userPermissions?.canManageAdvancedSettings. The page currently returns <PermissionDeniedAlert /> for users without permission; the new design shows the page in view-only mode: a banner below SubHeader and all textarea inputs set to readOnly.

    Certificates

    • Permission used: courses.manage_certificates

      Not yet in src/authz/constants.ts or src/authz/permissionHelpers.ts; both must be added as a prerequisite. The page currently shows <Placeholder /> on 403 (loadingStatus === RequestStatus.DENIED). The new design shows the page with a view-only banner and all management actions hidden (Add, edit, delete, mode dropdown, activate/deactivate); only "Preview certificate" remains.


    UI location 1 — Advanced Settings page: view-only banner for Course Auditor and Course Editor

    Files: advanced-settings/messages.ts → advanced-settings/AdvancedSettings.tsx

    advanced-settings/messages.ts — add view-only alert message:

      viewOnlyAlert: {
        id: 'course-authoring.advanced-settings.alert.view-only',
        defaultMessage: 'You have view-only access to this page. Contact your organization admin to request editing permissions.',
        description: 'Alert shown to users who can view but cannot manage advanced settings',
      },

    advanced-settings/AdvancedSettings.tsx — add Alert to paragon imports, add Lock icon, replace the PermissionDeniedAlert early-return with a canManageAdvancedSettings bool, show banner, and pass readOnly to each SettingCard:

    // paragon imports — add Alert
    import {
      Container,
      Alert,
      Button,
      Layout,
      StatefulButton,
      TransitionReplace,
    } from '@openedx/paragon';
    
    // icons imports — add Lock
    import { CheckCircle, Info, Lock as LockIcon, Warning } from '@openedx/paragon/icons';
    
    // remove: import PermissionDeniedAlert from 'CourseAuthoring/generic/PermissionDeniedAlert';
    
    ...
    
    // replace the authzIsEnabledAndNoPermission block (lines ~152–158) with:
    const canManageAdvancedSettings = !isAuthzEnabled || !!userPermissions?.canManageAdvancedSettings;
    
    ...
    
    // in JSX — after <SubHeader .../>, before <article>:
                  <SubHeader
                    subtitle={intl.formatMessage(messages.headingSubtitle)}
                    title={intl.formatMessage(messages.headingTitle)}
                    contentTitle={intl.formatMessage(messages.policy)}
                  />
                  {!canManageAdvancedSettings && (
                    <Alert variant="info" icon={LockIcon}>
                      {intl.formatMessage(messages.viewOnlyAlert)}
                    </Alert>
                  )}
                  <article>
    
    ...
    
    // pass disabled to SettingCard:
                              <SettingCard
                                key={settingName}
                                settingData={settingData}
                                name={settingName}
                                showSaveSettingsPrompt={showSaveSettingsPrompt}
                                saveSettingsPrompt={saveSettingsPrompt}
                                setEdited={setEditedSettings}
                                handleBlur={handleSettingBlur}
                                isEditableState={isEditableState}
                                setIsEditableState={setIsEditableState}
                                disabled={!canManageAdvancedSettings}
                              />

    UI location 2 — Advanced Settings page: disabled textarea inputs for Course Auditor and Course Editor

    File: advanced-settings/setting-card/SettingCard.tsx

    advanced-settings/setting-card/SettingCard.tsx — accept disabled prop and apply it to Form.Control (uses Paragon's disabled visual style — grayed out, non-interactive):

    const SettingCard = ({
      ...
      disabled = false,
    }) => {
      ...
                  <Form.Control
                    as={TextareaAutosize}
                    value={isEditableState ? newValue : initialValue}
                    name={name}
                    onChange={handleSettingChange}
                    aria-label={displayName}
                    onBlur={handleCardBlur}
                    disabled={disabled}
                  />
      ...
    // propTypes — add:
      disabled: PropTypes.bool,

    Prerequisite for Certificates — src/authz/constants.ts and src/authz/permissionHelpers.ts

    // constants.ts — add:
      MANAGE_CERTIFICATES: 'courses.manage_certificates',
    // permissionHelpers.ts — add:
    export const getCertificatesPermissions = (courseId: string) => ({
      canManageCertificates: {
        action: COURSE_PERMISSIONS.MANAGE_CERTIFICATES,
        scope: courseId,
      },
    });

    UI location 3 — Certificates page: view-only banner for Course Auditor and Course Editor

    Files: certificates/messages.ts → certificates/Certificates.tsx → certificates/layout/MainLayout.jsx

    certificates/messages.ts — add view-only alert message:

      viewOnlyAlert: {
        id: 'course-authoring.certificates.alert.view-only',
        defaultMessage: 'You have view-only access to this page. Contact your organization admin to request editing permissions.',
        description: 'Alert shown to users who can view but cannot manage certificates',
      },

    certificates/Certificates.tsx — resolve canManageCertificates via hook, thread it to MainLayout and the active mode component:

    import { useCourseUserPermissions } from '@src/authz/data/apiHooks';
    import { getCertificatesPermissions } from '@src/authz/permissionHelpers';
    
    const Certificates = () => {
      const { courseId } = useCourseAuthoringContext();
      const { canManageCertificates } = useCourseUserPermissions(courseId, getCertificatesPermissions(courseId));
      const { certificates, componentMode, isLoading, loadingStatus, pageHeadTitle, hasCertificateModes } = useCertificates({ courseId });
      ...
      return (
        <>
          <Helmet>...</Helmet>
          <MainLayout courseId={courseId} showHeaderButtons={hasCertificateModes && certificates?.length > 0} canManageCertificates={canManageCertificates}>
            <ModeComponent courseId={courseId} canManageCertificates={canManageCertificates} />
          </MainLayout>
        </>
      );
    };

    certificates/layout/MainLayout.jsx — accept canManageCertificates, show banner below SubHeader, forward to HeaderButtons:

    import { Alert, Container, Layout } from '@openedx/paragon';
    import { Lock as LockIcon } from '@openedx/paragon/icons';
    import { useIntl } from '@edx/frontend-platform/i18n';
    import messages from '../../messages';
    
    const MainLayout = ({ courseId, showHeaderButtons, canManageCertificates, children }) => {
      const intl = useIntl();
      ...
      return (
        <>
          <Container size="xl" className="certificates px-4">
            <div className="mt-5" />
            <SubHeader
              hideBorder
              title={intl.formatMessage(messages.headingTitle)}
              subtitle={intl.formatMessage(messages.headingSubtitle)}
              headerActions={showHeaderButtons && <HeaderButtons canManageCertificates={canManageCertificates} />}
            />
            {!canManageCertificates && (
              <Alert variant="info" icon={LockIcon}>
                {intl.formatMessage(messages.viewOnlyAlert)}
              </Alert>
            )}
            <section>
              ...
            </section>
          </Container>
          ...
        </>
      );
    };
    
    // defaultProps — add:
      canManageCertificates: true,
    
    // propTypes — add:
      canManageCertificates: PropTypes.bool,

    UI location 4 — Certificates page: disable mode dropdown and hide activate/deactivate button for Course Auditor and Course Editor

    File: certificates/layout/header-buttons/HeaderButtons.jsx

    certificates/layout/header-buttons/HeaderButtons.jsx — accept canManageCertificates; disable (not hide) the mode DropdownButton; hide the Activate/Deactivate Button; keep the Preview Hyperlink visible and functional:

    const HeaderButtons = ({ canManageCertificates = true }) => {
      ...
      return (
        <>
          <DropdownButton
            id="dropdown-basic-button"
            title={dropdowmItem}
            onSelect={(item) => setDropdowmItem(item)}
            disabled={!canManageCertificates}
          >
            {courseModes.map((mode) => <Dropdown.Item key={mode} eventKey={mode}>{mode}</Dropdown.Item>)}
          </DropdownButton>
          <Button variant="outline-primary" as={Hyperlink} destination={previewUrl} target="_blank" showLaunchIcon={false}>
            {intl.formatMessage(messages.headingActionsPreview)}
          </Button>
          {canManageCertificates && (
            <Button variant="outline-primary" onClick={handleActivationStatus}>
              {isCertificateActive
                ? intl.formatMessage(messages.headingActionsDeactivate)
                : intl.formatMessage(messages.headingActionsActivate)}
            </Button>
          )}
        </>
      );
    };
    
    // propTypes — add:
      canManageCertificates: PropTypes.bool,

    UI location 5 — Certificates page: replace empty-state message and hide Add button for Course Auditor and Course Editor

    Files: certificates/messages.ts → certificates/empty-certificates-with-modes/EmptyCertificatesWithModes.jsx

    certificates/messages.ts — add new view-only empty-state message:

      noCertificatesViewOnlyText: {
        id: 'course-authoring.certificates.no-certificates.view-only',
        defaultMessage: 'No certificates have been set up for this course yet.',
        description: 'Empty state message shown to view-only users on the certificates page',
      },

    certificates/empty-certificates-with-modes/EmptyCertificatesWithModes.jsx — accept canManageCertificates; swap message and hide the Add button when false:

    const EmptyCertificatesWithModes = ({ canManageCertificates = true }) => {
      ...
      return (
        <Card>
          <Card.Section>
            <ActionRow>
              <span className="small">
                {intl.formatMessage(
                  canManageCertificates ? messages.noCertificatesText : messages.noCertificatesViewOnlyText
                )}
              </span>
              <ActionRow.Spacer />
              {canManageCertificates && (
                <Button iconBefore={AddIcon} onClick={handleCreateMode}>
                  {intl.formatMessage(messages.setupCertificateBtn)}
                </Button>
              )}
            </ActionRow>
          </Card.Section>
        </Card>
      );
    };
    
    // propTypes — add:
      canManageCertificates: PropTypes.bool,

    UI location 6 — Certificates page: hide edit and delete icons per certificate for Course Auditor and Course Editor

    Files: certificates/certificates-list/CertificatesList.jsx → certificates/certificate-details/CertificateDetails.jsx

    certificates/certificates-list/CertificatesList.jsx — accept and forward canManageCertificates to CertificateDetails:

    const CertificatesList = ({ courseId, canManageCertificates = true }) => {
      ...
                        <CertificateDetails
                          detailsCourseTitle={courseTitle}
                          detailsCourseNumber={courseNumber}
                          courseNumberOverride={courseNumberOverride}
                          courseTitleOverride={certificate.courseTitle}
                          certificateId={certificate.id}
                          canManageCertificates={canManageCertificates}
                        />
    
    // propTypes — add:
      canManageCertificates: PropTypes.bool,

    certificates/certificate-details/CertificateDetails.jsx — accept canManageCertificates, pass null as actions when false:

    const CertificateDetails = ({
      certificateId,
      detailsCourseTitle,
      courseTitleOverride,
      detailsCourseNumber,
      courseNumberOverride,
      canManageCertificates = true,
    }) => {
      ...
      return (
        <CertificateSection
          title={intl.formatMessage(messages.detailsSectionTitle)}
          className="certificate-details"
          data-testid="certificate-details"
          actions={canManageCertificates ? (
            <Stack direction="horizontal" gap="2">
              <IconButtonWithTooltip
                src={EditOutlineIcon}
                iconAs={Icon}
                tooltipContent={<div>{intl.formatMessage(commonMessages.editTooltip)}</div>}
                alt={intl.formatMessage(commonMessages.editTooltip)}
                onClick={isCertificateActive ? editModalOpen : handleEditAll}
              />
              <IconButtonWithTooltip
                src={DeleteOutlineIcon}
                iconAs={Icon}
                tooltipContent={<div>{intl.formatMessage(commonMessages.deleteTooltip)}</div>}
                alt={intl.formatMessage(commonMessages.deleteTooltip)}
                onClick={confirmOpen}
              />
            </Stack>
          ) : null}
        >
    
    // defaultProps — add:
      canManageCertificates: true,
    
    // propTypes — add:
      canManageCertificates: PropTypes.bool,

    UI location 7 — Certificates page: hide edit icon on each signatory for Course Auditor and Course Editor

    Files: certificates/certificates-list/CertificatesList.jsx → certificates/certificate-signatories/CertificateSignatories.jsx → certificates/certificate-signatories/signatory/Signatory.jsx

    certificates/certificates-list/CertificatesList.jsx — forward canManageCertificates to CertificateSignatories (inside the existing FieldArray render prop):

                            <CertificateSignatories
                              signatories={values.signatories}
                              arrayHelpers={arrayHelpers}
                              editModes={editModes}
                              initialSignatoriesValues={initialValues[idx].signatories}
                              handleChange={handleChange}
                              handleBlur={handleBlur}
                              setFieldValue={setFieldValue}
                              setEditModes={setEditModes}
                              canManageCertificates={canManageCertificates}
                            />

    certificates/certificate-signatories/CertificateSignatories.jsx — accept and forward canManageCertificates to each Signatory:

    const CertificateSignatories = ({
      ...
      canManageCertificates = true,
    }) => {
      ...
                  (
                    <Signatory
                      key={id}
                      index={idx}
                      name={name}
                      title={title}
                      organization={organization}
                      signatureImagePath={signatureImagePath}
                      handleEdit={() => toggleEditSignatory(idx)}
                      canManageCertificates={canManageCertificates}
                    />
                  )
    
    // defaultProps — add:
      canManageCertificates: true,
    
    // propTypes — add:
      canManageCertificates: PropTypes.bool,

    certificates/certificate-signatories/signatory/Signatory.jsx — accept canManageCertificates, hide the edit IconButtonWithTooltip when false:

    const Signatory = ({
      index,
      name,
      title,
      organization,
      signatureImagePath,
      handleEdit,
      canManageCertificates = true,
    }) => {
      ...
          {canManageCertificates && (
            <IconButtonWithTooltip
              className="signatory__action-button"
              src={EditOutlineIcon}
              iconAs={Icon}
              alt={intl.formatMessage(commonMessages.editTooltip)}
              tooltipContent={<div>{intl.formatMessage(commonMessages.editTooltip)}</div>}
              onClick={handleEdit}
            />
          )}
    
    // propTypes — add:
      canManageCertificates: PropTypes.bool,
  2. MaferMazu commented on Jun 5, 2026

    @MaferMazu
    ContributorAuthor

    @bra-i-am thanks for bringing the caution message. I just opened #328 and #329, so this card will be blocked for those two. We need to improve the enforcement points and provide the frontend with enough information so you can properly configure the ui.

    Please feel free to enter those tickets and leave a message if you have a preference or an idea for how we can send that information to you more effectively.

  3. bra-i-am commented on Jun 5, 2026

    @bra-i-am

    @MaferMazu, thanks for your time reviewing this!

    I just opened #328 and #329, so this card will be blocked for those two

    right, thank you... I'm looking forward to the resolution so I can implement the changes.

    Please feel free to enter those tickets and leave a message if you have a preference or an idea for how we can send that information to you more effectively.

    I think that just following the current code structure is enough for both: they're blocked mainly because the request responds with 403, and that fixes the frontend in an unwanted view; I'll do some review and let you know in the tickets whatever I get

  4. moved this from Blocked to In Progress in RBAC AuthZ Boardon Aug 27, 2026
  5. moved this from In Progress to Ready for Review in RBAC AuthZ Boardon Sep 1, 2026
  6. moved this from Ready for Review to Done in RBAC AuthZ Boardon Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions