Rails - Save tenant in Audited (formerly acts_as_audited) - ruby-on-rails

I have Audited (formerly acts_as_audited) setup and working. The user_id is successfully saved in the audit table but I can't figure out an efficient way to save the tenant_id (I have multitenancy setup with scopes). I have tried using the Associated Audits technique described in the README but that doesn't work for me.
My current solution is to use the after_audit callback in every model (can be implemented with Rails concerns) to get the last audit and save the tenant_id:
def after_audit
audit = Audit.last
audit.tenant_id = self.tenant_id
audit.save!
end
Whilst this works it seems like it would be inefficient to have to query for the audit again and then update it. It would make more sense to me to add the tenant_id to the audit before it saves but I can't figure out how to do this. Is it possible to add the tenant_id to the audit before saving? If yes, then how?
EDIT:
I've also tried including my default tenant scope in my Audit model but it does not seem to be called:
audit.rb
class Audit < ActiveRecord::Base
default_scope { where(tenant_id: Tenant.current_id) }
application_controller.rb
class ApplicationController < ActionController::Base
around_action :scope_current_tenant
def scope_current_tenant
Tenant.current_id = current_tenant.id
yield
ensure
Tenant.current_id = nil
end
EDIT: 2/1/16
I still haven't implemented a solution to this however my current thoughts would be to use:
#model_name.rb
def after_audit
audit = self.audits.last
audit.business_id = self.business_id
audit.save!
end
In this code we get the last audit for the current model. This way we are only dealing with the current model, there is no chance of adding the audit to another business (as far as I can tell). I would add this code into a concern to keep it DRY.
I still can't get normal Rails callbacks to work within the Audit model. The only other way I see at the moment is to fork and modified the gem source code.

I was tasked with implementing Auditing, and also adding a reference to an Org. The migration adds this line:
t.references :org, type: :uuid, index: true, null: true
To save an org_id, I ended up writing an initializer - audited.rb. That file looks like this:
Rails.configuration.after_initialize do
Audited.audit_class.class_eval do
belongs_to :org, optional: true
default_scope MyAppContext.context_scope
before_create :ensure_org
private
def ensure_org
return unless auditable.respond_to? :org_id
self.org_id = auditable.org_id
end
end
end
Hope this helps!

I have recently added Acts As Tenant gem to a Rails app that is running the Audited gem. I was running into the same problem. I had added
acts_as_tenant :account
to the Audit model but it didn't do anything. I learned that you can't override in the Audit model but have to create a custom audit model that inherits from it. So I created the model:
custom_audit.rb
class CustomAudit < Audited::Audit
acts_as_tenant :account
end
I then added the initializer file audited.rb in confi/initializers like so:
Audited.config do |config|
config.audit_class = CustomAudit
end
I was still having the problem where all my multitenancy was working except the show_audit view. I finally deleted all of my audits from both tenants in my test setup. It worked! I can now add new audits and they scope just fine. But I still need to merge the actual client DBs into one, and I don't want to lose the history in the audit table... Not sure how to fix that.
So when I try to access the Audits it fails with current_tenant being nil. Not sure why deleting all of the current records in the table fixes it, but I need to find a way around it.

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

Rails preview update associations without saving to database

I want to preview what the model will look like when saved without currently saving to the database.
I am using #event.attributes = because that assigns but does not save attributes for #event to the database.
However, when I also try to assign the audiences association, Rails inserts new records into the audiences_events join table. Not cool. Is there a way to preview what these new associations will look like without inserting into the join table?
Model
class Event < ActiveRecord::Base
has_and_belongs_to_many :audiences # And vice versa for the Audience model.
end
Controller
class EventsController < ApplicationController
def preview
#event = Event.find(params[:id])
#event.attributes = event_params
end
private
def event_params
params[:event].permit(:name, :start_time, :audiences => [:id, :name]
end
end
Possible Solutions?
Possible solutions that I thought of, but don't know how to do:
Using some sort of method that assigns associations, but does not persist them.
disabling all database writes for this one action (I dont know how to do that).
Rolling back all database changes at the end of this action
Any help with these would be great!
UPDATE:
After the reading the great answers below, I ended up writing this service class that assigns the non-nested attributes to the Event model, then calls collection.build on each of the nested params. I made a little gist. Happy to receive comments/suggestions.
https://gist.github.com/jameskerr/69cedb2f30c95342f64a
In these docs you have:
When are Objects Saved?
When you assign an object to a has_and_belongs_to_many association, that object is automatically saved (in order to update the join table). If you assign multiple objects in one statement, then they are all saved.
If you want to assign an object to a has_and_belongs_to_many association without saving the object, use the collection.build method.
Here is a good answer for Rails 3 that goes over some of the same issues
Rails 3 has_and_belongs_to_many association: how to assign related objects without saving them to the database
Transactions
Creating transactions is pretty straight forward:
Event.transaction do
#event.audiences.create!
#event.audiences.first.destroy!
end
Or
#event.transaction do
#event.audiences.create!
#event.audiences.first.destroy!
end
Notice the use of the "bang" methods create! and destroy!, unlike create which returns false create! will raise an exception if it fails and cause the transaction to rollback.
You can also manually trigger a rollback anywhere in the a transaction by raising ActiveRecord::Rollback.
Build
build instantiates a new related object without saving.
event = Event.new(name: 'Party').audiences.build(name: 'Party People')
event.save # saves both event and audiences
I know that this is a pretty old question, but I found a solution that works perfectly for me and hope it could save time to someone else:
class A
has_many :bs, class_name 'B'
end
class B
belongs_to :a, class_name: 'A'
end
a.bs.target.clear
new_bs.each {|new_b| a.bs.build new_b.attributes.except('created_at', 'updated_at', 'id') }
you will avoid autosave that Rails does when you do a.bs = new_bs

how to run a one-time database change on a single user

I have Customer and each customer has_many Properties. Customers belong to a Company.
I'm trying to add a certain Property to each one of a single Company's Customers. I only want this change to happen once.
I'm thinking about using a migration but it doesn't seem right to create a migration for a change that I only ever want to happen once, and only on one of my users.
Is there a right way to do this?
You can just use rails console.
In rails c:
Company.where(conditions).last.customers.each do |customer|
customer.properties << Property.where(condition)
customer.save!
end
Validation
Depending on how you're changing the Customer model, I'd include a simple vaidation on the before_update callback to see if the attribute is populated or not:
#app/models/Customer.rb
class Customer < ActiveRecord::Base
before_update :is_valid?
private
def is_valid?
return if self.attribute.present?
end
end
This will basically check if the model has the attribute populated. If it does, it means you'll then be able to update it, else it will break
--
Strong_Params
An alternative will be to set the strong_params so that the attribute you want to remain constant will not be changed when you update / create the element:
#app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
...
private
def strong_params
params.require(:model).permit(:only, :attributes, :to, :update)
end
end
It would be much more helpful if you explained the context as to why you need this type of functionality - that will give people the ability to create a real solution, instead of proposing ideas

How can I achieve a functionality similar to "auto-insertion of timestamps"

I have a couple of fields created_by and updated_by in most of my tables. This would contain the user id of the user who created or updated the Object. is it possible to have a similar function like how rails handles created_at? I basically want it to function the same way as the timestamps insertion. I should be able to define in the columns in the migration script and configure rails to fetch the user object from a helper method everytime when it changes the particular object. Is there a direct way to do it or is there a plugin which does this?
Also you can do this without gems but with Rails Observers
You can create observer like this:
class UserTouchObserver < ActiveRecord::Observer
observe :product, :post, :comment
def after_create(model)
update_attribute(:created_by, current_user.id) if model.respond_to?(:created_by)
end
def after_update(model)
update_attribute(:updated_by, current_user.id) if model.respond_to?(:updated_by)
end
end
I was able to find a few plugins on github that do just this:
https://github.com/jnunemaker/user_stamp
https://github.com/bokmann/userstamp_basic

Removing the :has_many when the :belongs_to is updated/destroyed if the :has_many is now empty

I have states who have many cities (belongs_to :state) who have many businesses (belongs_to :city).
State also… has_many :businesses, :through => :cities
On my site everything is managed from the Business perspective. When a new Business is created/updated the state/city is created if it doesn't already exist. This happens in a :before_save call.
I'm having problems removing States/Cites when a Business gets updated. If the state/city that a business is in gets changed (again this happens from an edit business form) and the old state/city no longer has any businesses I want to destroy it. I've tried doing this in after_save calls but they're wrapped in a transaction and even if I assign variables to the names of the old state/city, they seem to get changed to the new state/city sometime during the transaction. It's crazy! I used "puts" calls to print the vars in some spots in my Business model and watched the vars change during a save. It was frustrating.
So, right now I'm handling this from the controller but it feels hackish.
Here's some of my code.
http://pastie.org/648832
Also, I'd love any input on how better to structure this whole thing.
Thanks
You want after_destroy callbacks to destroy the has many side of a relationship if it has none.
To ensure this behaviour after an update, we need to use the ActiveRecord::Dirty methods. Which are built into rails as of 2.1. If you're running an older version you'll need the Dirty plugin
class Business < ActiveRecord::Base
...
after_update :destroy_empty_city
after_destroy :destroy_empty_city
protected
def destroy_empty_city
c = city_changed? ? city_was : city
c.destroy if c.businesses.empty?
end
end
class City < ActiveRecord::Base
...
after_destroy :destroy_empty_state
protected
def destroy_empty_state
state.destroy if state.businesses.empty?
end
end
You might need to check if city/state.businesses == [self] instead of city/state.businesses.empty? if your associations are eager loaded. I can't remember how rails treats associations after destroy. I'm assuming that if they're eager loaded than the code above won't work and you will need the alternate check. Otherwise it should be fine.

Resources