diff --git a/app/models/response_rate_calculator.rb b/app/models/response_rate_calculator.rb new file mode 100644 index 00000000..48b3b6b3 --- /dev/null +++ b/app/models/response_rate_calculator.rb @@ -0,0 +1,35 @@ +module ResponseRateCalculator + TEACHER_RATE_THRESHOLD = 25 + STUDENT_RATE_THRESHOLD = 25 + + def initialize(subcategory:, school:, academic_year:) + @subcategory = subcategory + @school = school + @academic_year = academic_year + end + + def rate + return 100 if Respondent.where(school: @school, academic_year: @academic_year).count.zero? + + return 0 unless survey_item_count.positive? + + average_responses_per_survey_item = response_count / survey_item_count.to_f + + return 0 unless total_possible_responses.positive? + + response_rate = (average_responses_per_survey_item / total_possible_responses.to_f * 100).round + cap_at_100(response_rate) + end + + def meets_student_threshold? + rate >= STUDENT_RATE_THRESHOLD + end + + def meets_teacher_threshold? + rate >= TEACHER_RATE_THRESHOLD + end + + def cap_at_100(response_rate) + response_rate > 100 ? 100 : response_rate + end +end diff --git a/app/models/student_response_rate_calculator.rb b/app/models/student_response_rate_calculator.rb new file mode 100644 index 00000000..47713b2a --- /dev/null +++ b/app/models/student_response_rate_calculator.rb @@ -0,0 +1,58 @@ +class StudentResponseRateCalculator + include ResponseRateCalculator + + private + + def survey_item_count + @survey_item_count ||= begin + survey = Survey.where(school: @school, academic_year: @academic_year).first + survey_items = SurveyItem.includes(%i[scale + measure]).student_survey_items.where("scale.measure": @subcategory.measures) + survey_items = survey_items.where(on_short_form: true) if survey.form == 'short' + survey_items = survey_items.reject do |survey_item| + survey_item.survey_item_responses.where(school: @school, academic_year: @academic_year).none? + end + survey_items.count + end + end + + def response_count + @response_count ||= @subcategory.measures.map do |measure| + measure.student_survey_items.map do |survey_item| + survey_item.survey_item_responses.where(school: @school, + academic_year: @academic_year).exclude_boston.count + end.sum + end.sum + end + + def total_possible_responses + @total_possible_responses ||= begin + total_responses = Respondent.where(school: @school, academic_year: @academic_year).first + return 0 unless total_responses.present? + + total_responses.total_students + end + end +end + +# survey = Survey.where(school:, academic_year:).first +# total_possible_student_responses = Respondent.where(school:, academic_year:).first + +# student_survey_items = Subcategory.all.map do |subcategory| +# subcategory.measures.map do |measure| +# measure.student_scales.map do |scale| +# scale.survey_items.count +# end.sum +# end.sum +# end +# student_response_counts = Subcategory.all.map do |subcategory| +# subcategory.measures.map do |measure| +# measure.student_survey_items.map do |survey_item| +# survey_item.survey_item_responses.where(school:, academic_year:).exclude_boston.count +# end.sum +# end.sum +# end + +# student_response_counts.each_with_index.map do |value, index| +# value.to_f / student_survey_items[index] / total_possible_student_responses * 100 +# end diff --git a/app/models/teacher_response_rate_calculator.rb b/app/models/teacher_response_rate_calculator.rb new file mode 100644 index 00000000..8e5d3947 --- /dev/null +++ b/app/models/teacher_response_rate_calculator.rb @@ -0,0 +1,29 @@ +class TeacherResponseRateCalculator + include ResponseRateCalculator + + def survey_item_count + @survey_item_count ||= @subcategory.measures.map do |measure| + measure.teacher_survey_items.reject do |survey_item| + survey_item.survey_item_responses.where(school: @school, academic_year: @academic_year).none? + end.count + end.sum + end + + def response_count + @response_count ||= @subcategory.measures.map do |measure| + measure.teacher_survey_items.map do |survey_item| + survey_item.survey_item_responses.where(school: @school, + academic_year: @academic_year).exclude_boston.count + end.sum + end.sum + end + + def total_possible_responses + @total_possible_responses ||= begin + total_responses = Respondent.where(school: @school, academic_year: @academic_year).first + return 0 unless total_responses.present? + + total_responses.total_teachers + end + end +end diff --git a/spec/models/response_rate_calculator_spec.rb b/spec/models/response_rate_calculator_spec.rb new file mode 100644 index 00000000..b33ddf42 --- /dev/null +++ b/spec/models/response_rate_calculator_spec.rb @@ -0,0 +1,183 @@ +require 'rails_helper' + +describe ResponseRateCalculator, type: :model do + let(:school) { create(:school) } + let(:academic_year) { create(:academic_year) } + + describe StudentResponseRateCalculator do + let(:subcategory) { create(:subcategory) } + let(:sufficient_measure_1) { create(:measure, subcategory:) } + let(:sufficient_scale_1) { create(:scale, measure: sufficient_measure_1) } + let(:sufficient_measure_2) { create(:measure, subcategory:) } + let(:sufficient_scale_2) { create(:scale, measure: sufficient_measure_2) } + let(:sufficient_teacher_survey_item) { create(:teacher_survey_item, scale: sufficient_scale_1) } + let(:sufficient_student_survey_item_1) { create(:student_survey_item, scale: sufficient_scale_1) } + let(:insufficient_student_survey_item_1) { create(:student_survey_item, scale: sufficient_scale_1) } + let(:sufficient_student_survey_item_2) { create(:student_survey_item, scale: sufficient_scale_2) } + + before :each do + create_list(:survey_item_response, SurveyItemResponse::TEACHER_RESPONSE_THRESHOLD, survey_item: sufficient_teacher_survey_item, + academic_year:, school:, likert_score: 1) + create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD, survey_item: sufficient_student_survey_item_1, + academic_year:, school:, likert_score: 4) + create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD, survey_item: sufficient_student_survey_item_2, + academic_year:, school:, likert_score: 4) + end + context 'when a students take a regular survey' do + before :each do + create(:respondent, school:, academic_year:) + create(:survey, school:, academic_year:) + end + + context 'when the average number of student responses per question in a subcategory is equal to the student response threshold' do + it 'returns a response rate equal to the response threshold' do + expect(StudentResponseRateCalculator.new(subcategory:, school:, + academic_year:).rate).to eq 25 + end + end + end + + context 'when students take the short form survey' do + before :each do + create(:respondent, school:, academic_year:) + create(:survey, form: :short, school:, academic_year:) + end + + context 'when the average number of student responses per question in a subcategory is equal to the student response threshold' do + before :each do + sufficient_student_survey_item_1.update! on_short_form: true + sufficient_student_survey_item_2.update! on_short_form: true + end + + it 'returns 100 percent' do + expect(StudentResponseRateCalculator.new(subcategory:, school:, + academic_year:).rate).to eq 25 + end + + context 'for the same number of responses, if only one of the questions is a short form question, the response rate will be half' do + before do + sufficient_student_survey_item_2.update! on_short_form: false + end + + it 'returns 100 percent' do + expect(StudentResponseRateCalculator.new(subcategory:, school:, + academic_year:).rate).to eq 50 + end + end + end + end + + context 'when the average number of teacher responses is greater than the total possible responses' do + before do + create(:respondent, school:, academic_year:) + create(:survey, school:, academic_year:) + create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD * 11, survey_item: sufficient_student_survey_item_2, + academic_year:, school:, likert_score: 1) + end + it 'returns 100 percent' do + expect(StudentResponseRateCalculator.new(subcategory:, school:, + academic_year:).rate).to eq 100 + end + end + + context 'when no survey information exists for that school or year' do + it 'returns 100 percent' do + expect(StudentResponseRateCalculator.new(subcategory:, school:, academic_year:).rate).to eq 100 + end + end + + context 'when there is an imbalance in the response rate of the student items' do + context 'and one of the student items has no associated survey item responses' do + before do + create(:respondent, school:, academic_year:) + create(:survey, school:, academic_year:) + insufficient_student_survey_item_1 + end + it 'ignores the empty survey item and returns only the average response rate of student survey items with responses' do + expect(StudentResponseRateCalculator.new(subcategory:, school:, + academic_year:).rate).to eq 25 + end + end + end + end + + describe TeacherResponseRateCalculator do + let(:subcategory) { create(:subcategory) } + let(:sufficient_measure_1) { create(:measure, subcategory:) } + let(:sufficient_scale_1) { create(:scale, measure: sufficient_measure_1) } + let(:sufficient_measure_2) { create(:measure, subcategory:) } + let(:sufficient_scale_2) { create(:scale, measure: sufficient_measure_2) } + let(:sufficient_teacher_survey_item_1) { create(:teacher_survey_item, scale: sufficient_scale_1) } + let(:sufficient_teacher_survey_item_2) { create(:teacher_survey_item, scale: sufficient_scale_1) } + let(:sufficient_teacher_survey_item_3) { create(:teacher_survey_item, scale: sufficient_scale_1) } + let(:insufficient_teacher_survey_item_4) { create(:teacher_survey_item, scale: sufficient_scale_1) } + let(:sufficient_student_survey_item_1) { create(:student_survey_item, scale: sufficient_scale_1) } + + before :each do + create_list(:survey_item_response, SurveyItemResponse::TEACHER_RESPONSE_THRESHOLD, survey_item: sufficient_teacher_survey_item_1, + academic_year:, school:, likert_score: 1) + create_list(:survey_item_response, SurveyItemResponse::TEACHER_RESPONSE_THRESHOLD, survey_item: sufficient_teacher_survey_item_2, + academic_year:, school:, likert_score: 1) + create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD, survey_item: sufficient_student_survey_item_1, + academic_year:, school:, likert_score: 4) + end + + context 'when the average number of teacher responses per question in a subcategory is at the threshold' do + before :each do + create(:respondent, school:, academic_year:) + create(:survey, school:, academic_year:) + end + it 'returns 25 percent' do + expect(TeacherResponseRateCalculator.new(subcategory:, school:, + academic_year:).rate).to eq 25 + end + end + + context 'when the teacher response rate is not a whole number. eg 29.166%' do + before do + create(:respondent, school:, academic_year:) + create(:survey, school:, academic_year:) + create_list(:survey_item_response, SurveyItemResponse::TEACHER_RESPONSE_THRESHOLD + 1, survey_item: sufficient_teacher_survey_item_3, + academic_year:, school:, likert_score: 1) + end + it 'it will return the nearest whole number' do + expect(TeacherResponseRateCalculator.new(subcategory:, school:, + academic_year:).rate).to eq 29 + end + end + + context 'when the average number of teacher responses is greater than the total possible responses' do + before do + create(:respondent, school:, academic_year:) + create(:survey, school:, academic_year:) + create_list(:survey_item_response, SurveyItemResponse::TEACHER_RESPONSE_THRESHOLD * 11, survey_item: sufficient_teacher_survey_item_3, + academic_year:, school:, likert_score: 1) + end + it 'returns 100 percent' do + expect(TeacherResponseRateCalculator.new(subcategory:, school:, + academic_year:).rate).to eq 100 + end + end + + context 'when no survey information exists for that school and academic_year' do + it 'returns 100 percent' do + expect(TeacherResponseRateCalculator.new(subcategory:, school:, + academic_year:).rate).to eq 100 + end + end + + context 'when there is an imbalance in the response rate of the teacher items' do + context 'and one of the teacher items has no associated survey item responses' do + before do + create(:respondent, school:, academic_year:) + create(:survey, school:, academic_year:) + insufficient_teacher_survey_item_4 + end + it 'ignores the empty survey item and returns only the average response rate of teacher survey items with responses' do + expect(TeacherResponseRateCalculator.new(subcategory:, school:, + academic_year:).rate).to eq 25 + end + end + end + end +end