diff --git a/Gemfile.lock b/Gemfile.lock index b040cf8..0e67c3a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,8 @@ PATH remote: . specs: - dashboard (0.1.0) + dashboard (0.1.1) + friendly_id (~> 5.4.0) rails (>= 7.1.2) GEM @@ -97,6 +98,8 @@ GEM factory_bot_rails (6.4.2) factory_bot (~> 6.4) railties (>= 5.0.0) + friendly_id (5.4.2) + activerecord (>= 4.0.0) globalid (1.2.1) activesupport (>= 6.1) i18n (1.14.1) @@ -115,6 +118,7 @@ GEM net-smtp marcel (1.0.2) mini_mime (1.1.5) + mini_portile2 (2.8.5) minitest (5.20.0) mutex_m (0.2.0) net-imap (0.4.8) @@ -127,7 +131,8 @@ GEM net-smtp (0.4.0) net-protocol nio4r (2.7.0) - nokogiri (1.15.5-x86_64-linux) + nokogiri (1.15.5) + mini_portile2 (~> 2.8.2) racc (~> 1.4) pg (1.5.4) psych (5.1.2) diff --git a/app/controllers/dashboard/home_controller.rb b/app/controllers/dashboard/home_controller.rb new file mode 100644 index 0000000..897c1c6 --- /dev/null +++ b/app/controllers/dashboard/home_controller.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Dashboard + class HomeController < ApplicationController + helper HeaderHelper + + def index + @districts = districts + @district = district + + @schools = schools + @school = school + + @year = year + @categories = Category.sorted.map { |category| CategoryPresenter.new(category:) } + end + + private + + def districts + District.all.order(:name).map do |district| + [district.name, district.id] + end + end + + def district + return District.find(params[:district]) if params[:district].present? + + District.first + end + + def schools + if district.present? + district.dashboard_schools.order(:name).map do |school| + [school.name, school.id] + end + else + [] + end + end + + def school + School.find(params[:school]) if params[:school].present? + end + + def year + return nil unless school.present? + + academic_year = AcademicYear.all.order(range: :DESC).find do |ay| + Subcategory.all.any? do |subcategory| + rate = subcategory.response_rate(school:, academic_year: ay) + rate.meets_student_threshold || rate.meets_teacher_threshold + end + end + + academic_year&.range || AcademicYear.order("range DESC").first.range + end + end +end diff --git a/app/helpers/dashboard/header_helper.rb b/app/helpers/dashboard/header_helper.rb new file mode 100644 index 0000000..3d2847f --- /dev/null +++ b/app/helpers/dashboard/header_helper.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Dashboard + module HeaderHelper + def link_to_overview(district:, school:, academic_year:) + "/districts/#{district.slug}/schools/#{school.slug}/overview?year=#{academic_year.range}" + end + + def link_to_browse(district:, school:, academic_year:) + "/districts/#{district.slug}/schools/#{school.slug}/browse/teachers-and-leadership?year=#{academic_year.range}" + end + + def link_to_analyze(district:, school:, academic_year:) + year = academic_year.range + "/districts/#{district.slug}/schools/#{school.slug}/analyze?year=#{year}&category=1&academic_years=#{year}" + end + + def district_url_for(district:, academic_year:) + pages = %w[overview browse analyze] + pages.each do |page| + if request.fullpath.include? page + return send("#{page}_link", district_slug: district.slug, school_slug: district.schools.alphabetic.first.slug, + academic_year_range: academic_year.range) + end + end + end + + def school_url_for(school:, academic_year:) + pages = %w[overview browse analyze] + pages.each do |page| + if request.fullpath.include? page + return send("#{page}_link", district_slug: school.district.slug, school_slug: school.slug, + academic_year_range: academic_year.range) + end + end + end + + def link_weight(path:) + active?(path:) ? "weight-700" : "weight-400" + end + + private + + def overview_link(district_slug:, school_slug:, academic_year_range:) + "/districts/#{district_slug}/schools/#{school_slug}/overview?year=#{academic_year_range}" + end + + def analyze_link(district_slug:, school_slug:, academic_year_range:) + "/districts/#{district_slug}/schools/#{school_slug}/analyze?year=#{academic_year_range}&academic_years=#{academic_year_range}" + end + + def browse_link(district_slug:, school_slug:, academic_year_range:) + "/districts/#{district_slug}/schools/#{school_slug}/browse/teachers-and-leadership?year=#{academic_year_range}" + end + + def active?(path:) + request.fullpath.include? path + end + end +end diff --git a/app/models/dashboard/academic_year.rb b/app/models/dashboard/academic_year.rb new file mode 100644 index 0000000..7b56652 --- /dev/null +++ b/app/models/dashboard/academic_year.rb @@ -0,0 +1,40 @@ +module Dashboard + class AcademicYear < ApplicationRecord + def self.find_by_date(date) + year = parse_year_range(date:) + range = "#{year.start}-#{year.end.to_s[2, 3]}" + academic_years[range] + end + + def formatted_range + years = range.split("-") + "#{years.first} – 20#{years.second}" + end + + private + + def self.parse_year_range(date:) + year = date.year + if date.month > 6 + AcademicYearRange.new(year, year + 1) + else + AcademicYearRange.new(year - 1, year) + end + end + + # This may cause problems if academic years get loaded from csv instead of the current method that requires a code change to the seeder script. This is because a change in code will trigger a complete reload of the application whereas loading from csv does not. This means if we change academic year to load from csv, the set of academic years will be stale when new years are added. + def self.academic_years + @@academic_years ||= AcademicYear.all.map { |academic_year| [academic_year.range, academic_year] }.to_h + end + + # Needed to reset the academic years when testing with specs that create the same academic year in a before :each block + def self.reset_academic_years + @@academic_years = nil + end + + private_class_method :academic_years + private_class_method :parse_year_range + end +end + +AcademicYearRange = Struct.new(:start, :end) diff --git a/app/models/dashboard/admin_data_item.rb b/app/models/dashboard/admin_data_item.rb new file mode 100644 index 0000000..8595e18 --- /dev/null +++ b/app/models/dashboard/admin_data_item.rb @@ -0,0 +1,14 @@ +module Dashboard + class AdminDataItem < ApplicationRecord + belongs_to :dashboard_scale + has_many :dashboard_admin_data_values + + scope :for_measures, lambda { |measures| + joins(:scale).where('scale.measure': measures) + } + + scope :non_hs_items_for_measures, lambda { |measure| + for_measures(measure).where(hs_only_item: false) + } + end +end diff --git a/app/models/dashboard/admin_data_value.rb b/app/models/dashboard/admin_data_value.rb new file mode 100644 index 0000000..8eea0b6 --- /dev/null +++ b/app/models/dashboard/admin_data_value.rb @@ -0,0 +1,9 @@ +module Dashboard + class AdminDataValue < ApplicationRecord + belongs_to :dashboard_school + belongs_to :dashboard_admin_data_item + belongs_to :dashboard_academic_year + + validates :likert_score, numericality: { greater_than: 0, less_than_or_equal_to: 5 } + end +end diff --git a/app/models/dashboard/category.rb b/app/models/dashboard/category.rb new file mode 100644 index 0000000..ebe838f --- /dev/null +++ b/app/models/dashboard/category.rb @@ -0,0 +1,13 @@ +module Dashboard + class Category < ApplicationRecord + include FriendlyId + friendly_id :name, use: [:slugged] + + scope :sorted, -> { order(:category_id) } + + has_many :subcategories + has_many :measures, through: :subcategories + has_many :admin_data_items, through: :measures + has_many :scales, through: :subcategories + end +end diff --git a/app/models/dashboard/district.rb b/app/models/dashboard/district.rb new file mode 100644 index 0000000..8dedcc0 --- /dev/null +++ b/app/models/dashboard/district.rb @@ -0,0 +1,18 @@ +module Dashboard + class District < ApplicationRecord + has_many :schools, class_name: "Dashboard::School" + has_many :dashboard_schools, class_name: "Dashboard::School" + + validates :name, presence: true + + scope :alphabetic, -> { order(name: :asc) } + + include FriendlyId + + friendly_id :name, use: [:slugged] + + def short_name + name.split(" ").first.downcase + end + end +end diff --git a/app/models/dashboard/ell.rb b/app/models/dashboard/ell.rb new file mode 100644 index 0000000..f4a8c29 --- /dev/null +++ b/app/models/dashboard/ell.rb @@ -0,0 +1,20 @@ +module Dashboard + class Ell < ApplicationRecord + scope :by_designation, -> { all.map { |ell| [ell.designation, ell] }.to_h } + + include FriendlyId + + friendly_id :designation, use: [:slugged] + + def self.to_designation(ell) + case ell + in /lep student 1st year|LEP student not 1st year|EL Student First Year|LEP\s*student/i + "ELL" + in /Does not apply/i + "Not ELL" + else + "Unknown" + end + end + end +end diff --git a/app/models/dashboard/gender.rb b/app/models/dashboard/gender.rb new file mode 100644 index 0000000..c74b972 --- /dev/null +++ b/app/models/dashboard/gender.rb @@ -0,0 +1,26 @@ +module Dashboard + class Gender < ApplicationRecord + scope :by_qualtrics_code, lambda { + all.map { |gender| [gender.qualtrics_code, gender] }.to_h + } + + def self.qualtrics_code_from(word) + case word + when /Female|^F|1/i + 1 + when /Male|^M|2/i + 2 + when /Another\s*Gender|Gender Identity not listed above|3|7/i + 4 # We categorize any self reported gender as non-binary + when /Non-Binary|^N|4/i + 4 + when /Prefer not to disclose|6/i + 99 + when %r{^#*N/*A$}i + nil + else + 99 + end + end + end +end diff --git a/app/models/dashboard/income.rb b/app/models/dashboard/income.rb new file mode 100644 index 0000000..b5b7265 --- /dev/null +++ b/app/models/dashboard/income.rb @@ -0,0 +1,31 @@ +module Dashboard + class Income < ApplicationRecord + scope :by_designation, -> { all.map { |income| [income.designation, income] }.to_h } + scope :by_slug, -> { all.map { |income| [income.slug, income] }.to_h } + + include FriendlyId + + friendly_id :designation, use: [:slugged] + + def self.to_designation(income) + case income + in /Free\s*Lunch|Reduced\s*Lunch|Low\s*Income|Reduced\s*price\s*lunch/i + "Economically Disadvantaged - Y" + in /Not\s*Eligible/i + "Economically Disadvantaged - N" + else + "Unknown" + end + end + + LABELS = { + "Economically Disadvantaged - Y" => "Economically Disadvantaged", + "Economically Disadvantaged - N" => "Not Economically Disadvantaged", + "Unknown" => "Unknown" + } + + def label + LABELS[designation] + end + end +end diff --git a/app/models/dashboard/measure.rb b/app/models/dashboard/measure.rb new file mode 100644 index 0000000..c7d5533 --- /dev/null +++ b/app/models/dashboard/measure.rb @@ -0,0 +1,227 @@ +module Dashboard + class Measure < ApplicationRecord + belongs_to :dashboard_subcategory + has_one :dashboard_category, through: :dashboard_subcategory + has_many :dashboard_scales + has_many :dashboard_admin_data_items, through: :scales + has_many :dashboard_survey_items, through: :scales + has_many :dashboard_survey_item_responses, through: :survey_items + + def none_meet_threshold?(school:, academic_year:) + @none_meet_threshold ||= Hash.new do |memo, (school, academic_year)| + memo[[school, academic_year]] = !sufficient_survey_responses?(school:, academic_year:) + end + + @none_meet_threshold[[school, academic_year]] + end + + def teacher_survey_items + @teacher_survey_items ||= survey_items.teacher_survey_items + end + + def student_survey_items + @student_survey_items ||= survey_items.student_survey_items + end + + def student_survey_items_with_sufficient_responses(school:, academic_year:) + @student_survey_items_with_sufficient_responses ||= SurveyItem.where(id: SurveyItem.joins("inner join survey_item_responses on survey_item_responses.survey_item_id = survey_items.id") + .student_survey_items + .where("survey_item_responses.school": school, + "survey_item_responses.academic_year": academic_year, + "survey_item_responses.survey_item_id": survey_items.student_survey_items, + "survey_item_responses.grade": school.grades(academic_year:)) + .group("survey_items.id") + .having("count(*) >= 10") + .count.keys) + end + + def teacher_scales + @teacher_scales ||= scales.teacher_scales + end + + def student_scales + @student_scales ||= scales.student_scales + end + + def includes_teacher_survey_items? + @includes_teacher_survey_items ||= teacher_survey_items.any? + end + + def includes_student_survey_items? + @includes_student_survey_items ||= student_survey_items.any? + end + + def includes_admin_data_items? + @includes_admin_data_items ||= admin_data_items.any? + end + + def score(school:, academic_year:) + @score ||= Hash.new do |memo, (school, academic_year)| + next Score::NIL_SCORE if incalculable_score(school:, academic_year:) + + scores = collect_averages_for_teacher_student_and_admin_data(school:, academic_year:) + average = scores.flatten.compact.remove_blanks.average.round(2) + + next Score::NIL_SCORE if average.nan? + + memo[[school, academic_year]] = scorify(average:, school:, academic_year:) + end + @score[[school, academic_year]] + end + + def student_score(school:, academic_year:) + @student_score ||= Hash.new do |memo, (school, academic_year)| + meets_student_threshold = sufficient_student_data?(school:, academic_year:) + average = student_average(school:, academic_year:).round(2) if meets_student_threshold + memo[[school, academic_year]] = scorify(average:, school:, academic_year:) + end + + @student_score[[school, academic_year]] + end + + def teacher_score(school:, academic_year:) + @teacher_score ||= Hash.new do |memo, (school, academic_year)| + meets_teacher_threshold = sufficient_teacher_data?(school:, academic_year:) + average = teacher_average(school:, academic_year:).round(2) if meets_teacher_threshold + memo[[school, academic_year]] = scorify(average:, school:, academic_year:) + end + + @teacher_score[[school, academic_year]] + end + + def admin_score(school:, academic_year:) + @admin_score ||= Hash.new do |memo, (school, academic_year)| + meets_admin_threshold = sufficient_admin_data?(school:, academic_year:) + average = admin_data_averages(school:, academic_year:).average.round(2) if meets_admin_threshold + memo[[school, academic_year]] = scorify(average:, school:, academic_year:) + end + + @admin_score[[school, academic_year]] + end + + def warning_low_benchmark + 1 + end + + def watch_low_benchmark + @watch_low_benchmark ||= benchmark(:watch_low_benchmark) + end + + def growth_low_benchmark + @growth_low_benchmark ||= benchmark(:growth_low_benchmark) + end + + def approval_low_benchmark + @approval_low_benchmark ||= benchmark(:approval_low_benchmark) + end + + def ideal_low_benchmark + @ideal_low_benchmark ||= benchmark(:ideal_low_benchmark) + end + + def sufficient_admin_data?(school:, academic_year:) + any_admin_data_collected?(school:, academic_year:) + end + + def benchmark(name) + averages = [] + averages << student_survey_items.first.send(name) if includes_student_survey_items? + averages << teacher_survey_items.first.send(name) if includes_teacher_survey_items? + (averages << admin_data_items.map(&name)).flatten! if includes_admin_data_items? + averages.average + end + + private + + def any_admin_data_collected?(school:, academic_year:) + @any_admin_data_collected ||= Hash.new do |memo, (school, academic_year)| + total_collected_admin_data_items = + admin_data_items.map do |admin_data_item| + admin_data_item.admin_data_values.where(school:, academic_year:).count + end.flatten.sum + memo[[school, academic_year]] = total_collected_admin_data_items.positive? + end + @any_admin_data_collected[[school, academic_year]] + end + + def sufficient_survey_responses?(school:, academic_year:) + @sufficient_survey_responses ||= Hash.new do |memo, (school, academic_year)| + memo[[school, academic_year]] = + sufficient_student_data?(school:, academic_year:) || sufficient_teacher_data?(school:, academic_year:) + end + @sufficient_survey_responses[[school, academic_year]] + end + + def scorify(average:, school:, academic_year:) + meets_student_threshold = sufficient_student_data?(school:, academic_year:) + meets_teacher_threshold = sufficient_teacher_data?(school:, academic_year:) + meets_admin_data_threshold = any_admin_data_collected?(school:, academic_year:) + Score.new(average:, meets_teacher_threshold:, meets_student_threshold:, meets_admin_data_threshold:) + end + + def collect_survey_item_average(survey_items:, school:, academic_year:) + @collect_survey_item_average ||= Hash.new do |memo, (survey_items, school, academic_year)| + averages = survey_items.map do |survey_item| + SurveyItemResponse.grouped_responses(school:, academic_year:)[survey_item.id] + end.remove_blanks + memo[[survey_items, school, academic_year]] = averages.average || 0 + end + @collect_survey_item_average[[survey_items, school, academic_year]] + end + + def sufficient_student_data?(school:, academic_year:) + return false unless includes_student_survey_items? + + subcategory.response_rate(school:, academic_year:).meets_student_threshold? + end + + def sufficient_teacher_data?(school:, academic_year:) + return false unless includes_teacher_survey_items? + + subcategory.response_rate(school:, academic_year:).meets_teacher_threshold? + end + + def incalculable_score(school:, academic_year:) + @incalculable_score ||= Hash.new do |memo, (school, academic_year)| + lacks_sufficient_survey_data = !sufficient_student_data?(school:, academic_year:) && + !sufficient_teacher_data?(school:, academic_year:) + memo[[school, academic_year]] = lacks_sufficient_survey_data && !includes_admin_data_items? + end + + @incalculable_score[[school, academic_year]] + end + + def collect_averages_for_teacher_student_and_admin_data(school:, academic_year:) + scores = [] + scores << teacher_average(school:, academic_year:) if sufficient_teacher_data?(school:, academic_year:) + scores << student_average(school:, academic_year:) if sufficient_student_data?(school:, academic_year:) + scores << admin_data_averages(school:, academic_year:) if includes_admin_data_items? + scores + end + + def teacher_average(school:, academic_year:) + @teacher_average ||= Hash.new do |memo, (school, academic_year)| + memo[[school, academic_year]] = + collect_survey_item_average(survey_items: teacher_survey_items, school:, academic_year:) + end + + @teacher_average[[school, academic_year]] + end + + def student_average(school:, academic_year:) + @student_average ||= Hash.new do |memo, (school, academic_year)| + survey_items = student_survey_items_with_sufficient_responses(school:, academic_year:) + memo[[school, academic_year]] = collect_survey_item_average(survey_items:, school:, academic_year:) + end + @student_average[[school, academic_year]] + end + + def admin_data_averages(school:, academic_year:) + @admin_data_averages ||= Hash.new do |memo, (school, academic_year)| + memo[[school, academic_year]] = + AdminDataValue.where(school:, academic_year:, admin_data_item: admin_data_items).pluck(:likert_score) + end + @admin_data_averages[[school, academic_year]] + end + end +end diff --git a/app/models/dashboard/race.rb b/app/models/dashboard/race.rb new file mode 100644 index 0000000..f7e2fb9 --- /dev/null +++ b/app/models/dashboard/race.rb @@ -0,0 +1,51 @@ +module Dashboard + class Race < ApplicationRecord + include FriendlyId + has_many :dashboard_student_races + has_many :dashboard_students, through: :student_races + + friendly_id :designation, use: [:slugged] + + scope :by_qualtrics_code, lambda { + all.map { |race| [race.qualtrics_code, race] }.to_h + } + + def self.qualtrics_code_from(word) + case word + when /Native\s*American|American\s*Indian|Alaskan\s*Native|1/i + 1 + when /\bAsian|Pacific\s*Island|Hawaiian|2/i + 2 + when /Black|African\s*American|3/i + 3 + when /Hispanic|Latinx|4/i + 4 + when /White|Caucasian|5/i + 5 + when /Prefer not to disclose|6/i + 6 + when /Prefer to self-describe|7/i + 7 + when /Middle\s*Eastern|North\s*African|8/i + 8 + when %r{^#*N/*A$}i + nil + else + 99 + end + end + + def self.normalize_race_list(codes) + # if anyone selects not to disclose their race or prefers to self-describe, categorize that as unknown race + races = codes.map do |code| + code = 99 if [6, 7].include?(code) || code.nil? || code.zero? + code + end.uniq + + races.delete(99) if races.length > 1 # remove unkown race if other races present + races << 100 if races.length > 1 # add multiracial designation if multiple races present + races << 99 if races.length == 0 # add unknown race if other races missing + races + end + end +end diff --git a/app/models/dashboard/respondent.rb b/app/models/dashboard/respondent.rb new file mode 100644 index 0000000..2312f69 --- /dev/null +++ b/app/models/dashboard/respondent.rb @@ -0,0 +1,27 @@ +module Dashboard + class Respondent < ApplicationRecord + belongs_to :dashboard_school + belongs_to :dashboard_academic_year + + validates :school, uniqueness: { scope: :academic_year } + + def enrollment_by_grade + @enrollment_by_grade ||= {}.tap do |row| + attributes = %i[pk k one two three four five six seven eight nine ten eleven twelve] + grades = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + attributes.zip(grades).each do |attribute, grade| + count = send(attribute) if send(attribute).present? + row[grade] = count unless count.nil? || count.zero? + end + end + end + + def self.by_school_and_year(school:, academic_year:) + @by_school_and_year ||= Hash.new do |memo, (school, academic_year)| + memo[[school, academic_year]] = Respondent.find_by(school:, academic_year:) + end + + @by_school_and_year[[school, academic_year]] + end + end +end diff --git a/app/models/dashboard/response_rate.rb b/app/models/dashboard/response_rate.rb new file mode 100644 index 0000000..53a45fb --- /dev/null +++ b/app/models/dashboard/response_rate.rb @@ -0,0 +1,7 @@ +module Dashboard + class ResponseRate < ApplicationRecord + belongs_to :dashboard_subcategory + belongs_to :dashboard_school + belongs_to :dashboard_academic_year + end +end diff --git a/app/models/dashboard/response_rate_calculator.rb b/app/models/dashboard/response_rate_calculator.rb new file mode 100644 index 0000000..e9683a6 --- /dev/null +++ b/app/models/dashboard/response_rate_calculator.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Dashboard + class ResponseRateCalculator + TEACHER_RATE_THRESHOLD = 25 + STUDENT_RATE_THRESHOLD = 25 + attr_reader :subcategory, :school, :academic_year + + def initialize(subcategory:, school:, academic_year:) + @subcategory = subcategory + @school = school + @academic_year = academic_year + end + + def rate + return 100 if population_data_unavailable? + + return 0 unless survey_items_have_sufficient_responses? + + return 0 unless total_possible_responses.positive? + + cap_at_one_hundred(raw_response_rate).round + end + + def meets_student_threshold? + rate >= STUDENT_RATE_THRESHOLD + end + + def meets_teacher_threshold? + rate >= TEACHER_RATE_THRESHOLD + end + + private + + def cap_at_one_hundred(response_rate) + response_rate > 100 ? 100 : response_rate + end + + def average_responses_per_survey_item + response_count / survey_item_count.to_f + end + + def respondents + @respondents ||= Respondent.by_school_and_year(school:, academic_year:) + end + + def population_data_unavailable? + @population_data_unavailable ||= respondents.nil? + end + end +end diff --git a/app/models/dashboard/scale.rb b/app/models/dashboard/scale.rb new file mode 100644 index 0000000..f46a6c0 --- /dev/null +++ b/app/models/dashboard/scale.rb @@ -0,0 +1,44 @@ +module Dashboard + class Scale < ApplicationRecord + belongs_to :dashboard_measure + has_many :dashboard_survey_items + has_many :dashboard_survey_item_responses, through: :dashboard_survey_items + has_many :dashboard_admin_data_items + + def score(school:, academic_year:) + @score ||= Hash.new do |memo, (school, academic_year)| + memo[[school, academic_year]] = begin + items = [] + items << collect_survey_item_average(student_survey_items, school, academic_year) + items << collect_survey_item_average(teacher_survey_items, school, academic_year) + + items.remove_blanks.average + end + end + @score[[school, academic_year]] + end + + scope :teacher_scales, lambda { + where("scale_id LIKE 't-%'") + } + scope :student_scales, lambda { + where("scale_id LIKE 's-%'") + } + + private + + def collect_survey_item_average(survey_items, school, academic_year) + survey_items.map do |survey_item| + survey_item.score(school:, academic_year:) + end.remove_blanks.average + end + + def teacher_survey_items + survey_items.teacher_survey_items + end + + def student_survey_items + survey_items.student_survey_items + end + end +end diff --git a/app/models/dashboard/school.rb b/app/models/dashboard/school.rb new file mode 100644 index 0000000..c5588ee --- /dev/null +++ b/app/models/dashboard/school.rb @@ -0,0 +1,27 @@ +module Dashboard + class School < ApplicationRecord + belongs_to :district, class_name: "Dashboard::District" + + has_many :dashboard_survey_item_responses, dependent: :delete_all + + # validates :name, presence: true + + scope :alphabetic, -> { order(name: :asc) } + scope :school_hash, -> { all.map { |school| [school.dese_id, school] }.to_h } + + include FriendlyId + friendly_id :name, use: [:slugged] + + def self.find_by_district_code_and_school_code(district_code, school_code) + School + .joins(:district) + .where(districts: { qualtrics_code: district_code }) + .find_by_qualtrics_code(school_code) + end + + def grades(academic_year:) + @grades ||= Respondent.by_school_and_year(school: self, + academic_year:)&.enrollment_by_grade&.keys || (-1..12).to_a + end + end +end diff --git a/app/models/dashboard/score.rb b/app/models/dashboard/score.rb new file mode 100644 index 0000000..7dec3f4 --- /dev/null +++ b/app/models/dashboard/score.rb @@ -0,0 +1,28 @@ +module Dashboard + class Score < ApplicationRecord + belongs_to :dashboard_measure + belongs_to :dashboard_school + belongs_to :dashboard_academic_year + belongs_to :dashboard_race + + NIL_SCORE = Score.new(average: nil, meets_teacher_threshold: false, meets_student_threshold: false, + meets_admin_data_threshold: false) + + enum group: { + all_students: 0, + race: 1, + grade: 2, + gender: 3 + } + + def in_zone?(zone:) + return false if average.nil? || average.is_a?(Float) && average.nan? + + average.between?(zone.low_benchmark, zone.high_benchmark) + end + + def blank? + average.nil? || average.zero? || average.nan? + end + end +end diff --git a/app/models/dashboard/sped.rb b/app/models/dashboard/sped.rb new file mode 100644 index 0000000..e44eb11 --- /dev/null +++ b/app/models/dashboard/sped.rb @@ -0,0 +1,20 @@ +module Dashboard + class Sped < ApplicationRecord + scope :by_designation, -> { all.map { |sped| [sped.designation, sped] }.to_h } + + include FriendlyId + + friendly_id :designation, use: [:slugged] + + def self.to_designation(sped) + case sped + in /active/i + "Special Education" + in /^NA$|^#NA$/i + "Unknown" + else + "Not Special Education" + end + end + end +end diff --git a/app/models/dashboard/student.rb b/app/models/dashboard/student.rb new file mode 100644 index 0000000..8f769ab --- /dev/null +++ b/app/models/dashboard/student.rb @@ -0,0 +1,9 @@ +module Dashboard + class Student < ApplicationRecord + # has_many :dashboard_survey_item_responses + has_many :dashboard_student_races + has_and_belongs_to_many :races, join_table: :student_races + + encrypts :lasid, deterministic: true + end +end diff --git a/app/models/dashboard/student_race.rb b/app/models/dashboard/student_race.rb new file mode 100644 index 0000000..39f3d18 --- /dev/null +++ b/app/models/dashboard/student_race.rb @@ -0,0 +1,6 @@ +module Dashboard + class StudentRace < ApplicationRecord + belongs_to :dashboard_student + belongs_to :dashboard_race + end +end diff --git a/app/models/dashboard/student_response_rate_calculator.rb b/app/models/dashboard/student_response_rate_calculator.rb new file mode 100644 index 0000000..da563e8 --- /dev/null +++ b/app/models/dashboard/student_response_rate_calculator.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +class StudentResponseRateCalculator < ResponseRateCalculator + def raw_response_rate + rates_by_grade.values.length.positive? ? weighted_average : 0 + end + + def weighted_average + num_possible_responses = 0.0 + rates_by_grade.keys.map do |grade| + num_possible_responses += enrollment_by_grade[grade] + end + rates_by_grade.map do |grade, rate| + rate * (enrollment_by_grade[grade] / num_possible_responses) + end.sum + end + + def rates_by_grade + @rates_by_grade ||= enrollment_by_grade.map do |grade, num_of_students_in_grade| + responses = survey_items_with_sufficient_responses(grade:) + actual_response_count_for_grade = responses.values.sum.to_f + count_of_survey_items_with_sufficient_responses = responses.count + if actual_response_count_for_grade.nil? || count_of_survey_items_with_sufficient_responses.nil? || count_of_survey_items_with_sufficient_responses.zero? || num_of_students_in_grade.nil? || num_of_students_in_grade.zero? + next nil + end + + rate = actual_response_count_for_grade / count_of_survey_items_with_sufficient_responses / num_of_students_in_grade * 100 + [grade, rate] + end.compact.to_h + end + + def enrollment_by_grade + @enrollment_by_grade ||= respondents.enrollment_by_grade + end + + def survey_items_have_sufficient_responses? + rates_by_grade.values.length.positive? + end + + def survey_items_with_sufficient_responses(grade:) + @survey_items_with_sufficient_responses ||= Hash.new do |memo, grade| + threshold = 10 + quarter_of_grade = enrollment_by_grade[grade] / 4 + threshold = threshold > quarter_of_grade ? quarter_of_grade : threshold + + si = SurveyItemResponse.student_survey_items_with_sufficient_responses_by_grade(school:, + academic_year:) + si = si.reject do |_key, value| + value < threshold + end + + ssi = @subcategory.student_survey_items.map(&:id) + grade_array = Array.new(ssi.length, grade) + + memo[grade] = si.slice(*grade_array.zip(ssi)) + end + @survey_items_with_sufficient_responses[grade] + end + + def total_possible_responses + @total_possible_responses ||= begin + return 0 unless respondents.present? + + respondents.total_students + end + end +end diff --git a/app/models/dashboard/subcategory.rb b/app/models/dashboard/subcategory.rb new file mode 100644 index 0000000..38a04e6 --- /dev/null +++ b/app/models/dashboard/subcategory.rb @@ -0,0 +1,102 @@ +module Dashboard + class Subcategory < ApplicationRecord + belongs_to :dashboard_categories, class_name: "Dashboard::Category" + + has_many :dashboard_measures + has_many :dashboard_survey_items, through: :dashboard_measures + has_many :dashboard_admin_data_items, through: :dashboard_measures + has_many :dashboard_survey_items, through: :dashboard_measures + has_many :dashboard_scales, through: :dashboard_measures + + def score(school:, academic_year:) + measures.map do |measure| + measure.score(school:, academic_year:).average + end.remove_blanks.average + end + + def student_score(school:, academic_year:) + measures.map do |measure| + measure.student_score(school:, academic_year:).average + end.remove_blanks.average + end + + def teacher_score(school:, academic_year:) + measures.map do |measure| + measure.teacher_score(school:, academic_year:).average + end.remove_blanks.average + end + + def admin_score(school:, academic_year:) + measures.map do |measure| + measure.admin_score(school:, academic_year:).average + end.remove_blanks.average + end + + def response_rate(school:, academic_year:) + @response_rate ||= Hash.new do |memo, (school, academic_year)| + student = StudentResponseRateCalculator.new(subcategory: self, school:, academic_year:) + teacher = TeacherResponseRateCalculator.new(subcategory: self, school:, academic_year:) + memo[[school, academic_year]] = ResponseRate.new(school:, academic_year:, subcategory: self, student_response_rate: student.rate, + teacher_response_rate: teacher.rate, meets_student_threshold: student.meets_student_threshold?, + meets_teacher_threshold: teacher.meets_teacher_threshold?) + end + + @response_rate[[school, academic_year]] + end + + def warning_low_benchmark + 1 + end + + def watch_low_benchmark + @watch_low_benchmark ||= benchmark(:watch_low_benchmark) + end + + def growth_low_benchmark + @growth_low_benchmark ||= benchmark(:growth_low_benchmark) + end + + def approval_low_benchmark + @approval_low_benchmark ||= benchmark(:approval_low_benchmark) + end + + def ideal_low_benchmark + @ideal_low_benchmark ||= benchmark(:ideal_low_benchmark) + end + + def benchmark(name) + measures.map do |measure| + measure.benchmark(name) + end.average + end + + def zone(school:, academic_year:) + zone_for_score(score: score(school:, academic_year:)) + end + + def student_zone(school:, academic_year:) + zone_for_score(score: student_score(school:, academic_year:)) + end + + def teacher_zone(school:, academic_year:) + zone_for_score(score: teacher_score(school:, academic_year:)) + end + + def admin_zone(school:, academic_year:) + zone_for_score(score: admin_score(school:, academic_year:)) + end + + def zone_for_score(score:) + Zones.new(watch_low_benchmark:, growth_low_benchmark:, + approval_low_benchmark:, ideal_low_benchmark:).zone_for_score(score) + end + + def student_survey_items + @student_survey_items ||= survey_items.student_survey_items + end + + def teacher_survey_items + @teacher_survey_items ||= survey_items.teacher_survey_items + end + end +end diff --git a/app/models/dashboard/summary.rb b/app/models/dashboard/summary.rb new file mode 100644 index 0000000..2b0b9ad --- /dev/null +++ b/app/models/dashboard/summary.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Dashboard + class Summary < Struct.new(:id, :description, :available?) + end +end diff --git a/app/models/dashboard/survey_item.rb b/app/models/dashboard/survey_item.rb new file mode 100644 index 0000000..3a78e20 --- /dev/null +++ b/app/models/dashboard/survey_item.rb @@ -0,0 +1,78 @@ +module Dashboard + class SurveyItem < ApplicationRecord + belongs_to :dashboard_scale + + has_one :dashboard_measure, through: dashboard_scale + has_one :dashboard_subcategory, through: dashboard_measure + + has_many :dashboard_survey_item_responses + + def score(school:, academic_year:) + @score ||= Hash.new do |memo, (school, academic_year)| + memo[[school, academic_year]] = + survey_item_responses.exclude_boston.where(school:, academic_year:).average(:likert_score).to_f + end + @score[[school, academic_year]] + end + + scope :student_survey_items, lambda { + where("survey_items.survey_item_id LIKE 's-%'") + } + scope :standard_survey_items, lambda { + where("survey_items.survey_item_id LIKE 's-%-q%'") + } + scope :teacher_survey_items, lambda { + where("survey_items.survey_item_id LIKE 't-%'") + } + scope :short_form_survey_items, lambda { + where(on_short_form: true) + } + scope :early_education_survey_items, lambda { + where("survey_items.survey_item_id LIKE '%-%-es%'") + } + + scope :survey_items_for_grade, lambda { |school, academic_year, grade| + includes(:survey_item_responses) + .where("survey_item_responses.grade": grade, + "survey_item_responses.school": school, + "survey_item_responses.academic_year": academic_year).distinct + } + + scope :survey_item_ids_for_grade, lambda { |school, academic_year, grade| + survey_items_for_grade(school, academic_year, grade).pluck(:survey_item_id) + } + + scope :survey_items_for_grade_and_subcategory, lambda { |school, academic_year, grade, subcategory| + includes(:survey_item_responses) + .where( + survey_item_id: subcategory.survey_items.pluck(:survey_item_id), + "survey_item_responses.school": school, + "survey_item_responses.academic_year": academic_year, + "survey_item_responses.grade": grade + ) + } + + scope :survey_type_for_grade, lambda { |school, academic_year, grade| + survey_items_set_by_grade = survey_items_for_grade(school, academic_year, grade).pluck(:survey_item_id).to_set + if survey_items_set_by_grade.size > 0 && survey_items_set_by_grade.subset?(early_education_survey_items.pluck(:survey_item_id).to_set) + return :early_education + end + + :standard + } + + def description + Summary.new(survey_item_id, prompt, true) + end + + def self.survey_type(survey_item_ids:) + survey_item_ids = survey_item_ids.reject { |id| id.ends_with?("-1") }.to_set + return :short_form if survey_item_ids.subset? short_form_survey_items.map(&:survey_item_id).to_set + return :early_education if survey_item_ids.subset? early_education_survey_items.map(&:survey_item_id).to_set + return :teacher if survey_item_ids.subset? teacher_survey_items.map(&:survey_item_id).to_set + return :standard if survey_item_ids.subset? standard_survey_items.map(&:survey_item_id).to_set + + :unknown + end + end +end diff --git a/app/models/dashboard/survey_item_response.rb b/app/models/dashboard/survey_item_response.rb new file mode 100644 index 0000000..bec997c --- /dev/null +++ b/app/models/dashboard/survey_item_response.rb @@ -0,0 +1,85 @@ +module Dashboard + class SurveyItemResponse < ApplicationRecord + TEACHER_RESPONSE_THRESHOLD = 2 + STUDENT_RESPONSE_THRESHOLD = 10 + + belongs_to :dashboard_school + belongs_to :dashboard_survey_item + belongs_to :dashboard_academic_year + belongs_to :dashboard_student, optional: true + belongs_to :dashboard_gender, optional: true + belongs_to :dashboard_income, optional: true + belongs_to :dashboard_ell, optional: true + belongs_to :dashboard_sped, optional: true + + has_one :dashboard_measure, through: :dashboard_survey_item + + scope :exclude_boston, lambda { + includes(school: :district).where.not("district.name": "Boston") + } + + scope :averages_for_grade, lambda { |survey_items, school, academic_year, grade| + SurveyItemResponse.where(survey_item: survey_items, school:, + academic_year:, grade:).group(:survey_item).having("count(*) >= 10").average(:likert_score) + } + + scope :averages_for_gender, lambda { |survey_items, school, academic_year, gender| + SurveyItemResponse.where(survey_item: survey_items, school:, + academic_year:, gender:, grade: school.grades(academic_year:)).group(:survey_item).having("count(*) >= 10").average(:likert_score) + } + + scope :averages_for_income, lambda { |survey_items, school, academic_year, income| + SurveyItemResponse.where(survey_item: survey_items, school:, + academic_year:, income:, grade: school.grades(academic_year:)).group(:survey_item).having("count(*) >= 10").average(:likert_score) + } + + scope :averages_for_ell, lambda { |survey_items, school, academic_year, ell| + SurveyItemResponse.where(survey_item: survey_items, school:, + academic_year:, ell:, grade: school.grades(academic_year:)).group(:survey_item).having("count(*) >= 10").average(:likert_score) + } + + scope :averages_for_sped, lambda { |survey_items, school, academic_year, sped| + SurveyItemResponse.where(survey_item: survey_items, school:, + academic_year:, sped:, grade: school.grades(academic_year:)).group(:survey_item).having("count(*) >= 10").average(:likert_score) + } + + scope :averages_for_race, lambda { |school, academic_year, race| + SurveyItemResponse.joins("JOIN student_races on survey_item_responses.student_id = student_races.student_id JOIN students on students.id = student_races.student_id").where( + school:, academic_year:, grade: school.grades(academic_year:) + ).where("student_races.race_id": race.id).group(:survey_item_id).having("count(*) >= 10").average(:likert_score) + } + + def self.grouped_responses(school:, academic_year:) + @grouped_responses ||= Hash.new do |memo, (school, academic_year)| + memo[[school, academic_year]] = + SurveyItemResponse.where(school:, academic_year:).group(:survey_item_id).average(:likert_score) + end + @grouped_responses[[school, academic_year]] + end + + def self.teacher_survey_items_with_sufficient_responses(school:, academic_year:) + @teacher_survey_items_with_sufficient_responses ||= Hash.new do |memo, (school, academic_year)| + hash = SurveyItem.joins("inner join survey_item_responses on survey_item_responses.survey_item_id = survey_items.id") + .teacher_survey_items + .where("survey_item_responses.school": school, "survey_item_responses.academic_year": academic_year) + .group("survey_items.id") + .having("count(*) > 0").count + memo[[school, academic_year]] = hash + end + @teacher_survey_items_with_sufficient_responses[[school, academic_year]] + end + + def self.student_survey_items_with_sufficient_responses_by_grade(school:, academic_year:) + @student_survey_items_with_sufficient_responses_by_grade ||= Hash.new do |memo, (school, academic_year)| + hash = SurveyItem.joins("inner join survey_item_responses on survey_item_responses.survey_item_id = survey_items.id") + .student_survey_items + .where("survey_item_responses.school": school, "survey_item_responses.academic_year": academic_year) + .group(:grade, :id) + .count + memo[[school, academic_year]] = hash + end + + @student_survey_items_with_sufficient_responses_by_grade[[school, academic_year]] + end + end +end diff --git a/app/models/dashboard/teacher_response_rate_calculator.rb b/app/models/dashboard/teacher_response_rate_calculator.rb new file mode 100644 index 0000000..7c73943 --- /dev/null +++ b/app/models/dashboard/teacher_response_rate_calculator.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Dashboard + class TeacherResponseRateCalculator < ResponseRateCalculator + def survey_item_count + @survey_item_count ||= survey_items_with_sufficient_responses.length + end + + def survey_items_have_sufficient_responses? + survey_item_count.positive? + end + + def survey_items_with_sufficient_responses + @survey_items_with_sufficient_responses ||= SurveyItemResponse.teacher_survey_items_with_sufficient_responses( + school:, academic_year: + ).slice(*@subcategory.teacher_survey_items.map(&:id)) + end + + def response_count + @response_count ||= survey_items_with_sufficient_responses&.values&.sum + end + + def total_possible_responses + @total_possible_responses ||= respondents.total_teachers + end + + def raw_response_rate + (average_responses_per_survey_item / total_possible_responses.to_f * 100).round + end + end +end diff --git a/app/presenters/dashboard/admin_data_presenter.rb b/app/presenters/dashboard/admin_data_presenter.rb new file mode 100644 index 0000000..30881f0 --- /dev/null +++ b/app/presenters/dashboard/admin_data_presenter.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Dashboard + class AdminDataPresenter < DataItemPresenter + def initialize(measure_id:, admin_data_items:, has_sufficient_data:, school:, academic_year:) + super(measure_id:, has_sufficient_data:, school:, academic_year:) + @admin_data_items = admin_data_items + end + + def title + "School admin data" + end + + def id + "admin-data-items-#{@measure_id}" + end + + def reason_for_insufficiency + "limited availability" + end + + def descriptions_and_availability + @admin_data_items.map do |admin_data_item| + Summary.new(admin_data_item.admin_data_item_id, admin_data_item.description, + admin_data_item.admin_data_values.where(school:, academic_year:).count > 0) + end + end + end +end diff --git a/app/presenters/dashboard/analyze/bar_presenter.rb b/app/presenters/dashboard/analyze/bar_presenter.rb new file mode 100644 index 0000000..7dc9bdc --- /dev/null +++ b/app/presenters/dashboard/analyze/bar_presenter.rb @@ -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 diff --git a/app/presenters/dashboard/analyze/graph/all_data.rb b/app/presenters/dashboard/analyze/graph/all_data.rb new file mode 100644 index 0000000..3d6e738 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/all_data.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Analyze + module Graph + class AllData + include Analyze::Graph::Column + def to_s + "All Data" + end + + def slug + "all-data" + end + + def columns + [AllStudent, AllTeacher, AllAdmin, GroupedBarColumnPresenter] + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/all_admin.rb b/app/presenters/dashboard/analyze/graph/column/all_admin.rb new file mode 100644 index 0000000..d63c927 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/all_admin.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + class AllAdmin < GroupedBarColumnPresenter + def label + %w[All Admin] + end + + def basis + "admin data" + end + + def show_irrelevancy_message? + !measure.includes_admin_data_items? + end + + def show_insufficient_data_message? + !academic_years.any? do |year| + measure.sufficient_admin_data?(school:, academic_year: year) + end + end + + def insufficiency_message + ["data not", "available"] + end + + def score(year_index) + measure.admin_score(school:, academic_year: academic_years[year_index]) + end + + def type + :admin + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/all_student.rb b/app/presenters/dashboard/analyze/graph/column/all_student.rb new file mode 100644 index 0000000..788dca3 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/all_student.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + class AllStudent < GroupedBarColumnPresenter + def label + %w[All Students] + end + + def show_irrelevancy_message? + !measure.includes_student_survey_items? + end + + def show_insufficient_data_message? + scores = academic_years.map do |year| + measure.score(school:, academic_year: year) + end + + scores.all? { |score| !score.meets_student_threshold? } + end + + def score(year_index) + measure.student_score(school:, academic_year: academic_years[year_index]) + end + + def type + :student + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/all_survey_data.rb b/app/presenters/dashboard/analyze/graph/column/all_survey_data.rb new file mode 100644 index 0000000..c3e42c9 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/all_survey_data.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + class AllSurveyData < GroupedBarColumnPresenter + def label + %w[Survey Data] + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + scores = academic_years.map do |year| + combined_score(school:, academic_year: year) + end + + scores.all? { |score| !score.meets_student_threshold? && !score.meets_teacher_threshold? } + end + + def score(year_index) + combined_score(school:, academic_year: academic_years[year_index]) + end + + def type + :all_survey_data + end + + private + + def combined_score(school:, academic_year:) + teacher_score = measure.teacher_score(school:, academic_year:) + student_score = measure.student_score(school:, academic_year:) + + averages = [] + averages << student_score.average unless student_score.average.nil? + averages << teacher_score.average unless teacher_score.average.nil? + average = averages.average if averages.length > 0 + combined_score = Score.new(average:, meets_student_threshold: student_score.meets_student_threshold, + meets_teacher_threshold: teacher_score.meets_teacher_threshold) + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/all_teacher.rb b/app/presenters/dashboard/analyze/graph/column/all_teacher.rb new file mode 100644 index 0000000..dc3a844 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/all_teacher.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + class AllTeacher < GroupedBarColumnPresenter + def label + %w[All Teachers] + end + + def basis + "teacher surveys" + end + + def show_irrelevancy_message? + !measure.includes_teacher_survey_items? + end + + def show_insufficient_data_message? + scores = academic_years.map do |year| + measure.score(school:, academic_year: year) + end + + scores.all? { |score| !score.meets_teacher_threshold? } + end + + def score(year_index) + measure.teacher_score(school:, academic_year: academic_years[year_index]) + end + + def type + :teacher + end + + def n_size(year_index) + SurveyItemResponse.where(survey_item: measure.teacher_survey_items, school:, + academic_year: academic_years[year_index]).select(:response_id).distinct.count + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/ell_column/ell.rb b/app/presenters/dashboard/analyze/graph/column/ell_column/ell.rb new file mode 100644 index 0000000..cba0740 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/ell_column/ell.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module EllColumn + class Ell < GroupedBarColumnPresenter + include Analyze::Graph::Column::EllColumn::ScoreForEll + include Analyze::Graph::Column::EllColumn::EllCount + def label + %w[ELL] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def ell + ::Ell.find_by_slug "ell" + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/ell_column/ell_count.rb b/app/presenters/dashboard/analyze/graph/column/ell_column/ell_count.rb new file mode 100644 index 0000000..fa20518 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/ell_column/ell_count.rb @@ -0,0 +1,18 @@ +module Analyze + module Graph + module Column + module EllColumn + module EllCount + def type + :student + end + + def n_size(year_index) + SurveyItemResponse.where(ell:, survey_item: measure.student_survey_items, school:, grade: grades(year_index), + academic_year: academic_years[year_index]).select(:response_id).distinct.count + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/ell_column/not_ell.rb b/app/presenters/dashboard/analyze/graph/column/ell_column/not_ell.rb new file mode 100644 index 0000000..1c3ca59 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/ell_column/not_ell.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module EllColumn + class NotEll < GroupedBarColumnPresenter + include Analyze::Graph::Column::EllColumn::ScoreForEll + include Analyze::Graph::Column::EllColumn::EllCount + def label + ["Not ELL"] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def ell + ::Ell.find_by_slug "not-ell" + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/ell_column/score_for_ell.rb b/app/presenters/dashboard/analyze/graph/column/ell_column/score_for_ell.rb new file mode 100644 index 0000000..cd39cd0 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/ell_column/score_for_ell.rb @@ -0,0 +1,42 @@ +module Analyze + module Graph + module Column + module EllColumn + module ScoreForEll + def score(year_index) + academic_year = academic_years[year_index] + meets_student_threshold = sufficient_student_responses?(academic_year:) + return Score::NIL_SCORE unless meets_student_threshold + + averages = SurveyItemResponse.averages_for_ell(measure.student_survey_items, school, academic_year, + ell) + average = bubble_up_averages(averages:).round(2) + + Score.new(average:, + meets_teacher_threshold: false, + meets_student_threshold:, + meets_admin_data_threshold: false) + end + + def bubble_up_averages(averages:) + measure.student_scales.map do |scale| + scale.survey_items.map do |survey_item| + averages[survey_item] + end.remove_blanks.average + end.remove_blanks.average + end + + def sufficient_student_responses?(academic_year:) + return false unless measure.subcategory.response_rate(school:, academic_year:).meets_student_threshold? + + yearly_counts = SurveyItemResponse.where(school:, academic_year:, + ell:, survey_item: measure.student_survey_items).group(:ell).select(:response_id).distinct(:response_id).count + yearly_counts.any? do |count| + count[1] >= 10 + end + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/ell_column/unknown.rb b/app/presenters/dashboard/analyze/graph/column/ell_column/unknown.rb new file mode 100644 index 0000000..37f077a --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/ell_column/unknown.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module EllColumn + class Unknown < GroupedBarColumnPresenter + include Analyze::Graph::Column::EllColumn::ScoreForEll + include Analyze::Graph::Column::EllColumn::EllCount + def label + %w[Unknown] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def ell + ::Ell.find_by_slug "unknown" + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/gender_column/female.rb b/app/presenters/dashboard/analyze/graph/column/gender_column/female.rb new file mode 100644 index 0000000..54941fb --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/gender_column/female.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module GenderColumn + class Female < GroupedBarColumnPresenter + include Analyze::Graph::Column::GenderColumn::ScoreForGender + include Analyze::Graph::Column::GenderColumn::GenderCount + def label + %w[Female] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def gender + ::Gender.find_by_qualtrics_code 1 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/gender_column/gender_count.rb b/app/presenters/dashboard/analyze/graph/column/gender_column/gender_count.rb new file mode 100644 index 0000000..9746024 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/gender_column/gender_count.rb @@ -0,0 +1,18 @@ +module Analyze + module Graph + module Column + module GenderColumn + module GenderCount + def type + :student + end + + def n_size(year_index) + SurveyItemResponse.where(gender:, survey_item: measure.student_survey_items, school:, grade: grades(year_index), + academic_year: academic_years[year_index]).select(:response_id).distinct.count + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/gender_column/male.rb b/app/presenters/dashboard/analyze/graph/column/gender_column/male.rb new file mode 100644 index 0000000..031b91e --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/gender_column/male.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module GenderColumn + class Male < GroupedBarColumnPresenter + include Analyze::Graph::Column::GenderColumn::ScoreForGender + include Analyze::Graph::Column::GenderColumn::GenderCount + def label + %w[Male] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def gender + ::Gender.find_by_qualtrics_code 2 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/gender_column/non_binary.rb b/app/presenters/dashboard/analyze/graph/column/gender_column/non_binary.rb new file mode 100644 index 0000000..8b3c854 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/gender_column/non_binary.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module GenderColumn + class NonBinary < GroupedBarColumnPresenter + include Analyze::Graph::Column::GenderColumn::ScoreForGender + include Analyze::Graph::Column::GenderColumn::GenderCount + def label + %w[Non-Binary] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def gender + ::Gender.find_by_qualtrics_code 4 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/gender_column/score_for_gender.rb b/app/presenters/dashboard/analyze/graph/column/gender_column/score_for_gender.rb new file mode 100644 index 0000000..2b5e22c --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/gender_column/score_for_gender.rb @@ -0,0 +1,42 @@ +module Analyze + module Graph + module Column + module GenderColumn + module ScoreForGender + def score(year_index) + academic_year = academic_years[year_index] + meets_student_threshold = sufficient_student_responses?(academic_year:) + return Score::NIL_SCORE unless meets_student_threshold + + averages = SurveyItemResponse.averages_for_gender(measure.student_survey_items, school, academic_year, + gender) + average = bubble_up_averages(averages:).round(2) + + Score.new(average:, + meets_teacher_threshold: false, + meets_student_threshold:, + meets_admin_data_threshold: false) + end + + def bubble_up_averages(averages:) + measure.student_scales.map do |scale| + scale.survey_items.map do |survey_item| + averages[survey_item] + end.remove_blanks.average + end.remove_blanks.average + end + + def sufficient_student_responses?(academic_year:) + return false unless measure.subcategory.response_rate(school:, academic_year:).meets_student_threshold? + + yearly_counts = SurveyItemResponse.where(school:, academic_year:, + gender:, survey_item: measure.student_survey_items).group(:gender).select(:response_id).distinct(:response_id).count + yearly_counts.any? do |count| + count[1] >= 10 + end + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/gender_column/unknown.rb b/app/presenters/dashboard/analyze/graph/column/gender_column/unknown.rb new file mode 100644 index 0000000..d954cec --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/gender_column/unknown.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module GenderColumn + class Unknown < GroupedBarColumnPresenter + include Analyze::Graph::Column::GenderColumn::ScoreForGender + include Analyze::Graph::Column::GenderColumn::GenderCount + def label + %w[Unknown] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def gender + ::Gender.find_by_qualtrics_code 99 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/grade/eight.rb b/app/presenters/dashboard/analyze/graph/column/grade/eight.rb new file mode 100644 index 0000000..8d4bf62 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/grade/eight.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module Grade + class Eight < GroupedBarColumnPresenter + include Analyze::Graph::Column::Grade::ScoreForGrade + include Analyze::Graph::Column::Grade::GradeCount + def label + %w[Grade 8] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def grade + 8 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/grade/eleven.rb b/app/presenters/dashboard/analyze/graph/column/grade/eleven.rb new file mode 100644 index 0000000..dba1136 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/grade/eleven.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module Grade + class Eleven < GroupedBarColumnPresenter + include Analyze::Graph::Column::Grade::ScoreForGrade + include Analyze::Graph::Column::Grade::GradeCount + def label + %w[Grade 11] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def grade + 11 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/grade/five.rb b/app/presenters/dashboard/analyze/graph/column/grade/five.rb new file mode 100644 index 0000000..30033e3 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/grade/five.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module Grade + class Five < GroupedBarColumnPresenter + include Analyze::Graph::Column::Grade::ScoreForGrade + include Analyze::Graph::Column::Grade::GradeCount + def label + %w[Grade 5] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def grade + 5 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/grade/four.rb b/app/presenters/dashboard/analyze/graph/column/grade/four.rb new file mode 100644 index 0000000..1f56374 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/grade/four.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module Grade + class Four < GroupedBarColumnPresenter + include Analyze::Graph::Column::Grade::ScoreForGrade + include Analyze::Graph::Column::Grade::GradeCount + def label + %w[Grade 4] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def grade + 4 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/grade/grade_count.rb b/app/presenters/dashboard/analyze/graph/column/grade/grade_count.rb new file mode 100644 index 0000000..42ed3a2 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/grade/grade_count.rb @@ -0,0 +1,18 @@ +module Analyze + module Graph + module Column + module Grade + module GradeCount + def type + :student + end + + def n_size(year_index) + SurveyItemResponse.where(grade:, survey_item: measure.student_survey_items, school:, + academic_year: academic_years[year_index]).select(:response_id).distinct.count + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/grade/nine.rb b/app/presenters/dashboard/analyze/graph/column/grade/nine.rb new file mode 100644 index 0000000..9c3a939 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/grade/nine.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module Grade + class Nine < GroupedBarColumnPresenter + include Analyze::Graph::Column::Grade::ScoreForGrade + include Analyze::Graph::Column::Grade::GradeCount + def label + %w[Grade 9] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def grade + 9 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/grade/one.rb b/app/presenters/dashboard/analyze/graph/column/grade/one.rb new file mode 100644 index 0000000..b1e3bcd --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/grade/one.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module Grade + class One < GroupedBarColumnPresenter + include Analyze::Graph::Column::Grade::ScoreForGrade + include Analyze::Graph::Column::Grade::GradeCount + def label + %w[Grade 1] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def grade + 1 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/grade/score_for_grade.rb b/app/presenters/dashboard/analyze/graph/column/grade/score_for_grade.rb new file mode 100644 index 0000000..812c9cb --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/grade/score_for_grade.rb @@ -0,0 +1,42 @@ +module Analyze + module Graph + module Column + module Grade + module ScoreForGrade + def score(year_index) + academic_year = academic_years[year_index] + meets_student_threshold = sufficient_student_responses?(academic_year:) + return Score::NIL_SCORE unless meets_student_threshold + + averages = SurveyItemResponse.averages_for_grade(measure.student_survey_items, school, + academic_year, grade) + average = bubble_up_averages(averages:).round(2) + + Score.new(average:, + meets_teacher_threshold: false, + meets_student_threshold:, + meets_admin_data_threshold: false) + end + + def bubble_up_averages(averages:) + measure.student_scales.map do |scale| + scale.survey_items.map do |survey_item| + averages[survey_item] + end.remove_blanks.average + end.remove_blanks.average + end + + def sufficient_student_responses?(academic_year:) + return false unless measure.subcategory.response_rate(school:, academic_year:).meets_student_threshold? + + yearly_counts = SurveyItemResponse.where(school:, academic_year:, + survey_item: measure.student_survey_items).group(:grade).select(:response_id).distinct(:response_id).count + yearly_counts.any? do |count| + count[1] >= 10 + end + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/grade/seven.rb b/app/presenters/dashboard/analyze/graph/column/grade/seven.rb new file mode 100644 index 0000000..6aa5a81 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/grade/seven.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module Grade + class Seven < GroupedBarColumnPresenter + include Analyze::Graph::Column::Grade::ScoreForGrade + include Analyze::Graph::Column::Grade::GradeCount + def label + %w[Grade 7] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def grade + 7 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/grade/six.rb b/app/presenters/dashboard/analyze/graph/column/grade/six.rb new file mode 100644 index 0000000..1e40703 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/grade/six.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module Grade + class Six < GroupedBarColumnPresenter + include Analyze::Graph::Column::Grade::ScoreForGrade + include Analyze::Graph::Column::Grade::GradeCount + def label + %w[Grade 6] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def grade + 6 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/grade/ten.rb b/app/presenters/dashboard/analyze/graph/column/grade/ten.rb new file mode 100644 index 0000000..45839d3 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/grade/ten.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module Grade + class Ten < GroupedBarColumnPresenter + include Analyze::Graph::Column::Grade::ScoreForGrade + include Analyze::Graph::Column::Grade::GradeCount + def label + %w[Grade 10] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def grade + 10 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/grade/three.rb b/app/presenters/dashboard/analyze/graph/column/grade/three.rb new file mode 100644 index 0000000..44a3277 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/grade/three.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module Grade + class Three < GroupedBarColumnPresenter + include Analyze::Graph::Column::Grade::ScoreForGrade + include Analyze::Graph::Column::Grade::GradeCount + def label + %w[Grade 3] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def grade + 3 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/grade/twelve.rb b/app/presenters/dashboard/analyze/graph/column/grade/twelve.rb new file mode 100644 index 0000000..0a8f7a2 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/grade/twelve.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module Grade + class Twelve < GroupedBarColumnPresenter + include Analyze::Graph::Column::Grade::ScoreForGrade + include Analyze::Graph::Column::Grade::GradeCount + def label + %w[Grade 12] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def grade + 12 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/grade/two.rb b/app/presenters/dashboard/analyze/graph/column/grade/two.rb new file mode 100644 index 0000000..195661e --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/grade/two.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module Grade + class Two < GroupedBarColumnPresenter + include Analyze::Graph::Column::Grade::ScoreForGrade + include Analyze::Graph::Column::Grade::GradeCount + def label + %w[Grade 2] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def grade + 2 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/grade/zero.rb b/app/presenters/dashboard/analyze/graph/column/grade/zero.rb new file mode 100644 index 0000000..611f9d3 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/grade/zero.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module Grade + class Zero < GroupedBarColumnPresenter + include Analyze::Graph::Column::Grade::ScoreForGrade + include Analyze::Graph::Column::Grade::GradeCount + def label + %w[Kindergarten] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def grade + 0 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/grouped_bar_column_presenter.rb b/app/presenters/dashboard/analyze/graph/column/grouped_bar_column_presenter.rb new file mode 100644 index 0000000..d38563e --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/grouped_bar_column_presenter.rb @@ -0,0 +1,172 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + class GroupedBarColumnPresenter + include AnalyzeHelper + + attr_reader :measure_name, :measure_id, :category, :position, :measure, :school, :academic_years, + :number_of_columns + + def initialize(measure:, school:, academic_years:, position:, number_of_columns:) + @measure = measure + @measure_name = @measure.name + @measure_id = @measure.measure_id + @category = @measure.subcategory.category + @school = school + @academic_years = academic_years + @position = position + @number_of_columns = number_of_columns + end + + def academic_year_for_year_index(year_index) + academic_years[year_index] + end + + def score(year_index) + measure.score(school:, academic_year: academic_years[year_index]) || 0 + end + + def bars + @bars ||= yearly_scores.map.each_with_index do |yearly_score, index| + year = yearly_score.year + Analyze::BarPresenter.new(measure:, academic_year: year, + score: yearly_score.score, + x_position: bar_x(index), + color: bar_color(year)) + end + end + + def label + %w[All Data] + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + scores = academic_years.map do |year| + measure.score(school:, academic_year: year) + end + + scores.all? do |score| + !score.meets_teacher_threshold? && !score.meets_student_threshold? && !score.meets_admin_data_threshold? + end + end + + def column_midpoint + zone_label_width + (grouped_chart_column_width * (position + 1)) - (grouped_chart_column_width / 2) + end + + def bar_width + min_bar_width(10.5 / data_sources) + end + + def min_bar_width(number) + min_width = 2 + number < min_width ? min_width : number + end + + def message_x + column_midpoint - message_width / 2 + end + + def message_y + 17 + end + + def message_width + 20 + end + + def message_height + 34 + end + + def column_end_x + zone_label_width + (grouped_chart_column_width * (position + 1)) + end + + def column_start_x + zone_label_width + (grouped_chart_column_width * position) + end + + def grouped_chart_column_width + graph_width / data_sources + end + + def bar_label_height + (100 - ((100 - analyze_graph_height) / 2)) + end + + def data_sources + number_of_columns + end + + def basis + "student surveys" + end + + def type + :all_data + end + + def show_popover? + %i[student teacher].include? type + end + + def n_size(year_index) + SurveyItemResponse.where(survey_item: measure.student_survey_items, school:, grade: grades(year_index), + academic_year: academic_years[year_index]).select(:response_id).distinct.count + end + + def popover_content(year_index) + "#{n_size(year_index)} #{type.to_s.capitalize}s" + end + + def insufficiency_message + ["survey response", "rate below 25%"] + end + + def sufficient_data?(year_index) + case basis + when "student" + score(year_index).meets_student_threshold + when "teacher" + score(year_index).meets_teacher_threshold + else + true + end + end + + def grades(year_index) + Respondent.by_school_and_year(school:, academic_year: academic_years[year_index]).enrollment_by_grade.keys + end + + private + + YearlyScore = Struct.new(:year, :score) + def yearly_scores + yearly_scores = academic_years.each_with_index.map do |year, index| + YearlyScore.new(year, score(index)) + end + yearly_scores.reject do |yearly_score| + yearly_score.score.blank? + end + end + + def bar_color(year) + @available_academic_years ||= AcademicYear.order(:range).all + colors[@available_academic_years.find_index(year)] + end + + def bar_x(index) + column_start_x + (index * bar_width * 1.2) + + ((column_end_x - column_start_x) - (yearly_scores.size * bar_width * 1.2)) / 2 + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/income_column/disadvantaged.rb b/app/presenters/dashboard/analyze/graph/column/income_column/disadvantaged.rb new file mode 100644 index 0000000..6765d64 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/income_column/disadvantaged.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module IncomeColumn + class Disadvantaged < GroupedBarColumnPresenter + include Analyze::Graph::Column::IncomeColumn::ScoreForIncome + include Analyze::Graph::Column::IncomeColumn::IncomeCount + def label + %w[Economically Disadvantaged] + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def income + Income.find_by_designation "Economically Disadvantaged - Y" + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/income_column/income_count.rb b/app/presenters/dashboard/analyze/graph/column/income_column/income_count.rb new file mode 100644 index 0000000..642f730 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/income_column/income_count.rb @@ -0,0 +1,18 @@ +module Analyze + module Graph + module Column + module IncomeColumn + module IncomeCount + def type + :student + end + + def n_size(year_index) + SurveyItemResponse.where(income:, survey_item: measure.student_survey_items, school:, grade: grades(year_index), + academic_year: academic_years[year_index]).select(:response_id).distinct.count + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/income_column/not_disadvantaged.rb b/app/presenters/dashboard/analyze/graph/column/income_column/not_disadvantaged.rb new file mode 100644 index 0000000..cfca2ba --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/income_column/not_disadvantaged.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module IncomeColumn + class NotDisadvantaged < GroupedBarColumnPresenter + include Analyze::Graph::Column::IncomeColumn::ScoreForIncome + include Analyze::Graph::Column::IncomeColumn::IncomeCount + def label + ["Not Economically", "Disadvantaged"] + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def income + Income.find_by_designation "Economically Disadvantaged - N" + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/income_column/score_for_income.rb b/app/presenters/dashboard/analyze/graph/column/income_column/score_for_income.rb new file mode 100644 index 0000000..52a2324 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/income_column/score_for_income.rb @@ -0,0 +1,42 @@ +module Analyze + module Graph + module Column + module IncomeColumn + module ScoreForIncome + def score(year_index) + academic_year = academic_years[year_index] + meets_student_threshold = sufficient_student_responses?(academic_year:) + return Score::NIL_SCORE unless meets_student_threshold + + averages = SurveyItemResponse.averages_for_income(measure.student_survey_items, school, academic_year, + income) + average = bubble_up_averages(averages:).round(2) + + Score.new(average:, + meets_teacher_threshold: false, + meets_student_threshold:, + meets_admin_data_threshold: false) + end + + def bubble_up_averages(averages:) + measure.student_scales.map do |scale| + scale.survey_items.map do |survey_item| + averages[survey_item] + end.remove_blanks.average + end.remove_blanks.average + end + + def sufficient_student_responses?(academic_year:) + return false unless measure.subcategory.response_rate(school:, academic_year:).meets_student_threshold? + + yearly_counts = SurveyItemResponse.where(school:, academic_year:, + income:, survey_item: measure.student_survey_items).group(:income).select(:response_id).distinct(:response_id).count + yearly_counts.any? do |count| + count[1] >= 10 + end + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/income_column/unknown.rb b/app/presenters/dashboard/analyze/graph/column/income_column/unknown.rb new file mode 100644 index 0000000..7a03580 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/income_column/unknown.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module IncomeColumn + class Unknown < GroupedBarColumnPresenter + include Analyze::Graph::Column::IncomeColumn::ScoreForIncome + include Analyze::Graph::Column::IncomeColumn::IncomeCount + def label + ["Unknown"] + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def income + Income.find_by_designation "Unknown" + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/race_column/american_indian.rb b/app/presenters/dashboard/analyze/graph/column/race_column/american_indian.rb new file mode 100644 index 0000000..b5d2af8 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/race_column/american_indian.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module RaceColumn + class AmericanIndian < GroupedBarColumnPresenter + include Analyze::Graph::Column::ScoreForRace + include Analyze::Graph::Column::RaceColumn::RaceCount + def label + %w[American Indian] + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def race + Race.find_by_qualtrics_code 1 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/race_column/asian.rb b/app/presenters/dashboard/analyze/graph/column/race_column/asian.rb new file mode 100644 index 0000000..7b0af7f --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/race_column/asian.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module RaceColumn + class Asian < GroupedBarColumnPresenter + include Analyze::Graph::Column::ScoreForRace + include Analyze::Graph::Column::RaceColumn::RaceCount + def label + %w[Asian] + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def race + Race.find_by_qualtrics_code 2 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/race_column/black.rb b/app/presenters/dashboard/analyze/graph/column/race_column/black.rb new file mode 100644 index 0000000..72046c9 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/race_column/black.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module RaceColumn + class Black < GroupedBarColumnPresenter + include Analyze::Graph::Column::ScoreForRace + include Analyze::Graph::Column::RaceColumn::RaceCount + def label + %w[Black] + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def race + Race.find_by_qualtrics_code 3 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/race_column/hispanic.rb b/app/presenters/dashboard/analyze/graph/column/race_column/hispanic.rb new file mode 100644 index 0000000..6f92a77 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/race_column/hispanic.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module RaceColumn + class Hispanic < GroupedBarColumnPresenter + include Analyze::Graph::Column::ScoreForRace + include Analyze::Graph::Column::RaceColumn::RaceCount + def label + %w[Hispanic] + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def race + Race.find_by_qualtrics_code 4 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/race_column/middle_eastern.rb b/app/presenters/dashboard/analyze/graph/column/race_column/middle_eastern.rb new file mode 100644 index 0000000..36955bf --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/race_column/middle_eastern.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module RaceColumn + class MiddleEastern < GroupedBarColumnPresenter + include Analyze::Graph::Column::ScoreForRace + include Analyze::Graph::Column::RaceColumn::RaceCount + + def label + %w[Middle Eastern] + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def race + Race.find_by_qualtrics_code 8 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/race_column/multiracial.rb b/app/presenters/dashboard/analyze/graph/column/race_column/multiracial.rb new file mode 100644 index 0000000..c150cea --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/race_column/multiracial.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module RaceColumn + class Multiracial < GroupedBarColumnPresenter + include Analyze::Graph::Column::ScoreForRace + include Analyze::Graph::Column::RaceColumn::RaceCount + def label + %w[Multiracial] + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def race + Race.find_by_qualtrics_code 100 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/race_column/race_count.rb b/app/presenters/dashboard/analyze/graph/column/race_column/race_count.rb new file mode 100644 index 0000000..b46d3de --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/race_column/race_count.rb @@ -0,0 +1,20 @@ +module Analyze + module Graph + module Column + module RaceColumn + module RaceCount + def type + :student + end + + def n_size(year_index) + SurveyItemResponse.joins("JOIN student_races on survey_item_responses.student_id = student_races.student_id JOIN students on students.id = student_races.student_id").where( + school:, academic_year: academic_years[year_index], + survey_item: measure.student_survey_items + ).where("student_races.race_id": race.id).select(:response_id).distinct.count + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/race_column/unknown.rb b/app/presenters/dashboard/analyze/graph/column/race_column/unknown.rb new file mode 100644 index 0000000..8c6b786 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/race_column/unknown.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module RaceColumn + class Unknown < GroupedBarColumnPresenter + include Analyze::Graph::Column::ScoreForRace + include Analyze::Graph::Column::RaceColumn::RaceCount + def label + %w[Not Listed] + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def race + Race.find_by_qualtrics_code 99 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/race_column/white.rb b/app/presenters/dashboard/analyze/graph/column/race_column/white.rb new file mode 100644 index 0000000..221ecc8 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/race_column/white.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module RaceColumn + class White < GroupedBarColumnPresenter + include Analyze::Graph::Column::ScoreForRace + include Analyze::Graph::Column::RaceColumn::RaceCount + def label + %w[White] + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def race + Race.find_by_qualtrics_code 5 + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/score_for_race.rb b/app/presenters/dashboard/analyze/graph/column/score_for_race.rb new file mode 100644 index 0000000..5fdc2e9 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/score_for_race.rb @@ -0,0 +1,40 @@ +module Analyze + module Graph + module Column + module ScoreForRace + def score(year_index) + academic_year = academic_years[year_index] + meets_student_threshold = sufficient_student_responses?(academic_year:) + return Score::NIL_SCORE unless meets_student_threshold + + survey_items = measure.student_survey_items + + averages = SurveyItemResponse.averages_for_race(school, academic_year, race) + average = bubble_up_averages(averages:).round(2) + + Score.new(average:, + meets_teacher_threshold: false, + meets_student_threshold:, + meets_admin_data_threshold: false) + end + + def bubble_up_averages(averages:) + measure.student_scales.map do |scale| + scale.survey_items.map do |survey_item| + averages[survey_item.id] + end.remove_blanks.average + end.remove_blanks.average + end + + def sufficient_student_responses?(academic_year:) + return false unless measure.subcategory.response_rate(school:, academic_year:).meets_student_threshold? + + number_of_students_for_a_racial_group = SurveyItemResponse.joins("JOIN student_races on survey_item_responses.student_id = student_races.student_id JOIN students on students.id = student_races.student_id").where( + school:, academic_year: + ).where("student_races.race_id": race.id).distinct.pluck(:student_id).count + number_of_students_for_a_racial_group >= 10 + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/sped_column/not_sped.rb b/app/presenters/dashboard/analyze/graph/column/sped_column/not_sped.rb new file mode 100644 index 0000000..cd4ece2 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/sped_column/not_sped.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module SpedColumn + class NotSped < GroupedBarColumnPresenter + include Analyze::Graph::Column::SpedColumn::ScoreForSped + include Analyze::Graph::Column::SpedColumn::SpedCount + + def label + ["Not Special", "Education"] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def sped + ::Sped.find_by_slug "not-special-education" + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/sped_column/score_for_sped.rb b/app/presenters/dashboard/analyze/graph/column/sped_column/score_for_sped.rb new file mode 100644 index 0000000..4727e37 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/sped_column/score_for_sped.rb @@ -0,0 +1,42 @@ +module Analyze + module Graph + module Column + module SpedColumn + module ScoreForSped + def score(year_index) + academic_year = academic_years[year_index] + meets_student_threshold = sufficient_student_responses?(academic_year:) + return Score::NIL_SCORE unless meets_student_threshold + + averages = SurveyItemResponse.averages_for_sped(measure.student_survey_items, school, academic_year, + sped) + average = bubble_up_averages(averages:).round(2) + + Score.new(average:, + meets_teacher_threshold: false, + meets_student_threshold:, + meets_admin_data_threshold: false) + end + + def bubble_up_averages(averages:) + measure.student_scales.map do |scale| + scale.survey_items.map do |survey_item| + averages[survey_item] + end.remove_blanks.average + end.remove_blanks.average + end + + def sufficient_student_responses?(academic_year:) + return false unless measure.subcategory.response_rate(school:, academic_year:).meets_student_threshold? + + yearly_counts = SurveyItemResponse.where(school:, academic_year:, + sped:, survey_item: measure.student_survey_items).group(:sped).select(:response_id).distinct(:response_id).count + yearly_counts.any? do |count| + count[1] >= 10 + end + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/sped_column/sped.rb b/app/presenters/dashboard/analyze/graph/column/sped_column/sped.rb new file mode 100644 index 0000000..7e67e10 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/sped_column/sped.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module SpedColumn + class Sped < GroupedBarColumnPresenter + include Analyze::Graph::Column::SpedColumn::ScoreForSped + include Analyze::Graph::Column::SpedColumn::SpedCount + + def label + %w[Special Education] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def sped + ::Sped.find_by_slug "special-education" + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/sped_column/sped_count.rb b/app/presenters/dashboard/analyze/graph/column/sped_column/sped_count.rb new file mode 100644 index 0000000..72f1b78 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/sped_column/sped_count.rb @@ -0,0 +1,18 @@ +module Analyze + module Graph + module Column + module SpedColumn + module SpedCount + def type + :student + end + + def n_size(year_index) + SurveyItemResponse.where(sped:, survey_item: measure.student_survey_items, school:, grade: grades(year_index), + academic_year: academic_years[year_index]).select(:response_id).distinct.count + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/column/sped_column/unknown.rb b/app/presenters/dashboard/analyze/graph/column/sped_column/unknown.rb new file mode 100644 index 0000000..e3e7fa5 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/column/sped_column/unknown.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Analyze + module Graph + module Column + module SpedColumn + class Unknown < GroupedBarColumnPresenter + include Analyze::Graph::Column::SpedColumn::ScoreForSped + include Analyze::Graph::Column::SpedColumn::SpedCount + + def label + %w[Unknown] + end + + def basis + "student" + end + + def show_irrelevancy_message? + false + end + + def show_insufficient_data_message? + false + end + + def sped + ::Sped.find_by_slug "unknown" + end + end + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/students_and_teachers.rb b/app/presenters/dashboard/analyze/graph/students_and_teachers.rb new file mode 100644 index 0000000..482f817 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/students_and_teachers.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Analyze + module Graph + class StudentsAndTeachers + include Analyze::Graph::Column + def to_s + 'Students & Teachers' + end + + def slug + 'students-and-teachers' + end + + def columns + [AllStudent, AllTeacher, AllSurveyData] + end + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/students_by_ell.rb b/app/presenters/dashboard/analyze/graph/students_by_ell.rb new file mode 100644 index 0000000..7431faa --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/students_by_ell.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Analyze + module Graph + class StudentsByEll + include Analyze::Graph::Column::EllColumn + attr_reader :ells + + def initialize(ells:) + @ells = ells + end + + def to_s + "Students by Ell" + end + + def slug + "students-by-ell" + end + + def columns + [].tap do |array| + ells.each do |ell| + array << column_for_ell_code(code: ell.slug) + end + array.sort_by!(&:to_s) + array << Analyze::Graph::Column::AllStudent + end + end + + private + + def column_for_ell_code(code:) + CFR[code] + end + + CFR = { + "ell" => Analyze::Graph::Column::EllColumn::Ell, + "not-ell" => Analyze::Graph::Column::EllColumn::NotEll, + "unknown" => Analyze::Graph::Column::EllColumn::Unknown + }.freeze + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/students_by_gender.rb b/app/presenters/dashboard/analyze/graph/students_by_gender.rb new file mode 100644 index 0000000..97d3363 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/students_by_gender.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Analyze + module Graph + class StudentsByGender + include Analyze::Graph::Column::GenderColumn + attr_reader :genders + + def initialize(genders:) + @genders = genders + end + + def to_s + "Students by Gender" + end + + def slug + "students-by-gender" + end + + def columns + [].tap do |array| + genders.each do |gender| + array << column_for_gender_code(code: gender.qualtrics_code) + end + array.sort_by!(&:to_s) + array << Analyze::Graph::Column::AllStudent + end + end + + private + + def column_for_gender_code(code:) + CFR[code] + end + + CFR = { + 1 => Analyze::Graph::Column::GenderColumn::Female, + 2 => Analyze::Graph::Column::GenderColumn::Male, + 4 => Analyze::Graph::Column::GenderColumn::NonBinary, + 99 => Analyze::Graph::Column::GenderColumn::Unknown + }.freeze + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/students_by_grade.rb b/app/presenters/dashboard/analyze/graph/students_by_grade.rb new file mode 100644 index 0000000..3fb113d --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/students_by_grade.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Analyze + module Graph + class StudentsByGrade + include Analyze::Graph::Column::Grade + attr_reader :grades + + def initialize(grades:) + @grades = grades + end + + def to_s + "Students by Grade" + end + + def slug + "students-by-grade" + end + + def columns + [].tap do |array| + grades.each do |grade| + array << column_for_grade_code(code: grade) + end + array << Analyze::Graph::Column::AllStudent + end + end + + private + + def column_for_grade_code(code:) + CFR[code] + end + + CFR = { + 0 => Zero, + 1 => One, + 2 => Two, + 3 => Three, + 4 => Four, + 5 => Five, + 6 => Six, + 7 => Seven, + 8 => Eight, + 9 => Nine, + 10 => Ten, + 11 => Eleven, + 12 => Twelve + }.freeze + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/students_by_income.rb b/app/presenters/dashboard/analyze/graph/students_by_income.rb new file mode 100644 index 0000000..e72b6ef --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/students_by_income.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Analyze + module Graph + class StudentsByIncome + attr_reader :incomes + + def initialize(incomes:) + @incomes = incomes + end + + def to_s + "Students by income" + end + + def slug + "students-by-income" + end + + def columns + [].tap do |array| + incomes.each do |income| + array << column_for_income_code(code: income.slug) + end + array << Analyze::Graph::Column::AllStudent + end + end + + private + + def column_for_income_code(code:) + CFR[code.to_s] + end + + CFR = { + "economically-disadvantaged-y" => Analyze::Graph::Column::IncomeColumn::Disadvantaged, + "economically-disadvantaged-n" => Analyze::Graph::Column::IncomeColumn::NotDisadvantaged, + "unknown" => Analyze::Graph::Column::IncomeColumn::Unknown + }.freeze + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/students_by_race.rb b/app/presenters/dashboard/analyze/graph/students_by_race.rb new file mode 100644 index 0000000..74a7cb8 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/students_by_race.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Analyze + module Graph + class StudentsByRace + attr_reader :races + + def initialize(races:) + @races = races + end + + def to_s + "Students by Race" + end + + def slug + "students-by-race" + end + + def columns + [].tap do |array| + races.each do |race| + array << column_for_race_code(code: race.qualtrics_code) + end + array << Analyze::Graph::Column::AllStudent + end + end + + private + + def column_for_race_code(code:) + CFR[code.to_s] + end + + CFR = { + "1" => Analyze::Graph::Column::RaceColumn::AmericanIndian, + "2" => Analyze::Graph::Column::RaceColumn::Asian, + "3" => Analyze::Graph::Column::RaceColumn::Black, + "4" => Analyze::Graph::Column::RaceColumn::Hispanic, + "5" => Analyze::Graph::Column::RaceColumn::White, + "8" => Analyze::Graph::Column::RaceColumn::MiddleEastern, + "99" => Analyze::Graph::Column::RaceColumn::Unknown, + "100" => Analyze::Graph::Column::RaceColumn::Multiracial + }.freeze + end + end +end diff --git a/app/presenters/dashboard/analyze/graph/students_by_sped.rb b/app/presenters/dashboard/analyze/graph/students_by_sped.rb new file mode 100644 index 0000000..6129d62 --- /dev/null +++ b/app/presenters/dashboard/analyze/graph/students_by_sped.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Analyze + module Graph + class StudentsBySped + include Analyze::Graph::Column::SpedColumn + attr_reader :speds + + def initialize(speds:) + @speds = speds + end + + def to_s + "Students by SpEd" + end + + def slug + "students-by-sped" + end + + def columns + [].tap do |array| + speds.each do |sped| + array << column_for_sped_code(code: sped.slug) + end + array << Analyze::Graph::Column::AllStudent + end + end + + private + + def column_for_sped_code(code:) + CFR[code] + end + + CFR = { + "special-education" => Analyze::Graph::Column::SpedColumn::Sped, + "not-special-education" => Analyze::Graph::Column::SpedColumn::NotSped, + "unknown" => Analyze::Graph::Column::SpedColumn::Unknown + }.freeze + end + end +end diff --git a/app/presenters/dashboard/analyze/group/ell.rb b/app/presenters/dashboard/analyze/group/ell.rb new file mode 100644 index 0000000..512b3d5 --- /dev/null +++ b/app/presenters/dashboard/analyze/group/ell.rb @@ -0,0 +1,13 @@ +module Analyze + module Group + class Ell + def name + "ELL" + end + + def slug + "ell" + end + end + end +end diff --git a/app/presenters/dashboard/analyze/group/gender.rb b/app/presenters/dashboard/analyze/group/gender.rb new file mode 100644 index 0000000..901d24d --- /dev/null +++ b/app/presenters/dashboard/analyze/group/gender.rb @@ -0,0 +1,13 @@ +module Analyze + module Group + class Gender + def name + 'Gender' + end + + def slug + 'gender' + end + end + end +end diff --git a/app/presenters/dashboard/analyze/group/grade.rb b/app/presenters/dashboard/analyze/group/grade.rb new file mode 100644 index 0000000..9504469 --- /dev/null +++ b/app/presenters/dashboard/analyze/group/grade.rb @@ -0,0 +1,13 @@ +module Analyze + module Group + class Grade + def name + 'Grade' + end + + def slug + 'grade' + end + end + end +end diff --git a/app/presenters/dashboard/analyze/group/income.rb b/app/presenters/dashboard/analyze/group/income.rb new file mode 100644 index 0000000..21e364b --- /dev/null +++ b/app/presenters/dashboard/analyze/group/income.rb @@ -0,0 +1,13 @@ +module Analyze + module Group + class Income + def name + 'Income' + end + + def slug + 'income' + end + end + end +end diff --git a/app/presenters/dashboard/analyze/group/race.rb b/app/presenters/dashboard/analyze/group/race.rb new file mode 100644 index 0000000..1155d1c --- /dev/null +++ b/app/presenters/dashboard/analyze/group/race.rb @@ -0,0 +1,13 @@ +module Analyze + module Group + class Race + def name + 'Race' + end + + def slug + 'race' + end + end + end +end diff --git a/app/presenters/dashboard/analyze/group/sped.rb b/app/presenters/dashboard/analyze/group/sped.rb new file mode 100644 index 0000000..6f4b221 --- /dev/null +++ b/app/presenters/dashboard/analyze/group/sped.rb @@ -0,0 +1,13 @@ +module Analyze + module Group + class Sped + def name + "Special Education" + end + + def slug + "sped" + end + end + end +end diff --git a/app/presenters/dashboard/analyze/presenter.rb b/app/presenters/dashboard/analyze/presenter.rb new file mode 100644 index 0000000..99f3128 --- /dev/null +++ b/app/presenters/dashboard/analyze/presenter.rb @@ -0,0 +1,200 @@ +module Analyze + class Presenter + attr_reader :params, :school, :academic_year + + def initialize(params:, school:, academic_year:) + @params = params + @school = school + @academic_year = academic_year + end + + def category + @category ||= Category.find_by_category_id(params[:category]) || Category.order(:category_id).first + end + + def categories + @categories = Category.all.order(:category_id) + end + + def subcategory + @subcategory ||= Subcategory.find_by_subcategory_id(params[:subcategory]) || subcategories.first + end + + def subcategories + @subcategories = category.subcategories.order(:subcategory_id) + end + + def measures + @measures = subcategory.measures.order(:measure_id).includes(%i[admin_data_items subcategory]) + end + + def academic_years + @academic_years = AcademicYear.order(:range).all + end + + def selected_academic_years + @selected_academic_years ||= begin + year_params = params[:academic_years] + return [] unless year_params + + year_params.split(",").map { |year| AcademicYear.find_by_range(year) }.compact + end + end + + def races + @races ||= Race.all.order(designation: :ASC) + end + + def selected_races + @selected_races ||= begin + race_params = params[:races] + return races unless race_params + + race_params.split(",").map { |race| Race.find_by_slug race }.compact + end + end + + def ells + @ells ||= Ell.all.order(slug: :ASC) + end + + def selected_ells + @selected_ells ||= begin + ell_params = params[:ells] + return ells unless ell_params + + ell_params.split(",").map { |ell| Ell.find_by_slug ell }.compact + end + end + + def speds + @speds ||= Sped.all.order(id: :ASC) + end + + def selected_speds + @selected_speds ||= begin + sped_params = params[:speds] + return speds unless sped_params + + sped_params.split(",").map { |sped| Sped.find_by_slug sped }.compact + end + end + + def graphs + @graphs ||= [Analyze::Graph::AllData.new, + Analyze::Graph::StudentsAndTeachers.new, + Analyze::Graph::StudentsByRace.new(races: selected_races), + Analyze::Graph::StudentsByGrade.new(grades: selected_grades), + Analyze::Graph::StudentsByGender.new(genders: selected_genders), + Analyze::Graph::StudentsByIncome.new(incomes: selected_incomes), + Analyze::Graph::StudentsByEll.new(ells: selected_ells), + Analyze::Graph::StudentsBySped.new(speds: selected_speds)] + end + + def graph + @graph ||= graphs.reduce(graphs.first) do |acc, graph| + graph.slug == params[:graph] ? graph : acc + end + end + + def selected_grades + @selected_grades ||= begin + grade_params = params[:grades] + return grades unless grade_params + + grade_params.split(",").map(&:to_i) + end + end + + def selected_genders + @selected_genders ||= begin + gender_params = params[:genders] + return genders unless gender_params + + gender_params.split(",").sort.map { |gender| Gender.find_by_designation(gender) }.compact + end + end + + def genders + @genders ||= Gender.all.order(designation: :ASC) + end + + def groups + @groups = [Analyze::Group::Ell.new, Analyze::Group::Gender.new, Analyze::Group::Grade.new, Analyze::Group::Income.new, + Analyze::Group::Race.new, Analyze::Group::Sped.new] + end + + def group + @group ||= groups.reduce(groups.first) do |acc, group| + group.slug == params[:group] ? group : acc + end + end + + def slice + @slice ||= slices.reduce(slices.first) do |acc, slice| + slice.slug == params[:slice] ? slice : acc + end + end + + def slices + source.slices + end + + def source + @source ||= sources.reduce(sources.first) do |acc, source| + source.slug == params[:source] ? source : acc + end + end + + def sources + all_data_slices = [Analyze::Slice::AllData.new] + all_data_source = Analyze::Source::AllData.new(slices: all_data_slices) + + students_and_teachers = Analyze::Slice::StudentsAndTeachers.new + students_by_group = Analyze::Slice::StudentsByGroup.new(races:, grades:) + survey_data_slices = [students_and_teachers, students_by_group] + survey_data_source = Analyze::Source::SurveyData.new(slices: survey_data_slices) + + @sources = [all_data_source, survey_data_source] + end + + def grades + @grades ||= SurveyItemResponse.where(school:, academic_year:) + .where.not(grade: nil) + .group(:grade) + .select(:response_id) + .distinct(:response_id) + .count.reject do |_key, value| + value < 10 + end.keys + end + + def incomes + @incomes ||= Income.all + end + + def selected_incomes + @selected_incomes ||= begin + income_params = params[:incomes] + return incomes unless income_params + + income_params.split(",").map { |income| Income.find_by_slug(income) }.compact + end + end + + def cache_objects + [subcategory, + selected_academic_years, + graph, + selected_races, + selected_grades, + grades, + selected_genders, + genders, + selected_ells, + ells, + selected_speds, + speds] + end + end +end diff --git a/app/presenters/dashboard/analyze/slice/all_data.rb b/app/presenters/dashboard/analyze/slice/all_data.rb new file mode 100644 index 0000000..10137a7 --- /dev/null +++ b/app/presenters/dashboard/analyze/slice/all_data.rb @@ -0,0 +1,17 @@ +module Analyze + module Slice + class AllData + def to_s + 'All Data' + end + + def slug + 'all-data' + end + + def graphs + [Analyze::Graph::AllData.new] + end + end + end +end diff --git a/app/presenters/dashboard/analyze/slice/students_and_teachers.rb b/app/presenters/dashboard/analyze/slice/students_and_teachers.rb new file mode 100644 index 0000000..75eb921 --- /dev/null +++ b/app/presenters/dashboard/analyze/slice/students_and_teachers.rb @@ -0,0 +1,17 @@ +module Analyze + module Slice + class StudentsAndTeachers + def to_s + 'Students & Teachers' + end + + def slug + 'students-and-teachers' + end + + def graphs + [Analyze::Graph::StudentsAndTeachers.new] + end + end + end +end diff --git a/app/presenters/dashboard/analyze/slice/students_by_group.rb b/app/presenters/dashboard/analyze/slice/students_by_group.rb new file mode 100644 index 0000000..af62be6 --- /dev/null +++ b/app/presenters/dashboard/analyze/slice/students_by_group.rb @@ -0,0 +1,24 @@ +module Analyze + module Slice + class StudentsByGroup + attr_reader :races, :grades + + def initialize(races:, grades:) + @races = races + @grades = grades + end + + def to_s + 'Students by Group' + end + + def slug + 'students-by-group' + end + + def graphs + [Analyze::Graph::StudentsByRace.new(races:), Analyze::Graph::StudentsByGrade.new(grades:)] + end + end + end +end diff --git a/app/presenters/dashboard/analyze/source/all_data.rb b/app/presenters/dashboard/analyze/source/all_data.rb new file mode 100644 index 0000000..7288990 --- /dev/null +++ b/app/presenters/dashboard/analyze/source/all_data.rb @@ -0,0 +1,21 @@ +module Analyze + module Source + class AllData + attr_reader :slices + + include Analyze::Slice + + def initialize(slices:) + @slices = slices + end + + def to_s + 'All Data' + end + + def slug + 'all-data' + end + end + end +end diff --git a/app/presenters/dashboard/analyze/source/survey_data.rb b/app/presenters/dashboard/analyze/source/survey_data.rb new file mode 100644 index 0000000..654020b --- /dev/null +++ b/app/presenters/dashboard/analyze/source/survey_data.rb @@ -0,0 +1,21 @@ +module Analyze + module Source + class SurveyData + attr_reader :slices + + include Analyze::Slice + + def initialize(slices:) + @slices = slices + end + + def to_s + 'Survey Data Only' + end + + def slug + 'survey-data-only' + end + end + end +end diff --git a/app/presenters/dashboard/background_presenter.rb b/app/presenters/dashboard/background_presenter.rb new file mode 100644 index 0000000..21f8ed6 --- /dev/null +++ b/app/presenters/dashboard/background_presenter.rb @@ -0,0 +1,46 @@ +module Dashboard + class BackgroundPresenter + include AnalyzeHelper + attr_reader :num_of_columns + + def initialize(num_of_columns:) + @num_of_columns = num_of_columns + end + + def zone_label_x + 2 + end + + def benchmark_y + (analyze_zone_height * 2) - (benchmark_height / 2.0) + end + + def benchmark_height + 1 + end + + def grouped_chart_column_width + graph_width / data_sources + end + + def column_end_x(position) + zone_label_width + (grouped_chart_column_width * position) + end + + def column_start_x(position) + column_end_x(position - 1) + end + + def bar_label_height + (100 - ((100 - analyze_graph_height) / 2)) + end + + def zone_label_y(position) + 8.5 * (position + position - 1) + end + + def data_sources + num_of_columns + end + end +end diff --git a/app/presenters/dashboard/category_presenter.rb b/app/presenters/dashboard/category_presenter.rb new file mode 100644 index 0000000..f829830 --- /dev/null +++ b/app/presenters/dashboard/category_presenter.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module Dashboard + class CategoryPresenter + def initialize(category:) + @category = category + end + + def id + @category.category_id + end + + def name + @category.name + end + + def description + @category.description + end + + def short_description + @category.short_description + end + + def slug + @category.slug + end + + def icon_class + icon_suffix = classes[id.to_sym] + "fas fa-#{icon_suffix}" + end + + def icon_color_class + color_suffix = colors[id.to_sym] + "color-#{color_suffix}" + end + + def subcategories(academic_year:, school:) + @category.subcategories.includes([:measures]).sort_by(&:subcategory_id).map do |subcategory| + SubcategoryPresenter.new( + subcategory:, + academic_year:, + school: + ) + end + end + + def to_model + @category + end + + private + + def colors + { '1': "blue", + '2': "red", + '3': "black", + '4': "lime", + '5': "teal" } + end + + def classes + { '1': "apple-alt", + '2': "school", + '3': "users-cog", + '4': "graduation-cap", + '5': "heart" } + end + end +end diff --git a/app/presenters/dashboard/data_item_presenter.rb b/app/presenters/dashboard/data_item_presenter.rb new file mode 100644 index 0000000..9c0bc21 --- /dev/null +++ b/app/presenters/dashboard/data_item_presenter.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Dashboard + class DataItemPresenter + attr_reader :measure_id, :has_sufficient_data, :school, :academic_year + + def initialize(measure_id:, has_sufficient_data:, school:, academic_year:) + @measure_id = measure_id + @has_sufficient_data = has_sufficient_data + @school = school + @academic_year = academic_year + end + + def data_item_accordion_id + "data-item-accordion-#{@measure_id}" + end + + def sufficient_data? + @has_sufficient_data + end + end +end diff --git a/app/presenters/dashboard/gauge_presenter.rb b/app/presenters/dashboard/gauge_presenter.rb new file mode 100644 index 0000000..b436b82 --- /dev/null +++ b/app/presenters/dashboard/gauge_presenter.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module Dashboard + class GaugePresenter + def initialize(zones:, score:) + @zones = zones + @score = score&.round(2) + end + + def title + zone.type.to_s.titleize + end + + def color_class + "fill-#{zone.type}" + end + + def score_percentage + percentage_for @score + end + + def key_benchmark_percentage + percentage_for @zones.approval_zone.low_benchmark + end + + def boundary_percentage_for(zone) + case zone + when :watch_low + watch_low_boundary + when :growth_low + growth_low_boundary + when :ideal_low + ideal_low_boundary + end + end + + attr_reader :score + + private + + def watch_low_boundary + percentage_for @zones.watch_zone.low_benchmark + end + + def growth_low_boundary + percentage_for @zones.growth_zone.low_benchmark + end + + def approval_low_boundary + percentage_for @zones.approval_zone.low_benchmark + end + + def ideal_low_boundary + percentage_for @zones.ideal_zone.low_benchmark + end + + def zone + @zones.zone_for_score(@score) + end + + def percentage_for(number) + return nil if number.nil? + + zones_minimum = @zones.warning_zone.low_benchmark + zones_maximum = @zones.ideal_zone.high_benchmark + + (number - zones_minimum) / (zones_maximum - zones_minimum) + end + end +end diff --git a/app/presenters/dashboard/measure_presenter.rb b/app/presenters/dashboard/measure_presenter.rb new file mode 100644 index 0000000..5b272e9 --- /dev/null +++ b/app/presenters/dashboard/measure_presenter.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module Dashboard + class MeasurePresenter + attr_reader :measure, :academic_year, :school, :id, :name, :description + + def initialize(measure:, academic_year:, school:) + @measure = measure + @academic_year = academic_year + @school = school + @id = measure_id + @name = measure.name + @description = measure.description + end + + def gauge_presenter + GaugePresenter.new(zones:, score: score_for_measure.average) + end + + def data_item_accordion_id + "data-item-accordion-#{@measure.measure_id}" + end + + def data_item_presenters + [].tap do |array| + array << student_survey_presenter if student_survey_items.any? + array << teacher_survey_presenter if teacher_survey_items.any? + array << admin_data_presenter if admin_data_items.any? + end + end + + private + + def admin_data_items + measure.admin_data_items + end + + def score_for_measure + @score ||= @measure.score(school: @school, academic_year: @academic_year) + end + + def student_survey_items + measure.student_survey_items + end + + def teacher_survey_items + measure.teacher_survey_items + end + + def measure_id + measure.measure_id + end + + def student_survey_presenter + StudentSurveyPresenter.new(measure_id:, survey_items: student_survey_items, + has_sufficient_data: score_for_measure.meets_student_threshold?, school:, academic_year:) + end + + def teacher_survey_presenter + TeacherSurveyPresenter.new(measure_id:, survey_items: teacher_survey_items, + has_sufficient_data: score_for_measure.meets_teacher_threshold?, school:, academic_year:) + end + + def admin_data_presenter + AdminDataPresenter.new(measure_id:, + admin_data_items:, has_sufficient_data: score_for_measure.meets_admin_data_threshold?, school:, academic_year:) + end + + def 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 + ) + end + end +end diff --git a/app/presenters/dashboard/response_rate_presenter.rb b/app/presenters/dashboard/response_rate_presenter.rb new file mode 100644 index 0000000..8c7169b --- /dev/null +++ b/app/presenters/dashboard/response_rate_presenter.rb @@ -0,0 +1,110 @@ +module Dashboard + class ResponseRatePresenter + attr_reader :focus, :academic_year, :school, :survey_items + + def initialize(focus:, academic_year:, school:) + @focus = focus + @academic_year = academic_year + @school = school + if focus == :student + @survey_items = Measure.all.flat_map do |measure| + measure.student_survey_items_with_sufficient_responses(school:, academic_year:) + end + end + @survey_items = SurveyItem.teacher_survey_items if focus == :teacher + end + + def date + SurveyItemResponse.where(survey_item: survey_items, school:, + academic_year:).order(recorded_date: :DESC).first&.recorded_date + end + + def percentage + return 0 if respondents_count.zero? + + cap_at_100(actual_count.to_f / respondents_count.to_f * 100).round + end + + def color + percentage > 75 ? "purple" : "gold" + end + + def date_message + return "" if date.nil? + + "Response rate as of #{date.to_date.strftime('%m/%d/%y')}" + end + + def hover_message + return "" if date.nil? + + "Percentages based on #{actual_count} out of #{respondents_count.round} #{focus}s completing at least 25% of the survey." + end + + private + + def cap_at_100(value) + value > 100 ? 100 : value + end + + def actual_count + if focus == :teacher + response_count_for_survey_items(survey_items:) + else + non_early_ed_items = survey_items - SurveyItem.early_education_survey_items + non_early_ed_count = response_count_for_survey_items(survey_items: non_early_ed_items) + + early_ed_items = survey_items & SurveyItem.early_education_survey_items + early_ed_count = SurveyItemResponse.where(school:, academic_year:, + survey_item: early_ed_items) + .group(:survey_item) + .select(:response_id) + .distinct + .count + .reduce(0) do |largest, row| + count = row[1] + if count > largest + count + else + largest + end + end + + non_early_ed_count + early_ed_count + end + end + + def response_count_for_survey_items(survey_items:) + SurveyItemResponse.where(school:, academic_year:, + survey_item: survey_items).select(:response_id).distinct.count || 0 + end + + def respondents_count + return 0 if respondents.nil? + + count = enrollment if focus == :student + count = respondents.total_teachers if focus == :teacher + count + end + + def enrollment + SurveyItemResponse.where(school:, academic_year:, grade: grades, + survey_item: SurveyItem.student_survey_items) + .select(:grade) + .distinct + .pluck(:grade) + .reject(&:nil?) + .map do |grade| + respondents.enrollment_by_grade[grade] + end.sum.to_f + end + + def respondents + @respondents ||= Respondent.by_school_and_year(school:, academic_year:) + end + + def grades + respondents.enrollment_by_grade.keys + end + end +end diff --git a/app/presenters/dashboard/student_survey_presenter.rb b/app/presenters/dashboard/student_survey_presenter.rb new file mode 100644 index 0000000..4115471 --- /dev/null +++ b/app/presenters/dashboard/student_survey_presenter.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Dashboard + class StudentSurveyPresenter < DataItemPresenter + attr_reader :survey_items + + def initialize(measure_id:, survey_items:, has_sufficient_data:, school:, academic_year:) + super(measure_id:, has_sufficient_data:, school:, academic_year:) + @survey_items = survey_items + end + + def title + "Student survey" + end + + def id + "student-survey-items-#{@measure_id}" + end + + def reason_for_insufficiency + "low response rate" + end + + def descriptions_and_availability + survey_items.reject do |survey_item| + question_id = survey_item.survey_item_id.split("-")[2] + if question_id.present? + question_id.starts_with?("es") + else + false + end + end.map(&:description) + end + end +end diff --git a/app/presenters/dashboard/subcategory_card_presenter.rb b/app/presenters/dashboard/subcategory_card_presenter.rb new file mode 100644 index 0000000..cc4bb68 --- /dev/null +++ b/app/presenters/dashboard/subcategory_card_presenter.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Dashboard + class SubcategoryCardPresenter + attr_reader :name, :subcategory, :category, :subcategory_id + + def initialize(subcategory:, zones:, score:) + @name = subcategory.name + @subcategory = subcategory + @category = subcategory.category + @subcategory_id = subcategory.subcategory_id + @zones = zones + @score = score + end + + def harvey_ball_icon + "#{zone.type}-harvey-ball" + end + + def color + zone.type.to_s + end + + def insufficient_data? + zone.type == :insufficient_data + end + + def to_model + subcategory + end + + private + + def zone + @zones.zone_for_score(@score) + end + end +end diff --git a/app/presenters/dashboard/subcategory_presenter.rb b/app/presenters/dashboard/subcategory_presenter.rb new file mode 100644 index 0000000..a706911 --- /dev/null +++ b/app/presenters/dashboard/subcategory_presenter.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module Dashboard + class SubcategoryPresenter + attr_reader :subcategory, :academic_year, :school, :id, :name, :description + + def initialize(subcategory:, academic_year:, school:) + @subcategory = subcategory + @academic_year = academic_year + @school = school + @id = @subcategory.subcategory_id + @name = @subcategory.name + @description = @subcategory.description + end + + def gauge_presenter + GaugePresenter.new(zones:, score: average_score) + end + + def subcategory_card_presenter + SubcategoryCardPresenter.new(subcategory: @subcategory, zones:, score: average_score) + end + + def average_score + @average_score ||= @subcategory.score(school: @school, academic_year: @academic_year) + end + + def student_response_rate + return "N / A" if Respondent.where(school: @school, academic_year: @academic_year).count.zero? + + "#{@subcategory.response_rate(school: @school, academic_year: @academic_year).student_response_rate.round}%" + end + + def teacher_response_rate + return "N / A" if Respondent.where(school: @school, academic_year: @academic_year).count.zero? + + "#{@subcategory.response_rate(school: @school, academic_year: @academic_year).teacher_response_rate.round}%" + end + + def admin_collection_rate + rate = [admin_data_values_count, admin_data_item_count] + format_a_non_applicable_rate rate + end + + def measure_presenters + @subcategory.measures.sort_by(&:measure_id).map do |measure| + MeasurePresenter.new(measure:, academic_year: @academic_year, school: @school) + end + end + + private + + def admin_data_values_count + @subcategory.measures.map do |measure| + measure.scales.map do |scale| + scale.admin_data_items.map do |admin_data_item| + admin_data_item.admin_data_values.where(school: @school, academic_year: @academic_year).count + end + end + end.flatten.sum + end + + def admin_data_item_count + measures = @subcategory.measures + return AdminDataItem.for_measures(measures).count if @school.is_hs + + AdminDataItem.non_hs_items_for_measures(measures).count + end + + def format_a_non_applicable_rate(rate) + rate == [0, 0] ? %w[N A] : rate + end + + def zones + Zones.new( + watch_low_benchmark: measures.map(&:watch_low_benchmark).average, + growth_low_benchmark: measures.map(&:growth_low_benchmark).average, + approval_low_benchmark: measures.map(&:approval_low_benchmark).average, + ideal_low_benchmark: measures.map(&:ideal_low_benchmark).average + ) + end + + def measures + @measures ||= @subcategory.measures.includes([:admin_data_items]).order(:measure_id) + end + end +end diff --git a/app/presenters/dashboard/teacher_survey_presenter.rb b/app/presenters/dashboard/teacher_survey_presenter.rb new file mode 100644 index 0000000..cf3f487 --- /dev/null +++ b/app/presenters/dashboard/teacher_survey_presenter.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Dashboard + class TeacherSurveyPresenter < DataItemPresenter + attr_reader :survey_items + + def initialize(measure_id:, survey_items:, has_sufficient_data:, school:, academic_year:) + super(measure_id:, has_sufficient_data:, school:, academic_year:) + @survey_items = survey_items + end + + def title + "Teacher survey" + end + + def id + "teacher-survey-items-#{@measure_id}" + end + + def reason_for_insufficiency + "low response rate" + end + + def descriptions_and_availability + if @measure_id == "1B-i" + return [Summary.new("1B-i", "Items available upon request to ECP", + true)] + end + + survey_items.map do |survey_item| + Summary.new(survey_item.survey_item_id, survey_item.prompt, true) + end + end + end +end diff --git a/app/presenters/dashboard/variance_chart_row_presenter.rb b/app/presenters/dashboard/variance_chart_row_presenter.rb new file mode 100644 index 0000000..28b007e --- /dev/null +++ b/app/presenters/dashboard/variance_chart_row_presenter.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +module Dashboard + class VarianceChartRowPresenter + include Comparable + + attr_reader :score, :measure_name, :measure_id, :category + + def initialize(measure:, score:) + @measure = measure + @score = score.average + @meets_teacher_threshold = score.meets_teacher_threshold? + @meets_student_threshold = score.meets_student_threshold? + @measure_name = @measure.name + @measure_id = @measure.measure_id + @category = @measure.subcategory.category + end + + def sufficient_data? + @score != nil + end + + def bar_color + "fill-#{zone.type}" + end + + def bar_width + "#{(bar_width_percentage * 100).round(2)}%" + end + + def x_offset + case zone.type + when :ideal, :approval + "60%" + else + "#{((0.6 - bar_width_percentage) * 100).abs.round(2)}%" + end + end + + def order + case zone.type + when :ideal, :approval + bar_width_percentage + when :warning, :watch, :growth + -bar_width_percentage + when :insufficient_data + -100 + end + end + + def <=>(other) + other.order <=> order + end + + def show_partial_data_indicator? + partial_data_sources.present? + end + + def partial_data_sources + [].tap do |sources| + sources << "teacher survey results" if @measure.includes_teacher_survey_items? && !@meets_teacher_threshold + sources << "student survey results" if @measure.includes_student_survey_items? && !@meets_student_threshold + sources << "administrative data" if @measure.includes_admin_data_items? + end + end + + private + + IDEAL_ZONE_WIDTH_PERCENTAGE = 0.2 + APPROVAL_ZONE_WIDTH_PERCENTAGE = 0.2 + GROWTH_ZONE_WIDTH_PERCENTAGE = 0.2 + WATCH_ZONE_WIDTH_PERCENTAGE = 0.2 + WARNING_ZONE_WIDTH_PERCENTAGE = 0.2 + + def bar_width_percentage + send("#{zone.type}_bar_width_percentage") + end + + def ideal_bar_width_percentage + percentage * IDEAL_ZONE_WIDTH_PERCENTAGE + APPROVAL_ZONE_WIDTH_PERCENTAGE + end + + def approval_bar_width_percentage + percentage * APPROVAL_ZONE_WIDTH_PERCENTAGE + end + + def growth_bar_width_percentage + (1 - percentage) * GROWTH_ZONE_WIDTH_PERCENTAGE + end + + def watch_bar_width_percentage + (1 - percentage) * WATCH_ZONE_WIDTH_PERCENTAGE + GROWTH_ZONE_WIDTH_PERCENTAGE + end + + def warning_bar_width_percentage + (1 - percentage) * WARNING_ZONE_WIDTH_PERCENTAGE + WATCH_ZONE_WIDTH_PERCENTAGE + GROWTH_ZONE_WIDTH_PERCENTAGE + end + + def insufficient_data_bar_width_percentage + 0 + end + + def percentage + low_benchmark = zone.low_benchmark + (@score - 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) + end + end +end diff --git a/app/presenters/dashboard/zones.rb b/app/presenters/dashboard/zones.rb new file mode 100644 index 0000000..4966b3a --- /dev/null +++ b/app/presenters/dashboard/zones.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Dashboard + class Zones + def initialize(watch_low_benchmark:, growth_low_benchmark:, approval_low_benchmark:, ideal_low_benchmark:) + @watch_low_benchmark = watch_low_benchmark + @growth_low_benchmark = growth_low_benchmark + @approval_low_benchmark = approval_low_benchmark + @ideal_low_benchmark = ideal_low_benchmark + @warning_low_benchmark = 1 + end + + Zone = Struct.new(:low_benchmark, :high_benchmark, :type) + + def all_zones + [ + ideal_zone, approval_zone, growth_zone, watch_zone, warning_zone, insufficient_data + ] + end + + def warning_zone + Zone.new(1, @watch_low_benchmark, :warning) + end + + def watch_zone + Zone.new(@watch_low_benchmark, @growth_low_benchmark, :watch) + end + + def growth_zone + Zone.new(@growth_low_benchmark, @approval_low_benchmark, :growth) + end + + def approval_zone + Zone.new(@approval_low_benchmark, @ideal_low_benchmark, :approval) + end + + def ideal_zone + Zone.new(@ideal_low_benchmark, 5.0, :ideal) + end + + def insufficient_data + Zone.new(Float::MIN, Float::MAX, :insufficient_data) + end + + def zone_for_score(score) + all_zones.find { |zone| Score.new(average: score).in_zone?(zone:) } || insufficient_data + end + end +end diff --git a/app/views/dashboard/home/index.html.erb b/app/views/dashboard/home/index.html.erb new file mode 100644 index 0000000..73875cc --- /dev/null +++ b/app/views/dashboard/home/index.html.erb @@ -0,0 +1,94 @@ +
+
+

School Quality Measures Dashboard

+

A school quality framework with multiple measures that offers a fair and comprehensive + picture of school performance

+
+
+
+
+
+
+ + <%= form_with(url: welcome_path, method: :get, + data: { + turbo_frame: "schools", + turbo_action: "advance", + controller: "form", + action: "change->form#submit" + }) do |f| %> + + <%= turbo_frame_tag "schools" do %> +
+ <% if District.count > 1 %> +
+ <%= f.select :district, @districts, + {include_blank: "Select a District", selected: params[:district] } , {id: "district-dropdown", class: "form-select", hidden: @districts.count == 1} %> +
+ <% end %> + +
+ <%= f.select :school, @schools, + {include_blank: "Select a School", selected: params[:school]}, { id: "school-dropdown", class: "form-select mx-3"} if @schools %> +
+ + <% if @school.present? %> + <%= link_to "Go", district_school_overview_index_path(@district, @school, {year: @year} ), class: "mx-4 btn btn-secondary" , data: {turbo_frame: "_top"} %> + <% else %> + <%= button_to "Go", "/", class: "mx-4 btn btn-secondary" , data: {turbo_frame: "_top"}, disabled: true %> + <% end %> + +
+ <% end %> + <% end %> + +
+
+
+
+ +
+
+
+
+
+ <%= image_tag('framework_wheel.png', alt: 'School Quality Framework Wheel') %> +
+ +

The School Quality Measures Framework aims to describe the full measure of what makes a good + school. The three outer categories are essential inputs to school quality that influence the center two key + outcomes.

+
+
+

School Quality Measures Framework

+

Through a strand of work led by professor Jack Schneider at the University of Massachusetts Lowell and a team of researchers, ECP is reimagining how we measure the quality of our schools and the learning experiences of our students.

+

Specifically, ECP is the dissemination arm of the School Quality Measures framework developed by the eight original districts of the Massachusetts Consortium for Innovative Education Assessment (MCIEA). MCIEA sought input from stakeholders in each consortium district to build a school quality framework that reflects what the public wants to know about their schools.

+

The framework has been built around multiple measures, which include academic, social-emotional, and school culture indicators, in order to piece together a fairer and more comprehensive picture of school performance. It consists of five major categories.

+
+ <% @categories.each do |category| %> +
+

+ +

+
+
+ <%= category.description %> +
+
+
+ <% end %> +
+
+
+
+
+ +
+
+

About ECP

+

The Education Commonwealth Project works to challenge and expand the way student learning and school quality are assessed, advancing an approach to measurement and accountability that is valid, democratic, and equitable. ECP supports schools and communities with free and open-source tools, and broadly seeks to foster state- and national-level change.

+ Learn More +
+
diff --git a/config/routes.rb b/config/routes.rb index 3fb7434..738745d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,3 +1,4 @@ Dashboard::Engine.routes.draw do resources :examples + get "/welcome", to: "home#index" end diff --git a/dashboard.gemspec b/dashboard.gemspec index 7f9a37d..9a6ccd9 100644 --- a/dashboard.gemspec +++ b/dashboard.gemspec @@ -22,7 +22,8 @@ Gem::Specification.new do |spec| Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] end + spec.add_dependency "friendly_id", "~> 5.4.0" spec.add_dependency "rails", ">= 7.1.2" - spec.add_development_dependency "rspec-rails" spec.add_development_dependency "factory_bot_rails" + spec.add_development_dependency "rspec-rails" end diff --git a/db/migrate/20240103232412_create_dashboard_academic_years.rb b/db/migrate/20240103232412_create_dashboard_academic_years.rb new file mode 100644 index 0000000..df62f7b --- /dev/null +++ b/db/migrate/20240103232412_create_dashboard_academic_years.rb @@ -0,0 +1,11 @@ +class CreateDashboardAcademicYears < ActiveRecord::Migration[7.1] + def change + create_table :dashboard_academic_years do |t| + t.string :range + + t.timestamps + end + + add_index :dashboard_academic_years, :range + end +end diff --git a/db/migrate/20240103233517_create_dashboard_categories.rb b/db/migrate/20240103233517_create_dashboard_categories.rb new file mode 100644 index 0000000..d01a90d --- /dev/null +++ b/db/migrate/20240103233517_create_dashboard_categories.rb @@ -0,0 +1,13 @@ +class CreateDashboardCategories < ActiveRecord::Migration[7.1] + def change + create_table :dashboard_categories do |t| + t.string :name + t.text :description + t.string :slug, unique: true + t.string :category_id + t.string :short_description + + t.timestamps + end + end +end diff --git a/db/migrate/20240104005325_create_dashboard_subcategories.rb b/db/migrate/20240104005325_create_dashboard_subcategories.rb new file mode 100644 index 0000000..0d4ff2c --- /dev/null +++ b/db/migrate/20240104005325_create_dashboard_subcategories.rb @@ -0,0 +1,12 @@ +class CreateDashboardSubcategories < ActiveRecord::Migration[7.1] + def change + create_table :dashboard_subcategories do |t| + t.string :name + t.text :description + t.string :subcategory_id + t.references :dashboard_category_id, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20240104170806_create_dashboard_measures.rb b/db/migrate/20240104170806_create_dashboard_measures.rb new file mode 100644 index 0000000..ffa8e7e --- /dev/null +++ b/db/migrate/20240104170806_create_dashboard_measures.rb @@ -0,0 +1,12 @@ +class CreateDashboardMeasures < ActiveRecord::Migration[7.1] + def change + create_table :dashboard_measures do |t| + t.string :measure_id + t.string :name + t.references :dashboard_subcategory, null: false, foreign_key: true + t.text :description + + t.timestamps + end + end +end diff --git a/db/migrate/20240104170957_create_dashboard_scales.rb b/db/migrate/20240104170957_create_dashboard_scales.rb new file mode 100644 index 0000000..85bd058 --- /dev/null +++ b/db/migrate/20240104170957_create_dashboard_scales.rb @@ -0,0 +1,10 @@ +class CreateDashboardScales < ActiveRecord::Migration[7.1] + def change + create_table :dashboard_scales do |t| + t.string :scale_id + t.references :dashboard_measure, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20240104172921_create_dashboard_survey_items.rb b/db/migrate/20240104172921_create_dashboard_survey_items.rb new file mode 100644 index 0000000..a3b5752 --- /dev/null +++ b/db/migrate/20240104172921_create_dashboard_survey_items.rb @@ -0,0 +1,16 @@ +class CreateDashboardSurveyItems < ActiveRecord::Migration[7.1] + def change + create_table :dashboard_survey_items do |t| + t.string :survey_item_id + t.string :prompt + t.float :watch_low_benchmark + t.float :growth_low_benchmark + t.float :approval_low_benchmark + t.float :ideal_low_benchmark + t.references :dashboard_scale, null: false, foreign_key: true + t.boolean :on_short_form + + t.timestamps + end + end +end diff --git a/db/migrate/20240104173424_create_dashboard_admin_data_items.rb b/db/migrate/20240104173424_create_dashboard_admin_data_items.rb new file mode 100644 index 0000000..ae15dfe --- /dev/null +++ b/db/migrate/20240104173424_create_dashboard_admin_data_items.rb @@ -0,0 +1,16 @@ +class CreateDashboardAdminDataItems < ActiveRecord::Migration[7.1] + def change + create_table :dashboard_admin_data_items do |t| + t.string :admin_data_item_id + t.string :description + t.float :watch_low_benchmark + t.float :growth_low_benchmark + t.float :approval_low_benchmark + t.float :ideal_low_benchmark + t.boolean :hs_only_item + t.references :dashboard_scale, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20240104173931_create_dashboard_districts.rb b/db/migrate/20240104173931_create_dashboard_districts.rb new file mode 100644 index 0000000..8bdc25d --- /dev/null +++ b/db/migrate/20240104173931_create_dashboard_districts.rb @@ -0,0 +1,11 @@ +class CreateDashboardDistricts < ActiveRecord::Migration[7.1] + def change + create_table :dashboard_districts do |t| + t.string :name + t.string :slug, unique: true + t.integer :qualtrics_code + + t.timestamps + end + end +end diff --git a/db/migrate/20240104174053_create_dashboard_schools.rb b/db/migrate/20240104174053_create_dashboard_schools.rb new file mode 100644 index 0000000..10b5cd6 --- /dev/null +++ b/db/migrate/20240104174053_create_dashboard_schools.rb @@ -0,0 +1,15 @@ +class CreateDashboardSchools < ActiveRecord::Migration[7.1] + def change + create_table :dashboard_schools do |t| + t.string :name + t.references :dashboard_district_id, null: false, foreign_key: true + t.text :description + t.string :slug, unique: true + t.integer :qualtrics_code + t.integer :dese_id + t.boolean :is_hs + + t.timestamps + end + end +end diff --git a/db/migrate/20240104174331_create_dashboard_admin_data_values.rb b/db/migrate/20240104174331_create_dashboard_admin_data_values.rb new file mode 100644 index 0000000..f9ec684 --- /dev/null +++ b/db/migrate/20240104174331_create_dashboard_admin_data_values.rb @@ -0,0 +1,12 @@ +class CreateDashboardAdminDataValues < ActiveRecord::Migration[7.1] + def change + create_table :dashboard_admin_data_values do |t| + t.float :likert_score + t.references :dashboard_school, null: false, foreign_key: true + t.references :dashboard_admin_data_item, null: false, foreign_key: true + t.references :dashboard_academic_year, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20240104174458_create_dashboard_ells.rb b/db/migrate/20240104174458_create_dashboard_ells.rb new file mode 100644 index 0000000..3e787d4 --- /dev/null +++ b/db/migrate/20240104174458_create_dashboard_ells.rb @@ -0,0 +1,10 @@ +class CreateDashboardElls < ActiveRecord::Migration[7.1] + def change + create_table :dashboard_ells do |t| + t.string :designation + t.string :slug, unique: true + + t.timestamps + end + end +end diff --git a/db/migrate/20240104174606_create_dashboard_genders.rb b/db/migrate/20240104174606_create_dashboard_genders.rb new file mode 100644 index 0000000..be3bf07 --- /dev/null +++ b/db/migrate/20240104174606_create_dashboard_genders.rb @@ -0,0 +1,10 @@ +class CreateDashboardGenders < ActiveRecord::Migration[7.1] + def change + create_table :dashboard_genders do |t| + t.integer :qualtrics_code + t.string :designation + + t.timestamps + end + end +end diff --git a/db/migrate/20240104174649_create_dashboard_incomes.rb b/db/migrate/20240104174649_create_dashboard_incomes.rb new file mode 100644 index 0000000..cb1d581 --- /dev/null +++ b/db/migrate/20240104174649_create_dashboard_incomes.rb @@ -0,0 +1,10 @@ +class CreateDashboardIncomes < ActiveRecord::Migration[7.1] + def change + create_table :dashboard_incomes do |t| + t.string :designation + t.string :slug, unique: true + + t.timestamps + end + end +end diff --git a/db/migrate/20240104181033_create_dashboard_races.rb b/db/migrate/20240104181033_create_dashboard_races.rb new file mode 100644 index 0000000..217646e --- /dev/null +++ b/db/migrate/20240104181033_create_dashboard_races.rb @@ -0,0 +1,11 @@ +class CreateDashboardRaces < ActiveRecord::Migration[7.1] + def change + create_table :dashboard_races do |t| + t.string :designation + t.integer :qualtrics_code + t.string :slug, unique: true + + t.timestamps + end + end +end diff --git a/db/migrate/20240104181617_create_dashboard_respondents.rb b/db/migrate/20240104181617_create_dashboard_respondents.rb new file mode 100644 index 0000000..5c54156 --- /dev/null +++ b/db/migrate/20240104181617_create_dashboard_respondents.rb @@ -0,0 +1,26 @@ +class CreateDashboardRespondents < ActiveRecord::Migration[7.1] + def change + create_table :dashboard_respondents do |t| + t.references :dashboard_school, null: false, foreign_key: true + t.references :dashboard_academic_year, null: false, foreign_key: true + t.integer :total_students + t.float :total_teachers + t.integer :pk + t.integer :k + t.integer :one + t.integer :two + t.integer :three + t.integer :four + t.integer :five + t.integer :six + t.integer :seven + t.integer :eight + t.integer :nine + t.integer :ten + t.integer :eleven + t.integer :twelve + + t.timestamps + end + end +end diff --git a/db/migrate/20240104181856_create_dashboard_response_rates.rb b/db/migrate/20240104181856_create_dashboard_response_rates.rb new file mode 100644 index 0000000..cb3ef71 --- /dev/null +++ b/db/migrate/20240104181856_create_dashboard_response_rates.rb @@ -0,0 +1,13 @@ +class CreateDashboardResponseRates < ActiveRecord::Migration[7.1] + def change + create_table :dashboard_response_rates do |t| + t.references :dashboard_subcategory, null: false, foreign_key: true + t.references :dashboard_school, null: false, foreign_key: true + t.references :dashboard_academic_year, null: false, foreign_key: true + t.float :school_response_rate + t.float :teacher_response_rate + + t.timestamps + end + end +end diff --git a/db/migrate/20240104183857_create_dashboard_scores.rb b/db/migrate/20240104183857_create_dashboard_scores.rb new file mode 100644 index 0000000..e0c8843 --- /dev/null +++ b/db/migrate/20240104183857_create_dashboard_scores.rb @@ -0,0 +1,12 @@ +class CreateDashboardScores < ActiveRecord::Migration[7.1] + def change + create_table :dashboard_scores do |t| + t.float :average + t.boolean :meets_teacher_threshold + t.boolean :meets_student_threshold + t.boolean :meets_admin_data_threshold + + t.timestamps + end + end +end diff --git a/db/migrate/20240104183934_create_dashboard_speds.rb b/db/migrate/20240104183934_create_dashboard_speds.rb new file mode 100644 index 0000000..ca23115 --- /dev/null +++ b/db/migrate/20240104183934_create_dashboard_speds.rb @@ -0,0 +1,10 @@ +class CreateDashboardSpeds < ActiveRecord::Migration[7.1] + def change + create_table :dashboard_speds do |t| + t.string :designation + t.string :slug, unique: true + + t.timestamps + end + end +end diff --git a/db/migrate/20240104184053_create_dashboard_students.rb b/db/migrate/20240104184053_create_dashboard_students.rb new file mode 100644 index 0000000..be65236 --- /dev/null +++ b/db/migrate/20240104184053_create_dashboard_students.rb @@ -0,0 +1,10 @@ +class CreateDashboardStudents < ActiveRecord::Migration[7.1] + def change + create_table :dashboard_students do |t| + t.string :lasid + t.string :response_id + + t.timestamps + end + end +end diff --git a/db/migrate/20240104190838_create_dashboard_student_races.rb b/db/migrate/20240104190838_create_dashboard_student_races.rb new file mode 100644 index 0000000..54296da --- /dev/null +++ b/db/migrate/20240104190838_create_dashboard_student_races.rb @@ -0,0 +1,10 @@ +class CreateDashboardStudentRaces < ActiveRecord::Migration[7.1] + def change + create_table :dashboard_student_races do |t| + t.references :dashboard_student, null: false, foreign_key: true + t.references :dashboard_race, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20240104192128_create_dashboard_survey_item_responses.rb b/db/migrate/20240104192128_create_dashboard_survey_item_responses.rb new file mode 100644 index 0000000..b2d46d7 --- /dev/null +++ b/db/migrate/20240104192128_create_dashboard_survey_item_responses.rb @@ -0,0 +1,20 @@ +class CreateDashboardSurveyItemResponses < ActiveRecord::Migration[7.1] + def change + create_table :dashboard_survey_item_responses do |t| + t.integer :likert_score + t.references :dashboard_school, null: false, foreign_key: true + t.references :dashboard_survey_item, null: false, foreign_key: true + t.references :dashboard_academic_year, null: false, foreign_key: true + t.references :dashboard_student, foreign_key: true + t.references :dashboard_gender, foreign_key: true + t.references :dashboard_income, foreign_key: true + t.references :dashboard_ell, foreign_key: true + t.references :dashboard_sped, foreign_key: true + t.string :response_id + t.integer :grade + t.datetime :recorded_date + + t.timestamps + end + end +end diff --git a/lib/dashboard/engine.rb b/lib/dashboard/engine.rb index d676b9d..c55b089 100644 --- a/lib/dashboard/engine.rb +++ b/lib/dashboard/engine.rb @@ -1,3 +1,5 @@ +require "friendly_id" + module Dashboard class Engine < ::Rails::Engine isolate_namespace Dashboard diff --git a/spec/dummy/db/schema.rb b/spec/dummy/db/schema.rb index 1b07f5b..1535b78 100644 --- a/spec/dummy/db/schema.rb +++ b/spec/dummy/db/schema.rb @@ -10,10 +10,68 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2023_12_23_040511) do +ActiveRecord::Schema[7.1].define(version: 2024_01_04_192128) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "dashboard_academic_years", force: :cascade do |t| + t.string "range" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["range"], name: "index_dashboard_academic_years_on_range" + end + + create_table "dashboard_admin_data_items", force: :cascade do |t| + t.string "admin_data_item_id" + t.string "description" + t.float "watch_low_benchmark" + t.float "growth_low_benchmark" + t.float "approval_low_benchmark" + t.float "ideal_low_benchmark" + t.boolean "hs_only_item" + t.bigint "dashboard_scale_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["dashboard_scale_id"], name: "index_dashboard_admin_data_items_on_dashboard_scale_id" + end + + create_table "dashboard_admin_data_values", force: :cascade do |t| + t.float "likert_score" + t.bigint "dashboard_school_id", null: false + t.bigint "dashboard_admin_data_item_id", null: false + t.bigint "dashboard_academic_year_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["dashboard_academic_year_id"], name: "idx_on_dashboard_academic_year_id_1de27231d5" + t.index ["dashboard_admin_data_item_id"], name: "idx_on_dashboard_admin_data_item_id_edae2faad3" + t.index ["dashboard_school_id"], name: "index_dashboard_admin_data_values_on_dashboard_school_id" + end + + create_table "dashboard_categories", force: :cascade do |t| + t.string "name" + t.text "description" + t.string "slug" + t.string "category_id" + t.string "short_description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "dashboard_districts", force: :cascade do |t| + t.string "name" + t.string "slug" + t.integer "qualtrics_code" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "dashboard_ells", force: :cascade do |t| + t.string "designation" + t.string "slug" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "dashboard_examples", force: :cascade do |t| t.string "text" t.text "body" @@ -21,4 +79,200 @@ ActiveRecord::Schema[7.1].define(version: 2023_12_23_040511) do t.datetime "updated_at", null: false end + create_table "dashboard_genders", force: :cascade do |t| + t.integer "qualtrics_code" + t.string "designation" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "dashboard_incomes", force: :cascade do |t| + t.string "designation" + t.string "slug" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "dashboard_measures", force: :cascade do |t| + t.string "measure_id" + t.string "name" + t.bigint "dashboard_subcategory_id", null: false + t.text "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["dashboard_subcategory_id"], name: "index_dashboard_measures_on_dashboard_subcategory_id" + end + + create_table "dashboard_races", force: :cascade do |t| + t.string "designation" + t.integer "qualtrics_code" + t.string "slug" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "dashboard_respondents", force: :cascade do |t| + t.bigint "dashboard_school_id", null: false + t.bigint "dashboard_academic_year_id", null: false + t.integer "total_students" + t.float "total_teachers" + t.integer "pk" + t.integer "k" + t.integer "one" + t.integer "two" + t.integer "three" + t.integer "four" + t.integer "five" + t.integer "six" + t.integer "seven" + t.integer "eight" + t.integer "nine" + t.integer "ten" + t.integer "eleven" + t.integer "twelve" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["dashboard_academic_year_id"], name: "index_dashboard_respondents_on_dashboard_academic_year_id" + t.index ["dashboard_school_id"], name: "index_dashboard_respondents_on_dashboard_school_id" + end + + create_table "dashboard_response_rates", force: :cascade do |t| + t.bigint "dashboard_subcategory_id", null: false + t.bigint "dashboard_school_id", null: false + t.bigint "dashboard_academic_year_id", null: false + t.float "school_response_rate" + t.float "teacher_response_rate" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["dashboard_academic_year_id"], name: "index_dashboard_response_rates_on_dashboard_academic_year_id" + t.index ["dashboard_school_id"], name: "index_dashboard_response_rates_on_dashboard_school_id" + t.index ["dashboard_subcategory_id"], name: "index_dashboard_response_rates_on_dashboard_subcategory_id" + end + + create_table "dashboard_scales", force: :cascade do |t| + t.string "scale_id" + t.bigint "dashboard_measure_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["dashboard_measure_id"], name: "index_dashboard_scales_on_dashboard_measure_id" + end + + create_table "dashboard_schools", force: :cascade do |t| + t.string "name" + t.bigint "dashboard_district_id", null: false + t.text "description" + t.string "slug" + t.integer "qualtrics_code" + t.integer "dese_id" + t.boolean "is_hs" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["dashboard_district_id"], name: "index_dashboard_schools_on_dashboard_district_id" + end + + create_table "dashboard_scores", force: :cascade do |t| + t.float "average" + t.boolean "meets_teacher_threshold" + t.boolean "meets_student_threshold" + t.boolean "meets_admin_data_threshold" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "dashboard_speds", force: :cascade do |t| + t.string "designation" + t.string "slug" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "dashboard_student_races", force: :cascade do |t| + t.bigint "dashboard_student_id", null: false + t.bigint "dashboard_race_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["dashboard_race_id"], name: "index_dashboard_student_races_on_dashboard_race_id" + t.index ["dashboard_student_id"], name: "index_dashboard_student_races_on_dashboard_student_id" + end + + create_table "dashboard_students", force: :cascade do |t| + t.string "lasid" + t.string "response_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "dashboard_subcategories", force: :cascade do |t| + t.string "name" + t.text "description" + t.string "subcategory_id" + t.bigint "dashboard_categories_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["dashboard_categories_id"], name: "index_dashboard_subcategories_on_dashboard_categories_id" + end + + create_table "dashboard_survey_item_responses", force: :cascade do |t| + t.integer "likert_score" + t.bigint "dashboard_school_id", null: false + t.bigint "dashboard_survey_item_id", null: false + t.bigint "dashboard_academic_year_id", null: false + t.bigint "dashboard_student_id", null: false + t.bigint "dashboard_gender_id", null: false + t.bigint "dashboard_income_id", null: false + t.bigint "dashboard_ell_id", null: false + t.bigint "dashboard_sped_id", null: false + t.string "response_id" + t.integer "grade" + t.datetime "recorded_date" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["dashboard_academic_year_id"], name: "idx_on_dashboard_academic_year_id_98a9cc7783" + t.index ["dashboard_ell_id"], name: "index_dashboard_survey_item_responses_on_dashboard_ell_id" + t.index ["dashboard_gender_id"], name: "index_dashboard_survey_item_responses_on_dashboard_gender_id" + t.index ["dashboard_income_id"], name: "index_dashboard_survey_item_responses_on_dashboard_income_id" + t.index ["dashboard_school_id"], name: "index_dashboard_survey_item_responses_on_dashboard_school_id" + t.index ["dashboard_sped_id"], name: "index_dashboard_survey_item_responses_on_dashboard_sped_id" + t.index ["dashboard_student_id"], name: "index_dashboard_survey_item_responses_on_dashboard_student_id" + t.index ["dashboard_survey_item_id"], name: "idx_on_dashboard_survey_item_id_3f6652fbc6" + end + + create_table "dashboard_survey_items", force: :cascade do |t| + t.string "survey_item_id" + t.string "prompt" + t.float "watch_low_benchmark" + t.float "growth_low_benchmark" + t.float "approval_low_benchmark" + t.float "ideal_low_benchmark" + t.bigint "dashboard_scale_id", null: false + t.boolean "on_short_form" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["dashboard_scale_id"], name: "index_dashboard_survey_items_on_dashboard_scale_id" + end + + add_foreign_key "dashboard_admin_data_items", "dashboard_scales" + add_foreign_key "dashboard_admin_data_values", "dashboard_academic_years" + add_foreign_key "dashboard_admin_data_values", "dashboard_admin_data_items" + add_foreign_key "dashboard_admin_data_values", "dashboard_schools" + add_foreign_key "dashboard_measures", "dashboard_subcategories" + add_foreign_key "dashboard_respondents", "dashboard_academic_years" + add_foreign_key "dashboard_respondents", "dashboard_schools" + add_foreign_key "dashboard_response_rates", "dashboard_academic_years" + add_foreign_key "dashboard_response_rates", "dashboard_schools" + add_foreign_key "dashboard_response_rates", "dashboard_subcategories" + add_foreign_key "dashboard_scales", "dashboard_measures" + add_foreign_key "dashboard_schools", "dashboard_districts" + add_foreign_key "dashboard_student_races", "dashboard_races" + add_foreign_key "dashboard_student_races", "dashboard_students" + add_foreign_key "dashboard_subcategories", "dashboard_categories", column: "dashboard_categories_id" + add_foreign_key "dashboard_survey_item_responses", "dashboard_academic_years" + add_foreign_key "dashboard_survey_item_responses", "dashboard_ells" + add_foreign_key "dashboard_survey_item_responses", "dashboard_genders" + add_foreign_key "dashboard_survey_item_responses", "dashboard_incomes" + add_foreign_key "dashboard_survey_item_responses", "dashboard_schools" + add_foreign_key "dashboard_survey_item_responses", "dashboard_speds" + add_foreign_key "dashboard_survey_item_responses", "dashboard_students" + add_foreign_key "dashboard_survey_item_responses", "dashboard_survey_items" + add_foreign_key "dashboard_survey_items", "dashboard_scales" end diff --git a/spec/factories/dashboard/academic_years.rb b/spec/factories/dashboard/academic_years.rb new file mode 100644 index 0000000..eab57b7 --- /dev/null +++ b/spec/factories/dashboard/academic_years.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :academic_year do + range { "MyString" } + end +end diff --git a/spec/factories/dashboard/admin_data_items.rb b/spec/factories/dashboard/admin_data_items.rb new file mode 100644 index 0000000..57053f1 --- /dev/null +++ b/spec/factories/dashboard/admin_data_items.rb @@ -0,0 +1,12 @@ +FactoryBot.define do + factory :admin_data_item do + admin_data_item_id { "MyString" } + description { "MyString" } + watch_low_benchmark { 1.5 } + growth_low_benchmark { 1.5 } + approval_low_benchmark { 1.5 } + ideal_low_benchmark { 1.5 } + hs_only_item { false } + dashboard_scale { nil } + end +end diff --git a/spec/factories/dashboard/admin_data_values.rb b/spec/factories/dashboard/admin_data_values.rb new file mode 100644 index 0000000..4869759 --- /dev/null +++ b/spec/factories/dashboard/admin_data_values.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :admin_data_value do + likert_score { 1.5 } + dashboard_school { nil } + dashboard_admin_data_item { nil } + dashboard_academic_year { nil } + end +end diff --git a/spec/factories/dashboard/categories.rb b/spec/factories/dashboard/categories.rb new file mode 100644 index 0000000..f4548c7 --- /dev/null +++ b/spec/factories/dashboard/categories.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :category do + name { "MyString" } + description { "MyText" } + slug { "MyString" } + category_id { "MyString" } + short_description { "MyString" } + end +end diff --git a/spec/factories/dashboard/districts.rb b/spec/factories/dashboard/districts.rb new file mode 100644 index 0000000..f88e614 --- /dev/null +++ b/spec/factories/dashboard/districts.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :district do + name { "MyString" } + slug { "MyString" } + qualtrics_code { 1 } + end +end diff --git a/spec/factories/dashboard/ells.rb b/spec/factories/dashboard/ells.rb new file mode 100644 index 0000000..db1bced --- /dev/null +++ b/spec/factories/dashboard/ells.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :ell do + designation { "MyString" } + slug { "MyString" } + end +end diff --git a/spec/factories/dashboard/genders.rb b/spec/factories/dashboard/genders.rb new file mode 100644 index 0000000..39f7cc2 --- /dev/null +++ b/spec/factories/dashboard/genders.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :gender do + qualtrics_code { 1 } + designation { "MyString" } + end +end diff --git a/spec/factories/dashboard/incomes.rb b/spec/factories/dashboard/incomes.rb new file mode 100644 index 0000000..3a24e73 --- /dev/null +++ b/spec/factories/dashboard/incomes.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :income do + designation { "MyString" } + slug { "MyString" } + end +end diff --git a/spec/factories/dashboard/measures.rb b/spec/factories/dashboard/measures.rb new file mode 100644 index 0000000..b5279ee --- /dev/null +++ b/spec/factories/dashboard/measures.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :measure do + measure_id { "MyString" } + name { "MyString" } + dashboard_subcategory { nil } + description { "MyText" } + end +end diff --git a/spec/factories/dashboard/races.rb b/spec/factories/dashboard/races.rb new file mode 100644 index 0000000..9cee5bc --- /dev/null +++ b/spec/factories/dashboard/races.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :race do + designation { "MyString" } + qualtrics_code { 1 } + slug { "MyString" } + end +end diff --git a/spec/factories/dashboard/respondents.rb b/spec/factories/dashboard/respondents.rb new file mode 100644 index 0000000..8298cb7 --- /dev/null +++ b/spec/factories/dashboard/respondents.rb @@ -0,0 +1,22 @@ +FactoryBot.define do + factory :respondent do + dashboard_school { nil } + dashboard_academic_year { nil } + total_students { 1 } + total_teachers { 1.5 } + pk { 1 } + k { 1 } + one { 1 } + two { 1 } + three { 1 } + four { 1 } + five { 1 } + six { 1 } + seven { 1 } + eight { 1 } + nine { 1 } + ten { 1 } + eleven { 1 } + twelve { 1 } + end +end diff --git a/spec/factories/dashboard/response_rates.rb b/spec/factories/dashboard/response_rates.rb new file mode 100644 index 0000000..980e139 --- /dev/null +++ b/spec/factories/dashboard/response_rates.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :response_rate do + subcategory { nil } + school { nil } + academic_year { nil } + school_response_rate { 1.5 } + teacher_response_rate { 1.5 } + end +end diff --git a/spec/factories/dashboard/scales.rb b/spec/factories/dashboard/scales.rb new file mode 100644 index 0000000..7770de6 --- /dev/null +++ b/spec/factories/dashboard/scales.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :scale do + scale_id { "MyString" } + measure { nil } + end +end diff --git a/spec/factories/dashboard/schools.rb b/spec/factories/dashboard/schools.rb new file mode 100644 index 0000000..4a44c7d --- /dev/null +++ b/spec/factories/dashboard/schools.rb @@ -0,0 +1,11 @@ +FactoryBot.define do + factory :school do + name { "MyString" } + dashboard_district { nil } + description { "MyText" } + slug { "MyString" } + qualtrics_code { 1 } + dese_id { 1 } + is_hs { false } + end +end diff --git a/spec/factories/dashboard/scores.rb b/spec/factories/dashboard/scores.rb new file mode 100644 index 0000000..3f305a8 --- /dev/null +++ b/spec/factories/dashboard/scores.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :score do + average { 1.5 } + meets_teacher_threshold { false } + meets_student_threshold { false } + meets_admin_data_threshold { false } + end +end diff --git a/spec/factories/dashboard/speds.rb b/spec/factories/dashboard/speds.rb new file mode 100644 index 0000000..96bbf80 --- /dev/null +++ b/spec/factories/dashboard/speds.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :sped do + designation { "MyString" } + slug { "MyString" } + end +end diff --git a/spec/factories/dashboard/student_races.rb b/spec/factories/dashboard/student_races.rb new file mode 100644 index 0000000..6b6fc58 --- /dev/null +++ b/spec/factories/dashboard/student_races.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :student_race do + dashboard_student { nil } + dashboard_race { nil } + end +end diff --git a/spec/factories/dashboard/students.rb b/spec/factories/dashboard/students.rb new file mode 100644 index 0000000..9aebc0b --- /dev/null +++ b/spec/factories/dashboard/students.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :student do + lasid { "MyString" } + response_id { "MyString" } + end +end diff --git a/spec/factories/dashboard/subcategories.rb b/spec/factories/dashboard/subcategories.rb new file mode 100644 index 0000000..4f30c02 --- /dev/null +++ b/spec/factories/dashboard/subcategories.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :subcategory do + name { "MyString" } + description { "MyText" } + subcategory_id { "MyString" } + dashboard_categories { nil } + end +end diff --git a/spec/factories/dashboard/survey_item_responses.rb b/spec/factories/dashboard/survey_item_responses.rb new file mode 100644 index 0000000..807dd82 --- /dev/null +++ b/spec/factories/dashboard/survey_item_responses.rb @@ -0,0 +1,16 @@ +FactoryBot.define do + factory :survey_item_response do + likert_score { 1 } + dashboard_school { nil } + dashboard_survey_item { nil } + dashboard_academic_year { nil } + dashboard_student { nil } + dashboard_gender { nil } + dashboard_income { nil } + dashboard_ell { nil } + dashboard_sped { nil } + response_id { "MyString" } + grade { 1 } + recorded_date { "2024-01-04 11:21:28" } + end +end diff --git a/spec/factories/dashboard/survey_items.rb b/spec/factories/dashboard/survey_items.rb new file mode 100644 index 0000000..2253442 --- /dev/null +++ b/spec/factories/dashboard/survey_items.rb @@ -0,0 +1,12 @@ +FactoryBot.define do + factory :survey_item do + survey_item_id { "MyString" } + prompt { "MyString" } + watch_low_benchmark { 1.5 } + growth_low_benchmark { 1.5 } + approval_low_benchmark { 1.5 } + ideal_low_benchmark { 1.5 } + dashboard_scale { nil } + on_short_form { false } + end +end diff --git a/spec/models/dashboard/academic_year_spec.rb b/spec/models/dashboard/academic_year_spec.rb new file mode 100644 index 0000000..ba06232 --- /dev/null +++ b/spec/models/dashboard/academic_year_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +module Dashboard + RSpec.describe AcademicYear, type: :model do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/models/dashboard/admin_data_item_spec.rb b/spec/models/dashboard/admin_data_item_spec.rb new file mode 100644 index 0000000..fb4cdf3 --- /dev/null +++ b/spec/models/dashboard/admin_data_item_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +module Dashboard + RSpec.describe AdminDataItem, type: :model do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/models/dashboard/admin_data_value_spec.rb b/spec/models/dashboard/admin_data_value_spec.rb new file mode 100644 index 0000000..c092525 --- /dev/null +++ b/spec/models/dashboard/admin_data_value_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +module Dashboard + RSpec.describe AdminDataValue, type: :model do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/models/dashboard/category_spec.rb b/spec/models/dashboard/category_spec.rb new file mode 100644 index 0000000..b95f319 --- /dev/null +++ b/spec/models/dashboard/category_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +module Dashboard + RSpec.describe Category, type: :model do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/models/dashboard/district_spec.rb b/spec/models/dashboard/district_spec.rb new file mode 100644 index 0000000..2bed703 --- /dev/null +++ b/spec/models/dashboard/district_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +module Dashboard + RSpec.describe District, type: :model do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/models/dashboard/ell_spec.rb b/spec/models/dashboard/ell_spec.rb new file mode 100644 index 0000000..e76d984 --- /dev/null +++ b/spec/models/dashboard/ell_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +module Dashboard + RSpec.describe Ell, type: :model do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/models/dashboard/gender_spec.rb b/spec/models/dashboard/gender_spec.rb new file mode 100644 index 0000000..093507b --- /dev/null +++ b/spec/models/dashboard/gender_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +module Dashboard + RSpec.describe Gender, type: :model do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/models/dashboard/income_spec.rb b/spec/models/dashboard/income_spec.rb new file mode 100644 index 0000000..52c7e50 --- /dev/null +++ b/spec/models/dashboard/income_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +module Dashboard + RSpec.describe Income, type: :model do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/models/dashboard/measure_spec.rb b/spec/models/dashboard/measure_spec.rb new file mode 100644 index 0000000..aa420d9 --- /dev/null +++ b/spec/models/dashboard/measure_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +module Dashboard + RSpec.describe Measure, type: :model do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/models/dashboard/race_spec.rb b/spec/models/dashboard/race_spec.rb new file mode 100644 index 0000000..863ffa8 --- /dev/null +++ b/spec/models/dashboard/race_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +module Dashboard + RSpec.describe Race, type: :model do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/models/dashboard/respondent_spec.rb b/spec/models/dashboard/respondent_spec.rb new file mode 100644 index 0000000..e8c0c15 --- /dev/null +++ b/spec/models/dashboard/respondent_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +module Dashboard + RSpec.describe Respondent, type: :model do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/models/dashboard/response_rate_spec.rb b/spec/models/dashboard/response_rate_spec.rb new file mode 100644 index 0000000..e9ab20b --- /dev/null +++ b/spec/models/dashboard/response_rate_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +module Dashboard + RSpec.describe ResponseRate, type: :model do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/models/dashboard/scale_spec.rb b/spec/models/dashboard/scale_spec.rb new file mode 100644 index 0000000..f4f670d --- /dev/null +++ b/spec/models/dashboard/scale_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +module Dashboard + RSpec.describe Scale, type: :model do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/models/dashboard/school_spec.rb b/spec/models/dashboard/school_spec.rb new file mode 100644 index 0000000..89e5080 --- /dev/null +++ b/spec/models/dashboard/school_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +module Dashboard + RSpec.describe School, type: :model do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/models/dashboard/score_spec.rb b/spec/models/dashboard/score_spec.rb new file mode 100644 index 0000000..824e63e --- /dev/null +++ b/spec/models/dashboard/score_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +module Dashboard + RSpec.describe Score, type: :model do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/models/dashboard/sped_spec.rb b/spec/models/dashboard/sped_spec.rb new file mode 100644 index 0000000..221d159 --- /dev/null +++ b/spec/models/dashboard/sped_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +module Dashboard + RSpec.describe Sped, type: :model do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/models/dashboard/student_race_spec.rb b/spec/models/dashboard/student_race_spec.rb new file mode 100644 index 0000000..9c5e742 --- /dev/null +++ b/spec/models/dashboard/student_race_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +module Dashboard + RSpec.describe StudentRace, type: :model do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/models/dashboard/student_spec.rb b/spec/models/dashboard/student_spec.rb new file mode 100644 index 0000000..e36ec2e --- /dev/null +++ b/spec/models/dashboard/student_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +module Dashboard + RSpec.describe Student, type: :model do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/models/dashboard/subcategory_spec.rb b/spec/models/dashboard/subcategory_spec.rb new file mode 100644 index 0000000..21da29d --- /dev/null +++ b/spec/models/dashboard/subcategory_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +module Dashboard + RSpec.describe Subcategory, type: :model do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/models/dashboard/survey_item_response_spec.rb b/spec/models/dashboard/survey_item_response_spec.rb new file mode 100644 index 0000000..86cdcdc --- /dev/null +++ b/spec/models/dashboard/survey_item_response_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +module Dashboard + RSpec.describe SurveyItemResponse, type: :model do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/models/dashboard/survey_item_spec.rb b/spec/models/dashboard/survey_item_spec.rb new file mode 100644 index 0000000..0a9ecf0 --- /dev/null +++ b/spec/models/dashboard/survey_item_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +module Dashboard + RSpec.describe SurveyItem, type: :model do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/views/dashboard/examples/index.html.erb_spec.rb b/spec/views/dashboard/examples/index.html.erb_spec.rb index 649774b..8d1a36a 100644 --- a/spec/views/dashboard/examples/index.html.erb_spec.rb +++ b/spec/views/dashboard/examples/index.html.erb_spec.rb @@ -2,6 +2,7 @@ require "rails_helper" require "nokogiri" module Dashboard + include Engine.routes.url_helpers RSpec.describe "/dashboard/examples/index", type: :view do before(:each) do assign(:examples, [ diff --git a/spec/views/dashboard/examples/new.html.erb_spec.rb b/spec/views/dashboard/examples/new.html.erb_spec.rb index 6d97650..a1efb25 100644 --- a/spec/views/dashboard/examples/new.html.erb_spec.rb +++ b/spec/views/dashboard/examples/new.html.erb_spec.rb @@ -1,7 +1,8 @@ require "rails_helper" module Dashboard - RSpec.xdescribe "dashboard/examples/new", type: :view do + include Engine.routes.url_helpers + RSpec.describe "dashboard/examples/new", type: :view do before(:each) do assign(:example, Example.new( text: "MyString",