diff --git a/app/assets/stylesheets/utils/_margins.scss b/app/assets/stylesheets/utils/_margins.scss index 83151d1..1461cc8 100644 --- a/app/assets/stylesheets/utils/_margins.scss +++ b/app/assets/stylesheets/utils/_margins.scss @@ -10,3 +10,6 @@ .mt-10 { margin-top: 10px; } +.mt-20 { + margin-top: 20px; +} diff --git a/app/controllers/usage_controller.rb b/app/controllers/usage_controller.rb index a469ee9..005b1f3 100644 --- a/app/controllers/usage_controller.rb +++ b/app/controllers/usage_controller.rb @@ -15,6 +15,7 @@ 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 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/views/usage/index.js b/app/javascript/views/usage/index.js index d596af6..8967dd8 100644 --- a/app/javascript/views/usage/index.js +++ b/app/javascript/views/usage/index.js @@ -37,25 +37,40 @@ // 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); + 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_template_plans'); + window.templatePlansChart = document.getElementById('monthly_plans_by_template'); }); monthlyPlanTemplatesChart.addEventListener('destroyChart', () => { - if (drawnChart) { - drawnChart.destroy(); + if (drawnChartByTemplate) { + drawnChartByTemplate.destroy(); } }); + 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 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 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_created_plan.rb b/app/models/stat_created_plan.rb index 4d4bdf8..75b645d 100644 --- a/app/models/stat_created_plan.rb +++ b/app/models/stat_created_plan.rb @@ -21,14 +21,21 @@ 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 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/views/usage/_filter.html.erb b/app/views/usage/_filter.html.erb index c1de3f2..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 %>
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..5d0288f --- /dev/null +++ b/app/views/usage/_template_statistics.html.erb @@ -0,0 +1,43 @@ +<%# locals: expanded %> + +
+ + + + +
+
+ +
+
+

<%=_('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/_total_usage.html.erb b/app/views/usage/_total_usage.html.erb index c7814b7..6fb0a87 100644 --- a/app/views/usage/_total_usage.html.erb +++ b/app/views/usage/_total_usage.html.erb @@ -1,41 +1,34 @@ <%# locals: user_count, plan_count, separators %> -
-
-
-

Usage statistics

-
-
-
- - <%= 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', role: 'button', target: '_blank' do %> - <%= _('Download global usage') %> - <% end %> - <% end %> -
-
- <%= link_to usage_org_statistics_path(sep: ","), class: 'stat btn btn-default', role: 'button', target: '_blank' do %> - <%= _('Download Monthly Usage') %> - <% end %> -
-
-
-
-
+
+

<%= user_count.to_i %> Total users

-
+

<%= plan_count.to_i %> Total plans

+
+
+ + <%= 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', role: 'button', target: '_blank' do %> + <%= _('Download global usage') %> + <% end %> + <% end %> +
+
+ <%= link_to usage_org_statistics_path(sep: ","), class: 'stat btn btn-default pull-right', role: 'button', target: '_blank' do %> + <%= _('Download Monthly Usage') %> + <% 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..424b688 --- /dev/null +++ b/app/views/usage/_user_statistics.html.erb @@ -0,0 +1,127 @@ +<%# locals: expanded %> + +
+ + + + +
+
+ +
+
+

+ <%= _('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/index.html.erb b/app/views/usage/index.html.erb index 7ed3381..65446a0 100644 --- a/app/views/usage/index.html.erb +++ b/app/views/usage/index.html.erb @@ -1,98 +1,21 @@ <% title _('Usage statistics') %> +
+
+

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

+
+
+ <%= render partial: 'usage/total_usage', - locals: { user_count: @total_org_users, plan_count: @total_org_plans, separators: @separators } %> + locals: { user_count: @total_org_users, + plan_count: @total_org_plans, + separators: @separators } %> + + +
+ <%= render partial: 'usage/template_statistics', locals: {expanded: @funder.present?} %> + <%= render partial: 'usage/user_statistics', locals: {expanded: !@funder.present?} %> +
+ <%= render partial: 'usage/filter' %> - - -
-
-

* <%= _('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/spec/controllers/usage_controller_spec.rb b/spec/controllers/usage_controller_spec.rb index da77804..a9ac495 100644 --- a/spec/controllers/usage_controller_spec.rb +++ b/spec/controllers/usage_controller_spec.rb @@ -6,7 +6,7 @@ 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) @@ -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/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])