OSDN Git Service

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 f3292cd..784d620 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 8500e85..b688d2c 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 41ce17f..790ed7e 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 b19e1d7..89756e4 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 7c381d8..e74dbaf 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 7c227a9..28faba1 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 9d403bc..47fb305 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 c183b06..fe0c30c 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 31f6878..b8c8497 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 7c6ba0d..077cd1b 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 25b907c..fb47fb7 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 046bc60..278586a 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 6612262..cdc505a 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 34bf5e2..812a805 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 3d586a3..b988ef3 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 dad6b54..e4d4710 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 65272a9..08fe8f5 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 e3fef52..8e61728 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 34a37b6..117d0f7 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 2817393..f04e61b 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 aba79a4..49f57b5 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 cb14075..c983818 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 f0dbe63..fdc978b 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 e367d96..cf16950 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 36a685b..04b618a 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 14bab2d..49d7ec7 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 281292b..e8de22f 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 2eda20c..e43609b 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 6243bc2..31bcf70 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 1b192dc..e55699a 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 2e1c251..722ee54 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 882d486..becf2c6 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 bf628ce..d29b83f 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 ebcdf42..be8b589 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 c3ef5d2..56382db 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 bc51acc..ecfdba7 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 b9fd991..148642b 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 803bdc3..7b3238a 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 d0e648d..de037c8 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 c4a5837..f42975f 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 b45e67c..61f3451 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 f4bd102..a2ccb90 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 a274ce3..a1bb08e 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 f562ac9..dfea328 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