hope you are all well!
I have a general enquiry that I was wondering if some kind soul could help with? it's really a matter of curiosity at the minute but I feel like it could be quite a useful snippet of information in the future.
Is it possible to write a method that can be passed the name of a table and the name of an attribute (column) and perform operations on these? I suppose the main use for such methods would be for keeping code dry when doing repetitive operations on tables.
as an example (though entirely a toy example) suppose I had a method:
def switch(table_name, column_name)
#do some operation on table_name.column_name
end
I have figured out how to access a table by doing something like this:
def model_for_table(table_name)
table_name.to_s.classify.constantize
end
this will take an underscored_lowercase_string and return the table name so that something like model_for_table("registered_user").find(1) though this is unnecessary in situations where the table name can be hard coded
But it does not like model_for_table("registered_user").column_name as used in the example above. is there something analogous to the model_for_table method supplied above to turn a string into an attribute name?
Does anybody know how I could implement this? is it even possible?
Thanks in advance
The problem is that you need an instance of the model you are working on in order to access a column. If you have a RegisteredUser model try doing (in a Rails console) RegisteredUser.id (or any attribute name). It won't work. However, if you did RegisteredUser.first.id (assuming you have one saved) it will work.
So it depends on what you want to accomplish. If your switch method is meant to do something with instances of your model, then this can still work.
def switch(table_name, column_name)
model = model_for_table(table_name)
model.all.each do |model_instance|
puts "model_instance #{column_name} is #{model_instance.send(column_name)}"
end
end
Note: The send method takes in a symbol or a String and executes the method with that name on the instance it was called on. This is a normal Ruby thing, not a Rails thing.
Remember, your model_for_table method is returning back the class, not an instance.
Related
I'm in the process of updating a website I made almost 2 years ago. It was my first real website and I made some mistakes (some more serious that others).
What apparently is one of my biggest is making database calls from the view.
I'm pretty damn sure there is a better way to do this:
Use Case:
Someone fills out a form for a new subject, populating the Subject table, and they have been marked "enrolled", Subject.enrolled = 1
Based on that, I now need to create a record in 5 other tables (such as Baseline)
Downhill from here, here is my method
Determine if the record exist based on subject_id from Subject (sub)
<$ if Baseline.where(subject_id: sub.subject_id).first != nil $>
If it does not exist, create the record, (otherwise display the link)
<%= Baseline.create(subject_id: sub.subject_id) %>
This all happens in the view, and creates a front-end table with links to each record in the process. So I'm creating records based on for-loop logic...
Question:
So I'm looking for direction. I don't want to guess how to do this - I'm pretty sure the model/controller should do this - I want to learn how to do it correctly. How do I create records automatically, based on a value in a table?
Thank you for your time.
Not quite sure how your domain and code looks like, but to answer this question: 'How do I create records automatically, based on a value in a table?', it seems that you could use ActiveRecord callbacks, like this:
class Subject < ActiveRecord::Base
after_commit :create_baseline_if_enrolled, on: [:create, :update]
private
def create_baseline_if_enrolled
return unless enrolled?
# enrolled? == true, you may create these models here
end
end
To answer your question:
It depends :) This is just one possible solution. Another one would be to put such a custom logic in your SubjectsController and call it directly from the #create, #update methods. Both approaches have pros and cons. For example, abusing callbacks (anywhere) makes code less readable and harder to debug. On the other hand, putting such logic in controllers puts a burden on you that you have to remember about calling it if you happen to be editing subjects in other places (but is more explicit). Whichever way you choose, remember not to make your classes too fat, for example try to use service object pattern to separate such custom logic as soon as you feel like it is getting out of hand. :) And don't forget about tests - when things go wrong, tests make refactoring easier.
I have a little problem. I found it really hard to create a title for this, I hope I can explain it better:
One of our clients demanded that in every instance of "item_number", when it's from the Product model, the app will have to show it with 5 leading zeroes.
The "item_number" proprety in that model is in fact an integer on the database.
Our first approach would be just change in the model the value:
def item_number
item_number.to_s.rjust(5, '0')
end
Of course we get a:
SystemStackError at /
stack level too deep
And I'm pretty sure this will mess up when creating/updating records, but it doesn't matter, it doesn't work anyways.
The solution would be simple, just create:
def item_number_with_leading_zeroes
item_number.to_s.rjust(5, '0')
end
And replace item_number with it on our views.
But, we have over 5.000 usages of this, and some of them were not from this model (and we only need to replace the item_number on this model) and some of them are inside Iterators that we coded not using the parent object name (so I may not know if it's from this model or not), others are from custom form inputs, we can't just "replace all".
I would have to manually check every single instance of "item_number" and see if it's needed to change it to that new method or not. And I can still make mistakes and impact a lot of stuff.
I only need to change it on the views, I thought about a way to get it working with the helpers but that would still mean I would have to change it case-by-case.
There is another option to fix it, updating all records on the database to string with the zeroes and making a before_save option from now on, but we would like a rails solution first, before going that way (leaving the DB as is right now).
I'm out of ideas, I don't know if there is a solution, I thought about asking here first hoping someone can share some thought.
Is there any way I can do this without having to manually check the whole application?
Ruby 2, Rails 4.0
Thank you!
Try using read_attribute to avoid the recursion:
class Product < ActiveRecord::Base
...
def item_number
read_attribute(:item_number).to_s.rjust(5, '0')
end
...
end
I realize this is not necessarily the smartest way to do this, but now my curiosity is active and I am curious how to do it.
I have a model in the Rails project. We'll call it Deal. Per ActiveRecord and all that cool stuff there are columns defined in the database like UPDATED_AT and those become methods on Deal: deal.updated_at => '04/19/1966 3:15am'
Say I wanted to instead have methods that told me the day of the week rather than the whole date and time thing. I realize there are methods ON the DateTime class so I can do
deal.updated_at.day_of_week => 'Monday' (*)
but what if I just wanted
deal.updated_day => 'Monday'
I can write in deal.rb
def update_day
self.updated_at.day_of_week
end
Got it.
But what if I wanted it to ALWAYS have the method available for ANY date column that was added to the model?
I saw define_method out there (some here on StackOverflow). So I understand that. But I would want to call it right after ActiveRecord did its magic, right? So if my Deal model had updated_at, created_at, offered_at and lawsuit_at I would want matching methods for each one. More importantly, if another developer came and added a column called scammed_at I would want scammed_day created along with the scammed_at method.
How would I do that?
Thanks.
(*) Uh, or something like that, I always look that call up.
I guess something like the following should do the trick. In your model:
# looping through all model's columns
self.columns.each do |column|
#if column's name ends with "_at"
if column.name =~ /_at$/
#create method like "udpated_day"
define_method "#{column.name[0..-4]}_day" do
self.send(column.name).day_of_week
end
end
end
But it implies every column has a valid day_of_week method...
Well you get the idea I think. Don't hesitate to ask for details
I'm trying to use attr_accessor for a date which normally works fine except when I attempt to use it with the select_date helper method.
Looking at the code behind the helper method I'm guessing it looks for the table column with date type. And in this case since there is no table it's not handling it correctly and I get:
ActiveRecord::MultiparameterAssignmentErrors
"search"=>{"number_of_days"=>"3",
"searchable_id"=>"6933",
"startdate(1i)"=>"2011",
"startdate(2i)"=>"2",
"startdate(3i)"=>"11"}}
Is there a way around this? Or do I need to create some kind of before filter in the controller? I'd prefer doing it on the model level, but I'm not sure how to handle this case? An attr_accessor for each seems a bit over kill. Anyone else have an elegant solution?
attr_accessor fields don't usually get saved when you save/update to the model. How are you updating the model?
Also, you can convert the startdate params to a date object like this :
#start_date = Date.civil(params[:search][:"startdate(1i)"].to_i,params[:search][:"startdate(2i)"].to_i,params[:search][:"startdate(3i)"].to_i)
Check here
select_date is for building the dropdowns which are not associated with a model field (with the idea that you can then pick them up on the other side and do what you want with them). I assume you're meaning date_select which does run off the model?
In any case, as far as I know, long story short, there's no nice and pretty way to get this to work. It's not because of the way the helper works, but because of the way that active record deals with these attributes split into multiple parameters.
In a bit more detail if you're interested, the reason why this doesn't work easily is because when Active Record is dealing with the params you've passed in, it goes through execute_callstack_for_multiparameter_attributes which interprets the keys which have been split into the "date(1i)" style, and mungs them into the applicable class which they should be (a date or time object). The way it works out whether it should create a date or time is by checking it against the type of the attribute (see here), but since an your 'startdate' attribute isn't bound to a particular type, it doesn't get treated as a date or datetime column in the db would.
I think I would deal with it similarly to #Phyo-Wai-Win, but use select_date to set a different param, outside of the 'search' namespace which you then pass into the model as appropriate in the controller. This way, it's not much work, and it means you're not messing with the way you initialize the record or what attributes it expects.
Coming in way late, but in case anyone else stumbles by, the answer for modern rails lies in include ActiveRecord::AttributeAssignment in your model.
This answer did it for me.
I'm a little late here, but I just came across this problem and did not like the top answer. I found a method in the ActiveRecord source called extract_callstack_for_multiparameter_attributes (this is different than the method idlefingers mentioned)
I have the following method in my model. I am calling this method manually but you could probably override update_attributes to run it automatically when you save from the controller. The params argument is actually params[:my_model] from the controller.
attr_accessor :submit_from, :submit_to
def set_dates(params)
dates = extract_callstack_for_multiparameter_attributes(params)
dates.each_pair do |field, date_array|
send "#{field}=", Date.new(*date_array)
end
end
I've got a table that includes a column named "valid". This has caused a problem after updating to Rails 2. ActiveRecord is expecting "def valid?" to do validation, not return a boolean value from the database.
How do I work around this problem? Is renaming the column my only option?
As documented elsewhere, there are things you can do, but I'm going to suggest that they're probably going to be more trouble in the long run than biting the bullet and renaming the column.
If your database is not open to other apps, that is - otherwise you're just going to suffer to some extent whatever you do...
Why rename? One of the greatest benefits that we get from Rails is convention over configuration. The "magic", if you will. (Some say that it's actually a bad thing, but go with me one this). If you retain a column named "valid", then nyou're making your models inconsistent: this one needs to work differently from the others and that's bad. Or you could monkey-patch ActiveRecord::Base perhaps, so then all your models work the same but your app no longer follows convention.
From personal experience: I created a column named "user_id" which ActiveRecord, by convention, considered a foreign key (as it does anything ending in "_id"). I coded around it, which I now think was a mistake. Another item on the to-do list...
It's not necessarily wrong to go against Rails conventions: there are plenty of places where you can do so and they're well-documented. On the ActiveRecord side, many are specifically designed to reduce difficulty in connecting to legacy database schemas, for example. Take a good look at the pros and cons, as you're obviously doing, and weigh up your options.
I can prevent the crash by adding the following to my model, but it's not entirely satisfactory:
class << self
def instance_method_already_implemented?(method_name)
return true if method_name == 'valid?'
super
end
end
Do you need to see the column in your model? If not, overriding ActiveRecord::Base.columns will do the trick...
def self.columns
super.delete_if {|c| c.name == 'valid' }
end
You can access the attribute through the [] notation:
row[:valid] = "foo"
You'll get the DangerousAttributeError if you try to initialize an object like this:
row = MyModel.new :valid => "foo"
To prevent that, you can define an attribute setter for valid, like this:
def valid=(x)
self[:valid] = x
end
The valid? method will still be for row validation. You could define a different question method, like val? to get at the boolean, like this:
def val?
query_attribute('valid')
end
Now you can use row.val? to test the boolean