Children objects validation, at least one with a given value - ruby-on-rails

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?

Related

Underscore destroy method

for code below after checking checkbox on particular answer the answer will be deleted after submitting form.
Can somwone please explain if this destroy method needs to be defined anywhere or is that a default? I cannot find any documentation. Actually how this functionality works?
The source is from here:
http://railscasts.com/episodes/196-nested-model-form-part-1?autoplay=true
<p>
<%= f.label :content, "Answer" %>
<%= f.text_field :content %>
<%= f.check_box :_destroy %>
<%= f.label :_destroy, "Remove" %>
</p>
Short Answer:
For this, to work we define accepts_nested_attributes_for on the parent model which does all the magic.
Long Answer: (How accepts_nested_attributes_for does this magic?)
This is the source code:
def accepts_nested_attributes_for(*attr_names)
options = { allow_destroy: false, update_only: false }
options.update(attr_names.extract_options!)
options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
attr_names.each do |association_name|
if reflection = _reflect_on_association(association_name)
reflection.autosave = true
define_autosave_validation_callbacks(reflection)
nested_attributes_options = self.nested_attributes_options.dup
nested_attributes_options[association_name.to_sym] = options
self.nested_attributes_options = nested_attributes_options
type = (reflection.collection? ? :collection : :one_to_one)
generate_association_writer(association_name, type)
else
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
end
end
end
Here you can see it calls generate_association_writer(association_name, type) which defines the setter method like this:
def generate_association_writer(association_name, type)
generated_association_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
silence_redefinition_of_method :#{association_name}_attributes=
def #{association_name}_attributes=(attributes)
assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
end
eoruby
end
Suppose you have this class:
class BlogPost < ApplicationRecord
has_many :comments
accepts_nested_attributes_for :comments, allow_destroy: true
end
Then this will generate: comments_attributes= setter method dynamically on the model. Now from the view when you add a block:
<% f.fields_for :comments do |comment| %>
<% end %>
It passes the values in the comments_attributes key (if you check the params on the controller you will find that also you have to whitelist this attribute on strong params to make it work). So this automatically calls the generated setter method.
Now based on the type of association (single or collection) it calls assign_nested_attributes_for_one_to_one_association or assign_nested_attributes_for_collection_association method which calls assign_to_or_mark_for_destruction which marks the object for destruction if _destroy is true if you have allowed to destroy and it is an existing object.
For reference:
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
options = nested_attributes_options[association_name]
if attributes.respond_to?(:permitted?)
attributes = attributes.to_h
end
attributes = attributes.with_indifferent_access
existing_record = send(association_name)
if (options[:update_only] || !attributes["id"].blank?) && existing_record &&
(options[:update_only] || existing_record.id.to_s == attributes["id"].to_s)
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
elsif attributes["id"].present?
raise_nested_attributes_record_not_found!(association_name, attributes["id"])
elsif !reject_new_record?(association_name, attributes)
assignable_attributes = attributes.except(*UNASSIGNABLE_KEYS)
if existing_record && existing_record.new_record?
existing_record.assign_attributes(assignable_attributes)
association(association_name).initialize_attributes(existing_record)
else
method = :"build_#{association_name}"
if respond_to?(method)
send(method, assignable_attributes)
else
raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?"
end
end
end
end
def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
options = nested_attributes_options[association_name]
if attributes_collection.respond_to?(:permitted?)
attributes_collection = attributes_collection.to_h
end
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
raise ArgumentError, "Hash or Array expected for attribute `#{association_name}`, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
end
check_record_limit!(options[:limit], attributes_collection)
if attributes_collection.is_a? Hash
keys = attributes_collection.keys
attributes_collection = if keys.include?("id") || keys.include?(:id)
[attributes_collection]
else
attributes_collection.values
end
end
association = association(association_name)
existing_records = if association.loaded?
association.target
else
attribute_ids = attributes_collection.filter_map { |a| a["id"] || a[:id] }
attribute_ids.empty? ? [] : association.scope.where(association.klass.primary_key => attribute_ids)
end
attributes_collection.each do |attributes|
if attributes.respond_to?(:permitted?)
attributes = attributes.to_h
end
attributes = attributes.with_indifferent_access
if attributes["id"].blank?
unless reject_new_record?(association_name, attributes)
association.reader.build(attributes.except(*UNASSIGNABLE_KEYS))
end
elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes["id"].to_s }
unless call_reject_if(association_name, attributes)
# Make sure we are operating on the actual object which is in the association's
# proxy_target array (either by finding it, or adding it if not found)
# Take into account that the proxy_target may have changed due to callbacks
target_record = association.target.detect { |record| record.id.to_s == attributes["id"].to_s }
if target_record
existing_record = target_record
else
association.add_to_target(existing_record, skip_callbacks: true)
end
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
end
else
raise_nested_attributes_record_not_found!(association_name, attributes["id"])
end
end
end
def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
record.assign_attributes(attributes.except(*UNASSIGNABLE_KEYS))
record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy
end
def has_destroy_flag?(hash)
Type::Boolean.new.cast(hash["_destroy"])
end
You would find all the code in activerecord/lib/active_record/nested_attributes.rb class: https://github.com/rails/rails/blob/main/activerecord/lib/active_record/nested_attributes.rb
The _destroy 'attribute' functionality is part of working with nested forms/attributes.
Suppose you have a Quiz model that has_many :answers. To be able to update a Quiz and its child Answer objects in one operation, you declare that with accepts_nested_attributes_for:
class Quiz < ApplicationRecord
has_many :answers
accepts_nested_attributes_for :answers
end
If your form uses the <% f.fields_for :answers %> construct (shown in your video), Rails will organize the parameter hash so that you can update the parent quiz and it will also automatically create/update any child answers. By default, though, you can only update an existing child or create new ones. If you want to also be able to delete nested objects, you need first to explicitly enable it in the model:
class Quiz < ApplicationRecord
has_many :answers
accepts_nested_attributes_for :answers, allow_destroy: true
end
Then, per the API docs for NestedAttributes:
when you add the _destroy key to the attributes hash, with a value
that evaluates to true, you will destroy the associated model.
So, as long as you have enabled nested destruction on the model, adding a f.check_box :_destroy field to your nested form, Rails will automagically remove the marked record.

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

Default ActiveRecord Associations in Rails 4

I'm trying to implement a persitent model Setting storage in Rails, using the Active Record. I've already saw other gems like ledermann/rails-settings, but I don't want other dependency, because I'll use it only for one model and need the ability to customize it.
I've created 3 models, "Company", "Setting", "CompanySetting". For the association, I done the follow:
company.rb
has_many :company_settings
setting.rb
has_many :company_settings
has_many :company, through: :company_settings
company_setting.rb
belongs_to :company
belongs_to :setting
But I've a problem, for example, I seed the Settings table with N settings, and I need to have these Settings built when I try to access the Company settings, since they don't have an CompanySetting entry for that Setting.
My attempt was the follow:
company.rb
has_many :company_settings
accepts_nested_attributes_for :company_settings
def load_company_settings
Setting.all.collect { |setting|
company_settings.find_by( setting: setting ) || company_settings.build( { setting: setting, value: '' } )
}
end
And then, in my form (using Simple Form):
= f.simple_fields_for :company_settings, #company.load_company_settings do |s|
= s.input :value
It renders the correctly number of fields (the N fields in my Setting table), and return they values if exist, otherwise, returns an empty string as value. But when I do a POST, it doesn't saves.
I believe that I'm doing the right thing in Rails 4 Strong Params, so, my companies_controller look like that:
class Company::CompaniesController < Company::BaseController
def show
#company = current_company
end
def edit
#company = current_company
end
def update
#company = current_company
if #company.update(company_params)
redirect_to company_path
else
render 'edit'
end
end
private
def company_params
params.require(:company).permit(:name, company_settings_attributes: [:id, :value, :setting])
end
end
Table Structure - Company:
id
Table Structure - Setting:
title (value to show to user)
key (value used in application)
Table Structure - CompanySetting:
company_id
setting_id
value
Thanks (:
I fixed that, with these steps:
.1 Customize the Model adding the methods for return the list of all possible settings from Setting model, and build new ones with CompanySetting. You need to create an assign method in the class, because when the Strong Params try to save, it will try to find this method.
def settings
Setting.all.collect { |setting|
company_settings.find_by( setting: setting ) || company_settings.build( { setting: setting, value: '' } )
}
end
def set_setting(key, value)
company_settings.find_or_create_by( setting: Setting.find_by(key: key) ).update(value: value)
end
def settings=(attributes)
attributes.map { |key, value|
set_setting(key, value)
}
end
asd
.2 Update the Form to use this new method (Here I added two types of fields, booleans and not booleans. This is based on is_boolean property in Setting
= f.simple_fields_for :settings do |s|
- for setting in #company.settings
.form-group
- if setting.setting.is_boolean
.checkbox
%label
= s.input_field setting.setting.key.to_sym, as: :boolean, boolean_style: :inline, checked: setting.value == "1"
= setting.setting.title
%span.help-block= setting.setting.description
- else
= s.label setting.setting.key.to_sym, setting.setting.title, class: 'control-label'
= s.input_field setting.setting.key.to_sym, class: 'form-control', value: setting.value
%span.help-block= setting.setting.description
.3 Fix your Strong Params
def company_params
params.require(:company).permit(:name,
settings: [
:setting_key_1,
:setting_key_2,
...,
:my_other_n_setting
]
)
end
Done.

Rails 4 enum validation

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).

Rails: Virtual Attribute Reader Causing Error During Validations

Rails 3.0.3 application. . .
I'm using a virtual attribute in a model to convert a value stored in the database for display based on a user's preference (U.S. or metric units). I'm doing the conversion in the reader method, but when I test my presence validation I get a NoMethodError because the real attribute is nil. Here's the code:
class Weight < ActiveRecord::Base
belongs_to :user
validates :converted_weight, :numericality => {:greater_than_or_equal_to => 0.1}
before_save :convert_weight
attr_accessor :converted_weight
def converted_weight(attr)
self.weight_entry = attr
end
def converted_weight
unless self.user.nil?
if self.user.miles?
return (self.weight_entry * 2.2).round(1)
else
return self.weight_entry
end
else
return nil
end
end
...
This is the line that's causing the problem:
return (self.weight_entry * 2.2).round(1)
I understand why self.weight_entry is nil, but what's the best way to handle this? Should I just throw in an unless self.weight_entry.nil? check in the reader? Or should I perform this conversion somewhere else? (if yes, where?)
Thanks!
Here's what I've done:
Model
validates :weight_entry, :numericality => {:greater_than_or_equal_to => 0.1}
before_save :convert_weight
attr_reader :converted_weight
def converted_weight
unless self.user.nil?
unless self.weight_entry.nil?
if self.user.miles?
return (self.weight_entry * 2.2).round(1)
else
return self.weight_entry
end
end
else
return nil
end
end
Form
<%= f.label :weight_entry, 'Weight' %><br />
<%= f.text_field :weight_entry, :size => 8, :value => #weight.converted_weight %> <strong><%= weight_units %></strong> (<em>Is this not right? Go to your <%= link_to 'profile', edit_user_registration_path %> to change it</em>)
The unless.self.weight_entry.nil? check allows the validation to do it's job. If anyone knows of a better way to do this I'm open to suggestion.
Thanks!
P.S. The before_save convert_weight method converts U.S. units to metric. I want to store values in the same units consistently so if a user changes her preference later previously stored values don't become invalid.

Resources