Lets imagine that I have a custom image upload for a particular record and I add two columns into the model.
thumbnail_url
thumbnail_path
Now lets imagine that I have a form with a :file field which is the uploaded file in multipart form. I would need to somehow have the model pickup the file within the given hash and then issue it over to a custom method which performs the upload and saves it as apart of the model.
Right now I'm doing this:
def initialize(options = nil)
if options
if options[:file]
self.upload_thumbnail(options[:file])
options.delete(:file)
end
super options
else
super
end
end
def update_attributes(options = nil)
if options
if options[:file]
self.upload_thumbnail(options[:file])
options.delete(:file)
end
super options
else
super
end
end
It works, but I am doing some unnecessary overriding here. Is there a simpler way of doing this? Something that would only require overriding perhaps one method?
You are looking for virtual attributes. Just define:
def file
# Read from file or whatever
end
def file=(value)
# Upload thumbnail and store file
end
and initialize, update_attributes and cousins will pick the methods for you.
That, or save the hassle and use paperclip as Kandada suggested.
Have you considered using using paperclip gem? It performs the functions you describe in your question.
Related
I have this function in rails controller:
def validate_params(*props)
props.each do |prop|
unless params[prop].start_with?('abc')
# return error
end
end
end
im thinking if I have params[:name] and params[:bio] and I want to validate name & bio with this function (not every attribute I might want to validate), I will call it with validate_params(:name, :bio). But, for nested param it won't work like params[:user][:name]. Is there anything I can do to pass this nested property to my function or is there a completely different approach? Thanks
Rails Validations generally belong in the model. You should post some additional info about what you're trying to do. For example, if you wanted to run the validation in the controller because these validations should only run in a certain context (i.e., only when this resource is interacted with from this specific endpoint), use on: to define custom contexts.
If you don't want to do things the rails way (which you should, imo), then don't call params in the method body. i.e.
def validate_params(*args)
args.each do |arg|
unless arg.start_with?('abc')
# return error
end
end
end
and call with validate_params(params[:user], params[:user][:name]
but yeah... just do it the rails way, you'll thank yourself later.
In my Rails model, I have this code to force the filename to be changed when uploading:
before_save :set_filename
def set_filename
if file.attached?
self.file.blob.update(filename: "#{new_file_name()}.#{self.file.blob.content_type.split('/')[1]}")
end
end
The problem is the filename is changed even if a new file is not sent in the form (when editing).
My attachement is simply named file:
# active storage
has_one_attached :file
How to really test that a new file is attached when uploading ?
Thanks,
EDIT: more clarifications
I have a form with a file_field.
I want to test if a new file is sent via the form, when I add or modify the object of the form.
My model is called Image and the attached file is called file.
class Image
has_one_attached :file
end
I want to change the filename every time a new file is sent via the form, and not of course is the file_field stays empty.
You can use new_record? to check if file is new ie:
def set_filename
if file.attached? && file.new_record?
self.file.blob.update(filename: "#{new_file_name()}.#{self.file.blob.content_type.split('/')[1]}")
end
end
Alternatively, use before_create instead of before_save so that set_name only runs when uploading new file.
Updated
Interestingly, ActiveStorage handles blob change outside model hooks. Apparently, it doesn't even support validation right now. There's no way to verify blob has changed as its state is not persisted anywhere. If you peek into rails log, notice rails purge old blob as soon as a new one is added.
Few options I can think of:
1.Update filename in controller eg:
original_name = params[:file].original_name
params[:file].original_name = # your logic goes here
2.Store blob file name in parent model and compare in before_save.
def set_filename
if file.attached? && file.blob.filename != self.old_filename
self.file.blob.update(filename: "#{new_file_name()}.#{self.file.blob.content_type.split('/')[1]}")
end
end
None of these solutions are ideal but hope they give you some ideas.
I have just solved this problem (or something very similar) using the blob record associated with the attachment.
Since the blob is an ActiveStorage::Blob, which is derived from ActiveRecord::Base, you can use the "dirty" methods on it to see if it has been changed. e.g.
def set_filename
if file.attached? && (file_blob.has_changes_to_save? || file_blob.saved_changes?)
self.file.blob.update(filename: "#{new_file_name()}.#{self.file.blob.content_type.split('/')[1]}")
end
end
Depending on where in the lifecycle set_filename is called from, there may only be a need to check one of has_changes_to_save? or saved_changes?. Since, in your example, you're calling this in before_save, you would only need file_blob.has_changes_to_save?
I have a class I've extended from ActiveRecord::Base...
class Profile < ActiveRecord::Base
and I collect the records from it like so...
records = #profile.all
which works fine, but it doesn't seem that I can successfully Update the attributes. I don't want to save them back to the database, just modify them before I export them as JSON. My question is, why can't I update these? I'm doing the following (converting date formats before exporting):
records.collect! { |record|
unless record.term_start_date.nil?
record.term_start_date = Date.parse(record.term_start_date.to_s).strftime('%Y,%m,%d')
end
unless record.term_end_date.nil?
record.term_end_date = Date.parse(record.term_end_date.to_s).strftime('%Y,%m,%d')
end
record
}
At first I had just been doing this in a do each loop, but tried collect! to see if it would fix things, but no difference. What am I missing?
P.S. - I tried this in irb on one record and got the same results.
I suggest a different way to solve the problem, that keeps the logic encapsulated in the class itself.
Override the as_json instance method in your Profile class.
def as_json(options={})
attrs = super(options)
unless attrs['term_start_date'].nil?
attrs['term_start_date'] = Date.parse(attrs['term_start_date'].to_s).strftime('%Y,%m,%d')
end
unless attrs['term_end_date'].nil?
attrs['term_end_date'] = Date.parse(attrs['term_end_date'].to_s).strftime('%Y,%m,%d')
end
attrs
end
Now when you render the records to json, they'll automatically use this logic to generate the intermediate hash. You also don't run the risk of accidentally saving the formatted dates to the database.
You can also set up your own custom option name in the case that you don't want the formatting logic.
This blog post explains in more detail.
Try to add record.save! before record.
Actually, by using collect!, you just modifying records array, but to save modified record to database you should use save or save! (which raises exception if saving failed) on every record.
This is probably one of the things that all new users find out about Rails sooner or later. I just realized that rails is updating all fields with the serialize keyword, without checking if anything really changed inside. In a way that is the sensible thing to do for the generic framework.
But is there a way to override this behavior? If I can keep track of whether the values in a serialized fields have changed or not, is there a way to prevent it from being pushed in the update statement? I tried using "update_attributes" and limiting the hash to the fields of interest, but rails still updates all the serialized fields.
Suggestions?
Here is a similar solution for Rails 3.1.3.
From: https://sites.google.com/site/wangsnotes/ruby/ror/z00---topics/fail-to-partial-update-with-serialized-data
Put the following code in config/initializers/
ActiveRecord::Base.class_eval do
class_attribute :no_serialize_update
self.no_serialize_update = false
end
ActiveRecord::AttributeMethods::Dirty.class_eval do
def update(*)
if partial_updates?
if self.no_serialize_update
super(changed)
else
super(changed | (attributes.keys & self.class.serialized_attributes.keys))
end
else
super
end
end
end
Yes, that was bugging me too. This is what I did for Rails 2.3.14 (or lower):
# config/initializers/nopupdateserialize.rb
module ActiveRecord
class Base
class_attribute :no_serialize_update
self.no_serialize_update = false
end
end
module ActiveRecord2
module Dirty
def self.included(receiver)
receiver.alias_method_chain :update, :dirty2
end
private
def update_with_dirty2
if partial_updates?
if self.no_serialize_update
update_without_dirty(changed)
else
update_without_dirty(changed | (attributes.keys & self.class.serialized_attributes.keys))
end
else
update_without_dirty
end
end
end
end
ActiveRecord::Base.send :include, ActiveRecord2::Dirty
Then in your controller use:
model_item.no_serialize_update = true
model_item.update_attributes(params[:model_item])
model_item.increment!(:hits)
model_item.update_attribute(:nonserializedfield => "update me")
etc.
Or define it in your model if you do not expect any changes to the serialized field once created (but update_attribute(:serialized_field => "update me" still works!)
class Model < ActiveRecord::Base
serialize :serialized_field
def no_serialize_update
true
end
end
I ran into this problem today and ended up hacking my own serializer together with a getter and setter. First I renamed the field to #{column}_raw and then used the following code in the model (for the media attribute in my case).
require 'json'
...
def media=(media)
self.media_raw = JSON.dump(media)
end
def media
JSON.parse(media_raw) if media_raw.present?
end
Now partial updates work great for me, and the field is only updated when the data is actually changed.
The problem with Joris' answer is that it hooks into the alias_method_chain chain, disabling all the chains done after (like update_with_callbacks which accounts for the problems of triggers not being called). I'll try to make a diagram to make it easier to understand.
You may start with a chain like this
update -> update_with_foo -> update_with_bar -> update_with_baz
Notice that update_without_foo points to update_with_bar and update_without_bar to update_with_baz
Since you can't directly modify update_with_bar per the inner workings of alias_method_chain you might try to hook into the chain by adding a new link (bar2) and calling update_without_bar, so:
alias_method_chain :update, :bar2
Unfortunately, this will get you the following chain:
update -> update_with_bar2 -> update_with_baz
So update_with_foo is gone!
So, knowing that alias_method_chain won't let you redefine _with methods my solution so far has been to redefine update_without_dirty and do the attribute selection there.
Not quite a solution but a good workaround in many cases for me was simply to move the serialized column(s) to an associated model - often this actually was a good fit semantically anyway.
There is also discussions in https://github.com/rails/rails/issues/8328.
I'm using this regex in my model to validate an URL submitted by the user. I don't want to force the user to type the http part, but would like to add it myself if it's not there.
validates :url, :format => { :with => /^((http|https):\/\/)?[a-z0-9]+([-.]{1}[a-z0-9]+).[a-z]{2,5}(:[0-9]{1,5})?(\/.)?$/ix, :message => " is not valid" }
Any idea how I could do that? I have very little experience with validation and regex..
Use a before filter to add it if it is not there:
before_validation :smart_add_url_protocol
protected
def smart_add_url_protocol
unless url[/\Ahttp:\/\//] || url[/\Ahttps:\/\//]
self.url = "http://#{url}"
end
end
Leave the validation you have in, that way if they make a typo they can correct the protocol.
Don't do this with a regex, use URI.parse to pull it apart and then see if there is a scheme on the URL:
u = URI.parse('/pancakes')
if(!u.scheme)
# prepend http:// and try again
elsif(%w{http https}.include?(u.scheme))
# you're okay
else
# you've been give some other kind of
# URL and might want to complain about it
end
Using the URI library for this also makes it easy to clean up any stray nonsense (such as userinfo) that someone might try to put into a URL.
The accepted answer is quite okay.
But if the field (url) is optional, it may raise an error such as undefined method + for nil class.
The following should resolve that:
def smart_add_url_protocol
if self.url && !url_protocol_present?
self.url = "http://#{self.url}"
end
end
def url_protocol_present?
self.url[/\Ahttp:\/\//] || self.url[/\Ahttps:\/\//]
end
Preface, justification and how it should be done
I hate it when people change model in a before_validation hook. Then when someday it happens that for some reason models need to be persisted with save(validate: false), then some filter that was suppose to be always run on assigned fields does not get run. Sure, having invalid data is usually something you want to avoid, but there would be no need for such option if it wasn't used. Another problem with it is that every time you ask from a model is it valid these modifications also take place. The fact that simply asking if a model is valid may result in the model getting modified is just unexpected, perhaps even unwanted. There for if I'd have to choose a hook I'd go for before_save hook. However, that won't do it for me since we provide preview views for our models and that would break the URIs in the preview view since the hook would never get called. There for, I decided it's best to separate the concept in to a module or concern and provide a nice way for one to apply a "monkey patch" ensuring that changing the fields value always runs through a filter that adds a default protocol if it is missing.
The module
#app/models/helpers/uri_field.rb
module Helpers::URIField
def ensure_valid_protocol_in_uri(field, default_protocol = "http", protocols_matcher="https?")
alias_method "original_#{field}=", "#{field}="
define_method "#{field}=" do |new_uri|
if "#{field}_changed?"
if new_uri.present? and not new_uri =~ /^#{protocols_matcher}:\/\//
new_uri = "#{default_protocol}://#{new_uri}"
end
self.send("original_#{field}=", new_uri)
end
end
end
end
In your model
extend Helpers::URIField
ensure_valid_protocol_in_uri :url
#Should you wish to default to https or support other protocols e.g. ftp, it is
#easy to extend this solution to cover those cases as well
#e.g. with something like this
#ensure_valid_protocol_in_uri :url, "https", "https?|ftp"
As a concern
If for some reason, you'd rather use the Rails Concern pattern it is easy to convert the above module to a concern module (it is used in an exactly similar way, except you use include Concerns::URIField:
#app/models/concerns/uri_field.rb
module Concerns::URIField
extend ActiveSupport::Concern
included do
def self.ensure_valid_protocol_in_uri(field, default_protocol = "http", protocols_matcher="https?")
alias_method "original_#{field}=", "#{field}="
define_method "#{field}=" do |new_uri|
if "#{field}_changed?"
if new_uri.present? and not new_uri =~ /^#{protocols_matcher}:\/\//
new_uri = "#{default_protocol}://#{new_uri}"
end
self.send("original_#{field}=", new_uri)
end
end
end
end
end
P.S. The above approaches were tested with Rails 3 and Mongoid 2.
P.P.S If you find this method redefinition and aliasing too magical you could opt not to override the method, but rather use the virtual field pattern, much like password (virtual, mass assignable) and encrypted_password (gets persisted, non mass assignable) and use a sanitize_url (virtual, mass assignable) and url (gets persisted, non mass assignable).
Based on mu's answer, here's the code I'm using in my model. This runs when :link is saved without the need for model filters. Super is required to call the default save method.
def link=(_link)
u=URI.parse(_link)
if (!u.scheme)
link = "http://" + _link
else
link = _link
end
super(link)
end
Using some of the aforementioned regexps, here is a handy method for overriding the default url on a model (If your ActiveRecord model has an 'url' column, for instance)
def url
_url = read_attribute(:url).try(:downcase)
if(_url.present?)
unless _url[/\Ahttp:\/\//] || _url[/\Ahttps:\/\//]
_url = "http://#{_url}"
end
end
_url
end
I had to do it for multiple columns on the same model.
before_validation :add_url_protocol
def add_url_protocol
[
:facebook_url, :instagram_url, :linkedin_url,
:tiktok_url, :youtube_url, :twitter_url, :twitch_url
].each do |url_method|
url = self.send(url_method)
if url.present? && !(%w{http https}.include?(URI.parse(url).scheme))
self.send("#{url_method.to_s}=", 'https://'.concat(url))
end
end
end
I wouldn't try to do that in the validation, since it's not really part of the validation.
Have the validation optionally check for it; if they screw it up it'll be a validation error, which is good.
Consider using a callback (after_create, after_validation, whatever) to prepend a protocol if there isn't one there already.
(I voted up the other answers; I think they're both better than mine. But here's another option :)