mirror of
https://github.com/edcommonwealth/Dashboard.git
synced 2026-03-09 07:18:13 -07:00
dirty commit: can't get references to work correctly between any tables
This commit is contained in:
parent
e1f0b78236
commit
a4fddbeced
183 changed files with 5461 additions and 5 deletions
40
app/models/dashboard/academic_year.rb
Normal file
40
app/models/dashboard/academic_year.rb
Normal 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)
|
||||
14
app/models/dashboard/admin_data_item.rb
Normal file
14
app/models/dashboard/admin_data_item.rb
Normal 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
|
||||
9
app/models/dashboard/admin_data_value.rb
Normal file
9
app/models/dashboard/admin_data_value.rb
Normal 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
|
||||
13
app/models/dashboard/category.rb
Normal file
13
app/models/dashboard/category.rb
Normal 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
|
||||
18
app/models/dashboard/district.rb
Normal file
18
app/models/dashboard/district.rb
Normal 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
|
||||
20
app/models/dashboard/ell.rb
Normal file
20
app/models/dashboard/ell.rb
Normal 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
|
||||
26
app/models/dashboard/gender.rb
Normal file
26
app/models/dashboard/gender.rb
Normal 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
|
||||
31
app/models/dashboard/income.rb
Normal file
31
app/models/dashboard/income.rb
Normal 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
|
||||
227
app/models/dashboard/measure.rb
Normal file
227
app/models/dashboard/measure.rb
Normal 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
|
||||
51
app/models/dashboard/race.rb
Normal file
51
app/models/dashboard/race.rb
Normal 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
|
||||
27
app/models/dashboard/respondent.rb
Normal file
27
app/models/dashboard/respondent.rb
Normal 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
|
||||
7
app/models/dashboard/response_rate.rb
Normal file
7
app/models/dashboard/response_rate.rb
Normal 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
|
||||
51
app/models/dashboard/response_rate_calculator.rb
Normal file
51
app/models/dashboard/response_rate_calculator.rb
Normal 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
|
||||
44
app/models/dashboard/scale.rb
Normal file
44
app/models/dashboard/scale.rb
Normal 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
|
||||
27
app/models/dashboard/school.rb
Normal file
27
app/models/dashboard/school.rb
Normal 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
|
||||
28
app/models/dashboard/score.rb
Normal file
28
app/models/dashboard/score.rb
Normal 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
|
||||
20
app/models/dashboard/sped.rb
Normal file
20
app/models/dashboard/sped.rb
Normal 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
|
||||
9
app/models/dashboard/student.rb
Normal file
9
app/models/dashboard/student.rb
Normal 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
|
||||
6
app/models/dashboard/student_race.rb
Normal file
6
app/models/dashboard/student_race.rb
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module Dashboard
|
||||
class StudentRace < ApplicationRecord
|
||||
belongs_to :dashboard_student
|
||||
belongs_to :dashboard_race
|
||||
end
|
||||
end
|
||||
67
app/models/dashboard/student_response_rate_calculator.rb
Normal file
67
app/models/dashboard/student_response_rate_calculator.rb
Normal 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
|
||||
102
app/models/dashboard/subcategory.rb
Normal file
102
app/models/dashboard/subcategory.rb
Normal 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
|
||||
6
app/models/dashboard/summary.rb
Normal file
6
app/models/dashboard/summary.rb
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Dashboard
|
||||
class Summary < Struct.new(:id, :description, :available?)
|
||||
end
|
||||
end
|
||||
78
app/models/dashboard/survey_item.rb
Normal file
78
app/models/dashboard/survey_item.rb
Normal 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
|
||||
85
app/models/dashboard/survey_item_response.rb
Normal file
85
app/models/dashboard/survey_item_response.rb
Normal 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
|
||||
31
app/models/dashboard/teacher_response_rate_calculator.rb
Normal file
31
app/models/dashboard/teacher_response_rate_calculator.rb
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue