Paperclip, before_save, and deleting attachments - ruby-on-rails

I can't get this before_save filter to work. My methods are pretty standard, I think. Images are uploaded via Paperclip.
before_save :remove_checked_attachments
def attachments
%w(banner footer logo accreditation)
end
private
def remove_checked_attachments
attachments.each do |a|
if "remove_#{a}".to_sym && !"#{a}_updated_at_changed?".to_sym
"#{a}".to_sym.destroy
end
end
end
The remove_... params are passed, nothing's deleted though:
... "remove_banner"=>"1" ...
Any thoughts? Thanks.
Update
Even simplifying it to this doesn't work:
after_validation { banner.clear if remove_banner == '1' }
And "remove_banner"=>"1" comes through in the params. The u.banner.clear then u.banner.save works fine in the console.

I've solved this by making a concern like so:
# must be included after attachment declarations in model
module RemoveAttachment
extend ActiveSupport::Concern
included do
attachment_definitions.keys.each do |name|
attr_accessible :"remove_#{name}"
attr_accessor :"remove_#{name}"
before_validation { send(name).destroy if send("remove_#{name}") == '1' }
define_method :"remove_#{name}=" do |value|
instance_variable_set :"#remove_#{name}", value
send("#{name}_file_name_will_change!")
end
end
end
end
And just including the concern wherever need be. Thanks to this answer for a huge clue.

Related

has_and_belongs_to_many relationship updates not being added to changes_to_save

Using Rails 6 and having trouble understanding how best to solve the issue I am having with a has_to_and_belongs_to_many relationship model not being added to the changes_to_save hash when adding or removing it from the parent model (used to create a timeline of changes).
Currently I have solved the issue creating a custom concern but I wondered if there is a better way of doing this?
edit- To clarify I want to track changes to the video, so when a video_tag is added or removed it will be included in video.changes_to_save
Example of what I have (base model)
class video < Base::StandardModel
...
has_and_belongs_to_many :video_tags,
before_add: :make_dirty,
before_remove: :make_dirty
...
Dirty Association Concern
module DirtyAssociationsConcern
extend ActiveSupport::Concern
included do
attr_accessor :dirty_association_name
attr_accessor :dirty_association_before_changes
def make_dirty(record)
if self.dirty_association_before_changes.nil?
self.dirty_association_before_changes = self.send(association_name(record)).map(&:name).join(", ")
self.dirty_association_name = association_name(record)
end
end
def has_changes_to_save?
dirty_association_name.present? || super
end
def saved_changes?
dirty_association_name.present? || super
end
def changes_to_save
dirty_association_name.present? ? build_changes.merge(super) : super
end
def saved_changes
dirty_association_name.present? ? build_changes.merge(super) : super
end
private
def association_name(record)
record.class.to_s.underscore.pluralize
end
def association_name_ids
"#{dirty_association_name.singularize}_ids"
end
def build_changes
{ association_name_ids => [dirty_association_before_changes, self.send(dirty_association_name).map(&:name).join(", ")] }
end
end
end

How to add errors before updating attributes?

I'm trying to handle the situation where the user has entered info incorrectly, so I have a path that follows roughly:
class Thing < AR
before_validation :byebug_hook
def byebug_hook
byebug
end
end
thing = Thing.find x
thing.errors.add(:foo, "bad foo")
# Check byebug here, and errors added
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
byebug for byebug_hook> errors.messages #=> {}
Originally I thought that maybe the model was running its own validations and overwriting the ones I added, but as you can see even when I add the before hook the errors are missing, and I'm not sure what's causing it
ACTUAL SOLUTION
So, #SteveTurczyn was right that the errors needed to happen in a certain place, in this case a service object called in my controller
The change I made was
class Thing < AR
validate :includes_builder_added_errors
def builder_added_errors
#builder_added_errors ||= Hash.new { |hash, key| hash[key] = [] }
end
def includes_builder_added_errors
builder_added_errors.each {|k, v| errors.set(k, v) }
end
end
and in the builder object
thing = Thing.find x
# to my thinking this mirrors the `errors.add` syntax better
thing.builder_added_errors[:foo].push("bad foo") if unshown_code_does_stuff?
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
update_attributes will validate the model... this includes clearing all existing errors and then running any before_validation callbacks. Which is why there are never any errors at the pont of before_validation
If you want to add an error condition to the "normal" validation errors you would be better served to do it as a custom validation method in the model.
class Thing < ActiveRecord::Base
validate :add_foo_error
def add_foo_error
errors.add(:foo, "bad foo")
end
end
If you want some validations to occur only in certain controllers or conditions, you can do that by setting an attr_accessor value on the model, and setting a value before you run validations directly (:valid?) or indirectly (:update, :save).
class Thing < ActiveRecord::Base
attr_accessor :check_foo
validate :add_foo_error
def add_foo_error
errors.add(:foo, "bad foo") if check_foo
end
end
In the controller...
thing = Thing.find x
thing.check_foo = true
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end

detect if only one attribute is updated in Rails 4 on update_attributes

I am making a blogging app. I need to have two different methods based on how many attributes have been changed. Essentially, if ONLY the publication_date changes I do one thing...even the publication_date and ANYTHING ELSE changes, I do another thing.
posts_controller.rb
def special_update
if #detect change of #post.publication_date only
#do something
elsif # #post changes besides publication_date
elsif #no changes
end
end
One way to approach this is in your model using methods provided by ActiveModel::Dirty, which is available to all your Rails Models. In particular the changed method is helpful:
model.changed # returns an array of all attributes changed.
In your Post model, you could use an after_update or before_update callback method to do your dirty work.
class Post < ActiveRecord::Base
before_update :clever_method
private
def clever_method
if self.changed == ['publication_date']
# do something
else
# do something else
end
end
end
craig.kaminsky's answer is good, but if you prefer to mess with your controller instead of your model, you can do that as well:
def special_update
# the usual strong params thing
param_list = [:title, :body]
new_post_params = params.require(:post).permit(*param_list)
# old post attributes
post_params = #post.attributes.select{|k,v| param_list.include(k.to_sym)}
diff = (post_params.to_a - new_post_params.to_a).map(&:first)
if diff == ['publication_date']
#do something
elsif diff.empty? # no changes
else # other changes
end
end
Or simply compare parameter with existing value
if params[:my_model][:publication_date] != #my_model.publication_date
params[:my_model][:publication_date] = Time.now
end

Is it possible to omit "eval" in favor of "call" or "send" somehow in my case (Rails app)?

I'm newbie in Ruby, so need help, because can not find answer :(
I have Rails application, which has model Event like this:
class Event < ActiveRecord::Base
before_validation :clean_input
....
protected
def clean_input
fields = %w[title preview content]
fields.each do |field|
eval "self.#{field} = ActionController::Base.helpers.sanitize(self.#{field})"
end
end
end
The method purpose is cleaning input from dangerous data before validation and before storing it in DB.
Before I wrote this method it looked like the one below (with lot of duplication, that is not DRY at all). This code is very clear, but when adding new field I'll have to add new line instead of adding new element to an array:
def clean_input
self.title = ActionController::Base.helpers.sanitize(self.title)
self.preview = ActionController::Base.helpers.sanitize(self.preview)
self.content = ActionController::Base.helpers.sanitize(self.content)
end
So my questions are:
1) is it possible to omit eval ... in favor of call or send somehow (all my attemps were useless)?
2) is it possible to declare before_validation :clean_input like this before_validation clean_input: fields: { :title, :preview, :content}?
1) Sure:
def clean_input
%w(title preview content).each do |field|
self.send("#{field}=", ActionController::Base.helpers.sanitize(self.send(field)))
end
end
2) No, and your current implementation is ok
Since you are updating an active record model, there are a few other ways of updating attributes, for example:
def clean_input
%i(title preview content).each do |field|
self[field] = ActionController::Base.helpers.sanitize(self[field])
end
end

Refactor this Ruby and Rails code

I have this model:
class Event < Registration
serialize :fields, Hash
Activities=['Annonce', 'Butiksaktivitet', 'Salgskonkurrence']
CUSTOM_FIELDS=[:activity, :description, :date_from, :date_to, :budget_pieces, :budget_amount, :actual_pieces, :actual_amount]
attr_accessor *CUSTOM_FIELDS
before_save :gather_fields
after_find :distribute_fields
private
def gather_fields
self.fields={}
CUSTOM_FIELDS.each do |cf|
self.fields[cf]=eval("self.#{cf.to_s}")
end
end
def distribute_fields
unless self.fields.nil?
self.fields.each do |k,v|
eval("self.#{k.to_s}=v")
end
end
end
end
I have a feeling that this can be done shorter and more elegant. Does anyone have an idea?
Jacob
BTW. Can anyone tell me what the asterisk in front of CUSTOM_FIELDS does? I know what it does in a method definition (def foo(*args)) but not here...
Alright first off: never 10000000000.times { puts "ever" } use eval when you don't know what you're doing. It is the nuclear bomb of the Ruby world in the way that it can wreak devestation across a wide area, causing similar symptoms to radiation poisoning throughout your code. Just don't.
With that in mind:
class Event < Registration
serialize :fields, Hash
Activities = ['Annonce', 'Butiksaktivitet', 'Salgskonkurrence']
CUSTOM_FIELDS = [:activity,
:description,
:date_from,
:date_to,
:budget_pieces,
:budget_amount,
:actual_pieces,
:actual_amount] #1
attr_accessor *CUSTOM_FIELDS #2
before_save :gather_fields
after_find :distribute_fields
private
def gather_fields
CUSTOM_FIELDS.each do |cf|
self.fields[cf] = send(cf) #3
end
end
def distribute_fields
unless self.fields.empty?
self.fields.each do |k,v|
send("#{k.to_s}=", v) #3
end
end
end
end
Now for some notes:
By putting each custom field on its own line, you increase code readability. I don't want to have to scroll to the end of the line to read all the possible custom fields or to add my own.
The *CUSTOM_FIELDS passed into attr_accessor uses what is referred to as the "splat operator". By calling it in this way, the elements of the CUSTOM_FIELDS array will be passed as individual arguments to the attr_accessor method rather than as one (the array itself)
Finally, we use the send method to call methods we don't know the names of during programming, rather than the evil eval.
Other than that, I cannot find anything else to refactor about this code.
I agree with previous posters. In addition I would probably move the gather_fields and distribute_fields methods to the parent model to avoid having to repeat the code in every child model.
class Registration < ActiveRecord::Base
...
protected
def gather_fields(custom_fields)
custom_fields.each do |cf|
self.fields[cf] = send(cf)
end
end
def distribute_fields
unless self.fields.empty?
self.fields.each do |k,v|
send("#{k.to_s}=", v)
end
end
end
end
class Event < Registration
...
before_save :gather_fields
after_find :distribute_fields
private
def gather_fields(custom_fields = CUSTOM_FIELDS)
super
end
end
You can replace the two evals with send calls:
self.fields[cf] = self.send(cf.to_s)
self.send("#{k}=", v)
"#{}" does a to_s, so you don't need k.to_s
Activities, being a constant, should probably be ACTIVITIES.
For that asterisk *, check out this post: What is the splat/unary/asterisk operator useful for?
Activities=['Annonce', 'Butiksaktivitet', 'Salgskonkurrence']
can be written: ACTIVITIES = %w(Annonce, Butiksaktivitet, Salgskonkurrence).freeze since you are defining a constant.
def distribute_fields
unless self.fields.empty?
self.fields.each do |k,v|
send("#{k.to_s}=", v) #3
end
end
end
can be written as a one liner:
def distribute_fields
self.fields.each { |k,v| send("#{k.to_s}=", v) } unless self.fields.empty?
end
Ryan Bigg, gave a good answer.

Resources