Find out how 'deep' an association is - ruby-on-rails

Please read the following four lines carefully:
first_post = Discourse.create(title: 'I am the first post')
# => #<Discourse id: 56, user_id: nil, title: "I am the first post", body: nil, deleted: nil, delete_date: nil, created_at: "2014-04-07 12:34:53", updated_at: "2014-04-07 12:34:53", forum_id: nil>
first_reply = first_post.replies.create(title: 'I am the first reply to the first post')
# => #<Discourse id: 57, user_id: nil, title: "I am the first reply to the first post", body: nil, deleted: nil, delete_date: nil, created_at: "2014-04-07 12:35:29", updated_at: "2014-04-07 12:35:29", forum_id: nil>
second_reply = first_post.replies.create(title: 'I am the second reply to the first post')
# => #<Discourse id: 58, user_id: nil, title: "I am the second reply to the first post", body: nil, deleted: nil, delete_date: nil, created_at: "2014-04-07 12:35:41", updated_at: "2014-04-07 12:35:41", forum_id: nil>
first_reply_reply = second_reply.retorts.create(title: 'I am the first retort to the second reply to the first retort')
# => #<Discourse id: 59, user_id: nil, title: "I am the first retort to the second reply to the fi...", body: nil, deleted: nil, delete_date: nil, created_at: "2014-04-07 12:36:53", updated_at: "2014-04-07 12:36:53", forum_id: nil>
I regard first_post to have a "depth" of 0 because it isn't a reply to anything.
I regard first_reply to have a "depth" of 1 because it is a reply to first_post (0 + 1 = 1)
I regard second_reply to have a "depth" of 1 because it is a reply to first_post (0 + 1 = 1)
I regard first_reply_reply to have a "depth" of 2 because it is a reply to second_reply that is in-turn a reply to first_post (1 + 1 = 2)
And so on. A reply to first_reply_reply would be 3. (1 + 2 = 3)
To determine the depth of each association I could easily add a column depth to my table and simply create a model method with a before_save hook to iterate the depth, however I wonder if the depth can already be worked out in rails. Maybe an active record method?

You should be able to write:
class Discourse < AR::Base
...
def depth
<parent_association> ? <parent_association>.depth + 1 || 1
end
end
Where <parent_association> is your association to access to record to which this record is a response to. However this will require a number of database calls, so your approach with before_save hook is much better. You can also hava a look at ancestry gem, which very well handles tree-like associations (including depths).

Related

and sometimes I get failed spices because of Time.now, can't understand why

so i have a method in model
class << self
def last_week
start = Time.zone.now.beginning_of_week - 7.days
finish = start + 7.days
where('appointment_at >= ? AND appointment_at < ?', start, finish).order(appointment_at: :desc)
end
end
And I write spec for this method.
RSpec.describe Appointment, type: :model, vcr: { record: :none } do
let!(:time) { Time.now }
let(:appointment_at) { time }
context '.last_week' do
let!(:scoped_appointment) { create(:appointment, appointment_at: time - 2.days) }
let!(:another_appointment) { create(:appointment, appointment_at: time - 16.days) }
it do
travel_to(time) do
expect(Appointment.last_week).to include(scoped_appointment)
expect(Appointment.last_week).not_to include(another_appointment)
end
end
end
end
And sometime i get failed this spec with error.
expected #<ActiveRecord::Relation []> to include #<Appointment id: 18, lead_id: 27, body: nil, appointment_at: "2019-02-25 00:59:47", google_id: nil, ... "pending", user_id: 22, notify: nil, cc_emails: nil, appointment_minutes: nil, status_message: nil>
Diff:
## -1,2 +1,2 ##
-[#<Appointment id: 18, lead_id: 27, body: nil, appointment_at: "2019-02-25 00:59:47", google_id: nil, created_at: "2019-02-27 00:59:47", updated_at: "2019-02-27 00:59:47", timezone: nil, subject: "Meeting with Lead", address: nil, notification: nil, status: "pending", user_id: 22, notify: nil, cc_emails: nil, appointment_minutes: nil, status_message: nil>]
+[]
I can't understand why?
And I have a suggestion that I should tightly set time
in spec_helper.rb
$now = DateTime.parse('2020-01-01 00:00:01 -0500')
will it be right? and why ?
Your test setup is brittle. It will break depending on the day of the week you run your spec.
The scope in your model returns appointments from the previous week, Monday through Sunday (you are calling beginning_of_week and adding 7 days to it)
So if your tests run on a Wednesday, like in the example you provided, the appointment’s appointment_at field will be set to Monday (since you are calculating it as Time.now - 2.days). That means your scope will not cover that appointment.
I suggest you use a specific time in your setup. Given your current setup, using let(:time) { DateTime.parse('2019-02-25 00:00:00') } should work

Rails: why can I send a single record attribute via controller to new record but not array?

Creating articles via the controller in Rails. A simple method, which more or less works; just call the method from some other place and it generates a new article via the back end and fills in the values:
def test_create_briefing
a = Article.new
a.type_id = 27
a.status = 'published'
a.headline = 'This is a headline'
a.lede = 'Our article is about some interesting topic.'
a.body = test_article_text
a.save!
end
If test_article_text is just a single record, this works fine and prints the existing article body into the new article body. Looks right in the view and looks right in "edit". All perfect.
def test_article_text
a = Article.find_by_id(181)
a.body
end
But if I try to do the same thing with the last ten articles, it doesn't work:
def test_article_text
Article.lastten.each do |a|
a.body
end
end
In the view you get:
[#, #, #, #, #, #, #, #, #, #]
And in "edit" you get:
[#<Article id: 357, headline: "This is a headline", lede: "Our article is about some interesting topic.", body: "[#<Article id: 356, headline: \"This is a headline\"...", created_at: "2017-12-31 20:40:16", updated_at: "2017-12-31 20:40:16", type_id: 27, urgency: nil, main: nil, status: "published", caption: nil, source: nil, video: nil, summary: nil, summary_slug: nil, topstory: false, email_to: nil, notification_slug: nil, notification_message: nil, short_lede: nil, short_headline: nil, is_free: nil, briefing_point: nil>, #<Article id: 356, headline: "This is a headline"…etc, etc, etc.
What do I not know? What am I missing?
It is returned as below because the Article.lastten is the returned variable from your controller.
[#<Article id: 357, headline: "This is a headline", lede: "Our article is about some interesting topic.", body: "[#<Article id: 356, headline: \"This is a headline\"...", created_at: "2017-12-31 20:40:16", updated_at: "2017-12-31 20:40:16", type_id: 27, urgency: nil, main: nil, status: "published", caption: nil, source: nil, video: nil, summary: nil, summary_slug: nil, topstory: false, email_to: nil, notification_slug: nil, notification_message: nil, short_lede: nil, short_headline: nil, is_free: nil, briefing_point: nil>, #<Article id: 356, headline: "This is a headline"…etc, etc, etc.
To return all Article body, do as below:
def test_article_text
arr = Array.new
Article.lastten.each do |a|
arr << a.body
end
arr # should be added so it will be the last value returned from your controller
end
So, #Shiko was nearly right, certainly on the right path. Had to manipulate the array a bit and do two things to get it to work:
.join the bits of the array to strip out all of the rubbish;
Concatenate the different bits for each article in a different way than you normally do in a view. So to_sfor each of the attributes, concatenating things "" + "" and rebuilding the url with information available in the array (no link_to, etc.).
The "**"is markdown, because I'm using that, but I suppose you could bung html tags in there if you needed to.
This works:
def test_article_text
arr = Array.new
Article.lastten.each do |a|
arr << "**" + a.headline.to_s + "**: " + a.text.to_s + "[Read now](/articles/#{a.id}-#{a.created_at.strftime("%y%m%d%H%M%S")}-#{a.headline.parameterize})"
end
arr.join("\n\n")
end

How to change the object view in paper_trail

I have used paper_trail gem for auditing in the application i am developing. I am able to manage most of its features and i was able to display the data in versions table to the user. In doing so the data in object attribute of the versions table is some what unreadable. How can i make it be readable?
Here is my sample output of the object attribute:
version.object retrieves in the following format in a single cell:
--- budget_year: '2014' name: bbb reference_number: j789789 requesting_unit: '798789' quarter: II source_of_fund: Government budget_type: Recurrent procurement_method: Open Bidding procurement_level: National estimated_cost: 4455.0 currency: '4545' purchase_request_id: start_at: 2014-07-30 end_at: 2014-07-31 created_at: 2014-07-24 08:29:38.000000000 Z updated_at: 2014-07-24 08:29:38.000000000 Z id: 1
You could use reify method to see your data like,
> i=Invoice.last.versions.last
=> <PaperTrail::Version id: 158, item_type: "Invoice", item_id: 115, event: "update", whodunnit: "3", object: "---\nid: 115\ncreated_at: 2015-02-05 06:43:17.278448...">
:038 > i.object
=> "---\nid: 115\ncreated_at: 2015-02-05 06:43:17.278448000 Z\nupdated_at: 2015-02-05 06:43:17.278448000 Z\nentity_type: Site\nentity_id: 928\ninvoice_id: WS/14\nuser_id: \nperson_id: \nstatus: 0\nperson_email:\nentity_value:\naddress: ''\nprice: '666'\ndetails: '{\"domain\"=>\"\", \"package\"=>\"23\", \"site_id\"=>\"928\", \"user_id\"=>\"394\", \"years\"=>\"1\"}'\norder_id: '464'\nnorder_type: \nyear: 1\npayment_type: cash\naccount_type: \ncheque_name: ''\ncomment: \ntax: \ncomments: 'Customer made payment by cash with receipt Nos: 041 & 042.'\ncheque_number: \nchequenumber: ''\npayment_level: \ninvoice_type: \n"
> i.reify
=> <Invoice id: 115, created_at: "2015-02-05 06:43:17", updated_at: "2015-02-05 06:43:17", entity_type: "Site", entity_id: 928, invoice_id: "WS/14-15/D-2119", user_id: nil, person_id: nil, status: 0, address: "Near So...", price: "4999", details: "{\"domain\"=>\"\", \"package\"=>\"23\", \"site_id\"=>\"928\", ...", order_id: "464", order_type: nil, year: 1, payment_type: "cash", account_type: nil, cheque_name: "", comment: nil, tax: nil,, cheque_number: nil, chequenumber: "", payment_level: nil, invoice_type: nil>
or
> PaperTrail.serializer.load(i.object)
=> {"id"=>115, "created_at"=>2015-02-05 06:43:17 UTC, "updated_at"=>2015-02-05 06:43:17 UTC, "entity_type"=>"Site", "entity_id"=>928, "invoice_id"=>"W", "user_id"=>nil, "person_id"=>nil, "status"=>0, "person_email"=>"rium#gmail.com", "entity_value"=>"u.com", "address"=>"li ", "price"=>"4999", "details"=>"{}", "orr_id"=>"655", "processed_by"=>"", "order_type"=>nil, "year"=>1, "payment_type"=>"cash", "account_type"=>nil, "cheque_name"=>"", "comment"=>nil, "tax"=>nil, "comments"=>"Customer made payment by cash with receipt Nos: 041 & 042.", "cheque_number"=>nil, "chequenumber"=>"", "payment_level"=>nil, "invoice_type"=>nil}
second way will give the form of hash.

Rails 3: use contents of an array as variables in a where method

I have three models: Isbn, Sale and Channel.
Aim: to get a list in the isbns show.html.erb view which looks something like this:
Isbn: myisbn
Total sales for myisbn: 100
Myisbn sales for channel 1: 50
Myisbn sales for channel 2: 25
Myisbn sales for channel 3: 25
Here are my models.
Isbn.rb model
has_many :sales
has_many :channels, :through => :sales
Sale.rb model (has attributes sales_channel_id, isbn_id, quantity)
has_many :channels
belongs_to :isbn
Channel.rb model:
belongs_to :sale
I've been working in the isbns controller, in the show method, just to get something to work. I thought I'd refactor later - advice on whether any of this stuff should go in the model would be most welcome.
So far I've got this:
#channelisbn = Sale.where("sales_channel_id =?',1).where("isbn_id=?",3)
#channelsalesisbn = 0
#channelisbn.each {|y| #channelsalesisbn =+ y.quantity}
This successfully gets all the sales where Channel ID is 1 and ISBN id is 3. But it's not much use, as the IDs are hard coded. So I got the Channel IDs into an array:
#channellist = Channel.all
#channel = 0
#channelarray = #channellist.map {|z| #channel = z.id}
which gives me a lovely array of [1,2,3,4]
But I can't figure out how to pass the 1, then the 2, then the 3 and then the 4 into a block which can be used to look up an ISBN's sales which have that sales channel id. This is what I tried (still hardcoding the ISBN id - thought I'd tackle one problem at a time), which returned an empty array:
#channelarray.each do |channel|
#channelisbn = []
#channelisbn = Sale.where("sales_channel_id = ?", channel).where("isbn_id = ?",3)
#channelsalesisbn = 0
#result = []
#result << #channelisbn.each {|a| #channelsalesisbn =+ a.quantity}
end
I was then going to sum the contents of the array.
Any help would be gratefully received. This is my first post, so my zero acceptance rate will change soon!
UPDATE
Just to finish this question off, here's where I've ended up, which is great, and ready for tinkering with: an array, nicely grouped, giving me sales by isbn by channel. Thanks for the group_by tip off!
#in the show action in the isbns controller:
#isbn = Isbn.find(params[:id])
#channelarray = Channel.select(:id).all
#channelarray.group_by {|i| Sale.where("channel_id = ?",i).where("isbn_id =?", #isbn)}
From the console, line breaks added for clarity:
(sneakily set #isbn = 3 first of all, since in the console you can't pass params from a view, so the #isbn instance defined in the controller is nil in the console)
ruby-1.9.2-p180 :067 > #channelarray.group_by {|i| Sale.where("channel_id = ?",i).where("isbn_id =?", #isbn)}
=> {[#<Sale id: 1, isbn_id: 3, quantity: 10000, value: 12000, currency: "GBP", total_quantity: nil, created_at: "2011-05-06 12:30:35", updated_at: "2011-05-07 17:43:13", customer: "Waterstone's", retail_price: nil, discount: nil, invoice_date: "2011-05-24">, #<Sale id: 2, isbn_id: 3, quantity: 1000, value: 500, currency: "GBP", total_quantity: nil, created_at: "2011-05-07 09:37:53", updated_at: "2011-05-07 19:14:52", customer: "Borders", retail_price: nil, discount: nil, invoice_date: "2011-02-05">]=>[#<Channel id: 1>],
[#<Sale id: 3, isbn_id: 3, quantity: 500, value: 1500, currency: "", total_quantity: nil, created_at: "2011-05-07 09:38:11", updated_at: "2011-05-07 19:15:07", customer: "Borders", retail_price: nil, discount: nil, invoice_date: "2011-12-05">, #<Sale id: 4, isbn_id: 3, quantity: 45, value: 300, currency: "", total_quantity: nil, created_at: "2011-05-07 09:38:38", updated_at: "2011-05-07 19:15:36", customer: "Borders", retail_price: nil, discount: nil, invoice_date: "2011-06-05">]=>[#<Channel id: 2>],
[]=>[#<Channel id: 3>],
[]=>[#<Channel id: 4>]}
UPDATE 2
Ha, the hash I generated had the key value pairs the wrong way round. The array containing the sales data was the key - it should have been the value. Rubydocs saved the day:
#salesbychannel = #salesbychannelwrong.invert
The invert method switches the key-value pairs. Sweet.
What you're looking for is passing an array to a ARel#where(), like this:
Sale.where(:sales_channel_id => #channelarray)
This should execute an IN query. If that's not working, you can always pass the array to ActiveRecord#find, like this:
Sale.find(#channelarray)
Hope this helps

Finding elements in array of hashes

I'm trying to build a model of products which has many components. Some components are optional and depend on the choice the user is making to enable them or not.
I have two models, one is configuration and the other is elements (of that configuration).
At the beginning I bring all the elements of the array, and then create another array of those which will be shown by default.
But when I write the following code it gives me an error despite both objects being arrays of hashes.
So I bring my first array of all elements:
irb(main):252:0* #all = Configuration.find(1).elements
=> [#<Element id: 1, name: "elem1", quantity: 1, position: 1, subposition: nil, created_at: nil, updated_at: nil, configuration_id: 1>, #<Element id: 2, name: "elem2", quantity: 2, position: 2, subposition: 1, created_at: nil, updated_at: nil, configuration_id: 1>, #<Element id: 3, name: "elem3", quantity: 3, position: 2, subposition: 2, created_at: nil, updated_at: nil, configuration_id: 1>, #<Element id: 4, name: "elem4", quantity: 4, position: 3, subposition: nil, created_at: nil, updated_at: nil, configuration_id: 1>]
Then I filter to be only those that have a subposition nil or 1
irb(main):253:0> #default = #all.where(:subposition=>nil).concat(#all.where(:subposition=>1))
=> [#<Element id: 1, name: "elem1", quantity: 1, position: 1, subposition: nil, created_at: nil, updated_at: nil, configuration_id: 1>, #<Element id: 4, name: "elem4", quantity: 4, position: 3, subposition: nil, created_at: nil, updated_at: nil, configuration_id: 1>, #<Element id: 2, name: "elem2", quantity: 2, position: 2, subposition: 1, created_at: nil, updated_at: nil, configuration_id: 1>]
So far so good, as you can see, Elem3 is not being shown in #default as it doesn't meet the requiements.
The problem comes when I try to play with the arrays as I need to perform certain operations.
irb(main):257:0> #all.where(:position =>1)
=> [#<Element id: 1, name: "elem1", quantity: 1, position: 1, subposition: nil, created_at: nil, updated_at: nil, configuration_id: 1>]
But the same operation in #default will fail,
irb(main):258:0> #default.where(:position =>1)
NoMethodError: undefined method `where' for #<Array:0x2641660>
Now, they're both arrays of hashes and look the same, why is the same method failing in the second case?
Thanks a lot for your help!
Throughout your code, #all is an ActiveRecord::Relation, not an array. This lets you perform the standard .where call (among others). When you assigned to #default, you used .concat which evaluated the query and assigned an actual array to #default.
You might try a different approach in your second code block. Maybe something like this:
#default = #all.where("subposition is null or subposition = ?", 1)
Well, your problem is that concat transforms a collection into an array.
I'd replace:
irb(main):253:0> #default = #all.where(:subposition=>nil).concat(#all.where(:subposition=>1))
by:
#default = #all.where("subposition = '1' OR subposition = nil") #I'm unsure of the nil in the statement, I nerver remember, try NULL if it fails
This way, you make only one db query and you keep an ActiveRecord collection.
Thus, you'll be able to chain other where conditions on it.

Resources