Rails 4 enum validation - ruby-on-rails

This is the first time I'm using enums with rails 4 and I ran into some issues, have couple of dirty solutions in mind and wanted to check are there any more elegant solutions in place :
This is my table migration relevant part:
create_table :shippings do |t|
t.column :status, :integer, default: 0
end
My model:
class Shipping < ActiveRecord::Base
enum status: { initial_status: 0, frozen: 1, processed: 2 }
end
And I have this bit in my view (using simple form for) :
= f.input :status, :as => :select, :collection => Shipping.statuses, :required => true, :prompt => 'Please select', label: false
So in my controller:
def create
#shipping = Shipping.create!(shipping_params)
if #shipping.new_record?
return render 'new'
end
flash[:success] = 'Shipping saved successfully'
redirect_to home_path
end
private
def shipping_params
params.require(:shipping).permit(... :status)
end
So when I submit create form and the create action fire I get this validation error :
'1' is not a valid status
So I thought I knew that the issue was data type so I added this bit in the model :
before_validation :set_status_type
def set_status_type
self.status = status.to_i
end
But this didn't seem to do anything, how do I resolve this ? Has anyone had the similar experience?

You can find the solution here.
Basically, you need to pass the string ('initial_status', 'frozen' or 'processed'), not the integer. In other words, your form needs to look like this:
<select ...><option value="frozen">frozen</option>...</select>
You can achieve this by doing statuses.keys in your form. Also (I believe) you don't need the before_validation.
Optionally, you could add a validation like this:
validates_inclusion_of :status, in: Shipping.statuses.keys
However, I'm not sure that this validation makes sense, since trying to assign an invalid value to status raises an ArgumentError (see this).

Related

activeadmin and dynamic store accessors fails on new resource

I want to generate forms for a resource that has a postgres jsonb column :data, and I want the schema for these forms to be stored in a table in the database. After a lot of research I am 90% there but my method fails in ActiveAdmin forms upon create (not update). Can anyone explain this?
Sorry for the long code snippets. This is a fairly elaborate setup but I think it would be of some interest since if this works one could build arbitrary new schemas dynamically without hard-coding.
I am following along this previous discussion with Rails 6 and ActiveAdmin 2.6.1 and ruby 2.6.5.
I want to store Json Schemas in a table SampleActionSchema that belong_to SampleAction (using the json-schema gem for validation)
class SampleActionSchema < ApplicationRecord
validates :category, uniqueness: { case_sensitive: false }, allow_nil: false, allow_blank: true
validate :schema_is_json_schema
private
def schema_is_json_schema
metaschema = JSON::Validator.validator_for_name("draft4").metaschema
unless JSON::Validator.validate(metaschema, schema)
errors.add :schema, 'not a compliant json schema'
end
end
end
class SampleAction < ActiveRecord::Base
belongs_to :sample
validate :is_sample_action
validates :name, uniqueness: { case_sensitive: false }
after_initialize :add_field_accessors
before_create :add_field_accessors
before_update :add_field_accessors
def add_store_accessor field_name
singleton_class.class_eval {store_accessor :data, field_name.to_sym}
end
def add_field_accessors
num_fields = schema_properties.try(:keys).try(:count) || 0
schema_properties.keys.each {|field_name| add_store_accessor field_name} if num_fields > 0
end
def schema_properties
schema_arr=SampleActionSchema.where(category: category)
if schema_arr.size>0
sc=schema_arr[0]
if !sc.schema.empty?
props=sc.schema["properties"]
else
props=[]
end
else
[]
end
end
private
def is_sample_action
sa=SampleActionSchema.where(category: category)
errors.add :category, 'not a known sample action' unless (sa.size>0)
errors.add :base, 'incorrect json format' unless (sa.size>0) && JSON::Validator.validate(sa[0].schema, data)
end
end
This all works correctly; For example, for a simple schema called category: "cleave", where :data looks like data: {quality: "good"}, I can create a resource as follows in the rails console:
sa=SampleAction.new(sample_id: 6, name: "test0", data: {}, category: "cleave" )
=> #<SampleAction id: nil, name: "test0", category: "cleave", data: {}, created_at: nil, updated_at: nil, sample_id: 6>
sa.quality = "good" => true
sa.save => true
To make this system work in AA forms, I call the normal path (new or edit)_admix_sample_action_form with params: {category: "cleave"} and then I generate permit_params dynamically:
ActiveAdmin.register SampleAction, namespace: :admix do
permit_params do
prms=[:name, :category, :data, :sample_id, :created_at, :updated_at]
#the first case is creating a new record (gets parameter from admix/sample_actions/new?category="xxx"
#the second case is updating an existing record
#falls back to blank (no extra parameters)
categ = #_params[:category] || (#_params[:sample_action][:category] if #_params[:sample_action]) || nil
cat=SampleActionSchema.where(category: categ)
if cat.size>0 && !cat[0].schema.empty?
cat[0].schema["properties"].each do |key, value|
prms+=[key.to_sym]
end
end
prms
end
form do |f|
f.semantic_errors
new=f.object.new_record?
cat=params[:category] || f.object.category
f.object.category=cat if cat && new
f.object.add_field_accessors if new
sas=SampleActionSchema.where(category: cat)
is_schema=(sas.size>0) && !sas[0].schema.empty?
if session[:active_sample]
f.object.sample_id=session[:active_sample]
end
f.inputs "Sample Action" do
f.input :sample_id
f.input :name
f.input :category
if !is_schema
f.input :data, as: :jsonb
else
f.object.schema_properties.each do |key, value|
f.input key.to_sym, as: :string
end
end
end
f.actions
end
Everything works fine if I am editing an existing resource (as created in the console above). The form is displayed and all the dynamic fields are updated upon submit. But when creating a new resource where e.g. :data is of the form data: {quality: "good"} I get
ActiveModel::UnknownAttributeError in Admix::SampleActionsController#create
unknown attribute 'quality' for SampleAction.
I have tried to both add_accessors in the form and to override the new command to add the accessors after initialize (these should not be needed because the ActiveRecord callback appears to do the job at the right time).
def new
build_resource
resource.add_field_accessors
new!
end
Somehow when the resource is created in the AA controller, it seems impossible to get the accessors stored even though it works fine in the console. Does anyone have a strategy to initialize the resource correctly?
SOLUTION:
I traced what AA was doing to figure out the minimum number of commands needed. It was necessary to add code to build_new_resource to ensure that any new resource AA built had the correct :category field, and once doing so, make the call to dynamically add the store_accessor keys to the newly built instance.
Now users can create their own original schemas and records that use them, without any further programming! I hope others find this useful, I certainly will.
There are a couple ugly solutions here, one is that adding the parameters to the active admin new route call is not expected by AA, but it still works. I guess this parameter could be passed in some other way, but quick and dirty does the job. The other is that I had to have the form generate a session variable to store what kind of schema was used, in order for the post-form-submission build to know, since pressing the "Create Move" button clears the params from the url.
The operations are as follows: for a model called Move with field :data that should be dynamically serialized into fields according to the json schema tables, both
admin/moves/new?category="cleave" and admin/moves/#/edit find the "cleave" schema from the schema table, and correctly create and populate a form with the serialized parameters. And, direct writes to the db
m=Move.new(category: "cleave") ==> true
m.update(name: "t2", quality: "fine") ==> true
work as expected. The schema table is defined as:
require "json-schema"
class SampleActionSchema < ApplicationRecord
validates :category, uniqueness: { case_sensitive: false }, allow_nil: false, allow_blank: true
validate :schema_is_json_schema
def self.schema_keys(categ)
sas=SampleActionSchema.find_by(category: categ)
schema_keys= sas.nil? ? [] : sas[:schema]["properties"].keys.map{|k| k.to_sym}
end
private
def schema_is_json_schema
metaschema = JSON::Validator.validator_for_name("draft4").metaschema
unless JSON::Validator.validate(metaschema, schema)
errors.add :schema, 'not a compliant json schema'
end
end
end
The Move table that employs this schema is:
class Move < ApplicationRecord
after_initialize :add_field_accessors
def add_field_accessors
if category!=""
keys=SampleActionSchema.schema_keys(category)
keys.each {|k| singleton_class.class_eval{store_accessor :data, k}}
end
end
end
Finally, the working controller:
ActiveAdmin.register Move do
permit_params do
#choice 1 is for new records, choice 2 is for editing existing
categ = #_params[:category] || (#_params[:move][:category] if #_params[:move]) || ""
keys=SampleActionSchema.schema_keys(categ)
prms = [:name, :data] + keys
end
form do |f|
new=f.object.new_record?
f.object.category=params[:category] if new
if new
session[:current_category]=params[:category]
f.object.add_field_accessors
else
session[:current_category] = ""
end
keys=SampleActionSchema.schema_keys(f.object.category)
f.inputs do
f.input :name
f.input :category
keys.each {|k| f.input k}
end
f.actions
end
controller do
def build_new_resource
r=super
r.assign_attributes(category: session[:current_category])
r.add_field_accessors
r
end
end
end

Children objects validation, at least one with a given value

I am implementing a quiz - like feature. Each quiz is composed of several questions, each question has 3 possible answers.
I need to ensure that for each question there is at least one correct answer.
My code looks like this:
AssessmentQuestion.rb (model)
has_many :assessment_options, inverse_of: :assessment_question, autosave: true
accepts_nested_attributes_for :assessment_options
(...)
validate :has_correct_answer
(...)
def has_correct_answer
errors.add(:question, "no correct answer") unless self.assessment_options.exists?(is_correct: true)
end
Unfortunately this validation does not work - it raises an error ('no correct answer' even if there is an assessment_option with a correct answer.
Update (answer for NM Pennypacker question):
AssessmentQuestionsController.rb:
def new
params[:course_id].present? ? edited_course : all_courses
#assessment_question = AssessmentQuestion.new
3.times {#assessment_question.assessment_options.build}
end
def create
#assessment_question = AssessmentQuestion.new(assessment_question_params)
if #assessment_question.save
redirect_to course_path(assessment_question_params[:course_id]), notice: 'Assessment question was successfully created.'
else
params[:course_id].present? ? edited_course : all_courses
render :new
end
end
_form.htm.erb
assessment_options are being added after
<%= form.fields_for :assessment_options do |ao| %>
Update (answer for Emilio Menéndez question):
assessment_question_params:
def assessment_question_params
params.require(:assessment_question).permit(:id, :question, :course_id, :active,
{ assessment_options_attributes: [:id, :assessment_question_id, :answer, :is_correct] } )
end
I've run into this recently as well in a Rails 5.2 app. It seems strong_parameters isn't working with accepts_nested_attributes_for as described in the docs here: https://edgeapi.rubyonrails.org/classes/ActionController/StrongParameters.html
In my case I had to swap out my_model_attributes: [] for my_model: [], so in your case:
def assessment_question_params
params.require(:assessment_question).permit(
:id,
:question,
:course_id,
:active,
{
assessment_options: [
:id,
:assessment_question_id,
:answer,
:is_correct
]
}
)
end
The root cause is that following statement always returns false - probably because objects are not saved yet.
self.assessment_options.exists?(is_correct: true)
To solve this issue i had to modify has_correct_answer as follows:
def has_correct_answer
errors.add(:question, "no correct answer") unless self.assessment_options.select { |o| o.is_correct == true}.count > 0
end
Any better idea / solution?

How can we assign values to certain model variables in ruby on rails forms without the user having to input them?

I have generated a scaffold in rails to generate a model called transactions which has :
from(int)
to(int)
amount(double)
status(string)
kind(string)
start(datetime)
effective(datetime).
A form to this effect was automatically created. What I want to know is, is there a way to only get some of these values from the user, and add the others automatically? In this case, from, to, amount and kind need to be entered by the user. status should always be defaulted to "pending", and start should have the current date and time. effective should be null.
You can do this in many ways
First:
You can use Active Record Callbacks to accomplish this.
Add callback in your model app/models/transaction.rb
before_create :assign_default_attributes
def assign_default_attributes
self.status = 'pending' if self.pending.blank?
self.start = Time.now if self.start.blank?
end
Note: Make sure you remove status, start and effective from permitted params from controller.
Second
Modify app/controllers/transactions_controller.rb create action.
def create
transaction = Transaction.new(status: 'pending', start: Time.now)
transaction.assign_attributes(transaction_params)
if transaction.save
redirect_to transactions_path, notice: 'Transaction Created'
else
flash[:alert] = 'Enter Valid data'
render :new
end
end
Note: Make sure you remove status, start and effective from permitted params from controller.
You can add a migration to change the default values:
class ChangeDefaultValueForStatus < ActiveRecord::Migration
def change
change_column :transactions, :status, :string, default: "Pending"
change_column :transactions, :effective, :datetime, default: nil
end
end
Instead of using start, you can use the in-built timestamps to automatically get the date and time of when a record was created or updated:
class AddTimestampsToTransactions < ActiveRecord::Migration
def change_table
add_column(:transactions, :created_at, :datetime)
add_column(:transactions, :updated_at, :datetime)
end
end
i think you need default values
for status,set the default value.
Add this in your migration to add default value to columns-
t.string :status, default: "Pending"
check Migration api for more details
For start date,you can set todays time in your form.
<div class="form_group">
<label>Start time:</label></br>
<%= f.datetime_select :starts_at , :class=>"form-control",:start_year => Date.current.year, :end_year => Date.current.year,:selected=> Date.today, :order=> [:day, :month, :year],:start_year=> Time.now.year,:default=> 1.days.from_now,:prompt=> {day: 'Choose day', month: 'Choose month', year: 'Choose year'},:placeholder=>"Enter start time",:required=>true %>
</div>
Check the datetime_select api for more details.

Rails + Postgresql: data not saved first time

The first time I try to submit the form I get the error saying
"Price is not a valid number"
It's OK the second time I try to submit it (with the same valid data in :price field).
If I don't add validation in the model, then the form is submitted, but value of price is not saved.
What could be going on? Is there something special about .decimal field?
db schema:
t.decimal "price"
model
validates :price, numericality: { :greater_than => 0, :less_than_or_equal_to => 100000000 }
form view file
<%= f.number_field :price, class: "short red" %>
controller
def new
#product = Product.new
end
def create
#product = Product.new(product_params)
if #product.save
redirect_to #product
else
render :new
end
end
private
def product_params
params.require(:product).permit(:name, :description, :image, :price, :user_id)
end
logs
Started POST "/products" for xxx.132 at 2014-10-15 22:56:51 +0000
Processing by ProductsController#create as HTML
Parameters: {"utf8"=>"✓",
"authenticity_token"=>"abte/LtO0T/ZtSXQIuXVVjjUvwHw5jDUJ1yIKCOWRx2=",
"product"=>{"name"=>"", "description"=>"", "user_id"
=>"1"}, "commit"=>"Submit"}
Some things you can check:
The snippet from your form starts f.number_field. Check that you are using something like <%= form_for(#product) do |f| %> at the top of the form.
Try to create a product using the rails console.
In the rails console, try something like this:
> p = Product.new
> p.valid?
#=> TRUE or FALSE should appear
> p.errors.full_messages.to_sentence
# you should see a full list of all failed validations from your Product model
If these don't help, try pasting in the entire product_controller.rb and _form.html.erb files into your question, and I'll take a look again.
Try to change your migration to:
t.decimal :price, precision: 8, scale: 2 #for example
Then, change validation to:
validates :price, numericality: {greater_than_or_equal_to: 0.01, :less_than_or_equal_to => 100000000 }
In PostgreSQL next implementations behavior with :decimal columns:
PostgreSQL: :precision [1..infinity], :scale [0..infinity]. No
default.
I hope, this example from "Agile Web Development with Rails 4" help you to understand validation of decimal numbers:
it’s possible to enter a number such as 0.001 into this field. Because
the database stores just two digits after the decimal point, this
would end up being zero in the database, even though it would pass the
validation if we compared against zero. Checking that the number is at
least 1 cent ensures only correct values end up being stored.

Ruby Style Question: storing hash constant with different possible values

This is more of a style question, I'm wondering what other people do.
Let's say I have a field in my database called "status" for a blog post. And I want it to have several possible values, like "draft", "awaiting review", and "posted", just as an example.
Obviously we don't want to "hard code" in these magic values each time, that wouldn't be DRY.
So what I sometimes do is something like this:
class Post
STATUS = {
:draft => "draft",
:awaiting_review => "awaiting review",
:posted => "posted"
}
...
end
Then I can write code referring to it later as STATUS[:draft] or Post::STATUS[:draft] etc.
This works ok, but there are a few things I don't like about it.
If you have a typo and call something like STATUS[:something_that_does_not_exist] it won't throw an error, it just returns nil, and may end up setting this in the database, etc before you ever notice a bug
It doesn't look clean or ruby-ish to write stuff like if some_var == Post::STATUS[:draft] ...
I dunno, something tells me there is a better way, but just wanted to see what other people do. Thanks!
You can use Hash.new and give it a block argument which is called if a key is unknown.
class Post
STATUS = Hash.new{ |hash, key| raise( "Key #{ key } is unknown" )}.update(
:draft => "draft",
:awaiting_review => "awaiting review",
:posted => "posted" )
end
It's a bit messy but it works.
irb(main):007:0> Post::STATUS[ :draft ]
=> "draft"
irb(main):008:0> Post::STATUS[ :bogus ]
RuntimeError: Key bogus is unknown
from (irb):2
from (irb):8:in `call'
from (irb):8:in `default'
from (irb):8:in `[]'
from (irb):8
This is a common problem. Consider something like this:
class Post < ActiveRecord::Base
validates_inclusion_of :status, :in => [:draft, :awaiting_review, :posted]
def status
read_attribute(:status).to_sym
end
def status= (value)
write_attribute(:status, value.to_s)
end
end
You can use a third-party ActiveRecord plugin called symbolize to make this even easier:
class Post < ActiveRecord::Base
symbolize :status
end
You could use a class method to raise an exception on a missing key:
class Post
def self.status(key)
statuses = {
:draft => "draft",
:awaiting_review => "awaiting review",
:posted => "posted"
}
raise StatusError unless statuses.has_key?(key)
statuses[key]
end
end
class StatusError < StandardError; end
Potentially, you could also use this method to store the statuses as integers in the database by changing your strings to integers (in the hash), converting your column types, and adding a getter and a setter.
I do it like this:
class Post
DRAFT = "draft"
AWAITING_REPLY = "awaiting reply"
POSTED = "posted"
STATUSES = [DRAFT, AWAITING_REPLY, POSTED]
validates_inclusion_of :status, :in => STATUSES
...
end
This way you get errors if you misspell one. If I have multiple sets of constants, I might do something like DRAFT_STATUS to distinguish.
Take a look at the attribute_mapper gem.
There's a related article that shows how you can handle the problem declaratively, like this (borrowed from the article):
class Post < ActiveRecord::Base
include AttributeMapper
map_attribute :status, :to => {
:draft => 1,
:reviewed => 2,
:published => 3
}
end
...which looks rather stylish.
Even though this is an old post, for somebody stumbling across this, you can use the fetch method on Hash, which raises an error (when no default is passed) if the given key is not found.
STATUS = {
:draft => "draft",
:awaiting_review => "awaiting review",
:posted => "posted"
}
STATUS.fetch(:draft) #=> "draft"
STATUS.fetch(:invalid_key) #=> KeyError: key not found: invalid_key

Resources