diff --git a/app/models/measure.rb b/app/models/measure.rb index dad2fd37..22c0ad4e 100644 --- a/app/models/measure.rb +++ b/app/models/measure.rb @@ -25,13 +25,13 @@ class Measure < ActiveRecord::Base end def student_survey_items_with_sufficient_responses(school:, academic_year:) - SurveyItem.where(id: SurveyItem.joins("inner join survey_item_responses on survey_item_responses.survey_item_id = survey_items.id") + 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) - .group("survey_items.id") - .having("count(*) >= 10") + "survey_item_responses.academic_year": academic_year, + "survey_item_responses.survey_item_id": survey_items.student_survey_items) + .group('survey_items.id') + .having('count(*) >= 10') .count.keys) end @@ -123,6 +123,14 @@ class Measure < ActiveRecord::Base 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:) @@ -181,14 +189,6 @@ class Measure < ActiveRecord::Base @grouped_responses[[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 - def sufficient_student_data?(school:, academic_year:) return false unless includes_student_survey_items? return false if no_student_responses_exist?(school:, academic_year:) @@ -222,7 +222,7 @@ class Measure < ActiveRecord::Base 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:) + !sufficient_teacher_data?(school:, academic_year:) memo[[school, academic_year]] = lacks_sufficient_survey_data && !includes_admin_data_items? end diff --git a/app/models/report/subcategory.rb b/app/models/report/subcategory.rb new file mode 100644 index 00000000..fa2d898a --- /dev/null +++ b/app/models/report/subcategory.rb @@ -0,0 +1,94 @@ +module Report + class Subcategory + def self.create_report(schools: School.all, academic_years: AcademicYear.all, subcategories: ::Subcategory.all, filename: 'subcategories.csv') + data = [] + data << ['School', 'Academic Year', 'Subcategory', 'Student Score', 'Student Zone', 'Teacher Score', + 'Teacher Zone', 'Admin Score', 'Admin Zone', 'All Score (Average)', 'All Score Zone'] + schools.each do |school| + academic_years.each do |academic_year| + subcategories.each do |subcategory| + next if Respondent.where(school:, academic_year:).empty? + + response_rate = subcategory.response_rate(school:, academic_year:) + next unless response_rate.meets_student_threshold? || response_rate.meets_teacher_threshold? + + score = subcategory.score(school:, academic_year:) + zone = subcategory.zone(school:, academic_year:).type.to_s.capitalize + + row = [response_rate, subcategory, school, academic_year] + data << [school.name, + academic_year.range, + subcategory.subcategory_id, + student_score(row:), + student_zone(row:), + teacher_score(row:), + teacher_zone(row:), + admin_score(row:), + admin_zone(row:), + score, + zone] + end + end + end + + FileUtils.mkdir_p Rails.root.join('tmp', 'reports') + filepath = Rails.root.join('tmp', 'reports', filename) + write_csv(data:, filepath:) + data + end + + def self.write_csv(data:, filepath:) + csv = CSV.generate do |csv| + data.each do |row| + csv << row + end + end + File.write(filepath, csv) + end + + def self.student_score(row:) + row in [response_rate, subcategory, school, academic_year] + student_score = subcategory.student_score(school:, academic_year:) if response_rate.meets_student_threshold? + student_score || 'N/A' + end + + def self.student_zone(row:) + row in [response_rate, subcategory, school, academic_year] + if response_rate.meets_student_threshold? + student_zone = subcategory.student_zone(school:, + academic_year:).type.to_s.capitalize + end + + student_zone || 'N/A' + end + + def self.teacher_score(row:) + row in [response_rate, subcategory, school, academic_year] + teacher_score = subcategory.teacher_score(school:, academic_year:) if response_rate.meets_teacher_threshold? + + teacher_score || 'N/A' + end + + def self.teacher_zone(row:) + row in [response_rate, subcategory, school, academic_year] + if response_rate.meets_teacher_threshold? + teacher_zone = subcategory.teacher_zone(school:, academic_year:).type.to_s.capitalize + end + + teacher_zone || 'N/A' + end + + def self.admin_score(row:) + row in [response_rate, subcategory, school, academic_year] + admin_score = subcategory.admin_score(school:, academic_year:) + admin_score = 'N/A' unless admin_score >= 0 + admin_score + end + + def self.admin_zone(row:) + row in [response_rate, subcategory, school, academic_year] + tmp_zone = subcategory.admin_zone(school:, academic_year:).type + tmp_zone == :insufficient_data ? 'N/A' : tmp_zone.to_s.capitalize + end + end +end diff --git a/app/models/subcategory.rb b/app/models/subcategory.rb index 988eb286..eec98428 100644 --- a/app/models/subcategory.rb +++ b/app/models/subcategory.rb @@ -7,11 +7,27 @@ class Subcategory < ActiveRecord::Base has_many :survey_items, through: :measures def score(school:, academic_year:) - scores = measures.map do |measure| + measures.map do |measure| measure.score(school:, academic_year:).average - end - scores = scores.reject(&:nil?) - scores.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:) @@ -25,4 +41,51 @@ class Subcategory < ActiveRecord::Base @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 end diff --git a/lib/tasks/report.rake b/lib/tasks/report.rake new file mode 100644 index 00000000..3c87cb9c --- /dev/null +++ b/lib/tasks/report.rake @@ -0,0 +1,6 @@ +namespace :report do + desc 'create a report of the scores for all subcategories' + task subcategory: :environment do + Report::Subcategory.create_report(filename: 'mciea_subcategory_report.csv') + end +end diff --git a/spec/models/report/subcategory_spec.rb b/spec/models/report/subcategory_spec.rb new file mode 100644 index 00000000..339fff5e --- /dev/null +++ b/spec/models/report/subcategory_spec.rb @@ -0,0 +1,91 @@ +require 'rails_helper' +RSpec.describe Report::Subcategory, type: :model do + let(:school) { create(:school, name: 'Milford High', slug: 'milford-high') } + let(:academic_year) { create(:academic_year, range: '2018-2019') } + let(:subcategory) { create(:subcategory, subcategory_id: '1A') } + let(:respondent) { create(:respondent, school:, academic_year:) } + before :each do + school + academic_year + subcategory + respondent + end + let(:measure) { create(:measure, subcategory:) } + let(:scale) { create(:scale, measure:) } + let(:survey_item) { create(:student_survey_item, scale:) } + + context 'when creating a report for a subcategory' do + before :each do + create_list(:survey_item_response, 10, survey_item:, school:, academic_year:) + end + + it 'creates a report for subcategories' do + expect(Report::Subcategory.create_report).to be_a(Array) + headers = Report::Subcategory.create_report.first + expect(headers).to eq(['School', 'Academic Year', 'Subcategory', 'Student Score', + 'Student Zone', 'Teacher Score', 'Teacher Zone', 'Admin Score', 'Admin Zone', 'All Score (Average)', 'All Score Zone']) + end + + it 'Adds information about the first school and first academic year to the report' do + report = Report::Subcategory.create_report + report[1] in [school_name, academic_year, subcategory_id, *] + expect(school_name).to eq('Milford High') + expect(academic_year).to eq('2018-2019') + expect(subcategory_id).to eq('1A') + end + end + describe '.create_report' do + before do + allow(Report::Subcategory).to receive(:write_csv) + end + + it 'generates a CSV report' do + expect(FileUtils).to receive(:mkdir_p).with(Rails.root.join('tmp', 'reports')) + + Report::Subcategory.create_report + + expect(Report::Subcategory).to have_received(:write_csv) + end + + it 'returns the report data' do + data = Report::Subcategory.create_report + + expect(data).to be_an(Array) + end + end + describe '.write_csv' do + it 'writes the data to a CSV file' do + data = [['School', 'Academic Year', 'Subcategory'], ['School A', '2022-2023', 'Category A']] + filepath = Rails.root.join('tmp', 'spec', 'reports', 'subcategories.csv') + + FileUtils.mkdir_p Rails.root.join('tmp', 'spec', 'reports') + Report::Subcategory.write_csv(data:, filepath:) + + csv_data = File.read(filepath) + expect(csv_data).to include('School,Academic Year,Subcategory') + expect(csv_data).to include('School A,2022-2023,Category A') + end + end + + describe '.student_score' do + let(:response_rate) { create(:response_rate, subcategory:, school:, academic_year:) } + let(:row) { [response_rate, subcategory, school, academic_year] } + + it 'returns student score if response rate meets student threshold' do + allow(subcategory).to receive(:student_score).and_return(80) + allow(response_rate).to receive(:meets_student_threshold?).and_return(true) + + score = Report::Subcategory.student_score(row:) + + expect(score).to eq(80) + end + + it 'returns "N/A" if response rate does not meet student threshold' do + allow(response_rate).to receive(:meets_student_threshold?).and_return(false) + + score = Report::Subcategory.student_score(row:) + + expect(score).to eq('N/A') + end + end +end