Skip to content

Commit

Permalink
Merge pull request #212 from studentinsights/feature/overview-service…
Browse files Browse the repository at this point in the history
…s-and-notes

Update overview panels to show Services instead of Interventions
  • Loading branch information
kevinrobinson committed Mar 19, 2016
2 parents f8a72c2 + d4c7a45 commit 43d5198
Show file tree
Hide file tree
Showing 18 changed files with 535 additions and 228 deletions.
2 changes: 1 addition & 1 deletion app/assets/javascripts/components/fixed_table.js
Expand Up @@ -37,7 +37,7 @@
paddingBottom: 5
}
},
dom.div({ className: 'FixedTable', style: { marginBottom: 5, paddingLeft: 5, fontWeight: 'bold', height: '1em' }}, title),
dom.div({ className: 'fixed-table-title', style: { marginBottom: 5, paddingLeft: 5, fontWeight: 'bold', height: '1em' }}, title),
dom.table({},
dom.tbody({}, items.map(function(item) {
var key = item.caption;
Expand Down
27 changes: 13 additions & 14 deletions app/assets/javascripts/components/slice_panels.js
Expand Up @@ -14,7 +14,7 @@
filters: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
students: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
allStudents: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
InterventionTypes: React.PropTypes.object.isRequired,
serviceTypesIndex: React.PropTypes.object.isRequired,
onFilterToggled: React.PropTypes.func.isRequired
},

Expand Down Expand Up @@ -112,7 +112,6 @@
this.createItem('1', Filters.Equal(key, 1)),
this.createItem('2', Filters.Equal(key, 2)),
this.createItem('3 - 5', Filters.Range(key, [3, 6])),
// this.createItem('6+', Filters.Range(key, [5, 7])),
this.createItem('6+', Filters.Range(key, [6, Number.MAX_VALUE]))
]
});
Expand All @@ -134,9 +133,9 @@
renderInterventionsColumn: function() {
return dom.div({ className: 'column interventions-column' },
this.renderTable({
title: 'Interventions',
items: this.interventionItems(),
limit: 5
title: 'Services',
items: this.serviceItems(),
limit: 7
}),
this.renderSimpleTable('Program', 'program_assigned', { limit: 3 }),
this.renderSimpleTable('Homeroom', 'homeroom_name', {
Expand All @@ -155,21 +154,21 @@
};
},

interventionItems: function() {
serviceItems: function() {
var students = this.props.allStudents;
var allInterventions = _.compact(_.flatten(_.pluck(students, 'interventions')));
var allInterventionTypes = _.unique(allInterventions.map(function(intervention) {
return parseInt(intervention.intervention_type_id, 10);
var allServices = _.compact(_.flatten(_.pluck(students, 'services')));
var allServiceTypeIds = _.unique(allServices.map(function(service) {
return parseInt(service.service_type_id, 10);
}));
var interventionItems = allInterventionTypes.map(function(interventionTypeId) {
var interventionName = this.props.InterventionTypes[interventionTypeId].name;
return this.createItem(interventionName, Filters.InterventionType(interventionTypeId));
var serviceItems = allServiceTypeIds.map(function(serviceTypeId) {
var serviceName = this.props.serviceTypesIndex[serviceTypeId].name;
return this.createItem(serviceName, Filters.ServiceType(serviceTypeId));
}, this);
var sortedItems = _.sortBy(interventionItems, function(item) {
var sortedItems = _.sortBy(serviceItems, function(item) {
return -1 * students.filter(item.filter.filterFn).length;
});

return sortedItems.concat(this.createItem('None', Filters.InterventionType(null)));
return [this.createItem('None', Filters.ServiceType(null))].concat(sortedItems);
},

renderGradeTable: function() {
Expand Down
5 changes: 2 additions & 3 deletions app/assets/javascripts/components/students_table.js
Expand Up @@ -3,7 +3,6 @@
window.shared || (window.shared = {});
var Filters = window.shared.Filters;
var Routes = window.shared.Routes;
var SlicePanels = window.shared.SlicePanels;
var styles = window.shared.styles;
var colors = window.shared.colors;
var dom = window.shared.ReactHelpers.dom;
Expand Down Expand Up @@ -38,7 +37,7 @@
this.renderHeader('Discipline Incidents'),
this.renderHeader('Absences'),
this.renderHeader('Tardies'),
this.renderHeader('Interventions'),
this.renderHeader('Services'),
this.renderHeader('Program'),
this.renderHeader('Homeroom')
)
Expand All @@ -58,7 +57,7 @@
this.renderNumberCell(this.renderCount(student.discipline_incidents_count)),
this.renderNumberCell(this.renderCount(student.absences_count)),
this.renderNumberCell(this.renderCount(student.tardies_count)),
this.renderNumberCell(this.renderCount(student.interventions.length)),
this.renderNumberCell(this.renderCount(student.services.length)),
dom.td({}, this.renderUnless('Reg Ed', student.program_assigned)),
dom.td({}, dom.a({ href: Routes.homeroom(student.homeroom_id) }, student.homeroom_name))
);
Expand Down
12 changes: 12 additions & 0 deletions app/assets/javascripts/helpers/filters.js
Expand Up @@ -44,6 +44,18 @@
key: 'intervention_type'
};
},
ServiceType: function(serviceTypeId) {
return {
identifier: ['service_type', serviceTypeId].join(':'),
filterFn: function(student) {
if (serviceTypeId === null) return (student.services === undefined || student.services.length === 0);
return student.services.filter(function(service) {
return service.service_type_id === serviceTypeId;
}).length > 0;
},
key: 'service_type'
};
},
YearsEnrolled: function(value) {
return {
identifier: ['years_enrolled', value].join(':'),
Expand Down
2 changes: 1 addition & 1 deletion app/assets/javascripts/school_overview/main.js
Expand Up @@ -14,7 +14,7 @@ $(function() {

ReactDOM.render(createEl(SchoolOverviewPage, {
allStudents: serializedData.students,
InterventionTypes: serializedData.interventionTypesIndex,
serviceTypesIndex: serializedData.constantIndexes.service_types_index,
initialFilters: Filters.parseFiltersHash(window.location.hash)
}), document.getElementById('main'));
}
Expand Down
Expand Up @@ -19,7 +19,7 @@

propTypes: {
allStudents: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
InterventionTypes: React.PropTypes.object.isRequired,
serviceTypesIndex: React.PropTypes.object.isRequired,
initialFilters: React.PropTypes.arrayOf(React.PropTypes.object)
},

Expand Down Expand Up @@ -193,7 +193,7 @@
dom.div({ className: 'header', style: styles.header }, createEl(SlicePanels, {
allStudents: this.props.allStudents,
students: this.getFilteredStudents(),
InterventionTypes: this.props.InterventionTypes,
serviceTypesIndex: this.props.serviceTypesIndex,
filters: this.state.filters,
onFilterToggled: this.onFilterToggled
})),
Expand Down
6 changes: 1 addition & 5 deletions app/assets/javascripts/school_star/main.js
Expand Up @@ -5,10 +5,6 @@ $(function() {
if ($('body').hasClass('schools') && $('body').hasClass('star_reading')) {
var MixpanelUtils = window.shared.MixpanelUtils;
var StarReadingPage = window.shared.StarReadingPage;
var SlicePanels = window.shared.SlicePanels;
var Routes = window.shared.Routes;
var styles = window.shared.styles;
var colors = window.shared.colors;
var dom = window.shared.ReactHelpers.dom;
var createEl = window.shared.ReactHelpers.createEl;
var merge = window.shared.ReactHelpers.merge;
Expand All @@ -21,7 +17,7 @@ $(function() {
ReactDOM.render(createEl(StarReadingPage, {
students: serializedData.studentsWithStarReading,
dateNow: new Date(),
InterventionTypes: serializedData.interventionTypesIndex,
serviceTypesIndex: serializedData.constantIndexes.service_types_index,
initialFilters: Filters.parseFiltersHash(window.location.hash)
}), document.getElementById('main'));
}
Expand Down
3 changes: 2 additions & 1 deletion app/assets/javascripts/school_star/star_reading_page.js
Expand Up @@ -13,6 +13,7 @@ $(function() {
displayName: 'StarReadingPage',

propTypes: {
serviceTypesIndex: React.PropTypes.object.isRequired,
initialFilters: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
dateNow: React.PropTypes.object.isRequired,
recentThresholdInDays: React.PropTypes.number
Expand Down Expand Up @@ -137,7 +138,7 @@ $(function() {
dom.div({ className: 'header', style: styles.header }, createEl(SlicePanels, {
allStudents: this.props.students,
students: this.filteredStudents(),
InterventionTypes: this.props.InterventionTypes,
serviceTypesIndex: this.props.serviceTypesIndex,
filters: this.state.filters,
onFilterToggled: this.onFilterToggled
})),
Expand Down
82 changes: 57 additions & 25 deletions app/controllers/schools_controller.rb
@@ -1,37 +1,82 @@
class SchoolsController < ApplicationController
include SerializeDataHelper
before_action :authenticate_educator!,
:assign_school,
:authorize

before_action :assign_students_for_show_page, only: [:show]
before_action :assign_students_for_star_reading, only: [:star_reading]

def show
@serialized_students = @students.map do |student|
student.decorate.as_json_for_school_overview
authorized_students = current_educator.students_for_school_overview

# TODO(kr) Read from cache, since this only updates daily
student_hashes = authorized_students.map do |student|
HashWithIndifferentAccess.new(student_hash_for_slicing(student))
end

# Read data stored StudentInsights each time, with no caching
merged_student_hashes = merge_mutable_fields_for_slicing(student_hashes)

@serialized_data = {
students: @serialized_students,
students: merged_student_hashes,
current_educator: current_educator,
intervention_types_index: intervention_types_index
constant_indexes: constant_indexes
}
end

def star_reading
@serialized_students = @students.map do |student|
student.decorate.as_json_for_star_reading
authorized_students = current_educator.students_for_school_overview(:student_assessments)

# TODO(kr) Read from cache, since this only updates daily
student_hashes = authorized_students.map do |student|
HashWithIndifferentAccess.new(student_hash_for_slicing(student).merge({
star_reading_results: student.star_reading_results
}))
end

# Read data stored StudentInsights each time, with no caching
merged_student_hashes = merge_mutable_fields_for_slicing(student_hashes)

@serialized_data = {
students_with_star_reading: @serialized_students,
students_with_star_reading: merged_student_hashes,
current_educator: current_educator,
intervention_types_index: intervention_types_index
constant_indexes: constant_indexes
}
end

private
# Serialize what are essentially constants stored in the database down
# to the UI so it can use them for joins.
def constant_indexes
{
service_types_index: service_types_index,
event_note_types_index: event_note_types_index
}
end

# Queries for Services and EventNotes for each student, and merges the results
# into the list of student hashes.
def student_hash_for_slicing(student)
student.as_json.merge({
student_risk_level: student.student_risk_level.as_json,
discipline_incidents_count: student.most_recent_school_year.discipline_incidents.count,
absences_count: student.most_recent_school_year.absences.count,
tardies_count: student.most_recent_school_year.tardies.count,
homeroom_name: student.try(:homeroom).try(:name)
})
end

def merge_mutable_fields_for_slicing(student_hashes)
student_ids = student_hashes.map {|student_hash| student_hash[:id] }
all_event_notes = EventNote.where(student_id: student_ids)
all_services = Service.where(student_id: student_ids)
all_interventions = Intervention.where(student_id: student_ids)

student_hashes.map do |student_hash|
student_hash.merge({
event_notes: all_event_notes.select {|event_note| event_note.student_id == student_hash[:id] },
services: all_services.select {|service| service.student_id == student_hash[:id] },
interventions: all_interventions.select {|intervention| intervention.student_id == student_hash[:id] }
})
end
end

def authorize
redirect_to(homepage_path_for_current_educator) unless current_educator.schoolwide_access? ||
Expand All @@ -42,17 +87,4 @@ def authorize
flash[:notice] << grade_message if flash[:notice]
end
end

def assign_school
@school = School.friendly.find(params[:id])
end

def assign_students_for_show_page
@students = current_educator.students_for_school_overview
end

def assign_students_for_star_reading
@students = current_educator.students_for_school_overview(:student_assessments)
end

end
6 changes: 3 additions & 3 deletions app/decorators/student_decorator.rb
Expand Up @@ -31,18 +31,18 @@ def handle_none(value)
end

def as_json_for_school_overview
student.as_json.merge(
HashWithIndifferentAccess.new(student.as_json.merge(
interventions: student.interventions,
student_risk_level: student.student_risk_level.as_json,
discipline_incidents_count: most_recent_school_year.discipline_incidents.count,
absences_count: most_recent_school_year.absences.count,
tardies_count: most_recent_school_year.tardies.count,
homeroom_name: student.try(:homeroom).try(:name)
)
))
end

def as_json_for_star_reading
as_json.merge(star_reading_results: student.star_reading_results)
HashWithIndifferentAccess.new(as_json.merge(star_reading_results: student.star_reading_results))
end

def presentation_for_autocomplete
Expand Down
8 changes: 4 additions & 4 deletions app/helpers/serialize_data_helper.rb
Expand Up @@ -44,7 +44,7 @@ def serialize_student_note(student_note)
def intervention_types_index
index = {}
InterventionType.all.each do |intervention_type|
index[intervention_type.id] = intervention_type
index[intervention_type.id] = intervention_type.as_json.symbolize_keys.slice(:id, :name)
end
index
end
Expand All @@ -53,7 +53,7 @@ def intervention_types_index
def educators_index
index = {}
Educator.all.each do |educator|
index[educator.id] = educator
index[educator.id] = educator.as_json.symbolize_keys.slice(:id, :email, :full_name)
end
index
end
Expand All @@ -62,15 +62,15 @@ def educators_index
def service_types_index
index = {}
ServiceType.all.each do |service_type|
index[service_type.id] = service_type
index[service_type.id] = service_type.as_json.symbolize_keys.slice(:id, :name)
end
index
end

def event_note_types_index
index = {}
EventNoteType.all.each do |event_note_type|
index[event_note_type.id] = event_note_type
index[event_note_type.id] = event_note_type.as_json.symbolize_keys.slice(:id, :name)
end
index
end
Expand Down
22 changes: 14 additions & 8 deletions spec/controllers/schools_controller_spec.rb
Expand Up @@ -8,6 +8,12 @@ def make_request(school_id)
get :show, id: school_id
end

# Read the instance variable and pull out the ids of students
def extract_serialized_student_ids(controller)
serialized_data = controller.instance_variable_get(:@serialized_data)
serialized_data[:students].map {|student_hash| student_hash[:id] }
end

context 'educator is not an admin but does have a homeroom' do
let!(:school) { FactoryGirl.create(:healey) }
let!(:educator) { FactoryGirl.create(:educator_with_homeroom) }
Expand Down Expand Up @@ -38,10 +44,10 @@ def make_request(school_id)
make_request('hea')

expect(response).to be_success

expect(assigns(:students)).to include include_me
expect(assigns(:students)).to include include_me_too
expect(assigns(:students)).not_to include include_me_not
student_ids = extract_serialized_student_ids(controller)
expect(student_ids).to include include_me.id
expect(student_ids).to include include_me_too.id
expect(student_ids).not_to include include_me_not
end
end

Expand All @@ -63,10 +69,10 @@ def make_request(school_id)
make_request('hea')

expect(response).to be_success

expect(assigns(:students)).to include include_me
expect(assigns(:students)).to include include_me_too
expect(assigns(:students)).not_to include include_me_not
student_ids = extract_serialized_student_ids(controller)
expect(student_ids).to include include_me.id
expect(student_ids).to include include_me_too.id
expect(student_ids).not_to include include_me_not
end
end

Expand Down

0 comments on commit 43d5198

Please sign in to comment.