mirror of
https://github.com/edcommonwealth/Dashboard.git
synced 2026-03-07 13:38:12 -08:00
chore: start adding overview page
This commit is contained in:
parent
1b0af124f7
commit
64b4d599c7
33 changed files with 783 additions and 199 deletions
8
app/controllers/dashboard/analyze_controller.rb
Normal file
8
app/controllers/dashboard/analyze_controller.rb
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AnalyzeController < SqmApplicationController
|
||||||
|
def index
|
||||||
|
@presenter = Analyze::Presenter.new(params:, school: @school, academic_year: @academic_year)
|
||||||
|
@background ||= BackgroundPresenter.new(num_of_columns: @presenter.graph.columns.count)
|
||||||
|
end
|
||||||
|
end
|
||||||
11
app/controllers/dashboard/categories_controller.rb
Normal file
11
app/controllers/dashboard/categories_controller.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CategoriesController < SqmApplicationController
|
||||||
|
helper GaugeHelper
|
||||||
|
|
||||||
|
def show
|
||||||
|
@categories = Category.sorted.map { |category| CategoryPresenter.new(category:) }
|
||||||
|
|
||||||
|
@category = CategoryPresenter.new(category: Category.find_by_slug(params[:id]))
|
||||||
|
end
|
||||||
|
end
|
||||||
8
app/controllers/dashboard/gps_controller.rb
Normal file
8
app/controllers/dashboard/gps_controller.rb
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
class GpsController < ActionController::Base
|
||||||
|
def index
|
||||||
|
# respond_to do |format|
|
||||||
|
# format.html
|
||||||
|
# format.csv { send_data Report::Gps.to_csv, disposition: 'attachment', filename: "gps_#{Date.today}.csv" }
|
||||||
|
# end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -49,7 +49,7 @@ module Dashboard
|
||||||
academic_year = AcademicYear.all.order(range: :DESC).find do |ay|
|
academic_year = AcademicYear.all.order(range: :DESC).find do |ay|
|
||||||
Subcategory.all.any? do |subcategory|
|
Subcategory.all.any? do |subcategory|
|
||||||
rate = subcategory.response_rate(school:, academic_year: ay)
|
rate = subcategory.response_rate(school:, academic_year: ay)
|
||||||
rate.meets_student_threshold || rate.meets_teacher_threshold
|
rate.meets_student_threshold? || rate.meets_teacher_threshold?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
44
app/controllers/dashboard/overview_controller.rb
Normal file
44
app/controllers/dashboard/overview_controller.rb
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Dashboard
|
||||||
|
class OverviewController < SqmApplicationController
|
||||||
|
before_action :check_empty_dataset, only: [:index]
|
||||||
|
helper VarianceHelper
|
||||||
|
|
||||||
|
def index
|
||||||
|
@variance_chart_row_presenters = measures.map(&method(:presenter_for_measure))
|
||||||
|
@category_presenters = categories.map { |category| CategoryPresenter.new(category:) }
|
||||||
|
@student_response_rate_presenter = ResponseRatePresenter.new(focus: :student, school: @school,
|
||||||
|
academic_year: @academic_year)
|
||||||
|
@teacher_response_rate_presenter = ResponseRatePresenter.new(focus: :teacher, school: @school,
|
||||||
|
academic_year: @academic_year)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def presenter_for_measure(measure)
|
||||||
|
score = measure.score(school: @school, academic_year: @academic_year)
|
||||||
|
|
||||||
|
VarianceChartRowPresenter.new(measure:, score:)
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_empty_dataset
|
||||||
|
@has_empty_dataset = subcategories.none? do |subcategory|
|
||||||
|
response_rate = subcategory.response_rate(school: @school, academic_year: @academic_year)
|
||||||
|
response_rate.meets_student_threshold? || response_rate.meets_teacher_threshold?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def measures
|
||||||
|
@measures ||= subcategories.flat_map(&:measures)
|
||||||
|
end
|
||||||
|
|
||||||
|
def subcategories
|
||||||
|
@subcategories ||= categories.flat_map(&:subcategories)
|
||||||
|
end
|
||||||
|
|
||||||
|
def categories
|
||||||
|
@categories ||= Category.sorted.includes(%i[measures scales admin_data_items subcategories])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
3
app/controllers/dashboard/reports_controller.rb
Normal file
3
app/controllers/dashboard/reports_controller.rb
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
class ReportsController < ApplicationController
|
||||||
|
def index; end
|
||||||
|
end
|
||||||
44
app/controllers/dashboard/sqm_application_controller.rb
Normal file
44
app/controllers/dashboard/sqm_application_controller.rb
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Dashboard
|
||||||
|
class SqmApplicationController < ApplicationController
|
||||||
|
protect_from_forgery with: :exception, prepend: true
|
||||||
|
before_action :set_schools_and_districts
|
||||||
|
before_action :authenticate_district
|
||||||
|
|
||||||
|
helper HeaderHelper
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def authenticate_district
|
||||||
|
authenticate(district_name, "#{district_name}!")
|
||||||
|
end
|
||||||
|
|
||||||
|
def district_name
|
||||||
|
@district_name ||= @district.name.split(" ").first.downcase
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_schools_and_districts
|
||||||
|
@district = District.find_by_slug district_slug
|
||||||
|
@districts = District.all.order(:name)
|
||||||
|
@school = School.find_by_slug(school_slug)
|
||||||
|
@schools = School.includes([:district]).where(district: @district).order(:name)
|
||||||
|
@academic_year = AcademicYear.find_by_range params[:year]
|
||||||
|
@academic_years = AcademicYear.all.order(range: :desc)
|
||||||
|
end
|
||||||
|
|
||||||
|
def district_slug
|
||||||
|
params[:district_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
def school_slug
|
||||||
|
params[:school_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate(username, password)
|
||||||
|
authenticate_or_request_with_http_basic do |u, p|
|
||||||
|
u == username && p == password
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
70
app/helpers/dashboard/analyze_helper.rb
Normal file
70
app/helpers/dashboard/analyze_helper.rb
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Dashboard
|
||||||
|
module AnalyzeHelper
|
||||||
|
def svg_height
|
||||||
|
400
|
||||||
|
end
|
||||||
|
|
||||||
|
def zone_label_width
|
||||||
|
15
|
||||||
|
end
|
||||||
|
|
||||||
|
def graph_width
|
||||||
|
85
|
||||||
|
end
|
||||||
|
|
||||||
|
def analyze_graph_height
|
||||||
|
85
|
||||||
|
end
|
||||||
|
|
||||||
|
def analyze_zone_height
|
||||||
|
analyze_graph_height / 5
|
||||||
|
end
|
||||||
|
|
||||||
|
def zone_height_percentage
|
||||||
|
analyze_zone_height / 100.0
|
||||||
|
end
|
||||||
|
|
||||||
|
def analyze_category_link(district:, school:, academic_year:, category:)
|
||||||
|
year = academic_year.range
|
||||||
|
"/districts/#{district.slug}/schools/#{school.slug}/analyze?year=#{year}&academic_years=#{year}&category=#{category.category_id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def analyze_subcategory_link(district:, school:, academic_year:, category:, subcategory:)
|
||||||
|
"/districts/#{district.slug}/schools/#{school.slug}/analyze?year=#{academic_year.range}&category=#{category.category_id}&subcategory=#{subcategory.subcategory_id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def colors
|
||||||
|
@colors ||= ["#49416D", "#FFC857", "#920020", "#00B0B3", "#B2D236", "#004D61", "#FFB3CC", "#FF3D00", "#212121", "#9E9D24",
|
||||||
|
"#689F38", "#388E3C", "#00897B", "#00796B", "#00695C", "#004D40", "#1B5E20", "#FF6F00", "#33691E", "#D50000",
|
||||||
|
"#827717", "#F57F17", "#FF6F00", "#E65100", "#BF360C", "#3E2723", "#263238", "#37474F", "#455A64"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def empty_dataset?(measures:, school:, academic_year:)
|
||||||
|
@empty_dataset ||= Hash.new do |memo, (school, academic_year)|
|
||||||
|
memo[[school, academic_year]] = measures.none? do |measure|
|
||||||
|
response_rate = measure.subcategory.response_rate(school:, academic_year:)
|
||||||
|
response_rate.meets_student_threshold || response_rate.meets_teacher_threshold || measure.sufficient_admin_data?(school:, academic_year:)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@empty_dataset[[school, academic_year]]
|
||||||
|
end
|
||||||
|
|
||||||
|
def empty_survey_dataset?(measures:, school:, academic_year:)
|
||||||
|
@empty_survey_dataset ||= Hash.new do |memo, (school, academic_year)|
|
||||||
|
memo[[school, academic_year]] = measures.none? do |measure|
|
||||||
|
response_rate = measure.subcategory.response_rate(school:, academic_year:)
|
||||||
|
response_rate.meets_student_threshold || response_rate.meets_teacher_threshold
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@empty_survey_dataset[[school, academic_year]]
|
||||||
|
end
|
||||||
|
|
||||||
|
def base_url
|
||||||
|
analyze_subcategory_link(district: @district, school: @school, academic_year: @academic_year, category: @presenter.category,
|
||||||
|
subcategory: @presenter.subcategory)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
99
app/helpers/dashboard/gauge_helper.rb
Normal file
99
app/helpers/dashboard/gauge_helper.rb
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
Point = Struct.new(:x, :y)
|
||||||
|
Rect = Struct.new(:x, :y, :width, :height)
|
||||||
|
|
||||||
|
module GaugeHelper
|
||||||
|
def outer_radius
|
||||||
|
100
|
||||||
|
end
|
||||||
|
|
||||||
|
def inner_radius
|
||||||
|
50
|
||||||
|
end
|
||||||
|
|
||||||
|
def stroke_width
|
||||||
|
1
|
||||||
|
end
|
||||||
|
|
||||||
|
def effective_radius
|
||||||
|
outer_radius + stroke_width
|
||||||
|
end
|
||||||
|
|
||||||
|
def diameter
|
||||||
|
2 * effective_radius
|
||||||
|
end
|
||||||
|
|
||||||
|
def width
|
||||||
|
diameter
|
||||||
|
end
|
||||||
|
|
||||||
|
def height
|
||||||
|
outer_radius + 2 * stroke_width + key_benchmark_indicator_gutter
|
||||||
|
end
|
||||||
|
|
||||||
|
def key_benchmark_indicator_gutter
|
||||||
|
10
|
||||||
|
end
|
||||||
|
|
||||||
|
def viewbox
|
||||||
|
x = arc_center.x - effective_radius
|
||||||
|
y = arc_center.y - effective_radius - key_benchmark_indicator_gutter
|
||||||
|
Rect.new(x, y, width, height)
|
||||||
|
end
|
||||||
|
|
||||||
|
def arc_center
|
||||||
|
Point.new(0, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
def arc_radius(radius)
|
||||||
|
"#{radius} #{radius}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def angle_for(percentage:)
|
||||||
|
-Math::PI * (1 - percentage)
|
||||||
|
end
|
||||||
|
|
||||||
|
def arc_end_point_for(radius:, percentage:)
|
||||||
|
angle = angle_for(percentage:)
|
||||||
|
|
||||||
|
x = arc_center.x + radius * Math.cos(angle)
|
||||||
|
y = arc_center.y + radius * Math.sin(angle)
|
||||||
|
Point.new(x, y)
|
||||||
|
end
|
||||||
|
|
||||||
|
def arc_end_line_destination(radius:, percentage:)
|
||||||
|
angle = angle_for(percentage:)
|
||||||
|
x = arc_center.x + radius * Math.cos(angle)
|
||||||
|
y = arc_center.y + radius * Math.sin(angle)
|
||||||
|
Point.new(x, y)
|
||||||
|
end
|
||||||
|
|
||||||
|
def arc_start_point
|
||||||
|
Point.new(arc_center.x - outer_radius, arc_center.y)
|
||||||
|
end
|
||||||
|
|
||||||
|
def move_to(point:)
|
||||||
|
"M #{coordinates_for(point)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def draw_arc(radius:, percentage:, clockwise:)
|
||||||
|
sweep_flag = clockwise ? 1 : 0
|
||||||
|
"A #{arc_radius(radius)} 0 0 #{sweep_flag} #{coordinates_for(arc_end_point_for(radius:,
|
||||||
|
percentage:))}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def draw_line_to(point:)
|
||||||
|
"L #{coordinates_for(point)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def benchmark_line_point(radius, angle)
|
||||||
|
x = (radius * Math.cos(angle)).to_s
|
||||||
|
y = (radius * Math.sin(angle) + arc_center.y).to_s
|
||||||
|
Point.new(x, y)
|
||||||
|
end
|
||||||
|
|
||||||
|
def coordinates_for(point)
|
||||||
|
"#{point.x} #{point.y}"
|
||||||
|
end
|
||||||
|
end
|
||||||
54
app/helpers/dashboard/variance_helper.rb
Normal file
54
app/helpers/dashboard/variance_helper.rb
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Dashboard
|
||||||
|
module VarianceHelper
|
||||||
|
def heading_gutter
|
||||||
|
30
|
||||||
|
end
|
||||||
|
|
||||||
|
def footer_gutter
|
||||||
|
50
|
||||||
|
end
|
||||||
|
|
||||||
|
def measure_row_height
|
||||||
|
40
|
||||||
|
end
|
||||||
|
|
||||||
|
def graph_height(number_of_rows)
|
||||||
|
number_of_rows * measure_row_height + heading_gutter + footer_gutter
|
||||||
|
end
|
||||||
|
|
||||||
|
def graph_background_height(number_of_rows:)
|
||||||
|
number_of_rows += 1 if @has_empty_dataset
|
||||||
|
graph_height(number_of_rows) - footer_gutter
|
||||||
|
end
|
||||||
|
|
||||||
|
def measure_row_bar_height
|
||||||
|
20
|
||||||
|
end
|
||||||
|
|
||||||
|
def label_width_percentage
|
||||||
|
30
|
||||||
|
end
|
||||||
|
|
||||||
|
def graph_width_percentage
|
||||||
|
100 - label_width_percentage
|
||||||
|
end
|
||||||
|
|
||||||
|
def zones
|
||||||
|
%w[warning watch growth approval ideal]
|
||||||
|
end
|
||||||
|
|
||||||
|
def zone_width_percentage
|
||||||
|
100.0 / zones.size
|
||||||
|
end
|
||||||
|
|
||||||
|
def availability_indicator_percentage
|
||||||
|
3
|
||||||
|
end
|
||||||
|
|
||||||
|
def partial_data_indicator_size
|
||||||
|
20
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -37,6 +37,5 @@ module Dashboard
|
||||||
private_class_method :academic_years
|
private_class_method :academic_years
|
||||||
private_class_method :parse_year_range
|
private_class_method :parse_year_range
|
||||||
end
|
end
|
||||||
|
AcademicYearRange = Struct.new(:start, :end)
|
||||||
end
|
end
|
||||||
|
|
||||||
AcademicYearRange = Struct.new(:start, :end)
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
module Dashboard
|
module Dashboard
|
||||||
class Measure < ApplicationRecord
|
class Measure < ApplicationRecord
|
||||||
belongs_to :subcategory, class_name: "Subcategory", foreign_key: :dashboard_subcategory_id
|
belongs_to :subcategory, class_name: "Subcategory", foreign_key: :dashboard_subcategory_id
|
||||||
has_one :dashboard_category, through: :dashboard_subcategory
|
has_one :dashboard_category, through: :subcategory
|
||||||
has_many :dashboard_scales
|
has_many :scales, class_name: "Scale", foreign_key: :dashboard_measure_id
|
||||||
has_many :dashboard_admin_data_items, through: :scales
|
has_many :admin_data_items, through: :scales
|
||||||
has_many :dashboard_survey_items, through: :scales
|
has_many :survey_items, through: :scales
|
||||||
has_many :dashboard_survey_item_responses, through: :survey_items
|
has_many :survey_item_responses, through: :survey_items
|
||||||
|
|
||||||
def none_meet_threshold?(school:, academic_year:)
|
def none_meet_threshold?(school:, academic_year:)
|
||||||
@none_meet_threshold ||= Hash.new do |memo, (school, academic_year)|
|
@none_meet_threshold ||= Hash.new do |memo, (school, academic_year)|
|
||||||
|
|
@ -24,15 +24,17 @@ module Dashboard
|
||||||
end
|
end
|
||||||
|
|
||||||
def student_survey_items_with_sufficient_responses(school:, academic_year:)
|
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_with_sufficient_responses ||= SurveyItem.where(id: SurveyItem.joins("inner join dashboard_survey_item_responses on dashboard_survey_item_responses.survey_item_id = dashboard_survey_items.id")
|
||||||
.student_survey_items
|
# .student_survey_items
|
||||||
.where("survey_item_responses.school": school,
|
# .where("dashboard_survey_item_responses.school": school,
|
||||||
"survey_item_responses.academic_year": academic_year,
|
# "dashboard_survey_item_responses.academic_year": academic_year,
|
||||||
"survey_item_responses.survey_item_id": survey_items.student_survey_items,
|
# "dashboard_survey_item_responses.survey_item_id": survey_items.student_survey_items,
|
||||||
"survey_item_responses.grade": school.grades(academic_year:))
|
# "dashboard_survey_item_responses.grade": school.grades(academic_year:))
|
||||||
.group("survey_items.id")
|
# .group("survey_items.id")
|
||||||
.having("count(*) >= 10")
|
# .having("count(*) >= 10")
|
||||||
.count.keys)
|
# .count.keys)
|
||||||
|
|
||||||
|
@student_survey_items_with_sufficient_responses ||= student_survey_items
|
||||||
end
|
end
|
||||||
|
|
||||||
def teacher_scales
|
def teacher_scales
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,18 @@
|
||||||
module Dashboard
|
module Dashboard
|
||||||
class ResponseRate < ApplicationRecord
|
class ResponseRate < ApplicationRecord
|
||||||
belongs_to :dashboard_subcategory
|
TEACHER_RATE_THRESHOLD = 24.5
|
||||||
belongs_to :school
|
STUDENT_RATE_THRESHOLD = 24.5
|
||||||
belongs_to :dashboard_academic_year
|
|
||||||
|
belongs_to :subcategory, class_name: "Subcategory", foreign_key: :dashboard_subcategory_id
|
||||||
|
belongs_to :school, class_name: "School", foreign_key: :dashboard_school_id
|
||||||
|
belongs_to :academic_year, class_name: "AcademicYear", foreign_key: :dashboard_academic_year_id
|
||||||
|
|
||||||
|
def meets_student_threshold?
|
||||||
|
student_response_rate >= 24.5
|
||||||
|
end
|
||||||
|
|
||||||
|
def meets_teacher_threshold?
|
||||||
|
teacher_response_rate >= 24.5
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@
|
||||||
|
|
||||||
module Dashboard
|
module Dashboard
|
||||||
class ResponseRateCalculator
|
class ResponseRateCalculator
|
||||||
TEACHER_RATE_THRESHOLD = 25
|
|
||||||
STUDENT_RATE_THRESHOLD = 25
|
|
||||||
attr_reader :subcategory, :school, :academic_year
|
attr_reader :subcategory, :school, :academic_year
|
||||||
|
|
||||||
def initialize(subcategory:, school:, academic_year:)
|
def initialize(subcategory:, school:, academic_year:)
|
||||||
|
|
@ -22,14 +20,6 @@ module Dashboard
|
||||||
cap_at_one_hundred(raw_response_rate).round
|
cap_at_one_hundred(raw_response_rate).round
|
||||||
end
|
end
|
||||||
|
|
||||||
def meets_student_threshold?
|
|
||||||
rate >= STUDENT_RATE_THRESHOLD
|
|
||||||
end
|
|
||||||
|
|
||||||
def meets_teacher_threshold?
|
|
||||||
rate >= TEACHER_RATE_THRESHOLD
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def cap_at_one_hundred(response_rate)
|
def cap_at_one_hundred(response_rate)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
module Dashboard
|
module Dashboard
|
||||||
class Scale < ApplicationRecord
|
class Scale < ApplicationRecord
|
||||||
belongs_to :measure, class_name: "Measure", foreign_key: :dashboard_measure_id
|
belongs_to :measure, class_name: "Measure", foreign_key: :dashboard_measure_id
|
||||||
has_many :survey_items
|
has_many :survey_items, class_name: "SurveyItem", foreign_key: :dashboard_scale_id
|
||||||
has_many :survey_item_responses, through: :survey_items
|
has_many :survey_item_responses, through: :survey_items
|
||||||
has_many :admin_data_items, class_name: "AdminDataItem", foreign_key: :admin_data_item_id
|
has_many :admin_data_items, class_name: "AdminDataItem", foreign_key: :dashboard_scale_id
|
||||||
|
|
||||||
def score(school:, academic_year:)
|
def score(school:, academic_year:)
|
||||||
@score ||= Hash.new do |memo, (school, academic_year)|
|
@score ||= Hash.new do |memo, (school, academic_year)|
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,7 @@ module Dashboard
|
||||||
student = StudentResponseRateCalculator.new(subcategory: self, school:, academic_year:)
|
student = StudentResponseRateCalculator.new(subcategory: self, school:, academic_year:)
|
||||||
teacher = TeacherResponseRateCalculator.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,
|
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?,
|
teacher_response_rate: teacher.rate)
|
||||||
meets_teacher_threshold: teacher.meets_teacher_threshold?)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@response_rate[[school, academic_year]]
|
@response_rate[[school, academic_year]]
|
||||||
|
|
|
||||||
|
|
@ -15,26 +15,26 @@ module Dashboard
|
||||||
end
|
end
|
||||||
|
|
||||||
scope :student_survey_items, lambda {
|
scope :student_survey_items, lambda {
|
||||||
where("survey_items.survey_item_id LIKE 's-%'")
|
where("dashboard_survey_items.survey_item_id LIKE 's-%'")
|
||||||
}
|
}
|
||||||
scope :standard_survey_items, lambda {
|
scope :standard_survey_items, lambda {
|
||||||
where("survey_items.survey_item_id LIKE 's-%-q%'")
|
where("dashboard_survey_items.survey_item_id LIKE 's-%-q%'")
|
||||||
}
|
}
|
||||||
scope :teacher_survey_items, lambda {
|
scope :teacher_survey_items, lambda {
|
||||||
where("survey_items.survey_item_id LIKE 't-%'")
|
where("dashboard_survey_items.survey_item_id LIKE 't-%'")
|
||||||
}
|
}
|
||||||
scope :short_form_survey_items, lambda {
|
scope :short_form_survey_items, lambda {
|
||||||
where(on_short_form: true)
|
where(on_short_form: true)
|
||||||
}
|
}
|
||||||
scope :early_education_survey_items, lambda {
|
scope :early_education_survey_items, lambda {
|
||||||
where("survey_items.survey_item_id LIKE '%-%-es%'")
|
where("dashboard_survey_items.survey_item_id LIKE '%-%-es%'")
|
||||||
}
|
}
|
||||||
|
|
||||||
scope :survey_items_for_grade, lambda { |school, academic_year, grade|
|
scope :survey_items_for_grade, lambda { |school, academic_year, grade|
|
||||||
includes(:survey_item_responses)
|
includes(:survey_item_responses)
|
||||||
.where("survey_item_responses.grade": grade,
|
.where("dashboard_survey_item_responses.grade": grade,
|
||||||
"survey_item_responses.school": school,
|
"dashboard_survey_item_responses.school": school,
|
||||||
"survey_item_responses.academic_year": academic_year).distinct
|
"dashboard_survey_item_responses.academic_year": academic_year).distinct
|
||||||
}
|
}
|
||||||
|
|
||||||
scope :survey_item_ids_for_grade, lambda { |school, academic_year, grade|
|
scope :survey_item_ids_for_grade, lambda { |school, academic_year, grade|
|
||||||
|
|
@ -45,9 +45,9 @@ module Dashboard
|
||||||
includes(:survey_item_responses)
|
includes(:survey_item_responses)
|
||||||
.where(
|
.where(
|
||||||
survey_item_id: subcategory.survey_items.pluck(:survey_item_id),
|
survey_item_id: subcategory.survey_items.pluck(:survey_item_id),
|
||||||
"survey_item_responses.school": school,
|
"dashboard_survey_item_responses.school": school,
|
||||||
"survey_item_responses.academic_year": academic_year,
|
"dashboard_survey_item_responses.academic_year": academic_year,
|
||||||
"survey_item_responses.grade": grade
|
"dashboard_survey_item_responses.grade": grade
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,16 @@ module Dashboard
|
||||||
TEACHER_RESPONSE_THRESHOLD = 2
|
TEACHER_RESPONSE_THRESHOLD = 2
|
||||||
STUDENT_RESPONSE_THRESHOLD = 10
|
STUDENT_RESPONSE_THRESHOLD = 10
|
||||||
|
|
||||||
belongs_to :school
|
belongs_to :school, class_name: "School", foreign_key: :dashboard_school_id
|
||||||
belongs_to :dashboard_survey_item
|
belongs_to :survey_item, class_name: "SurveyItem", foreign_key: :dashboard_survey_item_id
|
||||||
belongs_to :dashboard_academic_year
|
belongs_to :academic_year, class_name: "AcademicYear", foreign_key: :dashboard_academic_year
|
||||||
belongs_to :dashboard_student, optional: true
|
belongs_to :dashboard_student, optional: true
|
||||||
belongs_to :dashboard_gender, optional: true
|
belongs_to :dashboard_gender, optional: true
|
||||||
belongs_to :dashboard_income, optional: true
|
belongs_to :dashboard_income, optional: true
|
||||||
belongs_to :dashboard_ell, optional: true
|
belongs_to :dashboard_ell, optional: true
|
||||||
belongs_to :dashboard_sped, optional: true
|
belongs_to :dashboard_sped, optional: true
|
||||||
|
|
||||||
has_one :dashboard_measure, through: :dashboard_survey_item
|
has_one :dashboard_measure, through: :survey_item
|
||||||
|
|
||||||
validates :likert_score, numericality: { greater_than: 0, less_than_or_equal_to: 5 }
|
validates :likert_score, numericality: { greater_than: 0, less_than_or_equal_to: 5 }
|
||||||
|
|
||||||
|
|
@ -61,10 +61,10 @@ module Dashboard
|
||||||
|
|
||||||
def self.teacher_survey_items_with_sufficient_responses(school:, academic_year:)
|
def self.teacher_survey_items_with_sufficient_responses(school:, academic_year:)
|
||||||
@teacher_survey_items_with_sufficient_responses ||= Hash.new do |memo, (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")
|
hash = SurveyItem.joins("inner join dashboard_survey_item_responses on dashboard_survey_item_responses.dashboard_survey_item_id = dashboard_survey_items.id")
|
||||||
.teacher_survey_items
|
.teacher_survey_items
|
||||||
.where("survey_item_responses.school": school, "survey_item_responses.academic_year": academic_year)
|
.where("dashboard_survey_item_responses.dashboard_school_id": school.id, "dashboard_survey_item_responses.dashboard_academic_year_id": academic_year.id)
|
||||||
.group("survey_items.id")
|
.group("dashboard_survey_items.id")
|
||||||
.having("count(*) > 0").count
|
.having("count(*) > 0").count
|
||||||
memo[[school, academic_year]] = hash
|
memo[[school, academic_year]] = hash
|
||||||
end
|
end
|
||||||
|
|
@ -73,9 +73,9 @@ module Dashboard
|
||||||
|
|
||||||
def self.student_survey_items_with_sufficient_responses_by_grade(school:, academic_year:)
|
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)|
|
@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")
|
hash = SurveyItem.joins("inner join dashboard_survey_item_responses on dashboard_survey_item_responses.dashboard_survey_item_id = dashboard_survey_items.id")
|
||||||
.student_survey_items
|
.student_survey_items
|
||||||
.where("survey_item_responses.school": school, "survey_item_responses.academic_year": academic_year)
|
.where("dashboard_survey_item_responses.dashboard_school_id": school.id, "dashboard_survey_item_responses.dashboard_academic_year_id": academic_year.id)
|
||||||
.group(:grade, :id)
|
.group(:grade, :id)
|
||||||
.count
|
.count
|
||||||
memo[[school, academic_year]] = hash
|
memo[[school, academic_year]] = hash
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ module Analyze
|
||||||
def sufficient_student_responses?(academic_year:)
|
def sufficient_student_responses?(academic_year:)
|
||||||
return false unless measure.subcategory.response_rate(school:, academic_year:).meets_student_threshold?
|
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(
|
number_of_students_for_a_racial_group = SurveyItemResponse.joins("JOIN student_races on dashboard_survey_item_responses.student_id = student_races.student_id JOIN students on students.id = student_races.student_id").where(
|
||||||
school:, academic_year:
|
school:, academic_year:
|
||||||
).where("student_races.race_id": race.id).distinct.pluck(:student_id).count
|
).where("student_races.race_id": race.id).distinct.pluck(:student_id).count
|
||||||
number_of_students_for_a_racial_group >= 10
|
number_of_students_for_a_racial_group >= 10
|
||||||
|
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
module Dashboard
|
|
||||||
class DisaggregationLoader
|
|
||||||
attr_reader :path
|
|
||||||
|
|
||||||
def initialize(path:)
|
|
||||||
@path = path
|
|
||||||
initialize_directory
|
|
||||||
end
|
|
||||||
|
|
||||||
def load
|
|
||||||
data = {}
|
|
||||||
Dir.glob(Rails.root.join(path, "*.csv")).each do |filepath|
|
|
||||||
puts filepath
|
|
||||||
File.open(filepath) do |file|
|
|
||||||
headers = CSV.parse(file.first).first
|
|
||||||
|
|
||||||
file.lazy.each_slice(1000) do |lines|
|
|
||||||
CSV.parse(lines.join, headers:).map do |row|
|
|
||||||
values = DisaggregationRow.new(row:, headers:)
|
|
||||||
data[[values.lasid, values.district, values.academic_year]] = values
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
data
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize_directory
|
|
||||||
FileUtils.mkdir_p(path)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
module Dashboard
|
|
||||||
class DisaggregationRow
|
|
||||||
attr_reader :row, :headers
|
|
||||||
|
|
||||||
def initialize(row:, headers:)
|
|
||||||
@row = row
|
|
||||||
@headers = headers
|
|
||||||
end
|
|
||||||
|
|
||||||
def district
|
|
||||||
@district ||= value_from(pattern: /District/i)
|
|
||||||
end
|
|
||||||
|
|
||||||
def academic_year
|
|
||||||
@academic_year ||= value_from(pattern: /Academic\s*Year/i)
|
|
||||||
end
|
|
||||||
|
|
||||||
def raw_income
|
|
||||||
@income ||= value_from(pattern: /Low\s*Income/i)
|
|
||||||
end
|
|
||||||
|
|
||||||
def lasid
|
|
||||||
@lasid ||= value_from(pattern: /LASID/i)
|
|
||||||
end
|
|
||||||
|
|
||||||
def raw_ell
|
|
||||||
@raw_ell ||= value_from(pattern: /EL Student First Year/i)
|
|
||||||
end
|
|
||||||
|
|
||||||
def ell
|
|
||||||
@ell ||= begin
|
|
||||||
value = value_from(pattern: /EL Student First Year/i).downcase
|
|
||||||
|
|
||||||
case value
|
|
||||||
when /lep student 1st year|LEP student not 1st year/i
|
|
||||||
"ELL"
|
|
||||||
when /Does not apply/i
|
|
||||||
"Not ELL"
|
|
||||||
else
|
|
||||||
"Unknown"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def value_from(pattern:)
|
|
||||||
output = nil
|
|
||||||
matches = headers.select do |header|
|
|
||||||
pattern.match(header)
|
|
||||||
end.map { |item| item.delete("\n") }
|
|
||||||
matches.each do |match|
|
|
||||||
output ||= row[match]
|
|
||||||
end
|
|
||||||
output
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Dashboard
|
|
||||||
class ResponseRateLoader
|
|
||||||
def self.reset(schools: School.all, academic_years: AcademicYear.all, subcategories: Subcategory.all)
|
|
||||||
subcategories.each do |subcategory|
|
|
||||||
schools.each do |school|
|
|
||||||
next if test_env? && (school != milford)
|
|
||||||
|
|
||||||
academic_years.each do |academic_year|
|
|
||||||
next if test_env? && (academic_year != test_year)
|
|
||||||
|
|
||||||
process_response_rate(subcategory:, school:, academic_year:)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def self.milford
|
|
||||||
School.find_by_slug "milford-high-school"
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.test_year
|
|
||||||
AcademicYear.find_by_range "2020-21"
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.rails_env
|
|
||||||
@rails_env ||= ENV["RAILS_ENV"]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.process_response_rate(subcategory:, school:, academic_year:)
|
|
||||||
student = StudentResponseRateCalculator.new(subcategory:, school:, academic_year:)
|
|
||||||
teacher = TeacherResponseRateCalculator.new(subcategory:, school:, academic_year:)
|
|
||||||
|
|
||||||
response_rate = ResponseRate.find_or_create_by!(subcategory:, school:, academic_year:)
|
|
||||||
|
|
||||||
response_rate.update!(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
|
|
||||||
|
|
||||||
def self.test_env?
|
|
||||||
rails_env == "test"
|
|
||||||
end
|
|
||||||
|
|
||||||
private_class_method :milford
|
|
||||||
private_class_method :test_year
|
|
||||||
private_class_method :rails_env
|
|
||||||
private_class_method :process_response_rate
|
|
||||||
private_class_method :test_env?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<div class="mt-5 school-quality-frameworks">
|
||||||
|
<% category_presenters.each do |category_presenter| %>
|
||||||
|
<div class="text-center">
|
||||||
|
<i class="<%= category_presenter.icon_class %> <%= category_presenter.icon_color_class %> fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<h3 class="sub-header-3">
|
||||||
|
|
||||||
|
<%= link_to [@district, @school, category_presenter, { year: @academic_year.range }] do %>
|
||||||
|
<%= category_presenter.name %>
|
||||||
|
<% end %>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="body-small text-center m-0"><%= category_presenter.short_description %></p>
|
||||||
|
|
||||||
|
<div class="subcategory-card">
|
||||||
|
<div class="subcategory-card__benchmark-list">
|
||||||
|
<%= render partial: 'subcategory_card', collection: category_presenter.subcategories(academic_year: @academic_year, school: @school).map(&:subcategory_card_presenter) %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
14
app/views/dashboard/overview/_response_rate.html.erb
Normal file
14
app/views/dashboard/overview/_response_rate.html.erb
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<div
|
||||||
|
class="overall-response-rate-container"
|
||||||
|
data-bs-toggle="popover"
|
||||||
|
data-bs-trigger="hover focus"
|
||||||
|
data-bs-content="<%= response_rate_presenter.hover_message %>"
|
||||||
|
data-bs-placement="top"
|
||||||
|
>
|
||||||
|
<div><%= response_rate_presenter.date_message %> </div>
|
||||||
|
<div style="display: flex; justify-content:space-between; width: 100px;">
|
||||||
|
<div><%= response_rate_presenter.focus.capitalize %> </div>
|
||||||
|
<%= render partial: "response_rate_graphic", locals: {response_rate_presenter: response_rate_presenter}, cached: true %>
|
||||||
|
<div><%= response_rate_presenter.percentage %>% </div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
35
app/views/dashboard/overview/_response_rate_graphic.html.erb
Normal file
35
app/views/dashboard/overview/_response_rate_graphic.html.erb
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<style>
|
||||||
|
/*
|
||||||
|
For some reason, none of the sizing in the pie class works, and it always
|
||||||
|
fills 100% of the containing frame, so the size has to be dictated by .prog
|
||||||
|
*/
|
||||||
|
.prog {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-top: 0.2em;
|
||||||
|
}
|
||||||
|
.pie {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
display: inline-grid;
|
||||||
|
place-content: center;
|
||||||
|
margin: 5px;
|
||||||
|
font-size: 25px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#response-rate-pie-<%= response_rate_presenter.focus %>:before{
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
inset: 0;
|
||||||
|
background: conic-gradient(var(--color-<%= response_rate_presenter.color %>) calc(<%= response_rate_presenter.percentage %>*1%),#0000 0);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="prog">
|
||||||
|
<div id="response-rate-pie-<%= response_rate_presenter.focus %>" class="pie"></div>
|
||||||
|
</div>
|
||||||
7
app/views/dashboard/overview/_subcategory_card.html.erb
Normal file
7
app/views/dashboard/overview/_subcategory_card.html.erb
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<div class="subcategory-card__benchmark-item">
|
||||||
|
<svg class="subcategory-card__circle" width="24" height="24" xmlns="http://www.w3.org/2000/svg" <%= "data-bs-toggle=popover" if subcategory_card.insufficient_data? %> data-bs-placement="top" data-bs-content="This subcategory is not displayed due to limited availability of school admin data and/or low survey response rates.">
|
||||||
|
<use class="harvey-ball harvey-ball--<%= subcategory_card.color %>" xlink:href="#<%= subcategory_card.harvey_ball_icon %>"></use>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<%= link_to(subcategory_card.name, district_school_category_path( @district, @school, subcategory_card.category, {year: @academic_year.range, anchor: "#{subcategory_card.subcategory_id}"})) %>
|
||||||
|
</div>
|
||||||
165
app/views/dashboard/overview/_variance_chart.html.erb
Normal file
165
app/views/dashboard/overview/_variance_chart.html.erb
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
<% displayed_presenters = presenters.filter { |p| p.sufficient_data? }.sort %>
|
||||||
|
<% not_displayed_presenters = presenters - displayed_presenters %>
|
||||||
|
|
||||||
|
<% if displayed_presenters.none? %>
|
||||||
|
<p class="caption mb-5">Note: No measures can be displayed due to limited availability of school admin data and/or low survey response rates.</p>
|
||||||
|
<% elsif not_displayed_presenters.present? %>
|
||||||
|
<p class="caption mb-5">Note: The following measures are not displayed due to limited availability of school admin data and/or low survey response rates: <%= not_displayed_presenters.map(&:measure_name).join('; ') %>.</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<svg width="100%" height=<%= graph_height(displayed_presenters.size) %> xmlns="http://www.w3.org/2000/svg">
|
||||||
|
|
||||||
|
<filter id="inset-shadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feComponentTransfer in=SourceAlpha>
|
||||||
|
<feFuncA type="table" tableValues="1 0"/>
|
||||||
|
</feComponentTransfer>
|
||||||
|
<feGaussianBlur stdDeviation="4"/>
|
||||||
|
<feOffset dx="0" dy="0" result="offsetblur"/>
|
||||||
|
<feFlood flood-color="rgb(62, 58, 56, 0.25)" result="color"/>
|
||||||
|
<feComposite in2="offsetblur" operator="in"/>
|
||||||
|
<feComposite in2="SourceAlpha" operator="in"/>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="SourceGraphic"/>
|
||||||
|
<feMergeNode/>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
id="graph-background"
|
||||||
|
x="<%= label_width_percentage %>%"
|
||||||
|
y="0"
|
||||||
|
width="<%= graph_width_percentage %>%"
|
||||||
|
height="<%= graph_background_height(number_of_rows: displayed_presenters.size) %>"
|
||||||
|
filter="url(#inset-shadow)"
|
||||||
|
>
|
||||||
|
<g id="zone-headings">
|
||||||
|
<% zones.each_with_index do |zone, index| %>
|
||||||
|
<text
|
||||||
|
class="zone-header"
|
||||||
|
x="<%= index * zone_width_percentage + zone_width_percentage / 2.0 %>%"
|
||||||
|
y="<%= heading_gutter / 2 %>"
|
||||||
|
text-anchor="middle"
|
||||||
|
dominant-baseline="middle"
|
||||||
|
>
|
||||||
|
<%= zone.capitalize %>
|
||||||
|
</text>
|
||||||
|
<% end %>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="zone-background" transform="translate(0, <%= heading_gutter %>)">
|
||||||
|
<% zones.each_with_index do |zone, index| %>
|
||||||
|
<rect
|
||||||
|
id="<%= zone %>-zone"
|
||||||
|
class="zone-background bg-fill-<%= zone %>"
|
||||||
|
x="<%= index * zone_width_percentage %>%"
|
||||||
|
y="0"
|
||||||
|
width="<%= zone_width_percentage %>%"
|
||||||
|
height="100%"
|
||||||
|
stroke="#CECECE"
|
||||||
|
stroke-width="1"
|
||||||
|
/>
|
||||||
|
<% end %>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<g id="measure-rows">
|
||||||
|
<svg id=measure-row-limited-availability-indicator x="0" y="<%= heading_gutter %>">
|
||||||
|
<% displayed_presenters.each_with_index do |presenter, index| %>
|
||||||
|
<% if presenter.show_partial_data_indicator? %>
|
||||||
|
<foreignObject
|
||||||
|
width="<%= partial_data_indicator_size %>"
|
||||||
|
height="<%= partial_data_indicator_size %>"
|
||||||
|
x="<%= partial_data_indicator_size / 2 %>"
|
||||||
|
y="<%= index * measure_row_height + measure_row_height / 2 - partial_data_indicator_size / 2 %>"
|
||||||
|
dominant-baseline="middle" >
|
||||||
|
<i class="fas fa-exclamation-circle"
|
||||||
|
data-bs-toggle="popover" data-bs-placement="right"
|
||||||
|
data-bs-content="The following sources are not included in this measure due to insufficient data: <%= presenter.partial_data_sources.join(' and ') %>." ></i>
|
||||||
|
</foreignObject>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg id="measure-row-labels" x="0" y=<%= heading_gutter %>>
|
||||||
|
<% displayed_presenters.each_with_index do |presenter, index| %>
|
||||||
|
<foreignObject
|
||||||
|
x="<%= availability_indicator_percentage %>%"
|
||||||
|
y="<%= index * measure_row_height + measure_row_height / 4 %>"
|
||||||
|
dominant-baseline="middle"
|
||||||
|
data-variance-row-label
|
||||||
|
width="550"
|
||||||
|
height="200">
|
||||||
|
|
||||||
|
<%= link_to(presenter.measure_name, district_school_category_path( @district, @school, presenter.category, {year: @academic_year.range, anchor: "#{presenter.measure_id}"}), class: "measure-row-label") %>
|
||||||
|
</foreignObject>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if displayed_presenters.none? %>
|
||||||
|
<text
|
||||||
|
class="font-cabin"
|
||||||
|
x="0"
|
||||||
|
y="<%= 0 * measure_row_height + measure_row_height / 2 %>"
|
||||||
|
dominant-baseline="middle"
|
||||||
|
data-variance-row-label
|
||||||
|
>
|
||||||
|
Insufficient data
|
||||||
|
</text>
|
||||||
|
<% end %>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
id="measure-row-bars"
|
||||||
|
x="<%= label_width_percentage %>%"
|
||||||
|
y="<%= heading_gutter %>"
|
||||||
|
width="<%= graph_width_percentage %>%"
|
||||||
|
>
|
||||||
|
<% displayed_presenters.each_with_index do |presenter, index| %>
|
||||||
|
<rect
|
||||||
|
class="measure-row-bar <%= presenter.bar_color %>"
|
||||||
|
x="<%= presenter.x_offset %>"
|
||||||
|
y="<%= index * measure_row_height + (measure_row_height - measure_row_bar_height) / 2 %>"
|
||||||
|
width="<%= presenter.bar_width %>"
|
||||||
|
height="<%= measure_row_bar_height %>"
|
||||||
|
data-for-measure-id="<%= presenter.measure_id %>"
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
<% end %>
|
||||||
|
</svg>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
id="key-benchmark"
|
||||||
|
x="<%= label_width_percentage %>%"
|
||||||
|
y="0"
|
||||||
|
width="<%= graph_width_percentage %>%"
|
||||||
|
height="<%= graph_background_height(number_of_rows: displayed_presenters.size) %>"
|
||||||
|
>
|
||||||
|
<g transform="translate(0, <%= heading_gutter %>)">
|
||||||
|
<rect
|
||||||
|
id="key-benchmark"
|
||||||
|
x="60%"
|
||||||
|
transform="translate(-1, 0)"
|
||||||
|
y="0"
|
||||||
|
width="4"
|
||||||
|
height="100%"
|
||||||
|
fill="black"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
id="legend"
|
||||||
|
x="<%= label_width_percentage %>%"
|
||||||
|
y="0"
|
||||||
|
width="<%= graph_width_percentage %>%"
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
class="graph-footer"
|
||||||
|
x="60%"
|
||||||
|
y="<%= graph_background_height(number_of_rows: displayed_presenters.size) + (footer_gutter / 2) %>"
|
||||||
|
text-anchor="middle"
|
||||||
|
>
|
||||||
|
Benchmark
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
100
app/views/dashboard/overview/index.html.erb
Normal file
100
app/views/dashboard/overview/index.html.erb
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
<% content_for :navigation do %>
|
||||||
|
<h2 class="sub-header-2 color-white m-0">Areas Of Interest</h2>
|
||||||
|
<select id="select-academic-year" class="form-select" name="academic-year">
|
||||||
|
<% @academic_years.each do |year| %>
|
||||||
|
<option value="<%= district_school_overview_index_path(@district, @school, {year: year.range}) %>" <%= @academic_year == year ? "selected" : nil %>><%= year.formatted_range %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
<% end %>
|
||||||
|
<% cache do %>
|
||||||
|
<svg class="d-none">
|
||||||
|
<symbol viewBox="0 0 24 24" id="warning-harvey-ball">
|
||||||
|
<circle cx="12" cy="12" r="11.5" fill="white" stroke="none" />
|
||||||
|
<path d="
|
||||||
|
M 12 0
|
||||||
|
A 12 12 0 0 1 24 12
|
||||||
|
L 12 12
|
||||||
|
L 12 0"
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
<circle cx="12" cy="12" r="11.5" fill="none" />
|
||||||
|
</symbol>
|
||||||
|
<symbol viewBox="0 0 24 24" id="watch-harvey-ball">
|
||||||
|
<circle cx="12" cy="12" r="11.5" fill="white" stroke="none" />
|
||||||
|
<path d="
|
||||||
|
M 12 0
|
||||||
|
A 12 12 0 1 1 12 24
|
||||||
|
L 12 12
|
||||||
|
L 12 0"
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
<circle cx="12" cy="12" r="11.5" fill="none" />
|
||||||
|
</symbol>
|
||||||
|
<symbol viewBox="0 0 24 24" id="growth-harvey-ball">
|
||||||
|
<circle cx="12" cy="12" r="11.5" fill="white" stroke="none" />
|
||||||
|
<path d="
|
||||||
|
M 12 0
|
||||||
|
A 12 12 0 1 1 0 12
|
||||||
|
L 12 12
|
||||||
|
L 12 0"
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
<circle cx="12" cy="12" r="11.5" fill="none" />
|
||||||
|
</symbol>
|
||||||
|
<symbol viewBox="0 0 24 24" id="approval-harvey-ball">
|
||||||
|
<circle cx="12" cy="12" r="11.5" />
|
||||||
|
<path d="M19 8C19 8.28125 18.875 8.53125 18.6875 8.71875L10.6875 16.7188C10.5 16.9062 10.25 17 10 17C9.71875 17 9.46875 16.9062 9.28125 16.7188L5.28125 12.7188C5.09375 12.5312 5 12.2812 5 12C5 11.4375 5.4375 11 6 11C6.25 11 6.5 11.125 6.6875 11.3125L10 14.5938L17.2812 7.3125C17.4688 7.125 17.7188 7 18 7C18.5312 7 19 7.4375 19 8Z"
|
||||||
|
stroke-width=".5" stroke="white" fill="white" />
|
||||||
|
</symbol>
|
||||||
|
<symbol viewBox="0 0 24 24" id="ideal-harvey-ball">
|
||||||
|
<circle cx="12" cy="12" r="11.5" />
|
||||||
|
<path d="M9.28125 11.7188C9.46875 11.9062 9.71875 12 10 12C10.25 12 10.5 11.9062 10.6875 11.7188L15.6875 6.71875C15.875 6.53125 16 6.28125 16 6C16 5.4375 15.5312 5 15 5C14.7188 5 14.4688 5.125 14.2812 5.3125L10 9.59375L8.1875 7.8125C8 7.625 7.75 7.5 7.5 7.5C6.9375 7.5 6.5 7.9375 6.5 8.5C6.5 8.78125 6.59375 9.03125 6.78125 9.21875L9.28125 11.7188ZM19 10C19 9.4375 18.5312 9 18 9C17.7188 9 17.4688 9.125 17.2812 9.3125L10 16.5938L6.6875 13.3125C6.5 13.125 6.25 13 6 13C5.4375 13 5 13.4375 5 14C5 14.2812 5.09375 14.5312 5.28125 14.7188L9.28125 18.7188C9.46875 18.9062 9.71875 19 10 19C10.25 19 10.5 18.9062 10.6875 18.7188L18.6875 10.7188C18.875 10.5312 19 10.2812 19 10Z"
|
||||||
|
stroke-width=".5" stroke="white" fill="white" />
|
||||||
|
</symbol>
|
||||||
|
<symbol viewBox="0 0 24 24" id="insufficient_data-harvey-ball">
|
||||||
|
<circle cx="12" cy="12" r="11.5" />
|
||||||
|
</symbol>
|
||||||
|
</svg>
|
||||||
|
<% end %>
|
||||||
|
<% cache [@school, @academic_year] do %>
|
||||||
|
<div class="card">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h2 class="sub-header-2">School Quality Framework Indicators</h2>
|
||||||
|
<div class="harvey-ball-legend">
|
||||||
|
<div class="font-size-14">Warning</div>
|
||||||
|
<svg class="ms-3 me-1" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<use class="harvey-ball harvey-ball--warning" xlink:href="#warning-harvey-ball"></use>
|
||||||
|
</svg>
|
||||||
|
<svg class="mx-1" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<use class="harvey-ball harvey-ball--watch" xlink:href="#watch-harvey-ball"></use>
|
||||||
|
</svg>
|
||||||
|
<svg class="mx-1" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<use class="harvey-ball harvey-ball--growth" xlink:href="#growth-harvey-ball"></use>
|
||||||
|
</svg>
|
||||||
|
<svg class="mx-1" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<use class="harvey-ball harvey-ball--approval" xlink:href="#approval-harvey-ball"></use>
|
||||||
|
</svg>
|
||||||
|
<svg class="ms-1 me-3" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<use class="harvey-ball harvey-ball--ideal" xlink:href="#ideal-harvey-ball"></use>
|
||||||
|
</svg>
|
||||||
|
<div class="font-size-14">Ideal</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<%= render partial: "quality_framework_indicators", locals: { category_presenters: @category_presenters } %>
|
||||||
|
|
||||||
|
<div class="overall-response-rate-row">
|
||||||
|
<%= render partial: "response_rate", locals: {response_rate_presenter: @student_response_rate_presenter} %>
|
||||||
|
<%= render partial: "response_rate", locals: {response_rate_presenter: @teacher_response_rate_presenter} %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="sub-header-2 mb-4">Distance From Benchmark</h2>
|
||||||
|
<%= render partial: "variance_chart", locals: { presenters: @variance_chart_row_presenters } %>
|
||||||
|
</div>
|
||||||
|
<% if @district == District.find_by_name("Boston") %>
|
||||||
|
<%= render partial: 'layouts/boston_modal' %>
|
||||||
|
<% elsif @has_empty_dataset %>
|
||||||
|
<%= render partial: 'layouts/empty_dataset_modal' %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
13
config/initializers/dashboard/array_monkey_patches.rb
Normal file
13
config/initializers/dashboard/array_monkey_patches.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ArrayMonkeyPatches
|
||||||
|
def average
|
||||||
|
sum.to_f / size
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_blanks
|
||||||
|
reject { |item| item.nil? || item.to_f.nan? || item.zero? }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Array.include ArrayMonkeyPatches
|
||||||
21
config/initializers/dashboard/float_monkey_patches.rb
Normal file
21
config/initializers/dashboard/float_monkey_patches.rb
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module FloatMonkeyPatches
|
||||||
|
def round_up_to_one
|
||||||
|
if positive? && self < 1
|
||||||
|
1.0
|
||||||
|
else
|
||||||
|
self
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def round_down_to_five
|
||||||
|
if positive? && self > 5
|
||||||
|
5.0
|
||||||
|
else
|
||||||
|
self
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Float.include FloatMonkeyPatches
|
||||||
|
|
@ -1,3 +1,10 @@
|
||||||
Dashboard::Engine.routes.draw do
|
Dashboard::Engine.routes.draw do
|
||||||
|
resources :districts do
|
||||||
|
resources :schools, only: %i[index show] do
|
||||||
|
resources :overview, only: [:index]
|
||||||
|
# resources :categories, only: [:show], path: "browse"
|
||||||
|
# resources :analyze, only: [:index]
|
||||||
|
end
|
||||||
|
end
|
||||||
get "/welcome", to: "home#index"
|
get "/welcome", to: "home#index"
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ class CreateDashboardResponseRates < ActiveRecord::Migration[7.1]
|
||||||
t.references :dashboard_subcategory, null: false, foreign_key: true
|
t.references :dashboard_subcategory, null: false, foreign_key: true
|
||||||
t.references :dashboard_school, 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.references :dashboard_academic_year, null: false, foreign_key: true
|
||||||
t.float :school_response_rate
|
t.float :student_response_rate
|
||||||
t.float :teacher_response_rate
|
t.float :teacher_response_rate
|
||||||
|
|
||||||
t.timestamps
|
t.timestamps
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_04_192128) do
|
||||||
t.bigint "dashboard_subcategory_id", null: false
|
t.bigint "dashboard_subcategory_id", null: false
|
||||||
t.bigint "dashboard_school_id", null: false
|
t.bigint "dashboard_school_id", null: false
|
||||||
t.bigint "dashboard_academic_year_id", null: false
|
t.bigint "dashboard_academic_year_id", null: false
|
||||||
t.float "school_response_rate"
|
t.float "student_response_rate"
|
||||||
t.float "teacher_response_rate"
|
t.float "teacher_response_rate"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue