diff --git a/app/models/measure.rb b/app/models/measure.rb index 394262cc..a4f3e08d 100644 --- a/app/models/measure.rb +++ b/app/models/measure.rb @@ -6,7 +6,7 @@ class Measure < ActiveRecord::Base has_many :survey_item_responses, through: :survey_items def none_meet_threshold?(school:, academic_year:) - !sufficient_data?(school:, academic_year:) + !sufficient_survey_responses?(school:, academic_year:) end def teacher_survey_items @@ -49,9 +49,10 @@ class Measure < ActiveRecord::Base @score ||= Hash.new do |memo| meets_student_threshold = sufficient_student_data?(school:, academic_year:) meets_teacher_threshold = sufficient_teacher_data?(school:, academic_year:) + meets_admin_data_threshold = all_admin_data_collected?(school:, academic_year:) lacks_sufficient_data = !meets_student_threshold && !meets_teacher_threshold && !includes_admin_data_items? - next Score.new(nil, false, false) if lacks_sufficient_data + next Score.new(nil, false, false, false) if lacks_sufficient_data scores = [] scores << teacher_scales.map { |scale| scale.score(school:, academic_year:) }.average if meets_teacher_threshold @@ -64,10 +65,10 @@ class Measure < ActiveRecord::Base end average = scores.flatten.compact.average - next Score.new(nil, false, false) if average.nan? + next Score.new(nil, false, false, false) if average.nan? memo[[school, academic_year]] = - Score.new(average, meets_teacher_threshold, meets_student_threshold) + Score.new(average, meets_teacher_threshold, meets_student_threshold, meets_admin_data_threshold) end @score[[school, academic_year]] @@ -105,7 +106,17 @@ class Measure < ActiveRecord::Base subcategory.teacher_response_rate(school:, academic_year:).meets_teacher_threshold? end - def sufficient_data?(school:, academic_year:) + def all_admin_data_collected?(school:, academic_year:) + total_possible_admin_data_items = scales.map { |scale| scale.admin_data_items.count }.sum + total_collected_admin_data_items = scales.map do |scale| + scale.admin_data_items.map do |admin_data_item| + admin_data_item.admin_data_values.where(school:, academic_year:).count + end + end.flatten.sum + total_possible_admin_data_items == total_collected_admin_data_items + end + + def sufficient_survey_responses?(school:, academic_year:) sufficient_student_data?(school:, academic_year:) || sufficient_teacher_data?(school:, academic_year:) end diff --git a/app/models/score.rb b/app/models/score.rb index 0426b046..dd6830a9 100644 --- a/app/models/score.rb +++ b/app/models/score.rb @@ -1,2 +1,2 @@ -class Score < Struct.new(:average, :meets_teacher_threshold?, :meets_student_threshold?) +class Score < Struct.new(:average, :meets_teacher_threshold?, :meets_student_threshold?, :meets_admin_data_threshold?) end diff --git a/app/presenters/admin_data_presenter.rb b/app/presenters/admin_data_presenter.rb index 9434ab96..7b73ef6b 100644 --- a/app/presenters/admin_data_presenter.rb +++ b/app/presenters/admin_data_presenter.rb @@ -1,6 +1,6 @@ class AdminDataPresenter < DataItemPresenter - def initialize(measure_id:, admin_data_items:) - super(measure_id:, has_sufficient_data: false) + def initialize(measure_id:, admin_data_items:, has_sufficient_data:) + super(measure_id:, has_sufficient_data:) @admin_data_items = admin_data_items end diff --git a/app/presenters/measure_presenter.rb b/app/presenters/measure_presenter.rb index 1553abe1..9853f79f 100644 --- a/app/presenters/measure_presenter.rb +++ b/app/presenters/measure_presenter.rb @@ -37,7 +37,7 @@ class MeasurePresenter end if @measure.admin_data_items.any? array << AdminDataPresenter.new(measure_id: @measure.measure_id, - admin_data_items: @measure.admin_data_items) + admin_data_items: @measure.admin_data_items, has_sufficient_data: score_for_measure.meets_admin_data_threshold?) end end end diff --git a/app/presenters/subcategory_presenter.rb b/app/presenters/subcategory_presenter.rb index 6eb6c7eb..ce092760 100644 --- a/app/presenters/subcategory_presenter.rb +++ b/app/presenters/subcategory_presenter.rb @@ -42,7 +42,7 @@ class SubcategoryPresenter end def admin_collection_rate - rate = [0, admin_data_item_count] + rate = [admin_data_values_count, admin_data_item_count] format_a_non_applicable_rate rate end @@ -54,6 +54,16 @@ class SubcategoryPresenter private + def admin_data_values_count + @subcategory.measures.map do |measure| + measure.scales.map do |scale| + scale.admin_data_items.map do |admin_data_item| + admin_data_item.admin_data_values.where(school: @school, academic_year: @academic_year).count + end + end + end.flatten.sum + end + def admin_data_item_count return AdminDataItem.for_measures(@subcategory.measures).count if @school.is_hs diff --git a/doc/architectural_decision_records/7.md b/doc/architectural_decision_records/7.md new file mode 100644 index 00000000..8c1472a9 --- /dev/null +++ b/doc/architectural_decision_records/7.md @@ -0,0 +1,19 @@ +# Decision record 7 + +# Intruduce concept of sufficiency for admin data items + +## Status + +Completed + +## Context + +The browse page contains accordions that inform the user if some data of missing data. In the case of survey items, a response rate of less than 25 percent will mean no scores for that scale will be reflected in the gauge graph. In the case of admin data, there is no such thing as response rate. Data was either collected or not. But we still want to inform the user that not all data was collected. if some admin data was not collected, we still want to let the user know not all data is included. + +## Decision + +We inform users of insufficient data if, for a measure, any admin data items could not be collected. If there are five admin data items for a measure, there must be a corresponding five admin data values in the database to be considered 'sufficient'. Otherwise we inform the user of missing admin data on the browse page. + +## Consequences + +At the moment, the admin_data_presenter class only knows if a measure has sufficient admin data or not. It does not know which admin data items lack corresponding values. We are not yet informing the user which admin data values are missing. A later story will need to be written to correct this. diff --git a/spec/factories.rb b/spec/factories.rb index f6490f52..ab540c09 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -77,6 +77,9 @@ FactoryBot.define do factory :student_scale do scale_id { "s-#{rand}" } end + factory :admin_scale do + scale_id { 'a-{rand' } + end end factory :survey_item do diff --git a/spec/models/measure_spec.rb b/spec/models/measure_spec.rb index 515fc01d..a34d6728 100644 --- a/spec/models/measure_spec.rb +++ b/spec/models/measure_spec.rb @@ -5,6 +5,7 @@ RSpec.describe Measure, type: :model do let(:scale) { create(:scale, measure:) } let(:teacher_scale) { create(:teacher_scale, measure:) } let(:student_scale) { create(:student_scale, measure:) } + let(:admin_scale) { create(:admin_scale, measure:) } let(:school) { create(:school) } let(:academic_year) { create(:academic_year) } @@ -473,5 +474,25 @@ RSpec.describe Measure, type: :model do end end end + + context 'when the measure includes admin data' do + let(:admin_data_item) { create(:admin_data_item, scale: admin_scale) } + let(:admin_data_item_2) { create(:admin_data_item, scale: admin_scale) } + context 'and the admin data does not meet the sufficiency threshold' do + it 'affirms the returned score does not meet the admin data threshold' do + expect(measure.score(school:, academic_year:).meets_admin_data_threshold?).to be false + end + end + context 'and the admin data does meet the sufficiency threshold' do + before :each do + create(:admin_data_value, admin_data_item:, school:, academic_year:) + create(:admin_data_value, admin_data_item: admin_data_item_2, school:, academic_year:) + end + + it 'affirms the returned score does meet the admin data threshold' do + expect(measure.score(school:, academic_year:).meets_admin_data_threshold?).to be true + end + end + end end end diff --git a/spec/presenters/subcategory_presenter_spec.rb b/spec/presenters/subcategory_presenter_spec.rb index d3ccecf2..560ba459 100644 --- a/spec/presenters/subcategory_presenter_spec.rb +++ b/spec/presenters/subcategory_presenter_spec.rb @@ -6,6 +6,10 @@ describe SubcategoryPresenter do let(:subcategory) do create(:subcategory, name: 'A great subcategory', subcategory_id: 'A', description: 'A great description') end + let(:measure_of_only_admin_data) { create(:measure, subcategory:) } + let(:scale_of_only_admin_data) { create(:scale, measure: measure_of_only_admin_data) } + let(:admin_data_item_1) { create(:admin_data_item, scale: scale_of_only_admin_data, hs_only_item: false) } + let(:admin_data_item_2) { create(:admin_data_item, scale: scale_of_only_admin_data, hs_only_item: false) } let(:subcategory_presenter) do measure1 = create(:measure, subcategory:) teacher_scale_1 = create(:teacher_scale, measure: measure1) @@ -104,13 +108,24 @@ describe SubcategoryPresenter do context 'and the school is not a high school' do context 'and the measure does not include high-school-only admin data items' do before do - measure_of_only_admin_data = create(:measure, subcategory:) - scale_of_only_admin_data = create(:scale, measure: measure_of_only_admin_data) - create(:admin_data_item, scale: scale_of_only_admin_data, hs_only_item: false) - create(:admin_data_item, scale: scale_of_only_admin_data, hs_only_item: false) + measure_of_only_admin_data + scale_of_only_admin_data + admin_data_item_1 + admin_data_item_2 end - it 'returns the admin collection rate' do - expect(subcategory_presenter.admin_collection_rate).to eq [0, 2] + context 'and there are no admin data values in the database' do + it 'returns the admin collection rate' do + expect(subcategory_presenter.admin_collection_rate).to eq [0, 2] + end + end + context 'and there are admin data values present in the database ' do + before do + create(:admin_data_value, admin_data_item: admin_data_item_1, school:, academic_year:) + create(:admin_data_value, admin_data_item: admin_data_item_2, school:, academic_year:) + end + it 'returns the admin collection rate' do + expect(subcategory_presenter.admin_collection_rate).to eq [2, 2] + end end end