use current reporting-engine master

pull/6827/head
jwollert 14 years ago
parent 52c5309396
commit 1d7352e6ad
  1. 64
      redmine_reporting/app/controllers/cost_reports_controller.rb
  2. 81
      redmine_reporting/app/helpers/reporting_helper.rb
  3. 15
      redmine_reporting/app/models/cost_query.rb
  4. 26
      redmine_reporting/app/models/cost_query/chainable.rb
  5. 9
      redmine_reporting/app/models/cost_query/filter/base.rb
  6. 3
      redmine_reporting/app/models/cost_query/filter/cost_type_id.rb
  7. 19
      redmine_reporting/app/models/cost_query/filter/issue_id.rb
  8. 3
      redmine_reporting/app/models/cost_query/filter/no_filter.rb
  9. 1
      redmine_reporting/app/models/cost_query/filter/permission_filter.rb
  10. 4
      redmine_reporting/app/models/cost_query/group_by.rb
  11. 7
      redmine_reporting/app/models/cost_query/group_by/assigned_to_id.rb
  12. 7
      redmine_reporting/app/models/cost_query/group_by/author_id.rb
  13. 7
      redmine_reporting/app/models/cost_query/group_by/category_id.rb
  14. 1
      redmine_reporting/app/models/cost_query/group_by/custom_field.rb
  15. 7
      redmine_reporting/app/models/cost_query/group_by/status_id.rb
  16. 2
      redmine_reporting/app/models/cost_query/query_utils.rb
  17. 4
      redmine_reporting/app/views/cost_reports/_cost_entry_table.rhtml
  18. 2
      redmine_reporting/app/views/cost_reports/_cost_report_table.rhtml
  19. 2
      redmine_reporting/app/views/cost_reports/_filters.rhtml
  20. 94
      redmine_reporting/app/views/cost_reports/_group_by.rhtml
  21. 5
      redmine_reporting/app/views/cost_reports/_restore_query.rhtml
  22. 2
      redmine_reporting/app/views/cost_reports/_simple_cost_report_table.rhtml
  23. 21
      redmine_reporting/app/views/cost_reports/filters/_heavy_values.rhtml
  24. 83
      redmine_reporting/app/views/cost_reports/index.rhtml
  25. BIN
      redmine_reporting/assets/images/arrow_both.png
  26. BIN
      redmine_reporting/assets/images/arrow_both_hover_left.png
  27. BIN
      redmine_reporting/assets/images/arrow_both_hover_right.png
  28. BIN
      redmine_reporting/assets/images/arrow_both_remove.png
  29. BIN
      redmine_reporting/assets/images/arrow_left.png
  30. BIN
      redmine_reporting/assets/images/arrow_left_hover.png
  31. BIN
      redmine_reporting/assets/images/arrow_left_remove.png
  32. BIN
      redmine_reporting/assets/images/arrow_right.png
  33. BIN
      redmine_reporting/assets/images/arrow_right_hover.png
  34. BIN
      redmine_reporting/assets/images/remove.png
  35. BIN
      redmine_reporting/assets/images/remove_hover.png
  36. 316
      redmine_reporting/assets/javascripts/reporting.js
  37. 166
      redmine_reporting/assets/javascripts/select_list_move_optgroup.js
  38. 80
      redmine_reporting/assets/stylesheets/reporting.css
  39. 1
      redmine_reporting/config/locales/de.yml
  40. 1
      redmine_reporting/config/locales/en.yml
  41. 13
      redmine_reporting/features/filter.feature
  42. 47
      redmine_reporting/features/grouping.feature
  43. 9
      redmine_reporting/features/step_definitions/custom_steps.rb
  44. 7
      redmine_reporting/init.rb
  45. 51
      redmine_reporting/spec/models/cost_query/chaining_spec.rb
  46. 6
      redmine_reporting/spec/models/cost_query/filter_spec.rb
  47. 8
      redmine_reporting/spec/models/cost_query/operator_spec.rb

@ -4,6 +4,13 @@ class CostReportsController < ApplicationController
before_filter :generate_query, :only => [:index, :drill_down]
before_filter :set_cost_types, :only => [:index, :drill_down]
rescue_from Exception do |exception|
session.delete(:cost_query)
@custom_errors ||= []
@custom_errors << l(:error_generic)
render :layout => !request.xhr?
end
helper :reporting
include ReportingHelper
@ -80,8 +87,8 @@ class CostReportsController < ApplicationController
def http_group_parameters
if params[:groups]
rows = params[:groups][:rows].reject { |gb| gb.empty? }
columns = params[:groups][:columns].reject { |gb| gb.empty? }
rows = params[:groups][:rows]
columns = params[:groups][:columns]
end
{:rows => (rows || []), :columns => (columns || [])}
end
@ -117,8 +124,8 @@ class CostReportsController < ApplicationController
##
# We apply a project filter, except when we are just applying a brand new query
def ensure_project_scope(filters)
return if set_filter? or set_unit?
def ensure_project_scope!(filters)
return unless ensure_project_scope?
if @project
filters[:operators].merge! :project_id => "="
filters[:values].merge! :project_id => @project.id.to_s
@ -128,6 +135,10 @@ class CostReportsController < ApplicationController
end
end
def ensure_project_scope?
!(set_filter? or set_unit?)
end
##
# Build the query from the current request and save it to
# the session.
@ -135,7 +146,8 @@ class CostReportsController < ApplicationController
CostQuery::QueryUtils.cache.clear
filters = force_default? ? default_filter_parameters : filter_params
groups = force_default? ? default_group_parameters : group_params
ensure_project_scope filters
ensure_project_scope! filters
session[:cost_query] = {:filters => filters, :groups => groups}
@query = CostQuery.new
@query.tap do |q|
@ -160,29 +172,37 @@ class CostReportsController < ApplicationController
end
##
# FIXME: Split, also ugly
# This method does three things:
# set the @unit_id -> this is used in the index for determining the active unit tab
# set the @cost_types -> this is used to determine which tabs to display
# possibly set the @cost_type -> this is used to select the proper units for display
def set_cost_types(value = nil)
@cost_types = session[:cost_query][:filters][:values][:cost_type_id].try(:collect, &:to_i) || (-1..CostType.count)
@unit_id = value || params[:unit].try(:to_i) || session[:unit_id].to_i
# Determine active cost types, the currently selected unit and corresponding cost type
def set_cost_types
set_active_cost_types
set_unit
set_cost_type
end
# Determine the currently active unit from the parameters or session
# sets the @unit_id -> this is used in the index for determining the active unit tab
def set_unit
@unit_id = params[:unit].try(:to_i) || session[:unit_id].to_i
@unit_id = 0 unless @cost_types.include? @unit_id
session[:unit_id] = @unit_id
end
# Determine the active cost type, if it is not labor or money, and add a hidden filter to the query
# sets the @cost_type -> this is used to select the proper units for display
def set_cost_type
if @unit_id != 0
@query.filter :cost_type_id, :operator => '=', :value => @unit_id.to_s, :display => false
@cost_type = CostType.find(@unit_id) if @unit_id > 0
end
@available_cost_types = @cost_types.to_a
@available_cost_types.delete 0
@available_cost_types.unshift 0
@available_cost_types.map! do |id|
case id
when 0 then [0, l(:label_money)]
when -1 then [-1, l(:caption_labor)]
else [id, CostType.find(id).unit_plural ]
end
end
# set the @cost_types -> this is used to determine which tabs to display
def set_active_cost_types
unless @cost_types = session[:cost_query][:filters][:values][:cost_type_id].try(:collect, &:to_i)
relevant_cost_types = CostType.find(:all, :select => "id", :order => "id ASC").select do |t|
t.cost_entries.count > 0
end.collect(&:id)
@cost_types = [-1, 0, *relevant_cost_types]
end
end

@ -14,6 +14,7 @@ module ReportingHelper
def html_elements(filter)
return text_elements filter if CostQuery::Operator.string_operators.all? { |o| filter.available_operators.include? o }
return date_elements filter if CostQuery::Operator.time_operators.all? { |o| filter.available_operators.include? o }
return heavy_object_elements filter if filter.heavy?
object_elements filter
end
@ -24,10 +25,6 @@ module ReportingHelper
@project = project_was
end
def debug?
(!!params[:debug]) and !Rails.env.production?
end
def object_elements(filter)
[
{:name => :activate_filter, :filter_name => filter.underscore_name, :label => l(filter.label)},
@ -36,6 +33,14 @@ module ReportingHelper
{:name => :remove_filter, :filter_name => filter.underscore_name}]
end
def heavy_object_elements(filter)
[
{:name => :activate_filter, :filter_name => filter.underscore_name, :label => l(filter.label)},
{:name => :text, :text => l(:label_equals)},
{:name => :heavy_values, :filter_name => filter.underscore_name, :disable_controls => true},
{:name => :remove_filter, :filter_name => filter.underscore_name}]
end
def date_elements(filter)
[
{:name => :activate_filter, :filter_name => filter.underscore_name, :label => l(filter.label)},
@ -69,7 +74,7 @@ module ReportingHelper
end
def debug_fields(result, prefix = ", ")
prefix << result.fields.inspect << ", " << result.key.inspect if debug?
#prefix << result.fields.inspect << ", " << result.key.inspect if params[:debug]
end
def show_field(key, value)
@ -94,22 +99,23 @@ module ReportingHelper
def field_representation_map(key, value)
return l(:label_none) if value.blank?
case key.to_sym
when :activity_id then mapped value, Enumeration, "<i>#{l(:caption_material_costs)}</i>"
when :project_id then link_to_project Project.find(value.to_i)
when :user_id, :assigned_to_id then link_to_user User.find(value.to_i)
when :tyear, :units then value
when :tweek then "#{l(:label_week)} ##{value}"
when :tmonth then month_name(value.to_i)
when :category_id then IssueCategory.find(value.to_i).name
when :cost_type_id then mapped value, CostType, l(:caption_labor)
when :cost_object_id then cost_object_link value
when :issue_id then link_to_issue Issue.find(value.to_i)
when :spent_on then format_date(value.to_date)
when :tracker_id then Tracker.find(value.to_i)
when :week then "#{l(:label_week)} #%s" % value.to_i.modulo(100)
when :priority_id then IssuePriority.find(value.to_i).name
when :fixed_version_id then Version.find(value.to_i).name
when :singleton_value then ""
when :activity_id then mapped value, Enumeration, "<i>#{l(:caption_material_costs)}</i>"
when :project_id then link_to_project Project.find(value.to_i)
when :user_id, :assigned_to_id, :author_id then link_to_user User.find(value.to_i)
when :tyear, :units then value
when :tweek then "#{l(:label_week)} ##{value}"
when :tmonth then month_name(value.to_i)
when :category_id then IssueCategory.find(value.to_i).name
when :cost_type_id then mapped value, CostType, l(:caption_labor)
when :cost_object_id then cost_object_link value
when :issue_id then link_to_issue Issue.find(value.to_i)
when :spent_on then format_date(value.to_date)
when :tracker_id then Tracker.find(value.to_i)
when :week then "#{l(:label_week)} #%s" % value.to_i.modulo(100)
when :priority_id then IssuePriority.find(value.to_i).name
when :fixed_version_id then Version.find(value.to_i).name
when :singleton_value then ""
when :status_id then IssueStatus.find(value.to_i).name
else value.to_s
end
end
@ -128,8 +134,8 @@ module ReportingHelper
when -1 then l_hours(row.units)
when 0 then row.real_costs ? number_to_currency(row.real_costs) : '-'
else
cost_type = @cost_type || CostType.find(unit_id)
"#{row.units} #{row.units != 1 ? cost_type.unit_plural : cost_type.unit}"
current_cost_type = @cost_type || CostType.find(unit_id)
pluralize(row.units, current_cost_type.unit, current_cost_type.unit_plural)
end
end
@ -138,6 +144,21 @@ module ReportingHelper
struct[:values][key] = value.to_s
end
def available_cost_type_tabs(cost_types)
tabs = cost_types.to_a
tabs.delete 0 # remove money from list
tabs.unshift 0 # add money as first tab
tabs.map {|cost_type_id| [cost_type_id, cost_type_label(cost_type_id)] }
end
def cost_type_label(cost_type_id, cost_type_inst = nil, plural = true)
case cost_type_id
when -1 then l(:caption_labor)
when 0 then l(:label_money)
else (cost_type_inst || CostType.find(cost_type_id)).name
end
end
def link_to_details(result)
return '' # unless result.respond_to? :fields # uncomment to display
session_filter = {:operators => session[:cost_query][:filters][:operators].dup, :values => session[:cost_query][:filters][:values].dup }
@ -179,6 +200,20 @@ module ReportingHelper
link_to_details(row) << row.render { |k,v| show_field(k,v) }
end
def delimit(items, options = {})
options[:step] ||= 1
options[:delim] ||= '&bull;'
delimited = []
items.each_with_index do |item, ix|
if ix != 0 and ix % options[:step] == 0
delimited << "<b> #{options[:delim]} </b>" + item
else
delimited << item
end
end
delimited
end
##
# Finds the Filter-Class for as specific filter name while being careful with the filter_name parameter as it is user input.
def filter_class(filter_name)

@ -13,7 +13,19 @@ class CostQuery < ActiveRecord::Base
end
def self.chain_initializer
return @chain_initializer ||= []
@chain_initializer ||= []
end
def self.deserialize(hash)
self.new.tap do |q|
# have to take the reverse to regain the original order
hash[:filters].reverse.each {|name, opts| q.filter(name, opts) }
hash[:group_bys].reverse.each {|name, opts| q.group_by(name, opts) }
end
end
def serialize
{ :filters => filters.collect(&:serialize), :group_bys => group_bys.collect(&:serialize) }
end
def available_filters
@ -76,7 +88,6 @@ class CostQuery < ActiveRecord::Base
def filters
chain.select { |c| c.filter? }
end
def depth_of(name)
@depths ||= {}

@ -123,8 +123,10 @@ class CostQuery < ActiveRecord::Base
def initialize(child = nil, options = {})
@options = options
options.each do |key, value|
raise ArgumentError, "may not set #{key}" unless CostQuery.accepted_properties.include? key.to_s
send "#{key}=", value if value
unless self.class.extra_options.include? key
raise ArgumentError, "may not set #{key}" unless CostQuery.accepted_properties.include? key.to_s
send "#{key}=", value if value
end
end
self.child, child.parent = child, self if child
move_down until correct_position?
@ -143,6 +145,10 @@ class CostQuery < ActiveRecord::Base
URI.escape to_a.map(&:join).join(',')
end
def serialize
[self.class.to_s.demodulize, @options]
end
def move_down
reorder parent, child, self, child.child
end
@ -243,6 +249,22 @@ class CostQuery < ActiveRecord::Base
selectable false
end
# Extra options this chainable accepts that are not defined in accepted_properties
def self.extra_options(*symbols)
@extra_option ||= []
@extra_option += symbols
end
# This chainable type can only ever occur once in a chain
def self.singleton
class << self
def new(chain = nil, options = {})
return chain if chain and chain.collect(&:class).include? self
super
end
end
end
def self.last_table
@last_table ||= 'entries'
end

@ -14,6 +14,13 @@ module CostQuery::Filter
attr_accessor :values
##
# 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.
def self.heavy?
false
end
def value=(val)
self.values = [val]
end
@ -106,7 +113,7 @@ module CostQuery::Filter
def sql_statement
super.tap do |query|
arity = operator.arity
values = self.values || []
values = [*self.values].compact
values = values[0, arity] if values and arity >= 0 and arity != values.size
operator.modify(query, field, *values) unless field.empty?
end

@ -1,9 +1,10 @@
class CostQuery::Filter::CostTypeId < CostQuery::Filter::Base
label :field_cost_type
extra_options :display
def initialize(child = nil, options = {})
@display = options.delete(:display)
super
@display = options[:display]
end
def display?

@ -3,6 +3,23 @@ class CostQuery::Filter::IssueId < CostQuery::Filter::Base
def self.available_values(*)
issues = Project.visible.collect { |p| p.issues }.flatten.uniq.sort_by { |i| i.id }
issues.map { |i| ["##{i.id} #{i.subject.length>30 ? i.subject.first(26)+'...': i.subject}", i.id] }
issues.map { |i| [text_for_issue i, i.id] }
end
def self.heavy?
true
end
not_selectable! if heavy?
def self.text_for_issue(i)
i = i.first if i.is_a? Array
str = "##{i.id} "
str << (i.subject.length > 30 ? i.subject.first(26)+'...': i.subject)
end
def self.text_for_id(i)
text_for_issue Issue.find(i)
rescue ActiveRecord::RecordNotFound
""
end
end

@ -1,6 +1,7 @@
class CostQuery::Filter::NoFilter < CostQuery::Filter::Base
dont_display!
singleton
def sql_statement
CostQuery::SqlStatement.for_entries
end

@ -2,6 +2,7 @@ class CostQuery::Filter::PermissionFilter < CostQuery::Filter::Base
dont_display!
not_selectable!
db_field ""
singleton
initialize_query_with { |query| query.filter self.to_s.demodulize.to_sym }

@ -18,6 +18,10 @@ module CostQuery::GroupBy
CostQuery::GroupBy::Tyear,
CostQuery::GroupBy::UserId,
CostQuery::GroupBy::Week,
CostQuery::GroupBy::AuthorId,
CostQuery::GroupBy::AssignedToId,
CostQuery::GroupBy::CategoryId,
CostQuery::GroupBy::StatusId,
*CostQuery::GroupBy::CustomField.all
]
end

@ -0,0 +1,7 @@
module CostQuery::GroupBy
class AssignedToId < Base
join_table Issue
applies_for :label_issue_attributes
label :field_assigned_to
end
end

@ -0,0 +1,7 @@
module CostQuery::GroupBy
class AuthorId < Base
join_table Issue
applies_for :label_issue_attributes
label :field_author
end
end

@ -0,0 +1,7 @@
module CostQuery::GroupBy
class CategoryId < Base
join_table Issue
applies_for :label_issue_attributes
label :field_category
end
end

@ -1,5 +1,6 @@
module CostQuery::GroupBy
class CustomField < Base
applies_for :label_issue_attributes
extend CostQuery::CustomFieldMixin
on_prepare { group_fields table_name }
end

@ -0,0 +1,7 @@
module CostQuery::GroupBy
class StatusId < Base
join_table Issue
applies_for :label_issue_attributes
label :field_status
end
end

@ -216,4 +216,4 @@ module CostQuery::QueryUtils
super
klass.extend self
end
end
end

@ -46,7 +46,7 @@
<% end %>
</td>
</tr>
<% if debug? %>
<% if params[:debug] %>
<tr>
<td colspan='<%= list.size + 3 %>'>
<%= result.fields.reject {|k,v| list.include? k.to_sym }.inspect %>
@ -57,4 +57,4 @@
</tbody>
</table>
<%= render :partial => 'sortable_init', :locals => {:sort_first_row => true } %>
<%= render :partial => 'sortable_init', :locals => { :sort_first_row => true } %>

@ -78,7 +78,7 @@ end
</tbody>
</table>
<% if debug? %>
<% if false or params[:debug] %>
<pre>
[ Query ]

@ -17,7 +17,7 @@
<tr id="tr_<%= filter.underscore_name %>" class="filter" style="display:none" data-label="tr_<%= label.to_s %>">
<% html_elements(filter).each do |element| %>
<%= render :partial => File.join(partial_prefix, element[:name].to_s),
:locals => {:element => element, :f => f, :filter => filter} %>
:locals => {:element => element, :f => f, :filter => filter, :query => query} %>
<% end %>
</tr>
<% end %>

@ -4,35 +4,71 @@
query A CostQuery object
%>
<% grouped_gbs = CostQuery::GroupBy.all_grouped %>
<% indices = {} %>
<% CostQuery::GroupBy.all.sort_by {|gb| l(gb.label)}.each_with_index {|gb, ix| indices[gb] = ix } %>
<%#TODO: replace me with a drag&drop group_by selector %>
<div id="group_by_area">
<%= l(:label_columns) %>:
<div id="group_columns" class="drag_target drag_container">
<select id="group_by_columns" name="groups[columns][]" class="select-small" onchange="add_group_by(this);">
<option value=""></option>
<% grouped_gbs.each do |label, group_by_ary| %>
<optgroup label="<%= l(label) %>">
<% group_by_ary.select(&:display?).sort_by {|gb| l(gb.label)}.each do |group_by| %>
<option data-sort_by="<%= indices[group_by] %>" value="<%= group_by.underscore_name %>"><%= l(group_by.label)%></option>
<% end %>
<table style="border-collapse: collapse; border: 0pt none;" id="group_by_table">
<tbody>
<tr>
<td colspan="2" rowspan="2">&nbsp;</td>
<td>&nbsp;</td>
<td><h3>Columns</h3><td>
</tr>
<tr>
<td align="center" valign="top">
<input type="button" class="buttons group_by sort sortUp" onclick="moveOptionUp(this.form.group_by_columns);"/><br />
<input type="button" class="buttons group_by sort sortDown" onclick="moveOptionDown(this.form.group_by_columns);"/>
</td>
<td>
<select style="width: 180px;" size="4" name="groups[columns][]" multiple="multiple" id="group_by_columns">
</select>
</td>
</tr>
<tr>
<td>
&nbsp;
</td>
<td valign="bottom" style="padding-bottom: 0;">
<h3>Rows</h3>
</td>
<td>
&nbsp;
</td>
<td align="left" valign="top">
<input type="button" class="buttons group_by move moveUp" onclick="moveOptions(this.form.group_by_container, this.form.group_by_columns);"/>
<input type="button" class="buttons group_by move moveDown" onclick="moveOptions(this.form.group_by_columns, this.form.group_by_container);"/>
</td>
</tr>
<tr>
<td align="center" valign="top">
<input type="button" class="buttons group_by sort sortUp" onclick="moveOptionUp(this.form.group_by_rows);" /><br />
<input type="button" class="buttons group_by sort sortDown" onclick="moveOptionDown(this.form.group_by_rows);"/>
</td>
<td style="padding-left: 0pt;" valign="top">
<select style="width: 180px;" size="4" name="groups[rows][]" multiple="multiple" id="group_by_rows">
</select>
</td>
<td align="center" valign="top">
<input type="button" class="buttons group_by move moveLeft" onclick="moveOptions(this.form.group_by_container, this.form.group_by_rows);"/><br />
<input type="button" class="buttons group_by move moveRight" onclick="moveOptions(this.form.group_by_rows, this.form.group_by_container);"/>
</td>
<td>
<select style="width: 180px;" size="9" multiple="multiple" id="group_by_container">
<% CostQuery::GroupBy.all_grouped.sort_by { |label, group_by_ary| l(label) }.each do |label, group_by_ary| %>
<optgroup label="<%= l(label) %>" data-category="<%= label.to_s %>">
<% group_by_ary.sort_by { |g| l(g.label)}.each do |group_by| %>
<% next unless group_by.selectable? %>
<option value="<%= group_by.underscore_name %>" data-category="<%= label.to_s %>"><%= l(group_by.label) %></option>
<% end %>
</optgroup>
<% end %>
</select>
</div>
<%= l(:label_rows) %>:
<div id="group_rows" class="drag_target drag_container">
<select id="group_by_rows" name="groups[rows][]" class="select-small" onchange="add_group_by(this);">
<option value=""></option>
<% grouped_gbs.each do |label, group_by_ary| %>
<optgroup label="<%= l(label) %>">
<% group_by_ary.select(&:display?).sort_by {|gb| l(gb.label)}.each do |group_by| %>
<option data-sort_by="<%= indices[group_by] %>" value="<%= group_by.underscore_name %>"><%= l(group_by.label)%></option>
<% end %>
</optgroup>
<% end %>
</select>
</div>
</div>
</select>
</td>
</tr></tbody>
</table>
<%#
up &#8593;
down &#8595;
left &#8592;
right &#8594;
%>

@ -2,6 +2,7 @@
//<![CDATA[
var set_filters, set_group_bys, restore_query_inputs;
window.global_prefix = '<%= ActionController::Base.relative_url_root %>';
set_filters = function () {
// Activate recent filters on loading
@ -21,7 +22,7 @@ set_group_bys = function () {
};
restore_query_inputs = function () {
init_group_bys();
// init_group_bys();
disable_all_filters();
disable_all_group_bys();
set_filters();
@ -30,4 +31,4 @@ restore_query_inputs = function () {
restore_query_inputs();
//]]>
</script>
</script>

@ -32,7 +32,7 @@ show_units = list.include? "cost_type_id"
<% end %>
<td raw-data="<%= result.real_costs -%>"><%= show_result result %></td>
</tr>
<% if debug? %>
<% if params[:debug] %>
<tr>
<td colspan='<%= list.size + 3 %>'>
<%= result.fields.reject {|k,v| list.include? k.to_sym }.inspect %>

@ -0,0 +1,21 @@
<%#
This partial requires the following locals:
element a Hash containing the following keys:
- :name => :heavy_values
- :filter_name => String: The name of a filter (e.g. activity_id)
- :hide => Boolean: If you want this to be hidden
query => the current CostQuery
%>
<% filter = query.filters.detect {|f| f.class.underscore_name == element[:filter_name] }
values_available = !!filter && !!filter.values %>
<td <%= 'style="display:none"' if element[:hide] %> >
<div style="" id="<%= element[:filter_name] %>_arg_1" class="filter_values">
<%= filter.values.map{ |v| filter.class.text_for_id v }.join '<br />' if values_available %>
<input type="hidden"
name="values[<%= element[:filter_name] %>][]"
id="<%= element[:filter_name] %>_arg_1_val"
<%= 'disabled="disabled"' unless values_available %>
value="<%= filter.values.to_json if values_available %>"/>
</div>
</td>

@ -5,7 +5,7 @@
<%= stylesheet_link_tag 'reporting', :plugin => 'redmine_reporting' %>
<% end %>
<% if @custom_errors && !@custom_errors.empty? %>
<% if @custom_errors.present? %>
<% @custom_errors.each do |err| %>
<div class="flash error"><%= err %></div>
<% end %>
@ -16,53 +16,52 @@
<% form_for @query, :url => {:controller => 'cost_report', :action => 'new' }, :html => {:id => 'query_form', :method => :post} do |query_form| %>
<div id="query_form_content">
<fieldset id="query_fieldset" class="collapsible <%= "collapsed" unless @query.new_record? %>">
<legend onclick="toggleFieldset(this);"><%= l(:label_query) %></legend>
<div id="query_settings">
<h1><%= l(:label_filter_plural) %></h1>
<div <%= 'style="display:none;"' unless @query.new_record? %>><%= render :partial => 'filters', :locals => {:f => query_form, :query => @query} %></div>
<h1><%= l(:label_group_by) %></h1>
<div <%= 'style="display:none;"' unless @query.new_record? %>><%= render :partial => 'group_by', :locals => {:f => query_form, :query => @query} %></div>
<%= render :partial => 'restore_query', :locals => {:f => query_form, :query => @query} %>
<p class="buttons">
<%= link_to_remote "<span><em>#{l(:button_apply)}</em></span>",
{ :url => { :set_filter => 1 },
:condition => 'Ajax.activeRequestCount === 0',
:before => 'select_active_group_bys();',
:after => 'reset_group_by_selects();',
:update => "content",
:with => "Form.serialize('query_form')",
:eval_scripts => true
}, :class => 'button apply' %>
<%= link_to_function l(:button_clear), "disable_all_filters(); disable_all_group_bys();", :class => 'icon icon-reload' %>
<% if User.current.allowed_to?(:save_queries, @project, :global => true) %>
<%
#link_to l(:button_save), {}, :onclick => "$('query_form').submit(); return false;", :class => 'icon icon-save'
%>
<% end %>
</p>
</div>
<fieldset id="filters" class="collapsible <%= "collapsed" unless @query.new_record? %>">
<legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
<div <%= 'style="display:none;"' unless @query.new_record? %>><%= render :partial => 'filters', :locals => {:f => query_form, :query => @query} %></div>
</fieldset>
<fieldset id="group-by" class="collapsible <%= "collapsed" unless @query.new_record? %>">
<legend onclick="toggleFieldset(this);"><%= l(:label_group_by) %></legend>
<div <%= 'style="display:none;"' unless @query.new_record? %>><%= render :partial => 'group_by', :locals => {:f => query_form, :query => @query} %></div>
</fieldset>
<%= render :partial => 'restore_query', :locals => {:f => query_form, :query => @query} %>
<p class="buttons">
<%= link_to_remote "<span><em>#{l(:button_apply)}</em></span>",
{ :url => { :set_filter => 1 },
:before => 'selectAllOptions("group_by_rows");selectAllOptions("group_by_columns");',
:condition => 'Ajax.activeRequestCount === 0',
:update => "content",
:with => "Form.serialize('query_form')",
:eval_scripts => true
}, :class => 'button apply' %>
<%= link_to_function l(:button_clear), "disable_all_filters(); disable_all_group_bys();", :class => 'icon icon-reload' %>
<% if User.current.allowed_to?(:save_queries, @project, :global => true) %>
<%
#link_to l(:button_save), {}, :onclick => "$('query_form').submit(); return false;", :class => 'icon icon-save'
%>
<% end %>
</p>
</div>
<div class='cost_types'>
<b><%= l(:label_report) %>:</b>
<% @available_cost_types.each do |id, label| %>
<%=
<%= delimit(
available_cost_type_tabs(@cost_types).map do |id, label|
if id != @unit_id
link_to_remote label, {
:url => { :set_filter => 1, :unit => id },
:before => 'select_active_group_bys();',
:after => 'reset_group_by_selects();',
:update => "content",
:with => "Form.serialize('query_form')",
:eval_scripts => true }
else
"<b>#{label}</b>"
end
%>
<% end %>
link_to_remote label, {
:url => { :set_filter => 1, :unit => id },
:condition => 'Ajax.activeRequestCount === 0',
:update => "content",
:before => 'selectAllOptions("group_by_rows");selectAllOptions("group_by_columns");',
:with => "Form.serialize('query_form')",
:eval_scripts => true }
else
"<b>#{label}</b>"
end
end)
%>
</div>
<% end %>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 B

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 B

@ -1,19 +1,10 @@
/*global $, selectAllOptions, moveOptions */
function toggle_filter(field) {
var to_toggle, label;
label = $('label_' + field);
to_toggle = label.up().siblings();
if (label.visible()) {
to_toggle.invoke('show');
} else {
to_toggle.invoke('hide');
}
}
function make_select_accept_multiple_values(select) {
select.multiple = true;
select.size = 4;
// first option just got selected, because THAT'S the kind of world we live in
select.options[0].selected = false;
}
function make_select_accept_single_value(select) {
@ -47,6 +38,9 @@ function change_argument_visibility(field, arg_nr) {
function operator_changed(field, select) {
var option_tag, arity;
if (select === null) {
return;
}
option_tag = select.options[select.selectedIndex];
arity = parseInt(option_tag.getAttribute("data-arity"), 10);
change_argument_visibility(field, arity);
@ -77,21 +71,49 @@ function set_remove_button_visibility(field, value) {
}
}
function show_filter_callback(field, callback_func) {
function load_available_values_for_filter(filter_name, callback_func) {
var select;
select = $('' + filter_name + '_arg_1_val');
if (select !== null && select.readAttribute('data-loading') === "ajax" && select.childElements().length === 0) {
new Ajax.Updater({ success: select }, window.global_prefix + '/cost_reports/available_values', {
parameters: { filter_name: filter_name },
insertion: 'bottom',
evalScripts: false,
onCreate: function (a, b) {
$('operators_' + filter_name).disable();
$('' + filter_name + '_arg_1_val').disable();
},
onComplete: function (a, b) {
$('operators_' + filter_name).enable();
$('' + filter_name + '_arg_1_val').enable();
callback_func();
}
});
make_select_accept_single_value(select);
}
else {
callback_func();
}
}
function show_filter_callback(field, slowly, callback_func) {
var field_el = $('tr_' + field);
if (field_el !== null) {
load_available_values_for_filter(field, callback_func);
// the following command might be included into the callback_function (which is called after the ajax request) later
field_el.show();
toggle_filter(field);
$('rm_' + field).value = field;
if (slowly) {
new Effect.Appear(field_el);
} else {
field_el.show();
}
operator_changed(field, $("operators_" + field));
display_category(field_el);
}
}
function show_filter(field) {
show_filter_callback(field, function () {});
show_filter_callback(field, true, function () {});
}
function occupied_category(tr_field) {
@ -106,13 +128,21 @@ function occupied_category(tr_field) {
return false; //not hit
}
function hide_filter(field) {
var field_el = $('tr_' + field);
function hide_filter(field, slowly) {
var field_el, operator_select;
field_el = $('tr_' + field);
if (field_el !== null) {
$('rm_' + field).value = "";
field_el.hide();
toggle_filter(field);
operator_changed(field, $("operators_" + field));
if (slowly) {
new Effect.Fade(field_el);
} else {
field_el.hide();
}
operator_select = $("operators_" + field);
if (operator_select !== null) {
// in case the filter doesn't have an operator select field'
operator_changed(field, $("operators_" + field));
}
if (!occupied_category(field_el)) {
hide_category(field_el);
}
@ -146,24 +176,36 @@ function add_filter(select) {
}
function remove_filter(field) {
hide_filter(field);
hide_filter(field, true);
enable_select_option($("add_filter_select"), field);
}
function show_group_by(group_by, source) {
// find group_by option-tag in source select-box
function show_group_by(group_by, target) {
var source, group_option, i;
source = $("group_by_container");
group_option = null;
// find group_by option-tag in target select-box
for (i = 0; i < source.options.length; i += 1) {
if (source.options[i].value === group_by) {
source.value = group_by;
add_group_by(source);
group_option = source.options[i];
source.options[i] = null;
break;
}
}
// die if the appropriate option-tag can not be found
if (group_option === null) {
return;
}
// move the option-tag to the taget select-box while keepings its data
target.options[target.length] = group_option;
}
function select_operator(field, operator) {
var select, i;
select = $("operators_" + field);
if (select === null) {
return; // there is no such operator select field
}
for (i = 0; i < select.options.length; i += 1) {
if (select.options[i].value === operator) {
select.selectedIndex = i;
@ -194,34 +236,6 @@ function restore_select_values(select, values) {
}
}
function select_active_group_bys() {
[$('group_by_columns'), $('group_by_rows')].each(function (sel) {
sel.multiple = true;
sort_group_bys(sel, sel.siblings());
});
}
function sort_group_bys(select, group_bys) {
for (var k = 0; k < group_bys.length; k++) {
for (var i = 0; i < select.options.length; i++) {
if (group_bys[k].getAttribute('data-backref') == select.options[i].value) {
select.options[i].setAttribute('data-sort_by', k);
select.options[i].selected = true;
}
}
}
moveOptionsToTopLevel(select);
sortOptions(select.id);
}
function reset_group_by_selects() {
[$('group_by_columns'), $('group_by_rows')].each(function(select) {
select.multiple = false;
putOptionsIntoOpgroups(select);
select.options[0].selected = true;
});
}
function find_arguments(field) {
var args = [], arg_count = 0, arg = null;
arg = $(field + '_arg_' + (arg_count + 1) + '_val');
@ -236,8 +250,16 @@ function find_arguments(field) {
function restore_values(field, values) {
var op_select, op_arity, args, i;
op_select = $("operators_" + field);
op_arity = op_select.options[op_select.selectedIndex].getAttribute("data-arity");
if (op_select !== null) {
op_arity = op_select.options[op_select.selectedIndex].getAttribute("data-arity");
}
else {
op_arity = 0;
}
args = find_arguments(field);
if (args.size() === 0) {
return; // there are no values to set
}
if (!Object.isArray(values)) {
values = [values];
}
@ -253,166 +275,28 @@ function restore_values(field, values) {
function restore_filter(field, operator, values) {
select_operator(field, operator);
disable_select_option($("add_filter_select"), field);
show_filter_callback(field, function () {
show_filter_callback(field, true, function () {
if (typeof(values) !== "undefined") {
restore_values(field, values);
}
});
}
function add_group_by(select) {
field = select.value;
group_by = init_group_by(field + "_" + select.id);
group_by.setAttribute('data-backref', field);
select.up().appendChild(group_by);
label = init_label(group_by);
label.innerHTML = sanitized_selected(select);
select.value = "";
group_by.appendChild(label);
group_by.appendChild(init_arrow(group_by));
if (!(first_in_row(group_by))) {
update_arrow(group_by.previous());
}
disable_select_option($('group_by_columns'), field);
disable_select_option($('group_by_rows'), field);
}
function remove_group_by(arrow) {
group_by = arrow.up();
enable_select_option($('group_by_columns'), group_by.getAttribute('data-backref'));
enable_select_option($('group_by_rows'), group_by.getAttribute('data-backref'));
previous = group_by.previous();
group_by.remove();
if (previous !== null) {
update_arrow(previous);
}
}
function init_arrow(group_by) {
arrow = document.createElement('div');
arrow.setAttribute('id', group_by.id + '_arrow');
arrow.setAttribute('class', 'arrow in_row arrow_left');
arrow.src = "/plugin_assets/redmine_reporting/images/arrow_left.png";
init_arrow_hover_effects(arrow);
return arrow;
}
function init_arrow_hover_effects(arrow) {
Event.observe(arrow, 'mouseover', function() { arrow_start_removal_hover(arrow) });
Event.observe(arrow, 'mouseout', function() { arrow_end_removal_hover(arrow) });
Event.observe(arrow, 'click', function() { remove_group_by(arrow) });
}
function arrow_start_removal_hover(arrow) {
group_by_start_hover(arrow.up());
update_arrow(arrow.up());
arrow.className = arrow.className + "_remove";
}
function arrow_end_removal_hover(arrow) {
group_by_end_hover(arrow.up());
arrow.className = arrow.className.replace(/\_remove/, "");
}
function update_arrow(group_by) {
arrow = $(group_by.id + "_arrow");
if (arrow == null) return;
if (last_in_row(group_by)) {
arrow.className = "arrow in_row arrow_left";
} else {
arrow.className = "arrow in_row arrow_both";
}
}
function init_label(group_by) {
group_by_label = document.createElement('label');
group_by_label.setAttribute('for', group_by.id);
group_by_label.setAttribute('class', 'in_row group_by_label');
group_by_label.setAttribute('id', group_by.id + '_label');
init_group_by_hover_effects(group_by_label);
return group_by_label;
}
function sanitized_selected(select) {
return select.descendants().select(function(e) { return e.value == select.value }).first().innerHTML.strip();
}
function init_group_by(field) {
group_by = document.createElement('span');
group_by.className = 'in_row drag_element group_by';
group_by.id = field;
return group_by;
}
function init_group_by_hover_effects(group_by_label) {
Event.observe(group_by_label, 'mouseover', function() {
group_by_start_hover(group_by_label.up());
});
Event.observe(group_by_label, 'mouseout', function() {
group_by_end_hover(group_by_label.up());
});
}
function group_by_start_hover(group_by) {
arrow = $(group_by.id + '_arrow');
group_by.className = group_by.className.replace(/group\_by/, 'group_by_hover');
if (last_in_row(group_by)) {
arrow.className = 'arrow in_row arrow_left_hover';
} else {
arrow.className = 'arrow in_row arrow_both_hover_left';
}
if (!(first_in_row(group_by))) {
$(group_by.previous().id + '_arrow').className = 'arrow in_row arrow_both_hover_right';
}
}
function group_by_end_hover(group_by) {
arrow = $(group_by.id + '_arrow');
group_by.className = group_by.className.replace(/\_hover/, '');
if (arrow !== null) {
if (last_in_row(group_by)) {
arrow.className = 'arrow in_row arrow_left';
} else {
arrow.className = 'arrow in_row arrow_both';
}
}
if (!(first_in_row(group_by))) {
$(group_by.previous().id + '_arrow').className = 'arrow in_row arrow_both';
}
}
function first_in_row(group_by) {
return ((group_by.previous() == null) || (!group_by.previous().hasClassName('group_by')));
}
function last_in_row(group_by) {
return ((group_by.next() == null) || (!group_by.next().hasClassName('group_by')));
}
function move_group_by(group_by, target) {
group_by = $(group_by);
target = $(target);
if (group_by === null || target === null) {
return;
}
target.insert({ bottom: group_by.remove() });
function show_group_by_column(group_by) {
show_group_by(group_by, $('group_by_columns'));
}
function show_group_by_row(group_by) {
show_group_by(group_by, $('group_by_rows'));
}
function show_group_by_column(group_by) {
show_group_by(group_by, $('group_by_columns'));
}
function disable_all_filters() {
$('filter_table').down().childElements().each(function (e) {
var field, possible_select;
e.hide();
if (e.readAttribute('class') === 'filter') {
field = e.id.gsub('tr_', '');
hide_filter(field);
hide_filter(field, false);
enable_select_option($('add_filter_select'), field);
possible_select = $(field + '_arg_1_val');
if (possible_select !== null && possible_select.type && possible_select.type.include('select')) {
@ -423,16 +307,11 @@ function disable_all_filters() {
}
function disable_all_group_bys() {
[$('group_columns'), $('group_rows')].each(function(origin) {
children = origin.childElements();
for (var i = 0; i < children.length; i++) {
if (children[i].hasClassName('group_by')) {
[$('group_by_columns'), $('group_by_rows')].each(function (sel) {
enable_select_option(sel, children[i].getAttribute('data-backref'));
});
children[i].remove();
}
}
var destination;
destination = $('group_by_container');
[$('group_by_columns'), $('group_by_rows')].each(function (origin) {
selectAllOptions(origin);
moveOptions(origin, destination);
});
}
@ -465,31 +344,6 @@ function init_group_bys() {
Sortable.create('group_rows', options);
}
function load_available_values_for_filter(filter_name, callback_func) {
var select;
select = $('' + filter_name + '_arg_1_val');
if (select.readAttribute('data-loading') === "ajax" && select.childElements().length === 0) {
new Ajax.Updater({ success: select }, '/cost_reports/available_values', {
parameters: { filter_name: filter_name },
insertion: 'bottom',
evalScripts: false,
onCreate: function (a, b) {
$('operators_' + filter_name).disable();
$('' + filter_name + '_arg_1_val').disable();
},
onComplete: function (a, b) {
$('operators_' + filter_name).enable();
$('' + filter_name + '_arg_1_val').enable();
callback_func();
}
});
make_select_accept_single_value(select);
}
else {
callback_func();
}
}
function defineElementGetter() {
if (document.getElementsByClassName === undefined) {
document.getElementsByClassName = function (className)

@ -42,55 +42,23 @@ function swapOptions(theSel, index1, index2)
theSel.options[index2].setAttribute("data-category", category);
}
function moveOptionUp(theSel) {
theSel = $(theSel);
var index = theSel.selectedIndex;
if (index > 0) {
swapOptions(theSel, index - 1, index);
theSel.selectedIndex = index - 1;
}
}
function moveOptionDown(theSel) {
function deleteOption(theSel, theIndex)
{
theSel = $(theSel);
var index = theSel.selectedIndex;
if (index < theSel.length - 1) {
swapOptions(theSel, index, index + 1);
theSel.selectedIndex = index + 1;
var selLength = theSel.length;
if (selLength > 0)
{
theSel.options[theIndex] = null;
}
}
function selectAllOptions(select) {
select = $(select);
for (var i = 0; i < select.options.length; i += 1) {
select.options[i].selected = true;
}
}
// Returns true if the given select-box has optgroups
// Returns true if the given select-box has optgroups.
// We assume that a possibly present optgroup is the first child element of the select-box.
function has_optgroups(theSel) {
theSel = $(theSel);
groups = theSel.select('optgroup');
return (groups.size() > 0);
}
// Returns true if the given select-box has optgroups and at least one of those contains a child
// We assume that a possibly present optgroup is the first child element of the select-box.
function filled_optgroups(theSel) {
if (!has_optgroups(theSel)) return false;
groups = theSel.select('optgroup');
hit = false;
for (var i = 0; i < groups.length; i += 1) {
if (groups[i].childElements().size() > 0) {
hit = true;
break;
}
}
return hit;
return (theSel.childElements().length > 0) && (theSel.down(0).tagName === "OPTGROUP");
}
// Compares two option elements (return -1 if a < b, if not return 1).
// If those elements have a 'data-sort_by' attribute, we compare that attribute.
// If this is not the case we just compare their labels.
@ -104,58 +72,80 @@ function compareOptions(a, b) {
// Sorts all elements of the given select-box.
// If that select-box contains optgroups, the options are sorted for each optgroup separately.
function sortOptions(theSel) {
theSel = $(theSel);
if (filled_optgroups(theSel)) {
// handle each optgroup separately
theSel.childElements().each(function(group){
var sorted_elements;
// get all elements of this optgroup and sort them
sorted_elements = $A(group.childElements()).sort(compareOptions);
// make optgroup empty
$A(group.childElements()).each(function(o){$(o).remove()});
// insert sorted elements into opgroup
sorted_elements.each(function(o){
$(group).insert({'bottom' : o});
});
});
}
// there is no optgroup so just sort the options
$A(theSel.options).sort(compareOptions).each(function(o,i) {
theSel.options[i] = o;
});
theSel = $(theSel);
if (has_optgroups(theSel)) {
// handle each optgroup separately
theSel.childElements().each(function (group) {
var sorted_elements;
// get all elements of this optgroup and sort them
sorted_elements = $A(group.childElements()).sort(compareOptions);
// make optgroup empty
$A(group.childElements()).each(function (o) {
$(o).remove();
});
// insert sorted elements into opgroup
sorted_elements.each(function (o) {
$(group).insert({'bottom' : o});
});
});
}
else {
// there is no optgroup, so just sort the options
$A(theSel.options).sort(compareOptions).each(function (o, i) {
theSel.options[i] = o;
});
}
}
// Clears any filled optgroup and puts those Elements to the Select Box Toplevel.
// A Backreference to the previous optgroups' label is held in the attribute 'data-optgroup'.
// Note that any Optgroups will still be present after moving the options
function moveOptionsToTopLevel(theSel) {
if (!(filled_optgroups(theSel))) return;
theSel.childElements().each(function(group) {
$A(group.childElements()).each(function(o) {
tmp = o;
o.remove();
$(theSel).insert({'bottom' : tmp});
tmp.setAttribute('data-optgroup', group.getAttribute('label'))
});
});
function moveOptions(theSelFrom, theSelTo)
{
var selLength, selectedText, selectedValues, selectedCategories, selectedCount, i;
theSelFrom = $(theSelFrom);
theSelTo = $(theSelTo);
selLength = theSelFrom.length;
selectedText = [];
selectedValues = [];
selectedCategories = [];
selectedCount = 0;
for (i = selLength - 1; i >= 0; i -= 1) {
if (theSelFrom.options[i].selected)
{
addOption(theSelTo, theSelFrom.options[i].cloneNode(true));
deleteOption(theSelFrom, i);
}
}
if (has_optgroups(theSelTo)) {
sortOptions(theSelTo);
}
if (NS4) {
history.go(0);
}
}
function moveOptionUp(theSel) {
theSel = $(theSel);
var index = theSel.selectedIndex;
if (index > 0) {
swapOptions(theSel, index - 1, index);
theSel.selectedIndex = index - 1;
}
}
// Moves any options in the Select Box to the optgroup with a label that equals the options'
// 'data-optgroup' field. That is reset after moving the respective option.
// Note that the Optgroup has to be present before calling this function, as it will not be
// created in the process
function putOptionsIntoOpgroups(theSel) {
if (!has_optgroups(theSel)) return;
groups = theSel.select('optgroup');
theSel.select('option').each(function (option) {
for (var i = 0; i < groups.length; i += 1) {
if (option.getAttribute('data-optgroup') == groups[i].getAttribute('label')) {
tmp = option;
option.remove();
groups[i].insert({'bottom' : tmp});
tmp.setAttribute('data-optgroup', '');
}
function moveOptionDown(theSel) {
theSel = $(theSel);
var index = theSel.selectedIndex;
if (index < theSel.length - 1) {
swapOptions(theSel, index, index + 1);
theSel.selectedIndex = index + 1;
}
});
}
function selectAllOptions(select)
{
select = $(select);
for (var i = 0; i < select.options.length; i += 1) {
select.options[i].selected = true;
}
}

@ -98,7 +98,7 @@
}
/* Aligning filter elements at the top. */
table#filter_table td {
.new_cost_query fieldset#filters table td {
vertical-align: top;
border-spacing: 5px 5px;
border-color: white;
@ -106,7 +106,7 @@ table#filter_table td {
border-width: 2px 0px 0px;
}
table#filter_table td > label {
.new_cost_query fieldset#filters table td > label {
left: 3px;
top: 3px;
position: relative;
@ -120,13 +120,63 @@ table#filter_table td > label {
}
.group_by {
background-color: #3F9ED7;
background-color: transparent;
background-position: 50%;
background-repeat: no-repeat;
border: 1px solid #900;
height: 10px;
width: 10px;
margin: 1px;
}
.group_by:hover {
background-color: #EEE;
}
.move {
height: 25px;
width: 25px;
}
.sort {
height: 15px;
width: 15px;
}
.moveUp {
margin-top: 0px;
margin-bottom: 0px;
background-image: url(../images/arrow_D_up.gif);
}
.moveDown {
margin-top: 0px;
margin-bottom: 0px;
background-image: url(../images/arrow_D_down.gif);
}
.moveLeft {
margin-left: 0px;
margin-right: 0px;
background-image: url(../images/arrow_D_left.gif);
}
.group_by_hover {
background-color: #398AD3;
.moveRight {
margin-left: 0px;
margin-right: 0px;
background-image: url(../images/arrow_D_right.gif);
}
.sortUp {
margin-left: 0px;
margin-right: 0px;
background-image: url(../images/arrow_B_up.gif);
}
.sortDown {
margin-left: 0px;
margin-right: 0px;
background-image: url(../images/arrow_B_down.gif);
}
.filter_rem {
@ -149,10 +199,6 @@ table#filter_table td > label {
position: absolute;
}
.icon-filter-rem:hover {
background-image: url(../images/remove_hover.png);
}
.filter {
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
@ -163,27 +209,19 @@ table#filter_table td > label {
margin-top: 6px;
}
div#query_settings h1 {
margin-top: 15px;
}
table#filter_table {
border-spacing: 0px 0px;
}
table#filter_table tr.filter:hover {
.new_cost_query fieldset#filters table tr.filter:hover {
background: #aaa;
}
table#filter_table tr.filter {
.new_cost_query fieldset#filters table tr.filter {
color: #000;
background-color: #ededed;
background: #ededed;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
}
.drag_element {
/*cursor: move;*/
}

@ -14,7 +14,6 @@ de:
label_money: "Geld"
label_columns: "Spalten"
label_rows: "Zeilen"
label_query: "Anfrage"
label_is_project_with_subprojects: "ist (mit Unterprojekten)"
label_is_not_project_with_subprojects: "ist nicht (mit Unterprojekten)"

@ -14,7 +14,6 @@ en:
label_money: "Money"
label_columns: "Columns"
label_rows: "Rows"
label_query: "Query"
label_is_project_with_subprojects: "is (includes subprojects)"
label_is_not_project_with_subprojects: "is not (includes subprojects)"

@ -1,12 +1,12 @@
Feature: Filter
@javascript
Scenario: When using jump-to-project comming from the overall cost report to a projects report sets the project filter to that project
Given there is a standard cost control project named "First Project"
And I am logged in as "controller"
And I am on the overall Cost Reports page
And I jump to project "First Project"
Then "First Project" should be selected for "project_id_arg_1_val"
Scenario: When using jump-to-project comming from the overall cost report to a projects report sets the project filter to that project
Given there is a standard cost control project named "First Project"
And I am logged in as "controller"
And I am on the overall Cost Reports page
And I jump to project "First Project"
Then "First Project" should be selected for "project_id_arg_1_val"
@javascript
Scenario: When using jump-to-project comming from a projects cost report to the overall cost report page unsets the project filter
@ -65,4 +65,3 @@ Feature: Filter
Then "user_id" should not be selectable from "add_filter_select"
When I send the query
Then "user_id" should not be selectable from "add_filter_select"

@ -0,0 +1,47 @@
Feature: Groups
@javascript
Scenario: We got some awesome default settings
Given there is a standard cost control project named "First Project"
And I am logged in as "controller"
And I am on the Cost Reports page for the project called "First Project"
Then I should see "Week (Spent)" within "select[@id='group_by_columns']"
And I should see "Issue" within "select[@id='group_by_rows']"
@javascript
Scenario: A click on clear removes all groups
Given there is a standard cost control project named "First Project"
And I am logged in as "controller"
And I am on the Cost Reports page for the project called "First Project"
And I click on "Clear"
Then I should not see "Week (Spent)" within "select[@id='group_by_columns']"
And I should not see "Issue" within "select[@id='group_by_rows']"
@javascript
Scenario: Groups can be added to either rows or columns
Given there is a standard cost control project named "First Project"
And I am logged in as "controller"
And I am on the Cost Reports page for the project called "First Project"
And I click on "Clear"
And I group columns by "Issue"
Then I should see "Issue" within "select[@id='group_by_columns']"
And I should not see "Issues" within "select[@id='group_by_container']"
When I group rows by "Project"
Then I should see "Project" within "select[@id='group_by_rows']"
And I should not see "Project" within "select[@id='group_by_container']"
@javascript
Scenario: Groups get restored after sending a query
Given there is a standard cost control project named "First Project"
And I am logged in as "controller"
And I am on the Cost Reports page for the project called "First Project"
And I click on "Clear"
And I group columns by "Issue"
And I group columns by "Project"
And I group rows by "User"
And I group rows by "Cost type"
And I send the query
Then I should see "Project" within "select[@id='group_by_columns']"
And I should see "Issue" within "select[@id='group_by_columns']"
And I should see "User" within "select[@id='group_by_rows']"
And I should see "User" within "select[@id='group_by_rows']"

@ -53,6 +53,10 @@ Given /^I set the filter "([^"]*)" to "([^"]*)" with the operator "([^"]*)"$/ do
page.evaluate_script("restore_filter(\"#{filter}\", \"#{operator}\", \"#{value}\")")
end
When /^I send the query$/ do
find(:xpath, '//p[@class="buttons"]/a[@class="button apply"]').click
end
Then /^filter "([^"]*)" should (not )?be visible$/ do |filter, negative|
bool = negative ? false : true
page.evaluate_script("$('tr_#{filter}').visible()") =~ /^#{bool}$/
@ -63,8 +67,3 @@ Given /^I group (rows|columns) by "([^"]*)"/ do |target, group|
When %{I select "#{group}" from "group_by_container"}
find(:xpath, "//input[@class='buttons group_by move #{destination}']").click
end
When /^I send the query$/ do
find(:xpath, '//p[@class="buttons"]/a[@class="button apply"]').click
end

@ -1,7 +1,10 @@
require 'redmine'
#FIXME
#require_plugin 'reporting_engine'
fail "upgrade ruby version, ruby < 1.8.7 suffers from Hash#hash bug" if {:a => 10}.hash != {:a => 10}.hash
#require "hwia_rails"
require 'big_decimal_patch'
require 'to_date_patch'
# Hooks
require 'view_projects_show_sidebar_bottom_hook'

@ -18,6 +18,10 @@ describe CostQuery do
fixtures :versions
describe :chain do
before do
CostQuery.chain_initializer.clear
end
it "should contain NoFilter" do
@query.chain.should be_a(CostQuery::Filter::NoFilter)
end
@ -99,6 +103,53 @@ describe CostQuery do
@query.group_bys.size.should == 1
@query.group_bys.collect {|g| g.class.underscore_name}.should include "project_id"
end
it "should initialize the chain through a block" do
class TestFilter < CostQuery::Filter::Base
initialize_query_with {|query| query.filter(:project_id, :value => Project.all.first.id)}
end
@query.build_new_chain
@query.filters.size.should == 2
@query.filters.collect {|f| f.class.underscore_name}.should include "project_id"
end
it "should serialize the chain correctly" do
@query.filter :project_id, :value => Project.all.first.id
@query.filter :cost_type_id, :value => CostQuery::Filter::CostTypeId.available_values.first
@query.filter :category_id, :value => CostQuery::Filter::CategoryId.available_values.first
@query.group_by :activity_id
@query.group_by :cost_object_id
@query.group_by :cost_type_id
[:filters, :group_bys].each do |type|
@query.send(type).each do |chainable|
@query.serialize[type].collect{|c| c[0]}.should include chainable.class.name.demodulize
end
end
end
it "should deserialize a serialized query correctly" do
@query.filter :project_id, :value => Project.all.first.id
@query.filter :cost_type_id, :value => CostQuery::Filter::CostTypeId.available_values.first
@query.filter :category_id, :value => CostQuery::Filter::CategoryId.available_values.first
@query.group_by :activity_id
@query.group_by :cost_object_id
@query.group_by :cost_type_id
new_query = CostQuery.deserialize(@query.serialize)
[:filters, :group_bys].each do |type|
@query.send(type).each_with_index do |chainable, index|
# check whether for presence
new_query.send(type).any? do |c|
c.class.name == chainable.class.name && (chainable.respond_to?(:values) ? c.values == chainable.values : true)
end.should be_true
# check for order
new_query.send(type).each_with_index do |c, ix|
if c.class.name == chainable.class.name
ix.should == index
end
end
end
end
end
end
describe CostQuery::Chainable do

@ -125,6 +125,12 @@ describe CostQuery do
@query.result.count.should == Entry.all.select { |e| e.updated_on.to_date > Date.today.years_ago(20) }.count
end
it "filters user_id" do
val = CostQuery::Filter::UserId.available_values.first[1].to_i
@query.filter :user_id, :value => val, :operator => '='
@query.result.count.should == Entry.all.select { |e| e.user_id == val }.count
end
it "filters overridden_costs" do
@query.filter :overridden_costs, :operator => 'y'
@query.result.count.should == Entry.all.select { |e| not e.overridden_costs.nil? }.count

@ -77,8 +77,16 @@ describe CostQuery do
query('cost_entries', 'project_id', '!*', []).size.should == 0
end
it "does ~ (contains)" do
query('projects', 'name', '~', 'o').size.should == Project.all.select { |p| p.name =~ /o/ }.count
query('projects', 'name', '~', 'test').size.should == Project.all.select { |p| p.name =~ /test/ }.count
query('projects', 'name', '~', 'child').size.should == Project.all.select { |p| p.name =~ /child/ }.count
end
it "does !~ (not contains)" do
query('projects', 'name', '!~', 'o').size.should == Project.all.select { |p| p.name !~ /o/ }.count
query('projects', 'name', '!~', 'test').size.should == Project.all.select { |p| p.name !~ /test/ }.count
query('projects', 'name', '!~', 'child').size.should == Project.all.select { |p| p.name !~ /child/ }.count
end
it "does c (closed issue)" do

Loading…
Cancel
Save