I'm trying to substitute ActiveRecord validations with Dry-validations, but I've been unable to find any in-app implementation examples to follow.
Dry-validation docs: http://dry-rb.org/gems/dry-validation/
I've added below to the form object, but I don't understand how to actually implement it so the validation fails if title is not inputted in the UI's form.
schema = Dry::Validation.Schema do
required(:title).filled
end
Form Object (setup with Virtus):
class PositionForm
include Virtus.model
include ActiveModel::Model
require 'dry-validation'
require 'dry/validation/schema/form'
# ATTRIBUTES
attribute :id, Integer
attribute :title, String
...
# OLD ACTIVE RECORD VALIDATIONS
#validates :title, presence: true
# NEW DRY VALIDATIONS
schema = Dry::Validation.Schema do
required(:title).filled
end
def save
if valid?
persist!
true
else
false
end
end
def persist!
#position = Position.create!(title: title...)
end
end
I've never used dry-validation before - any guidance would be super appreciated!
UPDATE
I've been able to "make it work" but it still doesn't feel like the correct design pattern.
Updated save method
def save
schema = Dry::Validation.Schema do
required(:title).filled
end
errors = schema.(title: title)
if valid? && errors.messages.empty?
persist!
true
else
false
end
end
If anyone could share guidance on the appropriate design pattern to implement dry-validation into a virtus-style form object it would be super appreciated!
I would try to keep validation at the model level.
Have a ModelValidations model in your initializers, each method named after the model it validates.
config/initialize/model_validations.rb
module ModelValidations
def position_form
Dry::Validation.Schema do
required(:title).filled
end
end
end
In the model, call the dry_validation module for that model.
app/models/position_form.rb
class PositionForm
validates :dry_validation
def dry_validation
ModelValidations.position_form(attributes).each do |field, message|
errors.add(field, message)
end
end
end
Related
I have created a custom validation for a ruby model (using the luhn algorithm). Even when I explicitly just return false from the custom validation, the object still saves.
This is what I have in my CreditCard model:
before_save :check_card_number
private
def check_card_number
return false unless card_number_luhn
end
def card_number_luhn
#luhn_algorithm_here_that_returns_true_or_false
end
but even if I just return false:
before_save :check_card_number
private
def check_card_number
return false
end
#so this is never even called
def card_number_luhn
#luhn_algorithm_here_that_returns_true_or_false
end
the object still saves. This is true EVEN IF I use validate instead of before_save. What is going on?
From Rails 5, you need to explicitly call throw(:abort)
# ...
before_save :check_card_number
# ...
def card_number_luhn
valid = #luhn_algorithm_here_that_returns_true_or_false
throw(:abort) unless valid
end
another way:
ActiveSupport.halt_callback_chains_on_return_false = true
reference: https://www.bigbinary.com/blog/rails-5-does-not-halt-callback-chain-when-false-is-returned
I want some of my model attributes to predefined dynamically. I have various models.And now I want My Bill model to create objects using other model instances.
Models :
leave.rb # belongs_to :residents
resident.rb # has_many:leaves,has_many:bills,has_one:account
bill.rb # belongs_to:residents
rate_card.rb # belongs_to:hostel
account.rb # belongs_to:resident
hostel.rb
now here is my bills controller create method :
def create
#bill = Resident.all.each { |resident| resident.bills.create(?) }
if #bill.save
flash[:success]="Bills successfully generated"
else
flash[:danger]="Something went wrong please try again !"
end
end
I want to build bill using all of the models eg:
resident.bills.create(is_date:using form,to_date:using form,expiry_date:using form,amount:30*(resident.rate_card.diet)+resident.rate_card.charge1+resident.rate_card.charge2)+(resident.account.leaves)*10+resident.account.fine)
///////Is this possible ?
And how to use strong params here ?
Pls help me out thxx..
I think the Rails way for this logic you want is with callbacks if you want calculated attributes either on create, update or delete, meaning attributes that depend on other models. For instance:
class Bill < ActiveRecord::Base
...
before_create :set_amount
...
protected
def set_amount
self.amount = 30 * self.resident.rate_card.diet + self.resident.rate_card.charge1 + self.resident.rate_card.charge2 + (self.resident.account.leaves) * 10 + self.resident.account.fine
end
end
If you want this logic to be used when updating the record also, then you should use before_save instead of before_create.
After you do this, you should accept the usual params (strong) of Bill model, as in:
def bill_params
params.require(:bill).permit(:is_date, :to_date, :expiry_date)
end
So your create call would be like:
resident.bills.create(bill_params)
Also, be wary of your create action, you should probably create a method either on your Bill or your Resident model that uses transactions to create all bills at the same time because you probably want either every bill created or none. This way you won't have the Resident.all.each logic in your BillsController.
create takes a hash, you can:
create_params = { amount: 30*(resident.rate_card.diet) }
create_params[:some_field] = params[:some_field]
# and so on
resident.bills.create(create_params)
or:
obj = resident.bills.build(your_strong_parameters_as_usual)
obj.amount = # that calculation
obj.save!
I'm confused at your syntax of your controller. #bill is being set to the value of a loop, which feels off. Each loops return the enumerable you cycle through, so you'll end up with #bill = Resident.all with some bills being created on the side.
What your controller really wants to know is, did my many new bills save correctly?
This seems like a perfect place to use a ruby object (or, colloquially, a Plain Old Ruby Object, as opposed to an ActiveRecord object) to encapsulate the specifics of this bill-generator.
If I'm reading this right, it appears that you are generating many bills at once, based on form-inputted data like:
is_date
to_date
expiry_date
...as well as some data about each individual resident.
Here's the model I'd create:
app/models/bill_generator.rb
class BillGenerator
include ActiveModel::Model
# This lets you do validations
attr_accessor :is_date, :to_date, :expiry_date
# This lets your form builder see these attributes when you go form.input
attr_accessor :bills
# ...for the bills we'll be generating in a sec
validates_presence_of :is_date, :to_date, :expiry_date
# You can do other validations here. Just an example.
validate :bills_are_valid?
def initialize(attributes = {})
super # This calls the Active Model initializer
build_new_bills # Called as soon as you do BillGenerator.new
end
def build_new_bills
#bills = []
Resident.all.each do |r|
#bills << r.bills.build(
# Your logic goes here. Not sure what goes into a bill-building...
# Note that I'm building (which means not-yet-saved), not creating
)
end
def save
if valid?
#bills.each { |b| b.save }
true
else
false
end
end
private
def bills_are_valid?
bill_validity = true
#bills.each do |b|
bill_validity = false unless b.valid?
end
bill_validity
end
end
Why all this mess? Because in your controller you can do...
app/controllers/bill_controller.rb
def create
#bill_generator = BillGenerator.new(bill_generator_params)
if #bill_generator.save?
# Redirect to somewhere with a flash?
else
# Re-render the form with a flash?
end
end
def bill_generator_params
params.require(:bill_generator).permit(:is_date, :to_date, :expiry_date)
# No extra garbage. No insecurity by letting all kinds of crud through!
end
...like a BillGenerator is any old object. Did it save? Great. It didn't, show the form again.
Now, my BillGenerator won't just be copy-and-paste. Your 'build_new_bills' probably will have some of that math you alluded to, which I'll leave to you.
Let me know what you think!
you can do it by using params.permit! as this allows any parameters to be passed. here's an example:
def create
...
#bill = Resident.all.each { |resident| resident.bills.create(any_params) }
end
private
def any_params
params.permit!
end
be careful with this of course, as you are opening this up to potential exploits.
I have an ActiveRecord object with multiple attributes that are allowed to be nil on creation and can later be updated by the user through a form. However, once an attribute is changed from nil to non-nil, that attribute may not be updated again. How should I go about setting up this behavior?
create_table :funky do |t|
t.integer :fireflies
end
class Funky < ActiveRecord::Base
def fireflies=(ff)
raise "Uh uh.. it was already set" unless self.fireflies.blank?
write_attribute(:fireflies, ff)
end
end
Editing post as user requested that many fields be edited
[:one, :two, :three].each do |s|
define_method "#{s}=" do |v|
raise "Uh uh.. it was already set" unless self.send(s).blank?
write_attribute(s, v)
end
end
Well, I think you should definitely allow users to change their information at any time, but anyway, if you want to add the restriction to the controller instead of the model you could do this:
def update
your_model = YourModel.find(params[:id])
# find the attributes with nil values:
nil_attributes = your_model.attributes.select {|k,v| v.nil?}.keys.map(&:to_sym)
# attributes that you allow to edit:
allowed_attributes = [:title, :description, :size]
# tell rails which are the allowed modifications:
allowed_params = params.require(:your_model).permit(*(allowed_attributes - nil_attributes))
# save the changes:
your_model.update_attributes(allowed_params)
# ...
end
I'm trying do dynamically validate an object.
On my app, a user can create questions that will be part of a form, and each question can have
validations.
So, I post this form, and pass the param to the following class:
require 'ostruct'
class QuestionResponse < OpenStruct
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
extend ActiveModel::Callbacks
def fields
#table.keys
end
def add_validators
stored_questions = AdmissionForm.find(self.form_id).questions.all
questions = fields.select{|f| f.to_s[0]=="q"}
questions.each do |question_param|
question = stored_questions.select{|f| f["id"] == question_param.to_s.gsub("q_","").to_i}.first
unless question.validations.empty?
validations = "validates :#{question_param} , #{question.validations.join(",")}"
self.class.instance_eval validations
end
end
end
def initialize(*args)
super
add_validators if self.fields.any?
end
def persisted? ; false ; end;
end
It almost works.
My problem is that subsequent form posts, concatenate ActiveModel::Errors
#<ActiveModel::Errors:0x00000004432520
#base=#<QuestionResponse q_7="", q_6="", form_id="1">,
#messages=
{:q_7=>["cant be blank", "cant be blank"],
:q_6=>["cant be blank", "cant be blank"]}>
What am I doing wrong?
Thanks!
Alex
add_validators gets called on every instance of QuestionResponse, which adds validations to the class of QuestionResponse. Each new instance adds it's own validations to the class, but you still have the ones added by other (previously created) instances.
I am new to Ruby on Rails and have been using Scaffolding. On one of the forms I want to be able to have two fields and then have the difference between the two submitted to the database.
Instead of :pickupamount, I want (Ending Amount - Starting Amount) to be calculated and entered into the pickupamount column of my database.
Thanks in advance!
You could do this in either your model or your controller. Going along with Skinny Controller Fat Model, it might be better to put the functionality in your model. Check out ActiveRecord callbacks.
class MyModel < ActiveRecord::Base
attr_accessor :start_amount, :end_amount
before_create :calculate_pickup_amount
private
def calculate_pickup_amount
self.pickupamount = end_amount.to_i - start_amount.to_i
end
end
Then, in your controller:
def create
# Assuming params[:my_model] has all the data for initializing a MyModel,
# including start_amount and end_amount but not pickupamount:
my_model = MyModel.new(params[:my_model])
if my_model.save
# Yay, do something
else
# Fail, do something else
end
end
It might be useful to include the following extension method to Ruby's String class (thanks to sikelianos), perhaps in a file in your Rails app's lib directory:
class String
def numeric?
Float self rescue false
end
end
Then you could perform a check before setting pickupamount:
def calculate_pickup_amount
if end_amount.numeric? && start_amount.numeric?
self.pickupamount = end_amount.to_i - start_amount.to_i
else
# Throw exception, set some default value, etc.
end
end
In your view:
form_tag '/some_action' do
text_field_tag 'start_amount'
text_field_tag 'end_amount'
end
In your controller:
def create
model = Model.new
model.pickupamount = params[:end_amount].to_i - params[:start_amount].to_i
model.save!
end