Merge branch 'feature/widgets' into feature/partition

pull/6827/head
Markus Kahl 14 years ago
commit 2a6fb20e36
  1. 3
      assets/javascripts/reporting.js
  2. 15
      assets/javascripts/reporting/controls.js
  3. 43
      assets/javascripts/reporting/progressbar.js
  4. 110
      assets/javascripts/reporting/prototype_progress_bar.js
  5. 29
      assets/stylesheets/help.css
  6. 20
      assets/stylesheets/reporting.css
  7. 4
      config/locales/de.yml
  8. 5
      config/locales/en.yml
  9. 5
      init.rb
  10. 17
      lib/report.rb
  11. 56
      lib/report/controller.rb
  12. 19
      lib/report/filter/base.rb
  13. 13
      lib/report/group_by/base.rb
  14. 58
      lib/widget.rb
  15. 106
      lib/widget/base.rb
  16. 3
      lib/widget/controls/apply.rb
  17. 6
      lib/widget/controls/clear.rb
  18. 23
      lib/widget/controls/delete.rb
  19. 4
      lib/widget/controls/help.rb
  20. 14
      lib/widget/controls/query_name.rb
  21. 4
      lib/widget/controls/save.rb
  22. 15
      lib/widget/controls/save_as.rb
  23. 16
      lib/widget/filters.rb
  24. 20
      lib/widget/filters/date.rb
  25. 5
      lib/widget/filters/label.rb
  26. 5
      lib/widget/filters/multi_choice.rb
  27. 5
      lib/widget/filters/multi_values.rb
  28. 9
      lib/widget/filters/operators.rb
  29. 4
      lib/widget/filters/remove_button.rb
  30. 4
      lib/widget/filters/text_box.rb
  31. 4
      lib/widget/group_bys.rb
  32. 15
      lib/widget/settings.rb
  33. 17
      lib/widget/settings/fieldset.rb
  34. 119
      lib/widget/table.rb
  35. 23
      lib/widget/table/progressbar.rb
  36. 145
      lib/widget/table/report_table.rb

@ -62,3 +62,6 @@ Reporting.require("filters");
Reporting.require("group_bys");
Reporting.require("restore_query");
Reporting.require("controls");
Reporting.require("prototype_progress_bar");
Reporting.require("progressbar");

@ -115,6 +115,7 @@ Reporting.Controls = {
update_result_table: function (response) {
$('result-table').update(response.responseText);
Reporting.Progress.confirm_question();
},
default_failure_callback: function (response) {
@ -132,12 +133,16 @@ Reporting.onload(function () {
}
// don't concern ourselves with new queries
if ($('query_saved_name').getAttribute("data-is_new") !== null) {
Reporting.Controls.observe_click("query-icon-delete", Reporting.Controls.toggle_delete_form);
Reporting.Controls.observe_click("query-icon-delete-cancel", Reporting.Controls.toggle_delete_form);
$('delete_form').hide();
if ($('query-icon-delete') !== null) {
Reporting.Controls.observe_click("query-icon-delete", Reporting.Controls.toggle_delete_form);
Reporting.Controls.observe_click("query-icon-delete-cancel", Reporting.Controls.toggle_delete_form);
$('delete_form').hide();
}
// When saving an update of an exisiting query or apply filters, we replace the table on success
Reporting.Controls.attach_settings_callback($("query-breadcrumb-save"), Reporting.Controls.update_result_table);
if ($("query-breadcrumb-save") !== null) {
// When saving an update of an exisiting query or apply filters, we replace the table on success
Reporting.Controls.attach_settings_callback($("query-breadcrumb-save"), Reporting.Controls.update_result_table);
}
}
Reporting.Controls.observe_click("query-icon-save-as", Reporting.Controls.toggle_save_as_form);

@ -0,0 +1,43 @@
/*jslint white: false, nomen: true, devel: true, on: true, debug: false, evil: true, onevar: false, browser: true, white: false, indent: 2 */
/*global window, $, $$, Reporting, Effect, Ajax */
Reporting.Progress = {
replace_with_bar: function (element) {
var parent = element.up();
var size = parseInt(element.getAttribute('data-size'), 10) || 500;
element.remove();
var bar = Reporting.Progress.add_bar_to_parent(parent);
// Speed determined through laborous experimentation!
bar.interval = (size * (Math.log(size))) / 100000;
bar.start();
},
add_bar_to_parent: function (parent) {
parent.appendChild(new Element('div', {
'id': 'progressbar_container',
'class': 'progressbar_container'
}));
return new Control.ProgressBar('progressbar_container');
},
confirm_question: function () {
var bar = $('progressbar');
if (bar !== null && bar !== undefined) {
var size = bar.getAttribute('data-size');
var question = bar.getAttribute('data-translation');
if (confirm(question)) {
var target = bar.getAttribute("data-target");
bar.up().show();
Reporting.Progress.replace_with_bar(bar);
Reporting.Controls.send_settings_data(target, Reporting.Controls.update_result_table);
} else {
bar.toggle();
}
}
}
};
Reporting.onload(function () {
Reporting.Progress.confirm_question();
});

@ -0,0 +1,110 @@
/**
* @author Ryan Johnson <http://syntacticx.com/>
* @copyright 2008 PersonalGrid Corporation <http://personalgrid.com/>
* @package LivePipe UI
* @license MIT
* @url http://livepipe.net/control/progressbar
* @require prototype.js, livepipe.js
*/
/*global document, Prototype, Ajax, Class, PeriodicalExecuter, $, $A, Control */
if (typeof(Prototype) === "undefined") {
throw "Control.ProgressBar requires Prototype to be loaded.";
}
if (typeof(Event) === "undefined") {
throw "Control.ProgressBar requires Event to be loaded.";
}
Control.ProgressBar = Class.create({
initialize: function(container, options) {
this.progress = 0;
this.executer = false;
this.active = false;
this.poller = false;
this.container = $(container);
this.containerWidth = this.container.getDimensions().width;
this.progressContainer = $(document.createElement('div'));
this.progressContainer.setStyle({
width: this.containerWidth + 'px',
height: '100%',
position: 'absolute',
top: '0px',
right: '0px'
});
this.container.appendChild(this.progressContainer);
this.options = {
afterChange: Prototype.emptyFunction,
interval: 0.25,
step: 1,
classNames: {
active: 'progress_bar_active',
inactive: 'progress_bar_inactive'
}
};
Object.extend(this.options, options || {});
this.container.addClassName(this.options.classNames.inactive);
this.active = false;
},
setProgress: function (value) {
this.progress = value;
this.draw();
if (this.progress >= 100) {
this.stop(false);
}
this.notify('afterChange', this.progress, this.active);
},
poll: function (url, interval, ajaxOptions) {
// Extend the passed ajax options and success callback with our own.
ajaxOptions = ajaxOptions || {};
var success = ajaxOptions.onSuccess || Prototype.emptyFunction;
ajaxOptions.onSuccess = success.wrap(function (callOriginal, request) {
this.setProgress(parseInt(request.responseText, 10));
if (!this.active) {
this.poller.stop();
}
callOriginal(request);
}).bind(this);
this.active = true;
this.poller = new PeriodicalExecuter(function () {
var a = new Ajax.Request(url, ajaxOptions);
}.bindAsEventListener(this), interval || 3);
},
start: function () {
this.active = true;
this.container.removeClassName(this.options.classNames.inactive);
this.container.addClassName(this.options.classNames.active);
this.executer = new PeriodicalExecuter(this.step.bind(this, this.options.step), this.options.interval);
},
stop: function (reset) {
this.active = false;
if (this.executer) {
this.executer.stop();
}
this.container.removeClassName(this.options.classNames.active);
this.container.addClassName(this.options.classNames.inactive);
if (typeof reset === 'undefined' || reset === true) {
this.reset();
}
},
step: function (amount) {
this.active = true;
this.setProgress(Math.min(100, this.progress + amount));
},
reset: function () {
this.active = false;
this.setProgress(0);
},
draw: function () {
this.progressContainer.setStyle({
width: (100 - this.progress) + "%"
});
},
notify: function (event_name) {
if (this.options[event_name]) {
return [this.options[event_name].apply(this.options[event_name], $A(arguments).slice(1))];
}
}
});
Event.extend(Control.ProgressBar);

@ -4,20 +4,21 @@
}
.tooltip {
position: absolute;
margin-top: 3px;
margin-bottom: 3px;
padding: 3px;
width: 400px;
z-index: 256;
color: #000000;
border: 1px solid #000000;
background: #FFFFCC;
font: 12px Verdana, sans-serif;
text-align: left;
padding: -50px;
line-height: 16px;
font-size: 11px;
position: absolute;
margin-top: 3px;
margin-bottom: 3px;
padding: 3px;
width: 400px;
z-index: 256;
color: #000000;
border: 1px solid #000000;
background: #FFFFCC;
font: 12px Verdana, sans-serif;
text-align: left;
padding: -50px;
line-height: 16px;
font-size: 11px;
white-space: normal;
}
.filter-icon {

@ -386,6 +386,7 @@ td:hover .drill_down, th:hover .drill_down {
border: none;
cursor: pointer;
margin: 0px;
margin-right: 4px;
overflow: visible;
text-align: center;
white-space: nowrap;
@ -479,6 +480,25 @@ div.button_form p * {
z-index: 999;
}
#progressbar {
display: none;
}
#progressbar_container {
width: 300px;
height: 16px;
border: 1px solid #ccc;
padding: 0;
margin: 0;
position: relative;
background-color: #9A9A9A;
background-repeat: repeat-x;
}
#progressbar_container div {
background-color:#fff;
}
/***** Ajax indicator ******/
#ajax-indicator {
font-family: Verdana, sans-serif;

@ -7,6 +7,8 @@ de:
label_report: "Report"
label_columns: "Spalten"
label_rows: "Zeilen"
label_yes: Ja
label_no: Nein
label_group_by: "Gruppieren nach"
label_group_by_add: "Gruppierung hinzufügen"
@ -25,3 +27,5 @@ de:
validation_failure_date: "ist kein gültiges Datum"
validation_failure_integer: "ist keine ganze Zahl"
load_query_question: "Der Report wird %{size} Tabellen-Zellen haben, was sehr rechenintensiv sein kann. Wollen Sie dennoch versuchen, den Report durch zu führen?"
label_progress_bar_explanation: "Report wird erstellt ..."

@ -13,6 +13,8 @@ en:
label_filter: "Filter"
label_filter_plural: "Filters"
label_filter_add: Add Filter
label_yes: "Yes"
label_no: "No"
label_count: Count
label_sum: Sum
@ -24,3 +26,6 @@ en:
validation_failure_date: "is not a valid date"
validation_failure_integer: "is not a valid integer"
load_query_question: "Report will have %{size} table cells and may take some time to render. Do you still want to try rendering it?"
label_progress_bar_explanation: "Generating report..."

@ -3,3 +3,8 @@ fail "upgrade ruby version, ruby < 1.8.7 suffers from Hash#hash bug" if {:a => 1
require 'big_decimal_patch'
require 'to_date_patch'
# Defines the minimum number of cells for a 'big' report
# Big reports may be handled differently in the UI - i.e. ask the user
# if he's really sure to execute such a heavy report
Widget::Table::Progressbar.const_set 'THRESHHOLD', 2000

@ -34,7 +34,7 @@ class Report < ActiveRecord::Base
end
def serialize
# have to take the reverse to retain the original order when deserializing
# have to take the reverse group_bys to retain the original order when deserializing
self.serialized = { :filters => filters.collect(&:serialize).sort, :group_bys => group_bys.collect(&:serialize).reverse }
end
@ -130,7 +130,7 @@ class Report < ActiveRecord::Base
def_delegators :transformer, :column_first, :row_first
def_delegators :chain, :empty_chain, :top, :bottom, :chain_collect, :sql_statement, :all_group_fields, :child, :clear, :result
def_delegators :result, :each_direct_result, :recursive_each, :recursive_each_with_level, :each, :each_row, :count,
:units, :size, :final_number
:units, :final_number
def_delegators :table, :row_index, :colum_index
def to_a
@ -141,12 +141,17 @@ class Report < ActiveRecord::Base
chain.to_s
end
def hash
(self.class.name + serialize.inspect).hash
def size
size = 0
recursive_each {|r| size += r.size }
size
end
def == another_report
hash == another_report.hash
def cache_key
deserialize unless @chain
parts = [self.class.table_name.sub('_reports', '')]
parts.concat [filters.sort, group_bys].map { |l| l.map(&:cache_key).join(" ") }
parts.join '/'
end
private

@ -2,6 +2,8 @@ module Report::Controller
def self.included(base)
base.class_eval do
attr_accessor :report_engine
helper_method :current_user
helper_method :allowed_to?
include ReportingHelper
helper ReportingHelper
@ -11,24 +13,47 @@ module Report::Controller
before_filter :prepare_query, :only => [:index, :create]
before_filter :find_optional_report, :only => [:index, :show, :update, :delete, :rename]
before_filter :possibly_only_narrow_values
before_filter { @no_progress = no_progress? }
end
end
##
# Render the report. Renders either the complete index or the table only
def index
table
end
##
# Render the table partial, if we are setting filters/groups
# Render the report. Renders either the complete index or the table only
def table
if set_filter?
render :partial => 'table'
if no_progress?
table_without_progress_info
else
table_with_progress_info
end
session[report_engine.name.underscore.to_sym].delete(:name)
end
end
def table_without_progress_info
stream do |response, output|
render_widget Widget::Table::ReportTable, @query, :to => output
end
end
def table_with_progress_info
render :text => render_widget(Widget::Table::Progressbar, @query), :layout => false
end
if Rails.version.start_with? "3"
def stream(&block)
self.response_body = block
end
else
def stream(&block)
render :text => block, :layout => false
end
end
##
# Create a new saved query. Returns the redirect url to an XHR or redirects directly
def create
@ -61,7 +86,7 @@ module Report::Controller
# RecordNotFound if the query at :id does not exist
def delete
if @query
@query.destroy
@query.destroy if allowed_to? :delete, @query
else
raise ActiveRecord::RecordNotFound
end
@ -154,6 +179,12 @@ module Report::Controller
params[:set_filter].to_i == 1
end
##
# Determines if the requested table should be rendered with a progressbar
def no_progress?
!!params[:immediately]
end
##
# Return the active filters
def filter_params
@ -273,6 +304,21 @@ module Report::Controller
'user_id'
end
##
# Fallback: @current_user needs to be set for the engine
def current_user
if @current_user.nil?
raise NotImplementedError, "The #{self.class} should have set @current_user before this request"
end
@current_user
end
##
# Abstract: Implementation required in application
def allowed_to?(action, subject, user = current_user)
raise NotImplementedError, "The #{self.class} should have implemented #allowed_to?(action, subject, user)"
end
##
# Find a report if :id was passed as parameter.
# Raises RecordNotFound if an invalid :id was passed.

@ -1,3 +1,5 @@
require 'abbrev'
class Report::Filter
class Base < Report::Chainable
include Report::QueryUtils
@ -16,6 +18,17 @@ class Report::Filter
attr_accessor :values
def self.cache_key
@cache_key ||= begin
abbrev = Abbrev.abbrev(engine::Filter.all.map(&:underscore_name))
abbrev.keys.detect { |key| abbrev[key] == underscore_name }.to_s
end
end
def cache_key
self.class.cache_key + operator.to_s + Array(values).join(',')
end
##
# A Filter is 'heavy' if it possibly returns a _hughe_ number of available_values.
# In that case the UI-guys should think twice about displaying all the values.
@ -47,6 +60,12 @@ class Report::Filter
alias :dependents :dependent
end
# need this for sort
def <=> other
self.class.underscore_name <=> other.class.underscore_name
end
def self.has_dependent?
!dependents.empty?
end

@ -1,3 +1,5 @@
require 'abbrev'
class Report::GroupBy
class Base < Report::Chainable
include Report::QueryUtils
@ -16,6 +18,17 @@ class Report::GroupBy
child.filter?
end
def self.cache_key
@cache_key ||= begin
abbrev = Abbrev.abbrev(engine::GroupBy.all.map(&:underscore_name))
abbrev.keys.detect { |key| abbrev[key] == underscore_name }.to_s
end
end
def cache_key
self.class.cache_key + type.to_s[0,1]
end
##
# @param [FalseClass, TrueClass] prefix Whether or not add a table prefix the field names
# @return [Array<String,Symbol>] List of group by fields corresponding to self and all parents'

@ -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,34 @@ class Widget < ActionView::Base
def protect_against_forgery?
false
end
def method_missing(name, *args, &block)
begin
controller.send(name, *args, &block)
rescue NoMethodError
raise NoMethodError, "undefined method `#{name}' for #<#{self.class}:0x#{self.object_id}>"
end
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

@ -1,22 +1,47 @@
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
@options = {}
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)
str ||= ""
@output ||= "".html_safe
@output.write str.html_safe
@cache_output.write(str.html_safe) if @cache_output
str.html_safe
end
##
# Render this widget. Abstract method. Needs to call #write at least once
def render
raise NotImplementedError, "#render is missing in my subclass"
raise NotImplementedError, "#render is missing in my subclass #{self.class}"
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)
self.help_text = options[:help_text]
if canvas = options[:to]
canvas << "\n" << render(&block)
else
render(&block)
end
@help_text = options[:help_text]
set_canvas(options.delete(:to)) if options.has_key? :to
@options = options
render_with_cache(options, &block)
@output
end
##
@ -31,20 +56,65 @@ class Widget::Base < Widget
@help_text = text
end
def cache_key
@cache_key ||= if subject.respond_to? :cache_key
"#{self.class.name.demodulize}/#{subject.cache_key}/#{@options.sort_by(&:to_s)}"
else
subject
end
end
def cached?
cache? && Rails.cache.exist?(cache_key)
end
private
def cache?
!self.class.dont_cache?
end
##
# Render this widget or serve it from cache
def render_with_cache(options = {}, &block)
if cached?
write Rails.cache.fetch(cache_key)
else
render(&block)
Rails.cache.write(cache_key, @cache_output || @output) if cache?
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)
@cache_output = "".html_safe
@output = canvas
end
##
# Appends the Help Widget with this Widget's help text
# if it is defined to the input.
# If the help text is not defined the input is returned.
def maybe_with_help(html, options = {})
text = options[:text]
text ||= help_text unless options[:ignore_default]
if text
help = render_widget Widget::Controls::Help, text do
# Appends the Help Widget with this Widget's help text.
# If no help-text was given and no default help-text is set,
# the given default html will be printed instead.
# Params:
# - fallback_html - the html code to render if no help-text was found
# - options-hash
# - :help_text (string) - the help text to render
# - :instant_write (bool, default: true) - wether to write
# the help-widget instantly to the output-buffer.
# If set to false you should care to save the rendered text.
def maybe_with_help(fallback_html, options = {})
output = "".html_safe
if text = options[:help_text] || help_text
output += render_widget Widget::Controls::Help, text do
options
end
html + help
else
html
output += fallback_html
end
write output if options[:instant_write]
output
end
end

@ -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')

@ -1,6 +1,8 @@
class Widget::Controls::Clear < Widget::Base
def render
html = link_to content_tag(:span, content_tag(:em, l(:"button_clear"), :class => "button-icon icon-clear")), '#', :id => 'query-link-clear', :class => 'button secondary'
maybe_with_help html
html = link_to(content_tag(:span, content_tag(:em, l(:"button_clear"), :class => "button-icon icon-clear")),
'#', :id => 'query-link-clear', :class => 'button secondary')
write maybe_with_help html
end
end

@ -1,19 +1,11 @@
class Widget::Controls::Delete < Widget::Base
def render
return "" if @query.new_record?
render_button + render_popup
end
def render_button
link_to(content_tag(:span, content_tag(:em, l(:button_delete), :class => "button-icon icon-delete")),
"#",
:class => 'button secondary',
:id => 'query-icon-delete',
:title => l(:button_delete))
end
def render_popup
content_tag :div, :id => "delete_form", :class => "button_form", :style => "display:none" do
return "" if @query.new_record? or !@options[:can_delete]
button = link_to content_tag(:span, content_tag(:em, l(:button_delete), :class => "button-icon icon-delete")), "#",
:class => 'button secondary',
:id => 'query-icon-delete',
:title => l(:button_delete)
popup = content_tag :div, :id => "delete_form", :class => "button_form" do
question = content_tag :p, l(:label_really_delete_question)
options = content_tag :p do
delete_button = content_tag :span do
@ -21,11 +13,12 @@ class Widget::Controls::Delete < Widget::Base
l(:button_delete)
end
end
opt1 = link_to delete_button, url_for(:action => 'delete', :id => @query.id), :class => "button apply"
opt1 = link_to delete_button, url_for(:action => 'delete', :id => @query.id), :method => :delete, :class => "button apply"
opt2 = link_to l(:button_cancel), "#", :id => "query-icon-delete-cancel", :class => 'icon icon-cancel'
opt1 + opt2
end
question + options
end
write(button + popup)
end
end

@ -3,6 +3,8 @@
#
# Where :text is a i18n key.
class Widget::Controls::Help < Widget::Base
dont_cache!
def render
id = "tip:#{@query}"
options = {:icon => {}, :tooltip => {}}
@ -15,7 +17,7 @@ class Widget::Controls::Help < Widget::Base
"new Tooltip('target:#{@query}', 'tip:#{@query}', {className: 'tooltip'#{sai}});",
{:type => 'text/javascript'}, false
target = content_tag :a, icon + tip, icon_config(options[:icon])
target + script
write(target + script)
end
def icon_config(options)

@ -1,4 +1,6 @@
class Widget::Controls::QueryName < Widget::Base
dont_cache! # The name might change, but the query stays the same...
def render
options = { :id => "query_saved_name", "data-translations" => translations }
if @query.new_record?
@ -6,15 +8,17 @@ class Widget::Controls::QueryName < Widget::Base
icon = ""
else
name = @query.name
icon = content_tag :a, :href => "#", :class => 'breadcrumb_icon icon-edit',
:id => "query-name-edit-button", :title => "#{l(:button_rename)}" do
l(:button_rename)
if @options[:can_rename]
icon = content_tag :a, :href => "#", :class => 'breadcrumb_icon icon-edit',
:id => "query-name-edit-button", :title => "#{l(:button_rename)}" do
l(:button_rename)
end
options["data-update-url"] = url_for(:action => "rename", :id => @query.id)
end
options["data-is_public"] = @query.is_public
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

@ -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"), {},
return "" if @query.new_record? or !@options[:can_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),

@ -10,7 +10,8 @@ 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
maybe_with_help(button) + render_popup
write(button + render_popup)
maybe_with_help(button)
end
def render_popup_form
@ -18,11 +19,15 @@ class Widget::Controls::SaveAs < Widget::Base
label_tag(:query_name, l(:field_name)) +
text_field_tag(:query_name, @query.name)
end
box = content_tag :p do
label_tag(:query_is_public, l(:field_is_public)) +
check_box_tag(:query_is_public)
if @options[:can_save_as_public]
box = content_tag :p do
label_tag(:query_is_public, l(:field_is_public)) +
check_box_tag(:query_is_public)
end
name + box
else
name
end
name + box
end
def render_popup_buttons

@ -25,7 +25,7 @@ class Widget::Filters < Widget::Base
}
}
end
content_tag(:div, table + select)
write content_tag(:div, table + select)
end
def selectables
@ -78,20 +78,6 @@ class Widget::Filters < Widget::Base
render_widget Filters::MultiValues, f, :to => html
end
end
render_filter_help f, :to => html
render_widget Filters::RemoveButton, f, :to => html
end
def render_filter_help(filter, options = {})
html = content_tag :td, :width => "25px" do
if filter.help_text
render_widget Widget::Controls::Help, filter.help_text
end
end
if canvas = options[:to]
canvas << "\n" << html
else
html
end
end
end

@ -1,29 +1,15 @@
class Widget::Filters::Date < Widget::Filters::Base
def calendar_for(field_id)
include_calendar_headers_tags
image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
end
def include_calendar_headers_tags
unless @calendar_headers_tags_included
@calendar_headers_tags_included = true
content_for :header_tags do
'Calendar._FD = 1;' # Monday
javascript_include_tag('calendar/calendar') +
javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
javascript_tag('Calendar._FD = 1;') + # Monday
javascript_include_tag('calendar/calendar-setup') +
stylesheet_link_tag('calendar')
end
end
end
def render
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 +21,6 @@ class Widget::Filters::Date < Widget::Filters::Base
text2 + cal2
end
arg1 + arg2
end
end)
end
end

@ -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.dependents.include?(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

@ -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,7 +22,7 @@ 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

@ -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}][]",
@ -54,6 +55,6 @@ class Widget::Filters::MultiValues < Widget::Filters::Base
:"data-filter-name" => filter_class.underscore_name
box + plus
end
end
end)
end
end

@ -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
@ -16,9 +17,11 @@ class Widget::Filters::Operators < Widget::Filters::Base
end.join.html_safe
end
label = content_tag :label do
l(filter_class.available_operators.first.label)
if filter_class.available_operators.any?
l(filter_class.available_operators.first.label)
end
end
hide_select_box ? select_box + label : select_box
end
end)
end
end

@ -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

@ -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

@ -62,10 +62,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), true
out += render_group 'rows', @query.group_bys(:row)
out.html_safe
end
end)
end
end

@ -1,6 +1,8 @@
class Widget::Settings < Widget::Base
dont_cache! # Settings may change due to permissions
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,
@ -15,14 +17,17 @@ class Widget::Settings < Widget::Base
controls = content_tag :div, :class => "buttons form_controls" do
widgets = render_widget(Widget::Controls::Apply, @query)
render_widget(Widget::Controls::Save, @query, :to => widgets)
render_widget(Widget::Controls::SaveAs, @query, :to => widgets)
render_widget(Widget::Controls::Save, @query, :to => widgets,
:can_save => allowed_to?(:save, @query, current_user))
render_widget(Widget::Controls::SaveAs, @query, :to => widgets,
:can_save_as_public => allowed_to?(:save_as_public, @query, current_user))
render_widget(Widget::Controls::Clear, @query, :to => widgets)
render_widget(Widget::Controls::Delete, @query, :to => widgets)
render_widget(Widget::Controls::Delete, @query, :to => widgets,
:can_delete => allowed_to?(:delete, @query, current_user))
end
fieldsets + controls
end
end
end)
end
def filter_help

@ -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"
@ -8,14 +10,15 @@ class Widget::Settings::Fieldset < Widget::Base
def render
hash = self.hash
content_tag :fieldset, :id => @id, :class => "collapsible collapsed" do
content = maybe_with_help l(@label),
write(content_tag :fieldset, :id => @id, :class => "collapsible collapsed" do
html = content_tag :legend,
:show_at_id => hash.to_s,
:icon => { :class => "#{@type}-legend-icon" },
:tooltip => { :class => "#{@type}-legend-tip" }
html = content_tag :legend, content,
{:onclick => "toggleFieldset(this);", :id => hash.to_s}, false #FIXME: onclick
:icon => "#{@type}-legend-icon",
:tooltip => "#{@type}-legend-tip",
:onclick => "toggleFieldset(this);", :id => hash.to_s do #FIXME: onclick
(l(@label) + maybe_with_help(l(@label), :instant_write => false)).html_safe
end
html + yield
end
end)
end
end

@ -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

@ -0,0 +1,23 @@
class Widget::Table::Progressbar < Widget::Base
dont_cache!
def render
if Widget::Table::ReportTable.new(@query).cached? || @query.size <= THRESHHOLD
render_widget Widget::Table::ReportTable, @query, :to => (@output ||= "".html_safe)
else
write(content_tag :div, :style => "display:none" do
content_tag(:div, l(:label_progress_bar_explanation).html_safe) +
render_progress_bar
end)
end
end
def render_progress_bar
content_tag(:div, "",
:id => "progressbar",
:class => "form_controls",
:"data-query-size" => @query.size,
:"data-translation" => ::I18n.translate(:label_load_query_question, :size => @query.size),
:"data-target" => url_for(:action => 'index', :set_filter => '1', :immediately => true))
end
end

@ -7,129 +7,134 @@ 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 = "<th class='normal inner left'>#{show_row row}#{debug_fields(row)}</th>"
html << cells.join
html << "<th class='normal inner right'>#{show_result(row)}#{debug_fields(row)}</th>"
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{
<th class='top left' rowspan='#{subrows.size}'>#{show_row row}#{debug_fields(row)}</th>
#{subrows[0].gsub("class='normal", "class='top")}
<th class='top right' rowspan='#{subrows.size}'>#{show_result(row)}#{debug_fields(row )}</th>
}.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
@walker.for_empty_cell { "<td class='normal empty'>&nbsp;</td>".html_safe }
@walker.for_cell do |result|
write(' '.html_safe) # XXX: This keeps the Apache from timing out on us. Keep-Alive byte!
"<td class='normal right'>#{show_result result}#{debug_fields(result)}</td>".html_safe
end
end
walker.for_cell do |result|
content_tag :td, :class => 'normal right' do
"#{show_result(result)}#{debug_fields(result)}"
def render
configure_query
configure_walker
write "<table class='list report'>"
render_thead
render_tfoot
render_tbody
write "</table>"
render_xls_export
end
def render_tbody
write "<tbody>"
first = true
odd = true
walker.body do |line|
if first
line.gsub!("class='normal", "class='top")
first = false
end
write "<tr class='#{odd ? "odd" : "even"}'>#{line}</tr>"
odd = !odd
end
write "</tbody>"
end
def header
header_content = ""
def render_thead
write "<thead>"
walker.headers do |list, first, first_in_col, last_in_col|
header_content += '<tr>' if first_in_col
write '<tr>' if first_in_col
if first
header_content += content_tag :th, :rowspan => @query.depth_of(:column), :colspan => @query.depth_of(:row) do
write (content_tag :th, :rowspan => @query.depth_of(:column), :colspan => @query.depth_of(:row) do
""
end
end)
end
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
write (content_tag :th, opts do
show_row column
end
end)
end
if first
header_content += content_tag :th, :rowspan => @query.depth_of(:column), :colspan => @query.depth_of(:row) do
write (content_tag :th, :rowspan => @query.depth_of(:column), :colspan => @query.depth_of(:row) do
""
end
end)
end
header_content += '</tr>' if last_in_col
write '</tr>' if last_in_col
end
content_tag :thead, header_content.html_safe
write "</thead>"
end
def footer
reverse_headers = ""
def render_tfoot
write "<tfoot>"
walker.reverse_headers do |list, first, first_in_col, last_in_col|
if first_in_col
reverse_headers += '<tr>'
write '<tr>'
if first
reverse_headers += content_tag :th, :rowspan => @query.depth_of(:column), :colspan => @query.depth_of(:row), :class => 'top' do
write (content_tag :th, :rowspan => @query.depth_of(:column), :colspan => @query.depth_of(:row), :class => 'top' do
" "
end
end)
end
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 (content_tag :th, opts do
"#{show_result(column)}" #{debug_fields(column)}
end)
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 (content_tag :th,
:rowspan => @query.depth_of(:column),
:colspan => @query.depth_of(:row),
:class => 'top result' do
show_result @query
end)
end
reverse_headers += '</tr>'
write '</tr>'
end
end
content_tag :tfoot, reverse_headers.html_safe
write "</tfoot>"
end
def body
first = true
walker_body = ""
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
end
content_tag :tbody, walker_body.html_safe
def render_xls_export
write (content_tag :div, :id => "result-formats", :style => "font-size: 14px; line-height: 2;" do
link_to l(:export_as_excel), :action => :index, :format => "xls"
end)
end
def debug_content

Loading…
Cancel
Save