Skip to content

Commit

Permalink
Merge branch 'master' into mashal-m/react-upgrade-to-v17
Browse files Browse the repository at this point in the history
  • Loading branch information
Mashal-m committed Aug 2, 2023
2 parents 8941843 + d55c0f4 commit 44606ed
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 30 deletions.
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ MFE_CONFIG_API_URL=''
ADDITIONAL_METADATA_REQUIRED_FIELDS='{}'
IS_NEW_SLUG_FORMAT_ENABLED='false'
MARKETING_SITE_PREVIEW_URL_ROOT=''
COURSE_URL_SLUGS_PATTERN = '{}'
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ SITE_NAME='edX'
ADDITIONAL_METADATA_REQUIRED_FIELDS='{}'
IS_NEW_SLUG_FORMAT_ENABLED='false'
MARKETING_SITE_PREVIEW_URL_ROOT=''
COURSE_URL_SLUGS_PATTERN = `{}`;
31 changes: 31 additions & 0 deletions decisions/0006-program-slugs-in-publisher.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
6. Program Slugs in Publisher
-------------------------------

Status
------

Accepted (July 2023)

Context
-------

Ability to update the slugs for programs is needed to be accessible to a group of users.
These slugs are used in generating marketing url for each type of programs.
These program slugs will be programmatically generated initially but can be edited if needed, just like courses.
In order to grant the ability to update slugs for programs, we will going to display programs on Publisher due to its ease of access.
Instead of displaying all program information on the publisher, we will only show the editable slug field.

Decision
--------

A new tab will be added on Publisher named "Programs/Degrees" in the header.
Upon clicking the tab, the user will be shown a list of programs with minimal info.
Each item in the list will take the user to a page/modal where an input field will be available for slugs and a save button.
To access the program list and detail pages, every user must be granted the necessary permissions.
These permissions necessitate that a user must have access to Publisher and be part of a designated user group.


Consequences
------------

Programs slugs will be available on Publisher for the users having permissions while the program authoring stays on discovery django admin.
8 changes: 4 additions & 4 deletions src/components/EditCoursePage/EditCourseForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import Collapsible from '../Collapsible';
import PriceList from '../PriceList';

import {
PUBLISHED, REVIEWED, EXECUTIVE_EDUCATION_SLUG, COURSE_URL_SLUG_VALIDATION_MESSAGE, REVIEW_BY_INTERNAL,
PUBLISHED, REVIEWED, EXECUTIVE_EDUCATION_SLUG, REVIEW_BY_INTERNAL,
} from '../../data/constants';
import {
titleHelp, typeHelp, getUrlSlugHelp, productSourceHelp,
Expand Down Expand Up @@ -314,7 +314,7 @@ export class BaseEditCourseForm extends React.Component {

const IS_NEW_SLUG_FORMAT_ENABLED = Boolean(process.env.IS_NEW_SLUG_FORMAT_ENABLED === 'true');
// eslint-disable-next-line max-len
const COURSE_URL_SLUG_PATTERN = getCourseUrlSlugPattern(IS_NEW_SLUG_FORMAT_ENABLED, courseInfo.data.course_run_statuses, productSource?.slug);
const COURSE_URL_SLUG_PATTERN = getCourseUrlSlugPattern(IS_NEW_SLUG_FORMAT_ENABLED, productSource?.slug, courseInfo.data.course_type);
const urlSlugHelp = getUrlSlugHelp(process.env.IS_NEW_SLUG_FORMAT_ENABLED);

return (
Expand Down Expand Up @@ -361,14 +361,14 @@ export class BaseEditCourseForm extends React.Component {
onInvalid: (e) => {
this.openCollapsible();
e.target.setCustomValidity(
`Please enter a valid URL slug. ${COURSE_URL_SLUG_VALIDATION_MESSAGE[COURSE_URL_SLUG_PATTERN] || ''}`,
`Please enter a valid URL slug. ${COURSE_URL_SLUG_PATTERN.error_msg || ''}`,
);
},
onInput: (e) => {
e.target.setCustomValidity('');
},
}}
pattern={COURSE_URL_SLUG_PATTERN}
pattern={COURSE_URL_SLUG_PATTERN.slug_format}
disabled={disabled || !administrator}
optional
/>
Expand Down
2 changes: 1 addition & 1 deletion src/data/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const COURSE_URL_SLUG_PATTERN = `${COURSE_URL_SLUG_PATTERN_NEW}|${COURSE_URL_SLU
const COURSE_URL_SLUG_VALIDATION_MESSAGE = {
[COURSE_URL_SLUG_PATTERN_OLD]: 'Course URL slug contains lowercase letters, numbers, underscores, and dashes only.',
[COURSE_URL_SLUG_PATTERN_NEW]: 'Course URL slug contains lowercase letters, numbers, underscores, and dashes only and must be in the format learn/<primary_subject>/<org-slug>-<course_slug>',
[COURSE_URL_SLUG_PATTERN]: 'Course URL slug contains lowercase letters, numbers, underscores, and dashes only and must in the format <custom-url-slug> or learn/<primary_subject>/<org-slug>-<course_slug>.',
[COURSE_URL_SLUG_PATTERN]: 'Course URL slug contains lowercase letters, numbers, underscores, and dashes only and must be in the format <custom-url-slug> or learn/<primary_subject>/<org-slug>-<course_slug>.',
};

export {
Expand Down
19 changes: 19 additions & 0 deletions src/setupTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,25 @@ jest.mock('@edx/frontend-platform/auth');
getAuthenticatedHttpClient.mockReturnValue(axios);
getAuthenticatedUser.mockReturnValue({ administrator: false });

process.env.COURSE_URL_SLUGS_PATTERN = `{
"edx": {
"default": {
"slug_format": "^learn/[a-z0-9_]+(?:-?[a-z0-9_]+)*/[a-z0-9_]+(?:-?[a-z0-9_]+)*$|^[a-z0-9_]+(?:-[a-z0-9_]+)*$",
"error_msg": "Course URL slug contains lowercase letters, numbers, underscores, and dashes only and must be in the format <custom-url-slug> or learn/<primary_subject>/<org-slug>-<course_slug>."
}
},
"external-source": {
"default": {
"slug_format": "^[a-z0-9_]+(?:-[a-z0-9_]+)*$",
"error_msg": "Course URL slug contains lowercase letters, numbers, underscores, and dashes only."
},
"executive-education-2u": {
"slug_format": "^executive-education/[a-z0-9_]+(?:-?[a-z0-9_]+)*$|^[a-z0-9_]+(?:-[a-z0-9_]+)*$",
"error_msg": "Course URL slug contains lowercase letters, numbers, underscores, and dashes only and must be in the format <custom-url-slug> or executive-education/<org-slug>-<course_slug>."
}
}
}`;

// We need this here because tinymce uses a method(s) which JSDOM has not
// implemented yet. To fix this, the following mocks matchMedia so that all tests
// execute properly. More info here:
Expand Down
32 changes: 20 additions & 12 deletions src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ import qs from 'query-string';

import history from '../data/history';
import {
COURSE_EXEMPT_FIELDS, COURSE_RUN_NON_EXEMPT_FIELDS, COURSE_URL_SLUG_PATTERN,
COURSE_URL_SLUG_PATTERN_OLD, MASTERS_TRACK, POST_REVIEW_STATUSES, IN_REVIEW_STATUS,
COURSE_EXEMPT_FIELDS, COURSE_RUN_NON_EXEMPT_FIELDS, COURSE_URL_SLUG_PATTERN_OLD,
MASTERS_TRACK, COURSE_URL_SLUG_VALIDATION_MESSAGE,
} from '../data/constants';
import DiscoveryDataApiService from '../data/services/DiscoveryDataApiService';
import { PAGE_SIZE } from '../data/constants/table';
import { DEFAULT_PRODUCT_SOURCE } from '../data/constants/productSourceOptions';

const getDateWithDashes = date => (date ? moment(date).format('YYYY-MM-DD') : '');
const getDateWithSlashes = date => (date ? moment(date).format('YYYY/MM/DD') : '');
Expand All @@ -32,18 +31,27 @@ const isValidDate = (dateStr) => {
return moment(dateStr) && date.isValid();
};

const getCourseUrlSlugPattern = (updatedSlugFlag, courseRunStatuses, productSource) => {
const getCourseUrlSlugPattern = (updatedSlugFlag, productSource, courseType) => {
/**
* This function returns the course url slug pattern based on the new slug enabled flag and courseRunStatuses
* This function returns the course url slug pattern based on the subidrectly slug
* format flag, product source and course type.
*/
if (updatedSlugFlag && productSource === DEFAULT_PRODUCT_SOURCE && courseRunStatuses.some((status) =>
// eslint-disable-next-line implicit-arrow-linebreak
(IN_REVIEW_STATUS.includes(status) || POST_REVIEW_STATUSES.includes(status)))) {
// change to COURSE_URL_SLUG_PATTERN_NEW when rollout is complete
return COURSE_URL_SLUG_PATTERN;
const COURSE_URL_SLUGS_PATTERN = JSON.parse(process.env.COURSE_URL_SLUGS_PATTERN || '{}');

let slugPattern = null;
const DEFAULT_SLUG_PATTERN = {
slug_format: COURSE_URL_SLUG_PATTERN_OLD,
error_msg: COURSE_URL_SLUG_VALIDATION_MESSAGE[COURSE_URL_SLUG_PATTERN_OLD],
};

if (updatedSlugFlag) {
const urlSlugsDictWrtProductSource = COURSE_URL_SLUGS_PATTERN[productSource] || {};
slugPattern = urlSlugsDictWrtProductSource[courseType]
|| urlSlugsDictWrtProductSource.default || DEFAULT_SLUG_PATTERN;
} else {
slugPattern = DEFAULT_SLUG_PATTERN;
}
// eslint-disable-next-line max-len
return updatedSlugFlag && productSource === DEFAULT_PRODUCT_SOURCE ? COURSE_URL_SLUG_PATTERN : COURSE_URL_SLUG_PATTERN_OLD;
return slugPattern;
};

const updateUrl = (queryOptions) => {
Expand Down
58 changes: 45 additions & 13 deletions src/utils/utils.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as utils from '.';
import { COURSE_URL_SLUG_PATTERN, COURSE_URL_SLUG_PATTERN_OLD } from '../data/constants';
import { COURSE_URL_SLUG_PATTERN_OLD, EXECUTIVE_EDUCATION_SLUG } from '../data/constants';
import { DEFAULT_PRODUCT_SOURCE } from '../data/constants/productSourceOptions';

const initialRuns = [
Expand Down Expand Up @@ -57,46 +57,78 @@ describe('getCourseNumber', () => {
});

describe('getCourseUrlSlugPattern', () => {
const { COURSE_URL_SLUGS_PATTERN } = process.env;
it(
'returns the new course url slug pattern when updatedSlugFlag is true and courseRunStatuses are in review',
() => {
const updatedSlugFlag = true;
const courseRunStatuses = ['review_by_legal', 'review_by_internal'];
const courseType = 'audit';
expect(
utils.getCourseUrlSlugPattern(updatedSlugFlag, courseRunStatuses, DEFAULT_PRODUCT_SOURCE),
).toEqual(COURSE_URL_SLUG_PATTERN);
utils.getCourseUrlSlugPattern(updatedSlugFlag, DEFAULT_PRODUCT_SOURCE, courseType),
).toEqual(
JSON.parse(COURSE_URL_SLUGS_PATTERN)[DEFAULT_PRODUCT_SOURCE].default,
);
},
);

it(
'returns the new course url slug pattern when updatedSlugFlag is true and courseRunStatuses are post review',
() => {
const updatedSlugFlag = true;
const courseRunStatuses = ['published', 'reviewed'];
const courseType = 'audit';
expect(
utils.getCourseUrlSlugPattern(updatedSlugFlag, courseRunStatuses, DEFAULT_PRODUCT_SOURCE),
).toEqual(COURSE_URL_SLUG_PATTERN);
utils.getCourseUrlSlugPattern(updatedSlugFlag, DEFAULT_PRODUCT_SOURCE, courseType),
).toEqual(JSON.parse(COURSE_URL_SLUGS_PATTERN)[DEFAULT_PRODUCT_SOURCE].default);
},
);

it(
'returns the both old & new pattern when updatedSlugFlag is true and courseRunStatuses are not in review or post review',
() => {
const updatedSlugFlag = true;
const courseRunStatuses = ['archived'];
const courseType = 'audit';
expect(
utils.getCourseUrlSlugPattern(updatedSlugFlag, courseRunStatuses, DEFAULT_PRODUCT_SOURCE),
).toEqual(COURSE_URL_SLUG_PATTERN);
utils.getCourseUrlSlugPattern(updatedSlugFlag, DEFAULT_PRODUCT_SOURCE, courseType),
).toEqual(JSON.parse(COURSE_URL_SLUGS_PATTERN)[DEFAULT_PRODUCT_SOURCE].default);
},
);

it('returns the old course url slug pattern when updatedSlugFlag is false', () => {
const updatedSlugFlag = false;
const courseRunStatuses = ['archived'];
const courseType = 'audit';
expect(
utils.getCourseUrlSlugPattern(updatedSlugFlag, courseRunStatuses, DEFAULT_PRODUCT_SOURCE),
).toEqual(COURSE_URL_SLUG_PATTERN_OLD);
utils.getCourseUrlSlugPattern(updatedSlugFlag, DEFAULT_PRODUCT_SOURCE, courseType),
).toEqual({
slug_format: COURSE_URL_SLUG_PATTERN_OLD,
error_msg: 'Course URL slug contains lowercase letters, numbers, underscores, and dashes only.',
});
});

it(
'returns the exec_ed subdirectory slug pattern when courseType is executive education and updatedSlugFlag is true',
() => {
const updatedSlugFlag = true;
const courseType = EXECUTIVE_EDUCATION_SLUG;
expect(
utils.getCourseUrlSlugPattern(updatedSlugFlag, 'external-source', courseType),
).toEqual(JSON.parse(COURSE_URL_SLUGS_PATTERN)['external-source'][EXECUTIVE_EDUCATION_SLUG]);
},
);

it(
'returns the old course url slug pattern when courseType is executive education and updatedSlugFlag is false',
() => {
const updatedSlugFlag = false;
const courseType = EXECUTIVE_EDUCATION_SLUG;

expect(
utils.getCourseUrlSlugPattern(updatedSlugFlag, 'external-source', courseType),
).toEqual({
slug_format: COURSE_URL_SLUG_PATTERN_OLD,
error_msg: 'Course URL slug contains lowercase letters, numbers, underscores, and dashes only.',
});
},
);
});

describe('getCourseError', () => {
Expand Down

0 comments on commit 44606ed

Please sign in to comment.