From 032c998356447c25994d693315a291c8056e1cf0 Mon Sep 17 00:00:00 2001 From: Tim Irwin Date: Wed, 1 Feb 2023 14:48:09 -0500 Subject: [PATCH 01/10] WIP: add Blazer generator --- .../rolemodel/saas/blazer/README.md | 23 + lib/generators/rolemodel/saas/blazer/USAGE | 25 ++ .../rolemodel/saas/blazer/blazer_generator.rb | 68 +++ .../app/assets/stylesheets/blazer.css | 84 ++++ .../app/assets/stylesheets/selectize.css | 403 ++++++++++++++++++ .../reports/dashboards_controller.rb | 134 ++++++ .../controllers/reports/queries_controller.rb | 21 + .../views/reports/dashboards/index.html.slim | 17 + .../views/reports/dashboards/show.html.slim | 38 ++ .../templates/config/initializers/blazer.rb | 7 + .../lib/blazer_extensions/data_source.rb | 22 + .../blazer/templates/lib/tasks/reports.rake | 57 +++ .../spec/factories/blazer_queries.rb | 11 + .../templates/spec/system/reporting_spec.rb | 25 ++ 14 files changed, 935 insertions(+) create mode 100644 lib/generators/rolemodel/saas/blazer/README.md create mode 100644 lib/generators/rolemodel/saas/blazer/USAGE create mode 100644 lib/generators/rolemodel/saas/blazer/blazer_generator.rb create mode 100644 lib/generators/rolemodel/saas/blazer/templates/app/assets/stylesheets/blazer.css create mode 100644 lib/generators/rolemodel/saas/blazer/templates/app/assets/stylesheets/selectize.css create mode 100644 lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/dashboards_controller.rb create mode 100644 lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/queries_controller.rb create mode 100644 lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/index.html.slim create mode 100644 lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/show.html.slim create mode 100644 lib/generators/rolemodel/saas/blazer/templates/config/initializers/blazer.rb create mode 100644 lib/generators/rolemodel/saas/blazer/templates/lib/blazer_extensions/data_source.rb create mode 100644 lib/generators/rolemodel/saas/blazer/templates/lib/tasks/reports.rake create mode 100644 lib/generators/rolemodel/saas/blazer/templates/spec/factories/blazer_queries.rb create mode 100644 lib/generators/rolemodel/saas/blazer/templates/spec/system/reporting_spec.rb diff --git a/lib/generators/rolemodel/saas/blazer/README.md b/lib/generators/rolemodel/saas/blazer/README.md new file mode 100644 index 00000000..f5af4017 --- /dev/null +++ b/lib/generators/rolemodel/saas/blazer/README.md @@ -0,0 +1,23 @@ +# Blazer Generator + +Depends on the blazer generator +Depends on [RSpec Generator](../../testing/rspec) +Depends on slim + +## What you get + +Installs [blazer](https://github.com/ankane/blazer) + + diff --git a/lib/generators/rolemodel/saas/blazer/USAGE b/lib/generators/rolemodel/saas/blazer/USAGE new file mode 100644 index 00000000..9a6400c8 --- /dev/null +++ b/lib/generators/rolemodel/saas/blazer/USAGE @@ -0,0 +1,25 @@ +Description: + installs and configures Blazer + +Example: + rails generate rolemodel:saas:blazer + + diff --git a/lib/generators/rolemodel/saas/blazer/blazer_generator.rb b/lib/generators/rolemodel/saas/blazer/blazer_generator.rb new file mode 100644 index 00000000..aa257d8f --- /dev/null +++ b/lib/generators/rolemodel/saas/blazer/blazer_generator.rb @@ -0,0 +1,68 @@ +require_relative '../../../bundler_helpers' + +module Rolemodel + module Saas + class BlazerGenerator < Rails::Generators::Base + include Rolemodel::BundlerHelpers + source_root File.expand_path('templates', __dir__) + + def install_blazer + gem 'sprockets-rails' + gem 'blazer' + run_bundle + + generate 'blazer:install' + end + + def add_routes + return if File.readlines('config/routes.rb').grep(/blazer/).any? + + route_info = " # authenticate :user, ->(u) { Admin::ReportPolicy.new(u, :report).manage? } do\n" + route_info += " mount Blazer::Engine => '/admin/reports', as: :blazer\n" + route_info += ' # end' + inject_into_file 'config/routes.rb', route_info, after: /Rails.application.routes.draw do$\n/ + + # add routes for the report controllers + route_info = " namespace :reports do\n" + route_info += " resources :dashboards\n" + route_info += " resources :queries do\n" + route_info += " post :run, on: :collection\n" + route_info += " post :cancel, on: :collection\n" + route_info += " end\n" + route_info += ' end' + route route_info + end + + def add_extensions + copy_file 'config/initializers/blazer.rb' + copy_file 'lib/blazer_extensions/data_source.rb' + end + + def add_controllers + directory 'app/controllers/reports' + end + + def add_views + directory 'app/views/reports' + end + + def add_styles + copy_file 'app/assets/stylesheets/blazer.css' + copy_file 'app/assets/stylesheets/selectize.css' + end + + def add_javascript + # what here? + end + + def add_tests + copy_file 'spec/factories/blazer_queries.rb' + copy_file 'spec/system/reporting_spec.rb' + end + + def add_rake_task + copy_file 'lib/tasks/reports.rake' + end + end + end +end diff --git a/lib/generators/rolemodel/saas/blazer/templates/app/assets/stylesheets/blazer.css b/lib/generators/rolemodel/saas/blazer/templates/app/assets/stylesheets/blazer.css new file mode 100644 index 00000000..b309d15e --- /dev/null +++ b/lib/generators/rolemodel/saas/blazer/templates/app/assets/stylesheets/blazer.css @@ -0,0 +1,84 @@ +/* + *= require ./selectize + *= require_self + *= require blazer/daterangepicker + */ + +/* These styles map Blazer's html structure into our look and feel. */ + +.results-container { /* Copied from .table__wrapper */ + overflow: auto; +} + +.text-muted { + color: var(--color-disabled); +} + +.chart-container { /* Copied from .card */ + position: relative; + border: 1px solid var(--color-contrast-medium); + border-radius: var(--radius); + padding: var(--space-lg); + width: 100%; + background: var(--color-white); + margin-bottom: var(--space-md); +} + +.btn-success { /* Copied from .btn--primary */ + background: var(--color-primary); + color: var(--color-white); + + &:hover { + opacity: 0.7; + } + &:visited { + color: var(--color-white); + } +} + +.form-inline { + display: flex; + align-items: center; +} + +.form-inline label { + margin-bottom: 0; +} + +.selectize-control { + margin-right: var(--space-md); +} + +/* date range picker */ +body .daterangepicker .drp-buttons .btn { + font-size: var(--text-sm); + padding: inherit; + + margin-top: var(--space-xs); + margin-bottom: var(--space-xs); +} + +body .daterangepicker .drp-buttons .btn.cancelBtn { + border: var(--border-width) solid var(--color-contrast-low); + background: var(--color-white); + color: var(--color-black); +} + +body .daterangepicker .drp-buttons .btn.applyBtn { + background: var(--color-primary); + color: var(--color-white); +} + +body .daterangepicker td.in-range { + background: var(--color-primary-light); +} + +body .daterangepicker td.active, body .daterangepicker td.active:hover { + background: var(--color-primary); + color: var(--color-white); +} + +body .daterangepicker .ranges li.active { + background: var(--color-primary); + color: var(--color-white); +} diff --git a/lib/generators/rolemodel/saas/blazer/templates/app/assets/stylesheets/selectize.css b/lib/generators/rolemodel/saas/blazer/templates/app/assets/stylesheets/selectize.css new file mode 100644 index 00000000..2cdb1c24 --- /dev/null +++ b/lib/generators/rolemodel/saas/blazer/templates/app/assets/stylesheets/selectize.css @@ -0,0 +1,403 @@ +/** + * selectize.default.css (v0.12.6) - Default Theme + * Copyright (c) 2013–2015 Brian Reavis & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + * + * @author Brian Reavis + */ +.selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder { + visibility: visible !important; + background: #f2f2f2 !important; + background: rgba(0, 0, 0, 0.06) !important; + border: 0 none !important; + -webkit-box-shadow: inset 0 0 12px 4px #fff; + box-shadow: inset 0 0 12px 4px #fff; +} +.selectize-control.plugin-drag_drop .ui-sortable-placeholder::after { + content: '!'; + visibility: hidden; +} +.selectize-control.plugin-drag_drop .ui-sortable-helper { + -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); +} +.selectize-dropdown-header { + position: relative; + padding: 5px 8px; + border-bottom: 1px solid #d0d0d0; + background: #f8f8f8; + -webkit-border-radius: 3px 3px 0 0; + -moz-border-radius: 3px 3px 0 0; + border-radius: 3px 3px 0 0; +} +.selectize-dropdown-header-close { + position: absolute; + right: 8px; + top: 50%; + color: #303030; + opacity: 0.4; + margin-top: -12px; + line-height: 20px; + font-size: 20px !important; +} +.selectize-dropdown-header-close:hover { + color: #000000; +} +.selectize-dropdown.plugin-optgroup_columns .optgroup { + border-right: 1px solid #f2f2f2; + border-top: 0 none; + float: left; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child { + border-right: 0 none; +} +.selectize-dropdown.plugin-optgroup_columns .optgroup:before { + display: none; +} +.selectize-dropdown.plugin-optgroup_columns .optgroup-header { + border-top: 0 none; +} +.selectize-control.plugin-remove_button [data-value] { + position: relative; + padding-right: 24px !important; +} +.selectize-control.plugin-remove_button [data-value] .remove { + z-index: 1; + /* fixes ie bug (see #392) */ + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 17px; + text-align: center; + font-weight: bold; + font-size: 12px; + color: inherit; + text-decoration: none; + vertical-align: middle; + display: inline-block; + padding: 2px 0 0 0; + border-left: 1px solid #0073bb; + -webkit-border-radius: 0 2px 2px 0; + -moz-border-radius: 0 2px 2px 0; + border-radius: 0 2px 2px 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.selectize-control.plugin-remove_button [data-value] .remove:hover { + background: rgba(0, 0, 0, 0.05); +} +.selectize-control.plugin-remove_button [data-value].active .remove { + border-left-color: #00578d; +} +.selectize-control.plugin-remove_button .disabled [data-value] .remove:hover { + background: none; +} +.selectize-control.plugin-remove_button .disabled [data-value] .remove { + border-left-color: #aaaaaa; +} +.selectize-control.plugin-remove_button .remove-single { + position: absolute; + right: 0; + top: 0; + font-size: 23px; +} +.selectize-control { + position: relative; +} +.selectize-dropdown, +.selectize-input, +.selectize-input input { + color: #303030; + font-family: inherit; + font-size: 13px; + line-height: 18px; + -webkit-font-smoothing: inherit; +} +.selectize-input, +.selectize-control.single .selectize-input.input-active { + background: #fff; + cursor: text; + display: inline-block; +} +.selectize-input { + border: 1px solid #d0d0d0; + padding: 8px 8px; + display: inline-block; + width: 100%; + overflow: hidden; + position: relative; + z-index: 1; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1); + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.selectize-control.multi .selectize-input.has-items { + padding: 5px 8px 2px; +} +.selectize-input.full { + background-color: #fff; +} +.selectize-input.disabled, +.selectize-input.disabled * { + cursor: default !important; +} +.selectize-input.focus { + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15); +} +.selectize-input.dropdown-active { + -webkit-border-radius: 3px 3px 0 0; + -moz-border-radius: 3px 3px 0 0; + border-radius: 3px 3px 0 0; +} +.selectize-input > * { + vertical-align: baseline; + display: -moz-inline-stack; + display: inline-block; + zoom: 1; + *display: inline; +} +.selectize-control.multi .selectize-input > div { + cursor: pointer; + margin: 0 3px 3px 0; + padding: 2px 6px; + background: #1da7ee; + color: #fff; + border: 1px solid #0073bb; +} +.selectize-control.multi .selectize-input > div.active { + background: #92c836; + color: #fff; + border: 1px solid #00578d; +} +.selectize-control.multi .selectize-input.disabled > div, +.selectize-control.multi .selectize-input.disabled > div.active { + color: #ffffff; + background: #d2d2d2; + border: 1px solid #aaaaaa; +} +.selectize-input > input { + display: inline-block !important; + padding: 0 !important; + min-height: 0 !important; + max-height: none !important; + max-width: 100% !important; + margin: 0 1px !important; + text-indent: 0 !important; + border: 0 none !important; + background: none !important; + line-height: inherit !important; + -webkit-user-select: auto !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; +} +.selectize-input > input::-ms-clear { + display: none; +} +.selectize-input > input:focus { + outline: none !important; +} +.selectize-input::after { + content: ' '; + display: block; + clear: left; +} +.selectize-input.dropdown-active::before { + content: ' '; + display: block; + position: absolute; + background: #f0f0f0; + height: 1px; + bottom: 0; + left: 0; + right: 0; +} +.selectize-dropdown { + position: absolute; + z-index: 10; + border: 1px solid #d0d0d0; + background: #fff; + margin: -1px 0 0 0; + border-top: 0 none; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + -webkit-border-radius: 0 0 3px 3px; + -moz-border-radius: 0 0 3px 3px; + border-radius: 0 0 3px 3px; +} +.selectize-dropdown [data-selectable] { + cursor: pointer; + overflow: hidden; +} +.selectize-dropdown [data-selectable] .highlight { + background: rgba(125, 168, 208, 0.2); + -webkit-border-radius: 1px; + -moz-border-radius: 1px; + border-radius: 1px; +} +.selectize-dropdown .option, +.selectize-dropdown .optgroup-header { + padding: 5px 8px; +} +.selectize-dropdown .option, +.selectize-dropdown [data-disabled], +.selectize-dropdown [data-disabled] [data-selectable].option { + cursor: inherit; + opacity: 0.5; +} +.selectize-dropdown [data-selectable].option { + opacity: 1; +} +.selectize-dropdown .optgroup:first-child .optgroup-header { + border-top: 0 none; +} +.selectize-dropdown .optgroup-header { + color: #303030; + background: #fff; + cursor: default; +} +.selectize-dropdown .active { + background-color: #f5fafd; + color: #495c68; +} +.selectize-dropdown .active.create { + color: #495c68; +} +.selectize-dropdown .create { + color: rgba(48, 48, 48, 0.5); +} +.selectize-dropdown-content { + overflow-y: auto; + overflow-x: hidden; + max-height: 200px; + -webkit-overflow-scrolling: touch; +} +.selectize-control.single .selectize-input, +.selectize-control.single .selectize-input input { + cursor: pointer; +} +.selectize-control.single .selectize-input.input-active, +.selectize-control.single .selectize-input.input-active input { + cursor: text; +} +.selectize-control.single .selectize-input:after { + content: ' '; + display: block; + position: absolute; + top: 50%; + right: 15px; + margin-top: -3px; + width: 0; + height: 0; + border-style: solid; + border-width: 5px 5px 0 5px; + border-color: #808080 transparent transparent transparent; +} +.selectize-control.single .selectize-input.dropdown-active:after { + margin-top: -4px; + border-width: 0 5px 5px 5px; + border-color: transparent transparent #808080 transparent; +} +.selectize-control.rtl.single .selectize-input:after { + left: 15px; + right: auto; +} +.selectize-control.rtl .selectize-input > input { + margin: 0 4px 0 -2px !important; +} +.selectize-control .selectize-input.disabled { + opacity: 0.5; + background-color: #fafafa; +} +.selectize-control.multi .selectize-input.has-items { + padding-left: 5px; + padding-right: 5px; +} +.selectize-control.multi .selectize-input.disabled [data-value] { + color: #999; + text-shadow: none; + background: none; + -webkit-box-shadow: none; + box-shadow: none; +} +.selectize-control.multi .selectize-input.disabled [data-value], +.selectize-control.multi .selectize-input.disabled [data-value] .remove { + border-color: #e6e6e6; +} +.selectize-control.multi .selectize-input.disabled [data-value] .remove { + background: none; +} +.selectize-control.multi .selectize-input [data-value] { + text-shadow: 0 1px 0 rgba(0, 51, 83, 0.3); + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + background-color: #1b9dec; + background-image: -moz-linear-gradient(top, #1da7ee, #178ee9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#1da7ee), to(#178ee9)); + background-image: -webkit-linear-gradient(top, #1da7ee, #178ee9); + background-image: -o-linear-gradient(top, #1da7ee, #178ee9); + background-image: linear-gradient(to bottom, #1da7ee, #178ee9); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff1da7ee', endColorstr='#ff178ee9', GradientType=0); + -webkit-box-shadow: 0 1px 0 rgba(0,0,0,0.2),inset 0 1px rgba(255,255,255,0.03); + box-shadow: 0 1px 0 rgba(0,0,0,0.2),inset 0 1px rgba(255,255,255,0.03); +} +.selectize-control.multi .selectize-input [data-value].active { + background-color: #0085d4; + background-image: -moz-linear-gradient(top, #008fd8, #0075cf); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#008fd8), to(#0075cf)); + background-image: -webkit-linear-gradient(top, #008fd8, #0075cf); + background-image: -o-linear-gradient(top, #008fd8, #0075cf); + background-image: linear-gradient(to bottom, #008fd8, #0075cf); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff008fd8', endColorstr='#ff0075cf', GradientType=0); +} +.selectize-control.single .selectize-input { + -webkit-box-shadow: 0 1px 0 rgba(0,0,0,0.05), inset 0 1px 0 rgba(255,255,255,0.8); + box-shadow: 0 1px 0 rgba(0,0,0,0.05), inset 0 1px 0 rgba(255,255,255,0.8); + background-color: #f9f9f9; + background-image: -moz-linear-gradient(top, #fefefe, #f2f2f2); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fefefe), to(#f2f2f2)); + background-image: -webkit-linear-gradient(top, #fefefe, #f2f2f2); + background-image: -o-linear-gradient(top, #fefefe, #f2f2f2); + background-image: linear-gradient(to bottom, #fefefe, #f2f2f2); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffefefe', endColorstr='#fff2f2f2', GradientType=0); +} +.selectize-control.single .selectize-input, +.selectize-dropdown.single { + border-color: #b8b8b8; +} +.selectize-dropdown .optgroup-header { + padding-top: 7px; + font-weight: bold; + font-size: 0.85em; +} +.selectize-dropdown .optgroup { + border-top: 1px solid #f0f0f0; +} +.selectize-dropdown .optgroup:first-child { + border-top: 0 none; +} diff --git a/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/dashboards_controller.rb b/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/dashboards_controller.rb new file mode 100644 index 00000000..f0aefb25 --- /dev/null +++ b/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/dashboards_controller.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +module Reports + class DashboardsController < ApplicationController + before_action :set_dashboard, only: [:show, :edit, :update] + + helper Blazer::BaseHelper + + def index + # authorize Blazer::Dashboard.new + # @dashboards = policy_scope(Blazer::Dashboard).distinct.order(:name) + @dashboards = Blazer::Dashboard.distinct.order(:name) + end + + def show # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity + # authorize @dashboard + # params['current_user_id'] = current_user.id # automatic variable for current_user specific queries + + # The rest is copied from Blazer::DashboardsController + @queries = @dashboard.dashboard_queries.order(:position).preload(:query).map(&:query) + @statements = [] + @queries.each do |query| + statement = query.statement.dup + process_vars(statement, query.data_source) + @statements << statement + end + @bind_vars ||= [] + # added v + @bind_vars = @bind_vars.without('current_user_id') + + @smart_vars = {} + @sql_errors = [] + @data_sources = @queries.map { |q| Blazer.data_sources[q.data_source] }.uniq + @bind_vars.each do |var| + @data_sources.each do |data_source| + smart_var, error = parse_smart_variables(var, data_source) + (@smart_vars[var] ||= []).concat(smart_var).uniq! if smart_var + @sql_errors << error if error + end + end + + add_cohort_analysis_vars if @queries.any?(&:cohort_analysis?) + end + + def edit + # authorize @dashboard + end + + def update + # authorize @dashboard + + if @dashboard.update(permitted_attributes(@dashboard)) + redirect_to reports_dashboards_url, notice: notice_msg('Dashboard successfully updated') + else + render :edit + end + end + + private + + # Copied from Blazer::DashboardsController + def set_dashboard + @dashboard = Blazer::Dashboard.find params[:id] + end + + # Copied from Blazer::BaseController + def process_vars(statement, data_source) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity + (@bind_vars ||= []).concat(Blazer.extract_vars(statement)).uniq! + @bind_vars.each do |var| + params[var] ||= Blazer.data_sources[data_source].variable_defaults[var] + end + @success = @bind_vars.all? { |v| params[v] } + + if @success # rubocop:disable Style/GuardClause + @bind_vars.each do |var| + value = params[var].presence + if value + if ['start_time', 'end_time'].include?(var) + value = value.to_s.gsub(' ', '+') # fix for Quip bug + end + + if var.end_with?('_at') + begin + value = Blazer.time_zone.parse(value) + rescue StandardError # rubocop:disable Metrics/BlockNesting + # do nothing + end + end + + case value + when /\A\d+\z/ + value = value.to_i + when /\A\d+\.\d+\z/ + value = value.to_f + end + end + value = Blazer.transform_variable.call(var, value) if Blazer.transform_variable + statement.gsub!("{#{var}}", ActiveRecord::Base.connection.quote(value)) + end + end + end + + # Copied from Blazer::BaseController + def parse_smart_variables(var, data_source) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity + smart_var_data_source = + ([data_source] + Array(data_source.settings['inherit_smart_settings']).map do |ds| + Blazer.data_sources[ds] + end).find { |ds| ds.smart_variables[var] } + + if smart_var_data_source + query = smart_var_data_source.smart_variables[var] + + if query.is_a? Hash + smart_var = query.map { |k, v| [v, k] } + elsif query.is_a? Array + smart_var = query.map { |v| [v, v] } + elsif query + result = smart_var_data_source.run_statement(query) + smart_var = result.rows.map(&:reverse) + error = result.error if result.error + end + end + + [smart_var, error] + end + + # Copied from Blazer::BaseController + def add_cohort_analysis_vars + @bind_vars << 'cohort_period' unless @bind_vars.include?('cohort_period') + @smart_vars['cohort_period'] = ['day', 'week', 'month'] + params[:cohort_period] ||= 'week' + end + end +end diff --git a/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/queries_controller.rb b/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/queries_controller.rb new file mode 100644 index 00000000..0758eb52 --- /dev/null +++ b/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/queries_controller.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Reports + # Since this doesn't rely on our layout, we can get away with inheriting straight from blazer, + # and mixing in our authorization + class QueriesController < Blazer::QueriesController + # after_action :verify_authorized + + # def run + # @query = Blazer::Query.find_by(id: params[:query_id]) + # authorize @query + # params[:statement] = 'select 666' + # super + # end + + # def cancel + # authorize Blazer::Query.new + # super + # end + end +end diff --git a/lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/index.html.slim b/lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/index.html.slim new file mode 100644 index 00000000..18739afb --- /dev/null +++ b/lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/index.html.slim @@ -0,0 +1,17 @@ +header.header--index-action + h1= t('.reports', default: 'Reports') + +.table__wrapper + - # Since blazer loads its own JS, we can't rely on turbolinks to work + table.table + thead + tr + th= t('activerecord.attributes.blazer_dashboard.name', default: 'Name') + th width="85px"= t('.actions', default: 'Actions') + + tbody + - @dashboards.each do |dashboard| + tr data-test-id="#{dashboard.id}" + td= dashboard.name + td.table__actions + = link_to t('.show', default: 'Show'), reports_dashboard_path(dashboard) diff --git a/lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/show.html.slim b/lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/show.html.slim new file mode 100644 index 00000000..baf2b8c7 --- /dev/null +++ b/lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/show.html.slim @@ -0,0 +1,38 @@ +- content_for :breadcrumb do + = link_to t('.reports', default: 'Reports'), reports_dashboards_path + = icon('chevron_right') + = @dashboard.name + +- # changed v += stylesheet_link_tag "blazer" += javascript_include_tag "blazer/application" +javascript: + #{blazer_js_var "rootPath", "/reports/"} // < changed +- if blazer_maps? + = stylesheet_link_tag "https://api.mapbox.com/mapbox.js/v3.3.1/mapbox.css", integrity: "sha384-vxzdEt+wZRPNQbhChjmiaFMLWg86IGuq1NGDehJHsD2mphYkxXll/eSs16WWi6Dq", crossorigin: "anonymous" + = javascript_include_tag "https://api.mapbox.com/mapbox.js/v3.3.1/mapbox.js", integrity: "sha384-CTBEiDLiZJ8gkAQ3fYGoeiRp81/ecNiBkGz11jXFALOZ6++rbnqmdo6OImkmr1MO", crossorigin: "anonymous" + +h1= @dashboard.name + +- if @bind_vars.any? + - # changed v + = render partial: "blazer/variables", locals: { action: reports_dashboard_path(@dashboard) } + +- @queries.each_with_index do |query, i| + .chart-container + h4= query.friendly_name + -if query.description.present? + div= markdown(query.filtered_description.html_safe) + .chart id="chart-#{i}" + - # changed v + p.text-muted= t('.loading', default: 'Loading...') + + javascript: + #{blazer_js_var "data", {statement: @statements[i], query_id: query.id, data_source: query.data_source, only_chart: true, cohort_period: params[:cohort_period]}} + + runQuery(data, function (data) { + $("#chart-#{i}").html(data) + $("#chart-#{i} table").stupidtable(stupidtableCustomSettings) + }, function (message) { + $("#chart-#{i}").addClass("query-error").html(message) + }); diff --git a/lib/generators/rolemodel/saas/blazer/templates/config/initializers/blazer.rb b/lib/generators/rolemodel/saas/blazer/templates/config/initializers/blazer.rb new file mode 100644 index 00000000..a63befe3 --- /dev/null +++ b/lib/generators/rolemodel/saas/blazer/templates/config/initializers/blazer.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'blazer_extensions/data_source' + +Rails.application.config.to_prepare do + Blazer::DataSource.prepend BlazerExtensions::DataSource +end diff --git a/lib/generators/rolemodel/saas/blazer/templates/lib/blazer_extensions/data_source.rb b/lib/generators/rolemodel/saas/blazer/templates/lib/blazer_extensions/data_source.rb new file mode 100644 index 00000000..b0fb0a81 --- /dev/null +++ b/lib/generators/rolemodel/saas/blazer/templates/lib/blazer_extensions/data_source.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module BlazerExtensions + module DataSource + # add Rails.env.test? so reports can run when testing + def adapter_instance # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + @adapter_instance ||= begin + unless settings['url'] || + Rails.env.development? || + Rails.env.test? || + ['bigquery', 'athena', 'snowflake', + 'salesforce'].include?(settings['adapter']) + raise Blazer::Error, "Empty url for data source: #{id}" + end + + raise Blazer::Error, 'Unknown adapter' unless Blazer.adapters[adapter] + + Blazer.adapters[adapter].new(self) + end + end + end +end diff --git a/lib/generators/rolemodel/saas/blazer/templates/lib/tasks/reports.rake b/lib/generators/rolemodel/saas/blazer/templates/lib/tasks/reports.rake new file mode 100644 index 00000000..c714258a --- /dev/null +++ b/lib/generators/rolemodel/saas/blazer/templates/lib/tasks/reports.rake @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +def import_blazer_query(author_user_id, query_path_name) + file_name = query_path_name.split('/').last + puts "Importing blazer query #{file_name}" + file_contents = File.readlines(query_path_name) + description = file_contents.select { |line| line.start_with?('--')} + .map { |comment_line| comment_line.gsub('--', '').strip } + .join('. ') + .presence + query = file_contents.select {|line| !line.start_with?('--') }.join(' ') + name = File.basename(file_name, File.extname(file_name)) + Blazer::Query.create_with( + description: description, + statement: query, + status: 'active', + data_source: 'main', + creator_id: author_user_id + ).find_or_create_by!(name: name) +end + +def import_blazer_dashboard(author_user_id, dashboard_path_name) + file_name = dashboard_path_name.split('/').last + puts "Importing blazer dashboard #{file_name}" + name = File.basename(file_name, File.extname(file_name)) + Blazer::Dashboard.where(name: name).destroy_all + blazer_dashboard = Blazer::Dashboard.create( + name: name, creator_id: author_user_id + ) + + file_contents = File.readlines(dashboard_path_name).reject { |s| s.strip.empty? } + file_contents.each do |query_name| + query = Blazer::Query.find_by(name: query_name.strip) + if query.nil? + puts "Unable to find blazer query #{query_name}" + next + end + blazer_dashboard.queries << query + end +end + +namespace :import do + desc 'import all stock reports' + task reports: :environment do + author_user_id = ENV['USER_ID'] + if author_user_id.blank? + puts "USER_ID is required" + exit(1) + end + Dir[Rails.root.join('db/blazer/queries/*.sql')].sort.each do |path_name| + import_blazer_query(author_user_id, path_name) + end + Dir[Rails.root.join('db/blazer/dashboards/*.txt')].sort.each do |path_name| + import_blazer_dashboard(author_user_id, path_name) + end + end +end diff --git a/lib/generators/rolemodel/saas/blazer/templates/spec/factories/blazer_queries.rb b/lib/generators/rolemodel/saas/blazer/templates/spec/factories/blazer_queries.rb new file mode 100644 index 00000000..f5ae7d03 --- /dev/null +++ b/lib/generators/rolemodel/saas/blazer/templates/spec/factories/blazer_queries.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :blazer_query, class: Blazer::Query do + association :creator, factory: :user + sequence(:name) { |n| "Query #{n}" } + statement { 'select * from users' } + data_source { 'main' } + status { 'active' } + end +end diff --git a/lib/generators/rolemodel/saas/blazer/templates/spec/system/reporting_spec.rb b/lib/generators/rolemodel/saas/blazer/templates/spec/system/reporting_spec.rb new file mode 100644 index 00000000..09f9e791 --- /dev/null +++ b/lib/generators/rolemodel/saas/blazer/templates/spec/system/reporting_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true +require 'rails_helper' + +RSpec.describe 'Reporting', type: :system, js: true do + let(:user) { create(:user) } + let!(:basic_query) do + sql = 'select 1' + create :blazer_query, name: 'Basic Report', statement: sql + end + + before { sign_in user } + + context 'Happy Path' do + it 'allows a user to select and download a report CSV' do + visit reports_path + expect(page).to have_current_path(reports_path) + expect(page).to have_content('Basic Report') + click_on 'Show' + expect(page).to have_current_path(reports_query_path(basic_query)) + click_on 'Run' + # find(data_test('download-csv-button')).click + # expect(page).to have_button() + end + end +end From 3bdf1193c7b17aefa7504cfdac08783e32be088b Mon Sep 17 00:00:00 2001 From: Tim Irwin Date: Wed, 1 Feb 2023 15:02:22 -0500 Subject: [PATCH 02/10] Update per code review --- .../rolemodel/saas/blazer/README.md | 14 +++++-------- lib/generators/rolemodel/saas/blazer/USAGE | 20 ------------------- .../rolemodel/saas/blazer/blazer_generator.rb | 6 +----- 3 files changed, 6 insertions(+), 34 deletions(-) diff --git a/lib/generators/rolemodel/saas/blazer/README.md b/lib/generators/rolemodel/saas/blazer/README.md index f5af4017..2f0dacde 100644 --- a/lib/generators/rolemodel/saas/blazer/README.md +++ b/lib/generators/rolemodel/saas/blazer/README.md @@ -8,16 +8,12 @@ Depends on slim Installs [blazer](https://github.com/ankane/blazer) - +* Report system test diff --git a/lib/generators/rolemodel/saas/blazer/USAGE b/lib/generators/rolemodel/saas/blazer/USAGE index 9a6400c8..5e9fe449 100644 --- a/lib/generators/rolemodel/saas/blazer/USAGE +++ b/lib/generators/rolemodel/saas/blazer/USAGE @@ -3,23 +3,3 @@ Description: Example: rails generate rolemodel:saas:blazer - - diff --git a/lib/generators/rolemodel/saas/blazer/blazer_generator.rb b/lib/generators/rolemodel/saas/blazer/blazer_generator.rb index aa257d8f..d675ae01 100644 --- a/lib/generators/rolemodel/saas/blazer/blazer_generator.rb +++ b/lib/generators/rolemodel/saas/blazer/blazer_generator.rb @@ -7,7 +7,7 @@ class BlazerGenerator < Rails::Generators::Base source_root File.expand_path('templates', __dir__) def install_blazer - gem 'sprockets-rails' + gem 'sprockets-rails' unless File.readlines('Gemfile').grep(/sprockets/).any? gem 'blazer' run_bundle @@ -51,10 +51,6 @@ def add_styles copy_file 'app/assets/stylesheets/selectize.css' end - def add_javascript - # what here? - end - def add_tests copy_file 'spec/factories/blazer_queries.rb' copy_file 'spec/system/reporting_spec.rb' From b0f20ffc62b8f3253be37cce8d8f2d235465a83b Mon Sep 17 00:00:00 2001 From: Tim Irwin Date: Wed, 1 Feb 2023 15:35:21 -0500 Subject: [PATCH 03/10] Fix tests --- .../spec/factories/blazer_dashboards.rb | 8 ++++++++ .../templates/spec/system/reporting_spec.rb | 17 +++++++---------- .../rolemodel/testing/rspec/rspec_generator.rb | 8 +++++--- 3 files changed, 20 insertions(+), 13 deletions(-) create mode 100644 lib/generators/rolemodel/saas/blazer/templates/spec/factories/blazer_dashboards.rb diff --git a/lib/generators/rolemodel/saas/blazer/templates/spec/factories/blazer_dashboards.rb b/lib/generators/rolemodel/saas/blazer/templates/spec/factories/blazer_dashboards.rb new file mode 100644 index 00000000..5b5eac4d --- /dev/null +++ b/lib/generators/rolemodel/saas/blazer/templates/spec/factories/blazer_dashboards.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :blazer_dashboard, class: Blazer::Dashboard do + association :creator, factory: :user + sequence(:name) { |n| "Dashboard #{n}" } + end +end diff --git a/lib/generators/rolemodel/saas/blazer/templates/spec/system/reporting_spec.rb b/lib/generators/rolemodel/saas/blazer/templates/spec/system/reporting_spec.rb index 09f9e791..c4f23f05 100644 --- a/lib/generators/rolemodel/saas/blazer/templates/spec/system/reporting_spec.rb +++ b/lib/generators/rolemodel/saas/blazer/templates/spec/system/reporting_spec.rb @@ -3,23 +3,20 @@ RSpec.describe 'Reporting', type: :system, js: true do let(:user) { create(:user) } - let!(:basic_query) do - sql = 'select 1' - create :blazer_query, name: 'Basic Report', statement: sql + let!(:basic_report) do + create :blazer_dashboard, name: 'Basic Report' end before { sign_in user } context 'Happy Path' do - it 'allows a user to select and download a report CSV' do - visit reports_path - expect(page).to have_current_path(reports_path) + it 'allows a user to show a report' do + visit reports_dashboards_path + expect(page).to have_current_path(reports_dashboards_path) expect(page).to have_content('Basic Report') click_on 'Show' - expect(page).to have_current_path(reports_query_path(basic_query)) - click_on 'Run' - # find(data_test('download-csv-button')).click - # expect(page).to have_button() + expect(page).to have_current_path(reports_dashboard_path(basic_report)) + expect(page).to have_content('Basic Report') end end end diff --git a/lib/generators/rolemodel/testing/rspec/rspec_generator.rb b/lib/generators/rolemodel/testing/rspec/rspec_generator.rb index 3eb5a62e..b0a872e3 100644 --- a/lib/generators/rolemodel/testing/rspec/rspec_generator.rb +++ b/lib/generators/rolemodel/testing/rspec/rspec_generator.rb @@ -12,9 +12,11 @@ def install_rspec end run_bundle - gem_group :test do - gem 'capybara' - gem 'webdrivers' + if File.readlines('Gemfile').grep(/capybara|webdrivers/).none? + gem_group :test do + gem 'capybara' unless File.readlines('Gemfile').grep(/capybara/).any? + gem 'webdrivers' unless File.readlines('Gemfile').grep(/webdrivers/).any? + end end run_bundle end From cad3e3bbfcc09c2c8c48f2244fca221fcfa4c532 Mon Sep 17 00:00:00 2001 From: Tim Irwin Date: Wed, 1 Feb 2023 16:21:05 -0500 Subject: [PATCH 04/10] Fix File.exists? issue --- lib/generators/rolemodel/css/base/base_generator.rb | 2 +- lib/generators/rolemodel/modals/modals_generator.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/generators/rolemodel/css/base/base_generator.rb b/lib/generators/rolemodel/css/base/base_generator.rb index 880137a2..a27100eb 100644 --- a/lib/generators/rolemodel/css/base/base_generator.rb +++ b/lib/generators/rolemodel/css/base/base_generator.rb @@ -36,7 +36,7 @@ def use_webpacker_styles_in_layout layouts = [ 'erb', 'slim' ].each do |template_language| layout_file = "app/views/layouts/application.html.#{template_language}" - next unless File.exists? layout_file + next unless File.exist? layout_file gsub_file layout_file, "stylesheet_link_tag 'application', media: 'all'", "stylesheet_pack_tag 'stylesheets'" end end diff --git a/lib/generators/rolemodel/modals/modals_generator.rb b/lib/generators/rolemodel/modals/modals_generator.rb index 6f7e9c5d..8cff4cae 100644 --- a/lib/generators/rolemodel/modals/modals_generator.rb +++ b/lib/generators/rolemodel/modals/modals_generator.rb @@ -3,7 +3,7 @@ class ModalsGenerator < Rails::Generators::Base source_root File.expand_path('templates', __dir__) def install_icons - generate 'rolemodel:css:base' unless File.exists?(Rails.root.join('app/javascript/packs/stylesheets.scss')) + generate 'rolemodel:css:base' unless File.exist?(Rails.root.join('app/javascript/packs/stylesheets.scss')) generate 'rolemodel:css:icons' end From 8273e2fdbed565f76c39900dc80ab23499f785be Mon Sep 17 00:00:00 2001 From: Tim Irwin Date: Thu, 2 Feb 2023 11:14:03 -0500 Subject: [PATCH 05/10] Update to use latest Blazer gem defaults --- .../rolemodel/saas/blazer/blazer_generator.rb | 17 +-- .../reports/dashboards_controller.rb | 124 +----------------- .../controllers/reports/queries_controller.rb | 10 +- .../views/reports/dashboards/show.html.slim | 15 +-- 4 files changed, 23 insertions(+), 143 deletions(-) diff --git a/lib/generators/rolemodel/saas/blazer/blazer_generator.rb b/lib/generators/rolemodel/saas/blazer/blazer_generator.rb index d675ae01..1e4a518d 100644 --- a/lib/generators/rolemodel/saas/blazer/blazer_generator.rb +++ b/lib/generators/rolemodel/saas/blazer/blazer_generator.rb @@ -17,19 +17,20 @@ def install_blazer def add_routes return if File.readlines('config/routes.rb').grep(/blazer/).any? - route_info = " # authenticate :user, ->(u) { Admin::ReportPolicy.new(u, :report).manage? } do\n" - route_info += " mount Blazer::Engine => '/admin/reports', as: :blazer\n" - route_info += ' # end' - inject_into_file 'config/routes.rb', route_info, after: /Rails.application.routes.draw do$\n/ - # add routes for the report controllers route_info = " namespace :reports do\n" - route_info += " resources :dashboards\n" - route_info += " resources :queries do\n" + route_info += " resources :dashboards, only: %i[index show]\n" + route_info += " resources :queries do, only: []\n" route_info += " post :run, on: :collection\n" route_info += " post :cancel, on: :collection\n" route_info += " end\n" - route_info += ' end' + route_info += " end\n" + route route_info + + # add routes for the Blazer engine + route_info = " # authenticate :user, ->(u) { Admin::ReportPolicy.new(u, :report).manage? } do\n" + route_info += " mount Blazer::Engine => '/admin/reports', as: :blazer\n" + route_info += " # end\n" route route_info end diff --git a/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/dashboards_controller.rb b/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/dashboards_controller.rb index f0aefb25..2ceb7172 100644 --- a/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/dashboards_controller.rb +++ b/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/dashboards_controller.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true module Reports - class DashboardsController < ApplicationController - before_action :set_dashboard, only: [:show, :edit, :update] - - helper Blazer::BaseHelper + class DashboardsController < Blazer::DashboardsController + layout 'application' def index # authorize Blazer::Dashboard.new @@ -12,123 +10,9 @@ def index @dashboards = Blazer::Dashboard.distinct.order(:name) end - def show # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity - # authorize @dashboard - # params['current_user_id'] = current_user.id # automatic variable for current_user specific queries - - # The rest is copied from Blazer::DashboardsController - @queries = @dashboard.dashboard_queries.order(:position).preload(:query).map(&:query) - @statements = [] - @queries.each do |query| - statement = query.statement.dup - process_vars(statement, query.data_source) - @statements << statement - end - @bind_vars ||= [] - # added v - @bind_vars = @bind_vars.without('current_user_id') - - @smart_vars = {} - @sql_errors = [] - @data_sources = @queries.map { |q| Blazer.data_sources[q.data_source] }.uniq - @bind_vars.each do |var| - @data_sources.each do |data_source| - smart_var, error = parse_smart_variables(var, data_source) - (@smart_vars[var] ||= []).concat(smart_var).uniq! if smart_var - @sql_errors << error if error - end - end - - add_cohort_analysis_vars if @queries.any?(&:cohort_analysis?) - end - - def edit + def show # authorize @dashboard - end - - def update - # authorize @dashboard - - if @dashboard.update(permitted_attributes(@dashboard)) - redirect_to reports_dashboards_url, notice: notice_msg('Dashboard successfully updated') - else - render :edit - end - end - - private - - # Copied from Blazer::DashboardsController - def set_dashboard - @dashboard = Blazer::Dashboard.find params[:id] - end - - # Copied from Blazer::BaseController - def process_vars(statement, data_source) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity - (@bind_vars ||= []).concat(Blazer.extract_vars(statement)).uniq! - @bind_vars.each do |var| - params[var] ||= Blazer.data_sources[data_source].variable_defaults[var] - end - @success = @bind_vars.all? { |v| params[v] } - - if @success # rubocop:disable Style/GuardClause - @bind_vars.each do |var| - value = params[var].presence - if value - if ['start_time', 'end_time'].include?(var) - value = value.to_s.gsub(' ', '+') # fix for Quip bug - end - - if var.end_with?('_at') - begin - value = Blazer.time_zone.parse(value) - rescue StandardError # rubocop:disable Metrics/BlockNesting - # do nothing - end - end - - case value - when /\A\d+\z/ - value = value.to_i - when /\A\d+\.\d+\z/ - value = value.to_f - end - end - value = Blazer.transform_variable.call(var, value) if Blazer.transform_variable - statement.gsub!("{#{var}}", ActiveRecord::Base.connection.quote(value)) - end - end - end - - # Copied from Blazer::BaseController - def parse_smart_variables(var, data_source) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity - smart_var_data_source = - ([data_source] + Array(data_source.settings['inherit_smart_settings']).map do |ds| - Blazer.data_sources[ds] - end).find { |ds| ds.smart_variables[var] } - - if smart_var_data_source - query = smart_var_data_source.smart_variables[var] - - if query.is_a? Hash - smart_var = query.map { |k, v| [v, k] } - elsif query.is_a? Array - smart_var = query.map { |v| [v, v] } - elsif query - result = smart_var_data_source.run_statement(query) - smart_var = result.rows.map(&:reverse) - error = result.error if result.error - end - end - - [smart_var, error] - end - - # Copied from Blazer::BaseController - def add_cohort_analysis_vars - @bind_vars << 'cohort_period' unless @bind_vars.include?('cohort_period') - @smart_vars['cohort_period'] = ['day', 'week', 'month'] - params[:cohort_period] ||= 'week' + super end end end diff --git a/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/queries_controller.rb b/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/queries_controller.rb index 0758eb52..119e8e5d 100644 --- a/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/queries_controller.rb +++ b/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/queries_controller.rb @@ -6,12 +6,12 @@ module Reports class QueriesController < Blazer::QueriesController # after_action :verify_authorized - # def run - # @query = Blazer::Query.find_by(id: params[:query_id]) + def run + @query = Blazer::Query.find_by(id: params[:query_id]) # authorize @query - # params[:statement] = 'select 666' - # super - # end + params[:statement] = @query.statement.dup + super + end # def cancel # authorize Blazer::Query.new diff --git a/lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/show.html.slim b/lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/show.html.slim index baf2b8c7..8d8d793d 100644 --- a/lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/show.html.slim +++ b/lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/show.html.slim @@ -1,8 +1,3 @@ -- content_for :breadcrumb do - = link_to t('.reports', default: 'Reports'), reports_dashboards_path - = icon('chevron_right') - = @dashboard.name - - # changed v = stylesheet_link_tag "blazer" = javascript_include_tag "blazer/application" @@ -15,20 +10,20 @@ javascript: h1= @dashboard.name - if @bind_vars.any? - - # changed v + - # changed v (route) = render partial: "blazer/variables", locals: { action: reports_dashboard_path(@dashboard) } - @queries.each_with_index do |query, i| .chart-container h4= query.friendly_name - -if query.description.present? - div= markdown(query.filtered_description.html_safe) .chart id="chart-#{i}" - - # changed v + - # changed v (translation added) p.text-muted= t('.loading', default: 'Loading...') + - data = {query_id: query.id, data_source: query.data_source, variables: variable_params(query), only_chart: true} + - data.merge!(cohort_period: params[:cohort_period]) if params[:cohort_period] javascript: - #{blazer_js_var "data", {statement: @statements[i], query_id: query.id, data_source: query.data_source, only_chart: true, cohort_period: params[:cohort_period]}} + #{blazer_js_var "data", data } runQuery(data, function (data) { $("#chart-#{i}").html(data) From 97b83326f3e2f936f1e85889cec04826df534808 Mon Sep 17 00:00:00 2001 From: Tim Irwin Date: Thu, 2 Feb 2023 14:03:48 -0500 Subject: [PATCH 06/10] Copy methods into Reports::DashboardsController --- .../rolemodel/saas/blazer/blazer_generator.rb | 2 +- .../reports/dashboards_controller.rb | 89 ++++++++++++++++++- .../controllers/reports/queries_controller.rb | 3 +- 3 files changed, 89 insertions(+), 5 deletions(-) diff --git a/lib/generators/rolemodel/saas/blazer/blazer_generator.rb b/lib/generators/rolemodel/saas/blazer/blazer_generator.rb index 1e4a518d..5dc1850e 100644 --- a/lib/generators/rolemodel/saas/blazer/blazer_generator.rb +++ b/lib/generators/rolemodel/saas/blazer/blazer_generator.rb @@ -20,7 +20,7 @@ def add_routes # add routes for the report controllers route_info = " namespace :reports do\n" route_info += " resources :dashboards, only: %i[index show]\n" - route_info += " resources :queries do, only: []\n" + route_info += " resources :queries, only: [] do\n" route_info += " post :run, on: :collection\n" route_info += " post :cancel, on: :collection\n" route_info += " end\n" diff --git a/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/dashboards_controller.rb b/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/dashboards_controller.rb index 2ceb7172..89479c45 100644 --- a/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/dashboards_controller.rb +++ b/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/dashboards_controller.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module Reports - class DashboardsController < Blazer::DashboardsController - layout 'application' + class DashboardsController < ApplicationController + helper Blazer::BaseHelper def index # authorize Blazer::Dashboard.new @@ -11,8 +11,91 @@ def index end def show + @dashboard = Blazer::Dashboard.find(params[:id]) # authorize @dashboard - super + + # The rest is copied from Blazer::DashboardsController + @queries = @dashboard.dashboard_queries.order(:position).preload(:query).map(&:query) + @queries.each do |query| + @success = process_vars(query.statement_object) + end + @bind_vars ||= [] + + @smart_vars = {} + @sql_errors = [] + @data_sources = @queries.map { |q| Blazer.data_sources[q.data_source] }.uniq + @bind_vars.each do |var| + @data_sources.each do |data_source| + smart_var, error = parse_smart_variables(var, data_source) + ((@smart_vars[var] ||= []).concat(smart_var)).uniq! if smart_var + @sql_errors << error if error + end + end + + add_cohort_analysis_vars if @queries.any?(&:cohort_analysis?) + end + + private + + # Copied from Blazer::BaseController + def process_vars(statement, var_params = nil) + var_params ||= request.query_parameters + (@bind_vars ||= []).concat(statement.variables).uniq! + # update in-place so populated in view and consistent across queries on dashboard + @bind_vars.each do |var| + if !var_params[var] + default = statement.data_source.variable_defaults[var] + # only add if default exists + var_params[var] = default if default + end + end + runnable = @bind_vars.all? { |v| var_params[v] } + statement.add_values(var_params) if runnable + runnable + end + + # Copied from Blazer::BaseController + def add_cohort_analysis_vars + @bind_vars << "cohort_period" unless @bind_vars.include?("cohort_period") + @smart_vars["cohort_period"] = ["day", "week", "month"] if @smart_vars + # TODO create var_params method + request.query_parameters["cohort_period"] ||= "week" + end + + # Copied from Blazer::BaseController + def parse_smart_variables(var, data_source) + smart_var_data_source = + ([data_source] + Array(data_source.settings["inherit_smart_settings"]).map { |ds| Blazer.data_sources[ds] }).find { |ds| ds.smart_variables[var] } + + if smart_var_data_source + query = smart_var_data_source.smart_variables[var] + + if query.is_a? Hash + smart_var = query.map { |k,v| [v, k] } + elsif query.is_a? Array + smart_var = query.map { |v| [v, v] } + elsif query + result = smart_var_data_source.run_statement(query) + smart_var = result.rows.map { |v| v.reverse } + error = result.error if result.error + end + end + + [smart_var, error] + end + + # Copied from Blazer::BaseController + def variable_params(resource, var_params = nil) + permitted_keys = resource.variables + var_params ||= request.query_parameters + var_params.slice(*permitted_keys) + end + helper_method :variable_params + + # Copied from Blazer::BaseController + def nested_variable_params(resource) + variable_params(resource, request.query_parameters["variables"] || {}) end + helper_method :nested_variable_params end end diff --git a/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/queries_controller.rb b/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/queries_controller.rb index 119e8e5d..d1673018 100644 --- a/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/queries_controller.rb +++ b/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/queries_controller.rb @@ -4,11 +4,12 @@ module Reports # Since this doesn't rely on our layout, we can get away with inheriting straight from blazer, # and mixing in our authorization class QueriesController < Blazer::QueriesController + # TODO: must be reenabled because Blazer disables all inherited before/around/after callbacks # after_action :verify_authorized def run @query = Blazer::Query.find_by(id: params[:query_id]) - # authorize @query + # authorize @query params[:statement] = @query.statement.dup super end From 41854673c3c2a172bbca73afce42388beb1a414c Mon Sep 17 00:00:00 2001 From: Tim Irwin Date: Thu, 2 Feb 2023 15:04:20 -0500 Subject: [PATCH 07/10] Update README, add db constraints, and manifest change --- .../rolemodel/saas/blazer/README.md | 4 +- .../rolemodel/saas/blazer/blazer_generator.rb | 6 +++ .../app/assets/stylesheets/blazer.css | 50 +++++++++---------- .../views/reports/dashboards/show.html.slim | 1 + 4 files changed, 34 insertions(+), 27 deletions(-) diff --git a/lib/generators/rolemodel/saas/blazer/README.md b/lib/generators/rolemodel/saas/blazer/README.md index 2f0dacde..660c2d57 100644 --- a/lib/generators/rolemodel/saas/blazer/README.md +++ b/lib/generators/rolemodel/saas/blazer/README.md @@ -3,10 +3,12 @@ Depends on the blazer generator Depends on [RSpec Generator](../../testing/rspec) Depends on slim +Works best with Optics +* If not using with Optics, update blazer.css to use your own tokens. ## What you get -Installs [blazer](https://github.com/ankane/blazer) +Installs [blazer](https://github.com/ankane/blazer) with customization for enabling non-admin users to run Dashboard-type reports (SQL queries with variables) at /reports/dashboards. Prevents SQL-injection vulnerability present in base gem which assumes super-admin access only. ### Controllers * Adds Reports::DashboardController diff --git a/lib/generators/rolemodel/saas/blazer/blazer_generator.rb b/lib/generators/rolemodel/saas/blazer/blazer_generator.rb index 5dc1850e..d5132900 100644 --- a/lib/generators/rolemodel/saas/blazer/blazer_generator.rb +++ b/lib/generators/rolemodel/saas/blazer/blazer_generator.rb @@ -14,6 +14,11 @@ def install_blazer generate 'blazer:install' end + def update_migration + filename = Dir.glob('db/migrate/*_install_blazer.rb').first + inject_into_file filename, ', foreign_key: { to_table: :users }', after: 't.references :creator' + end + def add_routes return if File.readlines('config/routes.rb').grep(/blazer/).any? @@ -50,6 +55,7 @@ def add_views def add_styles copy_file 'app/assets/stylesheets/blazer.css' copy_file 'app/assets/stylesheets/selectize.css' + append_to_file 'app/assets/config/manifest.js', "\n//= link blazer.css" end def add_tests diff --git a/lib/generators/rolemodel/saas/blazer/templates/app/assets/stylesheets/blazer.css b/lib/generators/rolemodel/saas/blazer/templates/app/assets/stylesheets/blazer.css index b309d15e..465d2621 100644 --- a/lib/generators/rolemodel/saas/blazer/templates/app/assets/stylesheets/blazer.css +++ b/lib/generators/rolemodel/saas/blazer/templates/app/assets/stylesheets/blazer.css @@ -11,28 +11,27 @@ } .text-muted { - color: var(--color-disabled); + color: var(--rm-color-disabled); } .chart-container { /* Copied from .card */ position: relative; - border: 1px solid var(--color-contrast-medium); - border-radius: var(--radius); - padding: var(--space-lg); + border: 1px solid var(--rm-color-contrast-medium); + border-radius: var(--rm-radius); + padding: var(--rm-space-large); width: 100%; - background: var(--color-white); - margin-bottom: var(--space-md); + background: var(--rm-color-white); + margin-bottom: var(--rm-space-medium); } -.btn-success { /* Copied from .btn--primary */ - background: var(--color-primary); - color: var(--color-white); - +.btn-success { /* Copied from .btn--rm-primary */ + background: var(--rm-color-primary-minus-four); + color: var(--rm-color-white); &:hover { opacity: 0.7; } &:visited { - color: var(--color-white); + color: var(--rm-color-white); } } @@ -46,39 +45,38 @@ } .selectize-control { - margin-right: var(--space-md); + margin-right: var(--rm-space-medium); } /* date range picker */ body .daterangepicker .drp-buttons .btn { - font-size: var(--text-sm); + font-size: var(--rm-text-small); padding: inherit; - - margin-top: var(--space-xs); - margin-bottom: var(--space-xs); + margin-top: var(--rm-space-x-small); + margin-bottom: var(--rm-space-x-small); } body .daterangepicker .drp-buttons .btn.cancelBtn { - border: var(--border-width) solid var(--color-contrast-low); - background: var(--color-white); - color: var(--color-black); + box-shadow: var(--rm-border-all) var(--rm-border-color); + background: var(--rm-color-white); + color: var(--rm-color-neutral-minus-max); } body .daterangepicker .drp-buttons .btn.applyBtn { - background: var(--color-primary); - color: var(--color-white); + background: var(--rm-color-primary-minus-four); + color: var(--rm-color-white); } body .daterangepicker td.in-range { - background: var(--color-primary-light); + background: var(--rm-color-primary-plus-four); } body .daterangepicker td.active, body .daterangepicker td.active:hover { - background: var(--color-primary); - color: var(--color-white); + background: var(--rm-color-primary-minus-four); + color: var(--rm-color-white); } body .daterangepicker .ranges li.active { - background: var(--color-primary); - color: var(--color-white); + background: var(--rm-color-primary-minus-four); + color: var(--rm-color-white); } diff --git a/lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/show.html.slim b/lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/show.html.slim index 8d8d793d..3ef39e8f 100644 --- a/lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/show.html.slim +++ b/lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/show.html.slim @@ -20,6 +20,7 @@ h1= @dashboard.name - # changed v (translation added) p.text-muted= t('.loading', default: 'Loading...') + - # changed v (removed statement) - data = {query_id: query.id, data_source: query.data_source, variables: variable_params(query), only_chart: true} - data.merge!(cohort_period: params[:cohort_period]) if params[:cohort_period] javascript: From 4a0024ef49e66084571b9659cffb83ea676aa56f Mon Sep 17 00:00:00 2001 From: Tim Irwin Date: Thu, 2 Feb 2023 15:49:00 -0500 Subject: [PATCH 08/10] Require authorization/authentication to use queries run --- lib/generators/rolemodel/saas/blazer/blazer_generator.rb | 2 +- .../templates/app/controllers/reports/queries_controller.rb | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/generators/rolemodel/saas/blazer/blazer_generator.rb b/lib/generators/rolemodel/saas/blazer/blazer_generator.rb index d5132900..3f7f982b 100644 --- a/lib/generators/rolemodel/saas/blazer/blazer_generator.rb +++ b/lib/generators/rolemodel/saas/blazer/blazer_generator.rb @@ -27,7 +27,7 @@ def add_routes route_info += " resources :dashboards, only: %i[index show]\n" route_info += " resources :queries, only: [] do\n" route_info += " post :run, on: :collection\n" - route_info += " post :cancel, on: :collection\n" + route_info += " # post :cancel, on: :collection\n" route_info += " end\n" route_info += " end\n" route route_info diff --git a/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/queries_controller.rb b/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/queries_controller.rb index d1673018..76d1f8b0 100644 --- a/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/queries_controller.rb +++ b/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/queries_controller.rb @@ -4,8 +4,9 @@ module Reports # Since this doesn't rely on our layout, we can get away with inheriting straight from blazer, # and mixing in our authorization class QueriesController < Blazer::QueriesController - # TODO: must be reenabled because Blazer disables all inherited before/around/after callbacks - # after_action :verify_authorized + # !!! Blazer disables all inherited before/around/after callbacks so add whatever you need here + before_action :authenticate_user! + after_action :verify_authorized def run @query = Blazer::Query.find_by(id: params[:query_id]) From 1a5b84694eb8cbbc8416197db26cc46b5befa774 Mon Sep 17 00:00:00 2001 From: Tim Irwin Date: Thu, 2 Feb 2023 16:47:27 -0500 Subject: [PATCH 09/10] Remove project specific styling, fix Turbo issue --- .../templates/app/views/reports/dashboards/index.html.slim | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/index.html.slim b/lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/index.html.slim index 18739afb..f6d33a4e 100644 --- a/lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/index.html.slim +++ b/lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/index.html.slim @@ -2,16 +2,16 @@ header.header--index-action h1= t('.reports', default: 'Reports') .table__wrapper - - # Since blazer loads its own JS, we can't rely on turbolinks to work + - # Since blazer loads its own JS, we can't rely on turbo to work table.table thead tr th= t('activerecord.attributes.blazer_dashboard.name', default: 'Name') - th width="85px"= t('.actions', default: 'Actions') + th= t('.actions', default: 'Actions') tbody - @dashboards.each do |dashboard| tr data-test-id="#{dashboard.id}" td= dashboard.name td.table__actions - = link_to t('.show', default: 'Show'), reports_dashboard_path(dashboard) + = link_to t('.show', default: 'Show'), reports_dashboard_path(dashboard), data: { turbo: false } From 1fdcd2ec070c3cb44486265d6cc654ba3063f4d9 Mon Sep 17 00:00:00 2001 From: Tim Irwin Date: Wed, 22 Feb 2023 10:10:00 -0600 Subject: [PATCH 10/10] Move Blazer generator to rolemodel:blazer --- lib/generators/rolemodel/README.md | 1 + lib/generators/rolemodel/all_generator.rb | 1 + .../rolemodel/{saas => }/blazer/README.md | 2 +- .../rolemodel/{saas => }/blazer/USAGE | 2 +- .../rolemodel/blazer/blazer_generator.rb | 69 ++++++++++++++++++ .../app/assets/stylesheets/blazer.css | 0 .../app/assets/stylesheets/selectize.css | 0 .../reports/dashboards_controller.rb | 0 .../controllers/reports/queries_controller.rb | 0 .../views/reports/dashboards/index.html.slim | 0 .../views/reports/dashboards/show.html.slim | 0 .../templates/config/initializers/blazer.rb | 0 .../lib/blazer_extensions/data_source.rb | 0 .../blazer/templates/lib/tasks/reports.rake | 0 .../spec/factories/blazer_dashboards.rb | 0 .../spec/factories/blazer_queries.rb | 0 .../templates/spec/system/reporting_spec.rb | 0 .../rolemodel/saas/blazer/blazer_generator.rb | 71 ------------------- 18 files changed, 73 insertions(+), 73 deletions(-) rename lib/generators/rolemodel/{saas => }/blazer/README.md (92%) rename lib/generators/rolemodel/{saas => }/blazer/USAGE (58%) create mode 100644 lib/generators/rolemodel/blazer/blazer_generator.rb rename lib/generators/rolemodel/{saas => }/blazer/templates/app/assets/stylesheets/blazer.css (100%) rename lib/generators/rolemodel/{saas => }/blazer/templates/app/assets/stylesheets/selectize.css (100%) rename lib/generators/rolemodel/{saas => }/blazer/templates/app/controllers/reports/dashboards_controller.rb (100%) rename lib/generators/rolemodel/{saas => }/blazer/templates/app/controllers/reports/queries_controller.rb (100%) rename lib/generators/rolemodel/{saas => }/blazer/templates/app/views/reports/dashboards/index.html.slim (100%) rename lib/generators/rolemodel/{saas => }/blazer/templates/app/views/reports/dashboards/show.html.slim (100%) rename lib/generators/rolemodel/{saas => }/blazer/templates/config/initializers/blazer.rb (100%) rename lib/generators/rolemodel/{saas => }/blazer/templates/lib/blazer_extensions/data_source.rb (100%) rename lib/generators/rolemodel/{saas => }/blazer/templates/lib/tasks/reports.rake (100%) rename lib/generators/rolemodel/{saas => }/blazer/templates/spec/factories/blazer_dashboards.rb (100%) rename lib/generators/rolemodel/{saas => }/blazer/templates/spec/factories/blazer_queries.rb (100%) rename lib/generators/rolemodel/{saas => }/blazer/templates/spec/system/reporting_spec.rb (100%) delete mode 100644 lib/generators/rolemodel/saas/blazer/blazer_generator.rb diff --git a/lib/generators/rolemodel/README.md b/lib/generators/rolemodel/README.md index 3b221cbf..671b3875 100644 --- a/lib/generators/rolemodel/README.md +++ b/lib/generators/rolemodel/README.md @@ -4,6 +4,7 @@ ## What you get +* [Blazer](./blazer) * [CSS](./css) * [Github](./github) * [Heroku](./heroku) diff --git a/lib/generators/rolemodel/all_generator.rb b/lib/generators/rolemodel/all_generator.rb index d57ecf71..9b4fbaf9 100644 --- a/lib/generators/rolemodel/all_generator.rb +++ b/lib/generators/rolemodel/all_generator.rb @@ -13,6 +13,7 @@ def run_all_the_generators generate 'rolemodel:simple_form' generate 'rolemodel:soft_destroyable' generate 'rolemodel:saas:all' + generate 'rolemodel:blazer' generate 'rolemodel:editors' generate 'rolemodel:linters:all' generate 'rolemodel:mailers' diff --git a/lib/generators/rolemodel/saas/blazer/README.md b/lib/generators/rolemodel/blazer/README.md similarity index 92% rename from lib/generators/rolemodel/saas/blazer/README.md rename to lib/generators/rolemodel/blazer/README.md index 660c2d57..9beda368 100644 --- a/lib/generators/rolemodel/saas/blazer/README.md +++ b/lib/generators/rolemodel/blazer/README.md @@ -1,7 +1,7 @@ # Blazer Generator Depends on the blazer generator -Depends on [RSpec Generator](../../testing/rspec) +Depends on [RSpec Generator](../testing/rspec) Depends on slim Works best with Optics * If not using with Optics, update blazer.css to use your own tokens. diff --git a/lib/generators/rolemodel/saas/blazer/USAGE b/lib/generators/rolemodel/blazer/USAGE similarity index 58% rename from lib/generators/rolemodel/saas/blazer/USAGE rename to lib/generators/rolemodel/blazer/USAGE index 5e9fe449..af9a0656 100644 --- a/lib/generators/rolemodel/saas/blazer/USAGE +++ b/lib/generators/rolemodel/blazer/USAGE @@ -2,4 +2,4 @@ Description: installs and configures Blazer Example: - rails generate rolemodel:saas:blazer + rails generate rolemodel:blazer diff --git a/lib/generators/rolemodel/blazer/blazer_generator.rb b/lib/generators/rolemodel/blazer/blazer_generator.rb new file mode 100644 index 00000000..40304a30 --- /dev/null +++ b/lib/generators/rolemodel/blazer/blazer_generator.rb @@ -0,0 +1,69 @@ +require_relative '../../../bundler_helpers' + +module Rolemodel + class BlazerGenerator < Rails::Generators::Base + include Rolemodel::BundlerHelpers + source_root File.expand_path('templates', __dir__) + + def install_blazer + gem 'sprockets-rails' unless File.readlines('Gemfile').grep(/sprockets/).any? + gem 'blazer' + run_bundle + + generate 'blazer:install' + end + + def update_migration + filename = Dir.glob('db/migrate/*_install_blazer.rb').first + inject_into_file filename, ', foreign_key: { to_table: :users }', after: 't.references :creator' + end + + def add_routes + return if File.readlines('config/routes.rb').grep(/blazer/).any? + + # add routes for the report controllers + route_info = " namespace :reports do\n" + route_info += " resources :dashboards, only: %i[index show]\n" + route_info += " resources :queries, only: [] do\n" + route_info += " post :run, on: :collection\n" + route_info += " # post :cancel, on: :collection\n" + route_info += " end\n" + route_info += " end\n" + route route_info + + # add routes for the Blazer engine + route_info = " # authenticate :user, ->(u) { Admin::ReportPolicy.new(u, :report).manage? } do\n" + route_info += " mount Blazer::Engine => '/admin/reports', as: :blazer\n" + route_info += " # end\n" + route route_info + end + + def add_extensions + copy_file 'config/initializers/blazer.rb' + copy_file 'lib/blazer_extensions/data_source.rb' + end + + def add_controllers + directory 'app/controllers/reports' + end + + def add_views + directory 'app/views/reports' + end + + def add_styles + copy_file 'app/assets/stylesheets/blazer.css' + copy_file 'app/assets/stylesheets/selectize.css' + append_to_file 'app/assets/config/manifest.js', "\n//= link blazer.css" + end + + def add_tests + copy_file 'spec/factories/blazer_queries.rb' + copy_file 'spec/system/reporting_spec.rb' + end + + def add_rake_task + copy_file 'lib/tasks/reports.rake' + end + end +end diff --git a/lib/generators/rolemodel/saas/blazer/templates/app/assets/stylesheets/blazer.css b/lib/generators/rolemodel/blazer/templates/app/assets/stylesheets/blazer.css similarity index 100% rename from lib/generators/rolemodel/saas/blazer/templates/app/assets/stylesheets/blazer.css rename to lib/generators/rolemodel/blazer/templates/app/assets/stylesheets/blazer.css diff --git a/lib/generators/rolemodel/saas/blazer/templates/app/assets/stylesheets/selectize.css b/lib/generators/rolemodel/blazer/templates/app/assets/stylesheets/selectize.css similarity index 100% rename from lib/generators/rolemodel/saas/blazer/templates/app/assets/stylesheets/selectize.css rename to lib/generators/rolemodel/blazer/templates/app/assets/stylesheets/selectize.css diff --git a/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/dashboards_controller.rb b/lib/generators/rolemodel/blazer/templates/app/controllers/reports/dashboards_controller.rb similarity index 100% rename from lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/dashboards_controller.rb rename to lib/generators/rolemodel/blazer/templates/app/controllers/reports/dashboards_controller.rb diff --git a/lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/queries_controller.rb b/lib/generators/rolemodel/blazer/templates/app/controllers/reports/queries_controller.rb similarity index 100% rename from lib/generators/rolemodel/saas/blazer/templates/app/controllers/reports/queries_controller.rb rename to lib/generators/rolemodel/blazer/templates/app/controllers/reports/queries_controller.rb diff --git a/lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/index.html.slim b/lib/generators/rolemodel/blazer/templates/app/views/reports/dashboards/index.html.slim similarity index 100% rename from lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/index.html.slim rename to lib/generators/rolemodel/blazer/templates/app/views/reports/dashboards/index.html.slim diff --git a/lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/show.html.slim b/lib/generators/rolemodel/blazer/templates/app/views/reports/dashboards/show.html.slim similarity index 100% rename from lib/generators/rolemodel/saas/blazer/templates/app/views/reports/dashboards/show.html.slim rename to lib/generators/rolemodel/blazer/templates/app/views/reports/dashboards/show.html.slim diff --git a/lib/generators/rolemodel/saas/blazer/templates/config/initializers/blazer.rb b/lib/generators/rolemodel/blazer/templates/config/initializers/blazer.rb similarity index 100% rename from lib/generators/rolemodel/saas/blazer/templates/config/initializers/blazer.rb rename to lib/generators/rolemodel/blazer/templates/config/initializers/blazer.rb diff --git a/lib/generators/rolemodel/saas/blazer/templates/lib/blazer_extensions/data_source.rb b/lib/generators/rolemodel/blazer/templates/lib/blazer_extensions/data_source.rb similarity index 100% rename from lib/generators/rolemodel/saas/blazer/templates/lib/blazer_extensions/data_source.rb rename to lib/generators/rolemodel/blazer/templates/lib/blazer_extensions/data_source.rb diff --git a/lib/generators/rolemodel/saas/blazer/templates/lib/tasks/reports.rake b/lib/generators/rolemodel/blazer/templates/lib/tasks/reports.rake similarity index 100% rename from lib/generators/rolemodel/saas/blazer/templates/lib/tasks/reports.rake rename to lib/generators/rolemodel/blazer/templates/lib/tasks/reports.rake diff --git a/lib/generators/rolemodel/saas/blazer/templates/spec/factories/blazer_dashboards.rb b/lib/generators/rolemodel/blazer/templates/spec/factories/blazer_dashboards.rb similarity index 100% rename from lib/generators/rolemodel/saas/blazer/templates/spec/factories/blazer_dashboards.rb rename to lib/generators/rolemodel/blazer/templates/spec/factories/blazer_dashboards.rb diff --git a/lib/generators/rolemodel/saas/blazer/templates/spec/factories/blazer_queries.rb b/lib/generators/rolemodel/blazer/templates/spec/factories/blazer_queries.rb similarity index 100% rename from lib/generators/rolemodel/saas/blazer/templates/spec/factories/blazer_queries.rb rename to lib/generators/rolemodel/blazer/templates/spec/factories/blazer_queries.rb diff --git a/lib/generators/rolemodel/saas/blazer/templates/spec/system/reporting_spec.rb b/lib/generators/rolemodel/blazer/templates/spec/system/reporting_spec.rb similarity index 100% rename from lib/generators/rolemodel/saas/blazer/templates/spec/system/reporting_spec.rb rename to lib/generators/rolemodel/blazer/templates/spec/system/reporting_spec.rb diff --git a/lib/generators/rolemodel/saas/blazer/blazer_generator.rb b/lib/generators/rolemodel/saas/blazer/blazer_generator.rb deleted file mode 100644 index 3f7f982b..00000000 --- a/lib/generators/rolemodel/saas/blazer/blazer_generator.rb +++ /dev/null @@ -1,71 +0,0 @@ -require_relative '../../../bundler_helpers' - -module Rolemodel - module Saas - class BlazerGenerator < Rails::Generators::Base - include Rolemodel::BundlerHelpers - source_root File.expand_path('templates', __dir__) - - def install_blazer - gem 'sprockets-rails' unless File.readlines('Gemfile').grep(/sprockets/).any? - gem 'blazer' - run_bundle - - generate 'blazer:install' - end - - def update_migration - filename = Dir.glob('db/migrate/*_install_blazer.rb').first - inject_into_file filename, ', foreign_key: { to_table: :users }', after: 't.references :creator' - end - - def add_routes - return if File.readlines('config/routes.rb').grep(/blazer/).any? - - # add routes for the report controllers - route_info = " namespace :reports do\n" - route_info += " resources :dashboards, only: %i[index show]\n" - route_info += " resources :queries, only: [] do\n" - route_info += " post :run, on: :collection\n" - route_info += " # post :cancel, on: :collection\n" - route_info += " end\n" - route_info += " end\n" - route route_info - - # add routes for the Blazer engine - route_info = " # authenticate :user, ->(u) { Admin::ReportPolicy.new(u, :report).manage? } do\n" - route_info += " mount Blazer::Engine => '/admin/reports', as: :blazer\n" - route_info += " # end\n" - route route_info - end - - def add_extensions - copy_file 'config/initializers/blazer.rb' - copy_file 'lib/blazer_extensions/data_source.rb' - end - - def add_controllers - directory 'app/controllers/reports' - end - - def add_views - directory 'app/views/reports' - end - - def add_styles - copy_file 'app/assets/stylesheets/blazer.css' - copy_file 'app/assets/stylesheets/selectize.css' - append_to_file 'app/assets/config/manifest.js', "\n//= link blazer.css" - end - - def add_tests - copy_file 'spec/factories/blazer_queries.rb' - copy_file 'spec/system/reporting_spec.rb' - end - - def add_rake_task - copy_file 'lib/tasks/reports.rake' - end - end - end -end