Uniqueness validation not functioning as expected - ruby-on-rails

I have a Call model with the following validations:
class Call < ActiveRecord::Base
validates_uniqueness_of :external_id, scope: :source
end
I generate new calls through a webhook that calls the following service:
class AircallWebhookService
include HubspotExtension
def initialize(params)
#event = params["event"]
#params = params["data"]
#call = nil
#aircall_number = nil
#employee_email = nil
end
def process
#call = Call.find_by(source: :aircall, external_id: #params["id"])
if #call.present?
p "Found existing call!"
else
p "Could not locate existing call."
#call = Call.new(source: :aircall, external_id: #params["id"])
end
#call.source = 1
#call.external_id = #params["id"]
#call.url = #params["direct_link"]
#call.direction = #params["direction"]
#call.status = #params["status"]
#call.missed_call_reason = #params["missed_call_reason"]
#call.started_at = Time.at(#params["started_at"]) if #params["started_at"].present?
#call.answered_at = Time.at(#params["answered_at"]) if #params["answered_at"].present?
#call.ended_at = Time.at(#params["ended_at"]) if #params["ended_at"].present?
#call.duration = #params["duration"]
#call.raw_digits = #params["raw_digits"]
#call.aircall_user_id = #params.dig("user", "id")
#call.contact_id = #params.dig("contact", "id")
#aircall_number = #params.dig("number", "digits").try{|n| n.gsub(/\s|-|\(|\)|\+/, "")}
#call.aircall_user_id = #params.dig("user", "id")
#employee_email = #params.dig("user", "email")
if !#params["tags"].empty?
mapTagToReferrer
end
#call.comments = mapComments
if #call.save
linkTagToCall
linkCallToEmployee
updateHubspotEngagement
end
end
...
end
For some reason, despite the uniqueness validation, I continue to see calls with the same external_id and source. For example these are 2 records in my DB:
[
[0] #<Call:0x000055d780f639b8> {
:id => 8149,
:location_id => nil,
:referrer => nil,
:consultation => nil,
:created_at => Tue, 07 Sep 2021 15:42:01 EDT -04:00,
:updated_at => Tue, 07 Sep 2021 15:42:01 EDT -04:00,
:worldwide => nil,
:external_id => 582402916,
:source => "aircall",
:direction => "inbound",
:started_at => Tue, 07 Sep 2021 15:41:03 EDT -04:00,
:answered_at => Tue, 07 Sep 2021 15:41:10 EDT -04:00,
:ended_at => Tue, 07 Sep 2021 15:41:57 EDT -04:00,
:duration => 54,
:status => "done",
:missed_call_reason => nil,
:aircall_user_id => 567754,
:contact_id => nil,
:comments => nil,
:lead_status => nil,
:call_type => "unknown"
},
[1] #<Call:0x000055d780f636e8> {
:id => 8150,
:location_id => nil,
:referrer => nil,
:consultation => nil,
:created_at => Tue, 07 Sep 2021 15:42:01 EDT -04:00,
:updated_at => Tue, 07 Sep 2021 15:42:01 EDT -04:00,
:worldwide => nil,
:external_id => 582402916,
:source => "aircall",
:direction => "inbound",
:started_at => Tue, 07 Sep 2021 15:41:03 EDT -04:00,
:answered_at => Tue, 07 Sep 2021 15:41:10 EDT -04:00,
:ended_at => Tue, 07 Sep 2021 15:41:57 EDT -04:00,
:duration => 54,
:status => "done",
:missed_call_reason => nil,
:aircall_user_id => 567754,
:contact_id => nil,
:comments => nil,
:lead_status => nil,
:call_type => "unknown"
}
]
They are identical and even the created_at is identical down to the millisecond. How is this possible?
Here's the controller in case it's necessary:
class API::WebhooksController < ApplicationController
def aircall_webhook
ac = AircallWebhookService.new(params)
ac.process
head :ok
end
end

validates_uniqueness_of doesn't actually guarantee that duplicate values cannot be inserted. It merely catches most of the cases where users input duplicated data and provides user feedback. Its very much prone to race conditions, and is foiled by stuff as simple as double clicking grannies.
If uniqueness is actually important you need to enforce it on the database layer with a unique index.
add_index :calls, [:external_id, :source], unique: true

Related

Rails group_by id and group created

I have a model call GcMission
it contains gamecharacter_id, mission_id and of course created_at.
Now I need to group_by gamecharacter_id, and then group the created_at to see how many mission_id per day.
I assum the result will be like
{
69 => {
'2017-08-18' => 1
},
75 => {
'2017-08-18' => 2
},
78 => {
'2017-08-18' => 1,
'2017-08-19' => 1,
}
}
But I am stuck at that.
Here is what I've written.
GcMission.select('gamecharacter_id, mission_id, created_at').where(mission_id: assign_mission_ids).where('finish_counter >= 1').group_by{|a| a.gamecharacter_id}
And the result is like
{
69 => [
[0] #<GcMission:0x007ff6c6a53650> {
:gamecharacter_id => 69,
:mission_id => 3,
:created_at => Tue, 18 Apr 2017 03:20:36 UTC +00:00
}
],
75 => [
[0] #<GcMission:0x007ff6c6a52d18> {
:gamecharacter_id => 75,
:mission_id => 3,
:created_at => Tue, 18 Apr 2017 06:38:27 UTC +00:00
},
[1] #<GcMission:0x007ff6c6a52408> {
:gamecharacter_id => 75,
:mission_id => 4,
:created_at => Tue, 18 Apr 2017 07:55:40 UTC +00:00
}
],
78 => [
[0] #<GcMission:0x007ff6c6a51be8> {
:gamecharacter_id => 78,
:mission_id => 3,
:created_at => Tue, 18 Apr 2017 17:29:24 UTC +00:00
},
[1] #<GcMission:0x007ff6c6a51580> {
:gamecharacter_id => 78,
:mission_id => 4,
:created_at => Wed, 19 Apr 2017 03:20:31 UTC +00:00
}
]
}
Try Following
gc_missions = GcMission.select("gamecharacter_id, DATE(created_at) AS date,
COUNT(mission_id) AS mission_count")
.group('gamecharacter_id, date')
If you want count of unique mission_id per gamecharacter_id per day use
COUNT(DISTINCT(mission_id))
You may like to verify using following code
gc_missions.each {|gc| puts "#{gc.gamecharacter_id}, #{gc.date}, #{gc.mission_count}"}

How to "desc" order group_by?

How to order("created_at desc") the months?
The articles within the month are in descending order, but not the months themselves.
controller
def archives
#posts = Post.order("created_at desc")
#posts_by_months = #posts.group_by { |t| t.created_at.beginning_of_month }
end
view
<% #posts_by_months.sort.each do |month, posts| %>
<%= month.strftime('%b') %>
<% for post in posts %>
<%= post.title %>
<% end %>
<% end %>
Using Enumerable#inject http://ruby-doc.org/core-2.3.1/Enumerable.html#method-i-inject:
#posts_by_months = #posts_by_months.inject({}) do |h,(k,v)|
h[k] = v.sort do |x,y|
y.created_at <=> x.created_at
end
h
end
For example:
irb(main):054:0> hash = #posts_by_months.inject({}) {|h,(k,v)| h[k] = v.sort {|x,y| y.created_at <=> x.created_at}; h}
#=> […]
irb(main):055:0> pp hash.first.map(&:created_at)
[Wed, 08 Jun 2016 22:26:34 UTC +00:00,
Wed, 08 Jun 2016 21:49:49 UTC +00:00,
Wed, 08 Jun 2016 18:30:44 UTC +00:00,
Wed, 08 Jun 2016 18:25:40 UTC +00:00]
UPDATE
Works for Rails view via controller.
# app/controllers/website_controller.rb
class WebsiteController < ApplicationController
def test
#posts = Post.order("created_at desc")
#posts_by_months = #posts.group_by {|t| t.created_at.beginning_of_month}
#posts_by_months = #posts_by_months.inject({}) do |h,(k,v)|
h[k] = v.sort do |x,y|
y.created_at <=> x.created_at
end
h
end
render(:layout => false, :template => 'website/test')
end
end
Using HAML (http://haml.info) template:
# app/views/website/test.html.haml
- #posts_by_months.sort.each do |month, posts|
= month.strftime('%b')
%br
%ul
- for post in posts
%li
= post.title
When sorting by month numbers you have to do some explicit conversions:
In controller:
#posts_by_months = #posts.group_by { |t| t.created_at.beginning_of_month }.
sort_by { |k, _| k.strftime('%-m').to_i }.reverse
#posts_by_months.each { |month, posts| puts month.strftime('%b') } ;
=> Dec
Nov
Oct
Sep
Aug
Jul
Jun
May
Apr
Mar
Feb
Jan
Here k.strftime('%-m') extracts month number without padding as a string and to_i converts that to a number. Without the conversion sort_by will apply lexical sorting which is not what's required.
Result of sort_by is not a hash but two-dimensional array. This does not affect the view code though.
Just in case you are using PostgreSQL:
#posts = Post.select('extract(month from created_at) as month, posts.*').order('month DESC, created_at DESC').group_by(&:month)
#posts.each do |month, posts|
puts "This is the month: #{Date::MONTHNAMES[month]}"
puts "And this is array of posts: #{posts}"
end

Sum of array in rails

I have a array suppose
[#<Data id: 1, date: "2016-01-06", value: "1">,
#<Data id: 2, date: "2015-12-31", value: "3">,
#<Data id: 3, date: "2016-01-06", value: "6">...]
and so on..
I want to sum the values having same date
i.e here first and third record are of same date so the result array will give
#<Data id: 1, date: "2016-01-06", value: "1">,
#<Data id: 3, date: "2016-01-06", value: "7">,
Hey you can use try this way if you have already fetch an array from database
arr.group_by{|a| a.date.to_date}.map{|k,v| {k => v.map(&:value).sum()}}
If you are are not fetch array/active record from database you can directly use database query as
If your database stores only date then you can use
Model.group("date").sum(:value)
If your database stores date with time here i have use DATE_FORMAT function for skipping Time part of date
Model.group("DATE_FORMAT(date, '%Y%m%d')").sum(:value)
You can use sql groupping on the model:
Data.where(date:(42.days.ago..Date.today)).group(:date).sum(:value)
This will return a hash of {date => sum}
On an array:
Hash[your_array.group_by(&:date).map{|k,v| [k, v.sum(&:value)]}]
sum = Hash.new(0)
array.each do |data|
<p>sum[data[:date]] += data[:value]</p>
end
# => {:id => 1, "Wed, 04 May 2011" => 300, "Tue, 03 May 2011" => 450...}
# => If you then want this in the same array format you started with:
new_array = sum.collect{ |key, value| {:date => key, :value => value} }
# => [{:id => 1,:date => "Wed, 04 May 2011", :value => 300}, {....}]

Why ActiveRecord not receiving these getting aggregate functions

Doing a query with aggregate functions directly on ActiveRecord with Postgres seems to be working ok.
ActiveRecord::Base.connection.execute("
SELECT created_at::date as date,
sum(item1_count) as sum_item1,
sum(item2_count) as sum_item2,
sum(item3) as sum_item3 from items
GROUP by
created_at::date ORDER BY date desc").to_a
And returns something like this which is ok.
[
{
"date" => "2014-01-23",
"sum_item1" => "3239",
"sum_item2" => "90",
"sum_item3" => "0.00000"
},
{
"date" => "2014-01-22",
"sum_item1" => "1981",
"sum_item2" => "19",
"sum_item3" => "0.00000"
}
]
The problem is when trying to do the same using scopes, for instance.
class Item < ActiveRecord::Base
scope :myscope, -> {
select("created_at::date as date, sum(item1_count) as sum_item1,
sum(item2_count) as sum_item2,
sum(item3) as sum_item3")
.group("created_at::date")
.order("date desc") }
end
The result here is different. When running user.items.myscope.to_a I get the following result missing the aggregate values and adding an id field that should not be there.
[
#<Item:0x00000103cc3d38> {
:id => nil,
:date => Thu, 23 Jan 2014
},
#<Item:0x00000103cc39a0> {
:id => nil,
:date => Wed, 22 Jan 2014
}
]
How it would be possible to pass the aggregate functions to the scope?

How to customize json output in rails?

I have a model for languages and i want to get all the languages as json but the json output looks as follows
[{"language":{"created_at":null,"id":1,"language":"English","updated_at":null}},{"language":{"created_at":null,"id":2,"language":"Swedish","updated_at":null}},{"language":{"created_at":null,"id":3,"language":"German","updated_at":null}},{"language":{"created_at":null,"id":4,"language":"French","updated_at":null}},{"language":{"created_at":null,"id":5,"language":"spanish","updated_at":null}},{"language":{"created_at":null,"id":6,"language":"dutch","updated_at":null}},{"language":{"created_at":"2012-12-03T05:01:18Z","id":7,"language":"Tamil","updated_at":"2012-12-03T05:01:18Z"}}]
but i want to make this as
{"language":[{"created_at":null,"id":1,"language":"English","updated_at":null},{"created_at":null,"id":2,"language":"Swedish","updated_at":null},{"created_at":null,"id":3,"language":"German","updated_at":null},{"created_at":null,"id":4,"language":"French","updated_at":null},{"created_at":null,"id":5,"language":"spanish","updated_at":null},{"created_at":null,"id":6,"language":"dutch","updated_at":null},{"created_at":null,"id":7,"language":"Tamil","updated_at":null} ] }
Update
def index
#languages = Language.all
respond_to do |format|
format.json { render json: #languages}
end
end
update 2
class Language < ActiveRecord::Base
ActiveRecord::Base.include_root_in_json = false
has_and_belongs_to_many :users
end
I believe this should work:
format.json { render json: { "language" => #languages.as_json(:root => false) }.to_json }
What this does it to convert the #languages array into an array of JSON-formatted hash models with no root keys (using as_json), then wraps the result in a hash with a root key "language", and convert that hash into a JSON-formatted string with to_json. (See the docs for details on including or not including a root node using as_json.)
For example, with a model Post:
posts = Post.all
#=> [#<Post id: 1, name: "foo", title: "jkl", content: "some content", created_at: "2012-11-22 01:05:46", updated_at: "2012-11-22 01:05:46">]
# convert to array of hashes with no root keys
posts.as_json(root: false)
#=> [{"content"=>"some content", "created_at"=>Thu, 22 Nov 2012 01:05:46 UTC +00:00, "id"=>1, "name"=>"foo", "title"=>"jkl", "updated_at"=>Thu, 22 Nov 2012 01:05:46 UTC +00:00}]
# add root back to collection:
{ "post" => posts.as_json(root: false) }
#=> {"post"=>[{"content"=>"some content", "created_at"=>Thu, 22 Nov 2012 01:05:46 UTC +00:00, "id"=>1, "name"=>"foo", "title"=>"jkl", "updated_at"=>Mon, 03 Dec 2012 09:41:42 UTC +00:00}]}
# convert to JSON-formatted string
{ "post" => posts.as_json(root: false) }.to_json
#=> "{\"post\":[{\"content\":\"some content\",\"created_at\":\"2012-11-22T01:05:46Z\",\"id\":1,\"name\":\"foo\",\"title\":\"jkl\",\"updated_at\":\"2012-12-03T09:43:37Z\"}]}"
override the as_json on the Model you want to customize
def as_json options={}
{
id: id,
login: login,
name: custom.value, #for custom name
...
}
end
==> or
def as_json(options={})
super(:only => [:id, :login, :name ....])
end
from : here
Other link: here
I suggest you to use rabl gem (https://github.com/nesquena/rabl) to format your data.
Override as_json method in your model, to include associations, hide columns and why not? calling custom methods as they were attributes
def as_json(options={})
super(:except => [:created_at,:updated_at],
:include => {
:members => {
:only => [:role, :account],
:include => {
:account => {
:only => [:name, :subdomain]
}
}
}
},
:methods => [:jwt_token]
)
end
This will output something like this:
{
"id": 2,
"name": "Test Teacher",
"email": "teacher#testing.io",
"jwt_token":"eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MiwiZXhwIjoxNTY2NzQ0OTQzfQ.HDGu7JiJEQEEpGo7inuXtOZBVQOfTaFquy8dr-QH5jY",
"members": [{
"role": "instructor",
"account": {
"name": "Testing",
"subdomain": "test"
}
}],
}
The easiest way of adding custom json output when you render json is by using gem that provide many json templates-
https://github.com/fabrik42/acts_as_api

Resources