Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reading: Limit reader profile by grade #2765

Merged
merged 10 commits into from Feb 12, 2020
16 changes: 15 additions & 1 deletion app/assets/javascripts/reader_profile_january/CohortChart.js
Expand Up @@ -52,14 +52,22 @@ export default class CohortChart extends React.Component {
const whenKey = [schoolYear, benchmarkPeriodKey].join('-');
const cell = json.cells[whenKey];
const pText = cell && cell.stats.p ? percentileWithSuffix(cell.stats.p) : null;
const tooltipText = (cell && cell.stats.p) ? [
'Within the school, at that grade level:',
` ${padFormatStudentsHave(cell.stats.n_higher, 3)} a higher score`,
` ${padFormatStudentsHave(cell.stats.n_equal, 3)} the same score`,
` ${padFormatStudentsHave(cell.stats.n_lower, 3)} a lower score`,
'',
`A score of "${cell.value}" is in the ${pText} percentile`
].join("\n") : null;
const cellStyle = {
...boxStyle,
outline: `1px solid ${PRESENT}`,
backgroundColor: BLANK,
zIndex: pText ? 1 : 0 // for outline overlapping
};
return (
<div key={benchmarkPeriodKey} title={pText} style={cellStyle}>
<div key={benchmarkPeriodKey} title={tooltipText} style={cellStyle}>
{pText}
</div>
);
Expand All @@ -77,3 +85,9 @@ CohortChart.propTypes = {
benchmarkAssessmentKey: PropTypes.string.isRequired,
readerJson: PropTypes.object.isRequired
};


function padFormatStudentsHave(num, n) {
let str = num.toString() + "\t";
return (num === 1) ? `${str} student has` : `${str} students have`;
}
Expand Up @@ -13,10 +13,10 @@ export function mockFetch() {
fetchMock.restore();
fetchMock.get('express:/api/students/:student_id/reader_profile_cohort_json', {
cells: {
'2018-winter': { stats: { p: 20 } },
'2018-spring': { stats: { p: 35 } },
'2019-fall': { stats: { p: 45 } },
'2019-winter': { stats: { p: 41 } }
'2018-winter': { value: 101, stats: { p: 20, n_lower: 1, n_equal: 0, n_higher: 4 } },
'2018-spring': { value: 101, stats: { p: 40, n_lower: 2, n_equal: 0, n_higher: 3 } },
'2019-fall': { value: 132, stats: { p: 60, n_lower: 2, n_equal: 2, n_higher: 1 } },
'2019-winter': { value: 132, stats: { p: 40, n_lower: 2, n_equal: 0, n_higher: 3 } },
}
});
}
Expand Down
Expand Up @@ -60,7 +60,12 @@ exports[`snapshots 1`] = `
"zIndex": 1,
}
}
title="20th"
title="Within the school, at that grade level:
4 students have a higher score
0 students have the same score
1 student has a lower score

A score of \\"101\\" is in the 20th percentile"
>
20th
</div>
Expand All @@ -78,9 +83,14 @@ exports[`snapshots 1`] = `
"zIndex": 1,
}
}
title="35th"
title="Within the school, at that grade level:
3 students have a higher score
0 students have the same score
2 students have a lower score

A score of \\"101\\" is in the 40th percentile"
>
35th
40th
</div>
</div>
<div
Expand Down Expand Up @@ -125,9 +135,14 @@ exports[`snapshots 1`] = `
"zIndex": 1,
}
}
title="45th"
title="Within the school, at that grade level:
1 student has a higher score
2 students have the same score
2 students have a lower score

A score of \\"132\\" is in the 60th percentile"
>
45th
60th
</div>
<div
style={
Expand All @@ -143,9 +158,14 @@ exports[`snapshots 1`] = `
"zIndex": 1,
}
}
title="41st"
title="Within the school, at that grade level:
3 students have a higher score
0 students have the same score
2 students have a lower score

A score of \\"132\\" is in the 40th percentile"
>
41st
40th
</div>
<div
style={
Expand Down
46 changes: 2 additions & 44 deletions app/assets/javascripts/student_profile/ElaDetails.js
@@ -1,8 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import {toMomentFromRailsDate} from '../helpers/toMoment';
import {high, medium, low} from '../helpers/colors';
import {Email} from '../components/PublicLinks';
import DetailsSection from './DetailsSection';
import StarChart from './StarChart';
Expand All @@ -21,8 +19,6 @@ export default class ElaDetails extends React.Component {
const {hideStar} = this.props;
const els = [
this.renderReaderProfile(),
this.renderDibels(),
this.renderFAndPs(),
(!hideStar && this.renderStarReading()),
this.renderMCASELANextGenScores(),
this.renderMCASELAScores(),
Expand Down Expand Up @@ -51,49 +47,13 @@ export default class ElaDetails extends React.Component {

return <div key="reader-profile">{readerProfileEl}</div>;
}

renderDibels() {
const {dibels} = this.props;
if (dibels.length === 0) return null;
const latestDibels = _.last(_.sortBy(dibels, 'date_taken'));
return (
<DetailsSection key="dibels" title="DIBELS, older data">
<div>{this.renderDibelsScore(latestDibels.benchmark)} on {toMomentFromRailsDate(latestDibels.date_taken).format('M/D/YY')}</div>
</DetailsSection>
);
}

renderDibelsScore(score) {
const backgroundColor = {
'INTENSIVE': low,
'STRATEGIC': medium,
'CORE': high
}[score] || '#ccc';
return <span style={{padding: 5, opacity: 0.85, fontWeight: 'bold', color: 'white', backgroundColor}}>{score}</span>;
}

renderFAndPs() {
const {fAndPs} = this.props;
if (fAndPs.length === 0) return null;
const fAndP = _.last(_.sortBy(fAndPs, 'benchmark_date'));
const maybeCode = (fAndP.f_and_p_code) ? ` with ${fAndP.f_and_p_code} code` : null;

return (
<DetailsSection key="f_and_ps" title="Fountas and Pinnell (F&P), older data">
<div>
<span style={{padding: 5, backgroundColor: '#ccc', fontWeight: 'bold'}}>Level {fAndP.instructional_level}{maybeCode}</span>
<span> on {toMomentFromRailsDate(fAndP.benchmark_date).format('M/D/YY')}</span>
</div>
</DetailsSection>
);
}


renderStarReading() {
const {chartData, studentGrade} = this.props;
return (
<DetailsSection key="star" title="STAR Reading, last 4 years">
<StarChart
starSeries={chartData.star_series_reading_percentile}
starSeries={chartData.star_series_reading_percentile || []}
studentGrade={studentGrade}
/>
</DetailsSection>
Expand Down Expand Up @@ -147,8 +107,6 @@ export default class ElaDetails extends React.Component {
}

ElaDetails.propTypes = {
dibels: PropTypes.array.isRequired,
fAndPs: PropTypes.array.isRequired,
chartData: PropTypes.shape({
star_series_reading_percentile: PropTypes.array,
mcas_series_ela_scaled: PropTypes.array,
Expand Down
39 changes: 27 additions & 12 deletions app/assets/javascripts/student_profile/LightProfilePage.js
Expand Up @@ -4,6 +4,7 @@ import moment from 'moment';
import _ from 'lodash';
import {alwaysShowVerticalScrollbars} from '../helpers/globalStylingWorkarounds';
import {percentileWithSuffix} from '../helpers/percentiles';
import {allGrades} from '../helpers/gradeText';
import * as InsightsPropTypes from '../helpers/InsightsPropTypes';
import {toMomentFromTimestamp} from '../helpers/toMoment';
import * as FeedHelpers from '../helpers/FeedHelpers';
Expand All @@ -21,6 +22,7 @@ import LightServiceDetails from './LightServiceDetails';
import LightNotesHelpContext from './LightNotesHelpContext';
import StudentSectionsRoster from './StudentSectionsRoster';
import ReflectionsAboutGrades from './ReflectionsAboutGrades';
import ReaderProfileDeprecated from './ReaderProfileDeprecated';
import {tags} from './lightTagger';
import DetailsSection from './DetailsSection';
import FullCaseHistory from './FullCaseHistory';
Expand Down Expand Up @@ -53,7 +55,6 @@ export default class LightProfilePage extends React.Component {

onColumnClicked(columnKey) {
const {isTakingNotes} = this.state;

if (isTakingNotes) {
const shouldDiscardNote = confirm("You have a note in progress.\n\nDiscard that note?"); // eslint-disable-line no-alert
if (!shouldDiscardNote) return;
Expand Down Expand Up @@ -507,28 +508,42 @@ export default class LightProfilePage extends React.Component {

renderReading() {
const {districtKey} = this.context;
const {student, chartData, currentEducator, dibels, fAndPs} = this.props.profileJson;
const showMinimalReadingData = currentEducator.labels.indexOf('profile_enable_minimal_reading_data') !== -1;
const showReaderProfileJanuary = currentEducator.labels.indexOf('enable_reader_profile_january') !== -1;
const readerProfileEl = (!showMinimalReadingData && !showReaderProfileJanuary) ? null : (
<div>
{showReaderProfileJanuary && <ReaderProfileJanuaryPage student={student} />}
{showMinimalReadingData && <ReaderProfileJunePage student={student} />}
</div>
);
const {student, chartData} = this.props.profileJson;
const readerProfileEl = this.renderReaderProfiles();
return (
<ElaDetails
className="LightProfilePage-ela"
chartData={chartData}
studentGrade={student.grade}
hideStar={!shouldUseStarData(districtKey)}
dibels={showMinimalReadingData ? dibels : []}
fAndPs={showMinimalReadingData ? fAndPs : []}
readerProfileEl={readerProfileEl}
/>
);
}

// This should wrap up all iterations of the reader profile, and the branching
// between then for features switches, etc. When this fully ships, we can remove the
// older paths.
renderReaderProfiles() {
const {student, currentEducator, dibels, fAndPs} = this.props.profileJson;

// Regardless of labels, only ever show for grades 5 and under.
// Also respect labels for January and June profiles.
const allGradeLevels = allGrades();
const allowReaderProfile = (allGradeLevels.indexOf(student.grade) <= allGradeLevels.indexOf('5'));
const showMinimalReadingData = currentEducator.labels.indexOf('profile_enable_minimal_reading_data') !== -1;
const showReaderProfileJune = currentEducator.labels.indexOf('enable_reader_profile_june') !== -1;
const showReaderProfileJanuary = currentEducator.labels.indexOf('enable_reader_profile_january') !== -1;
const showReaderProfileSection = (allowReaderProfile && (showMinimalReadingData || showReaderProfileJune || showReaderProfileJanuary));
return (!showReaderProfileSection) ? null : (
<div>
{showReaderProfileJanuary && <ReaderProfileJanuaryPage student={student} />}
{showReaderProfileJune && <ReaderProfileJunePage student={student} />}
{showMinimalReadingData && <ReaderProfileDeprecated dibels={dibels} fAndPs={fAndPs} />}
</div>
);
}

renderMath() {
const {districtKey} = this.context;
const {student, chartData} = this.props.profileJson;
Expand Down
83 changes: 83 additions & 0 deletions app/assets/javascripts/student_profile/LightProfilePage.test.js
Expand Up @@ -13,6 +13,11 @@ import {
} from './LightProfilePage.fixture';


jest.mock('./ReaderProfileDeprecated', () => 'mocked-reader-profile-deprecated');
jest.mock('../reader_profile/ReaderProfileJunePage', () => 'mocked-reader-profile-june-page');
jest.mock('../reader_profile_january/ReaderProfileJanuaryPage', () => 'mocked-reader-profile-january-page');


function testingTabTextLines(tabIndex, el) {
const leafEls = $(el).find('.LightProfileTab:eq(' + tabIndex+ ') *:not(:has(*))').toArray(); // magic selector from https://stackoverflow.com/questions/4602431/what-is-the-most-efficient-way-to-get-leaf-nodes-with-jquery#4602476
return leafEls.map(el => $(el).text());
Expand All @@ -32,6 +37,15 @@ function testRender(props, context = {}) {
return el;
}

function findReaderProfiles(el) {
const elaDetailsEl = $(el).find('.ElaDetails');
return {
deprecated: $(elaDetailsEl).find('mocked-reader-profile-deprecated').length == 1,
june: $(elaDetailsEl).find('mocked-reader-profile-june-page').length == 1,
january: $(elaDetailsEl).find('mocked-reader-profile-january-page').length == 1
};
}

it('renders without crashing', () => {
testRender(testPropsForPlutoPoppins());
});
Expand Down Expand Up @@ -239,4 +253,73 @@ describe('buttons', () => {
const el = testRender(props);
expect($(el).html()).toContain('Educators with access');
});
});

describe('reader profile', () => {
it('shows nothing for HS student, even with all labels and if reading tab were somehow selected', () => {
let props = testPropsForAladdinMouse();
props = mergeAtPath(props, ['profileJson', 'currentEducator'], {
labels: [
'profile_enable_minimal_reading_data',
'enable_reader_profile_june',
'enable_reader_profile_january'
]
});
props = {...props, selectedColumnKey: 'reading'};
const el = testRender(props);
expect(findReaderProfiles(el)).toEqual({
deprecated: false,
june: false,
january: false
});
});

it('shows nothing for 3rd grade student without labels', () => {
let props = testPropsForAladdinMouse();
props = mergeAtPath(props, ['profileJson', 'student'], {grade: '3'});
props = {...props, selectedColumnKey: 'reading'};

const el = testRender(props);
expect(findReaderProfiles(el)).toEqual({
deprecated: false,
june: false,
january: false
});
});

it('shows January profile when label set, PK grade student', () => {
let props = testPropsForAladdinMouse();
props = mergeAtPath(props, ['profileJson', 'currentEducator'], {
labels: ['enable_reader_profile_january']
});
props = mergeAtPath(props, ['profileJson', 'student'], {grade: 'PK'});
props = {...props, selectedColumnKey: 'reading'};

const el = testRender(props);
expect(findReaderProfiles(el)).toEqual({
deprecated: false,
june: false,
january: true
});
});

it('shows both profiles when both labels set, 5th grade student', () => {
let props = testPropsForAladdinMouse();
props = mergeAtPath(props, ['profileJson', 'currentEducator'], {
labels: [
'profile_enable_minimal_reading_data',
'enable_reader_profile_june',
'enable_reader_profile_january'
]
});
props = mergeAtPath(props, ['profileJson', 'student'], {grade: '5'});
props = {...props, selectedColumnKey: 'reading'};

const el = testRender(props);
expect(findReaderProfiles(el)).toEqual({
deprecated: true,
june: true,
january: true
});
});
});
2 changes: 1 addition & 1 deletion app/assets/javascripts/student_profile/MathDetails.js
Expand Up @@ -45,7 +45,7 @@ export default class MathDetails extends React.Component {
return (
<DetailsSection key="star" title="STAR Math, last 4 years">
<StarChart
starSeries={chartData.star_series_math_percentile}
starSeries={chartData.star_series_math_percentile || []}
studentGrade={studentGrade}
/>
</DetailsSection>
Expand Down