Compare commits

..

1 Commits

Author SHA1 Message Date
Jansen Kantor
8bc1fc82f2 Add Show Course Staff option and exclude all course roles by default (#168)
* Show Course Role Members

* add option to hide FilterBadge value for boolean filters

* chore: bump package to 1.4.20

Co-authored-by: Nathan Sprenkle <nsprenkle@edx.org>
2021-04-16 11:06:45 -04:00
15 changed files with 121 additions and 29 deletions

2
.env
View File

@@ -13,7 +13,7 @@ ACCESS_TOKEN_COOKIE_NAME=null,
CSRF_COOKIE_NAME='csrftoken',
NEW_RELIC_APP_ID=null,
NEW_RELIC_LICENSE_KEY=null,
SITE_NAME='',
SITE_NAME=null,
MARKETING_SITE_BASE_URL=null,
SUPPORT_URL=null,
CONTACT_URL=null,

View File

@@ -13,7 +13,7 @@ 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'
USER_INFO_COOKIE_NAME='edx-user-info'
SITE_NAME=localhost
SITE_NAME='edX'
DATA_API_BASE_URL='http://localhost:8000'
// LMS_CLIENT_ID should match the lms DOT client application id your LMS containe

12
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "@edx/frontend-app-gradebook",
"version": "1.4.18",
"version": "1.4.20",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -1280,9 +1280,9 @@
"integrity": "sha512-XylKOsWRyQm9sNanZnppRORXTLaL34uThyBQpTFwOGAYvNg9PeYsyTTfLA1FTCh02RV+kiwt/O/y14DR/OqpWg=="
},
"@edx/brand": {
"version": "npm:@edx/brand-edx.org@1.3.2",
"resolved": "https://registry.npmjs.org/@edx/brand-edx.org/-/brand-edx.org-1.3.2.tgz",
"integrity": "sha512-Bns1aTpcxZXyQcRXRToHTnLivR+CDgV5ME3EK4BkVx1qtL8ck1bOLyZMARrBrCyWHNFLHqeANbHdFmgTjLXR3A=="
"version": "npm:@edx/brand-edx.org@1.5.0",
"resolved": "https://registry.npmjs.org/@edx/brand-edx.org/-/brand-edx.org-1.5.0.tgz",
"integrity": "sha512-VH+z14VCP2/bH0wnNJ9WSFf/iFs5eZv4X8KcJTd0z/yyWnB3bgpbTR1f7tyuWmmVnMzeur+pA0DBKlNC9beYOg=="
},
"@edx/eslint-config": {
"version": "1.1.5",
@@ -15011,7 +15011,7 @@
"dependencies": {
"query-string": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-2.4.2.tgz",
"resolved": "http://registry.npmjs.org/query-string/-/query-string-2.4.2.tgz",
"integrity": "sha1-fbBmZCCAS6qSrp8miWKFWnYUPfs=",
"requires": {
"strict-uri-encode": "^1.0.0"
@@ -20148,7 +20148,7 @@
},
"got": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz",
"resolved": "http://registry.npmjs.org/got/-/got-6.7.1.tgz",
"integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=",
"dev": true,
"requires": {

View File

@@ -1,6 +1,6 @@
{
"name": "@edx/frontend-app-gradebook",
"version": "1.4.19",
"version": "1.4.20",
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
"repository": {
"type": "git",

View File

@@ -1,9 +1,9 @@
<!doctype html>
<html lang="en-us">
<head>
<title>Gradebook | <%= process.env.SITE_NAME %></title>
<title>Gradebook | edX</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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>

View File

@@ -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">&times;</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,
};

View File

@@ -5,6 +5,7 @@ import {
Button,
Collapsible,
Icon,
CheckBox,
InputSelect,
InputText,
SearchField,
@@ -466,6 +467,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.props.updateIncludeCourseRoleMembers}
/>
</Collapsible>
</Drawer>
);
}
@@ -487,6 +497,7 @@ Gradebook.defaultProps = {
showSpinner: false,
totalUsersCount: null,
tracks: [],
includeCourseRoleMembers: false,
};
Gradebook.propTypes = {
@@ -524,4 +535,6 @@ Gradebook.propTypes = {
name: PropTypes.string,
})),
updateCourseGradeFilter: PropTypes.func.isRequired,
includeCourseRoleMembers: PropTypes.bool,
updateIncludeCourseRoleMembers: PropTypes.func.isRequired,
};

View File

@@ -16,7 +16,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 {
@@ -73,6 +78,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),
@@ -108,6 +114,7 @@ const mapStateToProps = (state, ownProps) => (
tracks: state.tracks.results,
uploadSuccess: !!(state.grades.bulkManagement
&& state.grades.bulkManagement.uploadSuccess),
includeCourseRoleMembers: state.filters.includeCourseRoleMembers,
}
);
@@ -131,6 +138,7 @@ const mapDispatchToProps = {
updateAssignmentFilter,
updateAssignmentLimits,
updateCourseGradeFilter,
updateIncludeCourseRoleMembers,
};
const GradebookPage = connect(

View File

@@ -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,
},
});
@@ -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,
};

View File

@@ -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,
},
)

View File

@@ -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,

View File

@@ -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,
};

View File

@@ -7,6 +7,7 @@ const initialFilters = {
assignmentGradeMax: '100',
courseGradeMin: '0',
courseGradeMax: '100',
includeCourseRoleMembers: false,
};
export default initialFilters;

View File

@@ -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;
}

View File

@@ -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])}`)