How can I disallow updates except for on one field? - ruby-on-rails

I've been preventing updates to certain models by using this in the model:
def update
self.errors.add_to_base( "Cannot update a #{ self.to_s }" )
end
I'm now writing a plugin that delivers some extra functionality to the model, and I need to update one field in the model. If I weren't using a plugin I would do this directly in the model...
def update
if self.changed == ['my_field']
super
else
self.errors.add_to_base( "Cannot update a #{ self.to_s }" )
end
end
I can't do the same from my plugin since I don't know if the update behaviour is the ActiveRecord default, or has been overridden to prevent updates. Is there another way to prevent record updates while allowing me to override for a specific field (and only in the instance where my plugin is applied to this model).

First, you should be using a before_update callback for that sort of thing rather than overriding update. Second, you can store the updatable attributes on the model, and then update them with the plugin. I just wrote this in the browser, so it could be wrong.
attr_accessor :updatable_attributes
before_update :prevent_update
private
def prevent_update
return true if self.changed == self.updatable_attributes
self.errors.add_to_base "Cannot update a #{ self.to_s }"
false
end
end

Late to the game here, but for people viewing this question, you can use attr_readonly to allow writing to a field on create, but not allowing updates.
See http://api.rubyonrails.org/classes/ActiveRecord/ReadonlyAttributes/ClassMethods.html
I think it has been available since Rails 2.0
The tricky part is, if you have any attributes that are attr_accessible you have to list your read only attributes there also (or you get a mass assignment error on create):
class Post < ActiveRecord::Base
attr_readonly :original_title
attr_accessible :latest_title, :original_title
end

Is this to prevent mass assignment? Would attr_accessible / attr_protected not do what you need?
Edit, just to illustrate the general point about the callback.
module MyModule
def MyModule.included(base)
base.send :alias_method_chain, :prevent_update, :exceptions
end
def prevent_update_with_exceptions
end
end
class MyModel < ActiveRecord::Base
before_validation :prevent_update
def prevent_update
end
include MyModule
end

I just use the rails params.require method to whitelist attributes that you want to allow.
def update
if #model.update(update_model_params)
render json: #model, status: :ok
else
render json: #model.errors, status: :unprocessable_entity
end
end
private
def update_prediction_params
params.require(:model).permit(:editable_attribute)
end

Related

cannot update a new record on after_create callback

class Model < ActiveRecord::Base
after_create :set_slug
def set_slug
update_column(:slug, to_slug)
end
def to_slug
#code to create slug
end
end
Why does this return 'ActiveRecord::ActiveRecordError: cannot update a new record' if the callback is an after_create? The issue is with "update_column"
Your problem lies in the fact that update_columns doesn't work on new records.
Why not use update_attributes` instead
after_create do
self.update_attributes(slug: to_slug)
end
if you want then you can also try following approach
if new_record?
return
else
update_column(:slug, to_slug)
end
Also check the model side validations. That may also cause the problems.
There is ActiveRecord::Persistence::ClassMethods#update_columns method which contains line
raise ActiveRecordError, "cannot update a new record" if new_record?
therefore you should use update_all, for example:
def set_slug
self.class.where(id: id).update_all(slug: to_slug) if id
end
Hope it will help

Accessing current_user in a model in a Rails 3.2 app

I have a Rails 3.2 app. It is a publishing app where we kick off several Sidekiq jobs in response to changes in content. I was calling this from the controller but there's now getting to be multiple points of entry and are now duplicating logic in multiple controllers. The proper place for this to be is in a callback in the model. However, accessing current_user is frowned upon in the model but for things like logging changes or app events, it is critical.
So I have two questions (1) Is there something I'm missing regarding the argument about accessing current_user when you want to be logging changes across complex model structures? and (2) Is the proposed solution here an effective one with last update over 2 years ago in terms of thread-safety? I use a three Unicorn processes on Heroku. https://stackoverflow.com/a/2513456/152825
Edit 1
Thinking through this, wondering if I should just do something like this in my application.rb
class ArcCurrentUser
#current_user_id
def self.id
return #current_user_id
end
def self.id=id_val
#current_user_id=id_val
end
end
and then in my current_user method in application_controller, just update ArcCurrentUser.id to #current_user.id? I will only be using it for this logging functionality.
You're correct in that you can't access current_user from a model.
As for the answer you linked, I'm not entirely sure but I think it's not fully thread-safe. From the same question, I like this answer https://stackoverflow.com/a/12713768/4035338 more.
Say we have a controller with this action
...
def update
#my_object = MyModel.find(params[:id])
#my_object.current_user = current_user
#my_object.assign_attributes params[:my_model]
#my_object.save
end
...
and this model
class MyModel < ActiveRecord::Base
attr_accessor :current_user
before_save :log_who_did_it
private
def log_who_did_it
return unless current_user.present?
puts "It was #{current_user}!"
end
end
Or my favourite
...
def update
#my_object = MyModel.find(params[:id])
#my_object.update_and_log_user(params[:my_model], current_user)
end
...
class MyModel < ActiveRecord::Base
...
def update_and_log_user(params, user)
update_attributes(params)
puts "It was #{user}!" if user.present?
end
end

Rails Service Object, adding errors to new Model

I recently had a rails model that had several callbacks on it like so:
class Model < ActiveRecord::Base
before_validation :fetch_posts
after_create :build_posts
def fetch_posts
fetch_collection
rescue MyException => e
self.errors.add(:post, e.message)
end
def build_posts
fetch_collection.each do |item|
DifferentModel.build(item)
end
end
def fetch_collection
#collection ||= method_that_fetches_collection_from_external_source
end
end
This was working just fine but it was making it extremely difficult to write tests, as whenever I wanted to create a Model I had to stub out all the callbacks. Enter service objects:
class ModelFetcher
attr_reader :model
def initialize(model)
#model = model
end
def save
model.fetch_posts
if model.save
model.build_posts
return true
else
return false
end
end
end
The problem I'm seeing now, in the case where a model does indeed contain an error (from the fetch posts method), it doesn't get carried over to the model.save call in the SO. That is to say, the Model.new has an error, but once I call .save on that Model.new it doesn't maintain the error and the model saves properly.
I considered adding validate :fetch_posts but then I am back in the same situation I was before as this is essentially a callback.
Any advice on how to structure this better? Is it possible to maintain an error from Model.new to .save? Am I fundamentally misunderstanding something?
Thanks!
Here is an alternate solution which is to overwrite run_validations! since you have none.
class Model < ActiveRecord::Base
after_create :build_posts
def fetch_posts
fetch_collection
rescue MyException => e
self.errors.add(:post, e.message)
end
def build_posts
fetch_collection.each do |item|
DifferentModel.build(item)
end
end
def fetch_collection
#collection ||= method_that_fetches_collection_from_external_source
end
private
def run_validations!
fetch_posts
errors.empty?
end
end
Usually this method looks like
def run_validations!
run_callbacks :validate
errors.empty?
end
but since you have no validations it should serve a similar purpose on #save.
Or as I suggested in a comment you can replace save with model.errors.any? Since save will clear your original errors set by fetch_posts but errors.any? Will check if there were errors during the fecth_posts method.

Changing respond_to url for an ActiveModel object

Summary:
How do I customize the path that respond_to generates for an ActiveModel object?
Update: I'm looking for a hook, method override, or configuration change to accomplish this, not a workaround. (A workaround is easy but not elegant.)
Context & Example:
Here is an example to illustrate. I have a model, Contract, which has a lot of fields:
class Contract < ActiveRecord::Base
# cumbersome, too much for a UI form
end
To make the UI code easier to work with, I have a simpler class, SimpleContract:
class SimpleContract
include ActiveModel::Model
# ...
def contract_attributes
# convert SimpleContract attributes to Contract attributes
end
def save
Contract.new(contract_attributes).save
end
end
This works well, but I have a problem in my controller...
class ContractsController < ApplicationController
# ...
def create
#contract = SimpleContract.new(contract_params)
flash[:notice] = "Created Contract." if #contract.save
respond_with(#contract)
end
# ...
end
The problem is that respond_with points to simple_contract_url, but I want it to point to contract_url instead. What is the best way to do that? (Please note that I'm using ActiveModel.)
(Note: I'm using Rails 4 Beta, but that isn't central to my problem. I think a good answer for Rails 3 will work as well.)
Sidebar: if this approach to wrapping a model in a lightweight ActiveModel class seem unwise to you, please let me know in the comments. Personally, I like it because it keeps my original model simple. The 'wrapper' model handles some UI particulars, which are intentionally simplified and give reasonable defaults.
First, here is an answer that works:
class SimpleContract
include ActiveModel::Model
def self.model_name
ActiveModel::Name.new(self, nil, "Contract")
end
end
I adapted this answer from kinopyo's answer to Change input name of model.
Now, for the why. The call stack of respond_to is somewhat involved.
# Start with `respond_with` in `ActionController`. Here is part of it:
def respond_with(*resources, &block)
# ...
(options.delete(:responder) || self.class.responder).call(self, resources, options)
end
# That takes us to `call` in `ActionController:Responder`:
def self.call(*args)
new(*args).respond
end
# Now, to `respond` (still in `ActionController:Responder`):
def respond
method = "to_#{format}"
respond_to?(method) ? send(method) : to_format
end
# Then to `to_html` (still in `ActionController:Responder`):
def to_html
default_render
rescue ActionView::MissingTemplate => e
navigation_behavior(e)
end
# Then to `default_render`:
def default_render
if #default_response
#default_response.call(options)
else
controller.default_render(options)
end
end
And that is as far as I've gotten for the time being. I have not actually found where the URL gets constructed. I know that it happens based on model_name, but I have not yet found the line of code where it happens.
I'm not sure that I fully understand your problem, but could you do something like this?
class SimpleContract
include ActiveModel::Model
attr_accessor :contract
# ...
def contract_attributes
# convert SimpleContract attributes to Contract attributes
end
def save
self.contract = Contract.new(contract_attributes)
contract.save
end
end
-
class ContractsController < ApplicationController
# ...
def create
#simple_contract = SimpleContract.new(contract_params)
flash[:notice] = "Created Contract." if #simple_contract.save
respond_with(#simple_contract.contract)
end
# ...
end
I may be way off base. Hopefully that at least triggers an idea for you.

Hide a field in Ruby on Rails

I've a field in my database called IP where I put the user IP (in #create method) when he send a message in my blog built in Rails.
But the field is visible when I want to see the articles in another format (JSON).
How can I hide the field IP?
You can do it in a format block in your controller like this:
respond_to do |format|
format.json { render :json => #user, :except=> [:ip] } # or without format block: #user.to_json(:except => :ip)
end
If you want to generally exclude specific fields, just overwrite the to_json method in your user model:
class User < ActiveRecord::Base
def to_json(options={})
options[:except] ||= [:ip]
super(options)
end
end
Update: In Rails 6, the method became as_json:
class User < ApplicationRecord
def as_json(options={})
options[:except] ||= [:ip]
super(options)
end
end
While this is not quite the right solution for passwords or for what is specifically asked, this is what comes up when you google for hiding columns in ActiveRecord, so I'm going to put this here.
Rails5 introduced new API, ignored_columns, that can make activerecord ignore that a column exists entirely. Which is what I actually wanted, and many others arriving here via Google probably do too.
I haven't tried it yet myself.
class User < ApplicationRecord
self.ignored_columns = %w(employee_email)
end
https://blog.bigbinary.com/2016/05/24/rails-5-adds-active-record-ignored-columns.html

Resources