Testing model with Carrierwave uploader - ruby-on-rails

I have a pretty simple model:
class SocialGroup < ActiveRecord::Base
validates :name, presence: true
validates :file, presence: true
mount_uploader :file, SocialGroupFileUploader
end
And the question is: Should I test (with rspec) the model successful save with valid file type provided (my white list of file extensions includes only csv)? Or should I test the file uploader in isolation? If answer on first question is Yes, how test shoul look like?

If your uploader is simple I think it's fairly safe to assume that CarrierWave's developers have done the testing there, the test suite is fairly comprehensive (but it's very much a matter of opinion, some people will and some people won't).
I'd concentrate on making sure that the controller is tested either in rspec or the cucumber specs. There are a couple of examples of people doing this in a google search.

Related

Understanding ruby on rails documentation

I am currently creating a rails application.
I am writing a model and wants to add some validation.
from the documentation I see that doing something like this works
class Person < ApplicationRecord
validates :terms_of_service, acceptance: { message: 'must be abided' }
end
I am trying to understand the validates method here.
At a more general level I would like to understand rails documentation better.
My understanding is that validates is a class method of ApplicationRecord::Base.
It is possible to reuse it with various parameters and options.
The best doc I found is this.
I do not understand where I can find a list of all validates options and parameters.
In this case,
what is acceptance?
where can I find a description of it in the doc?
where can I find a list of all other possible validates parameters?
Any tips on how to understand ruby on rails documentation better would be appreciated.
validates :terms_of_service, acceptance: true
acceptance maps to AcceptanceValidator which is a default rails validator:
https://github.com/rails/rails/blob/main/activemodel/lib/active_model/validations/acceptance.rb
All of the default validators are listed in the example:
https://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html#method-i-validates
absence
acceptance
confirmation
exclusion
format
inclusion
length
numericality
presence
and additional validators that are added by ActiveRecord:
associated
uniqueness
https://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html
Available options for each validaror are documented in the helper methods here:
https://api.rubyonrails.org/classes/ActiveModel/Validations/HelperMethods.html
validates :terms_of_service, acceptance: true
# is the same as using a helper method
validates_acceptance_of :terms_of_service
# and both map to rails default `AcceptanceValidator`
You can have custom validators as well:
validates :terms_of_service, terms: true # maps to `TermsValidator`
# because there is no TermsValidator class in rails, you have to define it
# class TermsValidator
# # TODO: see docs for examples of custom validators
# end
https://guides.rubyonrails.org/active_record_validations.html#performing-custom-validations

It is possible to use the new ActiveRecord::Attributes API in a PORO?

I usually include ActiveModel::Model into some PORO (for example for a FormObject::SignUp). I've read about the new Rails 5 ActiveRecord::Attribute API, and I thought I will be able to use it for simpler casting, but not luck.
For example, given
class FormObject::SignUp
include ActiveRecord::Model
include ActiveRecord::Attributes
attribute :birthday, :date
validates :birthday, presence: true
end
I got an NameError: undefined local variable or method `reload_schema_from_cache' for FormObjects::SignUp:Class exception when I try to instantiate it.
It is not expected to be used standalone? Thanks
Rails >=5.2
This is now possible in Rails 5.2.
As of Rails 5.2 (#30985), ActiveModel::Atrributes is now available to use in POROs (at least POROs that include ActiveModel::Model)...
Reference: https://github.com/rails/rails/issues/28020#issuecomment-456616836
Documentation: https://www.rubydoc.info/gems/activemodel/ActiveModel/Attributes#attribute_names-instance_method
Rails <5.2
Utilizing the gem ActiveModelAttributesis the only easy way I've been able to find to do this. Depending on your use case, it will probably make sense to use that gem or take a different approach.
Here is the gem: https://github.com/Azdaroth/active_model_attributes
Side note: I got feedback that link-only answers can disappear. If this link disappears, then this answer does indeed become invalid since that will likely mean the gem dosen't exist anymore.
ActiveModel::Attributes is available as of Rails 5.2 (PR).
Try this:
class FormObject::SignUp
include ActiveModel::Model
include ActiveModel::Attributes
attribute :birthday, :date
validates :birthday, presence: true
end
If you look at the documentation it seems that this module cannot be used stand-alone, as it makes a lot of assumptions (mostly about a schema-backed model).
Even if you try with the define_attribute method, you still need to provide implementation for other class methods, like attribute_types.
What's wrong with using ActiveModel::Model in Rails 5?
class Poro
include ActiveModel::Model
attr_accessor :foo
end
I've submitted an issue and they replied that it's not supported currently, but it will be in the future.
Link: https://github.com/rails/rails/issues/28020#event-963668657

Server side validation of file size

I am trying to find a way to limit the size of files that users can upload, and it seems to be possible with javascript, but what bothers me is that what happens if a user just turns javascript off and upload like 1GB file to the server, and I did find a way to validate it on server side but it takes place once the file has been uploaded, which isnt an option. So I am trying to find the right way to do it but the best thing I came up with is to create a form for file uploading with javascript and make validations on client side but I guess there is still a way around it...
I am using RoR and CarrierWave if that matters ...
EDIT
1)Everything I can find on this page is to use size validation which happens after the file has been uploaded
2) I use that validation by carrierwave but again, it takes place once the file has been uploaded, it doesnt make sense to wait an hour till some huge file gets uploaded just to know its too big
You can use a Rails custom validator to verify your attachment meets specific file size requirements.
Grab a copy of the validator from https://gist.github.com/1009861 and save it to your lib/ folder as file_size_validator.rb. Add the error translations to config/locales/en.yml or wherever is appropriate for your setup. Then do this in your parent model:
# app/models/brand.rb
require 'file_size_validator'
class Brand < ActiveRecord::Base
mount_uploader :logo, BrandLogoUploader
validates :logo,
:presence => true,
:file_size => {
:maximum => 0.5.megabytes.to_i
}
end
Like validates_length_of, validates_file_size accepts :maximum, :minimum, :in [range], and :is options.
Another solution
A custom validator could also be used like this.
app/models/user.rb
class User< ActiveRecord::Base
attr_accessible :product_upload_limit
has_many :products
end
app/models/brand.rb
class Product < ActiveRecord::Base
mount_uploader :file, FileUploader
belongs_to :user
validate :file_size
def file_size
if file.file.size.to_f/(1000*1000) > user.product_upload_limit.to_f
errors.add(:file, "You cannot upload a file greater than #{upload_limit.to_f}MB")
end
end
end
Here, the upload limit varies from user to user & is saved in the user model.
The problem was solved by nginx, which works like a buffer so if somebody is trying to upload a 1GB file, it will go to your buffer first so it won't block your unicorn instance until it's completely uploaded, so that's one good thing. The other is that you can set the max content-length parameter in your nginx config file, so if you set to 5 Megabytes and somebody is trying to upload 1 GB file, it'll be denied. But again, I am not sure how it works, if it just checks the http header, then I have a feeling that somebody can simply tamper with this value and fool your nginx.

How to make localized Paperclip attachments with Globalize3?

I have a project using Paperclip gem for attachments and Globalize3 for attribute translation. Records need to have a different attachment for each locale.
I though about moving Paperclip attributes to translation table, and that might work, but I don't think that would work when Paperclip needs to delete attachments.
What's the best way to achieve something like that?
UPDATE: to be clear, I want this because my client wants to upload different images for each locale.
Unfortunately I didn't find a way to do this using Globalize3. In theory, I could have added a separate model for image and add image_id to list of translated columns (to have something like MainModel -> Translation -> Image), but it seems that Globalize has some migration issues with non-string columns.
Instead of using Globalize3, I did this with a separate Image model with locale attribute and main model which accepts nested attributes for it. Something along the lines of:
class MainModel < ActiveRecord::Base
has_many :main_model_images
accepts_nested_attributes_for :main_model_images
# return image for locale or any other as a fallback
def localized_image(locale)
promo_box_images.where(:locale => locale).first || promo_box_images.first
end
end
class MainModelImage < ActiveRecord::Base
belongs_to :main_model
has_attached_file :image
validates :locale,
:presence => true,
:uniqueness => { :scope => :main_model_id }
end
Tricky part was getting form to accept nested attributes only for one image, instead of all images in has_many relation.
=f.fields_for :main_model_images, #main_model.image_for_locale(I18n.locale) do |f_image|
=f_image.hidden_field :locale
=f_image.label :image
You could also try the paperclip-globalize3 gem, it should handle the case you describe. https://github.com/emjot/paperclip-globalize3
Ok since you asked me to share my solution to this problem even though I am using Carrierwave as a library for uploading here is it:
Ok so I would have a model setup like this:
class MyModel < ActiveRecord::Base
# ...
translates :attr_one, :attr_two, :uploaded_file
Now what I need for CarrierWave to work is place to attach the uploader to and that can be done on the Translation model
Translation.mount_uploader :uploaded_file, FileUploader
end
Now for your question about deleting, I think though I haven't needed to do it but it should work as the README says it should but on the translation model. https://github.com/jnicklas/carrierwave#removing-uploaded-files
MyModel.first.translation.remove_uploaded_file!
I haven't taken a look at paperclip for a good 2 years and if this is not applicable knowledge I suggest you try out carrierwave.

Using RSpec to test active record scope that uses the includes method

Given the following two classes:
class Location < ActiveRecord::Base
belongs_to :holiday_schedule
validates :name, :presence => true, :uniqueness => {:case_sensitive => false}
scope :with_holiday_schedule, includes(:holiday_schedule)
end
class HolidaySchedule < ActiveRecord::Base
validates_presence_of :name
has_many :locations
end
How would you spec the with_holiday_schedule scope to ensure that accessing location.holiday_schedule.name in a loop will not cause the N+1 Query problem?
After positing to the RSpec users mailing list and reading more about speccing in general, I ultimately came to the realization that this isn't worth a unit test. The :includes directive is well tested in rails and the overhead of testing that simple line is higher than the risk associated with it failing or being removed by another developer - at least in my case.
What I really care about is performance of the application. Speccing performance would be a lot more productive than jumping through hoops to unit test this line.
Have a look at Counting the number of queries performed.
This should work perfectly in your solution.
They've done this:
ActiveRecord::Base.count_queries do
Ticket.first
end
You can use it this way in your spec:
queries = ActiveRecord::Base.count_queries do
location.with_holiday_schedule.holiday_schedule.name
end
queries.should_be == 1
I hope this will work.

Resources