diff --git a/.github/workflows/brakeman.yml b/.github/workflows/brakeman.yml index e94cc3c..88b43bf 100644 --- a/.github/workflows/brakeman.yml +++ b/.github/workflows/brakeman.yml @@ -11,7 +11,7 @@ runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 # Will run Brakeman checks on dependencies # https://github.com/marketplace/actions/brakeman-linter diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 9a087ce..8f043ff 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -8,7 +8,7 @@ runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 # Will run ES Lint checks on javascript files # https://github.com/marketplace/actions/run-eslint diff --git a/.github/workflows/mysql.yml b/.github/workflows/mysql.yml index 3bb34d7..dbacba0 100644 --- a/.github/workflows/mysql.yml +++ b/.github/workflows/mysql.yml @@ -13,7 +13,7 @@ steps: # Checkout the repo - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 with: fetch-depth: 1 @@ -73,6 +73,16 @@ ${{ runner.os }}-yarn- ${{ runner.os }}- + # Figure out where wkhtmltopdf is installed + - name: 'Determine wkhtmltopdf location' + run: echo ::set-env name=WICKED_PDF_PATH::$(echo `bundle exec which wkhtmltopdf`) + + # Startup MySQL + - name: 'Start MySQL' + run: sudo systemctl start mysql + + + # Install the JS dependencies - name: 'Yarn Install' run: | diff --git a/.github/workflows/postgres.yml b/.github/workflows/postgres.yml index 71f752e..9fa69ff 100644 --- a/.github/workflows/postgres.yml +++ b/.github/workflows/postgres.yml @@ -26,11 +26,10 @@ env: RAILS_ENV: test DATABASE_URL: postgres://postgres:@localhost:5432/roadmap_test - WICKED_PDF_PATH: vendor/bundle/bin/wkhtmltopdf steps: # Checkout the repo - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 with: fetch-depth: 1 @@ -44,6 +43,7 @@ - name: 'Determine Ruby Version' run: echo ::set-env name=RUBY_VERSION::$(echo `cat ./Gemfile.lock | grep -A 1 'RUBY VERSION' | grep 'ruby' | grep -oE '[0-9]\.[0-9]'`) + # Install Ruby - using the version found in the Gemfile.lock - name: 'Install Ruby' uses: actions/setup-ruby@v1 diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml index 1bb1d76..1d3d2a7 100644 --- a/.github/workflows/rubocop.yml +++ b/.github/workflows/rubocop.yml @@ -10,7 +10,7 @@ runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 # Extract the Ruby version from the Gemfile.lock # - name: 'Determine Ruby Version' @@ -31,4 +31,4 @@ # additional-gems: 'rubocop-dmp_roadmap' - name: 'Placeholder for Rubocop' - run: echo "Rubocop has been temporarily disabled" \ No newline at end of file + run: echo "Rubocop has been temporarily disabled" diff --git a/Gemfile b/Gemfile index 730a998..9d269a2 100644 --- a/Gemfile +++ b/Gemfile @@ -156,9 +156,6 @@ # This simple gem allows you to create MS Word docx documents from simple html documents. This makes it easy to create dynamic reports and forms that can be downloaded by your users as simple MS Word docx files. (http://github.com/karnov/htmltoword) gem 'htmltoword', '1.1.0' -# A feed fetching and parsing library (http://feedjira.com) -gem 'feedjira' - # Filename sanitization for Ruby. This is useful when you generate filenames for downloads from user input gem 'zaru' @@ -188,6 +185,9 @@ gem 'activerecord-session_store' +# ------------------------------------------------- +# UTILITIES +gem 'parallel' # ------------------------------------------------ # ENVIRONMENT SPECIFIC DEPENDENCIES diff --git a/Gemfile.lock b/Gemfile.lock index ac8af96..8528d85 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -133,9 +133,6 @@ faraday (0.17.1) multipart-post (>= 1.2, < 3) fast_gettext (2.0.1) - feedjira (3.1.0) - loofah (>= 2.3.1) - sax-machine (>= 1.0) ffi (1.11.3) flag_shih_tzu (0.3.23) fog-aws (3.5.2) @@ -245,7 +242,7 @@ mysql2 (0.4.10) nenv (0.3.0) nio4r (2.5.2) - nokogiri (1.10.7) + nokogiri (1.10.8) mini_portile2 (~> 2.4.0) notiffany (0.1.3) nenv (~> 0.1) @@ -404,7 +401,6 @@ sprockets (> 3.0) sprockets-rails tilt - sax-machine (1.3.2) selenium-webdriver (3.142.6) childprocess (>= 0.5, < 4.0) rubyzip (>= 1.2.2) @@ -498,7 +494,6 @@ dragonfly-s3_data_store factory_bot_rails faker - feedjira flag_shih_tzu (~> 0.3.23) font-awesome-sass (~> 4.2.0) fuubar @@ -518,6 +513,7 @@ omniauth-orcid omniauth-rails_csrf_protection omniauth-shibboleth + parallel pg (~> 0.19.0) progress_bar puma diff --git a/app/assets/stylesheets/blocks/_new_window_popup.scss b/app/assets/stylesheets/blocks/_new_window_popup.scss index a1b764b..a499e26 100644 --- a/app/assets/stylesheets/blocks/_new_window_popup.scss +++ b/app/assets/stylesheets/blocks/_new_window_popup.scss @@ -1,9 +1,9 @@ a.has-new-window-popup-info, button.has-new-window-popup-info { - + position:relative; z-index:24; - + & > span.new-window-popup-info { position: absolute; left: -9000px; @@ -14,9 +14,9 @@ &:hover, &:focus, &:active { - + z-index:25; - + & > span.new-window-popup-info { display:block; position:absolute; @@ -29,7 +29,7 @@ color:#000; text-align: center; } - + } - + } diff --git a/app/assets/stylesheets/blocks/_readonly_textarea.scss b/app/assets/stylesheets/blocks/_readonly_textarea.scss index ec288cf..4ee99c6 100644 --- a/app/assets/stylesheets/blocks/_readonly_textarea.scss +++ b/app/assets/stylesheets/blocks/_readonly_textarea.scss @@ -1,17 +1,17 @@ /* For display of readonly textarea content without the TinyMCE editor */ .display-readonly-textarea-content { // Replicating some TinyMCE styling of textarea - overflow-y: hidden; + overflow: visible; padding-left: 1px; padding-right: 1px; padding-bottom: 10px; - + // Ensure table borders are not lost table { td { border: 1px solid black; } - + td, tr { padding: 10px; } diff --git a/app/assets/stylesheets/blocks/_usage.sccs b/app/assets/stylesheets/blocks/_usage.sccs new file mode 100644 index 0000000..1eac12f --- /dev/null +++ b/app/assets/stylesheets/blocks/_usage.sccs @@ -0,0 +1,4 @@ +.single-char-input { + display: inline; + width: 20px; +} diff --git a/app/assets/stylesheets/utils/_colours.scss b/app/assets/stylesheets/utils/_colours.scss index 5616249..eb7e455 100644 --- a/app/assets/stylesheets/utils/_colours.scss +++ b/app/assets/stylesheets/utils/_colours.scss @@ -1,3 +1,8 @@ .red { color: $color-text-red; } + +//default colour used on headings +.color-heading-text{ + color: $color-heading-text; +} diff --git a/app/assets/stylesheets/utils/_font_size.scss b/app/assets/stylesheets/utils/_font_size.scss new file mode 100644 index 0000000..18ea8b9 --- /dev/null +++ b/app/assets/stylesheets/utils/_font_size.scss @@ -0,0 +1,18 @@ + +//similar font size as an h2 tag +.fontsize-h2{ + font-size: 24 px; + +} + +//similar font size as an h3 tag +.fontsize-h3{ + font-size: 18.72 px; + +} + +//similar font size as an h4 tag +.fontsize-h4{ + font-size: 16 px; + +} diff --git a/app/assets/stylesheets/utils/_margins.scss b/app/assets/stylesheets/utils/_margins.scss index 7b0f764..1461cc8 100644 --- a/app/assets/stylesheets/utils/_margins.scss +++ b/app/assets/stylesheets/utils/_margins.scss @@ -4,6 +4,12 @@ .mb-10 { margin-bottom: 10px; } +.mt-5 { + margin-top: 5px; +} .mt-10 { margin-top: 10px; } +.mt-20 { + margin-top: 20px; +} diff --git a/app/controllers/static_pages_controller.rb b/app/controllers/static_pages_controller.rb index 3bd34cf..8a3a138 100644 --- a/app/controllers/static_pages_controller.rb +++ b/app/controllers/static_pages_controller.rb @@ -3,12 +3,6 @@ class StaticPagesController < ApplicationController def about_us - dcc_news_feed_url = "http://www.dcc.ac.uk/news/dmponline-0/feed" - @dcc_news_feed = Feedjira::Feed.fetch_and_parse dcc_news_feed_url - respond_to do |format| - format.rss { redirect_to dcc_news_feed_url } - format.html - end end def contact_us diff --git a/app/controllers/usage_controller.rb b/app/controllers/usage_controller.rb index 3f09f73..005b1f3 100644 --- a/app/controllers/usage_controller.rb +++ b/app/controllers/usage_controller.rb @@ -13,6 +13,9 @@ plan_data(args: args, as_json: true) total_plans(args: min_max_dates(args: args)) total_users(args: min_max_dates(args: args)) + #TODO: pull this in from branding.yml + @separators = [",", "|", "#"] + @funder = current_user.org.funder? end # POST /usage_plans_by_template @@ -35,7 +38,19 @@ authorize :usage data = Org::TotalCountStatService.call - data_csvified = Csvable.from_array_of_hashes(data) + sep = sep_param + data_csvified = Csvable.from_array_of_hashes(data, true, sep) + + send_data(data_csvified, filename: "totals.csv") + end + + # GET + def org_statistics + authorize :usage + + data = Org::MonthlyUsageService.call + sep = sep_param + data_csvified = Csvable.from_array_of_hashes(data, true, sep) send_data(data_csvified, filename: "totals.csv") end @@ -71,7 +86,8 @@ authorize :usage user_data(args: default_query_args) - send_data(CSV.generate do |csv| + sep = sep_param + send_data(CSV.generate({:col_sep => sep}) do |csv| csv << [_("Month"), _("No. Users joined")] total = 0 @users_per_month.each do |data| @@ -89,7 +105,8 @@ authorize :usage plan_data(args: default_query_args) - send_data(CSV.generate do |csv| + sep = sep_param + send_data(CSV.generate({:col_sep => sep}) do |csv| csv << [_("Month"), _("No. Completed Plans")] total = 0 @plans_per_month.each do |data| @@ -108,9 +125,11 @@ args = default_query_args args[:start_date] = first_plan_date + sep = sep_param + {:col_sep => sep} plan_data(args: args, sort: :desc) - data_csvified = StatCreatedPlan.to_csv(@plans_per_month, details: { by_template: true }) + data_csvified = StatCreatedPlan.to_csv(@plans_per_month, details: { by_template: true, sep: sep }) send_data(data_csvified, filename: "created_plan_by_template.csv") end @@ -153,6 +172,11 @@ } end + # set the csv separator or default to comma + def sep_param + params["sep"] || ',' + end + def min_max_dates(args:) args[:start_date] = first_plan_date.strftime("%Y-%m-%d") args[:end_date] = Date.today.strftime("%Y-%m-%d") @@ -181,7 +205,8 @@ end def first_plan_date - StatCreatedPlan.all.order(:date).limit(1).pluck(:date).first + StatCreatedPlan.all.order(:date).limit(1).pluck(:date).first \ + || Date.today.last_month.end_of_month end end diff --git a/app/helpers/usage_helper.rb b/app/helpers/usage_helper.rb index b38edc6..806c609 100644 --- a/app/helpers/usage_helper.rb +++ b/app/helpers/usage_helper.rb @@ -15,7 +15,7 @@ # appropriately by passing along the labels for the Y axis and the datasets # for the X axis # rubocop:disable Metrics/AbcSize, Metrics/MethodLength - def prep_data_for_template_plans_chart(data:) + def prep_data_for_template_plans_chart(data:, subset: "by_template") last_month = Date.today.last_month.end_of_month.strftime('%b-%y') return { labels: [last_month], datasets: [] }.to_json if data.blank? || data.empty? @@ -29,7 +29,7 @@ # Loop through the data and organize the datasets by template instead of date data.each do |rec| date = prep_date_for_charts(date: rec["date"]) - rec["by_template"].each do |template| + rec[subset].each do |template| # We need a placeholder for each month/year - template combo. The # default is to assume that there are zero plans for that month/year + template dflt = { diff --git a/app/javascript/utils/tinymce.js b/app/javascript/utils/tinymce.js deleted file mode 100644 index ad13f14..0000000 --- a/app/javascript/utils/tinymce.js +++ /dev/null @@ -1,146 +0,0 @@ -// Import TinyMCE -import tinymce from 'tinymce/tinymce'; -// Import TinyMCE theme -import 'tinymce/themes/modern/theme'; -// Plugins -import 'tinymce/plugins/table'; -import 'tinymce/plugins/lists'; -import 'tinymce/plugins/autoresize'; -import 'tinymce/plugins/link'; -import 'tinymce/plugins/paste'; -import 'tinymce/plugins/advlist'; -// Other dependencies -import { isObject, isString } from './isType'; - -// // Configuration extracted from -// // https://www.tinymce.com/docs/advanced/usage-with-module-loaders/ -export const defaultOptions = { - selector: '.tinymce', - statusbar: true, - menubar: false, - toolbar: 'bold italic | bullist numlist | link | table', - plugins: 'table autoresize link paste advlist lists', - browser_spellcheck: true, - advlist_bullet_styles: 'circle,disc,square', // Only disc bullets display on htmltoword - target_list: false, - elementpath: false, - resize: true, - autoresize_min_height: 130, - autoresize_bottom_margin: 10, - branding: false, - extended_valid_elements: 'iframe[tooltip] , a[href|target=_blank]', - paste_auto_cleanup_on_paste: true, - paste_remove_styles: true, - paste_retain_style_properties: 'none', - paste_convert_middot_lists: true, - paste_remove_styles_if_webkit: true, - paste_remove_spans: true, - paste_strip_class_attributes: 'all', - table_default_attributes: { - border: 1, - }, - // editorManager.baseURL is not resolved properly for IE since document.currentScript - // is not supported, see issue https://github.com/tinymce/tinymce/issues/358 - skin_url: '/tinymce/skins/lightgray', - content_css: ['/assets/blocks/_tinymce_content.css'], -}; -/* - This function is invoked anytime a new editor is initialised (e.g. Tinymce.init()) - and shrinks a tinymce editor to the minimum height specified at autoresize_min_height - editor's settings. Since there are cases that tinymce editor is loaded in the DOM - but has display:none style, the iframe associated gets the height of the screen's device - and using this function there is no need to wait until the tinymce gains focus to be autoresized. -*/ -const resizeEditors = (editors) => { - editors.forEach((editor) => { - $(editor.iframeElement).height(editor.settings.autoresize_min_height); - }); -}; - -/* - This function is invoked after the Tinymce widget is initialized. It moves the - connection with the label from the hidden field (that the Tinymce writes to - behind the scenes) to the Tinymce iframe so that screen readers read the correct - label when the tinymce iframe receives focus. - */ -const attachLabelToIframe = (tinymceContext) => { - const iframe = $(tinymceContext).siblings('.mce-container').find('iframe'); - if (isObject(iframe)) { - const lbl = iframe.closest('form').find('label'); - if (isObject(lbl)) { - // Connect the label to the iframe - lbl.attr('for', iframe.attr('id')); - } - } -}; - -export const Tinymce = { - /* - Initialises a tinymce editor given the object passed. If a non-valid object is passed, - the defaultOptions object is used instead - @param options - An object with tinyMCE properties - */ - init(options = {}) { - if (isObject(options)) { - tinymce.init($.extend(true, defaultOptions, options)).then(resizeEditors); - } else { - tinymce.init(defaultOptions).then(resizeEditors); - } - - // Connect the label to the Tinymce iframe - $(options.selector).each((idx, el) => { - attachLabelToIframe(el); - }); - }, - /* - Finds any tinyMCE editor whose target element/textarea has the className passed - @param className - A string representing the class name of the tinyMCE editor - target element/textarea to look for - @return An Array of tinymce.Editor objects - */ - findEditorsByClassName(className) { - if (isString(className)) { - return tinymce.editors.reduce((acc, e) => { - if ($(e.getElement()).hasClass(className)) { - return acc.concat([e]); - } - return acc; - }, []); - } - return []; - }, - /* - Finds a tinyMCE editor whose target element/textarea has the id passed - @param id - A string representing the id of the tinyMCE editor target - element/textarea to look for - @return tinymce.Editor object, otherwise undefined - */ - findEditorById(id) { - if (isString(id)) { - return tinymce.editors.find(el => el.id === id); - } - return undefined; - }, - /* - Destroy every editor instance whose target element/textarea has the className passed. This - method executes for each editor the method defined at tinymce.Editor.destroy (e.g. https://www.tinymce.com/docs/api/tinymce/tinymce.editor/#destroy). - @param className - A string representing the class name of the tinyMCE editor - target element/textarea to look for - @return undefined - */ - destroyEditorsByClassName(className) { - const editors = this.findEditorsByClassName(className); - editors.forEach(ed => ed.destroy(false)); - }, - /* - Destroy an editor instance whose target element/textarea has HTML id passed. This method - executes tinymce.Editor.destroy (e.g. https://www.tinymce.com/docs/api/tinymce/tinymce.editor/#destroy) for a successfull id found. - @return undefined - */ - destroyEditorById(id) { - const editor = this.findEditorById(id); - if (editor) { - editor.destroy(false); - } - }, -}; diff --git a/app/javascript/utils/tinymce.js.erb b/app/javascript/utils/tinymce.js.erb new file mode 100644 index 0000000..76b663e --- /dev/null +++ b/app/javascript/utils/tinymce.js.erb @@ -0,0 +1,148 @@ +// Import TinyMCE +import tinymce from 'tinymce/tinymce'; +// Import TinyMCE theme +import 'tinymce/themes/modern/theme'; +// Plugins +import 'tinymce/plugins/table'; +import 'tinymce/plugins/lists'; +import 'tinymce/plugins/autoresize'; +import 'tinymce/plugins/link'; +import 'tinymce/plugins/paste'; +import 'tinymce/plugins/advlist'; +// Other dependencies +import { isObject, isString } from './isType'; +// Pull in the rails helper functions +<% helpers = ActionController::Base.helpers %> + +// // Configuration extracted from +// // https://www.tinymce.com/docs/advanced/usage-with-module-loaders/ +export const defaultOptions = { + selector: '.tinymce', + statusbar: true, + menubar: false, + toolbar: 'bold italic | bullist numlist | link | table', + plugins: 'table autoresize link paste advlist lists', + browser_spellcheck: true, + advlist_bullet_styles: 'circle,disc,square', // Only disc bullets display on htmltoword + target_list: false, + elementpath: false, + resize: true, + autoresize_min_height: 130, + autoresize_bottom_margin: 10, + branding: false, + extended_valid_elements: 'iframe[tooltip] , a[href|target=_blank]', + paste_auto_cleanup_on_paste: true, + paste_remove_styles: true, + paste_retain_style_properties: 'none', + paste_convert_middot_lists: true, + paste_remove_styles_if_webkit: true, + paste_remove_spans: true, + paste_strip_class_attributes: 'all', + table_default_attributes: { + border: 1, + }, + // editorManager.baseURL is not resolved properly for IE since document.currentScript + // is not supported, see issue https://github.com/tinymce/tinymce/issues/358 + skin_url: '/tinymce/skins/lightgray', + content_css: ['<%= helpers.asset_path "/assets/blocks/_tinymce_content.css" %>'], +}; +/* + This function is invoked anytime a new editor is initialised (e.g. Tinymce.init()) + and shrinks a tinymce editor to the minimum height specified at autoresize_min_height + editor's settings. Since there are cases that tinymce editor is loaded in the DOM + but has display:none style, the iframe associated gets the height of the screen's device + and using this function there is no need to wait until the tinymce gains focus to be autoresized. +*/ +const resizeEditors = (editors) => { + editors.forEach((editor) => { + $(editor.iframeElement).height(editor.settings.autoresize_min_height); + }); +}; + +/* + This function is invoked after the Tinymce widget is initialized. It moves the + connection with the label from the hidden field (that the Tinymce writes to + behind the scenes) to the Tinymce iframe so that screen readers read the correct + label when the tinymce iframe receives focus. + */ +const attachLabelToIframe = (tinymceContext) => { + const iframe = $(tinymceContext).siblings('.mce-container').find('iframe'); + if (isObject(iframe)) { + const lbl = iframe.closest('form').find('label'); + if (isObject(lbl)) { + // Connect the label to the iframe + lbl.attr('for', iframe.attr('id')); + } + } +}; + +export const Tinymce = { + /* + Initialises a tinymce editor given the object passed. If a non-valid object is passed, + the defaultOptions object is used instead + @param options - An object with tinyMCE properties + */ + init(options = {}) { + if (isObject(options)) { + tinymce.init($.extend(true, defaultOptions, options)).then(resizeEditors); + } else { + tinymce.init(defaultOptions).then(resizeEditors); + } + + // Connect the label to the Tinymce iframe + $(options.selector).each((idx, el) => { + attachLabelToIframe(el); + }); + }, + /* + Finds any tinyMCE editor whose target element/textarea has the className passed + @param className - A string representing the class name of the tinyMCE editor + target element/textarea to look for + @return An Array of tinymce.Editor objects + */ + findEditorsByClassName(className) { + if (isString(className)) { + return tinymce.editors.reduce((acc, e) => { + if ($(e.getElement()).hasClass(className)) { + return acc.concat([e]); + } + return acc; + }, []); + } + return []; + }, + /* + Finds a tinyMCE editor whose target element/textarea has the id passed + @param id - A string representing the id of the tinyMCE editor target + element/textarea to look for + @return tinymce.Editor object, otherwise undefined + */ + findEditorById(id) { + if (isString(id)) { + return tinymce.editors.find(el => el.id === id); + } + return undefined; + }, + /* + Destroy every editor instance whose target element/textarea has the className passed. This + method executes for each editor the method defined at tinymce.Editor.destroy (e.g. https://www.tinymce.com/docs/api/tinymce/tinymce.editor/#destroy). + @param className - A string representing the class name of the tinyMCE editor + target element/textarea to look for + @return undefined + */ + destroyEditorsByClassName(className) { + const editors = this.findEditorsByClassName(className); + editors.forEach(ed => ed.destroy(false)); + }, + /* + Destroy an editor instance whose target element/textarea has HTML id passed. This method + executes tinymce.Editor.destroy (e.g. https://www.tinymce.com/docs/api/tinymce/tinymce.editor/#destroy) for a successfull id found. + @return undefined + */ + destroyEditorById(id) { + const editor = this.findEditorById(id); + if (editor) { + editor.destroy(false); + } + }, +}; diff --git a/app/javascript/views/answers/edit.js b/app/javascript/views/answers/edit.js index cc1ddbd..f3ee429 100644 --- a/app/javascript/views/answers/edit.js +++ b/app/javascript/views/answers/edit.js @@ -3,7 +3,7 @@ isNumber, isString, } from '../../utils/isType'; -import { Tinymce } from '../../utils/tinymce'; +import { Tinymce } from '../../utils/tinymce.js.erb'; import debounce from '../../utils/debounce'; import datePicker from '../../utils/datePicker'; import TimeagoFactory from '../../utils/timeagoFactory'; diff --git a/app/javascript/views/guidances/new_edit.js b/app/javascript/views/guidances/new_edit.js index 857ffa9..24fcc98 100644 --- a/app/javascript/views/guidances/new_edit.js +++ b/app/javascript/views/guidances/new_edit.js @@ -1,4 +1,4 @@ -import { Tinymce } from '../../utils/tinymce'; +import { Tinymce } from '../../utils/tinymce.js.erb'; $(() => { Tinymce.init({ selector: '#guidance-text' }); diff --git a/app/javascript/views/notes/index.js b/app/javascript/views/notes/index.js index 58803e1..64010ae 100644 --- a/app/javascript/views/notes/index.js +++ b/app/javascript/views/notes/index.js @@ -1,4 +1,4 @@ -import { Tinymce } from '../../utils/tinymce'; +import { Tinymce } from '../../utils/tinymce.js.erb'; import { isObject, isString } from '../../utils/isType'; import TimeagoFactory from '../../utils/timeagoFactory'; diff --git a/app/javascript/views/org_admin/phases/new_edit.js b/app/javascript/views/org_admin/phases/new_edit.js index b7439fc..4098896 100644 --- a/app/javascript/views/org_admin/phases/new_edit.js +++ b/app/javascript/views/org_admin/phases/new_edit.js @@ -1,5 +1,5 @@ import 'bootstrap-sass/assets/javascripts/bootstrap/collapse'; -import { Tinymce } from '../../../utils/tinymce'; +import { Tinymce } from '../../../utils/tinymce.js.erb'; import { isObject, isString } from '../../../utils/isType'; import getConstant from '../../../constants'; import expandCollapseAll from '../../../utils/expandCollapseAll'; diff --git a/app/javascript/views/org_admin/templates/edit.js b/app/javascript/views/org_admin/templates/edit.js index aaaf543..30e9e6d 100644 --- a/app/javascript/views/org_admin/templates/edit.js +++ b/app/javascript/views/org_admin/templates/edit.js @@ -1,4 +1,4 @@ -import { Tinymce } from '../../../utils/tinymce'; +import { Tinymce } from '../../../utils/tinymce.js.erb'; import { eachLinks } from '../../../utils/links'; import { isObject, isString } from '../../../utils/isType'; import { renderNotice, renderAlert } from '../../../utils/notificationHelper'; diff --git a/app/javascript/views/org_admin/templates/new.js b/app/javascript/views/org_admin/templates/new.js index 9dd7175..57a4b98 100644 --- a/app/javascript/views/org_admin/templates/new.js +++ b/app/javascript/views/org_admin/templates/new.js @@ -1,4 +1,4 @@ -import { Tinymce } from '../../../utils/tinymce'; +import { Tinymce } from '../../../utils/tinymce.js.erb'; import { eachLinks } from '../../../utils/links'; $(() => { diff --git a/app/javascript/views/orgs/admin_edit.js b/app/javascript/views/orgs/admin_edit.js index d1ec6b1..c184bd1 100644 --- a/app/javascript/views/orgs/admin_edit.js +++ b/app/javascript/views/orgs/admin_edit.js @@ -1,7 +1,7 @@ // TODO: we need to be able to swap in the appropriate locale here import 'number-to-text/converters/en-us'; import { isObject } from '../../utils/isType'; -import { Tinymce } from '../../utils/tinymce'; +import { Tinymce } from '../../utils/tinymce.js.erb'; import { eachLinks } from '../../utils/links'; $(() => { diff --git a/app/javascript/views/plans/edit_details.js b/app/javascript/views/plans/edit_details.js index b7c671f..6d655a4 100644 --- a/app/javascript/views/plans/edit_details.js +++ b/app/javascript/views/plans/edit_details.js @@ -1,4 +1,4 @@ -import { Tinymce } from '../../utils/tinymce'; +import { Tinymce } from '../../utils/tinymce.js.erb'; import getConstant from '../../constants'; import 'bootstrap-3-typeahead'; diff --git a/app/javascript/views/super_admin/notifications/edit.js b/app/javascript/views/super_admin/notifications/edit.js index 3ab39cf..077b8d8 100644 --- a/app/javascript/views/super_admin/notifications/edit.js +++ b/app/javascript/views/super_admin/notifications/edit.js @@ -1,4 +1,4 @@ -import { Tinymce } from '../../../utils/tinymce'; +import { Tinymce } from '../../../utils/tinymce.js.erb'; $(() => { Tinymce.init({ selector: '.notification-text', forced_root_block: '' }); diff --git a/app/javascript/views/super_admin/themes/new_edit.js b/app/javascript/views/super_admin/themes/new_edit.js index e03915a..c670dbc 100644 --- a/app/javascript/views/super_admin/themes/new_edit.js +++ b/app/javascript/views/super_admin/themes/new_edit.js @@ -1,4 +1,4 @@ -import { Tinymce } from '../../../utils/tinymce'; +import { Tinymce } from '../../../utils/tinymce.js.erb'; $(() => { Tinymce.init({ selector: '#theme_description' }); diff --git a/app/javascript/views/usage/index.js b/app/javascript/views/usage/index.js index 7014153..1877f90 100644 --- a/app/javascript/views/usage/index.js +++ b/app/javascript/views/usage/index.js @@ -1,42 +1,80 @@ -import { isObject } from '../../utils/isType'; +import { isObject, isUndefined } from '../../utils/isType'; import { initializeCharts, createChart, drawHorizontalBar } from '../../utils/charts'; $(() => { + // fns to handle the separator character menu + // for CSV download + const changeStatFnGen = (str) => { + const fn = (item) => { + /* eslint no-param-reassign: ["error", { "props": false }] */ + item.href = item.href.replace(/sep=.*/, str); + }; + return fn; + }; + + // attach listener to separator select menu + // on change look for "stat" elements and chnage their query param + document.getElementById('csv-field-sep').addEventListener('click', (e) => { + const statElems = document.getElementsByClassName('stat'); + const newSep = 'sep='.concat(encodeURIComponent(e.target.value)); + const changeStatFn = changeStatFnGen(newSep); + Array.from(statElems).forEach(changeStatFn); + }); + initializeCharts(); // Create the Users joined chart - const usersData = JSON.parse($('#users_joined').val()); - if (isObject(usersData)) { - createChart('#yearly_users', usersData); + if (!isUndefined($('#users_joined').val())) { + const usersData = JSON.parse($('#users_joined').val()); + if (isObject(usersData)) { + createChart('#yearly_users', usersData); + } } // Create the Plans created chart - const plansData = JSON.parse($('#plans_created').val()); - if (isObject(plansData)) { - createChart('#yearly_plans', plansData); + if (!isUndefined($('#plans_created').val())) { + const plansData = JSON.parse($('#plans_created').val()); + if (isObject(plansData)) { + createChart('#yearly_plans', plansData); + } } - // TODO: Most of these event listeners would not be necessary if JQuery and // all other JS libraries were available to the js.erb files. Reevaluate // this JS once we move to Rails 5 and properly configure webpacker - let drawnChart = null; - const monthlyPlanTemplatesChart = document.getElementById('monthly_template_plans'); + let drawnChartByTemplate = null; + const monthlyPlanTemplatesChart = document.getElementById('monthly_plans_by_template'); // Add event listeners that draw and destroy the chart - monthlyPlanTemplatesChart.addEventListener('renderChart', (e) => { - drawnChart = drawHorizontalBar($('#monthly_template_plans'), e.detail); - // Assigning the chart to a window variable here so that we can fire - // the events from the js.erb - window.templatePlansChart = document.getElementById('monthly_template_plans'); - }); - monthlyPlanTemplatesChart.addEventListener('destroyChart', () => { - if (drawnChart) { - drawnChart.destroy(); - } - }); + if (isObject(monthlyPlanTemplatesChart)) { + monthlyPlanTemplatesChart.addEventListener('renderChart', (e) => { + drawnChartByTemplate = drawHorizontalBar($('#monthly_plans_by_template'), e.detail); + // Assigning the chart to a window variable here so that we can fire + // the events from the js.erb + window.templatePlansChart = document.getElementById('monthly_plans_by_template'); + }); + monthlyPlanTemplatesChart.addEventListener('destroyChart', () => { + if (drawnChartByTemplate) { + drawnChartByTemplate.destroy(); + } + }); + } - // Create the initial Plans per template chart - const templatePlansData = JSON.parse($('#plans_by_template').val()); - if (isObject(templatePlansData)) { - const draw = new CustomEvent('renderChart', { detail: templatePlansData }); - document.getElementById('monthly_template_plans').dispatchEvent(draw); + const monthlyPlanUsingTemplatesChart = document.getElementById('monthly_plans_using_template'); + // Add event listeners that draw the chart if it exists + if (isObject(monthlyPlanUsingTemplatesChart)) { + monthlyPlanUsingTemplatesChart.addEventListener('renderChart', (e) => { + drawHorizontalBar($('#monthly_plans_using_template'), e.detail); + }); + } + + // Create the initial Plans per template chart if the chart exists + if (isObject(monthlyPlanTemplatesChart)) { + const templatePlansData = JSON.parse($('#plans_by_template').val()); + const drawPer = new CustomEvent('renderChart', { detail: templatePlansData }); + document.getElementById('monthly_plans_by_template').dispatchEvent(drawPer); + } + // Create the initial Plans using template chart if the chart exists + if (isObject(monthlyPlanUsingTemplatesChart)) { + const usingTemplatePlansData = JSON.parse($('#plans_using_template').val()); + const drawUsing = new CustomEvent('renderChart', { detail: usingTemplatePlansData }); + document.getElementById('monthly_plans_using_template').dispatchEvent(drawUsing); } }); diff --git a/app/models/stat.rb b/app/models/stat.rb index e322734..a8909a5 100644 --- a/app/models/stat.rb +++ b/app/models/stat.rb @@ -24,11 +24,11 @@ class << self - def to_csv(stats) + def to_csv(stats, sep=",") data = stats.map do |stat| { date: stat.date, count: stat.count } end - Csvable.from_array_of_hashes(data) + Csvable.from_array_of_hashes(data, sep) end end diff --git a/app/models/stat_created_plan.rb b/app/models/stat_created_plan.rb index b7101b7..75b645d 100644 --- a/app/models/stat_created_plan.rb +++ b/app/models/stat_created_plan.rb @@ -21,29 +21,36 @@ serialize :details, JSON def by_template - return [] unless details.present? + parse_details.fetch("by_template", []) + end - json = details.is_a?(String) ? JSON.parse(details) : details - json.fetch("by_template", []) + def using_template + parse_details.fetch("using_template", []) end def to_json(options = nil) - super(methods: :by_template) + super(methods: [:by_template, :using_template]) + end + + def parse_details + return JSON.parse({}) unless details.present? + + json = details.is_a?(String) ? JSON.parse(details) : details end class << self - def to_csv(created_plans, details: { by_template: false }) + def to_csv(created_plans, details: { by_template: false, sep: "," }) if details[:by_template] - to_csv_by_template(created_plans) + to_csv_by_template(created_plans, details[:sep]) else - super(created_plans) + super(created_plans, details[:sep]) end end private - def to_csv_by_template(created_plans) + def to_csv_by_template(created_plans, sep = ",") template_names = lambda do |created_plans| unique = Set.new created_plans.each do |created_plan| @@ -66,7 +73,7 @@ tuple[:Count] = created_plan.count tuple end - Csvable.from_array_of_hashes(data, false) + Csvable.from_array_of_hashes(data, false, sep) end end diff --git a/app/models/stat_created_plan/create_or_update.rb b/app/models/stat_created_plan/create_or_update.rb index 8802105..ece95dc 100644 --- a/app/models/stat_created_plan/create_or_update.rb +++ b/app/models/stat_created_plan/create_or_update.rb @@ -8,12 +8,13 @@ def do(start_date:, end_date:, org:) count = count_plans(start_date: start_date, end_date: end_date, org: org) - by_template = by_template(start_date: start_date, end_date: end_date, org: org) + by_template = plan_statistics(start_date: start_date, end_date: end_date, org: org) + using_template = plan_statistics(start_date: start_date, end_date: end_date, org: org, own_templates: true) attrs = { date: end_date.to_date, org_id: org.id, count: count, - details: { by_template: by_template } + details: { by_template: by_template, using_template: using_template } } stat_created_plan = StatCreatedPlan.find_by( date: attrs[:date], @@ -37,6 +38,10 @@ Plan.where(plans: { created_at: start_date..end_date }) end + def own_template_plans(org) + Plan.joins(:template).where(templates: { org_id: org.id }) + end + def count_plans(start_date:, end_date:, org:) Role.joins(:plan, :user) .administrator @@ -47,13 +52,16 @@ .count end - def by_template(start_date:, end_date:, org:) - roleable_plan_ids = Role.joins([:plan, :user]) + def plan_statistics(start_date:, end_date:, org:, own_templates: false) + roleable_plans = Role.joins([:plan, :user]) .administrator - .merge(users(org)) .merge(plans(start_date: start_date, end_date: end_date)) - .pluck(:plan_id) - .uniq + if own_templates + roleable_plans = roleable_plans.merge(own_template_plans(org)) + else + roleable_plans = roleable_plans.merge(users(org)) + end + roleable_plan_ids = roleable_plans.pluck(:plan_id).uniq template_counts = Plan.joins(:template).where(id: roleable_plan_ids) .group("templates.family_id").count diff --git a/app/models/stat_exported_plan.rb b/app/models/stat_exported_plan.rb new file mode 100644 index 0000000..0f11074 --- /dev/null +++ b/app/models/stat_exported_plan.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: stats +# +# id :integer not null, primary key +# count :integer default(0) +# date :date not null +# details :text +# type :string not null +# created_at :datetime not null +# updated_at :datetime not null +# org_id :integer +# + +class StatExportedPlan < Stat + + class << self + + def to_csv(exported_plans) + Stat.to_csv(exported_plans) + end + + end + +end diff --git a/app/models/stat_exported_plan/create_or_update.rb b/app/models/stat_exported_plan/create_or_update.rb new file mode 100644 index 0000000..a88b123 --- /dev/null +++ b/app/models/stat_exported_plan/create_or_update.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +class StatExportedPlan + + class CreateOrUpdate + + class << self + + def do(start_date:, end_date:, org:) + count = exported_plans(start_date: start_date, end_date: end_date, org_id: org.id) + attrs = { date: end_date.to_date, count: count, org_id: org.id } + + stat_exported_plan = StatExportedPlan.find_by( + date: attrs[:date], + org_id: attrs[:org_id] + ) + + if stat_exported_plan.present? + stat_exported_plan.update(attrs) + else + StatExportedPlan.create(attrs) + end + end + + private + + def users(org_id) + User.where(users: {org_id: org_id }) + end + + def org_plan_ids(org_id) + Role.joins(:user) + .creator + .merge(users(org_id)) + .pluck(:plan_id) + .uniq + end + + def exported_plans(start_date:, end_date:, org_id:) + ExportedPlan.where(plan_id: org_plan_ids(org_id)) + .where(created_at: start_date..end_date) + .count + end + + end + + end + +end diff --git a/app/models/stat_shared_plan.rb b/app/models/stat_shared_plan.rb new file mode 100644 index 0000000..b0ffe10 --- /dev/null +++ b/app/models/stat_shared_plan.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: stats +# +# id :integer not null, primary key +# count :integer default(0) +# date :date not null +# details :text +# type :string not null +# created_at :datetime not null +# updated_at :datetime not null +# org_id :integer +# + +class StatSharedPlan < Stat + + class << self + + def to_csv(shared_plans) + Stat.to_csv(shared_plans) + end + + end + +end diff --git a/app/models/stat_shared_plan/create_or_update.rb b/app/models/stat_shared_plan/create_or_update.rb new file mode 100644 index 0000000..3022d1e --- /dev/null +++ b/app/models/stat_shared_plan/create_or_update.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +class StatSharedPlan + + class CreateOrUpdate + + class << self + + def do(start_date:, end_date:, org:) + count = shared_plans(start_date: start_date, end_date: end_date, org_id: org.id) + attrs = { date: end_date.to_date, count: count, org_id: org.id } + + stat_shared_plan = StatSharedPlan.find_by( + date: attrs[:date], + org_id: attrs[:org_id] + ) + + if stat_shared_plan.present? + stat_shared_plan.update(attrs) + else + StatSharedPlan.create(attrs) + end + end + + private + + def users(org_id) + User.where(users: {org_id: org_id }) + end + + def org_plan_ids(org_id) + Role.joins(:user) + .creator + .merge(users(org_id)) + .pluck(:plan_id) + .uniq + end + + def shared_plans(start_date:, end_date:, org_id:) + Role.not_creator + .where(plan_id: org_plan_ids(org_id)) + .where(created_at: start_date..end_date) + .count + end + + end + + end + +end diff --git a/app/models/user/at_csv.rb b/app/models/user/at_csv.rb index 35a4689..014c228 100644 --- a/app/models/user/at_csv.rb +++ b/app/models/user/at_csv.rb @@ -1,7 +1,7 @@ class User class AtCsv - HEADERS = ['Name', 'E-Mail', 'Created Date', 'Last Activity', 'Plans', 'Current Privileges', 'Active'] + HEADERS = ['Name', 'E-Mail', 'Created Date', 'Last Activity', 'Plans', 'Current Privileges', 'Active', 'Department' ] def initialize(users) @users = users @@ -26,10 +26,12 @@ current_privileges = '' end - csv << [ name, email, created, last_activity, plans, current_privileges, active ] + department = user&.department&.name || '' + + csv << [ name, email, created, last_activity, plans, current_privileges, active, department ] end end end end -end \ No newline at end of file +end diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb index 972e584..3e7cf62 100644 --- a/app/policies/note_policy.rb +++ b/app/policies/note_policy.rb @@ -13,7 +13,7 @@ end def update? - Plan.find(@note.answer.plan_id).commentable_by?(@user.id) && @note.user_id = @user.id + Plan.find(@note.answer.plan_id).commentable_by?(@user.id) && @note.user_id == @user.id end def archive? diff --git a/app/policies/role_policy.rb b/app/policies/role_policy.rb index 8570ba1..5c234ad 100644 --- a/app/policies/role_policy.rb +++ b/app/policies/role_policy.rb @@ -21,6 +21,6 @@ end def deactivate? - @role.user_id = @user.id + @role.user_id == @user.id end -end \ No newline at end of file +end diff --git a/app/policies/usage_policy.rb b/app/policies/usage_policy.rb index 6c26e3c..88d1d92 100644 --- a/app/policies/usage_policy.rb +++ b/app/policies/usage_policy.rb @@ -21,6 +21,10 @@ end def global_statistics? + @user.can_super_admin? + end + + def org_statistics? @user.can_org_admin? end diff --git a/app/services/org/create_created_plan_service.rb b/app/services/org/create_created_plan_service.rb index c668a77..c1cd442 100644 --- a/app/services/org/create_created_plan_service.rb +++ b/app/services/org/create_created_plan_service.rb @@ -1,15 +1,25 @@ # frozen_string_literal: true +#import statements fix Circular dependancy errors due to threading +import OrgDateRangeable +import StatCreatedPlan +import StatCreatedPlan::CreateOrUpdate +import Role +import User +import Plan +import Perm +import Template + class Org class CreateCreatedPlanService class << self - def call(org = nil) + def call(org = nil, threads: 0) orgs = org.nil? ? Org.all : [org] - orgs.each do |org| + Parallel.each(orgs, in_threads: threads) do |org| OrgDateRangeable.split_months_from_creation(org) do |start_date, end_date| StatCreatedPlan::CreateOrUpdate.do( start_date: start_date, diff --git a/app/services/org/create_exported_plan_service.rb b/app/services/org/create_exported_plan_service.rb new file mode 100644 index 0000000..0d1de4c --- /dev/null +++ b/app/services/org/create_exported_plan_service.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +#import statements fix Circular dependancy errors +import OrgDateRangeable +import StatExportedPlan +import StatExportedPlan::CreateOrUpdate +import Role +import User +import ExportedPlan + +class Org + + class CreateExportedPlanService + + class << self + + def call(org = nil, threads: 0) + orgs = org.nil? ? Org.all : [org] + + Parallel.each(orgs, in_threads: threads) do |org| + OrgDateRangeable.split_months_from_creation(org) do |start_date, end_date| + StatExportedPlan::CreateOrUpdate.do( + start_date: start_date, + end_date: end_date, + org: org + ) + end + end + end + + end + + end + +end diff --git a/app/services/org/create_joined_user_service.rb b/app/services/org/create_joined_user_service.rb index e4842eb..44f8ed3 100644 --- a/app/services/org/create_joined_user_service.rb +++ b/app/services/org/create_joined_user_service.rb @@ -1,14 +1,21 @@ # frozen_string_literal: true +#import statements fix Circular dependancy errors due to threading +import OrgDateRangeable +import StatJoinedUser +import StatJoinedUser::CreateOrUpdate +import User + class Org class CreateJoinedUserService class << self - def call(org = nil) + def call(org = nil, threads: 0) orgs = org.nil? ? Org.all : [org] - orgs.each do |org| + + Parallel.each(orgs, in_threads: threads) do |org| OrgDateRangeable.split_months_from_creation(org) do |start_date, end_date| StatJoinedUser::CreateOrUpdate.do( start_date: start_date, @@ -17,6 +24,7 @@ ) end end + # pp StatJoinedUser.where.not(count: 0) end end diff --git a/app/services/org/create_last_month_created_plan_service.rb b/app/services/org/create_last_month_created_plan_service.rb index 8093862..4acea4a 100644 --- a/app/services/org/create_last_month_created_plan_service.rb +++ b/app/services/org/create_last_month_created_plan_service.rb @@ -1,15 +1,26 @@ # frozen_string_literal: true +#import statements fix Circular dependancy errors due to threading +import OrgDateRangeable +import StatCreatedPlan +import StatCreatedPlan::CreateOrUpdate +import Role +import User +import Plan +import Perm +import Template + + class Org class CreateLastMonthCreatedPlanService class << self - def call(org = nil) + def call(org = nil, threads: 0) orgs = org.nil? ? Org.all : [org] - orgs.each do |org| + Parallel.each(orgs, in_threads: threads) do |org| months = OrgDateRangeable.split_months_from_creation(org) last = months.last if last.present? diff --git a/app/services/org/create_last_month_exported_plan_service.rb b/app/services/org/create_last_month_exported_plan_service.rb new file mode 100644 index 0000000..50ff09f --- /dev/null +++ b/app/services/org/create_last_month_exported_plan_service.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +#import statements fix Circular dependancy errors +import OrgDateRangeable +import StatExportedPlan +import StatExportedPlan::CreateOrUpdate +import Role +import User +import ExportedPlan + +class Org + + class CreateLastMonthExportedPlanService + + class << self + + def call(org = nil, threads: 0) + orgs = org.nil? ? Org.all : [org] + + Parallel.each(orgs, in_threads: threads) do |org| + months = OrgDateRangeable.split_months_from_creation(org) + last = months.last + if last.present? + StatExportedPlan::CreateOrUpdate.do( + start_date: last[:start_date], + end_date: last[:end_date], + org: org + ) + end + end + end + + end + + end + +end diff --git a/app/services/org/create_last_month_joined_user_service.rb b/app/services/org/create_last_month_joined_user_service.rb index 026dfd8..7b53534 100644 --- a/app/services/org/create_last_month_joined_user_service.rb +++ b/app/services/org/create_last_month_joined_user_service.rb @@ -1,14 +1,21 @@ # frozen_string_literal: true +#import statements fix Circular dependancy errors due to threading +import OrgDateRangeable +import StatJoinedUser +import StatJoinedUser::CreateOrUpdate +import User + class Org class CreateLastMonthJoinedUserService class << self - def call(org = nil) + def call(org = nil, threads: 0) orgs = org.nil? ? ::Org.all : [org] - orgs.each do |org| + + Parallel.each(orgs, in_threads: threads) do |org| months = OrgDateRangeable.split_months_from_creation(org) last = months.last if last.present? diff --git a/app/services/org/create_last_month_shared_plan_service.rb b/app/services/org/create_last_month_shared_plan_service.rb new file mode 100644 index 0000000..9eba8d6 --- /dev/null +++ b/app/services/org/create_last_month_shared_plan_service.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +#import statements fix Circular dependancy errors due to threading +import OrgDateRangeable +import StatSharedPlan +import StatSharedPlan::CreateOrUpdate +import User +import Role + +class Org + + class CreateLastMonthSharedPlanService + + class << self + + def call(org = nil, threads: 0) + orgs = org.nil? ? Org.all : [org] + + Parallel.each(orgs, in_threads: threads) do |org| + months = OrgDateRangeable.split_months_from_creation(org) + last = months.last + if last.present? + StatSharedPlan::CreateOrUpdate.do( + start_date: last[:start_date], + end_date: last[:end_date], + org: org + ) + end + end + end + + end + + end + +end diff --git a/app/services/org/create_shared_plan_service.rb b/app/services/org/create_shared_plan_service.rb new file mode 100644 index 0000000..b784235 --- /dev/null +++ b/app/services/org/create_shared_plan_service.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +#import statements fix Circular dependancy errors due to threading +import OrgDateRangeable +import StatSharedPlan +import StatSharedPlan::CreateOrUpdate +import User +import Role + +class Org + + class CreateSharedPlanService + + class << self + + def call(org = nil, threads: 0) + orgs = org.nil? ? Org.all : [org] + + Parallel.each(orgs, in_threads: threads) do |org| + OrgDateRangeable.split_months_from_creation(org) do |start_date, end_date| + StatSharedPlan::CreateOrUpdate.do( + start_date: start_date, + end_date: end_date, + org: org + ) + end + end + end + + end + + end + +end diff --git a/app/services/org/monthly_usage_service.rb b/app/services/org/monthly_usage_service.rb new file mode 100644 index 0000000..fa406c9 --- /dev/null +++ b/app/services/org/monthly_usage_service.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +class Org + + class MonthlyUsageService + + class << self + + def call + total = build_from_joined_user + build_from_created_plan(total) + build_from_shared_plan(total) + build_from_exported_plan(total) + total.values + end + + private + + def current_user + User.find_by_email 'xsrust@gmail.com' + end + + def build_model(month:, new_plans: 0, new_users: 0, downloads: 0, plans_shared: 0) + { + month: month, + new_plans: new_plans, + new_users: new_users, + downloads: downloads, + plans_shared: plans_shared + } + end + + def reducer_body(acc, rec, key_target) + month = rec.date.strftime("%b-%y") + count = rec.count + + if acc[month].present? + acc[month][key_target] = count + else + args = { month: month } + args[key_target] = count + acc[month] = build_model(args) + end + + acc + end + + def build_from_joined_user(total = {}) + joined_users = Stat::StatJoinedUser.monthly_range(org: current_user.org).order(:date) + joined_users.reduce(total) do |acc, rec| + reducer_body(acc, rec, :new_users) + end + end + + def build_from_created_plan(total = {}) + created_plans = Stat::StatCreatedPlan.monthly_range(org: current_user.org).order(:date) + created_plans.reduce(total) do |acc, rec| + reducer_body(acc, rec, :new_plans) + end + end + + def build_from_shared_plan(total = {}) + shared_plans = Stat::StatSharedPlan.monthly_range(org: current_user.org).order(:date) + shared_plans.reduce(total) do |acc, rec| + reducer_body(acc, rec, :plans_shared) + end + end + + def build_from_exported_plan(total = {}) + exported_plans = Stat::StatExportedPlan.monthly_range(org: current_user.org).order(:date) + exported_plans.reduce(total) do |acc, rec| + reducer_body(acc, rec, :downloads) + end + end + + end + + end + +end diff --git a/app/views/answers/_locking.html.erb b/app/views/answers/_locking.html.erb index 5018ff9..ed8ee29 100644 --- a/app/views/answers/_locking.html.erb +++ b/app/views/answers/_locking.html.erb @@ -1,6 +1,9 @@ +

<%= _('The following answer cannot be saved') %>

<%# We do not need to re-show example answers in this lock conflict section so leave template nil %> <%= render partial: '/answers/new_edit', locals: { template: nil, question: question, answer: answer, readonly: true, locking: true } %>

<%= _('since %{name} saved the answer below while you were editing. Please, combine your changes and then save the answer again.') % { name: user.name} %>

-
\ No newline at end of file + diff --git a/app/views/answers/_status.html.erb b/app/views/answers/_status.html.erb index f842fb0..3b317d8 100644 --- a/app/views/answers/_status.html.erb +++ b/app/views/answers/_status.html.erb @@ -1,5 +1,7 @@ <%# locals: { answer } %> - + <% if answer.answered? %> @@ -7,4 +9,4 @@ <%= _('Answered')%> <%= _(' by %{user_name}') %{ :user_name => answer.user.name } if answer.user.present? %> -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/devise/registrations/_password_confirmation.html.erb b/app/views/devise/registrations/_password_confirmation.html.erb index ae39abb..26768c4 100644 --- a/app/views/devise/registrations/_password_confirmation.html.erb +++ b/app/views/devise/registrations/_password_confirmation.html.erb @@ -2,7 +2,7 @@ \ No newline at end of file + diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb index 5a8390a..ca5a3bd 100644 --- a/app/views/devise/registrations/new.html.erb +++ b/app/views/devise/registrations/new.html.erb @@ -1,19 +1,25 @@
+
+

<%= _('Sign in or Create account') %>

+
+
+ +
<% unless session["devise.shibboleth_data"].nil? %> <% cookies[:show_shib_link] = { value: 'show_shib_link', expires: 3.hours.from_now } %>
-

+

<%= _("Do you have a %{application_name} account?") % { application_name: Rails.configuration.branding[:application][:name]} %> -

-

+

+

- <%= _("Sign in") %> + <%= _("Sign in") %>

<%= _("This will link your existing account to your credentials.") %> @@ -24,17 +30,17 @@

-

+

<%= _("No %{application_name} account?") % { application_name: Rails.configuration.branding[:application][:name]} %> -

+

- <%= _("Create account") %> + <%= _("Create account") %>

<%= _("This will create an account and link it to your credentials.") %> diff --git a/app/views/guidances/admin_index.html.erb b/app/views/guidances/admin_index.html.erb index d7fc08b..15a7eb3 100644 --- a/app/views/guidances/admin_index.html.erb +++ b/app/views/guidances/admin_index.html.erb @@ -1,3 +1,4 @@ + <% title _('Guidance') %>

@@ -46,4 +47,3 @@
- diff --git a/app/views/layouts/_branding.html.erb b/app/views/layouts/_branding.html.erb index f951d53..f3ee708 100644 --- a/app/views/layouts/_branding.html.erb +++ b/app/views/layouts/_branding.html.erb @@ -15,7 +15,7 @@ class: "org-logo", title: current_user.org.name) %> <% else %> - + <% end %> <% end %>
diff --git a/app/views/layouts/_footer.html.erb b/app/views/layouts/_footer.html.erb index af00dbb..c71a971 100644 --- a/app/views/layouts/_footer.html.erb +++ b/app/views/layouts/_footer.html.erb @@ -8,7 +8,7 @@ <% if i != section.questions.length - 1 %> -
+
<% end %> <% end %>
diff --git a/app/views/phases/_overview.html.erb b/app/views/phases/_overview.html.erb index 89b9b01..5339d50 100644 --- a/app/views/phases/_overview.html.erb +++ b/app/views/phases/_overview.html.erb @@ -1,10 +1,10 @@ <%# locals: { phase } %>
-

+

<%= _('Instructions') %> <%= _('Write plan') %> -

+

<%= sanitize(phase.description) %> diff --git a/app/views/plans/_show_details.html.erb b/app/views/plans/_show_details.html.erb index 8f31806..01ba3ba 100644 --- a/app/views/plans/_show_details.html.erb +++ b/app/views/plans/_show_details.html.erb @@ -11,7 +11,7 @@
<%= plan.identifier %>

-

<%= _('Principal Investigator') %>

+

<%= _('Principal Investigator') %>

<%= _('Name') %>
<%= plan.principal_investigator %>
@@ -21,7 +21,7 @@
<%= plan.principal_investigator_email %>

-

<%= _('Data Contact Person') %>

+

><%= _('Data Contact Person') %>

<%= _('Name') %>
<%= plan.data_contact %>
diff --git a/app/views/shared/_copy_link_modal.html.erb b/app/views/shared/_copy_link_modal.html.erb index c9c8d07..7c454c7 100644 --- a/app/views/shared/_copy_link_modal.html.erb +++ b/app/views/shared/_copy_link_modal.html.erb @@ -2,9 +2,9 @@ \ No newline at end of file +
diff --git a/app/views/static_pages/help.html.erb b/app/views/static_pages/help.html.erb index 450cfda..e581de3 100644 --- a/app/views/static_pages/help.html.erb +++ b/app/views/static_pages/help.html.erb @@ -8,46 +8,46 @@
-

<%= _("When you log in to DMPRoadmap you will be directed to the 'My Dashboard' page. From here you can edit, share, download, copy or remove any of your plans. You will also see plans that have been shared with you by others.") %>

+

<%= _("When you log in to DMPRoadmap you will be directed to the 'My Dashboard' page. From here you can edit, share, download, copy or remove any of your plans. You will also see plans that have been shared with you by others.") %>

-

<%= _("Create a plan") %>

+

<%= _("Create a plan") %>

-

<%= _("To create a plan, click the 'Create plan' button from the 'My Dashboard' page or the top menu. Select options from the menus and tickboxes to determine what questions and guidance you should be presented with. Confirm your selection by clicking 'Create plan.'") %>

+

<%= _("To create a plan, click the 'Create plan' button from the 'My Dashboard' page or the top menu. Select options from the menus and tickboxes to determine what questions and guidance you should be presented with. Confirm your selection by clicking 'Create plan.'") %>

-

<%= _("Write your plan") %>

+

<%= _("Write your plan") %>

-

<%= _("The tabbed interface allows you to navigate through different functions when editing your plan.") %>

-
    +

    <%= _("The tabbed interface allows you to navigate through different functions when editing your plan.") %>

    +
    • <%= _("'Project Details' includes basic administrative details.") %>
    • -
    • <%= _("'Plan Overview' tells you what template and guidance your plan is based on and gives you an overview to the questions that you will be asked.") %>
    • -
    • <%= _("The following tab(s) present the questions to answer. There may be more than one tab if your funder or university asks different sets of questions at different stages e.g. at grant application and post-award.") %>
    • -
    • <%= _("'Share' allows you to invite others to read or contribute to your plan.") %>
    • +
    • <%= _("'Plan Overview' tells you what template and guidance your plan is based on and gives you an overview to the questions that you will be asked.") %>
    • +
    • <%= _("The following tab(s) present the questions to answer. There may be more than one tab if your funder or university asks different sets of questions at different stages e.g. at grant application and post-award.") %>
    • +
    • <%= _("'Share' allows you to invite others to read or contribute to your plan.") %>
    • <%= _("'Download' allows you to download your plan in various formats. This may be useful if you need to submit your plan as part of a grant application.") %>
    • -
    +
-

<%= _("When viewing any of the question tabs, you will see the different sections of your plan displayed. Click into these in turn to answer the questions. You can format your responses using the text editing buttons.") %>

+

<%= _("When viewing any of the question tabs, you will see the different sections of your plan displayed. Click into these in turn to answer the questions. You can format your responses using the text editing buttons.") %>

-

<%= _("Guidance is displayed in the right-hand panel. If you need more guidance or find there is too much, you can make adjustments on the ‘Project Details’ tab.") %>

+

<%= _("Guidance is displayed in the right-hand panel. If you need more guidance or find there is too much, you can make adjustments on the ‘Project Details’ tab.") %>

-

<%= _("Share plans") %>

+

<%= _("Share plans") %>

-

<%= _("Insert the email address of any collaborators you would like to invite to read or edit your plan. Set the level of permissions you would like to grant them via the radio buttons and click to 'Add collaborator.' Adjust permissions or remove collaborators at any time via the drop-down options.") %>

+

<%= _("Insert the email address of any collaborators you would like to invite to read or edit your plan. Set the level of permissions you would like to grant them via the radio buttons and click to 'Add collaborator.' Adjust permissions or remove collaborators at any time via the drop-down options.") %>

<%= _("The ‘Share’ tab is also where you can set your plan visibility.") %>

-
    +
    • <%= _("Private: restricted to you and your collaborators.") %>
    • -
    • <%= _("Organisational: anyone at your organisation can view your plan.") %>
    • -
    • <%= _("Public: anyone can view your plan in the Public DMPs list.") %>
    • -
    +
  • <%= _("Organisational: anyone at your organisation can view your plan.") %>
  • +
  • <%= _("Public: anyone can view your plan in the Public DMPs list.") %>
  • +

<%= _("By default all new and test plans will be set to ‘Private’ visibility. ‘Public’ and ‘Organisational’ visibility are intended for finished plans. You must answer at least 50% of the questions to enable these options.") %>

- -

<%= _("Request feedback") %>

-

<%= _("There may also be an option to request feedback on your plan. This is available when research support staff at your organisation have enabled this service. Click to ‘Request feedback’ and your local administrators will be alerted to your request. Their comments will be visible in the ‘Comments’ field adjacent to each question. You will be notified by email when an administrator provides feedback.") %>

+ +

<%= _("Request feedback") %>

+

<%= _("There may also be an option to request feedback on your plan. This is available when research support staff at your organisation have enabled this service. Click to ‘Request feedback’ and your local administrators will be alerted to your request. Their comments will be visible in the ‘Comments’ field adjacent to each question. You will be notified by email when an administrator provides feedback.") %>

-

<%= _("Download plans") %>

+

<%= _("Download plans") %>

<%= _("From here you can download your plan in various formats. This may be useful if you need to submit your plan as part of a grant application. Choose what format you would like to view/download your plan in and click to download. You can also adjust the formatting (font type, size and margins) for PDF files, which may be helpful if working to page limits.

") %>
-
\ No newline at end of file +
diff --git a/app/views/static_pages/privacy.html.erb b/app/views/static_pages/privacy.html.erb index 12270ab..a3b4f8f 100644 --- a/app/views/static_pages/privacy.html.erb +++ b/app/views/static_pages/privacy.html.erb @@ -7,7 +7,7 @@
-

<%= _("Information about you: how we use it and with whom we share it") %>

+

<%= _("Information about you: how we use it and with whom we share it") %>

<%= _("The information you provide will be used by the %{org_name} to offer you access to and personalisation of the %{application_name} service.") % { :application_name => Rails.configuration.branding[:application][:name], :org_name => Rails.configuration.branding[:organisation][:name] } %>

<%= _("The %{org_name} processes the personal data of %{application_name} users in order to deliver and improve the %{application_name} service in a customised manner and to ensure each user receives relevant information.") % @@ -25,7 +25,7 @@ { :application_name => Rails.configuration.branding[:application][:name] } %>

<%= sanitize _("If you have any questions, please contact the %{application_name} team at: %{helpdesk_email}") % { :application_name => Rails.configuration.branding[:application][:name], :helpdesk_email => Rails.configuration.branding[:organisation][:helpdesk_email] } %>

-

Revisions

+

Revisions

<%= _("This statement was last revised on %{revdate} and may be revised at any time with prior notice.") % { :revdate => "May 21st, 2018" }%>

diff --git a/app/views/static_pages/termsuse.html.erb b/app/views/static_pages/termsuse.html.erb index 2fcbcfb..eccaf43 100644 --- a/app/views/static_pages/termsuse.html.erb +++ b/app/views/static_pages/termsuse.html.erb @@ -11,7 +11,7 @@

The <%= link_to 'Digital Curation Centre', 'https://dmponline.dcc.ac.uk/' %> (DCC) and <%= link_to 'California Digital Library', 'http://www.cdlib.org/' %> <%= _("(CDL) are consortia supported by the University of Edinburgh and the University of California, respectively. Our primary constituency is the research community. We provide services to the UK, US and international higher education sector. ") %>

-

<%= _('DMPRoadmap') %>

+

<%= _('DMPRoadmap') %>

<%= _("DMPRoadmap ('the tool', 'the system') is a tool developed by the DCC and CDL as a shared resource for the research community. It is hosted at CDL by the University of California Curation Center.") %>

@@ -47,4 +47,4 @@ tags: %w( a h3 p span em )) %>
-
\ No newline at end of file +
diff --git a/app/views/super_admin/notifications/edit.html.erb b/app/views/super_admin/notifications/edit.html.erb index ffcec89..63e1148 100644 --- a/app/views/super_admin/notifications/edit.html.erb +++ b/app/views/super_admin/notifications/edit.html.erb @@ -1,8 +1,8 @@ <% title _('Editing Notification') %> -

+

<%= _('Editing Notification') %> <%= link_to(_('View all notifications'), super_admin_notifications_path, class: 'btn btn-default pull-right', role: 'button') %> -

+ <%= render 'form' %> diff --git a/app/views/super_admin/notifications/index.html.erb b/app/views/super_admin/notifications/index.html.erb index 216d7ee..f55c619 100644 --- a/app/views/super_admin/notifications/index.html.erb +++ b/app/views/super_admin/notifications/index.html.erb @@ -1,7 +1,7 @@ <% title _('Notifications') %>
-

<%= _('Notifications') %>

+

<%= _('Notifications') %>

diff --git a/app/views/super_admin/notifications/new.html.erb b/app/views/super_admin/notifications/new.html.erb index 7acb055..a35eeda 100644 --- a/app/views/super_admin/notifications/new.html.erb +++ b/app/views/super_admin/notifications/new.html.erb @@ -1,8 +1,8 @@ <% title _('New notification') %> -

+

<%= _('New Notification') %> <%= link_to(_('View all notifications'), super_admin_notifications_path, class: 'btn btn-default pull-right', role: 'button') %> -

+ <%= render 'form' %> diff --git a/app/views/super_admin/themes/edit.html.erb b/app/views/super_admin/themes/edit.html.erb index 98d2975..29a9f38 100644 --- a/app/views/super_admin/themes/edit.html.erb +++ b/app/views/super_admin/themes/edit.html.erb @@ -1,6 +1,6 @@ -

+

<%= @theme.title %> <%= link_to(_('View all themes'), super_admin_themes_path, class: 'btn btn-default pull-right', role: 'button') %> -

-<%= render partial: 'form' %> \ No newline at end of file + +<%= render partial: 'form' %> diff --git a/app/views/super_admin/themes/index.html.erb b/app/views/super_admin/themes/index.html.erb index 917e48a..ebc567d 100644 --- a/app/views/super_admin/themes/index.html.erb +++ b/app/views/super_admin/themes/index.html.erb @@ -1,7 +1,7 @@ <% title _('Themes') %>
-

<%= _('Themes') %>

+

<%= _('Themes') %>

@@ -13,9 +13,9 @@ <%= paginable_renderise( partial: '/paginable/themes/index', controller: 'paginable/themes', - action: 'index', + action: 'index', scope: themes, - query_params: { sort_field: 'themes.title', sort_direction: :asc }) + query_params: { sort_field: 'themes.title', sort_direction: :asc }) %>
diff --git a/app/views/super_admin/themes/new.html.erb b/app/views/super_admin/themes/new.html.erb index 000c53b..40682ee 100644 --- a/app/views/super_admin/themes/new.html.erb +++ b/app/views/super_admin/themes/new.html.erb @@ -1,6 +1,6 @@ -

+

<%= _("New theme") %> <%= link_to(_('View all themes'), super_admin_themes_path, class: 'btn btn-default pull-right', role: 'button') %> -

-<%= render partial: 'form' %> \ No newline at end of file + +<%= render partial: 'form' %> diff --git a/app/views/usage/_filter.html.erb b/app/views/usage/_filter.html.erb index eff2765..7a5f6c3 100644 --- a/app/views/usage/_filter.html.erb +++ b/app/views/usage/_filter.html.erb @@ -2,16 +2,16 @@
-

<%= _('Use the filters to generate organisational usage statistics for a custom date range. The graphs display new users and plans for your organisation over the past year. You can download a CSV report for each graph.') %>

+

<%= _('Use the filters to run organisational usage statistics for a custom date range.') %>

-

<%= _('Run your own filter') %>

+

<%= _('Run your own filter') %>

<%= form_for :usage, url: usage_filter_path, remote: true do |f| %>
-
+
<%= f.label :topic, _('Topic') %> <%= f.select :topic, [ @@ -31,20 +31,21 @@ <%= f.date_field :end_date, class: 'form-control' %>
-
- <%= f.submit _('Go'), class: 'btn btn-default' %> -
-
- <% if current_user.can_super_admin? %> -
-
-
- <%= f.label :org_id, _('Organisation') %> - <%= f.select :org_id, options_from_collection_for_select(Org.all, :id, :name, current_user.org_id), {}, class: 'form-control' %> + <% if current_user.can_super_admin? %> +
+
+ <%= f.label :org_id, _('Organisation') %> + <%= f.select :org_id, options_from_collection_for_select(Org.all, :id, :name, current_user.org_id), {}, class: 'form-control' %> +
+ <% end %> +
+
+ <%= f.submit _('Go'), class: 'btn btn-default pull-right' %>
- <% end %> +
+ <% end %>
@@ -58,4 +59,4 @@
-
\ No newline at end of file + diff --git a/app/views/usage/_plans_by_template_chart.html.erb b/app/views/usage/_plans_by_template_chart.html.erb index eaf5651..c70df06 100644 --- a/app/views/usage/_plans_by_template_chart.html.erb +++ b/app/views/usage/_plans_by_template_chart.html.erb @@ -1,6 +1,6 @@ -<%# locals: data %> - - +<%# locals: data, ?subset%> +<% subset ||= "by_template" %> + + id="plans_<%= subset %>" + value="<%= prep_data_for_template_plans_chart(data: data, subset: subset) %>" /> diff --git a/app/views/usage/_template_statistics.html.erb b/app/views/usage/_template_statistics.html.erb new file mode 100644 index 0000000..90a68db --- /dev/null +++ b/app/views/usage/_template_statistics.html.erb @@ -0,0 +1,14 @@ +
+
+

<%=_('No. plans created based off your templates')%>

+
+
+ +
+
+
+ <%= render partial: 'usage/plans_by_template_chart', + locals: { data: @plans_per_month, subset: "using_template"} %> +
+
+
diff --git a/app/views/usage/_template_statistics_accordion.html.erb b/app/views/usage/_template_statistics_accordion.html.erb new file mode 100644 index 0000000..8bb0ecd --- /dev/null +++ b/app/views/usage/_template_statistics_accordion.html.erb @@ -0,0 +1,29 @@ +<%# locals: expanded %> + +
+ + + + +
+
+ + <%= render partial: 'usage/template_statistics' %> + +
+
+ +
diff --git a/app/views/usage/_total_usage.html.erb b/app/views/usage/_total_usage.html.erb index 13d247c..f62431c 100644 --- a/app/views/usage/_total_usage.html.erb +++ b/app/views/usage/_total_usage.html.erb @@ -1,26 +1,42 @@ -<%# locals: user_count, plan_count %> +<%# locals: user_count, plan_count, separators %> -
-
-
-

Usage statistics

+
+
+
+ +

<%= user_count.to_i %> Total users

-
- <%= link_to usage_global_statistics_path, class: 'stat btn btn-default', role: 'button', target: '_blank' do %> +
+ +

<%= plan_count.to_i %> Total plans

+
+
+ <% if @funder.present? %> +
+ <% end %> + <% unless current_user.can_super_admin? %> +
+ <% end %> +
+
+ + <%= select_tag "csv-field-sep", options_for_select(separators, separators[0]), {class: "single-char-select"} %> +
+
+ <% if current_user.can_super_admin? %> +
+ <%= link_to usage_global_statistics_path(sep: ","), class: "stat btn btn-default #{'pull-right' if @funder.present?}", role: 'button', target: '_blank' do %> <%= _('Download global usage') %> <% end %>
-
-
-
-
-
- -

<%= user_count.to_i %> Total users

+ <% end %> + <% unless @funder.present? %> +
+ <%= link_to usage_org_statistics_path(sep: ","), class: 'stat btn btn-default pull-right', role: 'button', target: '_blank' do %> + <%= _('Download Monthly Usage') %> + <% end %>
-
- -

<%= plan_count.to_i %> Total plans

-
-
+ <% end %>
diff --git a/app/views/usage/_user_statistics.html.erb b/app/views/usage/_user_statistics.html.erb new file mode 100644 index 0000000..20d3551 --- /dev/null +++ b/app/views/usage/_user_statistics.html.erb @@ -0,0 +1,99 @@ +
+
+

+ <%= _('The graphs display new users and plans for your organisation over the past year. You can download a CSV report for each graph.')%> +

+
+
+ + +
+
+

* <%= _('Move the mouse pointer over the bars of a chart to see numbers.') %>

+
+
+ +
+
+
+

<%= _('No. users joined during last year') %>

+
+
+ <%= link_to usage_yearly_users_path(sep: ","), class: 'stat btn btn-default', role: 'button', target: '_blank' do %> + <%= _('Download') %> + <% end %> +
+
+
+ <%= render partial: 'usage/users_joined_chart', + locals: { data: @users_per_month } %> +
+
+
+
+

<%= _('No. plans during last year') %>

+
+
+ <%= link_to usage_yearly_plans_path(sep: ","), class: 'stat btn btn-default', role: 'button', target: '_blank' do %> + <%= _('Download') %> + <% end %> +
+
+
+ <%= render partial: 'usage/plans_created_chart', + locals: { data: @plans_per_month } %> +
+
+
+
+
+
+
+
+
+
+
+
+

<%= _('No. plans by template') %>

+
+
+ +
+
+
    +
  • + <%= form_for :usage, url: usage_plans_by_template_path, remote: true do |f| %> +
    + <%= f.label :template_plans_range, _('Time period') %> +
      +
    • + <%= f.select :template_plans_range, plans_per_template_ranges.reverse, {}, { class: "form-control" } %> +
    • +
    • + <%= f.submit _('Go'), class: 'btn btn-default mt-25' %> +
    • +
    +
    + <% end %> +
  • +
  • +
    + <%= link_to usage_all_plans_by_template_path(sep: ","), class: 'btn btn-default stat', role: 'button', target: '_blank' do %> + <%= _('Download all') %> + <% end %> +
    +
  • +
+
+
+
+
+
+
+
+ <%# pp @plans_per_month %> + <%= render partial: 'usage/plans_by_template_chart', + locals: { data: @plans_per_month } %> +
+
+
diff --git a/app/views/usage/_user_statistics_accordion.html.erb b/app/views/usage/_user_statistics_accordion.html.erb new file mode 100644 index 0000000..287420b --- /dev/null +++ b/app/views/usage/_user_statistics_accordion.html.erb @@ -0,0 +1,29 @@ +<%# locals: expanded %> + +
+ + + + +
+
+ + <%= render partial: 'usage/user_statistics' %> + +
+
+ +
diff --git a/app/views/usage/index.html.erb b/app/views/usage/index.html.erb index f35589d..6b5eab4 100644 --- a/app/views/usage/index.html.erb +++ b/app/views/usage/index.html.erb @@ -1,97 +1,23 @@ <% title _('Usage statistics') %> +
+
+

<%=_('Usage statistics')%>

+
+
+ <%= render partial: 'usage/total_usage', - locals: { user_count: @total_org_users, plan_count: @total_org_plans } %> + locals: { user_count: @total_org_users, + plan_count: @total_org_plans, + separators: @separators } %> -<%= render partial: 'usage/filter' %> +<% if @funder.present? %> + <%= render partial: 'usage/template_statistics' %> +<% else %> +
+ <%= render partial: 'usage/user_statistics_accordion', locals: {expanded: true} %> + <%= render partial: 'usage/template_statistics_accordion', locals: {expanded: false} %> +
-
-
-

* <%= _('Move the mouse pointer over the bars of a chart to see numbers.') %>

-
-
- -
-
-
-

<%= _('No. users joined during last year') %>

-
-
- <%= link_to usage_yearly_users_path, class: 'stat btn btn-default', role: 'button', target: '_blank' do %> - <%= _('Download') %> - <% end %> -
-
-
- <%= render partial: 'usage/users_joined_chart', - locals: { data: @users_per_month } %> -
-
-
-
-

<%= _('No. plans during last year') %>

-
-
- <%= link_to usage_yearly_plans_path, class: 'stat btn btn-default', role: 'button', target: '_blank' do %> - <%= _('Download') %> - <% end %> -
-
-
- <%= render partial: 'usage/plans_created_chart', - locals: { data: @plans_per_month } %> -
-
-
-
-
-
-
-
-
-
-
-
-

<%= _('No. plans by template') %>

-
-
- -
-
-
    -
  • - <%= form_for :usage, url: usage_plans_by_template_path, remote: true do |f| %> -
    - <%= f.label :template_plans_range, _('Time period') %> -
      -
    • - <%= f.select :template_plans_range, plans_per_template_ranges.reverse, {}, { class: "form-control" } %> -
    • -
    • - <%= f.submit _('Go'), class: 'btn btn-default mt-25' %> -
    • -
    -
    - <% end %> -
  • -
  • -
    - <%= link_to usage_all_plans_by_template_path, class: 'btn btn-default', role: 'button', target: '_blank' do %> - <%= _('Download all') %> - <% end %> -
    -
  • -
-
-
-
-
-
-
-
- <%# pp @plans_per_month %> - <%= render partial: 'usage/plans_by_template_chart', - locals: { data: @plans_per_month } %> -
-
-
+ <%= render partial: 'usage/filter' %> +<% end %> diff --git a/config/database.yml.sample b/config/database.yml.sample index a538d9b..76ec376 100644 --- a/config/database.yml.sample +++ b/config/database.yml.sample @@ -3,7 +3,7 @@ encoding: <%= ENV['DB_ADAPTER'] == "mysql2" ? "utf8mb4" : "" %> username: <%= ENV["DB_ADAPTER"] == "postgresql" ? 'postgres' : '' %> database: roadmap_<%= ENV['RAILS_ENV'] %> - pool: 5 + pool: 16 development: <<: *defaults diff --git a/config/initializers/xss-scripting-fix.rb b/config/initializers/xss-scripting-fix.rb new file mode 100644 index 0000000..7a7dbe7 --- /dev/null +++ b/config/initializers/xss-scripting-fix.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# This is a fix for CVE-2020-5267 which isn't patched in Rails4 since it's out +# of lifetime support now + +# TODO: remove this once we are Rails 5+ + +ActionView::Helpers::JavaScriptHelper::JS_ESCAPE_MAP.merge!( + { "`": "\\`", "$": "\\$" } +) + +module ActionView::Helpers::JavaScriptHelper + + alias :old_ej :escape_javascript + alias :old_j :j + + # Monkey patch Rails' :escape_javascript method used in a few of our views + def escape_javascript(javascript) + javascript = javascript.to_s + if javascript.empty? + result = "" + else + result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"']|[`]|[$])/u, JS_ESCAPE_MAP) + end + javascript.html_safe? ? result.html_safe : result + end + + alias :j :escape_javascript + +end diff --git a/config/routes.rb b/config/routes.rb index baa9f38..c995684 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -129,6 +129,7 @@ post 'usage_filter', controller: 'usage', action: 'filter' get 'usage_all_plans_by_template', controller: 'usage', action: 'all_plans_by_template' get 'usage_global_statistics', controller: 'usage', action: 'global_statistics' + get 'usage_org_statistics', controller: 'usage', action: 'org_statistics' get 'usage_yearly_users', controller: 'usage', action: 'yearly_users' get 'usage_yearly_plans', controller: 'usage', action: 'yearly_plans' diff --git a/config/webpack/environment.js b/config/webpack/environment.js index b01021a..96c9836 100644 --- a/config/webpack/environment.js +++ b/config/webpack/environment.js @@ -1,4 +1,5 @@ const { environment } = require('@rails/webpacker') +const erb = require('./loaders/erb') const webpack = require('webpack') const eslint = require('./loaders/eslint') @@ -14,4 +15,5 @@ environment.loaders.prepend('ESLint', eslint) environment.loaders.append('Babel-Loader', babelLoader) +environment.loaders.append('erb', erb) module.exports = environment diff --git a/config/webpack/loaders/erb.js b/config/webpack/loaders/erb.js new file mode 100644 index 0000000..a4049f1 --- /dev/null +++ b/config/webpack/loaders/erb.js @@ -0,0 +1,11 @@ +module.exports = { + test: /\.erb$/, + enforce: 'pre', + exclude: /node_modules/, + use: [{ + loader: 'rails-erb-loader', + options: { + runner: (/^win/.test(process.platform) ? 'ruby ' : '') + 'bin/rails runner' + } + }] +} diff --git a/config/webpacker.yml b/config/webpacker.yml index 6da493b..24546aa 100644 --- a/config/webpacker.yml +++ b/config/webpacker.yml @@ -14,7 +14,9 @@ cache_manifest: false extensions: + - .erb - .js + - .js.erb - .sass - .scss - .css diff --git a/lib/csvable.rb b/lib/csvable.rb index d9ca3ac..7817335 100644 --- a/lib/csvable.rb +++ b/lib/csvable.rb @@ -5,7 +5,7 @@ require "csv" class << self - def from_array_of_hashes(data = [], humanize = true) + def from_array_of_hashes(data = [], humanize = true, sep = ",") return "" unless data.first&.keys if humanize headers = data.first.keys @@ -16,7 +16,7 @@ .map(&:to_s) end - CSV.generate do |csv| + CSV.generate({col_sep: sep}) do |csv| csv << headers data.each do |row| csv << row.values diff --git a/lib/tasks/stat.rake b/lib/tasks/stat.rake index c965c43..78522bc 100644 --- a/lib/tasks/stat.rake +++ b/lib/tasks/stat.rake @@ -4,31 +4,96 @@ task build: :environment do Rake::Task['stat:create:created_plan'].execute Rake::Task['stat:create:joined_user'].execute + Rake::Task['stat:create:shared_plan'].execute + Rake::Task['stat:create:exported_plan'].execute Rake::Task['stat:create_last_month:created_plan'].execute Rake::Task['stat:create_last_month:joined_user'].execute + Rake::Task['stat:create_last_month:shared_plan'].execute + Rake::Task['stat:create_last_month:exported_plan'].execute + end + + task build_parallel: :environment do + tasks = ["stat:create:created_plan", + "stat:create:joined_user", + "stat:create:shared_plan", + "stat:create:exported_plan", + "stat:create_last_month:created_plan", + "stat:create_last_month:joined_user", + "stat:create_last_month:shared_plan", + "stat:create_last_month:exported_plan"] + + Parallel.each(tasks, progress: "Building Stats", in_processes: 4) do |task| + Rake::Task[task].execute + task + end + end + + task build_last_month: :environment do + tasks = ["stat:create_last_month:created_plan", + "stat:create_last_month:joined_user", + "stat:create_last_month:shared_plan", + "stat:create_last_month:exported_plan"] + + tasks.each do |task| + Rake::Task[task].execute + end + end + + task build_last_month_parallel: :environment do + tasks = ["stat:create_last_month:created_plan", + "stat:create_last_month:joined_user", + "stat:create_last_month:shared_plan", + "stat:create_last_month:exported_plan"] + + Parallel.each(tasks) do |task| + Rake::Task[task].execute + task + end end namespace :create do desc "Creates created plan stats for every org since they joined" task created_plan: :environment do - Org::CreateCreatedPlanService.call + Org::CreateCreatedPlanService.call(threads: 2) end desc "Creates joined user stats for every org since they joined" task joined_user: :environment do - Org::CreateJoinedUserService.call + Org::CreateJoinedUserService.call(threads: 2) end + + desc "Creates shared plan stats for every org since they joined" + task shared_plan: :environment do + Org::CreateSharedPlanService.call(threads: 2) + end + + desc "Creates exported plan stats for every org since they joined" + task exported_plan: :environment do + Org::CreateExportedPlanService.call(threads: 2) + end + end namespace :create_last_month do desc "Creates created plan stats for today's last month for every org" task created_plan: :environment do - Org::CreateLastMonthCreatedPlanService.call + Org::CreateLastMonthCreatedPlanService.call(threads: 2) end desc "Creates joined user stats for today's last month for every org" task joined_user: :environment do - Org::CreateLastMonthJoinedUserService.call + Org::CreateLastMonthJoinedUserService.call(threads: 2) + end + + desc "Creates shared plan stats for today's last month for every org" + task shared_plan: :environment do + Org::CreateLastMonthSharedPlanService.call(threads: 2) + end + + desc "created exported plan stats for today's last month for every org" + task exported_plan: :environment do + Org::CreateLastMonthExportedPlanService.call(threads: 2) end end + end diff --git a/package.json b/package.json index 7db48ae..b0e6f39 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "js-cookie": "^2.2.0", "moment": "^2.22.2", "number-to-text": "^0.3.5", + "rails-erb-loader": "^5.5.2", "timeago.js": "4.0.0-beta.1", "tinymce": "^4.9.7", "webpack": "^3.12.0", @@ -55,7 +56,7 @@ "jasmine": "^3.3.0", "jasmine-core": "^3.3.0", "js-yaml": "^3.13.1", - "karma": "^3.1.1", + "karma": "^4.4.1", "karma-babel-preprocessor": "^7.0.0", "karma-chrome-launcher": "^2.2.0", "karma-fixture": "^0.2.6", diff --git a/spec/controllers/usage_controller_spec.rb b/spec/controllers/usage_controller_spec.rb index a0d07f7..a9ac495 100644 --- a/spec/controllers/usage_controller_spec.rb +++ b/spec/controllers/usage_controller_spec.rb @@ -6,11 +6,11 @@ before(:each) do @date = Date.today.last_month.end_of_month @org = create(:org, :organisation) - @details = { "by_template": [stat_details] } + @details = { "by_template": [stat_details], "using_template": [] } @plan_stat = create(:stat_created_plan, date: @date, org: @org, details: @details) @user_stat = create(:stat_joined_user, date: @date, org: @org) - sign_in(create(:user, :org_admin, org: @org)) + sign_in(create(:user, :super_admin, org: @org)) end describe "GET /usage (aka index)" do @@ -156,6 +156,7 @@ def obj_to_hash(obj:) hash = { "count": obj.count, "date": obj.date.strftime("%Y-%m-%d") } hash["by_template"] = obj.details.fetch("by_template", []) if obj.details.present? + hash["using_template"] = obj.details.fetch("using_template", []) if obj.details.present? [hash.to_json] end diff --git a/spec/helpers/usage_helper_spec.rb b/spec/helpers/usage_helper_spec.rb index a77f0d2..84ddd92 100644 --- a/spec/helpers/usage_helper_spec.rb +++ b/spec/helpers/usage_helper_spec.rb @@ -47,8 +47,8 @@ context "with data" do before(:each) do - @template1 = { name: Faker::Lorem.word, count: Faker::Number.number(digits: 1) } - @template2 = { name: Faker::Lorem.word, count: Faker::Number.number(digits: 1) } + @template1 = { name: Faker::Lorem.unique.word, count: Faker::Number.number(digits: 1) } + @template2 = { name: Faker::Lorem.unique.word, count: Faker::Number.number(digits: 1) } @last_month = Date.today.last_month.end_of_month @two_months = Date.today.months_ago(2).end_of_month diff --git a/spec/javascripts/tinymceSpec.js b/spec/javascripts/tinymceSpec.js index 70419c5..a518ec1 100644 --- a/spec/javascripts/tinymceSpec.js +++ b/spec/javascripts/tinymceSpec.js @@ -1,4 +1,4 @@ -import { Tinymce } from '../../app/javascript/utils/tinymce'; +import { Tinymce } from '../../app/javascript/utils/tinymce.js.erb'; beforeEach(() => { $('body').append(''); diff --git a/spec/karma/unit.js b/spec/karma/unit.js index 9e146d1..c97a404 100644 --- a/spec/karma/unit.js +++ b/spec/karma/unit.js @@ -29,7 +29,7 @@ // list of files / patterns to load in the browser files: [ 'node_modules/babel-polyfill/dist/polyfill.js', - 'node_modules/jasmine-jquery/lib/jasmine-jquery.js', + // 'node_modules/jasmine-jquery/lib/jasmine-jquery.js', 'spec/javascripts/**/*Spec.js', 'spec/javascripts/fixtures/*', ], @@ -42,10 +42,14 @@ // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: { 'spec/javascripts/**/*.js': ['webpack'], + 'spec/javascripts/**/*.js.erb': ['webpack'], 'spec/javascripts/fixtures/*.html': ['html2js'], 'spec/javascripts/fixtures/*.json': ['json_fixtures'], }, + webpack: require('../../config/webpack/test.js'), + + // Preprocessor configuration jsonFixturesPreprocessor: { variableName: '__json__', @@ -77,8 +81,15 @@ // start these browsers // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher browsers: [ - 'ChromeHeadless' + 'ChromeHeadlessCustom' ], + // defining a custom browser to let this run in docker + customLaunchers: { + ChromeHeadlessCustom: { + base: 'ChromeHeadless', + flags: ['--no-sandbox'] + } + }, // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits diff --git a/spec/models/stat_created_plan_spec.rb b/spec/models/stat_created_plan_spec.rb index 6ed0f80..da8c586 100644 --- a/spec/models/stat_created_plan_spec.rb +++ b/spec/models/stat_created_plan_spec.rb @@ -67,7 +67,7 @@ count: 0) data = [may, june, july] - csv = described_class.to_csv(data, details: { by_template: true }) + csv = described_class.to_csv(data, details: { by_template: true, sep: ","}) expected_csv = <<~HERE Date,Template1,Template2,Template3,Count diff --git a/spec/policies/usage_policy_spec.rb b/spec/policies/usage_policy_spec.rb index 9949510..3097392 100644 --- a/spec/policies/usage_policy_spec.rb +++ b/spec/policies/usage_policy_spec.rb @@ -6,36 +6,44 @@ subject { described_class.new(user, :usage) } - let(:actions) do + let(:super_actions) do %i[index filter global_statistics yearly_users yearly_plans - all_plans_by_template plans_by_template] + all_plans_by_template plans_by_template org_statistics] + end + let(:org_actions) do + %i[index filter yearly_users yearly_plans + all_plans_by_template plans_by_template org_statistics] end context "super_admin" do let(:user) { create(:user, :super_admin) } it "has access to all actions" do - is_expected.to permit_actions(actions) + is_expected.to permit_actions(super_actions) end end context "org_admin" do let(:user) { create(:user, :org_admin) } - it "has access to all actions" do - is_expected.to permit_actions(actions) + it "has access to all org-admin actions" do + is_expected.to permit_actions(org_actions) + end + + it "does not have access to global statistics" do + is_expected.to forbid_actions(%i[global_statistics]) end end context "user" do let(:user) { create(:user) } it "not have access to any of the actions" do - is_expected.to forbid_actions(actions) + is_expected.to forbid_actions(super_actions) end end context "unauthenticated" do let(:user) { nil } it "not have access to any of the actions" do - actions.each do |action| + super_actions.each do |action| # rubocop:disable Layout/LineLength expect { is_expected.to permit_action(action) }.to raise_error(Pundit::NotAuthorizedError), "expected :#{action} to raise a NotAuthorizedError" # rubocop:enable Layout/LineLength diff --git a/spec/services/org/create_created_plan_service_spec.rb b/spec/services/org/create_created_plan_service_spec.rb index dbd3e61..3a50725 100644 --- a/spec/services/org/create_created_plan_service_spec.rb +++ b/spec/services/org/create_created_plan_service_spec.rb @@ -6,6 +6,9 @@ let(:org) do FactoryBot.create(:org, created_at: DateTime.new(2018, 4, 1)) end + let(:org2) do + FactoryBot.create(:org, created_at: DateTime.new(2018, 4, 1)) + end let(:template) do FactoryBot.create(:template, org: org) end @@ -18,7 +21,9 @@ let(:user2) do FactoryBot.create(:user, org: org) end - + let(:user3) do + FactoryBot.create(:user, org: org2) + end before(:each) do plan = FactoryBot.create(:plan, template: template, @@ -35,6 +40,9 @@ plan5 = FactoryBot.create(:plan, template: template2, created_at: DateTime.new(2018, 6, 3)) + plan6 = FactoryBot.create(:plan, + template: template2, + created_at: DateTime.new(2018, 6, 3)) FactoryBot.create(:role, :creator, plan: plan, @@ -59,6 +67,10 @@ :administrator, plan: plan5, user: user2) + FactoryBot.create(:role, + :creator, + plan: plan6, + user: user3) end def find_by_dates(dates:, org_id:) diff --git a/spec/services/org/create_last_month_created_plan_service_spec.rb b/spec/services/org/create_last_month_created_plan_service_spec.rb index a20807d..765243d 100644 --- a/spec/services/org/create_last_month_created_plan_service_spec.rb +++ b/spec/services/org/create_last_month_created_plan_service_spec.rb @@ -6,6 +6,9 @@ let(:org) do FactoryBot.create(:org, created_at: DateTime.new(2018, 04, 01)) end + let(:org2) do + FactoryBot.create(:org) + end let(:template) do FactoryBot.create(:template, org: org) end @@ -18,6 +21,9 @@ let(:user2) do FactoryBot.create(:user, org: org) end + let(:user3) do + FactoryBot.create(:user, org: org2) + end let(:creator) { Role.access_values_for(:creator).first } let(:administrator) { Role.access_values_for(:administrator).first } before(:each) do @@ -30,10 +36,14 @@ plan3 = FactoryBot.create(:plan, template: template2, created_at: Date.today.last_month) + plan4 = FactoryBot.create(:plan, + template: template2, + created_at: Date.today.last_month) FactoryBot.create(:role, :creator, plan: plan, user: user1) FactoryBot.create(:role, :administrator, plan: plan, user: user1) FactoryBot.create(:role, :creator, plan: plan2, user: user1) FactoryBot.create(:role, :creator, plan: plan3, user: user2) + FactoryBot.create(:role, :creator, plan: plan4, user: user3) end describe ".call" do @@ -52,16 +62,31 @@ last_month_details = StatCreatedPlan.find_by( date: Date.today.last_month.end_of_month, - org_id: org.id).details + org_id: org.id).by_template expect(last_month_details).to match_array( - "by_template" => [ + [ { "name" => template.title, "count" => 2 }, { "name" => template2.title, "count" => 1 }, ] ) end + it "generates counts by template from today's last month" do + described_class.call(org) + + last_month_details = StatCreatedPlan.find_by( + date: Date.today.last_month.end_of_month, + org_id: org.id).using_template + + expect(last_month_details).to match_array( + [ + { "name" => template.title, "count" => 2 }, + { "name" => template2.title, "count" => 2 }, + ] + ) + end + it "monthly records are either created or updated" do described_class.call(org) @@ -108,16 +133,33 @@ last_month_details = StatCreatedPlan.find_by( date: Date.today.last_month.end_of_month, - org_id: org.id).details + org_id: org.id).by_template expect(last_month_details).to match_array( - "by_template" => [ + [ { "name" => template.title, "count" => 2 }, { "name" => template2.title, "count" => 1 }, ] ) end + it "generates counts using template from today's last month" do + Org.expects(:all).returns([org]) + + described_class.call + + last_month_details = StatCreatedPlan.find_by( + date: Date.today.last_month.end_of_month, + org_id: org.id).using_template + + expect(last_month_details).to match_array( + [ + { "name" => template.title, "count" => 2 }, + { "name" => template2.title, "count" => 2 }, + ] + ) + end + it "monthly records are either created or updated" do Org.stubs(:all).returns([org]) diff --git a/yarn.lock b/yarn.lock index dadc79b..34b85a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -440,11 +440,6 @@ define-properties "^1.1.3" es-abstract "^1.17.0-next.0" -array-slice@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5" - integrity sha1-3Tz7gO15c6dRF82sabC5nshhhvU= - array-union@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" @@ -1376,13 +1371,6 @@ balanced-match "^1.0.0" concat-map "0.0.1" -braces@^0.1.2: - version "0.1.5" - resolved "https://registry.yarnpkg.com/braces/-/braces-0.1.5.tgz#c085711085291d8b75fdd74eab0f8597280711e6" - integrity sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY= - dependencies: - expand-range "^0.1.0" - braces@^1.8.2: version "1.8.5" resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" @@ -1408,7 +1396,7 @@ split-string "^3.0.2" to-regex "^3.0.1" -braces@~3.0.2: +braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -2007,7 +1995,7 @@ resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= -chokidar@*: +chokidar@*, chokidar@^3.0.0: version "3.3.1" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450" integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg== @@ -2038,7 +2026,7 @@ optionalDependencies: fsevents "^1.0.0" -chokidar@^2.0.2, chokidar@^2.0.3, chokidar@^2.1.8: +chokidar@^2.0.2, chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== @@ -2070,11 +2058,6 @@ inherits "^2.0.1" safe-buffer "^5.0.1" -circular-json@^0.5.5: - version "0.5.9" - resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.5.9.tgz#932763ae88f4f7dead7a0d09c8a51a4743a53b1d" - integrity sha512-4ivwqHpIFJZBuhN3g/pEcdbnGUywkBblloGbkglyloVjjR3uT6tieI89MVOfbP2tHX5sgb01FuLgAOzebNlJNQ== - clap@^1.0.9: version "1.2.3" resolved "https://registry.yarnpkg.com/clap/-/clap-1.2.3.tgz#4f36745b32008492557f46412d66d50cb99bce51" @@ -2273,13 +2256,6 @@ resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" integrity sha1-FopHAXVran9RoSzgyXv6KMCE7WM= -combine-lists@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/combine-lists/-/combine-lists-1.0.1.tgz#458c07e09e0d900fc28b70a3fec2dacd1d2cb7f6" - integrity sha1-RYwH4J4NkA/Ci3Cj/sLazR0st/Y= - dependencies: - lodash "^4.5.0" - combine-source-map@^0.8.0, combine-source-map@~0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.8.0.tgz#a58d0df042c186fcf822a8e8015f5450d2d79a8b" @@ -2496,7 +2472,7 @@ resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= -core-js@^2.2.0, core-js@^2.4.0, core-js@^2.5.0: +core-js@^2.4.0, core-js@^2.5.0: version "2.6.11" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg== @@ -2865,10 +2841,10 @@ dependencies: assert-plus "^1.0.0" -date-format@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/date-format/-/date-format-1.2.0.tgz#615e828e233dd1ab9bb9ae0950e0ceccfa6ecad8" - integrity sha1-YV6CjiM90aubua4JUODOzPpuytg= +date-format@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-2.1.0.tgz#31d5b5ea211cf5fd764cd38baf9d033df7e125cf" + integrity sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA== debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: version "2.6.9" @@ -3660,15 +3636,6 @@ signal-exit "^3.0.0" strip-eof "^1.0.0" -expand-braces@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/expand-braces/-/expand-braces-0.1.2.tgz#488b1d1d2451cb3d3a6b192cfc030f44c5855fea" - integrity sha1-SIsdHSRRyz06axks/AMPRMWFX+o= - dependencies: - array-slice "^0.2.3" - array-unique "^0.2.1" - braces "^0.1.2" - expand-brackets@^0.1.4: version "0.1.5" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" @@ -3689,14 +3656,6 @@ snapdragon "^0.8.1" to-regex "^3.0.1" -expand-range@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-0.1.1.tgz#4cb8eda0993ca56fa4f41fc42f3cbb4ccadff044" - integrity sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ= - dependencies: - is-number "^0.1.1" - repeat-string "^0.2.2" - expand-range@^1.8.1: version "1.8.2" resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" @@ -4129,7 +4088,7 @@ path-is-absolute "^1.0.0" rimraf "^2.2.8" -fs-extra@^7.0.0: +fs-extra@^7.0.0, fs-extra@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== @@ -5084,11 +5043,6 @@ dependencies: is-extglob "^2.1.1" -is-number@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-0.1.1.tgz#69a7af116963d47206ec9bd9b48a14216f1e3806" - integrity sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY= - is-number@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" @@ -5502,28 +5456,26 @@ source-map "^0.5.6" webpack-dev-middleware "^2.0.6" -karma@^3.1.1: - version "3.1.4" - resolved "https://registry.yarnpkg.com/karma/-/karma-3.1.4.tgz#3890ca9722b10d1d14b726e1335931455788499e" - integrity sha512-31Vo8Qr5glN+dZEVIpnPCxEGleqE0EY6CtC2X9TagRV3rRQ3SNrvfhddICkJgUK3AgqpeKSZau03QumTGhGoSw== +karma@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/karma/-/karma-4.4.1.tgz#6d9aaab037a31136dc074002620ee11e8c2e32ab" + integrity sha512-L5SIaXEYqzrh6b1wqYC42tNsFMx2PWuxky84pK9coK09MvmL7mxii3G3bZBh/0rvD27lqDd0le9jyhzvwif73A== dependencies: bluebird "^3.3.0" body-parser "^1.16.1" - chokidar "^2.0.3" + braces "^3.0.2" + chokidar "^3.0.0" colors "^1.1.0" - combine-lists "^1.0.0" connect "^3.6.0" - core-js "^2.2.0" di "^0.0.1" dom-serialize "^2.2.0" - expand-braces "^0.1.1" flatted "^2.0.0" glob "^7.1.1" graceful-fs "^4.1.2" http-proxy "^1.13.0" isbinaryfile "^3.0.0" - lodash "^4.17.5" - log4js "^3.0.0" + lodash "^4.17.14" + log4js "^4.0.0" mime "^2.3.1" minimatch "^3.0.2" optimist "^0.6.1" @@ -5751,7 +5703,7 @@ resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.5.0, lodash@~4.17.10: +"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@~4.17.10: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -5763,16 +5715,16 @@ dependencies: chalk "^2.0.1" -log4js@^3.0.0: - version "3.0.6" - resolved "https://registry.yarnpkg.com/log4js/-/log4js-3.0.6.tgz#e6caced94967eeeb9ce399f9f8682a4b2b28c8ff" - integrity sha512-ezXZk6oPJCWL483zj64pNkMuY/NcRX5MPiB0zE6tjZM137aeusrOnW1ecxgF9cmwMWkBMhjteQxBPoZBh9FDxQ== +log4js@^4.0.0: + version "4.5.1" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-4.5.1.tgz#e543625e97d9e6f3e6e7c9fc196dd6ab2cae30b5" + integrity sha512-EEEgFcE9bLgaYUKuozyFfytQM2wDHtXn4tAN41pkaxpNjAykv11GVdeI4tHtmPWW4Xrgh9R/2d7XYghDVjbKKw== dependencies: - circular-json "^0.5.5" - date-format "^1.2.0" - debug "^3.1.0" - rfdc "^1.1.2" - streamroller "0.7.0" + date-format "^2.0.0" + debug "^4.1.1" + flatted "^2.0.0" + rfdc "^1.1.4" + streamroller "^1.0.6" loglevel@^1.6.4: version "1.6.6" @@ -8062,6 +8014,14 @@ resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== +rails-erb-loader@^5.5.2: + version "5.5.2" + resolved "https://registry.yarnpkg.com/rails-erb-loader/-/rails-erb-loader-5.5.2.tgz#db3fa8ac89600f09d179a1a70a2ca18c592576ea" + integrity sha512-cjQH9SuSvRPhnWkvjmmAW/S4AFVDfAtYnQO4XpKJ8xpRdZayT73iXoE+IPc3VzN03noZXhVmyvsCvKvHj4LY6w== + dependencies: + loader-utils "^1.1.0" + lodash.defaults "^4.2.0" + random-bytes@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" @@ -8154,7 +8114,7 @@ normalize-package-data "^2.3.2" path-type "^2.0.0" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== @@ -8338,11 +8298,6 @@ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== -repeat-string@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-0.2.2.tgz#c7a8d3236068362059a7e4651fc6884e8b1fb4ae" - integrity sha1-x6jTI2BoNiBZp+RlH8aITosftK4= - repeat-string@^1.5.2, repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" @@ -8469,7 +8424,7 @@ resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= -rfdc@^1.1.2: +rfdc@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2" integrity sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug== @@ -9241,15 +9196,16 @@ inherits "^2.0.1" readable-stream "^2.0.2" -streamroller@0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-0.7.0.tgz#a1d1b7cf83d39afb0d63049a5acbf93493bdf64b" - integrity sha512-WREzfy0r0zUqp3lGO096wRuUp7ho1X6uo/7DJfTlEi0Iv/4gT7YHqXDjKC2ioVGBZtE8QzsQD9nx1nIuoZ57jQ== +streamroller@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-1.0.6.tgz#8167d8496ed9f19f05ee4b158d9611321b8cacd9" + integrity sha512-3QC47Mhv3/aZNFpDDVO44qQb9gwB9QggMEE0sQmkTAwBVYdBRWISdsywlkfm5II1Q5y/pmrHflti/IgmIzdDBg== dependencies: - date-format "^1.2.0" - debug "^3.1.0" - mkdirp "^0.5.1" - readable-stream "^2.3.0" + async "^2.6.2" + date-format "^2.0.0" + debug "^3.2.6" + fs-extra "^7.0.1" + lodash "^4.17.14" strict-uri-encode@^1.0.0: version "1.1.0"