From 2264b40ace41a45a71160afe87584e25f8d9243d Mon Sep 17 00:00:00 2001 From: kevinrobinson Date: Fri, 16 Aug 2019 12:17:32 -0400 Subject: [PATCH 01/21] Update JS static bits, factor out choices for RecordService UI --- app/assets/javascripts/helpers/PerDistrict.js | 21 +++- .../javascripts/helpers/serviceColor.js | 25 ++--- .../student_profile/RecordService.js | 16 +-- .../testing/fixtures/serviceTypesIndex.js | 99 ++++++++++++++++--- 4 files changed, 121 insertions(+), 40 deletions(-) diff --git a/app/assets/javascripts/helpers/PerDistrict.js b/app/assets/javascripts/helpers/PerDistrict.js index 71ccb8fda8..840896c986 100644 --- a/app/assets/javascripts/helpers/PerDistrict.js +++ b/app/assets/javascripts/helpers/PerDistrict.js @@ -189,7 +189,6 @@ export function eventNoteTypeIdsForSearch(districtKey) { return _.uniq(leftEventNoteTypeIds.concat(rightEventNoteTypeIds)); } - // What choices do educators have for taking notes in the product? export function takeNotesChoices(districtKey) { if (districtKey === SOMERVILLE || districtKey === DEMO) { @@ -231,6 +230,26 @@ export function studentTableEventNoteTypeIds(districtKey, schoolType) { } +// What choices do educators have for recording services in the product? +export function recordServiceChoices(districtKey) { + if (districtKey === BEDFORD) { + return { + leftServiceTypeIds: [701, 708, 706, 707], + rightServiceTypeIds: [703, 704, 702, 704, 709] + }; + } + + if ([SOMERVILLE, DEMO, NEW_BEDFORD].indexOf(districtKey) !== -1) { + return { + leftServiceTypeIds: [503, 502, 504], + rightServiceTypeIds: [505, 506, 507] + }; + } + + throw new Error(`unsupported districtKey: ${districtKey}`); +} + + // See PerDistrict.rb#does_students_export_include_rows_for_inactive_students? export function isStudentActive(districtKey, student) { if (districtKey === BEDFORD) return !student.missing_from_last_export; diff --git a/app/assets/javascripts/helpers/serviceColor.js b/app/assets/javascripts/helpers/serviceColor.js index f6a4a1f9ed..3f203f9b31 100644 --- a/app/assets/javascripts/helpers/serviceColor.js +++ b/app/assets/javascripts/helpers/serviceColor.js @@ -1,20 +1,9 @@ -export default function serviceColor(serviceTypeId) { - const Colors = { - purple: '#e8e9fc', - orange: '#ffe7d6', - green: '#e8fce8', - gray: '#eee' - }; - - const map = { - 507: Colors.gray, - 502: Colors.gray, - 503: Colors.gray, - 504: Colors.gray, - 505: Colors.gray, - 506: Colors.gray, - 508: Colors.gray - }; +const Colors = { + gray: '#eee' +}; - return map[serviceTypeId] || Colors.gray; +// These used to be colored differently, keeping the abstraction even though +// they aren't in current designs. +export default function serviceColor(serviceTypeId) { + return Colors.gray; } diff --git a/app/assets/javascripts/student_profile/RecordService.js b/app/assets/javascripts/student_profile/RecordService.js index 1771e93db1..efadbfaae3 100644 --- a/app/assets/javascripts/student_profile/RecordService.js +++ b/app/assets/javascripts/student_profile/RecordService.js @@ -4,6 +4,7 @@ import Datepicker from '../components/Datepicker'; import {toMoment} from '../helpers/toMoment'; import {merge} from '../helpers/merge'; import serviceColor from '../helpers/serviceColor'; +import {recordServiceChoices} from '../helpers/PerDistrict'; import {toSchoolYear, lastDayOfSchool} from '../helpers/schoolYear'; import ProvidedByEducatorDropdown from './ProvidedByEducatorDropdown'; @@ -115,6 +116,8 @@ export default class RecordService extends React.Component { } renderWhichService() { + const {districtKey} = this.context; + const {leftServiceTypeIds, rightServiceTypeIds} = recordServiceChoices(districtKey); return (
@@ -122,21 +125,17 @@ export default class RecordService extends React.Component {
- {this.renderServiceButton(503)} - {this.renderServiceButton(502)} - {this.renderServiceButton(504)} + {leftServiceTypeIds.map(this.renderServiceButton, this)}
- {this.renderServiceButton(505)} - {this.renderServiceButton(506)} - {this.renderServiceButton(507)} + {rightServiceTypeIds.map(this.renderServiceButton, this)}
); } - renderServiceButton(serviceTypeId, options) { + renderServiceButton(serviceTypeId) { const serviceText = this.props.serviceTypesIndex[serviceTypeId].name; const color = serviceColor(serviceTypeId); @@ -243,6 +242,9 @@ export default class RecordService extends React.Component { ); } } +RecordService.contextTypes = { + districtKey: PropTypes.string.isRequired +}; RecordService.propTypes = { studentFirstName: PropTypes.string.isRequired, studentId: PropTypes.number.isRequired, diff --git a/app/assets/javascripts/testing/fixtures/serviceTypesIndex.js b/app/assets/javascripts/testing/fixtures/serviceTypesIndex.js index cf7d9b68bc..964e51dd33 100644 --- a/app/assets/javascripts/testing/fixtures/serviceTypesIndex.js +++ b/app/assets/javascripts/testing/fixtures/serviceTypesIndex.js @@ -1,54 +1,125 @@ +/* to generate: +puts (ServiceType.all.reduce({}) do |map, service_type| + json = service_type.as_json(except: [:created_at, :updated_at]) + map.merge(service_type.id => json) +end).to_json;nil +*/ export default { "502": { "id": 502, - "name": "Attendance Officer" + "name": "Attendance Officer", + "summer_program": false, + "description": null, + "intensity": null, + "data_owner": null, + "fdescription": null }, "503": { "id": 503, - "name": "Attendance Contract" + "name": "Attendance Contract", + "summer_program": false, + "description": null, + "intensity": null, + "data_owner": null, + "fdescription": null }, "504": { "id": 504, - "name": "Behavior Contract" + "name": "Behavior Contract", + "summer_program": false, + "description": null, + "intensity": null, + "data_owner": null, + "fdescription": null }, "505": { "id": 505, - "name": "Counseling, in-house" + "name": "Counseling, in-house", + "summer_program": false, + "description": null, + "intensity": null, + "data_owner": null, + "fdescription": null }, "506": { "id": 506, - "name": "Counseling, outside" + "name": "Counseling, outside", + "summer_program": false, + "description": null, + "intensity": null, + "data_owner": null, + "fdescription": null }, "507": { "id": 507, - "name": "Reading intervention" + "name": "Reading intervention", + "summer_program": false, + "description": null, + "intensity": null, + "data_owner": null, + "fdescription": null }, "508": { "id": 508, - "name": "Math intervention" + "name": "Math intervention", + "summer_program": false, + "description": null, + "intensity": null, + "data_owner": null, + "fdescription": null }, "509": { "id": 509, - "name": "SomerSession" + "name": "SomerSession", + "summer_program": false, + "description": null, + "intensity": null, + "data_owner": null, + "fdescription": null }, "510": { "id": 510, - "name": "Summer Program for English Language Learners" + "name": "Summer Program for English Language Learners", + "summer_program": false, + "description": null, + "intensity": null, + "data_owner": null, + "fdescription": null }, "511": { "id": 511, - "name": "Afterschool Tutoring" + "name": "Afterschool Tutoring", + "summer_program": false, + "description": null, + "intensity": null, + "data_owner": null, + "fdescription": null }, "512": { "id": 512, - "name": "Freedom School" + "name": "Freedom School", + "summer_program": false, + "description": null, + "intensity": null, + "data_owner": null, + "fdescription": null }, "513": { "id": 513, - "name": "Community Schools" + "name": "Community Schools", + "summer_program": false, + "description": null, + "intensity": null, + "data_owner": null, + "fdescription": null }, "514": { "id": 514, - "name": "X-Block" + "name": "X-Block", + "summer_program": false, + "description": null, + "intensity": null, + "data_owner": null, + "fdescription": null } -}; +}; \ No newline at end of file From 8e2e223f9b6c41e3471b909948350cca27a202de Mon Sep 17 00:00:00 2001 From: kevinrobinson Date: Fri, 16 Aug 2019 12:25:53 -0400 Subject: [PATCH 02/21] Done generalizing for phaselines --- app/assets/javascripts/helpers/PerDistrict.js | 13 +++++++++++++ .../student_profile/LightAttendanceDetails.js | 9 +++++++-- .../student_profile/LightBehaviorDetails.js | 8 +++++++- .../javascripts/student_profile/ProfileBarChart.js | 5 ++--- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/helpers/PerDistrict.js b/app/assets/javascripts/helpers/PerDistrict.js index 840896c986..1616d352f3 100644 --- a/app/assets/javascripts/helpers/PerDistrict.js +++ b/app/assets/javascripts/helpers/PerDistrict.js @@ -250,6 +250,19 @@ export function recordServiceChoices(districtKey) { } +// What service types should be in included in phaseslines for non-academic +// information (eg, attendance, behavior, social or emotional?) +export function nonAcademicServiceTypeIdsForPhaselines(districtKey) { + if (districtKey === BEDFORD) { + return [703, 704, 702, 704, 709]; + } + if ([SOMERVILLE, DEMO, NEW_BEDFORD].indexOf(districtKey) !== -1) { + return [703, 704, 702, 704, 709]; + } + throw new Error(`unsupported districtKey: ${districtKey}`); +} + + // See PerDistrict.rb#does_students_export_include_rows_for_inactive_students? export function isStudentActive(districtKey, student) { if (districtKey === BEDFORD) return !student.missing_from_last_export; diff --git a/app/assets/javascripts/student_profile/LightAttendanceDetails.js b/app/assets/javascripts/student_profile/LightAttendanceDetails.js index 979c596d83..dfda7af3e7 100644 --- a/app/assets/javascripts/student_profile/LightAttendanceDetails.js +++ b/app/assets/javascripts/student_profile/LightAttendanceDetails.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import _ from 'lodash'; +import {nonAcademicServiceTypeIdsForPhaselines} from '../helpers/PerDistrict'; import ProfileBarChart, { tooltipEventTextFn, createUnsafeTooltipFormatter, @@ -10,8 +11,10 @@ import ProfileBarChart, { export default class LightAttendanceDetails extends React.Component { phaselines() { + const {districtKey} = this.context; const {activeServices, serviceTypesIndex} = this.props; - return servicePhaselines(activeServices, serviceTypesIndex); + const relevantServiceTypeIds = nonAcademicServiceTypeIdsForPhaselines(districtKey); + return servicePhaselines(relevantServiceTypeIds, activeServices, serviceTypesIndex); } render() { @@ -48,7 +51,9 @@ export default class LightAttendanceDetails extends React.Component { ); } } - +LightAttendanceDetails.contextTypes = { + districtKey: PropTypes.string.isRequired +}; LightAttendanceDetails.propTypes = { absences: PropTypes.array.isRequired, tardies: PropTypes.array.isRequired, diff --git a/app/assets/javascripts/student_profile/LightBehaviorDetails.js b/app/assets/javascripts/student_profile/LightBehaviorDetails.js index a810636e0e..563386c677 100644 --- a/app/assets/javascripts/student_profile/LightBehaviorDetails.js +++ b/app/assets/javascripts/student_profile/LightBehaviorDetails.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import {toMomentFromTimestamp, toMomentFromRailsDate} from '../helpers/toMoment'; import {toSchoolYear, firstDayOfSchool} from '../helpers/schoolYear'; +import {nonAcademicServiceTypeIdsForPhaselines} from '../helpers/PerDistrict'; import IncidentCard from '../feed/IncidentCard'; import DetailsSection from './DetailsSection'; import ProfileBarChart, {servicePhaselines} from './ProfileBarChart'; @@ -53,13 +54,15 @@ export default class LightBehaviorDetails extends React.Component { } servicePhaselines() { + const {districtKey} = this.context; const {activeServices, serviceTypesIndex} = this.props; const cutoffMoment = this.filterCutoffMoment(); const filteredPhaselines = activeServices.filter(service => { const phaselineMoment = toMomentFromRailsDate(service.date_started); return phaselineMoment.isAfter(cutoffMoment); }); - return servicePhaselines(filteredPhaselines, serviceTypesIndex); + const relevantServiceTypeIds = nonAcademicServiceTypeIdsForPhaselines(districtKey); + return servicePhaselines(relevantServiceTypeIds, filteredPhaselines, serviceTypesIndex); } onToggleCaseHistory() { @@ -140,6 +143,9 @@ export default class LightBehaviorDetails extends React.Component { ); } } +LightBehaviorDetails.contextTypes = { + districtKey: PropTypes.string.isRequired +}; LightBehaviorDetails.propTypes = { disciplineIncidents: PropTypes.array.isRequired, activeServices: PropTypes.arrayOf(PropTypes.shape({ diff --git a/app/assets/javascripts/student_profile/ProfileBarChart.js b/app/assets/javascripts/student_profile/ProfileBarChart.js index d34a4b1986..2747707d55 100644 --- a/app/assets/javascripts/student_profile/ProfileBarChart.js +++ b/app/assets/javascripts/student_profile/ProfileBarChart.js @@ -133,10 +133,9 @@ export function tooltipEventTextFn(e) { // Given a list of activeServices, return a list of `phaselines` that can be passed to `ProfileBarChart` // to render the services as phaseline interventions. -export function servicePhaselines(activeServices, serviceTypesIndex) { - const attendanceServiceTypes = [502, 503, 504, 505, 506]; +export function servicePhaselines(relevantServiceTypeIds, activeServices, serviceTypesIndex) { const attendanceServices = activeServices.filter(service => { - return (attendanceServiceTypes.indexOf(service.service_type_id) > -1); + return (relevantServiceTypeIds.indexOf(service.service_type_id) > -1); }); return attendanceServices.map(service => { From 753559507c01c837753babd8533c8897c913c01b Mon Sep 17 00:00:00 2001 From: kevinrobinson Date: Fri, 6 Sep 2019 12:14:40 -0400 Subject: [PATCH 03/21] Fix bugs in PerDistrict methods, and add tests to keep them from changing --- app/assets/javascripts/helpers/PerDistrict.js | 6 ++-- .../javascripts/helpers/PerDistrict.test.js | 34 ++++++++++++++++++- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/helpers/PerDistrict.js b/app/assets/javascripts/helpers/PerDistrict.js index 002a9ec44a..86554469f1 100644 --- a/app/assets/javascripts/helpers/PerDistrict.js +++ b/app/assets/javascripts/helpers/PerDistrict.js @@ -236,7 +236,7 @@ export function recordServiceChoices(districtKey) { if (districtKey === BEDFORD) { return { leftServiceTypeIds: [701, 708, 706, 707], - rightServiceTypeIds: [703, 704, 702, 704, 709] + rightServiceTypeIds: [703, 702, 705, 704, 709] }; } @@ -255,10 +255,10 @@ export function recordServiceChoices(districtKey) { // information (eg, attendance, behavior, social or emotional?) export function nonAcademicServiceTypeIdsForPhaselines(districtKey) { if (districtKey === BEDFORD) { - return [703, 704, 702, 704, 709]; + return [702, 703, 704, 705, 709]; } if ([SOMERVILLE, DEMO, NEW_BEDFORD].indexOf(districtKey) !== -1) { - return [703, 704, 702, 704, 709]; + return [502, 503, 504, 505, 506]; } throw new Error(`unsupported districtKey: ${districtKey}`); } diff --git a/app/assets/javascripts/helpers/PerDistrict.test.js b/app/assets/javascripts/helpers/PerDistrict.test.js index b0a13b1ba8..8de9a0474d 100644 --- a/app/assets/javascripts/helpers/PerDistrict.test.js +++ b/app/assets/javascripts/helpers/PerDistrict.test.js @@ -3,7 +3,9 @@ import { sortSchoolSlugsByGrade, studentTableEventNoteTypeIds, eventNoteTypeIdsForSearch, - hasActive504Plan + hasActive504Plan, + recordServiceChoices, + nonAcademicServiceTypeIdsForPhaselines } from './PerDistrict'; it('#shouldShowLowGradesBox', () => { @@ -59,4 +61,34 @@ describe('#hasActive504Plan', () => { expect(hasActive504Plan('Exited')).toEqual(false); expect(hasActive504Plan('unknown')).toEqual(false); }); +}); + +describe('#recordServiceChoices', () => { + it('works across districts', () => { + expect(recordServiceChoices('bedford')).toEqual({ + leftServiceTypeIds: [701, 708, 706, 707], + rightServiceTypeIds: [703, 702, 705, 704, 709] + }); + expect(recordServiceChoices('somerville')).toEqual({ + leftServiceTypeIds: [503, 502, 504], + rightServiceTypeIds: [505, 506, 507] + }); + expect(recordServiceChoices('new_bedford')).toEqual({ + leftServiceTypeIds: [503, 502, 504], + rightServiceTypeIds: [505, 506, 507] + }); + expect(recordServiceChoices('demo')).toEqual({ + leftServiceTypeIds: [503, 502, 504], + rightServiceTypeIds: [505, 506, 507] + }); + }); +}); + +describe('#nonAcademicServiceTypeIdsForPhaselines', () => { + it('works across districts', () => { + expect(nonAcademicServiceTypeIdsForPhaselines('bedford')).toEqual([702, 703, 704, 705, 709]); + expect(nonAcademicServiceTypeIdsForPhaselines('somerville')).toEqual([502, 503, 504, 505, 506]); + expect(nonAcademicServiceTypeIdsForPhaselines('new_bedford')).toEqual([502, 503, 504, 505, 506]); + expect(nonAcademicServiceTypeIdsForPhaselines('demo')).toEqual([502, 503, 504, 505, 506]); + }); }); \ No newline at end of file From bf6c6e844add0aef35e27426dd63d6d89db731d9 Mon Sep 17 00:00:00 2001 From: kevinrobinson Date: Fri, 6 Sep 2019 12:16:21 -0400 Subject: [PATCH 04/21] Rebuild serviceTypesIndex --- .../testing/fixtures/serviceTypesIndex.js | 171 ++++++++++++++---- 1 file changed, 131 insertions(+), 40 deletions(-) diff --git a/app/assets/javascripts/testing/fixtures/serviceTypesIndex.js b/app/assets/javascripts/testing/fixtures/serviceTypesIndex.js index 964e51dd33..f16954c3fa 100644 --- a/app/assets/javascripts/testing/fixtures/serviceTypesIndex.js +++ b/app/assets/javascripts/testing/fixtures/serviceTypesIndex.js @@ -11,8 +11,7 @@ export default { "summer_program": false, "description": null, "intensity": null, - "data_owner": null, - "fdescription": null + "data_owner": null }, "503": { "id": 503, @@ -20,8 +19,7 @@ export default { "summer_program": false, "description": null, "intensity": null, - "data_owner": null, - "fdescription": null + "data_owner": null }, "504": { "id": 504, @@ -29,8 +27,7 @@ export default { "summer_program": false, "description": null, "intensity": null, - "data_owner": null, - "fdescription": null + "data_owner": null }, "505": { "id": 505, @@ -38,8 +35,7 @@ export default { "summer_program": false, "description": null, "intensity": null, - "data_owner": null, - "fdescription": null + "data_owner": null }, "506": { "id": 506, @@ -47,8 +43,7 @@ export default { "summer_program": false, "description": null, "intensity": null, - "data_owner": null, - "fdescription": null + "data_owner": null }, "507": { "id": 507, @@ -56,8 +51,7 @@ export default { "summer_program": false, "description": null, "intensity": null, - "data_owner": null, - "fdescription": null + "data_owner": null }, "508": { "id": 508, @@ -65,61 +59,158 @@ export default { "summer_program": false, "description": null, "intensity": null, - "data_owner": null, - "fdescription": null + "data_owner": null + }, + "511": { + "id": 511, + "name": "Afterschool Tutoring", + "summer_program": false, + "description": null, + "intensity": null, + "data_owner": null + }, + "513": { + "id": 513, + "name": "Community Schools", + "summer_program": false, + "description": null, + "intensity": null, + "data_owner": null + }, + "514": { + "id": 514, + "name": "X-Block", + "summer_program": false, + "description": null, + "intensity": null, + "data_owner": null }, "509": { "id": 509, "name": "SomerSession", - "summer_program": false, + "summer_program": true, "description": null, "intensity": null, - "data_owner": null, - "fdescription": null + "data_owner": null }, "510": { "id": 510, "name": "Summer Program for English Language Learners", - "summer_program": false, + "summer_program": true, "description": null, "intensity": null, - "data_owner": null, - "fdescription": null + "data_owner": null }, - "511": { - "id": 511, - "name": "Afterschool Tutoring", + "512": { + "id": 512, + "name": "Freedom School", + "summer_program": true, + "description": null, + "intensity": null, + "data_owner": null + }, + "515": { + "id": 515, + "name": "Calculus Project", "summer_program": false, "description": null, "intensity": null, - "data_owner": null, - "fdescription": null + "data_owner": null }, - "512": { - "id": 512, - "name": "Freedom School", + "516": { + "id": 516, + "name": "Boston Breakthrough", "summer_program": false, "description": null, "intensity": null, - "data_owner": null, - "fdescription": null + "data_owner": null }, - "513": { - "id": 513, - "name": "Community Schools", + "517": { + "id": 517, + "name": "Summer Explore", "summer_program": false, "description": null, "intensity": null, - "data_owner": null, - "fdescription": null + "data_owner": null }, - "514": { - "id": 514, - "name": "X-Block", + "518": { + "id": 518, + "name": "Focused Math Intervention", "summer_program": false, "description": null, "intensity": null, - "data_owner": null, - "fdescription": null + "data_owner": null + }, + "701": { + "id": 701, + "name": "Title 1 Math intervention", + "summer_program": false, + "description": "Title One Math afterschool program", + "intensity": "2 x 45", + "data_owner": "AP at Lane, this is only a Lane program" + }, + "702": { + "id": 702, + "name": "Lunch bunch", + "summer_program": false, + "description": "Small group lunch bunch with counseling staff focusing on social skills", + "intensity": "1x20min", + "data_owner": "counselor" + }, + "703": { + "id": 703, + "name": "Soc.emo check in", + "summer_program": false, + "description": "Short regular check ins", + "intensity": "1-3x 10min", + "data_owner": "counselor" + }, + "704": { + "id": 704, + "name": "Individual Counseling", + "summer_program": false, + "description": "1:1 counseling sessions", + "intensity": "1x30 min", + "data_owner": "counselor" + }, + "705": { + "id": 705, + "name": "Social Group", + "summer_program": false, + "description": "Small groups", + "intensity": "1 or 2 x 30", + "data_owner": "counselor" + }, + "706": { + "id": 706, + "name": "Reading intervention, with specialist", + "summer_program": false, + "description": "Specific documented reading intervention", + "intensity": "2-3 x 30", + "data_owner": "specialist" + }, + "707": { + "id": 707, + "name": "LLI Reading Instruction", + "summer_program": false, + "description": "Leveled Literacy Instruction by trained staff", + "intensity": "5x30", + "data_owner": "specialist" + }, + "708": { + "id": 708, + "name": "Math Intervention, small group", + "summer_program": false, + "description": "Interventions developed with math CC, typically delivered in small group, occasionally one on one", + "intensity": "Varies 2-5x per week", + "data_owner": "Classroom staff w/ math CC consult" + }, + "709": { + "id": 709, + "name": "Formal Behavior Plan", + "summer_program": false, + "description": "Behavior plan written by or in consultation with BCBA", + "intensity": "varies", + "data_owner": "BCBA" } }; \ No newline at end of file From b2f059445f917022ff89e3a69b9b4f407f5d2654 Mon Sep 17 00:00:00 2001 From: kevinrobinson Date: Fri, 6 Sep 2019 12:29:26 -0400 Subject: [PATCH 05/21] Done updating RecordService tests, but no new examples yet --- .../student_profile/RecordService.js | 1 + .../student_profile/RecordService.test.js | 66 +++++++++---------- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/app/assets/javascripts/student_profile/RecordService.js b/app/assets/javascripts/student_profile/RecordService.js index efadbfaae3..8dc90108c9 100644 --- a/app/assets/javascripts/student_profile/RecordService.js +++ b/app/assets/javascripts/student_profile/RecordService.js @@ -141,6 +141,7 @@ export default class RecordService extends React.Component { return ( ); } @@ -334,13 +336,17 @@ const styles = { fontSize: 12, background: '#eee', // override CSS color: 'black', - width: 135, // tuned for three columns at min screen width - height: 60, + width: '14em', // tuned for two columns, when page at max screen width + height: 45, display: 'flex', justifyContent: 'center', alignItems: 'center', padding: 8 }, + serviceButtonText: { + whiteSpace: 'nowrap', + overflow: 'hidden', // in case overflow + }, infoBox: { margin: 10, fontSize: 12, diff --git a/app/models/educator_label.rb b/app/models/educator_label.rb index 5f791770f0..f055def5bf 100644 --- a/app/models/educator_label.rb +++ b/app/models/educator_label.rb @@ -26,7 +26,8 @@ class EducatorLabel < ApplicationRecord # profile 'enable_viewing_educators_with_access_to_student', - + 'show_services_info', + # transition notes 'k8_counselor', 'high_school_house_master', From ecb0a03d1bceea5b4edbdb7122d0621884d35f1a Mon Sep 17 00:00:00 2001 From: kevinrobinson Date: Wed, 9 Oct 2019 14:07:28 -0400 Subject: [PATCH 14/21] Tooltip, test buttons across districts --- .../student_profile/RecordService.js | 16 +++- .../student_profile/RecordService.test.js | 80 ++++++++++++++++--- app/models/educator_label.rb | 2 +- 3 files changed, 84 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/student_profile/RecordService.js b/app/assets/javascripts/student_profile/RecordService.js index 0b8ecebbe4..908a89b1a3 100644 --- a/app/assets/javascripts/student_profile/RecordService.js +++ b/app/assets/javascripts/student_profile/RecordService.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import _ from 'lodash'; import Datepicker from '../components/Datepicker'; import Nbsp from '../components/Nbsp'; import {toMoment} from '../helpers/toMoment'; @@ -137,7 +138,7 @@ export default class RecordService extends React.Component { ); } - + renderServiceInfoBox(serviceTypeId) { const {currentEducator, serviceTypesIndex, servicesInfoDocUrl} = this.props; if (currentEducator.labels.indexOf('show_services_info') === -1) return null; @@ -182,6 +183,17 @@ export default class RecordService extends React.Component { ); } + renderServiceInfoText(serviceTypeId) { + const {serviceTypesIndex} = this.props; + const service = serviceTypesIndex[serviceTypeId]; + return _.compact([ + service.name, + service.description ? service.description : null, + service.intensity ? service.intensity : null, + service.data_owner ? `Data owner: ${service.data_owner}` : null + ]).join("\n"); + } + renderServiceButton(serviceTypeId) { const serviceText = this.props.serviceTypesIndex[serviceTypeId].name; const color = serviceColor(serviceTypeId); @@ -192,7 +204,7 @@ export default class RecordService extends React.Component { className={`btn service-type service-type-${serviceTypeId}`} onClick={this.onServiceClicked.bind(this, serviceTypeId)} tabIndex={-1} - title={serviceText} + title={this.renderServiceInfoText(serviceTypeId)} style={merge(styles.serviceButton, { background: color, outline: 0, diff --git a/app/assets/javascripts/student_profile/RecordService.test.js b/app/assets/javascripts/student_profile/RecordService.test.js index eb8b87eaed..aba35bb90b 100644 --- a/app/assets/javascripts/student_profile/RecordService.test.js +++ b/app/assets/javascripts/student_profile/RecordService.test.js @@ -26,6 +26,17 @@ export function testProps(props) { }; } +function propsWithServiceInfoLabel(props = {}) { + return { + ...props, + servicesInfoDocUrl: 'https://example.com/', + currentEducator: { + ...props.currentEducator, + labels: ['show_services_info'] + } + }; +} + function forDistrict(el, districtKey) { return {el}; } @@ -44,7 +55,7 @@ function testRender(props, options = {}) { const helpers = { serviceTypes(el) { return $(el).find('.btn.service-type').toArray().map(el => { - return el.innerHTML.trim(); + return $(el).text().trim(); }); }, @@ -99,17 +110,7 @@ const helpers = { describe('integration tests', () => { it('renders dialog for recording services', () => { const {el} = testRender(testProps()); - expect($(el).text()).toContain('Which service?'); - expect(helpers.serviceTypes(el)).toEqual([ - 'Attendance Contract', - 'Attendance Officer', - 'Behavior Contract', - 'Counseling, in-house', - 'Counseling, outside', - 'Reading intervention' - ]); - expect($(el).text()).toContain('Who is working with Tamyra?'); // TODO (as): test staff dropdown autocomplete async @@ -125,6 +126,63 @@ describe('integration tests', () => { expect(helpers.isSaveButtonEnabled(el)).toEqual(false); }); + describe('shows different service types across districts', () => { + it('works for bedford', () => { + const props = propsWithServiceInfoLabel(testProps()); + const {el} = testRender(props, {districtKey: 'bedford'}); + expect(helpers.serviceTypes(el)).toEqual([ + 'Soc.emo check in', + 'Lunch bunch', + 'Social Group', + 'Individual Counseling', + 'Formal Behavior Plan', + 'LLI Reading Instruction', + 'Reading intervention, with specialist', + 'Title 1 Math intervention', + 'Math Intervention, small group', + ]); + }); + + it('works for somerville', () => { + const props = propsWithServiceInfoLabel(testProps()); + const {el} = testRender(props, {districtKey: 'somerville'}); + expect(helpers.serviceTypes(el)).toEqual([ + 'Attendance Contract', + 'Attendance Officer', + 'Behavior Contract', + 'Counseling, in-house', + 'Counseling, outside', + 'Reading intervention' + ]); + }); + + it('works for new_bedford', () => { + const props = propsWithServiceInfoLabel(testProps()); + const {el} = testRender(props, {districtKey: 'new_bedford'}); + expect(helpers.serviceTypes(el)).toEqual([ + 'Attendance Contract', + 'Attendance Officer', + 'Behavior Contract', + 'Counseling, in-house', + 'Counseling, outside', + 'Reading intervention' + ]); + }); + + it('works for demo', () => { + const props = propsWithServiceInfoLabel(testProps()); + const {el} = testRender(props, {districtKey: 'demo'}); + expect(helpers.serviceTypes(el)).toEqual([ + 'Attendance Contract', + 'Attendance Officer', + 'Behavior Contract', + 'Counseling, in-house', + 'Counseling, outside', + 'Reading intervention' + ]); + }); + }); + describe('validation', () => { it('shows warning on invalid start date', () => { const {el} = testRender(testProps()); diff --git a/app/models/educator_label.rb b/app/models/educator_label.rb index f055def5bf..bb18aecde0 100644 --- a/app/models/educator_label.rb +++ b/app/models/educator_label.rb @@ -27,7 +27,7 @@ class EducatorLabel < ApplicationRecord # profile 'enable_viewing_educators_with_access_to_student', 'show_services_info', - + # transition notes 'k8_counselor', 'high_school_house_master', From 31102c53a2693cdcb498b6aa6662e756e1aa7780 Mon Sep 17 00:00:00 2001 From: kevinrobinson Date: Mon, 21 Oct 2019 15:21:29 -0400 Subject: [PATCH 15/21] Fix server-side typo on services_info_doc_url --- app/config_objects/per_district.rb | 2 +- app/controllers/profile_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config_objects/per_district.rb b/app/config_objects/per_district.rb index d3bed11877..3a46a7055e 100644 --- a/app/config_objects/per_district.rb +++ b/app/config_objects/per_district.rb @@ -616,7 +616,7 @@ def include_bedford_end_of_year_transition? end end - def service_info_doc_url + def services_info_doc_url ENV.fetch('SERVICES_INFO_DOC_URL', nil) end diff --git a/app/controllers/profile_controller.rb b/app/controllers/profile_controller.rb index 521691e628..92015e8bf3 100644 --- a/app/controllers/profile_controller.rb +++ b/app/controllers/profile_controller.rb @@ -30,7 +30,7 @@ def json tardies: filtered_events(student.tardies), absences: filtered_events(student.absences) }, - service_info_doc_url: PerDistrict.new.service_info_doc_url + services_info_doc_url: PerDistrict.new.services_info_doc_url } end From b857a2e8b83cef1e332e806c237c06394ebc5001 Mon Sep 17 00:00:00 2001 From: kevinrobinson Date: Mon, 21 Oct 2019 15:33:39 -0400 Subject: [PATCH 16/21] Add full expectation for service types as separate spec --- spec/controllers/profile_controller_spec.rb | 247 +++++++++++++++++--- 1 file changed, 217 insertions(+), 30 deletions(-) diff --git a/spec/controllers/profile_controller_spec.rb b/spec/controllers/profile_controller_spec.rb index 3bcaf758e8..df03202f26 100644 --- a/spec/controllers/profile_controller_spec.rb +++ b/spec/controllers/profile_controller_spec.rb @@ -263,7 +263,8 @@ def create_ed_plan!(attrs = {}) 'latest_iep_document', 'sections', 'current_educator_allowed_sections', - 'attendance_data' + 'attendance_data', + 'services_info_doc_url' ]) expect(json['current_educator']['id']).to eq educator['id'] expect(json['current_educator']['email']).to eq educator['email'] @@ -290,35 +291,6 @@ def create_ed_plan!(attrs = {}) } }) - expect(json['service_types_index']).to eq({ - '502' => {'id'=>502, 'name'=>"Attendance Officer"}, - '503' => {'id'=>503, 'name'=>"Attendance Contract"}, - '504' => {'id'=>504, 'name'=>"Behavior Contract"}, - '505' => {'id'=>505, 'name'=>"Counseling, in-house"}, - '506' => {'id'=>506, 'name'=>"Counseling, outside"}, - '507' => {'id'=>507, 'name'=>"Reading intervention"}, - '508' => {'id'=>508, 'name'=>"Math intervention"}, - '509' => {'id'=>509, 'name'=>"SomerSession"}, - '510' => {'id'=>510, 'name'=>"Summer Program for English Language Learners"}, - '511' => {'id'=>511, 'name'=>"Afterschool Tutoring"}, - '512' => {'id'=>512, 'name'=>"Freedom School"}, - '513' => {'id'=>513, 'name'=>"Community Schools"}, - '514' => {'id'=>514, 'name'=>"X-Block"}, - '515' => {'id'=>515, 'name'=>"Calculus Project"}, - '516' => {'id'=>516, 'name'=>"Boston Breakthrough"}, - '517' => {'id'=>517, 'name'=>"Summer Explore"}, - '518' => {'id'=>518, 'name'=>"Focused Math Intervention"}, - '701' => {'id'=>701, 'name'=>"Title 1 Math intervention"}, - '702' => {'id'=>702, 'name'=>"Lunch bunch"}, - '703' => {'id'=>703, 'name'=>"Soc.emo check in"}, - '704' => {'id'=>704, 'name'=>"Individual Counseling"}, - '705' => {'id'=>705, 'name'=>"Social Group"}, - '706' => {'id'=>706, 'name'=>"Reading intervention, with specialist"}, - '707' => {'id'=>707, 'name'=>"LLI Reading Instruction"}, - '708' => {'id'=>708, 'name'=>"Math Intervention, small group"}, - '709' => {'id'=>709, 'name'=>"Formal Behavior Plan"} - }) - expect(json['educators_index']).to include({ educator.id.to_s => { 'id'=>educator.id, @@ -340,6 +312,221 @@ def create_ed_plan!(attrs = {}) expect(json['current_educator_allowed_sections']).to include(section.id) end + it 'returns accurate data for service types' do + make_request(educator, student.id) + json = parse_json(response.body) + expect(json['service_types_index']).to eq({ + "502"=>{ + "id"=>502, + "name"=>"Attendance Officer", + "summer_program"=>false, + "description"=>nil, + "intensity"=>nil, + "data_owner"=>nil + }, + "503"=>{ + "id"=>503, + "name"=>"Attendance Contract", + "summer_program"=>false, + "description"=>nil, + "intensity"=>nil, + "data_owner"=>nil + }, + "504"=>{ + "id"=>504, + "name"=>"Behavior Contract", + "summer_program"=>false, + "description"=>nil, + "intensity"=>nil, + "data_owner"=>nil + }, + "505"=>{ + "id"=>505, + "name"=>"Counseling, in-house", + "summer_program"=>false, + "description"=>nil, + "intensity"=>nil, + "data_owner"=>nil + }, + "506"=>{ + "id"=>506, + "name"=>"Counseling, outside", + "summer_program"=>false, + "description"=>nil, + "intensity"=>nil, + "data_owner"=>nil + }, + "507"=>{ + "id"=>507, + "name"=>"Reading intervention", + "summer_program"=>false, + "description"=>nil, + "intensity"=>nil, + "data_owner"=>nil + }, + "508"=>{ + "id"=>508, + "name"=>"Math intervention", + "summer_program"=>false, + "description"=>nil, + "intensity"=>nil, + "data_owner"=>nil + }, + "511"=>{ + "id"=>511, + "name"=>"Afterschool Tutoring", + "summer_program"=>false, + "description"=>nil, + "intensity"=>nil, + "data_owner"=>nil + }, + "513"=>{ + "id"=>513, + "name"=>"Community Schools", + "summer_program"=>false, + "description"=>nil, + "intensity"=>nil, + "data_owner"=>nil + }, + "514"=>{ + "id"=>514, + "name"=>"X-Block", + "summer_program"=>false, + "description"=>nil, + "intensity"=>nil, + "data_owner"=>nil + }, + "509"=>{ + "id"=>509, + "name"=>"SomerSession", + "summer_program"=>true, + "description"=>nil, + "intensity"=>nil, + "data_owner"=>nil + }, + "510"=>{ + "id"=>510, + "name"=>"Summer Program for English Language Learners", + "summer_program"=>true, + "description"=>nil, + "intensity"=>nil, + "data_owner"=>nil + }, + "512"=>{ + "id"=>512, + "name"=>"Freedom School", + "summer_program"=>true, + "description"=>nil, + "intensity"=>nil, + "data_owner"=>nil + }, + "515"=>{ + "id"=>515, + "name"=>"Calculus Project", + "summer_program"=>false, + "description"=>nil, + "intensity"=>nil, + "data_owner"=>nil + }, + "516"=>{ + "id"=>516, + "name"=>"Boston Breakthrough", + "summer_program"=>false, + "description"=>nil, + "intensity"=>nil, + "data_owner"=>nil + }, + "517"=>{ + "id"=>517, + "name"=>"Summer Explore", + "summer_program"=>false, + "description"=>nil, + "intensity"=>nil, + "data_owner"=>nil + }, + "518"=>{ + "id"=>518, + "name"=>"Focused Math Intervention", + "summer_program"=>false, + "description"=>nil, + "intensity"=>nil, + "data_owner"=>nil + }, + "701"=>{ + "id"=>701, + "name"=>"Title 1 Math intervention", + "summer_program"=>false, + "description"=>"Title One Math afterschool program", + "intensity"=>"2 x 45", + "data_owner"=>"AP at Lane, this is only a Lane program" + }, + "702"=>{ + "id"=>702, + "name"=>"Lunch bunch", + "summer_program"=>false, + "description"=>"Small group lunch bunch with counseling staff focusing on social skills", + "intensity"=>"1x20min", + "data_owner"=>"counselor" + }, + "703"=>{ + "id"=>703, + "name"=>"Soc.emo check in", + "summer_program"=>false, + "description"=>"Short regular check ins", + "intensity"=>"1-3x 10min", + "data_owner"=>"counselor" + }, + "704"=>{ + "id"=>704, + "name"=>"Individual Counseling", + "summer_program"=>false, + "description"=>"1:1 counseling sessions", + "intensity"=>"1x30 min", + "data_owner"=>"counselor" + }, + "705"=>{ + "id"=>705, + "name"=>"Social Group", + "summer_program"=>false, + "description"=>"Small groups", + "intensity"=>"1 or 2 x 30", + "data_owner"=>"counselor" + }, + "706"=>{ + "id"=>706, + "name"=>"Reading intervention, with specialist", + "summer_program"=>false, + "description"=>"Specific documented reading intervention", + "intensity"=>"2-3 x 30", + "data_owner"=>"specialist" + }, + "707"=>{ + "id"=>707, + "name"=>"LLI Reading Instruction", + "summer_program"=>false, + "description"=>"Leveled Literacy Instruction by trained staff", + "intensity"=>"5x30", + "data_owner"=>"specialist" + }, + "708"=>{ + "id"=>708, + "name"=>"Math Intervention, small group", + "summer_program"=>false, + "description"=>"Interventions developed with math CC, typically delivered in small group, occasionally one on one", + "intensity"=>"Varies 2-5x per week", + "data_owner"=>"Classroom staff w/ math CC consult" + }, + "709"=>{ + "id"=>709, + "name"=>"Formal Behavior Plan", + "summer_program"=>false, + "description"=>"Behavior plan written by or in consultation with BCBA", + "intensity"=>"varies", + "data_owner"=>"BCBA" + } + }) + end + context 'student has multiple discipline incidents' do let!(:student) { FactoryBot.create(:student, school: school) } let(:most_recent_school_year) { student.most_recent_school_year } From 417953c6ac4b2f4321ad1af70d0677f36720d9a6 Mon Sep 17 00:00:00 2001 From: kevinrobinson Date: Mon, 21 Oct 2019 15:51:24 -0400 Subject: [PATCH 17/21] Working out specs for recording and list --- .../student_profile/LightHelpBubble.js | 2 +- .../student_profile/RecordService.js | 2 +- .../student_profile/RecordService.test.js | 26 ++++++++ .../student_profile/ServicesList.test.js | 63 +++++++++++-------- 4 files changed, 64 insertions(+), 29 deletions(-) diff --git a/app/assets/javascripts/student_profile/LightHelpBubble.js b/app/assets/javascripts/student_profile/LightHelpBubble.js index 9ea4117f47..195d7b715a 100644 --- a/app/assets/javascripts/student_profile/LightHelpBubble.js +++ b/app/assets/javascripts/student_profile/LightHelpBubble.js @@ -33,7 +33,7 @@ export function questionMarkEl(pixelSize, style = {}) { ...style }; return ( - + diff --git a/app/assets/javascripts/student_profile/RecordService.js b/app/assets/javascripts/student_profile/RecordService.js index 908a89b1a3..20e5f4f7dd 100644 --- a/app/assets/javascripts/student_profile/RecordService.js +++ b/app/assets/javascripts/student_profile/RecordService.js @@ -170,7 +170,7 @@ export default class RecordService extends React.Component { // show info return ( -
+
{service.description &&
{service.description}
} {service.intensity &&
{service.intensity}
} {service.data_owner &&
Data owner: {service.data_owner}
} diff --git a/app/assets/javascripts/student_profile/RecordService.test.js b/app/assets/javascripts/student_profile/RecordService.test.js index aba35bb90b..c8858a936c 100644 --- a/app/assets/javascripts/student_profile/RecordService.test.js +++ b/app/assets/javascripts/student_profile/RecordService.test.js @@ -71,6 +71,10 @@ const helpers = { return $(el).find('.datepicker').get(1); }, + findServiceInfoText(el) { + return $(el).find('.RecordService-service-info-box').text(); + }, + isSaveButtonEnabled(el) { return helpers.findSaveButton(el).attr('disabled') !== 'disabled'; }, @@ -183,6 +187,28 @@ describe('integration tests', () => { }); }); + describe('service info', () => { + it('shows no info at first, then shows details on click', () => { + const props = propsWithServiceInfoLabel(testProps()); + const {el} = testRender(props, {districtKey: 'bedford'}); + expect(helpers.findServiceInfoText(el)).toEqual(''); + helpers.simulateClickOnService(el, 703); + expect(helpers.findServiceInfoText(el)).toEqual([ + 'Short regular check ins', + '1-3x 10min', + 'Data owner: counselor', + 'Learn more' + ].join('')); + }); + + it('shows nothing unless URL and label are set', () => { + const props = testProps(); + const {el} = testRender(props, {districtKey: 'bedford'}); + helpers.simulateClickOnService(el, 703); + expect(helpers.findServiceInfoText(el)).toEqual(''); + }); + }); + describe('validation', () => { it('shows warning on invalid start date', () => { const {el} = testRender(testProps()); diff --git a/app/assets/javascripts/student_profile/ServicesList.test.js b/app/assets/javascripts/student_profile/ServicesList.test.js index 37f085b489..5ed5256875 100644 --- a/app/assets/javascripts/student_profile/ServicesList.test.js +++ b/app/assets/javascripts/student_profile/ServicesList.test.js @@ -5,6 +5,27 @@ import {studentProfile} from './fixtures/fixtures'; import {withDefaultNowContext} from '../testing/NowContainer'; import ServicesList from './ServicesList'; +function testProps(props = {}) { + return { + servicesFeed: { + active: [], + discontinued: [] + }, + educatorsIndex: studentProfile.educatorsIndex, + serviceTypesIndex: studentProfile.serviceTypesIndex, + discontinueServiceRequests: {}, + onClickDiscontinueService: jest.fn(), + servicesInfoDocUrl: 'https://example.com/info', + ...props + }; +} + +function testRender(props) { + const el = document.createElement('div'); + ReactDOM.render(withDefaultNowContext(), el); + return {el}; +} + const helpers = { emptyServicesFeed() { return { active: [], discontinued: [] }; @@ -29,73 +50,61 @@ const helpers = { discontinued_by_educator_id: null, discontinued_recorded_at: null }; - }, - - renderInto(el, props) { - const mergedProps = { - servicesFeed: { - active: [], - discontinued: [] - }, - educatorsIndex: studentProfile.educatorsIndex, - serviceTypesIndex: studentProfile.serviceTypesIndex, - discontinueServiceRequests: {}, - onClickDiscontinueService: jest.fn(), - ...props - }; - ReactDOM.render(withDefaultNowContext(), el); } }; describe('high-level integration tests', () => { it('renders message when no services', () => { - const el = document.createElement('div'); - helpers.renderInto(el, { servicesFeed: helpers.emptyServicesFeed() }); + const {el} = testRender(testProps({ servicesFeed: helpers.emptyServicesFeed() })); expect($(el).text()).toContain('No services'); }); it('renders everything on the happy path', () => { - const el = document.createElement('div'); - helpers.renderInto(el, { servicesFeed: helpers.oneActiveServiceFeed() }); + const {el} = testRender(testProps({ + servicesFeed: helpers.oneActiveServiceFeed(), + })); expect($(el).text()).toContain('Reading intervention'); expect($(el).text()).toContain('With'); expect($(el).text()).toContain('Started April 3, 2016'); expect($(el).text()).toContain('Discontinue'); + expect($(el).find('.LightHelpBubble-question-mark').length).toEqual(1); + }); + + it('no service info if servicesInfoDocUrl not set', () => { + const {el} = testRender(testProps({ servicesInfoDocUrl: null })); + expect($(el).find('.LightHelpBubble-question-mark').length).toEqual(0); }); it('asks for confirmation before discontinuing', () => { - const el = document.createElement('div'); - helpers.renderInto(el, { servicesFeed: helpers.oneActiveServiceFeed() }); + const {el} = testRender(testProps({ servicesFeed: helpers.oneActiveServiceFeed() })); ReactTestUtils.Simulate.click($(el).find('.btn').get(0)); expect($(el).text()).toContain('Confirm'); expect($(el).text()).toContain('Cancel'); }); it('shows a message when request in progress', () => { - const el = document.createElement('div'); const service = helpers.fixtureService(); - helpers.renderInto(el, { + const {el} = testRender(testProps({ servicesFeed: helpers.oneActiveServiceFeed(), discontinueServiceRequests: { [service.id]: 'pending' } - }); + })); expect($(el).find('.btn').text()).toEqual('Updating...'); }); it('renders discontinued services correctly', () => { - const el = document.createElement('div'); const discontinuedService = { ...helpers.fixtureService(), discontinued_by_educator_id: 1, discontinued_recorded_at: "2016-04-05T01:43:15.256Z" }; - helpers.renderInto(el, { + const {el} = testRender(testProps({ servicesFeed: { active: [], discontinued: [discontinuedService] } - }); + })); expect($(el).text()).toContain('Ended'); expect($(el).text()).toContain('April 5, 2016'); }); From f3ec90cc8ea2c1bd9e9f32eb71c995ba3d0939c1 Mon Sep 17 00:00:00 2001 From: kevinrobinson Date: Mon, 21 Oct 2019 16:23:12 -0400 Subject: [PATCH 18/21] Specs for various cases on info or not" --- .../student_profile/RecordService.js | 6 ++- .../student_profile/RecordService.test.js | 42 +++++++++++++++---- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/student_profile/RecordService.js b/app/assets/javascripts/student_profile/RecordService.js index 20e5f4f7dd..0bfed075bf 100644 --- a/app/assets/javascripts/student_profile/RecordService.js +++ b/app/assets/javascripts/student_profile/RecordService.js @@ -134,7 +134,9 @@ export default class RecordService extends React.Component { {rightServiceTypeIds.map(this.renderServiceButton, this)}
- {this.renderServiceInfoBox(serviceTypeId)} +
+ {this.renderServiceInfoBox(serviceTypeId)} +
); } @@ -170,7 +172,7 @@ export default class RecordService extends React.Component { // show info return ( -
+
{service.description &&
{service.description}
} {service.intensity &&
{service.intensity}
} {service.data_owner &&
Data owner: {service.data_owner}
} diff --git a/app/assets/javascripts/student_profile/RecordService.test.js b/app/assets/javascripts/student_profile/RecordService.test.js index c8858a936c..42a62c4e77 100644 --- a/app/assets/javascripts/student_profile/RecordService.test.js +++ b/app/assets/javascripts/student_profile/RecordService.test.js @@ -59,6 +59,10 @@ const helpers = { }); }, + findServiceButton(el, serviceTypeId) { + return $(el).find(`.btn.service-type-${serviceTypeId}`).get(0); + }, + findSaveButton(el) { return $(el).find('.btn.save'); }, @@ -72,7 +76,7 @@ const helpers = { }, findServiceInfoText(el) { - return $(el).find('.RecordService-service-info-box').text(); + return $(el).find('.RecordService-service-info-box').text().trim(); }, isSaveButtonEnabled(el) { @@ -84,7 +88,8 @@ const helpers = { }, simulateClickOnService(el, serviceTypeId) { - ReactTestUtils.Simulate.click($(el).find(`.btn.service-type-${serviceTypeId}`).get(0)); + const buttonEl = helpers.findServiceButton(el, serviceTypeId); + ReactTestUtils.Simulate.click(buttonEl); }, simulateStartDateChange(el, text) { @@ -188,10 +193,29 @@ describe('integration tests', () => { }); describe('service info', () => { + it('shows nothing unless URL and label are set', () => { + const props = testProps(); + const {el} = testRender(props, {districtKey: 'bedford'}); + helpers.simulateClickOnService(el, 703); + expect(helpers.findServiceInfoText(el)).toEqual(''); + }); + + it('shows service info as tooltip', () => { + const props = propsWithServiceInfoLabel(testProps()); + const {el} = testRender(props, {districtKey: 'bedford'}); + const buttonEl = helpers.findServiceButton(el, 703); + expect(buttonEl.getAttribute('title')).toEqual([ + 'Soc.emo check in', + 'Short regular check ins', + '1-3x 10min', + 'Data owner: counselor' + ].join('\n')); + }); + it('shows no info at first, then shows details on click', () => { const props = propsWithServiceInfoLabel(testProps()); const {el} = testRender(props, {districtKey: 'bedford'}); - expect(helpers.findServiceInfoText(el)).toEqual(''); + expect(helpers.findServiceInfoText(el)).toEqual('Select a service for more description.'); helpers.simulateClickOnService(el, 703); expect(helpers.findServiceInfoText(el)).toEqual([ 'Short regular check ins', @@ -201,12 +225,12 @@ describe('integration tests', () => { ].join('')); }); - it('shows nothing unless URL and label are set', () => { - const props = testProps(); - const {el} = testRender(props, {districtKey: 'bedford'}); - helpers.simulateClickOnService(el, 703); - expect(helpers.findServiceInfoText(el)).toEqual(''); - }); + it('says no info explicitly if enabled but no info present', () => { + const props = propsWithServiceInfoLabel(testProps()); + const {el} = testRender(props, {districtKey: 'somerville'}); + helpers.simulateClickOnService(el, 502); + expect(helpers.findServiceInfoText(el)).toEqual('No service info found.'); + }); }); describe('validation', () => { From 1f3312b90e406c321f1deacb63f2da12cfbdbf1f Mon Sep 17 00:00:00 2001 From: kevinrobinson Date: Mon, 21 Oct 2019 17:25:30 -0400 Subject: [PATCH 19/21] Add specs for phaselines on behavior details page,, mocking highcharts component' --- .../student_profile/LightBehaviorDetails.js | 1 - .../LightBehaviorDetails.test.js | 52 +++++++++++++++++-- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/student_profile/LightBehaviorDetails.js b/app/assets/javascripts/student_profile/LightBehaviorDetails.js index cd6c434635..b8c8e0a2fe 100644 --- a/app/assets/javascripts/student_profile/LightBehaviorDetails.js +++ b/app/assets/javascripts/student_profile/LightBehaviorDetails.js @@ -97,7 +97,6 @@ export default class LightBehaviorDetails extends React.Component { renderDisciplineIncidents(filteredDisciplineIncidents) { const phaselines = this.servicePhaselines().concat(this.filteredDataPhaselines()); - return ( ), el); + ReactDOM.render(withDefaultTestContext(, context), el); return el; } +// Read them from the mocked component +function parseHighchartPlotLineLabelsForDisciplineChart(el) { + const json = $(el).find('#disciplineChart .Mocked-HighchartsWrapper').text(); + const highchartOptions = JSON.parse(json); + return highchartOptions.xAxis[0].plotLines.map(plotLine => plotLine.label.text); +} + +// KR: I couldn't get assertions working to verify that HighCharts +// draws this on the page, even with delay or trying to explicitly +// set sizing on DOM element containers; this clearly works as expected +// in dev, story and production, so punting for now by mocking the module +// and serializing some props for simpler assertions. +jest.mock('../components/HighchartsWrapper', () => + props =>
{JSON.stringify(props)}
+); + + it('renders empty data without crashing', () => { testRender(testProps()); }); @@ -83,8 +100,8 @@ it('always hides older data by default', () => { expect($(el).text()).toContain('A note about student privacy'); expect($(el).html()).toContain('Something happened recently'); expect($(el).html()).not.toContain('Something else happened a long time ago...'); - expect($(el).find('svg').length).toEqual(2); //now also renders a heatmap element - // test setup isn't working for highcharts contents + expect($(el).find('.Mocked-HighchartsWrapper').length).toEqual(2); // test setup isn't working for full highcharts content + expect($(el).find('.DisciplineScatterPlot').length).toEqual(1); }); it('allows showing older data when access', () => { @@ -94,3 +111,30 @@ it('allows showing older data when access', () => { expect($(el).text()).toContain('hide full case history'); expect($(el).html()).toContain('Something else happened a long time ago...'); }); + +it('can pass props propertly to draw phaselines for relevant services', () => { + const activeServices = [{ + service_type_id: 502, + date_started: '2018-02-18T07:22:11.123Z' + }]; + const el = testRender(testProps({activeServices})); + expect(parseHighchartPlotLineLabelsForDisciplineChart(el)).toEqual([ + 'Started Attendance Officer', + 'Only one year of data is shown to respect privacy' + ]); +}); + +it('ignores services if not included in nonAcademicServiceTypeIdsForPhaselines', () => { + const activeServices = [{ + service_type_id: 707, // not relevant + date_started: '2018-02-18T07:22:11.123Z' + }, { + service_type_id: 709, // include + date_started: '2018-01-11T07:22:11.123Z' + }]; + const el = testRender(testProps({activeServices}), {districtKey: 'bedford'}); + expect(parseHighchartPlotLineLabelsForDisciplineChart(el)).toEqual([ + 'Started Formal Behavior Plan', + 'Only one year of data is shown to respect privacy' + ]); +}); \ No newline at end of file From 3819e762508495a654b2cc98720c822d4fff1c50 Mon Sep 17 00:00:00 2001 From: kevinrobinson Date: Mon, 21 Oct 2019 17:28:32 -0400 Subject: [PATCH 20/21] Update JS tests, lint, snapsopts for help bubble class name used in specs" --- app/assets/javascripts/helpers/PerDistrict.test.js | 10 ++++++++-- .../student_profile/LightBehaviorDetails.test.js | 2 +- .../__snapshots__/LightHelpBubble.test.js.snap | 1 + .../__snapshots__/LightProfilePage.test.js.snap | 6 ++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/helpers/PerDistrict.test.js b/app/assets/javascripts/helpers/PerDistrict.test.js index 3200c03449..0f2caf4ee7 100644 --- a/app/assets/javascripts/helpers/PerDistrict.test.js +++ b/app/assets/javascripts/helpers/PerDistrict.test.js @@ -73,12 +73,18 @@ describe('#hasActive504Plan', () => { describe('#recordServiceChoices', () => { it('works across districts', () => { - const defaultServiceChoices = [503, 505, 502, 506, 504, 507]; + const defaultServiceChoices = { + leftServiceTypeIds: [503, 502, 504], + rightServiceTypeIds: [505, 506, 507] + }; expect(recordServiceChoices('somerville')).toEqual(defaultServiceChoices); expect(recordServiceChoices('new_bedford')).toEqual(defaultServiceChoices); expect(recordServiceChoices('demo')).toEqual(defaultServiceChoices); - expect(recordServiceChoices('bedford')).toEqual([703, 707, 701, 702, 706, 708, 705, 704, 709]); + expect(recordServiceChoices('bedford')).toEqual( + leftServiceTypeIds: [703, 707, 701, 702, 706], + rightServiceTypeIds: [708, 705, 704, 709] + }); }); }); diff --git a/app/assets/javascripts/student_profile/LightBehaviorDetails.test.js b/app/assets/javascripts/student_profile/LightBehaviorDetails.test.js index 56bedb02df..8df14ea7e4 100644 --- a/app/assets/javascripts/student_profile/LightBehaviorDetails.test.js +++ b/app/assets/javascripts/student_profile/LightBehaviorDetails.test.js @@ -77,7 +77,7 @@ function parseHighchartPlotLineLabelsForDisciplineChart(el) { // in dev, story and production, so punting for now by mocking the module // and serializing some props for simpler assertions. jest.mock('../components/HighchartsWrapper', () => - props =>
{JSON.stringify(props)}
+ props =>
{JSON.stringify(props)}
// eslint-disable-line react/display-name ); diff --git a/app/assets/javascripts/student_profile/__snapshots__/LightHelpBubble.test.js.snap b/app/assets/javascripts/student_profile/__snapshots__/LightHelpBubble.test.js.snap index 07b717c00d..e0ec9bdedc 100644 --- a/app/assets/javascripts/student_profile/__snapshots__/LightHelpBubble.test.js.snap +++ b/app/assets/javascripts/student_profile/__snapshots__/LightHelpBubble.test.js.snap @@ -21,6 +21,7 @@ exports[`snapshots view 1`] = ` } > Date: Mon, 21 Oct 2019 17:30:05 -0400 Subject: [PATCH 21/21] Update button specs to be two-column again --- app/assets/javascripts/helpers/PerDistrict.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/helpers/PerDistrict.test.js b/app/assets/javascripts/helpers/PerDistrict.test.js index 0f2caf4ee7..a3d846a2df 100644 --- a/app/assets/javascripts/helpers/PerDistrict.test.js +++ b/app/assets/javascripts/helpers/PerDistrict.test.js @@ -81,9 +81,9 @@ describe('#recordServiceChoices', () => { expect(recordServiceChoices('new_bedford')).toEqual(defaultServiceChoices); expect(recordServiceChoices('demo')).toEqual(defaultServiceChoices); - expect(recordServiceChoices('bedford')).toEqual( - leftServiceTypeIds: [703, 707, 701, 702, 706], - rightServiceTypeIds: [708, 705, 704, 709] + expect(recordServiceChoices('bedford')).toEqual({ + leftServiceTypeIds: [703, 702, 705, 704, 709], + rightServiceTypeIds: [707, 706, 701, 708] }); }); });