Compare commits

..

5 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
Jansen Kantor
1c26aa1d71 fix: typo preventing display of assignment name (#169)
* fix: typo preventing display of assignment name

* bump version
2021-03-23 10:08:06 -04:00
Michael Roytman
582b6cb1c5 Merge pull request #166 from edx/mroytman/update-openedx-yaml-file
update openedx.yaml to use current best practices
2021-02-02 16:12:12 -05:00
Michael Roytman
bc04f6d86f update openedx.yaml to use current best practices 2021-02-02 15:46:57 -05:00
Kyle McCormick
84f1efefb3 Allow special access to bulk management tools (#165)
Access is configured on a per-course-run basis
via the new setting:
`BULK_MANAGEMENT_SPECIAL_ACCESS_COURSE_IDS`

TNL-7901
2021-01-25 17:20:21 -05:00
16 changed files with 139 additions and 31 deletions

2
.env
View File

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

View File

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

View File

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

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.18",
"version": "1.4.20",
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
"repository": {
"type": "git",

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,
@@ -252,7 +253,7 @@ export default class Gradebook extends React.Component {
safeSetState = this.createLimitedSetter(
'adjustedGradePossible',
'adjustedGradeValue',
'assignmnentName',
'assignmentName',
'modalOpen',
'reasonForChange',
'todaysDate',
@@ -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 {
@@ -28,6 +33,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 +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),
@@ -97,13 +104,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,
}
);
@@ -127,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

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

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