mirror of
https://github.com/edcommonwealth/sqm-dashboards.git
synced 2026-03-07 13:38:18 -08:00
feat: create a parents by language graph
Update demographics table with lanugage options Create a lanugage table to hold the new languages Update the demographic loader to input languages into the database Update the cleaner to read the language column Update the parent table to hold a reference to a language Update the data uploader script to read the language from the csv and update the language information for any parent items that already exist (or create database entries if none already exist) update the analyze interface to add controls for selecting ‘parents by group’ and a dropdown for ‘parent by language’ Update the analyze controller to read the parent-by-group parameter Create a graph for the parent-by-group view Bubble up averages for language calculations. Make sure n-size only counts responses for a given measure.
This commit is contained in:
parent
bedab713af
commit
0f457becf0
31 changed files with 413 additions and 109 deletions
|
|
@ -1,4 +1,7 @@
|
|||
class Housing < ApplicationRecord
|
||||
has_many :parents, dependent: :nullify
|
||||
scope :by_designation, -> { all.map { |housing| [housing.designation, housing] }.to_h }
|
||||
|
||||
def self.to_designation(housing)
|
||||
return "Unknown" if housing.blank?
|
||||
|
||||
|
|
|
|||
33
app/models/language.rb
Normal file
33
app/models/language.rb
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
class Language < ApplicationRecord
|
||||
scope :by_designation, -> { all.map { |language| [language.designation, language] }.to_h }
|
||||
has_many :parent_languages, dependent: :destroy
|
||||
has_many :parents, through: :parent_languages, dependent: :nullify
|
||||
|
||||
include FriendlyId
|
||||
|
||||
friendly_id :designation, use: [:slugged]
|
||||
def self.to_designation(language)
|
||||
return "Prefer not to disclose" if language.blank?
|
||||
|
||||
case language
|
||||
in /^1$/i
|
||||
"English"
|
||||
in /^2$/i
|
||||
"Portuguese"
|
||||
in /^3$/i
|
||||
"Spanish"
|
||||
in /^99$/i
|
||||
"Prefer not to disclose"
|
||||
in /|^100$/i
|
||||
"Prefer to self-describe"
|
||||
else
|
||||
puts "************************************"
|
||||
puts "******** ERROR **********"
|
||||
puts ""
|
||||
puts "Error parsing Language column. '#{language}' is not a known value. Halting execution"
|
||||
puts ""
|
||||
puts "************************************"
|
||||
exit
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -51,6 +51,10 @@ class Measure < ActiveRecord::Base
|
|||
@student_scales ||= scales.student_scales
|
||||
end
|
||||
|
||||
def parent_scales
|
||||
@parent_scales ||= scales.parent_scales
|
||||
end
|
||||
|
||||
def includes_teacher_survey_items?
|
||||
@includes_teacher_survey_items ||= teacher_survey_items.length.positive?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,2 +1,5 @@
|
|||
class Parent < ApplicationRecord
|
||||
belongs_to :housing, optional: true
|
||||
has_many :parent_languages
|
||||
has_and_belongs_to_many :languages, join_table: :parent_languages
|
||||
end
|
||||
|
|
|
|||
4
app/models/parent_language.rb
Normal file
4
app/models/parent_language.rb
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
class ParentLanguage < ApplicationRecord
|
||||
belongs_to :parent
|
||||
belongs_to :language
|
||||
end
|
||||
|
|
@ -21,15 +21,11 @@ module Report
|
|||
Thread.new do
|
||||
while measure = jobs.pop(true)
|
||||
academic_years.each do |academic_year|
|
||||
all_grades = Set.new
|
||||
|
||||
respondents = Respondent.where(school: schools, academic_year:)
|
||||
respondents.each do |respondent|
|
||||
respondent.enrollment_by_grade.keys.each do |grade|
|
||||
all_grades.add(grade)
|
||||
end
|
||||
end
|
||||
all_grades = all_grades.to_a
|
||||
|
||||
enrollment = respondents.map do | respondent| respondent.enrollment_by_grade.keys end.flatten.compact.uniq.sort
|
||||
grades_with_responses = ::SurveyItemResponse.where(school: schools, academic_year:).where.not(grade: nil).pluck(:grade).uniq.sort
|
||||
all_grades = (enrollment & grades_with_responses).sort
|
||||
grades = "#{all_grades.first}-#{all_grades.last}"
|
||||
|
||||
begin_date = ::SurveyItemResponse.where(school: schools,
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ class Sped < ApplicationRecord
|
|||
case sped
|
||||
in /active|^A$|1|^Special\s*Education$/i
|
||||
"Special Education"
|
||||
in %r{^I$|exited|0|^Not\s*Special\s*Education$|Does\s*not\s*apply|Referred|Ineligible|^No\s*special\s*needs$|Not\s*SPED|^#*N/*A$}i
|
||||
in /^I$|exited|0|^Not\s*Special\s*Education$|Does\s*not\s*apply|Referred|Ineligible|^No\s*special\s*needs$|Not\s*SPED/i
|
||||
"Not Special Education"
|
||||
in /^Unknown|^SpecialEdStatus|^SPED/i
|
||||
in %r{^#*N/*A$|^Unknown|^SpecialEdStatus|^SPED}i
|
||||
"Unknown"
|
||||
else
|
||||
puts "************************************"
|
||||
|
|
|
|||
|
|
@ -51,6 +51,10 @@ class SurveyItemResponse < ActiveRecord::Base
|
|||
).where("student_races.race_id": race.id).group(:survey_item).having("count(*) >= 10").average(:likert_score)
|
||||
}
|
||||
|
||||
scope :averages_for_language, lambda { |survey_items, school, academic_year, designations|
|
||||
SurveyItemResponse.joins([parent: :languages]).where(languages: { designation: designations }, survey_item: survey_items, school:, academic_year:).group(:survey_item).average(:likert_score)
|
||||
}
|
||||
|
||||
def self.grouped_responses(school:, academic_year:)
|
||||
@grouped_responses ||= Hash.new do |memo, (school, academic_year)|
|
||||
memo[[school, academic_year]] =
|
||||
|
|
|
|||
60
app/presenters/analyze/graph/column/language.rb
Normal file
60
app/presenters/analyze/graph/column/language.rb
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Analyze
|
||||
module Graph
|
||||
module Column
|
||||
class Language < ColumnBase
|
||||
attr_reader :language, :label
|
||||
|
||||
def initialize(languages:, label:)
|
||||
@language = languages
|
||||
@label = label
|
||||
end
|
||||
|
||||
def basis
|
||||
"parent surveys"
|
||||
end
|
||||
|
||||
def show_irrelevancy_message?(measure:)
|
||||
false
|
||||
end
|
||||
|
||||
def show_insufficient_data_message?(measure:, school:, academic_years:)
|
||||
false
|
||||
end
|
||||
|
||||
def type
|
||||
:parent
|
||||
end
|
||||
|
||||
def n_size(measure:, school:, academic_year:)
|
||||
SurveyItemResponse.joins([parent: :languages]).where(languages: { designation: designations }, survey_item: measure.parent_survey_items, school:, academic_year:).select(:parent_id).distinct.count
|
||||
end
|
||||
|
||||
def score(measure:, school:, academic_year:)
|
||||
return Score::NIL_SCORE if n_size(measure:, school:, academic_year:) < 10
|
||||
|
||||
averages = SurveyItemResponse.averages_for_language(measure.parent_survey_items, school, academic_year,
|
||||
designations)
|
||||
average = bubble_up_averages(measure:, averages:).round(2)
|
||||
Score.new(average:,
|
||||
meets_teacher_threshold: false,
|
||||
meets_student_threshold: true,
|
||||
meets_admin_data_threshold: false)
|
||||
end
|
||||
|
||||
def designations
|
||||
language.map(&:designation)
|
||||
end
|
||||
|
||||
def bubble_up_averages(measure:, averages:)
|
||||
measure.parent_scales.map do |scale|
|
||||
scale.survey_items.map do |survey_item|
|
||||
averages[survey_item]
|
||||
end.remove_blanks.average
|
||||
end.remove_blanks.average
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
43
app/presenters/analyze/graph/parents_by_language.rb
Normal file
43
app/presenters/analyze/graph/parents_by_language.rb
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Analyze
|
||||
module Graph
|
||||
class ParentsByLanguage
|
||||
attr_reader :speds
|
||||
|
||||
ALL_LANGUAGES = Language.all
|
||||
ENGLISH_LANGUAGES = ALL_LANGUAGES.select { |language| language.designation == "English" }
|
||||
UNKNOWN_LANGUAGES = ALL_LANGUAGES.select { |language| language.designation == "Prefer not to disclose" }
|
||||
NON_ENGLISH_LANGUAGES = (ALL_LANGUAGES - ENGLISH_LANGUAGES - UNKNOWN_LANGUAGES)
|
||||
|
||||
def to_s
|
||||
"Parents by Language"
|
||||
end
|
||||
|
||||
def slug
|
||||
"parents-by-language"
|
||||
end
|
||||
|
||||
def columns
|
||||
[].tap do |array|
|
||||
array << Analyze::Graph::Column::Language.new(languages: ENGLISH_LANGUAGES, label: ["English", "Speaking"])
|
||||
array << Analyze::Graph::Column::Language.new(languages: NON_ENGLISH_LANGUAGES, label: ["Non English", "Speaking"])
|
||||
array << Analyze::Graph::Column::Language.new(languages: UNKNOWN_LANGUAGES, label: ["Unknown"])
|
||||
array << Analyze::Graph::Column::Language.new(languages: ALL_LANGUAGES, label: ["All", "Parents"])
|
||||
end
|
||||
end
|
||||
|
||||
def source
|
||||
Analyze::Source::SurveyData.new(slices: nil, graph: self)
|
||||
end
|
||||
|
||||
def slice
|
||||
Analyze::Slice::ParentsByGroup.new(graph: self)
|
||||
end
|
||||
|
||||
def group
|
||||
Analyze::Group::Base.new(name: "Language", slug: "language", graph: self)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -96,10 +96,14 @@ module Analyze
|
|||
end
|
||||
|
||||
def groups
|
||||
@groups = graphs.map(&:group)
|
||||
.reject { |group| group.name.nil? }
|
||||
.sort_by { |group| group.name }
|
||||
.uniq
|
||||
@groups ||=
|
||||
begin
|
||||
first_char_of_class_name = graph.class.name.demodulize.first
|
||||
graphs.select { |graph| graph.class.name.demodulize.first == first_char_of_class_name }.map(&:group)
|
||||
.reject { |group| group.name.nil? }
|
||||
.sort_by { |group| group.name }
|
||||
.uniq
|
||||
end
|
||||
end
|
||||
|
||||
def group
|
||||
|
|
@ -159,7 +163,8 @@ module Analyze
|
|||
Analyze::Graph::StudentsByGender.new(genders: selected_genders),
|
||||
Analyze::Graph::StudentsByIncome.new(incomes: selected_incomes),
|
||||
Analyze::Graph::StudentsBySped.new(speds: selected_speds),
|
||||
Analyze::Graph::StudentsByEll.new(ells: selected_ells)]
|
||||
Analyze::Graph::StudentsByEll.new(ells: selected_ells),
|
||||
Analyze::Graph::ParentsByLanguage.new]
|
||||
end
|
||||
|
||||
def graph
|
||||
|
|
|
|||
9
app/presenters/analyze/slice/parents_by_group.rb
Normal file
9
app/presenters/analyze/slice/parents_by_group.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
module Analyze
|
||||
module Slice
|
||||
class ParentsByGroup < Base
|
||||
def initialize(graph:, label: "Parents by Group", slug: "parents-by-group")
|
||||
super(label:, slug:, graph:)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -79,7 +79,7 @@ class Cleaner
|
|||
|
||||
headers = headers.to_set
|
||||
headers = headers.merge(Set.new(["Raw Income", "Income", "Raw ELL", "ELL", "Raw SpEd", "SpEd", "Progress Count",
|
||||
"Race", "Gender", "Raw Housing Status", "Housing Status"])).to_a
|
||||
"Race", "Gender", "Raw Housing Status", "Housing Status", "Home Language", "Home Languages"])).to_a
|
||||
filtered_headers = include_all_headers(headers:)
|
||||
filtered_headers = remove_unwanted_headers(headers: filtered_headers)
|
||||
log_headers = (filtered_headers + ["Valid Duration?", "Valid Progress?", "Valid Grade?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ class DemographicLoader
|
|||
create_from_column(column: "ELL", row:, model: Ell)
|
||||
create_from_column(column: "Special Ed Status", row:, model: Sped)
|
||||
create_from_column(column: "Housing", row:, model: Housing)
|
||||
create_from_column(column: "Language", row:, model: Language)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class SurveyItemValues
|
|||
row["Gender"] ||= gender
|
||||
row["Raw Housing Status"] = raw_housing
|
||||
row["Housing Status"] = housing
|
||||
row["Home Languages"] = languages.join(",")
|
||||
|
||||
copy_data_to_main_column(main: /Race/i, secondary: /Race Secondary|Race-1/i)
|
||||
copy_data_to_main_column(main: /Gender/i, secondary: /Gender Secondary|Gender-1/i)
|
||||
|
|
@ -161,7 +162,7 @@ class SurveyItemValues
|
|||
# Only check the secondary hispanic column if we don't have self reported data and are relying on SIS data
|
||||
if self_report.nil? && sis.present?
|
||||
hispanic = value_from(pattern: /Hispanic\s*Latino/i)&.downcase
|
||||
race_codes = race_codes.reject { |code| code == 5 } if hispanic == "true" && race_codes.count == 1
|
||||
race_codes = race_codes.reject { |code| code == 5 } if ["true", "1"].include?(hispanic) || race_codes.count == 1
|
||||
race_codes = race_codes.push(4) if %w[true 1].include?(hispanic)
|
||||
end
|
||||
|
||||
|
|
@ -170,7 +171,7 @@ class SurveyItemValues
|
|||
end
|
||||
|
||||
def lasid
|
||||
@lasid ||= value_from(pattern: /LASID/i)
|
||||
@lasid ||= value_from(pattern: /LASID/i) || ""
|
||||
end
|
||||
|
||||
def raw_income
|
||||
|
|
@ -205,6 +206,20 @@ class SurveyItemValues
|
|||
@housing ||= Housing.to_designation(raw_housing)
|
||||
end
|
||||
|
||||
def raw_language
|
||||
@raw_language ||= value_from(pattern: /^Language$/i) || ""
|
||||
end
|
||||
|
||||
def languages
|
||||
@languages ||= [].tap do |languages|
|
||||
if raw_language.present?
|
||||
raw_language.split(",").each do |item|
|
||||
languages << Language.to_designation(item)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def number_of_children
|
||||
@number_of_children ||= value_from(pattern: /Number\s*Of\s*Children/i).to_i
|
||||
end
|
||||
|
|
@ -219,6 +234,9 @@ class SurveyItemValues
|
|||
output ||= row[match]&.strip
|
||||
end
|
||||
|
||||
output = output.delete("\u0000") if output.present?
|
||||
output = output.delete("\x00") if output.present?
|
||||
output.encode!('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '') if output.present?
|
||||
output
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -86,19 +86,31 @@ class SurveyResponsesDataLoader
|
|||
process_survey_items(row:)
|
||||
end
|
||||
|
||||
def languages
|
||||
@languages ||= Language.by_designation
|
||||
end
|
||||
|
||||
def housings
|
||||
@housings ||= Housing.by_designation
|
||||
end
|
||||
|
||||
def process_survey_items(row:)
|
||||
student = nil
|
||||
parent = nil
|
||||
if row.respondent_type == :student
|
||||
student = Student.find_or_create_by(response_id: row.response_id, lasid: row.lasid)
|
||||
student.races.delete_all
|
||||
tmp_races = row.races.map { |race| races[race] }
|
||||
tmp_races = row.races.map { |race| races[race] }.reject(&:nil?)
|
||||
student.races += tmp_races
|
||||
end
|
||||
|
||||
if row.respondent_type == :parent
|
||||
parent = Parent.find_or_create_by(response_id: row.response_id)
|
||||
parent.number_of_children = row.number_of_children
|
||||
tmp_languages = row.languages.map { |language| languages[language] }.reject(&:nil?)
|
||||
parent.languages.delete_all
|
||||
parent.languages.concat(tmp_languages)
|
||||
parent.housing = housings[row.housing] if row.housing.present?
|
||||
parent.save
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +1,8 @@
|
|||
<h3 class="sub-header-4 mt-5">Data Filters</h3>
|
||||
<div class="bg-gray p-3">
|
||||
|
||||
<% @presenter.sources.each do |source| %>
|
||||
|
||||
<%= form_with(url: district_school_analyze_index_path,
|
||||
method: :get,
|
||||
data: {
|
||||
turbo_frame: "results",
|
||||
turbo_action: "advance",
|
||||
controller: "analyze",
|
||||
action: "input->analyze#submit"
|
||||
}) do |f| %>
|
||||
|
||||
<% params.reject{|key,_| key == "graph"}.each do |key, value| %>
|
||||
<input type="hidden" id="year" name="<%= key %>" value="<%= value %>">
|
||||
<% end %>
|
||||
<input type="radio"
|
||||
id="<%= source.slug %>"
|
||||
class="form-check-input"
|
||||
name="graph"
|
||||
value="<%= source.graph.slug %>"
|
||||
<%= source.slug == @presenter.source.slug ? "checked" : "" %>>
|
||||
<label for="<%= source.slug %>"><%= source.to_s %></label>
|
||||
|
||||
<% end %>
|
||||
|
||||
|
||||
<% if source.slug != "all-data" %>
|
||||
<% @presenter.sources.each do |source| %>
|
||||
<%# Source options; e.g. 'All Data' or 'Survey Data only' %>
|
||||
<%= form_with(url: district_school_analyze_index_path,
|
||||
method: :get,
|
||||
data: {
|
||||
|
|
@ -36,28 +12,52 @@
|
|||
action: "input->analyze#submit"
|
||||
}) do |f| %>
|
||||
|
||||
<% params.reject{|key,_| key == "graph"}.each do |key, value| %>
|
||||
<input type="hidden" id="year" name="<%= key %>" value="<%= value %>">
|
||||
<% end %>
|
||||
<% params.reject{|key,_| key == "graph"}.each do |key, value| %>
|
||||
<input type="hidden" id="year" name="<%= key %>" value="<%= value %>">
|
||||
<% end %>
|
||||
|
||||
<% @presenter.slices.each do | slice | %>
|
||||
<div class="d-flex flex-row mx-3">
|
||||
<input type="radio"
|
||||
id="<%= slice.slug %>"
|
||||
class="form-check-input me-2"
|
||||
name="graph"
|
||||
value="<%= slice.graph.slug %>"
|
||||
<%= slice.slug == @presenter.slice.slug ? "checked" : "" %>
|
||||
<%= slice.slug == "all-data" ? "hidden" : "" %>>
|
||||
id="<%= source.slug %>"
|
||||
class="form-check-input"
|
||||
name="graph"
|
||||
value="<%= source.graph.slug %>"
|
||||
<%= source.slug == @presenter.source.slug ? "checked" : "" %>>
|
||||
<label for="<%= source.slug %>"><%= source.to_s %></label>
|
||||
<% end %>
|
||||
|
||||
<label class="text-break" for="<%= slice.slug %>"
|
||||
<%= slice.slug == "all-data" ? "hidden" : "" %>>
|
||||
<%= slice.to_s %></label>
|
||||
</div>
|
||||
|
||||
<% if source.slug != "all-data" %>
|
||||
<%= form_with(url: district_school_analyze_index_path,
|
||||
method: :get,
|
||||
data: {
|
||||
turbo_frame: "results",
|
||||
turbo_action: "advance",
|
||||
controller: "analyze",
|
||||
action: "input->analyze#submit"
|
||||
}) do |f| %>
|
||||
|
||||
<% params.reject{|key,_| key == "graph"}.each do |key, value| %>
|
||||
<input type="hidden" id="year" name="<%= key %>" value="<%= value %>">
|
||||
<% end %>
|
||||
|
||||
<% @presenter.slices.each do | slice | %>
|
||||
<div class="d-flex flex-row mx-3">
|
||||
<input type="radio"
|
||||
id="<%= slice.slug %>"
|
||||
class="form-check-input me-2"
|
||||
name="graph"
|
||||
value="<%= slice.graph.slug %>"
|
||||
<%= slice.slug == @presenter.slice.slug ? "checked" : "" %>
|
||||
<%= slice.slug == "all-data" ? "hidden" : "" %>>
|
||||
|
||||
<label class="text-break" for="<%= slice.slug %>"
|
||||
<%= slice.slug == "all-data" ? "hidden" : "" %>>
|
||||
<%= slice.to_s %></label>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= render partial: "group_selectors" %>
|
||||
<%= render partial: "group_selectors" %>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue