dirty commit: can't get references to work correctly between any tables

This commit is contained in:
Nelson Jovel 2024-01-04 19:36:10 -08:00
parent e1f0b78236
commit a4fddbeced
183 changed files with 5461 additions and 5 deletions

View file

@ -0,0 +1,40 @@
module Dashboard
class AcademicYear < ApplicationRecord
def self.find_by_date(date)
year = parse_year_range(date:)
range = "#{year.start}-#{year.end.to_s[2, 3]}"
academic_years[range]
end
def formatted_range
years = range.split("-")
"#{years.first} 20#{years.second}"
end
private
def self.parse_year_range(date:)
year = date.year
if date.month > 6
AcademicYearRange.new(year, year + 1)
else
AcademicYearRange.new(year - 1, year)
end
end
# This may cause problems if academic years get loaded from csv instead of the current method that requires a code change to the seeder script. This is because a change in code will trigger a complete reload of the application whereas loading from csv does not. This means if we change academic year to load from csv, the set of academic years will be stale when new years are added.
def self.academic_years
@@academic_years ||= AcademicYear.all.map { |academic_year| [academic_year.range, academic_year] }.to_h
end
# Needed to reset the academic years when testing with specs that create the same academic year in a before :each block
def self.reset_academic_years
@@academic_years = nil
end
private_class_method :academic_years
private_class_method :parse_year_range
end
end
AcademicYearRange = Struct.new(:start, :end)

View file

@ -0,0 +1,14 @@
module Dashboard
class AdminDataItem < ApplicationRecord
belongs_to :dashboard_scale
has_many :dashboard_admin_data_values
scope :for_measures, lambda { |measures|
joins(:scale).where('scale.measure': measures)
}
scope :non_hs_items_for_measures, lambda { |measure|
for_measures(measure).where(hs_only_item: false)
}
end
end

View file

@ -0,0 +1,9 @@
module Dashboard
class AdminDataValue < ApplicationRecord
belongs_to :dashboard_school
belongs_to :dashboard_admin_data_item
belongs_to :dashboard_academic_year
validates :likert_score, numericality: { greater_than: 0, less_than_or_equal_to: 5 }
end
end

View file

@ -0,0 +1,13 @@
module Dashboard
class Category < ApplicationRecord
include FriendlyId
friendly_id :name, use: [:slugged]
scope :sorted, -> { order(:category_id) }
has_many :subcategories
has_many :measures, through: :subcategories
has_many :admin_data_items, through: :measures
has_many :scales, through: :subcategories
end
end

View file

@ -0,0 +1,18 @@
module Dashboard
class District < ApplicationRecord
has_many :schools, class_name: "Dashboard::School"
has_many :dashboard_schools, class_name: "Dashboard::School"
validates :name, presence: true
scope :alphabetic, -> { order(name: :asc) }
include FriendlyId
friendly_id :name, use: [:slugged]
def short_name
name.split(" ").first.downcase
end
end
end

View file

@ -0,0 +1,20 @@
module Dashboard
class Ell < ApplicationRecord
scope :by_designation, -> { all.map { |ell| [ell.designation, ell] }.to_h }
include FriendlyId
friendly_id :designation, use: [:slugged]
def self.to_designation(ell)
case ell
in /lep student 1st year|LEP student not 1st year|EL Student First Year|LEP\s*student/i
"ELL"
in /Does not apply/i
"Not ELL"
else
"Unknown"
end
end
end
end

View file

@ -0,0 +1,26 @@
module Dashboard
class Gender < ApplicationRecord
scope :by_qualtrics_code, lambda {
all.map { |gender| [gender.qualtrics_code, gender] }.to_h
}
def self.qualtrics_code_from(word)
case word
when /Female|^F|1/i
1
when /Male|^M|2/i
2
when /Another\s*Gender|Gender Identity not listed above|3|7/i
4 # We categorize any self reported gender as non-binary
when /Non-Binary|^N|4/i
4
when /Prefer not to disclose|6/i
99
when %r{^#*N/*A$}i
nil
else
99
end
end
end
end

View file

@ -0,0 +1,31 @@
module Dashboard
class Income < ApplicationRecord
scope :by_designation, -> { all.map { |income| [income.designation, income] }.to_h }
scope :by_slug, -> { all.map { |income| [income.slug, income] }.to_h }
include FriendlyId
friendly_id :designation, use: [:slugged]
def self.to_designation(income)
case income
in /Free\s*Lunch|Reduced\s*Lunch|Low\s*Income|Reduced\s*price\s*lunch/i
"Economically Disadvantaged - Y"
in /Not\s*Eligible/i
"Economically Disadvantaged - N"
else
"Unknown"
end
end
LABELS = {
"Economically Disadvantaged - Y" => "Economically Disadvantaged",
"Economically Disadvantaged - N" => "Not Economically Disadvantaged",
"Unknown" => "Unknown"
}
def label
LABELS[designation]
end
end
end

View file

@ -0,0 +1,227 @@
module Dashboard
class Measure < ApplicationRecord
belongs_to :dashboard_subcategory
has_one :dashboard_category, through: :dashboard_subcategory
has_many :dashboard_scales
has_many :dashboard_admin_data_items, through: :scales
has_many :dashboard_survey_items, through: :scales
has_many :dashboard_survey_item_responses, through: :survey_items
def none_meet_threshold?(school:, academic_year:)
@none_meet_threshold ||= Hash.new do |memo, (school, academic_year)|
memo[[school, academic_year]] = !sufficient_survey_responses?(school:, academic_year:)
end
@none_meet_threshold[[school, academic_year]]
end
def teacher_survey_items
@teacher_survey_items ||= survey_items.teacher_survey_items
end
def student_survey_items
@student_survey_items ||= survey_items.student_survey_items
end
def student_survey_items_with_sufficient_responses(school:, academic_year:)
@student_survey_items_with_sufficient_responses ||= SurveyItem.where(id: 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.survey_item_id": survey_items.student_survey_items,
"survey_item_responses.grade": school.grades(academic_year:))
.group("survey_items.id")
.having("count(*) >= 10")
.count.keys)
end
def teacher_scales
@teacher_scales ||= scales.teacher_scales
end
def student_scales
@student_scales ||= scales.student_scales
end
def includes_teacher_survey_items?
@includes_teacher_survey_items ||= teacher_survey_items.any?
end
def includes_student_survey_items?
@includes_student_survey_items ||= student_survey_items.any?
end
def includes_admin_data_items?
@includes_admin_data_items ||= admin_data_items.any?
end
def score(school:, academic_year:)
@score ||= Hash.new do |memo, (school, academic_year)|
next Score::NIL_SCORE if incalculable_score(school:, academic_year:)
scores = collect_averages_for_teacher_student_and_admin_data(school:, academic_year:)
average = scores.flatten.compact.remove_blanks.average.round(2)
next Score::NIL_SCORE if average.nan?
memo[[school, academic_year]] = scorify(average:, school:, academic_year:)
end
@score[[school, academic_year]]
end
def student_score(school:, academic_year:)
@student_score ||= Hash.new do |memo, (school, academic_year)|
meets_student_threshold = sufficient_student_data?(school:, academic_year:)
average = student_average(school:, academic_year:).round(2) if meets_student_threshold
memo[[school, academic_year]] = scorify(average:, school:, academic_year:)
end
@student_score[[school, academic_year]]
end
def teacher_score(school:, academic_year:)
@teacher_score ||= Hash.new do |memo, (school, academic_year)|
meets_teacher_threshold = sufficient_teacher_data?(school:, academic_year:)
average = teacher_average(school:, academic_year:).round(2) if meets_teacher_threshold
memo[[school, academic_year]] = scorify(average:, school:, academic_year:)
end
@teacher_score[[school, academic_year]]
end
def admin_score(school:, academic_year:)
@admin_score ||= Hash.new do |memo, (school, academic_year)|
meets_admin_threshold = sufficient_admin_data?(school:, academic_year:)
average = admin_data_averages(school:, academic_year:).average.round(2) if meets_admin_threshold
memo[[school, academic_year]] = scorify(average:, school:, academic_year:)
end
@admin_score[[school, academic_year]]
end
def warning_low_benchmark
1
end
def watch_low_benchmark
@watch_low_benchmark ||= benchmark(:watch_low_benchmark)
end
def growth_low_benchmark
@growth_low_benchmark ||= benchmark(:growth_low_benchmark)
end
def approval_low_benchmark
@approval_low_benchmark ||= benchmark(:approval_low_benchmark)
end
def ideal_low_benchmark
@ideal_low_benchmark ||= benchmark(:ideal_low_benchmark)
end
def sufficient_admin_data?(school:, academic_year:)
any_admin_data_collected?(school:, academic_year:)
end
def benchmark(name)
averages = []
averages << student_survey_items.first.send(name) if includes_student_survey_items?
averages << teacher_survey_items.first.send(name) if includes_teacher_survey_items?
(averages << admin_data_items.map(&name)).flatten! if includes_admin_data_items?
averages.average
end
private
def any_admin_data_collected?(school:, academic_year:)
@any_admin_data_collected ||= Hash.new do |memo, (school, academic_year)|
total_collected_admin_data_items =
admin_data_items.map do |admin_data_item|
admin_data_item.admin_data_values.where(school:, academic_year:).count
end.flatten.sum
memo[[school, academic_year]] = total_collected_admin_data_items.positive?
end
@any_admin_data_collected[[school, academic_year]]
end
def sufficient_survey_responses?(school:, academic_year:)
@sufficient_survey_responses ||= Hash.new do |memo, (school, academic_year)|
memo[[school, academic_year]] =
sufficient_student_data?(school:, academic_year:) || sufficient_teacher_data?(school:, academic_year:)
end
@sufficient_survey_responses[[school, academic_year]]
end
def scorify(average:, school:, academic_year:)
meets_student_threshold = sufficient_student_data?(school:, academic_year:)
meets_teacher_threshold = sufficient_teacher_data?(school:, academic_year:)
meets_admin_data_threshold = any_admin_data_collected?(school:, academic_year:)
Score.new(average:, meets_teacher_threshold:, meets_student_threshold:, meets_admin_data_threshold:)
end
def collect_survey_item_average(survey_items:, school:, academic_year:)
@collect_survey_item_average ||= Hash.new do |memo, (survey_items, school, academic_year)|
averages = survey_items.map do |survey_item|
SurveyItemResponse.grouped_responses(school:, academic_year:)[survey_item.id]
end.remove_blanks
memo[[survey_items, school, academic_year]] = averages.average || 0
end
@collect_survey_item_average[[survey_items, school, academic_year]]
end
def sufficient_student_data?(school:, academic_year:)
return false unless includes_student_survey_items?
subcategory.response_rate(school:, academic_year:).meets_student_threshold?
end
def sufficient_teacher_data?(school:, academic_year:)
return false unless includes_teacher_survey_items?
subcategory.response_rate(school:, academic_year:).meets_teacher_threshold?
end
def incalculable_score(school:, academic_year:)
@incalculable_score ||= Hash.new do |memo, (school, academic_year)|
lacks_sufficient_survey_data = !sufficient_student_data?(school:, academic_year:) &&
!sufficient_teacher_data?(school:, academic_year:)
memo[[school, academic_year]] = lacks_sufficient_survey_data && !includes_admin_data_items?
end
@incalculable_score[[school, academic_year]]
end
def collect_averages_for_teacher_student_and_admin_data(school:, academic_year:)
scores = []
scores << teacher_average(school:, academic_year:) if sufficient_teacher_data?(school:, academic_year:)
scores << student_average(school:, academic_year:) if sufficient_student_data?(school:, academic_year:)
scores << admin_data_averages(school:, academic_year:) if includes_admin_data_items?
scores
end
def teacher_average(school:, academic_year:)
@teacher_average ||= Hash.new do |memo, (school, academic_year)|
memo[[school, academic_year]] =
collect_survey_item_average(survey_items: teacher_survey_items, school:, academic_year:)
end
@teacher_average[[school, academic_year]]
end
def student_average(school:, academic_year:)
@student_average ||= Hash.new do |memo, (school, academic_year)|
survey_items = student_survey_items_with_sufficient_responses(school:, academic_year:)
memo[[school, academic_year]] = collect_survey_item_average(survey_items:, school:, academic_year:)
end
@student_average[[school, academic_year]]
end
def admin_data_averages(school:, academic_year:)
@admin_data_averages ||= Hash.new do |memo, (school, academic_year)|
memo[[school, academic_year]] =
AdminDataValue.where(school:, academic_year:, admin_data_item: admin_data_items).pluck(:likert_score)
end
@admin_data_averages[[school, academic_year]]
end
end
end

View file

@ -0,0 +1,51 @@
module Dashboard
class Race < ApplicationRecord
include FriendlyId
has_many :dashboard_student_races
has_many :dashboard_students, through: :student_races
friendly_id :designation, use: [:slugged]
scope :by_qualtrics_code, lambda {
all.map { |race| [race.qualtrics_code, race] }.to_h
}
def self.qualtrics_code_from(word)
case word
when /Native\s*American|American\s*Indian|Alaskan\s*Native|1/i
1
when /\bAsian|Pacific\s*Island|Hawaiian|2/i
2
when /Black|African\s*American|3/i
3
when /Hispanic|Latinx|4/i
4
when /White|Caucasian|5/i
5
when /Prefer not to disclose|6/i
6
when /Prefer to self-describe|7/i
7
when /Middle\s*Eastern|North\s*African|8/i
8
when %r{^#*N/*A$}i
nil
else
99
end
end
def self.normalize_race_list(codes)
# if anyone selects not to disclose their race or prefers to self-describe, categorize that as unknown race
races = codes.map do |code|
code = 99 if [6, 7].include?(code) || code.nil? || code.zero?
code
end.uniq
races.delete(99) if races.length > 1 # remove unkown race if other races present
races << 100 if races.length > 1 # add multiracial designation if multiple races present
races << 99 if races.length == 0 # add unknown race if other races missing
races
end
end
end

View file

@ -0,0 +1,27 @@
module Dashboard
class Respondent < ApplicationRecord
belongs_to :dashboard_school
belongs_to :dashboard_academic_year
validates :school, uniqueness: { scope: :academic_year }
def enrollment_by_grade
@enrollment_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
def self.by_school_and_year(school:, academic_year:)
@by_school_and_year ||= Hash.new do |memo, (school, academic_year)|
memo[[school, academic_year]] = Respondent.find_by(school:, academic_year:)
end
@by_school_and_year[[school, academic_year]]
end
end
end

View file

@ -0,0 +1,7 @@
module Dashboard
class ResponseRate < ApplicationRecord
belongs_to :dashboard_subcategory
belongs_to :dashboard_school
belongs_to :dashboard_academic_year
end
end

View file

@ -0,0 +1,51 @@
# frozen_string_literal: true
module Dashboard
class ResponseRateCalculator
TEACHER_RATE_THRESHOLD = 25
STUDENT_RATE_THRESHOLD = 25
attr_reader :subcategory, :school, :academic_year
def initialize(subcategory:, school:, academic_year:)
@subcategory = subcategory
@school = school
@academic_year = academic_year
end
def rate
return 100 if population_data_unavailable?
return 0 unless survey_items_have_sufficient_responses?
return 0 unless total_possible_responses.positive?
cap_at_one_hundred(raw_response_rate).round
end
def meets_student_threshold?
rate >= STUDENT_RATE_THRESHOLD
end
def meets_teacher_threshold?
rate >= TEACHER_RATE_THRESHOLD
end
private
def cap_at_one_hundred(response_rate)
response_rate > 100 ? 100 : response_rate
end
def average_responses_per_survey_item
response_count / survey_item_count.to_f
end
def respondents
@respondents ||= Respondent.by_school_and_year(school:, academic_year:)
end
def population_data_unavailable?
@population_data_unavailable ||= respondents.nil?
end
end
end

View file

@ -0,0 +1,44 @@
module Dashboard
class Scale < ApplicationRecord
belongs_to :dashboard_measure
has_many :dashboard_survey_items
has_many :dashboard_survey_item_responses, through: :dashboard_survey_items
has_many :dashboard_admin_data_items
def score(school:, academic_year:)
@score ||= Hash.new do |memo, (school, academic_year)|
memo[[school, academic_year]] = begin
items = []
items << collect_survey_item_average(student_survey_items, school, academic_year)
items << collect_survey_item_average(teacher_survey_items, school, academic_year)
items.remove_blanks.average
end
end
@score[[school, academic_year]]
end
scope :teacher_scales, lambda {
where("scale_id LIKE 't-%'")
}
scope :student_scales, lambda {
where("scale_id LIKE 's-%'")
}
private
def collect_survey_item_average(survey_items, school, academic_year)
survey_items.map do |survey_item|
survey_item.score(school:, academic_year:)
end.remove_blanks.average
end
def teacher_survey_items
survey_items.teacher_survey_items
end
def student_survey_items
survey_items.student_survey_items
end
end
end

View file

@ -0,0 +1,27 @@
module Dashboard
class School < ApplicationRecord
belongs_to :district, class_name: "Dashboard::District"
has_many :dashboard_survey_item_responses, dependent: :delete_all
# validates :name, presence: true
scope :alphabetic, -> { order(name: :asc) }
scope :school_hash, -> { all.map { |school| [school.dese_id, school] }.to_h }
include FriendlyId
friendly_id :name, use: [:slugged]
def self.find_by_district_code_and_school_code(district_code, school_code)
School
.joins(:district)
.where(districts: { qualtrics_code: district_code })
.find_by_qualtrics_code(school_code)
end
def grades(academic_year:)
@grades ||= Respondent.by_school_and_year(school: self,
academic_year:)&.enrollment_by_grade&.keys || (-1..12).to_a
end
end
end

View file

@ -0,0 +1,28 @@
module Dashboard
class Score < ApplicationRecord
belongs_to :dashboard_measure
belongs_to :dashboard_school
belongs_to :dashboard_academic_year
belongs_to :dashboard_race
NIL_SCORE = Score.new(average: nil, meets_teacher_threshold: false, meets_student_threshold: false,
meets_admin_data_threshold: false)
enum group: {
all_students: 0,
race: 1,
grade: 2,
gender: 3
}
def in_zone?(zone:)
return false if average.nil? || average.is_a?(Float) && average.nan?
average.between?(zone.low_benchmark, zone.high_benchmark)
end
def blank?
average.nil? || average.zero? || average.nan?
end
end
end

View file

@ -0,0 +1,20 @@
module Dashboard
class Sped < ApplicationRecord
scope :by_designation, -> { all.map { |sped| [sped.designation, sped] }.to_h }
include FriendlyId
friendly_id :designation, use: [:slugged]
def self.to_designation(sped)
case sped
in /active/i
"Special Education"
in /^NA$|^#NA$/i
"Unknown"
else
"Not Special Education"
end
end
end
end

View file

@ -0,0 +1,9 @@
module Dashboard
class Student < ApplicationRecord
# has_many :dashboard_survey_item_responses
has_many :dashboard_student_races
has_and_belongs_to_many :races, join_table: :student_races
encrypts :lasid, deterministic: true
end
end

View file

@ -0,0 +1,6 @@
module Dashboard
class StudentRace < ApplicationRecord
belongs_to :dashboard_student
belongs_to :dashboard_race
end
end

View file

@ -0,0 +1,67 @@
# frozen_string_literal: true
class StudentResponseRateCalculator < ResponseRateCalculator
def raw_response_rate
rates_by_grade.values.length.positive? ? weighted_average : 0
end
def weighted_average
num_possible_responses = 0.0
rates_by_grade.keys.map do |grade|
num_possible_responses += enrollment_by_grade[grade]
end
rates_by_grade.map do |grade, rate|
rate * (enrollment_by_grade[grade] / num_possible_responses)
end.sum
end
def rates_by_grade
@rates_by_grade ||= enrollment_by_grade.map do |grade, num_of_students_in_grade|
responses = survey_items_with_sufficient_responses(grade:)
actual_response_count_for_grade = responses.values.sum.to_f
count_of_survey_items_with_sufficient_responses = responses.count
if actual_response_count_for_grade.nil? || 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
rate = actual_response_count_for_grade / count_of_survey_items_with_sufficient_responses / num_of_students_in_grade * 100
[grade, rate]
end.compact.to_h
end
def enrollment_by_grade
@enrollment_by_grade ||= respondents.enrollment_by_grade
end
def survey_items_have_sufficient_responses?
rates_by_grade.values.length.positive?
end
def survey_items_with_sufficient_responses(grade:)
@survey_items_with_sufficient_responses ||= Hash.new do |memo, grade|
threshold = 10
quarter_of_grade = enrollment_by_grade[grade] / 4
threshold = threshold > quarter_of_grade ? quarter_of_grade : threshold
si = SurveyItemResponse.student_survey_items_with_sufficient_responses_by_grade(school:,
academic_year:)
si = si.reject do |_key, value|
value < threshold
end
ssi = @subcategory.student_survey_items.map(&:id)
grade_array = Array.new(ssi.length, grade)
memo[grade] = si.slice(*grade_array.zip(ssi))
end
@survey_items_with_sufficient_responses[grade]
end
def total_possible_responses
@total_possible_responses ||= begin
return 0 unless respondents.present?
respondents.total_students
end
end
end

View file

@ -0,0 +1,102 @@
module Dashboard
class Subcategory < ApplicationRecord
belongs_to :dashboard_categories, class_name: "Dashboard::Category"
has_many :dashboard_measures
has_many :dashboard_survey_items, through: :dashboard_measures
has_many :dashboard_admin_data_items, through: :dashboard_measures
has_many :dashboard_survey_items, through: :dashboard_measures
has_many :dashboard_scales, through: :dashboard_measures
def score(school:, academic_year:)
measures.map do |measure|
measure.score(school:, academic_year:).average
end.remove_blanks.average
end
def student_score(school:, academic_year:)
measures.map do |measure|
measure.student_score(school:, academic_year:).average
end.remove_blanks.average
end
def teacher_score(school:, academic_year:)
measures.map do |measure|
measure.teacher_score(school:, academic_year:).average
end.remove_blanks.average
end
def admin_score(school:, academic_year:)
measures.map do |measure|
measure.admin_score(school:, academic_year:).average
end.remove_blanks.average
end
def response_rate(school:, academic_year:)
@response_rate ||= Hash.new do |memo, (school, academic_year)|
student = StudentResponseRateCalculator.new(subcategory: self, school:, academic_year:)
teacher = TeacherResponseRateCalculator.new(subcategory: self, school:, academic_year:)
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?,
meets_teacher_threshold: teacher.meets_teacher_threshold?)
end
@response_rate[[school, academic_year]]
end
def warning_low_benchmark
1
end
def watch_low_benchmark
@watch_low_benchmark ||= benchmark(:watch_low_benchmark)
end
def growth_low_benchmark
@growth_low_benchmark ||= benchmark(:growth_low_benchmark)
end
def approval_low_benchmark
@approval_low_benchmark ||= benchmark(:approval_low_benchmark)
end
def ideal_low_benchmark
@ideal_low_benchmark ||= benchmark(:ideal_low_benchmark)
end
def benchmark(name)
measures.map do |measure|
measure.benchmark(name)
end.average
end
def zone(school:, academic_year:)
zone_for_score(score: score(school:, academic_year:))
end
def student_zone(school:, academic_year:)
zone_for_score(score: student_score(school:, academic_year:))
end
def teacher_zone(school:, academic_year:)
zone_for_score(score: teacher_score(school:, academic_year:))
end
def admin_zone(school:, academic_year:)
zone_for_score(score: admin_score(school:, academic_year:))
end
def zone_for_score(score:)
Zones.new(watch_low_benchmark:, growth_low_benchmark:,
approval_low_benchmark:, ideal_low_benchmark:).zone_for_score(score)
end
def student_survey_items
@student_survey_items ||= survey_items.student_survey_items
end
def teacher_survey_items
@teacher_survey_items ||= survey_items.teacher_survey_items
end
end
end

View file

@ -0,0 +1,6 @@
# frozen_string_literal: true
module Dashboard
class Summary < Struct.new(:id, :description, :available?)
end
end

View file

@ -0,0 +1,78 @@
module Dashboard
class SurveyItem < ApplicationRecord
belongs_to :dashboard_scale
has_one :dashboard_measure, through: dashboard_scale
has_one :dashboard_subcategory, through: dashboard_measure
has_many :dashboard_survey_item_responses
def score(school:, academic_year:)
@score ||= Hash.new do |memo, (school, academic_year)|
memo[[school, academic_year]] =
survey_item_responses.exclude_boston.where(school:, academic_year:).average(:likert_score).to_f
end
@score[[school, academic_year]]
end
scope :student_survey_items, lambda {
where("survey_items.survey_item_id LIKE 's-%'")
}
scope :standard_survey_items, lambda {
where("survey_items.survey_item_id LIKE 's-%-q%'")
}
scope :teacher_survey_items, lambda {
where("survey_items.survey_item_id LIKE 't-%'")
}
scope :short_form_survey_items, lambda {
where(on_short_form: true)
}
scope :early_education_survey_items, lambda {
where("survey_items.survey_item_id LIKE '%-%-es%'")
}
scope :survey_items_for_grade, lambda { |school, academic_year, grade|
includes(:survey_item_responses)
.where("survey_item_responses.grade": grade,
"survey_item_responses.school": school,
"survey_item_responses.academic_year": academic_year).distinct
}
scope :survey_item_ids_for_grade, lambda { |school, academic_year, grade|
survey_items_for_grade(school, academic_year, grade).pluck(:survey_item_id)
}
scope :survey_items_for_grade_and_subcategory, lambda { |school, academic_year, grade, subcategory|
includes(:survey_item_responses)
.where(
survey_item_id: subcategory.survey_items.pluck(:survey_item_id),
"survey_item_responses.school": school,
"survey_item_responses.academic_year": academic_year,
"survey_item_responses.grade": grade
)
}
scope :survey_type_for_grade, lambda { |school, academic_year, grade|
survey_items_set_by_grade = survey_items_for_grade(school, academic_year, grade).pluck(:survey_item_id).to_set
if survey_items_set_by_grade.size > 0 && survey_items_set_by_grade.subset?(early_education_survey_items.pluck(:survey_item_id).to_set)
return :early_education
end
:standard
}
def description
Summary.new(survey_item_id, prompt, true)
end
def self.survey_type(survey_item_ids:)
survey_item_ids = survey_item_ids.reject { |id| id.ends_with?("-1") }.to_set
return :short_form if survey_item_ids.subset? short_form_survey_items.map(&:survey_item_id).to_set
return :early_education if survey_item_ids.subset? early_education_survey_items.map(&:survey_item_id).to_set
return :teacher if survey_item_ids.subset? teacher_survey_items.map(&:survey_item_id).to_set
return :standard if survey_item_ids.subset? standard_survey_items.map(&:survey_item_id).to_set
:unknown
end
end
end

View file

@ -0,0 +1,85 @@
module Dashboard
class SurveyItemResponse < ApplicationRecord
TEACHER_RESPONSE_THRESHOLD = 2
STUDENT_RESPONSE_THRESHOLD = 10
belongs_to :dashboard_school
belongs_to :dashboard_survey_item
belongs_to :dashboard_academic_year
belongs_to :dashboard_student, optional: true
belongs_to :dashboard_gender, optional: true
belongs_to :dashboard_income, optional: true
belongs_to :dashboard_ell, optional: true
belongs_to :dashboard_sped, optional: true
has_one :dashboard_measure, through: :dashboard_survey_item
scope :exclude_boston, lambda {
includes(school: :district).where.not("district.name": "Boston")
}
scope :averages_for_grade, lambda { |survey_items, school, academic_year, grade|
SurveyItemResponse.where(survey_item: survey_items, school:,
academic_year:, grade:).group(:survey_item).having("count(*) >= 10").average(:likert_score)
}
scope :averages_for_gender, lambda { |survey_items, school, academic_year, gender|
SurveyItemResponse.where(survey_item: survey_items, school:,
academic_year:, gender:, grade: school.grades(academic_year:)).group(:survey_item).having("count(*) >= 10").average(:likert_score)
}
scope :averages_for_income, lambda { |survey_items, school, academic_year, income|
SurveyItemResponse.where(survey_item: survey_items, school:,
academic_year:, income:, grade: school.grades(academic_year:)).group(:survey_item).having("count(*) >= 10").average(:likert_score)
}
scope :averages_for_ell, lambda { |survey_items, school, academic_year, ell|
SurveyItemResponse.where(survey_item: survey_items, school:,
academic_year:, ell:, grade: school.grades(academic_year:)).group(:survey_item).having("count(*) >= 10").average(:likert_score)
}
scope :averages_for_sped, lambda { |survey_items, school, academic_year, sped|
SurveyItemResponse.where(survey_item: survey_items, school:,
academic_year:, sped:, grade: school.grades(academic_year:)).group(:survey_item).having("count(*) >= 10").average(:likert_score)
}
scope :averages_for_race, lambda { |school, academic_year, race|
SurveyItemResponse.joins("JOIN student_races on survey_item_responses.student_id = student_races.student_id JOIN students on students.id = student_races.student_id").where(
school:, academic_year:, grade: school.grades(academic_year:)
).where("student_races.race_id": race.id).group(:survey_item_id).having("count(*) >= 10").average(:likert_score)
}
def self.grouped_responses(school:, academic_year:)
@grouped_responses ||= Hash.new do |memo, (school, academic_year)|
memo[[school, academic_year]] =
SurveyItemResponse.where(school:, academic_year:).group(:survey_item_id).average(:likert_score)
end
@grouped_responses[[school, academic_year]]
end
def self.teacher_survey_items_with_sufficient_responses(school:, academic_year:)
@teacher_survey_items_with_sufficient_responses ||= Hash.new do |memo, (school, academic_year)|
hash = SurveyItem.joins("inner join survey_item_responses on survey_item_responses.survey_item_id = survey_items.id")
.teacher_survey_items
.where("survey_item_responses.school": school, "survey_item_responses.academic_year": academic_year)
.group("survey_items.id")
.having("count(*) > 0").count
memo[[school, academic_year]] = hash
end
@teacher_survey_items_with_sufficient_responses[[school, academic_year]]
end
def self.student_survey_items_with_sufficient_responses_by_grade(school:, academic_year:)
@student_survey_items_with_sufficient_responses_by_grade ||= Hash.new do |memo, (school, academic_year)|
hash = 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)
.group(:grade, :id)
.count
memo[[school, academic_year]] = hash
end
@student_survey_items_with_sufficient_responses_by_grade[[school, academic_year]]
end
end
end

View file

@ -0,0 +1,31 @@
# frozen_string_literal: true
module Dashboard
class TeacherResponseRateCalculator < ResponseRateCalculator
def survey_item_count
@survey_item_count ||= survey_items_with_sufficient_responses.length
end
def survey_items_have_sufficient_responses?
survey_item_count.positive?
end
def survey_items_with_sufficient_responses
@survey_items_with_sufficient_responses ||= SurveyItemResponse.teacher_survey_items_with_sufficient_responses(
school:, academic_year:
).slice(*@subcategory.teacher_survey_items.map(&:id))
end
def response_count
@response_count ||= survey_items_with_sufficient_responses&.values&.sum
end
def total_possible_responses
@total_possible_responses ||= respondents.total_teachers
end
def raw_response_rate
(average_responses_per_survey_item / total_possible_responses.to_f * 100).round
end
end
end