feat: Add income table to the database. Add seeder for income. Add a reference to income from survey item response. Update the loader to import income data from the survey response csv. Refactor analyze controller to extract presenter. Add corresponding specs. Add income graph to analyze page

rpp-main
rebuilt 3 years ago
parent 8e33a4178c
commit 2ac30bb107

@ -12,4 +12,4 @@ Style/Documentation:
Enabled: false Enabled: false
Style/StringLiterals: Style/StringLiterals:
EnforcedStyle: single_quotes EnforcedStyle: double_quotes

@ -1,172 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class AnalyzeController < SqmApplicationController class AnalyzeController < SqmApplicationController
before_action :assign_categories, :assign_subcategories, :assign_measures, :assign_academic_years, def index
:races, :selected_races, :graph, :graphs, :background, :race_score_timestamp, @presenter = Analyze::Presenter.new(params:, school: @school, academic_year: @academic_year)
:source, :sources, :group, :groups, :selected_grades, :grades, :slice, :selected_genders, :genders, only: [:index] @background ||= BackgroundPresenter.new(num_of_columns: @presenter.graph.columns.count)
def index; end
private
def assign_categories
@category ||= Category.find_by_category_id(params[:category])
@category ||= Category.order(:category_id).first
@categories = Category.all.order(:category_id)
end
def assign_subcategories
@subcategories = @category.subcategories.order(:subcategory_id)
@subcategory ||= Subcategory.find_by_subcategory_id(params[:subcategory])
@subcategory ||= @subcategories.first
end
def assign_measures
@measures = @subcategory.measures.order(:measure_id).includes(%i[admin_data_items subcategory])
end
def assign_academic_years
@available_academic_years = AcademicYear.order(:range).all
year_params = params[:academic_years]
@academic_year_params = year_params.split(',') if year_params
@selected_academic_years = []
@academic_year_params ||= []
@academic_year_params.each do |year|
@selected_academic_years << AcademicYear.find_by_range(year)
end
end
def races
@races ||= Race.all.order(designation: :ASC)
end
def selected_races
@selected_races ||= begin
race_params = params[:races]
return @selected_races = races unless race_params
race_list = race_params.split(',') if race_params
if race_list
race_list = race_list.map do |race|
Race.find_by_slug race
end
end
race_list
end
end
def graph
graphs.each do |graph|
@graph = graph if graph.slug == params[:graph]
end
@graph ||= graphs.first
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)]
end
def background
@background ||= BackgroundPresenter.new(num_of_columns: graph.columns.count)
end
def race_score_timestamp
@race_score_timestamp ||= begin
score = RaceScore.where(school: @school,
academic_year: @academic_year).order(updated_at: :DESC).first || Today.new
score.updated_at
end
end
def source
source_param = params[:source]
sources.each do |source|
@source = source if source.slug == source_param
end
@source ||= sources.first
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 slice
slice_param = params[:slice]
slices.each do |slice|
@slice = slice if slice.slug == slice_param
end
@slice ||= slices.first
end
def slices
source.slices
end
def group
groups.each do |group|
@group = group if group.slug == params[:group]
end
@group ||= groups.first
end
def groups
@groups = [Analyze::Group::Race.new, Analyze::Group::Grade.new, Analyze::Group::Gender.new]
end
def selected_grades
@selected_grades ||= begin
grade_params = params[:grades]
return @selected_grades = grades unless grade_params
grade_list = grade_params.split(',') if grade_params
if grade_list
grade_list = grade_list.map do |grade|
grade.to_i
end
end
grade_list
end
end
def grades
@grades ||= SurveyItemResponse.where(school: @school, academic_year: @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 selected_genders
@selected_genders ||= begin
gender_params = params[:genders]
return @selected_genders = genders unless gender_params
gender_list = gender_params.split(',') if gender_params
if gender_list
gender_list = gender_list.map do |gender|
Gender.find_by_designation(gender)
end
end
gender_list
end
end
def genders
@genders ||= Gender.all
end end
end end

@ -62,7 +62,7 @@ module AnalyzeHelper
end end
def base_url def base_url
analyze_subcategory_link(district: @district, school: @school, academic_year: @academic_year, category: @category, analyze_subcategory_link(district: @district, school: @school, academic_year: @academic_year, category: @presenter.category,
subcategory: @subcategory) subcategory: @presenter.subcategory)
end end
end end

@ -22,16 +22,17 @@ export default class extends Controller {
"&graph=" + "&graph=" +
this.selected_graph(target) + this.selected_graph(target) +
"&races=" + "&races=" +
this.selected_races().join(",") + this.selected_items("race").join(",") +
"&genders=" + "&genders=" +
this.selected_genders().join(",") + this.selected_items("gender").join(",") +
"&incomes=" +
this.selected_items("income").join(",") +
"&grades=" + "&grades=" +
this.selected_grades().join(","); this.selected_items("grade").join(",");
this.go_to(url); this.go_to(url);
} }
go_to(location) { go_to(location) {
window.location = location; window.location = location;
} }
@ -121,58 +122,34 @@ export default class extends Controller {
return item.id; return item.id;
})[0]; })[0];
const groups = new Map([
['gender', 'students-by-gender'],
['grade', 'students-by-grade'],
['income', 'students-by-income'],
['race', 'students-by-race']
])
if (target.name === 'slice' || target.name === 'group') { if (target.name === 'slice' || target.name === 'group') {
if (selected_slice === 'students-and-teachers') { if (selected_slice === 'students-and-teachers') {
return 'students-and-teachers'; return 'students-and-teachers';
} else if (this.selected_group() === 'race') {
return 'students-by-race';
} else if (this.selected_group() === 'gender') {
return 'students-by-gender';
} else if (this.selected_group() === 'grade') {
return 'students-by-grade';
} }
return groups.get(this.selected_group());
} }
return window.graph; return window.graph;
} }
selected_races() { selected_items(type) {
let race_checkboxes = [...document.getElementsByName("race-checkbox")] let checkboxes = [...document.getElementsByName(`${type}-checkbox`)]
let races = race_checkboxes let items = checkboxes
.filter((item) => {
return item.checked;
})
.map((item) => {
return item.id;
});
return races;
}
selected_grades() {
let grade_checkboxes = [...document.getElementsByName("grade-checkbox")]
let grades = grade_checkboxes
.filter((item) => {
return item.checked;
})
.map((item) => {
return item.id.replace('grade-', '');
});
return grades;
}
selected_genders() {
let gender_checkboxes = [...document.getElementsByName("gender-checkbox")]
let genders = gender_checkboxes
.filter((item) => { .filter((item) => {
return item.checked; return item.checked;
}) })
.map((item) => { .map((item) => {
return item.id.replace('gender-', ''); return item.id.replace(`${type}-`, '');
}); });
return genders; return items;
} }
} }

@ -0,0 +1,7 @@
class Income < ApplicationRecord
scope :by_designation, -> { all.map { |income| [income.designation, income] }.to_h }
include FriendlyId
friendly_id :designation, use: [:slugged]
end

@ -9,6 +9,7 @@ class SurveyItemResponse < ActiveRecord::Base
belongs_to :survey_item, counter_cache: true belongs_to :survey_item, counter_cache: true
belongs_to :student, foreign_key: :student_id, optional: true belongs_to :student, foreign_key: :student_id, optional: true
belongs_to :gender belongs_to :gender
belongs_to :income
has_one :measure, through: :survey_item has_one :measure, through: :survey_item
@ -25,4 +26,9 @@ class SurveyItemResponse < ActiveRecord::Base
SurveyItemResponse.where(survey_item: survey_items, school:, SurveyItemResponse.where(survey_item: survey_items, school:,
academic_year:, gender:).group(:survey_item).average(:likert_score) academic_year:, gender:).group(:survey_item).average(:likert_score)
} }
scope :averages_for_income, lambda { |survey_items, school, academic_year, income|
SurveyItemResponse.where(survey_item: survey_items, school:,
academic_year:, income:).group(:survey_item).average(:likert_score)
}
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

@ -27,10 +27,10 @@ module Analyze
def bars def bars
@bars ||= yearly_scores.map.each_with_index do |yearly_score, index| @bars ||= yearly_scores.map.each_with_index do |yearly_score, index|
year = yearly_score.year year = yearly_score.year
AnalyzeBarPresenter.new(measure:, academic_year: year, Analyze::BarPresenter.new(measure:, academic_year: year,
score: yearly_score.score, score: yearly_score.score,
x_position: bar_x(index), x_position: bar_x(index),
color: bar_color(year)) color: bar_color(year))
end end
end end

@ -0,0 +1,28 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module IncomeColumn
class Disadvantaged < GroupedBarColumnPresenter
include Analyze::Graph::Column::IncomeColumn::ScoreForIncome
def label
"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,28 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module IncomeColumn
class NotDisadvantaged < GroupedBarColumnPresenter
include Analyze::Graph::Column::IncomeColumn::ScoreForIncome
def label
"Not 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

@ -5,17 +5,11 @@ module Analyze
module ScoreForIncome module ScoreForIncome
def score(year_index) def score(year_index)
academic_year = academic_years[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, averages = SurveyItemResponse.averages_for_income(measure.student_survey_items, school, academic_year,
income) income)
average = bubble_up_averages(averages:).round(2) average = bubble_up_averages(averages:).round(2)
Score.new(average:, scorify(average:, meets_student_threshold: sufficient_student_responses?(academic_year:))
meets_teacher_threshold: false,
meets_student_threshold:,
meets_admin_data_threshold: false)
end end
def bubble_up_averages(averages:) def bubble_up_averages(averages:)
@ -26,9 +20,16 @@ module Analyze
end.remove_blanks.average end.remove_blanks.average
end end
def sufficient_student_responses?(academic_year:) def scorify(average:, meets_student_threshold:)
return false unless measure.subcategory.response_rate(school:, academic_year:).meets_student_threshold? return Score::NIL_SCORE unless meets_student_threshold
Score.new(average:,
meets_teacher_threshold: false,
meets_student_threshold: true,
meets_admin_data_threshold: false)
end
def sufficient_student_responses?(academic_year:)
yearly_counts = SurveyItemResponse.where(school:, academic_year:, yearly_counts = SurveyItemResponse.where(school:, academic_year:,
income:, survey_item: measure.student_survey_items).group(:income).select(:response_id).distinct(:response_id).count income:, survey_item: measure.student_survey_items).group(:income).select(:response_id).distinct(:response_id).count
yearly_counts.any? do |count| yearly_counts.any? do |count|

@ -0,0 +1,28 @@
# frozen_string_literal: true
module Analyze
module Graph
module Column
module IncomeColumn
class Unknown < GroupedBarColumnPresenter
include Analyze::Graph::Column::IncomeColumn::ScoreForIncome
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,40 @@
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,13 @@
module Analyze
module Group
class Income
def name
'Income'
end
def slug
'income'
end
end
end
end

@ -0,0 +1,159 @@
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 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)]
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(",").map { |gender| Gender.find_by_designation(gender) }.compact
end
end
def genders
@genders ||= Gender.all
end
def groups
@groups = [Analyze::Group::Gender.new, Analyze::Group::Grade.new, Analyze::Group::Income.new,
Analyze::Group::Race.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 race_score_timestamp
score = RaceScore.where(school: @school,
academic_year: @academic_year).order(updated_at: :DESC).first || Today.new
score.updated_at
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
end
end

@ -1,8 +0,0 @@
module Analyze
class Ui
attr_reader :params
def initialize(params:)
@params = params
end
end
end

@ -1,83 +0,0 @@
# frozen_string_literal: true
class AnalyzeBarPresenter
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

@ -5,6 +5,7 @@ class DemographicLoader
CSV.parse(File.read(filepath), headers: true) do |row| CSV.parse(File.read(filepath), headers: true) do |row|
process_race(row:) process_race(row:)
process_gender(row:) process_gender(row:)
process_income(row:)
end end
end end
@ -28,6 +29,13 @@ class DemographicLoader
gender = ::Gender.find_or_create_by!(qualtrics_code:, designation:) gender = ::Gender.find_or_create_by!(qualtrics_code:, designation:)
gender.save gender.save
end end
def self.process_income(row:)
designation = row['Income']
return unless designation
Income.find_or_create_by!(designation:)
end
end end
class KnownRace class KnownRace

@ -93,6 +93,36 @@ class SurveyItemValues
genders[gender_code] genders[gender_code]
end end
def lasid
@lasid ||= value_from(pattern: /LASID/i)
end
def raw_income
@raw_income ||= value_from(pattern: /Low\s*Income|Raw\s*Income/i)
return @raw_income if @raw_income.present?
return "Unknown" unless disaggregation_data.present?
disaggregation = disaggregation_data[[lasid, district.name, academic_year.range]]
return "Unknown" unless disaggregation.present?
@raw_income ||= disaggregation.income
end
def income
@income ||= value_from(pattern: /^Income$/i)
return @income if @income.present?
@income ||= case raw_income
in /Free\s*Lunch|Reduced\s*Lunch|Low\s*Income/i
"Economically Disadvantaged - Y"
in /Not\s*Eligible/i
"Economically Disadvantaged - N"
else
"Unknown"
end
end
def value_from(pattern:) def value_from(pattern:)
output = nil output = nil
matches = headers.select do |header| matches = headers.select do |header|
@ -107,9 +137,9 @@ class SurveyItemValues
def to_a def to_a
copy_likert_scores_from_variant_survey_items copy_likert_scores_from_variant_survey_items
headers.select(&:present?) headers.select(&:present?)
.reject { |key, _value| key.start_with? "Q" } .reject { |key, _value| key.start_with? "Q" }
.reject { |key, _value| key.end_with? "-1" } .reject { |key, _value| key.end_with? "-1" }
.map { |header| row[header] } .map { |header| row[header] }
end end
def duration def duration
@ -122,17 +152,17 @@ class SurveyItemValues
def respondent_type def respondent_type
return :teacher if headers return :teacher if headers
.filter(&:present?) .filter(&:present?)
.filter { |header| header.start_with? "t-" }.count > 0 .filter { |header| header.start_with? "t-" }.count > 0
:student :student
end end
def survey_type def survey_type
survey_item_ids = headers survey_item_ids = headers
.filter(&:present?) .filter(&:present?)
.reject { |header| header.end_with?("-1") } .reject { |header| header.end_with?("-1") }
.filter { |header| header.start_with?("t-", "s-") } .filter { |header| header.start_with?("t-", "s-") }
SurveyItem.survey_type(survey_item_ids:) SurveyItem.survey_type(survey_item_ids:)
end end

@ -7,12 +7,13 @@ class SurveyResponsesDataLoader
headers_array = CSV.parse(headers).first headers_array = CSV.parse(headers).first
genders = Gender.gender_hash genders = Gender.gender_hash
schools = School.school_hash schools = School.school_hash
incomes = Income.by_designation
all_survey_items = survey_items(headers:) all_survey_items = survey_items(headers:)
file.lazy.each_slice(500) do |lines| file.lazy.each_slice(500) do |lines|
survey_item_responses = CSV.parse(lines.join, headers:).map do |row| survey_item_responses = CSV.parse(lines.join, headers:).map do |row|
process_row(row: SurveyItemValues.new(row:, headers: headers_array, genders:, survey_items: all_survey_items, schools:), process_row(row: SurveyItemValues.new(row:, headers: headers_array, genders:, survey_items: all_survey_items, schools:),
rules:) rules:, incomes:)
end end
SurveyItemResponse.import survey_item_responses.compact.flatten, batch_size: 500 SurveyItemResponse.import survey_item_responses.compact.flatten, batch_size: 500
end end
@ -24,6 +25,7 @@ class SurveyResponsesDataLoader
headers_array = CSV.parse(headers).first headers_array = CSV.parse(headers).first
genders = Gender.gender_hash genders = Gender.gender_hash
schools = School.school_hash schools = School.school_hash
incomes = Income.by_designation
all_survey_items = survey_items(headers:) all_survey_items = survey_items(headers:)
survey_item_responses = [] survey_item_responses = []
@ -34,7 +36,7 @@ class SurveyResponsesDataLoader
CSV.parse(line, headers:).map do |row| CSV.parse(line, headers:).map do |row|
survey_item_responses << process_row(row: SurveyItemValues.new(row:, headers: headers_array, genders:, survey_items: all_survey_items, schools:), survey_item_responses << process_row(row: SurveyItemValues.new(row:, headers: headers_array, genders:, survey_items: all_survey_items, schools:),
rules:) rules:, incomes:)
end end
row_count += 1 row_count += 1
@ -50,7 +52,7 @@ class SurveyResponsesDataLoader
private private
def self.process_row(row:, rules:) def self.process_row(row:, rules:, incomes:)
return unless row.dese_id? return unless row.dese_id?
return unless row.school.present? return unless row.school.present?
@ -58,32 +60,32 @@ class SurveyResponsesDataLoader
return if rule.new(row:).skip_row? return if rule.new(row:).skip_row?
end end
# byebug if row.response_id == 'butler_student_survey_response_1' process_survey_items(row:, incomes:)
process_survey_items(row:)
end end
def self.process_survey_items(row:) def self.process_survey_items(row:, incomes:)
row.survey_items.map do |survey_item| row.survey_items.map do |survey_item|
likert_score = row.likert_score(survey_item_id: survey_item.survey_item_id) || next likert_score = row.likert_score(survey_item_id: survey_item.survey_item_id) || next
unless likert_score.valid_likert_score? unless likert_score.valid_likert_score?
puts "Response ID: #{row.response_id}, Likert score: #{likert_score} rejected" unless likert_score == 'NA' puts "Response ID: #{row.response_id}, Likert score: #{likert_score} rejected" unless likert_score == "NA"
next next
end end
response = row.survey_item_response(survey_item:) response = row.survey_item_response(survey_item:)
create_or_update_response(survey_item_response: response, likert_score:, row:, survey_item:) create_or_update_response(survey_item_response: response, likert_score:, row:, survey_item:, incomes:)
end.compact end.compact
end end
def self.create_or_update_response(survey_item_response:, likert_score:, row:, survey_item:) def self.create_or_update_response(survey_item_response:, likert_score:, row:, survey_item:, incomes:)
gender = row.gender gender = row.gender
grade = row.grade grade = row.grade
income = incomes[row.income]
if survey_item_response.present? if survey_item_response.present?
survey_item_response.update!(likert_score:, grade:, gender:, recorded_date: row.recorded_date) survey_item_response.update!(likert_score:, grade:, gender:, recorded_date: row.recorded_date, income:)
[] []
else else
SurveyItemResponse.new(response_id: row.response_id, academic_year: row.academic_year, school: row.school, survey_item:, SurveyItemResponse.new(response_id: row.response_id, academic_year: row.academic_year, school: row.school, survey_item:,
likert_score:, grade:, gender:, recorded_date: row.recorded_date) likert_score:, grade:, gender:, recorded_date: row.recorded_date, income:)
end end
end end
@ -94,7 +96,7 @@ class SurveyResponsesDataLoader
def self.get_survey_item_ids_from_headers(headers:) def self.get_survey_item_ids_from_headers(headers:)
CSV.parse(headers).first CSV.parse(headers).first
.filter(&:present?) .filter(&:present?)
.filter { |header| header.start_with? 't-', 's-' } .filter { |header| header.start_with? "t-", "s-" }
end end
private_class_method :process_row private_class_method :process_row

@ -0,0 +1,17 @@
<div class="d-flex align-items-center mx-5">
<input
id="<%= id %>"
class="m-3 <%= name %>-checkbox form-check-input"
type="checkbox"
name="<%= name %>-checkbox"
value="<%= base_url %>"
data-action="click->analyze#refresh"
<%= selected_items.include?(item) ? "checked" : "" %>
<%= @presenter.graph.slug == 'students-and-teachers' || @presenter.source.slug == 'all-data' ? "disabled" : "" %>
<%= @presenter.group.slug == name ? "" : "hidden" %>>
<label for="<%= id %>"
<%= @presenter.group.slug == name ? "" : "hidden" %>>
<%= label_text %>
</label>
</div>

@ -1,7 +1,7 @@
<h3 class="sub-header-4 mt-5">Data Filters</h3> <h3 class="sub-header-4 mt-5">Data Filters</h3>
<div class="bg-gray p-3" data-controller="analyze"> <div class="bg-gray p-3" data-controller="analyze">
<% @sources.each do |source| %> <% @presenter.sources.each do |source| %>
<input type="radio" <input type="radio"
id="<%= source.slug %>" id="<%= source.slug %>"
@ -9,7 +9,7 @@
name="source" name="source"
value="<%= base_url %>" value="<%= base_url %>"
data-action="click->analyze#refresh" data-action="click->analyze#refresh"
<%= source.slug == @source.slug ? "checked" : "" %>> <%= source.slug == @presenter.source.slug ? "checked" : "" %>>
<label for="<%= source.slug %>"><%= source.to_s %></label> <label for="<%= source.slug %>"><%= source.to_s %></label>
<% source.slices.each do | slice | %> <% source.slices.each do | slice | %>
@ -20,7 +20,7 @@
name="slice" name="slice"
value="<%= base_url %>" value="<%= base_url %>"
data-action="click->analyze#refresh" data-action="click->analyze#refresh"
<%= slice.slug == @slice.slug ? "checked" : "" %> <%= slice.slug == @presenter.slice.slug ? "checked" : "" %>
<%= slice.slug == "all-data" ? "hidden" : "" %>> <%= slice.slug == "all-data" ? "hidden" : "" %>>
<label for="<%= slice.slug %>" <label for="<%= slice.slug %>"
@ -34,8 +34,8 @@
</div> </div>
<script> <script>
window.source = "<%= @source.slug %>"; window.source = "<%= @presenter.source.slug %>";
window.slice = "<%= @slice.slug %>"; window.slice = "<%= @presenter.slice.slug %>";
window.group = "<%= @group.slug %>"; window.group = "<%= @presenter.group.slug %>";
window.graph = "<%= @graph.slug %>"; window.graph = "<%= @presenter.graph.slug %>";
</script> </script>

@ -2,12 +2,12 @@
<p>Select a category & subcategory to analyze measure-level results</p> <p>Select a category & subcategory to analyze measure-level results</p>
<select id="select-category" class="mx-3 form-select" data-id="category-dropdown" data-action="analyze#refresh"> <select id="select-category" class="mx-3 form-select" data-id="category-dropdown" data-action="analyze#refresh">
<% categories.each do |category| %> <% categories.each do |category| %>
<option value="<%= analyze_category_link(district: district, school: school, academic_year: academic_year, category: category) %>" <%= category.id == @category.id ? "selected": "" %>><%= "#{category.category_id}: #{category.name}" %></option> <option value="<%= analyze_category_link(district: district, school: school, academic_year: academic_year, category: category) %>" <%= category.id == @presenter.category.id ? "selected": "" %>><%= "#{category.category_id}: #{category.name}" %></option>
<% end %> <% end %>
</select> </select>
<select id="select-subcategory" class="mx-3 form-select mt-3" data-id="subcategory-dropdown" data-action="analyze#refresh"> <select id="select-subcategory" class="mx-3 form-select mt-3" data-id="subcategory-dropdown" data-action="analyze#refresh">
<% subcategories.each do |subcategory| %> <% subcategories.each do |subcategory| %>
<option value="<%= analyze_subcategory_link(district: district, school: school, academic_year: academic_year, category: category, subcategory: subcategory) %>" <%= subcategory.subcategory_id == @subcategory.subcategory_id ? "selected": "" %>> <option value="<%= analyze_subcategory_link(district: district, school: school, academic_year: academic_year, category: category, subcategory: subcategory) %>" <%= subcategory.subcategory_id == @presenter.subcategory.subcategory_id ? "selected": "" %>>
<%= "#{subcategory.subcategory_id}: #{subcategory.name}" %> <%= "#{subcategory.subcategory_id}: #{subcategory.name}" %>
</option> </option>
<% end %> <% end %>

@ -1,67 +1,23 @@
<select id="select-group" name="group" class="mx-4 form-select" data-id="group-dropdown" data-action="analyze#refresh"> <select id="select-group" name="group" class="mx-4 form-select" data-id="group-dropdown" data-action="analyze#refresh">
<% @groups.each do |group| %> <% @presenter.groups.each do |group| %>
<option id="<%= group.slug %>" name="group-option" value="<%= base_url %>" <%= group.slug == @group.slug ? "Selected": "" %>><%= group.name %> </option> <option id="<%= group.slug %>" name="group-option" value="<%= base_url %>" <%= group.slug == @presenter.group.slug ? "Selected": "" %>><%= group.name %> </option>
<% end %> <% end %>
</select> </select>
<p class="sub-header-5 mx-4 mt-3 font-size-14"> Select a group </p> <p class="sub-header-5 mx-4 mt-3 font-size-14"> Select a group </p>
<% @races.each do |race| %> <% @presenter.races.each do |race| %>
<div class="d-flex align-items-center mx-5"> <%= render(partial: "checkboxes", locals: {id: "race-#{race.slug}", item: race, selected_items: @presenter.selected_races, name: "race", label_text: race.designation}) %>
<input
id="<%= race.slug %>"
class="m-3 race-checkbox form-check-input"
type="checkbox"
name="race-checkbox"
value="<%= base_url %>"
data-action="click->analyze#refresh"
<%= @selected_races.map(&:slug).include?(race.slug) ? "checked" : "" %>
<%= @graph.slug == 'students-and-teachers' || @source.slug == 'all-data' ? "disabled" : "" %>
<%= @group.slug == 'race' ? "" : "hidden" %>>
<label for="<%= race.qualtrics_code %>"
<%= @group.slug == 'race' ? "" : "hidden" %>>
<%= race.designation %>
</label>
</div>
<% end %> <% end %>
<% @grades.each do |grade| %> <% @presenter.grades.each do |grade| %>
<div class="d-flex align-items-center mx-5"> <%= render(partial: "checkboxes", locals: {id: "grade-#{grade}", item: grade, selected_items: @presenter.selected_grades, name: "grade", label_text: grade}) %>
<input
id="grade-<%= grade %>"
class="m-3 grade-checkbox form-check-input"
type="checkbox"
name="grade-checkbox"
value="<%= base_url %>"
data-action="click->analyze#refresh"
<%= @selected_grades.include?(grade) ? "checked" : "" %>
<%= @graph.slug == 'students-and-teachers' || @source.slug == 'all-data' ? "disabled" : "" %>
<%= @group.slug == 'grade' ? "" : "hidden" %>>
<label for="grade-<%= grade %>"
<%= @group.slug == 'grade' ? "" : "hidden" %>>
<%= grade %>
</label>
</div>
<% end %> <% end %>
<% @genders.each do |gender| %> <% @presenter.genders.each do |gender| %>
<div class="d-flex align-items-center mx-5"> <%= render(partial: "checkboxes", locals: {id: "gender-#{gender.designation}", item: gender, selected_items: @presenter.selected_genders, name: "gender", label_text: gender.designation}) %>
<input <% end %>
id="gender-<%= gender.designation %>"
class="m-3 gender-checkbox form-check-input"
type="checkbox"
name="gender-checkbox"
value="<%= base_url %>"
data-action="click->analyze#refresh"
<%= @selected_genders.include?(gender) ? "checked" : "" %>
<%= @graph.slug == 'students-and-teachers' || @source.slug == 'all-data' ? "disabled" : "" %>
<%= @group.slug == 'gender' ? "" : "hidden" %>>
<label for="gender-<%= gender %>" <% @presenter.incomes.each do |income| %>
<%= @group.slug == 'gender' ? "" : "hidden" %>> <%= render(partial: "checkboxes", locals: {id: "income-#{income.slug}", item: income, selected_items: @presenter.selected_incomes, name: "income", label_text: income.designation}) %>
<%= gender.designation %>
</label>
</div>
<% end %> <% end %>

@ -1,9 +1,9 @@
<svg width="100%" height="<%= svg_height %>"> <svg width="100%" height="<%= svg_height %>">
<%= render partial: "graph_background", locals: {background: @background} %> <%= render partial: "graph_background", locals: {background: @background} %>
<% number_of_columns = @graph.columns.length %> <% number_of_columns = @presenter.graph.columns.length %>
<% @graph.columns.each_with_index do |column, index| %> <% @presenter.graph.columns.each_with_index do |column, index| %>
<% p = column.new(measure: measure, school: @school, academic_years: @selected_academic_years, position: index , number_of_columns:) %> <% p = column.new(measure: measure, school: @school, academic_years: @presenter.selected_academic_years, position: index , number_of_columns:) %>
<%= render partial: "grouped_bar_column", locals: {column: p} %> <%= render partial: "grouped_bar_column", locals: {column: p} %>
<% end %> <% end %>

Before

Width:  |  Height:  |  Size: 463 B

After

Width:  |  Height:  |  Size: 493 B

@ -10,7 +10,7 @@
data-action="click->analyze#refresh" data-action="click->analyze#refresh"
<% empty_dataset = empty_dataset?(measures: measures, school: school, academic_year: year) %> <% empty_dataset = empty_dataset?(measures: measures, school: school, academic_year: year) %>
<% empty_survey_dataset = empty_survey_dataset?(measures: measures, school: school, academic_year: year) %> <% empty_survey_dataset = empty_survey_dataset?(measures: measures, school: school, academic_year: year) %>
<% if @graph.slug == 'all-data' %> <% if graph.slug == 'all-data' %>
<%= empty_dataset ? "disabled" : "" %> <%= empty_dataset ? "disabled" : "" %>
<% else %> <% else %>
<%= empty_survey_dataset ? "disabled" : "" %> <%= empty_survey_dataset ? "disabled" : "" %>
@ -18,13 +18,13 @@
> >
<label class="px-3" for="<%= year.range %>"><%= year.range %></label><br> <label class="px-3" for="<%= year.range %>"><%= year.range %></label><br>
<div class="bg-color-blue px-3" style="width:20px;height:20px;background-color:<%= colors[index] %>;"></div> <div class="bg-color-blue px-3" style="width:20px;height:20px;background-color:<%= colors[index] %>;"></div>
<% if @graph.slug == 'all-data' && empty_dataset %> <% if graph.slug == 'all-data' && empty_dataset %>
<i class="fa-solid fa-circle-exclamation px-3" <i class="fa-solid fa-circle-exclamation px-3"
data-bs-toggle="popover" data-bs-placement="right" data-bs-toggle="popover" data-bs-placement="right"
data-bs-content="No admin data OR teacher and student survey response rates below <%= ResponseRateCalculator::TEACHER_RATE_THRESHOLD %>%"> data-bs-content="No admin data OR teacher and student survey response rates below <%= ResponseRateCalculator::TEACHER_RATE_THRESHOLD %>%">
</i> </i>
<% end %> <% end %>
<% if @graph.slug != 'all-data' && empty_survey_dataset %> <% if graph.slug != 'all-data' && empty_survey_dataset %>
<i class="fa-solid fa-circle-exclamation px-3" <i class="fa-solid fa-circle-exclamation px-3"
data-bs-toggle="popover" data-bs-placement="right" data-bs-toggle="popover" data-bs-placement="right"
data-bs-content="Teacher and student survey response rates below <%= ResponseRateCalculator::TEACHER_RATE_THRESHOLD %>%"> data-bs-content="Teacher and student survey response rates below <%= ResponseRateCalculator::TEACHER_RATE_THRESHOLD %>%">

@ -3,19 +3,19 @@
<% end %> <% end %>
<div class="graph-content"> <div class="graph-content">
<div class="breadcrumbs sub-header-4"> <div class="breadcrumbs sub-header-4">
<%= @category.category_id %>:<%= @category.name %> > <%= @subcategory.subcategory_id %>:<%= @subcategory.name %> <%= @presenter.category.category_id %>:<%= @presenter.category.name %> > <%= @presenter.subcategory.subcategory_id %>:<%= @presenter.subcategory.name %>
</div> </div>
<hr> <hr>
</div> </div>
<div class="d-flex flex-row pt-5 row"> <div class="d-flex flex-row pt-5 row">
<div class="d-flex flex-column flex-grow-6 bg-color-white col-3 px-5" data-controller="analyze"> <div class="d-flex flex-column flex-grow-6 bg-color-white col-3 px-5" data-controller="analyze">
<%= render partial: "focus_area", locals: {categories: @categories, district: @district, school: @school, academic_year: @academic_year, category: @category, subcategories: @subcategories} %> <%= render partial: "focus_area", locals: {categories: @presenter.categories, district: @district, school: @school, academic_year: @academic_year, category: @presenter.category, subcategories: @presenter.subcategories} %>
<%= render partial: "school_years", locals: {available_academic_years: @available_academic_years, selected_academic_years: @selected_academic_years, district: @district, school: @school, academic_year: @academic_year, category: @category, subcategory: @subcategory, measures: @measures} %> <%= render partial: "school_years", locals: {available_academic_years: @presenter.academic_years, selected_academic_years: @presenter.selected_academic_years, district: @district, school: @school, academic_year: @academic_year, category: @presenter.category, subcategory: @presenter.subcategory, measures: @presenter.measures, graph: @presenter.graph} %>
<%= render partial: "data_filters", locals: {district: @district, school: @school, academic_year: @academic_year, category: @category, subcategory: @subcategory} %> <%= render partial: "data_filters", locals: {district: @district, school: @school, academic_year: @academic_year, category: @presenter.category, subcategory: @presenter.subcategory} %>
</div> </div>
<% cache [@subcategory, @school, @selected_academic_years, @graph, @selected_races, @race_score_timestamp, @selected_grades, @grades, @selected_genders, @genders] do %> <% cache [@presenter.subcategory, @school, @presenter.selected_academic_years, @presenter.graph, @presenter.selected_races, @presenter.race_score_timestamp, @presenter.selected_grades, @presenter.grades, @presenter.selected_genders, @presenter.genders] do %>
<div class="bg-color-white flex-grow-1 col-9"> <div class="bg-color-white flex-grow-1 col-9">
<% @measures.each do |measure| %> <% @presenter.measures.each do |measure| %>
<section class="mb-6"> <section class="mb-6">
<p class="construct-id">Measure <%= measure.measure_id %></p> <p class="construct-id">Measure <%= measure.measure_id %></p>
<h2> <%= measure.name %> </h2> <h2> <%= measure.name %> </h2>

@ -1,11 +1,11 @@
Race Qualtrics Code,Race/Ethnicity,Gender Qualtrics Code,Sex/Gender Race Qualtrics Code,Race/Ethnicity,Gender Qualtrics Code,Sex/Gender,Income
1,American Indian or Alaskan Native,2,Male 1,American Indian or Alaskan Native,2,Male,Economically Disadvantaged - N
2,Asian or Pacific Islander,1,Female 2,Asian or Pacific Islander,1,Female,Economically Disadvantaged - Y
3,Black or African American,4,Non-Binary 3,Black or African American,4,Non-Binary,Unknown
4,Hispanic or Latinx,99,Unknown 4,Hispanic or Latinx,99,Unknown,
5,White or Caucasian,, 5,White or Caucasian,,,
6,Prefer not to disclose,, 6,Prefer not to disclose,,,
7,Prefer to self-describe,, 7,Prefer to self-describe,,,
8,Middle Eastern,, 8,Middle Eastern,,,
99,Race/Ethnicity Not Listed,, 99,Race/Ethnicity Not Listed,,,
100,Multiracial,, 100,Multiracial,,,

1 Race Qualtrics Code Race/Ethnicity Gender Qualtrics Code Sex/Gender Income
2 1 American Indian or Alaskan Native 2 Male Economically Disadvantaged - N
3 2 Asian or Pacific Islander 1 Female Economically Disadvantaged - Y
4 3 Black or African American 4 Non-Binary Unknown
5 4 Hispanic or Latinx 99 Unknown
6 5 White or Caucasian
7 6 Prefer not to disclose
8 7 Prefer to self-describe
9 8 Middle Eastern
10 99 Race/Ethnicity Not Listed
11 100 Multiracial

@ -0,0 +1,9 @@
class CreateIncomes < ActiveRecord::Migration[7.0]
def change
create_table :incomes do |t|
t.string :designation
t.timestamps
end
end
end

@ -0,0 +1,5 @@
class AddIncomeToSurveyItemResponses < ActiveRecord::Migration[7.0]
def change
add_reference :survey_item_responses, :income, foreign_key: true
end
end

@ -0,0 +1,6 @@
class AddSlugToIncome < ActiveRecord::Migration[7.0]
def change
add_column :incomes, :slug, :string
add_index :incomes, :slug, unique: true
end
end

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_06_10_165508) do ActiveRecord::Schema[7.0].define(version: 20_230_630_215_110) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -76,6 +76,14 @@ ActiveRecord::Schema[7.0].define(version: 2023_06_10_165508) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
end end
create_table "incomes", force: :cascade do |t|
t.string "designation"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "slug"
t.index ["slug"], name: "index_incomes_on_slug", unique: true
end
create_table "legacy_attempts", id: :serial, force: :cascade do |t| create_table "legacy_attempts", id: :serial, force: :cascade do |t|
t.integer "recipient_id" t.integer "recipient_id"
t.integer "schedule_id" t.integer "schedule_id"
@ -344,7 +352,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_06_10_165508) do
t.integer "eleven" t.integer "eleven"
t.integer "twelve" t.integer "twelve"
t.index ["academic_year_id"], name: "index_respondents_on_academic_year_id" t.index ["academic_year_id"], name: "index_respondents_on_academic_year_id"
t.index ["school_id", "academic_year_id"], name: "index_respondents_on_school_id_and_academic_year_id", unique: true t.index %w[school_id academic_year_id], name: "index_respondents_on_school_id_and_academic_year_id", unique: true
end end
create_table "response_rates", force: :cascade do |t| create_table "response_rates", force: :cascade do |t|
@ -358,7 +366,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_06_10_165508) do
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["academic_year_id"], name: "index_response_rates_on_academic_year_id" t.index ["academic_year_id"], name: "index_response_rates_on_academic_year_id"
t.index ["school_id", "subcategory_id"], name: "index_response_rates_on_school_id_and_subcategory_id" t.index %w[school_id subcategory_id], name: "index_response_rates_on_school_id_and_subcategory_id"
t.index ["school_id"], name: "index_response_rates_on_school_id" t.index ["school_id"], name: "index_response_rates_on_school_id"
t.index ["subcategory_id"], name: "index_response_rates_on_subcategory_id" t.index ["subcategory_id"], name: "index_response_rates_on_subcategory_id"
end end
@ -411,7 +419,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_06_10_165508) do
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["race_id"], name: "index_student_races_on_race_id" t.index ["race_id"], name: "index_student_races_on_race_id"
t.index ["student_id", "race_id"], name: "index_student_races_on_student_id_and_race_id" t.index %w[student_id race_id], name: "index_student_races_on_student_id_and_race_id"
t.index ["student_id"], name: "index_student_races_on_student_id" t.index ["student_id"], name: "index_student_races_on_student_id"
end end
@ -448,9 +456,9 @@ ActiveRecord::Schema[7.0].define(version: 2023_06_10_165508) do
t.index ["academic_year_id"], name: "index_survey_item_responses_on_academic_year_id" t.index ["academic_year_id"], name: "index_survey_item_responses_on_academic_year_id"
t.index ["gender_id"], name: "index_survey_item_responses_on_gender_id" t.index ["gender_id"], name: "index_survey_item_responses_on_gender_id"
t.index ["response_id"], name: "index_survey_item_responses_on_response_id" t.index ["response_id"], name: "index_survey_item_responses_on_response_id"
t.index ["school_id", "academic_year_id", "survey_item_id"], name: "by_school_year_and_survey_item" t.index %w[school_id academic_year_id survey_item_id], name: "by_school_year_and_survey_item"
t.index ["school_id", "academic_year_id"], name: "index_survey_item_responses_on_school_id_and_academic_year_id" t.index %w[school_id academic_year_id], name: "index_survey_item_responses_on_school_id_and_academic_year_id"
t.index ["school_id", "survey_item_id", "academic_year_id", "grade"], name: "index_survey_responses_on_grade" t.index %w[school_id survey_item_id academic_year_id grade], name: "index_survey_responses_on_grade"
t.index ["school_id"], name: "index_survey_item_responses_on_school_id" t.index ["school_id"], name: "index_survey_item_responses_on_school_id"
t.index ["student_id"], name: "index_survey_item_responses_on_student_id" t.index ["student_id"], name: "index_survey_item_responses_on_student_id"
t.index ["survey_item_id"], name: "index_survey_item_responses_on_survey_item_id" t.index ["survey_item_id"], name: "index_survey_item_responses_on_survey_item_id"

@ -5,10 +5,10 @@ namespace :one_off do
qualtrics_schools = {} qualtrics_schools = {}
csv_file = Rails.root.join('data', 'master_list_of_schools_and_districts.csv') csv_file = Rails.root.join("data", "master_list_of_schools_and_districts.csv")
CSV.parse(File.read(csv_file), headers: true) do |row| CSV.parse(File.read(csv_file), headers: true) do |row|
district_id = row['District Code'].to_i district_id = row["District Code"].to_i
school_id = row['School Code'].to_i school_id = row["School Code"].to_i
if qualtrics_schools[[district_id, school_id]].present? if qualtrics_schools[[district_id, school_id]].present?
puts "Duplicate entry row #{row}" puts "Duplicate entry row #{row}"
@ -24,14 +24,14 @@ namespace :one_off do
end end
if school.nil? if school.nil?
school_name = csv_row['School Name'].strip school_name = csv_row["School Name"].strip
puts "Could not find school '#{school_name}' with district id: #{district_id}, school id: #{school_id}" puts "Could not find school '#{school_name}' with district id: #{district_id}, school id: #{school_id}"
potential_school_ids = School.where('name like ?', "%#{school_name}%").map(&:id) potential_school_ids = School.where("name like ?", "%#{school_name}%").map(&:id)
puts "Potential ID matches: #{potential_school_ids}" if potential_school_ids.present? puts "Potential ID matches: #{potential_school_ids}" if potential_school_ids.present?
next next
end end
school.update!(dese_id: csv_row['DESE School ID']) school.update!(dese_id: csv_row["DESE School ID"])
updated_schools << school.id updated_schools << school.id
end end
@ -40,7 +40,7 @@ namespace :one_off do
end end
end end
desc 'list scales that have no survey responses' desc "list scales that have no survey responses"
task list_scales_that_lack_survey_responses: :environment do task list_scales_that_lack_survey_responses: :environment do
output = AcademicYear.all.map do |academic_year| output = AcademicYear.all.map do |academic_year|
Scale.all.map do |scale| Scale.all.map do |scale|
@ -48,11 +48,11 @@ namespace :one_off do
end end
end end
output = output.map { |year| year.reject { |scale| scale[2] > 0 || scale[1].starts_with?('a-') } } output = output.map { |year| year.reject { |scale| scale[2] > 0 || scale[1].starts_with?("a-") } }
pp output pp output
end end
desc 'list survey_items that have no survey responses by district' desc "list survey_items that have no survey responses by district"
task list_survey_items_that_lack_responses: :environment do task list_survey_items_that_lack_responses: :environment do
output = AcademicYear.all.map do |academic_year| output = AcademicYear.all.map do |academic_year|
District.all.map do |district| District.all.map do |district|
@ -66,14 +66,14 @@ namespace :one_off do
output = output.map do |year| output = output.map do |year|
year.map do |district| year.map do |district|
district.reject do |survey_item| district.reject do |survey_item|
survey_item[2] > 0 || survey_item[1].starts_with?('a-') survey_item[2] > 0 || survey_item[1].starts_with?("a-")
end end
end end
end end
pp output pp output
end end
desc 'list the most recent admin data values' desc "list the most recent admin data values"
task list_recent_admin_data_values: :environment do task list_recent_admin_data_values: :environment do
range = 4.weeks.ago..1.second.ago range = 4.weeks.ago..1.second.ago
values = AdminDataValue.where(updated_at: range).group(:admin_data_item).count.map do |item| values = AdminDataValue.where(updated_at: range).group(:admin_data_item).count.map do |item|
@ -82,49 +82,49 @@ namespace :one_off do
puts values puts values
end end
desc 'load survey responses for lowell schools' desc "load survey responses for lowell schools"
task load_survey_responses_for_lowell: :environment do task load_survey_responses_for_lowell: :environment do
survey_item_response_count = SurveyItemResponse.count survey_item_response_count = SurveyItemResponse.count
student_count = Student.count student_count = Student.count
Sftp::Directory.open(path: '/test/survey_responses/') do |file| Sftp::Directory.open(path: "/test/survey_responses/") do |file|
SurveyResponsesDataLoader.from_file(file:) SurveyResponsesDataLoader.from_file(file:)
end end
puts "=====================> Completed loading #{SurveyItemResponse.count - survey_item_response_count} survey responses. #{SurveyItemResponse.count} total responses in the database" puts "=====================> Completed loading #{SurveyItemResponse.count - survey_item_response_count} survey responses. #{SurveyItemResponse.count} total responses in the database"
Sftp::Directory.open(path: '/test/survey_responses/') do |file| Sftp::Directory.open(path: "/test/survey_responses/") do |file|
StudentLoader.from_file(file:, rules: [Rule::SkipNonLowellSchools]) StudentLoader.from_file(file:, rules: [Rule::SkipNonLowellSchools])
end end
puts "=====================> Completed loading #{Student.count - student_count} students. #{Student.count} total students" puts "=====================> Completed loading #{Student.count - student_count} students. #{Student.count} total students"
puts 'Resetting race scores' puts "Resetting race scores"
RaceScoreLoader.reset(fast_processing: false) RaceScoreLoader.reset(fast_processing: false)
puts "=====================> Completed loading #{RaceScore.count} race scores" puts "=====================> Completed loading #{RaceScore.count} race scores"
Rails.cache.clear Rails.cache.clear
end end
desc 'delete 2022-23 survey responses' desc "delete 2022-23 survey responses"
task delete_survey_responses_2022_23: :environment do task delete_survey_responses_2022_23: :environment do
response_count = SurveyItemResponse.all.count response_count = SurveyItemResponse.all.count
SurveyItemResponse.where(academic_year: AcademicYear.find_by_range('2022-23')).delete_all SurveyItemResponse.where(academic_year: AcademicYear.find_by_range("2022-23")).delete_all
puts "=====================> Deleted #{response_count - SurveyItemResponse.all.count} survey responses" puts "=====================> Deleted #{response_count - SurveyItemResponse.all.count} survey responses"
# should be somewhere near 295738 # should be somewhere near 295738
end end
desc 'delete 2023-24 survey responses' desc "delete 2023-24 survey responses"
task delete_survey_responses_2023_24: :environment do task delete_survey_responses_2023_24: :environment do
response_count = SurveyItemResponse.all.count response_count = SurveyItemResponse.all.count
SurveyItemResponse.where(academic_year: AcademicYear.find_by_range('2023-24')).delete_all SurveyItemResponse.where(academic_year: AcademicYear.find_by_range("2023-24")).delete_all
puts "=====================> Deleted #{response_count - SurveyItemResponse.all.count} survey responses" puts "=====================> Deleted #{response_count - SurveyItemResponse.all.count} survey responses"
end end
desc 'load survey responses for lowell schools 2022-23' desc "load survey responses for lowell schools 2022-23"
task load_survey_responses_for_lowell_2022_23: :environment do task load_survey_responses_for_lowell_2022_23: :environment do
survey_item_response_count = SurveyItemResponse.count survey_item_response_count = SurveyItemResponse.count
student_count = Student.count student_count = Student.count
path = '/data/survey_responses/2022-23/' path = "/data/survey_responses/2022-23/"
Sftp::Directory.open(path:) do |file| Sftp::Directory.open(path:) do |file|
SurveyResponsesDataLoader.from_file(file:) SurveyResponsesDataLoader.from_file(file:)
@ -136,60 +136,8 @@ namespace :one_off do
end end
puts "=====================> Completed loading #{Student.count - student_count} students. #{Student.count} total students" puts "=====================> Completed loading #{Student.count - student_count} students. #{Student.count} total students"
puts 'Resetting race scores' puts "Resetting race scores"
RaceScoreLoader.reset(fast_processing: false, academic_years: [AcademicYear.find_by_range('2022-23')]) RaceScoreLoader.reset(fast_processing: false, academic_years: [AcademicYear.find_by_range("2022-23")])
puts "=====================> Completed loading #{RaceScore.count} race scores"
Rails.cache.clear
end
desc 'delete errant response'
task delete_responses: :environment do
SurveyItemResponse.where(response_id: 'R_diYAw7qOj4W1UZ3').delete_all
SurveyItemResponse.where(response_id: 'R_27fKhVfyeKGMF5q').delete_all
SurveyItemResponse.where(response_id: 'R_2cjPX1Ngxr2Hc4c').delete_all
end
desc 'upload spring survey results to 23-24'
task upload_spring23: :environment do
new_files = Array.new
input_filepath = Rails.root.join('tmp', 'data', 'rpp_data', 'clean')
Dir.foreach(input_filepath) do |filename|
next if filename.start_with?('.') # skip hidden files and ./.. directories
# this can probably be replaced with Dir.join or similar
input_filename = Rails.root.join('tmp', 'data', 'rpp_data', 'clean', filename).to_s
sftptogo_url = ENV['SFTPTOGO_URL']
uri = URI.parse(sftptogo_url)
Net::SFTP.start(uri.host, uri.user, password: uri.password) do |sftp|
puts "Uploading #{filename}..."
sftp.upload!(input_filename, "/data/survey_responses/2023-24/#{filename}")
end
new_files.append(filename)
end
# print remote directory contents with new files marked
path = '/data/survey_responses/2023-24/'
Sftp::Directory.open(path:) do |file|
# the open method already prints all the contents...
end
end
desc 'load spring survey responses'
task load_spring_survey_responses: :environment do
survey_item_response_count = SurveyItemResponse.count
student_count = Student.count
path = '/data/survey_responses/2023-24/'
Sftp::Directory.open(path:) do |file|
SurveyResponsesDataLoader.from_file(file:)
end
puts "=====================> Completed loading #{SurveyItemResponse.count - survey_item_response_count} survey responses"
Sftp::Directory.open(path:) do |file|
StudentLoader.from_file(file:, rules: [Rule::SkipNonLowellSchools])
end
puts "=====================> Completed loading #{Student.count - student_count} students. #{Student.count} total students"
puts 'Resetting race scores'
RaceScoreLoader.reset(fast_processing: false)
puts "=====================> Completed loading #{RaceScore.count} race scores" puts "=====================> Completed loading #{RaceScore.count} race scores"
Rails.cache.clear Rails.cache.clear

@ -21,7 +21,7 @@
"debounce": "^1.2.1", "debounce": "^1.2.1",
"esbuild": "^0.18.12", "esbuild": "^0.18.12",
"sass": "^1.43.4", "sass": "^1.43.4",
"semver": "6.3.1" "semver": "^7.5.2"
}, },
"scripts": { "scripts": {
"build": "esbuild app/javascript/*.* --bundle --outdir=app/assets/builds", "build": "esbuild app/javascript/*.* --bundle --outdir=app/assets/builds",

@ -1,4 +1,8 @@
FactoryBot.define do FactoryBot.define do
factory :income do
designation { "MyString" }
end
factory :gender do factory :gender do
qualtrics_code { 1 } qualtrics_code { 1 }
designation { 'MyString' } designation { 'MyString' }

@ -1,11 +1,11 @@
Race Qualtrics Code,Race/Ethnicity,Gender Qualtrics Code,Sex/Gender Race Qualtrics Code,Race/Ethnicity,Gender Qualtrics Code,Sex/Gender,Income
1,American Indian or Alaskan Native,2,Male 1,American Indian or Alaskan Native,2,Male,Economically Disadvantaged N
2,Asian or Pacific Islander,1,Female 2,Asian or Pacific Islander,1,Female,Economically Disadvantaged Y
3,Black or African American,4,Non-Binary 3,Black or African American,4,Non-Binary,Unknown
4,Hispanic or Latinx,99,Unknown 4,Hispanic or Latinx,99,Unknown,
5,White or Caucasian,, 5,White or Caucasian,,,
6,Prefer not to disclose,, 6,Prefer not to disclose,,,
7,Prefer to self-describe,, 7,Prefer to self-describe,,,
8,Middle Eastern,, 8,Middle Eastern,,,
99,Race/Ethnicity Not Listed,, 99,Race/Ethnicity Not Listed,,,
100,Multiracial,, 100,Multiracial,,,

1 Race Qualtrics Code Race/Ethnicity Gender Qualtrics Code Sex/Gender Income
2 1 American Indian or Alaskan Native 2 Male Economically Disadvantaged – N
3 2 Asian or Pacific Islander 1 Female Economically Disadvantaged – Y
4 3 Black or African American 4 Non-Binary Unknown
5 4 Hispanic or Latinx 99 Unknown
6 5 White or Caucasian
7 6 Prefer not to disclose
8 7 Prefer to self-describe
9 8 Middle Eastern
10 99 Race/Ethnicity Not Listed
11 100 Multiracial

@ -1,9 +1,9 @@
Start Date,End Date,Response Type,IP Address,Progress,Duration (in seconds),Finished,RecordedDate,ResponseId,LASID,Recipient Last Name,Recipient First Name,Recipient Email,External Data Reference,Location Latitude,Location Longitude,Distribution Channel,User Language,district,school,DESE ID,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,s-emsa-q1,s-emsa-q2,s-emsa-q3,s-tint-q1,s-tint-q2,#N/A,s-tint-q4,s-tint-q5,s-acpr-q1,s-acpr-q2,s-acpr-q3,s-acpr-q4,#N/A,#N/A,s-cure-q3,s-cure-q4,#N/A,s-sten-q2,s-sten-q3,s-sper-q1,s-sper-q2,s-sper-q3,s-sper-q4,s-civp-q1,s-civp-q2,s-civp-q3,s-civp-q4,s-grmi-q1,#N/A,#N/A,s-grmi-q4,s-appa-q1,s-appa-q2,#N/A,s-peff-q1,s-peff-q2,s-peff-q3,s-peff-q4,s-peff-q5,s-peff-q6,s-sbel-q1,s-sbel-q2,s-sbel-q3,s-sbel-q4,s-sbel-q5,s-phys-q1,s-phys-q1-1,s-phys-q2,s-phys-q3,s-phys-q4,s-vale-q1,s-vale-q2,s-vale-q3,s-vale-q4,s-acst-q1,s-acst-q2,s-acst-q3,s-acst-q4,s-acst-q5,s-grit-q1,s-grit-q2,s-grit-q3,s-grit-q4,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,race,What is your race/ethnicity?(Please select all that apply) - Selected Choice,grade,gender Start Date,End Date,Response Type,IP Address,Progress,Duration (in seconds),Finished,RecordedDate,ResponseId,LASID,Recipient Last Name,Recipient First Name,Recipient Email,External Data Reference,Location Latitude,Location Longitude,Distribution Channel,User Language,district,school,DESE ID,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,s-emsa-q1,s-emsa-q2,s-emsa-q3,s-tint-q1,s-tint-q2,#N/A,s-tint-q4,s-tint-q5,s-acpr-q1,s-acpr-q2,s-acpr-q3,s-acpr-q4,#N/A,#N/A,s-cure-q3,s-cure-q4,#N/A,s-sten-q2,s-sten-q3,s-sper-q1,s-sper-q2,s-sper-q3,s-sper-q4,s-civp-q1,s-civp-q2,s-civp-q3,s-civp-q4,s-grmi-q1,#N/A,#N/A,s-grmi-q4,s-appa-q1,s-appa-q2,#N/A,s-peff-q1,s-peff-q2,s-peff-q3,s-peff-q4,s-peff-q5,s-peff-q6,s-sbel-q1,s-sbel-q2,s-sbel-q3,s-sbel-q4,s-sbel-q5,s-phys-q1,s-phys-q1-1,s-phys-q2,s-phys-q3,s-phys-q4,s-vale-q1,s-vale-q2,s-vale-q3,s-vale-q4,s-acst-q1,s-acst-q2,s-acst-q3,s-acst-q4,s-acst-q5,s-grit-q1,s-grit-q2,s-grit-q3,s-grit-q4,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,#N/A,race,What is your race/ethnicity?(Please select all that apply) - Selected Choice,grade,gender,Raw Income,Income
2020-09-29 18:28:41,2020-09-29 18:48:28,0,73.249.89.226,6,1186,0,2020-09-30T18:48:50,student_survey_response_1,123456,,,,,,,anonymous,EN,1,8,1500025,,,,dddd,4,3,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,3,0,some non-integer response,6,,,,5,,,1,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,EN,,,,,1,888,11th,1 2020-09-29 18:28:41,2020-09-29 18:48:28,0,73.249.89.226,6,1186,0,2020-09-30T18:48:50,student_survey_response_1,123456,,,,,,,anonymous,EN,1,8,1500025,,,,dddd,4,3,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,3,0,some non-integer response,6,,,,5,,,1,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,EN,,,,,1,888,11th,1,Free Lunch,Economically Disadvantaged Y
2021-02-23 15:12:58,2021-02-23 15:13:17,0,50.207.254.114,0,19,0,2021-02-24T15:13:19,student_survey_response_2,234567,,,,,,,anonymous,EN,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,NA,,,,,,,,,,,,,,,,,,,,,EN,,,,,"2,3,4",888,10, 2021-02-23 15:12:58,2021-02-23 15:13:17,0,50.207.254.114,0,19,0,2021-02-24T15:13:19,student_survey_response_2,234567,,,,,,,anonymous,EN,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,NA,,,,,,,,,,,,,,,,,,,,,EN,,,,,"2,3,4",888,10,,Not Eligible,Economically Disadvantaged N
2021-03-31 9:50:19,2021-03-31 9:59:01,0,108.7.17.250,100,522,1,2021-03-31T09:59:02,student_survey_response_3,345678,,,,,42.53340149,-70.96530151,anonymous,EN,3,2,1500505,12,4,108,3300,7,1,,,,,,,,,,,,,,2,4,2,1,4,3,3,,,,,3,3,3,3,,,,,NA,,,,,,,,,3,2,3,3,2,1,3,3,4,1,3,3,4,4,2,4,3,3,4,3,3,3,4,3,3,3,3,3,,,,,,,,,,3,4,4,2,3,3,1,,3,,EN,Math teacher,,,,6,888,8,2 2021-03-31 9:50:19,2021-03-31 9:59:01,0,108.7.17.250,100,522,1,2021-03-31T09:59:02,student_survey_response_3,345678,,,,,42.53340149,-70.96530151,anonymous,EN,3,2,1500505,12,4,108,3300,7,1,,,,,,,,,,,,,,2,4,2,1,4,3,3,,,,,3,3,3,3,,,,,NA,,,,,,,,,3,2,3,3,2,1,3,3,4,1,3,3,4,4,2,4,3,3,4,3,3,3,4,3,3,3,3,3,,,,,,,,,,3,4,4,2,3,3,1,,3,,EN,Math teacher,,,,6,888,8,2,Reduced Lunch,Economically Disadvantaged Y
2021-03-31 9:50:09,2021-03-31 10:00:16,0,67.186.188.168,100,607,1,2021-03-31T10:00:17,student_survey_response_4,456789,,,,,42.63510132,-71.30139923,anonymous,EN,3,2,1500505,12,18,108,2064,7,1,,2,2,1,,,,,,,,,,,,,,,,,,,,,,,,,3,5,3,3,,,,,,,,,,4,4,3,4,5,1,,1,5,1,3,2,4,4,1,2,1,3,2,3,3,3,4,2,5,3,4,5,5,3,3,4,3,,,,,4,4,4,4,3,5,2,,2,,EN,,,,English teacher,7,888,8,3 2021-03-31 9:50:09,2021-03-31 10:00:16,0,67.186.188.168,100,607,1,2021-03-31T10:00:17,student_survey_response_4,456789,,,,,42.63510132,-71.30139923,anonymous,EN,3,2,1500505,12,18,108,2064,7,1,,2,2,1,,,,,,,,,,,,,,,,,,,,,,,,,3,5,3,3,,,,,,,,,,4,4,3,4,5,1,,1,5,1,3,2,4,4,1,2,1,3,2,3,3,3,4,2,5,3,4,5,5,3,3,4,3,,,,,4,4,4,4,3,5,2,,2,,EN,,,,English teacher,7,888,8,3,,Unknown
2021-03-31 9:51:39,2021-03-31 10:01:36,0,73.47.153.77,100,596,1,2021-03-31T10:01:36,student_survey_response_5,567890,,,,,42.65820313,-71.30580139,anonymous,EN,3,2,1500505,6,15,109,3710,7,1,,2,2,2,,,,,,,,,,3,3,4,3,3,3,3,4,3,4,3,4,4,5,4,3,4,3,5,2,2,3,,,,,,,,,,,,1,,2,5,1,3,3,2,4,3,5,4,,,,,,,,,,,,5,4,3,4,4,4,4,4,4,,,,,,,2,,2,,EN,,,Social Studies teacher,,"1,2,3,4,5,8,6,7",888,7,4 2021-03-31 9:51:39,2021-03-31 10:01:36,0,73.47.153.77,100,596,1,2021-03-31T10:01:36,student_survey_response_5,567890,,,,,42.65820313,-71.30580139,anonymous,EN,3,2,1500505,6,15,109,3710,7,1,,2,2,2,,,,,,,,,,3,3,4,3,3,3,3,4,3,4,3,4,4,5,4,3,4,3,5,2,2,3,,,,,,,,,,,,1,,2,5,1,3,3,2,4,3,5,4,,,,,,,,,,,,5,4,3,4,4,4,4,4,4,,,,,,,2,,2,,EN,,,Social Studies teacher,,"1,2,3,4,5,8,6,7",888,7,4,Free Lunch,Economically Disadvantaged Y
2021-03-31 9:51:39,2021-03-31 10:01:36,0,73.47.153.77,100,596,1,2021-03-31T10:01:37,student_survey_response_6,,,,,,42.65820313,-71.30580139,anonymous,EN,3,2,1500505,6,15,109,3710,7,1,,2,2,2,,,,,,,,,,3,3,4,3,3,3,3,4,3,4,3,4,4,5,4,3,4,3,5,2,2,3,,,,,,,,,,,,1,,2,5,1,3,3,2,4,3,5,4,,,,,,,,,,,,5,4,3,4,4,4,4,4,4,,,,,,,2,,2,,EN,,,Social Studies teacher,,"1,2,3,4,5,8",888,3,NA 2021-03-31 9:51:39,2021-03-31 10:01:36,0,73.47.153.77,100,596,1,2021-03-31T10:01:36,student_survey_response_6,,,,,,42.65820313,-71.30580139,anonymous,EN,3,2,1500505,6,15,109,3710,7,1,,2,2,2,,,,,,,,,,3,3,4,3,3,3,3,4,3,4,3,4,4,5,4,3,4,3,5,2,2,3,,,,,,,,,,,,1,,2,5,1,3,3,2,4,3,5,4,,,,,,,,,,,,5,4,3,4,4,4,4,4,4,,,,,,,2,,2,,EN,,,Social Studies teacher,,"1,2,3,4,5,8",888,3,NA,Not Eligible,Economically Disadvantaged N
2021-03-31 9:51:39,2021-03-31 10:01:36,0,73.47.153.77,100,596,1,2021-03-31T10:01:38,student_survey_response_7,,,,,,42.65820313,-71.30580139,anonymous,EN,3,2,1500505,6,15,109,3710,7,1,,2,2,2,,,,,,,,,,3,3,4,3,3,3,3,4,3,4,3,4,4,5,4,3,4,3,5,2,2,3,,,,,,,,,,,,1,,2,5,1,3,3,2,4,3,5,4,,,,,,,,,,,,5,4,3,4,4,4,4,4,4,,,,,,,2,,2,,EN,,,Social Studies teacher,,,,4, 2021-03-31 9:51:39,2021-03-31 10:01:36,0,73.47.153.77,100,596,1,2021-03-31T10:01:36,student_survey_response_7,,,,,,42.65820313,-71.30580139,anonymous,EN,3,2,1500505,6,15,109,3710,7,1,,2,2,2,,,,,,,,,,3,3,4,3,3,3,3,4,3,4,3,4,4,5,4,3,4,3,5,2,2,3,,,,,,,,,,,,1,,2,5,1,3,3,2,4,3,5,4,,,,,,,,,,,,5,4,3,4,4,4,4,4,4,,,,,,,2,,2,,EN,,,Social Studies teacher,,,,4,,Reduced Lunch,Economically Disadvantaged Y
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"1,2,3,4,5,8",,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"1,2,3,4,5,8",,,,,

1 Start Date End Date Response Type IP Address Progress Duration (in seconds) Finished RecordedDate ResponseId LASID Recipient Last Name Recipient First Name Recipient Email External Data Reference Location Latitude Location Longitude Distribution Channel User Language district school DESE ID #N/A #N/A #N/A #N/A #N/A #N/A #N/A s-emsa-q1 s-emsa-q2 s-emsa-q3 s-tint-q1 s-tint-q2 #N/A s-tint-q4 s-tint-q5 s-acpr-q1 s-acpr-q2 s-acpr-q3 s-acpr-q4 #N/A #N/A s-cure-q3 s-cure-q4 #N/A s-sten-q2 s-sten-q3 s-sper-q1 s-sper-q2 s-sper-q3 s-sper-q4 s-civp-q1 s-civp-q2 s-civp-q3 s-civp-q4 s-grmi-q1 #N/A #N/A s-grmi-q4 s-appa-q1 s-appa-q2 #N/A s-peff-q1 s-peff-q2 s-peff-q3 s-peff-q4 s-peff-q5 s-peff-q6 s-sbel-q1 s-sbel-q2 s-sbel-q3 s-sbel-q4 s-sbel-q5 s-phys-q1 s-phys-q1-1 s-phys-q2 s-phys-q3 s-phys-q4 s-vale-q1 s-vale-q2 s-vale-q3 s-vale-q4 s-acst-q1 s-acst-q2 s-acst-q3 s-acst-q4 s-acst-q5 s-grit-q1 s-grit-q2 s-grit-q3 s-grit-q4 #N/A #N/A #N/A #N/A #N/A #N/A #N/A #N/A #N/A #N/A #N/A #N/A #N/A #N/A #N/A #N/A #N/A #N/A #N/A #N/A #N/A #N/A #N/A #N/A #N/A #N/A #N/A #N/A #N/A race What is your race/ethnicity?(Please select all that apply) - Selected Choice grade gender Raw Income Income
2 2020-09-29 18:28:41 2020-09-29 18:48:28 0 73.249.89.226 6 1186 0 2020-09-30T18:48:50 student_survey_response_1 123456 anonymous EN 1 8 1500025 dddd 4 3 3 0 some non-integer response 6 5 1 EN 1 888 11th 1 Free Lunch Economically Disadvantaged – Y
3 2021-02-23 15:12:58 2021-02-23 15:13:17 0 50.207.254.114 0 19 0 2021-02-24T15:13:19 student_survey_response_2 234567 anonymous EN NA EN 2,3,4 888 10 Not Eligible Economically Disadvantaged – N
4 2021-03-31 9:50:19 2021-03-31 9:59:01 0 108.7.17.250 100 522 1 2021-03-31T09:59:02 student_survey_response_3 345678 42.53340149 -70.96530151 anonymous EN 3 2 1500505 12 4 108 3300 7 1 2 4 2 1 4 3 3 3 3 3 3 NA 3 2 3 3 2 1 3 3 4 1 3 3 4 4 2 4 3 3 4 3 3 3 4 3 3 3 3 3 3 4 4 2 3 3 1 3 EN Math teacher 6 888 8 2 Reduced Lunch Economically Disadvantaged – Y
5 2021-03-31 9:50:09 2021-03-31 10:00:16 0 67.186.188.168 100 607 1 2021-03-31T10:00:17 student_survey_response_4 456789 42.63510132 -71.30139923 anonymous EN 3 2 1500505 12 18 108 2064 7 1 2 2 1 3 5 3 3 4 4 3 4 5 1 1 5 1 3 2 4 4 1 2 1 3 2 3 3 3 4 2 5 3 4 5 5 3 3 4 3 4 4 4 4 3 5 2 2 EN English teacher 7 888 8 3 Unknown
6 2021-03-31 9:51:39 2021-03-31 10:01:36 0 73.47.153.77 100 596 1 2021-03-31T10:01:36 student_survey_response_5 567890 42.65820313 -71.30580139 anonymous EN 3 2 1500505 6 15 109 3710 7 1 2 2 2 3 3 4 3 3 3 3 4 3 4 3 4 4 5 4 3 4 3 5 2 2 3 1 2 5 1 3 3 2 4 3 5 4 5 4 3 4 4 4 4 4 4 2 2 EN Social Studies teacher 1,2,3,4,5,8,6,7 888 7 4 Free Lunch Economically Disadvantaged – Y
7 2021-03-31 9:51:39 2021-03-31 10:01:36 0 73.47.153.77 100 596 1 2021-03-31T10:01:37 2021-03-31T10:01:36 student_survey_response_6 42.65820313 -71.30580139 anonymous EN 3 2 1500505 6 15 109 3710 7 1 2 2 2 3 3 4 3 3 3 3 4 3 4 3 4 4 5 4 3 4 3 5 2 2 3 1 2 5 1 3 3 2 4 3 5 4 5 4 3 4 4 4 4 4 4 2 2 EN Social Studies teacher 1,2,3,4,5,8 888 3 NA Not Eligible Economically Disadvantaged – N
8 2021-03-31 9:51:39 2021-03-31 10:01:36 0 73.47.153.77 100 596 1 2021-03-31T10:01:38 2021-03-31T10:01:36 student_survey_response_7 42.65820313 -71.30580139 anonymous EN 3 2 1500505 6 15 109 3710 7 1 2 2 2 3 3 4 3 3 3 3 4 3 4 3 4 4 5 4 3 4 3 5 2 2 3 1 2 5 1 3 3 2 4 3 5 4 5 4 3 4 4 4 4 4 4 2 2 EN Social Studies teacher 4 Reduced Lunch Economically Disadvantaged – Y
9 1,2,3,4,5,8

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe Income, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end

@ -0,0 +1,468 @@
require 'rails_helper'
describe Analyze::Presenter do
let(:category) { create(:category, category_id: '1', name: 'first category') }
let(:category_2) { create(:category, category_id: '2', name: 'second category') }
let(:wrong_category) { create(:category, category_id: '999', name: 'wrong category') }
let(:subcategory) { create(:subcategory, subcategory_id: '1A', name: 'first subcategory', category:) }
let(:subcategory_2) { create(:subcategory, subcategory_id: '2A', name: 'second subcategory', category: category_2) }
let(:wrong_subcategory) do
create(:subcategory, subcategory_id: '99A', name: 'wrong subcategory', category: wrong_category)
end
let(:measure) { create(:measure, measure_id: '1A-i', name: 'first measure', subcategory:) }
let(:measure_2) { create(:measure, measure_id: '2A-i', name: 'second measure', subcategory: subcategory_2) }
let(:wrong_measure) do
create(:wrong_measure, measure_id: '99A', name: 'wrong measure', subcategory: wrong_subcategory)
end
let(:scale) { create(:student_scale, measure:) }
let(:survey_item) { create(:student_survey_item, scale:) }
let(:school) { create(:school) }
let(:academic_year) { create(:academic_year) }
let(:ay_2021_22) { create(:academic_year, range: '2021-22') }
let(:ay_2022_23) { create(:academic_year, range: '2022-23') }
let(:wrong_ay) { create(:academic_year, range: '9999-99') }
let(:black) { create(:race, designation: 'black', qualtrics_code: 1) }
let(:white) { create(:race, designation: 'white', qualtrics_code: 2) }
let(:wrong_race) { create(:race, designation: 'wrong race', qualtrics_code: 9999) }
let(:female) { create(:gender, designation: 'female', qualtrics_code: 1) }
let(:male) { create(:gender, designation: 'male', qualtrics_code: 2) }
let(:wrong_gender) { create(:gender, designation: 'wrong_gender', qualtrics_code: 2) }
context '.category' do
before :each do
category
category_2
wrong_category
end
context 'when a category is provided in the params hash' do
it 'returns the category with the given id' do
params = { category: '1' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.category).to eq category
params = { category: '2' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.category).to eq category_2
end
end
context 'when no category is provided in the params hash' do
it 'returns the first category' do
params = {}
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.category).to eq category
end
end
context 'when a category that doesnt exist in the database is passed' do
it 'returns the first category' do
params = { category: '4' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.category).to eq category
end
end
end
context '.categories' do
it 'returns all categories' do
params = {}
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.categories).to eq Category.all.order(:category_id)
end
end
context '.subcategory' do
before :each do
category
category_2
wrong_category
subcategory
subcategory_2
wrong_subcategory
end
context 'when a subcategory is provided in the params hash' do
it 'returns the subcategory with the given id' do
params = { subcategory: '1A' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.subcategory).to eq subcategory
params = { subcategory: '2A' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.subcategory).to eq subcategory_2
end
end
context 'when no subcategory is provided in the params hash' do
it 'returns the first subcategory' do
params = {}
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.subcategory).to eq subcategory
end
end
context 'when a subcategory that doesnt exist in the database is passed' do
it 'returns the first subcategory' do
params = { subcategory: '4A' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.subcategory).to eq subcategory
end
end
end
context '.subcategories' do
before :each do
category
wrong_category
subcategory
wrong_subcategory
end
it 'returns all subcategories for a given category' do
params = { category: '1' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.subcategories).to eq category.subcategories.all.order(:subcategory_id)
end
end
context '.measures' do
before :each do
category
subcategory
measure
end
it 'returns all measures for a given subcategory' do
params = { category: '1' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.measures).to eq category.subcategories.flat_map(&:measures)
end
end
context '.academic_years' do
before :each do
ay_2021_22
ay_2022_23
end
it 'returns all academic years' do
params = {}
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.academic_years).to eq AcademicYear.all.order(:range)
end
end
context '.selected_academic_years' do
before :each do
ay_2021_22
ay_2022_23
wrong_ay
end
context 'when no academic years are provided in the params hash' do
it 'returns an empty array if no params are provided' do
params = {}
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.selected_academic_years).to eq []
end
end
context 'when a single academic year is provided in the params hash' do
it 'returns the academic year with the given id' do
params = { academic_years: '2021-22' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.selected_academic_years).to eq [AcademicYear.find_by_range('2021-22')]
params = { academic_years: '2022-23' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.selected_academic_years).to eq [AcademicYear.find_by_range('2022-23')]
end
end
context 'when multiple academic years are provided in the params hash' do
it 'returns the academic year with the given ids' do
params = { academic_years: '2021-22,2022-23' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.selected_academic_years).to eq [AcademicYear.find_by_range('2021-22'),
AcademicYear.find_by_range('2022-23')]
end
end
end
context '.races' do
before :each do
black
white
wrong_race
end
it 'returns all races' do
params = {}
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.races).to eq Race.all.order(designation: :ASC)
end
end
context '.selected_races' do
before :each do
black
white
wrong_race
end
context 'when no races are provided in the params hash' do
it 'returns an array with all races if no params are provided' do
params = {}
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.selected_races).to eq presenter.races
end
end
context 'when one race is provided in the params hash' do
it 'returns a single race with the given slug' do
params = { races: 'white' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.selected_races).to eq [Race.find_by_slug('white')]
params = { races: 'black' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.selected_races).to eq [Race.find_by_slug('black')]
end
end
context 'when multiple races are provided in the params hash' do
it 'returns multiple races with the given slugs' do
params = { races: 'white,black' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.selected_races).to eq [Race.find_by_slug('white'), Race.find_by_slug('black')]
end
end
end
context '.graph' do
context 'when no params are provided' do
it 'returns the default(first) graph object' do
params = {}
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.graph.to_s).to eq Analyze::Graph::AllData.new.to_s
end
end
context 'when a graph is provided in the params hash' do
it 'returns the graph object with the given slug' do
params = { graph: 'all_data' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.graph.to_s).to eq Analyze::Graph::AllData.new.to_s
params = { graph: 'students-and-teachers' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.graph.to_s).to eq Analyze::Graph::StudentsAndTeachers.new.to_s
params = { graph: 'students-by-race' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.graph.to_s).to eq Analyze::Graph::StudentsByRace.new(races: nil).to_s
params = { graph: 'students-by-grade' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.graph.to_s).to eq Analyze::Graph::StudentsByGrade.new(grades: nil).to_s
params = { graph: 'students-by-gender' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.graph.to_s).to eq Analyze::Graph::StudentsByGender.new(genders: nil).to_s
end
end
context 'when a parameter that does not match a graph is provided' do
it 'returns the default(first) graph object' do
params = { graph: 'invalid parameter' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.graph.to_s).to eq Analyze::Graph::AllData.new.to_s
end
end
end
context '.grades' do
before :each do
school
academic_year
create_list(:survey_item_response, 20, school:, academic_year:, grade: 1, survey_item:)
create_list(:survey_item_response, 20, school:, academic_year:, grade: 2, survey_item:)
create_list(:survey_item_response, 20, school:, academic_year:, grade: 3, survey_item:)
create_list(:survey_item_response, 20, school:, academic_year:, grade: 4, survey_item:)
end
context 'when no grades are provided in the params hash' do
it 'returns the set of grades for which there are more than ten responses' do
params = {}
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.grades).to eq [1, 2, 3, 4]
end
end
end
context '.selected_grades' do
before :each do
school
academic_year
create_list(:survey_item_response, 20, school:, academic_year:, grade: 1, survey_item:)
create_list(:survey_item_response, 20, school:, academic_year:, grade: 2, survey_item:)
create_list(:survey_item_response, 20, school:, academic_year:, grade: 3, survey_item:)
create_list(:survey_item_response, 20, school:, academic_year:, grade: 4, survey_item:)
end
context 'when no grades are provided in the params hash' do
it 'returns only the set of grades selected even if other grades have sufficient responses' do
params = { grades: '1,2,3' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.selected_grades).to eq [1, 2, 3]
end
end
end
context '.selected_genders' do
before :each do
male
female
wrong_gender
end
context 'when no parameters are provided' do
it 'returns all genders' do
params = {}
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.selected_genders).to eq Gender.all
end
end
context 'when a single gender is provided in the params hash' do
it 'returns the gender with the given designation' do
params = { genders: 'female' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.selected_genders).to eq [Gender.find_by_designation('female')]
params = { genders: 'male' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.selected_genders).to eq [Gender.find_by_designation('male')]
end
end
context 'when multiple genders are provided in the params hash' do
it 'returns multilple genders with the given designations' do
params = { genders: 'female,male' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.selected_genders).to eq [Gender.find_by_designation('female'),
Gender.find_by_designation('male')]
end
end
end
context '.genders' do
before :each do
female
male
end
it 'returns all genders' do
params = {}
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.genders).to eq Gender.all
end
end
context '.group' do
context 'when no parameters are provided' do
it 'returns the first item in the list of groups' do
params = {}
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.group.slug).to eq presenter.groups.first.slug
end
end
context 'when a group is provided in the params hash' do
it 'returns the group with the given slug' do
params = { group: 'gender' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.group.slug).to eq 'gender'
params = { group: 'grade' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.group.slug).to eq 'grade'
params = { group: 'race' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.group.slug).to eq 'race'
# Not yet implemented
# params = { group: 'income' }
# presenter = Analyze::Presenter.new(params:, school:, academic_year:)
# expect(presenter.group.slug).to eq 'income'
end
end
context 'when a parameter that does not match a group is provided' do
it 'returns the first item in the list of groups' do
params = { group: 'invalid group' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.group.slug).to eq presenter.groups.first.slug
end
end
end
context '.slice' do
context 'when no parameters are provided' do
it 'returns the first item in the list of slices' do
params = {}
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.slice.slug).to eq 'all-data'
end
end
context 'when a slice is provided in the params hash' do
it 'returns the slice with the given slug' do
params = { source: 'survey-data-only', slice: 'students-and-teachers' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.slice.slug).to eq 'students-and-teachers'
end
end
context 'when a slice is provided but the source is left blank ' do
it 'returns the slice from the default source (all-data)' do
params = { slice: 'students-and-teachers' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.slice.slug).to eq 'all-data'
end
end
context 'when a parameter that does not match a slice is provided' do
it 'it returns the first slice from the chosen source' do
params = { source: 'survey-data-only', slice: 'invalid-slice' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.slice.slug).to eq 'students-and-teachers'
end
end
end
context '.source' do
context 'when no parameters are provided' do
it 'returns the first item in the list of sources' do
params = {}
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.slice.slug).to eq 'all-data'
end
end
context 'when a source is provided in the params hash' do
it 'returns the source with the given slug' do
params = { source: 'all-data' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.source.slug).to eq 'all-data'
params = { source: 'survey-data-only' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.source.slug).to eq 'survey-data-only'
end
end
context 'when a parameter that does not match a source is provided' do
it 'returns the first item in the list of sources' do
params = { source: 'invalid-source' }
presenter = Analyze::Presenter.new(params:, school:, academic_year:)
expect(presenter.slice.slug).to eq 'all-data'
end
end
end
end

@ -227,12 +227,12 @@ describe GroupedBarColumnPresenter do
# it_behaves_like 'y_offset' # it_behaves_like 'y_offset'
context 'and the score is right at the approval low benchmark' do context 'and the score is right at the approval low benchmark' do
it "where bar would normally have a height of 0, we inflate the height to be at least the minimum bar height of #{AnalyzeBarPresenter::MINIMUM_BAR_HEIGHT}" do it "where bar would normally have a height of 0, we inflate the height to be at least the minimum bar height of #{Analyze::BarPresenter::MINIMUM_BAR_HEIGHT}" do
expect(student_presenter.bars[year_index].bar_height_percentage).to be_within(0.01).of(AnalyzeBarPresenter::MINIMUM_BAR_HEIGHT) expect(student_presenter.bars[year_index].bar_height_percentage).to be_within(0.01).of(Analyze::BarPresenter::MINIMUM_BAR_HEIGHT)
end end
it "where the bar would normally start at the approval low benchmark, it shifts up to accomodate it being grown to the minimum bar height of #{AnalyzeBarPresenter::MINIMUM_BAR_HEIGHT}" do it "where the bar would normally start at the approval low benchmark, it shifts up to accomodate it being grown to the minimum bar height of #{Analyze::BarPresenter::MINIMUM_BAR_HEIGHT}" do
expect(student_presenter.bars[year_index].y_offset).to be_within(0.01).of(analyze_zone_height * 2 - AnalyzeBarPresenter::MINIMUM_BAR_HEIGHT) expect(student_presenter.bars[year_index].y_offset).to be_within(0.01).of(analyze_zone_height * 2 - Analyze::BarPresenter::MINIMUM_BAR_HEIGHT)
end end
end end
end end
@ -257,8 +257,8 @@ describe GroupedBarColumnPresenter do
academic_year:, likert_score: 4) academic_year:, likert_score: 4)
end end
it "it rounds to the the minimum bar height of #{AnalyzeBarPresenter::MINIMUM_BAR_HEIGHT} " do it "it rounds to the the minimum bar height of #{Analyze::BarPresenter::MINIMUM_BAR_HEIGHT} " do
expect(student_presenter.bars[year_index].bar_height_percentage).to be_within(0.01).of(AnalyzeBarPresenter::MINIMUM_BAR_HEIGHT) expect(student_presenter.bars[year_index].bar_height_percentage).to be_within(0.01).of(Analyze::BarPresenter::MINIMUM_BAR_HEIGHT)
end end
end end
end end

@ -13,6 +13,10 @@ describe DemographicLoader do
} }
end end
let(:incomes) do
['Economically Disadvantaged N', 'Economically Disadvantaged Y', 'Unknown']
end
before :each do before :each do
DemographicLoader.load_data(filepath:) DemographicLoader.load_data(filepath:)
end end
@ -46,5 +50,12 @@ describe DemographicLoader do
expect(Gender.find_by_designation(key).qualtrics_code).to eq value expect(Gender.find_by_designation(key).qualtrics_code).to eq value
end end
end end
it 'loads all the income designations' do
expect(Income.all.count).to eq 3
incomes.each do |income|
expect(Income.find_by_designation(income).designation).to eq income
end
end
end end
end end

@ -1,9 +1,9 @@
require 'rails_helper' require "rails_helper"
RSpec.describe SurveyItemValues, type: :model do RSpec.describe SurveyItemValues, type: :model do
let(:headers) do let(:headers) do
['StartDate', 'EndDate', 'Status', 'IPAddress', 'Progress', 'Duration (in seconds)', 'Finished', 'RecordedDate', ["StartDate", "EndDate", "Status", "IPAddress", "Progress", "Duration (in seconds)", "Finished", "RecordedDate",
'ResponseId', 'RecipientLastName', 'RecipientFirstName', 'RecipientEmail', 'ExternalReference', 'LocationLatitude', 'LocationLongitude', 'DistributionChannel', 'UserLanguage', 'District', 'School- Lee', 'School- Maynard', 'LASID', 'Grade', 's-emsa-q1', 's-emsa-q2', 's-emsa-q3', 's-tint-q1', 's-tint-q2', 's-tint-q3', 's-tint-q4', 's-tint-q5', 's-acpr-q1', 's-acpr-q2', 's-acpr-q3', 's-acpr-q4', 's-cure-q1', 's-cure-q2', 's-cure-q3', 's-cure-q4', 's-sten-q1', 's-sten-q2', 's-sten-q3', 's-sper-q1', 's-sper-q2', 's-sper-q3', 's-sper-q4', 's-civp-q1', 's-civp-q2', 's-civp-q3', 's-civp-q4', 's-grmi-q1', 's-grmi-q2', 's-grmi-q3', 's-grmi-q4', 's-appa-q1', 's-appa-q2', 's-appa-q3', 's-peff-q1', 's-peff-q2', 's-peff-q3', 's-peff-q4', 's-peff-q5', 's-peff-q6', 's-sbel-q1', 's-sbel-q2', 's-sbel-q3', 's-sbel-q4', 's-sbel-q5', 's-phys-q1', 's-phys-q2', 's-phys-q3', 's-phys-q4', 's-vale-q1', 's-vale-q2', 's-vale-q3', 's-vale-q4', 's-acst-q1', 's-acst-q2', 's-acst-q3', 's-sust-q1', 's-sust-q2', 's-grit-q1', 's-grit-q2', 's-grit-q3', 's-grit-q4', 's-expa-q1', 's-poaf-q1', 's-poaf-q2', 's-poaf-q3', 's-poaf-q4', 's-tint-q1-1', 's-tint-q2-1', 's-tint-q3-1', 's-tint-q4-1', 's-tint-q5-1', 's-acpr-q1-1', 's-acpr-q2-1', 's-acpr-q3-1', 's-acpr-q4-1', 's-peff-q1-1', 's-peff-q2-1', 's-peff-q3-1', 's-peff-q4-1', 's-peff-q5-1', 's-peff-q6-1', 'Gender', 'Race'] "ResponseId", "RecipientLastName", "RecipientFirstName", "RecipientEmail", "ExternalReference", "LocationLatitude", "LocationLongitude", "DistributionChannel", "UserLanguage", "District", "School- Lee", "School- Maynard", "LASID", "Grade", "s-emsa-q1", "s-emsa-q2", "s-emsa-q3", "s-tint-q1", "s-tint-q2", "s-tint-q3", "s-tint-q4", "s-tint-q5", "s-acpr-q1", "s-acpr-q2", "s-acpr-q3", "s-acpr-q4", "s-cure-q1", "s-cure-q2", "s-cure-q3", "s-cure-q4", "s-sten-q1", "s-sten-q2", "s-sten-q3", "s-sper-q1", "s-sper-q2", "s-sper-q3", "s-sper-q4", "s-civp-q1", "s-civp-q2", "s-civp-q3", "s-civp-q4", "s-grmi-q1", "s-grmi-q2", "s-grmi-q3", "s-grmi-q4", "s-appa-q1", "s-appa-q2", "s-appa-q3", "s-peff-q1", "s-peff-q2", "s-peff-q3", "s-peff-q4", "s-peff-q5", "s-peff-q6", "s-sbel-q1", "s-sbel-q2", "s-sbel-q3", "s-sbel-q4", "s-sbel-q5", "s-phys-q1", "s-phys-q2", "s-phys-q3", "s-phys-q4", "s-vale-q1", "s-vale-q2", "s-vale-q3", "s-vale-q4", "s-acst-q1", "s-acst-q2", "s-acst-q3", "s-sust-q1", "s-sust-q2", "s-grit-q1", "s-grit-q2", "s-grit-q3", "s-grit-q4", "s-expa-q1", "s-poaf-q1", "s-poaf-q2", "s-poaf-q3", "s-poaf-q4", "s-tint-q1-1", "s-tint-q2-1", "s-tint-q3-1", "s-tint-q4-1", "s-tint-q5-1", "s-acpr-q1-1", "s-acpr-q2-1", "s-acpr-q3-1", "s-acpr-q4-1", "s-peff-q1-1", "s-peff-q2-1", "s-peff-q3-1", "s-peff-q4-1", "s-peff-q5-1", "s-peff-q6-1", "Gender", "Race"]
end end
let(:genders) do let(:genders) do
create(:gender, qualtrics_code: 1) create(:gender, qualtrics_code: 1)
@ -15,20 +15,21 @@ RSpec.describe SurveyItemValues, type: :model do
gender_hash gender_hash
end end
let(:survey_items) { [] } let(:survey_items) { [] }
let(:district) { create(:district, name: "Attleboro") }
let(:attleboro) do let(:attleboro) do
create(:school, name: 'Attleboro', dese_id: 1234) create(:school, name: "Attleboro", dese_id: 1234, district:)
end end
let(:attleboro_respondents) do let(:attleboro_respondents) do
create(:respondent, school: attleboro, academic_year: ay_2022_23, nine: 40, ten: 40, eleven: 40, twelve: 40) create(:respondent, school: attleboro, academic_year: ay_2022_23, nine: 40, ten: 40, eleven: 40, twelve: 40)
end end
let(:schools) { School.school_hash } let(:schools) { School.school_hash }
let(:recorded_date) { '2023-04-01' } let(:recorded_date) { "2023-04-01" }
let(:ay_2022_23) do let(:ay_2022_23) do
create(:academic_year, range: '2022-23') create(:academic_year, range: "2022-23")
end end
let(:common_headers) do let(:common_headers) do
['Recorded Date', 'DeseID', 'ResponseID', 'Duration (in seconds)', 'Gender', 'Grade'] ["Recorded Date", "DeseID", "ResponseID", "Duration (in seconds)", "Gender", "Grade"]
end end
let(:standard_survey_items) do let(:standard_survey_items) do
@ -46,9 +47,9 @@ RSpec.describe SurveyItemValues, type: :model do
end end
let(:short_form_survey_items) do let(:short_form_survey_items) do
survey_item_ids = [create(:survey_item, survey_item_id: 's-phys-q1', on_short_form: true), survey_item_ids = [create(:survey_item, survey_item_id: "s-phys-q1", on_short_form: true),
create(:survey_item, survey_item_id: 's-phys-q2', on_short_form: true), create(:survey_item, survey_item_id: "s-phys-q2", on_short_form: true),
create(:survey_item, survey_item_id: 's-phys-q3', create(:survey_item, survey_item_id: "s-phys-q3",
on_short_form: true)].map(&:survey_item_id) on_short_form: true)].map(&:survey_item_id)
survey_item_ids.map do |survey_item_id| survey_item_ids.map do |survey_item_id|
create(:survey_item, survey_item_id:) create(:survey_item, survey_item_id:)
@ -57,9 +58,9 @@ RSpec.describe SurveyItemValues, type: :model do
end end
let(:early_education_survey_items) do let(:early_education_survey_items) do
survey_item_ids = [create(:survey_item, survey_item_id: 's-emsa-es1'), survey_item_ids = [create(:survey_item, survey_item_id: "s-emsa-es1"),
create(:survey_item, survey_item_id: 's-emsa-es2'), create(:survey_item, survey_item_id: "s-emsa-es2"),
create(:survey_item, survey_item_id: 's-emsa-es3')].map(&:survey_item_id) create(:survey_item, survey_item_id: "s-emsa-es3")].map(&:survey_item_id)
survey_item_ids.map do |survey_item_id| survey_item_ids.map do |survey_item_id|
create(:survey_item, survey_item_id:) create(:survey_item, survey_item_id:)
end end
@ -81,49 +82,49 @@ RSpec.describe SurveyItemValues, type: :model do
(survey_item_ids << common_headers).flatten (survey_item_ids << common_headers).flatten
end end
context '.recorded_date' do context ".recorded_date" do
it 'returns the recorded date' do it "returns the recorded date" do
row = { 'RecordedDate' => '2017-01-01' } row = { "RecordedDate" => "2017-01-01" }
values = SurveyItemValues.new(row:, headers:, genders:, survey_items:, schools:) values = SurveyItemValues.new(row:, headers:, genders:, survey_items:, schools:)
expect(values.recorded_date).to eq Date.parse('2017-01-01') expect(values.recorded_date).to eq Date.parse("2017-01-01")
headers = ['Recorded Date'] headers = ["Recorded Date"]
row = { 'Recorded Date' => '2017-01-02' } row = { "Recorded Date" => "2017-01-02" }
values = SurveyItemValues.new(row:, headers:, genders:, survey_items:, schools:) values = SurveyItemValues.new(row:, headers:, genders:, survey_items:, schools:)
expect(values.recorded_date).to eq Date.parse('2017-01-02') expect(values.recorded_date).to eq Date.parse("2017-01-02")
end end
end end
context '.school' do context ".school" do
it 'returns the school that maps to the dese id provided' do it "returns the school that maps to the dese id provided" do
attleboro attleboro
row = { 'Dese ID' => '1234' } row = { "Dese ID" => "1234" }
values = SurveyItemValues.new(row:, headers:, genders:, survey_items:, schools:) values = SurveyItemValues.new(row:, headers:, genders:, survey_items:, schools:)
expect(values.school).to eq attleboro expect(values.school).to eq attleboro
row = { 'DeseID' => '1234' } row = { "DeseID" => "1234" }
values = SurveyItemValues.new(row:, headers:, genders:, survey_items:, schools:) values = SurveyItemValues.new(row:, headers:, genders:, survey_items:, schools:)
expect(values.school).to eq attleboro expect(values.school).to eq attleboro
end end
end end
context '.grade' do context ".grade" do
it 'returns the grade that maps to the grade provided' do it "returns the grade that maps to the grade provided" do
row = { 'Grade' => '1' } row = { "Grade" => "1" }
values = SurveyItemValues.new(row:, headers:, genders:, survey_items:, schools:) values = SurveyItemValues.new(row:, headers:, genders:, survey_items:, schools:)
expect(values.grade).to eq 1 expect(values.grade).to eq 1
end end
end end
context '.gender' do context ".gender" do
it 'returns the grade that maps to the grade provided' do it "returns the grade that maps to the grade provided" do
row = { 'Gender' => '1' } row = { "Gender" => "1" }
values = SurveyItemValues.new(row:, headers:, genders:, survey_items:, schools:) values = SurveyItemValues.new(row:, headers:, genders:, survey_items:, schools:)
expect(values.gender.qualtrics_code).to eq 1 expect(values.gender.qualtrics_code).to eq 1
end end
end end
context '.respondent_type' do context ".respondent_type" do
it 'reads header to find the survey type' do it "reads header to find the survey type" do
headers = %w[s-sbel-q5 s-phys-q2 RecordedDate] headers = %w[s-sbel-q5 s-phys-q2 RecordedDate]
values = SurveyItemValues.new(row: {}, headers:, genders:, survey_items:, schools:) values = SurveyItemValues.new(row: {}, headers:, genders:, survey_items:, schools:)
expect(values.respondent_type).to eq :student expect(values.respondent_type).to eq :student
@ -134,32 +135,32 @@ RSpec.describe SurveyItemValues, type: :model do
end end
end end
context '.survey_type' do context ".survey_type" do
context 'when survey type is standard form' do context "when survey type is standard form" do
it 'returns the survey type' do it "returns the survey type" do
headers = standard_survey_items headers = standard_survey_items
values = SurveyItemValues.new(row: {}, headers:, genders:, survey_items:, schools:) values = SurveyItemValues.new(row: {}, headers:, genders:, survey_items:, schools:)
expect(values.survey_type).to eq :standard expect(values.survey_type).to eq :standard
end end
end end
context 'when survey type is teacher form' do context "when survey type is teacher form" do
it 'returns the survey type' do it "returns the survey type" do
headers = teacher_survey_items headers = teacher_survey_items
values = SurveyItemValues.new(row: {}, headers:, genders:, survey_items:, schools:) values = SurveyItemValues.new(row: {}, headers:, genders:, survey_items:, schools:)
expect(values.survey_type).to eq :teacher expect(values.survey_type).to eq :teacher
end end
end end
context 'when survey type is short form' do context "when survey type is short form" do
it 'returns the survey type' do it "returns the survey type" do
headers = short_form_survey_items headers = short_form_survey_items
values = SurveyItemValues.new(row: {}, headers:, genders:, survey_items:, schools:) values = SurveyItemValues.new(row: {}, headers:, genders:, survey_items:, schools:)
expect(values.survey_type).to eq :short_form expect(values.survey_type).to eq :short_form
end end
end end
context 'when survey type is early education' do context "when survey type is early education" do
it 'returns the survey type' do it "returns the survey type" do
headers = early_education_survey_items headers = early_education_survey_items
values = SurveyItemValues.new(row: {}, headers:, genders:, survey_items:, schools:) values = SurveyItemValues.new(row: {}, headers:, genders:, survey_items:, schools:)
expect(values.survey_type).to eq :early_education expect(values.survey_type).to eq :early_education
@ -167,158 +168,235 @@ RSpec.describe SurveyItemValues, type: :model do
end end
end end
context '.valid_duration' do context ".income" do
context 'when duration is valid' do context "when no disaggregation data is provided" do
it 'returns true' do it "returns an empty string " do
disaggregation_data = {}
values = SurveyItemValues.new(row: {}, headers:, genders:, survey_items:, schools:, disaggregation_data:)
expect(values.income).to eq "Unknown"
end
end
context "when disaggregation data is provided" do
before :each do
attleboro
ay_2022_23
end
it "translates Free Lunch to Economically Disadvantaged - Y" do
headers = ["District", "Academic Year", "LASID", "LowIncome"]
row = { "District" => "Attleboro", "AcademicYear" => "2022-23", "LASID" => "1", "LowIncome" => "Free Lunch" }
disaggregation_data = { %w[1 Attleboro 2022-23] => DisaggregationRow.new(row:, headers:) }
headers = ["LASID", "Dese Id", "RecordedDate"]
row = { "LASID" => "1", "DESE ID" => "1234", "RecordedDate" => "2023-1-1" }
values = SurveyItemValues.new(row:, headers:, genders:, survey_items:, schools:,
disaggregation_data:)
expect(values.income).to eq "Economically Disadvantaged - Y"
end
it "translates Reduced Lunch to Economically Disadvantaged - Y" do
headers = ["District", "Academic Year", "LASID", "LowIncome"]
row = { "District" => "Attleboro", "AcademicYear" => "2022-23", "LASID" => "1", "LowIncome" => "Reduced Lunch" }
disaggregation_data = { %w[1 Attleboro 2022-23] => DisaggregationRow.new(row:, headers:) }
headers = ["LASID", "Dese Id", "RecordedDate"]
row = { "LASID" => "1", "DESE ID" => "1234", "RecordedDate" => "2023-1-1" }
values = SurveyItemValues.new(row:, headers:, genders:, survey_items:, schools:,
disaggregation_data:)
expect(values.income).to eq "Economically Disadvantaged - Y"
end
it "translates LowIncome to Economically Disadvantaged - Y" do
headers = ["District", "Academic Year", "LASID", "LowIncome"]
row = { "District" => "Attleboro", "AcademicYear" => "2022-23", "LASID" => "1", "LowIncome" => "LowIncome" }
disaggregation_data = { %w[1 Attleboro 2022-23] => DisaggregationRow.new(row:, headers:) }
headers = ["LASID", "Dese Id", "RecordedDate"]
row = { "LASID" => "1", "DESE ID" => "1234", "RecordedDate" => "2023-1-1" }
values = SurveyItemValues.new(row:, headers:, genders:, survey_items:, schools:,
disaggregation_data:)
expect(values.income).to eq "Economically Disadvantaged - Y"
end
it "translates Not Eligible to Economically Disadvantaged - N" do
headers = ["District", "Academic Year", "LASID", "LowIncome"]
row = { "District" => "Attleboro", "AcademicYear" => "2022-23", "LASID" => "1", "LowIncome" => "Not Eligible" }
disaggregation_data = { %w[1 Attleboro 2022-23] => DisaggregationRow.new(row:, headers:) }
headers = ["LASID", "Dese Id", "RecordedDate"]
row = { "LASID" => "1", "DESE ID" => "1234", "RecordedDate" => "2023-1-1" }
values = SurveyItemValues.new(row:, headers:, genders:, survey_items:, schools:,
disaggregation_data:)
expect(values.income).to eq "Economically Disadvantaged - N"
end
it "translates blanks to Unknown" do
headers = ["District", "Academic Year", "LASID", "LowIncome"]
row = { "District" => "Attleboro", "AcademicYear" => "2022-23", "LASID" => "1", "LowIncome" => "" }
disaggregation_data = { %w[1 Attleboro 2022-23] => DisaggregationRow.new(row:, headers:) }
headers = ["LASID", "Dese Id", "RecordedDate"]
row = { "LASID" => "1", "DESE ID" => "1234", "RecordedDate" => "2023-1-1" }
values = SurveyItemValues.new(row:, headers:, genders:, survey_items:, schools:,
disaggregation_data:)
expect(values.income).to eq "Unknown"
end
end
end
context ".valid_duration" do
context "when duration is valid" do
it "returns true" do
headers = standard_survey_items headers = standard_survey_items
values = SurveyItemValues.new(row: { 'Duration (in seconds)' => '240', 'Gender' => 'Male' }, headers:, genders:, survey_items:, values = SurveyItemValues.new(row: { "Duration (in seconds)" => "240", "Gender" => "Male" }, headers:, genders:, survey_items:,
schools:) schools:)
expect(values.valid_duration?).to eq true expect(values.valid_duration?).to eq true
headers = teacher_survey_items headers = teacher_survey_items
values = SurveyItemValues.new(row: { 'Duration (in seconds)' => '300' }, headers:, genders:, survey_items:, values = SurveyItemValues.new(row: { "Duration (in seconds)" => "300" }, headers:, genders:, survey_items:,
schools:) schools:)
expect(values.valid_duration?).to eq true expect(values.valid_duration?).to eq true
headers = short_form_survey_items headers = short_form_survey_items
values = SurveyItemValues.new(row: { 'Duration (in seconds)' => '100' }, headers:, genders:, survey_items:, values = SurveyItemValues.new(row: { "Duration (in seconds)" => "100" }, headers:, genders:, survey_items:,
schools:) schools:)
expect(values.valid_duration?).to eq true expect(values.valid_duration?).to eq true
# When duration is blank or N/A or NA, we don't have enough information to kick out the row as invalid so we keep it in # When duration is blank or N/A or NA, we don't have enough information to kick out the row as invalid so we keep it in
headers = short_form_survey_items headers = short_form_survey_items
values = SurveyItemValues.new(row: { 'Duration (in seconds)' => '' }, headers:, genders:, survey_items:, values = SurveyItemValues.new(row: { "Duration (in seconds)" => "" }, headers:, genders:, survey_items:,
schools:) schools:)
expect(values.valid_duration?).to eq true expect(values.valid_duration?).to eq true
headers = short_form_survey_items headers = short_form_survey_items
values = SurveyItemValues.new(row: { 'Duration (in seconds)' => 'N/A' }, headers:, genders:, survey_items:, values = SurveyItemValues.new(row: { "Duration (in seconds)" => "N/A" }, headers:, genders:, survey_items:,
schools:) schools:)
expect(values.valid_duration?).to eq true expect(values.valid_duration?).to eq true
headers = short_form_survey_items headers = short_form_survey_items
values = SurveyItemValues.new(row: { 'Duration (in seconds)' => 'NA' }, headers:, genders:, survey_items:, values = SurveyItemValues.new(row: { "Duration (in seconds)" => "NA" }, headers:, genders:, survey_items:,
schools:) schools:)
expect(values.valid_duration?).to eq true expect(values.valid_duration?).to eq true
end end
end end
context 'when duration is invalid' do context "when duration is invalid" do
it 'returns false' do it "returns false" do
headers = standard_survey_items headers = standard_survey_items
values = SurveyItemValues.new(row: { 'Duration (in seconds)' => '239' }, headers:, genders:, survey_items:, values = SurveyItemValues.new(row: { "Duration (in seconds)" => "239" }, headers:, genders:, survey_items:,
schools:) schools:)
expect(values.valid_duration?).to eq false expect(values.valid_duration?).to eq false
headers = teacher_survey_items headers = teacher_survey_items
values = SurveyItemValues.new(row: { 'Duration (in seconds)' => '299' }, headers:, genders:, survey_items:, values = SurveyItemValues.new(row: { "Duration (in seconds)" => "299" }, headers:, genders:, survey_items:,
schools:) schools:)
expect(values.valid_duration?).to eq false expect(values.valid_duration?).to eq false
headers = short_form_survey_items headers = short_form_survey_items
values = SurveyItemValues.new(row: { 'Duration (in seconds)' => '99' }, headers:, genders:, survey_items:, values = SurveyItemValues.new(row: { "Duration (in seconds)" => "99" }, headers:, genders:, survey_items:,
schools:) schools:)
expect(values.valid_duration?).to eq false expect(values.valid_duration?).to eq false
end end
end end
end end
context '.valid_progress' do context ".valid_progress" do
context 'when progress is valid' do context "when progress is valid" do
it 'returns true' do it "returns true" do
headers = %w[s-sbel-q5 s-phys-q2 RecordedDate] headers = %w[s-sbel-q5 s-phys-q2 RecordedDate]
values = SurveyItemValues.new(row: { 'Progress' => '25' }, headers:, genders:, survey_items:, values = SurveyItemValues.new(row: { "Progress" => "25" }, headers:, genders:, survey_items:,
schools:) schools:)
expect(values.valid_progress?).to eq true expect(values.valid_progress?).to eq true
# When progress is blank or N/A or NA, we don't have enough information to kick out the row as invalid so we keep it in # When progress is blank or N/A or NA, we don't have enough information to kick out the row as invalid so we keep it in
headers = %w[s-sbel-q5 s-phys-q2 RecordedDate] headers = %w[s-sbel-q5 s-phys-q2 RecordedDate]
values = SurveyItemValues.new(row: { 'Progress' => '' }, headers:, genders:, survey_items:, values = SurveyItemValues.new(row: { "Progress" => "" }, headers:, genders:, survey_items:,
schools:) schools:)
expect(values.valid_progress?).to eq true expect(values.valid_progress?).to eq true
headers = %w[s-sbel-q5 s-phys-q2 RecordedDate] headers = %w[s-sbel-q5 s-phys-q2 RecordedDate]
values = SurveyItemValues.new(row: { 'Progress' => 'N/A' }, headers:, genders:, survey_items:, values = SurveyItemValues.new(row: { "Progress" => "N/A" }, headers:, genders:, survey_items:,
schools:) schools:)
expect(values.valid_progress?).to eq true expect(values.valid_progress?).to eq true
headers = %w[s-sbel-q5 s-phys-q2 RecordedDate] headers = %w[s-sbel-q5 s-phys-q2 RecordedDate]
values = SurveyItemValues.new(row: { 'Progress' => 'NA' }, headers:, genders:, survey_items:, values = SurveyItemValues.new(row: { "Progress" => "NA" }, headers:, genders:, survey_items:,
schools:) schools:)
expect(values.valid_progress?).to eq true expect(values.valid_progress?).to eq true
end end
end end
context 'when progress is invalid' do context "when progress is invalid" do
it 'returns false' do it "returns false" do
headers = %w[s-sbel-q5 s-phys-q2 RecordedDate] headers = %w[s-sbel-q5 s-phys-q2 RecordedDate]
values = SurveyItemValues.new(row: { 'Progress' => '24' }, headers:, genders:, survey_items:, values = SurveyItemValues.new(row: { "Progress" => "24" }, headers:, genders:, survey_items:,
schools:) schools:)
expect(values.valid_progress?).to eq false expect(values.valid_progress?).to eq false
end end
end end
end end
context '.valid_grade?' do context ".valid_grade?" do
context 'when grade is valid' do context "when grade is valid" do
before :each do before :each do
attleboro attleboro
attleboro_respondents attleboro_respondents
end end
it 'returns true for students' do it "returns true for students" do
headers = %w[s-sbel-q5 s-phys-q2 grade RecordedDate] headers = %w[s-sbel-q5 s-phys-q2 grade RecordedDate]
values = SurveyItemValues.new(row: { 'grade' => '9', 'RecordedDate' => recorded_date, 'Dese ID' => '1234' }, headers:, genders:, survey_items:, values = SurveyItemValues.new(row: { "grade" => "9", "RecordedDate" => recorded_date, "Dese ID" => "1234" }, headers:, genders:, survey_items:,
schools:) schools:)
expect(values.valid_grade?).to eq true expect(values.valid_grade?).to eq true
end end
it 'returns true for teachers' do it "returns true for teachers" do
headers = %w[t-sbel-q5 t-phys-q2 grade RecordedDate] headers = %w[t-sbel-q5 t-phys-q2 grade RecordedDate]
values = SurveyItemValues.new(row: { 'RecordedDate' => recorded_date, 'Dese ID' => '1234' }, headers:, genders:, survey_items:, values = SurveyItemValues.new(row: { "RecordedDate" => recorded_date, "Dese ID" => "1234" }, headers:, genders:, survey_items:,
schools:) schools:)
expect(values.valid_grade?).to eq true expect(values.valid_grade?).to eq true
end end
end end
context 'when grade is invalid' do context "when grade is invalid" do
before :each do before :each do
attleboro attleboro
attleboro_respondents attleboro_respondents
end end
it 'returns false' do it "returns false" do
headers = %w[s-sbel-q5 s-phys-q2 grade RecordedDate] headers = %w[s-sbel-q5 s-phys-q2 grade RecordedDate]
values = SurveyItemValues.new(row: { 'grade' => '2', 'RecordedDate' => recorded_date, 'Dese ID' => '1234' }, headers:, genders:, survey_items:, values = SurveyItemValues.new(row: { "grade" => "2", "RecordedDate" => recorded_date, "Dese ID" => "1234" }, headers:, genders:, survey_items:,
schools: School.school_hash) schools: School.school_hash)
expect(values.valid_grade?).to eq false expect(values.valid_grade?).to eq false
end end
end end
end end
context '.valid_sd?' do context ".valid_sd?" do
context 'when the standard deviation is valid' do context "when the standard deviation is valid" do
it 'returns true for student questions' do it "returns true for student questions" do
headers = %w[s-sbel-q5 s-phys-q1 s-phys-q2 RecordedDate] headers = %w[s-sbel-q5 s-phys-q1 s-phys-q2 RecordedDate]
values = SurveyItemValues.new(row: { 'RecordedDate' => recorded_date, 'Dese ID' => '1234', 's-sbel-q5' => '1', 's-phys-q1' => '', 's-phys-q2' => '5' }, headers:, genders:, survey_items:, values = SurveyItemValues.new(row: { "RecordedDate" => recorded_date, "Dese ID" => "1234", "s-sbel-q5" => "1", "s-phys-q1" => "", "s-phys-q2" => "5" }, headers:, genders:, survey_items:,
schools: School.school_hash) schools: School.school_hash)
expect(values.valid_sd?).to eq true expect(values.valid_sd?).to eq true
end end
it 'returns true for teacher questions' do it "returns true for teacher questions" do
headers = %w[t-sbel-q5 t-phys-q2] headers = %w[t-sbel-q5 t-phys-q2]
values = SurveyItemValues.new(row: { 'RecordedDate' => recorded_date, 'Dese ID' => '1234', 't-sbel-q5' => '1', 't-phys-q2' => '5' }, headers:, genders:, survey_items:, values = SurveyItemValues.new(row: { "RecordedDate" => recorded_date, "Dese ID" => "1234", "t-sbel-q5" => "1", "t-phys-q2" => "5" }, headers:, genders:, survey_items:,
schools: School.school_hash) schools: School.school_hash)
expect(values.valid_sd?).to eq true expect(values.valid_sd?).to eq true
end end
end end
context 'when the standard deviation is invalid' do context "when the standard deviation is invalid" do
it 'returns false for student questions' do it "returns false for student questions" do
headers = %w[s-sbel-q5 s-phys-q1 s-phys-q2 RecordedDate] headers = %w[s-sbel-q5 s-phys-q1 s-phys-q2 RecordedDate]
values = SurveyItemValues.new(row: { 'RecordedDate' => recorded_date, 'Dese ID' => '1234', 's-sbel-q5' => '1', 's-phys-q2' => '', 's-phys-q2' => '1' }, headers:, genders:, survey_items:, values = SurveyItemValues.new(row: { "RecordedDate" => recorded_date, "Dese ID" => "1234", "s-sbel-q5" => "1", "s-phys-q2" => "1" }, headers:, genders:, survey_items:,
schools: School.school_hash) schools: School.school_hash)
expect(values.valid_sd?).to eq false expect(values.valid_sd?).to eq false
end end
it 'returns false for teacher questions' do it "returns false for teacher questions" do
headers = %w[t-sbel-q5 t-phys-q1 t-phys-q2 RecordedDate] headers = %w[t-sbel-q5 t-phys-q1 t-phys-q2 RecordedDate]
values = SurveyItemValues.new(row: { 'RecordedDate' => recorded_date, 'Dese ID' => '1234', 't-sbel-q5' => '1', 't-phys-q2' => '', 't-phys-q2' => '1' }, headers:, genders:, survey_items:, values = SurveyItemValues.new(row: { "RecordedDate" => recorded_date, "Dese ID" => "1234", "t-sbel-q5" => "1", "t-phys-q2" => "1" }, headers:, genders:, survey_items:,
schools: School.school_hash) schools: School.school_hash)
expect(values.valid_sd?).to eq false expect(values.valid_sd?).to eq false
end end

@ -1,53 +1,59 @@
require "rails_helper" require 'rails_helper'
describe SurveyResponsesDataLoader do describe SurveyResponsesDataLoader do
let(:path_to_teacher_responses) { Rails.root.join("spec", "fixtures", "test_2020-21_teacher_survey_responses.csv") } let(:path_to_teacher_responses) { Rails.root.join('spec', 'fixtures', 'test_2020-21_teacher_survey_responses.csv') }
let(:path_to_student_responses) { Rails.root.join("spec", "fixtures", "test_2020-21_student_survey_responses.csv") } let(:path_to_student_responses) { Rails.root.join('spec', 'fixtures', 'test_2020-21_student_survey_responses.csv') }
let(:path_to_butler_student_responses) do let(:path_to_butler_student_responses) do
Rails.root.join("spec", "fixtures", "test_2022-23_butler_student_survey_responses.csv") Rails.root.join('spec', 'fixtures', 'test_2022-23_butler_student_survey_responses.csv')
end end
let(:ay_2020_21) { create(:academic_year, range: "2020-21") } let(:ay_2020_21) { create(:academic_year, range: '2020-21') }
let(:ay_2022_23) { create(:academic_year, range: "2022-23") } let(:ay_2022_23) { create(:academic_year, range: '2022-23') }
let(:school) { create(:school, name: "Lee Elementary School", slug: "lee-elementary-school", dese_id: 1_500_025) } let(:school) { create(:school, name: 'Lee Elementary School', slug: 'lee-elementary-school', dese_id: 1_500_025) }
let(:lowell) { create(:district, name: "Lowell", slug: "lowell") } let(:lowell) { create(:district, name: 'Lowell', slug: 'lowell') }
let(:second_school) { create(:school, name: "Lee Middle High School", slug: "lee-middle-high-school", dese_id: 1_500_505, district: lowell) } let(:second_school) do
create(:school, name: 'Lee Middle High School', slug: 'lee-middle-high-school', dese_id: 1_500_505,
district: lowell)
end
let(:butler_school) do let(:butler_school) do
create(:school, name: "Butler Elementary School", slug: "butler-elementary-school", dese_id: 1_600_310, create(:school, name: 'Butler Elementary School', slug: 'butler-elementary-school', dese_id: 1_600_310,
district: lowell) district: lowell)
end end
let(:t_pcom_q3) { create(:survey_item, survey_item_id: "t-pcom-q3") } let(:t_pcom_q3) { create(:survey_item, survey_item_id: 't-pcom-q3') }
let(:t_pcom_q2) { create(:survey_item, survey_item_id: "t-pcom-q2") } let(:t_pcom_q2) { create(:survey_item, survey_item_id: 't-pcom-q2') }
let(:t_coll_q1) { create(:survey_item, survey_item_id: "t-coll-q1") } let(:t_coll_q1) { create(:survey_item, survey_item_id: 't-coll-q1') }
let(:t_coll_q2) { create(:survey_item, survey_item_id: "t-coll-q2") } let(:t_coll_q2) { create(:survey_item, survey_item_id: 't-coll-q2') }
let(:t_coll_q3) { create(:survey_item, survey_item_id: "t-coll-q3") } let(:t_coll_q3) { create(:survey_item, survey_item_id: 't-coll-q3') }
let(:t_sach_q1) { create(:survey_item, survey_item_id: "t-sach-q1") } let(:t_sach_q1) { create(:survey_item, survey_item_id: 't-sach-q1') }
let(:t_sach_q2) { create(:survey_item, survey_item_id: "t-sach-q2") } let(:t_sach_q2) { create(:survey_item, survey_item_id: 't-sach-q2') }
let(:t_sach_q3) { create(:survey_item, survey_item_id: "t-sach-q3") } let(:t_sach_q3) { create(:survey_item, survey_item_id: 't-sach-q3') }
let(:s_phys_q1) { create(:survey_item, survey_item_id: "s-phys-q1") } let(:s_phys_q1) { create(:survey_item, survey_item_id: 's-phys-q1') }
let(:s_phys_q2) { create(:survey_item, survey_item_id: "s-phys-q2") } let(:s_phys_q2) { create(:survey_item, survey_item_id: 's-phys-q2') }
let(:s_phys_q3) { create(:survey_item, survey_item_id: "s-phys-q3") } let(:s_phys_q3) { create(:survey_item, survey_item_id: 's-phys-q3') }
let(:s_phys_q4) { create(:survey_item, survey_item_id: "s-phys-q4") } let(:s_phys_q4) { create(:survey_item, survey_item_id: 's-phys-q4') }
let(:s_vale_q1) { create(:survey_item, survey_item_id: "s-phys-q1") } let(:s_vale_q1) { create(:survey_item, survey_item_id: 's-phys-q1') }
let(:s_vale_q2) { create(:survey_item, survey_item_id: "s-phys-q2") } let(:s_vale_q2) { create(:survey_item, survey_item_id: 's-phys-q2') }
let(:s_vale_q3) { create(:survey_item, survey_item_id: "s-phys-q3") } let(:s_vale_q3) { create(:survey_item, survey_item_id: 's-phys-q3') }
let(:s_vale_q4) { create(:survey_item, survey_item_id: "s-phys-q4") } let(:s_vale_q4) { create(:survey_item, survey_item_id: 's-phys-q4') }
let(:s_acst_q1) { create(:survey_item, survey_item_id: "s-acst-q1") } let(:s_acst_q1) { create(:survey_item, survey_item_id: 's-acst-q1') }
let(:s_acst_q2) { create(:survey_item, survey_item_id: "s-acst-q2") } let(:s_acst_q2) { create(:survey_item, survey_item_id: 's-acst-q2') }
let(:s_acst_q3) { create(:survey_item, survey_item_id: "s-acst-q3") } let(:s_acst_q3) { create(:survey_item, survey_item_id: 's-acst-q3') }
let(:s_acst_q4) { create(:survey_item, survey_item_id: "s-acst-q4") } let(:s_acst_q4) { create(:survey_item, survey_item_id: 's-acst-q4') }
let(:s_emsa_q1) { create(:survey_item, survey_item_id: "s-emsa-q1") } let(:s_emsa_q1) { create(:survey_item, survey_item_id: 's-emsa-q1') }
let(:s_emsa_q2) { create(:survey_item, survey_item_id: "s-emsa-q2") } let(:s_emsa_q2) { create(:survey_item, survey_item_id: 's-emsa-q2') }
let(:s_emsa_q3) { create(:survey_item, survey_item_id: "s-emsa-q3") } let(:s_emsa_q3) { create(:survey_item, survey_item_id: 's-emsa-q3') }
let(:female) { create(:gender, qualtrics_code: 1) } let(:female) { create(:gender, qualtrics_code: 1) }
let(:male) { create(:gender, qualtrics_code: 2) } let(:male) { create(:gender, qualtrics_code: 2) }
let(:another_gender) { create(:gender, qualtrics_code: 3) } let(:another_gender) { create(:gender, qualtrics_code: 3) }
let(:non_binary) { create(:gender, qualtrics_code: 4) } let(:non_binary) { create(:gender, qualtrics_code: 4) }
let(:unknown_gender) { create(:gender, qualtrics_code: 99) } let(:unknown_gender) { create(:gender, qualtrics_code: 99) }
let(:low_income) { create(:income, designation: 'Economically Disadvantaged Y') }
let(:high_income) { create(:income, designation: 'Economically Disadvantaged N') }
let(:unknown_income) { create(:income, designation: 'Unknown') }
let(:setup) do let(:setup) do
ay_2020_21 ay_2020_21
@ -83,18 +89,21 @@ describe SurveyResponsesDataLoader do
another_gender another_gender
non_binary non_binary
unknown_gender unknown_gender
low_income
high_income
unknown_income
end end
before :each do before :each do
setup setup
end end
describe "loading teacher survey responses" do describe 'loading teacher survey responses' do
before do before do
SurveyResponsesDataLoader.load_data filepath: path_to_teacher_responses SurveyResponsesDataLoader.load_data filepath: path_to_teacher_responses
end end
it "ensures teacher responses load correctly" do it 'ensures teacher responses load correctly' do
assigns_academic_year_to_survey_item_responses assigns_academic_year_to_survey_item_responses
assigns_school_to_the_survey_item_responses assigns_school_to_the_survey_item_responses
assigns_recorded_date_to_teacher_responses assigns_recorded_date_to_teacher_responses
@ -105,12 +114,12 @@ describe SurveyResponsesDataLoader do
end end
end end
describe "student survey responses" do describe 'student survey responses' do
before do before do
SurveyResponsesDataLoader.load_data filepath: path_to_student_responses SurveyResponsesDataLoader.load_data filepath: path_to_student_responses
end end
it "ensures student responses load correctly" do it 'ensures student responses load correctly' do
assigns_academic_year_to_student_survey_item_responses assigns_academic_year_to_student_survey_item_responses
assigns_school_to_student_survey_item_responses assigns_school_to_student_survey_item_responses
assigns_recorded_date_to_student_responses assigns_recorded_date_to_student_responses
@ -119,85 +128,86 @@ describe SurveyResponsesDataLoader do
captures_likert_scores_for_student_survey_item_responses captures_likert_scores_for_student_survey_item_responses
assigns_grade_level_to_responses assigns_grade_level_to_responses
assigns_gender_to_responses assigns_gender_to_responses
assigns_income_to_responses
is_idempotent_for_students is_idempotent_for_students
end end
context "when updating student survey responses from another csv file" do context 'when updating student survey responses from another csv file' do
before :each do before :each do
SurveyResponsesDataLoader.load_data filepath: Rails.root.join("spec", "fixtures", SurveyResponsesDataLoader.load_data filepath: Rails.root.join('spec', 'fixtures',
"secondary_test_2020-21_student_survey_responses.csv") 'secondary_test_2020-21_student_survey_responses.csv')
end end
it "updates the likert score to the score on the new csv file" do it 'updates the likert score to the score on the new csv file' do
s_emsa_q1 = SurveyItem.find_by_survey_item_id "s-emsa-q1" s_emsa_q1 = SurveyItem.find_by_survey_item_id 's-emsa-q1'
expect(SurveyItemResponse.where(response_id: "student_survey_response_3", expect(SurveyItemResponse.where(response_id: 'student_survey_response_3',
survey_item: s_emsa_q1).first.likert_score).to eq 1 survey_item: s_emsa_q1).first.likert_score).to eq 1
expect(SurveyItemResponse.where(response_id: "student_survey_response_4", expect(SurveyItemResponse.where(response_id: 'student_survey_response_4',
survey_item: s_emsa_q1).first.likert_score).to eq 1 survey_item: s_emsa_q1).first.likert_score).to eq 1
expect(SurveyItemResponse.where(response_id: "student_survey_response_5", expect(SurveyItemResponse.where(response_id: 'student_survey_response_5',
survey_item: s_emsa_q1).first.likert_score).to eq 1 survey_item: s_emsa_q1).first.likert_score).to eq 1
expect(SurveyItemResponse.where(response_id: "student_survey_response_5", expect(SurveyItemResponse.where(response_id: 'student_survey_response_5',
survey_item: s_acst_q3).first.likert_score).to eq 4 survey_item: s_acst_q3).first.likert_score).to eq 4
end end
end end
end end
# figure out why this is failing # figure out why this is failing
describe "when using Lowell rules to skip rows in the csv file" do describe 'when using Lowell rules to skip rows in the csv file' do
before :each do before :each do
SurveyResponsesDataLoader.load_data filepath: path_to_student_responses, SurveyResponsesDataLoader.load_data filepath: path_to_student_responses,
rules: [Rule::SkipNonLowellSchools] rules: [Rule::SkipNonLowellSchools]
end end
it "rejects any non-lowell school" do it 'rejects any non-lowell school' do
expect(SurveyItemResponse.where(response_id: "student_survey_response_1").count).to eq 0 expect(SurveyItemResponse.where(response_id: 'student_survey_response_1').count).to eq 0
expect(SurveyItemResponse.count).to eq 69 expect(SurveyItemResponse.count).to eq 69
end end
it "loads the correct number of responses for lowell schools" do it 'loads the correct number of responses for lowell schools' do
expect(SurveyItemResponse.where(response_id: "student_survey_response_2").count).to eq 0 expect(SurveyItemResponse.where(response_id: 'student_survey_response_2').count).to eq 0
expect(SurveyItemResponse.where(response_id: "student_survey_response_3").count).to eq 12 expect(SurveyItemResponse.where(response_id: 'student_survey_response_3').count).to eq 12
expect(SurveyItemResponse.where(response_id: "student_survey_response_4").count).to eq 15 expect(SurveyItemResponse.where(response_id: 'student_survey_response_4').count).to eq 15
expect(SurveyItemResponse.where(response_id: "student_survey_response_5").count).to eq 14 expect(SurveyItemResponse.where(response_id: 'student_survey_response_5').count).to eq 14
end end
context "when loading 22-23 butler survey responses" do context 'when loading 22-23 butler survey responses' do
before :each do before :each do
SurveyResponsesDataLoader.load_data filepath: path_to_butler_student_responses, SurveyResponsesDataLoader.load_data filepath: path_to_butler_student_responses,
rules: [Rule::SkipNonLowellSchools] rules: [Rule::SkipNonLowellSchools]
end end
it "loads all the responses for Butler" do it 'loads all the responses for Butler' do
expect(SurveyItemResponse.where(school: butler_school).count).to eq 56 expect(SurveyItemResponse.where(school: butler_school).count).to eq 56
end end
it "blank entries for grade get loaded as nils, not zero values" do it 'blank entries for grade get loaded as nils, not zero values' do
expect(SurveyItemResponse.where(response_id: "butler_student_survey_response_1").first.grade).to eq 7 expect(SurveyItemResponse.where(response_id: 'butler_student_survey_response_1').first.grade).to eq 7
expect(SurveyItemResponse.where(response_id: "butler_student_survey_response_2").first.grade).to eq 7 expect(SurveyItemResponse.where(response_id: 'butler_student_survey_response_2').first.grade).to eq 7
expect(SurveyItemResponse.where(response_id: "butler_student_survey_response_3").first.grade).to eq 7 expect(SurveyItemResponse.where(response_id: 'butler_student_survey_response_3').first.grade).to eq 7
expect(SurveyItemResponse.where(response_id: "butler_student_survey_response_4").first.grade).to eq 5 expect(SurveyItemResponse.where(response_id: 'butler_student_survey_response_4').first.grade).to eq 5
expect(SurveyItemResponse.where(response_id: "butler_student_survey_response_5").first.grade).to eq 7 expect(SurveyItemResponse.where(response_id: 'butler_student_survey_response_5').first.grade).to eq 7
expect(SurveyItemResponse.where(response_id: "butler_student_survey_response_6").first.grade).to eq 6 expect(SurveyItemResponse.where(response_id: 'butler_student_survey_response_6').first.grade).to eq 6
expect(SurveyItemResponse.where(response_id: "butler_student_survey_response_7").first.grade).to eq nil expect(SurveyItemResponse.where(response_id: 'butler_student_survey_response_7').first.grade).to eq nil
expect(SurveyItemResponse.where(response_id: "butler_student_survey_response_8").first.grade).to eq 0 expect(SurveyItemResponse.where(response_id: 'butler_student_survey_response_8').first.grade).to eq 0
end end
end end
end end
end end
def assigns_academic_year_to_survey_item_responses def assigns_academic_year_to_survey_item_responses
expect(SurveyItemResponse.find_by_response_id("teacher_survey_response_1").academic_year).to eq ay_2020_21 expect(SurveyItemResponse.find_by_response_id('teacher_survey_response_1').academic_year).to eq ay_2020_21
end end
def assigns_school_to_the_survey_item_responses def assigns_school_to_the_survey_item_responses
expect(SurveyItemResponse.find_by_response_id("teacher_survey_response_1").school).to eq school expect(SurveyItemResponse.find_by_response_id('teacher_survey_response_1').school).to eq school
end end
def loads_survey_item_responses_for_a_given_survey_response def loads_survey_item_responses_for_a_given_survey_response
expect(SurveyItemResponse.where(response_id: "teacher_survey_response_1").count).to eq 5 expect(SurveyItemResponse.where(response_id: 'teacher_survey_response_1').count).to eq 5
expect(SurveyItemResponse.where(response_id: "teacher_survey_response_2").count).to eq 0 expect(SurveyItemResponse.where(response_id: 'teacher_survey_response_2').count).to eq 0
expect(SurveyItemResponse.where(response_id: "teacher_survey_response_3").count).to eq 8 expect(SurveyItemResponse.where(response_id: 'teacher_survey_response_3').count).to eq 8
expect(SurveyItemResponse.where(response_id: "teacher_survey_response_4").count).to eq 8 expect(SurveyItemResponse.where(response_id: 'teacher_survey_response_4').count).to eq 8
expect(SurveyItemResponse.where(response_id: "teacher_survey_response_5").count).to eq 8 expect(SurveyItemResponse.where(response_id: 'teacher_survey_response_5').count).to eq 8
end end
def loads_all_survey_item_responses_for_a_given_survey_item def loads_all_survey_item_responses_for_a_given_survey_item
@ -206,20 +216,20 @@ def loads_all_survey_item_responses_for_a_given_survey_item
end end
def captures_likert_scores_for_survey_item_responses def captures_likert_scores_for_survey_item_responses
expect(SurveyItemResponse.where(response_id: "teacher_survey_response_1").where(survey_item: t_pcom_q2)).to be_empty expect(SurveyItemResponse.where(response_id: 'teacher_survey_response_1').where(survey_item: t_pcom_q2)).to be_empty
expect(SurveyItemResponse.where(response_id: "teacher_survey_response_1").where(survey_item: t_pcom_q3).first.likert_score).to eq 3 expect(SurveyItemResponse.where(response_id: 'teacher_survey_response_1').where(survey_item: t_pcom_q3).first.likert_score).to eq 3
expect(SurveyItemResponse.where(response_id: "teacher_survey_response_2").where(survey_item: t_pcom_q2)).to be_empty expect(SurveyItemResponse.where(response_id: 'teacher_survey_response_2').where(survey_item: t_pcom_q2)).to be_empty
expect(SurveyItemResponse.where(response_id: "teacher_survey_response_2").where(survey_item: t_pcom_q3)).to be_empty expect(SurveyItemResponse.where(response_id: 'teacher_survey_response_2').where(survey_item: t_pcom_q3)).to be_empty
expect(SurveyItemResponse.where(response_id: "teacher_survey_response_3").where(survey_item: t_pcom_q2).first.likert_score).to eq 5 expect(SurveyItemResponse.where(response_id: 'teacher_survey_response_3').where(survey_item: t_pcom_q2).first.likert_score).to eq 5
expect(SurveyItemResponse.where(response_id: "teacher_survey_response_3").where(survey_item: t_pcom_q3).first.likert_score).to eq 5 expect(SurveyItemResponse.where(response_id: 'teacher_survey_response_3').where(survey_item: t_pcom_q3).first.likert_score).to eq 5
expect(SurveyItemResponse.where(response_id: "teacher_survey_response_4").where(survey_item: t_pcom_q2).first.likert_score).to eq 4 expect(SurveyItemResponse.where(response_id: 'teacher_survey_response_4').where(survey_item: t_pcom_q2).first.likert_score).to eq 4
expect(SurveyItemResponse.where(response_id: "teacher_survey_response_4").where(survey_item: t_pcom_q3).first.likert_score).to eq 4 expect(SurveyItemResponse.where(response_id: 'teacher_survey_response_4').where(survey_item: t_pcom_q3).first.likert_score).to eq 4
expect(SurveyItemResponse.where(response_id: "teacher_survey_response_5").where(survey_item: t_pcom_q2).first.likert_score).to eq 2 expect(SurveyItemResponse.where(response_id: 'teacher_survey_response_5').where(survey_item: t_pcom_q2).first.likert_score).to eq 2
expect(SurveyItemResponse.where(response_id: "teacher_survey_response_5").where(survey_item: t_pcom_q3).first.likert_score).to eq 4 expect(SurveyItemResponse.where(response_id: 'teacher_survey_response_5').where(survey_item: t_pcom_q3).first.likert_score).to eq 4
end end
def is_idempotent def is_idempotent
@ -231,19 +241,19 @@ def is_idempotent
end end
def assigns_academic_year_to_student_survey_item_responses def assigns_academic_year_to_student_survey_item_responses
expect(SurveyItemResponse.find_by_response_id("student_survey_response_3").academic_year).to eq ay_2020_21 expect(SurveyItemResponse.find_by_response_id('student_survey_response_3').academic_year).to eq ay_2020_21
end end
def assigns_school_to_student_survey_item_responses def assigns_school_to_student_survey_item_responses
expect(SurveyItemResponse.find_by_response_id("student_survey_response_3").school).to eq second_school expect(SurveyItemResponse.find_by_response_id('student_survey_response_3').school).to eq second_school
end end
def loads_student_survey_item_response_values def loads_student_survey_item_response_values
expect(SurveyItemResponse.where(response_id: "student_survey_response_1").count).to eq 3 expect(SurveyItemResponse.where(response_id: 'student_survey_response_1').count).to eq 3
expect(SurveyItemResponse.where(response_id: "student_survey_response_2").count).to eq 0 expect(SurveyItemResponse.where(response_id: 'student_survey_response_2').count).to eq 0
expect(SurveyItemResponse.where(response_id: "student_survey_response_3").count).to eq 12 expect(SurveyItemResponse.where(response_id: 'student_survey_response_3').count).to eq 12
expect(SurveyItemResponse.where(response_id: "student_survey_response_4").count).to eq 15 expect(SurveyItemResponse.where(response_id: 'student_survey_response_4').count).to eq 15
expect(SurveyItemResponse.where(response_id: "student_survey_response_5").count).to eq 14 expect(SurveyItemResponse.where(response_id: 'student_survey_response_5').count).to eq 14
end end
def student_survey_item_response_count_matches_expected def student_survey_item_response_count_matches_expected
@ -252,20 +262,20 @@ def student_survey_item_response_count_matches_expected
end end
def captures_likert_scores_for_student_survey_item_responses def captures_likert_scores_for_student_survey_item_responses
expect(SurveyItemResponse.where(response_id: "student_survey_response_1").where(survey_item: s_phys_q1).first.likert_score).to eq 3 expect(SurveyItemResponse.where(response_id: 'student_survey_response_1').where(survey_item: s_phys_q1).first.likert_score).to eq 3
expect(SurveyItemResponse.where(response_id: "student_survey_response_1").where(survey_item: s_phys_q2)).to be_empty expect(SurveyItemResponse.where(response_id: 'student_survey_response_1').where(survey_item: s_phys_q2)).to be_empty
expect(SurveyItemResponse.where(response_id: "student_survey_response_2").where(survey_item: s_phys_q1)).to be_empty expect(SurveyItemResponse.where(response_id: 'student_survey_response_2').where(survey_item: s_phys_q1)).to be_empty
expect(SurveyItemResponse.where(response_id: "student_survey_response_2").where(survey_item: s_phys_q2)).to be_empty expect(SurveyItemResponse.where(response_id: 'student_survey_response_2').where(survey_item: s_phys_q2)).to be_empty
expect(SurveyItemResponse.where(response_id: "student_survey_response_3").where(survey_item: s_phys_q1).first.likert_score).to eq 1 expect(SurveyItemResponse.where(response_id: 'student_survey_response_3').where(survey_item: s_phys_q1).first.likert_score).to eq 1
expect(SurveyItemResponse.where(response_id: "student_survey_response_3").where(survey_item: s_phys_q2).first.likert_score).to eq 3 expect(SurveyItemResponse.where(response_id: 'student_survey_response_3').where(survey_item: s_phys_q2).first.likert_score).to eq 3
expect(SurveyItemResponse.where(response_id: "student_survey_response_4").where(survey_item: s_phys_q1).first.likert_score).to eq 1 expect(SurveyItemResponse.where(response_id: 'student_survey_response_4').where(survey_item: s_phys_q1).first.likert_score).to eq 1
expect(SurveyItemResponse.where(response_id: "student_survey_response_4").where(survey_item: s_phys_q2).first.likert_score).to eq 1 expect(SurveyItemResponse.where(response_id: 'student_survey_response_4').where(survey_item: s_phys_q2).first.likert_score).to eq 1
expect(SurveyItemResponse.where(response_id: "student_survey_response_5").where(survey_item: s_phys_q1).first.likert_score).to eq 1 expect(SurveyItemResponse.where(response_id: 'student_survey_response_5').where(survey_item: s_phys_q1).first.likert_score).to eq 1
expect(SurveyItemResponse.where(response_id: "student_survey_response_5").where(survey_item: s_phys_q2).first.likert_score).to eq 2 expect(SurveyItemResponse.where(response_id: 'student_survey_response_5').where(survey_item: s_phys_q2).first.likert_score).to eq 2
end end
def is_idempotent_for_students def is_idempotent_for_students
@ -277,26 +287,26 @@ def is_idempotent_for_students
end end
def assigns_grade_level_to_responses def assigns_grade_level_to_responses
results = {"student_survey_response_1" => 11, results = { 'student_survey_response_1' => 11,
"student_survey_response_3" => 8, 'student_survey_response_3' => 8,
"student_survey_response_4" => 8, 'student_survey_response_4' => 8,
"student_survey_response_5" => 7, 'student_survey_response_5' => 7,
"student_survey_response_6" => 3, 'student_survey_response_6' => 3,
"student_survey_response_7" => 4} 'student_survey_response_7' => 4 }
results.each do |key, value| results.each do |key, value|
expect(SurveyItemResponse.where(response_id: key).all? do |response| expect(SurveyItemResponse.where(response_id: key).all? do |response|
response.grade == value response.grade == value
end).to eq true end).to eq true
end end
end end
def assigns_gender_to_responses def assigns_gender_to_responses
results = {"student_survey_response_1" => female, results = { 'student_survey_response_1' => female,
"student_survey_response_3" => male, 'student_survey_response_3' => male,
"student_survey_response_4" => non_binary, 'student_survey_response_4' => non_binary,
"student_survey_response_5" => non_binary, 'student_survey_response_5' => non_binary,
"student_survey_response_6" => unknown_gender, 'student_survey_response_6' => unknown_gender,
"student_survey_response_7" => unknown_gender} 'student_survey_response_7' => unknown_gender }
results.each do |key, value| results.each do |key, value|
expect(SurveyItemResponse.where(response_id: key).first.gender).to eq value expect(SurveyItemResponse.where(response_id: key).first.gender).to eq value
@ -304,23 +314,36 @@ def assigns_gender_to_responses
end end
def assigns_recorded_date_to_student_responses def assigns_recorded_date_to_student_responses
results = {"student_survey_response_1" => "2020-09-30T18:48:50", results = { 'student_survey_response_1' => '2020-09-30T18:48:50',
"student_survey_response_3" => "2021-03-31T09:59:02", 'student_survey_response_3' => '2021-03-31T09:59:02',
"student_survey_response_4" => "2021-03-31T10:00:17", 'student_survey_response_4' => '2021-03-31T10:00:17',
"student_survey_response_5" => "2021-03-31T10:01:36", 'student_survey_response_5' => '2021-03-31T10:01:36',
"student_survey_response_6" => "2021-03-31T10:01:37", 'student_survey_response_6' => '2021-03-31T10:01:37',
"student_survey_response_7" => "2021-03-31T10:01:38"} 'student_survey_response_7' => '2021-03-31T10:01:38' }
results.each do |key, value| results.each do |key, value|
expect(SurveyItemResponse.find_by_response_id(key).recorded_date).to eq Date.parse(value) expect(SurveyItemResponse.find_by_response_id(key).recorded_date).to eq Date.parse(value)
end end
end end
def assigns_recorded_date_to_teacher_responses def assigns_recorded_date_to_teacher_responses
results = {"teacher_survey_response_1" => "2020-10-16 11:09:03", results = { 'teacher_survey_response_1' => '2020-10-16 11:09:03',
"teacher_survey_response_3" => "2020-12-06 8:36:52", 'teacher_survey_response_3' => '2020-12-06 8:36:52',
"teacher_survey_response_4" => "2020-12-06 8:51:25", 'teacher_survey_response_4' => '2020-12-06 8:51:25',
"teacher_survey_response_5" => "2020-12-06 8:55:58"} 'teacher_survey_response_5' => '2020-12-06 8:55:58' }
results.each do |key, value| results.each do |key, value|
expect(SurveyItemResponse.find_by_response_id(key).recorded_date).to eq Date.parse(value) expect(SurveyItemResponse.find_by_response_id(key).recorded_date).to eq Date.parse(value)
end end
end end
def assigns_income_to_responses
results = { 'student_survey_response_1' => low_income,
'student_survey_response_3' => low_income,
'student_survey_response_4' => unknown_income,
'student_survey_response_5' => low_income,
'student_survey_response_6' => high_income,
'student_survey_response_7' => low_income }
results.each do |key, value|
expect(SurveyItemResponse.where(response_id: key).first.income).to eq value
end
end

@ -1,25 +1,20 @@
require 'rails_helper' require "rails_helper"
include AnalyzeHelper include AnalyzeHelper
include Analyze::Graph include Analyze::Graph
describe 'analyze/index' do describe "analyze/index" do
subject { Nokogiri::HTML(rendered) } subject { Nokogiri::HTML(rendered) }
let(:category) { create(:category) } let(:category) { create(:category) }
let(:subcategory) { create(:subcategory, category:) } let(:subcategory) { create(:subcategory, category:) }
let(:school) { create(:school) } let(:school) { create(:school) }
let(:academic_year) { create(:academic_year) } let(:academic_year) { create(:academic_year) }
let(:races) do let(:races) do
DemographicLoader.load_data(filepath: 'spec/fixtures/sample_demographics.csv') DemographicLoader.load_data(filepath: "spec/fixtures/sample_demographics.csv")
Race.all Race.all
end end
let(:graph) { StudentsAndTeachers.new }
let(:graphs) do
[StudentsAndTeachers.new, StudentsByRace.new(races:)]
end
let(:background) { BackgroundPresenter.new(num_of_columns: graph.columns.count) } let(:background) { BackgroundPresenter.new(num_of_columns: graph.columns.count) }
let(:selected_races) { races }
let(:support_for_teaching) do let(:support_for_teaching) do
measure = create(:measure, name: 'Support For Teaching Development & Growth', measure_id: '1A-I', subcategory:) measure = create(:measure, name: "Support For Teaching Development & Growth", measure_id: "1A-I", subcategory:)
scale = create(:scale, measure:) scale = create(:scale, measure:)
create(:student_survey_item, create(:student_survey_item,
scale:, scale:,
@ -31,7 +26,7 @@ describe 'analyze/index' do
end end
let(:effective_leadership) do let(:effective_leadership) do
measure = create(:measure, name: 'Effective Leadership', measure_id: '1A-II', subcategory:) measure = create(:measure, name: "Effective Leadership", measure_id: "1A-II", subcategory:)
scale = create(:scale, measure:) scale = create(:scale, measure:)
create(:teacher_survey_item, create(:teacher_survey_item,
scale:, scale:,
@ -43,7 +38,7 @@ describe 'analyze/index' do
end end
let(:professional_qualifications) do let(:professional_qualifications) do
measure = create(:measure, name: 'Professional Qualifications', measure_id: '1A-III', subcategory:) measure = create(:measure, name: "Professional Qualifications", measure_id: "1A-III", subcategory:)
scale = create(:scale, measure:) scale = create(:scale, measure:)
create(:admin_data_item, create(:admin_data_item,
scale:, scale:,
@ -54,73 +49,30 @@ describe 'analyze/index' do
measure measure
end end
let(:sources) do
[Analyze::Source::SurveyData.new(slices:)]
end
let(:slices) do
students_and_teachers = Analyze::Slice::StudentsAndTeachers.new
students_by_group = Analyze::Slice::StudentsByGroup.new(races:, grades:)
[students_and_teachers, students_by_group]
end
let(:slice) do
slices.first
end
let(:groups) do
[Analyze::Group::Race.new, Analyze::Group::Grade.new]
end
let(:group) do
groups.first
end
let(:grades) do
(1..12).to_a
end
let(:genders) do let(:genders) do
DemographicLoader.load_data(filepath: 'spec/fixtures/sample_demographics.csv') DemographicLoader.load_data(filepath: "spec/fixtures/sample_demographics.csv")
Gender.all Gender.all
end end
let(:selected_genders) do let(:respondent) { create(:respondent, school:, academic_year:) }
genders
end
let(:selected_grades) do
grades
end
before :each do before :each do
assign :races, races races
assign :selected_races, selected_races category
assign :graph, graph subcategory
assign :graphs, graphs support_for_teaching
assign :background, background effective_leadership
professional_qualifications
respondent
assign :academic_year, academic_year assign :academic_year, academic_year
assign :available_academic_years, [academic_year]
assign :selected_academic_years, [academic_year]
assign :district, create(:district) assign :district, create(:district)
assign :school, school assign :school, school
assign :category, category assign :presenter,
assign :categories, [category] Analyze::Presenter.new(school:, academic_year:,
assign :subcategory, subcategory params: { category: category.category_id, subcategory: subcategory.subcategory_id, races: "american-indian-or-alaskan-native,asian-or-pacific-islander,black-or-african-american,hispanic-or-latinx,middle-eastern,multiracial,race-ethnicity-not-listed,white-or-caucasian", source: "survey-data-only", slice: "students-and-teachers", group: "race", graph: "students-by-race" })
assign :subcategories, category.subcategories assign :background, BackgroundPresenter.new(num_of_columns: 4)
assign :measures, [support_for_teaching, effective_leadership, professional_qualifications]
assign :sources, sources
assign :source, sources.first
assign :groups, groups
assign :group, group
assign :slice, slice
assign :grades, grades
assign :selected_grades, selected_grades
assign :genders, genders
assign :selected_genders, selected_genders
create(:respondent, school:, academic_year:)
end end
context 'when all the presenters have a nil score' do context "when all the presenters have a nil score" do
before do before do
render render
end end
@ -139,63 +91,72 @@ describe 'analyze/index' do
# ] # ]
# end # end
it 'displays a set of grouped bars for each presenter' do it "displays a set of grouped bars for each presenter" do
displayed_variance_columns = subject.css('.grouped-bar-column') displayed_variance_columns = subject.css(".grouped-bar-column")
expect(displayed_variance_columns.count).to eq 9 expect(displayed_variance_columns.count).to eq 27
displayed_variance_rows = subject.css('[data-for-measure-id]') displayed_variance_rows = subject.css("[data-for-measure-id]")
expect(displayed_variance_rows.first.attribute('data-for-measure-id').value).to eq '1A-I' expect(displayed_variance_rows.first.attribute("data-for-measure-id").value).to eq "1A-I"
displayed_academic_years = subject.css('[data-for-academic-year]') displayed_academic_years = subject.css("[data-for-academic-year]")
expect(displayed_academic_years.count).to eq 0 expect(displayed_academic_years.count).to eq 0
displayed_variance_labels = subject.css('[data-grouped-bar-label]') displayed_variance_labels = subject.css("[data-grouped-bar-label]")
expect(displayed_variance_labels.count).to eq 18
expect(displayed_variance_labels.first.inner_text).to include 'All' expect(displayed_variance_labels.count).to eq 39
expect(displayed_variance_labels[1].inner_text).to include 'Students' expect(displayed_variance_labels.first.inner_text).to include "American"
expect(displayed_variance_labels.last.inner_text).to include 'Data' expect(displayed_variance_labels.text).to include "Indian"
expect(displayed_variance_labels.text).to include "Asian"
expect(displayed_variance_labels.text).to include "Black"
expect(displayed_variance_labels.text).to include "White"
expect(displayed_variance_labels.text).to include "Hispanic"
expect(displayed_variance_labels.text).to include "Middle"
expect(displayed_variance_labels.text).to include "Eastern"
expect(displayed_variance_labels.text).to include "Multiracial"
expect(displayed_variance_labels.text).to include "Not"
expect(displayed_variance_labels.text).to include "Listed"
end end
it 'displays all measures for the first subcategory' do it "displays all measures for the first subcategory" do
expect(rendered).to have_text '1A-I' expect(rendered).to have_text "1A-I"
expect(rendered).to have_text '1A-II' expect(rendered).to have_text "1A-II"
expect(rendered).to have_text '1A-III' expect(rendered).to have_text "1A-III"
end end
it 'displays user interface controls' do it "displays user interface controls" do
expect(subject).to have_text 'Focus Area' expect(subject).to have_text "Focus Area"
expect(subject).to have_css '#select-category' expect(subject).to have_css "#select-category"
expect(subject).to have_css '#select-subcategory' expect(subject).to have_css "#select-subcategory"
expect(subject).to have_css "##{academic_year.range}" expect(subject).to have_css "##{academic_year.range}"
end end
it 'displays disabled checkboxes for years that dont have data' do it "displays disabled checkboxes for years that dont have data" do
year_checkbox = subject.css("##{academic_year.range}").first year_checkbox = subject.css("##{academic_year.range}").first
expect(year_checkbox.name).to eq 'input' expect(year_checkbox.name).to eq "input"
expect(academic_year.range).to eq '2050-51' expect(academic_year.range).to eq "2050-51"
expect(year_checkbox).to have_attribute 'disabled' expect(year_checkbox).to have_attribute "disabled"
end end
it 'displays a radio box selector for each type of data filter' do it "displays a radio box selector for each type of data filter" do
expect(subject).to have_css '#students-and-teachers' expect(subject).to have_css "#students-and-teachers"
expect(subject).to have_css '#students-by-group' expect(subject).to have_css "#students-by-group"
end end
it 'displays a checkbox for each race designation' do it "displays a checkbox for each race designation" do
race_slugs = %w[american-indian-or-alaskan-native asian-or-pacific-islander black-or-african-american race_slugs = %w[race-american-indian-or-alaskan-native race-asian-or-pacific-islander race-black-or-african-american
hispanic-or-latinx middle-eastern multiracial race-ethnicity-not-listed white-or-caucasian] race-hispanic-or-latinx race-middle-eastern race-multiracial race-race-ethnicity-not-listed race-white-or-caucasian]
race_slugs.each do |slug| race_slugs.each do |slug|
expect(subject).to have_css("//input[@type='checkbox'][@id='#{slug}']") expect(subject).to have_css("//input[@type='checkbox'][@id='#{slug}']")
end end
end end
end end
context 'when presenters have a displayable score' do context "when presenters have a displayable score" do
before do before do
render render
end end
context 'when displaying a student and teacher graph' do context "when displaying a student and teacher graph" do
end end
end end
end end

@ -3960,10 +3960,15 @@ semver@6.3.1, semver@^6.0.0, semver@^6.3.0, semver@^6.3.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.5.3: semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0:
version "7.5.4" version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@^7.3.2, semver@^7.5.2:
version "7.5.3"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e"
integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==
dependencies: dependencies:
lru-cache "^6.0.0" lru-cache "^6.0.0"

Loading…
Cancel
Save