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

main
Nelson Jovel 2 years ago
parent e1f0b78236
commit a4fddbeced

@ -1,7 +1,8 @@
PATH
remote: .
specs:
dashboard (0.1.0)
dashboard (0.1.1)
friendly_id (~> 5.4.0)
rails (>= 7.1.2)
GEM
@ -97,6 +98,8 @@ GEM
factory_bot_rails (6.4.2)
factory_bot (~> 6.4)
railties (>= 5.0.0)
friendly_id (5.4.2)
activerecord (>= 4.0.0)
globalid (1.2.1)
activesupport (>= 6.1)
i18n (1.14.1)
@ -115,6 +118,7 @@ GEM
net-smtp
marcel (1.0.2)
mini_mime (1.1.5)
mini_portile2 (2.8.5)
minitest (5.20.0)
mutex_m (0.2.0)
net-imap (0.4.8)
@ -127,7 +131,8 @@ GEM
net-smtp (0.4.0)
net-protocol
nio4r (2.7.0)
nokogiri (1.15.5-x86_64-linux)
nokogiri (1.15.5)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
pg (1.5.4)
psych (5.1.2)

@ -0,0 +1,59 @@
# frozen_string_literal: true
module Dashboard
class HomeController < ApplicationController
helper HeaderHelper
def index
@districts = districts
@district = district
@schools = schools
@school = school
@year = year
@categories = Category.sorted.map { |category| CategoryPresenter.new(category:) }
end
private
def districts
District.all.order(:name).map do |district|
[district.name, district.id]
end
end
def district
return District.find(params[:district]) if params[:district].present?
District.first
end
def schools
if district.present?
district.dashboard_schools.order(:name).map do |school|
[school.name, school.id]
end
else
[]
end
end
def school
School.find(params[:school]) if params[:school].present?
end
def year
return nil unless school.present?
academic_year = AcademicYear.all.order(range: :DESC).find do |ay|
Subcategory.all.any? do |subcategory|
rate = subcategory.response_rate(school:, academic_year: ay)
rate.meets_student_threshold || rate.meets_teacher_threshold
end
end
academic_year&.range || AcademicYear.order("range DESC").first.range
end
end
end

@ -0,0 +1,60 @@
# frozen_string_literal: true
module Dashboard
module HeaderHelper
def link_to_overview(district:, school:, academic_year:)
"/districts/#{district.slug}/schools/#{school.slug}/overview?year=#{academic_year.range}"
end
def link_to_browse(district:, school:, academic_year:)
"/districts/#{district.slug}/schools/#{school.slug}/browse/teachers-and-leadership?year=#{academic_year.range}"
end
def link_to_analyze(district:, school:, academic_year:)
year = academic_year.range
"/districts/#{district.slug}/schools/#{school.slug}/analyze?year=#{year}&category=1&academic_years=#{year}"
end
def district_url_for(district:, academic_year:)
pages = %w[overview browse analyze]
pages.each do |page|
if request.fullpath.include? page
return send("#{page}_link", district_slug: district.slug, school_slug: district.schools.alphabetic.first.slug,
academic_year_range: academic_year.range)
end
end
end
def school_url_for(school:, academic_year:)
pages = %w[overview browse analyze]
pages.each do |page|
if request.fullpath.include? page
return send("#{page}_link", district_slug: school.district.slug, school_slug: school.slug,
academic_year_range: academic_year.range)
end
end
end
def link_weight(path:)
active?(path:) ? "weight-700" : "weight-400"
end
private
def overview_link(district_slug:, school_slug:, academic_year_range:)
"/districts/#{district_slug}/schools/#{school_slug}/overview?year=#{academic_year_range}"
end
def analyze_link(district_slug:, school_slug:, academic_year_range:)
"/districts/#{district_slug}/schools/#{school_slug}/analyze?year=#{academic_year_range}&academic_years=#{academic_year_range}"
end
def browse_link(district_slug:, school_slug:, academic_year_range:)
"/districts/#{district_slug}/schools/#{school_slug}/browse/teachers-and-leadership?year=#{academic_year_range}"
end
def active?(path:)
request.fullpath.include? path
end
end
end

@ -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)

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

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

@ -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

@ -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

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

@ -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

@ -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

@ -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

@ -0,0 +1,29 @@
# frozen_string_literal: true
module Dashboard
class AdminDataPresenter < DataItemPresenter
def initialize(measure_id:, admin_data_items:, has_sufficient_data:, school:, academic_year:)
super(measure_id:, has_sufficient_data:, school:, academic_year:)
@admin_data_items = admin_data_items
end
def title
"School admin data"
end
def id
"admin-data-items-#{@measure_id}"
end
def reason_for_insufficiency
"limited availability"
end
def descriptions_and_availability
@admin_data_items.map do |admin_data_item|
Summary.new(admin_data_item.admin_data_item_id, admin_data_item.description,
admin_data_item.admin_data_values.where(school:, academic_year:).count > 0)
end
end
end
end

@ -0,0 +1,85 @@
# frozen_string_literal: true
module Analyze
class BarPresenter
include AnalyzeHelper
attr_reader :score, :x_position, :academic_year, :measure_id, :measure, :color
MINIMUM_BAR_HEIGHT = 2
def initialize(measure:, academic_year:, score:, x_position:, color:)
@score = score
@x_position = x_position
@academic_year = academic_year
@measure = measure
@measure_id = measure.measure_id
@color = color
end
def y_offset
benchmark_height = analyze_zone_height * 2
case zone.type
when :ideal, :approval
benchmark_height - bar_height_percentage
else
benchmark_height
end
end
def bar_color
"fill-#{zone.type}"
end
def bar_height_percentage
bar_height = send("#{zone.type}_bar_height_percentage") || 0
enforce_minimum_height(bar_height:)
end
def percentage
low_benchmark = zone.low_benchmark
(score.average - low_benchmark) / (zone.high_benchmark - low_benchmark)
end
def zone
zones = Zones.new(
watch_low_benchmark: measure.watch_low_benchmark,
growth_low_benchmark: measure.growth_low_benchmark,
approval_low_benchmark: measure.approval_low_benchmark,
ideal_low_benchmark: measure.ideal_low_benchmark
)
zones.zone_for_score(score.average)
end
def average
average = score.average || 0
average.round(6)
end
private
def enforce_minimum_height(bar_height:)
bar_height < MINIMUM_BAR_HEIGHT ? MINIMUM_BAR_HEIGHT : bar_height
end
def ideal_bar_height_percentage
(percentage * zone_height_percentage + zone_height_percentage) * 100
end
def approval_bar_height_percentage
(percentage * zone_height_percentage) * 100
end
def growth_bar_height_percentage
((1 - percentage) * zone_height_percentage) * 100
end
def watch_bar_height_percentage
((1 - percentage) * zone_height_percentage + zone_height_percentage) * 100
end
def warning_bar_height_percentage
((1 - percentage) * zone_height_percentage + zone_height_percentage + zone_height_percentage) * 100
end
end
end

@ -0,0 +1,20 @@
# frozen_string_literal: true
module Analyze
module Graph
class AllData
include Analyze::Graph::Column
def to_s
"All Data"
end
def slug
"all-data"
end
def columns
[AllStudent, AllTeacher, AllAdmin, GroupedBarColumnPresenter]
end
end
end
end

@ -0,0 +1,39 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
class AllAdmin < GroupedBarColumnPresenter
def label
%w[All Admin]
end
def basis
"admin data"
end
def show_irrelevancy_message?
!measure.includes_admin_data_items?
end
def show_insufficient_data_message?
!academic_years.any? do |year|
measure.sufficient_admin_data?(school:, academic_year: year)
end
end
def insufficiency_message
["data not", "available"]
end
def score(year_index)
measure.admin_score(school:, academic_year: academic_years[year_index])
end
def type
:admin
end
end
end
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
class AllStudent < GroupedBarColumnPresenter
def label
%w[All Students]
end
def show_irrelevancy_message?
!measure.includes_student_survey_items?
end
def show_insufficient_data_message?
scores = academic_years.map do |year|
measure.score(school:, academic_year: year)
end
scores.all? { |score| !score.meets_student_threshold? }
end
def score(year_index)
measure.student_score(school:, academic_year: academic_years[year_index])
end
def type
:student
end
end
end
end
end

@ -0,0 +1,47 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
class AllSurveyData < GroupedBarColumnPresenter
def label
%w[Survey Data]
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
scores = academic_years.map do |year|
combined_score(school:, academic_year: year)
end
scores.all? { |score| !score.meets_student_threshold? && !score.meets_teacher_threshold? }
end
def score(year_index)
combined_score(school:, academic_year: academic_years[year_index])
end
def type
:all_survey_data
end
private
def combined_score(school:, academic_year:)
teacher_score = measure.teacher_score(school:, academic_year:)
student_score = measure.student_score(school:, academic_year:)
averages = []
averages << student_score.average unless student_score.average.nil?
averages << teacher_score.average unless teacher_score.average.nil?
average = averages.average if averages.length > 0
combined_score = Score.new(average:, meets_student_threshold: student_score.meets_student_threshold,
meets_teacher_threshold: teacher_score.meets_teacher_threshold)
end
end
end
end
end

@ -0,0 +1,42 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
class AllTeacher < GroupedBarColumnPresenter
def label
%w[All Teachers]
end
def basis
"teacher surveys"
end
def show_irrelevancy_message?
!measure.includes_teacher_survey_items?
end
def show_insufficient_data_message?
scores = academic_years.map do |year|
measure.score(school:, academic_year: year)
end
scores.all? { |score| !score.meets_teacher_threshold? }
end
def score(year_index)
measure.teacher_score(school:, academic_year: academic_years[year_index])
end
def type
:teacher
end
def n_size(year_index)
SurveyItemResponse.where(survey_item: measure.teacher_survey_items, school:,
academic_year: academic_years[year_index]).select(:response_id).distinct.count
end
end
end
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module EllColumn
class Ell < GroupedBarColumnPresenter
include Analyze::Graph::Column::EllColumn::ScoreForEll
include Analyze::Graph::Column::EllColumn::EllCount
def label
%w[ELL]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def ell
::Ell.find_by_slug "ell"
end
end
end
end
end
end

@ -0,0 +1,18 @@
module Analyze
module Graph
module Column
module EllColumn
module EllCount
def type
:student
end
def n_size(year_index)
SurveyItemResponse.where(ell:, survey_item: measure.student_survey_items, school:, grade: grades(year_index),
academic_year: academic_years[year_index]).select(:response_id).distinct.count
end
end
end
end
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module EllColumn
class NotEll < GroupedBarColumnPresenter
include Analyze::Graph::Column::EllColumn::ScoreForEll
include Analyze::Graph::Column::EllColumn::EllCount
def label
["Not ELL"]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def ell
::Ell.find_by_slug "not-ell"
end
end
end
end
end
end

@ -0,0 +1,42 @@
module Analyze
module Graph
module Column
module EllColumn
module ScoreForEll
def score(year_index)
academic_year = academic_years[year_index]
meets_student_threshold = sufficient_student_responses?(academic_year:)
return Score::NIL_SCORE unless meets_student_threshold
averages = SurveyItemResponse.averages_for_ell(measure.student_survey_items, school, academic_year,
ell)
average = bubble_up_averages(averages:).round(2)
Score.new(average:,
meets_teacher_threshold: false,
meets_student_threshold:,
meets_admin_data_threshold: false)
end
def bubble_up_averages(averages:)
measure.student_scales.map do |scale|
scale.survey_items.map do |survey_item|
averages[survey_item]
end.remove_blanks.average
end.remove_blanks.average
end
def sufficient_student_responses?(academic_year:)
return false unless measure.subcategory.response_rate(school:, academic_year:).meets_student_threshold?
yearly_counts = SurveyItemResponse.where(school:, academic_year:,
ell:, survey_item: measure.student_survey_items).group(:ell).select(:response_id).distinct(:response_id).count
yearly_counts.any? do |count|
count[1] >= 10
end
end
end
end
end
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module EllColumn
class Unknown < GroupedBarColumnPresenter
include Analyze::Graph::Column::EllColumn::ScoreForEll
include Analyze::Graph::Column::EllColumn::EllCount
def label
%w[Unknown]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def ell
::Ell.find_by_slug "unknown"
end
end
end
end
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module GenderColumn
class Female < GroupedBarColumnPresenter
include Analyze::Graph::Column::GenderColumn::ScoreForGender
include Analyze::Graph::Column::GenderColumn::GenderCount
def label
%w[Female]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def gender
::Gender.find_by_qualtrics_code 1
end
end
end
end
end
end

@ -0,0 +1,18 @@
module Analyze
module Graph
module Column
module GenderColumn
module GenderCount
def type
:student
end
def n_size(year_index)
SurveyItemResponse.where(gender:, survey_item: measure.student_survey_items, school:, grade: grades(year_index),
academic_year: academic_years[year_index]).select(:response_id).distinct.count
end
end
end
end
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module GenderColumn
class Male < GroupedBarColumnPresenter
include Analyze::Graph::Column::GenderColumn::ScoreForGender
include Analyze::Graph::Column::GenderColumn::GenderCount
def label
%w[Male]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def gender
::Gender.find_by_qualtrics_code 2
end
end
end
end
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module GenderColumn
class NonBinary < GroupedBarColumnPresenter
include Analyze::Graph::Column::GenderColumn::ScoreForGender
include Analyze::Graph::Column::GenderColumn::GenderCount
def label
%w[Non-Binary]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def gender
::Gender.find_by_qualtrics_code 4
end
end
end
end
end
end

@ -0,0 +1,42 @@
module Analyze
module Graph
module Column
module GenderColumn
module ScoreForGender
def score(year_index)
academic_year = academic_years[year_index]
meets_student_threshold = sufficient_student_responses?(academic_year:)
return Score::NIL_SCORE unless meets_student_threshold
averages = SurveyItemResponse.averages_for_gender(measure.student_survey_items, school, academic_year,
gender)
average = bubble_up_averages(averages:).round(2)
Score.new(average:,
meets_teacher_threshold: false,
meets_student_threshold:,
meets_admin_data_threshold: false)
end
def bubble_up_averages(averages:)
measure.student_scales.map do |scale|
scale.survey_items.map do |survey_item|
averages[survey_item]
end.remove_blanks.average
end.remove_blanks.average
end
def sufficient_student_responses?(academic_year:)
return false unless measure.subcategory.response_rate(school:, academic_year:).meets_student_threshold?
yearly_counts = SurveyItemResponse.where(school:, academic_year:,
gender:, survey_item: measure.student_survey_items).group(:gender).select(:response_id).distinct(:response_id).count
yearly_counts.any? do |count|
count[1] >= 10
end
end
end
end
end
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module GenderColumn
class Unknown < GroupedBarColumnPresenter
include Analyze::Graph::Column::GenderColumn::ScoreForGender
include Analyze::Graph::Column::GenderColumn::GenderCount
def label
%w[Unknown]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def gender
::Gender.find_by_qualtrics_code 99
end
end
end
end
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module Grade
class Eight < GroupedBarColumnPresenter
include Analyze::Graph::Column::Grade::ScoreForGrade
include Analyze::Graph::Column::Grade::GradeCount
def label
%w[Grade 8]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def grade
8
end
end
end
end
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module Grade
class Eleven < GroupedBarColumnPresenter
include Analyze::Graph::Column::Grade::ScoreForGrade
include Analyze::Graph::Column::Grade::GradeCount
def label
%w[Grade 11]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def grade
11
end
end
end
end
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module Grade
class Five < GroupedBarColumnPresenter
include Analyze::Graph::Column::Grade::ScoreForGrade
include Analyze::Graph::Column::Grade::GradeCount
def label
%w[Grade 5]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def grade
5
end
end
end
end
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module Grade
class Four < GroupedBarColumnPresenter
include Analyze::Graph::Column::Grade::ScoreForGrade
include Analyze::Graph::Column::Grade::GradeCount
def label
%w[Grade 4]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def grade
4
end
end
end
end
end
end

@ -0,0 +1,18 @@
module Analyze
module Graph
module Column
module Grade
module GradeCount
def type
:student
end
def n_size(year_index)
SurveyItemResponse.where(grade:, survey_item: measure.student_survey_items, school:,
academic_year: academic_years[year_index]).select(:response_id).distinct.count
end
end
end
end
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module Grade
class Nine < GroupedBarColumnPresenter
include Analyze::Graph::Column::Grade::ScoreForGrade
include Analyze::Graph::Column::Grade::GradeCount
def label
%w[Grade 9]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def grade
9
end
end
end
end
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module Grade
class One < GroupedBarColumnPresenter
include Analyze::Graph::Column::Grade::ScoreForGrade
include Analyze::Graph::Column::Grade::GradeCount
def label
%w[Grade 1]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def grade
1
end
end
end
end
end
end

@ -0,0 +1,42 @@
module Analyze
module Graph
module Column
module Grade
module ScoreForGrade
def score(year_index)
academic_year = academic_years[year_index]
meets_student_threshold = sufficient_student_responses?(academic_year:)
return Score::NIL_SCORE unless meets_student_threshold
averages = SurveyItemResponse.averages_for_grade(measure.student_survey_items, school,
academic_year, grade)
average = bubble_up_averages(averages:).round(2)
Score.new(average:,
meets_teacher_threshold: false,
meets_student_threshold:,
meets_admin_data_threshold: false)
end
def bubble_up_averages(averages:)
measure.student_scales.map do |scale|
scale.survey_items.map do |survey_item|
averages[survey_item]
end.remove_blanks.average
end.remove_blanks.average
end
def sufficient_student_responses?(academic_year:)
return false unless measure.subcategory.response_rate(school:, academic_year:).meets_student_threshold?
yearly_counts = SurveyItemResponse.where(school:, academic_year:,
survey_item: measure.student_survey_items).group(:grade).select(:response_id).distinct(:response_id).count
yearly_counts.any? do |count|
count[1] >= 10
end
end
end
end
end
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module Grade
class Seven < GroupedBarColumnPresenter
include Analyze::Graph::Column::Grade::ScoreForGrade
include Analyze::Graph::Column::Grade::GradeCount
def label
%w[Grade 7]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def grade
7
end
end
end
end
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module Grade
class Six < GroupedBarColumnPresenter
include Analyze::Graph::Column::Grade::ScoreForGrade
include Analyze::Graph::Column::Grade::GradeCount
def label
%w[Grade 6]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def grade
6
end
end
end
end
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module Grade
class Ten < GroupedBarColumnPresenter
include Analyze::Graph::Column::Grade::ScoreForGrade
include Analyze::Graph::Column::Grade::GradeCount
def label
%w[Grade 10]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def grade
10
end
end
end
end
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module Grade
class Three < GroupedBarColumnPresenter
include Analyze::Graph::Column::Grade::ScoreForGrade
include Analyze::Graph::Column::Grade::GradeCount
def label
%w[Grade 3]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def grade
3
end
end
end
end
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module Grade
class Twelve < GroupedBarColumnPresenter
include Analyze::Graph::Column::Grade::ScoreForGrade
include Analyze::Graph::Column::Grade::GradeCount
def label
%w[Grade 12]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def grade
12
end
end
end
end
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module Grade
class Two < GroupedBarColumnPresenter
include Analyze::Graph::Column::Grade::ScoreForGrade
include Analyze::Graph::Column::Grade::GradeCount
def label
%w[Grade 2]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def grade
2
end
end
end
end
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module Grade
class Zero < GroupedBarColumnPresenter
include Analyze::Graph::Column::Grade::ScoreForGrade
include Analyze::Graph::Column::Grade::GradeCount
def label
%w[Kindergarten]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def grade
0
end
end
end
end
end
end

@ -0,0 +1,172 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
class GroupedBarColumnPresenter
include AnalyzeHelper
attr_reader :measure_name, :measure_id, :category, :position, :measure, :school, :academic_years,
:number_of_columns
def initialize(measure:, school:, academic_years:, position:, number_of_columns:)
@measure = measure
@measure_name = @measure.name
@measure_id = @measure.measure_id
@category = @measure.subcategory.category
@school = school
@academic_years = academic_years
@position = position
@number_of_columns = number_of_columns
end
def academic_year_for_year_index(year_index)
academic_years[year_index]
end
def score(year_index)
measure.score(school:, academic_year: academic_years[year_index]) || 0
end
def bars
@bars ||= yearly_scores.map.each_with_index do |yearly_score, index|
year = yearly_score.year
Analyze::BarPresenter.new(measure:, academic_year: year,
score: yearly_score.score,
x_position: bar_x(index),
color: bar_color(year))
end
end
def label
%w[All Data]
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
scores = academic_years.map do |year|
measure.score(school:, academic_year: year)
end
scores.all? do |score|
!score.meets_teacher_threshold? && !score.meets_student_threshold? && !score.meets_admin_data_threshold?
end
end
def column_midpoint
zone_label_width + (grouped_chart_column_width * (position + 1)) - (grouped_chart_column_width / 2)
end
def bar_width
min_bar_width(10.5 / data_sources)
end
def min_bar_width(number)
min_width = 2
number < min_width ? min_width : number
end
def message_x
column_midpoint - message_width / 2
end
def message_y
17
end
def message_width
20
end
def message_height
34
end
def column_end_x
zone_label_width + (grouped_chart_column_width * (position + 1))
end
def column_start_x
zone_label_width + (grouped_chart_column_width * position)
end
def grouped_chart_column_width
graph_width / data_sources
end
def bar_label_height
(100 - ((100 - analyze_graph_height) / 2))
end
def data_sources
number_of_columns
end
def basis
"student surveys"
end
def type
:all_data
end
def show_popover?
%i[student teacher].include? type
end
def n_size(year_index)
SurveyItemResponse.where(survey_item: measure.student_survey_items, school:, grade: grades(year_index),
academic_year: academic_years[year_index]).select(:response_id).distinct.count
end
def popover_content(year_index)
"#{n_size(year_index)} #{type.to_s.capitalize}s"
end
def insufficiency_message
["survey response", "rate below 25%"]
end
def sufficient_data?(year_index)
case basis
when "student"
score(year_index).meets_student_threshold
when "teacher"
score(year_index).meets_teacher_threshold
else
true
end
end
def grades(year_index)
Respondent.by_school_and_year(school:, academic_year: academic_years[year_index]).enrollment_by_grade.keys
end
private
YearlyScore = Struct.new(:year, :score)
def yearly_scores
yearly_scores = academic_years.each_with_index.map do |year, index|
YearlyScore.new(year, score(index))
end
yearly_scores.reject do |yearly_score|
yearly_score.score.blank?
end
end
def bar_color(year)
@available_academic_years ||= AcademicYear.order(:range).all
colors[@available_academic_years.find_index(year)]
end
def bar_x(index)
column_start_x + (index * bar_width * 1.2) +
((column_end_x - column_start_x) - (yearly_scores.size * bar_width * 1.2)) / 2
end
end
end
end
end

@ -0,0 +1,29 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module IncomeColumn
class Disadvantaged < GroupedBarColumnPresenter
include Analyze::Graph::Column::IncomeColumn::ScoreForIncome
include Analyze::Graph::Column::IncomeColumn::IncomeCount
def label
%w[Economically Disadvantaged]
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def income
Income.find_by_designation "Economically Disadvantaged - Y"
end
end
end
end
end
end

@ -0,0 +1,18 @@
module Analyze
module Graph
module Column
module IncomeColumn
module IncomeCount
def type
:student
end
def n_size(year_index)
SurveyItemResponse.where(income:, survey_item: measure.student_survey_items, school:, grade: grades(year_index),
academic_year: academic_years[year_index]).select(:response_id).distinct.count
end
end
end
end
end
end

@ -0,0 +1,29 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module IncomeColumn
class NotDisadvantaged < GroupedBarColumnPresenter
include Analyze::Graph::Column::IncomeColumn::ScoreForIncome
include Analyze::Graph::Column::IncomeColumn::IncomeCount
def label
["Not Economically", "Disadvantaged"]
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def income
Income.find_by_designation "Economically Disadvantaged - N"
end
end
end
end
end
end

@ -0,0 +1,42 @@
module Analyze
module Graph
module Column
module IncomeColumn
module ScoreForIncome
def score(year_index)
academic_year = academic_years[year_index]
meets_student_threshold = sufficient_student_responses?(academic_year:)
return Score::NIL_SCORE unless meets_student_threshold
averages = SurveyItemResponse.averages_for_income(measure.student_survey_items, school, academic_year,
income)
average = bubble_up_averages(averages:).round(2)
Score.new(average:,
meets_teacher_threshold: false,
meets_student_threshold:,
meets_admin_data_threshold: false)
end
def bubble_up_averages(averages:)
measure.student_scales.map do |scale|
scale.survey_items.map do |survey_item|
averages[survey_item]
end.remove_blanks.average
end.remove_blanks.average
end
def sufficient_student_responses?(academic_year:)
return false unless measure.subcategory.response_rate(school:, academic_year:).meets_student_threshold?
yearly_counts = SurveyItemResponse.where(school:, academic_year:,
income:, survey_item: measure.student_survey_items).group(:income).select(:response_id).distinct(:response_id).count
yearly_counts.any? do |count|
count[1] >= 10
end
end
end
end
end
end
end

@ -0,0 +1,29 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module IncomeColumn
class Unknown < GroupedBarColumnPresenter
include Analyze::Graph::Column::IncomeColumn::ScoreForIncome
include Analyze::Graph::Column::IncomeColumn::IncomeCount
def label
["Unknown"]
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def income
Income.find_by_designation "Unknown"
end
end
end
end
end
end

@ -0,0 +1,29 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module RaceColumn
class AmericanIndian < GroupedBarColumnPresenter
include Analyze::Graph::Column::ScoreForRace
include Analyze::Graph::Column::RaceColumn::RaceCount
def label
%w[American Indian]
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def race
Race.find_by_qualtrics_code 1
end
end
end
end
end
end

@ -0,0 +1,29 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module RaceColumn
class Asian < GroupedBarColumnPresenter
include Analyze::Graph::Column::ScoreForRace
include Analyze::Graph::Column::RaceColumn::RaceCount
def label
%w[Asian]
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def race
Race.find_by_qualtrics_code 2
end
end
end
end
end
end

@ -0,0 +1,29 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module RaceColumn
class Black < GroupedBarColumnPresenter
include Analyze::Graph::Column::ScoreForRace
include Analyze::Graph::Column::RaceColumn::RaceCount
def label
%w[Black]
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def race
Race.find_by_qualtrics_code 3
end
end
end
end
end
end

@ -0,0 +1,29 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module RaceColumn
class Hispanic < GroupedBarColumnPresenter
include Analyze::Graph::Column::ScoreForRace
include Analyze::Graph::Column::RaceColumn::RaceCount
def label
%w[Hispanic]
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def race
Race.find_by_qualtrics_code 4
end
end
end
end
end
end

@ -0,0 +1,30 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module RaceColumn
class MiddleEastern < GroupedBarColumnPresenter
include Analyze::Graph::Column::ScoreForRace
include Analyze::Graph::Column::RaceColumn::RaceCount
def label
%w[Middle Eastern]
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def race
Race.find_by_qualtrics_code 8
end
end
end
end
end
end

@ -0,0 +1,29 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module RaceColumn
class Multiracial < GroupedBarColumnPresenter
include Analyze::Graph::Column::ScoreForRace
include Analyze::Graph::Column::RaceColumn::RaceCount
def label
%w[Multiracial]
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def race
Race.find_by_qualtrics_code 100
end
end
end
end
end
end

@ -0,0 +1,20 @@
module Analyze
module Graph
module Column
module RaceColumn
module RaceCount
def type
:student
end
def n_size(year_index)
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: academic_years[year_index],
survey_item: measure.student_survey_items
).where("student_races.race_id": race.id).select(:response_id).distinct.count
end
end
end
end
end
end

@ -0,0 +1,29 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module RaceColumn
class Unknown < GroupedBarColumnPresenter
include Analyze::Graph::Column::ScoreForRace
include Analyze::Graph::Column::RaceColumn::RaceCount
def label
%w[Not Listed]
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def race
Race.find_by_qualtrics_code 99
end
end
end
end
end
end

@ -0,0 +1,29 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module RaceColumn
class White < GroupedBarColumnPresenter
include Analyze::Graph::Column::ScoreForRace
include Analyze::Graph::Column::RaceColumn::RaceCount
def label
%w[White]
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def race
Race.find_by_qualtrics_code 5
end
end
end
end
end
end

@ -0,0 +1,40 @@
module Analyze
module Graph
module Column
module ScoreForRace
def score(year_index)
academic_year = academic_years[year_index]
meets_student_threshold = sufficient_student_responses?(academic_year:)
return Score::NIL_SCORE unless meets_student_threshold
survey_items = measure.student_survey_items
averages = SurveyItemResponse.averages_for_race(school, academic_year, race)
average = bubble_up_averages(averages:).round(2)
Score.new(average:,
meets_teacher_threshold: false,
meets_student_threshold:,
meets_admin_data_threshold: false)
end
def bubble_up_averages(averages:)
measure.student_scales.map do |scale|
scale.survey_items.map do |survey_item|
averages[survey_item.id]
end.remove_blanks.average
end.remove_blanks.average
end
def sufficient_student_responses?(academic_year:)
return false unless measure.subcategory.response_rate(school:, academic_year:).meets_student_threshold?
number_of_students_for_a_racial_group = 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:
).where("student_races.race_id": race.id).distinct.pluck(:student_id).count
number_of_students_for_a_racial_group >= 10
end
end
end
end
end

@ -0,0 +1,34 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module SpedColumn
class NotSped < GroupedBarColumnPresenter
include Analyze::Graph::Column::SpedColumn::ScoreForSped
include Analyze::Graph::Column::SpedColumn::SpedCount
def label
["Not Special", "Education"]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def sped
::Sped.find_by_slug "not-special-education"
end
end
end
end
end
end

@ -0,0 +1,42 @@
module Analyze
module Graph
module Column
module SpedColumn
module ScoreForSped
def score(year_index)
academic_year = academic_years[year_index]
meets_student_threshold = sufficient_student_responses?(academic_year:)
return Score::NIL_SCORE unless meets_student_threshold
averages = SurveyItemResponse.averages_for_sped(measure.student_survey_items, school, academic_year,
sped)
average = bubble_up_averages(averages:).round(2)
Score.new(average:,
meets_teacher_threshold: false,
meets_student_threshold:,
meets_admin_data_threshold: false)
end
def bubble_up_averages(averages:)
measure.student_scales.map do |scale|
scale.survey_items.map do |survey_item|
averages[survey_item]
end.remove_blanks.average
end.remove_blanks.average
end
def sufficient_student_responses?(academic_year:)
return false unless measure.subcategory.response_rate(school:, academic_year:).meets_student_threshold?
yearly_counts = SurveyItemResponse.where(school:, academic_year:,
sped:, survey_item: measure.student_survey_items).group(:sped).select(:response_id).distinct(:response_id).count
yearly_counts.any? do |count|
count[1] >= 10
end
end
end
end
end
end
end

@ -0,0 +1,34 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module SpedColumn
class Sped < GroupedBarColumnPresenter
include Analyze::Graph::Column::SpedColumn::ScoreForSped
include Analyze::Graph::Column::SpedColumn::SpedCount
def label
%w[Special Education]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def sped
::Sped.find_by_slug "special-education"
end
end
end
end
end
end

@ -0,0 +1,18 @@
module Analyze
module Graph
module Column
module SpedColumn
module SpedCount
def type
:student
end
def n_size(year_index)
SurveyItemResponse.where(sped:, survey_item: measure.student_survey_items, school:, grade: grades(year_index),
academic_year: academic_years[year_index]).select(:response_id).distinct.count
end
end
end
end
end
end

@ -0,0 +1,34 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module SpedColumn
class Unknown < GroupedBarColumnPresenter
include Analyze::Graph::Column::SpedColumn::ScoreForSped
include Analyze::Graph::Column::SpedColumn::SpedCount
def label
%w[Unknown]
end
def basis
"student"
end
def show_irrelevancy_message?
false
end
def show_insufficient_data_message?
false
end
def sped
::Sped.find_by_slug "unknown"
end
end
end
end
end
end

@ -0,0 +1,20 @@
# frozen_string_literal: true
module Analyze
module Graph
class StudentsAndTeachers
include Analyze::Graph::Column
def to_s
'Students & Teachers'
end
def slug
'students-and-teachers'
end
def columns
[AllStudent, AllTeacher, AllSurveyData]
end
end
end
end

@ -0,0 +1,44 @@
# frozen_string_literal: true
module Analyze
module Graph
class StudentsByEll
include Analyze::Graph::Column::EllColumn
attr_reader :ells
def initialize(ells:)
@ells = ells
end
def to_s
"Students by Ell"
end
def slug
"students-by-ell"
end
def columns
[].tap do |array|
ells.each do |ell|
array << column_for_ell_code(code: ell.slug)
end
array.sort_by!(&:to_s)
array << Analyze::Graph::Column::AllStudent
end
end
private
def column_for_ell_code(code:)
CFR[code]
end
CFR = {
"ell" => Analyze::Graph::Column::EllColumn::Ell,
"not-ell" => Analyze::Graph::Column::EllColumn::NotEll,
"unknown" => Analyze::Graph::Column::EllColumn::Unknown
}.freeze
end
end
end

@ -0,0 +1,45 @@
# frozen_string_literal: true
module Analyze
module Graph
class StudentsByGender
include Analyze::Graph::Column::GenderColumn
attr_reader :genders
def initialize(genders:)
@genders = genders
end
def to_s
"Students by Gender"
end
def slug
"students-by-gender"
end
def columns
[].tap do |array|
genders.each do |gender|
array << column_for_gender_code(code: gender.qualtrics_code)
end
array.sort_by!(&:to_s)
array << Analyze::Graph::Column::AllStudent
end
end
private
def column_for_gender_code(code:)
CFR[code]
end
CFR = {
1 => Analyze::Graph::Column::GenderColumn::Female,
2 => Analyze::Graph::Column::GenderColumn::Male,
4 => Analyze::Graph::Column::GenderColumn::NonBinary,
99 => Analyze::Graph::Column::GenderColumn::Unknown
}.freeze
end
end
end

@ -0,0 +1,53 @@
# frozen_string_literal: true
module Analyze
module Graph
class StudentsByGrade
include Analyze::Graph::Column::Grade
attr_reader :grades
def initialize(grades:)
@grades = grades
end
def to_s
"Students by Grade"
end
def slug
"students-by-grade"
end
def columns
[].tap do |array|
grades.each do |grade|
array << column_for_grade_code(code: grade)
end
array << Analyze::Graph::Column::AllStudent
end
end
private
def column_for_grade_code(code:)
CFR[code]
end
CFR = {
0 => Zero,
1 => One,
2 => Two,
3 => Three,
4 => Four,
5 => Five,
6 => Six,
7 => Seven,
8 => Eight,
9 => Nine,
10 => Ten,
11 => Eleven,
12 => Twelve
}.freeze
end
end
end

@ -0,0 +1,42 @@
# frozen_string_literal: true
module Analyze
module Graph
class StudentsByIncome
attr_reader :incomes
def initialize(incomes:)
@incomes = incomes
end
def to_s
"Students by income"
end
def slug
"students-by-income"
end
def columns
[].tap do |array|
incomes.each do |income|
array << column_for_income_code(code: income.slug)
end
array << Analyze::Graph::Column::AllStudent
end
end
private
def column_for_income_code(code:)
CFR[code.to_s]
end
CFR = {
"economically-disadvantaged-y" => Analyze::Graph::Column::IncomeColumn::Disadvantaged,
"economically-disadvantaged-n" => Analyze::Graph::Column::IncomeColumn::NotDisadvantaged,
"unknown" => Analyze::Graph::Column::IncomeColumn::Unknown
}.freeze
end
end
end

@ -0,0 +1,47 @@
# frozen_string_literal: true
module Analyze
module Graph
class StudentsByRace
attr_reader :races
def initialize(races:)
@races = races
end
def to_s
"Students by Race"
end
def slug
"students-by-race"
end
def columns
[].tap do |array|
races.each do |race|
array << column_for_race_code(code: race.qualtrics_code)
end
array << Analyze::Graph::Column::AllStudent
end
end
private
def column_for_race_code(code:)
CFR[code.to_s]
end
CFR = {
"1" => Analyze::Graph::Column::RaceColumn::AmericanIndian,
"2" => Analyze::Graph::Column::RaceColumn::Asian,
"3" => Analyze::Graph::Column::RaceColumn::Black,
"4" => Analyze::Graph::Column::RaceColumn::Hispanic,
"5" => Analyze::Graph::Column::RaceColumn::White,
"8" => Analyze::Graph::Column::RaceColumn::MiddleEastern,
"99" => Analyze::Graph::Column::RaceColumn::Unknown,
"100" => Analyze::Graph::Column::RaceColumn::Multiracial
}.freeze
end
end
end

@ -0,0 +1,43 @@
# frozen_string_literal: true
module Analyze
module Graph
class StudentsBySped
include Analyze::Graph::Column::SpedColumn
attr_reader :speds
def initialize(speds:)
@speds = speds
end
def to_s
"Students by SpEd"
end
def slug
"students-by-sped"
end
def columns
[].tap do |array|
speds.each do |sped|
array << column_for_sped_code(code: sped.slug)
end
array << Analyze::Graph::Column::AllStudent
end
end
private
def column_for_sped_code(code:)
CFR[code]
end
CFR = {
"special-education" => Analyze::Graph::Column::SpedColumn::Sped,
"not-special-education" => Analyze::Graph::Column::SpedColumn::NotSped,
"unknown" => Analyze::Graph::Column::SpedColumn::Unknown
}.freeze
end
end
end

@ -0,0 +1,13 @@
module Analyze
module Group
class Ell
def name
"ELL"
end
def slug
"ell"
end
end
end
end

@ -0,0 +1,13 @@
module Analyze
module Group
class Gender
def name
'Gender'
end
def slug
'gender'
end
end
end
end

@ -0,0 +1,13 @@
module Analyze
module Group
class Grade
def name
'Grade'
end
def slug
'grade'
end
end
end
end

@ -0,0 +1,13 @@
module Analyze
module Group
class Income
def name
'Income'
end
def slug
'income'
end
end
end
end

@ -0,0 +1,13 @@
module Analyze
module Group
class Race
def name
'Race'
end
def slug
'race'
end
end
end
end

@ -0,0 +1,13 @@
module Analyze
module Group
class Sped
def name
"Special Education"
end
def slug
"sped"
end
end
end
end

@ -0,0 +1,200 @@
module Analyze
class Presenter
attr_reader :params, :school, :academic_year
def initialize(params:, school:, academic_year:)
@params = params
@school = school
@academic_year = academic_year
end
def category
@category ||= Category.find_by_category_id(params[:category]) || Category.order(:category_id).first
end
def categories
@categories = Category.all.order(:category_id)
end
def subcategory
@subcategory ||= Subcategory.find_by_subcategory_id(params[:subcategory]) || subcategories.first
end
def subcategories
@subcategories = category.subcategories.order(:subcategory_id)
end
def measures
@measures = subcategory.measures.order(:measure_id).includes(%i[admin_data_items subcategory])
end
def academic_years
@academic_years = AcademicYear.order(:range).all
end
def selected_academic_years
@selected_academic_years ||= begin
year_params = params[:academic_years]
return [] unless year_params
year_params.split(",").map { |year| AcademicYear.find_by_range(year) }.compact
end
end
def races
@races ||= Race.all.order(designation: :ASC)
end
def selected_races
@selected_races ||= begin
race_params = params[:races]
return races unless race_params
race_params.split(",").map { |race| Race.find_by_slug race }.compact
end
end
def ells
@ells ||= Ell.all.order(slug: :ASC)
end
def selected_ells
@selected_ells ||= begin
ell_params = params[:ells]
return ells unless ell_params
ell_params.split(",").map { |ell| Ell.find_by_slug ell }.compact
end
end
def speds
@speds ||= Sped.all.order(id: :ASC)
end
def selected_speds
@selected_speds ||= begin
sped_params = params[:speds]
return speds unless sped_params
sped_params.split(",").map { |sped| Sped.find_by_slug sped }.compact
end
end
def graphs
@graphs ||= [Analyze::Graph::AllData.new,
Analyze::Graph::StudentsAndTeachers.new,
Analyze::Graph::StudentsByRace.new(races: selected_races),
Analyze::Graph::StudentsByGrade.new(grades: selected_grades),
Analyze::Graph::StudentsByGender.new(genders: selected_genders),
Analyze::Graph::StudentsByIncome.new(incomes: selected_incomes),
Analyze::Graph::StudentsByEll.new(ells: selected_ells),
Analyze::Graph::StudentsBySped.new(speds: selected_speds)]
end
def graph
@graph ||= graphs.reduce(graphs.first) do |acc, graph|
graph.slug == params[:graph] ? graph : acc
end
end
def selected_grades
@selected_grades ||= begin
grade_params = params[:grades]
return grades unless grade_params
grade_params.split(",").map(&:to_i)
end
end
def selected_genders
@selected_genders ||= begin
gender_params = params[:genders]
return genders unless gender_params
gender_params.split(",").sort.map { |gender| Gender.find_by_designation(gender) }.compact
end
end
def genders
@genders ||= Gender.all.order(designation: :ASC)
end
def groups
@groups = [Analyze::Group::Ell.new, Analyze::Group::Gender.new, Analyze::Group::Grade.new, Analyze::Group::Income.new,
Analyze::Group::Race.new, Analyze::Group::Sped.new]
end
def group
@group ||= groups.reduce(groups.first) do |acc, group|
group.slug == params[:group] ? group : acc
end
end
def slice
@slice ||= slices.reduce(slices.first) do |acc, slice|
slice.slug == params[:slice] ? slice : acc
end
end
def slices
source.slices
end
def source
@source ||= sources.reduce(sources.first) do |acc, source|
source.slug == params[:source] ? source : acc
end
end
def sources
all_data_slices = [Analyze::Slice::AllData.new]
all_data_source = Analyze::Source::AllData.new(slices: all_data_slices)
students_and_teachers = Analyze::Slice::StudentsAndTeachers.new
students_by_group = Analyze::Slice::StudentsByGroup.new(races:, grades:)
survey_data_slices = [students_and_teachers, students_by_group]
survey_data_source = Analyze::Source::SurveyData.new(slices: survey_data_slices)
@sources = [all_data_source, survey_data_source]
end
def grades
@grades ||= SurveyItemResponse.where(school:, academic_year:)
.where.not(grade: nil)
.group(:grade)
.select(:response_id)
.distinct(:response_id)
.count.reject do |_key, value|
value < 10
end.keys
end
def incomes
@incomes ||= Income.all
end
def selected_incomes
@selected_incomes ||= begin
income_params = params[:incomes]
return incomes unless income_params
income_params.split(",").map { |income| Income.find_by_slug(income) }.compact
end
end
def cache_objects
[subcategory,
selected_academic_years,
graph,
selected_races,
selected_grades,
grades,
selected_genders,
genders,
selected_ells,
ells,
selected_speds,
speds]
end
end
end

@ -0,0 +1,17 @@
module Analyze
module Slice
class AllData
def to_s
'All Data'
end
def slug
'all-data'
end
def graphs
[Analyze::Graph::AllData.new]
end
end
end
end

@ -0,0 +1,17 @@
module Analyze
module Slice
class StudentsAndTeachers
def to_s
'Students & Teachers'
end
def slug
'students-and-teachers'
end
def graphs
[Analyze::Graph::StudentsAndTeachers.new]
end
end
end
end

@ -0,0 +1,24 @@
module Analyze
module Slice
class StudentsByGroup
attr_reader :races, :grades
def initialize(races:, grades:)
@races = races
@grades = grades
end
def to_s
'Students by Group'
end
def slug
'students-by-group'
end
def graphs
[Analyze::Graph::StudentsByRace.new(races:), Analyze::Graph::StudentsByGrade.new(grades:)]
end
end
end
end

@ -0,0 +1,21 @@
module Analyze
module Source
class AllData
attr_reader :slices
include Analyze::Slice
def initialize(slices:)
@slices = slices
end
def to_s
'All Data'
end
def slug
'all-data'
end
end
end
end

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save