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} %>
-<%= _("Do you have a %{application_name} account?") % { application_name: Rails.configuration.branding[:application][:name]} %> - -
+
+
<%= _("This will link your existing account to your credentials.") %> @@ -24,17 +30,17 @@
<%= _("No %{application_name} account?") % { application_name: Rails.configuration.branding[:application][:name]} %> - +
<%= _("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') %>