I'm writing an app in Sinatra, using activerecord, so I guess my question is the same as in Rails.
class Entry < ActiveRecord::Base
require 'date'
belongs_to :bankaccount
end
class Recurrent < Entry
attr_accessor :date_1, :date_2, :monthly_entry
def initialize (date_1, date_2)
#date_1 = date_1 # format DateTime.new(2020,12,5)
#date_2 = date_2
end
# other methods
When I run this code, I get :
>> date_1 = DateTime.new(2020,12,5)
>> date_2 = DateTime.new(2021,11,5)
>> recurrent = Recurrent.new(date_1, date_2)
**ArgumentError (wrong number of arguments (given 2, expected 0..1))**
and when I remove the arguments, I get this error message:
>> recurrent = Recurrent.new
**ArgumentError (wrong number of arguments (given 1, expected 2))**
When I do this in plain ruby and run it in irb, so without activerecord, it works fine.
According to the documentation:
Creation
Active Records accept constructor parameters either in a hash or as a block. The hash method is especially useful when you're receiving the data from somewhere else, like an HTTP request. It works like this:
user = User.new(name: "David", occupation: "Code Artist")
user.name # => "David"
You can also use block initialization:
user = User.new do |u|
u.name = "David"
u.occupation = "Code Artist"
end
And of course you can just create a bare object and specify the attributes after the fact:
user = User.new
user.name = "David"
user.occupation = "Code Artist"
So, ActiveRecord objects allow three different kinds of creating them:
No argument, set attributes later.
Block argument.
One Hash argument.
They don't allow two arguments.
Related
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"
I'm running Ruby 2.3.1 x64 on Windows 10 x64.
My code:
class Credentials
attr_reader :username, :password
def initialize(username = nil, password = nil)
#username = username
#password = password
get_credentials if !#username || !#password #Gets credentials if none are specified
end
def get_credentials
#username = ask("Username: ") { |q| q.echo = true }
#password = ask("Password: ") { |q| q.echo = "*" }
end
end
Ignore the get_credentials wackyness, it's a gem called Highline that I'm using to hide input for security reasons.
When I do the following:
$user = Credentials.new(username: "foo", password: "bar")
I get this return:
#<Credentials:0x000000038ecf30 #password=nil, #username={:username=>"foo", :password=>"bar"}>
Likewise, calling $user.username returns the following:
{:username=>"foo", :password=>"bar"}
when it should be returning:
"foo"
and calling $user.password returns nil.
Can someone tell me why in the name of Henry Hamilton this is happening?! I've used hashed parameters many times, and it always works just fine. Why is it stuffing every parameter setting into a single parameter?
$user = Credentials.new(username: "foo", password: "bar")
You are passing just one parameter to the initialize method, a hash. The hash for the username attribute and nil for the password attribute. Try
$user = Credentials.new("foo", "bar")
Or, if you really want keyword arguments then
def initialize(username: nil, password: nil)
When you define a method/constructor you don't pass arguments by name but by value just like any other programming language, So :
$user=Credentials.new("foo","bar")
Will do what you want.
This is the default in almost every programming language, your question should have been "How did this work", it worked because ruby is dynamically typed and the syntax key1: val1,key2: val2,... is the new hash syntax(since ruby 1.9), a hash is a key-value data structure , so your :
$user=Credentials.new(username: 'foo',password: 'bar')
Is actually calling the constructor with one argument only which is username with the value {username: 'foo',password: 'bar'} and because initialize is defined with default arguments , password got a value of nil.
Now if you do want to pass arguments by name, you have to define the constructor like so :
def initialize(username: nil,password: nil)
//code
end
After that you can do :
$user=Credentials.new(username: 'foo',password: 'bar')
And expect it to behave like you want.
Notice that keyword arguments(that is passing arguments by name) are introduced in ruby 2, also notice that you can achieve the same with a constructor that accepts one parameter which is a hash like this :
def initialize(params={})
//code
end
But this way doesn't limit the number of arguments nor their names(you can call Credentials.new(fooprop: 'foovalue') and no error will be thrown), also it needs some change in code.
The Keyword arguments feature is found in some programming languages and it's useful when the function have many parameters or to make it clear for the programmer what is the parameter for.
def initialize(params={})
#username = params[:username]
#password = params[:password]
#username || #password || get_credentials #simply
end
And then:
$user = Credentials.new(username: "foo", password: "bar")
Background
The Entity class is a base class that gets inherited by several subclasses that holds entities received over a REST API. The entity classes are immutable and should return a new instance of themselves whenever a change is attempted.
The Entity class has an .update() method that takes a hash of values to update, if the changes aren't really changes it returns itself and if there are real changes it returns a new instance of itself with the changes effected before instantiation.
To be user friendly Entity also allows for direct assignment to properties (so that if a subclass of Entity has a name attribute you can do instance.name = 'New Name') that also returns a new instance of the class. This is implemented in terms of update using dynamic methods that are created when the class is instantiated.
And they are the problem.
Problem
The code in the Entity class looks, in part, like this (for a complete code listing and tests check out the Github repo: https://github.com/my-codeworks/fortnox-api.git):
require "virtus"
require "ice_nine"
class Entity
extend Forwardable
include Virtus.model
def initialize( hash = {} )
super
create_attribute_setter_methods
IceNine.deep_freeze( self )
end
def update( hash )
attributes = self.to_hash.merge( hash )
return self if attributes == self.to_hash
self.class.new( attributes )
end
private
def create_attribute_setter_methods
attribute_set.each do |attribute|
name = attribute.options[ :name ]
create_attribute_setter_method( name )
end
end
def create_attribute_setter_method( name )
self.define_singleton_method "#{name}=" do | value |
self.update( name => value )
end
end
end
Doing this:
instance.update( name: 'New Name' )
and this:
instance.name = 'New Name'
Should be the same, literally since one is implemented in terms of the other.
While .update() works perfectly the .attr=() methods return the value you assign.
So in the above example .update() returns a new instance of the Entity subclass but .attr=() returns 'New Name' ...
I have tries capturing the output inside the .attr=() method and log it before returning so that I have this:
self.define_singleton_method "#{name}=" do | value |
p "Called as :#{name}=, redirecting to update( #{name}: #{value} )"
r = self.update( name => value )
p "Got #{r} back from update"
return r
end
And the log lines say:
"Called as :name=, redirecting to update( name: 'New Name' )"
"Got #<TestEntity:0x007ffedbd0ad18> back from update"
But all I get is the string 'New Name'...
My forehead is bloody and no posts I find show anything close to this. I bet I'm doing something wrong but I can't find it.
Getting dirty
The Github repo has tests in rspec that you can run, the failing ones are focused right now and some extra logging is in the Entity class to capture the different internal steps.
Comments, links and/or pull requests are welcome.
Turns out that the = methods always return the value being assigned.
o = Struct.new(:key).new(1)
o.define_singleton_method("something") { #something }
o.define_singleton_method("something=") do |v|
#something = v
return 6
end
As you can see, I've 'fixed' the return value to 6 each time something= is called. Let's see if it works:
o.something = 1 #=> outputs 1, not 6
o.something #=> outputs 1, so the method did indeed run
Conclusion? My guess is that an = method will return the value that you are assigning through it. And IMO it's better this way; one reason would be to ensure proper functioning of assignment chains:
new_val = o.something = some_val
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
I am parsing Excel and Excelx file using Roo gem. But I am not sure how to write in those files. set_value(row, column, text) method is not working.
Code
#oo = Excelx.new('tes.xlsx')
#oo.default_sheet = #oo.sheets.first
def return_column
keywords = ["website", "url"]
keywords.each do |keyword|
1.upto(#oo.last_column) do |n|
data = #oo.cell(1, n)
return n if data.downcase=~/#{keyword}/i
end
end
end
def return_rows
n = return_n
2.upto(#oo.last_row) do |row|
data = #oo.cell(row, n)
stack << data
end
end
def appender
#oo.set_value(1,11, "hey")
end
appender
The Error Message I'm getting is
/.rvm/gems/ruby-1.8.7-p352/gems/roo-1.10.1/lib/roo/generic_spreadsheet.rb:441:in `method_missing': private method `set_value' called for #<Excelx:0x101221f08> (NoMethodError)
from /Users/bhushan/.rvm/gems/ruby-1.8.7-p352/gems/roo-1.10.1/lib/roo/excelx.rb:168:in `method_missing'
from parser.rb:32:in `appender'
from parser.rb:35
No answers here actually answer the question of how to do this with Roo, so I'll add the solution that I just tested in our app.
Roo recently added functionality for editing cells: https://github.com/roo-rb/roo/blob/master/lib/roo/csv.rb#L42
You can use it like such:
sheet.set_value(1, 5, 'TEST', nil) # to set the 1st row, 5th column to the string 'TEST'
Notes:
The last argument nil is not used in the function but has no default so it's required.
This is only added in version 2.7.0.
Try 'set' method instead of 'set_value' method in Excelx or OpenOffice object. For more refer API http://rubydoc.info/gems/roo/1.10.1/frames and I think roo gem specializes in reading excel contents than writing. For instance using set method will not save back to the spreadsheet file. It saves on the buffer I think. Try some other gems for writing
You can set the value of a column by pushing a string into it.
sheet.row(0).push 'some value'
The code below writes to a spreadsheet
require 'spreadsheet'
class Util::Table < ActiveRecord::Migration
def self.create_import_template
# create an xls workbook template for data importing based on models in activerecord
#format = Spreadsheet::Format.new(:weight => :bold)
#template_folder = File.join(Dir.home, 'Dropbox', 'horizon', 'data', 'templates')
#template_file = File.join(#template_folder, "data_import_template_#{Time.now.round(3).to_s.chomp(' -0700').gsub(':','-').gsub(' ','_').chop.chop.chop}.xls")
#book = Spreadsheet::Workbook.new
ActiveRecord::Base.send(:subclasses).each {|model| add_worksheet_to_template(model)}
#book.write #template_file
end
def self.add_worksheet_to_template(model)
# create a tab for each model that you wish to import data into
write_sheet = #book.create_worksheet :name => model
write_sheet.row(0).set_format(0, #format)
model.columns.each_with_index do |c,i|
column = ""
column << "*" unless c.null # indicate required field
column << c.name
write_sheet.row(0).set_format(i+1, #format)
write_sheet.row(0).push column
end
end
end
you can use set method
sheet.set(row, col, value)