Modify each block entry to call method on block argument - ruby-on-rails

In one of my views, I rendered a table with a helper method, so the view (haml) looked like this:
= table do
- action "Add"
- column :id
- column :name
After I changed the helper and used the ViewComponent lib instead, I need to call it the following way:
= table do |t|
- t.action "Add"
- t.column :id
- t.column :name
I wondered if it's possible to convert the block in example 1 to the block in example 1 in a helper method, so I don't need to rewrite every view that uses a table.
The helper method would look like:
def table(*args, **kwargs, &block)
# ...
render TableComponent.new(*args, **kwargs, &new_block)
end

Let's simplify your example to this:
class Table
def initialize
#rows = []
end
def action(name)
#rows << "Action #{name}"
end
def column(name)
#rows << "Column #{name}"
end
def to_s
"Table:\n======\n#{#rows.join("\n")}"
end
end
def table(&block)
t = Table.new
block.call(t)
t
end
puts(
table do |t|
t.action "Add"
t.column :id
t.column :name
end
)
That gives:
Table:
======
Action Add
Column id
Column name
Now you want to do:
table do
action "Add"
column :id
column :name
end
So you want to have the body of the block be in the same context as the instance (e.g. like being in a Table instance), so:
def table(&block)
t = Table.new
t.instance_eval(&block)
t
end
(that's how most Domain Specific Languages are made :) )

Related

How does Ruby's block syntax work?

I'm new in Ruby and trying to understand this syntax:
create_table :posts do |t|
t.string :title
t.string :content
t.string :likes
t.string :comments
t.timestamps null: false
end
I fully understand what this code is doing, but I don't understand how it works. More specifically, I understand that create_table is a method and :posts is a parameter, but I don't understand the rest of the code.
Brace for it :)
create_table is a method. :posts is a symbol that is passed as parameter. Parenthesis are optional, so it looks odd but it is a simple method call.
Everything between do and end is a code block. It is one of the many ways how to pass code as an argument to a method. Code blocks are great for common case. Other similar (but different) ways to do it is to use Proc or lambda or ->.
|t| is an argument passed into the code block by create_table. create_table will execute your code block, and will pass a table object as single argument to it. You chose to name that object t.
Now inside your code block you are calling method string on object t and passing it symbol as an argument. You are doing it four times. Again parenthesis are optional.
You are calling timestamps method on same object t. Here you are passing it one parameter, which is a Hash with the value { :null => false }.
Not only parenthesis are optional, but also curly braces are optional when passing hash as last or only parameter to a method.
null: false, is a shortcut syntax for { :null => false }.
So all of the above is equivalent to:
create_table(:posts) do |t|
t.string(:title)
t.string(:content)
t.string(:likes)
t.string(:comments)
t.timestamps({:null => false})
end
Let's first forget about Active Record and focus on the code structure itself. Here is a super simple version of that structure.
class MyBuilder
def initialize
# keys are property names, values are options
#properties = {}
end
def property(name, options={})
#properties[name] = options
end
def build
# For simplicity, just return all properties
#properties
end
end
def create_thing(name)
puts "Begin creating #{name}"
builder = MyBuilder.new
puts "Let user use the builder to define properties"
yield builder
puts "Consume the builder"
properties = builder.build
puts "Persist changes to #{name}..."
# For simplicity just print them out
p properties
puts 'done'
end
create_thing :bar do |builder|
builder.property :counter, color: 'brown'
builder.property :wine_storage, texture: 'wood'
end
Please type the code above by hand to grab some feel.
Although the code above has nothing to do with Active Record, it has the same structure as the migration.
When ever create_table is called, it instantiates a builder (of type TableDefinition), and "pushes" that builder to the block (by yielding it) in order to let user define the tables columns. The builder is consumed later by create_table when the user is done defining the columns.
One of the struggles with learning Ruby is that, like smalltalk et al, you get to pass code around as well as data.
One way you can pass code to a method is with a code block.
You can then call the code block in the method definition with yield which says "insert this block of code in place of yield":
def do_it
yield
end
do_it { 2 + 4 }
=> 6
You also get to send parameters into the code block from the method definition.
That's where the |t| comes in:
def do_it_with_ten
yield 10
end
do_it_with_ten { |t| (2 + 4) * t }
=> 60
Note that the curly braces are equivalent to do..end.
I'm guessing that this is the code you found with yield in it:
def create_table(name, options = {})
table_definition = TableDefinition.new(self)
table_definition.primary_key(options[:primary_key] || "id") unless options[:id] == false
yield table_definition
if options[:force]
drop_table(name) rescue nil
end
create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE "
create_sql << "#{name} ("
create_sql << table_definition.to_sql
create_sql << ") #{options[:options]}"
execute create_sql
end
This is exactly what you're looking for. This is the definition of the create_table method we are calling. You can see the yield with the parameter table_definition.

Displaing closure_tree in ActiveAdmin. How to create hierarhical view?

At minimal I want to achieve some indentation in index table. Like this:
+Parent
+--Child
+--Child of Child
+--Child
So I create the following:
ActiveAdmin.register Section do
config.filters = false
index do
column :name do |s|
" #{ "――" * s.depth } #{s.name}"
end
default_actions
end
controller do
def collection
Section.some_method_to_get_things_in_right_order
end
end
end
It's need to some_method return active record relation, but I haven't succeed. And ended up with this hacky way.
The sortable_tree activeadmin plugin works for me well with closure tree.
https://github.com/zorab47/active_admin-sortable_tree
It creates a simple hierarchical and sortable view.
Just put the following in your tree model resource (app/admin/TreeModel):
(assuming Rails >4, replace <..> with your values)
ActiveAdmin.register TreeModel do
config.filters = false # the default filters don't work unfortunately
permit_params <YOUR_TREE_MODEL_ATTRIBUTES>
sortable tree: true,
sorting_attribute: :<YOUR_SORT_ATTRIBUTE>,
parent_method: :parent,
children_method: :children,
roots_method: :roots
index :as => :sortable do
label :name # item content
actions
end
end

ruby/rails how to ignore a comma in when ordering

I have a price field for a product in a catalog. Sometimes the admin user is putting a comma when dealing with thousands (ex: $10,000) and sometimes he is just doing $6000. While I would like to simply tell him to do it one way or the other, I would also like to solve the issue programmatically.
The #show action responsible is here:
def show
#category = Category.find_by_url_name(params[:category_id])
#brand = Brand.find(params[:id])
#search = Product.find(:all, :conditions => ['brand_id = ? and category_id = ?', #brand.id, #category.id],
:order=> params[:order] || 'price DESC')
#products = #search.paginate(:page => params[:page], :per_page => 12 )
#meta_title = "#{#brand.name}"
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => #brand }
end
end
I also have a sort_options helper in my application helper that is providing the ordering options to the site user:
def product_sort_options
options_for_select([
['', nil],
['Newest to Oldest', 'descend_by_date'],
['Oldest to Newest', 'ascend_by_date'],
['Price: Highest to Lowest', 'descend_by_price'],
['Price: Lowest to Highest', 'ascend_by_price'],
['Name', 'ascend_by_name']
])
end
any ideas?
To make it a full answer - price should not be a string. The fact that you have 300 products now is not a big deal.
Make a migration:
rails generate migration decimalise
Then edit it (db/migrate/*decimalise.rb), and write something like this:
class Decimalise < ActiveRecord::Migration
def up
connection = ActiveRecord::Base.connection()
# kill the weird chars in the string field
connection.execute("UPDATE products SET price = REPLACE(REPLACE(price, ',', ''), '$', '')")
# convert the string column into a decimal one
change_table :products do |t|
# adjust for your use case - this gives you values up to 9999999.99
# if you need more, increase the 10
t.column :price, :decimal, :precision => 10, :scale => 2
end
end
def down
change_table :products do |t|
t.column :price, :string, :limit => 10
end
end
end
then finally, run
rake db:migrate
(untested, you will probably need to tweak. also, back up your DB before any tinkering - I'll not be responsible for any data loss you suffer)
EDIT One thing I forgot: how to print it out.
<%= number_to_currency #product.price %>
should give you something like $1,999.99 for a price of 1999.99.
You can use String.gsub to search the commas and replace them by nothing.

Globalize2 and migrations

I have used globalize2 to add i18n to an old site. There is already a lot of content in spanish, however it isn't stored in globalize2 tables. Is there a way to convert this content to globalize2 with a migration in rails?
The problem is I can't access the stored content:
>> Panel.first
=> #<Panel id: 1, name: "RT", description: "asd", proje....
>> Panel.first.name
=> nil
>> I18n.locale = nil
=> nil
>> Panel.first.name
=> nil
Any ideas?
I'm sure you solved this one way or another but here goes. You should be able to use the read_attribute method to dig out what you're looking for.
I just used the following to migrate content from the main table into a globalize2 translations table.
Add the appropriate translates line to your model.
Place the following in config/initializers/globalize2_data_migration.rb:
require 'globalize'
module Globalize
module ActiveRecord
module Migration
def move_data_to_translation_table
klass = self.class_name.constantize
return unless klass.count > 0
translated_attribute_columns = klass.first.translated_attributes.keys
klass.all.each do |p|
attribs = {}
translated_attribute_columns.each { |c| attribs[c] = p.read_attribute(c) }
p.update_attributes(attribs)
end
end
def move_data_to_model_table
# Find all of the translated attributes for all records in the model.
klass = self.class_name.constantize
return unless klass.count > 0
all_translated_attributes = klass.all.collect{|m| m.attributes}
all_translated_attributes.each do |translated_record|
# Create a hash containing the translated column names and their values.
translated_attribute_names.inject(fields_to_update={}) do |f, name|
f.update({name.to_sym => translated_record[name.to_s]})
end
# Now, update the actual model's record with the hash.
klass.update_all(fields_to_update, {:id => translated_record['id']})
end
end
end
end
end
Created a migration with the following:
class TranslateAndMigratePages < ActiveRecord::Migration
def self.up
Page.create_translation_table!({
:title => :string,
:custom_title => :string,
:meta_keywords => :string,
:meta_description => :text,
:browser_title => :string
})
say_with_time('Migrating Page data to translation tables') do
Page.move_data_to_translation_table
end
end
def self.down
say_with_time('Moving Page translated values into main table') do
Page.move_data_to_model_table
end
Page.drop_translation_table!
end
end
Borrows heavily from Globalize 3 and refinerycms.

ruby object array... or hash

I have an object now:
class Items
attr_accessor :item_id, :name, :description, :rating
def initialize(options = {})
options.each {
|k,v|
self.send( "#{k.to_s}=".intern, v)
}
end
end
I have it being assigned as individual objects into an array...
#result = []
some loop>>
#result << Items.new(options[:name] => 'name', options[:description] => 'blah')
end loop>>
But instead of assigning my singular object to an array... how could I make the object itself a collection?
Basically want to have the object in such a way so that I can define methods such as
def self.names
#items.each do |item|
item.name
end
end
I hope that makes sense, possibly I am overlooking some grand scheme that would make my life infinitely easier in 2 lines.
A few observations before I post an example of how to rework that.
Giving a class a plural name can lead to a lot of semantic issues when declaring new objects, as in this case you'd call Items.new, implying you're creating several items when in fact actually making one. Use the singular form for individual entities.
Be careful when calling arbitrary methods, as you'll throw an exception on any misses. Either check you can call them first, or rescue from the inevitable disaster where applicable.
One way to approach your problem is to make a custom collection class specifically for Item objects where it can give you the information you need on names and such. For example:
class Item
attr_accessor :item_id, :name, :description, :rating
def initialize(options = { })
options.each do |k,v|
method = :"#{k}="
# Check that the method call is valid before making it
if (respond_to?(method))
self.send(method, v)
else
# If not, produce a meaningful error
raise "Unknown attribute #{k}"
end
end
end
end
class ItemsCollection < Array
# This collection does everything an Array does, plus
# you can add utility methods like names.
def names
collect do |i|
i.name
end
end
end
# Example
# Create a custom collection
items = ItemsCollection.new
# Build a few basic examples
[
{
:item_id => 1,
:name => 'Fastball',
:description => 'Faster than a slowball',
:rating => 2
},
{
:item_id => 2,
:name => 'Jack of Nines',
:description => 'Hypothetical playing card',
:rating => 3
},
{
:item_id => 3,
:name => 'Ruby Book',
:description => 'A book made entirely of precious gems',
:rating => 1
}
].each do |example|
items << Item.new(example)
end
puts items.names.join(', ')
# => Fastball, Jack of Nines, Ruby Book
Do you know the Ruby key word yield?
I'm not quite sure what exactly you want to do. I have two interpretations of your intentions, so I give an example that makes two completely different things, one of them hopefully answering your question:
class Items
#items = []
class << self
attr_accessor :items
end
attr_accessor :name, :description
def self.each(&args)
#items.each(&args)
end
def initialize(name, description)
#name, #description = name, description
Items.items << self
end
def each(&block)
yield name
yield description
end
end
a = Items.new('mug', 'a big cup')
b = Items.new('cup', 'a small mug')
Items.each {|x| puts x.name}
puts
a.each {|x| puts x}
This outputs
mug
cup
mug
a big cup
Did you ask for something like Items.each or a.each or for something completely different?
Answering just the additional question you asked in your comment to tadman's solution: If you replace in tadman's code the definition of the method names in the class ItemsCollection by
def method_missing(symbol_s, *arguments)
symbol, s = symbol_s.to_s[0..-2], symbol_s.to_s[-1..-1]
if s == 's' and arguments.empty?
select do |i|
i.respond_to?(symbol) && i.instance_variables.include?("##{symbol}")
end.map {|i| i.send(symbol)}
else
super
end
end
For his example data you will get following outputs:
puts items.names.join(', ')
# => Fastball, Jack of Nines, Ruby Book
puts items.descriptions.join(', ')
# => Faster than a slowball, Hypothetical playing card, A book made entirely of precious gems
As I don't know about any way to check if a method name comes from an attribute or from another method (except you redefine attr_accessor, attr, etc in the class Module) I added some sanity checks: I test if the corresponding method and an instance variable of this name exist. As the class ItemsCollection does not enforce that only objects of class Item are added, I select only the elements fulfilling both checks. You can also remove the select and put the test into the map and return nil if the checks fail.
The key is the return value. If not 'return' statement is given, the result of the last statement is returned. You last statement returns a Hash.
Add 'return self' as the last line of initialize and you're golden.
Class Item
def initialize(options = {})
## Do all kinds of stuff.
return self
end
end

Resources