diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..5f277000 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_size = 2 +indent_style = space +trim_trailing_whitespace = true diff --git a/Gemfile b/Gemfile index c1d9ca29..15ef0232 100644 --- a/Gemfile +++ b/Gemfile @@ -54,6 +54,7 @@ gem 'activerecord-import' group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platform: :mri + gem 'factory_bot_rails' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 2aeb3474..a5f0bf87 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -78,6 +78,11 @@ GEM diff-lcs (1.4.4) erubi (1.10.0) execjs (2.8.1) + factory_bot (6.2.0) + activesupport (>= 5.0.0) + factory_bot_rails (6.2.0) + factory_bot (~> 6.2.0) + railties (>= 5.0.0) ffi (1.15.4) friendly_id (5.1.0) activerecord (>= 4.0.0) @@ -258,6 +263,7 @@ DEPENDENCIES capybara database_cleaner devise + factory_bot_rails friendly_id (~> 5.1.0) haml jbuilder (~> 2.5) diff --git a/app/assets/stylesheets/colors.scss b/app/assets/stylesheets/colors.scss index f9a6a72a..cfabfc5e 100644 --- a/app/assets/stylesheets/colors.scss +++ b/app/assets/stylesheets/colors.scss @@ -17,10 +17,14 @@ $growth: #E0BA9A; $approval: #D0DD86; $ideal: #C0FF73; -.red { +.color-red { color: $red; } +.color-black { + color: $black; +} + .bg-beige { background-color: $beige; } @@ -29,6 +33,10 @@ $ideal: #C0FF73; background-color: $white; } +.fill-black { + fill: $black; +} + .fill-warning { fill: $warning; } @@ -48,3 +56,15 @@ $ideal: #C0FF73; .fill-ideal { fill: $ideal; } + +.stroke-black { + stroke: $black; +} + +.stroke-gray-1 { + stroke: $gray-1; +} + +.stroke-gray-2 { + stroke: $gray-2; +} diff --git a/app/controllers/browse_controller.rb b/app/controllers/browse_controller.rb new file mode 100644 index 00000000..0dc69cdd --- /dev/null +++ b/app/controllers/browse_controller.rb @@ -0,0 +1,23 @@ +class BrowseController < ApplicationController + def show + @category = CategoryPresenter.new( + category: SqmCategory.find_by_name('Teachers & Leadership'), + academic_year: academic_year, + school: school, + ) + end + + private + + def school + @school ||= School.find_by_slug school_slug + end + + def school_slug + params[:school_id] + end + + def academic_year + @academic_year ||= AcademicYear.find_by_range params[:year] + end +end diff --git a/app/models/measure.rb b/app/models/measure.rb index a7fc4c6b..50c724f5 100644 --- a/app/models/measure.rb +++ b/app/models/measure.rb @@ -1,44 +1,6 @@ class Measure < ActiveRecord::Base belongs_to :subcategory + has_many :survey_items - Zone = Struct.new(:low_benchmark, :high_benchmark, :type) do - def includes_score?(score) - score > low_benchmark and score < high_benchmark - end - 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 zone_for_score(score) - case score - when ideal_zone.low_benchmark..ideal_zone.high_benchmark - ideal_zone - when approval_zone.low_benchmark..approval_zone.high_benchmark - approval_zone - when growth_zone.low_benchmark..growth_zone.high_benchmark - growth_zone - when watch_zone.low_benchmark..watch_zone.high_benchmark - watch_zone - else - warning_zone - end - end + has_many :survey_item_responses, through: :survey_items end diff --git a/app/models/sqm_category.rb b/app/models/sqm_category.rb index 3b2841c4..f07b455a 100644 --- a/app/models/sqm_category.rb +++ b/app/models/sqm_category.rb @@ -1,3 +1,3 @@ class SqmCategory < ActiveRecord::Base - + has_many :subcategories end diff --git a/app/models/subcategory.rb b/app/models/subcategory.rb index 1e1afcc9..5bc2a454 100644 --- a/app/models/subcategory.rb +++ b/app/models/subcategory.rb @@ -1,3 +1,5 @@ class Subcategory < ActiveRecord::Base belongs_to :sqm_category + + has_many :measures end diff --git a/app/models/survey_item.rb b/app/models/survey_item.rb index 1a5f5c52..bada2753 100644 --- a/app/models/survey_item.rb +++ b/app/models/survey_item.rb @@ -1,3 +1,4 @@ class SurveyItem < ActiveRecord::Base belongs_to :measure + has_many :survey_item_responses end diff --git a/app/models/survey_item_response.rb b/app/models/survey_item_response.rb index 9776a2d1..92a4428f 100644 --- a/app/models/survey_item_response.rb +++ b/app/models/survey_item_response.rb @@ -2,4 +2,7 @@ class SurveyItemResponse < ActiveRecord::Base belongs_to :academic_year belongs_to :school belongs_to :survey_item + + scope :for_measures, ->(measures) { joins(:survey_item).where('survey_items.measure_id': measures.map(&:id)) } + scope :for_measure, ->(measure) { joins(:survey_item).where('survey_items.measure_id': measure.id) } end diff --git a/app/presenters/category_presenter.rb b/app/presenters/category_presenter.rb new file mode 100644 index 00000000..6a8ff999 --- /dev/null +++ b/app/presenters/category_presenter.rb @@ -0,0 +1,21 @@ +class CategoryPresenter + def initialize(category:, academic_year:, school:) + @category = category + @academic_year = academic_year + @school = school + end + + def name + @category.name + end + + def subcategories + @category.subcategories.map do |subcategory| + SubcategoryPresenter.new( + subcategory: subcategory, + academic_year: @academic_year, + school: @school, + ) + end + end +end diff --git a/app/presenters/gauge_presenter.rb b/app/presenters/gauge_presenter.rb new file mode 100644 index 00000000..96c78135 --- /dev/null +++ b/app/presenters/gauge_presenter.rb @@ -0,0 +1,34 @@ +class GaugePresenter + def initialize(scale:, score:) + @scale = scale + @score = score + end + + def title + zone.type.to_s.capitalize + end + + def color_class + "fill-#{zone.type}" + end + + def score_percentage + percentage_for @score + end + + def key_benchmark_percentage + percentage_for @scale.approval_zone.low_benchmark + end + + private + + def zone + @scale.zone_for_score(@score) + end + + def percentage_for(number) + scale_minimum = @scale.warning_zone.low_benchmark + scale_maximum = @scale.ideal_zone.high_benchmark + (number - scale_minimum) / (scale_maximum - scale_minimum) + end +end diff --git a/app/presenters/measure_graph_row_presenter.rb b/app/presenters/measure_graph_row_presenter.rb index 7bb57a8d..ba984aee 100644 --- a/app/presenters/measure_graph_row_presenter.rb +++ b/app/presenters/measure_graph_row_presenter.rb @@ -26,7 +26,7 @@ class MeasureGraphRowPresenter when :ideal, :approval "50%" else - "#{((0.5 - bar_width_percentage) * 100).round(2)}%" + "#{((0.5 - bar_width_percentage) * 100).abs.round(2)}%" end end @@ -66,6 +66,12 @@ class MeasureGraphRowPresenter end def zone - @measure.zone_for_score(@score) + scale = Scale.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, + ) + scale.zone_for_score(@score) end end diff --git a/app/presenters/scale.rb b/app/presenters/scale.rb new file mode 100644 index 00000000..c520e7ef --- /dev/null +++ b/app/presenters/scale.rb @@ -0,0 +1,45 @@ +class Scale + 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 + end + + Zone = Struct.new(:low_benchmark, :high_benchmark, :type) + + 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 zone_for_score(score) + case score + when ideal_zone.low_benchmark..ideal_zone.high_benchmark + ideal_zone + when approval_zone.low_benchmark..approval_zone.high_benchmark + approval_zone + when growth_zone.low_benchmark..growth_zone.high_benchmark + growth_zone + when watch_zone.low_benchmark..watch_zone.high_benchmark + watch_zone + else + warning_zone + end + end +end diff --git a/app/presenters/subcategory_presenter.rb b/app/presenters/subcategory_presenter.rb new file mode 100644 index 00000000..202629d7 --- /dev/null +++ b/app/presenters/subcategory_presenter.rb @@ -0,0 +1,34 @@ +class SubcategoryPresenter + def initialize(subcategory:, academic_year:, school:) + @subcategory = subcategory + @academic_year = academic_year + @school = school + end + + def name + @subcategory.name + end + + def gauge_presenter + average_score = SurveyItemResponse.for_measures(measures) + .where(academic_year: @academic_year, school: @school) + .average(:likert_score) + + GaugePresenter.new(scale: scale, score: average_score) + end + + private + + def scale + Scale.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 + end +end diff --git a/app/services/survey_response_aggregator.rb b/app/services/survey_response_aggregator.rb index 66e2064b..99ff53a8 100644 --- a/app/services/survey_response_aggregator.rb +++ b/app/services/survey_response_aggregator.rb @@ -1,11 +1,10 @@ +# TODO inline me pls class SurveyResponseAggregator # Returns an average score for all SurveyItemResponses for the given AcademicYear, School, and Measure def self.score(academic_year:, school:, measure:) - SurveyItemResponse + SurveyItemResponse.for_measure(measure) .where(academic_year: academic_year, school: school) - .joins(:survey_item).where('survey_items.measure_id': measure.id) - .map { |survey_item_response| survey_item_response.likert_score } - .average + .average(:likert_score) end # Returns an array of SurveyItemResponses for the given AcademicYear, School, and Measure diff --git a/app/views/browse/_gauge_graph.html.erb b/app/views/browse/_gauge_graph.html.erb new file mode 100644 index 00000000..06f54c6c --- /dev/null +++ b/app/views/browse/_gauge_graph.html.erb @@ -0,0 +1,60 @@ +<% outer_radius = 100 %> +<% inner_radius = 50 %> +<% stroke_width = 1 %> +<% key_benchmark_indicator_height = 10 %> +<% scale = -Math::PI %> + + diff --git a/app/views/browse/show.html.erb b/app/views/browse/show.html.erb new file mode 100644 index 00000000..d894d1fd --- /dev/null +++ b/app/views/browse/show.html.erb @@ -0,0 +1,7 @@ +
This graph shows how much a score is above or below the benchmark of any given scale.