Many rails Models with the same flag. What's the best practice? - ruby-on-rails

One of mine rails projects has many models with the same flag: approved.
I don't like to manage the flag 'approved' for so many models, and I am seeking a DRY solution.
I have found some plugin like flag_shih_tzu or can_flag, but I think they work only with a model.
Do you know some plugin to flag many models at once ?
I think that I a good solution (without plugin) should use the polymorphic associations, do you agree ?
many thanks,
Alessandro

If all you're looking for is a way to store all the functions in a single place, but have them accessible from all your flagable models, I'd recommend writing a mixin for them. For example, in lib/approved.rb, you could have the module:
module Approved
# Any approval functions/constants that don't belong in a model go here...
module Mixin
def self.included(klass)
klass.class_eval do
# Class-levell model macros can be run here
named_scope :approved, {:conditions => {:approved => true}}
named_scope :unapproved, {:conditions => {:approved => false}}
end
end
def approved?
return (self.approved == true)
end
# Other shared model functions go here...
end
end
And then it's just a matter of including the mixin in all the models that need those functions:
class Approvable < ActiveRecord::Base
include Approved::Mixin
# etc.
end
Hope that helps!

I have a similar problem in my application, we have 10 or so models that all require approval and didn't want to copy the code everywhere. In our case we are using transitions as our workflow gem, so instead of having a flag approved we have a string column state.
A model that requires approval looks like this:
class Comment < A:RB
include ApprovalWorkflow
end
Then we have a workflow that looks like this:
# /app/workflows/approval_workflow.rb
module ApprovalWorkflow
def self.included(klass)
klass.class_eval do
state_machine do
.. workflow junk goes here ..
end
end
end
end
So what's going on here is that we've created a module, which you can think of like an anonymous piece of code which does't belong anywhere (read more about modules to understand why this is an awful description), which we then include in our classes which mixes in the functionality. Now our comment class has the approval workflow!
In you case, assuming you were to keep the approval flag, you might add default validations, some methods like approve!(user) or scopes for querying.
I hope this helps.

Using polymorphic associations is not the solution unless they are all the same base object. Keep in mind that with inheritance the parent should have an is-a relationship with the child.
What you could do is create an Approval model and have a one-to-one relationship with the approvable models.

Related

Override gem behaviour

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

extend rails model into something else to keep integration code seperate

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

Extending application's model in rails engine

I have an application which defines some models. I want to extend the functionality of some models(eg. adding methods,adding associations) from application to my engine.
I tried adding a model in the engine with the same name as my application's model and Rails will automatically merge them, however it doesn't work.
eg:
(Application's model)
class Article < ActiveRecord:Base
def from_application
puts "application"
end
end
(Inside my Engine)
module MyEngine
class Article < ::Article
has_many :metrics, :class_name => 'Metric'
end
end
has_many association is not getting applied to my Articles model when I try to access #articles.metrics. Any ideas ?
You have the right idea and are close. But your implementation is a little off.
Generally, your engine should have no knowledge of your host app. That way, your engine and the host app(s) stay loosely coupled. So, classes in your engine should not inherit from classes in your host app. (BTW, your approach doesn't work, I believe, because of the way ruby does constant lookups, but that's a different discussion.)
Instead, use the included hook. In the host app, you do something like:
class Article < ActiveRecord:Base
include FooEngine::BarModule
def from_application
puts "application"
end
end
And inside the engine:
module FooEngine
module BarModule
def self.included(base)
base.class_eval do
has_many :metrics, :class_name => 'Metric'
end
end
end
end
When the ruby interpreter goes to include FooEngine::BarModule in Article, it will look for and run (if found) the self.included method in FooEngine::BarModule, passing in the Articleclass as base.
You then call class_eval on the base (Article) which re-opens the Article class so that you can add methods or whatever monkey business you're up to (define new methods in situ, include or extend other modules, etc.).
In your example, you call the has_many method, which will create the various association methods provided by has_many.
If (a) you're going to add a lot of metrics-related functionality through your engine, (b) you want to have lots of classes make use of the metrics-related functionality, and (c) you want some of the functionality to vary from class-to-class (where included), you might consider creating an acts_as_having_metrics (or similar). Once you head down this path, it's a whole new world of wondrous metaprogramming.
Best of luck.
Do you have your metrics model have a belongs_to association with Articles.
You might want to give the other side of the association, Metrics a belongs_to Articles to have this work properly. Also, make sure to have a migration to hold articles_id on the metrics table. Everything should work fine.

Rails: Using different model fields in ActiveRecord Concern

Quite new to Rails and have run into an issue I just can't seem to figure out.
I have 2 models, User & Post. Users will have a "name" attribute, Posts will have a "title" attribute.
In both cases, I would like to also maintain a slug that will, on before_save, convert the appropriate column to a "sluggified" version and store that as the slug. I've already got the logic I want in place and have had this working, however, I'd like to abstract the behavior into a Concern.
I cannot seem to figure out a way to set this up - mostly because of the dynamic nature of the source field. I'd like to be able to do something like the following:
class User < ActiveRecord::Base
include Sluggable
act_as_slug :name
end
class Post < ActiveRecord::Base
include Sluggable
act_as_slug :title
end
Unfortunately, no matter what I've tried on the implementation of the concern, I've run into walls.
While I'd like to know what type of implementation is possible either way, I'd also be interested in hearing if this is a good use case for concerns or not?
This seems to work, in the event anyone else is looking for an answer (definitely open to better suggestions from those with more experience). The models look as suggested in the original post.
module Sluggable
extend ActiveSupport::Concern
included do
before_save :generate_slug
class_attribute :sluggable_attribute
def generate_slug
self.sluggify(self.class.sluggable_attribute)
end
def sluggify(attribute)
# Sluggify logic goes here
end
end
module ClassMethods
def acts_as_slug(value)
self.sluggable_attribute = value
end
end
end

Conditional class inheritance for rails model

I have a multi-tenant rails app up and running.
Models that i want scoped to the current tenant (like this article model here) inherit the tenantscoped model like this
class Article < TenantScoped
end
this works great. i only recieve objects scoped to the current tenant.
but now im creating an admin interface where i want to be able to add articles to all tenants. but my admin interface is acting as a tenant and the models are being scoped to it.
Which ends with no entries being shown.
I am proposing that the best solution to this is to conditionally inherit from the tenant scoped model like this
class Article
unless SudoTenant.current?
< TenantScoped
else
< ActiveRecord::Base
end
end
i've been searching around to conditional inheritance for ruby classes and havent found anything yet. my syntax is wrong here or is this even possible?
Thanks in advance
You can define the class using the block syntax:
if SudoTenant.current?
Article = Class.new(ActiveRecord::Base) do
# your code
end
else
Article = Class.new(TenantScoped) do
# your code
end
end
I strongly recommend to use mixins instead of conditionally inheriting, it's cleaner, clearer and more obvious.
Not exactly what you're asking, but I happen to be doing the same thing (global articles on a tenant app), and I just created a Tenant for Admin for using it in my global Articles.
I've got something like this:
#article.rb
def self.global
unscoped.where(:company => Company.admin)
end
#company.rb
def self.admin
where(:name => 'admin').first # this can pretty much be anything that fits to you.
end

Resources