diff --git a/app/actions/stat_created_plan/generate.rb b/app/actions/stat_created_plan/generate.rb new file mode 100644 index 0000000..ac88619 --- /dev/null +++ b/app/actions/stat_created_plan/generate.rb @@ -0,0 +1,48 @@ +module Actions + module StatCreatedPlan + class Generate + class << self + def full(org) + OrgDateRangeable.split_months_from_creation(org) do |start_date, end_date| + create_count_for_date(start_date: start_date, end_date: end_date, org: org) + end + end + + def last_month(org) + months = OrgDateRangeable.split_months_from_creation(org) + last = months.last + if last.present? + create_count_for_date(start_date: last[:start_date], end_date: last[:end_date], org: org) + end + end + + def full_all_orgs + Org.all.each do |org| + full(org) + end + end + + def last_month_all_orgs + Org.all.each do |org| + last_month(org) + end + end + + private + + def count_plans(start_date: , end_date: , org:) + users = User.where('users.org_id = ?', org.id) + plans = Plan.where('plans.created_at >= ? AND plans.created_at <= ?', start_date, end_date) + creator_admon = Role.with_access_flags(:creator, :administrator) + + Role.joins([:plan, :user]).merge(creator_admon).merge(users).merge(plans).select(:plan_id).distinct.count + end + + def create_count_for_date(start_date:, end_date:, org:) + count = count_plans(start_date: start_date, end_date: end_date, org: org) + ::StatCreatedPlan.create(date: end_date.to_date, count: count, org_id: org.id) + end + end + end + end +end diff --git a/app/actions/stat_joined_user/generate.rb b/app/actions/stat_joined_user/generate.rb new file mode 100644 index 0000000..bb03181 --- /dev/null +++ b/app/actions/stat_joined_user/generate.rb @@ -0,0 +1,43 @@ +module Actions + module StatJoinedUser + class Generate + class << self + def full(org) + OrgDateRangeable.split_months_from_creation(org) do |start_date, end_date| + create_count_for_date(start_date: start_date, end_date: end_date, org: org) + end + end + + def last_month(org) + months = OrgDateRangeable.split_months_from_creation(org) + last = months.last + if last.present? + create_count_for_date(start_date: last[:start_date], end_date: last[:end_date], org: org) + end + end + + def full_all_orgs + Org.all.each do |org| + full(org) + end + end + + def last_month_all_orgs + Org.all.each do |org| + last_month(org) + end + end + + private + def count_users(start_date: , end_date: , org_id: ) + User.where('created_at >= ? AND created_at <= ? AND org_id = ?', start_date, end_date, org_id).count + end + + def create_count_for_date(start_date:, end_date:, org:) + count = count_users(start_date: start_date, end_date: end_date, org_id: org.id) + ::StatJoinedUser.create(date: end_date.to_date, count: count, org_id: org.id) + end + end + end + end +end diff --git a/app/models/stat.rb b/app/models/stat.rb new file mode 100644 index 0000000..18a59e2 --- /dev/null +++ b/app/models/stat.rb @@ -0,0 +1,12 @@ +class Stat < ActiveRecord::Base + belongs_to :org + + class << self + def to_csv(stats) + data = stats.map do |stat| + { date: stat.date, count: stat.count } + end + Csvable.from_array_of_hashes(data) + end + end +end diff --git a/app/models/stat_created_plan.rb b/app/models/stat_created_plan.rb new file mode 100644 index 0000000..4158c7c --- /dev/null +++ b/app/models/stat_created_plan.rb @@ -0,0 +1,9 @@ +class StatCreatedPlan < Stat + extend OrgDateRangeable + + class << self + def to_csv(created_plans) + Stat.to_csv(created_plans) + end + end +end diff --git a/app/models/stat_joined_user.rb b/app/models/stat_joined_user.rb new file mode 100644 index 0000000..d9e0601 --- /dev/null +++ b/app/models/stat_joined_user.rb @@ -0,0 +1,9 @@ +class StatJoinedUser < Stat + extend OrgDateRangeable + + class << self + def to_csv(joined_users) + Stat.to_csv(joined_users) + end + end +end diff --git a/db/migrate/20180901095920_create_stats.rb b/db/migrate/20180901095920_create_stats.rb new file mode 100644 index 0000000..7e1848e --- /dev/null +++ b/db/migrate/20180901095920_create_stats.rb @@ -0,0 +1,11 @@ +class CreateStats < ActiveRecord::Migration + def change + create_table :stats do |t| + t.bigint :count, default: 0 + t.date :date, null: false + t.string :type, null: false + t.belongs_to :org + t.timestamps null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 4c756f5..974c823 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180815180221) do +ActiveRecord::Schema.define(version: 20180901095920) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -301,6 +301,15 @@ t.datetime "updated_at", null: false end + create_table "stats", force: :cascade do |t| + t.integer "count", limit: 8, default: 0 + t.date "date", null: false + t.string "type", null: false + t.integer "org_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "templates", force: :cascade do |t| t.string "title" t.text "description" diff --git a/lib/csvable.rb b/lib/csvable.rb new file mode 100644 index 0000000..2309ae2 --- /dev/null +++ b/lib/csvable.rb @@ -0,0 +1,14 @@ +module Csvable + class << self + def from_array_of_hashes(data = []) + return '' unless data.first&.keys + headers = data.first.keys + CSV.generate do |csv| + csv << headers + data.each do |row| + csv << row.values + end + end + end + end +end diff --git a/lib/org_date_rangeable.rb b/lib/org_date_rangeable.rb new file mode 100644 index 0000000..67c96e5 --- /dev/null +++ b/lib/org_date_rangeable.rb @@ -0,0 +1,23 @@ +module OrgDateRangeable + def monthly_range(org:, start_date: Date.today.end_of_month, end_date: Date.today.end_of_month) + where("org_id = :org_id and date >= :start_date and date <= :end_date", org_id: org&.id, start_date: start_date, end_date: end_date) + end + + class << self + def split_months_from_creation(org, &block) + starts_at = org.created_at + ends_at = starts_at.end_of_month + callable = block.nil? ? Proc.new {} : lambda{ | start_date, end_date| block.call(start_date, end_date) } + enumerable = [] + + while !(starts_at.future? || ends_at.future?) do + callable.call(starts_at, ends_at) + enumerable << { start_date: starts_at, end_date: ends_at } + starts_at = starts_at.next_month.beginning_of_month + ends_at = starts_at.end_of_month + end + + enumerable + end + end +end diff --git a/lib/tasks/stat.rake b/lib/tasks/stat.rake new file mode 100644 index 0000000..c5989f8 --- /dev/null +++ b/lib/tasks/stat.rake @@ -0,0 +1,32 @@ +require_relative '../../app/actions/stat_created_plan/generate' +require_relative '../../app/actions/stat_joined_user/generate' + +namespace :stat do + namespace :created_plan do + namespace :generate do + desc "Generate created plan stats for every org since they joined" + task full_all_orgs: :environment do + Actions::StatCreatedPlan::Generate.full_all_orgs + end + + desc "Generate created plan stats for today's last month for every org" + task last_month_all_orgs: :environment do + Actions::StatCreatedPlan::Generate.last_month_all_orgs + end + end + end + + namespace :joined_user do + namespace :generate do + desc "Generate joined user stats for every org since they joined" + task full_all_orgs: :environment do + Actions::StatJoinedUser::Generate.full_all_orgs + end + + desc "Generate joined user stats for today's last month for every org" + task last_month_all_orgs: :environment do + Actions::StatJoinedUser::Generate.last_month_all_orgs + end + end + end +end diff --git a/spec/actions/stat_created_plan/generate_spec.rb b/spec/actions/stat_created_plan/generate_spec.rb new file mode 100644 index 0000000..ea7b023 --- /dev/null +++ b/spec/actions/stat_created_plan/generate_spec.rb @@ -0,0 +1,104 @@ +require 'rails_helper' +require_relative '../../../app/actions/stat_created_plan/generate' + +RSpec.describe Actions::StatCreatedPlan::Generate do + let(:org) do + FactoryBot.create(:org, created_at: DateTime.new(2018,04,01)) + end + let(:template) do + FactoryBot.create(:template, org: org) + end + let(:user1) do + FactoryBot.create(:user, org: org) + end + let(:user2) do + FactoryBot.create(:user, org: org) + end + let(:creator) { Role.access_values_for(:creator).first } + let(:administrator) { Role.access_values_for(:administrator).first } + describe '.full' do + it "returns monthly aggregates since org's creation" do + plan = FactoryBot.create(:plan, template: template, created_at: DateTime.new(2018,04,01)) + plan2 = FactoryBot.create(:plan, template: template, created_at: DateTime.new(2018,04,03)) + plan3 = FactoryBot.create(:plan, template: template, created_at: DateTime.new(2018,05,02)) + plan4 = FactoryBot.create(:plan, template: template, created_at: DateTime.new(2018,06,02)) + plan5 = FactoryBot.create(:plan, template: template, created_at: DateTime.new(2018,06,03)) + FactoryBot.create(:role, plan: plan, user: user1, access: creator) + FactoryBot.create(:role, plan: plan, user: user2, access: administrator) + FactoryBot.create(:role, plan: plan2, user: user1, access: creator) + FactoryBot.create(:role, plan: plan3, user: user1, access: creator) + FactoryBot.create(:role, plan: plan4, user: user2, access: administrator) + FactoryBot.create(:role, plan: plan5, user: user2, access: administrator) + + described_class.full(org) + + april = StatCreatedPlan.find_by(date: '2018-04-30', org_id: org.id).count + may = StatCreatedPlan.find_by(date: '2018-05-31', org_id: org.id).count + june = StatCreatedPlan.find_by(date: '2018-06-30', org_id: org.id).count + july = StatCreatedPlan.find_by(date: '2018-07-31', org_id: org.id).count + + expect([april, may, june, july]).to eq([2,1,2,0]) + end + end + + describe '.last_month' do + it "returns aggregates from today's last month" do + plan = FactoryBot.create(:plan, created_at: Date.today.last_month) + plan2 = FactoryBot.create(:plan, created_at: Date.today.last_month) + plan3 = FactoryBot.create(:plan, created_at: Date.today.last_month) + FactoryBot.create(:role, plan: plan, user: user1, access: creator) + FactoryBot.create(:role, plan: plan, user: user1, access: administrator) + FactoryBot.create(:role, plan: plan2, user: user1, access: creator) + FactoryBot.create(:role, plan: plan3, user: user2, access: creator) + + described_class.last_month(org) + + last_month = StatCreatedPlan.find_by(date: Date.today.last_month.end_of_month, org_id: org.id).count + expect(last_month).to eq(3) + end + end + + describe '.full_all_orgs' do + it 'returns monthly aggregates for each org since their creation' do + Org.stubs(:all).returns([org]) + plan = FactoryBot.create(:plan, template: template, created_at: DateTime.new(2018,04,01)) + plan2 = FactoryBot.create(:plan, template: template, created_at: DateTime.new(2018,04,03)) + plan3 = FactoryBot.create(:plan, template: template, created_at: DateTime.new(2018,05,02)) + plan4 = FactoryBot.create(:plan, template: template, created_at: DateTime.new(2018,06,02)) + plan5 = FactoryBot.create(:plan, template: template, created_at: DateTime.new(2018,06,03)) + FactoryBot.create(:role, plan: plan, user: user1, access: creator) + FactoryBot.create(:role, plan: plan, user: user2, access: administrator) + FactoryBot.create(:role, plan: plan2, user: user1, access: creator) + FactoryBot.create(:role, plan: plan3, user: user1, access: creator) + FactoryBot.create(:role, plan: plan4, user: user2, access: administrator) + FactoryBot.create(:role, plan: plan5, user: user2, access: administrator) + + described_class.full_all_orgs + + april = StatCreatedPlan.find_by(date: '2018-04-30', org_id: org.id).count + may = StatCreatedPlan.find_by(date: '2018-05-31', org_id: org.id).count + june = StatCreatedPlan.find_by(date: '2018-06-30', org_id: org.id).count + july = StatCreatedPlan.find_by(date: '2018-07-31', org_id: org.id).count + + expect([april, may, june, july]).to eq([2,1,2,0]) + end + end + + describe '.last_month_all_orgs' do + it "returns aggregates from today's last month" do + Org.expects(:all).returns([org]) + plan = FactoryBot.create(:plan, created_at: Date.today.last_month) + plan2 = FactoryBot.create(:plan, created_at: Date.today.last_month) + plan3 = FactoryBot.create(:plan, created_at: Date.today.last_month) + FactoryBot.create(:role, plan: plan, user: user1, access: creator) + FactoryBot.create(:role, plan: plan, user: user1, access: administrator) + FactoryBot.create(:role, plan: plan2, user: user1, access: creator) + FactoryBot.create(:role, plan: plan3, user: user2, access: creator) + + described_class.last_month_all_orgs + + last_month = StatCreatedPlan.find_by(date: Date.today.last_month.end_of_month, org_id: org.id).count + expect(last_month).to eq(3) + end + end +end diff --git a/spec/actions/stat_joined_user/generate_spec.rb b/spec/actions/stat_joined_user/generate_spec.rb new file mode 100644 index 0000000..f102a09 --- /dev/null +++ b/spec/actions/stat_joined_user/generate_spec.rb @@ -0,0 +1,67 @@ +require 'rails_helper' +require_relative '../../../app/actions/stat_joined_user/generate' + +RSpec.describe Actions::StatJoinedUser::Generate do + let(:org) do + FactoryBot.create(:org, created_at: DateTime.new(2018,04,01)) + end + describe '.full' do + it "returns monthly aggregates since org's creation" do + april = [FactoryBot.create(:user, org: org, created_at: DateTime.new(2018,04,03,0,0,0)), FactoryBot.create(:user, org: org, created_at: DateTime.new(2018,04,04,0,0,0))] + may = [FactoryBot.create(:user, org: org, created_at: DateTime.new(2018,05,03,0,0,0))] + june = [FactoryBot.create(:user, org: org, created_at: DateTime.new(2018,06,03,0,0,0)), FactoryBot.create(:user, org: org, created_at: DateTime.new(2018,06,04,0,0,0))] + + described_class.full(org) + + april = StatJoinedUser.find_by(date: '2018-04-30', org_id: org.id).count + may = StatJoinedUser.find_by(date: '2018-05-31', org_id: org.id).count + june = StatJoinedUser.find_by(date: '2018-06-30', org_id: org.id).count + july = StatJoinedUser.find_by(date: '2018-07-31', org_id: org.id).count + expect([april, may, june, july]).to eq([2,1,2,0]) + end + end + + describe '.last_month' do + it "returns aggregates from today's last month" do + 5.times do + FactoryBot.create(:user, org: org, created_at: Date.today.last_month) + end + + described_class.last_month(org) + + last_month = StatJoinedUser.find_by(date: Date.today.last_month.end_of_month, org_id: org.id).count + expect(last_month).to eq(5) + end + end + + describe '.full_all_orgs' do + it "returns monthly aggregates for each org since their creation" do + Org.expects(:all).returns([org]) + april = [FactoryBot.create(:user, org: org, created_at: DateTime.new(2018,04,03,0,0,0)), FactoryBot.create(:user, org: org, created_at: DateTime.new(2018,04,04,0,0,0))] + may = [FactoryBot.create(:user, org: org, created_at: DateTime.new(2018,05,03,0,0,0))] + june = [FactoryBot.create(:user, org: org, created_at: DateTime.new(2018,06,03,0,0,0)), FactoryBot.create(:user, org: org, created_at: DateTime.new(2018,06,04,0,0,0))] + + described_class.full_all_orgs + + april = StatJoinedUser.find_by(date: '2018-04-30', org_id: org.id).count + may = StatJoinedUser.find_by(date: '2018-05-31', org_id: org.id).count + june = StatJoinedUser.find_by(date: '2018-06-30', org_id: org.id).count + july = StatJoinedUser.find_by(date: '2018-07-31', org_id: org.id).count + expect([april, may, june, july]).to eq([2,1,2,0]) + end + end + + describe '.last_month_all_orgs' do + it "returns aggregates from today's last month" do + Org.expects(:all).returns([org]) + 5.times do + FactoryBot.create(:user, org: org, created_at: Date.today.last_month) + end + + described_class.last_month_all_orgs + + last_month = StatJoinedUser.find_by(date: Date.today.last_month.end_of_month, org_id: org.id).count + expect(last_month).to eq(5) + end + end +end diff --git a/spec/csvable_spec.rb b/spec/csvable_spec.rb new file mode 100644 index 0000000..e92ce63 --- /dev/null +++ b/spec/csvable_spec.rb @@ -0,0 +1,38 @@ +require 'rails_helper' + +RSpec.describe Csvable do + describe '.from_array_of_hashes' do + let(:data) do + [ + { column1: 'value row1.1', column2: 'value row1.2' }, + { column1: 'value row2.1', column2: 'value row2.2' }, + { column1: 'value row3.1', column2: 'value row3.2' }, + ] + end + + it 'returns empty string' do + stringified_csv = described_class.from_array_of_hashes([]) + + expect(stringified_csv).to be_empty + end + + it 'first row describes columns' do + stringified_csv = described_class.from_array_of_hashes(data) + + header = /[^\n]+/.match(stringified_csv)[0] + expect("column1,column2").to eq(header) + end + + it 'returns each hash within the array' do + stringified_csv = described_class.from_array_of_hashes(data) + + output = <<~HERE + column1,column2 + value row1.1,value row1.2 + value row2.1,value row2.2 + value row3.1,value row3.2 + HERE + expect(stringified_csv).to eq(output) + end + end +end diff --git a/spec/factories/stat_created_plan.rb b/spec/factories/stat_created_plan.rb new file mode 100644 index 0000000..f0ad5b9 --- /dev/null +++ b/spec/factories/stat_created_plan.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :stat_created_plan do + date { Date.today } + org { create(:org) } + count { Faker::Number.number(10) } + end +end diff --git a/spec/factories/stat_joined_user.rb b/spec/factories/stat_joined_user.rb new file mode 100644 index 0000000..32205ce --- /dev/null +++ b/spec/factories/stat_joined_user.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :stat_joined_user do + date { Date.today } + org { create(:org) } + count { Faker::Number.number(10) } + end +end diff --git a/spec/models/stat_created_plan_spec.rb b/spec/models/stat_created_plan_spec.rb new file mode 100644 index 0000000..f2a3333 --- /dev/null +++ b/spec/models/stat_created_plan_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' + +RSpec.describe StatCreatedPlan, type: :model do + describe '.to_csv' do + context 'when no instances' do + it 'returns empty' do + csv = described_class.to_csv([]) + + expect(csv).to be_empty + end + end + context 'when instances' do + let(:org) { FactoryBot.create(:org) } + it 'returns instances in a comma-separated row' do + may = FactoryBot.create(:stat_created_plan, date: Date.new(2018, 05, 31), org: org, count: 20) + june = FactoryBot.create(:stat_created_plan, date: Date.new(2018, 06, 30), org: org, count: 10) + data = [may, june] + + csv = described_class.to_csv(data) + + expected_csv = <<~HERE + date,count + 2018-05-31,20 + 2018-06-30,10 + HERE + expect(csv).to eq(expected_csv) + end + end + end +end diff --git a/spec/models/stat_joined_user_spec.rb b/spec/models/stat_joined_user_spec.rb new file mode 100644 index 0000000..c096dcd --- /dev/null +++ b/spec/models/stat_joined_user_spec.rb @@ -0,0 +1,55 @@ +require 'rails_helper' + +RSpec.describe StatJoinedUser, type: :model do + before(:example) do + @org = FactoryBot.create(:org) + end + + describe '.monthly_range' do + context 'when org is missing' do + it 'raises ArgumentError' do + expect do + described_class.monthly_range + end.to raise_error(ArgumentError) + end + end + it 'returns matching instances' do + start_date = Date.new(2018, 04, 30) + end_date = Date.new(2018, 05, 31) + june = FactoryBot.create(:stat_joined_user, date: Date.new(2018, 06, 30), org: @org) + may = FactoryBot.create(:stat_joined_user, date: end_date, org: @org) + april = FactoryBot.create(:stat_joined_user, date: start_date, org: @org) + + april_to_may = described_class.monthly_range(org: @org, start_date: start_date, end_date: end_date) + + expect(april_to_may).to include(april, may) + end + end + + describe '.to_csv' do + context 'when no instances' do + it 'returns empty' do + csv = described_class.to_csv([]) + + expect(csv).to be_empty + end + end + context 'when instances' do + let(:org) { FactoryBot.create(:org) } + it 'returns instances in a comma-separated row' do + may = FactoryBot.create(:stat_joined_user, date: Date.new(2018, 05, 31), org: org, count: 20) + june = FactoryBot.create(:stat_joined_user, date: Date.new(2018, 06, 30), org: org, count: 10) + data = [may, june] + + csv = described_class.to_csv(data) + + expected_csv = <<~HERE + date,count + 2018-05-31,20 + 2018-06-30,10 + HERE + expect(csv).to eq(expected_csv) + end + end + end +end diff --git a/spec/org_date_rangeable_spec.rb b/spec/org_date_rangeable_spec.rb new file mode 100644 index 0000000..70c00ca --- /dev/null +++ b/spec/org_date_rangeable_spec.rb @@ -0,0 +1,46 @@ +require 'rails_helper' + +RSpec.describe OrgDateRangeable do + describe '.split_months_from_creation' do + let(:org) do + FactoryBot.create(:org, created_at: DateTime.new(2018,05,28,0,0,0)) + end + + it "starts at org's created_at" do + expected_date = DateTime.new(2018,05,28,0,0,0) + + described_class.split_months_from_creation(org) do |start_date, end_date| + expect(start_date).to eq(expected_date) + break + end + end + + it "finishes at today's last month" do + expected_date = DateTime.current.last_month.end_of_month.to_i + actual_date = nil + + described_class.split_months_from_creation(org) do |start_date, end_date| + actual_date = end_date.to_i + end + + expect(actual_date).to eq(expected_date) + end + + context 'when is an Enumerable' do + subject { described_class.split_months_from_creation(org) } + + it 'responds to each method' do + is_expected.to respond_to(:each) + end + + it "starts at org's created_at" do + first = subject.first + start_date = org.created_at + end_date = DateTime.new(2018,05,31,23,59,59).to_i + + expect(first[:start_date]).to eq(start_date) + expect(first[:end_date].to_i).to eq(end_date) + end + end + end +end