diff --git a/package-lock.json b/package-lock.json index ad9dce24..da0379a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1447,9 +1447,9 @@ } }, "@edx/paragon": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-15.2.2.tgz", - "integrity": "sha512-C4YMd4zjRalS8pPglpAvDnribrA/3x8XXcbyTq0Xwwotp9HSld2yndASczZGdjNcqG0b1gpmPdxkzx2kaogCiw==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-16.2.0.tgz", + "integrity": "sha512-13xGUU+BezQ27NvR1gtm7YxbcborgUxX78PlUXjamSM3bqMt4LfviyxZjewyxBuevAF+Tj4PlLzKMLe3SkuwFw==", "requires": { "@fortawesome/fontawesome-svg-core": "^1.2.30", "@fortawesome/free-solid-svg-icons": "^5.14.0", @@ -1470,7 +1470,6 @@ "react-responsive": "^6.1.1", "react-table": "^7.6.1", "react-transition-group": "^4.0.0", - "sanitize-html": "^1.27.5", "tabbable": "^4.0.0", "uncontrollable": "7.2.1" }, @@ -3081,9 +3080,9 @@ } }, "@types/react-transition-group": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.1.tgz", - "integrity": "sha512-vIo69qKKcYoJ8wKCJjwSgCTM+z3chw3g18dkrDfVX665tMH7tmbDxEAnPdey4gTlwZz5QuHGzd+hul0OVZDqqQ==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.2.tgz", + "integrity": "sha512-KibDWL6nshuOJ0fu8ll7QnV/LVTo3PzQ9aCPnRUYPfX7eZohHwLIdNHj7pftanREzHNP4/nJa8oeM73uSiavMQ==", "requires": { "@types/react": "*" } @@ -3592,6 +3591,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -5420,6 +5420,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -5430,6 +5431,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -5809,6 +5811,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "requires": { "color-name": "1.1.3" } @@ -5816,7 +5819,8 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true }, "color-string": { "version": "1.5.5", @@ -7084,37 +7088,12 @@ "csstype": "^3.0.2" } }, - "dom-serializer": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", - "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "dependencies": { - "domhandler": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz", - "integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==", - "requires": { - "domelementtype": "^2.2.0" - } - } - } - }, "domain-browser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", "dev": true }, - "domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" - }, "domexception": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", @@ -7132,34 +7111,6 @@ } } }, - "domhandler": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", - "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", - "requires": { - "domelementtype": "^2.0.1" - } - }, - "domutils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz", - "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==", - "requires": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "dependencies": { - "domhandler": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz", - "integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==", - "requires": { - "domelementtype": "^2.2.0" - } - } - } - }, "dot-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", @@ -7605,7 +7556,8 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true }, "escodegen": { "version": "2.0.0", @@ -9417,7 +9369,8 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true }, "has-symbol-support-x": { "version": "1.4.2", @@ -9718,17 +9671,6 @@ } } }, - "htmlparser2": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz", - "integrity": "sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==", - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^3.0.0", - "domutils": "^2.0.0", - "entities": "^2.0.0" - } - }, "http-cache-semantics": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", @@ -14184,7 +14126,8 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "lodash.assignin": { "version": "4.2.0", @@ -15915,11 +15858,6 @@ "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", "dev": true }, - "parse-srcset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", - "integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE=" - }, "parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", @@ -16269,6 +16207,7 @@ "version": "7.0.35", "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "dev": true, "requires": { "chalk": "^2.4.2", "source-map": "^0.6.1", @@ -17621,16 +17560,16 @@ "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, "react-focus-lock": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.5.1.tgz", - "integrity": "sha512-gOToRZKVEymGEjFaTRUKgJsdYQrNosoiK7yZnXnnd8bYew4vMzk3Rxb0Q4nyrGwsFuUmgQiSAulQirA0J+v4hA==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.5.2.tgz", + "integrity": "sha512-WzpdOnEqjf+/A3EH9opMZWauag7gV0BxFl+EY4ElA4qFqYsUsBLnmo2sELbN5OC30S16GAWMy16B9DLPpdJKAQ==", "requires": { "@babel/runtime": "^7.0.0", "focus-lock": "^0.9.1", "prop-types": "^15.6.2", - "react-clientside-effect": "^1.2.2", - "use-callback-ref": "^1.2.1", - "use-sidecar": "^1.0.1" + "react-clientside-effect": "^1.2.5", + "use-callback-ref": "^1.2.5", + "use-sidecar": "^1.0.5" } }, "react-focus-on": { @@ -17687,9 +17626,9 @@ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, "react-overlays": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.1.0.tgz", - "integrity": "sha512-Qp8dqDIIYgQoHxOGVKHwvQUkDe70/Ja/6dn8iCQAXyPvvpks3+T8scLTZLK8MPBBu+X8ustas6y4U6M6zdmCjA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.1.1.tgz", + "integrity": "sha512-eCN2s2/+GVZzpnId4XVWtvDPYYBD2EtOGP74hE+8yDskPzFy9+pV1H3ZZihxuRdEbQzzacySaaDkR7xE0ydl4Q==", "requires": { "@babel/runtime": "^7.13.8", "@popperjs/core": "^2.8.6", @@ -17729,9 +17668,9 @@ } }, "react-remove-scroll": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.4.2.tgz", - "integrity": "sha512-mMSIZYQF3jS2uRJXeFDRaVGA+BGs/hIryV64YUKsHFtpgwZloOUcdu0oW8K6OU8uLHt/kM5d0lUZbdpIVwgXtQ==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.4.3.tgz", + "integrity": "sha512-lGWYXfV6jykJwbFpsuPdexKKzp96f3RbvGapDSIdcyGvHb7/eqyn46C7/6h+rUzYar1j5mdU+XECITHXCKBk9Q==", "requires": { "react-remove-scroll-bar": "^2.1.0", "react-style-singleton": "^2.1.0", @@ -18749,17 +18688,6 @@ "walker": "~1.0.5" } }, - "sanitize-html": { - "version": "1.27.5", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.27.5.tgz", - "integrity": "sha512-M4M5iXDAUEcZKLXkmk90zSYWEtk5NH3JmojQxKxV371fnMh+x9t1rqdmXaGoyEHw3z/X/8vnFhKjGL5xFGOJ3A==", - "requires": { - "htmlparser2": "^4.1.0", - "lodash": "^4.17.15", - "parse-srcset": "^1.0.2", - "postcss": "^7.0.27" - } - }, "sass": { "version": "1.26.11", "resolved": "https://registry.npmjs.org/sass/-/sass-1.26.11.tgz", @@ -19352,7 +19280,8 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true }, "source-map-loader": { "version": "0.2.4", @@ -20106,6 +20035,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, "requires": { "has-flag": "^3.0.0" } diff --git a/package.json b/package.json index d437bd0e..6e733bd8 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@edx/frontend-enterprise": "4.2.3", "@edx/frontend-lib-special-exams": "1.9.0", "@edx/frontend-platform": "1.11.0", - "@edx/paragon": "15.2.2", + "@edx/paragon": "16.2.0", "@fortawesome/fontawesome-svg-core": "1.2.34", "@fortawesome/free-brands-svg-icons": "5.13.1", "@fortawesome/free-regular-svg-icons": "5.13.1", diff --git a/src/course-home/data/__factories__/progressTabData.factory.js b/src/course-home/data/__factories__/progressTabData.factory.js index 0ed0056a..ab21a13e 100644 --- a/src/course-home/data/__factories__/progressTabData.factory.js +++ b/src/course-home/data/__factories__/progressTabData.factory.js @@ -24,6 +24,7 @@ Factory.define('progressTabData') assignment_type: 'Homework', block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345', display_name: 'First subsection', + learner_has_access: true, has_graded_assignment: true, num_points_earned: 0, num_points_possible: 3, diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap index 81038a98..467ae8e2 100644 --- a/src/course-home/data/__snapshots__/redux.test.js.snap +++ b/src/course-home/data/__snapshots__/redux.test.js.snap @@ -5,7 +5,6 @@ Object { "courseHome": Object { "courseId": "course-v1:edX+DemoX+Demo_Course_1", "courseStatus": "loaded", - "gradesFeatureIsLocked": false, "targetUserId": undefined, "toastBodyLink": null, "toastBodyText": null, @@ -302,7 +301,6 @@ Object { "courseHome": Object { "courseId": "course-v1:edX+DemoX+Demo_Course_1", "courseStatus": "loaded", - "gradesFeatureIsLocked": false, "targetUserId": undefined, "toastBodyLink": null, "toastBodyText": null, @@ -482,7 +480,6 @@ Object { "courseHome": Object { "courseId": "course-v1:edX+DemoX+Demo_Course_1", "courseStatus": "loaded", - "gradesFeatureIsLocked": false, "targetUserId": undefined, "toastBodyLink": null, "toastBodyText": null, @@ -564,7 +561,8 @@ Object { "courseId": "course-v1:edX+DemoX+Demo_Course_1", "end": "3027-03-31T00:00:00Z", "enrollmentMode": "audit", - "gradesFeatureIsLocked": false, + "gradesFeatureIsFullyLocked": false, + "gradesFeatureIsPartiallyLocked": false, "gradingPolicy": Object { "assignmentPolicies": Array [ Object { @@ -591,6 +589,7 @@ Object { "blockKey": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345", "displayName": "First subsection", "hasGradedAssignment": true, + "learnerHasAccess": true, "numPointsEarned": 0, "numPointsPossible": 3, "percentGraded": 0, diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index 128a8599..ae0858d4 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -245,7 +245,25 @@ export async function getProgressTabData(courseId, targetUserId) { // in order to preserve a course team's desired grade formatting. camelCasedData.gradingPolicy.gradeRange = data.grading_policy.grade_range; - camelCasedData.gradesFeatureIsLocked = camelCasedData.completionSummary.lockedCount > 0; + camelCasedData.gradesFeatureIsFullyLocked = camelCasedData.completionSummary.lockedCount > 0; + + camelCasedData.gradesFeatureIsPartiallyLocked = false; + if (camelCasedData.gradesFeatureIsFullyLocked) { + camelCasedData.sectionScores.forEach((chapter) => { + chapter.subsections.forEach((subsection) => { + // If something is eligible to be gated by content type gating and would show up on the progress page + if (subsection.assignmentType !== null && subsection.hasGradedAssignment && subsection.showGrades + && (subsection.numPointsPossible > 0 || subsection.numPointsEarned > 0)) { + // but the learner still has access to it, then we are in a partially locked, rather than fully locked state + // since the learner has access to some (but not all) content that would normally be locked + if (subsection.learnerHasAccess) { + camelCasedData.gradesFeatureIsPartiallyLocked = true; + camelCasedData.gradesFeatureIsFullyLocked = false; + } + } + }); + }); + } return camelCasedData; } catch (error) { diff --git a/src/course-home/data/slice.js b/src/course-home/data/slice.js index 4534887e..adb20979 100644 --- a/src/course-home/data/slice.js +++ b/src/course-home/data/slice.js @@ -10,7 +10,6 @@ const slice = createSlice({ initialState: { courseStatus: 'loading', courseId: null, - gradesFeatureIsLocked: false, toastBodyText: null, toastBodyLink: null, toastHeader: '', @@ -39,9 +38,6 @@ const slice = createSlice({ state.toastBodyText = linkText; state.toastHeader = header; }, - setGradesFeatureStatus: (state, { payload }) => { - state.gradesFeatureIsLocked = payload.gradesFeatureIsLocked; - }, }, }); @@ -50,7 +46,6 @@ export const { fetchTabSuccess, fetchTabFailure, setCallToActionToast, - setGradesFeatureStatus, } = slice.actions; export const { diff --git a/src/course-home/progress-tab/ProgressTab.jsx b/src/course-home/progress-tab/ProgressTab.jsx index db3935ac..d9a22c4d 100644 --- a/src/course-home/progress-tab/ProgressTab.jsx +++ b/src/course-home/progress-tab/ProgressTab.jsx @@ -18,10 +18,10 @@ function ProgressTab() { } = useSelector(state => state.courseHome); const { - gradesFeatureIsLocked, + gradesFeatureIsFullyLocked, } = useModel('progress', courseId); - const applyLockedOverlay = gradesFeatureIsLocked ? 'locked-overlay' : ''; + const applyLockedOverlay = gradesFeatureIsFullyLocked ? 'locked-overlay' : ''; const layout = layoutGenerator({ mobile: 0, @@ -41,7 +41,7 @@ function ProgressTab() { -
+
diff --git a/src/course-home/progress-tab/ProgressTab.test.jsx b/src/course-home/progress-tab/ProgressTab.test.jsx index dbde50f2..128e382c 100644 --- a/src/course-home/progress-tab/ProgressTab.test.jsx +++ b/src/course-home/progress-tab/ProgressTab.test.jsx @@ -111,6 +111,7 @@ describe('Progress Tab', () => { assignment_type: 'Homework', block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345', display_name: 'First subsection', + learner_has_access: true, has_graded_assignment: true, num_points_earned: 1, num_points_possible: 2, @@ -176,6 +177,7 @@ describe('Progress Tab', () => { assignment_type: 'Homework', block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345', display_name: 'First subsection', + learner_has_access: true, has_graded_assignment: true, num_points_earned: 8, num_points_possible: 10, @@ -252,6 +254,26 @@ describe('Progress Tab', () => { sku: 'ABCD1234', upgrade_url: 'edx.org/upgrade', }, + section_scores: [ + { + display_name: 'First section', + subsections: [ + { + assignment_type: 'Homework', + block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345', + display_name: 'First subsection', + learner_has_access: false, + has_graded_assignment: true, + num_points_earned: 8, + num_points_possible: 10, + percent_graded: 1.0, + show_correctness: 'always', + show_grades: true, + url: 'http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection', + }, + ], + }, + ], }); await fetchAndRender(); expect(screen.getByText('locked feature')).toBeInTheDocument(); @@ -275,6 +297,26 @@ describe('Progress Tab', () => { sku: 'ABCD1234', upgrade_url: 'edx.org/upgrade', }, + section_scores: [ + { + display_name: 'First section', + subsections: [ + { + assignment_type: 'Homework', + block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345', + display_name: 'First subsection', + learner_has_access: false, + has_graded_assignment: true, + num_points_earned: 8, + num_points_possible: 10, + percent_graded: 1.0, + show_correctness: 'always', + show_grades: true, + url: 'http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection', + }, + ], + }, + ], }); await fetchAndRender(); expect(screen.getByText('locked feature')).toBeInTheDocument(); @@ -298,6 +340,26 @@ describe('Progress Tab', () => { incomplete_count: 1, locked_count: 1, }, + section_scores: [ + { + display_name: 'First section', + subsections: [ + { + assignment_type: 'Homework', + block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345', + display_name: 'First subsection', + learner_has_access: false, + has_graded_assignment: true, + num_points_earned: 1, + num_points_possible: 2, + percent_graded: 1.0, + show_correctness: 'always', + show_grades: true, + url: 'http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection', + }, + ], + }, + ], }); await fetchAndRender(); expect(screen.getByText('locked feature')).toBeInTheDocument(); @@ -309,6 +371,62 @@ describe('Progress Tab', () => { expect(screen.queryByText('locked feature')).not.toBeInTheDocument(); }); + it('renders limited feature preview with upgrade button when user has access to some content that would typically be locked', async () => { + setTabData({ + completion_summary: { + complete_count: 1, + incomplete_count: 1, + locked_count: 1, + }, + verified_mode: { + access_expiration_date: '2050-01-01T12:00:00', + currency: 'USD', + currency_symbol: '$', + price: 149, + sku: 'ABCD1234', + upgrade_url: 'edx.org/upgrade', + }, + section_scores: [ + { + display_name: 'First section', + subsections: [ + { + assignment_type: 'Homework', + block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@123456', + display_name: 'First subsection', + learner_has_access: false, + has_graded_assignment: true, + num_points_earned: 8, + num_points_possible: 10, + percent_graded: 1.0, + show_correctness: 'always', + show_grades: true, + url: 'http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection', + }, + { + assignment_type: 'Exam', + display_name: 'Second subsection', + learner_has_access: true, + has_graded_assignment: true, + num_points_earned: 1, + num_points_possible: 1, + percent_graded: 1.0, + show_correctness: 'always', + show_grades: true, + url: 'http://learning.edx.org/course/course-v1:edX+Test+run/second_subsection', + }, + ], + }, + ], + }); + await fetchAndRender(); + expect(screen.getByText('limited feature')).toBeInTheDocument(); + expect(screen.getByText('Unlock to work towards a certificate.')).toBeInTheDocument(); + expect(screen.queryAllByText('You have limited access to graded assignments as part of the audit track in this course.')).toHaveLength(2); + + expect(screen.queryAllByTestId('blocked-icon')).toHaveLength(4); + }); + it('renders correct current grade tooltip when showGrades is false', async () => { // The learner has a 50% on the first assignment and a 100% on the second, making their grade a 75% // The second assignment has showGrades set to false, so the grade reflected to the learner should be 50%. @@ -321,6 +439,7 @@ describe('Progress Tab', () => { assignment_type: 'Homework', block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345', display_name: 'First subsection', + learner_has_access: true, has_graded_assignment: true, num_points_earned: 1, num_points_possible: 2, @@ -337,6 +456,7 @@ describe('Progress Tab', () => { { assignment_type: 'Homework', display_name: 'Second subsection', + learner_has_access: true, has_graded_assignment: true, num_points_earned: 1, num_points_possible: 1, @@ -531,6 +651,7 @@ describe('Progress Tab', () => { { assignment_type: 'Homework', display_name: 'Second subsection', + learner_has_access: true, has_graded_assignment: true, num_points_earned: 1, num_points_possible: 1, @@ -554,8 +675,8 @@ describe('Progress Tab', () => { await fetchAndRender(); expect(screen.getByText('Detailed grades')).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'First subsection' })); - expect(screen.getByRole('link', { name: 'Second subsection' })); + expect(screen.getByText('First subsection')); + expect(screen.getByText('Second subsection')); }); it('sends event on click of subsection link', async () => { diff --git a/src/course-home/progress-tab/grades/course-grade/CourseGrade.jsx b/src/course-home/progress-tab/grades/course-grade/CourseGrade.jsx index 5b113dcf..c350296a 100644 --- a/src/course-home/progress-tab/grades/course-grade/CourseGrade.jsx +++ b/src/course-home/progress-tab/grades/course-grade/CourseGrade.jsx @@ -16,7 +16,8 @@ function CourseGrade({ intl }) { } = useSelector(state => state.courseHome); const { - gradesFeatureIsLocked, + gradesFeatureIsFullyLocked, + gradesFeatureIsPartiallyLocked, gradingPolicy: { gradeRange, }, @@ -24,12 +25,12 @@ function CourseGrade({ intl }) { const passingGrade = Number((Math.min(...Object.values(gradeRange)) * 100).toFixed(0)); - const applyLockedOverlay = gradesFeatureIsLocked ? 'locked-overlay' : ''; + const applyLockedOverlay = gradesFeatureIsFullyLocked ? 'locked-overlay' : ''; return (
- {gradesFeatureIsLocked && } -
+ {(gradesFeatureIsFullyLocked || gradesFeatureIsPartiallyLocked) && } +

{intl.formatMessage(messages.grades)}

diff --git a/src/course-home/progress-tab/grades/course-grade/CourseGradeHeader.jsx b/src/course-home/progress-tab/grades/course-grade/CourseGradeHeader.jsx index 45128488..79b5b715 100644 --- a/src/course-home/progress-tab/grades/course-grade/CourseGradeHeader.jsx +++ b/src/course-home/progress-tab/grades/course-grade/CourseGradeHeader.jsx @@ -19,6 +19,7 @@ function CourseGradeHeader({ intl }) { } = useModel('courseHomeMeta', courseId); const { verifiedMode, + gradesFeatureIsFullyLocked, } = useModel('progress', courseId); const { administrator } = getAuthenticatedUser(); @@ -29,6 +30,14 @@ function CourseGradeHeader({ intl }) { is_staff: administrator, }); }; + let previewText; + if (verifiedMode) { + previewText = gradesFeatureIsFullyLocked + ? intl.formatMessage(messages.courseGradePreviewUnlockCertificateBody) + : intl.formatMessage(messages.courseGradePartialPreviewUnlockCertificateBody); + } else { + previewText = intl.formatMessage(messages.courseGradePreviewUpgradeDeadlinePassedBody); + } return (
@@ -40,13 +49,14 @@ function CourseGradeHeader({ intl }) { - {intl.formatMessage(messages.courseGradePreviewHeader)} + {gradesFeatureIsFullyLocked + ? intl.formatMessage(messages.courseGradePreviewHeaderLocked) + : intl.formatMessage(messages.courseGradePreviewHeaderLimited)}
- {verifiedMode ? intl.formatMessage(messages.courseGradePreviewUnlockCertificateBody) - : intl.formatMessage(messages.courseGradePreviewUpgradeDeadlinePassedBody)} + {previewText}
diff --git a/src/course-home/progress-tab/grades/course-grade/GradeBar.jsx b/src/course-home/progress-tab/grades/course-grade/GradeBar.jsx index 6f5f2d25..01e5c107 100644 --- a/src/course-home/progress-tab/grades/course-grade/GradeBar.jsx +++ b/src/course-home/progress-tab/grades/course-grade/GradeBar.jsx @@ -19,12 +19,12 @@ function GradeBar({ intl, passingGrade }) { isPassing, visiblePercent, }, - gradesFeatureIsLocked, + gradesFeatureIsFullyLocked, } = useModel('progress', courseId); const currentGrade = Number((visiblePercent * 100).toFixed(0)); - const lockedTooltipClassName = gradesFeatureIsLocked ? 'locked-overlay' : ''; + const lockedTooltipClassName = gradesFeatureIsFullyLocked ? 'locked-overlay' : ''; return (
diff --git a/src/course-home/progress-tab/grades/course-grade/GradeRangeTooltip.jsx b/src/course-home/progress-tab/grades/course-grade/GradeRangeTooltip.jsx index e1778572..e983d962 100644 --- a/src/course-home/progress-tab/grades/course-grade/GradeRangeTooltip.jsx +++ b/src/course-home/progress-tab/grades/course-grade/GradeRangeTooltip.jsx @@ -17,7 +17,7 @@ function GradeRangeTooltip({ intl, iconButtonClassName, passingGrade }) { } = useSelector(state => state.courseHome); const { - gradesFeatureIsLocked, + gradesFeatureIsFullyLocked, gradingPolicy: { gradeRange, }, @@ -68,7 +68,7 @@ function GradeRangeTooltip({ intl, iconButtonClassName, passingGrade }) { src={InfoOutline} iconAs={Icon} size="inline" - disabled={gradesFeatureIsLocked} + disabled={gradesFeatureIsFullyLocked} /> ); diff --git a/src/course-home/progress-tab/grades/detailed-grades/DetailedGrades.jsx b/src/course-home/progress-tab/grades/detailed-grades/DetailedGrades.jsx index 7550d138..9a5a0a5e 100644 --- a/src/course-home/progress-tab/grades/detailed-grades/DetailedGrades.jsx +++ b/src/course-home/progress-tab/grades/detailed-grades/DetailedGrades.jsx @@ -5,7 +5,8 @@ import { getConfig } from '@edx/frontend-platform'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { Hyperlink } from '@edx/paragon'; +import { Blocked } from '@edx/paragon/icons'; +import { Icon, Hyperlink } from '@edx/paragon'; import { useModel } from '../../../../generic/model-store'; import DetailedGradesTable from './DetailedGradesTable'; @@ -21,7 +22,8 @@ function DetailedGrades({ intl }) { org, } = useModel('courseHomeMeta', courseId); const { - gradesFeatureIsLocked, + gradesFeatureIsFullyLocked, + gradesFeatureIsPartiallyLocked, sectionScores, } = useModel('progress', courseId); @@ -40,7 +42,7 @@ function DetailedGrades({ intl }) { className="muted-link inline-link" destination={`${getConfig().LMS_BASE_URL}/courses/${courseId}/course`} onClick={logOutlineLinkClick} - tabIndex={gradesFeatureIsLocked ? '-1' : '0'} + tabIndex={gradesFeatureIsFullyLocked ? '-1' : '0'} > {intl.formatMessage(messages.courseOutline)} @@ -49,6 +51,12 @@ function DetailedGrades({ intl }) { return (

{intl.formatMessage(messages.detailedGrades)}

+ {gradesFeatureIsPartiallyLocked && ( +
+ + {intl.formatMessage(messages.gradeSummaryLimitedAccessExplanation)} +
+ )} {hasSectionScores && ( )} diff --git a/src/course-home/progress-tab/grades/detailed-grades/DetailedGradesTable.jsx b/src/course-home/progress-tab/grades/detailed-grades/DetailedGradesTable.jsx index f83c618f..e9d5e362 100644 --- a/src/course-home/progress-tab/grades/detailed-grades/DetailedGradesTable.jsx +++ b/src/course-home/progress-tab/grades/detailed-grades/DetailedGradesTable.jsx @@ -12,6 +12,7 @@ function DetailedGradesTable({ intl }) { const { courseId, } = useSelector(state => state.courseHome); + const { sectionScores, } = useModel('progress', courseId); @@ -31,7 +32,7 @@ function DetailedGradesTable({ intl }) { const detailedGradesData = subsectionScores.map((subsection) => ({ subsectionTitle: , - score: `${subsection.numPointsEarned}/${subsection.numPointsPossible}`, + score: {subsection.numPointsEarned}/{subsection.numPointsPossible}, })); return ( diff --git a/src/course-home/progress-tab/grades/detailed-grades/SubsectionTitleCell.jsx b/src/course-home/progress-tab/grades/detailed-grades/SubsectionTitleCell.jsx index 4cf780ee..e4cab4e0 100644 --- a/src/course-home/progress-tab/grades/detailed-grades/SubsectionTitleCell.jsx +++ b/src/course-home/progress-tab/grades/detailed-grades/SubsectionTitleCell.jsx @@ -6,7 +6,7 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Collapsible, Icon, Row } from '@edx/paragon'; -import { ArrowDropDown, ArrowDropUp } from '@edx/paragon/icons'; +import { ArrowDropDown, ArrowDropUp, Blocked } from '@edx/paragon/icons'; import messages from '../messages'; import { useModel } from '../../../../generic/model-store'; @@ -20,7 +20,7 @@ function SubsectionTitleCell({ intl, subsection }) { org, } = useModel('courseHomeMeta', courseId); const { - gradesFeatureIsLocked, + gradesFeatureIsFullyLocked, } = useModel('progress', courseId); const { @@ -46,19 +46,22 @@ function SubsectionTitleCell({ intl, subsection }) { - - {displayName} - + + {gradesFeatureIsFullyLocked || subsection.learnerHasAccess ? '' : } + + {displayName} + + diff --git a/src/course-home/progress-tab/grades/grade-summary/AssignmentTypeCell.jsx b/src/course-home/progress-tab/grades/grade-summary/AssignmentTypeCell.jsx index 064248e4..6a16638a 100644 --- a/src/course-home/progress-tab/grades/grade-summary/AssignmentTypeCell.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/AssignmentTypeCell.jsx @@ -1,18 +1,26 @@ import React from 'react'; import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; +import { Blocked } from '@edx/paragon/icons'; +import { Icon } from '@edx/paragon'; import { useModel } from '../../../../generic/model-store'; -function AssignmentTypeCell({ assignmentType, footnoteMarker, footnoteId }) { +function AssignmentTypeCell({ + assignmentType, footnoteMarker, footnoteId, locked, +}) { const { courseId, } = useSelector(state => state.courseHome); + const { - gradesFeatureIsLocked, + gradesFeatureIsFullyLocked, } = useModel('progress', courseId); + + const lockedIcon = locked ? : ''; + return (
- {assignmentType} + {lockedIcon}{assignmentType} {footnoteId && footnoteMarker && ( {footnoteMarker} @@ -34,11 +42,13 @@ AssignmentTypeCell.propTypes = { assignmentType: PropTypes.string.isRequired, footnoteId: PropTypes.string, footnoteMarker: PropTypes.number, + locked: PropTypes.bool, }; AssignmentTypeCell.defaultProps = { footnoteId: '', footnoteMarker: null, + locked: false, }; export default AssignmentTypeCell; diff --git a/src/course-home/progress-tab/grades/grade-summary/DroppableAssignmentFootnote.jsx b/src/course-home/progress-tab/grades/grade-summary/DroppableAssignmentFootnote.jsx index 660d548d..56d1a739 100644 --- a/src/course-home/progress-tab/grades/grade-summary/DroppableAssignmentFootnote.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/DroppableAssignmentFootnote.jsx @@ -12,7 +12,7 @@ function DroppableAssignmentFootnote({ footnotes, intl }) { courseId, } = useSelector(state => state.courseHome); const { - gradesFeatureIsLocked, + gradesFeatureIsFullyLocked, } = useModel('progress', courseId); return ( <> @@ -29,7 +29,7 @@ function DroppableAssignmentFootnote({ footnotes, intl }) { assignmentType: footnote.assignmentType, }} /> - + {intl.formatMessage(messages.backToContent)} diff --git a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.jsx b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.jsx index 4c021a16..e2b355c6 100644 --- a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.jsx @@ -5,7 +5,7 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Icon, IconButton, OverlayTrigger, Popover, } from '@edx/paragon'; -import { InfoOutline } from '@edx/paragon/icons'; +import { Blocked, InfoOutline } from '@edx/paragon/icons'; import messages from '../messages'; import { useModel } from '../../../../generic/model-store'; @@ -15,7 +15,8 @@ function GradeSummaryHeader({ intl }) { courseId, } = useSelector(state => state.courseHome); const { - gradesFeatureIsLocked, + gradesFeatureIsFullyLocked, + gradesFeatureIsPartiallyLocked, } = useModel('progress', courseId); const [showTooltip, setShowTooltip] = useState(false); return ( @@ -41,9 +42,15 @@ function GradeSummaryHeader({ intl }) { iconAs={Icon} className="mb-3" size="sm" - disabled={gradesFeatureIsLocked} + disabled={gradesFeatureIsFullyLocked} /> + {gradesFeatureIsPartiallyLocked && ( +
+ + {intl.formatMessage(messages.gradeSummaryLimitedAccessExplanation)} +
+ )}
); } diff --git a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx index 09a0edb9..670e447f 100644 --- a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx @@ -20,6 +20,8 @@ function GradeSummaryTable({ intl }) { gradingPolicy: { assignmentPolicies, }, + gradesFeatureIsFullyLocked, + sectionScores, } = useModel('progress', courseId); const footnotes = []; @@ -29,6 +31,19 @@ function GradeSummaryTable({ intl }) { return footnoteId.replace(/[^A-Za-z0-9.-_]+/g, '-'); }; + const hasNoAccessToAssignmentsOfType = (assignmentType) => { + const subsectionAssignmentsOfType = sectionScores.map((chapter) => chapter.subsections.filter((subsection) => ( + subsection.assignmentType === assignmentType && subsection.hasGradedAssignment + && (subsection.numPointsPossible > 0 || subsection.numPointsEarned > 0) + ))).flat(); + if (subsectionAssignmentsOfType.length) { + return !subsectionAssignmentsOfType.some((subsection) => ( + subsection.learnerHasAccess === true + )); + } + return false; + }; + const gradeSummaryData = assignmentPolicies.map((assignment) => { let footnoteId = ''; let footnoteMarker; @@ -44,11 +59,15 @@ function GradeSummaryTable({ intl }) { footnoteMarker = footnotes.length; } + const locked = !gradesFeatureIsFullyLocked && hasNoAccessToAssignmentsOfType(assignment.type) ? 'locked-overlay' : ''; + return { - type: { footnoteId, footnoteMarker, type: assignment.type }, - weight: `${(assignment.weight * 100).toFixed(0)}%`, - grade: `${(assignment.averageGrade * 100).toFixed(0)}%`, - weightedGrade: `${(assignment.weightedGrade * 100).toFixed(0)}%`, + type: { + footnoteId, footnoteMarker, type: assignment.type, locked, + }, + weight: { weight: `${(assignment.weight * 100).toFixed(0)}%`, locked }, + grade: { grade: `${(assignment.averageGrade * 100).toFixed(0)}%`, locked }, + weightedGrade: { weightedGrade: `${(assignment.weightedGrade * 100).toFixed(0)}%`, locked }, }; }); @@ -67,6 +86,7 @@ function GradeSummaryTable({ intl }) { assignmentType={value.type} // eslint-disable-line react/prop-types footnoteId={value.footnoteId} // eslint-disable-line react/prop-types footnoteMarker={value.footnoteMarker} // eslint-disable-line react/prop-types + locked={value.locked} // eslint-disable-line react/prop-types /> ), headerClassName: 'h5 mb-0', @@ -75,18 +95,30 @@ function GradeSummaryTable({ intl }) { Header: `${intl.formatMessage(messages.weight)}`, accessor: 'weight', headerClassName: 'justify-content-end h5 mb-0', + // eslint-disable-next-line react/prop-types + Cell: ({ value }) => ( + {value.weight} // eslint-disable-line react/prop-types + ), cellClassName: 'float-right small', }, { Header: `${intl.formatMessage(messages.grade)}`, accessor: 'grade', headerClassName: 'justify-content-end h5 mb-0', + // eslint-disable-next-line react/prop-types + Cell: ({ value }) => ( + {value.grade} // eslint-disable-line react/prop-types + ), cellClassName: 'float-right small', }, { Header: `${intl.formatMessage(messages.weightedGrade)}`, accessor: 'weightedGrade', headerClassName: 'justify-content-end h5 mb-0 text-right', + // eslint-disable-next-line react/prop-types + Cell: ({ value }) => ( + {value.weightedGrade} // eslint-disable-line react/prop-types + ), cellClassName: 'float-right font-weight-bold small', }, ]} diff --git a/src/course-home/progress-tab/grades/messages.js b/src/course-home/progress-tab/grades/messages.js index 37b581a8..652684c2 100644 --- a/src/course-home/progress-tab/grades/messages.js +++ b/src/course-home/progress-tab/grades/messages.js @@ -29,10 +29,14 @@ const messages = defineMessages({ id: 'progress.courseGrade.footer.passing', defaultMessage: 'You’re currently passing this course with a grade of {letterGrade} ({minGrade}-{maxGrade}%)', }, - courseGradePreviewHeader: { - id: 'progress.courseGrade.preview.header', + courseGradePreviewHeaderLocked: { + id: 'progress.courseGrade.preview.headerLocked', defaultMessage: 'locked feature', }, + courseGradePreviewHeaderLimited: { + id: 'progress.courseGrade.preview.headerLimited', + defaultMessage: 'limited feature', + }, courseGradePreviewHeaderAriaHidden: { id: 'progress.courseGrade.preview.header.ariaHidden', defaultMessage: 'Preview of a ', @@ -41,6 +45,10 @@ const messages = defineMessages({ id: 'progress.courseGrade.preview.body.unlockCertificate', defaultMessage: 'Unlock to view grades and work towards a certificate.', }, + courseGradePartialPreviewUnlockCertificateBody: { + id: 'progress.courseGrade.partialpreview.body.unlockCertificate', + defaultMessage: 'Unlock to work towards a certificate.', + }, courseGradePreviewUpgradeDeadlinePassedBody: { id: 'progress.courseGrade.preview.body.upgradeDeadlinePassed', defaultMessage: 'The deadline to upgrade in this course has passed.', @@ -89,6 +97,10 @@ const messages = defineMessages({ id: 'progress.gradeSummary', defaultMessage: 'Grade summary', }, + gradeSummaryLimitedAccessExplanation: { + id: 'progress.gradeSummary.limitedAccessExplanation', + defaultMessage: 'You have limited access to graded assignments as part of the audit track in this course.', + }, gradeSummaryTooltipAlt: { id: 'progress.gradeSummary.tooltip.alt', defaultMessage: 'Grade summary tooltip',