Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GT-1314, implement saving user count values #826

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions app/controllers/user_counters_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@
class UserCountersController < WithUserController
def update
counter = current_user.user_counters.where(counter_name: counter_name).first_or_initialize
counter.decay
increment = permitted_params[:increment].to_f
counter.count += increment
counter.decay
counter.decayed_count += increment
counter.save! if counter.new_record? # need to save before applying values if new user counter
counter.apply_values(permitted_params[:values]) if permitted_params[:values]
counter.save!
render json: counter, status: :ok
end

def index
counters = current_user.user_counters
counters = current_user.user_counters.order(:id)
# decay but don't save as it's not needed and it'll be more efficient this way -- the in-memory decay calculation is very fast
counters.each(&:decay)
render json: counters, status: :ok
Expand All @@ -21,7 +23,7 @@ def index
protected

def permitted_params
params.require(:data).require(:attributes).permit(:increment)
params.require(:data).require(:attributes).permit(:increment, values: [])
end

def counter_name
Expand Down
11 changes: 9 additions & 2 deletions app/models/user_counter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@ class UserCounter < ApplicationRecord
validates :counter_name, format: {with: /\A[-_.a-zA-Z0-9]+\z/, message: "has invalid characters"}

def decay
date_now = Date.today
self.last_decay ||= Date.today
date_now = Time.now.utc.to_date
self.last_decay ||= date_now
days_to_decay = date_now - last_decay

if days_to_decay > 0
self.decayed_count *= (Math::E**(DECAY_RATE * days_to_decay))
self.last_decay = date_now
end
end

def apply_values(values)
new_values = values - self.values
self.values += new_values
self.count += new_values.count
self.decayed_count += new_values.count
end
end
2 changes: 1 addition & 1 deletion db/migrate/20211110153757_create_user_counters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ def change
t.string :counter_name
t.integer :count, default: 0
t.float :decayed_count, default: 0
t.date :last_decay, default: -> { "NOW()" }
t.date :last_decay, default: -> { timezone("utc", NOW()) }

t.timestamps
end
Expand Down
8 changes: 8 additions & 0 deletions db/migrate/20220504203747_add_values_to_user_counters.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class AddValuesToUserCounters < ActiveRecord::Migration[6.1]
def change
change_table :user_counters do |t|
t.string "values", array: true, default: "{}"
end
add_index :user_counters, :values, using: "gin"
end
end
6 changes: 6 additions & 0 deletions db/migrate/20220504204229_remove_user_counter_values_table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class RemoveUserCounterValuesTable < ActiveRecord::Migration[6.1]
def change
return unless ActiveRecord::Base.connection.table_exists?("user_counter_values")
drop_table :user_counter_values
end
end
7 changes: 4 additions & 3 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file added public/xmlns/meta.xsd
Empty file.
59 changes: 57 additions & 2 deletions spec/acceptance/user_counters_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,55 @@
expect(UserCounter.last.last_decay).to eq(Date.today)
end
end

context "with values" do
it "creates user_counter" do
expect {
do_request data: {type: "user_counter", attributes: {increment: 20, values: ["v1", "v2"]}}
}.to change { UserCounter.count }.by(1)

expect(status).to eq(200)
json_response = JSON.parse(response_body)["data"]
expect(json_response).not_to be_nil
expect(json_response["id"]).to eq("tool_opens.kgp")
expect(json_response["attributes"]["count"]).to eq(22)
expect(json_response["attributes"]["decayed-count"]).to eq(22)
expect(json_response["attributes"]["last-decay"]).to eq(Date.today.to_s)
expect(UserCounter.last.counter_name).to eq("tool_opens.kgp")
expect(UserCounter.last.count).to eq(22)
expect(UserCounter.last.decayed_count).to eq(22)
expect(UserCounter.last.last_decay).to eq(Date.today)
end

context "when user_counter exists" do
let!(:user_counter) { FactoryBot.create(:user_counter, user: user, counter_name: "tool_opens.kgp", count: 50, decayed_count: 50, last_decay: 90.days.ago) }

it "updates the count and decay" do
expect {
do_request data: {type: "user_counter", attributes: {increment: 20, values: ["v1", "v2"]}}
}.to_not change { user_counter.count }

expect(status).to eq(200)
json_response = JSON.parse(response_body)["data"]
expect(json_response).not_to be_nil
expect(json_response["id"]).to eq("tool_opens.kgp")
expect(json_response["attributes"]["count"]).to eq(72)
expect((json_response["attributes"]["decayed-count"] - 47).abs).to be <= 0.004 # look within 0.004, close enough
expect(json_response["attributes"]["last-decay"]).to eq(Date.today.to_s)
expect(UserCounter.last.count).to eq(72)
# get close to 45 -- original value of 50 should decay to 25 with the 90 day half-life, then +20 from the patch count incremement
expect((UserCounter.last.decayed_count - 47).abs).to be <= 0.004
expect(UserCounter.last.last_decay).to eq(Date.today)
expect(UserCounter.last.values).to eq(["v1", "v2"])
end
end
end
end

get "user/counters" do
let(:user) { FactoryBot.create(:user) }
let!(:user_counter) { FactoryBot.create(:user_counter, user: user, counter_name: "tool_opens.kgp", count: 50, decayed_count: 50, last_decay: 90.days.ago) }
let!(:user_counter2) { FactoryBot.create(:user_counter, user: user, counter_name: "other.kgp", count: 60, decayed_count: 40, last_decay: 90.days.ago) }
let!(:user_counter) { FactoryBot.create(:user_counter, user: user, counter_name: "tool_opens.kgp", count: 50, decayed_count: 50, last_decay: 90.days.ago.utc) }
let!(:user_counter2) { FactoryBot.create(:user_counter, user: user, counter_name: "other.kgp", count: 60, decayed_count: 40, last_decay: 90.days.ago.utc) }
requires_okta_login

it "gets counts" do
Expand All @@ -73,5 +116,17 @@
expected_result = %({"data":[{"id":"tool_opens.kgp","type":"user-counter","attributes":{"count":50,"decayed-count":25.00367978478838,"last-decay":"#{today}"}},{"id":"other.kgp","type":"user-counter","attributes":{"count":60,"decayed-count":20.002943827830705,"last-decay":"#{today}"}}]})
expect(response_body).to eq(expected_result)
end

context "with values" do
it "includes values" do
user_counter.update(values: ["v1", "v2"])
do_request

expect(status).to eq(200)
today = Date.today.to_s
expected_result = %({"data":[{"id":"tool_opens.kgp","type":"user-counter","attributes":{"count":50,"decayed-count":25.00367978478838,"last-decay":"#{today}"}},{"id":"other.kgp","type":"user-counter","attributes":{"count":60,"decayed-count":20.002943827830705,"last-decay":"#{today}"}}]})
expect(response_body).to eq(expected_result)
end
end
end
end