setting activerecord attribute based on virtual attributes - ruby-on-rails

i have an attribute called dimensions which i want to set based on my width, height, and depth attributes.
for example, i want to do ShippingProfile.find(1).width = 4, and have it save to dimensions as {:width => 4, :height => 0, :depth => 0}`
is this possible?
class ShippingProfile < ActiveRecord::Base
after_initialize :set_default_dimensions
serialize :dimensions, Hash
attr_accessor :width, :height, :depth
attr_accessible :width, :height, :depth, :dimensions
private
def set_default_dimensions
self.dimensions ||= {:width => 0, :height => 0, :depth => 0}
end
end

Very much so, all you need to do is use a callback to set the value of self.dimensions:
class ShippingProfile < ActiveRecord::Base
after_initialize :set_default_dimensions
after_validation :set_dimensions
serialize :dimensions, Hash
attr_accessor :width, :height, :depth
attr_accessible :width, :height, :depth, :dimensions
private
def set_default_dimensions
self.dimensions ||= {:width => 0, :height => 0, :depth => 0}
end
def set_dimensions
self.dimensions = {
:width => self.width || self.dimensions[:width],
:height => self.height || self.dimensions[:height],
:depth => self.depth || self.dimensions[:depth],
}
end
end
You need to use self.foo || self.dimensions[:foo] to ensure that you preserve any existing values already set in the hash. Why? Your dimension attributes (I'm assuming) aren't being persisted in the database - you're using attr_accessor, rather than setting them up as fields in your table.
As an aside, I think you're going about your model design the wrong way. By storing the dimensions as a hash in the database, not only do you lose the ability to query based on those attributes, but you add a level of fragility you don't need.
If you are storing your individual dimension attributes as separate fields, then you're introducing redundancy and complexity. You would be better served by having the three attributes as fields in your database (if you aren't already), then generating the dimensions hash on the fly when it's needed:
class ShippingProfile < ActiveRecord::Base
def dimensions
{ :width => self.width, :height => self.height, :depth => self.depth }
end
end
This way, you retain the functionality and gain some flexibility.

ShippingProfile.find(1).dimensions[:width] = 4

You can use a class in serialize, so
class ShippingProfile < ActiveRecord::Base
serialize :dimensions, Dimensions
end
class Dimensions
attr_accessor :width, :height,:depth
def initialize
#width = 0
#height = 0
#depth = 0
end
def volume
width * height * depth
end
end
Now you can do ShippingProfile.dimensions.width = 1 and later ShippingProfile.dimension.volume etc..
A model would be a richer representation than a Hash

Related

how to use an instance of a model in another serializer

I'm stuck here and couldn't find solution to proceed my work,
I have 3 models: plans, days, and meals.
This is my Plan Controller I've managed to get the correct answer in the controller, I want it nested and inside the serializer because I'm using URL helper to retrieve my images URLs, is there a possible way to use the #plan.id inside the DaySerializer?
def meals
#plan = Plan.find(params[:id])
#days = #plan.days
#meals = Meal.where("plan_id = ? ", #plan.id)
render :json => { :plan => #plan, :days => #days,
:meals => #meals }
end
This is my Plan model
class Plan < ApplicationRecord
has_many :days
has_one_attached :image, dependent: :destroy
end
This is my Day model
class Day < ApplicationRecord
has_many :meals
has_many :plans
end
This is my Meal model
class Meal < ApplicationRecord
belongs_to :plan
belongs_to :day
has_one_attached :image, dependent: :destroy
end
I want to show all meals for a specific Plan, to do that I need to use a variable inside the daySerializer but I couldn't find how to do it.
This is my planSerializer
class PlanSerializer < ActiveModel::Serializer
attributes :id, :name, :monthly_price, :plan_days
def plan_days
object.days.map do |day|
DaySerializer.new(day, scope: scope, root: false, event: object)
end
end
end
and this is my DaySerializer which I need to use the instance of the plan inside
class DaySerializer < ActiveModel::Serializer
attributes :number, :plan_meals
def plan_meals
#how to be able to use this line in Serilizer? !important
#plan = Plan.find(params[:id])
object.meals.map do |meal|
if meal.plan_id == #plan.id
MealSerializer.new(meal, scope: scope, root: false, event: object)
end
end
end
end
target reason response :
{
id: 8,
name: "Plan1",
monthly_price: 88,
plan_days: [
{
number: 5,
plan_meals: [],
},
{
number: 4,
plan_meals: [],
},
{
number: 3,
plan_meals: [],
},
{
number: 2,
plan_meals: [],
},
{
number: 1,
plan_meals: [
{
id: 11,
name: "test meal",
calories: 32,
protein: 32,
fat: 32,
carbohydrates: 32,
plan_id: 8,
},
],
},
],
}
currently it's showing all meals that belongs to each day,
not only the meals with the plan_id = Plan.find(params[:id])
In general I think you could use something like this should work.
ActiveModel::Serializer::CollectionSerializer.new. It actually by itself allows you to pass additional information to your serializer. It does the same as your current code just you are able to explicitly pass new data.
Controller:
def meals
#plan = Plan.find(params[:id])
#days = #plan.days
#meals = Meal.where("plan_id = ? ", #plan.id)
render :json => {
:plan => #plan,
:days => ActiveModel::Serializer::CollectionSerializer.new(#days, serializer: DaySerializer, plan_id: #plan.id),
:meals => #meals
}
end
And then in DaySerializer:
class DaySerializer < ActiveModel::Serializer
attributes :number, :plan_meals
def plan_meals
object.meals.map do |meal|
if meal.plan_id == instance_options[:plan_id]
MealSerializer.new(meal, scope: scope, root: false, event: object)
end
end
end
end
So in short ActiveModel::Serializer::CollectionSerializer.new in controller and instance_options in serializer to access passed additional parameters.
UPDATED:
How about add meal serializer?
class MealSerializer < ActiveModel::Serializer
attributes :id, :name, :calories, :protein, :fat, # etc
end
class DaySerializer < ActiveModel::Serializer
attributes :number
has_many :meals, serializer: MealSerializer
end
ORIGINAL:
class PlanSerializer < ActiveModel::Serializer
attributes :id, :name, :monthly_price, :plan_days
has_many :plan_days, serializer: DaySerializer
end
something like this.

Rails - model attribute as a variable in function

I think it is really basic question, but I cannot find solution.
I have simple help method:
def getProperNutrientValue(meal)
result = 0
if meal.ingredients.empty?
result
else
meal.ingredients.each do |i|
result += (i.quantity * #meal.products.find(i.product_id).calorific) / 100
end
result
end
end
Where "calorific" is an attribute in Product model.
class Product < ActiveRecord::Base
has_many :ingredients
has_many :meals, :through => :ingredients
validates :calorific, :numericality => true, :allow_nil => true
validates :water, :numericality => true, :allow_nil => true
I want to DRY this function, and set attribute as a variable. Then I will be can use this function for example for water attribute.
So I want to achieve something like that:
def getProperNutrientValue(meal, attribute)
result = 0
if meal.ingredients.empty?
result
else
meal.ingredients.each do |i|
result += (i.quantity * #meal.products.find(i.product_id).attribute) / 100
end
result
end
end
But of course it doesn't work... How can I fix it?
You can use send(method_name) method. I don't understand the logic behind using #meal variable. Either way there are some options to improve your code, for example:
def getProperNutrientValue(meal, attribute)
result = 0
meal.ingredients.each do |i|
result += (i.quantity * #meal.products.find(i.product_id).send(attribute).to_i) / 100
end
result
end
getProperNutrientValue(meal, :calorific)

Raising errors in attribute accessor overrides?

I am overriding an attribute accessor in ActiveRecord to convert a string in the format "hh:mm:ss" into seconds. Here is my code:
class Call < ActiveRecord::Base
attr_accessible :duration
def duration=(val)
begin
result = val.to_s.split(/:/)
.map { |t| Integer(t) }
.reverse
.zip([60**0, 60**1, 60**2])
.map { |i,j| i*j }
.inject(:+)
rescue ArgumentError
#TODO: How can I correctly report this error?
errors.add(:duration, "Duration #{val} is not valid.")
end
write_attribute(:duration, result)
end
validates :duration, :presence => true,
:numericality => { :greater_than_or_equal_to => 0 }
validate :duration_string_valid
def duration_string_valid
if !duration.is_valid? and duration_before_type_cast
errors.add(:duration, "Duration #{duration_before_type_cast} is not valid.")
end
end
end
I am trying to meaningfully report on this error during validation. The first two ideas that I have had are included in the code sample.
Adding to errors inside of the accessor override - works but I am not certain if it is a nice solution.
Using the validation method duration_string_valid. Check if the other validations failed and report on duration_before_type_cast. In this scenario duration.is_valid? is not a valid method and I am not certain how I can check that duration has passed the other validations.
I could set a instance variable inside of duration=(val) and report on it inside duration_string_valid.
I would love some feedback as to whether this is a good way to go about this operation, and how I could improve the error reporting.
First, clean up your code. Move string to duration converter to the service layer. Inside the lib/ directory create StringToDurationConverter:
# lib/string_to_duration_converter.rb
class StringToDurationConverter
class << self
def convert(value)
value.to_s.split(/:/)
.map { |t| Integer(t) }
.reverse
.zip([60**0, 60**1, 60**2])
.map { |i,j| i*j }
.inject(:+)
end
end
end
Second, add custom DurationValidator validator
# lib/duration_validator.rb
class DurationValidator < ActiveModel::EachValidator
# implement the method called during validation
def validate_each(record, attribute, value)
begin
StringToDurationConverter.convert(value)
resque ArgumentError
record.errors[attribute] << 'is not valid.'
end
end
end
And your model will be looking something like this:
class Call < ActiveRecord::Base
attr_accessible :duration
validates :duration, :presence => true,
:numericality => { :greater_than_or_equal_to => 0 },
:duration => true
def duration=(value)
result = StringToDurationConverter.convert(value)
write_attribute(:duration, result)
end
end

Paperclip image dimensions custom validator

This is my Image model, in which I've implemented a method for validating the attachment's dimensions:
class Image < ActiveRecord::Base
attr_accessible :file
belongs_to :imageable, polymorphic: true
has_attached_file :file,
styles: { thumb: '220x175#', thumb_big: '460x311#' }
validates_attachment :file,
presence: true,
size: { in: 0..600.kilobytes },
content_type: { content_type: 'image/jpeg' }
validate :file_dimensions
private
def file_dimensions(width = 680, height = 540)
dimensions = Paperclip::Geometry.from_file(file.queued_for_write[:original].path)
unless dimensions.width == width && dimensions.height == height
errors.add :file, "Width must be #{width}px and height must be #{height}px"
end
end
end
This works fine, but it's not reusable since the method takes fixed values for width & height. I want to transform this to a Custom Validator, so I can use it in other models too. I've read the guides about this, I know it'll be something like this in app/models/dimensions_validator.rb:
class DimensionsValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
dimensions = Paperclip::Geometry.from_file(record.queued_for_write[:original].path)
unless dimensions.width == 680 && dimensions.height == 540
record.errors[attribute] << "Width must be #{width}px and height must be #{height}px"
end
end
end
but I know I'm missing something cause this code doesn't work. The thing is that I want to call the validation like this in my model:
validates :attachment, dimensions: { width: 300, height: 200}.
Any idea on how this validator should be implemented?
Put this in app/validators/dimensions_validator.rb:
class DimensionsValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# I'm not sure about this:
dimensions = Paperclip::Geometry.from_file(value.queued_for_write[:original].path)
# But this is what you need to know:
width = options[:width]
height = options[:height]
record.errors[attribute] << "Width must be #{width}px" unless dimensions.width == width
record.errors[attribute] << "Height must be #{height}px" unless dimensions.height == height
end
end
Then, in the model:
validates :file, :dimensions => { :width => 300, :height => 300 }
There is a gem that does this called paperclip-dimension-validator.

Validate image size in carrierwave uploader

All uploads should be at least 150x150 pixels. How to validate it with Carrierwave?
Why not to use MiniMagick? Modified DelPiero's answer:
validate :validate_minimum_image_size
def validate_minimum_image_size
image = MiniMagick::Image.open(picture.path)
unless image[:width] > 400 && image[:height] > 400
errors.add :image, "should be 400x400px minimum!"
end
end
I made a slightly more complete validator based on #skalee's answer
class ImageSizeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value.blank?
image = MiniMagick::Image.open(value.path)
checks = [
{ :option => :width,
:field => :width,
:function => :'==',
:message =>"Image width must be %d px."},
{ :option => :height,
:field => :height,
:function => :'==',
:message =>"Image height must be %d px."},
{ :option => :max_width,
:field => :width,
:function => :'<=',
:message =>"Image width must be at most %d px."},
{ :option => :max_height,
:field => :height,
:function => :'<=',
:message =>"Image height must be at most %d px."},
{ :option => :min_width,
:field => :width,
:function => :'>=',
:message =>"Image width must be at least %d px."},
{ :option => :min_height,
:field => :height,
:function => :'>=',
:message =>"Image height must be at least %d px."},
]
checks.each do |p|
if options.has_key?(p[:option]) and
!image[p[:field]].send(p[:function], options[p[:option]])
record.errors[attribute] << p[:message] % options[p[:option]]
end
end
end
end
end
Use it like validates :image, :image_size => {:min_width=>400, :min_height => 400}.
It surprised me just how difficult it was to search around for a clear-cut way to validate image width & height with CarrierWave. #Kir's solution above is right on, but I wanted to go a step further in explaining what he did, and the minor changes I made.
If you look at his gist https://gist.github.com/1239078, the answer lies in the before :cache callback he has in the Uploader class. The magic line is
model.avatar_upload_width, model.avatar_upload_height = `identify -format "%wx %h" #{new_file.path}`.split(/x/).map { |dim| dim.to_i }
in his case, avatar_upload_width & avatar_upload_height are attributes of his User model. I didn't want to have to store width&height in the database, so in my model I said:
attr_accessor :image_width, :image_height
Remember, you can use attr_accessor for attributes you want to have on hand when messing with a record, but just don't want to persist them to the db. So my magic line actually turned into
model.image_width, model.image_height = `identify -format "%wx %h" #{new_file.path}`.split(/x/).map { |dim| dim.to_i }
So now I have the width & height of my image stored in the model object. The last step is to write a custom validation for the dimensions, so in your model you need something like
validate :validate_minimum_image_size
And then below it define your custom validation method, same as in the gist
# custom validation for image width & height minimum dimensions
def validate_minimum_image_size
if self.image_width < 400 && self.image_height < 400
errors.add :image, "should be 400x400px minimum!"
end
end
I just made a custom validator that aims to be more Rails 4+ syntax friendly.
I took ideas from the others responses on this thread.
Here is the gist: https://gist.github.com/lou/2881f1aa183078687f1e
And you can use it like this:
validates :image, image_size: { width: { min: 1024 }, height: { in: 200..500 } }
In this particular case it should be:
validates :image, image_size: { width: 150, height: 150 }

Resources