mirror of
https://github.com/edcommonwealth/sqm-dashboards.git
synced 2026-03-07 21:48:16 -08:00
initialized with a season, i.e. "2024-25 Fall". Update scapers for admin data, enrollment and staffing to use the new range standard correctly. Update the loaders for admin data, enrollment and staffing so that it populates all seasons in a given year. So admin data for 2024-25 gets loaded into "2024-25 Fall" and "2024-25 Spring". Add tests for the new range format. Set the default cutoff for the start of Spring season will be the last Sunday in February
313 lines
9.4 KiB
Ruby
313 lines
9.4 KiB
Ruby
class SurveyItemValues
|
|
attr_reader :row, :headers, :survey_items, :schools, :academic_years
|
|
|
|
def initialize(row:, headers:, survey_items:, schools:, academic_years: AcademicYear.all)
|
|
@row = row
|
|
# Remove any newlines in headers
|
|
headers = headers.map { |item| item.delete("\n") if item.present? }
|
|
@headers = include_all_headers(headers:)
|
|
@survey_items = survey_items
|
|
@schools = schools
|
|
@academic_years = academic_years
|
|
|
|
copy_likert_scores_from_variant_survey_items
|
|
row["Income"] = income
|
|
row["Raw Income"] = raw_income
|
|
row["Raw ELL"] = raw_ell
|
|
row["ELL"] = ell
|
|
row["Raw SpEd"] = raw_sped
|
|
row["SpEd"] = sped
|
|
row["Progress Count"] = progress
|
|
row["Race"] ||= races.join(",")
|
|
row["Gender"] ||= gender
|
|
|
|
copy_data_to_main_column(main: /Race/i, secondary: /Race Secondary|Race-1/i)
|
|
copy_data_to_main_column(main: /Gender/i, secondary: /Gender Secondary|Gender-1/i)
|
|
end
|
|
|
|
def copy_data_to_main_column(main:, secondary:)
|
|
main_column = headers.find { |header| main.match(header) }
|
|
row[main_column] = value_from(pattern: secondary) if row[main_column].nil?
|
|
end
|
|
|
|
# Some survey items have variants, i.e. a survey item with an id of s-tint-q1 might have a variant that looks like s-tint-q1-1. We must ensure that all variants in the form of s-tint-q1-1 have a matching pair.
|
|
# We don't ensure that ids in the form of s-tint-q1 have a matching pair because not all questions have variants
|
|
def include_all_headers(headers:)
|
|
alternates = headers.filter(&:present?)
|
|
.filter { |header| header.match?(/^[st]-\w*-\w*-1$/i) }
|
|
alternates.each do |header|
|
|
main = header.sub(/-1\z/, "")
|
|
headers.push(main) unless headers.include?(main)
|
|
end
|
|
headers
|
|
end
|
|
|
|
def dese_id?
|
|
dese_id.present?
|
|
end
|
|
|
|
def recorded_date
|
|
@recorded_date ||= begin
|
|
recorded_date = value_from(pattern: /Recorded\s*Date/i)
|
|
date = if recorded_date.match(%r{\d+/\d+/\d+})
|
|
Date.strptime(recorded_date, "%m/%d/%Y")
|
|
elsif recorded_date.match(/\d+-\d+-\d+(T|\s)\d+:\d+:\d+/)
|
|
Date.parse(recorded_date)
|
|
else
|
|
puts "Recorded date in unknown format"
|
|
end
|
|
end
|
|
end
|
|
|
|
def academic_year
|
|
@academic_year ||= begin
|
|
range = AcademicYear.range_from_date(recorded_date, academic_years.map(&:range))
|
|
academic_years.find { |item| item.range == range }
|
|
end
|
|
end
|
|
|
|
def survey_item_response(survey_item:)
|
|
@survey_item_response ||= Hash.new do |memo, survey_item|
|
|
memo[survey_item] = survey_item_responses[[response_id, survey_item.id]]
|
|
end
|
|
|
|
@survey_item_response[survey_item]
|
|
end
|
|
|
|
def survey_item_responses
|
|
@survey_item_responses ||= Hash.new do |memo|
|
|
responses_hash = {}
|
|
SurveyItemResponse.where(school:, academic_year:, response_id:).each do |response|
|
|
responses_hash[[response.response_id, response.survey_item.id]] = response
|
|
end
|
|
memo[[school, academic_year]] = responses_hash
|
|
end
|
|
|
|
@survey_item_responses[[school, academic_year]]
|
|
end
|
|
|
|
def response_id
|
|
@response_id ||= value_from(pattern: /Response\s*ID/i)
|
|
end
|
|
|
|
def dese_id
|
|
@dese_id ||= begin
|
|
dese_id = value_from(pattern: /Dese\s*ID/i)
|
|
dese_id ||= value_from(pattern: /^School$/i)
|
|
dese_id ||= value_from(pattern: /School-\s*\w/i)
|
|
|
|
dese_id.to_i
|
|
end
|
|
end
|
|
|
|
def likert_score(survey_item_id:)
|
|
row[survey_item_id] || row["#{survey_item_id}-1"]
|
|
end
|
|
|
|
def school
|
|
@school ||= schools[dese_id]
|
|
end
|
|
|
|
def district
|
|
@district ||= school&.district
|
|
end
|
|
|
|
def grade
|
|
@grade ||= begin
|
|
raw_grade = value_from(pattern: /Grade|What grade are you in?/i)
|
|
|
|
return nil if raw_grade.blank?
|
|
|
|
raw_grade.to_i
|
|
end
|
|
end
|
|
|
|
def gender
|
|
@gender ||= begin
|
|
gender_code ||= value_from(pattern: /Gender self report/i)
|
|
gender_code ||= value_from(pattern: /^Gender$/i)
|
|
gender_code ||= value_from(pattern: /What is your gender?|What is your gender? - Selected Choice/i)
|
|
gender_code ||= value_from(pattern: /Gender-\s*SIS/i)
|
|
gender_code ||= value_from(pattern: /Gender-\s*Qcode/i)
|
|
gender_code ||= value_from(pattern: /Gender - do not use/i)
|
|
gender_code ||= value_from(pattern: /Gender/i)
|
|
Gender.qualtrics_code_from(gender_code)
|
|
end
|
|
end
|
|
|
|
def races
|
|
@races ||= begin
|
|
race_codes ||= self_report = value_from(pattern: /Race\s*self\s*report/i)
|
|
race_codes ||= value_from(pattern: /^RACE$/i)
|
|
race_codes ||= value_from(pattern: %r{What is your race/ethnicity?(Please select all that apply) - Selected Choice}i)
|
|
race_codes ||= value_from(pattern: /Race Secondary/i)
|
|
race_codes ||= sis ||= value_from(pattern: /Race-\s*SIS/i)
|
|
race_codes ||= sis ||= value_from(pattern: /Race\s*-\s*Qcodes/i)
|
|
race_codes ||= value_from(pattern: /RACE/i) || ""
|
|
race_codes ||= []
|
|
|
|
race_codes = race_codes.split(",")
|
|
.map do |word|
|
|
word.split(/\s+and\s+/i)
|
|
end.flatten
|
|
.reject(&:blank?)
|
|
.map { |race| Race.qualtrics_code_from(race) }.map(&:to_i)
|
|
|
|
# Only check the secondary hispanic column if we don't have self reported data and are relying on SIS data
|
|
if self_report.nil? && sis.present?
|
|
hispanic = value_from(pattern: /Hispanic\s*Latino/i)&.downcase
|
|
race_codes = race_codes.reject { |code| code == 5 } if hispanic == "true" && race_codes.count == 1
|
|
race_codes = race_codes.push(4) if hispanic == "true"
|
|
end
|
|
|
|
Race.normalize_race_list(race_codes)
|
|
end
|
|
end
|
|
|
|
def lasid
|
|
@lasid ||= value_from(pattern: /LASID/i)
|
|
end
|
|
|
|
def raw_income
|
|
@raw_income ||= value_from(pattern: /Low\s*Income|Raw\s*Income|SES-\s*SIS/i)
|
|
end
|
|
|
|
def income
|
|
@income ||= Income.to_designation(raw_income)
|
|
end
|
|
|
|
def raw_ell
|
|
@raw_ell ||= value_from(pattern: /EL Student First Year|Raw\s*ELL|ELL-\s*SIS/i)
|
|
end
|
|
|
|
def ell
|
|
@ell ||= Ell.to_designation(raw_ell)
|
|
end
|
|
|
|
def raw_sped
|
|
@raw_sped ||= value_from(pattern: /Special\s*Ed\s*Status|Raw\s*SpEd|SpEd-\s*SIS/i)
|
|
end
|
|
|
|
def sped
|
|
@sped ||= Sped.to_designation(raw_sped)
|
|
end
|
|
|
|
def value_from(pattern:)
|
|
output = nil
|
|
matches = headers.select do |header|
|
|
pattern.match(header)
|
|
end.map { |item| item.delete("\n") }
|
|
|
|
matches.each do |match|
|
|
output ||= row[match]&.strip
|
|
end
|
|
|
|
return nil if output&.match?(%r{^#*N/*A$}i) || output.blank?
|
|
|
|
output
|
|
end
|
|
|
|
def sanitized_headers
|
|
@sanitized_headers ||= headers.select(&:present?)
|
|
.reject { |key, _value| key.start_with? "Q" }
|
|
.reject { |key, _value| key.match?(/^[st]-\w*-\w*-1$/i) }
|
|
end
|
|
|
|
def to_a
|
|
sanitized_headers.map { |header| row[header] }
|
|
end
|
|
|
|
def duration
|
|
@duration ||= value_from(pattern: /Duration|Duration \(in seconds\)|Duration\.\.\(in\.seconds\)/i)
|
|
end
|
|
|
|
def valid?
|
|
valid_duration? && valid_progress? && valid_grade? && valid_sd?
|
|
end
|
|
|
|
def respondent_type
|
|
return :teacher if headers
|
|
.filter(&:present?)
|
|
.filter { |header| header.start_with? "t-" }.count > 0
|
|
|
|
:student
|
|
end
|
|
|
|
def survey_type
|
|
@survey_type ||= SurveyItem.survey_type(survey_item_ids:)
|
|
end
|
|
|
|
def survey_item_ids
|
|
@survey_item_ids ||= sanitized_headers.filter { |header| header.start_with?("t-", "s-") }
|
|
end
|
|
|
|
def valid_duration?
|
|
return true if duration.nil? || duration == "" || duration.downcase == "n/a" || duration.downcase == "na"
|
|
|
|
span_in_seconds = duration.to_i
|
|
return span_in_seconds >= 300 if survey_type == :teacher
|
|
return span_in_seconds >= 240 if survey_type == :standard
|
|
return span_in_seconds >= 100 if survey_type == :short_form
|
|
|
|
true
|
|
end
|
|
|
|
def progress
|
|
survey_item_ids.reject { |header| row[header].nil? }.count
|
|
end
|
|
|
|
def valid_progress?
|
|
return false if progress.nil?
|
|
|
|
return progress >= 12 if survey_type == :teacher
|
|
return progress >= 11 if survey_type == :standard
|
|
return progress >= 5 if survey_type == :short_form
|
|
return progress >= 5 if survey_type == :early_education
|
|
|
|
false
|
|
end
|
|
|
|
def valid_grade?
|
|
return true if grade.nil?
|
|
|
|
return true if respondent_type == :teacher
|
|
|
|
respondents = Respondent.where(school:, academic_year:).first
|
|
if respondents.present? && respondents.enrollment_by_grade[grade].present?
|
|
enrollment = respondents.enrollment_by_grade[grade]
|
|
end
|
|
return false if enrollment.nil?
|
|
|
|
valid = enrollment > 0
|
|
puts "Invalid grade #{grade} for #{school.name} #{academic_year.formatted_range}" unless valid
|
|
valid
|
|
end
|
|
|
|
def valid_sd?
|
|
return true if survey_type == :early_education
|
|
|
|
survey_item_headers = headers.filter(&:present?).filter { |header| header.start_with?("s-", "t-") }
|
|
likert_scores = []
|
|
survey_item_headers.each do |header|
|
|
likert_scores << likert_score(survey_item_id: header).to_i
|
|
end
|
|
likert_scores = likert_scores.compact.reject(&:zero?)
|
|
return false if likert_scores.count < 2
|
|
|
|
!likert_scores.stdev.zero?
|
|
end
|
|
|
|
def valid_school?
|
|
school.present?
|
|
end
|
|
|
|
private
|
|
|
|
def copy_likert_scores_from_variant_survey_items
|
|
headers.filter(&:present?).filter { |header| header.end_with? "-1" }.each do |header|
|
|
likert_score = row[header]
|
|
main_item = header.gsub("-1", "")
|
|
row[main_item] = likert_score if likert_score.present? && row[main_item].blank?
|
|
end
|
|
end
|
|
end
|