I am working on mongoDB with Rails. So using gem mongoid, Anyone know how to validate Hash fields in model?
We have to write custom validation methods
Here explained how we are writing custom validation methods
Looking for a solution, I came to a custom validator that appears good to me and it can be used generically.
private
def fix_content(input_hash, valid_fields)
temphash = {}
input_hash.each do |k,v|
k=k.to_sym
if valid_fields.has_key? k
case valid_fields[k]
when 'integer'
v=v.to_i
when 'boolean'
v=(v=='true' || v==true)
when 'float'
v=v.to_f
when 'array'
v = "#{v.class}"=="Array" ? v : []
else
v=v.to_s
end
temphash[k]=v
end
end
temphash
end
Let's suppose we have this field:
field :fieldname, type: Hash, default: {hfield1: 0, hfield2: [], hfield3: false}
Actually, it's not a validator, it's a callback. It works like this:
before_save :fieldname_fix_content
Under private:
def fieldname_fix_content
# we show the callback what fields will be processed. All others will be disposed of
self.fieldname = fix_content(self.fieldname, {:hfield1=> 'integer', :hfield2=>'array', :hfield3=>'boolean'})
end
Related
I have this class ProfitLoss which consists of these fields. I want to call this method zero_if_blank on before_validation as before_save is not working and what i want to do is assign 0 to those values which are blank and are of Integer when coming from the form. I searched a lot like the Mysql columns_hash but didn't find any method to give the type of each field in mongo.
class ProfitLoss < Generic
include Mongoid::Document
include Mongoid::Timestamps
include MongoMysqlRelations
field :fiscal_year, type: String
field :sales_excluding_other_incomes, type: Integer
field :other_incomes, type: Integer
field :closing_stock, type: Integer
before_validation :zero_if_blank
def zero_if_blank
pp self.columns_hash.each {|k,v| puts "#{k} => #{v.type}"}
end
end
this piece of code did the work :)
def zero_if_blank
self.fields.each do |k,v|
if v.type == Integer && self.attributes[k] == nil
self.__send__("#{k}=", 0)
end
end
end
I added the following filter in ActiveAdmin.
filter :roles, as: :select, collection Model::ROLES, multiple: true
but when i choose the filter value to search the roles. it gives me following error
PG::InvalidTextRepresentation: ERROR: malformed array literal: "teacher"LINE 1: ...ted" = $1 AND roles" IN('teacher
DETAIL: Array value must start with "{" or dimension information. ^
Any idea ? How we can search/Filter ARRAY field using AA filters? I'm using Rails 4.2.4,
ruby 2.2.2p95
I came up to a solution slightly different (and inspired by) this one over here: https://stackoverflow.com/a/45728004/1170086
Mine involves some changes (and prevent breaking contains operator in other cases). So, you're going to basically create two initializer files:
This one is for Arel, in order to support #> operator (array's contain operator in PG) for a given table column.
# config/initializers/arel.rb
module Arel
class Nodes::ContainsArray < Arel::Nodes::Binary
def operator
:"#>"
end
end
class Visitors::PostgreSQL
private
def visit_Arel_Nodes_ContainsArray(o, collector)
infix_value o, collector, ' #> '
end
end
module Predications
def contains(other)
Nodes::ContainsArray.new self, Nodes.build_quoted(other, self)
end
end
end
The other file aims to create a new Ransack predicate but I also decided to support the :array type (that's not natively supported in Ransack in terms of predicates).
# config/initializers/ransack.rb
module Ransack
module Nodes
class Value < Node
alias_method :original_cast, :cast
def cast(type)
return Array(value) if type == :array
original_cast(type)
end
end
end
end
Ransack.configure do |config|
config.add_predicate 'contains_array',
arel_predicate: 'contains',
formatter: proc { |v| "{#{v.join(',')}}" },
validator: proc { |v| v.present? },
type: :array
end
And in other to use it. All you need to do is:
User.ransack(roles_contains_array: %i[admin manager])
Or as a filter in ActiveAdmin (which is my case):
ActiveAdmin.register User do
# ...
filter :roles_contains_array, as: :select, collection: User.roles_for_select
# ...
end
I hope it works for you as it worked for me. ;)
You can set up a custom ransacker method to first collect the ids you want returned using a regular postgres search, and then return the results based on those ids:
class User < ApplicationRecord
ransacker :roles,
formatter: proc { |str|
data = where("? = ANY (roles)", str).map(&:id)
data.present? ? data : nil
} do |parent|
parent.table[:id]
end
end
If your filter is a select drop-down, then this should work fine. If you have a free-form text box, then make sure to use the "in" predicate:
filter :roles_in, as: :string
leandroico solutions works well.
But if you add the predicate with this formatter
formatter: proc { |v| "{#{v.join(', ')}}" }, (note the space after the comma)
Then you could use the multiple: true keyword in the filter input and filter by more than one value:
filter :roles_contains_array, as: :select, multiple: true, collection: User.roles_for_select
I used the answer from #leandroico to come up with the below wiki-type approach to doing this.
How to Create Custom SQL Searches for ActiveAdmin (using Arel and Ransack)
In ActiveAdmin, filters are declared in app/admin/model.rb like:
ActiveAdmin.register Model do
filter 'column_name', label: 'column_name', as: :string
end
That will make a searchbox available on the front-end with options to choose between
contains
equals
starts with
ends with
You can even do something like...
filter 'column_name_contains', label: 'column_name', as: :string
...to only have a contains type search available on the front-end.
You can also (after defining some custom methods elsewhere) specify other, non-built-in search methods, like:
filter 'column_name_custom_contains', label: 'column_name', as: :string
The rest of this doc will be about how to define this custom search method, custom_contains
Within config/initializers/arel.rb, define the following:
module Arel
# this example of custom_contains will cast the SQL column as ::text and then do a wildcard-wrapped ILIKE
class Nodes::CustomContains < Arel::Nodes::Binary
def operator
'::text ILIKE'.to_sym
end
end
class Visitors::PostgreSQL
private
def visit_Arel_Nodes_CustomContains(o, collector)
infix_value o, collector, '::text ILIKE '
end
end
module Predications
def custom_contains(column_value)
column_value = self.relation.engine.column_types[self.name.to_s].type_cast_for_database(column_value)
column_value = "%#{self.relation.engine.send(:sanitize_sql_like, column_value)}%" # wrap escaped value with % wildcard
column_value = Nodes.build_quoted(column_value, self)
Nodes::CustomContains.new(self, column_value)
end
end
end
module ActiveRecord::QueryMethods
def custom_contains(predicates)
return none if predicates.length == 0
predicates.map{ |column_name, column_value|
column_value = table.engine.column_types[column_name.to_s].type_cast_for_database(column_value)
column_value = "%#{table.engine.send(:sanitize_sql_like, column_value)}%" # wrap escaped value with % wildcard
column_value = Arel::Nodes.build_quoted(column_value)
where Arel::Nodes::CustomContains.new(table[column_name], column_value)
}.inject(:merge)
end
end
module ActiveRecord::Querying
delegate :custom_contains, :to => :all
end
Within config/initializers/ransack.rb, define the following:
Ransack.configure do |config|
config.add_predicate(
'custom_contains',
arel_predicate: 'custom_contains',
formatter: proc { |v| v.to_s },
validator: proc { |v| v.present? },
type: :string
)
end
The above has accomplished a couple of things:
1) You can use the custom_contains method that was delegate'd to all ActiveRecord models:
puts Model.custom_contains(column_name: 'search for me').to_sql
2) You can use Ransack to search against the Arel predicates that were defined:
puts Model.ransack(column_name_custom_contains: 'search for me').result.to_sql
However, in order to do the below in ActiveAdmin...
filter 'column_name_custom_contains', label: 'column_name', as: :string
...we must add a scope to Model so that there is a method, column_name_custom_contains, on Model
scope_name = "#{column_name}_custom_contains".to_sym
unless Model.methods.include?(scope_name)
Model.scope(
scope_name,
->(value) {
Model.custom_contains({column_name.to_sym => value})
}
)
end
Voila!
I've been looking all over the place and I'm wondering if I'm doing something wrong. And just to double check, I'll ask you guys!
So I'm receiving params in a Rails controller. One key, value pair is :status => true/false. However, I find that when I try to post status as a string like
:status => "THIS IS NOT A BOOLEAN"
and create my object in my controller, the :status attribute of my object becomes false.
Therefore, is there any clean way in rails to validate that my :status corresponds to a boolean?
Thanks!
This very strange method will to the trick
def is_boolean?(item)
!!item == item
end
params[:status] = 'some string'
is_boolean?(params[:status])
# => false
params[:status] = true
is_boolean?(params[:status])
# => true
A slightly more intuitive version would be
def is_boolean?(item)
item == false || item == true
end
Validation
The Rails way to do it is to validate in the model (from the docs):
#app/models/model.rb
Class Model < ActiveRecord::Base
validates :status, inclusion: { in: [true, false] }, message: "True / False Required!"
end
--
MVC
The reason for this is twofold:
DRY
MVC
If you want to keep your application DRY, you need to make sure you have only one reference to a validation throughout. Known as the "Single Source Of Truth", it means if you try and populate the model with other controllers / methods, you'll still invoke the same validation
Secondly, you need to consider the MVC (Model-View-Controller) pattern. MVC is a core aspect of Rails, and means you have to use your controller to collate data only - pulling & compiling data in the model. This is also true for validations -- always make sure you keep your validations with the data (IE in the model)
The above #Iceman solution is good if you are only doing it once place but you keep doing/repeating it in other places i suggest you to create to_bool method. i.e
class String
def to_bool
return true if self == true || self =~ (/(true|t|yes|y|1)$/i)
return false if self == false || self.blank? || self =~ (/(false|f|no|n|0)$/i)
raise ArgumentError.new("invalid value for Boolean: \"#{self}\"")
end
end
and put this method in intializer or in library. And, you can simply do this
Mymodel.new(status: params[:status].to_s.to_bool)
we are doing to_s just because to convert nil to '' incase the status key isn't in params .
So I know that hstore only stores in string either key and especially value. I'm looking for a way to set their datatypes. Is there a way in rails or hstore to do this?
So far what I did was to override the getters and depending on the datatype I want. This is what I have so far:
class ModelWithHstore < ActiveRecord::Base
store_accessor :properties, :some_boolean_field, :some_integer_field, :some_datetime_field
validate :validate_range
def some_boolean_field
return if self[:properties].nil? || self[:properties][__method__.to_s].nil?
if [true, 'true', '1'].include? self[:properties][__method__.to_s]
return true
elsif [false, 'false', '0'].include? self[:properties][__method__.to_s]
return false
end
self[:properties][__method__.to_s]
end
def some_integer_field
return if self[:properties].nil? || self[:properties][__method__.to_s].nil?
self[:properties][__method__.to_s].to_i
end
def some_datetime_field
return if self[:properties].nil? || self[:properties][__method__.to_s].nil?
DateTime.strptime(self[:properties][__method__.to_s].to_s, '%F %T')
end
private
def validate_range
errors.add(:some_integer_field, "value out of range") if !some_integer_field.between?(10, 90)
end
end
And since they are getters. I think they are being used too in validators and some other places. But I really am not sure if something like this already exists or is there a better way to implement this.
Cheers!
You can try hstore_accessor gem
I'm trying to build an application on RoR that uses MongoDB via Mongoid for the main objects but has a like and dislike process using Redis via Opinions https://github.com/leehambley/opinions/.
It sort of works but when I run the methods on my objects I just get an error "undefined method `like_by'" where I think the methods are supposed to be autogenerated.
My model looks like:
class Punchline
include Mongoid::Document
include Opinions::Pollable
opinions :like, :dislike
field :key, type: String
field :text, type: String
field :won, type: Boolean
field :created, type: Time, default: ->{ Time.now }
field :score, type: Integer
index({ key: 1 }, { unique: true, name: "key_index" })
belongs_to :user
embedded_in :joke
end
and I run:
user = User.find(session[:userid])
#joke.punchlines.sample.like_by(user);
But it fails with the undefined method error :(
Do I need to initialize Opinions somewhere beyond
/config/initializers/opinions.rb
Opinions.backend = Opinions::RedisBackend.new
Redis.new(:host => 'localhost', :port => 6379)
So, it turns out that Opinions doesn't really work. Why is a bit beyond my two weeks with Rails :)
Anyway, it turns out that this is really easy to do by hand anyway especially as I only had a like and dislike to handle.
I used a Redis sorted set which allows a unique key - value pair with a score. I used a score of +1 or -1 to denote like or dislike and then encoded the key to represent the liked object and the value to be the user id.
This looked like:
def like(user)
$redis.zadd('joke:'+self.key+':likes', 1, user._id)
end
def dislike(user)
$redis.zadd('joke:'+self.key+':likes', -1, user._id)
end
def numLikes
res = $redis.zrangebyscore('joke:'+self.key+':likes',1,1);
return res.count
end
def numDislikes
res = $redis.zrangebyscore('joke:'+self.key+':likes',-1,-1);
return res.count
end
def likedBy(user)
res = $redis.zscore('joke:'+self.key+':likes',user._id)
return (res == 1)
end
def dislikedBy(user)
res = $redis.zscore('joke:'+self.key+':likes',user._id)
return (res == -1)
end