diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index f3292cd4a..784d620e3 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -58,16 +58,27 @@ class IssuesController < ApplicationController end @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement) @issue_pages = Paginator.new self, @issue_count, limit, params['page'] - @issues = Issue.find :all, :order => sort_clause, + @issues = Issue.find :all, :order => [@query.group_by_sort_order, sort_clause].compact.join(','), :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ], :conditions => @query.statement, :limit => limit, :offset => @issue_pages.current.offset respond_to do |format| - format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? } + format.html { + if @query.grouped? + # Retrieve the issue count by group + @issue_count_by_group = begin + Issue.count(:group => @query.group_by, :include => [:status, :project], :conditions => @query.statement) + # Rails will raise an (unexpected) error if there's only a nil group value + rescue ActiveRecord::RecordNotFound + {nil => @issue_count} + end + end + render :template => 'issues/index.rhtml', :layout => !request.xhr? + } format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") } format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') } - format.pdf { send_data(issues_to_pdf(@issues, @project), :type => 'application/pdf', :filename => 'export.pdf') } + format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') } end else # Send html if the query is not valid @@ -483,10 +494,11 @@ private @query.add_short_filter(field, params[field]) if params[field] end end - session[:query] = {:project_id => @query.project_id, :filters => @query.filters} + @query.group_by = params[:group_by] + session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by} else @query = Query.find_by_id(session[:query][:id]) if session[:query][:id] - @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters]) + @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by]) @query.project = @project end end diff --git a/app/controllers/queries_controller.rb b/app/controllers/queries_controller.rb index 8500e853a..b688d2c43 100644 --- a/app/controllers/queries_controller.rb +++ b/app/controllers/queries_controller.rb @@ -30,6 +30,7 @@ class QueriesController < ApplicationController params[:fields].each do |field| @query.add_filter(field, params[:operators][field], params[:values][field]) end if params[:fields] + @query.group_by ||= params[:group_by] if request.post? && params[:confirm] && @query.save flash[:notice] = l(:notice_successful_create) diff --git a/app/models/query.rb b/app/models/query.rb index 41ce17ff6..790ed7e23 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -16,12 +16,13 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class QueryColumn - attr_accessor :name, :sortable, :default_order + attr_accessor :name, :sortable, :groupable, :default_order include Redmine::I18n def initialize(name, options={}) self.name = name self.sortable = options[:sortable] + self.groupable = options[:groupable] || false self.default_order = options[:default_order] end @@ -98,20 +99,20 @@ class Query < ActiveRecord::Base cattr_reader :operators_by_filter_type @@available_columns = [ - QueryColumn.new(:project, :sortable => "#{Project.table_name}.name"), - QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"), - QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"), - QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'), + QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true), + QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true), + QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true), + QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc', :groupable => true), QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"), QueryColumn.new(:author), - QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname"]), + QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true), QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'), - QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"), - QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc'), + QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true), + QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true), QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"), QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"), QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), - QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"), + QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true), QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), ] cattr_reader :available_columns @@ -241,6 +242,11 @@ class Query < ActiveRecord::Base ).collect {|cf| QueryCustomFieldColumn.new(cf) } end + # Returns an array of columns that can be used to group the results + def groupable_columns + available_columns.select {|c| c.groupable} + end + def columns if has_default_columns? available_columns.select do |c| @@ -288,6 +294,24 @@ class Query < ActiveRecord::Base sort_criteria && sort_criteria[arg] && sort_criteria[arg].last end + # Returns the SQL sort order that should be prepended for grouping + def group_by_sort_order + if grouped? && (column = group_by_column) + column.sortable.is_a?(Array) ? + column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') : + "#{column.sortable} #{column.default_order}" + end + end + + # Returns true if the query is a grouped query + def grouped? + !group_by.blank? + end + + def group_by_column + groupable_columns.detect {|c| c.name.to_s == group_by} + end + def project_statement project_clauses = [] if project && !@project.descendants.active.empty? diff --git a/app/views/issues/_list.rhtml b/app/views/issues/_list.rhtml index b19e1d719..89756e42c 100644 --- a/app/views/issues/_list.rhtml +++ b/app/views/issues/_list.rhtml @@ -9,8 +9,18 @@ <%= column_header(column) %> <% end %> + <% group = false %>
<% issues.each do |issue| -%> + <% if @query.grouped? && issue.send(@query.group_by) != group %> + <% group = issue.send(@query.group_by) %> + <% reset_cycle %> +<%= l(:field_group_by) %> + <%= select_tag('group_by', options_for_select([[]] + @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, @query.group_by)) %>
+<%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns', :onclick => 'if (this.checked) {Element.hide("columns")} else {Element.show("columns")}' %>
+ ++<%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %>