Update logic for calculating student response rate. Remove references

to survey table.  We no longer check or keep track of the survey type.
Instead we look in the database to see if a survey item has at least 10
responses.  If it does, that survey item was presented to the respondent
and we count it, and all responses when calculating the response rate.

Remove response rate timestamp from caching logic because we no longer
add the response rate to the database. All response rates are calculated
on the fly

Update three_b_two scraper to use teacher only numbers

swap over to using https://profiles.doe.mass.edu/statereport/gradesubjectstaffing.aspx as the source of staffing information
mciea-main
rebuilt 3 years ago
parent 0bfde2805a
commit 07ed8dd259

@ -2,7 +2,7 @@
class AnalyzeController < SqmApplicationController class AnalyzeController < SqmApplicationController
before_action :assign_categories, :assign_subcategories, :assign_measures, :assign_academic_years, before_action :assign_categories, :assign_subcategories, :assign_measures, :assign_academic_years,
:response_rate_timestamp, :races, :selected_races, :graph, :graphs, :background, :race_score_timestamp, :races, :selected_races, :graph, :graphs, :background, :race_score_timestamp,
:source, :sources, :group, :groups, :selected_grades, :grades, :slice, :selected_genders, :genders, only: [:index] :source, :sources, :group, :groups, :selected_grades, :grades, :slice, :selected_genders, :genders, only: [:index]
def index; end def index; end
@ -35,18 +35,6 @@ class AnalyzeController < SqmApplicationController
end end
end end
def response_rate_timestamp
@response_rate_timestamp = begin
academic_year = @selected_academic_years.last
academic_year ||= @academic_year
rate = ResponseRate.where(school: @school,
academic_year:).order(updated_at: :DESC).first || Today.new
rate.updated_at
end
@response_rate_timestamp
end
def races def races
@races ||= Race.all.order(designation: :ASC) @races ||= Race.all.order(designation: :ASC)
end end

@ -1,7 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class CategoriesController < SqmApplicationController class CategoriesController < SqmApplicationController
before_action :response_rate_timestamp, only: [:index]
helper GaugeHelper helper GaugeHelper
def show def show
@ -9,16 +8,4 @@ class CategoriesController < SqmApplicationController
@category = CategoryPresenter.new(category: Category.find_by_slug(params[:id])) @category = CategoryPresenter.new(category: Category.find_by_slug(params[:id]))
end end
private
def response_rate_timestamp
@response_rate_timestamp = begin
rate = ResponseRate.where(school: @school,
academic_year: @academic_year).order(updated_at: :DESC).first || Today.new
rate.updated_at
end
@response_rate_timestamp
end
end end

@ -2,7 +2,6 @@
class OverviewController < SqmApplicationController class OverviewController < SqmApplicationController
before_action :check_empty_dataset, only: [:index] before_action :check_empty_dataset, only: [:index]
before_action :response_rate_timestamp, only: [:index]
helper VarianceHelper helper VarianceHelper
def index def index
@ -32,14 +31,4 @@ class OverviewController < SqmApplicationController
def subcategories def subcategories
@subcategories ||= Subcategory.all @subcategories ||= Subcategory.all
end end
def response_rate_timestamp
@response_rate_timestamp = begin
rate = ResponseRate.where(school: @school,
academic_year: @academic_year).order(updated_at: :DESC).first || Today.new
rate.updated_at
end
@response_rate_timestamp
end
end end

@ -3,7 +3,7 @@
class SqmApplicationController < ApplicationController class SqmApplicationController < ApplicationController
protect_from_forgery with: :exception, prepend: true protect_from_forgery with: :exception, prepend: true
before_action :set_schools_and_districts before_action :set_schools_and_districts
before_action :response_rate_timestamp
helper HeaderHelper helper HeaderHelper
private private

@ -1,5 +1,3 @@
require 'csv'
class Seeder class Seeder
attr_reader :rules attr_reader :rules
@ -48,66 +46,9 @@ class Seeder
School.import schools, on_duplicate_key_update: :all School.import schools, on_duplicate_key_update: :all
Respondent.joins(:school).where.not("school.dese_id": dese_ids).destroy_all Respondent.joins(:school).where.not("school.dese_id": dese_ids).destroy_all
Survey.joins(:school).where.not("school.dese_id": dese_ids).destroy_all
School.where.not(dese_id: dese_ids).destroy_all School.where.not(dese_id: dese_ids).destroy_all
end end
def seed_surveys(csv_file)
surveys = []
CSV.parse(File.read(csv_file), headers: true) do |row|
district_name = row['District'].strip
next if rules.any? do |rule|
rule.new(row:).skip_row?
end
district = District.find_or_create_by! name: district_name
dese_id = row['DESE School ID'].strip
school = School.find_or_initialize_by(dese_id:, district:)
academic_years = AcademicYear.all
academic_years.each do |academic_year|
short_form = row["Short Form Only (#{academic_year.range})"]
survey = Survey.find_or_initialize_by(school:, academic_year:)
is_short_form_school = marked?(short_form)
survey.form = is_short_form_school ? Survey.forms[:short] : Survey.forms[:normal]
surveys << survey
end
end
Survey.import surveys, on_duplicate_key_update: :all
end
def seed_respondents(csv_file)
schools = []
CSV.parse(File.read(csv_file), headers: true) do |row|
dese_id = row['DESE School ID'].strip.to_i
district_name = row['District'].strip
next if rules.any? do |rule|
rule.new(row:).skip_row?
end
district = District.find_or_create_by! name: district_name
school = School.find_by(dese_id:, district:)
schools << school
academic_years = AcademicYear.all
academic_years.each do |academic_year|
total_students = row["Total Students for Response Rate (#{academic_year.range})"]
total_teachers = row["Total Teachers for Response Rate (#{academic_year.range})"]
total_students = remove_commas(total_students)
total_teachers = remove_commas(total_teachers)
respondent = Respondent.find_or_initialize_by(school:, academic_year:)
respondent.total_students = total_students
respondent.total_teachers = total_teachers
respondent.academic_year = academic_year
respondent.save
end
end
Respondent.where.not(school: schools).destroy_all
end
def seed_sqm_framework(csv_file) def seed_sqm_framework(csv_file)
admin_data_item_ids = [] admin_data_item_ids = []
CSV.parse(File.read(csv_file), headers: true) do |row| CSV.parse(File.read(csv_file), headers: true) do |row|
@ -178,6 +119,18 @@ class Seeder
DemographicLoader.load_data(filepath: csv_file) DemographicLoader.load_data(filepath: csv_file)
end end
def seed_enrollment(csv_file)
EnrollmentLoader.load_data(filepath: csv_file)
end
def seed_staffing(csv_file)
StaffingLoader.load_data(filepath: csv_file)
missing_staffing_for_current_year = Respondent.where(academic_year: AcademicYear.order(:range).last).none? do |respondent|
respondent.total_teachers.present?
end
StaffingLoader.clone_previous_year_data if missing_staffing_for_current_year
end
private private
def marked?(mark) def marked?(mark)
@ -185,6 +138,6 @@ class Seeder
end end
def remove_commas(target) def remove_commas(target)
target.gsub(',', '') if target.present? target.delete(',') if target.present?
end end
end end

@ -51,16 +51,6 @@ class Measure < ActiveRecord::Base
@includes_admin_data_items ||= admin_data_items.any? @includes_admin_data_items ||= admin_data_items.any?
end end
# def sources
# @sources ||= begin
# sources = []
# sources << Source.new(name: :admin_data, collection: admin_data_items) if includes_admin_data_items?
# sources << Source.new(name: :student_surveys, collection: student_survey_items) if includes_student_survey_items?
# sources << Source.new(name: :teacher_surveys, collection: teacher_survey_items) if includes_teacher_survey_items?
# sources
# end
# end
def score(school:, academic_year:) def score(school:, academic_year:)
@score ||= Hash.new do |memo, (school, academic_year)| @score ||= Hash.new do |memo, (school, academic_year)|
next Score::NIL_SCORE if incalculable_score(school:, academic_year:) next Score::NIL_SCORE if incalculable_score(school:, academic_year:)
@ -72,7 +62,6 @@ class Measure < ActiveRecord::Base
memo[[school, academic_year]] = scorify(average:, school:, academic_year:) memo[[school, academic_year]] = scorify(average:, school:, academic_year:)
end end
@score[[school, academic_year]] @score[[school, academic_year]]
end end
@ -212,18 +201,16 @@ class Measure < ActiveRecord::Base
def no_student_responses_exist?(school:, academic_year:) def no_student_responses_exist?(school:, academic_year:)
@no_student_responses_exist ||= Hash.new do |memo, (school, academic_year)| @no_student_responses_exist ||= Hash.new do |memo, (school, academic_year)|
memo[[school, academic_year]] = student_survey_items_by_survey_type(school:, academic_year:).all? do |survey_item| memo[[school, academic_year]] =
survey_item.survey_item_responses.where(school:, academic_year:).none? SurveyItemResponse.where(school:, academic_year:, survey_item: survey_items.student_survey_items).count.zero?
end
end end
@no_student_responses_exist[[school, academic_year]] @no_student_responses_exist[[school, academic_year]]
end end
def no_teacher_responses_exist?(school:, academic_year:) def no_teacher_responses_exist?(school:, academic_year:)
@no_teacher_responses_exist ||= Hash.new do |memo, (school, academic_year)| @no_teacher_responses_exist ||= Hash.new do |memo, (school, academic_year)|
memo[[school, academic_year]] = teacher_survey_items.all? do |survey_item| memo[[school, academic_year]] =
survey_item.survey_item_responses.where(school:, academic_year:).none? SurveyItemResponse.where(school:, academic_year:, survey_item: survey_items.teacher_survey_items).count.zero?
end
end end
@no_teacher_responses_exist[[school, academic_year]] @no_teacher_responses_exist[[school, academic_year]]
end end

@ -3,4 +3,17 @@
class Respondent < ApplicationRecord class Respondent < ApplicationRecord
belongs_to :school belongs_to :school
belongs_to :academic_year belongs_to :academic_year
validates :school, uniqueness: { scope: :academic_year }
def counts_by_grade
@counts_by_grade ||= {}.tap do |row|
attributes = %i[pk k one two three four five six seven eight nine ten eleven twelve]
grades = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
attributes.zip(grades).each do |attribute, grade|
count = send(attribute) if send(attribute).present?
row[grade] = count unless count.nil? || count.zero?
end
end
end
end end

@ -14,7 +14,7 @@ class ResponseRateCalculator
def rate def rate
return 100 if population_data_unavailable? return 100 if population_data_unavailable?
return 0 unless survey_item_count.positive? return 0 unless survey_items_have_sufficient_responses?
return 0 unless total_possible_responses.positive? return 0 unless total_possible_responses.positive?
@ -35,14 +35,6 @@ class ResponseRateCalculator
response_rate > 100 ? 100 : response_rate response_rate > 100 ? 100 : response_rate
end end
def survey
Survey.find_by(school:, academic_year:)
end
def raw_response_rate
(average_responses_per_survey_item / total_possible_responses.to_f * 100).round
end
def average_responses_per_survey_item def average_responses_per_survey_item
response_count / survey_item_count.to_f response_count / survey_item_count.to_f
end end

@ -1,37 +1,54 @@
# frozen_string_literal: true # frozen_string_literal: true
class StudentResponseRateCalculator < ResponseRateCalculator class StudentResponseRateCalculator < ResponseRateCalculator
private def raw_response_rate
rates_by_grade.length.positive? ? rates_by_grade.average : 0
end
def rates_by_grade
@rates_by_grade ||= counts_by_grade.map do |grade, num_of_students_in_grade|
sufficient_survey_items = survey_items_with_sufficient_responses(grade:).keys
actual_response_count_for_grade = SurveyItemResponse.where(school:, academic_year:, grade:,
survey_item: sufficient_survey_items).count.to_f
count_of_survey_items_with_sufficient_responses = survey_item_count(grade:)
if count_of_survey_items_with_sufficient_responses.nil? || count_of_survey_items_with_sufficient_responses.zero? || num_of_students_in_grade.nil? || num_of_students_in_grade.zero?
next nil
end
actual_response_count_for_grade / count_of_survey_items_with_sufficient_responses / num_of_students_in_grade * 100
end.compact
end
def survey_item_count def counts_by_grade
@survey_item_count ||= begin @counts_by_grade ||= respondents.counts_by_grade
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:, academic_year:).none?
end end
survey_items.count
def survey_items_have_sufficient_responses?
rates_by_grade.length.positive?
end end
def survey_items_with_sufficient_responses(grade:)
SurveyItem.joins('inner join survey_item_responses on survey_item_responses.survey_item_id = survey_items.id')
.student_survey_items
.where("survey_item_responses.school": school, "survey_item_responses.academic_year": academic_year, "survey_item_responses.grade": grade, "survey_item_responses.survey_item_id": subcategory.survey_items.student_survey_items)
.group('survey_items.id')
.having('count(*) >= 10')
.count
end end
def response_count def survey_item_count(grade:)
@response_count ||= @subcategory.measures.map do |measure| survey_items_with_sufficient_responses(grade:).count
measure.student_survey_items.map do |survey_item| end
next 0 if survey.form == 'short' && survey_item.on_short_form == false
survey_item.survey_item_responses.where(school:, def respondents
academic_year:).exclude_boston.count @respondents ||= Respondent.find_by(school:, academic_year:)
end.sum
end.sum
end end
def total_possible_responses def total_possible_responses
@total_possible_responses ||= begin @total_possible_responses ||= begin
total_responses = Respondent.find_by(school:, academic_year:) return 0 unless respondents.present?
return 0 unless total_responses.present?
total_responses.total_students respondents.total_students
end end
end end
end end

@ -4,6 +4,7 @@ class Subcategory < ActiveRecord::Base
belongs_to :category, counter_cache: true belongs_to :category, counter_cache: true
has_many :measures has_many :measures
has_many :survey_items, through: :measures
def score(school:, academic_year:) def score(school:, academic_year:)
scores = measures.map do |measure| scores = measures.map do |measure|
@ -15,19 +16,13 @@ class Subcategory < ActiveRecord::Base
def response_rate(school:, academic_year:) def response_rate(school:, academic_year:)
@response_rate ||= Hash.new do |memo, (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
@response_rate[[school, academic_year]] || create_response_rate(school:, academic_year:)
end
private
def create_response_rate(school:, academic_year:)
student = StudentResponseRateCalculator.new(subcategory: self, school:, academic_year:) student = StudentResponseRateCalculator.new(subcategory: self, school:, academic_year:)
teacher = TeacherResponseRateCalculator.new(subcategory: self, school:, academic_year:) teacher = TeacherResponseRateCalculator.new(subcategory: self, school:, academic_year:)
ResponseRate.new(school:, academic_year:, subcategory: self, student_response_rate: student.rate, memo[[school, academic_year]] = ResponseRate.new(school:, academic_year:, subcategory: self, student_response_rate: student.rate,
teacher_response_rate: teacher.rate, meets_student_threshold: student.meets_student_threshold?, teacher_response_rate: teacher.rate, meets_student_threshold: student.meets_student_threshold?,
meets_teacher_threshold: teacher.meets_teacher_threshold?) meets_teacher_threshold: teacher.meets_teacher_threshold?)
end end
@response_rate[[school, academic_year]]
end
end end

@ -16,14 +16,17 @@ class SurveyItem < ActiveRecord::Base
end end
scope :student_survey_items, lambda { scope :student_survey_items, lambda {
where("survey_item_id LIKE 's-%'") where("survey_items.survey_item_id LIKE 's-%'")
} }
scope :teacher_survey_items, lambda { scope :teacher_survey_items, lambda {
where("survey_item_id LIKE 't-%'") where("survey_items.survey_item_id LIKE 't-%'")
} }
scope :short_form_items, lambda { scope :short_form_items, lambda {
where(on_short_form: true) where(on_short_form: true)
} }
scope :early_education_surveys, lambda {
where("survey_items.survey_item_id LIKE '%-%-es%'")
}
def description def description
DataAvailability.new(survey_item_id, prompt, true) DataAvailability.new(survey_item_id, prompt, true)

@ -2,7 +2,7 @@
class SurveyItemResponse < ActiveRecord::Base class SurveyItemResponse < ActiveRecord::Base
TEACHER_RESPONSE_THRESHOLD = 2 TEACHER_RESPONSE_THRESHOLD = 2
STUDENT_RESPONSE_THRESHOLD = 2 STUDENT_RESPONSE_THRESHOLD = 10
belongs_to :academic_year belongs_to :academic_year
belongs_to :school belongs_to :school

@ -9,6 +9,10 @@ class TeacherResponseRateCalculator < ResponseRateCalculator
end.sum end.sum
end end
def survey_items_have_sufficient_responses?
survey_item_count.positive?
end
def response_count def response_count
@response_count ||= @subcategory.measures.map do |measure| @response_count ||= @subcategory.measures.map do |measure|
measure.teacher_survey_items.map do |survey_item| measure.teacher_survey_items.map do |survey_item|
@ -26,4 +30,8 @@ class TeacherResponseRateCalculator < ResponseRateCalculator
total_responses.total_teachers total_responses.total_teachers
end end
end end
def raw_response_rate
(average_responses_per_survey_item / total_possible_responses.to_f * 100).round
end
end end

@ -1,6 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
# TODO: resize bars so they never extend beyond the bounds of the column
module Analyze module Analyze
module Graph module Graph
module Column module Column

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

@ -35,7 +35,7 @@ module Dese
browser.goto(url) browser.goto(url)
selectors.each do |key, value| selectors.each do |key, value|
return unless browser.option(text: value).present? next unless browser.option(text: value).present?
browser.select(id: key).select(text: value) browser.select(id: key).select(text: value)
end end

@ -0,0 +1,35 @@
require 'watir'
module Dese
class Staffing
include Dese::Scraper
attr_reader :filepath
def initialize(filepath: Rails.root.join('data', 'staffing', 'staffing.csv'))
@filepath = filepath
end
def run_all
scrape_staffing(filepath:)
end
def scrape_staffing(filepath:)
headers = ['Raw likert calculation', 'Likert Score', 'Admin Data Item', 'Academic Year',
'School Name', 'DESE ID',
'PK-2 (#)', '3-5 (#)', '6-8 (#)', '9-12 (#)', 'Multiple Grades (#)',
'All Grades (#)', 'FTE Count']
write_headers(filepath:, headers:)
run do |academic_year|
admin_data_item_id = 'NA'
url = 'https://profiles.doe.mass.edu/statereport/gradesubjectstaffing.aspx'
range = academic_year.range
selectors = { 'ctl00_ContentPlaceHolder1_ddReportType' => 'School',
'ctl00_ContentPlaceHolder1_ddYear' => range,
'ctl00_ContentPlaceHolder1_ddDisplay' => 'Full-time Equivalents' }
submit_id = 'btnViewReport'
calculation = ->(_headers, _items) { 'NA' }
Prerequisites.new(filepath, url, selectors, submit_id, admin_data_item_id, calculation)
end
end
end
end

@ -43,7 +43,8 @@ module Dese
range = academic_year.range range = academic_year.range
selectors = { 'ctl00_ContentPlaceHolder1_ddReportType' => 'School', selectors = { 'ctl00_ContentPlaceHolder1_ddReportType' => 'School',
'ctl00_ContentPlaceHolder1_ddYear' => range, 'ctl00_ContentPlaceHolder1_ddYear' => range,
'ctl00_ContentPlaceHolder1_ddDisplay' => 'Percentages' } 'ctl00_ContentPlaceHolder1_ddDisplay' => 'Percentages',
'ctl00_ContentPlaceHolder1_ddClassification' => 'Teacher' }
submit_id = 'ctl00_ContentPlaceHolder1_btnViewReport' submit_id = 'ctl00_ContentPlaceHolder1_btnViewReport'
calculation = lambda { |headers, items| calculation = lambda { |headers, items|
african_american_index = headers['African American (%)'] african_american_index = headers['African American (%)']

@ -1,6 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
# TODO
require 'csv' require 'csv'
class StaffingLoader class StaffingLoader

@ -9,21 +9,17 @@
</div> </div>
<% end %> <% end %>
</nav> </nav>
<select id="select-academic-year" class="form-select" name="academic-year"> <select id="select-academic-year" class="form-select" name="academic-year">
<% @academic_years.each do |year| %> <% @academic_years.each do |year| %>
<option value="<%= url_for [@district, @school, @category , {year: year.range} ]%>" <%= @academic_year == year ? "selected" : nil %>><%= year.formatted_range %></option> <option value="<%= url_for [@district, @school, @category , {year: year.range} ]%>" <%= @academic_year == year ? "selected" : nil %>><%= year.formatted_range %></option>
<% end %> <% end %>
</select> </select>
<% end %> <% end %>
<% cache [@category, @school, @academic_year] do %> <% cache [@category, @school, @academic_year] do %>
<p class="construct-id">Category <%= @category.id %></p> <p class="construct-id">Category <%= @category.id %></p>
<h1 class="sub-header font-bitter color-red"><%= @category.name %></h1> <h1 class="sub-header font-bitter color-red"><%= @category.name %></h1>
<p class="col-8 body-large"><%= @category.description %></p> <p class="col-8 body-large"><%= @category.description %></p>
<% @category.subcategories(academic_year: @academic_year, school: @school).each do |subcategory| %> <% @category.subcategories(academic_year: @academic_year, school: @school).each do |subcategory| %>
<%= render partial: "subcategory_section", locals: {subcategory: subcategory} %> <%= render partial: "subcategory_section", locals: {subcategory: subcategory} %>
<% end %> <% end %>
<% end %> <% end %>

@ -1,16 +1,13 @@
<% content_for :navigation do %> <% 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"> <select id="select-academic-year" class="form-select" name="academic-year">
<% @academic_years.each do |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> <option value="<%= district_school_overview_index_path(@district, @school, {year: year.range}) %>" <%= @academic_year == year ? "selected" : nil %>><%= year.formatted_range %></option>
<% end %> <% end %>
</select> </select>
<% end %> <% end %>
<% cache do %> <% cache do %>
<svg class="d-none"> <svg class="d-none">
<symbol viewBox="0 0 24 24" id="warning-harvey-ball"> <symbol viewBox="0 0 24 24" id="warning-harvey-ball">
<circle cx="12" cy="12" r="11.5" fill="white" stroke="none" /> <circle cx="12" cy="12" r="11.5" fill="white" stroke="none" />
<path d=" <path d="
@ -22,7 +19,6 @@
/> />
<circle cx="12" cy="12" r="11.5" fill="none" /> <circle cx="12" cy="12" r="11.5" fill="none" />
</symbol> </symbol>
<symbol viewBox="0 0 24 24" id="watch-harvey-ball"> <symbol viewBox="0 0 24 24" id="watch-harvey-ball">
<circle cx="12" cy="12" r="11.5" fill="white" stroke="none" /> <circle cx="12" cy="12" r="11.5" fill="white" stroke="none" />
<path d=" <path d="
@ -34,7 +30,6 @@
/> />
<circle cx="12" cy="12" r="11.5" fill="none" /> <circle cx="12" cy="12" r="11.5" fill="none" />
</symbol> </symbol>
<symbol viewBox="0 0 24 24" id="growth-harvey-ball"> <symbol viewBox="0 0 24 24" id="growth-harvey-ball">
<circle cx="12" cy="12" r="11.5" fill="white" stroke="none" /> <circle cx="12" cy="12" r="11.5" fill="white" stroke="none" />
<path d=" <path d="
@ -46,30 +41,25 @@
/> />
<circle cx="12" cy="12" r="11.5" fill="none" /> <circle cx="12" cy="12" r="11.5" fill="none" />
</symbol> </symbol>
<symbol viewBox="0 0 24 24" id="approval-harvey-ball"> <symbol viewBox="0 0 24 24" id="approval-harvey-ball">
<circle cx="12" cy="12" r="11.5" /> <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" <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" /> stroke-width=".5" stroke="white" fill="white" />
</symbol> </symbol>
<symbol viewBox="0 0 24 24" id="ideal-harvey-ball"> <symbol viewBox="0 0 24 24" id="ideal-harvey-ball">
<circle cx="12" cy="12" r="11.5" /> <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" <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" /> stroke-width=".5" stroke="white" fill="white" />
</symbol> </symbol>
<symbol viewBox="0 0 24 24" id="insufficient_data-harvey-ball"> <symbol viewBox="0 0 24 24" id="insufficient_data-harvey-ball">
<circle cx="12" cy="12" r="11.5" /> <circle cx="12" cy="12" r="11.5" />
</symbol> </symbol>
</svg> </svg>
<% end %> <% end %>
<% cache [@school, @academic_year] do %>
<% cache [@school, @academic_year, @response_rate_timestamp] do %>
<div class="card"> <div class="card">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<h2 class="sub-header-2">School Quality Framework Indicators</h2> <h2 class="sub-header-2">School Quality Framework Indicators</h2>
<div class="harvey-ball-legend"> <div class="harvey-ball-legend">
<div class="font-size-14">Warning</div> <div class="font-size-14">Warning</div>
<svg class="ms-3 me-1" width="16" height="16" xmlns="http://www.w3.org/2000/svg"> <svg class="ms-3 me-1" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
@ -90,16 +80,12 @@
<div class="font-size-14">Ideal</div> <div class="font-size-14">Ideal</div>
</div> </div>
</div> </div>
<%= render partial: "quality_framework_indicators", locals: { category_presenters: @category_presenters }, cached: true %> <%= render partial: "quality_framework_indicators", locals: { category_presenters: @category_presenters }, cached: true %>
</div> </div>
<div class="card"> <div class="card">
<h2 class="sub-header-2 mb-4">Distance From Benchmark</h2> <h2 class="sub-header-2 mb-4">Distance From Benchmark</h2>
<%= render partial: "variance_chart", locals: { presenters: @variance_chart_row_presenters } , cached: true %> <%= render partial: "variance_chart", locals: { presenters: @variance_chart_row_presenters } , cached: true %>
</div> </div>
<% if @district == District.find_by_name("Boston") %> <% if @district == District.find_by_name("Boston") %>
<%= render partial: 'layouts/boston_modal' %> <%= render partial: 'layouts/boston_modal' %>
<% elsif @has_empty_dataset %> <% elsif @has_empty_dataset %>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,18 @@
class AddGradesToRespondent < ActiveRecord::Migration[7.0]
def change
add_column :respondents, :pk, :integer
add_column :respondents, :k, :integer
add_column :respondents, :one, :integer
add_column :respondents, :two, :integer
add_column :respondents, :three, :integer
add_column :respondents, :four, :integer
add_column :respondents, :five, :integer
add_column :respondents, :six, :integer
add_column :respondents, :seven, :integer
add_column :respondents, :eight, :integer
add_column :respondents, :nine, :integer
add_column :respondents, :ten, :integer
add_column :respondents, :eleven, :integer
add_column :respondents, :twelve, :integer
end
end

@ -0,0 +1,5 @@
class AddUniqueIndextoRespondent < ActiveRecord::Migration[7.0]
def change
add_index :respondents, %i[school_id academic_year_id], unique: true
end
end

@ -0,0 +1,5 @@
class RemoveSchoolIndexFromRespondents < ActiveRecord::Migration[7.0]
def change
remove_index :respondents, name: 'index_respondents_on_school_id'
end
end

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2022_10_22_225523) do ActiveRecord::Schema[7.0].define(version: 2023_03_04_132801) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements" enable_extension "pg_stat_statements"
enable_extension "plpgsql" enable_extension "plpgsql"
@ -330,8 +330,22 @@ ActiveRecord::Schema[7.0].define(version: 2022_10_22_225523) do
t.float "total_teachers" t.float "total_teachers"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "pk"
t.integer "k"
t.integer "one"
t.integer "two"
t.integer "three"
t.integer "four"
t.integer "five"
t.integer "six"
t.integer "seven"
t.integer "eight"
t.integer "nine"
t.integer "ten"
t.integer "eleven"
t.integer "twelve"
t.index ["academic_year_id"], name: "index_respondents_on_academic_year_id" t.index ["academic_year_id"], name: "index_respondents_on_academic_year_id"
t.index ["school_id"], name: "index_respondents_on_school_id" t.index ["school_id", "academic_year_id"], name: "index_respondents_on_school_id_and_academic_year_id", unique: true
end end
create_table "response_rates", force: :cascade do |t| create_table "response_rates", force: :cascade do |t|

@ -2,9 +2,9 @@ require "#{Rails.root}/app/lib/seeder"
seeder = Seeder.new seeder = Seeder.new
seeder.seed_academic_years '2016-17', '2017-18', '2018-19', '2019-20', '2020-21', '2021-22', '2022-23' seeder.seed_academic_years "2016-17", "2017-18", "2018-19", "2019-20", "2020-21", "2021-22", "2022-23"
seeder.seed_districts_and_schools Rails.root.join('data', 'master_list_of_schools_and_districts.csv') seeder.seed_districts_and_schools Rails.root.join("data", "master_list_of_schools_and_districts.csv")
seeder.seed_surveys Rails.root.join('data', 'master_list_of_schools_and_districts.csv') seeder.seed_sqm_framework Rails.root.join("data", "sqm_framework.csv")
seeder.seed_respondents Rails.root.join('data', 'master_list_of_schools_and_districts.csv') seeder.seed_demographics Rails.root.join("data", "demographics.csv")
seeder.seed_sqm_framework Rails.root.join('data', 'sqm_framework.csv') seeder.seed_enrollment Rails.root.join("data", "enrollment", "enrollment.csv")
seeder.seed_demographics Rails.root.join('data', 'demographics.csv') seeder.seed_staffing Rails.root.join("data", "staffing", "staffing.csv")

@ -0,0 +1,21 @@
namespace :scrape do
desc 'scrape dese site for admin data'
task admin: :environment do
puts 'scraping data from dese'
scrapers = [Dese::OneAOne, Dese::OneAThree, Dese::TwoAOne, Dese::TwoCOne, Dese::ThreeAOne, Dese::ThreeATwo,
Dese::ThreeBOne, Dese::ThreeBTwo, Dese::FourAOne, Dese::FourBTwo, Dese::FourDOne, Dese::FiveCOne, Dese::FiveDTwo]
scrapers.each do |scraper|
scraper.new.run_all
end
end
desc 'scrape dese site for teacher staffing information'
task enrollment: :environment do
Dese::ThreeATwo.new.scrape_enrollments(filepath: Rails.root.join('data', 'enrollment', 'enrollment.csv'))
end
desc 'scrape dese site for student staffing information'
task staffing: :environment do
Dese::Staffing.new.run_all
end
end

@ -152,6 +152,7 @@ FactoryBot.define do
factory :survey_item_response do factory :survey_item_response do
likert_score { 3 } likert_score { 3 }
response_id { rand.to_s } response_id { rand.to_s }
grade { 1 }
academic_year academic_year
school school
survey_item factory: :teacher_survey_item survey_item factory: :teacher_survey_item
@ -166,6 +167,8 @@ FactoryBot.define do
factory :respondent do factory :respondent do
school school
academic_year academic_year
one { 40 }
total_students { SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD * 4 } total_students { SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD * 4 }
total_teachers { SurveyItemResponse::TEACHER_RESPONSE_THRESHOLD * 4 } total_teachers { SurveyItemResponse::TEACHER_RESPONSE_THRESHOLD * 4 }
end end

@ -26,7 +26,7 @@ RSpec.describe Measure, type: :model do
let(:teacher_ideal_low_benchmark) { 4.2 } let(:teacher_ideal_low_benchmark) { 4.2 }
before do before do
create(:respondent, school:, academic_year:) create(:respondent, school:, academic_year:, one: 40)
create(:survey, school:, academic_year:) create(:survey, school:, academic_year:)
create(:respondent, school: short_form_school, academic_year:) create(:respondent, school: short_form_school, academic_year:)
create(:survey, school: short_form_school, academic_year:, form: 'short') create(:survey, school: short_form_school, academic_year:, form: 'short')
@ -369,27 +369,6 @@ RSpec.describe Measure, type: :model do
expect(measure.score(school:, academic_year:).meets_student_threshold?).to be false expect(measure.score(school:, academic_year:).meets_student_threshold?).to be false
end end
end end
context 'and the school is a short form school' do
before :each do
create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD,
survey_item: student_survey_item_1, academic_year:, school: short_form_school, likert_score: 1)
create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD,
survey_item: student_survey_item_2, academic_year:, school: short_form_school, likert_score: 1)
create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD,
survey_item: student_survey_item_3, academic_year:, school: short_form_school, likert_score: 1)
create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD,
survey_item: short_form_student_survey_item_1, academic_year:, school: short_form_school, likert_score: 3)
create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD,
survey_item: short_form_student_survey_item_2, academic_year:, school: short_form_school, likert_score: 4)
create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD,
survey_item: short_form_student_survey_item_3, academic_year:, school: short_form_school, likert_score: 5)
end
it 'ignores any responses not on the short form and gives the average of short form survey items' do
expect(measure.score(school: short_form_school, academic_year:).average).to eq 4
end
end
end end
context 'when the measure includes both teacher and student data' do context 'when the measure includes both teacher and student data' do
@ -475,7 +454,7 @@ RSpec.describe Measure, type: :model do
create_list(:survey_item_response, SurveyItemResponse::TEACHER_RESPONSE_THRESHOLD - 1, create_list(:survey_item_response, SurveyItemResponse::TEACHER_RESPONSE_THRESHOLD - 1,
survey_item: teacher_survey_item_1, academic_year:, school:, likert_score: 1) survey_item: teacher_survey_item_1, academic_year:, school:, likert_score: 1)
create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD, create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD,
survey_item: student_survey_item_1, academic_year:, school:, likert_score: 5) survey_item: student_survey_item_1, academic_year:, school:, likert_score: 5, grade: 1)
end end
it 'returns the average of the likert scores of the student survey items' do it 'returns the average of the likert scores of the student survey items' do

@ -0,0 +1,76 @@
require 'rails_helper'
RSpec.describe Report::Pillar, type: :model do
let(:school) { create(:school, name: 'Abraham Lincoln Elementary School') }
let(:subcategory) { create(:subcategory, subcategory_id: '1A') }
let(:measure_1) { create(:measure, measure_id: '1A-iii', subcategory:) }
let(:measure_2) { create(:measure, measure_id: '1B-ii', subcategory:) }
let(:scale_1) { create(:scale, measure: measure_1) }
let(:scale_2) { create(:scale, measure: measure_2) }
let(:survey_item_1) { create(:student_survey_item, scale: scale_1) }
let(:survey_item_2) do
create(:student_survey_item, scale: scale_2, ideal_low_benchmark: 5)
end
let(:measures) do
subcategory.measures
end
let(:academic_year_1) { create(:academic_year, range: '2017-2018') }
let(:academic_year_2) { create(:academic_year, range: '2018-2019') }
let(:academic_years) { [academic_year_1, academic_year_2] }
before :each do
create(:respondent, school:, academic_year: academic_year_1)
create(:survey, school:, academic_year: academic_year_1)
measures
end
context '.pillar' do
it 'returns the GPS pillar' do
pillar = Report::Pillar.new(school:, measures:, indicator: 'Teaching Environment',
period: 'Current', academic_year: academic_year_1)
expect(pillar.pillar).to eq('Operational Efficiency')
end
end
context '.school' do
it 'returns the name of the school' do
pillar = Report::Pillar.new(school:, measures:, indicator: 'The Teaching Environment', period: 'Current',
academic_year: academic_year_1)
expect(pillar.school_name).to eq('Abraham Lincoln Elementary School')
end
end
context '.score' do
before do
create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD, survey_item: survey_item_1, school:, academic_year: academic_year_1,
likert_score: 3)
create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD, survey_item: survey_item_1, school:, academic_year: academic_year_1,
likert_score: 5)
end
it 'returns the average score for all the measures in the pillar' do
pillar = Report::Pillar.new(school:, measures:, indicator: 'The Teaching Environment', period: 'Current',
academic_year: academic_year_1)
expect(pillar.score).to eq 4
end
end
context '.zone' do
before do
create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD, survey_item: survey_item_1, school:, academic_year: academic_year_1,
likert_score: 4)
create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD, survey_item: survey_item_1, school:, academic_year: academic_year_1,
likert_score: 5)
create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD, survey_item: survey_item_2, school:, academic_year: academic_year_1,
likert_score: 4)
create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD, survey_item: survey_item_2, school:, academic_year: academic_year_1,
likert_score: 5)
end
it 'returns the zone for the average score for all the measures in the pillar' do
pillar = Report::Pillar.new(school:, measures:, indicator: 'The Teaching Environment', period: 'Current',
academic_year: academic_year_1)
expect(pillar.score).to eq 4.5
expect(pillar.zone).to eq 'Approval'
end
end
end

@ -0,0 +1,17 @@
require 'rails_helper'
RSpec.describe Respondent, type: :model do
describe 'grade_counts' do
let(:single_grade_of_respondents) { create(:respondent, one: 10) }
let(:two_grades_of_respondents) { create(:respondent, pk: 10, k: 5, one: 0, two: 0, three: 0) }
let(:three_grades_of_respondents) { create(:respondent, one: 10, two: 5, twelve: 6, eleven: 0) }
context 'when the student respondents include one or more counts for the number of respondents' do
it 'returns a hash with only the grades that have a non-zero count of students' do
expect(single_grade_of_respondents.one).to eq(10)
expect(single_grade_of_respondents.counts_by_grade).to eq({ 1 => 10 })
expect(two_grades_of_respondents.counts_by_grade).to eq({ -1 => 10, 0 => 5 })
expect(three_grades_of_respondents.counts_by_grade).to eq({ 1 => 10, 2 => 5, 12 => 6 })
end
end
end
end

@ -4,11 +4,10 @@ describe ResponseRateCalculator, type: :model do
let(:school) { create(:school) } let(:school) { create(:school) }
let(:academic_year) { create(:academic_year) } let(:academic_year) { create(:academic_year) }
let(:survey) { create(:survey, school:, 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 describe StudentResponseRateCalculator do
let(:subcategory) { create(:subcategory) } let(:subcategory) { create(:subcategory) }
let(:second_subcategory) { create(:subcategory_with_measures) }
let(:sufficient_measure_1) { create(:measure, subcategory:) } let(:sufficient_measure_1) { create(:measure, subcategory:) }
let(:sufficient_scale_1) { create(:scale, measure: sufficient_measure_1) } let(:sufficient_scale_1) { create(:scale, measure: sufficient_measure_1) }
let(:sufficient_measure_2) { create(:measure, subcategory:) } let(:sufficient_measure_2) { create(:measure, subcategory:) }
@ -17,118 +16,177 @@ describe ResponseRateCalculator, type: :model do
let(:sufficient_student_survey_item_1) { create(:student_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(: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) } let(:sufficient_student_survey_item_2) { create(:student_survey_item, scale: sufficient_scale_2) }
let(:sufficient_student_survey_item_3) { create(:student_survey_item, scale: sufficient_scale_2) }
context 'when a students take a regular survey' do context '.raw_response_rate' do
context 'when the average number of student responses per question in a subcategory is equal to the student response threshold' do context 'when no survey item responses exist' do
before :each do before do
create_list(:survey_item_response, SurveyItemResponse::TEACHER_RESPONSE_THRESHOLD, survey_item: sufficient_teacher_survey_item, create(:respondent, school:, academic_year:, pk: 20)
academic_year:, school:, likert_score: 1) end
create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD, survey_item: sufficient_student_survey_item_1, it 'returns an average of the response rates for all grades' do
academic_year:, school:, likert_score: 4) expect(StudentResponseRateCalculator.new(subcategory:, school:, academic_year:).rate).to eq 0
create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD, survey_item: sufficient_student_survey_item_2,
academic_year:, school:, likert_score: 4)
respondent
survey
end end
it 'returns a response rate equal to the response threshold' do context 'or when the count of survey items does not meet the minimum threshold' do
expect(StudentResponseRateCalculator.new(subcategory:, school:, before do
academic_year:).rate).to eq 25 create_list(:survey_item_response, 9, survey_item: sufficient_student_survey_item_1, academic_year:,
school:, grade: 1)
end
it 'returns an average of the response rates for all grades' do
expect(StudentResponseRateCalculator.new(subcategory:, school:, academic_year:).rate).to eq 0
end
end end
end end
context 'when the average number of student responses per question is below the student threshold' do context 'when at least one survey item has sufficient responses' do
before :each do before do
create_list(:survey_item_response, 1, survey_item: sufficient_student_survey_item_1, create(:respondent, school:, academic_year:, total_students: 20, one: 20)
academic_year:, school:, likert_score: 4) create_list(:survey_item_response, 10, survey_item: sufficient_student_survey_item_1, academic_year:,
create_list(:survey_item_response, 1, survey_item: sufficient_student_survey_item_2, school:, grade: 1)
academic_year:, school:, likert_score: 4)
respondent
survey
end end
it 'reports insufficient student responses' do context 'and half of students responded' do
it 'reports a response rate of fifty percent' do
expect(StudentResponseRateCalculator.new(subcategory:, school:, expect(StudentResponseRateCalculator.new(subcategory:, school:,
academic_year:).rate).to eq 13 academic_year:).rate).to eq 50
end
end
context 'and another unrelated subcategory has responses' do
before do
create_list(:survey_item_response, 10,
survey_item: second_subcategory.measures.first.scales.first.survey_items.first, academic_year:, school:, grade: 1)
end
it 'does not count the responses for the unrelated subcategory' do
expect(StudentResponseRateCalculator.new(subcategory:, school:, expect(StudentResponseRateCalculator.new(subcategory:, school:,
academic_year:).meets_student_threshold?).to eq false academic_year:).rate).to eq 50
end
end
context 'there are responses for another survey item but not enough to meet the minimum threshold' do
before do
create_list(:survey_item_response, 9, survey_item: insufficient_student_survey_item_1, academic_year:,
school:, grade: 1)
end
it 'returns an average of the response rates for all grades' do
expect(StudentResponseRateCalculator.new(subcategory:, school:, academic_year:).rate).to eq 50
end end
end end
end end
context 'when students take the short form survey' do context 'when two survey items have sufficient responses' do
before :each do before do
create_list(:survey_item_response, SurveyItemResponse::TEACHER_RESPONSE_THRESHOLD, survey_item: sufficient_teacher_survey_item, create(:respondent, school:, academic_year:, total_students: 20, one: 20)
academic_year:, school:, likert_score: 1) create_list(:survey_item_response, 10, survey_item: sufficient_student_survey_item_1, academic_year:,
create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD, survey_item: sufficient_student_survey_item_1, school:, grade: 1)
academic_year:, school:, likert_score: 4) create_list(:survey_item_response, 20, survey_item: sufficient_student_survey_item_2, academic_year:,
create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD, survey_item: sufficient_student_survey_item_2, school:, grade: 1)
academic_year:, school:, likert_score: 4)
respondent
short_form_survey
end end
context 'when the average number of student responses per question in a subcategory is equal to the student response threshold' do context 'one one question got half the students to respond and the other got all the students to respond' do
before :each do it 'reports a response rate that averages fifty and 100' do
sufficient_student_survey_item_1.update! on_short_form: true expect(StudentResponseRateCalculator.new(subcategory:, school:, academic_year:).rate).to eq 75
sufficient_student_survey_item_2.update! on_short_form: true end
end end
it 'takes into account the responses from both survey items' do context 'and another unrelated subcategory has responses' do
expect(StudentResponseRateCalculator.new(subcategory:, school:, before do
academic_year:).rate).to eq 25 create_list(:survey_item_response, 10,
survey_item: second_subcategory.measures.first.scales.first.survey_items.first, academic_year:, school:, grade: 1)
end end
context 'and only one of the survey items is on the short form' do it 'does not count the responses for the unrelated subcategory' do
expect(StudentResponseRateCalculator.new(subcategory:, school:, academic_year:).rate).to eq 75
end
end
end
context 'when there survey items between two scales' do
before do before do
sufficient_student_survey_item_2.update! on_short_form: false create(:respondent, school:, academic_year:, total_students: 20, one: 20)
create_list(:survey_item_response, 20, survey_item: sufficient_student_survey_item_1, academic_year:,
school:, grade: 1)
create_list(:survey_item_response, 15, survey_item: sufficient_student_survey_item_2, academic_year:,
school:, grade: 1)
create_list(:survey_item_response, 10, survey_item: sufficient_student_survey_item_3, academic_year:,
school:, grade: 1)
end end
it 'the response rate ignores the responses in the non-short form item' do context 'one scale got all students to respond and another scale got an average response rate of fifty percent' do
expect(StudentResponseRateCalculator.new(subcategory:, school:, it 'computes the response rate by dividing the actual responses over possible responses' do
academic_year:).rate).to eq 25 # (20 + 15 + 10) / (20 + 20 + 20) * 100 = 75%
expect(StudentResponseRateCalculator.new(subcategory:, school:, academic_year:).rate).to eq 75
end
end
context 'and another unrelated subcategory has responses' do
before do
create_list(:survey_item_response, 10,
survey_item: second_subcategory.measures.first.scales.first.survey_items.first, academic_year:, school:, grade: 1)
end
it 'does not count the responses for the unrelated subcategory' do
expect(StudentResponseRateCalculator.new(subcategory:, school:, academic_year:).rate).to eq 75
end
end
end end
context 'when two grades have sufficient responses' do
context 'and half of one grade responded and all of the other grade responded' do
before do
create(:respondent, school:, academic_year:, total_students: 20, one: 20, two: 20)
create_list(:survey_item_response, 10, survey_item: sufficient_student_survey_item_1, academic_year:,
school:, grade: 1)
create_list(:survey_item_response, 20, survey_item: sufficient_student_survey_item_1, academic_year:,
school:, grade: 2)
end end
it 'reports a response rate that averages fifty and 100' do
expect(StudentResponseRateCalculator.new(subcategory:, school:, academic_year:).rate).to eq 75
end end
end end
context 'when the average number of teacher responses is greater than the total possible responses' do context 'and two grades responded to different questions at different rates' do
before do before do
respondent create(:respondent, school:, academic_year:, total_students: 20, one: 20, two: 20)
survey create_list(:survey_item_response, 10, survey_item: sufficient_student_survey_item_1, academic_year:,
create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD * 11, survey_item: sufficient_student_survey_item_2, school:, grade: 1)
academic_year:, school:, likert_score: 1) create_list(:survey_item_response, 20, survey_item: sufficient_student_survey_item_2, academic_year:,
school:, grade: 2)
end
it 'reports a response rate that averages fifty and 100' do
expect(StudentResponseRateCalculator.new(subcategory:, school:, academic_year:).rate).to eq 75
end end
it 'returns 100 percent' do
expect(StudentResponseRateCalculator.new(subcategory:, school:,
academic_year:).rate).to eq 100
end end
end end
context 'when no survey information exists for that school or year' do context 'when one grade gets surveyed but another does not, the grade that does not get surveyed is not counted' do
it 'returns 100 percent' do before do
expect(StudentResponseRateCalculator.new(subcategory:, school:, academic_year:).rate).to eq 100 create(:respondent, school:, academic_year:, total_students: 20, one: 20, two: 20)
create_list(:survey_item_response, 10, survey_item: sufficient_student_survey_item_1, academic_year:,
school:, grade: 1)
end
it 'reports a response rate that averages fifty and 100' do
expect(StudentResponseRateCalculator.new(subcategory:, school:, academic_year:).rate).to eq 50
end
end end
end end
context 'when there is an imbalance in the response rate of the student items' do context 'when the average number of student responses is greater than the total possible responses' do
context 'and one of the student items has no associated survey item responses' do
before do before do
create_list(:survey_item_response, SurveyItemResponse::TEACHER_RESPONSE_THRESHOLD, survey_item: sufficient_teacher_survey_item, create(:respondent, school:, academic_year:, total_students: 20, one: 20, two: 20)
academic_year:, school:, likert_score: 1) create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD * 11, survey_item: sufficient_student_survey_item_2,
create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD, survey_item: sufficient_student_survey_item_1, academic_year:, school:, likert_score: 1, grade: 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)
create(:respondent, school:, academic_year:)
create(:survey, school:, academic_year:)
insufficient_student_survey_item_1
end end
it 'ignores the empty survey item and returns only the average response rate of student survey items with responses' do it 'returns 100 percent' do
expect(StudentResponseRateCalculator.new(subcategory:, school:, expect(StudentResponseRateCalculator.new(subcategory:, school:,
academic_year:).rate).to eq 25 academic_year:).rate).to eq 100
end end
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 end
end end
@ -143,6 +201,7 @@ describe ResponseRateCalculator, type: :model do
let(:sufficient_teacher_survey_item_3) { 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(: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) } let(:sufficient_student_survey_item_1) { create(:student_survey_item, scale: sufficient_scale_1) }
let(:respondent) { create(:respondent, school:, academic_year:, total_teachers: 8) }
before :each do before :each do
create_list(:survey_item_response, SurveyItemResponse::TEACHER_RESPONSE_THRESHOLD, survey_item: sufficient_teacher_survey_item_1, create_list(:survey_item_response, SurveyItemResponse::TEACHER_RESPONSE_THRESHOLD, survey_item: sufficient_teacher_survey_item_1,

@ -110,12 +110,12 @@ describe GroupedBarColumnPresenter do
context 'for a grouped column presenter with both student and teacher responses' do context 'for a grouped column presenter with both student and teacher responses' do
context 'with a single year' context 'with a single year'
before do before do
create(:survey_item_response, create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD,
survey_item: student_survey_item_for_composite_measure, survey_item: student_survey_item_for_composite_measure,
school:, school:,
academic_year:, academic_year:,
likert_score: 4) likert_score: 4)
create(:survey_item_response, create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD,
survey_item: student_survey_item_for_composite_measure, school:, survey_item: student_survey_item_for_composite_measure, school:,
academic_year:, academic_year:,
likert_score: 5) likert_score: 5)
@ -165,9 +165,9 @@ describe GroupedBarColumnPresenter do
context 'when the score is in the Ideal zone' do context 'when the score is in the Ideal zone' do
before do before do
create(:survey_item_response, survey_item: student_survey_item, school:, create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD, survey_item: student_survey_item, school:,
academic_year:, likert_score: 5) academic_year:, likert_score: 5)
create(:survey_item_response, survey_item: student_survey_item, school:, create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD, survey_item: student_survey_item, school:,
academic_year:, likert_score: 4) academic_year:, likert_score: 4)
end end
@ -219,7 +219,7 @@ describe GroupedBarColumnPresenter do
context 'when the score is in the Approval zone' do context 'when the score is in the Approval zone' do
before do before do
create(:survey_item_response, survey_item: student_survey_item, school:, create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD, survey_item: student_survey_item, school:,
academic_year:, likert_score: 4) academic_year:, likert_score: 4)
end end
@ -240,7 +240,7 @@ describe GroupedBarColumnPresenter do
end end
context 'when the score is in the Growth zone' do context 'when the score is in the Growth zone' do
before do before do
create(:survey_item_response, survey_item: student_survey_item, school:, create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD, survey_item: student_survey_item, school:,
academic_year:, likert_score: 3) academic_year:, likert_score: 3)
end end
@ -255,7 +255,7 @@ describe GroupedBarColumnPresenter do
context 'when the score is less than 5 percent away from the approval low benchmark line' do context 'when the score is less than 5 percent away from the approval low benchmark line' do
before do before do
create_list(:survey_item_response, 40, survey_item: student_survey_item, school:, create_list(:survey_item_response, 80, survey_item: student_survey_item, school:,
academic_year:, likert_score: 4) academic_year:, likert_score: 4)
end end
@ -267,7 +267,7 @@ describe GroupedBarColumnPresenter do
context 'when the score is in the Watch zone' do context 'when the score is in the Watch zone' do
before do before do
create(:survey_item_response, survey_item: student_survey_item, school:, create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD, survey_item: student_survey_item, school:,
academic_year:, likert_score: 2) academic_year:, likert_score: 2)
end end
@ -282,7 +282,7 @@ describe GroupedBarColumnPresenter do
end end
context 'when the score is in the Warning zone' do context 'when the score is in the Warning zone' do
before do before do
create(:survey_item_response, survey_item: student_survey_item, school:, create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD, survey_item: student_survey_item, school:,
academic_year:, likert_score: 1) academic_year:, likert_score: 1)
end end

@ -39,10 +39,8 @@ describe SubcategoryPresenter do
academic_year:, school:, likert_score: 1) academic_year:, school:, likert_score: 1)
create_list(:survey_item_response, SurveyItemResponse::TEACHER_RESPONSE_THRESHOLD, survey_item: survey_item3, create_list(:survey_item_response, SurveyItemResponse::TEACHER_RESPONSE_THRESHOLD, survey_item: survey_item3,
academic_year:, school:, likert_score: 5) academic_year:, school:, likert_score: 5)
create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD / 2, survey_item: survey_item4, create_list(:survey_item_response, 10, survey_item: survey_item4,
academic_year:, school:, likert_score: 3) academic_year:, school:, likert_score: 3, grade: 1)
create_list(:survey_item_response, SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD / 2, survey_item: survey_item4,
academic_year:, school:, likert_score: 3)
# Adding responses corresponding to different years and schools should not pollute the score calculations # Adding responses corresponding to different years and schools should not pollute the score calculations
create_survey_item_responses_for_different_years_and_schools(survey_item1) create_survey_item_responses_for_different_years_and_schools(survey_item1)
@ -51,7 +49,7 @@ describe SubcategoryPresenter do
end end
before do before do
create(:respondent, school:, academic_year:) create(:respondent, school:, academic_year:, one: 40)
create(:survey, school:, academic_year:) create(:survey, school:, academic_year:)
end end

@ -43,15 +43,17 @@ describe 'District Admin', js: true do
# let(:username) { 'winchester' } # let(:username) { 'winchester' }
# let(:password) { 'winchester!' } # let(:password) { 'winchester!' }
let(:respondents) do let(:respondents) do
respondents = Respondent.where(school:, academic_year: ay_2021_22).first respondent = Respondent.find_or_initialize_by(school:, academic_year: ay_2021_22)
respondents.total_students = 8 respondent.total_students = 8
respondents.total_teachers = 8 respondent.total_teachers = 8
respondents.save respondent.one = 20
respondent.save
respondents = Respondent.where(school:, academic_year: ay_2019_20).first respondent = Respondent.find_or_initialize_by(school:, academic_year: ay_2019_20)
respondents.total_students = 8 respondent.total_students = 8
respondents.total_teachers = 8 respondent.total_teachers = 8
respondents.save respondent.one = 20
respondent.save
end end
before :each do before :each do
@ -71,28 +73,28 @@ describe 'District Admin', js: true do
survey_items_for_measure_2A_i.each do |survey_item| survey_items_for_measure_2A_i.each do |survey_item|
SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD.times do SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD.times do
survey_item_responses << SurveyItemResponse.new(response_id: rand.to_s, academic_year: ay_2021_22, survey_item_responses << SurveyItemResponse.new(response_id: rand.to_s, academic_year: ay_2021_22,
school:, survey_item:, likert_score: 5) school:, survey_item:, likert_score: 5, grade: 1)
end end
end end
survey_items_for_measure_2A_ii.each do |survey_item| survey_items_for_measure_2A_ii.each do |survey_item|
SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD.times do SurveyItemResponse::STUDENT_RESPONSE_THRESHOLD.times do
survey_item_responses << SurveyItemResponse.new(response_id: rand.to_s, academic_year: ay_2021_22, survey_item_responses << SurveyItemResponse.new(response_id: rand.to_s, academic_year: ay_2021_22,
school:, survey_item:, likert_score: 5) school:, survey_item:, likert_score: 5, grade: 1)
end end
end end
survey_items_for_measure_4C_i.each do |survey_item| survey_items_for_measure_4C_i.each do |survey_item|
SurveyItemResponse::TEACHER_RESPONSE_THRESHOLD.times do SurveyItemResponse::TEACHER_RESPONSE_THRESHOLD.times do
survey_item_responses << SurveyItemResponse.new(response_id: rand.to_s, academic_year: ay_2021_22, survey_item_responses << SurveyItemResponse.new(response_id: rand.to_s, academic_year: ay_2021_22,
school:, survey_item:, likert_score: 1) school:, survey_item:, likert_score: 1, grade: 1)
end end
end end
survey_items_for_subcategory.each do |survey_item| survey_items_for_subcategory.each do |survey_item|
2.times do 2.times do
survey_item_responses << SurveyItemResponse.new(response_id: rand.to_s, academic_year: ay_2021_22, survey_item_responses << SurveyItemResponse.new(response_id: rand.to_s, academic_year: ay_2021_22,
school:, survey_item:, likert_score: 4) school:, survey_item:, likert_score: 4, grade: 1)
end end
end end

@ -171,7 +171,6 @@ describe 'analyze/index' do
end end
it 'displays disabled checkboxes for years that dont have data' do it 'displays disabled checkboxes for years that dont have data' do
ResponseRateLoader.reset
year_checkbox = subject.css("##{academic_year.range}").first year_checkbox = subject.css("##{academic_year.range}").first
expect(year_checkbox.name).to eq 'input' expect(year_checkbox.name).to eq 'input'
expect(academic_year.range).to eq '2050-51' expect(academic_year.range).to eq '2050-51'

Loading…
Cancel
Save