Rails Datetime split setter method - ruby-on-rails

I have a datetime column in a model and I want to display the form for this as separate text_fields for the time instead of the datetime_select helper drop down menus.
I know I will somehow have to validate and recreate the date to go back to the DB, but I am not sure how.
Should I use virtual attributes getter and setter methods? EG...
def virtual_minute=(minute)
...
end
def virtual_hour=(hour)
...
end
Or maybe in a callback?
A bit stucko!

You don't need to write these yourself if you're using an ActiveRecord model. Multi-parameter attributes can be submitted via forms in pieces. As an example look at how the datetime_select method you're not too fond of works:
http://apidock.com/rails/ActionView/Helpers/DateHelper/datetime_select
Internally the various parameters get submitted with order and type information so that they can be properly encoded. You can build off of this functionality by simply naming your fields the same way. It is not too hard to copy a helper method you like and put the adjusted version in your ApplicationHelper module.
The values here show up as parameters named something along the lines of:
model_type[attribute(1i)]
model_type[attribute(2i)]
model_type[attribute(3i)]
These are combined in the correct order, converted as required, and submitted to a constructor for that attributes particular type, such as DateTime.
This avoids having to create your own wrapper methods.
In any case, the easy way to create attributes is to use the attr_accessor method, such as:
class MyModel < ActiveRecord::Base
attr_accessor :some_date
end
That will create the appropriate accessor and mutator methods.
You can use a before_validation trigger to recombine the values in a single pass, or write a wrapper around the date retrieval method to reconsruct it as required.

Virtual attributes are good IMO since they store the user data in an instance variable.
Then in a before_save you construct a Time object assign it to the real variable.

I was trying to split the time from the date (i think this is your question) when I took a look at the source code and found the :ignore_date option:
time_select will also generate 3 input hidden tags, for the actual
year, month and day unless the option :ignore_date is set to
true.
If you set the :ignore_date to true, you must have a date_select on the same method within the form otherwise an
exception will be raised.
In other words, by setting it to true it's possible to split the date and the time.
See time_select and date_select
(it's also possible to use the datetime select with :discard_day, :discard_hour ...)

Related

How to make comma separated ids turn to array in rails parameters like ids[] would

So the normal way would be to have multiple input fields with a name of "obj_ids[]", then the values in rails ends up magically zipping them into an array.
Does rails have some secret way of alternately doing this with simply one field?
I'm needing to separate the input fields from the actual form, and so right now I'm basically grabbing them all, and running a JS join on them to send as one field in a separate form. I could duplicate them all as hidden fields but that seems a bit much. Also obviously could do a split in the controller, but I'm curious if there's another way.
Depending on your specific needs there are tons of ways to do this kind of thing in rails
hacking the params as #jrochkind has mentioned is one way but there are several more elegant solutions
create a virtual attribute in your model
class SomeModel
attr_reader :my_field
def my_field= value_as_comma_delimited_string
# here the the_field_i_really_want_set is an array type which is supported in mongo and postgres for exampld
self.the_field_i_really_want_set = value_as_comma_delimited_string.split(",")
end
end
You could do basically the same thing in a before_validation callback in this case you don't need to create an accessor
class User < ActiveRecord::Base
before_validation :normalize_name, on: :create
protected
def normalize_name
self.name = self.name.downcase.titleize # or whatever transformation you need
end
end
Another technique that I've been finding very useful and that is similar to the first one I showed is to override the accessor method directly. Here my_field is the actual field in your model's schema.
class SomeModel
def my_field= value_as_comma_delimited_string
self.write_attribute(:my_field,value_as_comma_delimited_string.split(","))
end
end
I don't think there's another way, I've looked into it before, for other cases.
For your case, if I understand right, you are constructing a form with JS and submitting the form with JS, and it's up to you whether to construct the form with multiple values mashed together in one input (which will turn into one URL query parameter) separated by commas, or separate fields/params.
I'd just go with separate fields/params.
Otherwise, on the controller side, you could do it in a before_filter rather than just the action method, even in a before_filter on your application_controller that applies to all actions, and you can even have the before_filter mutate Rails own params hash.
before_filter :split_commas
def split_commas
params[:my_field] = params[:my_field].split(",")
end
That should work, although mutating params hash like that has been the cause of so many frustrating bugs for me that I'm loathe to ever do it.
Adding another setter to model (or overriding existing one) as suggested by #hraynaud is one possibility. However in my opinion supporting some non-standard input format is more a controller's duty than model's. Therefore, I wouldn't put that logic into a model.
Instead, I'd introduce another controller's method which returns preprocessed parameters. Works great with Strong Parameters.
protected
def preprocessed_params
p = params.deep_dup
p[:obj_ids] = p[:obj_ids].split(",") if p[:obj_ids]
p
end
def permitted_params
preprocessed_params.permit(:obj_ids => [])
end
This snippet could be further modified by using alias_method_chain so that the preprocessed params are accessible via params whereas the raw ones are kept in params_without_preprocessing or something.

How to use read_attribute when manually defining getter/setter (attr_accessor or attr_writers)

I set up a search model and want to require at least one field is filled out. I found a question that helps with the validation, Rails: how to require at least one field not to be blank. (I tried all answers, but Voyta's seems the best.)
The validation is working, except when I want to redefine the getter/setter via attr_accessor or attr_writer. (I have virtual attributes on the form that need to be apart of the validation.) To figure out what the problem is, I tested with an attribute that is a regular attribute item_length. If I add attr_accessor :item_length, the validation stops to work. So, I guess the question is how to I read an attribute's value without using dot notation. Since the validation uses strings, I can't use the normal way of reading.
Here is a snippet:
if %w(keywords
item_length
item_length_feet
item_length_inches).all?{|attr| read_attribute(attr).blank?}
errors.add(:base, "Please fill out at least one field")
end
Like I said, the virtual attrbutes (length_inches and length_feet) do not work at all, and the normal attribute (length) works except if I redefine the getter/setter.
You should consider read_attribute as a private method for reading Active Record columns. Otherwise you should always use readers directly.
self.read_attribute(:item_length) # does not work
self.item_length # ok
Since you are trying to call this dynamically, you can use generic ruby method public_send to call the specified method
self.public_send(:item_length) # the same as self.item_length
As stated in comment, use send
array.all? {|attr| send(attr).blank?}
For those wondering if send is ok in this case, yes it is: object calls its own instance methods.
But send is a sharp tool, so whenever you use with other objects, ensure you use their public api with public_send

attr_accessor for a date and problems using select_date

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

How do you use multiple form fields to interact with a single method in rails?

I have a model which has start_at and end_at attributes. In the form for the end-user, I am displaying start_at using the standard datetime_select, but I'd rather not have a second datetime select presented to the user. I'm hoping I can create two fields that represent a duration; one for hours, the other for minutes. My question is, how in my view do I use form helpers to automatically fill in the fields when editing an existing entry. Furthermore, how would I connect that to the model and subsequently save the recording using the real attribute, end_at?
Thanks in advance for any advice you can give!
I have to do this a bunch and i've been doing the following:
Use the FormTagHelper versions of the calls for the field to be handled specially.
In the controller, read the form_tag values out of the params object.
delete the extra values:
params[:examplemodelname].delete :distance if params[:examplemodelname].has_key? :distance
put the 'real' values into the params object (in your example, ends_at)
call ExampleModelName.new(params[:examplemodelname]) or #examplemodelname.update_attributes(params[:examplemodelname]) as per usual.
Wouldn't logic like this be better
suited for the model? Fat model,
skinny controller?
I think this is absolutely right. I despise using a controller for stuff like this. In my opinion, controllers are best used for a few things:
Shuffling data between views and models, ala CRUD operations
Access control (usually with before_filters)
Support to fancy UI actions (multiple select, wizards, etc)
Of course everyone has to find their own balance, but I find "making an exception" for something like this tends to lead to exceptions everywhere. Not to mention the controller logic is harder to test.
To answer your question: you should simply define virtual attributes in your model that help you convert between start_at and a duration. Something like:
# Return the duration in seconds. Will look different if you want to use
# multiparameter assignment (which you definitely should consider)
def duration
(end_at - start_at).seconds
end
# Store the duration as an instance variable. Might need to use multiparameter
# assignment if you use a time_select() in the view.
def duration=(arg)
#duration = arg
end
before_save :set_end_at
def set_end_at
end_at = start_at + #duration.seconds
end
I'd usually set the actual attribute in a before_save to avoid any race conditions from the form assignment. You have no guarantee that start_at will get assigned before or after duration, and that can introduce bugs in your otherwise good logic.

How to iterate ActiveRecord Attributes, including attr_accessor methods

I've looked everywhere for an elegant solution. The essential problem seems to be that ActiveRecord attributes that map to database columns are handled completely differently in ActiveRecord::Base than attr_accessor methods.
I would like to do something like:
model.attribute_names.each do |name|
# do stuff
end
in a way that also includes attr_accessor fields, but not any other instance methods. I know this in not built-in, but what is the most elegant way to do it?
You can't really solve this. You can approximate a hack, but it's not something that will ever work nicely.
model.attribute_names should get you all the ActiveRecord ones, but the attr_accessor fields are not fields. They are just ordinary ruby methods, and the only way to get them is with model.instance_methods.
Idea 1
You could do model.attribute_names + model.instance_methods, but then you'd have to filter out all your other normal ruby methods initialize, save, etc which would be impractical.
To help filter the instance_methods you could match them up against model.instance_variables (you'd have to account for the # sign in the instance variables manually), but the problem with this is that instance variables don't actually exist at all until they are first assigned.
Idea 2
In your environment.rb, before anything else ever gets loaded, define your own self.attr_accessor in ActiveRecord::Base. This could then wrap the underlying attr_accessor but also save the attribute names to a private list. Then you'd be able to pull out of this list later on. However I'd advise against this... monkey-patching core language facilities like attr_accessor is guaranteed to bring you a lot of pain.
Idea 3
Define your own custom_attr_accessor in ActiveRecord::Base, which does the same thing as Idea 2, and use it in your code where you want to be able to retrieve the attribute names. This would be safe as you won't be clobbering the built-in attr_accessor method any more, but you'll have to change all your code to use custom_attr_accessor where neccessary
I guess in summary, what are you trying to do that needs to know about all the attr_accessor fields? Try look at your problem from a different angle if you can.
I came here looking to do the same thing, and found out it was the wrong approach altogether thanks to Orion's answer.
Incase anyone else's use case is similar to mine, here's my solution. I was using attr_accessor to add extra properties to the models after querying them from ActiveRecord. I then wanted to output the results as JSON etc.
A better solution is to first convert the Models from ActiveRecord into regular hashes, and then add the attr_accessor properties as regular hash keys.
Example:
model_hash = model_from_activerecord.attributes.to_options
model_hash[:key] = value
The solution I came up with for myself builds upon Orion Edwards' answer.
The code:
klass_attributes = klass.column_names + klass.instance_methods(false).
map(&:to_s).keep_if{|a| a.include?('=')}.map{|a| a.sub('=', '')}
The breakdown:
klass.instance_methods(false) brings back only instance methods, and not inherited methods.
map(&:to_s) converts the array of symbols into an array of strings so we can use the include? method. Also needed to merge with array of strings returned by column_names.
keep_if{|a| a.include?('=')} will remove all strings within the array that do not have an equals sign. This was important, since I noticed that attr_accessor attributes all had '='. Ex: 'app_url' vs 'login_url='
map{|a| a.sub('=', '')} is the final piece that then removes the '=' from each string in the array.
You end up with an array of strings that represent attributes, including attr_accessor attributes.

Resources