Do action after upload file with Active Storage - ruby-on-rails

My model has a has_many_attached :photos.
The first time this model is created, it has 0 photos. If I run photos.attached? I get false.
When the user uploads some files to photos, I need to do some actions, but only the first time. I tried using before_update :if photos.attached?. But I need to know if the user is updating photos specifically.
Is there a way to know if the user is trying to update photos? Or is there a simple way to do this?

There is the dirty? method that you can use
class Post < ApplicationRecord
has_many_attached :photos
before_update :do_whatever, if: -> { photos.dirty? }
def do_whatever
# doing something
end
end
You might also be able to try before_update :do_whatever, if: -> { photos_changed? }

Check callbacks (after_create, after_save, etc) in rails models and options "on: :create" and "update".

Rails has not added the ability to add validations to file attachments. See Issue #31656
Try this approach:
Validate for mime-types:
<%= f.file_field :photos, accept: "image/png,image/gif,image/jpeg', multiple: true %>
You still need to add model validations. See this post Validating the Content-Type of Active Storage Attachments
on how to use a custom validator.
To limit your action to be executed only once, you could add a boolean field to your model and set it to true after your action terminates successfully. Use a before_update as Antarr Byrd suggested to check after each photo is attached to your model.

Related

setting mandatory field automatically in Rails from Model callback, when and how?

I have this model which has a mandatory field which needs to be automatically set just before save. I'm struggling with the correct way to implement this:
build the logic in the controller before the save (and have validates rule in model)
build the logic in a before_save callback and have validates rule in model, but this seems to late in the flow? I do get validation errors this way.
build the logic in a before_save callback and don't define validation for this particular field
do it any of the ways above and don't assign a validates rule for the particular field
I was working on 2 since this seems like the correct way to implement this. Was considering the usage of before_validation, but I don't know what would happen when my other fields don't get validated... this could cause double assignment of the same value..
code for 2 which gives a basic idea of what I'm trying to achieve:
#category.rb
class Category < ActiveRecord::Base
before_create :set_position_number
def set_position_number
highest = Category.maximum(:position)
self.position = highest.to_i + 1
end
end
I'm struggling with the correct way to implement this
The most efficient way will be to use an ActiveRecord callback hook, such as you've posted:
#app/models/category.rb
class Category < ActiveRecord::Base
before_create :your_action
private
def your_action
#fires before create
end
end
but this seems to late in the flow
As mentioned in the comments, you can see the order of the callbacks (and thus their order in the flow):
Thus, if you want to populate some data before you validate, and then validate that data, you'll be best using the before_validation callback:
#app/models/category.rb
class Category < ActiveRecord::Base
before_validation :set_position_number, on: :create
validates :position, ______________
private
def set_position_number
highest = Category.maximum(:position)
self.position = highest.to_i + 1
end
end
Remember, a Rails model just populates certain attributes which are then to be either saved to the db, or validated. Rails does not care where those attributes come from; populating them before_validation is a good a source as the controller.
If you are setting a value automatically and don't take user input, you don't need validation. Write a unit test.
If the field is something like a position value, then you should indeed set it in a before_create callback.

How can I programmatically copy ActiveModel validators from one model to another?

I'm writing a library that will require programmatically copying validations from one model to another, but I'm stumped on how to pull this off.
I have a model that is an ActiveModel::Model with some validation:
class User < ActiveRecord::Base
validates :name, presence: true
end
And another model that I'd like to have the same validations:
class UserForm
include ActiveModel::Model
attr_accessor :name
end
Now I'd like to give UserForm the same validations as User, and without modifying User. Copying the validators over doesn't work, because ActiveModel::Validations hooks into callbacks during the validation check:
UserForm._validators = User._validators
UserForm.new.valid?
# => true # We wanted to see `false` here, but no validations
# are actually running because the :validate callback
# is empty.
Unfortunately, there doesn't seem to be an easy way that I can see to programmatically give one model another's validation callbacks and still have it work. I think my best bet is if I can ask Rails to regenerate the validation callbacks based on the validators that are present at a given moment in time.
Is that possible? If not, is there a better way to do this?
Checking into the code of activerecord/lib/active_record/validations/presence.rb reveals how this can be achieved:
# File activerecord/lib/active_record/validations/presence.rb, line 60
def validates_presence_of(*attr_names)
validates_with PresenceValidator, _merge_attributes(attr_names)
end
So I guess I would try to hook into validates_with with an alias_method
alias_method :orig_validates_with :validates_with
Now you have a chance to get ahold of the values passed, so you can store them somewhere and retrieve them when you need to recreate the validation on UserForm
alias_method :orig_validates_with, :validates_with
def validates_with(*args)
# save the stuff you need, so you can recreate this method call on UserForm
orig_validates_with(*args)
end
Then you should be able to just call UserForm.validates_with(*saved_attrs). Sorry this is not something you can just copy/paste, but this should get you started. HTH

Rails - How to prevent deleting all children records of a parent

Listing < AR
has_many :images
accepts_nested_attributes_for :images, :allow_destroy => true
validate :validate_image_count
def validate_image_count
errors.add_to_base("too few") if images.length < 1
end
end
Image < AR
belongs_to :listing
end
In my Listing#edit form I use fields_for to provide fields for all the images along with checkboxes to delete images. This is working fine. I want to enforce a check such that a listing is valid only if it had at least one image and at most 6.
In my current setup I can go to edit and delete all the images, and then update the listing.
I have tried using a validation as shown above but thats not being called. Could be just the way nested_attributes work in rails. Whats the best way to enforce this check?
as the images won't be deleted when you call the validation method it would return true on the image length. You can use marked_for_destruction?
def validate_image_count
errors.add_to_base("too few") self.images.any? { |i| i.marked_for_destruction? }
end

Using after_find callback to overwrite nil values

Right now I have a Rails 3 model for storing profile data. One of the columns in that database table contains an image url if the user chooses to display a profile picture (this also integrates with the Facebook Graph API to store the user's profile picture url if they login with Facebook but that's irrelevant). The problem I am having is that when the image column is nil, I need a way to set it to a default path on my server. Please note that I cannot use a default value in a migration or model here. My thought was to use an after_find but the following is not working:
In Profile Model:
def after_find
if self.image.nil?
self.image = "/assets/generic_profile_image.png"
end
end
In view (HAML):
.profile_pic
= image_tag #user.profile.image
The Profile model is linked to a User model via a has_one association. Right now instead of dynamically turning the image attribute into "/assets/generic_profile_image.png", it seems to do nothing leaving me with the following generated code on my page:
<div class='profile_pic'>
<img alt="Assets" src="/assets/" />
</div>
Any suggestions on how to fix this issue would be greatly appreciated!
P.S. A conditional in the view is out of the question as the profile image is shown in way too many places!
Just make the condition in the model and reference it in the view.
class User
delegate :image, :to => :profile, :prefix => true, :allow_nil => true
def picture_url
if profile_image.present?
profile_image
else
"/assets/generic_profile_image.png"
end
end
end
I like this approach because you won't have to run a sql query when you want to change the default picture.
I added the delegate to prevent breaking the law of demeter.
Of course you've already guessed the view code:
.profile_pic
= image_tag #user.picture_url
My guess is your after_find callback ins't actually getting called. You need to define it this way:
class Profile < ActiveRecord::Base
after_find :update_image
def update_image
if self.image.nil?
self.image = "/assets/generic_profile_image.png"
end
end
end
Now everything should work fine.

How to set some field in model as readonly when a condition is met?

I have models like this:
class Person
has_many :phones
...
end
class Phone
belongs_to :person
end
I want to forbid changing phones associated to person when some condition is met. Forbidden field is set to disabled in html form. When I added a custom validation to check it, it caused save error even when phone doesn't change. I think it is because a hash with attributes is passed to
#person.update_attributes(params[:person])
and there is some data with phone number (because form include fields for phone). How to update only attributes that changed? Or how to create validation that ignore saves when a field isn't changing? Or maybe I'm doing something wrong?
You might be able to use the
changed # => []
changed? # => true|false
changes # => {}
methods that are provided.
The changed method will return an array of changed attributes which you might be able to do an include?(...) against to build the functionality you are looking for.
Maybe something like
validate :check_for_changes
def check_for_changes
errors.add_to_base("Field is not changed") unless changed.include?("field")
end
def validate
errors.add :phone_number, "can't be updated" if phone_number_changed?
end
-- don't know if this works with associations though
Other way would be to override update_attributes, find values that haven't changed and remove them from params hash and finally call original update_attributes.
Why don't you use before_create, before_save callbacks in model to restrict create/update/save/delete or virtually any such operation. I think hooking up observers to decide whether you want to restrict the create or allow; would be a good approach. Following is a short example.
class Person < ActiveRecord::Base
#These callbacks are run every time a save/create is done.
before_save :ensure_my_condition_is_met
before_create :some_other_condition_check
protected
def some_other_condition_check
#checks here
end
def ensure_my_condition_is_met
# checks here
end
end
More information for callbacks can be obtained here:
http://guides.rubyonrails.org/activerecord_validations_callbacks.html#callbacks-overview
Hope it helps.

Resources