I have a helper method, states_list, that is returning an array of US states that I want to access in a few different places of my Rails app including:
User model: validates :state, inclusion: { in: states_list }
User model spec: test for this validation
These will be reused elsewhere in addition to the User model. I am wondering where the proper place to store this helper method is, and how to access it from the model and tests. (My initial thought was in a GeographyHelper file inside the helpers directory, but I read that those are meant specifically to be view helpers...) Thanks!
You'd probably be best served by putting your states_list method in its own module and including it in your User model. The advantage of creating a module is that your concerns are nicely separated and reusable (in case you want to validate states in other models.
1) Create a place to put your module by going into your /lib directory and creating a directory for your custom modules (we'll call it custom_modules here).
2) Create your module file: /lib/custom_modules/States.rb
3) Write your module:
module CustomModules
module States
def states_list
#your logic here
end
end
end
4) Include your new States module in your User model or any other model where you'd like this functionality.
class User < ActiveRecord::Base
include CustomModules::States
validates :state, inclusion: { in: states_list }
end
You can store this method in either application helper or in user model it self.
Related
I'm using the acts_as_bookable gem for some basic reservation/booking stuff in a Rails app, and I need to add an additional validation to the Booking model that the gem creates.
What I mean by that is, inside the gem, located at lib/acts_as_bookable/booking.rb is the following module/class:
module ActsAsBookable
class Booking < ::ActiveRecord::Base
self.table_name = 'acts_as_bookable_bookings'
belongs_to :bookable, polymorphic: true
belongs_to :booker, polymorphic: true
validates_presence_of :bookable
validates_presence_of :booker
validate :bookable_must_be_bookable,
:booker_must_be_booker
# A bunch of other stuff
end
end
Which is fine. However, I want to add an additional piece of logic that stops a booker from booking the same instance of a bookable. Basically, a new validator.
I thought I could just add a file in my /models directory called acts_as_bookable.rb and just modify the class like this:
module ActsAsBookable
class Booking
validates_uniqueness_of :booker, scope: [:time, :bookable]
end
end
But this doesn't work. I could modify the gem itself (I've already forked it to bring a few dependencies up to date, since it's a pretty old gem) but that doesn't feel like the right solution. This is logic specific to this app's implementation, and so my gut feeling is that it belongs in an override inside this specific project, not the base gem.
What am I doing wrong here? And is there a better/alternative approach that would be more suitable?
A clean way to create monkeypatches/augmentations to objects outside of your control is to create a seperate module:
module BookingMonkeyPatch
extend ActiveSupport::Concern
included do
validates_uniqueness_of :booker, scope: [:time, :bookable]
end
end
This lets you test the monkeypatch seperately - and you can "turn the monkeypatch on" by including the module:
ActsAsBookable::Booking.include(BookingMonkeyPatch)
This can be done in an initializer or anywhere else in the lifecycle.
Altough if bookable is a polymorpic assocation you need to use:
validates_uniqueness_of :booker_id, scope: [:time, :bookable_id, :bookable_type]
The uniqueness validation does not work correctly when just passed the name of an assocation as it creates a query based on database columns. This is an example of a leaky abstraction.
See:
Justin Weiss - 3 Ways to Monkey-Patch Without Making a Mess
I am integrating a rails app (rails 3 at the moment, moving to 5) with another application. The user model in the rails app will have associations that are related to the integration, scopes, and a bunch of methods.
I would like to separate these from the user model file to avoid cluttering it up and keep all the associations, scopes and methods related to the integration in a single place rather than watch the user model become cluttered with things only relevant to users with the integration enabled.
Is this possible, and if so, what mechanism would I use?
You can use concerns:
module AdditionalLogic
extend ActiveSupport::Concern
included do
scope :disabled, -> { where(disabled: true) }
belongs_to :user
# etc..
end
# other methods
end
class YourModel < ActiveRecord::Base
include AdditionalLogic
end
I've created a rails engine which contains some common functionality I need when creating new users. For example, there's a before_validation(on: :create) hook that populates a certain field with something to ensure no user can be created without this field having something in it. It looks similar to:
module OfflineUser
extend ActiveSupport::Concern
included do
before_validation(on: :create) do
self.member_number = rand(0..100000)
end
end
end
ActiveRecord::Base.send(:include, OfflineUser)
If I include the engine into another project and do User.create it correctly populates a member_number field for me. However, because it's added the methods to ActiveRecord::Base, it also tries to populate that field in every model I try to run create on! How can I restrict this functionality to only the User model or other model of my choosing rather than globally on every model. Thanks.
By including it in the specific class:
class User < ActiveRecord::Base
include OfflineUser
end
Get rid of the your last line where you include the module in ActiveRecord::Base.
I have an app with a user model in app/models/user.rb.
Now I wrote an engine witch needs additional methods in the user class.
class User < ActiveRecord::Base
# some code
end
When I add a user model to vendor/plugins/foo/app/models/user.rb with additional methods, the methods are undefined and rails cant find them.
class User < ActiveRecord::Base
# some additional code
end
When I add a foo_user model which inherit from User, it works, but this is not what I would to have :(
class FooUser < User
# some additional code
end
How may I extend the User class from the origin App in the engine?
When defining the User model you set the inheritance scheme, in the first scenario both instance and class methods are inherited from ActiveRecord::Base.
Since you want to use the methods defined in your custom User model you will need to extend this functionality in your seperate class: FooUser in this case.
I do not know why you're convinced such method of implementing is not wanted, since it's the way Rails is setup.
Another way to implement an array of methods is monkey patching, here is another SO question on the subject:
Monkey Patching in Rails 3
Looking at the rails source for ActiveModel::Validations, the HelperMethods module is both included and extended in the underlying model:
module Validations
extend ActiveSupport::Concern
included do
extend ActiveModel::Callbacks
extend ActiveModel::Translation
extend HelperMethods
include HelperMethods
...
Theoretically, this allows you to not only call helper methods like validates_presence_of as class macros (as shown in all the standard examples), but to also call these helper methods directly on the model instance:
myobject.validates_presence_of :name
Where I can see this being useful is in the context of a custom validator that wants to leverage some of the existing helper methods:
class CustomValidator < ActiveModel::Validator
def validate(record)
record.validates_presence_of :name
record.validates_acceptance_of :terms
...
end
end
And whether or not this is encouraged behavior, it did seem to work until some changes with specific validators in Rails 4.1. So my question is if rails does not support calling these helper methods in this fashion (as instance methods on the model), why are they included in the model instead of just extended?
So according to this commit from roughly 4 years ago, there was indeed the intention to make these validation helpers callable from both the class and instance:
https://github.com/rails/rails/commit/9131a88bb8e82f139ec49b4057fb6065ba0a2c6a
I'm evaluating the changes made in 4.1 to see about getting these affected validation methods working again.