select vs distinct vs uniq? - sql

I am confused by Ruby's ActiveRecord uniq method. I am using it to try to get back an array of complete objects, not just a particular field.
In my Padrino app script, which saves newspaper names and scores as Score objects, the uniq method by attribute on an ActiveRecord Relation is not working, and neither is distinct, with or without SQL syntax. Can anyone explain what is going on?
class Score < ActiveRecord::Base
def self.from_today
self.where('created_at > ?', Date.today)
end
end
scores = Score.from_today
scores.class
=> Score::ActiveRecord_Relation
scores.first
=> #<Score id: 123, score: -2.55, source: "Mail", created_at: "2016-08-11 04:29:24", updated_at: "2016-08-11 04:29:24">
scores.map(&:source)
=> ["Mail", "Guardian", "Telegraph", "Independent", "Express", "Mail"]
scores.uniq(:source).count
=> 6
scores.distinct(:source).count
=> 6
scores.select('distinct (SOURCE)').count
=> 5 #AHA, it works!
scores.select(:source).distinct
=> #<ActiveRecord::Relation [#<Score id: nil, source: "Telegraph">, #<Score id: nil, source: "Mail">, #<Score id: nil, source: "Independent">, #<Score id: nil, source: "Express">, #<Score id: nil, source: "Guardian">]>
#oops, no it doesn't

In Rails 5 distinct has no parameter. In Rails 4.2 the parameter is only true or false. When true, return distinct records, when false return none distinct records. uniq is in this case only an alias for distinct
So for Rails 4
scores.select(:source).distinct.count is what you want. This restricts distinct to column source

Related

With ActionCable, is there a way to count how many subscribers from inside a channel?

For an app I'm building, I have a "lobby" page where people configure which area they'd like to join. Pretty basic.
I'd like to have a running total of active consumers that are currently subscribed to the channel for this page, so that users know whether or not there's other people around to interact with.
Is there an easy way to do this?
I defined a helper method:
app/channels/application_cable/channel.rb
module ApplicationCable
class Channel < ActionCable::Channel::Base
def connections_info
connections_array = []
connection.server.connections.each do |conn|
conn_hash = {}
conn_hash[:current_user] = conn.current_user
conn_hash[:subscriptions_identifiers] = conn.subscriptions.identifiers.map {|k| JSON.parse k}
connections_array << conn_hash
end
connections_array
end
end
end
Now you can call connections_info anywhere inside your derived channel. The method returns an informational array of data about all the available server socket connections, their respective current_users and all their current subscriptions.
Here is an example of my data connections_info returns:
[1] pry(#<ChatChannel>)> connections_info
=> [{:current_user=>"D8pg2frw5db9PyHzE6Aj8LRf",
:subscriptions_identifiers=>
[{"channel"=>"ChatChannel",
"secret_chat_token"=>"f5a6722dfe04fc883b59922bc99aef4b5ac266af"},
{"channel"=>"AppearanceChannel"}]},
{:current_user=>
#<User id: 2, email: "client1#example.com", created_at: "2017-03-27 13:22:14", updated_at: "2017-04-28 11:13:37", provider: "email", uid: "client1#example.com", first_name: "John", active: nil, last_name: nil, middle_name: nil, email_public: nil, phone: nil, experience: nil, qualification: nil, price: nil, university: nil, faculty: nil, dob_issue: nil, work: nil, staff: nil, dob: nil, balance: nil, online: true>,
:subscriptions_identifiers=>
[{"channel"=>"ChatChannel",
"secret_chat_token"=>"f5a6722dfe04fc883b59922bc99aef4b5ac266af"}]}]
You can then parse this structure the way you want and extract the desired data. You can distinguish your own connection in this list by the same current_user (the current_user method is available inside class Channel < ActionCable::Channel::Base).
If a user connects twice (or more times), then corresponding array elements just double.
Yup there is one :
In your app/channel/what_so_ever_you_called_it.rb:
class WhatSoEverYouCalledItChannel < ApplicationCable::Channel
def subscribed
stream_from "your_streamer_thingy"
#subscriber +=1 #<==== like this
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
#subscriber -=1 #<===== like this
end
def send_message(data)
your_message_mechanic
end
Setup a variable increasing in subscribed
and decreasing in unsubscribed.
You may want store the value in your 'lobby' model , in this case '#subscriber' may be called #lobby.connected_total, i dont know, make this fit your needs.
But this is a way to keep track of number of stream.
Njoy

Set ActiveRecord Scopes In A Loop

Why doesn't this work?
class Foo
...
Status.each do |status|
scope status, where(status: status)
end
...
end
Now Foo.new it returns not an instance of Foo but an ActiveRecord::Relation.
Try this in ruby 1.9
Status.each do |status|
scope status, -> { where(status: status) }
end
or in previous ruby versions
Status.each do |status|
scope status, lambda { where(status: status) }
end
-- EDIT --
I guess your problem is somewhere else, since this code is working for me:
class Agency < ActiveRecord::Base
attr_accessible :logo, :name
validate :name, presence: true, uniqueness: true
NAMES = %w(john matt david)
NAMES.each do |name|
scope name, -> { where(name: name) }
end
end
I can create new models just fine and use the scopes
irb(main):003:0> Agency.new
=> #<Agency id: nil, name: nil, logo: nil, created_at: nil, updated_at: nil>
irb(main):004:0> Agency.matt
Agency Load (0.5ms) SELECT "agencies".* FROM "agencies" WHERE "agencies"."name" = 'matt'
=> []
irb(main):005:0> Agency.john
Agency Load (0.3ms) SELECT "agencies".* FROM "agencies" WHERE "agencies"."name" = 'john'
=> []
irb(main):006:0> Agency.david
Agency Load (0.3ms) SELECT "agencies".* FROM "agencies" WHERE "agencies"."name" = 'david'
=> []

Get rspec controller test to do a shallow comparison of BigDecimals

I have an rspec controller with the test:
it "assigns all rate_card_details as #rate_card_details" do
rate_card_detail = FactoryGirl.create(:rate_card_detail)
get :index, {}, valid_session
assigns(:rate_card_details).should eq([rate_card_detail])
end
For most models, this works fine. However, in this case, the rate field is a decimal. This causes the rspec comparison to (for some reason) compare 1 instance of BigDecimal with another, including its location in memory. Here is the error:
Failure/Error: assigns(:rate_card_details).should eq([rate_card_detail])
expected: [#<RateCardDetail rate_card_id: 1, item_id: 1, rate: #<BigDecimal:7f82dcdb0ae0,'0.6941E2',18(18)>, created_at: "2013-06-05 18:12:53", updated_at: "2013-06-05 18:12:53">]
got: [#<RateCardDetail rate_card_id: 1, item_id: 1, rate: #<BigDecimal:7f82dc9a74d0,'0.6941E2',18(18)>, created_at: "2013-06-05 18:12:53", updated_at: "2013-06-05 18:12:53">]
The 2 BigDecimals have the same value, but are different objects. Is there a way to get rspec to treat these as equal when doing a comparison?
it's not pretty but this works with me
it "assigns all rate_card_details as #rate_card_details" do
rate_card_detail = FactoryGirl.create(:rate_card_detail)
get :index, {}, valid_session
assigns(:rate_card_details).first.attributes.values.each_with_index do |rcd,i|
r_c_d = rate_card_detail[i]
if rcd.is_a?BigDecimal
rcd = rcd.to_s
r_c_d = r_c_d.to_s
end
expect(rcd).to eq(r_c_d)
end
end

How to write an rspec test to specify that invalid dates should not be valid for a Rails model?

View
<%= form_for(#new_credit_entry) do |f| %>
<%= f.date_select :created_on%>
I see that I am allowed to specify an invalid date. This results in a params hash as shown below
"credit"=>{"created_on(1i)"=>"2013",
"created_on(2i)"=>"2",
"created_on(3i)"=>"31"
This is of course an incorrect date. So I know my model needs to have validation for this - will probably use the validates_timeliness gem. What I need to know is how to simulate this in a spec
Here's some rails console output
irb(main):056:0> x = Credit.new(created_on: "2013-02-30")
=> #<Credit id: nil, description: nil, credit_category_id: nil, amount: nil, created_on: nil, created_at: nil, updated_at: nil>
irb(main):058:0> x.created_on_before_type_cast
=> "2013-02-30"
irb(main):060:0> x.created_on
=> nil
irb(main):057:0> y = Credit.new(created_on: "2013-03-03")
=> #<Credit id: nil, description: nil, credit_category_id: nil, amount: nil, created_on: "2013-03-03", created_at: nil, updated_at: nil>
irb(main):059:0> y.created_on_before_type_cast
=> "2013-03-03"
irb(main):061:0> y.created_on
=> Sun, 03 Mar 2013
irb(main):062:0> z = Credit.new("created_on(1i)"=>"2013",
irb(main):063:1* "created_on(2i)"=>"2",
irb(main):064:1* "created_on(3i)"=>"31")
=> #<Credit id: nil, description: nil, credit_category_id: nil, amount: nil, created_on: "2013-03-03", created_at: nil, updated_at: nil>
irb(main):065:0> z.created_on
=> Sun, 03 Mar 2013
irb(main):066:0> z.created_on_before_type_cast
=> Sun, 03 Mar 2013
If you use validates_timeliness, it will reject the invalid dates you're trying above. The model will not be valid and created_on will be nil. That includes the date wrapping issue you mention.
After installing validates_timeliness, all you need in your model is:
validates_date :created_on
Here's a spec that tests a variety of scenarios:
describe Credit do
it "rejects single digits" do
credit = Credit.new created_on: "3"
credit.should_not be_valid
credit.created_on.should be_nil
end
it "rejects bad dates" do
credit = Credit.new created_on: "2013-02-31"
credit.should_not be_valid
credit.created_on.should be_nil
end
it "rejects words" do
credit = Credit.new created_on: "some nonsense"
credit.should_not be_valid
credit.created_on.should be_nil
end
it "accepts good dates" do
date = "2013-02-28"
credit = Credit.new created_on: date
credit.should be_valid
credit.created_on.should == Date.parse(date)
end
end
You could probably validate this fairly easily even without the gem, but that looks like a handy gem to use. Don't forget to run the generator to complete installation.
If you do choose to roll your own validation, you might run into some issues.
One is that underlying databases behave somewhat differently when you try to stick a bad value into a date or datetime column; some are more flexible in the values they store and the automatic conversions they do. It's best to validate on the Rails side ahead of time.
A bigger issue is that by the time validations are run, ActiveRecord has already tried to cast the value into a type that matches the database field type. If you've got a date field in the database for created_on, ActiveRecord will convert every value assigned to created_on into a Date object. That will end up being nil for many malformed dates. In your validator, you'll want to look at created_on_before_type_cast, which will be the raw string. ActiveRecord creates a dynamic *_before_type_cast method for every DB column. You can check the format of that and reject bad values.
I mention these caveats to encourage you to stick with the gem. :)

Search multiple entries (hash values) in controller

Here's what I'm trying to do:
I've got three tables:
Appointments Users and Shares -
I'd like to assign users to an appointment.
So what I did until now is that I created a table in between appointment and user which
stores the user_id and appointment_id which a user is assigned to.
I'm able to search the shares for a specific appointment in my appointments_controller
with
#shares = Shares.find(:all, :conditions => { :appointment_id => #appointment.id })
After that I get a hash #shares which looks like this:
[#<Shares id: 10, user_id: 3, appointment_id: 1, created_at: "2012-12-04 10:24:16", updated_at: "2012-12-04 10:24:17">, #<Shares id: 12, user_id: 2, appointment_id: 1, created_at: "2012-12-04 10:28:38", updated_at: "2012-12-04 10:28:39">]
What I'd like to do now is a query in the users table to find the usernames that belong to the user_ids i found in the shares hash.
I tried that with something like this:
#shares.each do |s|
#participants = User.all(:conditions => {:id => s.user_id})
end
But it doesn't work because the participants hash gets overwritten every time the loop starts...
Thanks for your help
#shares = Shares.find(:all, :conditions => { :appointment_id => #appointment.id })
#participants = User.all(:conditions => ["id IN (?)", #shares.map{|s| s.user_id}.compact])