From cf6e80ce6bd9bd49d3d63324df1fa7c617f498da Mon Sep 17 00:00:00 2001 From: Liam Morley Date: Wed, 10 Nov 2021 15:44:21 -0500 Subject: [PATCH] Show modal when no measures for a school/year have meet their threshold --- .../stylesheets/bootstrap-overrides.scss | 6 ++ app/assets/stylesheets/sqm_bootstrap.scss | 4 ++ app/controllers/dashboard_controller.rb | 1 - app/controllers/sqm_application_controller.rb | 1 + app/javascript/application.js | 2 + app/javascript/modal.js | 8 +++ app/models/measure.rb | 4 ++ app/models/sqm_category.rb | 1 + app/models/survey_item_response.rb | 11 ++-- .../layouts/sqm/_empty_dataset_modal.html.erb | 20 +++++++ app/views/layouts/sqm/application.html.erb | 2 + schooldashfeaturespec.png | Bin 0 -> 44449 bytes spec/factories.rb | 2 +- spec/javascript/modal.spec.js | 49 +++++++++++++++ spec/spec_helper.rb | 5 ++ spec/system/authentication_spec.rb | 43 ++++++++++++++ .../journey_spec.rb} | 26 ++++---- spec/system/sqm_application_spec.rb | 56 ++++++++++++++++++ 18 files changed, 220 insertions(+), 21 deletions(-) create mode 100644 app/javascript/modal.js create mode 100644 app/views/layouts/sqm/_empty_dataset_modal.html.erb create mode 100644 schooldashfeaturespec.png create mode 100644 spec/javascript/modal.spec.js create mode 100644 spec/system/authentication_spec.rb rename spec/{features/school_dashboard_feature_spec.rb => system/journey_spec.rb} (93%) create mode 100644 spec/system/sqm_application_spec.rb diff --git a/app/assets/stylesheets/bootstrap-overrides.scss b/app/assets/stylesheets/bootstrap-overrides.scss index b96f6cc7..8f795a7c 100644 --- a/app/assets/stylesheets/bootstrap-overrides.scss +++ b/app/assets/stylesheets/bootstrap-overrides.scss @@ -30,6 +30,9 @@ $input-border-radius: 0; $input-btn-focus-color-opacity: 0.35; $btn-border-radius: 0; +$btn-close-width: 1.5em; +$btn-close-opacity: 1; + $nav-link-color: $white; $nav-link-font-weight: 600; $nav-link-hover-color: $nav-link-color; @@ -48,3 +51,6 @@ $popover-border-color: $gray-2; $popover-border-radius: 4px; $popover-body-padding-x: map-get($spacers, 4); $popover-body-padding-y: map-get($spacers, 4); + +$modal-header-border-color: transparent; +$modal-content-border-radius: 0; diff --git a/app/assets/stylesheets/sqm_bootstrap.scss b/app/assets/stylesheets/sqm_bootstrap.scss index 148c3f41..5fbe4db4 100644 --- a/app/assets/stylesheets/sqm_bootstrap.scss +++ b/app/assets/stylesheets/sqm_bootstrap.scss @@ -50,3 +50,7 @@ .popover { box-shadow: 0 0 8px rgba($black, 0.25); } + +.modal-content { + padding: 40px; +} diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index fac83f40..b0f65164 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -2,7 +2,6 @@ class DashboardController < SqmApplicationController def index @variance_chart_row_presenters = Measure.all.map(&method(:presenter_for_measure)) - @category_presenters = SqmCategory.sorted.map { |sqm_category| CategoryPresenter.new(category: sqm_category) } end diff --git a/app/controllers/sqm_application_controller.rb b/app/controllers/sqm_application_controller.rb index f3c45307..cc1d044b 100644 --- a/app/controllers/sqm_application_controller.rb +++ b/app/controllers/sqm_application_controller.rb @@ -16,6 +16,7 @@ class SqmApplicationController < ActionController::Base @school = School.find_by_slug school_slug @schools = School.where(district: @district).sort_by(&:name) @academic_year = AcademicYear.find_by_range params[:year] + @has_empty_dataset = Measure.none_meet_threshold? school: @school, academic_year: @academic_year end def district_slug diff --git a/app/javascript/application.js b/app/javascript/application.js index 75d32dc6..50788622 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -9,9 +9,11 @@ Turbolinks.start() ActiveStorage.start() import { initializeListenersForNavDropdowns, initializePopovers } from "./dashboard" import { initializeListenersForHomeDropdowns } from "./home" +import { showEmptyDatasetModal } from "./modal" document.addEventListener("turbolinks:load", () => { initializeListenersForNavDropdowns() initializeListenersForHomeDropdowns() initializePopovers() + showEmptyDatasetModal() }) diff --git a/app/javascript/modal.js b/app/javascript/modal.js new file mode 100644 index 00000000..e25b3c49 --- /dev/null +++ b/app/javascript/modal.js @@ -0,0 +1,8 @@ +import { Modal } from "bootstrap"; + +export function showEmptyDatasetModal() { + const modal = document.querySelector('.modal'); + if(modal){ + new Modal(modal).show(); + } +} diff --git a/app/models/measure.rb b/app/models/measure.rb index 86a89021..239ac579 100644 --- a/app/models/measure.rb +++ b/app/models/measure.rb @@ -7,6 +7,10 @@ class Measure < ActiveRecord::Base scope :source_includes_survey_items, ->() { joins(:survey_items).uniq } + def self.none_meet_threshold?(school:, academic_year:) + none? { |measure| SurveyItemResponse.sufficient_data?(measure: measure, school: school, academic_year: academic_year) } + end + def teacher_survey_items @teacher_survey_items ||= survey_items.where("survey_item_id LIKE 't-%'") end diff --git a/app/models/sqm_category.rb b/app/models/sqm_category.rb index f4237e84..5bbebe29 100644 --- a/app/models/sqm_category.rb +++ b/app/models/sqm_category.rb @@ -5,4 +5,5 @@ class SqmCategory < ActiveRecord::Base scope :sorted, ->() { order(:sort_index) } has_many :subcategories + has_many :measures, through: :subcategories end diff --git a/app/models/survey_item_response.rb b/app/models/survey_item_response.rb index 640d8fab..40c5bf9c 100644 --- a/app/models/survey_item_response.rb +++ b/app/models/survey_item_response.rb @@ -25,6 +25,12 @@ class SurveyItemResponse < ActiveRecord::Base end end + def self.sufficient_data?(measure:, school:, academic_year:) + meets_teacher_threshold = teacher_sufficient_data?(measure: measure, school: school, academic_year: academic_year) + meets_student_threshold = student_sufficient_data?(measure: measure, school: school, academic_year: academic_year) + meets_teacher_threshold || meets_student_threshold + end + private def self.for_measure_meeting_threshold(measure:, school:, academic_year:) @@ -46,11 +52,6 @@ class SurveyItemResponse < ActiveRecord::Base scope :teacher_responses_for_measure, ->(measure) { for_measure(measure).where("survey_items.survey_item_id LIKE 't-%'") } scope :student_responses_for_measure, ->(measure) { for_measure(measure).where("survey_items.survey_item_id LIKE 's-%'") } - def self.sufficient_data?(measure:, school:, academic_year:) - meets_teacher_threshold = teacher_sufficient_data?(measure: measure, school: school, academic_year: academic_year) - meets_student_threshold = student_sufficient_data?(measure: measure, school: school, academic_year: academic_year) - meets_teacher_threshold || meets_student_threshold - end def self.student_sufficient_data?(measure:, school:, academic_year:) if measure.includes_student_survey_items? diff --git a/app/views/layouts/sqm/_empty_dataset_modal.html.erb b/app/views/layouts/sqm/_empty_dataset_modal.html.erb new file mode 100644 index 00000000..a12cc52a --- /dev/null +++ b/app/views/layouts/sqm/_empty_dataset_modal.html.erb @@ -0,0 +1,20 @@ + diff --git a/app/views/layouts/sqm/application.html.erb b/app/views/layouts/sqm/application.html.erb index 3b7933b7..7803e56d 100644 --- a/app/views/layouts/sqm/application.html.erb +++ b/app/views/layouts/sqm/application.html.erb @@ -44,5 +44,7 @@ <%= render partial: 'layouts/sqm/footer' %> +<%= render partial: 'layouts/sqm/empty_dataset_modal' if @has_empty_dataset %> + diff --git a/schooldashfeaturespec.png b/schooldashfeaturespec.png new file mode 100644 index 0000000000000000000000000000000000000000..06d8af1a98593f45e6547ed74122dc358e70227b GIT binary patch literal 44449 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 literal 0 HcmV?d00001 diff --git a/spec/factories.rb b/spec/factories.rb index e34f1aed..d79e455b 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -20,7 +20,7 @@ FactoryBot.define do name { "A #{rand} category" } category_id { rand.to_s } description { "A description of a category" } - slug { "a-#{rand}-category" } + slug { name.parameterize } sort_index { 1 } end diff --git a/spec/javascript/modal.spec.js b/spec/javascript/modal.spec.js new file mode 100644 index 00000000..ea0665d5 --- /dev/null +++ b/spec/javascript/modal.spec.js @@ -0,0 +1,49 @@ +import { showEmptyDatasetModal } from "modal"; + +describe("Empty data set modal", () => { + describe("When a modal element exists on the page", () => { + beforeEach(() => { + document.body.innerHTML = ` + + `; + }); + + it("Adds a class to make the modal visible", () => { + showEmptyDatasetModal(); + const modal = document.querySelector(".modal"); + expect(modal.classList.contains('show')).toBe(true); + }); + }); + + describe("When a modal doesn't exist on the page", () => { + beforeEach(() => { + document.body.innerHTML = ` + + + + ` + }) + + it("ignores the content", () =>{ + showEmptyDatasetModal(); + const modal = document.querySelector(".modal"); + expect(modal).toBe(null); + }) + }) +}); diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 03dad712..ea9911af 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -54,7 +54,12 @@ RSpec.configure do |config| config.include Capybara::DSL + config.before(:each, type: :system) do + driven_by :rack_test + end + config.before(:each, js: true) do + driven_by :apparition Capybara.default_max_wait_time = 10 Capybara.page.driver.resize(3000, 3000) end diff --git a/spec/system/authentication_spec.rb b/spec/system/authentication_spec.rb new file mode 100644 index 00000000..33db2b18 --- /dev/null +++ b/spec/system/authentication_spec.rb @@ -0,0 +1,43 @@ +require 'rails_helper' + +describe 'authentication' do + let(:district) { create(:district) } + let(:school) { create(:school, district: district) } + let(:academic_year) { create(:academic_year) } + + context 'when using the wrong credentials' do + before :each do + page.driver.browser.basic_authorize('wrong username', 'wrong password') + end + it 'does not show any information' do + visit dashboard_path + + expect(page).not_to have_text(school.name) + end + end + + context 'when using the right credentials' do + before :each do + page.driver.browser.basic_authorize(username, password) + end + it 'does show information' do + visit dashboard_path + + expect(page).to have_text(school.name) + end + end + + + private + + def username + district.name.downcase + end + def password + "#{username}!" + end + + def dashboard_path + district_school_dashboard_index_path(district, school, year: academic_year.range) + end +end diff --git a/spec/features/school_dashboard_feature_spec.rb b/spec/system/journey_spec.rb similarity index 93% rename from spec/features/school_dashboard_feature_spec.rb rename to spec/system/journey_spec.rb index cefdf270..b849ddeb 100644 --- a/spec/features/school_dashboard_feature_spec.rb +++ b/spec/system/journey_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'School dashboard', type: feature do +describe 'District Admin', js: true do let(:district) { District.find_by_slug 'winchester' } let(:different_district) { District.find_by_slug 'boston' } let(:school) { School.find_by_slug 'winchester-high-school' } @@ -60,19 +60,7 @@ feature 'School dashboard', type: feature do SurveyItemResponse.import survey_item_responses end - after :each do - DatabaseCleaner.clean - end - - scenario 'User authentication fails' do - page.driver.browser.basic_authorize('wrong username', 'wrong password') - - visit "/districts/#{district.slug}/schools/#{school.slug}/dashboard?year=2020-21" - - expect(page).not_to have_text(school.name) - end - - scenario 'District Admin navigates the site', js: true do + it 'navigates through the site' do page.driver.basic_authorize(username, password) visit '/welcome' @@ -132,6 +120,16 @@ def go_to_different_district(district) select district.name, from: 'select-district' end +def go_to_browse_page_for_school_without_data(school) + click_on 'Browse' + select school.name, from: 'select-school' +end + +def go_to_dashboard_page_for_school_without_data(school) + click_on 'Dashboard' + select school.name, from: 'select-school' +end + def district_admin_sees_schools_change expected_path = "/districts/#{school_in_same_district.district.slug}/schools/#{school_in_same_district.slug}/dashboard?year=2020-21" expect(page).to have_current_path(expected_path) diff --git a/spec/system/sqm_application_spec.rb b/spec/system/sqm_application_spec.rb new file mode 100644 index 00000000..89ca503d --- /dev/null +++ b/spec/system/sqm_application_spec.rb @@ -0,0 +1,56 @@ +require 'rails_helper' + +describe 'SQM Application' do + let(:district) { create(:district) } + let(:school) { create(:school, district: district) } + let(:academic_year) { create(:academic_year) } + let(:category) { create(:sqm_category) } + let(:measure) { create(:measure) } + + before :each do + driven_by :rack_test + page.driver.browser.basic_authorize(username, password) + end + + context 'when no measures meet their threshold' do + it 'shows a modal on all pages' do + [dashboard_path, browse_path].each do |path| + visit path + expect(page).to have_css '.modal' + end + end + end + + context 'at least one measure meets its threshold' do + before :each do + teacher_survey_item = create(:teacher_survey_item, measure: measure) + create_list(:survey_item_response, SurveyItemResponse::TEACHER_RESPONSE_THRESHOLD, + survey_item: teacher_survey_item, academic_year: academic_year, school: school) + end + + it 'does not show a modal on any page' do + [dashboard_path, browse_path].each do |path| + visit path + expect(page).not_to have_css '.modal' + end + end + end + + private + + def username + district.name.downcase + end + + def password + "#{username}!" + end + + def dashboard_path + district_school_dashboard_index_path(district, school, year: academic_year.range) + end + + def browse_path + district_school_sqm_category_path(district, school, category, year: academic_year.range) + end +end