summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJean-Philippe Lang <jp_lang@yahoo.fr>2009-04-26 13:09:14 +0000
committerJean-Philippe Lang <jp_lang@yahoo.fr>2009-04-26 13:09:14 +0000
commitb557393252cc961c2f47dc90d50b75f76d7d7dca (patch)
treef7ebe7cfa18e256431393c7183146450e5377048
parent24875be705fdca8e53b7c39a58023d13421628e3 (diff)
downloadredmine-b557393252cc961c2f47dc90d50b75f76d7d7dca.tar.gz
redmine-b557393252cc961c2f47dc90d50b75f76d7d7dca.zip
Ticket grouping (#2679).
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@2696 e93f8b46-1217-0410-a6f0-8f06a7374b81
-rw-r--r--app/controllers/issues_controller.rb22
-rw-r--r--app/controllers/queries_controller.rb1
-rw-r--r--app/models/query.rb42
-rw-r--r--app/views/issues/_list.rhtml10
-rw-r--r--app/views/issues/index.rhtml8
-rw-r--r--app/views/queries/_form.rhtml3
-rw-r--r--config/locales/bg.yml1
-rw-r--r--config/locales/bs.yml1
-rw-r--r--config/locales/ca.yml1
-rw-r--r--config/locales/cs.yml1
-rw-r--r--config/locales/da.yml1
-rw-r--r--config/locales/de.yml1
-rw-r--r--config/locales/en.yml1
-rw-r--r--config/locales/es.yml1
-rw-r--r--config/locales/fi.yml1
-rw-r--r--config/locales/fr.yml1
-rw-r--r--config/locales/gl.yml1
-rw-r--r--config/locales/he.yml1
-rw-r--r--config/locales/hu.yml1
-rw-r--r--config/locales/it.yml1
-rw-r--r--config/locales/ja.yml1
-rw-r--r--config/locales/ko.yml1
-rw-r--r--config/locales/lt.yml1
-rw-r--r--config/locales/nl.yml1
-rw-r--r--config/locales/no.yml1
-rw-r--r--config/locales/pl.yml1
-rw-r--r--config/locales/pt-BR.yml1
-rw-r--r--config/locales/pt.yml1
-rw-r--r--config/locales/ro.yml1
-rw-r--r--config/locales/ru.yml1
-rw-r--r--config/locales/sk.yml1
-rw-r--r--config/locales/sl.yml1
-rw-r--r--config/locales/sr.yml1
-rw-r--r--config/locales/sv.yml1
-rw-r--r--config/locales/th.yml1
-rw-r--r--config/locales/tr.yml1
-rw-r--r--config/locales/uk.yml1
-rw-r--r--config/locales/vi.yml1
-rw-r--r--config/locales/zh-TW.yml1
-rw-r--r--config/locales/zh.yml1
-rw-r--r--db/migrate/20090425161243_add_queries_group_by.rb9
-rw-r--r--lib/redmine/export/pdf.rb15
-rw-r--r--public/stylesheets/application.css12
-rw-r--r--test/fixtures/queries.yml20
-rw-r--r--test/functional/issues_controller_test.rb21
45 files changed, 178 insertions, 19 deletions
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 %>
</tr></thead>
+ <% group = false %>
<tbody>
<% issues.each do |issue| -%>
+ <% if @query.grouped? && issue.send(@query.group_by) != group %>
+ <% group = issue.send(@query.group_by) %>
+ <% reset_cycle %>
+ <tr class="group">
+ <td colspan="<%= query.columns.size + 2 %>">
+ <%= group.blank? ? 'None' : group %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
+ </td>
+ </tr>
+ <% end %>
<tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>">
<td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
<td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
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 %>
+ <div id="query_form_content">
<fieldset id="filters"><legend><%= l(:label_filter_plural) %></legend>
<%= render :partial => 'queries/filters', :locals => {:query => @query} %>
+ </fieldset>
+ <p><%= 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)) %></p>
+ </div>
<p class="buttons">
+
<%= 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 %>
</p>
- </fieldset>
<% end %>
<% else %>
<div class="contextual">
@@ -36,6 +41,7 @@
<div id="query_form"></div>
<% 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 @@
<p><label for="query_default_columns"><%=l(:label_default_columns)%></label>
<%= 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")}' %></p>
+
+<p><label for="query_group_by"><%= l(:field_group_by) %></label>
+<%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
</div>
<fieldset><legend><%= l(:label_filter_plural) %></legend>
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