ActiveRecord integrated too! (jump)
$ gem install rose
Start with any Ruby Object
class Flower < Struct.new(:id, :type, :color, :age)
end
Specify your column headers and how values should be generated
Rose.make(:poem, :class => Flower) do # :class restricts reporting to Flowers
rows do
column(:type => "Type")
column("Color", &:color) # Equivalent to column(:color => "Color")
# Equivalent to column("Color") { |item| item.color }
end
end
Of the DSL, rows {} is required, the rest is optional.
Rose(:poem).bloom([
Flower.new(0, :roses, :red),
Flower.new(1, :violets, :blue)
])
Rose(:poem).bloom returns a Ruport::Data::RoseTable (which is just a Ruport::Data::Table with a few extra methods). And like it’s predecessor, you can output to Text, HTML, PDF, CSV, etc. For example:
+-----------------+
| Type | Color |
+-----------------+
| roses | red |
| violets | blue |
+-----------------+
Sort :ascending (default) or :descending
Rose.make(:sorted_by_age) {
rows do
column(:type => "Type")
column(:color => "Color")
column(:age => "Age")
end
# Notice I'm referring to the column header "Age" instead of the attribute :age
sort("Age", :descending)
}
Sorting happens with the text value of each cell (data is coerced to a String so it can be displayed in the report). If String comparisons don’t satisfy you, or you just want custom sorting
Rose.make(:sort_it_my_way) {
rows do
column(:type => "Type")
column(:age => "Age")
end
sort("Age", :descending) { |age| age.to_i * 2 }
}
You can apply filtering to reject rows from your report.
Rose.make(:no_blue_please) {
rows do
column(:type => "Type")
column(:color => "Color")
column(:age => "Age")
end
filter do |row|
# Notice I'm using "Color", not :color
row["Color"] != "blue"
end
}
We use “Color” instead of :color, because row is a Ruport::Data::Record.
Do sigma-type operations on your report, for say if you wanted a summary of each roses’ petal count distribution.
Rose.make(:count_all_colors) {
rows do
column(:type => "Type")
column(:color => "Color")
end
summary("Type") do
column("Color") { |colors| colors.uniq.join(", ") }
column("Count") { |colors| colors.size }
end
}
For a pivot with row Color, column Type, and data Ages
Rose.make(:pivot_example) {
rows do
column(:type => "Type")
column(:color => "Color")
column(:age => "Age")
end
pivot("Color", "Type") do |rows|
# Sum of Ages
rows.map(&:Age).map(&:to_i).inject(0) { |sum,x| sum+x }
end
}
Not only can you output data, you can import data too!
Rose.make(:with_find_and_update) do
rows do
identity(:id => "ID") # !!!
column(:type => "Type")
column(:color => "Color")
column(:age => "Age")
end
# The bare minimum for an importable report
roots do
# find is optional. By default will return first item with item["ID"] == idy
find do |items, idy|
items.find { |item| item.id.to_s == idy }
end
update do |item, updates|
item.color = updates["Color"]
end
end
end
In order to update via import, you must specify one column as the identity column. Rose will pass in the correct cell as idy in your find {}
Assuming
@flowers = [
Flower.new(0, :roses, :red, 1),
Flower.new(1, :violets, :blue, 2),
Flower.new(2, :roses, :red, 3)
]
Provide hash :with, paired idy => updates.
Rose(:with_find_and_update).photosynthesize(@flowers, {
:with => {
"0" => { "Color" => "blue" }
}
})
“0” will be passed into find {}, returning @flowers.first. That will then be passed into update {} as item, along with { “Color” => “blue” } as updates. The resulting @flowers would be
[ #<struct Flower id=0, type=:roses, color="blue", age=1>,
#<struct Flower id=1, type=:violets, color=:blue, age=2>,
#<struct Flower id=2, type=:roses, color=:red, age=3>
]
Rose(:with_find_and_update).photosynthesize(@flowers, {
:csv_file => "change_flowers.csv"
})
Where change_flowers.csv is
ID,Type,Color,Age
0,roses,blue,1
1,violets,red,2
2,roses,green,3
Would result in
[ #<struct Flower id=0, type=:roses, color="blue", age=1>,
#<struct Flower id=1, type=:violets, color="red", age=2>,
#<struct Flower id=2, type=:roses, color="green", age=3>
]
Sometimes you only want to see changes and not commit them (especially in the case of ActiveRecord)
Rose.make(:with_preview) do
rows do
identity(:id => "ID")
column(:type => "Type")
column(:color => "Color")
column(:age => "Age")
end
roots do
preview_update do |item, updates|
# For example...
item.preview(true); item.color = updates["Color"]
end
update { raise Exception, "you shouldn't be calling me" }
end
end
Rose(:with_preview).photosynthesize(@flowers, {
:with => {
"0" => { "Color" => "blue" }
},
:preview => true # This is new
})
Rose(:with_preview).photosynthesize(@flowers, {
:with => "change_flowers.csv",
:preview => true # This is new
})
What to do when additional information shows up in your import? Create!
Rose.make(:with_preview_create) do
rows do
identity(:id => "ID")
column(:type => "Type")
column(:color => "Color")
column(:age => "Age")
end
roots do
preview_create { |idy, updates| # Initialize }
create { |idy, updates| # Initialize and save }
update {}
end
end
Rose will create when the find {} returns nil. And depending on whether or not :preview is true, it will call preview_create or create, respectively.
config.gem 'rose', :lib => 'rose/active_record'
The API is the same for the most part, except:
Employee.rose(:department_salaries) { ... }
# Almost equivalent to
# Rose.make(:department_salaries, :class => Employee)
Employee.rose_for(:department_salaries)
# Almost equivalent to
# Rose(:department_salaries).photosynthesize(Employee.all)
Employee.rose_for(:department_salaries, :conditions => ["salary <> ?", nil])
# Almost equivalent to
# Rose(:department_salaries).photosynthesize(
# Employee.find(:all, :conditions => ["salary <> ?", nil])
# )
Employee.root_for(:department_salaries, {
:with => {
"1" => { "Title" => "New Title" }
},
:preview => true
})
# Almost equivalent to
# Rose(:department_salaries).photosynthesize(
# Employee.find(:all, :conditions => ["salary <> ?", nil]), {
# :with => {
# "1" => { "Title" => "New Title" }
# },
# :preview => true
# })
I say “Almost”, because for AR, seedlings are namespaced within the class. If you want direct access, you can use Employee.seedlings(:department_salaries).
Also, for AR, reporting happens within a transaction that is rolled-back on completion to prevent unwanted commits (at least on the :class)…
Rose obeys order of execution. If you do summary first and then sort, you can only sort on the resulting column headers from your summary.
To illustrate
Rose.make(:okay) {
rows do
column("Type", &:type)
column(:color => "Color")
end
sort("Type", :descending)
summary("Type") do
column("Color") { |colors| colors.join(", ") }
end
}
is valid, but
Rose.make(:not_okay) {
rows do
column("Type", &:type)
column(:color => "Color")
end
summary("Type") do
column("Color") { |colors| colors.join(", ") }
end
sort("Type", :descending)
}
is not.
gem 'ruport', '1.6.3'
Inspired by Machinist and factory_girl.
Copyright © 2010 Henry Hsu. See LICENSE for details.