diff --git a/lib/widget.rb b/lib/widget.rb index 9314ee1c08..1a77d36894 100644 --- a/lib/widget.rb +++ b/lib/widget.rb @@ -1,32 +1,20 @@ -class ActionView::Base - def render_widget(widget, subject, options = {}, &block) - i = widget.new(subject) - if Rails.version.start_with? "3" - i.config = config - i._routes = _routes - else - i.output_buffer = "" - end - i._content_for = @_content_for - i.controller = controller - i.render_with_options(options, &block).html_safe - end -end - -if Rails.version.start_with? "2" - class ::String; def html_safe; self; end; end -end - class Widget < ActionView::Base include ActionView::Helpers::TagHelper include ActionView::Helpers::AssetTagHelper include ActionView::Helpers::FormTagHelper include ActionView::Helpers::JavaScriptHelper - attr_accessor :output_buffer, :controller, :config, :_content_for, :_routes + attr_accessor :output_buffer, :controller, :config, :_content_for, :_routes, :subject extend ProactiveAutoloader + def self.new(subject) + super(subject).tap do |o| + o.subject = subject + end + end + + # FIXME: There's a better one in ReportingHelper, remove this one def l(s) ::I18n.t(s.to_sym, :default => s.to_s.humanize) end @@ -38,4 +26,26 @@ class Widget < ActionView::Base def protect_against_forgery? false end + + module RenderWidgetInstanceMethods + def render_widget(widget, subject, options = {}, &block) + i = widget.new(subject) + if Rails.version.start_with? "3" + i.config = config + i._routes = _routes + else + i.output_buffer = "" + end + i._content_for = @_content_for + i.controller = respond_to?(:controller) ? controller : self + i.render_with_options(options, &block) + end + end +end + +ActionView::Base.send(:include, Widget::RenderWidgetInstanceMethods) +ActionController::Base.send(:include, Widget::RenderWidgetInstanceMethods) +if Rails.version.start_with? "2" + class ::String; def html_safe; self; end; end end +class ::String; def write(s); concat(s); end; end diff --git a/lib/widget/base.rb b/lib/widget/base.rb index b44567b4d6..970a554332 100644 --- a/lib/widget/base.rb +++ b/lib/widget/base.rb @@ -1,20 +1,72 @@ class Widget::Base < Widget - attr_reader :engine + attr_reader :engine, :output + + def self.dont_cache! + @dont_cache = true + end + + def self.dont_cache? + @dont_cache + end def initialize(query) @query = query @engine = query.class end + ## + # Write a string to the canvas. The string is marked as html_safe. + # This will write twice, if @cache_output is set. + def write(str) + @output ||= "".html_safe + @output.write str.html_safe + @cache_output.write(str.html_safe) if @cache_output + end + + ## + # Render this widget. Abstract method. Needs to call #write at least once def render raise NotImplementedError, "#render is missing in my subclass" end + ## + # Render this widget, passing options. + # Available options: + # :to => canvas - The canvas (streaming or otherwise) to render to. Has to respond to #write def render_with_options(options = {}, &block) - if canvas = options[:to] - canvas << "\n" << render(&block) + set_canvas(options[:to]) if options.has_key? :to + render_with_cache(options, &block) + @output + end + + def cache_key + "#{self.class.name}/#{subject.hash}" + end + + private + + def cache? + !self.class.dont_cache? + end + + ## + # Render this widget or serve it from cache + def render_with_cache(options = {}, &block) + if Rails.cache.exist? cache_key and cache? + write Rails.cache.fetch(cache_key) else render(&block) + Rails.cache.write(cache_key, @cache_output || @output) + end + end + + ## + # Set the canvas. If the canvas object isn't a string (e.g. cannot be cached easily), + # a @cache_output String is created, that will mirror what is being written to the canvas. + def set_canvas(canvas) + unless canvas.respond_to? :to_str + @cache_output = @output || "".html_safe end + @output = canvas end end diff --git a/lib/widget/controls/apply.rb b/lib/widget/controls/apply.rb index 4851a452c8..d8bcecbde0 100644 --- a/lib/widget/controls/apply.rb +++ b/lib/widget/controls/apply.rb @@ -1,6 +1,7 @@ + class Widget::Controls::Apply < Widget::Base def render - link_to content_tag(:span, content_tag(:em, l(:button_apply))), {}, + write link_to content_tag(:span, content_tag(:em, l(:button_apply))), {}, :href => "#", :id => "query-icon-apply-button", :class => "button apply reporting_button", :"data-target" => url_for(:action => 'index', :set_filter => '1') diff --git a/lib/widget/controls/clear.rb b/lib/widget/controls/clear.rb index f460105a8d..35c0a7bef5 100644 --- a/lib/widget/controls/clear.rb +++ b/lib/widget/controls/clear.rb @@ -1,5 +1,7 @@ + class Widget::Controls::Clear < Widget::Base def render - link_to content_tag(:span, content_tag(:em, l(:"button_clear"), :class => "button-icon icon-clear")), '#', :id => 'query-link-clear', :class => 'button secondary' + write link_to(content_tag(:span, content_tag(:em, l(:"button_clear"), :class => "button-icon icon-clear")), + '#', :id => 'query-link-clear', :class => 'button secondary') end end diff --git a/lib/widget/controls/delete.rb b/lib/widget/controls/delete.rb index b60d6ad07a..cb8859c171 100644 --- a/lib/widget/controls/delete.rb +++ b/lib/widget/controls/delete.rb @@ -19,6 +19,6 @@ class Widget::Controls::Delete < Widget::Base end question + options end - button + popup + write(button + popup) end end diff --git a/lib/widget/controls/query_name.rb b/lib/widget/controls/query_name.rb index 5cfe51f15e..7fb9ca2b52 100644 --- a/lib/widget/controls/query_name.rb +++ b/lib/widget/controls/query_name.rb @@ -14,7 +14,7 @@ class Widget::Controls::QueryName < Widget::Base options["data-update-url"] = url_for(:action => "rename", :id => @query.id) options["data-is_new"] = @query.new_record? end - content_tag(:span, name, options) + icon + write(content_tag(:span, name, options) + icon) end def translations diff --git a/lib/widget/controls/save.rb b/lib/widget/controls/save.rb index 900d3491dc..68891ba656 100644 --- a/lib/widget/controls/save.rb +++ b/lib/widget/controls/save.rb @@ -1,7 +1,7 @@ class Widget::Controls::Save < Widget::Base def render return "" if @query.new_record? - link_to content_tag(:span, content_tag(:em, l(:button_save)), :class => "button-icon icon-save"), {}, + write link_to content_tag(:span, content_tag(:em, l(:button_save)), :class => "button-icon icon-save"), {}, :href => "#", :id => "query-breadcrumb-save", :class => "button secondary", :title => l(:button_save), diff --git a/lib/widget/controls/save_as.rb b/lib/widget/controls/save_as.rb index 5ae48ed33e..93f05d7fc6 100644 --- a/lib/widget/controls/save_as.rb +++ b/lib/widget/controls/save_as.rb @@ -10,7 +10,7 @@ class Widget::Controls::SaveAs < Widget::Base button = link_to content_tag(:span, content_tag(:em, link_name, :class => "button-icon icon-save-as")), "#", :class => "button secondary", :id => 'query-icon-save-as', :title => link_name - button + render_popup + write(button + render_popup) end def render_popup_form diff --git a/lib/widget/filters.rb b/lib/widget/filters.rb index c924cdd107..1730d6a469 100644 --- a/lib/widget/filters.rb +++ b/lib/widget/filters.rb @@ -17,7 +17,7 @@ class Widget::Filters < Widget::Base :class => "select-small", :name => nil end - content_tag(:div, table + select) + write content_tag(:div, table + select) end def selectables @@ -72,4 +72,4 @@ class Widget::Filters < Widget::Base end render_widget Filters::RemoveButton, f, :to => html end -end \ No newline at end of file +end diff --git a/lib/widget/filters/date.rb b/lib/widget/filters/date.rb index dafcf4c3a6..8ea167e5f0 100644 --- a/lib/widget/filters/date.rb +++ b/lib/widget/filters/date.rb @@ -23,7 +23,7 @@ class Widget::Filters::Date < Widget::Filters::Base name = "values[#{filter_class.underscore_name}][]" id_prefix = "#{filter_class.underscore_name}_" - content_tag :td do + write(content_tag :td do arg1 = content_tag :span, :id => "#{id_prefix}arg_1", :class => "filter_values" do text1 = text_field_tag name, @filter.values.first.to_s, :size => 10, :class => "select-small", :id => "#{id_prefix}arg_1_val" cal1 = calendar_for("#{id_prefix}arg_1_val") @@ -35,6 +35,6 @@ class Widget::Filters::Date < Widget::Filters::Base text2 + cal2 end arg1 + arg2 - end + end) end end diff --git a/lib/widget/filters/label.rb b/lib/widget/filters/label.rb index bc520d6e93..a43cf28794 100644 --- a/lib/widget/filters/label.rb +++ b/lib/widget/filters/label.rb @@ -1,6 +1,7 @@ + class Widget::Filters::Label < Widget::Filters::Base def render - content_tag :td, :width => 150 do + write(content_tag :td, :width => 150 do options = { :id => filter_class.underscore_name } if (engine::Filter.all.any? {|f| f.dependent == filter_class}) options.merge! :class => 'dependent-filter-label' @@ -8,6 +9,6 @@ class Widget::Filters::Label < Widget::Filters::Base content_tag :label, options do l(filter_class.label) end - end + end) end end diff --git a/lib/widget/filters/multi_choice.rb b/lib/widget/filters/multi_choice.rb index 365a982692..a7864120b2 100644 --- a/lib/widget/filters/multi_choice.rb +++ b/lib/widget/filters/multi_choice.rb @@ -1,8 +1,9 @@ + class Widget::Filters::MultiChoice < Widget::Filters::Base def render filterName = filter_class.underscore_name - content_tag :td do + write(content_tag :td do content_tag :div, :id => "#{filterName}_arg_1", :class => "filter_values" do choices = filter_class.available_values.each_with_index.map do |(label, value), i| opts = { @@ -21,11 +22,11 @@ class Widget::Filters::MultiChoice < Widget::Filters::Base content_tag :div, choices.join.html_safe, :id => "#{filter_class.underscore_name}_arg_1_val" end - end + end) end - + private - + def translate(label) if label.is_a?(Symbol) ::I18n.t(label) @@ -33,5 +34,5 @@ class Widget::Filters::MultiChoice < Widget::Filters::Base label end end - + end diff --git a/lib/widget/filters/multi_values.rb b/lib/widget/filters/multi_values.rb index 5872d540c5..f1015fef91 100644 --- a/lib/widget/filters/multi_values.rb +++ b/lib/widget/filters/multi_values.rb @@ -1,7 +1,8 @@ + class Widget::Filters::MultiValues < Widget::Filters::Base def render - content_tag :td do + write(content_tag :td do content_tag :div, :id => "#{filter_class.underscore_name}_arg_1", :class => "filter_values" do select_options = { :style => "vertical-align: top;", # FIXME: Do CSS :name => "values[#{filter_class.underscore_name}][]", @@ -52,6 +53,6 @@ class Widget::Filters::MultiValues < Widget::Filters::Base :"data-filter-name" => filter_class.underscore_name box + plus end - end + end) end end diff --git a/lib/widget/filters/operators.rb b/lib/widget/filters/operators.rb index 3b5f7e908e..fcacd92a42 100644 --- a/lib/widget/filters/operators.rb +++ b/lib/widget/filters/operators.rb @@ -1,6 +1,7 @@ + class Widget::Filters::Operators < Widget::Filters::Base def render - content_tag :td, :width => 100 do + write(content_tag :td, :width => 100 do hide_select_box = filter_class.available_operators.count == 1 options = {:class => "select-small filters-select filter_operator", :style => "vertical-align: top", # FIXME: put into CSS @@ -19,6 +20,6 @@ class Widget::Filters::Operators < Widget::Filters::Base l(filter_class.available_operators.first.label) end hide_select_box ? select_box + label : select_box - end + end) end end diff --git a/lib/widget/filters/remove_button.rb b/lib/widget/filters/remove_button.rb index a8c6bbde40..3cdbd16664 100644 --- a/lib/widget/filters/remove_button.rb +++ b/lib/widget/filters/remove_button.rb @@ -1,11 +1,11 @@ class Widget::Filters::RemoveButton < Widget::Filters::Base def render - content_tag :td, :width => "25px" do + write( content_tag :td, :width => "25px" do hidden_field = tag :input, :id => "rm_#{filter_class.underscore_name}", :name => "fields[]", :type => "hidden", :value => "" button = tag :input, :type => "button", :value => "", :class => "icon filter_rem icon-filter-rem" content_tag(:div, hidden_field + button, :id => "rm_box_#{filter_class.underscore_name}", :class => "remove-box") - end + end) end end diff --git a/lib/widget/filters/text_box.rb b/lib/widget/filters/text_box.rb index ae25d29b67..d32dec5c96 100644 --- a/lib/widget/filters/text_box.rb +++ b/lib/widget/filters/text_box.rb @@ -1,6 +1,6 @@ class Widget::Filters::TextBox < Widget::Filters::Base def render - content_tag :td do + write(content_tag :td do content_tag :div, :id => "#{filter_class.underscore_name}_arg_1", :class => "filter_values" do text_field_tag("values[#{filter_class.underscore_name}]", "", :size => "6", @@ -8,6 +8,6 @@ class Widget::Filters::TextBox < Widget::Filters::Base :id => "#{filter_class.underscore_name}_arg_1_val", :'data-filter-name' => filter_class.underscore_name) end - end + end) end end diff --git a/lib/widget/group_bys.rb b/lib/widget/group_bys.rb index 5265204d6a..5639990ebe 100644 --- a/lib/widget/group_bys.rb +++ b/lib/widget/group_bys.rb @@ -51,10 +51,10 @@ class Widget::GroupBys < Widget::Base end def render - content_tag :div, :id => 'group_by_area' do + write(content_tag :div, :id => 'group_by_area' do out = render_group 'columns', @query.group_bys(:column) out += render_group 'rows', @query.group_bys(:row) out.html_safe - end + end) end end diff --git a/lib/widget/settings.rb b/lib/widget/settings.rb index f134cef9c2..5634eff8a1 100644 --- a/lib/widget/settings.rb +++ b/lib/widget/settings.rb @@ -1,6 +1,6 @@ class Widget::Settings < Widget::Base def render - form_tag("#", {:id => 'query_form', :method => :post}) do + write(form_tag("#", {:id => 'query_form', :method => :post}) do content_tag :div, :id => "query_form_content" do fieldsets = render_widget Widget::Settings::Fieldset, @query, { :type => "filter" } do @@ -21,6 +21,6 @@ class Widget::Settings < Widget::Base fieldsets + controls end - end + end) end end diff --git a/lib/widget/settings/fieldset.rb b/lib/widget/settings/fieldset.rb index c4f71d2d22..affe251548 100644 --- a/lib/widget/settings/fieldset.rb +++ b/lib/widget/settings/fieldset.rb @@ -1,4 +1,6 @@ class Widget::Settings::Fieldset < Widget::Base + dont_cache! + def render_with_options(options, &block) @type = options.delete(:type) || "filter" @id = "#{@type}-settings" @@ -7,9 +9,9 @@ class Widget::Settings::Fieldset < Widget::Base end def render - content_tag :fieldset, :id => @id, :class => "collapsible collapsed" do + write(content_tag :fieldset, :id => @id, :class => "collapsible collapsed" do html = content_tag :legend, l(@label), :onclick => "toggleFieldset(this);" #FIXME: onclick html + yield - end + end) end end diff --git a/lib/widget/table.rb b/lib/widget/table.rb index b6c9322d44..d37bf4b657 100644 --- a/lib/widget/table.rb +++ b/lib/widget/table.rb @@ -1,5 +1,6 @@ class Widget::Table < Widget::Base extend Report::InheritedAttribute + include ReportingHelper attr_accessor :debug attr_accessor :fields @@ -10,65 +11,65 @@ class Widget::Table < Widget::Base super end - def debug? - !!debug - end - - def show_result(*args) - map :show_result, *args - end - - def show_row(*args) - map :show_row, *args - end - - def label_for(*args) - map :label, *args - end - - def entry_for(*args) - map :entry, *args - end - - def edit_content(*args) - map :edit, *args - end - - def debug_fields(*args) - map :debug_fields, *args - end - - def raw_result(result) - mapped = mapping[:raw_result] - if mapped - mapped.call self, result - else - show_result(result, 0) - end - end - - def show_field(field, *args) - mapped = mapping[:show_field] - if mapped - mapped.call self, field, *args - else - engine::Chainable.mapping_for(field).first.call field, *args - end - end - - def map(to_map, *args) - fail "Table Widget #{self.class} needs a mapping for :#{to_map}" unless mapping[to_map] - mapping[to_map.to_sym].call self, *args - end - - def render_with_options(options = {}, &block) - @fields ||= (options[:fields] || @query.result.important_fields) - @debug ||= (options[:debug] || false) - @mapping ||= options[:mapping] - fail "mappings need to respond to #call" if mapping.values.any? { |val| not val.respond_to? :call } - canvas = options[:to] ? options[:to] << "\n" : "" - canvas << render(&block) - end + # def debug? + # !!debug + # end + + # def show_result(*args) + # map :show_result, *args + # end + + # def show_row(*args) + # map :show_row, *args + # end + + # def label_for(*args) + # map :label, *args + # end + + # def entry_for(*args) + # map :entry, *args + # end + + # def edit_content(*args) + # map :edit, *args + # end + + # def debug_fields(*args) + # map :debug_fields, *args + # end + + # def raw_result(result) + # mapped = mapping[:raw_result] + # if mapped + # mapped.call self, result + # else + # show_result(result, 0) + # end + # end + + # def show_field(field, *args) + # mapped = mapping[:show_field] + # if mapped + # mapped.call self, field, *args + # else + # engine::Chainable.mapping_for(field).first.call field, *args + # end + # end + + # def map(to_map, *args) + # fail "Table Widget #{self.class} needs a mapping for :#{to_map}" unless mapping[to_map] + # mapping[to_map.to_sym].call self, *args + # end + + # def render_with_options(options = {}, &block) + # @fields ||= (options[:fields] || @query.result.important_fields) + # @debug ||= (options[:debug] || false) + # @mapping ||= options[:mapping] + # fail "mappings need to respond to #call" if mapping.values.any? { |val| not val.respond_to? :call } + # canvas = options[:to] ? options[:to] << "\n" : "" + # canvas << render(&block) + # end def fancy_table if @query.depth_of(:row) == 0 diff --git a/lib/widget/table/report_table.rb b/lib/widget/table/report_table.rb index dbad5d5d9a..e3ff1c3628 100644 --- a/lib/widget/table/report_table.rb +++ b/lib/widget/table/report_table.rb @@ -7,129 +7,113 @@ class Widget::Table::ReportTable < Widget::Table @walker = query.walker end - def render - configure_walker - content = content_tag :table, :class => 'list report' do - header + footer + body + def configure_query + if @query.depth_of(:row) == 0 + @query.row(:singleton_value) + elsif @query.depth_of(:column) == 0 + @query.column(:singleton_value) end - content += (debug_content if debug?) end def configure_walker - walker.for_final_row do |row, cells| - final_row_html = content_tag :th, :class => 'normal inner left' do - "#{show_row(row)}#{debug_fields(row)}" - end - final_row_html += cells.join.html_safe - final_row_html += content_tag :th, :class => 'normal inner right' do - "#{show_result(row)}#{debug_fields(row)}" - end - final_row_html + @walker.for_final_row do |row, cells| + html = "#{show_row row}#{debug_fields(row)}" + html << cells.join + html << "#{show_result(row)}#{debug_fields(row)}" + html.html_safe end - walker.for_row do |row, subrows| + @walker.for_row do |row, subrows| subrows.flatten! unless row.fields.empty? - subrows[0] = '' - subrows[0] += content_tag(:th, :class => 'top left', :rowspan => subrows.size) do - "#{show_row(row)}#{debug_fields(row)}" - end - subrows[0].gsub("class='normal'", "class='top'") - subrows[0] += content_tag(:th, :class => 'top right', :rowspan => subrows.size) do - "#{show_result(row)}#{debug_fields(row)}" - end + subrows[0] = %Q{ + #{show_row row}#{debug_fields(row)} + #{subrows[0].gsub("class='normal", "class='top")} + #{show_result(row)}#{debug_fields(row )} + }.html_safe end subrows.last.gsub!("class='normal", "class='bottom") subrows.last.gsub!("class='top", "class='bottom top") subrows end - walker.for_empty_cell do - content_tag(:td, :class =>'normal empty') do - " " - end - end + @walker.for_empty_cell { " ".html_safe } - walker.for_cell do |result| - content_tag :td, :class => 'normal right' do - "#{show_result(result)}#{debug_fields(result)}" - end + @walker.for_cell do |result| + "#{show_result result}#{debug_fields(result)}".html_safe end end - def header - header_content = "" + def render + configure_query + configure_walker + write "" + render_thead + render_tfoot + render_tbody + write "
" + end + + def render_thead + write "" walker.headers do |list, first, first_in_col, last_in_col| - header_content += '' if first_in_col - if first - header_content += content_tag :th, :rowspan => @query.depth_of(:column), :colspan => @query.depth_of(:row) do - "" - end - end + write '' if first_in_col + write "" if first list.each do |column| - opts = { :colspan => column.final_number(:column) } - opts.merge!(:class => "inner") if column.final?(:column) - header_content += content_tag :th, opts do - show_row column - end - end - if first - header_content += content_tag :th, :rowspan => @query.depth_of(:column), :colspan => @query.depth_of(:row) do - "" - end - end - header_content += '' if last_in_col + write "" + write show_row(column) + write "" + end + write "" if first + write '' if last_in_col end - content_tag :thead, header_content.html_safe + write "" end - def footer - reverse_headers = "" + def render_tfoot + write "" walker.reverse_headers do |list, first, first_in_col, last_in_col| - if first_in_col - reverse_headers += '' - if first - reverse_headers += content_tag :th, :rowspan => @query.depth_of(:column), :colspan => @query.depth_of(:row), :class => 'top' do - " " - end - end + write "" if first_in_col + if first + write " " end - list.each do |column| - opts = { :colspan => column.final_number(:column) } - opts.merge!(:class => "inner") if first - reverse_headers += content_tag :th, opts do - "#{show_result(column)}#{debug_fields(column)}" - end + write "' + write show_result(column) + # FIXME: write debug_fields(column) + write "" end if last_in_col if first - reverse_headers += content_tag :th, - :rowspan => @query.depth_of(:column), - :colspan => @query.depth_of(:row), - :class => 'top result' do - show_result @query - end + write "" + write show_result(@query) + write "" end - reverse_headers += '' + write "" end end - content_tag :tfoot, reverse_headers.html_safe + write "" end - def body + def render_tbody + write "" first = true - walker_body = "" + odd = true walker.body do |line| if first line.gsub!("class='normal", "class='top") first = false end - walker_body += content_tag :tr, :class => cycle("odd", "even") do - line.html_safe - end + write "#{line}" + odd = !odd end - content_tag :tbody, walker_body.html_safe + write "" end def debug_content