]> source.dussan.org Git - redmine.git/commitdiff
Ticket grouping (#2679).
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Sun, 26 Apr 2009 13:09:14 +0000 (13:09 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Sun, 26 Apr 2009 13:09:14 +0000 (13:09 +0000)
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@2696 e93f8b46-1217-0410-a6f0-8f06a7374b81

45 files changed:
app/controllers/issues_controller.rb
app/controllers/queries_controller.rb
app/models/query.rb
app/views/issues/_list.rhtml
app/views/issues/index.rhtml
app/views/queries/_form.rhtml
config/locales/bg.yml
config/locales/bs.yml
config/locales/ca.yml
config/locales/cs.yml
config/locales/da.yml
config/locales/de.yml
config/locales/en.yml
config/locales/es.yml
config/locales/fi.yml
config/locales/fr.yml
config/locales/gl.yml
config/locales/he.yml
config/locales/hu.yml
config/locales/it.yml
config/locales/ja.yml
config/locales/ko.yml
config/locales/lt.yml
config/locales/nl.yml
config/locales/no.yml
config/locales/pl.yml
config/locales/pt-BR.yml
config/locales/pt.yml
config/locales/ro.yml
config/locales/ru.yml
config/locales/sk.yml
config/locales/sl.yml
config/locales/sr.yml
config/locales/sv.yml
config/locales/th.yml
config/locales/tr.yml
config/locales/uk.yml
config/locales/vi.yml
config/locales/zh-TW.yml
config/locales/zh.yml
db/migrate/20090425161243_add_queries_group_by.rb [new file with mode: 0644]
lib/redmine/export/pdf.rb
public/stylesheets/application.css
test/fixtures/queries.yml
test/functional/issues_controller_test.rb

index f3292cd4a7a4c691533cdaff995276aeb6a67967..784d620e390d3f429b7cbe8a7e9a5fb9749d1311 100644 (file)
@@ -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
index 8500e853a4f18859d399a8a701530599f7f3f54a..b688d2c432b15022166bfef567e7845e8926c859 100644 (file)
@@ -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)
index 41ce17ff658048a3cc137206f551c85bab8b7015..790ed7e23d67b71e257d86785211579c0b0098d1 100644 (file)
 # 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?
index b19e1d7191eafa166e963247d7f219370e8c21b8..89756e42c2df2239044c58e4fbfda3b126a18730 100644 (file)
@@ -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>
index 7c381d82507edbc5537eafcedfb5712cdd468297..e74dbafc1696b9c7ef5241a77c9e0212c4edd45a 100644 (file)
@@ -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? %>
index 7c227a9f69cc50b5a332d41520daddbb79701a72..28faba17770ef1104b0f047579ab41551d535d4a 100644 (file)
@@ -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>
index 9d403bcd5337d7294fc6829a0f8fe5dabee49051..47fb305c41e890f8bee71dfd64a82586252f4fb3 100644 (file)
@@ -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
index c183b068e90979decb5342919efc7cf06fed9fa6..fe0c30c28c0f60f5283657459bf7a8caff552bfb 100644 (file)
@@ -822,3 +822,4 @@ bs:
   text_wiki_page_nullify_children: Keep child pages as root pages\r
   text_wiki_page_destroy_children: Delete child pages and all their descendants\r
   setting_password_min_length: Minimum password length\r
+  field_group_by: Group results by\r
index 31f687812bbc45815bff07614c420bcdcf725c4c..b8c84975c24e9330388cab122360bd3d784e77bd 100644 (file)
@@ -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
index 7c6ba0d228626a07d36ed5a6b656661fe1b01122..077cd1ba15a4a78207ed051004ca32ad40865e9c 100644 (file)
@@ -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
index 25b907c74c4f2ca41f249325b47d3a57cd5941cc..fb47fb7984fa64098c77e4725318128c2a8b6614 100644 (file)
@@ -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
index 046bc6028282a009c45ae3af9790c8aed0dc98b8..278586afc4246b09d9d13f0da378e5bb1c4e1172 100644 (file)
@@ -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
index 66122625ffdc296a28b69dbdea3197835547e682..cdc505a53b8ce222650c2991ecf85a49df56fcb8 100644 (file)
@@ -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
index 34bf5e254bb6642ddcddbd0657421db42993904e..812a8059d07e37dee31007fa822d43a5e124ffff 100644 (file)
@@ -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
index 3d586a3bdccc34fc662eeb242c739cb0371737aa..b988ef394069d846d8d8be00a8bb2b8210aabcf3 100644 (file)
@@ -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
index dad6b54c064e1d9937d8a106aa3505ee96d2048e..e4d4710190e87ea28d31fda23c7bec0d4be78fd8 100644 (file)
@@ -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
index 65272a9ca6f7ad4b3121085bec8188cab46799f2..08fe8f56320c719fb6101ec928bf5ee7f132cc91 100644 (file)
@@ -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
index e3fef525d10649c90f0b1a3f7d81d7799c81e3f1..8e617286f5979aa72d382229de9bbae57ce3da5f 100644 (file)
@@ -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
index 34a37b63b12cf9d08ad9520e6c1b5d2c32743955..117d0f78f9c1d847b664a534b348ce3096faacba 100644 (file)
   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
index 28173931b0952a146780d0ad313d6406f330aad3..f04e61ba0f1cae35f8600e141f06a757e0982de4 100644 (file)
@@ -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
index aba79a49964598da9cddbef1100a90d5e865bc74..49f57b5695b7c711968ff630190023b86fccabf4 100644 (file)
@@ -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
index cb14075279204157d5afaad611fc1cce4fee1db7..c983818567c294510c664862de7ff37f75e7fc2c 100644 (file)
@@ -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
index f0dbe63e47163f69c2bad8fed786a9aebff4ddbb..fdc978b708ea2475e3ec1672036f4442acc6eec2 100644 (file)
@@ -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
index e367d965c6d8eaa080e3c59af4cb5ae22726e19a..cf16950e0126d89d28a13fa77e3c74704a1b7213 100644 (file)
@@ -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
index 36a685b4ca50f7a370e9880d7f3c37484cf8cd7e..04b618ac9d4980cd62d6c9e6ed784a8c0cb9c51c 100644 (file)
   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
index 14bab2d77b1dfdeafff8cb09e7542797560125a9..49d7ec7d952c4d001475f767012aca9d70f37c3c 100644 (file)
@@ -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
index 281292b1c1176cd5ec4d6e9c6f704634bb2bc28c..e8de22fb4d86d2c6e0ff299c7c1289b291097040 100644 (file)
@@ -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
index 2eda20cad34d451ec75680808489dc4216e59087..e43609be3ddec3f5e3ae2251b5e613ab985e209f 100644 (file)
@@ -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
index 6243bc24a08658b37e327410b5a38dda8bebd2a7..31bcf70fde95a9d61c58fa93de06815ebec243cc 100644 (file)
@@ -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
index 1b192dcf05b63f4d2e8be903a1be863875f5a312..e55699a86a5461cf978fb331fb6112eea0681b37 100644 (file)
@@ -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
index 2e1c2512360317ed39bc5b450d7b92def0216cb6..722ee545df0a17aa013a7c2e2ee420c58d8eb3f6 100644 (file)
@@ -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
index 882d486b04c77e19603cca4dd9f47edd0af8d183..becf2c62157fd5084ec7f5dab706389b2fe8cb83 100644 (file)
@@ -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
index bf628ce31316fb486a7d57940b6117c9f2d62bc2..d29b83fb9a6f72a91ec25236fbeb68e1893c3831 100644 (file)
   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
index ebcdf42ba1a3ccee8e555c687794afd6020f4f87..be8b5890064e3111f604b2f22b90dad04c1c31e6 100644 (file)
@@ -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
index c3ef5d277a207c1fc6867f80dd3aa8f11dc072d3..56382db22a79bff0807a13ef055883eadf97701a 100644 (file)
@@ -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
index bc51accc5fe36c6de646ae2f338f22fe2ef1db80..ecfdba74abd88b427b31fad656da086d78926a11 100644 (file)
@@ -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
index b9fd9915b052a656bc5756907da1dbfdf061b9ec..148642b660b6a82eac2b4e0c45a33cefdf180b7a 100644 (file)
@@ -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
index 803bdc3cae3dd34105ffa1de7c6219d1d7f0826d..7b3238a8f812c9d24a0ee85844e863d3928d7dbf 100644 (file)
@@ -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
index d0e648d203eb3934733988eac271d75d43b912f2..de037c8e9d3f349a10186f9192b9522a106dbc86 100644 (file)
   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
index c4a5837e6313d9a1ce3537b10c32f9c0b6c39ebf..f42975f45c3352c4c9f19e283e56ee4c0b6eaa35 100644 (file)
@@ -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 (file)
index 0000000..1405f3d
--- /dev/null
@@ -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
index b45e67cb763c125c7b068b2306a2e482a0e92721..61f3451a8c0e7ac68532209dc88714bd7fb07dc2 100644 (file)
@@ -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)
index f4bd102180932d8c3dd4d84e0767acc0660e842b..a2ccb9026c89e38515ac90df3096badd2a45cfa0 100644 (file)
@@ -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;}
index a274ce350a5f9d3025f4febb6feebf2f3f95a7cf..a1bb08eff512dd729148af8973bdd74003810736 100644 (file)
@@ -73,6 +73,7 @@ queries_005:
   is_public: true\r
   name: Open issues by priority and tracker\r
   filters: |\r
+    --- \r
     status_id: \r
       :values: \r
       - "1"\r
@@ -86,4 +87,23 @@ queries_005:
       - desc\r
     - - tracker\r
       - asc\r
+queries_006: \r
+  id: 6\r
+  project_id: \r
+  is_public: true\r
+  name: Open issues grouped by tracker\r
+  filters: |\r
+    --- \r
+    status_id: \r
+      :values: \r
+      - "1"\r
+      :operator: o\r
+\r
+  user_id: 1\r
+  column_names: \r
+  group_by: tracker\r
+  sort_criteria: |\r
+    --- \r
+    - - priority\r
+      - desc\r
   
\ No newline at end of file
index f562ac9c536cfc997cd6cdf83720301b90d78425..dfea32899b5bde5efd8393029a7699f08ea88a47 100644 (file)
@@ -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