Rename ResponseRate to ResponseRateCalculator. Create a new response

rate model.  Create a loader to refresh response rates for all
subcategories.

Use precalculated response rates in views

Wrap more elements in page caching

Calculate a response rate for a subcategory if one does not already
exist
pull/1/head
rebuilt 4 years ago
parent dfc5202b88
commit c03615cb43

@ -16,13 +16,17 @@ class OverviewController < SqmApplicationController
end
def check_empty_dataset
@has_empty_dataset = measures.all? do |measure|
measure.none_meet_threshold? school: @school, academic_year: @academic_year
@has_empty_dataset = subcategories.all? do |subcategory|
response_rate = subcategory.response_rate(school: @school, academic_year: @academic_year)
!response_rate.meets_student_threshold && !response_rate.meets_teacher_threshold
end
end
def measures
@measures ||= Measure.all.includes(%i[scales admin_data_items subcategory
category]).includes(subcategory: :measures)
@measures ||= Measure.all.includes(%i[scales admin_data_items category])
end
def subcategories
@subcategories ||= Subcategory.all
end
end

@ -74,7 +74,8 @@ module AnalyzeHelper
def empty_dataset?(measures:, school:, academic_year:)
@empty_dataset ||= Hash.new do |memo, (school, academic_year)|
memo[[school, academic_year]] = measures.all? do |measure|
measure.survey_item_responses.where(school:, academic_year:).none? || measure.none_meet_threshold?(school:, academic_year:)
response_rate = measure.subcategory.response_rate(school:, academic_year:)
!response_rate.meets_student_threshold && !response_rate.meets_teacher_threshold
end
end

@ -202,35 +202,15 @@ class Measure < ActiveRecord::Base
averages.average
end
def student_survey_items_have_no_responses?(school:, academic_year:)
@student_survey_items_have_no_responses ||= Hash.new do |memo, (school, academic_year)|
memo[[school, academic_year]] = student_scales.all? do |scale|
scale.survey_item_responses.where(school:, academic_year:).none?
end
end
@student_survey_items_have_no_responses[[school, academic_year]]
end
def teacher_survey_items_have_no_responses?(school:, academic_year:)
@teacher_survey_items_have_no_responses ||= Hash.new do |memo, (school, academic_year)|
memo[[school, academic_year]] = teacher_scales.all? do |scale|
scale.survey_item_responses.where(school:, academic_year:).none?
end
end
@teacher_survey_items_have_no_responses[[school, academic_year]]
end
def sufficient_student_data?(school:, academic_year:)
return false unless includes_student_survey_items?
return false if student_survey_items_have_no_responses?(school:, academic_year:)
@sufficient_student_data ||= subcategory.student_response_rate(school:, academic_year:).meets_student_threshold?
@sufficient_student_data ||= subcategory.response_rate(school:, academic_year:).meets_student_threshold
end
def sufficient_teacher_data?(school:, academic_year:)
return false unless includes_teacher_survey_items?
return false if teacher_survey_items_have_no_responses?(school:, academic_year:)
@sufficient_teacher_data ||= subcategory.teacher_response_rate(school:, academic_year:).meets_teacher_threshold?
@sufficient_teacher_data ||= subcategory.response_rate(school:, academic_year:).meets_teacher_threshold
end
end

@ -1,6 +1,7 @@
module ResponseRateCalculator
TEACHER_RATE_THRESHOLD = 25
STUDENT_RATE_THRESHOLD = 25
attr_reader :subcategory, :school, :academic_year
def initialize(subcategory:, school:, academic_year:)
@subcategory = subcategory
@ -29,6 +30,8 @@ module ResponseRateCalculator
rate >= TEACHER_RATE_THRESHOLD
end
private
def cap_at_100(response_rate)
response_rate > 100 ? 100 : response_rate
end

@ -5,12 +5,13 @@ class StudentResponseRateCalculator
def survey_item_count
@survey_item_count ||= begin
survey = Survey.where(school: @school, academic_year: @academic_year).first
survey = Survey.find_by(school:, academic_year:)
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?
survey_item.survey_item_responses.where(school:, academic_year:).none?
end
survey_items.count
end
@ -19,40 +20,21 @@ class StudentResponseRateCalculator
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
survey = Survey.find_by(school:, academic_year:)
next 0 if survey.form == 'short' && survey_item.on_short_form == false
survey_item.survey_item_responses.where(school:,
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
total_responses = Respondent.find_by(school:, academic_year:)
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

@ -4,26 +4,31 @@ class Subcategory < ActiveRecord::Base
has_many :measures
def score(school:, academic_year:)
scores = measures.includes([:survey_items]).map do |measure|
scores = measures.map do |measure|
measure.score(school:, academic_year:).average
end
scores = scores.reject(&:nil?)
scores.average
end
def student_response_rate(school:, academic_year:)
@student_response_rate ||= Hash.new do |memo, (school, academic_year)|
memo[[school, academic_year]] = StudentResponseRateCalculator.new(subcategory: self, school:, academic_year:)
def response_rate(school:, academic_year:)
@response_rate ||= Hash.new do |memo, (school, academic_year)|
memo[[school, academic_year]] = ResponseRate.find_by(subcategory: self, school:, academic_year:)
end
@student_response_rate[[school, academic_year]]
if @response_rate[[school, academic_year]].nil?
@response_rate[[school, academic_year]] = create_response_rate(subcategory: self, school:, academic_year:)
end
@response_rate[[school, academic_year]]
end
def teacher_response_rate(school:, academic_year:)
@teacher_response_rate ||= Hash.new do |memo, (school, academic_year)|
memo[[school, academic_year]] = TeacherResponseRateCalculator.new(subcategory: self, school:, academic_year:)
end
private
@teacher_response_rate[[school, academic_year]]
def create_response_rate(subcategory:, school:, academic_year:)
student = StudentResponseRateCalculator.new(subcategory: self, school:, academic_year:)
teacher = TeacherResponseRateCalculator.new(subcategory: self, school:, academic_year:)
ResponseRate.create(school:, academic_year:, subcategory: self, student_response_rate: student.rate, teacher_response_rate: teacher.rate,
meets_student_threshold: student.meets_student_threshold?, meets_teacher_threshold: teacher.meets_teacher_threshold?)
end
end

@ -5,6 +5,7 @@ class SurveyItemResponse < ActiveRecord::Base
belongs_to :academic_year
belongs_to :school
belongs_to :survey_item
has_one :measure, through: :survey_item
scope :exclude_boston, lambda {
boston = District.find_by_name('Boston')

@ -32,13 +32,13 @@ class SubcategoryPresenter
def student_response_rate
return 'N / A' if Respondent.where(school: @school, academic_year: @academic_year).count.zero?
"#{@subcategory.student_response_rate(school: @school, academic_year: @academic_year).rate}%"
"#{@subcategory.response_rate(school: @school, academic_year: @academic_year).student_response_rate.to_i}%"
end
def teacher_response_rate
return 'N / A' if Respondent.where(school: @school, academic_year: @academic_year).count.zero?
"#{@subcategory.teacher_response_rate(school: @school, academic_year: @academic_year).rate}%"
"#{@subcategory.response_rate(school: @school, academic_year: @academic_year).teacher_response_rate.to_i}%"
end
def admin_collection_rate

@ -0,0 +1,34 @@
class ResponseRateLoader
def self.refresh
schools = School.all
academic_years = AcademicYear.all
subcategories = Subcategory.all
milford = School.find_by_slug 'milford-high-school'
# ResponseRate.new(school:, academic_year:, subcategory:, student_response_rate: 50, teacher_response_rate: 50,
# meets_student_threshold: true, meets_teacher_threshold: true).save
test_year = AcademicYear.find_by_range '2020-21'
subcategories.each do |subcategory|
schools.each do |school|
next if ENV['RAILS_ENV'] == 'test' && !(school == milford)
academic_years.each do |academic_year|
next if ENV['RAILS_ENV'] == 'test' && !(academic_year == test_year)
student = StudentResponseRateCalculator.new(subcategory:, school:, academic_year:)
teacher = TeacherResponseRateCalculator.new(subcategory:, school:, academic_year:)
response_rate = ResponseRate.find_or_create_by!(subcategory:, school:, academic_year:)
response_rate.student_response_rate = student.rate
response_rate.teacher_response_rate = teacher.rate
response_rate.meets_student_threshold = student.meets_student_threshold?
response_rate.meets_teacher_threshold = teacher.meets_teacher_threshold?
response_rate.save
end
end
end
end
end

@ -1,105 +1,105 @@
<% content_for :navigation do %>
<h2 class="sub-header-2 color-white m-0">Areas Of Interest</h2>
<h2 class="sub-header-2 color-white m-0">Areas Of Interest</h2>
<select id="select-academic-year" class="form-select" name="academic-year">
<% @academic_years.each do |year| %>
<option value="<%= district_school_overview_index_path(@district, @school, {year: year.range}) %>" <%= @academic_year == year ? "selected" : nil %>><%= year.formatted_range %></option>
<% end %>
</select>
<% end %>
<select id="select-academic-year" class="form-select" name="academic-year">
<% @academic_years.each do |year| %>
<option value="<%= district_school_overview_index_path(@district, @school, {year: year.range}) %>" <%= @academic_year == year ? "selected" : nil %>><%= year.formatted_range %></option>
<% end %>
</select>
<% end %>
<svg class="d-none">
<% cache do %>
<svg class="d-none">
<symbol viewBox="0 0 24 24" id="warning-harvey-ball">
<circle cx="12" cy="12" r="11.5" fill="white" stroke="none" />
<path d="
M 12 0
A 12 12 0 0 1 24 12
L 12 12
L 12 0"
stroke="none"
/>
<circle cx="12" cy="12" r="11.5" fill="none" />
</symbol>
<symbol viewBox="0 0 24 24" id="warning-harvey-ball">
<circle cx="12" cy="12" r="11.5" fill="white" stroke="none" />
<path d="
M 12 0
A 12 12 0 0 1 24 12
L 12 12
L 12 0"
stroke="none"
/>
<circle cx="12" cy="12" r="11.5" fill="none" />
</symbol>
<symbol viewBox="0 0 24 24" id="watch-harvey-ball">
<circle cx="12" cy="12" r="11.5" fill="white" stroke="none" />
<path d="
M 12 0
A 12 12 0 1 1 12 24
L 12 12
L 12 0"
stroke="none"
/>
<circle cx="12" cy="12" r="11.5" fill="none" />
</symbol>
<symbol viewBox="0 0 24 24" id="watch-harvey-ball">
<circle cx="12" cy="12" r="11.5" fill="white" stroke="none" />
<path d="
M 12 0
A 12 12 0 1 1 12 24
L 12 12
L 12 0"
stroke="none"
/>
<circle cx="12" cy="12" r="11.5" fill="none" />
</symbol>
<symbol viewBox="0 0 24 24" id="growth-harvey-ball">
<circle cx="12" cy="12" r="11.5" fill="white" stroke="none" />
<path d="
M 12 0
A 12 12 0 1 1 0 12
L 12 12
L 12 0"
stroke="none"
/>
<circle cx="12" cy="12" r="11.5" fill="none" />
</symbol>
<symbol viewBox="0 0 24 24" id="growth-harvey-ball">
<circle cx="12" cy="12" r="11.5" fill="white" stroke="none" />
<path d="
M 12 0
A 12 12 0 1 1 0 12
L 12 12
L 12 0"
stroke="none"
/>
<circle cx="12" cy="12" r="11.5" fill="none" />
</symbol>
<symbol viewBox="0 0 24 24" id="approval-harvey-ball">
<circle cx="12" cy="12" r="11.5" />
<path d="M19 8C19 8.28125 18.875 8.53125 18.6875 8.71875L10.6875 16.7188C10.5 16.9062 10.25 17 10 17C9.71875 17 9.46875 16.9062 9.28125 16.7188L5.28125 12.7188C5.09375 12.5312 5 12.2812 5 12C5 11.4375 5.4375 11 6 11C6.25 11 6.5 11.125 6.6875 11.3125L10 14.5938L17.2812 7.3125C17.4688 7.125 17.7188 7 18 7C18.5312 7 19 7.4375 19 8Z"
stroke-width=".5" stroke="white" fill="white" />
</symbol>
<symbol viewBox="0 0 24 24" id="approval-harvey-ball">
<circle cx="12" cy="12" r="11.5" />
<path d="M19 8C19 8.28125 18.875 8.53125 18.6875 8.71875L10.6875 16.7188C10.5 16.9062 10.25 17 10 17C9.71875 17 9.46875 16.9062 9.28125 16.7188L5.28125 12.7188C5.09375 12.5312 5 12.2812 5 12C5 11.4375 5.4375 11 6 11C6.25 11 6.5 11.125 6.6875 11.3125L10 14.5938L17.2812 7.3125C17.4688 7.125 17.7188 7 18 7C18.5312 7 19 7.4375 19 8Z"
stroke-width=".5" stroke="white" fill="white" />
</symbol>
<symbol viewBox="0 0 24 24" id="ideal-harvey-ball">
<circle cx="12" cy="12" r="11.5" />
<path d="M9.28125 11.7188C9.46875 11.9062 9.71875 12 10 12C10.25 12 10.5 11.9062 10.6875 11.7188L15.6875 6.71875C15.875 6.53125 16 6.28125 16 6C16 5.4375 15.5312 5 15 5C14.7188 5 14.4688 5.125 14.2812 5.3125L10 9.59375L8.1875 7.8125C8 7.625 7.75 7.5 7.5 7.5C6.9375 7.5 6.5 7.9375 6.5 8.5C6.5 8.78125 6.59375 9.03125 6.78125 9.21875L9.28125 11.7188ZM19 10C19 9.4375 18.5312 9 18 9C17.7188 9 17.4688 9.125 17.2812 9.3125L10 16.5938L6.6875 13.3125C6.5 13.125 6.25 13 6 13C5.4375 13 5 13.4375 5 14C5 14.2812 5.09375 14.5312 5.28125 14.7188L9.28125 18.7188C9.46875 18.9062 9.71875 19 10 19C10.25 19 10.5 18.9062 10.6875 18.7188L18.6875 10.7188C18.875 10.5312 19 10.2812 19 10Z"
stroke-width=".5" stroke="white" fill="white" />
</symbol>
<symbol viewBox="0 0 24 24" id="ideal-harvey-ball">
<circle cx="12" cy="12" r="11.5" />
<path d="M9.28125 11.7188C9.46875 11.9062 9.71875 12 10 12C10.25 12 10.5 11.9062 10.6875 11.7188L15.6875 6.71875C15.875 6.53125 16 6.28125 16 6C16 5.4375 15.5312 5 15 5C14.7188 5 14.4688 5.125 14.2812 5.3125L10 9.59375L8.1875 7.8125C8 7.625 7.75 7.5 7.5 7.5C6.9375 7.5 6.5 7.9375 6.5 8.5C6.5 8.78125 6.59375 9.03125 6.78125 9.21875L9.28125 11.7188ZM19 10C19 9.4375 18.5312 9 18 9C17.7188 9 17.4688 9.125 17.2812 9.3125L10 16.5938L6.6875 13.3125C6.5 13.125 6.25 13 6 13C5.4375 13 5 13.4375 5 14C5 14.2812 5.09375 14.5312 5.28125 14.7188L9.28125 18.7188C9.46875 18.9062 9.71875 19 10 19C10.25 19 10.5 18.9062 10.6875 18.7188L18.6875 10.7188C18.875 10.5312 19 10.2812 19 10Z"
stroke-width=".5" stroke="white" fill="white" />
</symbol>
<symbol viewBox="0 0 24 24" id="insufficient_data-harvey-ball">
<circle cx="12" cy="12" r="11.5" />
</symbol>
</svg>
<symbol viewBox="0 0 24 24" id="insufficient_data-harvey-ball">
<circle cx="12" cy="12" r="11.5" />
</symbol>
</svg>
<% end %>
<% cache [@school, @academic_year] do %>
<div class="card">
<div class="d-flex justify-content-between align-items-center">
<h2 class="sub-header-2">School Quality Framework Indicators</h2>
<div class="card">
<div class="d-flex justify-content-between align-items-center">
<h2 class="sub-header-2">School Quality Framework Indicators</h2>
<div class="harvey-ball-legend">
<div class="font-size-14">Warning</div>
<svg class="ms-3 me-1" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<use class="harvey-ball harvey-ball--warning" xlink:href="#warning-harvey-ball"></use>
</svg>
<svg class="mx-1" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<use class="harvey-ball harvey-ball--watch" xlink:href="#watch-harvey-ball"></use>
</svg>
<svg class="mx-1" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<use class="harvey-ball harvey-ball--growth" xlink:href="#growth-harvey-ball"></use>
</svg>
<svg class="mx-1" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<use class="harvey-ball harvey-ball--approval" xlink:href="#approval-harvey-ball"></use>
</svg>
<svg class="ms-1 me-3" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<use class="harvey-ball harvey-ball--ideal" xlink:href="#ideal-harvey-ball"></use>
</svg>
<div class="font-size-14">Ideal</div>
<div class="harvey-ball-legend">
<div class="font-size-14">Warning</div>
<svg class="ms-3 me-1" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<use class="harvey-ball harvey-ball--warning" xlink:href="#warning-harvey-ball"></use>
</svg>
<svg class="mx-1" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<use class="harvey-ball harvey-ball--watch" xlink:href="#watch-harvey-ball"></use>
</svg>
<svg class="mx-1" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<use class="harvey-ball harvey-ball--growth" xlink:href="#growth-harvey-ball"></use>
</svg>
<svg class="mx-1" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<use class="harvey-ball harvey-ball--approval" xlink:href="#approval-harvey-ball"></use>
</svg>
<svg class="ms-1 me-3" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<use class="harvey-ball harvey-ball--ideal" xlink:href="#ideal-harvey-ball"></use>
</svg>
<div class="font-size-14">Ideal</div>
</div>
</div>
</div>
<%= render partial: "quality_framework_indicators", locals: { category_presenters: @category_presenters }, cached: true %>
</div>
<%= render partial: "quality_framework_indicators", locals: { category_presenters: @category_presenters }, cached: true %>
</div>
<div class="card">
<h2 class="sub-header-2 mb-4">Distance From Benchmark</h2>
<div class="card">
<h2 class="sub-header-2 mb-4">Distance From Benchmark</h2>
<%= render partial: "variance_chart", locals: { presenters: @variance_chart_row_presenters } , cached: true %>
</div>
<% end %>
<%= render partial: "variance_chart", locals: { presenters: @variance_chart_row_presenters } , cached: true %>
</div>
<% cache [@district, @school, @academic_year] do %>
<% if @district == District.find_by_name("Boston") %>
<%= render partial: 'layouts/boston_modal' %>
<% elsif @has_empty_dataset %>

@ -42,6 +42,17 @@ namespace :data do
SurveyResponsesDataLoader.load_data filepath:
end
puts "=====================> Completed loading #{SurveyItemResponse.count} survey responses"
puts 'Refreshing response rates'
ResponseRateLoader.refresh
puts "=====================> Completed loading #{ResponseRate.count} survey responses"
end
desc 'refresh response rate values'
task refresh_response_rates: :environment do
puts 'Refreshing response rates'
ResponseRateLoader.refresh
puts "=====================> Completed loading #{ResponseRate.count} survey responses"
end
desc 'load admin_data'

@ -51,6 +51,9 @@ namespace :one_off do
puts "=====================> Loading data from csv at path: #{filepath}"
SurveyResponsesDataLoader.load_data filepath: filepath
puts "=====================> Completed loading #{SurveyItemResponse.count} survey responses"
puts 'Refreshing response rates'
ResponseRateLoader.refresh
puts "=====================> Completed loading #{ResponseRate.count} survey responses"
end
desc 'load winchester results for 2021-22'
@ -61,6 +64,9 @@ namespace :one_off do
puts "=====================> Loading data from csv at path: #{filepath}"
SurveyResponsesDataLoader.load_data filepath:
end
puts 'Refreshing response rates'
ResponseRateLoader.refresh
puts "=====================> Completed loading #{ResponseRate.count} survey responses"
end
desc 'list scales that have no survey responses'

@ -3,6 +3,9 @@ require 'rails_helper'
describe ResponseRateCalculator, type: :model do
let(:school) { create(:school) }
let(:academic_year) { create(:academic_year) }
let(:survey) { create(:survey, school:, academic_year:) }
let(:short_form_survey) { create(:survey, form: :short, school:, academic_year:) }
let(:respondent) { create(:respondent, school:, academic_year:) }
describe StudentResponseRateCalculator do
let(:subcategory) { create(:subcategory) }
@ -23,13 +26,14 @@ describe ResponseRateCalculator, type: :model do
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 a students take a regular survey' do
context 'when the average number of student responses per question in a subcategory is equal to the student response threshold' do
before :each do
respondent
survey
end
it 'returns a response rate equal to the response threshold' do
expect(StudentResponseRateCalculator.new(subcategory:, school:,
academic_year:).rate).to eq 25
@ -39,8 +43,8 @@ describe ResponseRateCalculator, type: :model do
context 'when students take the short form survey' do
before :each do
create(:respondent, school:, academic_year:)
create(:survey, form: :short, school:, academic_year:)
respondent
short_form_survey
end
context 'when the average number of student responses per question in a subcategory is equal to the student response threshold' do
@ -49,19 +53,19 @@ describe ResponseRateCalculator, type: :model do
sufficient_student_survey_item_2.update! on_short_form: true
end
it 'returns 100 percent' do
it 'takes into account the responses from both survey items' 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
context 'and only one of the survey items is on the short form' do
before do
sufficient_student_survey_item_2.update! on_short_form: false
end
it 'returns 100 percent' do
it 'the response rate ignores the responses in the non-short form item' do
expect(StudentResponseRateCalculator.new(subcategory:, school:,
academic_year:).rate).to eq 50
academic_year:).rate).to eq 25
end
end
end
@ -69,8 +73,8 @@ describe ResponseRateCalculator, type: :model do
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:)
respondent
survey
create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD * 11, survey_item: sufficient_student_survey_item_2,
academic_year:, school:, likert_score: 1)
end
@ -124,8 +128,8 @@ describe ResponseRateCalculator, type: :model do
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:)
respondent
survey
end
it 'returns 25 percent' do
expect(TeacherResponseRateCalculator.new(subcategory:, school:,
@ -135,8 +139,8 @@ describe ResponseRateCalculator, type: :model do
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:)
respondent
survey
create_list(:survey_item_response, SurveyItemResponse::TEACHER_RESPONSE_THRESHOLD + 1, survey_item: sufficient_teacher_survey_item_3,
academic_year:, school:, likert_score: 1)
end
@ -148,8 +152,8 @@ describe ResponseRateCalculator, type: :model do
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:)
respondent
survey
create_list(:survey_item_response, SurveyItemResponse::TEACHER_RESPONSE_THRESHOLD * 11, survey_item: sufficient_teacher_survey_item_3,
academic_year:, school:, likert_score: 1)
end
@ -169,8 +173,8 @@ describe ResponseRateCalculator, type: :model do
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:)
respondent
survey
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

@ -0,0 +1,140 @@
require 'rails_helper'
describe ResponseRateLoader do
let(:school) { School.find_by_slug 'milford-high-school' }
let(:academic_year) { AcademicYear.find_by_range '2020-21' }
let(:respondents) do
respondents = Respondent.where(school:, academic_year:).first
respondents.total_students = 10
respondents.total_teachers = 10
respondents.save
end
let(:short_form_survey) do
survey = Survey.find_by(school:, academic_year:)
survey.form = :short
survey.save
survey
end
let(:subcategory) { Subcategory.find_by_subcategory_id '5D' }
let(:s_acst_q1) { SurveyItem.find_by_survey_item_id 's-acst-q1' }
let(:s_acst_q2) { SurveyItem.find_by_survey_item_id 's-acst-q2' } # short form
let(:s_acst_q3) { SurveyItem.find_by_survey_item_id 's-acst-q3' }
let(:s_poaf_q1) { SurveyItem.find_by_survey_item_id 's-poaf-q1' }
let(:s_poaf_q2) { SurveyItem.find_by_survey_item_id 's-poaf-q2' }
let(:s_poaf_q3) { SurveyItem.find_by_survey_item_id 's-poaf-q3' } # short form
let(:s_poaf_q4) { SurveyItem.find_by_survey_item_id 's-poaf-q4' }
let(:t_phya_q2) { SurveyItem.find_by_survey_item_id 't-phya-q2' }
let(:t_phya_q3) { SurveyItem.find_by_survey_item_id 't-phya-q3' }
let(:s_acst) { Scale.find_by_scale_id 's-acst' }
let(:s_poaf) { Scale.find_by_scale_id 's-poaf' }
let(:t_phya) { Scale.find_by_scale_id 't-phya' }
let(:response_rate) { ResponseRate.find_by(subcategory:, school:, academic_year:) }
before :each do
Rails.application.load_seed
respondents
end
after :each do
DatabaseCleaner.clean
end
describe 'self.refresh' do
context 'When refreshing response rates' do
context 'and half the students responded to each question' do
before :each do
create_list(:survey_item_response, 5, survey_item: s_acst_q1, likert_score: 3, school:, academic_year:)
create_list(:survey_item_response, 5, survey_item: s_acst_q2, likert_score: 3, school:, academic_year:)
create_list(:survey_item_response, 5, survey_item: s_acst_q3, likert_score: 3, school:, academic_year:)
create_list(:survey_item_response, 5, survey_item: s_poaf_q1, likert_score: 3, school:, academic_year:)
create_list(:survey_item_response, 5, survey_item: s_poaf_q2, likert_score: 3, school:, academic_year:)
create_list(:survey_item_response, 5, survey_item: s_poaf_q3, likert_score: 3, school:, academic_year:)
create_list(:survey_item_response, 5, survey_item: s_poaf_q4, likert_score: 3, school:, academic_year:)
create_list(:survey_item_response, 5, survey_item: t_phya_q2, likert_score: 3, school:, academic_year:)
create_list(:survey_item_response, 5, survey_item: t_phya_q3, likert_score: 3, school:, academic_year:)
ResponseRateLoader.refresh
end
it 'populates the database with response rates' do
expect(s_acst_q1.survey_item_id).to eq 's-acst-q1'
expect(subcategory.subcategory_id).to eq '5D'
expect(subcategory.name).to eq 'Health'
expect(s_acst.score(school:, academic_year:)).to eq 3
expect(s_poaf.score(school:, academic_year:)).to eq 3
expect(t_phya.score(school:, academic_year:)).to eq 3
expect(response_rate.student_response_rate).to eq 50
expect(response_rate.teacher_response_rate).to eq 50
expect(response_rate.meets_student_threshold).to be true
expect(response_rate.meets_teacher_threshold).to be true
end
context 'when running the loader a second time' do
it 'is idempotent' do
response_count = ResponseRate.count
ResponseRateLoader.refresh
second_count = ResponseRate.count
expect(response_count).to eq second_count
end
end
end
context 'and only the first question for each scale was asked; e.g. like on a short form' do
before :each do
create_list(:survey_item_response, 5, survey_item: s_acst_q1, likert_score: 3, school:, academic_year:)
create_list(:survey_item_response, 5, survey_item: s_poaf_q1, likert_score: 3, school:, academic_year:)
create_list(:survey_item_response, 5, survey_item: t_phya_q2, likert_score: 3, school:, academic_year:)
ResponseRateLoader.refresh
end
it 'only takes into account the first question and ignores the other questions in the scale' do
expect(response_rate.student_response_rate).to eq 50
expect(response_rate.teacher_response_rate).to eq 50
end
end
context 'and no respondent entry exists for the school and year' do
before do
Respondent.destroy_all
create_list(:survey_item_response, 5, survey_item: s_acst_q1, likert_score: 3, school:, academic_year:)
create_list(:survey_item_response, 5, survey_item: s_poaf_q1, likert_score: 3, school:, academic_year:)
create_list(:survey_item_response, 5, survey_item: t_phya_q2, likert_score: 3, school:, academic_year:)
ResponseRateLoader.refresh
end
it 'since no score can be calculated, it returns a default of 100' do
expect(response_rate.student_response_rate).to eq 100
expect(response_rate.teacher_response_rate).to eq 100
end
end
context 'and the school took the short form student survey' do
before :each do
create_list(:survey_item_response, 1, survey_item: s_acst_q1, likert_score: 3, school:, academic_year:)
create_list(:survey_item_response, 6, survey_item: s_acst_q2, likert_score: 3, school:, academic_year:) # short form
create_list(:survey_item_response, 1, survey_item: s_acst_q3, likert_score: 3, school:, academic_year:)
create_list(:survey_item_response, 1, survey_item: s_poaf_q1, likert_score: 3, school:, academic_year:)
create_list(:survey_item_response, 1, survey_item: s_poaf_q2, likert_score: 3, school:, academic_year:)
create_list(:survey_item_response, 6, survey_item: s_poaf_q3, likert_score: 3, school:, academic_year:) # short form
create_list(:survey_item_response, 1, survey_item: s_poaf_q4, likert_score: 3, school:, academic_year:)
create_list(:survey_item_response, 1, survey_item: t_phya_q2, likert_score: 3, school:, academic_year:)
create_list(:survey_item_response, 1, survey_item: t_phya_q3, likert_score: 3, school:, academic_year:)
short_form_survey
ResponseRateLoader.refresh
end
it 'only counts responses from survey items on the short form' do
expect(response_rate.student_response_rate).to eq 60
end
end
end
end
end

@ -12,6 +12,7 @@ describe 'SQM Application' do
driven_by :rack_test
page.driver.browser.basic_authorize(username, password)
create(:respondent, school:, academic_year:)
create(:survey, school:, academic_year:)
end
context 'when no measures meet their threshold' do

@ -5,6 +5,8 @@ describe 'analyze/index' do
subject { Nokogiri::HTML(rendered) }
let(:category) { create(:category) }
let(:subcategory) { create(:subcategory, category:) }
let(:school) { create(:school) }
let(:academic_year) { create(:academic_year) }
let(:support_for_teaching) do
measure = create(:measure, name: 'Support For Teaching Development & Growth', measure_id: '1A-I', subcategory:)
@ -41,22 +43,20 @@ describe 'analyze/index' do
ideal_low_benchmark: 4.5)
measure
end
let(:academic_year) { create(:academic_year) }
before :each do
# assign :category_presenters, []
# assign :grouped_bar_column_presenters, grouped_bar_column_presenters
assign :academic_year, academic_year
assign :available_academic_years, [academic_year]
assign :selected_academic_years, [academic_year]
# assign :academic_years, [academic_year]
assign :district, create(:district)
assign :school, create(:school)
assign :school, school
assign :category, category
assign :categories, [category]
assign :subcategory, subcategory
assign :subcategories, category.subcategories
assign :measures, [support_for_teaching, effective_leadership, professional_qualifications]
create(:respondent, school:, academic_year:)
create(:survey, school:, academic_year:)
render
end
@ -107,7 +107,10 @@ describe 'analyze/index' do
end
it 'displays disabled checkboxes for years that dont have data' do
ResponseRateLoader.refresh
year_checkbox = subject.css("##{academic_year.range}").first
expect(year_checkbox.name).to eq 'input'
expect(academic_year.range).to eq '2050-51'
expect(year_checkbox).to have_attribute 'disabled'
end
end

Loading…
Cancel
Save