parent
490522eb1e
commit
2fd56047d4
@ -0,0 +1,7 @@
|
||||
class Ell < ApplicationRecord
|
||||
scope :by_designation, -> { all.map { |ell| [ell.designation, ell] }.to_h }
|
||||
|
||||
include FriendlyId
|
||||
|
||||
friendly_id :designation, use: [:slugged]
|
||||
end
|
||||
@ -1,5 +1,5 @@
|
||||
class Gender < ApplicationRecord
|
||||
scope :gender_hash, lambda {
|
||||
scope :by_qualtrics_code, lambda {
|
||||
all.map { |gender| [gender.qualtrics_code, gender] }.to_h
|
||||
}
|
||||
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
|
||||
%w[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,44 @@
|
||||
# frozen_string_literal: true
|
||||
#
|
||||
module Analyze
|
||||
module Graph
|
||||
class StudentsByEll
|
||||
include Analyze::Graph::Column::GenderColumn
|
||||
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,13 @@
|
||||
module Analyze
|
||||
module Group
|
||||
class Ell
|
||||
def name
|
||||
"ELL"
|
||||
end
|
||||
|
||||
def slug
|
||||
"ell"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,54 @@
|
||||
class DisaggregationRow
|
||||
attr_reader :row, :headers
|
||||
|
||||
def initialize(row:, headers:)
|
||||
@row = row
|
||||
@headers = headers
|
||||
end
|
||||
|
||||
def district
|
||||
@district ||= value_from(pattern: /District/i)
|
||||
end
|
||||
|
||||
def academic_year
|
||||
@academic_year ||= value_from(pattern: /Academic\s*Year/i)
|
||||
end
|
||||
|
||||
def raw_income
|
||||
@income ||= value_from(pattern: /Low\s*Income/i)
|
||||
end
|
||||
|
||||
def lasid
|
||||
@lasid ||= value_from(pattern: /LASID/i)
|
||||
end
|
||||
|
||||
def raw_ell
|
||||
@raw_ell ||= value_from(pattern: /EL Student First Year/i)
|
||||
end
|
||||
|
||||
def ell
|
||||
@ell ||= begin
|
||||
value = value_from(pattern: /EL Student First Year/i).downcase
|
||||
|
||||
case value
|
||||
when /lep student 1st year|LEP student not 1st year/i
|
||||
"ELL"
|
||||
when /Does not apply/i
|
||||
"Not ELL"
|
||||
else
|
||||
"Unknown"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def value_from(pattern:)
|
||||
output = nil
|
||||
matches = headers.select do |header|
|
||||
pattern.match(header)
|
||||
end.map { |item| item.delete("\n") }
|
||||
matches.each do |match|
|
||||
output ||= row[match]
|
||||
end
|
||||
output
|
||||
end
|
||||
end
|
||||
|
@ -0,0 +1,12 @@
|
||||
class CreateEll < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
create_table :ells do |t|
|
||||
t.string :designation
|
||||
t.string :slug
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :ells, :designation, unique: true
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,5 @@
|
||||
class AddEllToSurveyItemResponse < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_reference :survey_item_responses, :ell, foreign_key: true
|
||||
end
|
||||
end
|
||||
@ -1,33 +1,33 @@
|
||||
namespace :clean do
|
||||
# These tasks must be run in their respective project so the correct schools are in the database
|
||||
desc 'clean ecp data'
|
||||
desc "clean ecp data"
|
||||
task ecp: :environment do
|
||||
input_filepath = Rails.root.join('tmp', 'data', 'ecp_data', 'raw')
|
||||
output_filepath = Rails.root.join('tmp', 'data', 'ecp_data', 'clean')
|
||||
log_filepath = Rails.root.join('tmp', 'data', 'ecp_data', 'removed')
|
||||
input_filepath = Rails.root.join("tmp", "data", "ecp_data", "raw")
|
||||
output_filepath = Rails.root.join("tmp", "data", "ecp_data", "clean")
|
||||
log_filepath = Rails.root.join("tmp", "data", "ecp_data", "removed")
|
||||
Cleaner.new(input_filepath:, output_filepath:, log_filepath:).clean
|
||||
end
|
||||
|
||||
desc 'clean prepped data'
|
||||
desc "clean prepped data"
|
||||
task prepped: :environment do
|
||||
input_filepath = Rails.root.join('tmp', 'data', 'ecp_data', 'prepped')
|
||||
output_filepath = Rails.root.join('tmp', 'data', 'ecp_data', 'prepped', 'clean')
|
||||
log_filepath = Rails.root.join('tmp', 'data', 'ecp_data', 'prepped', 'removed')
|
||||
input_filepath = Rails.root.join("tmp", "data", "ecp_data", "prepped")
|
||||
output_filepath = Rails.root.join("tmp", "data", "ecp_data", "prepped", "clean")
|
||||
log_filepath = Rails.root.join("tmp", "data", "ecp_data", "prepped", "removed")
|
||||
Cleaner.new(input_filepath:, output_filepath:, log_filepath:).clean
|
||||
end
|
||||
desc 'clean mciea data'
|
||||
desc "clean mciea data"
|
||||
task mciea: :environment do
|
||||
input_filepath = Rails.root.join('tmp', 'data', 'mciea_data', 'raw')
|
||||
output_filepath = Rails.root.join('tmp', 'data', 'mciea_data', 'clean')
|
||||
log_filepath = Rails.root.join('tmp', 'data', 'mciea_data', 'removed')
|
||||
input_filepath = Rails.root.join("tmp", "data", "mciea_data", "raw")
|
||||
output_filepath = Rails.root.join("tmp", "data", "mciea_data", "clean")
|
||||
log_filepath = Rails.root.join("tmp", "data", "mciea_data", "removed")
|
||||
Cleaner.new(input_filepath:, output_filepath:, log_filepath:).clean
|
||||
end
|
||||
|
||||
desc 'clean rpp data'
|
||||
desc "clean rpp data"
|
||||
task rpp: :environment do
|
||||
input_filepath = Rails.root.join('tmp', 'data', 'rpp_data', 'raw')
|
||||
output_filepath = Rails.root.join('tmp', 'data', 'rpp_data', 'clean')
|
||||
log_filepath = Rails.root.join('tmp', 'data', 'rpp_data', 'removed')
|
||||
input_filepath = Rails.root.join("tmp", "data", "rpp_data", "raw")
|
||||
output_filepath = Rails.root.join("tmp", "data", "rpp_data", "clean")
|
||||
log_filepath = Rails.root.join("tmp", "data", "rpp_data", "removed")
|
||||
Cleaner.new(input_filepath:, output_filepath:, log_filepath:).clean
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -0,0 +1,39 @@
|
||||
require "rails_helper"
|
||||
require "fileutils"
|
||||
|
||||
RSpec.describe DisaggregationLoader do
|
||||
let(:path) do
|
||||
Rails.root.join("spec", "fixtures", "disaggregation")
|
||||
end
|
||||
let(:academic_year) { create(:academic_year, range: "2022-23") }
|
||||
let(:district) { create(:district, name: "Maynard Public Schools") }
|
||||
context ".load" do
|
||||
it "loads data from the file into a hash" do
|
||||
data = DisaggregationLoader.new(path:).load
|
||||
expect(data.values.first.lasid).to eq("1")
|
||||
expect(data.values.first.academic_year).to eq("2022-23")
|
||||
expect(data.values.first.district).to eq("Maynard Public Schools")
|
||||
|
||||
expect(data.values.last.lasid).to eq("500")
|
||||
expect(data.values.last.academic_year).to eq("2022-23")
|
||||
expect(data.values.last.district).to eq("Maynard Public Schools")
|
||||
end
|
||||
|
||||
it "loads income data" do
|
||||
data = DisaggregationLoader.new(path:).load
|
||||
expect(data.values.first.raw_income).to eq("Free Lunch")
|
||||
expect(data.values.last.raw_income).to eq("Not Eligible")
|
||||
|
||||
expect(data[["1", "Maynard Public Schools", "2022-23"]].raw_income).to eq("Free Lunch")
|
||||
expect(data[["2", "Maynard Public Schools", "2022-23"]].raw_income).to eq("Not Eligible")
|
||||
expect(data[["3", "Maynard Public Schools", "2022-23"]].raw_income).to eq("Reduced Lunch")
|
||||
end
|
||||
end
|
||||
|
||||
context "Creating a new loader" do
|
||||
it "creates a directory for the loader file" do
|
||||
DisaggregationLoader.new(path:)
|
||||
expect(path).to exist
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,108 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe DisaggregationRow do
|
||||
let(:headers) do
|
||||
["District", "Academic Year", "LASID", "HispanicLatino", "Race", "Gender", "SpecialEdStatus", "In 504 Plan",
|
||||
"LowIncome", "EL Student First Year"]
|
||||
end
|
||||
|
||||
context ".district" do
|
||||
context "when the column heading is any upper or lowercase variant of the word district" do
|
||||
it "returns the correct value for district" do
|
||||
row = { "District" => "Maynard Public Schools" }
|
||||
expect(DisaggregationRow.new(row:, headers:).district).to eq "Maynard Public Schools"
|
||||
|
||||
headers = ["dISTRICT"]
|
||||
headers in [district]
|
||||
row = { district => "Maynard Public Schools" }
|
||||
expect(DisaggregationRow.new(row:, headers:).district).to eq "Maynard Public Schools"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context ".academic_year" do
|
||||
context "when the column heading is any upper or lowercase variant of the words academic year" do
|
||||
it "returns the correct value for district" do
|
||||
row = { "Academic Year" => "2022-23" }
|
||||
expect(DisaggregationRow.new(row:, headers:).academic_year).to eq "2022-23"
|
||||
|
||||
headers = ["aCADEMIC yEAR"]
|
||||
headers in [academic_year]
|
||||
row = { academic_year => "2022-23" }
|
||||
expect(DisaggregationRow.new(row:, headers:).academic_year).to eq "2022-23"
|
||||
|
||||
headers = ["AcademicYear"]
|
||||
headers in [academic_year]
|
||||
row = { academic_year => "2022-23" }
|
||||
expect(DisaggregationRow.new(row:, headers:).academic_year).to eq "2022-23"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context ".raw_income" do
|
||||
context "when the column heading is any upper or lowercase variant of the words low income" do
|
||||
it "returns the correct value for low_income" do
|
||||
row = { "LowIncome" => "Free Lunch" }
|
||||
expect(DisaggregationRow.new(row:, headers:).raw_income).to eq "Free Lunch"
|
||||
|
||||
headers = ["Low income"]
|
||||
headers in [income]
|
||||
row = { income => "Free Lunch" }
|
||||
expect(DisaggregationRow.new(row:, headers:).raw_income).to eq "Free Lunch"
|
||||
|
||||
headers = ["LoW InCOme"]
|
||||
headers in [income]
|
||||
row = { income => "Free Lunch" }
|
||||
expect(DisaggregationRow.new(row:, headers:).raw_income).to eq "Free Lunch"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context ".lasid" do
|
||||
context "when the column heading is any upper or lowercase variant of the words lasid" do
|
||||
it "returns the correct value for lasid" do
|
||||
row = { "LASID" => "2366" }
|
||||
expect(DisaggregationRow.new(row:, headers:).lasid).to eq "2366"
|
||||
|
||||
headers = ["LaSiD"]
|
||||
headers in [lasid]
|
||||
row = { lasid => "2366" }
|
||||
expect(DisaggregationRow.new(row:, headers:).lasid).to eq "2366"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context ".ell" do
|
||||
context "when the column heading is any upper or lowercase variant of the words 'ELL' or 'El Student First Year'" do
|
||||
it "returns the correct value for a student" do
|
||||
row = { "EL Student First Year" => "LEP student 1st year" }
|
||||
expect(DisaggregationRow.new(row:, headers:).ell).to eq "ELL"
|
||||
|
||||
headers = ["EL Student First Year"]
|
||||
headers in [ell]
|
||||
row = { ell => "LEP student not 1st year" }
|
||||
expect(DisaggregationRow.new(row:, headers:).ell).to eq "ELL"
|
||||
|
||||
headers = ["EL Student First Year"]
|
||||
headers in [ell]
|
||||
row = { ell => "Does not apply" }
|
||||
expect(DisaggregationRow.new(row:, headers:).ell).to eq "Not ELL"
|
||||
|
||||
headers = ["EL Student First Year"]
|
||||
headers in [ell]
|
||||
row = { ell => "Unknown" }
|
||||
expect(DisaggregationRow.new(row:, headers:).ell).to eq "Unknown"
|
||||
|
||||
headers = ["EL Student First Year"]
|
||||
headers in [ell]
|
||||
row = { ell => "Any other text" }
|
||||
expect(DisaggregationRow.new(row:, headers:).ell).to eq "Unknown"
|
||||
|
||||
headers = ["EL Student First Year"]
|
||||
headers in [ell]
|
||||
row = { ell => "" }
|
||||
expect(DisaggregationRow.new(row:, headers:).ell).to eq "Unknown"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in new issue