I'm trying to save a Form Object in Rails through and association like this:
document.translations_forms.save(translation_params)
And on my Document model I associated it this way:
class Document < ApplicationRecord
has_many :translations_forms
...
end
But when I run the first command above, I getting this error:
NoMethodError: undefined method `relation_delegate_class' for Document::TranslationsForm:Class
I tried declaring the TranslationFrom Object adding the Document namespace
class Document::TranslationsForm
include ActiveModel::Model
belongs_to :document
def save(params: {})
return false if invalid?
self.document.translation.create(params)
end
end
But didn't work either, my TranslationForm object is in the app/forms/translations_form.rb directory, and I'm using rails 6, what can I do to associate the model with my form object?
A form object (which is a vague term) is usually just a variation of the Decorator pattern.
So you could simply setup the form object so that it wraps an instance of the model class:
class Document
class TranslationForm
include ActiveModel::Model
attribute_reader :document
def initialize(record = nil, attributes = {})
# lets you use the form object for existing records
if record
#document = record
#document.assign_attributes(attributes)
end
#document ||= Document.new(attributes)
end
def to_model
document
end
def save
# triggers validations on the form object
if valid?
document.save
else
false
end
end
end
end
def create
#document = Document::TranslationForm.new(document_params)
if #document.save
redirect_to #document
else
render :new
end
end
def update
#document = Document::TranslationForm.new(
Document.find(params[:id]),
document_params
)
if #document.save
redirect_to #document
else
render :edit
end
end
To add a validation to the form object (instead of directly to the model) just use delegatation:
class Document
class TranslationForm
# ...
validates :foo, presence: true
delegate :errors, to: :document
delegate :foo, to: :document
end
end
delegate :errors, to: :document makes it so that your validations will add errors to the underlying model instead of the errors object of your form object.
Related
I am struggling with understanding how to save a model to MongoDB using mongoid and rails_admin.
I've got my model:
class PictureAsset < ActiveRecord::Base
include Mongoid::Document
field :data_file_name, type: String
field :data_content_type, type: String
field :data_file_size, type: Integer
end
This was made through a generation:
bundle exec rails g model PictureAsset data_file_name:string data_content_type:string data_file_size:integer
Rails admin side loads up just fine, and I can navigate to the index page for PictureAsset.
When I try to access my custom action, asset_action, I get the following error:
undefined method `belongs_to' for #<RailsAdmin::AbstractModel:0xbf2e791>
My action looks like this:
module RailsAdmin
module Config
module Actions
class AssetAction < RailsAdmin::Config::Actions::Base
RailsAdmin::Config::Actions.register(self)
register_instance_option :collection do
true
end
register_instance_option :http_methods do
[:get, :put]
end
register_instance_option :controller do
proc do
if request.get? # EDIT
binding.pry
respond_to do |format|
format.html { render #action.template_name }
format.js { render #action.template_name, layout: false }
end
elsif request.put? # UPDATE
binding.pry
#newUserPropVal.save
end
end
end
register_instance_option :link_icon do
'icon-list-alt'
end
end
end
end
end
Can someone please explain what I'm doing wrong here?
Your model includes code for both ActiveRecord and Mongoid. Pick one and remove code for the other.
ActiveRecord will look like this:
class PictureAsset < ActiveRecord::Base
# No field definitions needed
end
Mongoid will look like this:
# Does not derive from anything
class PictureAsset
include Mongoid::Document
field :data_file_name, type: String
field :data_content_type, type: String
field :data_file_size, type: Integer
end
This may not solve the error you are getting but is a good start.
I am trying to implement my own validations in Ruby for practice.
Here is a class Item that has 2 validations, which I need to implement in the BaseClass:
require_relative "base_class"
class Item < BaseClass
attr_accessor :price, :name
def initialize(attributes = {})
#price = attributes[:price]
#name = attributes[:name]
end
validates_presence_of :name
validates_numericality_of :price
end
My problem is: the validations validates_presence_of, and validates_numericality_of will be class methods. How can I access the instance object to validate the name, and price data within these class methods?
class BaseClass
attr_accessor :errors
def initialize
#errors = []
end
def valid?
#errors.empty?
end
class << self
def validates_presence_of(attribute)
begin
# HERE IS THE PROBLEM, self HERE IS THE CLASS NOT THE INSTANCE!
data = self.send(attribute)
if data.empty?
#errors << ["#{attribute} can't be blank"]
end
rescue
end
end
def validates_numericality_of(attribute)
begin
data = self.send(attribute)
if data.empty? || !data.integer?
#valid = false
#errors << ["#{attribute} must be number"]
end
rescue
end
end
end
end
Looking at ActiveModel, you can see that it doesn't do the actual validation when validate_presence_of is called. Reference: presence.rb.
It actually creates an instance of a Validator to a list of validators (which is a class variable _validators) via validates_with; this list of validators is then called during the record's instantiation via callbacks. Reference: with.rb and validations.rb.
I made a simplified version of the above, but it is similar to what ActiveModel does I believe. (Skipping callbacks and all that)
class PresenceValidator
attr_reader :attributes
def initialize(*attributes)
#attributes = attributes
end
def validate(record)
begin
#attributes.each do |attribute|
data = record.send(attribute)
if data.nil? || data.empty?
record.errors << ["#{attribute} can't be blank"]
end
end
rescue
end
end
end
class BaseClass
attr_accessor :errors
def initialize
#errors = []
end
end
EDIT: Like what SimpleLime pointed out, the list of validators will be shared across and if they are in the base class, it would cause all the items to share the attributes (which would obviously fail if the set of attributes are any different).
They can be extracted out into a separate module Validations and included but I've left them in in this answer.
require_relative "base_class"
class Item < BaseClass
attr_accessor :price, :name
##_validators = []
def initialize(attributes = {})
super()
#price = attributes[:price]
#name = attributes[:name]
end
def self.validates_presence_of(attribute)
##_validators << PresenceValidator.new(attribute)
end
validates_presence_of :name
def valid?
##_validators.each do |v|
v.validate(self)
end
#errors.empty?
end
end
p Item.new(name: 'asdf', price: 2).valid?
p Item.new(price: 2).valid?
References:
presence.rb
with.rb
validators.rb
class variable _validators
First, let's try to have validation baked into the model. We'll extract it once it's working.
Our starting point is Item without any kind of validation:
class Item
attr_accessor :name, :price
def initialize(name: nil, price: nil)
#name = name
#price = price
end
end
We'll add a single method Item#validate that'll return an array of strings representing errors messages. If a model is valid the array will be empty.
class Item
attr_accessor :name, :price
def initialize(name: nil, price: nil)
#name = name
#price = price
end
def validate
validators.flat_map do |validator|
validator.run(self)
end
end
private
def validators
[]
end
end
Validating a model means iterating over all associated validators, running them on the model and collecting results. Notice we provided a dummy implementation of Item#validators that returns an empty array.
A validator is an object that responds to #run and returns an array of errors (if any). Let's define NumberValidator that verifies whether a given attribute is an instance of Numeric. Each instance of this class is responsible for validating a single argument. We need to pass the attribute name to the validator's constructor to make it aware which attribute to validate:
class NumberValidator
def initialize(attribute)
#attribute = attribute
end
def run(model)
unless model.public_send(#attribute).is_a?(Numeric)
["#{#attribute} should be an instance of Numeric"]
end
end
end
If we return this validator from Item#validators and set price to "foo" it'll work as expected.
Let's extract validation-related methods to a module.
module Validation
def validate
validators.flat_map do |validator|
validator.run(self)
end
end
private
def validators
[NumberValidator.new(:price)]
end
end
class Item
include Validation
# ...
end
Validators should be defined on a per-model basis. In order to keep track of them, we'll define a class instance variable #validators on the model class. It'll simply by an array of validators specified for the given model. We need a bit of meta-programming to make this happen.
When we include any model into a class then included is called on the model and receives the class the model is included in as an argument. We can use this method to customize the class at inclusion time. We'll use #class_eval to do so:
module Validation
def self.included(klass)
klass.class_eval do
# Define a class instance variable on the model class.
#validators = [NumberValidator.new(:price)]
def self.validators
#validators
end
end
end
def validate
validators.flat_map do |validator|
validator.run(self)
end
end
def validators
# The validators are defined on the class so we need to delegate.
self.class.validators
end
end
We need a way to add validators to the model. Let's make Validation define add_validator on the model class:
module Validation
def self.included(klass)
klass.class_eval do
#validators = []
# ...
def self.add_validator(validator)
#validators << validator
end
end
end
# ...
end
Now, we can do the following:
class Item
include Validation
attr_accessor :name, :price
add_validator NumberValidator.new(:price)
def initialize(name: nil, price: nil)
#name = name
#price = price
end
end
This should be a good starting point. There're lots of further enhancements you can make:
More validators.
Configurable validators.
Conditional validators.
A DSL for validators (e.g. validate_presence_of).
Automatic validator discovery (e.g. if you define FooValidator you'll automatically be able to call validate_foo).
If your goal is to mimic ActiveRecord, the other answers have you covered. But if you really want to focus on a simple PORO, then you might reconsider the class methods:
class Item < BaseClass
attr_accessor :price, :name
def initialize(attributes = {})
#price = attributes[:price]
#name = attributes[:name]
end
# validators are defined in BaseClass and are expected to return
# an error message if the attribute is invalid
def valid?
errors = [
validates_presence_of(name),
validates_numericality_of(price)
]
errors.compact.none?
end
end
If you need access to the errors afterwards, you'll need to store them:
class Item < BaseClass
attr_reader :errors
# ...
def valid?
#errors = {
name: [validates_presence_of(name)].compact,
price: [validates_numericality_of(price)].compact
}
#errors.values.flatten.compact.any?
end
end
I don't understand the point to implement PORO validations in Ruby. I'd do that in Rails rather than in Ruby.
So let's assume you have a Rails project. In order to mimic the Active Record validations for your PORO, you need to have also 3 things:
Some kind of a save instance method within your PORO (to call the validation from).
A Rails controller handling CRUD on your PORO.
A Rails view with a scaffold flash messages area.
Provided all 3 these conditions I implemented the PORO validation (just for name for simplicity) this way:
require_relative "base_class"
class Item < BaseClass
attr_accessor :price, :name
include ActiveModel::Validations
class MyValidator
def initialize(attrs, record)
#attrs = attrs
#record = record
end
def validate!
if #attrs['name'].blank?
#record.errors[:name] << 'can\'t be blank.'
end
raise ActiveRecord::RecordInvalid.new(#record) unless #record.errors[:name].blank?
end
end
def initialize(attributes = {})
#price = attributes[:price]
#name = attributes[:name]
end
# your PORO save method
def update_attributes(attrs)
MyValidator.new(attrs, self).validate!
#...actual update code here
save
end
end
In your controller you have to manually process the exception (as your PORO is outside ActiveRecord):
class PorosController < ApplicationController
rescue_from ActiveRecord::RecordInvalid do |exception|
redirect_to :back, alert: exception.message
end
...
end
And in a view - just a common scaffold-generated code. Something like this (or similar):
<%= form_with(model: poro, local: true) do |form| %>
<% if poro.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(poro.errors.count, "error") %> prohibited this poro from being saved:</h2>
<ul>
<% poro.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :name %>
<%= form.text_field :name, id: :poro_name %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
That's it. Just keep it all simple.
I'm doing an update of a form. I can't add my validation in my model for x reason, so I'm adding an error in my projects_controller in the method update. When I update it should raise the error and render :edit but it doesn't. Here is my method
def update
#project = Project.find(params[:id])
#stuff to update
#add error if no legal_media checked, unless if creative upload its own conditions
unless has_media?(#project.legal_option.authorized_format)
#project.legal_option.authorized_format.errors[:base] << "error message"
end
if #project.update_attributes(project_params)
redirect_to brief_path(#project.order.brief)
else
render :edit
end
end
the method has_media? returns false dans when I type #project.legal_option.authorized_format.errors[:base]I have my error message ["error message"].
But when I type #project.legal_option.authorized_format.valid?, it returns true
Any idea how I could make my method raise this error?
Thank you!
UPDATE trying to do the validation in the model :
Since the beginning I want to check that if my column custom_document in legal_option isn't nil (therefore the user uploaded it in the update method of the projects_controller), then, check if there is at least one media in legal_media.
Here are my models :
class LegalOption < ActiveRecord::Base
belongs_to :project
has_one :authorized_format, class_name: "LegalMedia", foreign_key: "legal_option_id"
accepts_nested_attributes_for :authorized_format
has_attached_file :custom_document
validates_attachment :custom_document, content_type: { content_type: "application/pdf" }
end
class LegalMedia < ActiveRecord::Base
belongs_to :legal_option
def self.formats
{all_media: "Tous Media", internet: "Internet", paper: "Presse papier", object: "Objets", television: "TV", radio: "Radio", cinema: "Cinéma", poster_campaign: "Affiches", :press_relation => "Relations Presse", :plv => "Publicité sur lieux de vente", :event => 'Evènementiel'}
end
end
When I did the validation in the beginning with a validate :has_media? My LegalOption.LegalMedia because legal_option_id is nil in legal_media
in the unless block, put the line:
render :edit and return
like:
unless has_media?(#project.legal_option.authorized_format)
#project.legal_option.authorized_format.errors[:base] << "error message"
render :edit and return
end
You should add a validation to the model in order for the valid? to do what you are looking for it to do.
If you look at the docs here, you'll see that valid? just runs all the validations. It doesn't check for any errors that you manually add to the object.
Rails convention dictates that validations shouldn't be implemented in the controller but rather in the model. More specifically, update_attributes just runs valid? after assigning the attributes, which itself just runs validations defined on the model. Any errors already on the model are cleared out beforehand.
If you re-write this as a custom validation on the model, update_attributes should behave as you expect:
class Project < ActiveRecord::Base
validate :legal_option_has_media
private
def legal_option_has_media
unless has_media? legal_option.authorized_format
errors.add :base, "error message"
end
end
end
What is the difference? Also, why does this not work:
The variables such as base_path are not being set.
class Cvit < ActiveRecord::Base
attr_accessible :species,:program,:textup,:e_value,:filter,:min_identity,:cluster_dist,:fileup_file_name
attr_accessor :base_path, :fa_file, :text_file, :dbase, :source, :bl_file, :bl_sorted, :gff_file, :cvt_file, :db, :overlay_coords_gray
def initilize(*args)
super(*args)
end
def cvitSetup()
self.base_path = "blast_cvit/"
self.fa_file = "input.fa"
.
.
end
end
in the rails console the attributes get set correctly however when I try to do this:
controller:
def show
#cvit = Cvit.find(params[:id])
#cvit.cvitSetup()
#cvit.blast()
#cvit.generateGff()
#cvit.generateCvitImage()
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => #cvit }
end
end
and in my view I reference #cvit.some_attribute.html_safe but that attribute is null so I get an error. Any ideas?
attr_accessor creates the getter method.attribute and setter method.attribute= for the specified attributes.
attr_accessible is from ActiveRecord::Base and "Specifies a white list of model attributes that can be set via mass-assignment." See documentation and example here.
EDIT:
As for your second question, I don't know. I tried this dummy code and it worked:
class Test
attr_accessor :base_path, :fa_file
def cvitSetup()
self.base_path = "blast_cvit/"
self.fa_file = "input.fa"
end
end
t = Test.new
t.cvitSetup
p t.base_path
#=> "blast_cvit/"
Are you sure that you properly instantiated your class?
attr_accessor simply creates a getter-setter method for an attribute.
attr_accessible specifies a white list of model attributes that can be set via mass-assignment, such as new(attributes), update_attributes(attributes), or attributes=(attributes). This has been excerpted from the link here
I have a model class that is not bound to Active record.
class ProcessingStatus
attr_accessor :status, :timestamp
end
The model acts as a processing status holder and will eventually be returned to the calling method.
Since this is invoked as an active resource method, this needs to go back (serialized) as xml.
Here is my action method:
def activate
#process_status = ProcessingStatus.new
if Account.activate(params[:account])
#process_status.status = "success"
else
#process_status.status = "fail"
end
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => #process_status }
end
end
This doesn't seem to return a valid xml though.
If I try and output the #process_status like below
return render :text => "The object is #{#process_status}"
this is what I get:
The object is #<ProcessingStatus:0x00000005e98860>
Please tell me what I am missing.
Edit #1,
Based on the comment below, I modified my code to include the serialization libraries.
class ProcessingStatus
include ActiveModel::Serialization
include ActiveModel::Serializers::JSON
include ActiveModel::Serializers::Xml
attr_accessor :status
def attributes
#attributes ||= {'status' => 'nil'}
end
end
I am getting closer:) Now get the output as follows for .xml request.
but the value that I assigned is not reflected.
#process_status.status = "success" / "fail"
<processing-status><status>nil</status></processing-status>
but when i make a json request, it is appearing correct!
{"processing_status":{"status":"success"}}
You need to define method to_xml in your model, or include Serialization module as below:
class ProcessingStatus
include ActiveModel::Serialization
attr_accessor :status, :timestamp
end
Here you've got more info: http://api.rubyonrails.org/classes/ActiveModel/Serialization.html