feat: AA-1020: Add Credit Information to Progress tab (#726)

This is to match what the old Progress tab would show so we
can enable this for all credit courses as well
This commit is contained in:
Dillon Dumesnil
2021-11-05 11:08:29 -07:00
committed by GitHub
parent f7428db3c3
commit 64eb268cb0
17 changed files with 279 additions and 27 deletions

10
.env
View File

@@ -2,14 +2,19 @@
# If you add a new learning MFE-specific variable, please note it there!
NODE_ENV='production'
ACCESS_TOKEN_COOKIE_NAME=''
BASE_URL=''
CONTACT_URL=''
CREDENTIALS_BASE_URL=''
CREDIT_HELP_LINK_URL=''
CSRF_TOKEN_API_PATH=''
DISCOVERY_API_BASE_URL=''
ECOMMERCE_BASE_URL=''
ENABLE_JUMPNAV='true'
ENABLE_NOTICES=''
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=''
FAVICON_URL=''
IGNORED_ERROR_REGEX=''
INSIGHTS_BASE_URL=''
LANGUAGE_PREFERENCE_COOKIE_NAME=''
@@ -19,12 +24,12 @@ LOGOUT_URL=''
LOGO_URL=''
LOGO_TRADEMARK_URL=''
LOGO_WHITE_URL=''
FAVICON_URL=''
MARKETING_SITE_BASE_URL=''
ORDER_HISTORY_URL=''
REFRESH_ACCESS_TOKEN_ENDPOINT=''
SEARCH_CATALOG_URL=''
SEGMENT_KEY=''
SESSION_COOKIE_DOMAIN=''
SITE_NAME=''
SOCIAL_UTM_MILESTONE_CAMPAIGN=''
STUDIO_BASE_URL=''
@@ -36,6 +41,3 @@ TERMS_OF_SERVICE_URL=''
TWITTER_HASHTAG=''
TWITTER_URL=''
USER_INFO_COOKIE_NAME=''
SESSION_COOKIE_DOMAIN=''
ENABLE_JUMPNAV='true'
ENABLE_NOTICES=''

View File

@@ -2,14 +2,19 @@
# If you add a new learning MFE-specific variable, please note it there!
NODE_ENV='development'
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='http://localhost:2000'
CONTACT_URL='http://localhost:18000/contact'
CREDENTIALS_BASE_URL='http://localhost:18150'
CREDIT_HELP_LINK_URL='https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_credit_courses.html#keep-track-of-credit-requirements'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
DISCOVERY_API_BASE_URL='http://localhost:18381'
ECOMMERCE_BASE_URL='http://localhost:18130'
ENABLE_JUMPNAV='true'
ENABLE_NOTICES=''
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
IGNORED_ERROR_REGEX=''
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
@@ -18,7 +23,6 @@ LOGOUT_URL='http://localhost:18000/logout'
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
MARKETING_SITE_BASE_URL='http://localhost:18000'
ORDER_HISTORY_URL='http://localhost:1996/orders'
PORT=2000
@@ -37,5 +41,3 @@ TWITTER_HASHTAG='myedxjourney'
TWITTER_URL='https://twitter.com/edXOnline'
USER_INFO_COOKIE_NAME='edx-user-info'
SESSION_COOKIE_DOMAIN='localhost'
ENABLE_JUMPNAV='true'
ENABLE_NOTICES=''

View File

@@ -2,14 +2,19 @@
# If you add a new learning MFE-specific variable, please note it there!
NODE_ENV='test'
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='http://localhost:2000'
CONTACT_URL='http://localhost:18000/contact'
CREDENTIALS_BASE_URL='http://localhost:18150'
CREDIT_HELP_LINK_URL='https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_credit_courses.html#keep-track-of-credit-requirements'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
DISCOVERY_API_BASE_URL='http://localhost:18381'
ECOMMERCE_BASE_URL='http://localhost:18130'
ENABLE_JUMPNAV='true'
ENABLE_NOTICES=''
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
IGNORED_ERROR_REGEX=''
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
@@ -18,7 +23,6 @@ LOGOUT_URL='http://localhost:18000/logout'
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
MARKETING_SITE_BASE_URL='http://localhost:18000'
ORDER_HISTORY_URL='http://localhost:1996/orders'
PORT=2000
@@ -36,5 +40,3 @@ TERMS_OF_SERVICE_URL='https://www.edx.org/edx-terms-service'
TWITTER_HASHTAG='myedxjourney'
TWITTER_URL='https://twitter.com/edXOnline'
USER_INFO_COOKIE_NAME='edx-user-info'
ENABLE_JUMPNAV='true'
ENABLE_NOTICES=''

View File

@@ -71,6 +71,15 @@ as documented in the Open edX Developer Guide under
The learning micro-frontend also supports the following additional variables:
CREDIT_HELP_LINK_URL
A link to resources to help explain what course credit is and how to earn it.
ENABLE_JUMPNAV
Enables the new Jump Navigation feature in the course breadcrumbs, defaulted to the string 'true'.
Disable to have simple hyperlinks for breadcrumbs. Setting it to any other value but 'true' ('false','I love flags', 'etc' would disable the Jumpnav).
This feature flag is slated to be removed as jumpnav becomes default. Follow the progress of this ticket here:
https://openedx.atlassian.net/browse/TNL-8678
SOCIAL_UTM_MILESTONE_CAMPAIGN
This value is passed as the ``utm_campaign`` parameter for social-share
links when celebrating learning milestones in the course. Optional.
@@ -110,8 +119,3 @@ TWITTER_URL
Example: https://twitter.com/edXOnline
ENABLE_JUMPNAV
Enables the new Jump Navigation feature in the course breadcrumbs, defaulted to the string 'true'.
Disable to have simple hyperlinks for breadcrumbs. Setting it to any other value but 'true' ('false','I love flags', 'etc' would disable the Jumpnav).
This feature flag is slated to be removed as jumpnav becomes default. Follow the progress of this ticket here:
https://openedx.atlassian.net/browse/TNL-8678

View File

@@ -17,6 +17,7 @@ Factory.define('progressTabData')
percent: 1,
is_passing: true,
},
credit_course_requirements: null,
section_scores: [
{
display_name: 'First section',

View File

@@ -581,6 +581,7 @@ Object {
"visiblePercent": 1,
},
"courseId": "course-v1:edX+DemoX+Demo_Course_1",
"creditCourseRequirements": null,
"end": "3027-03-31T00:00:00Z",
"enrollmentMode": "audit",
"gradesFeatureIsFullyLocked": false,

View File

@@ -487,6 +487,29 @@ describe('Progress Tab', () => {
// visible to them, which is non-passing
expect(screen.getByText('A weighted grade of 75% is required to pass in this course')).toBeInTheDocument();
});
it('renders correct title when credit information is available', async () => {
setTabData({
credit_course_requirements: {
eligibility_status: 'eligible',
requirements: [
{
namespace: 'proctored_exam',
name: 'i4x://edX/DemoX/proctoring-block/final_uuid',
display_name: 'Proctored Mid Term Exam',
criteria: {},
reason: {},
status: 'satisfied',
status_date: '2015-06-26 11:07:42',
order: 1,
},
],
},
});
await fetchAndRender();
expect(screen.getByText('Grades & Credit')).toBeInTheDocument();
});
});
describe('Grade Summary', () => {
@@ -1189,6 +1212,54 @@ describe('Progress Tab', () => {
});
});
describe('Credit Information', () => {
it('renders credit information when provided', async () => {
setTabData({
credit_course_requirements: {
eligibility_status: 'eligible',
requirements: [
{
namespace: 'proctored_exam',
name: 'i4x://edX/DemoX/proctoring-block/final_uuid',
display_name: 'Proctored Mid Term Exam',
criteria: {},
reason: {},
status: null,
status_date: '2015-06-26 11:07:42',
order: 1,
},
{
namespace: 'grade',
name: 'i4x://edX/DemoX/proctoring-block/final_uuid',
display_name: 'Minimum Passing Grade',
criteria: { min_grade: 0.8 },
reason: { final_grade: 0.95 },
status: 'satisfied',
status_date: '2015-06-26 11:07:44',
order: 2,
},
],
},
});
await fetchAndRender();
expect(screen.getByText('Grades & Credit')).toBeInTheDocument();
expect(screen.getByText('Requirements for course credit')).toBeInTheDocument();
expect(screen.getByText('You have met the requirements for credit in this course.', { exact: false })).toBeInTheDocument();
expect(screen.getByText('Proctored Mid Term Exam:')).toBeInTheDocument();
// 80% comes from the criteria.minGrade being 0.8
expect(screen.getByText('Minimum grade for credit (80%):')).toBeInTheDocument();
// Completed because the grade requirement has been satisfied
expect(screen.getByText('Completed')).toBeInTheDocument();
});
it('does not render credit information when it is not provided', async () => {
await fetchAndRender();
expect(screen.queryByText('Grades & Credit')).not.toBeInTheDocument();
expect(screen.queryByText('Requirements for course credit.')).not.toBeInTheDocument();
});
});
describe('Access expiration masquerade banner', () => {
it('renders banner when masquerading as a user', async () => {
setMetadata({ is_enrolled: true, original_user_is_staff: true });

View File

@@ -0,0 +1,112 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { CheckCircle, WarningFilled, WatchFilled } from '@edx/paragon/icons';
import { Hyperlink, Icon } from '@edx/paragon';
import { useModel } from '../../../generic/model-store';
import { DashboardLink } from '../../../shared/links';
import messages from './messages';
function CreditInformation({ intl }) {
const {
courseId,
} = useSelector(state => state.courseHome);
const {
creditCourseRequirements,
} = useModel('progress', courseId);
if (!creditCourseRequirements) { return null; }
let eligibilityStatus;
let requirementStatus;
const requirements = [];
const dashboardLink = <DashboardLink />;
const creditLink = (
<Hyperlink
variant="muted"
isInline
destination={getConfig().CREDIT_HELP_LINK_URL}
>{intl.formatMessage(messages.courseCredit)}
</Hyperlink>
);
switch (creditCourseRequirements.eligibilityStatus) {
case 'not_eligible':
eligibilityStatus = (
<FormattedMessage
id="progress.creditInformation.creditNotEligible"
defaultMessage="You are no longer eligible for credit in this course. Learn more about {creditLink}."
values={{ creditLink }}
/>
);
break;
case 'eligible':
eligibilityStatus = (
<FormattedMessage
id="progress.creditInformation.creditEligible"
defaultMessage="
You have met the requirements for credit in this course. Go to your
{dashboardLink} to purchase course credit. Or learn more about {creditLink}."
values={{ dashboardLink, creditLink }}
/>
);
break;
case 'partial_eligible':
eligibilityStatus = (
<FormattedMessage
id="progress.creditInformation.creditPartialEligible"
defaultMessage="You have not yet met the requirements for credit. Learn more about {creditLink}."
values={{ creditLink }}
/>
);
break;
default:
break;
}
creditCourseRequirements.requirements.forEach(requirement => {
switch (requirement.status) {
case 'submitted':
requirementStatus = (<>{intl.formatMessage(messages.verificationSubmitted)} <Icon src={CheckCircle} className="text-success-500 d-inline-flex align-bottom" /></>);
break;
case 'failed':
case 'declined':
requirementStatus = (<>{intl.formatMessage(messages.verificationFailed)} <Icon src={WarningFilled} className="d-inline-flex align-bottom" /></>);
break;
case 'satisfied':
requirementStatus = (<>{intl.formatMessage(messages.completed)} <Icon src={CheckCircle} className="text-success-500 d-inline-flex align-bottom" /></>);
break;
default:
requirementStatus = (<>{intl.formatMessage(messages.upcoming)} <Icon src={WatchFilled} className="text-gray-500 d-inline-flex align-bottom" /></>);
}
requirements.push((
<div className="row w-100 m-0 small" key={`requirement-${requirement.order}`}>
<p className="font-weight-bold">
{requirement.namespace === 'grade'
? `${intl.formatMessage(messages.minimumGrade, { minGrade: Number(requirement.criteria.minGrade) * 100 })}:`
: `${requirement.displayName}:`}
</p>
<div className="ml-1">
{requirementStatus}
</div>
</div>
));
});
return (
<>
<h3 className="h4 col-12 p-0">{intl.formatMessage(messages.requirementsHeader)}</h3>
<p className="small">{eligibilityStatus}</p>
{requirements}
</>
);
}
CreditInformation.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(CreditInformation);

View File

@@ -0,0 +1,34 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
completed: {
id: 'progress.creditInformation.completed',
defaultMessage: 'Completed',
},
courseCredit: {
id: 'progress.creditInformation.courseCredit',
defaultMessage: 'course credit',
},
minimumGrade: {
id: 'progress.creditInformation.minimumGrade',
defaultMessage: 'Minimum grade for credit ({minGrade}%)',
},
requirementsHeader: {
id: 'progress.creditInformation.requirementsHeader',
defaultMessage: 'Requirements for course credit',
},
upcoming: {
id: 'progress.creditInformation.upcoming',
defaultMessage: 'Upcoming',
},
verificationFailed: {
id: 'progress.creditInformation.verificationFailed',
defaultMessage: 'Verification failed',
},
verificationSubmitted: {
id: 'progress.creditInformation.verificationSubmitted',
defaultMessage: 'Verification submitted',
},
});
export default messages;

View File

@@ -7,6 +7,7 @@ import { useModel } from '../../../../generic/model-store';
import CourseGradeFooter from './CourseGradeFooter';
import CourseGradeHeader from './CourseGradeHeader';
import GradeBar from './GradeBar';
import CreditInformation from '../../credit-information/CreditInformation';
import messages from '../messages';
@@ -16,6 +17,7 @@ function CourseGrade({ intl }) {
} = useSelector(state => state.courseHome);
const {
creditCourseRequirements,
gradesFeatureIsFullyLocked,
gradesFeatureIsPartiallyLocked,
gradingPolicy: {
@@ -32,14 +34,20 @@ function CourseGrade({ intl }) {
{(gradesFeatureIsFullyLocked || gradesFeatureIsPartiallyLocked) && <CourseGradeHeader />}
<div className={applyLockedOverlay} aria-hidden={gradesFeatureIsFullyLocked}>
<div className="row w-100 m-0 p-4">
<div className="col-12 col-sm-6 p-0 pr-sm-2">
<h2>{intl.formatMessage(messages.grades)}</h2>
<div className="col-12 col-sm-6 p-0 pr-sm-5.5">
<h2>{creditCourseRequirements
? intl.formatMessage(messages.gradesAndCredit)
: intl.formatMessage(messages.grades)}
</h2>
<p className="small">
{intl.formatMessage(messages.courseGradeBody)}
</p>
</div>
<GradeBar passingGrade={passingGrade} />
</div>
<div className="row w-100 m-0 px-4">
<CreditInformation />
</div>
<CourseGradeFooter passingGrade={passingGrade} />
</div>
</section>

View File

@@ -27,7 +27,7 @@ function GradeBar({ intl, passingGrade }) {
const lockedTooltipClassName = gradesFeatureIsFullyLocked ? 'locked-overlay' : '';
return (
<div className="col-12 col-sm-6 align-self-center">
<div className="col-12 col-sm-6 align-self-center p-0">
<div className="sr-only">{intl.formatMessage(messages.courseGradeBarAltText, { currentGrade, passingGrade })}</div>
<svg width="100%" height="100px" className="grade-bar" aria-hidden="true">
<g style={{ transform: 'translateY(2.61em)' }}>

View File

@@ -39,7 +39,8 @@ function DetailedGrades({ intl }) {
const outlineLink = (
<Hyperlink
className="muted-link inline-link"
variant="muted"
isInline
destination={`${getConfig().LMS_BASE_URL}/courses/${courseId}/course`}
onClick={logOutlineLinkClick}
tabIndex={gradesFeatureIsFullyLocked ? '-1' : '0'}

View File

@@ -87,7 +87,16 @@ function SubsectionTitleCell({ intl, subsection }) {
SubsectionTitleCell.propTypes = {
intl: intlShape.isRequired,
subsection: PropTypes.shape.isRequired,
subsection: PropTypes.shape({
blockKey: PropTypes.string.isRequired,
displayName: PropTypes.string.isRequired,
learnerHasAccess: PropTypes.bool.isRequired,
problemScores: PropTypes.arrayOf(PropTypes.shape({
earned: PropTypes.number.isRequired,
possible: PropTypes.number.isRequired,
})).isRequired,
url: PropTypes.string,
}).isRequired,
};
export default injectIntl(SubsectionTitleCell);

View File

@@ -64,7 +64,7 @@ function GradeSummaryTable({ intl, setAllOfSomeAssignmentTypeIsLocked }) {
footnoteMarker = footnotes.length;
}
const locked = !gradesFeatureIsFullyLocked && hasNoAccessToAssignmentsOfType(assignment.type) ? 'greyed-out' : '';
const locked = !gradesFeatureIsFullyLocked && hasNoAccessToAssignmentsOfType(assignment.type);
return {
type: {

View File

@@ -89,6 +89,10 @@ const messages = defineMessages({
id: 'progress.courseGrade.grades',
defaultMessage: 'Grades',
},
gradesAndCredit: {
id: 'progress.courseGrade.gradesAndCredit',
defaultMessage: 'Grades & Credit',
},
gradeRangeTooltipAlt: {
id: 'progress.courseGrade.gradeRange.Tooltip',
defaultMessage: 'Grade range tooltip',

View File

@@ -97,6 +97,7 @@ initialize({
mergeConfig({
CONTACT_URL: process.env.CONTACT_URL || null,
CREDENTIALS_BASE_URL: process.env.CREDENTIALS_BASE_URL || null,
CREDIT_HELP_LINK_URL: process.env.CREDIT_HELP_LINK_URL || null,
ENTERPRISE_LEARNER_PORTAL_HOSTNAME: process.env.ENTERPRISE_LEARNER_PORTAL_HOSTNAME || null,
ENABLE_JUMPNAV: process.env.ENABLE_JUMPNAV || null,
ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,

View File

@@ -9,8 +9,8 @@ import messages from '../courseware/course/course-exit/messages';
function IntlDashboardLink({ intl }) {
return (
<Hyperlink
className="text-gray-700"
style={{ textDecoration: 'underline' }}
variant="muted"
isInline
destination={`${getConfig().LMS_BASE_URL}/dashboard`}
>
{intl.formatMessage(messages.dashboardLink)}
@@ -28,8 +28,8 @@ function IntlIdVerificationSupportLink({ intl }) {
}
return (
<Hyperlink
className="text-gray-700"
style={{ textDecoration: 'underline' }}
variant="muted"
isInline
destination={getConfig().SUPPORT_URL_ID_VERIFICATION}
>
{intl.formatMessage(messages.idVerificationSupportLink)}
@@ -46,8 +46,8 @@ function IntlProfileLink({ intl }) {
return (
<Hyperlink
className="text-gray-700"
style={{ textDecoration: 'underline' }}
variant="muted"
isInline
destination={`${getConfig().LMS_BASE_URL}/u/${username}`}
>
{intl.formatMessage(messages.profileLink)}