In my Rails 4 app, I'd like to accept a year as a string in a text field but store it as a date in the database, in case I ever decide to accept full dates. However, when I try to do this, I keep getting a nil date value. I've verified that the controller is properly passing along the parameters to the model, but at some point before the validation happens, the date has been set to nil.
Is what I'm trying to do possible? And if so, what critical step have I missed? Thanks!
This is the relevant part of the form:
<p>
<%= f.label :publication_date %><br>
<%= f.text_field :publication_date, :size => 4 %>
</p>
And the schema:
create_table "books", force: true do |t|
t.string "title"
t.datetime "created_at"
t.datetime "updated_at"
t.date "publication_date"
...
end
And the model validation:
validates :publication_date, length: { is: 4 }, numericality: true
before_validation :check_pub_date
def check_pub_date
# this prints nil
logger.debug "Publication date: #{self.publication_date}"
end
before_save :convert_pub_year
def convert_pub_year
# Never gets here because validation never passes
logger.debug "Publication year: #{self.publication_date}"
if self.publication_date
self.publication_date = DateTime.strptime(self.publication_date, "%Y")
end
end
Given that your model is expecting a full date to come through, you are hitting a problem when you are passing only a string from your form - I would create a simple text_field to capture the year and go from there...
In your view
<%= text_field_tag :pub_year %>
In your controller
publication_year = params[:pub_year]
#book.publication_date = DateTime.strptime(publication_year, "%Y")
or, of course, factor the parsing of year into a full date into the model.
Edit - expanding on this, it's also possible to create a property in your model that doesn't get saved to the database if you prefer, so in your model, you would have this:
attr_accessor :pub_year
Then, in your convert_pub_year, substitute in pub_year instead of publication_date
def convert_pub_year
if self.pub_year
self.publication_date = DateTime.strptime(self.pub_year, "%Y")
end
end
You controller would need to allow the pub_year parameter in addition to any others you want - so in the private methods - something like the following:
def book_params
params.require(:book).permit(:title, :pub_year)
end
And finally in your view, remove publication_date' and includepub_year`:
<%= f.label :pub_year %><br>
<%= f.text_field :pub_year %>
Hope that helps
The problem exists because validation occurs AFTER assignment of the string passed from the controller, i.e. what you are validating is not the string but the attribute after the type cast to a date. If this fails (because - as in your case - the date is incomplete and consists only of a year), the attribute is nil.
For situations like yours you might want to look into the attribute_before_type_cast functions/variables which will give you access to the original date string BEFORE it was type cast into nil.
Related
I have a form in a commerce application where users can add an item listing.
In this create#item form, I'd like the user to be able to select (from a dropdown menu) what currency their pricing is in.
I've created a Currency model, views and a controller so the admin can add currency types. (I want the admin to be able to limit currency types).
Here is the currency migration file:
class CreateCurrencies < ActiveRecord::Migration
def change
create_table :currencies do |t|
t.string :name
t.string :symbol
t.timestamps null: false
end
end
end
(The "symbol" being a string that holds the currency HTML code)
I connected Currency and Item with a belongs_to/has_many relationship in the db. I then implemented a dropdown menu in the create#item form where users can select the currency.
My question is, how can I display the currency symbol in the dropdown menu?
Here's what I tried.
<%= f.collection_select :currency, Currency.order(:name),:id, "#{:symbol}".html_safe %>
The problem is, this doesn't display the currency symbols as I would have hoped; it just displays the string that was entered (the currency HTML code).
For example, with the code as it is, if an Admin entered the currency HTML code for $ ($), the dropdown shows "$" isntead of the expected "$")
Thanks in advance!!
Quick answer is: use raw method to unescape the html code.
I've just reproduced your code on my machine and noted a strange behavior.
For some reason raw doesn't work with collection_select and I can't figure why. Consider using select helper and 'manually' iterate your collection. Here is two identical variants:
= form_for "test" do |f|
%p collection_select variant
= f.collection_select :currency, User.all, :id, raw(:symbol.to_s)
%p select variant (works!)
= f.select("currency", User.all.collect {|u| [ raw(u.symbol), u.id ] })
You can use the HTMLEntities gem. I would recommend setting it up as a helper method which you can use in the view.
helper file
def currency_symbol(currency_code)
HTMLEntities.new.decode(currency_code)
end
I am creating a web application as a training project that will take three inputs (Integer values for number of dice, number of sides and number of rolls) and input that into a series of tables that will facilitate calculations and the output of the results, and the saving of historical data (past rolls.)
I'm stuck.
Here is my ERB for the form:
<%= form_for(#run) do |f| %>
How many dice: <%= f.number_field :die_count, :placeholder => "# of dice" %><br>
How many sides: <%= f.number_field :die_sides, :placeholder => "# of sides" %><br>
How many rolls: <%= f.number_field :rolls, :placeholder => "# of rolls" %><br>
<%= f.submit "Roll!" %>
<% end %>
Here's my Model:
class Run < ActiveRecord::Base
has_many :rolls
belongs_to :user
validates_presence_of :die_count, :rolls, :die_sides
validates_numericality_of :die_count, :die_sides, :rolls
end
Here's my pertinent Controller info:
def new
#run = Run.new
end
def create
#run = Run.new(run_params)
end
private
def run_params
params.require(:run).permit(:die_count, :die_sides, :rolls)
end
For good measure, here's the migrate for making my DB table:
class CreateRuns < ActiveRecord::Migration
def change
create_table :runs do |t|
t.integer :die_count
t.integer :die_sides
t.integer :roll_count
t.timestamps null: false
end
end
end
At this point, I just want it to create a new record in the Run model. Here's the error I'm getting with the input of 5, 5, and 5 into the number_fields and click my submit button:
If I am reading this correctly, my application wants to iterate through the individual numbers as if they were arrays, but it can't because they are strings. This confuses me because they aren't strings in the first place. They are integers.
Pulling my hair out on this one.
:rolls is a reference to another model called Roll (have you created it?).
:roll_count is the attribute of your Run model that defines the number of rolls (as visible in your migration table).
They are two very different things, even though it stands to reason that :roll_count should in theory be equal to the number of Roll records your Run record 'owns'.
In your form, replace :rolls by :roll_count. Do the same in the run_params method in your controller. And also in the two validations in your model. (checklist: that's 4 places where you need to modify that symbol name).
In your controller, also change the content of your create method to #run = Run.create(run_params).
Edited (clarity): Run.new(params) creates a new instance of a Run object but doesn't save it in your DB. Run.create(params) creates the object AND saves it.
Finally, in your model, comment out the has_many :rolls until you have actually implemented the Roll model (no pun intended!) ; do the same for belongs_to :user if the User model is not implemented either.
After these changes, your form should normally work and data should be saved in your runs database table.
I believe you meant to replace rolls with roll_count everywhere in your code.
How many rolls: <%= f.number_field :roll_count, :placeholder => "# of rolls" %><br>
and
validates_presence_of :die_count, :roll_count, :die_sides
validates_numericality_of :die_count, :die_sides, :roll_count
and
def run_params
params.require(:run).permit(:die_count, :die_sides, :roll_count)
end
Can somebody explain this ? , it is from ruby guides
<%= collection_select(:person, :city_id, City.all, :id, :name) %>
I have an attachment model, I want to select its version with combobox when I create an object, and also I want a new version option.
here what attachment has
def change
create_table :attachments do |t|
t.string :filename
t.attachment :file
t.string :version
t.text :description
t.timestamps null: false
end
UPDATE:
<%= f.collection_select( :version, Attachment.where.not(version: nil), :version, :version) %>
it is working like that, but I don't understand,
Try this to avoid the nil value of the version:
collection_select(:f, :attachment_id, Attachment.where.not(version: nil), :id, :version)
Explanation of How collection_select Works:
collection_select(
:f, # field namespace
:attachment_id, # field name
# result of these two params will be: <select name="f[attachment_id]">...
# then you should specify some collection or array of rows.
# In your example it is:
Attachment.where.not(version: nil)
# Then, you should specify methods for generating options
:id, # this is name of method that will be called for every row, result will be set as key
:version # this is name of method that will be called for every row, result will be set as value
)
See this and this for more information.
check accepted answer from this thread for explanation on how collection_select works: Can someone explain collection_select to me in clear, simple terms?
In this select:
<%= collection_select(:f, :attachment_id, Attachment.all, :id, :version) %>
you display all versions from already created attachments, so if attachments table is empty you will get null
How can you convert a mysql datetime field into two form fields (1) date only, (2) time only, and combine both fields back into datetime format on form submit?
This would allow the use of the following gems, but store the dates in a single datetime field:
gem 'bootstrap-datepicker-rails'
gem 'bootstrap-timepicker-rails'
Thanks in advance!
Found the solution with help from #Althaf
Added virtual attributes to model.rb
Used before_save callback to convert back to datetime.
before_save :convert_to_datetime
def sched_date_field
sched_date.strftime("%d/%m/%Y") if sched_date.present?
end
def sched_time_field
sched_time.strftime("%I:%M%p") if sched_time.present?
end
def sched_date_field=(date)
# Change back to datetime friendly format
#sched_date_field = Date.parse(date).strftime("%Y-%m-%d")
end
def sched_time_field=(time)
# Change back to datetime friendly format
#sched_time_field = Time.parse(time).strftime("%H:%M:%S")
end
def convert_to_datetime
self.sched_time = DateTime.parse("#{#sched_date_field} #{#sched_time_field}")
end
Using Rails 4, needed to add sched_date_field and sched_time_field to strong params in controller.rb
Here are the fields in _form.html.erb
<%= f.label :sched_date_field, "Scheduled Date" %>
<%= f.text_field :sched_date_field, :class => "datepicker" %>
<%= f.label :sched_time_field, "Scheduled Time" %>
<%= f.text_field :sched_time_field, :class => "timepicker" %>
You can use date_time_attribute gem:
class MyModel < ActiveRecord::Base
include DateTimeAttribute
date_time_attribute :scheduled_at
end
It will allow you to set schedule_at_date and scheduled_at_time separately. Once attributes are set, values will be combined into schedule_at.
You could use virtual attributes See this Railscast and if you have a pro subscription the revised one.
Basically in the view you would the following
<%= f.label :date_field %>
<%= f.text :date_field %>
<%= f.label :time_field %>
<%= f.text :time_field %>
Your database would still keep a field which I'll call full_date
Now in your model you would have to define the above 2 fields as follows.
def date_field # What this returns will be what is shown in the field
full_date.strftime("%m-%d'%y") if full_date.present?
end
def time_field
full_date.strftime("%I:%M%p") if full_date.present?
end
def time_field=(time)
full_date = DateTime.parse("#{date_field} #{time_field})
end
Since it looks like you are using Rails 4, you'll have to permit date_field and time_field in your strong parameters.
Alternatively, I set up a solution in the controller that does all the datetime conversions before the object gets created, because changing the data in the model impacted all my tests and validations. "Event" is the object I'm creating here with the datetime values being assigned to it.
#In the controller:
def convert_to_datetime_and_assign(event, params)
date_field = Date.parse(params[:date_field]).strftime("%Y-%m-%d")
start_time_field = Time.parse(params[:start_time_field]).strftime("%H:%M:%S")
end_time_field = Time.parse(params[:end_time_field]).strftime("%H:%M:%S")
event.start_time = DateTime.parse("#{date_field} #{start_time_field}")
event.end_time = DateTime.parse("#{date_field} #{end_time_field}")
event
rescue ArgumentError
event.errors.add(:start_time, :invalid, message: "Date or time was invalid")
event
end
in the create and update controller methods I called the method above:
#event = convert_to_datetime_and_assign(#event, event_params)
I added fields for date_field, start_time_field and end_time_field in my forms for creating/updating "events". And in the model I added an accessor to be able to access those values.
attr_accessor :date_field, :start_time_field, :end_time_field
How to make two separated fields so can be handled to one datetime field in database?
I'm using jquery datetime picker that puts values like value in code example below.
Parameters includes only field with time but ignores field with date.
If I comment field with time then date is sent and saved but parameter looks like "2013-23-02" and it isn't forwarded like multi parameter.
#model
field :start_at, type: DateTime
#view
= f.text_field :start_at, :value => "2013-02-23"
= f.text_field :start_at, :value => "20:20:20"
you can use virtual attributes and callback (this is not tested but you should get the idea)
# model
attr_writer :start_at_time, :start_at_date
before_validation :build_start_at
def start_at_date
#start_at_date ||= start_at.to_date
end
def start_at_time
#start_at_time ||= start_at.strftime("%H:%M")
end
def build_start_at
self.start_at = Time.parse "#{start_at_date} #{start_at_time}"
end
# view
= f.text_field :start_at_date
= f.text_field :start_at_time