Compare commits
36 Commits
open-relea
...
v1.4.21
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84a7531530 | ||
|
|
27296449b4 | ||
|
|
2b37919222 | ||
|
|
384d6cc296 | ||
|
|
a0943b3946 | ||
|
|
8bc1fc82f2 | ||
|
|
1c26aa1d71 | ||
|
|
582b6cb1c5 | ||
|
|
bc04f6d86f | ||
|
|
84f1efefb3 | ||
|
|
e9f01ea3a3 | ||
|
|
100fbc08bf | ||
|
|
500364dc99 | ||
|
|
609c0a8d3a | ||
|
|
5f81624342 | ||
|
|
647ecbab75 | ||
|
|
de539382bd | ||
|
|
cc01ab0a92 | ||
|
|
8881e62337 | ||
|
|
92e7cc39cd | ||
|
|
d6d09205f4 | ||
|
|
b2e4e330bf | ||
|
|
d10dc54116 | ||
|
|
15d7dcfe85 | ||
|
|
6717663c07 | ||
|
|
ac229ebc85 | ||
|
|
4c481721bc | ||
|
|
40f52b2dc9 | ||
|
|
a25e446998 | ||
|
|
326ae93ed7 | ||
|
|
95e9b51aca | ||
|
|
4d76329946 | ||
|
|
5c565bebb0 | ||
|
|
d1ca314565 | ||
|
|
351bf48561 | ||
|
|
30c51668c4 |
2
.env
2
.env
@@ -31,3 +31,5 @@ ENTERPRISE_MARKETING_URL=null,
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE=null,
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN=null,
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=null,
|
||||
|
||||
BULK_MANAGEMENT_SPECIAL_ACCESS_COURSE_IDS=null,
|
||||
|
||||
@@ -4,6 +4,11 @@ BASE_URL='localhost:1994'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGOUT_URL='http://localhost:18000/login'
|
||||
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
|
||||
LOGO_POWERED_BY_OPEN_EDX_URL_SVG=https://edx-cdn.org/v3/stage/open-edx-tag.svg
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
@@ -33,3 +38,5 @@ ENTERPRISE_MARKETING_URL='http://example.com'
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
||||
|
||||
BULK_MANAGEMENT_SPECIAL_ACCESS_COURSE_IDS=null
|
||||
|
||||
6
.github/CODEOWNERS
vendored
Normal file
6
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Code owners for frontend-app-gradebook, editable gradebook micro-frontend (MFE)
|
||||
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence, they will
|
||||
# be requested for review when someone opens a pull request.
|
||||
* @edx/masters-devs-gta
|
||||
29
.github/pull_request_template.md
vendored
Normal file
29
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
**TL;DR -** [ A short summary of what this PR does and why ]
|
||||
|
||||
JIRA: [JIRA-XXXX](https://openedx.atlassian.net/browse/JIRA-XXXX)
|
||||
|
||||
**What changed?**
|
||||
|
||||
- [ More in depth breakdown of changes ]
|
||||
- [ Peripheral things that got changed ]
|
||||
- [ etc... ]
|
||||
|
||||
**Developer Checklist**
|
||||
- [ ] Test suites passing
|
||||
- [ ] Documentation and test plan updated, if applicable
|
||||
- [ ] Received code-owner approving review
|
||||
- [ ] Bumped version number [package.json](../package.json)
|
||||
|
||||
**Testing Instructions**
|
||||
|
||||
[ How should a reviewer test this PR? ]
|
||||
|
||||
**Reviewer Checklist**
|
||||
|
||||
Collectively, these should be completed by reviewers of this PR:
|
||||
|
||||
- [ ] I've done a visual code review
|
||||
- [ ] I've tested the new functionality
|
||||
|
||||
|
||||
FYI: @edx/masters-devs-gta
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,6 +1,5 @@
|
||||
.DS_Store
|
||||
.eslintcache
|
||||
.idea
|
||||
node_modules
|
||||
npm-debug.log
|
||||
coverage
|
||||
@@ -14,3 +13,7 @@ dist/
|
||||
*~
|
||||
*.swo
|
||||
*.swp
|
||||
|
||||
### Development environments ###
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
69
README.md
69
README.md
@@ -1,18 +1,60 @@
|
||||
[](https://travis-ci.org/edx/frontend-app-gradebook) [](https://coveralls.io/github/edx/frontend-app-gradebook)
|
||||
[](https://travis-ci.com/edx/frontend-app-gradebook) [](https://coveralls.io/github/edx/frontend-app-gradebook)
|
||||
[](@edx/frontend-app-gradebook)
|
||||
[](@edx/frontend-app-gradebook)
|
||||
[](@edx/frontend-app-gradebook)
|
||||
[](https://github.com/semantic-release/semantic-release)
|
||||
|
||||
# gradebook
|
||||
# Gradebook
|
||||
|
||||
Please tag **@edx/educator-neem** on any PRs or issues.
|
||||
Gradebook allows course staff to view, filter, and override subsection grades for a course. Additionally for Masters courses, Gradebook enables bulk management of subsection grades.
|
||||
|
||||
## Introduction
|
||||
Jump to:
|
||||
|
||||
The front-end of our editable Gradebook feature.
|
||||
- [Should I use Gradebook in my course?](#should-i-use-gradebook-in-my-course)
|
||||
- [Quickstart](#quickstart)
|
||||
|
||||
## Usage
|
||||
For existing documentation see:
|
||||
|
||||
- Basic Usage: [Review Learner Grades (read-the-docs)](https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/student_progress/course_grades.html#review-learner-grades-on-the-instructor-dashboard)
|
||||
- Bulk Grade Management: [Override Learner Subsection Scores in Bulk (read-the-docs)](https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/student_progress/course_grades.html#review-learner-grades-on-the-instructor-dashboard)
|
||||
|
||||
## Should I use Gradebook in my course?
|
||||
|
||||
### What does this offer over the legacy gradebook?
|
||||
|
||||

|
||||
|
||||
The micro-frontend offers a great deal more granularity when searching for problems, an easy interface for editing grades, an
|
||||
audit trail for seeing who edited what grade and what reason they gave (if any) for doing so.
|
||||
|
||||

|
||||
|
||||
UsageProblems can be filtered by student as in the traditional gradebook, but can also be filtered by scores to see who
|
||||
scored within a certain range, and by assignment types (note: Not problem types, but categories like ‘Exams’ or
|
||||
‘Homework’).
|
||||
|
||||

|
||||
|
||||
### What does the legacy gradebook offer that this project does not?
|
||||
|
||||
This project does not (yet, at least) create any graphs, which the traditional gradebook does. It also does not give
|
||||
quick links to the problems for the instructor to visit. It expects the instructor to be familiar with the problems they
|
||||
are grading and which unit they refer to.
|
||||
|
||||
The gradebook is expected to be much more performant for larger numbers of students as well. The Instructor Dashboard
|
||||
link for the legacy gradebook reports that "this feature is available only to courses with a small number of enrolled
|
||||
learners." However, this project comes with no such warning.
|
||||
|
||||
### Who should not change to this gradebook?
|
||||
|
||||
Groups whose instructors need not ever manually override grades do not need this project, but may not be any worse off
|
||||
depending on their needs. Instructors that expect to review grades infrequently enough that not having a direct link
|
||||
to the problem in question will have a worse UX than the legacy gradebook provides. Instructors that rely on the graphs
|
||||
generated by the current gradebook might find the lack of autogenerated graphs to be frustrating.
|
||||
|
||||
## Quickstart
|
||||
|
||||
### Installation
|
||||
|
||||
To install gradebook into your project:
|
||||
```
|
||||
@@ -32,7 +74,7 @@ Note that starting the container executes the `npm run start` script which will
|
||||
## Configuring for local use in edx-platform
|
||||
|
||||
Assuming you've got the UI running at `http://localhost:1994`, you can configure the LMS in edx-platform
|
||||
to point to your local gradebook from the instructor dashboard by putting this settings in `lms/env/private.py`:
|
||||
to point to your local gradebook from the instructor dashboard by putting this setting in `lms/env/private.py`:
|
||||
```
|
||||
WRITABLE_GRADEBOOK_URL = 'http://localhost:1994'
|
||||
```
|
||||
@@ -44,10 +86,15 @@ check the ``enabled`` and ``enabled for all courses`` boxes.
|
||||
|
||||
2. Waffle > Switches. Add the ``grades.assume_zero_grade_if_absent`` switch and make it active.
|
||||
|
||||
3. Waffle_utils > Waffle flag course overrides. You want to activate this flag for any course
|
||||
in which you'd like to enable the gradebook. Add a course override flag using a course id and the flag name
|
||||
``grades.writable_gradebook``. Make sure to check the ``enabled`` box. Alternatively, you could add this as a
|
||||
regular waffle flag to enable the gradebook for all courses.
|
||||
3. Waffle_utils > Waffle flag course overrides. Activate waffle flags for courses where you want to enable Gradebook functionality:
|
||||
- Enable Gradebook by adding the ``grades.writable_gradebook`` add checking the ``enabled`` box.
|
||||
- Enable Bulk Grade Management by adding the ``grades.bulk_management`` flag and checking the ``enabled`` box.
|
||||
|
||||
Alternatively, you could add these as regular waffle flags to enable the functionality for all courses.
|
||||
|
||||
**NOTE:** IF the above flags are not configured correctly, the gradebook may appear to work, but will return bogus
|
||||
numbers for grades. If your gradebook isn't accepting your changes, or the changes aren't resulting in sane,
|
||||
recalculated grade values, verify you've set all flags correctly.
|
||||
|
||||
## Running tests
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
BIN
documentation/screenshots/grade-editing.png
Normal file
BIN
documentation/screenshots/grade-editing.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 254 KiB |
BIN
documentation/screenshots/grade-filtering.png
Normal file
BIN
documentation/screenshots/grade-filtering.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 240 KiB |
BIN
documentation/screenshots/grade-listings.png
Normal file
BIN
documentation/screenshots/grade-listings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 319 KiB |
100
documentation/testing/test-plan.md
Normal file
100
documentation/testing/test-plan.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Test Plan
|
||||
|
||||
Designed to be a catalog of major Gradebook workflows to aid in testing. This should be kept up-to-date with new feature changes.
|
||||
|
||||
## Quickstart
|
||||
|
||||
Check that the items below are complete and continue to [Workflow Tests](#workflow-tests). Otherwise, followed the detailed setup in [test-setup.md](./test-setup.md).
|
||||
|
||||
- [ ] Course set up with graded content.
|
||||
- [ ] Gradebook & feature toggles set up for course.
|
||||
- [ ] Course has a Master's track for testing Master's-only features.
|
||||
- [ ] Different types of students enrolled in course (e.g. Master's, TA's).
|
||||
- [ ] Gradebook running.
|
||||
|
||||
## Workflow Tests
|
||||
|
||||
Visit a course as an instructor/staff then **Instructor** tab > **Student Admin** sub-tab > click **Show Gradebook**. Should navigate to `<root-url>:1994/{course-id}`.
|
||||
|
||||
Confirm the following workflows:
|
||||
|
||||
- [ ] Grades table results can be filtered from the "Filter" panel.
|
||||
- The "Edit Filters" button renders for all courses.
|
||||
- Click the "Edit Filters" button to open the "Filter" panel.
|
||||
- [ ] Filter panel shows the sections: Assignments, Overall Grade, Student Groups, Include Course Team Members.
|
||||
- **Note:** Filters are cumulative and act with other applied filters.
|
||||
- Assignments pane
|
||||
- [ ] Applying the "Assignment Types" filter limits the assignment columns show in the grades table to the selected assignment types.
|
||||
- [ ] Applying an "Assignment" filter shows only the selected assignment column in the grades table.
|
||||
- [ ] With an "Assignment" filter already selected, setting a "Min/Max Grade" filter shows only student rows with grades for the assignment within the filtered range.
|
||||
- Overall Grade pane
|
||||
- [ ] Applying a "Min/Max Grade" filter shows only students with Total Course Grades within the filtered range.
|
||||
- Student Groups pane
|
||||
- [ ] Applying a "Tracks" filter shows only student rows matching the selected track.
|
||||
- [ ] Applying a "Cohorts" filter shows only student rows matching the selected cohort.
|
||||
- Include Course Team Members pane
|
||||
- By default, any user with a course role (e.g. staff, beta testers, TA's) are hidden from the grades table.
|
||||
- [ ] Selecting "Include Course Team Members" shows course team members in the grades table.
|
||||
- [ ] Deselecting "Include Course Team Members" shows only students without course roles in the grades table.
|
||||
|
||||
- [ ] Users can be searched/filtered using the Search box.
|
||||
- The Search Box renders for all courses.
|
||||
- [ ] Entering characters into the Search Box filters students on top of already applied filters.
|
||||
- Note: characters can appear anywhere in a name or email, even though emails are only shown for masters-track students. It doesn't appear that search actually works for student keys.
|
||||
|
||||
- [ ] Grades table "Score View" allows selecting how scores are displayed.
|
||||
- [ ] The "Score View" selector renders with the options: Absolute, Percent.
|
||||
- [ ] Changing the "Score View" dropdown to "Percent" shows scores as percentages in the assignment columns (note that scores can be over 100%).
|
||||
- [ ] Changing the "Score View" dropdown to "Absolute" shows scores as {awarded-points}/{possible-points} values, rounded to 2 decimal points.
|
||||
- [ ] For unattempted problems score shows '0'.
|
||||
- [ ] For attempted problems, score always shows an {awarded-points}/{possible-points} value.
|
||||
- [ ] "Total Course Grade" always shows scores as percentages (including 0% for unattempted).
|
||||
|
||||
- [ ] Grades table displays correctly.
|
||||
- [ ] The grades table shows with columns: Username, Email, {numbered-assignments}, Total.
|
||||
- [ ] Usernames appear in the "Username" column.
|
||||
- [ ] Student external keys (where applicable) also appear in the "Username" column.
|
||||
- [ ] Student emails appear in the "Email" column only for masters-track students.
|
||||
- [ ] Assignment scores show in their respective assignment columns.
|
||||
- [ ] Total course grade shows in the "Total Course Grade" column.
|
||||
|
||||
- [ ] Grade overrides can be applied.
|
||||
- [ ] Clicking on an assignment score in the grades table opens the "Edit Grades" modal.
|
||||
- [ ] "Assignment name", "Student username", "Original grade", and "Current grade" display in the modal.
|
||||
- [ ] A history of grade overrides including "Date", "Grader", "Reason", and "Adjusted Grade" shows (if the subsection was previously overridden).
|
||||
- [ ] An entry with the current time appears in the table with areas to enter adjusted grades and reasons for adjusting.
|
||||
- Enter an "Adjusted Grade" and "Reason" for the override.
|
||||
- [ ] Modal can be navigated away from by clicking outside the modal, clicking the "x" button, or hitting "Cancel".
|
||||
- [ ] Clicking "Save Grade" applies the override, shows the successful "grade has been edited" banner and updates score in grades table (may take a few seconds).
|
||||
- [ ] Opening back up the "Edit Grades" modal shows the change as an entry in the override history table.
|
||||
|
||||
- [ ] *Masters only*: "Bulk Management" allows overriding grades in bulk.
|
||||
- Open a non-masters-track course.
|
||||
- [ ] Verify that the "Bulk Management" tab does not appear.
|
||||
- [ ] Verify that the "Bulk Management" button does not appear.
|
||||
- Open a masters-track course.
|
||||
- [ ] Verify that the "Bulk Management" tab appears to the right of the "Grades" tab.
|
||||
- [ ] Verify that the "Bulk Management" button appears.
|
||||
- Click the "Bulk Management" button. This downloads existing student/assignment info.
|
||||
- [ ] Open the downloaded CSV and verify that students and assignments in the file match applied filters/searches.
|
||||
- Add values in the "new_override-{subsection-short-id}" columns for student grades to be overridden and save the CSV file.
|
||||
- [ ] Clicking the "Bulk Management" tab shows the Bulk Management page.
|
||||
- [ ] The bulk management history table appears with columns: "Gradebook", "Download Summary", "Who", "When".
|
||||
- [ ] Previous bulk management imports (if applicable) appear in the table.
|
||||
- Click the "Import Grades" button and select the modified CSV file.
|
||||
- [ ] Verify that the "CSV processing" banner appears.
|
||||
- Wait for processing to complete and reload the page. (Can take seconds to minutes depending on environment and size of the override.)
|
||||
- Navigate back to the "Bulk Management" tab.
|
||||
- [ ] Verify that a new entry appears in the results table indicating how many students were affected by the bulk grade change.
|
||||
- Click the "Download Summary" link to see the summary of changes from the bulk grade changes.
|
||||
- [ ] Verify that students are shown with modified subsections and actions: "No Action" for unchanged users, "Success" for successful overrides.
|
||||
|
||||
- [ ] *Masters only*: Interventions report shows student activity in the course.
|
||||
- Open a non-masters-track course.
|
||||
- [ ] Verify that the "Interventions" tab does not appear.
|
||||
- [ ] Verify that the "Interventions" button does not appear.
|
||||
- Open a masters-track course.
|
||||
- [ ] Verify that the "Interventions" tab appears to the right of the "Grades" tab.
|
||||
- [ ] Verify that the "Interventions" button appears.
|
||||
- Click on the "Interventions" button to generate a CSV students and activity info.
|
||||
- Open the interventions report and verify student info and activity info appear.
|
||||
52
documentation/testing/test-setup.md
Normal file
52
documentation/testing/test-setup.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Test Setup
|
||||
|
||||
Instructions for setting up environments and data for testing Gradebook.
|
||||
|
||||
## Set up a course with graded content
|
||||
|
||||
A course with graded content is the first prerequisite to testing. Use an existing course (e.g. the DemoX Demonstration Course in Devstack) or see [Building and Running an edX Course > Developing Your Course](https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/developing_course/index.html) for notes on how to develop a course from scratch.
|
||||
|
||||
Notably, the course needs a grading policy and subsections with scoreable content.
|
||||
After creating subsections with content, they need to be configured with an "Assignment Type" to be included in grading.
|
||||
|
||||
Suggested resources:
|
||||
- [Establishing a Grading Policy For Your Course](https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/grading/index.html)
|
||||
- [Adding Exercises and Tools](https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/grading/index.html)
|
||||
- [Set the Assignment Type and Due Date for a Subsection](https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/developing_course/course_subsections.html#set-the-assignment-type-and-due-date-for-a-subsection)
|
||||
|
||||
## Enable Gradebook and feature toggles for course
|
||||
|
||||
See README.md #Quickstart for more detailed instructions.
|
||||
|
||||
As an admin user, visit Django Admin (`{lms-url}/admin`) to modify features.
|
||||
- In Grades > Persistent Grades Enabled flag, click "Add persistent grades enabled flag"
|
||||
- [ ] Enable the flag globally or for the course and click "Save"
|
||||
- In Django-Waffle > Switches, click "Add switch"
|
||||
- [ ] Set name to `grades.assume_zero_grade_if_absent`, select "Active", and click "Save"
|
||||
- In Waffle_Utils > Waffle flag course overrides:
|
||||
- [ ] Add a new flag called `grades.writeable_gradebook`, select "Force On", and enable it for your course
|
||||
- [ ] Add a new flag called `grades.bulk_management`, select "Force On", and enable it for your course
|
||||
|
||||
## Create a Master's track for testing Master's-only features
|
||||
|
||||
[source](https://openedx.atlassian.net/wiki/spaces/MS/pages/1453818012/Add+a+learner+into+a+master+s+track)
|
||||
|
||||
Add a Master's track in your course:
|
||||
- As an admin user, go to Django Admin (`{lms-url}/admin`) > Course Modes and add a new course mode
|
||||
- Set the Mode to "Master's"
|
||||
- Set any valid price and currency values
|
||||
- Click "Save"
|
||||
|
||||
Enroll a student in the Master's track:
|
||||
- As a staff/admin user, go to `{lms-url}/support/enrollment`
|
||||
- Search for the username or email of student to enroll
|
||||
- In the results table row matching the user/course, click the "Change Enrollment" button
|
||||
- Select the "Master's" enrollment mode and click "Submit enrollment change"
|
||||
|
||||
## Setup different types of students in course
|
||||
|
||||
To fully test features the course should have at least:
|
||||
- [ ] An audit-track student
|
||||
- [ ] A master's-track student
|
||||
- [ ] A staff member
|
||||
- [ ] A non-staff user
|
||||
11
jest.config.js
Normal file
11
jest.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('jest', {
|
||||
setupFilesAfterEnv: [
|
||||
'<rootDir>/src/setupTest.js',
|
||||
],
|
||||
modulePaths: ['<rootDir>/src/'],
|
||||
snapshotSerializers: [
|
||||
'enzyme-to-json/serializer',
|
||||
],
|
||||
});
|
||||
@@ -1,13 +1,9 @@
|
||||
# This file describes this Open edX repo, as described in OEP-2:
|
||||
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
|
||||
|
||||
nick: grbk
|
||||
tags:
|
||||
- frontend-app
|
||||
- masters
|
||||
oeps:
|
||||
oep-2: true # Repository metadata
|
||||
openedx-release: {ref: master}
|
||||
owner:
|
||||
type: team
|
||||
team: edx/masters-devs-gta
|
||||
|
||||
48027
package-lock.json
generated
48027
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@edx/frontend-app-gradebook",
|
||||
"version": "1.4.14",
|
||||
"version": "1.4.21",
|
||||
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -26,9 +26,10 @@
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/frontend-component-footer": "10.0.11",
|
||||
"@edx/frontend-platform": "1.6.1",
|
||||
"@edx/paragon": "10.0.1",
|
||||
"@edx/brand": "npm:@edx/brand-edx.org@^1.3.2",
|
||||
"@edx/frontend-component-footer": "10.1.1",
|
||||
"@edx/frontend-platform": "1.8.1",
|
||||
"@edx/paragon": "12.4.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.25",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.11.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.11.2",
|
||||
@@ -37,6 +38,7 @@
|
||||
"classnames": "^2.2.6",
|
||||
"core-js": "3.6.5",
|
||||
"email-prop-type": "^1.1.7",
|
||||
"enzyme-to-json": "^3.6.2",
|
||||
"font-awesome": "4.7.0",
|
||||
"history": "4.10.1",
|
||||
"node-sass": "^4.14.1",
|
||||
@@ -58,8 +60,8 @@
|
||||
"whatwg-fetch": "^2.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "5.3.2",
|
||||
"axios": "0.19.2",
|
||||
"@edx/frontend-build": "5.5.2",
|
||||
"axios": "0.21.1",
|
||||
"axios-mock-adapter": "^1.17.0",
|
||||
"codecov": "^3.6.1",
|
||||
"enzyme": "^3.10.0",
|
||||
@@ -68,10 +70,11 @@
|
||||
"fetch-mock": "^6.5.2",
|
||||
"husky": "2.7.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "24.9.0",
|
||||
"react-dev-utils": "^5.0.3",
|
||||
"react-test-renderer": "^16.10.1",
|
||||
"redux-mock-store": "^1.5.3",
|
||||
"semantic-release": "^15.13.24",
|
||||
"semantic-release": "^17.2.3",
|
||||
"travis-deploy-once": "^5.0.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<title>Gradebook | edX</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
|
||||
@import "~@edx/paragon/scss/edx/theme.scss";
|
||||
// frontend-app-*/src/index.scss
|
||||
@import "~@edx/brand/paragon/fonts";
|
||||
@import "~@edx/brand/paragon/variables";
|
||||
@import "~@edx/paragon/scss/core/core";
|
||||
@import "~@edx/brand/paragon/overrides";
|
||||
|
||||
$fa-font-path: "~font-awesome/fonts";
|
||||
@import "~font-awesome/scss/font-awesome";
|
||||
@@ -10,4 +13,3 @@ $input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.
|
||||
|
||||
@import "./components/Gradebook/gradebook";
|
||||
@import "./components/Drawer/Drawer";
|
||||
|
||||
|
||||
@@ -4,11 +4,13 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import initialFilters from '../../data/constants/filters';
|
||||
|
||||
function FilterBadge({ name, value, onClick }) {
|
||||
function FilterBadge({
|
||||
name, value, onClick, showValue,
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<span className="badge badge-info">
|
||||
<span>{`${name}: ${value}`}</span>
|
||||
<span>{name}{showValue && `: ${value}`}</span>
|
||||
<button type="button" className="btn-info" aria-label="Close" onClick={onClick}>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
@@ -18,6 +20,20 @@ function FilterBadge({ name, value, onClick }) {
|
||||
);
|
||||
}
|
||||
|
||||
FilterBadge.defaultProps = {
|
||||
showValue: true,
|
||||
};
|
||||
|
||||
FilterBadge.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
]).isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
showValue: PropTypes.bool,
|
||||
};
|
||||
|
||||
function RangeFilterBadge({
|
||||
displayName,
|
||||
filterName1,
|
||||
@@ -46,7 +62,7 @@ RangeFilterBadge.propTypes = {
|
||||
};
|
||||
|
||||
function SingleValueFilterBadge({
|
||||
displayName, filterName, filterValue, handleBadgeClose,
|
||||
displayName, filterName, filterValue, handleBadgeClose, showValue,
|
||||
}) {
|
||||
return (filterValue !== initialFilters[filterName])
|
||||
&& (
|
||||
@@ -54,14 +70,24 @@ function SingleValueFilterBadge({
|
||||
name={displayName}
|
||||
value={filterValue}
|
||||
onClick={handleBadgeClose}
|
||||
showValue={showValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
SingleValueFilterBadge.defaultProps = {
|
||||
showValue: true,
|
||||
};
|
||||
|
||||
SingleValueFilterBadge.propTypes = {
|
||||
displayName: PropTypes.string.isRequired,
|
||||
filterName: PropTypes.string.isRequired,
|
||||
filterValue: PropTypes.string.isRequired,
|
||||
filterValue: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
]).isRequired,
|
||||
handleBadgeClose: PropTypes.func.isRequired,
|
||||
showValue: PropTypes.bool,
|
||||
};
|
||||
|
||||
function FilterBadges({
|
||||
@@ -73,6 +99,7 @@ function FilterBadges({
|
||||
assignmentGradeMax,
|
||||
courseGradeMin,
|
||||
courseGradeMax,
|
||||
includeCourseRoleMembers,
|
||||
handleFilterBadgeClose,
|
||||
}) {
|
||||
return (
|
||||
@@ -113,10 +140,17 @@ function FilterBadges({
|
||||
/>
|
||||
<SingleValueFilterBadge
|
||||
displayName="Cohort"
|
||||
filterName="track"
|
||||
filterName="cohort"
|
||||
filterValue={cohort}
|
||||
handleBadgeClose={handleFilterBadgeClose(['cohort'])}
|
||||
/>
|
||||
<SingleValueFilterBadge
|
||||
displayName="Including Course Team Members"
|
||||
filterName="includeCourseRoleMembers"
|
||||
filterValue={includeCourseRoleMembers}
|
||||
showValue={false}
|
||||
handleBadgeClose={handleFilterBadgeClose(['includeCourseRoleMembers'])}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -131,18 +165,13 @@ const mapStateToProps = state => (
|
||||
assignmentGradeMax: state.filters.assignmentGradeMax,
|
||||
courseGradeMin: state.filters.courseGradeMin,
|
||||
courseGradeMax: state.filters.courseGradeMax,
|
||||
includeCourseRoleMembers: state.filters.includeCourseRoleMembers,
|
||||
}
|
||||
);
|
||||
|
||||
const ConnectedFilterBadges = connect(mapStateToProps)(FilterBadges);
|
||||
export default ConnectedFilterBadges;
|
||||
|
||||
FilterBadge.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
FilterBadges.defaultProps = {
|
||||
assignment: initialFilters.assignmentType,
|
||||
assignmentType: initialFilters.assignmentType,
|
||||
@@ -152,6 +181,7 @@ FilterBadges.defaultProps = {
|
||||
assignmentGradeMax: initialFilters.assignmentGradeMax,
|
||||
courseGradeMin: initialFilters.courseGradeMin,
|
||||
courseGradeMax: initialFilters.courseGradeMax,
|
||||
includeCourseRoleMembers: initialFilters.includeCourseRoleMembers,
|
||||
};
|
||||
|
||||
FilterBadges.propTypes = {
|
||||
@@ -163,5 +193,6 @@ FilterBadges.propTypes = {
|
||||
assignmentGradeMax: PropTypes.string,
|
||||
courseGradeMin: PropTypes.string,
|
||||
courseGradeMax: PropTypes.string,
|
||||
includeCourseRoleMembers: PropTypes.bool,
|
||||
handleFilterBadgeClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ export class Assignments extends React.Component {
|
||||
const { type, id } = selectedFilterOption || {};
|
||||
const typedValue = { label: assignment, type, id };
|
||||
this.props.updateAssignmentFilter(typedValue);
|
||||
this.updateQueryParams({ assignment: id });
|
||||
this.props.updateQueryParams({ assignment: id });
|
||||
this.props.updateGradesIfAssignmentGradeFiltersSet(
|
||||
this.props.courseId,
|
||||
this.props.selectedCohort,
|
||||
@@ -70,17 +70,14 @@ export class Assignments extends React.Component {
|
||||
|
||||
updateAssignmentTypes = (assignmentType) => {
|
||||
this.props.filterAssignmentType(assignmentType);
|
||||
this.updateQueryParams({ assignmentType });
|
||||
this.props.updateQueryParams({ assignmentType });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Collapsible title="Assignments" open className="filter-group mb-3">
|
||||
<Collapsible title="Assignments" defaultOpen className="filter-group mb-3">
|
||||
<div>
|
||||
<div className="student-filters">
|
||||
<span className="label">
|
||||
Assignment Types:
|
||||
</span>
|
||||
<InputSelect
|
||||
label="Assignment Types"
|
||||
name="assignment-types"
|
||||
@@ -92,9 +89,6 @@ export class Assignments extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
<div className="student-filters">
|
||||
<span className="label">
|
||||
Assignment:
|
||||
</span>
|
||||
<InputSelect
|
||||
label="Assignment"
|
||||
name="assignment"
|
||||
@@ -105,40 +99,45 @@ export class Assignments extends React.Component {
|
||||
disabled={this.props.assignmentFilterOptions.length === 0}
|
||||
/>
|
||||
</div>
|
||||
<p>Grade Range (0% - 100%)</p>
|
||||
<form className="d-fnlex justify-content-between align-items-center" onSubmit={this.handleSubmitAssignmentGrade}>
|
||||
<InputText
|
||||
label="Min Grade"
|
||||
name="assignmentGradeMin"
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={this.props.assignmentGradeMin}
|
||||
disabled={!this.props.selectedAssignment}
|
||||
onChange={this.props.setAssignmentGradeMin}
|
||||
/>
|
||||
<span className="input-percent-label">%</span>
|
||||
<InputText
|
||||
label="Max Grade"
|
||||
name="assignmentGradeMax"
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={this.props.assignmentGradeMax}
|
||||
disabled={!this.props.selectedAssignment}
|
||||
onChange={this.props.setAssignmentGradeMax}
|
||||
/>
|
||||
<span className="input-percent-label">%</span>
|
||||
<Button
|
||||
type="submit"
|
||||
className="btn-outline-secondary"
|
||||
name="assignmentGradeMinMax"
|
||||
disabled={!this.props.selectedAssignment}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
<form className="grade-filter-inputs" onSubmit={this.handleSubmitAssignmentGrade}>
|
||||
<div className="percent-group">
|
||||
<InputText
|
||||
label="Min Grade"
|
||||
name="assignmentGradeMin"
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={this.props.assignmentGradeMin}
|
||||
disabled={!this.props.selectedAssignment}
|
||||
onChange={this.props.setAssignmentGradeMin}
|
||||
/>
|
||||
<span className="input-percent-label">%</span>
|
||||
</div>
|
||||
<div className="percent-group">
|
||||
<InputText
|
||||
label="Max Grade"
|
||||
name="assignmentGradeMax"
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={this.props.assignmentGradeMax}
|
||||
disabled={!this.props.selectedAssignment}
|
||||
onChange={this.props.setAssignmentGradeMax}
|
||||
/>
|
||||
<span className="input-percent-label">%</span>
|
||||
</div>
|
||||
<div className="grade-filter-action">
|
||||
<Button
|
||||
type="submit"
|
||||
className="btn-outline-secondary"
|
||||
name="assignmentGradeMinMax"
|
||||
disabled={!this.props.selectedAssignment}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
Button,
|
||||
StatusAlert,
|
||||
Table,
|
||||
Tab,
|
||||
} from '@edx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faDownload } from '@fortawesome/free-solid-svg-icons';
|
||||
@@ -84,7 +83,7 @@ export class BulkManagement extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Tab eventKey="bulk_management" title="Bulk Management">
|
||||
<div>
|
||||
<h4>Use this feature by downloading a CSV for bulk management,
|
||||
overriding grades locally, and coming back here to upload.
|
||||
</h4>
|
||||
@@ -92,7 +91,7 @@ export class BulkManagement extends React.Component {
|
||||
<StatusAlert
|
||||
alertType="danger"
|
||||
dialog={this.props.bulkImportError}
|
||||
open={this.props.bulkImportError}
|
||||
isOpen={this.props.bulkImportError}
|
||||
dismissible={false}
|
||||
/>
|
||||
<StatusAlert
|
||||
@@ -151,7 +150,7 @@ export class BulkManagement extends React.Component {
|
||||
]}
|
||||
className="table-striped"
|
||||
/>
|
||||
</Tab>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
110
src/components/Gradebook/SearchControls.jsx
Normal file
110
src/components/Gradebook/SearchControls.jsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Button, Icon, SearchField } from '@edx/paragon';
|
||||
|
||||
import {
|
||||
fetchGrades,
|
||||
fetchMatchingUserGrades,
|
||||
} from '../../data/actions/grades';
|
||||
|
||||
/**
|
||||
* Controls for filtering the GradebookTable. Contains the "Edit Filters" button for opening the filter drawer
|
||||
* as well as the search box for searching by username/email.
|
||||
*/
|
||||
export class SearchControls extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onClear = this.onClear.bind(this);
|
||||
}
|
||||
|
||||
/** Submitting searches for user matching the username/email in `value` */
|
||||
onSubmit(value) {
|
||||
this.props.searchForUser(
|
||||
this.props.courseId,
|
||||
value,
|
||||
this.props.selectedCohort,
|
||||
this.props.selectedTrack,
|
||||
this.props.selectedAssignmentType,
|
||||
);
|
||||
}
|
||||
|
||||
/** Changing the search value stores the key in Gradebook. Currently unused */
|
||||
onChange(filterValue) {
|
||||
this.props.setFilterValue(filterValue);
|
||||
}
|
||||
|
||||
/** Clearing the search box falls back to showing students with already applied filters */
|
||||
onClear() {
|
||||
this.props.getUserGrades(
|
||||
this.props.courseId,
|
||||
this.props.selectedCohort,
|
||||
this.props.selectedTrack,
|
||||
this.props.selectedAssignmentType,
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<h4>Step 1: Filter the Grade Report</h4>
|
||||
<div className="d-flex justify-content-between">
|
||||
<Button
|
||||
id="edit-filters-btn"
|
||||
className="btn-primary align-self-start"
|
||||
onClick={this.props.toggleFilterDrawer}
|
||||
>
|
||||
<Icon className="fa fa-filter" /> Edit Filters
|
||||
</Button>
|
||||
<div>
|
||||
<SearchField
|
||||
onSubmit={this.onSubmit}
|
||||
inputLabel="Search for a learner"
|
||||
onChange={this.onChange}
|
||||
onClear={this.onClear}
|
||||
value={this.props.filterValue}
|
||||
/>
|
||||
<small className="form-text text-muted search-help-text">Search by username, email, or student key</small>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SearchControls.defaultProps = {
|
||||
courseId: '',
|
||||
filterValue: '',
|
||||
selectedAssignmentType: '',
|
||||
selectedCohort: null,
|
||||
selectedTrack: null,
|
||||
};
|
||||
|
||||
SearchControls.propTypes = {
|
||||
courseId: PropTypes.string,
|
||||
filterValue: PropTypes.string,
|
||||
setFilterValue: PropTypes.func.isRequired,
|
||||
toggleFilterDrawer: PropTypes.func.isRequired,
|
||||
// From Redux
|
||||
getUserGrades: PropTypes.func.isRequired,
|
||||
searchForUser: PropTypes.func.isRequired,
|
||||
selectedAssignmentType: PropTypes.string,
|
||||
selectedCohort: PropTypes.string,
|
||||
selectedTrack: PropTypes.string,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
selectedAssignmentType: state.filters.assignmentType,
|
||||
selectedTrack: state.filters.track,
|
||||
selectedCohort: state.filters.cohort,
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
getUserGrades: fetchGrades,
|
||||
searchForUser: fetchMatchingUserGrades,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SearchControls);
|
||||
118
src/components/Gradebook/SearchControls.test.jsx
Normal file
118
src/components/Gradebook/SearchControls.test.jsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import {
|
||||
fetchGrades,
|
||||
fetchMatchingUserGrades,
|
||||
} from '../../data/actions/grades';
|
||||
import { mapDispatchToProps, mapStateToProps, SearchControls } from './SearchControls';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Icon: 'Icon',
|
||||
Button: 'Button',
|
||||
SearchField: 'SearchField',
|
||||
}));
|
||||
|
||||
describe('SearchControls', () => {
|
||||
let props;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
props = {
|
||||
courseId: 'course-v1:edX+DEV101+T1',
|
||||
filterValue: 'alice',
|
||||
selectedAssignmentType: 'homework',
|
||||
selectedCohort: 'spring term',
|
||||
selectedTrack: 'masters',
|
||||
getUserGrades: jest.fn(),
|
||||
searchForUser: jest.fn(),
|
||||
setFilterValue: jest.fn(),
|
||||
toggleFilterDrawer: jest.fn().mockName('toggleFilterDrawer'),
|
||||
};
|
||||
});
|
||||
|
||||
const searchControls = (overriddenProps) => {
|
||||
props = { ...props, ...overriddenProps };
|
||||
return shallow(<SearchControls {...props} />);
|
||||
};
|
||||
|
||||
describe('Component', () => {
|
||||
describe('onSubmit', () => {
|
||||
it('calls props.searchForUser with correct data', () => {
|
||||
const wrapper = searchControls();
|
||||
wrapper.instance().onSubmit('bob');
|
||||
|
||||
expect(props.searchForUser).toHaveBeenCalledWith(
|
||||
props.courseId,
|
||||
'bob',
|
||||
props.selectedCohort,
|
||||
props.selectedTrack,
|
||||
props.selectedAssignmentType,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onChange', () => {
|
||||
it('saves the changed search value to Gradebook state', () => {
|
||||
const wrapper = searchControls();
|
||||
wrapper.instance().onChange('bob');
|
||||
expect(props.setFilterValue).toHaveBeenCalledWith('bob');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onClear', () => {
|
||||
it('re-runs search with existing filters', () => {
|
||||
const wrapper = searchControls();
|
||||
wrapper.instance().onClear();
|
||||
expect(props.getUserGrades).toHaveBeenCalledWith(
|
||||
props.courseId,
|
||||
props.selectedCohort,
|
||||
props.selectedTrack,
|
||||
props.selectedAssignmentType,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const state = {
|
||||
filters: {
|
||||
assignmentType: 'labs',
|
||||
track: 'honor',
|
||||
cohort: 'fall term',
|
||||
},
|
||||
};
|
||||
|
||||
it('maps assignment type filter correctly', () => {
|
||||
expect(mapStateToProps(state).selectedAssignmentType).toEqual(state.filters.assignmentType);
|
||||
});
|
||||
|
||||
it('maps track filter correctly', () => {
|
||||
expect(mapStateToProps(state).selectedTrack).toEqual(state.filters.track);
|
||||
});
|
||||
|
||||
it('maps cohort filter correctly', () => {
|
||||
expect(mapStateToProps(state).selectedCohort).toEqual(state.filters.cohort);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('getUserGrades', () => {
|
||||
expect(mapDispatchToProps.getUserGrades).toEqual(fetchGrades);
|
||||
});
|
||||
|
||||
test('searchForUser', () => {
|
||||
expect(mapDispatchToProps.searchForUser).toEqual(fetchMatchingUserGrades);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Snapshots', () => {
|
||||
test('basic snapshot', () => {
|
||||
const wrapper = searchControls();
|
||||
wrapper.instance().onChange = jest.fn().mockName('onChange');
|
||||
wrapper.instance().onClear = jest.fn().mockName('onClear');
|
||||
wrapper.instance().onSubmit = jest.fn().mockName('onSubmit');
|
||||
expect(wrapper.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SearchControls Component Snapshots basic snapshot 1`] = `
|
||||
<React.Fragment>
|
||||
<h4>
|
||||
Step 1: Filter the Grade Report
|
||||
</h4>
|
||||
<div
|
||||
className="d-flex justify-content-between"
|
||||
>
|
||||
<Button
|
||||
className="btn-primary align-self-start"
|
||||
id="edit-filters-btn"
|
||||
onClick={[MockFunction toggleFilterDrawer]}
|
||||
>
|
||||
<Icon
|
||||
className="fa fa-filter"
|
||||
/>
|
||||
Edit Filters
|
||||
</Button>
|
||||
<div>
|
||||
<SearchField
|
||||
inputLabel="Search for a learner"
|
||||
onChange={[MockFunction onChange]}
|
||||
onClear={[MockFunction onClear]}
|
||||
onSubmit={[MockFunction onSubmit]}
|
||||
value="alice"
|
||||
/>
|
||||
<small
|
||||
className="form-text text-muted search-help-text"
|
||||
>
|
||||
Search by username, email, or student key
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
`;
|
||||
@@ -31,13 +31,9 @@
|
||||
}
|
||||
|
||||
.student-filters{
|
||||
display: flex;
|
||||
.label{
|
||||
padding-top: 30px;
|
||||
}
|
||||
.form-group{
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.grade-history-header{
|
||||
float: left;
|
||||
@@ -113,10 +109,10 @@
|
||||
td:nth-child(1),
|
||||
th:nth-child(2),
|
||||
td:nth-child(2) {
|
||||
min-width: 240px;
|
||||
width: 240px;
|
||||
}
|
||||
th, td {
|
||||
min-width: 120px;
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
.table tbody th {
|
||||
@@ -151,10 +147,33 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.input-percent-label {
|
||||
margin-top: 10px;
|
||||
.form-group {
|
||||
label {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
.grade-filter-inputs {
|
||||
.percent-group {
|
||||
display: inline-block;
|
||||
.form-group {
|
||||
width: 115px;
|
||||
display: inline-block;
|
||||
}
|
||||
.input-percent-label {
|
||||
margin-top: 22px;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.grade-filter-action {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.mb-85 {
|
||||
|
||||
@@ -4,10 +4,10 @@ import PropTypes from 'prop-types';
|
||||
import {
|
||||
Button,
|
||||
Collapsible,
|
||||
CheckBox,
|
||||
Icon,
|
||||
InputSelect,
|
||||
InputText,
|
||||
SearchField,
|
||||
StatusAlert,
|
||||
Tab,
|
||||
Tabs,
|
||||
@@ -26,6 +26,7 @@ import BulkManagement from './BulkManagement';
|
||||
import BulkManagementControls from './BulkManagementControls';
|
||||
import EditModal from './EditModal';
|
||||
import GradebookTable from './GradebookTable';
|
||||
import SearchControls from './SearchControls';
|
||||
|
||||
export default class Gradebook extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -238,6 +239,11 @@ export default class Gradebook extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
handleIncludeTeamMembersChange = (includeCourseRoleMembers) => {
|
||||
this.props.updateIncludeCourseRoleMembers(includeCourseRoleMembers);
|
||||
this.updateQueryParams({ includeCourseRoleMembers });
|
||||
};
|
||||
|
||||
createStateFieldSetter = (key) => (value) => this.setState({ [key]: value });
|
||||
|
||||
createStateFieldOnChange = (key) => ({ target }) => this.setState({ [key]: target.value });
|
||||
@@ -252,7 +258,8 @@ export default class Gradebook extends React.Component {
|
||||
safeSetState = this.createLimitedSetter(
|
||||
'adjustedGradePossible',
|
||||
'adjustedGradeValue',
|
||||
'assignmnentName',
|
||||
'assignmentName',
|
||||
'filterValue',
|
||||
'modalOpen',
|
||||
'reasonForChange',
|
||||
'todaysDate',
|
||||
@@ -288,32 +295,17 @@ export default class Gradebook extends React.Component {
|
||||
)}
|
||||
<Tabs defaultActiveKey="grades">
|
||||
<Tab eventKey="grades" title="Grades">
|
||||
<h4>Step 1: Filter the Grade Report</h4>
|
||||
<div className="d-flex justify-content-between">
|
||||
{this.props.showSpinner && <div className="spinner-overlay"><Icon className="fa fa-spinner fa-spin fa-5x color-black" /></div>}
|
||||
<Button className="btn-primary align-self-start" onClick={toggleFilterDrawer}><FontAwesomeIcon icon={faFilter} /> Edit Filters</Button>
|
||||
<div>
|
||||
<SearchField
|
||||
onSubmit={value => this.props.searchForUser(
|
||||
this.props.courseId,
|
||||
value,
|
||||
this.props.selectedCohort,
|
||||
this.props.selectedTrack,
|
||||
this.props.selectedAssignmentType,
|
||||
)}
|
||||
inputLabel="Search for a learner"
|
||||
onChange={filterValue => this.setState({ filterValue })}
|
||||
onClear={() => this.props.getUserGrades(
|
||||
this.props.courseId,
|
||||
this.props.selectedCohort,
|
||||
this.props.selectedTrack,
|
||||
this.props.selectedAssignmentType,
|
||||
)}
|
||||
value={this.state.filterValue}
|
||||
/>
|
||||
<small className="form-text text-muted search-help-text">Search by username, email, or student key</small>
|
||||
{this.props.showSpinner && (
|
||||
<div className="spinner-overlay">
|
||||
<Icon className="fa fa-spinner fa-spin fa-5x color-black" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<SearchControls
|
||||
courseId={this.props.courseId}
|
||||
filterValue={this.state.filterValue}
|
||||
setFilterValue={this.createStateFieldSetter('filterValue')}
|
||||
toggleFilterDrawer={toggleFilterDrawer}
|
||||
/>
|
||||
<ConnectedFilterBadges
|
||||
handleFilterBadgeClose={this.handleFilterBadgeClose}
|
||||
/>
|
||||
@@ -385,10 +377,12 @@ export default class Gradebook extends React.Component {
|
||||
</Tab>
|
||||
{this.props.showBulkManagement
|
||||
&& (
|
||||
<BulkManagement
|
||||
courseId={this.props.courseId}
|
||||
gradeExportUrl={this.props.gradeExportUrl}
|
||||
/>
|
||||
<Tab eventKey="bulk_management" title="Bulk Management">
|
||||
<BulkManagement
|
||||
courseId={this.props.courseId}
|
||||
gradeExportUrl={this.props.gradeExportUrl}
|
||||
/>
|
||||
</Tab>
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
@@ -408,28 +402,34 @@ export default class Gradebook extends React.Component {
|
||||
setAssignmentGradeMax={this.createStateFieldSetter('assignmentGradeMax')}
|
||||
updateQueryParams={this.updateQueryParams}
|
||||
/>
|
||||
<Collapsible title="Overall Grade" open className="filter-group mb-3">
|
||||
<div className="d-flex justify-content-between align-items-center">
|
||||
<InputText
|
||||
value={this.state.courseGradeMin}
|
||||
name="minimum-grade"
|
||||
label="Min Grade"
|
||||
onChange={value => this.handleCourseGradeFilterChange('min', value)}
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
/>
|
||||
<span className="input-percent-label">%</span>
|
||||
<InputText
|
||||
value={this.state.courseGradeMax}
|
||||
name="max-grade"
|
||||
label="Max Grade"
|
||||
onChange={value => this.handleCourseGradeFilterChange('max', value)}
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
/>
|
||||
<span className="input-percent-label">%</span>
|
||||
<Collapsible title="Overall Grade" defaultOpen className="filter-group mb-3">
|
||||
<div className="grade-filter-inputs">
|
||||
<div className="percent-group">
|
||||
<InputText
|
||||
value={this.state.courseGradeMin}
|
||||
name="minimum-grade"
|
||||
label="Min Grade"
|
||||
onChange={value => this.handleCourseGradeFilterChange('min', value)}
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
/>
|
||||
<span className="input-percent-label">%</span>
|
||||
</div>
|
||||
<div className="percent-group">
|
||||
<InputText
|
||||
value={this.state.courseGradeMax}
|
||||
name="max-grade"
|
||||
label="Max Grade"
|
||||
onChange={value => this.handleCourseGradeFilterChange('max', value)}
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
/>
|
||||
<span className="input-percent-label">%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grade-filter-action">
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
onClick={this.handleCourseGradeFilterApplyButtonClick}
|
||||
@@ -438,7 +438,7 @@ export default class Gradebook extends React.Component {
|
||||
</Button>
|
||||
</div>
|
||||
</Collapsible>
|
||||
<Collapsible title="Student Groups" open className="filter-group mb-3">
|
||||
<Collapsible title="Student Groups" defaultOpen className="filter-group mb-3">
|
||||
<InputSelect
|
||||
label="Tracks"
|
||||
name="Tracks"
|
||||
@@ -458,6 +458,15 @@ export default class Gradebook extends React.Component {
|
||||
onChange={this.updateCohorts}
|
||||
/>
|
||||
</Collapsible>
|
||||
<Collapsible title="Include Course Team Members" className="filter-group mb-3">
|
||||
<CheckBox
|
||||
name="include-course-team-members"
|
||||
aria-label="Include Course Team Members"
|
||||
label="Include Course Team Members"
|
||||
checked={this.props.includeCourseRoleMembers}
|
||||
onChange={this.handleIncludeTeamMembersChange}
|
||||
/>
|
||||
</Collapsible>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
@@ -479,6 +488,7 @@ Gradebook.defaultProps = {
|
||||
showSpinner: false,
|
||||
totalUsersCount: null,
|
||||
tracks: [],
|
||||
includeCourseRoleMembers: false,
|
||||
};
|
||||
|
||||
Gradebook.propTypes = {
|
||||
@@ -503,7 +513,6 @@ Gradebook.propTypes = {
|
||||
search: PropTypes.string,
|
||||
}),
|
||||
resetFilters: PropTypes.func.isRequired,
|
||||
searchForUser: PropTypes.func.isRequired,
|
||||
selectedAssignmentType: PropTypes.string,
|
||||
selectedCohort: PropTypes.string,
|
||||
selectedTrack: PropTypes.string,
|
||||
@@ -516,4 +525,6 @@ Gradebook.propTypes = {
|
||||
name: PropTypes.string,
|
||||
})),
|
||||
updateCourseGradeFilter: PropTypes.func.isRequired,
|
||||
includeCourseRoleMembers: PropTypes.bool,
|
||||
updateIncludeCourseRoleMembers: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import EdxLogo from '../../../assets/edx-sm.png';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
export default class Header extends React.Component {
|
||||
renderLogo() {
|
||||
return (
|
||||
<img src={EdxLogo} alt="edX logo" height="30" width="60" />
|
||||
<img src={getConfig().LOGO_URL} alt="edX logo" height="30" width="60" />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
closeBanner,
|
||||
fetchGradeOverrideHistory,
|
||||
fetchGrades,
|
||||
fetchMatchingUserGrades,
|
||||
fetchPrevNextGrades,
|
||||
filterAssignmentType,
|
||||
submitFileUploadFormData,
|
||||
@@ -16,7 +15,12 @@ import {
|
||||
import { fetchCohorts } from '../../data/actions/cohorts';
|
||||
import { fetchTracks } from '../../data/actions/tracks';
|
||||
import {
|
||||
initializeFilters, resetFilters, updateAssignmentFilter, updateAssignmentLimits, updateCourseGradeFilter,
|
||||
initializeFilters,
|
||||
resetFilters,
|
||||
updateAssignmentFilter,
|
||||
updateAssignmentLimits,
|
||||
updateCourseGradeFilter,
|
||||
updateIncludeCourseRoleMembers,
|
||||
} from '../../data/actions/filters';
|
||||
import stateHasMastersTrack from '../../data/selectors/tracks';
|
||||
import {
|
||||
@@ -28,6 +32,7 @@ import {
|
||||
formatMaxCourseGrade,
|
||||
} from '../../data/selectors/grades';
|
||||
import { selectableAssignmentLabels } from '../../data/selectors/filters';
|
||||
import { hasSpecialBulkManagementAccess } from '../../data/selectors/special';
|
||||
import { getCohortNameById } from '../../data/selectors/cohorts';
|
||||
import { fetchAssignmentTypes } from '../../data/actions/assignmentTypes';
|
||||
import { getRoles } from '../../data/actions/roles';
|
||||
@@ -72,6 +77,7 @@ const mapStateToProps = (state, ownProps) => (
|
||||
),
|
||||
courseGradeMin: formatMinCourseGrade(state.filters.courseGradeMin),
|
||||
courseGradeMax: formatMaxCourseGrade(state.filters.courseGradeMax),
|
||||
excludedCourseRoles: state.filters.includeCourseRoleMembers ? '' : 'all',
|
||||
}),
|
||||
grades: state.grades.results,
|
||||
headings: getHeadings(state),
|
||||
@@ -97,13 +103,17 @@ const mapStateToProps = (state, ownProps) => (
|
||||
selectedCohort: state.filters.cohort,
|
||||
selectedAssignmentType: state.filters.assignmentType,
|
||||
selectedAssignment: (state.filters.assignment || {}).label,
|
||||
showBulkManagement: stateHasMastersTrack(state) && state.config.bulkManagementAvailable,
|
||||
showBulkManagement: (
|
||||
hasSpecialBulkManagementAccess(ownProps.match.params.courseId)
|
||||
|| (stateHasMastersTrack(state) && state.config.bulkManagementAvailable)
|
||||
),
|
||||
showSpinner: shouldShowSpinner(state),
|
||||
showSuccess: state.grades.showSuccess,
|
||||
totalUsersCount: state.grades.totalUsersCount,
|
||||
tracks: state.tracks.results,
|
||||
uploadSuccess: !!(state.grades.bulkManagement
|
||||
&& state.grades.bulkManagement.uploadSuccess),
|
||||
includeCourseRoleMembers: state.filters.includeCourseRoleMembers,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -121,12 +131,12 @@ const mapDispatchToProps = {
|
||||
getUserGrades: fetchGrades,
|
||||
initializeFilters,
|
||||
resetFilters,
|
||||
searchForUser: fetchMatchingUserGrades,
|
||||
submitFileUploadFormData,
|
||||
toggleFormat: toggleGradeFormat,
|
||||
updateAssignmentFilter,
|
||||
updateAssignmentLimits,
|
||||
updateCourseGradeFilter,
|
||||
updateIncludeCourseRoleMembers,
|
||||
};
|
||||
|
||||
const GradebookPage = connect(
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import initialFilters from '../constants/filters';
|
||||
import {
|
||||
INITIALIZE_FILTERS, RESET_FILTERS, UPDATE_ASSIGNMENT_FILTER, UPDATE_ASSIGNMENT_LIMITS, UPDATE_COURSE_GRADE_LIMITS,
|
||||
INITIALIZE_FILTERS,
|
||||
RESET_FILTERS,
|
||||
UPDATE_ASSIGNMENT_FILTER,
|
||||
UPDATE_ASSIGNMENT_LIMITS,
|
||||
UPDATE_COURSE_GRADE_LIMITS,
|
||||
UPDATE_INCLUDE_COURSE_ROLE_MEMBERS,
|
||||
} from '../constants/actionTypes/filters';
|
||||
import { getFilters } from '../selectors/filters';
|
||||
import { fetchGrades } from './grades';
|
||||
|
||||
const initializeFilters = ({
|
||||
assignment = initialFilters.assignment,
|
||||
@@ -12,6 +19,7 @@ const initializeFilters = ({
|
||||
assignmentGradeMax = initialFilters.assignmentGradeMax,
|
||||
courseGradeMin = initialFilters.courseGradeMin,
|
||||
courseGradeMax = initialFilters.assignmentGradeMax,
|
||||
includeCourseRoleMembers = initialFilters.includeCourseRoleMembers,
|
||||
}) => ({
|
||||
type: INITIALIZE_FILTERS,
|
||||
data: {
|
||||
@@ -23,6 +31,7 @@ const initializeFilters = ({
|
||||
assignmentGradeMax,
|
||||
courseGradeMin,
|
||||
courseGradeMax,
|
||||
includeCourseRoleMembers: Boolean(includeCourseRoleMembers),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -50,7 +59,21 @@ const updateCourseGradeFilter = (courseGradeMin, courseGradeMax, courseId) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const updateIncludeCourseRoleMembersFilter = (includeCourseRoleMembers) => ({
|
||||
type: UPDATE_INCLUDE_COURSE_ROLE_MEMBERS,
|
||||
data: {
|
||||
includeCourseRoleMembers,
|
||||
},
|
||||
});
|
||||
|
||||
const updateIncludeCourseRoleMembers = includeCourseRoleMembers => (dispatch, getState) => {
|
||||
dispatch(updateIncludeCourseRoleMembersFilter(includeCourseRoleMembers));
|
||||
const state = getState();
|
||||
const { cohort, track, assignmentType } = getFilters(state);
|
||||
dispatch(fetchGrades(state.grades.courseId, cohort, track, assignmentType));
|
||||
};
|
||||
|
||||
export {
|
||||
initializeFilters, resetFilters, updateAssignmentFilter,
|
||||
updateAssignmentLimits, updateCourseGradeFilter,
|
||||
updateAssignmentLimits, updateCourseGradeFilter, updateIncludeCourseRoleMembers,
|
||||
};
|
||||
|
||||
@@ -141,6 +141,7 @@ const fetchGrades = (
|
||||
assignmentGradeMin: assignmentMin,
|
||||
courseGradeMin,
|
||||
courseGradeMax,
|
||||
includeCourseRoleMembers,
|
||||
} = getFilters(getState());
|
||||
const { id: assignmentId } = assignment || {};
|
||||
const assignmentGradeMax = formatMaxAssignmentGrade(assignmentMax, { assignmentId });
|
||||
@@ -158,6 +159,7 @@ const fetchGrades = (
|
||||
assignmentGradeMin,
|
||||
courseGradeMin: courseGradeMinFormatted,
|
||||
courseGradeMax: courseGradeMaxFormatted,
|
||||
includeCourseRoleMembers,
|
||||
},
|
||||
|
||||
)
|
||||
|
||||
@@ -36,7 +36,7 @@ describe('actions', () => {
|
||||
const expectedCohort = 1;
|
||||
const expectedTrack = 'verified';
|
||||
const expectedAssignmentType = 'Exam';
|
||||
const fetchGradesURL = `${configuration.LMS_BASE_URL}/api/grades/v1/gradebook/${courseId}/?page_size=25&cohort_id=${expectedCohort}&enrollment_mode=${expectedTrack}`;
|
||||
const fetchGradesURL = `${configuration.LMS_BASE_URL}/api/grades/v1/gradebook/${courseId}/?page_size=25&cohort_id=${expectedCohort}&enrollment_mode=${expectedTrack}&excluded_course_roles=all`;
|
||||
const responseData = {
|
||||
next: `${fetchGradesURL}&cursor=2344fda`,
|
||||
previous: null,
|
||||
|
||||
@@ -3,7 +3,8 @@ const RESET_FILTERS = 'RESET_FILTERS';
|
||||
const UPDATE_ASSIGNMENT_FILTER = 'UPDATE_ASSIGNMENT_FILTER';
|
||||
const UPDATE_ASSIGNMENT_LIMITS = 'UPDATE_ASSIGNMENT_LIMITS';
|
||||
const UPDATE_COURSE_GRADE_LIMITS = 'UPDATE_COURSE_GRADE_LIMITS';
|
||||
const UPDATE_INCLUDE_COURSE_ROLE_MEMBERS = 'UPDATE_INCLUDE_COURSE_ROLE_MEMBERS';
|
||||
export {
|
||||
INITIALIZE_FILTERS, RESET_FILTERS, UPDATE_ASSIGNMENT_FILTER,
|
||||
UPDATE_ASSIGNMENT_LIMITS, UPDATE_COURSE_GRADE_LIMITS,
|
||||
UPDATE_ASSIGNMENT_LIMITS, UPDATE_COURSE_GRADE_LIMITS, UPDATE_INCLUDE_COURSE_ROLE_MEMBERS,
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ const initialFilters = {
|
||||
assignmentGradeMax: '100',
|
||||
courseGradeMin: '0',
|
||||
courseGradeMax: '100',
|
||||
includeCourseRoleMembers: false,
|
||||
};
|
||||
|
||||
export default initialFilters;
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { GOT_GRADES, FILTER_BY_ASSIGNMENT_TYPE } from '../constants/actionTypes/grades';
|
||||
|
||||
import {
|
||||
INITIALIZE_FILTERS, UPDATE_ASSIGNMENT_FILTER, UPDATE_ASSIGNMENT_LIMITS, UPDATE_COURSE_GRADE_LIMITS, RESET_FILTERS,
|
||||
INITIALIZE_FILTERS,
|
||||
UPDATE_ASSIGNMENT_FILTER,
|
||||
UPDATE_ASSIGNMENT_LIMITS,
|
||||
UPDATE_COURSE_GRADE_LIMITS,
|
||||
RESET_FILTERS,
|
||||
UPDATE_INCLUDE_COURSE_ROLE_MEMBERS,
|
||||
} from '../constants/actionTypes/filters';
|
||||
|
||||
import initialFilters from '../constants/filters';
|
||||
@@ -70,6 +75,11 @@ const reducer = (state = initialState, action) => {
|
||||
courseGradeMin: action.data.courseGradeMin,
|
||||
courseGradeMax: action.data.courseGradeMax,
|
||||
};
|
||||
case UPDATE_INCLUDE_COURSE_ROLE_MEMBERS:
|
||||
return {
|
||||
...state,
|
||||
includeCourseRoleMembers: action.data.includeCourseRoleMembers,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
12
src/data/selectors/special.js
Normal file
12
src/data/selectors/special.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// Certain course runs may be expressly allowed to view the
|
||||
// bulk management tools, bypassing the other checks.
|
||||
// Note that this does not affect whether or not the backend
|
||||
// LMS API will permit usage of the tool.
|
||||
|
||||
const hasSpecialBulkManagementAccess = courseId => {
|
||||
const specialIdList = process.env.BULK_MANAGEMENT_SPECIAL_ACCESS_COURSE_IDS || '';
|
||||
return specialIdList.split(',').includes(courseId);
|
||||
};
|
||||
|
||||
export { hasSpecialBulkManagementAccess };
|
||||
export default hasSpecialBulkManagementAccess;
|
||||
@@ -36,6 +36,9 @@ class LmsApiService {
|
||||
if (options.courseGradeMax) {
|
||||
queryParams.course_grade_max = options.courseGradeMax;
|
||||
}
|
||||
if (!options.includeCourseRoleMembers) {
|
||||
queryParams.excluded_course_roles = ['all'];
|
||||
}
|
||||
|
||||
const queryParamString = Object.keys(queryParams)
|
||||
.map(attr => `${attr}=${encodeURIComponent(queryParams[attr])}`)
|
||||
@@ -96,7 +99,7 @@ class LmsApiService {
|
||||
|
||||
static getGradeExportCsvUrl(courseId, options = {}) {
|
||||
const queryParams = ['track', 'cohort', 'assignment', 'assignmentType', 'assignmentGradeMax',
|
||||
'assignmentGradeMin', 'courseGradeMin', 'courseGradeMax']
|
||||
'assignmentGradeMin', 'courseGradeMin', 'courseGradeMax', 'excludedCourseRoles']
|
||||
.filter(opt => options[opt]
|
||||
&& options[opt] !== 'All')
|
||||
.map(opt => `${opt}=${encodeURIComponent(options[opt])}`)
|
||||
|
||||
@@ -5,7 +5,6 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import SiteFooter, { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
import {
|
||||
APP_READY,
|
||||
getConfig,
|
||||
@@ -22,10 +21,11 @@ import {
|
||||
} from '@fortawesome/free-brands-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import SiteFooter, { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
|
||||
import GradebookPage from './containers/GradebookPage';
|
||||
import Header from './components/Header';
|
||||
import store from './data/store';
|
||||
import FooterLogo from '../assets/edx-footer.png';
|
||||
import './App.scss';
|
||||
|
||||
const socialLinks = [
|
||||
@@ -68,7 +68,7 @@ const App = () => (
|
||||
</main>
|
||||
<SiteFooter
|
||||
siteName={process.env.SITE_NAME}
|
||||
siteLogo={FooterLogo}
|
||||
logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG}
|
||||
marketingSiteBaseUrl={process.env.MARKETING_SITE_BASE_URL}
|
||||
supportUrl={process.env.SUPPORT_URL}
|
||||
contactUrl={process.env.CONTACT_URL}
|
||||
|
||||
13
webpack.dev.config.js
Normal file
13
webpack.dev.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const path = require('path');
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
const config = createConfig('webpack-dev');
|
||||
|
||||
config.resolve.modules = [
|
||||
path.resolve(__dirname, './src'),
|
||||
'node_modules',
|
||||
];
|
||||
|
||||
config.module.rules[0].exclude = /node_modules\/(?!(query-string|split-on-first|strict-uri-encode|@edx))/;
|
||||
|
||||
module.exports = config;
|
||||
13
webpack.prod.config.js
Normal file
13
webpack.prod.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const path = require('path');
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
const config = createConfig('webpack-prod');
|
||||
|
||||
config.resolve.modules = [
|
||||
path.resolve(__dirname, './src'),
|
||||
'node_modules',
|
||||
];
|
||||
|
||||
config.module.rules[0].exclude = /node_modules\/(?!(query-string|split-on-first|strict-uri-encode|@edx))/;
|
||||
|
||||
module.exports = config;
|
||||
Reference in New Issue
Block a user