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 %> + + + <%= group.blank? ? 'None' : group %> (<%= @issue_count_by_group[group] %>) + + + <% end %> <%= check_box_tag("ids[]", issue.id, false, :id => nil) %> <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %> diff --git a/app/views/issues/index.rhtml b/app/views/issues/index.rhtml index 7c381d825..e74dbafc1 100644 --- a/app/views/issues/index.rhtml +++ b/app/views/issues/index.rhtml @@ -4,9 +4,15 @@ <% form_tag({ :controller => 'queries', :action => 'new' }, :id => 'query_form') do %> <%= hidden_field_tag('project_id', @project.to_param) if @project %> +
<%= l(:label_filter_plural) %> <%= render :partial => 'queries/filters', :locals => {:query => @query} %> +
+

<%= 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)) %>

+

+ <%= link_to_remote l(:button_apply), { :url => { :set_filter => 1 }, :update => "content", @@ -23,7 +29,6 @@ <%= link_to l(:button_save), {}, :onclick => "$('query_form').submit(); return false;", :class => 'icon icon-save' %> <% end %>

- <% end %> <% else %>
@@ -36,6 +41,7 @@
<% html_title @query.name %> <% end %> + <%= error_messages_for 'query' %> <% if @query.valid? %> <% if @issues.empty? %> diff --git a/app/views/queries/_form.rhtml b/app/views/queries/_form.rhtml index 7c227a9f6..28faba177 100644 --- a/app/views/queries/_form.rhtml +++ b/app/views/queries/_form.rhtml @@ -19,6 +19,9 @@

<%= 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 %>

<%= l(:label_filter_plural) %> diff --git a/config/locales/bg.yml b/config/locales/bg.yml index 9d403bcd5..47fb305c4 100644 --- a/config/locales/bg.yml +++ b/config/locales/bg.yml @@ -789,3 +789,4 @@ bg: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/bs.yml b/config/locales/bs.yml index c183b068e..fe0c30c28 100644 --- a/config/locales/bs.yml +++ b/config/locales/bs.yml @@ -822,3 +822,4 @@ bs: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 31f687812..b8c84975c 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -792,3 +792,4 @@ ca: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 7c6ba0d22..077cd1ba1 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -795,3 +795,4 @@ cs: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/da.yml b/config/locales/da.yml index 25b907c74..fb47fb798 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -822,3 +822,4 @@ da: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/de.yml b/config/locales/de.yml index 046bc6028..278586afc 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -821,3 +821,4 @@ de: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/en.yml b/config/locales/en.yml index 66122625f..cdc505a53 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -242,6 +242,7 @@ en: field_watcher: Watcher field_identity_url: OpenID URL field_content: Content + field_group_by: Group results by setting_app_title: Application title setting_app_subtitle: Application subtitle diff --git a/config/locales/es.yml b/config/locales/es.yml index 34bf5e254..812a8059d 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -842,3 +842,4 @@ es: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 3d586a3bd..b988ef394 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -832,3 +832,4 @@ fi: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/fr.yml b/config/locales/fr.yml index dad6b54c0..e4d471019 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -274,6 +274,7 @@ fr: field_watcher: Observateur field_identity_url: URL OpenID field_content: Contenu + field_group_by: Grouper par setting_app_title: Titre de l'application setting_app_subtitle: Sous-titre de l'application diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 65272a9ca..08fe8f563 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -821,3 +821,4 @@ gl: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/he.yml b/config/locales/he.yml index e3fef525d..8e617286f 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -804,3 +804,4 @@ he: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/hu.yml b/config/locales/hu.yml index 34a37b63b..117d0f78f 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -827,3 +827,4 @@ text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/it.yml b/config/locales/it.yml index 28173931b..f04e61ba0 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -807,3 +807,4 @@ it: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/ja.yml b/config/locales/ja.yml index aba79a499..49f57b569 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -820,3 +820,4 @@ ja: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/ko.yml b/config/locales/ko.yml index cb1407527..c98381856 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -851,3 +851,4 @@ ko: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/lt.yml b/config/locales/lt.yml index f0dbe63e4..fdc978b70 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -832,3 +832,4 @@ lt: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/nl.yml b/config/locales/nl.yml index e367d965c..cf16950e0 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -777,3 +777,4 @@ nl: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/no.yml b/config/locales/no.yml index 36a685b4c..04b618ac9 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -794,3 +794,4 @@ text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 14bab2d77..49d7ec7d9 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -825,3 +825,4 @@ pl: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 281292b1c..e8de22fb4 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -827,3 +827,4 @@ pt-BR: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/pt.yml b/config/locales/pt.yml index 2eda20cad..e43609be3 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -813,3 +813,4 @@ pt: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/ro.yml b/config/locales/ro.yml index 6243bc24a..31bcf70fd 100644 --- a/config/locales/ro.yml +++ b/config/locales/ro.yml @@ -792,3 +792,4 @@ ro: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 1b192dcf0..e55699a86 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -919,3 +919,4 @@ ru: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 2e1c25123..722ee545d 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -793,3 +793,4 @@ sk: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/sl.yml b/config/locales/sl.yml index 882d486b0..becf2c621 100644 --- a/config/locales/sl.yml +++ b/config/locales/sl.yml @@ -791,3 +791,4 @@ sl: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/sr.yml b/config/locales/sr.yml index bf628ce31..d29b83fb9 100644 --- a/config/locales/sr.yml +++ b/config/locales/sr.yml @@ -815,3 +815,4 @@ text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/sv.yml b/config/locales/sv.yml index ebcdf42ba..be8b58900 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -849,3 +849,4 @@ sv: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/th.yml b/config/locales/th.yml index c3ef5d277..56382db22 100644 --- a/config/locales/th.yml +++ b/config/locales/th.yml @@ -792,3 +792,4 @@ th: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/tr.yml b/config/locales/tr.yml index bc51accc5..ecfdba74a 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -828,3 +828,4 @@ tr: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/uk.yml b/config/locales/uk.yml index b9fd9915b..148642b66 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -791,3 +791,4 @@ uk: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/vi.yml b/config/locales/vi.yml index 803bdc3ca..7b3238a8f 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -861,3 +861,4 @@ vi: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index d0e648d20..de037c8e9 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -899,3 +899,4 @@ text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/config/locales/zh.yml b/config/locales/zh.yml index c4a5837e6..f42975f45 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -824,3 +824,4 @@ zh: text_wiki_page_nullify_children: Keep child pages as root pages text_wiki_page_destroy_children: Delete child pages and all their descendants setting_password_min_length: Minimum password length + field_group_by: Group results by diff --git a/db/migrate/20090425161243_add_queries_group_by.rb b/db/migrate/20090425161243_add_queries_group_by.rb new file mode 100644 index 000000000..1405f3d0b --- /dev/null +++ b/db/migrate/20090425161243_add_queries_group_by.rb @@ -0,0 +1,9 @@ +class AddQueriesGroupBy < ActiveRecord::Migration + def self.up + add_column :queries, :group_by, :string + end + + def self.down + remove_column :queries, :group_by + end +end diff --git a/lib/redmine/export/pdf.rb b/lib/redmine/export/pdf.rb index b45e67cb7..61f3451a8 100644 --- a/lib/redmine/export/pdf.rb +++ b/lib/redmine/export/pdf.rb @@ -108,7 +108,7 @@ module Redmine end # Returns a PDF string of a list of issues - def issues_to_pdf(issues, project) + def issues_to_pdf(issues, project, query) pdf = IFPDF.new(current_language) title = project ? "#{project} - #{l(:label_issue_plural)}" : "#{l(:label_issue_plural)}" pdf.SetTitle(title) @@ -140,7 +140,18 @@ module Redmine # rows pdf.SetFontStyle('',9) pdf.SetFillColor(255, 255, 255) - issues.each do |issue| + group = false + issues.each do |issue| + if query.grouped? && issue.send(query.group_by) != group + group = issue.send(query.group_by) + pdf.SetFontStyle('B',10) + pdf.Cell(0, row_height, "#{group.blank? ? 'None' : group.to_s}", 0, 1, 'L') + pdf.Line(10, pdf.GetY, 287, pdf.GetY) + pdf.SetY(pdf.GetY() + 0.5) + pdf.Line(10, pdf.GetY, 287, pdf.GetY) + pdf.SetY(pdf.GetY() + 1) + pdf.SetFontStyle('',9) + end pdf.Cell(15, row_height, issue.id.to_s, 0, 0, 'L', 1) pdf.Cell(30, row_height, issue.tracker.name, 0, 0, 'L', 1) pdf.Cell(30, row_height, issue.status.name, 0, 0, 'L', 1) diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index f4bd10218..a2ccb9026 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -87,7 +87,7 @@ table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; } table.list td { vertical-align: top; } table.list td.id { width: 2%; text-align: center;} table.list td.checkbox { width: 15px; padding: 0px;} - + tr.project td.name a { padding-left: 16px; white-space:nowrap; } tr.project.parent td.name a { background: url('../images/bullet_toggle_minus.png') no-repeat; } @@ -136,7 +136,11 @@ table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; table.plugins span.description { display: block; font-size: 0.9em; } table.plugins span.url { display: block; font-size: 0.9em; } +table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; } +table.list tbody tr.group span.count { color: #aaa; font-size: 80%; } + table.list tbody tr:hover { background-color:#ffffdd; } +table.list tbody tr.group:hover { background-color:inherit; } table td {padding:2px;} table p {margin:0;} .odd {background-color:#f6f7f8;} @@ -187,13 +191,17 @@ p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;} p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; } p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; } +#query_form_content { font-size: 0.9em; padding: 4px; background: #f6f6f6; border: 1px solid #e4e4e4; } +#query_form_content fieldset#filters { border-left: 0; border-right: 0; } +#query_form_content p { margin-top: 0.5em; margin-bottom: 0.5em; } + fieldset#filters, fieldset#date-range { padding: 0.7em; margin-bottom: 8px; } fieldset#filters p { margin: 1.2em 0 0.8em 2px; } fieldset#filters table { border-collapse: collapse; } fieldset#filters table td { padding: 0; vertical-align: middle; } fieldset#filters tr.filter { height: 2em; } fieldset#filters td.add-filter { text-align: right; vertical-align: top; } -.buttons { font-size: 0.9em; } +.buttons { font-size: 0.9em; margin-bottom: 1.4em; } div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;} div#issue-changesets .changeset { padding: 4px;} diff --git a/test/fixtures/queries.yml b/test/fixtures/queries.yml index a274ce350..a1bb08eff 100644 --- a/test/fixtures/queries.yml +++ b/test/fixtures/queries.yml @@ -73,6 +73,7 @@ queries_005: is_public: true name: Open issues by priority and tracker filters: | + --- status_id: :values: - "1" @@ -86,4 +87,23 @@ queries_005: - desc - - tracker - asc +queries_006: + id: 6 + project_id: + is_public: true + name: Open issues grouped by tracker + filters: | + --- + status_id: + :values: + - "1" + :operator: o + + user_id: 1 + column_names: + group_by: tracker + sort_criteria: | + --- + - - priority + - desc \ No newline at end of file diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index f562ac9c5..dfea32899 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -161,6 +161,22 @@ class IssuesControllerTest < Test::Unit::TestCase assert_not_nil assigns(:issues) end + def test_index_with_query + get :index, :project_id => 1, :query_id => 5 + assert_response :success + assert_template 'index.rhtml' + assert_not_nil assigns(:issues) + assert_nil assigns(:issue_count_by_group) + end + + def test_index_with_grouped_query + get :index, :project_id => 1, :query_id => 6 + assert_response :success + assert_template 'index.rhtml' + assert_not_nil assigns(:issues) + assert_not_nil assigns(:issue_count_by_group) + end + def test_index_csv_with_project get :index, :format => 'csv' assert_response :success @@ -194,6 +210,11 @@ class IssuesControllerTest < Test::Unit::TestCase assert_response :success assert_not_nil assigns(:issues) assert_equal 'application/pdf', @response.content_type + + get :index, :project_id => 1, :query_id => 6, :format => 'pdf' + assert_response :success + assert_not_nil assigns(:issues) + assert_equal 'application/pdf', @response.content_type end def test_index_sort