@@ -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