= ({
lmsLinkForAboutPage,
courseDisplayName,
platformName,
+ isEditable,
}}
>
diff --git a/src/schedule-and-details/ScheduleAndDetails.test.tsx b/src/schedule-and-details/ScheduleAndDetails.test.tsx
index 35287ccbff..fc0acffc95 100644
--- a/src/schedule-and-details/ScheduleAndDetails.test.tsx
+++ b/src/schedule-and-details/ScheduleAndDetails.test.tsx
@@ -215,6 +215,26 @@ describe(' permissions', () => {
dateInputs.forEach((input) => expect(input).toBeDisabled());
});
+ it('shows the schedule section alert, not the page alert, when only edit_schedule is missing', async () => {
+ mockWaffleFlags({ enableAuthzCourseAuthoring: true });
+ mockPermissions({ canEditSchedule: false });
+ renderComponent();
+ expect(
+ await screen.findByText(scheduleMessages.scheduleReadOnlyAlert.defaultMessage),
+ ).toBeInTheDocument();
+ expect(screen.queryByText(
+ 'You have view-only access to this page. Contact your organization admin to request editing permissions.',
+ )).not.toBeInTheDocument();
+ });
+
+ it('shows no read-only alert when only edit_details is missing', async () => {
+ mockWaffleFlags({ enableAuthzCourseAuthoring: true });
+ mockPermissions({ canEditDetails: false });
+ renderComponent();
+ expect((await screen.findAllByText(messages.headingTitle.defaultMessage)).length).toBeGreaterThan(0);
+ expect(screen.queryByTestId('viewOnlyPermissionsAlert')).not.toBeInTheDocument();
+ });
+
it('disables pacing and details inputs when user lacks edit_details permission', async () => {
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
mockPermissions({ canEditDetails: false });
@@ -234,4 +254,18 @@ describe(' permissions', () => {
// No changes can be made so the save button never appears
expect(screen.queryByText(messages.buttonSaveText.defaultMessage)).not.toBeInTheDocument();
});
+
+ it('shows the page-level view-only alert when user has no edit permissions', async () => {
+ mockWaffleFlags({ enableAuthzCourseAuthoring: true });
+ mockPermissions({ canEditSchedule: false, canEditDetails: false });
+ renderComponent();
+ expect(await screen.findByTestId('viewOnlyPermissionsAlert')).toBeInTheDocument();
+ expect(screen.getByText(
+ 'You have view-only access to this page. Contact your organization admin to request editing permissions.',
+ )).toBeInTheDocument();
+ // The page-level alert stands in for the section-level one
+ expect(
+ screen.queryByText(scheduleMessages.scheduleReadOnlyAlert.defaultMessage),
+ ).not.toBeInTheDocument();
+ });
});
diff --git a/src/schedule-and-details/basic-section/CoursePromotionCard.jsx b/src/schedule-and-details/basic-section/CoursePromotionCard.jsx
index 4409f2d1e1..f90847f93f 100644
--- a/src/schedule-and-details/basic-section/CoursePromotionCard.jsx
+++ b/src/schedule-and-details/basic-section/CoursePromotionCard.jsx
@@ -9,10 +9,14 @@ import {
} from '@openedx/paragon';
import { Email as EmailIcon } from '@openedx/paragon/icons';
-import { INVITE_STUDENTS_LINK_ID } from './constants';
import messages from './messages';
-const CoursePromotionCard = ({ lmsLinkForAboutPage, courseDisplayName, platformName }) => {
+const CoursePromotionCard = ({
+ lmsLinkForAboutPage,
+ courseDisplayName,
+ platformName,
+ isEditable = true,
+}) => {
const intl = useIntl();
const emailSubject = intl.formatMessage(
@@ -42,6 +46,17 @@ const CoursePromotionCard = ({ lmsLinkForAboutPage, courseDisplayName, platformN
/>
);
+ const inviteButton = (
+
+ );
+
return (
-
-
-
+ {isEditable ?
+ (
+
+ {inviteButton}
+
+ ) :
+ inviteButton}
);
@@ -80,6 +96,7 @@ CoursePromotionCard.propTypes = {
lmsLinkForAboutPage: PropTypes.string.isRequired,
courseDisplayName: PropTypes.string.isRequired,
platformName: PropTypes.string.isRequired,
+ isEditable: PropTypes.bool,
};
export default CoursePromotionCard;
diff --git a/src/schedule-and-details/basic-section/CoursePromotionCard.test.jsx b/src/schedule-and-details/basic-section/CoursePromotionCard.test.jsx
index 89f4050ea9..ed032ff33a 100644
--- a/src/schedule-and-details/basic-section/CoursePromotionCard.test.jsx
+++ b/src/schedule-and-details/basic-section/CoursePromotionCard.test.jsx
@@ -2,7 +2,6 @@ import React from 'react';
import { render } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
-import { INVITE_STUDENTS_LINK_ID } from './constants';
import messages from './messages';
import CoursePromotionCard from './CoursePromotionCard';
@@ -34,10 +33,26 @@ describe('', () => {
});
it('generates correct invite mailto link', () => {
- const { getByTestId } = render();
- const inviteLink = getByTestId(INVITE_STUDENTS_LINK_ID);
+ const { getByRole } = render();
+ const inviteLink = getByRole('link', {
+ name: messages.basicPromotionButton.defaultMessage,
+ });
expect(decodeURIComponent(inviteLink.href)).toEqual(
`mailto:${process.env.INVITE_STUDENTS_EMAIL_TO}?body=The course ${props.courseDisplayName}, provided by ${props.platformName}, is open for enrollment. Please navigate to this course at ${props.lmsLinkForAboutPage} to enroll.&subject=Enroll in ${props.courseDisplayName}.`,
);
});
+
+ it('disables the invite link when not editable', () => {
+ const { getByRole, queryByRole } = render(
+ ,
+ );
+ const inviteButton = getByRole('button', {
+ name: messages.basicPromotionButton.defaultMessage,
+ });
+
+ expect(inviteButton).toBeDisabled();
+ expect(
+ queryByRole('link', { name: messages.basicPromotionButton.defaultMessage }),
+ ).not.toBeInTheDocument();
+ });
});
diff --git a/src/schedule-and-details/basic-section/constants.ts b/src/schedule-and-details/basic-section/constants.ts
deleted file mode 100644
index 232b9f4eb9..0000000000
--- a/src/schedule-and-details/basic-section/constants.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const INVITE_STUDENTS_LINK_ID = 'invite-students-link';
diff --git a/src/schedule-and-details/basic-section/index.jsx b/src/schedule-and-details/basic-section/index.jsx
index b2142f4691..46d49e8a64 100644
--- a/src/schedule-and-details/basic-section/index.jsx
+++ b/src/schedule-and-details/basic-section/index.jsx
@@ -17,6 +17,7 @@ const BasicSection = ({
lmsLinkForAboutPage,
courseDisplayName,
platformName,
+ isEditable,
}) => {
const intl = useIntl();
const [showPageBanner, setShowPageBanner] = useState(true);
@@ -53,6 +54,7 @@ const BasicSection = ({
lmsLinkForAboutPage={lmsLinkForAboutPage}
courseDisplayName={courseDisplayName}
platformName={platformName}
+ isEditable={isEditable}
>
{intl.formatMessage(messages.basicBannerTitle, { platformName })}
@@ -82,6 +84,7 @@ BasicSection.propTypes = {
lmsLinkForAboutPage: PropTypes.string.isRequired,
courseDisplayName: PropTypes.string.isRequired,
platformName: PropTypes.string.isRequired,
+ isEditable: PropTypes.bool,
};
export default BasicSection;
diff --git a/src/schedule-and-details/index.tsx b/src/schedule-and-details/index.tsx
index 7c1403c34f..f5b805c92d 100644
--- a/src/schedule-and-details/index.tsx
+++ b/src/schedule-and-details/index.tsx
@@ -22,6 +22,7 @@ import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
import { useCourseUserPermissions } from '@src/authz/hooks';
import { getScheduleAndDetailsPermissions } from '@src/authz/permissionHelpers';
import PermissionDeniedAlert from '@src/generic/PermissionDeniedAlert';
+import ViewOnlyPermissionsAlert from '@src/generic/ViewOnlyPermissionsAlert';
import BasicSection from './basic-section';
import CreditSection from './credit-section';
@@ -243,6 +244,7 @@ const ScheduleAndDetails = () => {
{intl.formatMessage(messages.headingTitle)}
+ {!canEdit && }
{
lmsLinkForAboutPage={lmsLinkForAboutPage}
courseDisplayName={courseDisplayName}
platformName={platformName}
+ isEditable={canEdit}
/>
{showCreditSection && (
{
certificatesDisplayBehavior={certificatesDisplayBehavior}
canShowCertificateAvailableDateField={canShowCertificateAvailableDateField}
isEditable={canEditSchedule}
+ showReadOnlyAlert={!canEditSchedule && canEditDetails}
onChange={handleValuesChange}
/>
{aboutPageEditable && (
diff --git a/src/schedule-and-details/schedule-section/index.jsx b/src/schedule-and-details/schedule-section/index.jsx
index f6cf4b7b5c..f5eb0a4558 100644
--- a/src/schedule-and-details/schedule-section/index.jsx
+++ b/src/schedule-and-details/schedule-section/index.jsx
@@ -2,6 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
+import ViewOnlyPermissionsAlert from '@src/generic/ViewOnlyPermissionsAlert';
+
import SectionSubHeader from '../../generic/section-sub-header';
import { ScheduleRow, SCHEDULE_ROW_TYPES } from './schedule-row';
import { CertificateDisplayRow } from './certificate-display-row';
@@ -20,6 +22,7 @@ const ScheduleSection = ({
certificatesDisplayBehavior,
canShowCertificateAvailableDateField,
isEditable = true,
+ showReadOnlyAlert = false,
onChange,
}) => {
const intl = useIntl();
@@ -119,6 +122,11 @@ const ScheduleSection = ({
title={intl.formatMessage(messages.scheduleTitle)}
description={intl.formatMessage(messages.scheduleDescription)}
/>
+ {showReadOnlyAlert && (
+
+ {intl.formatMessage(messages.scheduleReadOnlyAlert)}
+
+ )}
{propsForScheduleFields
.filter((field) => !field.skip)
@@ -170,6 +178,7 @@ ScheduleSection.propTypes = {
certificateAvailableDate: PropTypes.string,
certificatesDisplayBehavior: PropTypes.string.isRequired,
canShowCertificateAvailableDateField: PropTypes.bool.isRequired,
+ showReadOnlyAlert: PropTypes.bool,
onChange: PropTypes.func.isRequired,
};
diff --git a/src/schedule-and-details/schedule-section/messages.ts b/src/schedule-and-details/schedule-section/messages.ts
index aa3da3da71..1c508d2b9c 100644
--- a/src/schedule-and-details/schedule-section/messages.ts
+++ b/src/schedule-and-details/schedule-section/messages.ts
@@ -73,6 +73,11 @@ const messages = defineMessages({
id: 'course-authoring.schedule.schedule-section.upgrade-deadline.time.label',
defaultMessage: 'Upgrade deadline time',
},
+ scheduleReadOnlyAlert: {
+ id: 'course-authoring.schedule.schedule-section.read-only-alert',
+ defaultMessage: 'You don\'t have permission to edit the "Course Schedule". Contact your organization admin to request access.',
+ description: 'Alert shown inside the Course Schedule section when the user cannot edit schedule fields',
+ },
});
export default messages;