##// END OF EJS Templates
Ticket grouping (#2679)....
Jean-Philippe Lang -
r2604:b557393252cc
parent child
Show More
@@ -0,0 +1,9
1 class AddQueriesGroupBy < ActiveRecord::Migration
2 def self.up
3 add_column :queries, :group_by, :string
4 end
5
6 def self.down
7 remove_column :queries, :group_by
8 end
9 end
@@ -58,16 +58,27 class IssuesController < ApplicationController
58 end
58 end
59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
61 @issues = Issue.find :all, :order => sort_clause,
61 @issues = Issue.find :all, :order => [@query.group_by_sort_order, sort_clause].compact.join(','),
62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
63 :conditions => @query.statement,
63 :conditions => @query.statement,
64 :limit => limit,
64 :limit => limit,
65 :offset => @issue_pages.current.offset
65 :offset => @issue_pages.current.offset
66 respond_to do |format|
66 respond_to do |format|
67 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
67 format.html {
68 if @query.grouped?
69 # Retrieve the issue count by group
70 @issue_count_by_group = begin
71 Issue.count(:group => @query.group_by, :include => [:status, :project], :conditions => @query.statement)
72 # Rails will raise an (unexpected) error if there's only a nil group value
73 rescue ActiveRecord::RecordNotFound
74 {nil => @issue_count}
75 end
76 end
77 render :template => 'issues/index.rhtml', :layout => !request.xhr?
78 }
68 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
79 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
69 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
80 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
70 format.pdf { send_data(issues_to_pdf(@issues, @project), :type => 'application/pdf', :filename => 'export.pdf') }
81 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
71 end
82 end
72 else
83 else
73 # Send html if the query is not valid
84 # Send html if the query is not valid
@@ -483,10 +494,11 private
483 @query.add_short_filter(field, params[field]) if params[field]
494 @query.add_short_filter(field, params[field]) if params[field]
484 end
495 end
485 end
496 end
486 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
497 @query.group_by = params[:group_by]
498 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by}
487 else
499 else
488 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
500 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
489 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
501 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by])
490 @query.project = @project
502 @query.project = @project
491 end
503 end
492 end
504 end
@@ -30,6 +30,7 class QueriesController < ApplicationController
30 params[:fields].each do |field|
30 params[:fields].each do |field|
31 @query.add_filter(field, params[:operators][field], params[:values][field])
31 @query.add_filter(field, params[:operators][field], params[:values][field])
32 end if params[:fields]
32 end if params[:fields]
33 @query.group_by ||= params[:group_by]
33
34
34 if request.post? && params[:confirm] && @query.save
35 if request.post? && params[:confirm] && @query.save
35 flash[:notice] = l(:notice_successful_create)
36 flash[:notice] = l(:notice_successful_create)
@@ -16,12 +16,13
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :default_order
19 attr_accessor :name, :sortable, :groupable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.groupable = options[:groupable] || false
25 self.default_order = options[:default_order]
26 self.default_order = options[:default_order]
26 end
27 end
27
28
@@ -98,20 +99,20 class Query < ActiveRecord::Base
98 cattr_reader :operators_by_filter_type
99 cattr_reader :operators_by_filter_type
99
100
100 @@available_columns = [
101 @@available_columns = [
101 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name"),
102 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
102 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
103 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
103 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
104 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
104 QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
105 QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc', :groupable => true),
105 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
106 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
106 QueryColumn.new(:author),
107 QueryColumn.new(:author),
107 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname"]),
108 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
108 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
109 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
109 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
110 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
110 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc'),
111 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
111 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
112 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
112 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
113 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
113 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
114 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
114 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"),
115 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
115 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
116 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
116 ]
117 ]
117 cattr_reader :available_columns
118 cattr_reader :available_columns
@@ -241,6 +242,11 class Query < ActiveRecord::Base
241 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
242 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
242 end
243 end
243
244
245 # Returns an array of columns that can be used to group the results
246 def groupable_columns
247 available_columns.select {|c| c.groupable}
248 end
249
244 def columns
250 def columns
245 if has_default_columns?
251 if has_default_columns?
246 available_columns.select do |c|
252 available_columns.select do |c|
@@ -288,6 +294,24 class Query < ActiveRecord::Base
288 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
294 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
289 end
295 end
290
296
297 # Returns the SQL sort order that should be prepended for grouping
298 def group_by_sort_order
299 if grouped? && (column = group_by_column)
300 column.sortable.is_a?(Array) ?
301 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
302 "#{column.sortable} #{column.default_order}"
303 end
304 end
305
306 # Returns true if the query is a grouped query
307 def grouped?
308 !group_by.blank?
309 end
310
311 def group_by_column
312 groupable_columns.detect {|c| c.name.to_s == group_by}
313 end
314
291 def project_statement
315 def project_statement
292 project_clauses = []
316 project_clauses = []
293 if project && !@project.descendants.active.empty?
317 if project && !@project.descendants.active.empty?
@@ -9,8 +9,18
9 <%= column_header(column) %>
9 <%= column_header(column) %>
10 <% end %>
10 <% end %>
11 </tr></thead>
11 </tr></thead>
12 <% group = false %>
12 <tbody>
13 <tbody>
13 <% issues.each do |issue| -%>
14 <% issues.each do |issue| -%>
15 <% if @query.grouped? && issue.send(@query.group_by) != group %>
16 <% group = issue.send(@query.group_by) %>
17 <% reset_cycle %>
18 <tr class="group">
19 <td colspan="<%= query.columns.size + 2 %>">
20 <%= group.blank? ? 'None' : group %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
21 </td>
22 </tr>
23 <% end %>
14 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>">
24 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>">
15 <td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
25 <td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
16 <td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
26 <td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
@@ -4,9 +4,15
4
4
5 <% form_tag({ :controller => 'queries', :action => 'new' }, :id => 'query_form') do %>
5 <% form_tag({ :controller => 'queries', :action => 'new' }, :id => 'query_form') do %>
6 <%= hidden_field_tag('project_id', @project.to_param) if @project %>
6 <%= hidden_field_tag('project_id', @project.to_param) if @project %>
7 <div id="query_form_content">
7 <fieldset id="filters"><legend><%= l(:label_filter_plural) %></legend>
8 <fieldset id="filters"><legend><%= l(:label_filter_plural) %></legend>
8 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
9 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
10 </fieldset>
11 <p><%= l(:field_group_by) %>
12 <%= select_tag('group_by', options_for_select([[]] + @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, @query.group_by)) %></p>
13 </div>
9 <p class="buttons">
14 <p class="buttons">
15
10 <%= link_to_remote l(:button_apply),
16 <%= link_to_remote l(:button_apply),
11 { :url => { :set_filter => 1 },
17 { :url => { :set_filter => 1 },
12 :update => "content",
18 :update => "content",
@@ -23,7 +29,6
23 <%= link_to l(:button_save), {}, :onclick => "$('query_form').submit(); return false;", :class => 'icon icon-save' %>
29 <%= link_to l(:button_save), {}, :onclick => "$('query_form').submit(); return false;", :class => 'icon icon-save' %>
24 <% end %>
30 <% end %>
25 </p>
31 </p>
26 </fieldset>
27 <% end %>
32 <% end %>
28 <% else %>
33 <% else %>
29 <div class="contextual">
34 <div class="contextual">
@@ -36,6 +41,7
36 <div id="query_form"></div>
41 <div id="query_form"></div>
37 <% html_title @query.name %>
42 <% html_title @query.name %>
38 <% end %>
43 <% end %>
44
39 <%= error_messages_for 'query' %>
45 <%= error_messages_for 'query' %>
40 <% if @query.valid? %>
46 <% if @query.valid? %>
41 <% if @issues.empty? %>
47 <% if @issues.empty? %>
@@ -19,6 +19,9
19 <p><label for="query_default_columns"><%=l(:label_default_columns)%></label>
19 <p><label for="query_default_columns"><%=l(:label_default_columns)%></label>
20 <%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns',
20 <%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns',
21 :onclick => 'if (this.checked) {Element.hide("columns")} else {Element.show("columns")}' %></p>
21 :onclick => 'if (this.checked) {Element.hide("columns")} else {Element.show("columns")}' %></p>
22
23 <p><label for="query_group_by"><%= l(:field_group_by) %></label>
24 <%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
22 </div>
25 </div>
23
26
24 <fieldset><legend><%= l(:label_filter_plural) %></legend>
27 <fieldset><legend><%= l(:label_filter_plural) %></legend>
@@ -789,3 +789,4 bg:
789 text_wiki_page_nullify_children: Keep child pages as root pages
789 text_wiki_page_nullify_children: Keep child pages as root pages
790 text_wiki_page_destroy_children: Delete child pages and all their descendants
790 text_wiki_page_destroy_children: Delete child pages and all their descendants
791 setting_password_min_length: Minimum password length
791 setting_password_min_length: Minimum password length
792 field_group_by: Group results by
@@ -822,3 +822,4 bs:
822 text_wiki_page_nullify_children: Keep child pages as root pages
822 text_wiki_page_nullify_children: Keep child pages as root pages
823 text_wiki_page_destroy_children: Delete child pages and all their descendants
823 text_wiki_page_destroy_children: Delete child pages and all their descendants
824 setting_password_min_length: Minimum password length
824 setting_password_min_length: Minimum password length
825 field_group_by: Group results by
@@ -792,3 +792,4 ca:
792 text_wiki_page_nullify_children: Keep child pages as root pages
792 text_wiki_page_nullify_children: Keep child pages as root pages
793 text_wiki_page_destroy_children: Delete child pages and all their descendants
793 text_wiki_page_destroy_children: Delete child pages and all their descendants
794 setting_password_min_length: Minimum password length
794 setting_password_min_length: Minimum password length
795 field_group_by: Group results by
@@ -795,3 +795,4 cs:
795 text_wiki_page_nullify_children: Keep child pages as root pages
795 text_wiki_page_nullify_children: Keep child pages as root pages
796 text_wiki_page_destroy_children: Delete child pages and all their descendants
796 text_wiki_page_destroy_children: Delete child pages and all their descendants
797 setting_password_min_length: Minimum password length
797 setting_password_min_length: Minimum password length
798 field_group_by: Group results by
@@ -822,3 +822,4 da:
822 text_wiki_page_nullify_children: Keep child pages as root pages
822 text_wiki_page_nullify_children: Keep child pages as root pages
823 text_wiki_page_destroy_children: Delete child pages and all their descendants
823 text_wiki_page_destroy_children: Delete child pages and all their descendants
824 setting_password_min_length: Minimum password length
824 setting_password_min_length: Minimum password length
825 field_group_by: Group results by
@@ -821,3 +821,4 de:
821 text_wiki_page_nullify_children: Keep child pages as root pages
821 text_wiki_page_nullify_children: Keep child pages as root pages
822 text_wiki_page_destroy_children: Delete child pages and all their descendants
822 text_wiki_page_destroy_children: Delete child pages and all their descendants
823 setting_password_min_length: Minimum password length
823 setting_password_min_length: Minimum password length
824 field_group_by: Group results by
@@ -242,6 +242,7 en:
242 field_watcher: Watcher
242 field_watcher: Watcher
243 field_identity_url: OpenID URL
243 field_identity_url: OpenID URL
244 field_content: Content
244 field_content: Content
245 field_group_by: Group results by
245
246
246 setting_app_title: Application title
247 setting_app_title: Application title
247 setting_app_subtitle: Application subtitle
248 setting_app_subtitle: Application subtitle
@@ -842,3 +842,4 es:
842 text_wiki_page_nullify_children: Keep child pages as root pages
842 text_wiki_page_nullify_children: Keep child pages as root pages
843 text_wiki_page_destroy_children: Delete child pages and all their descendants
843 text_wiki_page_destroy_children: Delete child pages and all their descendants
844 setting_password_min_length: Minimum password length
844 setting_password_min_length: Minimum password length
845 field_group_by: Group results by
@@ -832,3 +832,4 fi:
832 text_wiki_page_nullify_children: Keep child pages as root pages
832 text_wiki_page_nullify_children: Keep child pages as root pages
833 text_wiki_page_destroy_children: Delete child pages and all their descendants
833 text_wiki_page_destroy_children: Delete child pages and all their descendants
834 setting_password_min_length: Minimum password length
834 setting_password_min_length: Minimum password length
835 field_group_by: Group results by
@@ -274,6 +274,7 fr:
274 field_watcher: Observateur
274 field_watcher: Observateur
275 field_identity_url: URL OpenID
275 field_identity_url: URL OpenID
276 field_content: Contenu
276 field_content: Contenu
277 field_group_by: Grouper par
277
278
278 setting_app_title: Titre de l'application
279 setting_app_title: Titre de l'application
279 setting_app_subtitle: Sous-titre de l'application
280 setting_app_subtitle: Sous-titre de l'application
@@ -821,3 +821,4 gl:
821 text_wiki_page_nullify_children: Keep child pages as root pages
821 text_wiki_page_nullify_children: Keep child pages as root pages
822 text_wiki_page_destroy_children: Delete child pages and all their descendants
822 text_wiki_page_destroy_children: Delete child pages and all their descendants
823 setting_password_min_length: Minimum password length
823 setting_password_min_length: Minimum password length
824 field_group_by: Group results by
@@ -804,3 +804,4 he:
804 text_wiki_page_nullify_children: Keep child pages as root pages
804 text_wiki_page_nullify_children: Keep child pages as root pages
805 text_wiki_page_destroy_children: Delete child pages and all their descendants
805 text_wiki_page_destroy_children: Delete child pages and all their descendants
806 setting_password_min_length: Minimum password length
806 setting_password_min_length: Minimum password length
807 field_group_by: Group results by
@@ -827,3 +827,4
827 text_wiki_page_nullify_children: Keep child pages as root pages
827 text_wiki_page_nullify_children: Keep child pages as root pages
828 text_wiki_page_destroy_children: Delete child pages and all their descendants
828 text_wiki_page_destroy_children: Delete child pages and all their descendants
829 setting_password_min_length: Minimum password length
829 setting_password_min_length: Minimum password length
830 field_group_by: Group results by
@@ -807,3 +807,4 it:
807 text_wiki_page_nullify_children: Keep child pages as root pages
807 text_wiki_page_nullify_children: Keep child pages as root pages
808 text_wiki_page_destroy_children: Delete child pages and all their descendants
808 text_wiki_page_destroy_children: Delete child pages and all their descendants
809 setting_password_min_length: Minimum password length
809 setting_password_min_length: Minimum password length
810 field_group_by: Group results by
@@ -820,3 +820,4 ja:
820 text_wiki_page_nullify_children: Keep child pages as root pages
820 text_wiki_page_nullify_children: Keep child pages as root pages
821 text_wiki_page_destroy_children: Delete child pages and all their descendants
821 text_wiki_page_destroy_children: Delete child pages and all their descendants
822 setting_password_min_length: Minimum password length
822 setting_password_min_length: Minimum password length
823 field_group_by: Group results by
@@ -851,3 +851,4 ko:
851 text_wiki_page_nullify_children: Keep child pages as root pages
851 text_wiki_page_nullify_children: Keep child pages as root pages
852 text_wiki_page_destroy_children: Delete child pages and all their descendants
852 text_wiki_page_destroy_children: Delete child pages and all their descendants
853 setting_password_min_length: Minimum password length
853 setting_password_min_length: Minimum password length
854 field_group_by: Group results by
@@ -832,3 +832,4 lt:
832 text_wiki_page_nullify_children: Keep child pages as root pages
832 text_wiki_page_nullify_children: Keep child pages as root pages
833 text_wiki_page_destroy_children: Delete child pages and all their descendants
833 text_wiki_page_destroy_children: Delete child pages and all their descendants
834 setting_password_min_length: Minimum password length
834 setting_password_min_length: Minimum password length
835 field_group_by: Group results by
@@ -777,3 +777,4 nl:
777 text_wiki_page_nullify_children: Keep child pages as root pages
777 text_wiki_page_nullify_children: Keep child pages as root pages
778 text_wiki_page_destroy_children: Delete child pages and all their descendants
778 text_wiki_page_destroy_children: Delete child pages and all their descendants
779 setting_password_min_length: Minimum password length
779 setting_password_min_length: Minimum password length
780 field_group_by: Group results by
@@ -794,3 +794,4
794 text_wiki_page_nullify_children: Keep child pages as root pages
794 text_wiki_page_nullify_children: Keep child pages as root pages
795 text_wiki_page_destroy_children: Delete child pages and all their descendants
795 text_wiki_page_destroy_children: Delete child pages and all their descendants
796 setting_password_min_length: Minimum password length
796 setting_password_min_length: Minimum password length
797 field_group_by: Group results by
@@ -825,3 +825,4 pl:
825 text_wiki_page_nullify_children: Keep child pages as root pages
825 text_wiki_page_nullify_children: Keep child pages as root pages
826 text_wiki_page_destroy_children: Delete child pages and all their descendants
826 text_wiki_page_destroy_children: Delete child pages and all their descendants
827 setting_password_min_length: Minimum password length
827 setting_password_min_length: Minimum password length
828 field_group_by: Group results by
@@ -827,3 +827,4 pt-BR:
827 text_wiki_page_nullify_children: Keep child pages as root pages
827 text_wiki_page_nullify_children: Keep child pages as root pages
828 text_wiki_page_destroy_children: Delete child pages and all their descendants
828 text_wiki_page_destroy_children: Delete child pages and all their descendants
829 setting_password_min_length: Minimum password length
829 setting_password_min_length: Minimum password length
830 field_group_by: Group results by
@@ -813,3 +813,4 pt:
813 text_wiki_page_nullify_children: Keep child pages as root pages
813 text_wiki_page_nullify_children: Keep child pages as root pages
814 text_wiki_page_destroy_children: Delete child pages and all their descendants
814 text_wiki_page_destroy_children: Delete child pages and all their descendants
815 setting_password_min_length: Minimum password length
815 setting_password_min_length: Minimum password length
816 field_group_by: Group results by
@@ -792,3 +792,4 ro:
792 text_wiki_page_nullify_children: Keep child pages as root pages
792 text_wiki_page_nullify_children: Keep child pages as root pages
793 text_wiki_page_destroy_children: Delete child pages and all their descendants
793 text_wiki_page_destroy_children: Delete child pages and all their descendants
794 setting_password_min_length: Minimum password length
794 setting_password_min_length: Minimum password length
795 field_group_by: Group results by
@@ -919,3 +919,4 ru:
919 text_wiki_page_nullify_children: Keep child pages as root pages
919 text_wiki_page_nullify_children: Keep child pages as root pages
920 text_wiki_page_destroy_children: Delete child pages and all their descendants
920 text_wiki_page_destroy_children: Delete child pages and all their descendants
921 setting_password_min_length: Minimum password length
921 setting_password_min_length: Minimum password length
922 field_group_by: Group results by
@@ -793,3 +793,4 sk:
793 text_wiki_page_nullify_children: Keep child pages as root pages
793 text_wiki_page_nullify_children: Keep child pages as root pages
794 text_wiki_page_destroy_children: Delete child pages and all their descendants
794 text_wiki_page_destroy_children: Delete child pages and all their descendants
795 setting_password_min_length: Minimum password length
795 setting_password_min_length: Minimum password length
796 field_group_by: Group results by
@@ -791,3 +791,4 sl:
791 text_wiki_page_nullify_children: Keep child pages as root pages
791 text_wiki_page_nullify_children: Keep child pages as root pages
792 text_wiki_page_destroy_children: Delete child pages and all their descendants
792 text_wiki_page_destroy_children: Delete child pages and all their descendants
793 setting_password_min_length: Minimum password length
793 setting_password_min_length: Minimum password length
794 field_group_by: Group results by
@@ -815,3 +815,4
815 text_wiki_page_nullify_children: Keep child pages as root pages
815 text_wiki_page_nullify_children: Keep child pages as root pages
816 text_wiki_page_destroy_children: Delete child pages and all their descendants
816 text_wiki_page_destroy_children: Delete child pages and all their descendants
817 setting_password_min_length: Minimum password length
817 setting_password_min_length: Minimum password length
818 field_group_by: Group results by
@@ -849,3 +849,4 sv:
849 text_wiki_page_nullify_children: Keep child pages as root pages
849 text_wiki_page_nullify_children: Keep child pages as root pages
850 text_wiki_page_destroy_children: Delete child pages and all their descendants
850 text_wiki_page_destroy_children: Delete child pages and all their descendants
851 setting_password_min_length: Minimum password length
851 setting_password_min_length: Minimum password length
852 field_group_by: Group results by
@@ -792,3 +792,4 th:
792 text_wiki_page_nullify_children: Keep child pages as root pages
792 text_wiki_page_nullify_children: Keep child pages as root pages
793 text_wiki_page_destroy_children: Delete child pages and all their descendants
793 text_wiki_page_destroy_children: Delete child pages and all their descendants
794 setting_password_min_length: Minimum password length
794 setting_password_min_length: Minimum password length
795 field_group_by: Group results by
@@ -828,3 +828,4 tr:
828 text_wiki_page_nullify_children: Keep child pages as root pages
828 text_wiki_page_nullify_children: Keep child pages as root pages
829 text_wiki_page_destroy_children: Delete child pages and all their descendants
829 text_wiki_page_destroy_children: Delete child pages and all their descendants
830 setting_password_min_length: Minimum password length
830 setting_password_min_length: Minimum password length
831 field_group_by: Group results by
@@ -791,3 +791,4 uk:
791 text_wiki_page_nullify_children: Keep child pages as root pages
791 text_wiki_page_nullify_children: Keep child pages as root pages
792 text_wiki_page_destroy_children: Delete child pages and all their descendants
792 text_wiki_page_destroy_children: Delete child pages and all their descendants
793 setting_password_min_length: Minimum password length
793 setting_password_min_length: Minimum password length
794 field_group_by: Group results by
@@ -861,3 +861,4 vi:
861 text_wiki_page_nullify_children: Keep child pages as root pages
861 text_wiki_page_nullify_children: Keep child pages as root pages
862 text_wiki_page_destroy_children: Delete child pages and all their descendants
862 text_wiki_page_destroy_children: Delete child pages and all their descendants
863 setting_password_min_length: Minimum password length
863 setting_password_min_length: Minimum password length
864 field_group_by: Group results by
@@ -899,3 +899,4
899 text_wiki_page_nullify_children: Keep child pages as root pages
899 text_wiki_page_nullify_children: Keep child pages as root pages
900 text_wiki_page_destroy_children: Delete child pages and all their descendants
900 text_wiki_page_destroy_children: Delete child pages and all their descendants
901 setting_password_min_length: Minimum password length
901 setting_password_min_length: Minimum password length
902 field_group_by: Group results by
@@ -824,3 +824,4 zh:
824 text_wiki_page_nullify_children: Keep child pages as root pages
824 text_wiki_page_nullify_children: Keep child pages as root pages
825 text_wiki_page_destroy_children: Delete child pages and all their descendants
825 text_wiki_page_destroy_children: Delete child pages and all their descendants
826 setting_password_min_length: Minimum password length
826 setting_password_min_length: Minimum password length
827 field_group_by: Group results by
@@ -108,7 +108,7 module Redmine
108 end
108 end
109
109
110 # Returns a PDF string of a list of issues
110 # Returns a PDF string of a list of issues
111 def issues_to_pdf(issues, project)
111 def issues_to_pdf(issues, project, query)
112 pdf = IFPDF.new(current_language)
112 pdf = IFPDF.new(current_language)
113 title = project ? "#{project} - #{l(:label_issue_plural)}" : "#{l(:label_issue_plural)}"
113 title = project ? "#{project} - #{l(:label_issue_plural)}" : "#{l(:label_issue_plural)}"
114 pdf.SetTitle(title)
114 pdf.SetTitle(title)
@@ -140,7 +140,18 module Redmine
140 # rows
140 # rows
141 pdf.SetFontStyle('',9)
141 pdf.SetFontStyle('',9)
142 pdf.SetFillColor(255, 255, 255)
142 pdf.SetFillColor(255, 255, 255)
143 issues.each do |issue|
143 group = false
144 issues.each do |issue|
145 if query.grouped? && issue.send(query.group_by) != group
146 group = issue.send(query.group_by)
147 pdf.SetFontStyle('B',10)
148 pdf.Cell(0, row_height, "#{group.blank? ? 'None' : group.to_s}", 0, 1, 'L')
149 pdf.Line(10, pdf.GetY, 287, pdf.GetY)
150 pdf.SetY(pdf.GetY() + 0.5)
151 pdf.Line(10, pdf.GetY, 287, pdf.GetY)
152 pdf.SetY(pdf.GetY() + 1)
153 pdf.SetFontStyle('',9)
154 end
144 pdf.Cell(15, row_height, issue.id.to_s, 0, 0, 'L', 1)
155 pdf.Cell(15, row_height, issue.id.to_s, 0, 0, 'L', 1)
145 pdf.Cell(30, row_height, issue.tracker.name, 0, 0, 'L', 1)
156 pdf.Cell(30, row_height, issue.tracker.name, 0, 0, 'L', 1)
146 pdf.Cell(30, row_height, issue.status.name, 0, 0, 'L', 1)
157 pdf.Cell(30, row_height, issue.status.name, 0, 0, 'L', 1)
@@ -87,7 +87,7 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
87 table.list td { vertical-align: top; }
87 table.list td { vertical-align: top; }
88 table.list td.id { width: 2%; text-align: center;}
88 table.list td.id { width: 2%; text-align: center;}
89 table.list td.checkbox { width: 15px; padding: 0px;}
89 table.list td.checkbox { width: 15px; padding: 0px;}
90
90
91 tr.project td.name a { padding-left: 16px; white-space:nowrap; }
91 tr.project td.name a { padding-left: 16px; white-space:nowrap; }
92 tr.project.parent td.name a { background: url('../images/bullet_toggle_minus.png') no-repeat; }
92 tr.project.parent td.name a { background: url('../images/bullet_toggle_minus.png') no-repeat; }
93
93
@@ -136,7 +136,11 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px;
136 table.plugins span.description { display: block; font-size: 0.9em; }
136 table.plugins span.description { display: block; font-size: 0.9em; }
137 table.plugins span.url { display: block; font-size: 0.9em; }
137 table.plugins span.url { display: block; font-size: 0.9em; }
138
138
139 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
140 table.list tbody tr.group span.count { color: #aaa; font-size: 80%; }
141
139 table.list tbody tr:hover { background-color:#ffffdd; }
142 table.list tbody tr:hover { background-color:#ffffdd; }
143 table.list tbody tr.group:hover { background-color:inherit; }
140 table td {padding:2px;}
144 table td {padding:2px;}
141 table p {margin:0;}
145 table p {margin:0;}
142 .odd {background-color:#f6f7f8;}
146 .odd {background-color:#f6f7f8;}
@@ -187,13 +191,17 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
187 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
191 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
188 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
192 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
189
193
194 #query_form_content { font-size: 0.9em; padding: 4px; background: #f6f6f6; border: 1px solid #e4e4e4; }
195 #query_form_content fieldset#filters { border-left: 0; border-right: 0; }
196 #query_form_content p { margin-top: 0.5em; margin-bottom: 0.5em; }
197
190 fieldset#filters, fieldset#date-range { padding: 0.7em; margin-bottom: 8px; }
198 fieldset#filters, fieldset#date-range { padding: 0.7em; margin-bottom: 8px; }
191 fieldset#filters p { margin: 1.2em 0 0.8em 2px; }
199 fieldset#filters p { margin: 1.2em 0 0.8em 2px; }
192 fieldset#filters table { border-collapse: collapse; }
200 fieldset#filters table { border-collapse: collapse; }
193 fieldset#filters table td { padding: 0; vertical-align: middle; }
201 fieldset#filters table td { padding: 0; vertical-align: middle; }
194 fieldset#filters tr.filter { height: 2em; }
202 fieldset#filters tr.filter { height: 2em; }
195 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
203 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
196 .buttons { font-size: 0.9em; }
204 .buttons { font-size: 0.9em; margin-bottom: 1.4em; }
197
205
198 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
206 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
199 div#issue-changesets .changeset { padding: 4px;}
207 div#issue-changesets .changeset { padding: 4px;}
@@ -73,6 +73,7 queries_005:
73 is_public: true
73 is_public: true
74 name: Open issues by priority and tracker
74 name: Open issues by priority and tracker
75 filters: |
75 filters: |
76 ---
76 status_id:
77 status_id:
77 :values:
78 :values:
78 - "1"
79 - "1"
@@ -86,4 +87,23 queries_005:
86 - desc
87 - desc
87 - - tracker
88 - - tracker
88 - asc
89 - asc
90 queries_006:
91 id: 6
92 project_id:
93 is_public: true
94 name: Open issues grouped by tracker
95 filters: |
96 ---
97 status_id:
98 :values:
99 - "1"
100 :operator: o
101
102 user_id: 1
103 column_names:
104 group_by: tracker
105 sort_criteria: |
106 ---
107 - - priority
108 - desc
89 No newline at end of file
109
@@ -161,6 +161,22 class IssuesControllerTest < Test::Unit::TestCase
161 assert_not_nil assigns(:issues)
161 assert_not_nil assigns(:issues)
162 end
162 end
163
163
164 def test_index_with_query
165 get :index, :project_id => 1, :query_id => 5
166 assert_response :success
167 assert_template 'index.rhtml'
168 assert_not_nil assigns(:issues)
169 assert_nil assigns(:issue_count_by_group)
170 end
171
172 def test_index_with_grouped_query
173 get :index, :project_id => 1, :query_id => 6
174 assert_response :success
175 assert_template 'index.rhtml'
176 assert_not_nil assigns(:issues)
177 assert_not_nil assigns(:issue_count_by_group)
178 end
179
164 def test_index_csv_with_project
180 def test_index_csv_with_project
165 get :index, :format => 'csv'
181 get :index, :format => 'csv'
166 assert_response :success
182 assert_response :success
@@ -194,6 +210,11 class IssuesControllerTest < Test::Unit::TestCase
194 assert_response :success
210 assert_response :success
195 assert_not_nil assigns(:issues)
211 assert_not_nil assigns(:issues)
196 assert_equal 'application/pdf', @response.content_type
212 assert_equal 'application/pdf', @response.content_type
213
214 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
215 assert_response :success
216 assert_not_nil assigns(:issues)
217 assert_equal 'application/pdf', @response.content_type
197 end
218 end
198
219
199 def test_index_sort
220 def test_index_sort
General Comments 0
You need to be logged in to leave comments. Login now