I tried to to use concerns in my project. I would like to build a method to get the date in french for every model I have. Here is my code. Currently, I get the error : wrong argument type Class (expected Module) at the line include DateTime in the model.
Here is my file models/concerns/date_time.rb
module DateTime
extend ActiveSupport::Concern
def self.included(base)
base.extend ClassMethods
base.class_eval do
scope :disabled, -> { where(disabled: true) }
end
end
# methods defined here are going to extend the class, not the instance of it
module ClassMethods
def date_string
h = {1=>'Janvier',2=>'Février',3=>'Mars',4=>'Avril',5=>'Mai',6=>'Juin',7=>'Juillet',8=>'Août',9=>'Septembre',10=>'Octobre',11=>'Novembre',12=>'Décembre'}
"#{self.created_at.day}-#{h[self.created_at.month]}-#{self.created_at.year}"
end
end
end
Here is my file models/demands.rb
class Demand < ActiveRecord::Base
include DateTime
belongs_to :skill
belongs_to :project
belongs_to :user
has_many :transactions
validates :project, presence: true
validates :skill, presence: true
validates :title, presence: true
end
Thanks in advance for your help !
Use the Rails built in I18n functionality instead. Doing localization in the model layer is just wrong. Models should only be concerned with data and business logic - not how data (like dates) are presented.
Your immediate issue here is that, because DateTime is a class in the Ruby standard library, Ruby is trying to include that class, not your module. If you rename the module to something unique, say, UsesDateTime, your error should go away.
That said, for this particular method, I agree with max.
Related
I am having an issue, I have the next class
class Question < ApplicationRecord
include Mappable
...
end
so my problem is at the time to create a Question, I need to keep the Question validations but skip the ones that are incoming from Mappable
because by now I am using question.save(validate: false) and I have to update that to something like question.save(mappable_validate: false), just to skip the validations from Mappable
EDIT
Mappable:
module Mappable
extend ActiveSupport::Concern
included do
attr_accessor :skip_map
has_one :map_location, dependent: :destroy
accepts_nested_attributes_for :map_location, allow_destroy: true, reject_if: :all_blank
validate :map_must_be_valid, on: :create, if: :feature_maps?
def map_must_be_valid
return true if skip_map?
unless map_location.try(:available?)
skip_map_error = "Map error"
errors.add(:skip_map, skip_map_error)
end
end
def feature_maps?
Setting["feature.map"].present?
end
def skip_map?
skip_map == "1"
end
end
end
There are quite a few ways to solve this. But no reliable ones that don't involve modifying the module.
One would be simply to use composition and move the validations to its own module:
module Mappable
module Validations
extend ActiveSupport::Concern
included do
validate :map_must_be_valid, on: :create, if: :feature_maps?
end
end
end
class Question < ApplicationRecord
include Mappable
end
class Foo < ApplicationRecord
include Mappable
include Mappable::Validations
end
Another very common way to make the behavior provided by a module customizeable is to not just cram all your code into the Module#included hook which doesn't let you pass options.
Instead create a class method:
module Mappable
extend ActiveSupport::Concern
def map_must_be_valid
return true if skip_map?
unless map_location.try(:available?)
skip_map_error = "Map error"
errors.add(:skip_map, skip_map_error)
end
end
def feature_maps?
Setting["feature.map"].present?
end
def skip_map?
skip_map == "1"
end
module ClassMethods
def make_mappable(validate: true)
attr_accessor :skip_map
has_one :map_location, dependent: :destroy
accepts_nested_attributes_for :map_location,
allow_destroy: true, reject_if: :all_blank
if validate
validate :map_must_be_valid, on: :create, if: :feature_maps?
end
end
end
end
And then just call the class method in the class you want to modify.
class Question < ApplicationRecord
include Mappable
make_mappable(validate: false)
end
This pattern can be found everywhere in Rails and Ruby in general and lets you make the functionality you're providing much flexible.
I understand that this might not seem to be immediately helpful as the code is coming from a gem. But it can help you understand what to do to fix the gem or evaluate if its actually worthwhile/needed.
I'm trying to understand if it's possible, given two models that share some methods and fields, to put the validations that are common between the two of them in an abstract base class. Below code represents a simplified version of my situation.
There are two classes of invoice line items: sales and collections. These line items share a common field invoice_amount I want to validate the presence of the invoice_amount from an abstract base class but fields that are not common to both models get validated by the subclass.
class Collection < InvoiceLineItem
belongs_to :invoice
validates :c_number, :invoice_number, :invoice_date, presence: true
.
.
.
end
class Sale < InvoiceLineItem
belongs_to :invoice
.
.
.
end
class InvoiceLineItem < ApplicationRecord
self.abstract_class = true
def self.inherited(base)
super
base.send(:extend, NumberFormatter)
base.send(:commafy, :invoice_amount)
end
def invoice_amount
self[:invoice_amount] || '0.00'
end
def export_date
invoice_date
end
end
I've tried several things to get this to work with no success. Some of my attempts included adding the following code to my InvoiceLineItem base class
def self.inherited(base)
base.class_eval do
validates :invoice_amount, presence: true
end
super
base.send(:extend, NumberFormatter)
base.send(:commafy, :invoice_amount)
end
and
def self.inherited(base)
base.class_eval do
base.send(:validates, :invoice_amount, presence: true)
end
super
base.send(:extend, NumberFormatter)
base.send(:commafy, :invoice_amount)
end
and this as described here (https://medium.com/#jeremy_96642/deep-rails-how-to-use-abstract-classes-6aee9b686e75) which seemed promising because it described exactly what I want to do however it does not work for me.
with_options presence:true do
validates :invoice_amount
end
In all these cases the code executes without error however if I write a test like below it fails because validation succeeds!
RSpec.describe Collection, type: :model do
it "Requires an invoice amount" do
result = Collection.create(invoice_amount: nil, c_number: 'CUST012', invoice_number: 'INV001', invoice_date: Date.new(1999, 1,1))
expect(result.valid?).to be false
expect(result.errors[:invoice_amount]).to include("can't be blank")
end
end
I'm not really interested in hearing answers about how it should be done using composition instead of inheritance I won't go into the details but just assume that it has to be done using inheritance. It seems like it should be possible but I'm not sure and I can't find any source on the internet that has a solution that actually works.
Any help would be very much appreciated!
I figured out my issue, it turns out this code was working:
class InvoiceLineItem < ApplicationRecord
self.abstract_class = true
def self.inherited(base)
super
base.send(:extend, NumberFormatter)
base.send(:commafy, :invoice_amount)
end
with_options presence: true do
validates :invoice_amount
end
def invoice_amount
self[:invoice_amount] || 0.00
end
def export_date
invoice_date
end
def debtor_number_up_to_space
debtor_number.split[0]
end
end
the validator was using the method invoice_amount which was shadowing the field on the model so invoice_amount wasn't being seen as nil but as 0.00 thus my test validation was passing correctly.
Once I removed the || 0.00 I could get the test to fail.
I'm not sure I understand how concerns work. I am trying to wrap up some common code into two modules that extend ActiveSupport::Concern, but when I include both, I get a error:
`included': Cannot define multiple 'included' blocks for a Concern
(ActiveSupport::Concern::MultipleIncludedBlocks)
module AppCore
class Student
include Mongoid::Document
include Mongoid::Timestamps
include AppCore::Extensions::Models::TenantScoped
include AppCore::Extensions::Models::UserScoped
end
end
module AppCore::Extensions::Models
module TenantScoped
extend ActiveSupport::Concern
included do
field :tenant_id, type: Integer
belongs_to :tenant, class_name: 'AppCore::Tenant'
association_name = self.to_s.downcase.pluralize
AppCore::Tenant.has_many association_name.to_sym, class_name: self.to_s
end
end
end
module AppCore::Extensions::Models
module UserScoped
extend ActiveSupport::Concern
included do
field :user_id, type: Integer
belongs_to :user, class_name: 'AppCore::User'
end
end
end
Can I only include one Concern at a time? Should I move the two Scoped modules to tenant_scoped and user_scoped to ClassMethods and just make one model extension concern?
Your problem is that you most likely not follow the Rails auto loading conventions regarding folder/file naming that follows the module / class name structure.
See Cannot define multiple 'included' blocks for a Concern (ActiveSupport::Concern::MultipleIncludedBlocks) with cache_classes = true for more info.
I'm not exactly sure what the problem with ActiveSupport::Concern but I'm not a huge fan of its abstraction. I would just use standard ruby to do what you're trying to accomplish and you won't have a problem. Change both of your modules to look like the following
module AppCore::Extensions::Models
module UserScoped
def self.included(klass)
klass.class_eval do
field :user_id, type: Integer
belongs_to :user, class_name: 'AppCore::User'
end
end
end
end
I got two models: Source and SourceType. Source of course belongs to SourceType.
I want to create new source and assign proper sourcetype object to it. 'Proper' means that one virtual attribute of the source object match some of sourceType objects test regexpression, which becomes source's Type.
I got an attribute writer in source object
class Source < ActiveRecord::Base
belongs_to :source_type
def url=(value)
SourceType.each do |type|
# here i match type's regexp to input value and if match,
# assign it to the new source object
end
end
end
I don't want to build any custom validator for it'll be need to run through SourceTypes twice. How to raise validate error if no sourcetypes is fit to the input so user could see error reasons in a form?
Validation
If you set the virtual attribute using attr_accessor, you should be able to validate on the model you're sending data to (alternatively using inverse_of if you'd like to validate on the nested model):
http://api.rubyonrails.org/classes/ActiveModel/Validator.html
This can now be used in combination with the validates method (see ActiveModel::Validations::ClassMethods.validates for more on this).
class Person
include ActiveModel::Validations
attr_accessor :title
validates :title, presence: true
end
Code
I'd do this:
class Source < ActiveRecord::Base
belongs_to :source_type, inverse_of: :sources
attr_accessor :url
end
class SourceType < ActiveRecord::Base
has_many :sources, inverse_of: :source_type
validates :source_type_attr, presence: { if: :url_match? }
def url_match?
self.sources.url == [your_regex]
end
end
Rails 3 includes the validates_associated which is automatically called when saving a nested model. The problem with the method is the message is terrible - "Model(s) is invalid"
There have been a few posts attacking this issue for Rails 2:
http://rpheath.com/posts/412-a-better-validates-associated
http://pivotallabs.com/users/nick/blog/articles/359-alias-method-chain-validates-associated-informative-error-message
and there are probably more. It would be great to have a better version as described in these posts that is Rails 3 compatible. The main improvement would be to include why the associated model fails.
On the relationship, you can use :autosave => true instead which will try to save children models when you save the parent. This will automatically run the validations of the children and they will report with proper error messages.
Moreover, if you add a presence validation on the child that the parent must be set, and you construct the child objects through the association, you don't even need the autosave flag, and you get a beautiful error message. For example:
class Trip < ActiveRecord::Base
validates :name, :presence => true
attr_accessible :name
has_many :places, dependent: :destroy, :inverse_of => :trip
end
class Place < ActiveRecord::Base
belongs_to :trip
validates :name, :trip, presence: true
attr_accessible :name
end
Then you can get an nice error message with the following usage scenario:
> trip = Trip.new(name: "California")
=> #<Trip id: nil, name: "California">
> trip.places.build
=> #<Place id: nil, name: nil, trip_id: nil>
> trip.valid?
=> false
> trip.errors
=> #<ActiveModel::Errors:0x00000004d36518 #base=#<Trip id: nil, name: "California">, #messages={:places=>["is invalid"]}>
> trip.errors[:places]
=> ["is invalid"]
I think validates_associated is a relic of the era before autosaving of children and isn't the best way to do things any more. Of course that's not necessarily documented well. I'm not 100% sure that this also applies to Rails 2.3, but I have a feeling it does. These changes came when the nested attributes feature was added (which was sometime in 2.x).
This is a simplified snippet of code from a training project I posted on github.
I was having this problem, and in the end I used the solution given here by Ben Lee:
validates associated with model's error message
Ben says:
You can write your own custom validator, based on the code for the built-in validator.
Looking up the source code for validates_associated, we see that it uses the "AssociatedValidator". The source code for that is:
module ActiveRecord
module Validations
class AssociatedValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if (value.is_a?(Array) ? value : [value]).collect{ |r| r.nil? || r.valid? }.all?
record.errors.add(attribute, :invalid, options.merge(:value => value))
end
end
module ClassMethods
def validates_associated(*attr_names)
validates_with AssociatedValidator, _merge_attributes(attr_names)
end
end
end
end
So you can use this as an example to create a custom validator that bubbles error messages like this:
module ActiveRecord
module Validations
class AssociatedBubblingValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
(value.is_a?(Array) ? value : [value]).each do |v|
unless v.valid?
v.errors.full_messages.each do |msg|
record.errors.add(attribute, msg, options.merge(:value => value))
end
end
end
end
end
module ClassMethods
def validates_associated_bubbling(*attr_names)
validates_with AssociatedBubblingValidator, _merge_attributes(attr_names)
end
end
end
end
You can put this code in an initializer, something like /initializers/associated_bubbling_validator.rb.
Finally, you'd validate like so:
class User < ActiveRecord::Base
validates_associated_bubbling :account
end
NOTE: the above code is completely untested, but if it doesn't work outright, it is hopefully enough to put you on the right track
validates_associated runs the validations specified in the associated object's class. Errors at the parent class level simply say 'my child is invalid'. If you want the details, expose the errors on the child object (at the level of the child's form in the view).
Most of the time validates_existence_of is all I need.