List dynamic attributes in a Mongoid Model - ruby

I have gone over the documentation, and I can't find a specific way to go about this. I have already added some dynamic attributes to a model, and I would like to be able to iterate over all of them.
So, for a concrete example:
class Order
include Mongoid::Document
field :status, type: String, default: "pending"
end
And then I do the following:
Order.new(status: "processed", internal_id: "1111")
And later I want to come back and be able to get a list/array of all the dynamic attributes (in this case, "internal_id" is it).
I'm still digging, but I'd love to hear if anyone else has solved this already.

Just include something like this in your model:
module DynamicAttributeSupport
def self.included(base)
base.send :include, InstanceMethods
end
module InstanceMethods
def dynamic_attributes
attributes.keys - _protected_attributes[:default].to_a - fields.keys
end
def static_attributes
fields.keys - dynamic_attributes
end
end
end
and here is a spec to go with it:
require 'spec_helper'
describe "dynamic attributes" do
class DynamicAttributeModel
include Mongoid::Document
include DynamicAttributeSupport
field :defined_field, type: String
end
it "provides dynamic_attribute helper" do
d = DynamicAttributeModel.new(age: 45, defined_field: 'George')
d.dynamic_attributes.should == ['age']
end
it "has static attributes" do
d = DynamicAttributeModel.new(foo: 'bar')
d.static_attributes.should include('defined_field')
d.static_attributes.should_not include('foo')
end
it "allows creation with dynamic attributes" do
d = DynamicAttributeModel.create(age: 99, blood_type: 'A')
d = DynamicAttributeModel.find(d.id)
d.age.should == 99
d.blood_type.should == 'A'
d.dynamic_attributes.should == ['age', 'blood_type']
end
end

this will give you only the dynamic field names for a given record x:
dynamic_attribute_names = x.attributes.keys - x.fields.keys
if you use additional Mongoid features, you need to subtract the fields associated with those features:
e.g. for Mongoid::Versioning :
dynamic_attribute_names = (x.attributes.keys - x.fields.keys) - ['versions']
To get the key/value pairs for only the dynamic attributes:
make sure to clone the result of attributes(), otherwise you modify x !!
attr_hash = x.attributes.clone #### make sure to clone this, otherwise you modify x !!
dyn_attr_hash = attr_hash.delete_if{|k,v| ! dynamic_attribute_names.include?(k)}
or in one line:
x.attributes.clone.delete_if{|k,v| ! dynamic_attribute_names.include?(k)}

So, what I ended up doing is this. I'm not sure if it's the best way to go about it, but it seems to give me the results I'm looking for.
class Order
def dynamic_attributes
self.attributes.delete_if { |attribute|
self.fields.keys.member? attribute
}
end
end
Attributes appears to be a list of the actual attributes on the object, while fields appears to be a hash of the fields that were predefined. Couldn't exactly find that in the documentation, but I'm going with it for now unless someone else knows of a better way!

try .methods or .instance_variables

Not sure if I liked the clone approach, so I wrote one too. From this you could easily build a hash of the content too. This merely outputs it all the dynamic fields (flat structure)
(d.attributes.keys - d.fields.keys).each {|a| puts "#{a} = #{d[a]}"};

I wasn't able to get any of the above solutions to work (as I didn't want to have to add slabs and slabs of code to each model, and, for some reason, the attributes method does not exist on a model instance, for me. :/), so I decided to write my own helper to do this for me. Please note that this method includes both dynamic and predefined fields.
helpers/mongoid_attribute_helper.rb:
module MongoidAttributeHelper
def self.included(base)
base.extend(AttributeMethods)
end
module AttributeMethods
def get_all_attributes
map = %Q{
function() {
for(var key in this)
{
emit(key, null);
}
}
}
reduce = %Q{
function(key, value) {
return null;
}
}
hashedResults = self.map_reduce(map, reduce).out(inline: true) # Returns an array of Hashes (i.e. {"_id"=>"EmailAddress", "value"=>nil} )
# Build an array of just the "_id"s.
results = Array.new
hashedResults.each do |value|
results << value["_id"]
end
return results
end
end
end
models/user.rb:
class User
include Mongoid::Document
include MongoidAttributeHelper
...
end
Once I've added the aforementioned include (include MongoidAttributeHelper) to each model which I would like to use this method with, I can get a list of all fields using User.get_all_attributes.
Granted, this may not be the most efficient or elegant of methods, but it definitely works. :)

Related

How do I tack a string onto a variable and evaluated the entire thing as a variable in Ruby?

I have the following Ruby code:
module BigTime
FOO1_MONEY_PIT = 500
FOO2_MONEY_PIT = 501
class LoseMoney
##SiteName = 'FOO1'
#site_num = ##SiteName_MONEY_PIT
def other_unimportant_stuff
whatever
end
end
end
So, what I'm trying to do here is set the SiteName and then use SiteName and combine it with the string _MONEY_PIT so I can access FOO1_MONEY_PIT and store its contents (500 in this case) in #site_num. Of course, the above code doesn't work, but there must be a way I can do this?
Thanks!!
If you want to dynamically get the value of a constant, you can use Module#const_get:
module BigTime
FOO1_MONEY_PIT = 500
FOO2_MONEY_PIT = 501
class LoseMoney
##SiteName = 'FOO1'
#site_num = BigTime.const_get(:"#{##SiteName}_MONEY_PIT")
end
end
Do not, under any circumstance, use Kernel#eval for this. Kernel#eval is extremely dangerous in any context where there is even the slightest possibility that an attacker may be able to control parts of the argument.
For example, if a user can choose the name of the site, and they name their site require 'fileutils'; FileUtils.rm_rf('/'), then Ruby will happily evaluate that code, just like you told it to!
Kernel#eval is very dangerous and you should not get into the habit of just throwing an eval at a problem. It is a very specialized tool that should only be employed when there is no other option (spoiler alert: there almost always is another option), and only after a thorough security review.
Please note that dynamically constructing variable names is already a code smell by itself, regardless of whether you use eval or not. It pretty much always points to a design flaw somewhere. In general, you can almost guaranteed replace the multiple variables with a data structure. E.g. in this case something like this:
module BigTime
MONEY_PITS = {
'FOO1' => 500,
'FOO2' => 501,
}.freeze
class LoseMoney
##SiteName = 'FOO1'
#site_num = MONEY_PITS[##SiteName]
end
end
You can refactor this as to use a Hash for your name lookups, and a getter method to retrieve it for easy testing/validation. For example:
module BigTime
MONEY_PITS = { FOO1: 500, FOO2: 501 }
MONEY_PIT_SUFFIX = '_MONEY_PIT'
class LoseMoney
##site = :FOO1
def initialize
site_name
end
def site_name
#site_name ||= '%d%s' % [MONEY_PITS[##site], MONEY_PIT_SUFFIX]
end
end
end
BigTime::LoseMoney.new.site_name
#=> "500_MONEY_PIT"

Loop through variables in different classes to match string in Ruby

I have a ruby (sinatra) app that I am working on, and my input is a url and if verbose or not (true or false), so basically like this:
The url would look like this: http://localhost:4567/git.company.com&v=false for example.
And the code to fetch those is this:
get '/:url' do |tool_url|
url = params[:url].to_s
is_verbose = params[:v].to_s
I have different classes separated in different files and I'm including them into my main script like this:
Dir["#{File.dirname(__FILE__)}/lib/*.rb"].each { |f| require(f) }
(And a sample file would be something like this), gitlab.rb:
class Gitlab
$gitlab_token = 'TOKEN_GOES_HERE'
def initialize(url, v)
##regex =~ /git.company.com/
##gitlab_url = url
##is_verbose = v
end
def check_gitlab(gitlab_url, is_verbose)
_gitlab_overall = '/health_check.json?token=#{gitlab_token}'
_gitlab_cache = '/health_check/cache.json?token=#{gitlab_token}'
_gitlab_database = '/health_check/database.json?token=#{gitlab_token}'
_gitlab_migrations = '/health_check/migrations.json?token=#{gitlab_token}'
unless is_verbose = true
CheckString.check_string_from_page('https://' + gitlab_url + gitlab_overall, 'success')
else
end
end
end
Now, I want to be able to dynamically know which "class" to use to do a certain job based on the URL that's entered by the user, so my idea was to iterate through those classes looking for a particular variable to match with the input.
I need guidance in this because I've been stuck on this for quite some time now; I've tried so many things that I can think of, but none worked.
Disclaimer: Please bear with me here, because I'm very new to Ruby and I'm not that great in OOP languages (haven't really practiced them that much).
EDIT: I'm open to any suggestion, like if there's a different logic that's better than this, please do let me know.
Make a hash { Regexp ⇒ Class }:
HASH = {
/git.company.com/ => Gitlab,
/github.com/ => Github
}
and then do:
handler = HASH.detect { |k, _| k =~ url }.last.new
The above will give you an instance of the class you wanted.
Sidenotes:
is_verbose = params[:v].to_s always results in is_verbose set to truthy value, check for params[:v].to_s == "true"
is_verbose = true is an assignment, you wanted to use just unless is_verbose.
To make it runtime-resolving, force the plugins to a) include Plugin and b) declare resolve method. Plugin module should define a callback hook:
module Plugin
def self.included(base)
Registry::HASH[-> { base.resolve }] = base
end
end
resolve method should return a regexp, the lambda is here to make it resolved on parsing stage:
class PluginImpl
include Plugin
def resolve
/git.company.com/
end
end
And then match when needed:
handler = HASH.detect { |k, _| k.() =~ url }.last.new
Other way round would be to use ObjectSpace to detect classes, including the module, or declare the TracePoint on base in included callback to provide a direct map, but all this is overcomplicating.

How to check if value exists in params.permit?

I can't seem to figure out how to accomplish what I am trying to do here on my create method.
What I have right now works if there are no values, the item is deleted. However, if 1 or more param values exist, it passes and is saved. Not what I needed. I need an all or nothing scenario. I want to save only if all the permitted keys have their value. params.permit(:name, :description, :copyright)
Before an entry is saved using organizations.save!, I need to make sure none of the params that are permitted are nil or empty.
I search all over and can't seem to narrow down on an answer to my exact issue.
Here is my code:
class OrganizationsController < ApplicationController
def index
query_params = params.permit(:id, :name,)
if query_params.blank?
organizations = Organization.all
else
organizations = Organization.where(query_params)
end
render json: organizations, root: "organizations"
end
def create
organizations = Organization.new(organization_params)
if organization_params.present?
organizations.delete
else
organizations.save!
render json: organizations
end
end
private
def organization_params
params.permit(:name, :description, :copyright)
end
end
You should add validations to your model.
From your question i understand that you want to save details only if you get values in all the field, if not you don't want to save, right?. If yes, then adding validations to your model will give you what you wanted.
Add the following to your organization model
validates_presence_of :name
validates_presence_of :description
validates_presence_of :copyright
by doing so, the user won't be allowed to save the details unless and until all three fields have some value in it.
There is no need to use delete as the incomplete information will not be saved.
for more and advanced info click here
To check none of the values of organization_params hash is empty, you can do something like this:
organization_params.values.all? { |x| !x.empty? }
or, this:
organization_params.all? { |k,v| !v.empty? }
You can also check if any param value is empty:
organization_params.any? { |k,v| v.empty? }
So, your create method can be re-written as:
def create
organizations = Organization.new(organization_params)
if organization_params.any? { |k,v| v.empty? }
# at least one param is empty, so delete the record
organizations.delete
else
# all the params values are present, so save the record
organizations.save!
render json: organizations
end
end

Duplicate an object while changing one of its attributes

I have a CartItem object that I want to make a duplicate of. Each CartItem belongs to a Cart.
I am writing a method that will take an old order and duplicate all of its cart_items and place it in the current cart.
order.add_items_to_cart(current_cart, current_user)
Order.rb
def add_items_to_cart(cart, cart_user)
cart.update_attributes(merchant_id: self.merchant_id)
self.cart_items.each do |ci|
new_cart_item = ci.dup
new_cart_item.save
new_cart_item.update_attributes(cart_id: cart.id, cart_user_id: cart_user.id)
end
end
Currently, I have the above. Is there a better way to dupe and change the attributes in one line?
If copying only attributes but associations is ok for you, then you are doing good. http://apidock.com/rails/ActiveRecord/Core/dup
But, I suggest you use assign_attributes, so you'll make only one query.
def add_items_to_cart(cart, cart_user)
cart.update_attributes(merchant_id: self.merchant_id)
self.cart_items.each do |ci|
new_cart_item = ci.dup
new_cart_item.assign_attributes(cart_id: cart.id, cart_user_id: cart_user.id)
new_cart_item.save
end
end
EDIT:
Make a method Cart#duplicate, which returns what you need
class CartItem
...
# returns copy of an item
def duplicate
c = Cart.new cart_id: cart.id, cart_user_id: cart_user.id
# copy another attributes inside this method
c
end
end
# And use it
self.cart_items.each do |ci|
new_card_item = ci.duplicate
new_card_item.save
end

Testing a lambda

I am creating an import feature that imports CSV files into several tables. I made a module called CsvParser which parses a CSV file and creates records. My models that receive the create actions extends theCsvParser. They make a call to CsvParser.create and pass the correct attribute order and an optional lambda called value_parser. This lambda transforms values in a hash to a preffered format.
class Mutation < ActiveRecord::Base
extend CsvParser
def self.import_csv(csv_file)
attribute_order = %w[reg_nr receipt_date reference_number book_date is_credit sum balance description]
value_parser = lambda do |h|
h["is_credit"] = ((h["is_credit"] == 'B') if h["is_credit"].present?)
h["sum"] = -1 * h["sum"].to_f unless h["is_credit"]
return [h]
end
CsvParser.create(csv_file, self, attribute_order, value_parser)
end
end
The reason that I'm using a lambda instead of checks inside the CsvParser.create method is because the lambda is like a business rule that belongs to this model.
My question is how i should test this lambda. Should i test it in the model or the CsvParser? Should i test the lambda itself or the result of an array of the self.import method? Maybe i should make another code structure?
My CsvParser looks as follows:
require "csv"
module CsvParser
def self.create(csv_file, klass, attribute_order, value_parser = nil)
parsed_csv = CSV.parse(csv_file, col_sep: "|")
records = []
ActiveRecord::Base.transaction do
parsed_csv.each do |row|
record = Hash.new {|h, k| h[k] = []}
row.each_with_index do |value, index|
record[attribute_order[index]] = value
end
if value_parser.blank?
records << klass.create(record)
else
value_parser.call(record).each do |parsed_record|
records << klass.create(parsed_record)
end
end
end
end
return records
end
end
I'm testing the module itself:
require 'spec_helper'
describe CsvParser do
it "should create relations" do
file = File.new(Rails.root.join('spec/fixtures/files/importrelaties.txt'))
Relation.should_receive(:create).at_least(:once)
Relation.import_csv(file).should be_kind_of Array
end
it "should create mutations" do
file = File.new(Rails.root.join('spec/fixtures/files/importmutaties.txt'))
Mutation.should_receive(:create).at_least(:once)
Mutation.import_csv(file).should be_kind_of Array
end
it "should create strategies" do
file = File.new(Rails.root.join('spec/fixtures/files/importplan.txt'))
Strategy.should_receive(:create).at_least(:once)
Strategy.import_csv(file).should be_kind_of Array
end
it "should create reservations" do
file = File.new(Rails.root.join('spec/fixtures/files/importreservering.txt'))
Reservation.should_receive(:create).at_least(:once)
Reservation.import_csv(file).should be_kind_of Array
end
end
Some interesting questions. A couple of notes:
You probably shouldn't have a return within the lambda. Just make the last statement [h].
If I understand the code correctly, the first and second lines of your lambda are overcomplicated. Reduce them to make them more readable and easier to refactor:
h["is_credit"] = (h['is_credit'] == 'B') # I *think* that will do the same
h['sum'] = h['sum'].to_f # Your original code would have left this a string
h['sum'] *= -1 unless h['is_credit']
It looks like your lambda doesn't depend on anything external (aside from h), so I would test it separately. You could even make it a constant:
class Mutation < ActiveRecord::Base
extend CsvParser # <== See point 5 below
PARSE_CREDIT_AND_SUM = lambda do |h|
h["is_credit"] = (h['is_credit'] == 'B')
h['sum'] = h['sum'].to_f
h['sum'] *= -1 unless h['is_credit']
[h]
end
Without knowing the rationale, it's hard to say where you should put this code. My gut instinct is that it is not the job of the CSV parser (although a good parser may detect floating point numbers and convert them from strings?) Keep your CSV parser reusable. (Note: Re-reading, I think you've answered this question yourself - it is business logic, tied to the model. Go with your gut!)
Lastly, you are defining and the method CsvParser.create. You don't need to extend CsvParser to get access to it, although if you have other facilities in CsvParser, consider making CsvParser.create a normal module method called something like create_from_csv_file

Resources