i have a model with a user selectable option that is set up in an array on the model.
def Pie < ActiveRecored::Base
def self.sel_options
[ [ "Apple Blueberry", "AB" ],
[ "Cranberry Date", "CD" ] ]
end
end
while the short string is retrieved from elsewhere and stored in the database, i would like to display the longer string when showing the object. e.g. in the view use:
Pie.display_customeor_choice[#pie_flavor]
i don't want to hard code the reverse hash, but if i create a display_options method that converts the array to a hash with reverse mapping will it run the conversion every time display_options is called? this could be resource intensive with large arrays that are converted a lot, is there a way to create the reverse hash once when the app is started and never again? (using rails 3 and ruby 1.9.2)
You are looking for Array#rassoc
Pie.display_customeor_choice.rassoc("#pie_flavor")
Here's how you could do it:
def Pie < ActiveRecored::Base
def self.sel_options
[ [ "Apple Blueberry", "AB" ],
[ "Cranberry Date", "CD" ] ]
end
def self.display_customeor_choice
unless #options
#options = {}
sel_options.each { |items| #options[items.last] = items.first }
end
#options
end
end
This guarantees it's going to be loaded only once on production (or other environments where cache_classes is set to true) but always reloads on development mode, making it simpler for you to change and see the changes.
Related
Perhaps my understanding of how this is supposed to work is wrong, but I seeing strings stored in my DB when I would expect them to be a jsonb array. Here is how I have things setup:
Migration
t.jsonb :variables, array: true
Model
attribute :variables, :variable, array: true
Custom ActiveRecord::Type
ActiveRecord::Type.register(:variable, Variable::Type)
Custom Variable Type
class Variable::Type < ActiveRecord::Type::Json
include ActiveModel::Type::Helpers::Mutable
# Type casts a value from user input (e.g. from a setter). This value may be a string from the form builder, or a ruby object passed to a setter. There is currently no way to differentiate between which source it came from.
# - value: The raw input, as provided to the attribute setter.
def cast(value)
unless value.nil?
value = Variable.new(value) if !value.kind_of?(Variable)
value
end
end
# Converts a value from database input to the appropriate ruby type. The return value of this method will be returned from ActiveRecord::AttributeMethods::Read#read_attribute. The default implementation just calls #cast.
# - value: The raw input, as provided from the database.
def deserialize(value)
unless value.nil?
value = super if value.kind_of?(String)
value = Variable.new(value) if value.kind_of?(Hash)
value
end
end
So this method does work from the application's perspective. I can set the value as variables = [Variable.new, Variable.new] and it correctly stores in the DB, and retrieves back as an array of [Variable, Variable].
What concerns me, and the root of this question, is that in the database, the variable is stored using double escaped strings rather than json objects:
{
"{\"token\": \"a\", \"value\": 1, \"default_value\": 1}",
"{\"token\": \"b\", \"value\": 2, \"default_value\": 2}"
}
I would expect them to be stored something more resembling a json object like this:
{
{"token": "a", "value": 1, "default_value": 1},
{"token": "b", "value": 2, "default_value": 2}
}
The reason for this is that, from my understanding, future querying on this column directly from the DB will be faster/easier if in a json format, rather than a string format. Querying through rails would remain unaffected.
How can I get my Postgres DB to store the array of jsonb properly through rails?
So it turns out that the Rails 5 attribute api is not perfect yet (and not well documented), and the Postgres array support was causing some problems, at least with the way I wanted to use it. I used the same approach to the problem for the solution, but rather than telling rails to use an array of my custom type, I am using a custom type array. Code speaks louder than words:
Migration
t.jsonb :variables, default: []
Model
attribute :variables, :variable_array, default: []
Custom ActiveRecord::Type
ActiveRecord::Type.register(:variable_array, VariableArrayType)
Custom Variable Type
class VariableArrayType < ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Jsonb
def deserialize(value)
value = super # turns raw json string into array of hashes
if value.kind_of? Array
value.map {|h| Variable.new(h)} # turns array of hashes into array of Variables
else
value
end
end
end
And now, as expected, the db entry is no longer stored as a string, but rather as searchable/indexable jsonb. The whole reason for this song and dance is that I can set the variables attribute using plain old ruby objects...
template.variables = [Variable.new(token: "a", default_value: 1), Variable.new(token: "b", default_value: 2)]
...then have it serialized as its jsonb representation in the DB...
[
{"token": "a", "default_value": 1},
{"token": "b", "default_value": 2}
]
...but more importantly, automatically deserialized and rehydrated back into the plain old ruby object, ready for me to interact with it.
Template.find(123).variables = [#<Variable:0x87654321 token: "a", default_value: 1>, #<Variable:0x12345678 token: "b", default_value: 2>]
Using the old serialize api causes a write with every save (intentionally by Rails architectural design), regardless of whether or not the serialized attribute had changed. Doing this all manually by overriding setters/getters is an unnecessary complication due to the numerous ways attributes can be assigned, and is partly the reason for the newer attributes api.
If it helps anyone else, Rails wants you to provide the possible keys to permit in the controller as well if you're using strong params:
def controller_params
params.require(:parent_key)
.permit(
jsonb_field: [:allowed_key1, :allowed_key2, :allowed_key3]
)
end
One solution could be to just parse the variable via JSON.parse, push it inside an empty array, then assign it to the attribute.
variables = []
variable = "{\"token\": \"a\", \"value\": 1, \"default_value\": 1}"
variable.class #String
parsed_variable = JSON.parse(variable) #{"token"=>"a", "value"=>1, "default_value"=>1}
parsed_variable.class #Hash
variables.push parsed_variable
Assume we have a rails params hash full of nested hashes and arrays. Is there a way to alter every string value (whether in nested hashes or arrays) which matches a certain criteria (e.g. regex) and still keep the output as a params hash (still containing nested hashes arrays?
I want to do some sort of string manipulation on some attributes before even assigning them to a model. Is there any better way to achieve this?
[UPDATE]
Let's say we want to select the strings that have an h in the beginning and replace it with a 'b'. so we have:
before:
{ a: "h343", b: { c: ["h2", "s21"] } }
after:
{ a: "b343", b: { c: ["b2", "s21"] } }
For some reasons I can't do this with model callbacks and stuff, so it should have be done before assigning to the respective attributes.
still keep the output as a params hash (still containing nested hashes arrays
Sure.
You'll have to manipulate the params hash, which is done in the controller.
Whilst I don't have lots of experience with this I just spent a bunch of time testing -- you can use a blend of the ActionController::Parameters class and then using gsub! -- like this:
#app/controllers/your_controller.rb
class YourController < ApplicationController
before_action :set_params, only: :create
def create
# Params are passed from the browser request
#model = Model.new params_hash
end
private
def params_hash
params.require(:x).permit(:y).each do |k,v|
v.gsub!(/[regex]/, 'string')
end
end
end
I tested this on one of our test apps, and it worked perfectly:
--
There are several important points.
Firstly, when you call a strong_params hash, params.permit creates a new hash out of the passed params. This means you can't just modify the passed params with params[:description] = etc. You have to do it to the permitted params.
Secondly, I could only get the .each block working with a bang-operator (gsub!), as this changes the value directly. I'd have to spend more time to work out how to do more elaborate changes.
--
Update
If you wanted to include nested hashes, you'd have to call another loop:
def params_hash
params.require(:x).permit(:y).each do |k,v|
if /_attributes/ ~= k
k.each do |deep_k, deep_v|
deep_v.gsub!(/[regex]/, 'string'
end
else
v.gsub!(/[regex]/, 'string')
end
end
end
In general you should not alter the original params hash. When you use strong parameters to whitelist the params you are actually creating a copy of the params - which can be modified if you really need to.
def whitelist_params
params.require(:foo).permit(:bar, :baz)
end
But if mapping the input to a model is too complex or you don't want to do it on the model layer you should consider using a service object.
Assuming you have a hash like this:
hash = { "hello" => { "hello" => "hello", "world" => { "hello" => "world", "world" => { "hello" => "world" } } }, "world" => "hello" }
Then add a function that transforms the "ello" part of all keys and values into "i" (meaning that "hello" and "yellow" will become "hi" and "yiw")
def transform_hash(hash, &block)
hash.inject({}){ |result, (key,value)|
value = value.is_a?(Hash) ? transform_hash(value, &block) : value.gsub(/ello/, 'i')
block.call(result, key.gsub(/ello/, 'i'), value)
result
}
end
Use the function like:
new_hash = transform_hash(hash) {|hash, key, value| hash[key] = value }
This will transform your hash and it's values regardless of the nesting level. However, the values should be strings (or another Hash) otherwise you'll get an error. to solve this problem just change the value.is_a?(Hash) conditional a bit.
NOTE that I strongly recommend you NOT to change the keys of the hash!
Currently my JSON request is returning the below, where each person/lender has many inventories.
#output of /test.json
[
{"id":13, "email":"johndoe#example.com", "inventories":
[
{"id":10,"name":"2-Person Tent","category":"Camping"},
{"id":11,"name":"Sleeping bag","category":"Camping"},
{"id":27,"name":"6-Person Tent","category":"Camping"}
]
},
{"id":14, "email":"janedoe#example.com", "inventories":
[
{"id":30,"name":"Electric drill","category":"Tools"},
{"id":1,"name":"Hammer","category":"Tools"},
{"id":37,"name":"Plane","category":"Tools"}
]
}
]
I need to nest in one more thing and am having trouble doing so. For context, each inventory item is referenced via it's id as a foreign key in a borrow record. Each borrow record belongs to a request parent that stores returndate and pickupdate. What I need now, is for each inventory item, to nest an array of all the request records, with information on pickupdate and returndate. In other words, desired output:
[
{"id":13, "email":"johndoe#example.com", "inventories":
[
{"id":10,"name":"2-Person Tent","category":"Camping", "requests":
[
{"id":1, "pickupdate":"2014-07-07","returndate":"2014-07-10"},
{"id":2, "pickupdate":"2014-06-02","returndate":"2014-06-05"},
{"id":3, "pickupdate":"2014-08-14","returndate":"2014-08-20"}
]
},
{"id":11,"name":"Sleeping bag","category":"Camping", "requests":
[
{"id":4, "pickupdate":"2014-05-27","returndate":"2014-05-30"},
{"id":5, "pickupdate":"2014-04-22","returndate":"2014-04-25"}
]
},
{"id":27,"name":"6-Person Tent","category":"Camping", "requests":
[
{"id":6, "pickupdate":"2014-07-10","returndate":"2014-07-12"}
]
}
]
},
{"id":14, "email":"janedoe#example.com", "inventories":
...
I have written the following code:
json.array!(#lenders) do |json, lender|
json.(lender, :id, :email)
json.inventories lender.inventories do |json, inventory|
json.id inventory.id
json.name Itemlist.find_by_id(inventory.itemlist_id).name
#code below says, json.requests should equal all the Requests where there is a Borrows within that Request that is using the Inventory in question
json.requests Request.select { |r| r.borrows.select { |b| b.inventory_id == inventory.id }.present? } do |json, request|
json.pickupdate request.pickupdate
json.returndate request.returndate
end
end
end
When I refresh the page, I get wrong number of arguments (0 for 2..5)
I feel like the issue is that the Request.select... is returning an Array which isn't what needs to go here... but in the earlier nested function lender.inventories is an Inventory::ActiveRecord_Associations_CollectionProxy though I'm not sure how to correct for this.
NOTE: Someone said the problem could be that unlike with the nesting between inventories and lender, there's not an explicit association between inventory and request, but then again the line json.name Itemlist.find_by_id(inventory.itemlist_id).name worked so I'm not sure this is right. (Also if this is the case, I'm not sure how to bypass this limitation... I currently don't want to create a relationship between the two.)
Thanks!
ARG. Ok so this code is perfectly right. The issue was that I"m using the Gon gem in conjunction with Jbuilder, and Request is a predefined class in Gon.
So just changed code to
#requestrecords.select....
And in the controller:
#requestrecords = Request.all
-__-
I have a rails backend that recieves JSON that (when parsed) looks like:
[
{"kind"=>"Magazine", "price"=>["$20.99"]},
{"kind"=>"Book", "price"=>"", "title"=>""}
]
Basically what I want to do is for each kind of product (e.g. Magazine or book), if all other attributes except for the kind key are blank, then don't save that array key/value. So in my example, Magazine would stay in the array, but the Book kind would be deleted (because both attributes price and title are blank.
I know I could loop through with something like (list is the parsed JSON before):
list.each do |l|
if l["kind"] == "Magazine"
if l["price"].blank?
# THEN DELETE THIS ITERATION
end
end
end
but this seems very repetitive and not clean. How do I do this better?
The idiomatic way to do this would involve using Array#reject! to remove the unwanted lines. You can also extract just the values, remove the blank ones, and count the remaining values... making sure that 'kind' is one of them...
list.reject! {|item| item.values.reject(&:blank?).size < 2 && item['kind'].present?}
Notice the difference between reject and reject! ... one returns a new hash while the ! method modifies it in place.
You can extend Hash with a method to remove blank values (this supports nested hashes also):
class Hash
def delete_blank
delete_if{|k, v| v.blank? or v.instance_of?(Hash) && v.delete_blank.blank?}
end
end
And after the blank values are removed, if there is only one key left and it is kind, then remove the array element:
list.each do |l|
l.delete_blank
end
list.reject! {|l| l.key?('kind') && l.length < 2}
#=> [{"kind"=>"Magazine", "price"=>["$20.99"]}]
Sorry for the confusing title, not sure how to describe this issue.
Inside a Ruby on Rails controller I'm creating a list named #commits, where each item in #commits should contain a hash table whose elements are the values of various properties for each commit. These property values are stored in a Redis database.
Below, I iterate through a list of properties whose values should be grabbed from Redis, and then grab those values for each of 8 different commits. Then I place the values from redis into a different hash table for each commit, using the commit property name as the key for the hash.
# Initialize #commits as a list of eight empty hash tables
#commits = Array.new(8, {})
# Iterate over the attributes that need hashed for each item in #commits
[:username, :comment, :rev, :repo].each do |attrib|
# 8 items in #commits
8.times do |i|
# Get a value from redis and store it in #commits[i]'s hash table
#commits[i][attrib] = $redis.lindex(attrib, i)
# Print the value stored in the hash
# Outputs 7, 6, .., 0 for #commits[i][:rev]
puts #commits[i][attrib].to_s
end
end
# Print the value of every item that was stored in the hash tables above,
# but only for the :rev key
# Outputs 0 eight times
8.times do |i|
puts #commits[i][:rev]
end
However, per the comments above, #commits[0..7] all seem to have the same values in their hashes, despite them being seemingly stored correctly a few lines above. Using the hash key :rev as an example, the first puts outputs 7..0, which is correct, but the second puts outputs the number 0 eight times.
Anyone know why?
It would help if you show how #commits is initialized, but it looks like you've created a structure with multiple references to the same object.
Incorrect, same object recycled for all keys:
#commits = Hash.new([ ])
Correct, new object created for each key:
#commits = Hash.new { |h, k| h[k] = [ ] }
You could be using an Array with the same mistake:
#commits = Array.new(8, [ ])
This will lead to the following behaviour:
a = Array.new(4, [ ])
a[0]
# => []
a[0] << 'x'
# => ["x"]
a
# => [["x"], ["x"], ["x"], ["x"]]
It can be fixed by passing in a block:
a = Array.new(4) { [ ] }
a[0]
# => []
a[0] << 'x'
# => ["x"]
a
# => [["x"], [], [], []]
It is highly unusual to see an array pre-initialized with values, though. Normally these are just lazy-initialized, or a Hash is used in place of an Array.