How to get the string version of an enum? - ruby-on-rails

I have a model enum like:
class User < AR::Base
enum status [:pending, :member, :banned]
end
Now I want to output the string value of 'banned' but it outputs the int value:
User.statuses[:banned]

I'm not sure that's how they work. Looking at some docs:
http://api.rubyonrails.org/classes/ActiveRecord/Enum.html
You would have something like
# User.status = 2
User.status = "banned"

This is something of a non-answer, but the question belies the implementation of ActiveRecord::Enum:
# to get the string value for User.statuses[:banned]…
"banned"
# or
:banned.to_s
# to get the string value for all values in the User.statuses enum…
User.statuses.keys
# => ["pending", "member", "banned"]
The key isn't the important part here, really. All Rails is doing is taking the array of symbols you give it here…
enum status: [:pending, :member, :banned]
…and assigning it to a hash with incremented integer values while providing you a bunch of convenient methods for accessing the value:
user.status #=> 'pending'
user.pending? #=> true
You can verify this if you like…
User.defined_enums.class #=> Hash
User.defined_enums
#=> { "status" => { "pending" => 0, "member" => 1, "banned" => 2 } }

Related

Default value for Mongoid Hash field accessor

Given a Mongoid model:
class Counts
include Mongoid::Document
# lists of tag counts
field :tags, type: Hash, default: {}
end
c = Counts.new( tags = {new: 12, old: 7})
I would like to override c#tags[] so that if a key isn't set on the tags field, it should return a default of 0 instead of nil like this:
c.tags['ancient']
# => 0
Try setting default hash values as below:
class Counts
...
field :tags, type: Hash, default: Hash.new{ |h, k| h[k] = 0 }
end

Update has_many attributes with irregular params

I have model LoanPlan and Career, they are associated by a join_table
The request params from another frontend developer will be like this
"loan_plan" => {
"id" => 32,
"careers" => [
[0] {
"id" => 8,
},
[1] {
"id" => 9,
}
]
},
However, I got ActiveRecord::AssociationTypeMismatch: Career(#70198754219580) expected, got ActionController::Parameters(#70198701106200) in the update method
def update
#loan_plan.update(loan_plan_params)
end
When I tried to update the loan_plan model with careers params, it expects the params["careers"] should be careers object of a array instead of ids of a array.
So my workround is to manually fectch the careers objects of a array and replace the sanitized params.
It seems dirty and smells bad, any better solution in Rails way? Thanks
def loan_plan_params
# params.fetch(:loan_plan, {})
cleaned_params = params.require(:loan_plan).permit(
:id,
:name,
{:careers=>:id}
)
cleaned_params["careers"] = Career.find(cleaned_params["careers"].map{|t| t["id"]})
cleaned_params
end
model
class LoanPlan < ActiveRecord::Base
has_and_belongs_to_many :careers
accepts_nested_attributes_for :careers
end
In Rails way, params should be
"loan_plan" => {
"id" => "32",
"career_ids" => ["8", "9"]
}
and the strong parameter loan_plan_params should be
def loan_plan_params
params.require(:loan_plan).permit(
:id,
:name,
:career_ids => []
)
end

Rails/mongoid: Advanced querying of arrays

Im stuck with an advanced query in rails. I need a solution that works in mongoid and if possible also active record (probably not possible). I've put together a simplified example below.
Consider the following model:
class Announcement
include Mongoid::Document
field :title, type: String
field :user_group, type: Array
field :year, type: Array
field :tags, type: Array
has_and_belongs_to_many :subjects
before_save :generate_tags
private
def generate_tags
tags = []
if self.subjects.present?
self.subjects.each { |x| tags << x.name.downcase.gsub(" ", "_") }
end
if self.year.present?
self.year.each { |x| tags << "year_" + x.to_s }
end
self.tags = tags
end
end
Given the tags array of document 1:
["hsc_mathematics", "hsc_chemistry", "year_9"]
And document 2:
["hsc_mathematics", "hsc_chemistry"]
And document 3:
["hsc_mathematics", "hsc_chemistry", "year_9", "year_10"]
And document 4:
["year_9", "year_10"]
Now consider the following model:
class Student < User
include Mongoid::Document
field :year, type: Integer
has_many :subjects
def announcements
tags = []
if self.subjects.present?
self.subjects.each { |x| subjects << x.name.downcase.gsub(" ", "_") }
end
tags << "year_" + self.year.to_s
Announcement.where("user_group" => { "$in" => ["Student", "all_groups"]}).any_of({"tags" => { "$in" => tags }}, {tags: []})
end
end
For the purpose of our example our student has the following tags:
[ "hsc_mathematics", "hsc_physics", "year_10" ]
My query is incorrect as I want to return documents 2, 3 and 4 but not document 1.
I need the query to adhere to the following when returning announcements:
i. If the announcement has subject tags match on any subject
ii. If the announcement has year tags match on any year
iii. If announcement has year and subject tags match on any year and any subject
How would I go about writing this?
EDIT
Im happy to split year out of my tags but im still stuck
Announcement.where("user_group" => { "$in" => ["Student", "all_groups"]}).any_of({"tags" => { "$in" => ["hsc_mathematics", "hsc_physics"] }}, {tags: []}).any_of({"year_at_school" => { "$in" => 10 }}, {year_at_school: []})
So the solution was to adjust my models and use a more organised query rather then an entire tag bank.
Announcement model:
class Announcement
include Mongoid::Document
field :title, type: String
field :user_group, type: Array, default: [""]
field :year, type: Array, default: [""]
field :tags, type: Array, default: [""]
has_and_belongs_to_many :subjects
before_save :generate_tags
private
def generate_tags
tags = []
if self.subjects.present?
self.subjects.each { |x| tags << x.name.downcase.gsub(" ", "_") }
end
self.tags = tags
end
end
User model:
class Student < User
include Mongoid::Document
field :year, type: Integer
has_many :subjects
def announcements
year = "year_" + self.year.to_s
tags = [""]
if self.subjects.present?
self.subjects.each { |x| tags << x.name.downcase.gsub(" ", "_") }
end
Announcement.where("user_group" => { "$in" => ["Student", ""] }).and("year" => { "$in" => [year, ""]}).any_in(tags: tags).all.entries
end
end
EDIT: Heres a neater version of the query as suggested
This example also has an expiry field which assumes nil = never expires
Announcement.where(:user_group.in => ["Student", ""], :year.in => [year, ""], :tags.in => tags).any_of({:expires_at.gte => Time.zone.now}, {:expires_at => nil}).all.entries

Mapping hash maps to class instances

Looking for gem or at least idea how to approach this problem, the ones I have are not exactly elegant :)
Idea is simple I would like to map hashes such as:
{ :name => 'foo',
:age => 15,
:job => {
:job_name => 'bar',
:position => 'something'
...
}
}
To objects of classes (with flat member structure) or Struct such as:
class Person {
#name
#age
#job_name
...
}
Thanks all.
Assuming that you can be certain sub-entry keys won't conflict with containing entry keys, here's some code that should work...
require 'ostruct'
def flatten_hash(hash)
hash = hash.dup
hash.entries.each do |k,v|
next unless v.is_a?(Hash)
v = flatten_hash(v)
hash.delete(k)
hash.merge! v
end
hash
end
def flat_struct_from_hash(hash)
hash = flatten_hash(hash)
OpenStruct.new(hash)
end
Solution that I used it solves problem with same key names but it does not give flat class structure. Somebody might find this handy just keep in mind that values with reserved names such as id, type need to be handled.
require 'ostruct'
def to_open_struct(element)
struct = OpenStruct.new
element.each do |k,v|
value = Hash === v ? to_open_struct(v) : v
eval("object.#{k}=value")
end
return struct
end
An alternate answer where you know the keys before hand
class Job
attr_accessor :job_name, :position
def initialize(params = {})
self.job_name = params.fetch(:job_name, nil)
self.position = params.fetch(:position, nil)
end
end
class Person
attr_accessor :name, :age, :job
def initialize(params = {})
self.name = params.fetch(:name, nil)
self.age = params.fetch(:age, nil)
self.job = Job.new(params.fetch(:job, {}))
end
end
hash = { :name => 'foo', :age => 1, :job => { :job_name => 'bar', :position => 'soetmhing' }}
p = Person.new(hash)
p.name
==> "foo"
p.job
==> #<Job:0x96cacd8 #job_name="bar", #position="soetmhing">
p.job.name
==> "bar"

How can I refactor a simple dynamic attribute?

I have a form that handles four different types of facets of the same form. In my SQL column, I have the four different attributes.
Only one of them is going to have data in it.
Distribution =>
zip_code: nil
me_topic: nil
sex: nil
age: nil
In order to differentiate between them, I wanted to set up a case statement, and add a dynamic attribute to the create call :
#type = case params[:type]
when "zip" then ":zip_code"
when "interest" then ":me_topic"
when "sex" then ":sex"
when "age" then ":age"
end
#cur_item = Distribution.new(#type => params[:value])
# Unfortunately, this is not the proper way to create a dynamic attribute
#distribution = #email.distributions.create(params[:distributions])
What is the proper syntax for completing this statement?
Declare a method called type_map
def type_map params
##type_map ||= {
"zip" => :zip_code,
"interest" => :me_topic,
"sex" => :sex,
"age" => :age
}
{ ##type_map[params[:type]] => params[:value]
end
Now you can use the map as follows:
#distribution = #email.distributions.create(type_map(params))
This is what I went with, but feel free to best my answer.
#cur_item = case params[:type]
when "zip" then {:zip_code => params[:value]}
when "interest" then {:me_topic => params[:value]}
when "sex" then {:sex => params[:value]}
when "age" then {:age => params[:value]}
end
#distribution = #email.distributions.create(#cur_item)
Well, one way to improve your code (what you posted in your answer) would be to factor out the repeated params[:value] as follows:
key = case params[:type]
when "zip" then :zip_code
when "interest" then :me_topic
when "sex" then :sex
when "age" then :age
end
#cur_item = { key => params[:value] }
#distribution = #email.distributions.create #cur_item

Resources