@@ -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 | 58 | end |
|
59 | 59 | @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement) |
|
60 | 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 | 62 | :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ], |
|
63 | 63 | :conditions => @query.statement, |
|
64 | 64 | :limit => limit, |
|
65 | 65 | :offset => @issue_pages.current.offset |
|
66 | 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 | 79 | format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") } |
|
69 | 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 | 82 | end |
|
72 | 83 | else |
|
73 | 84 | # Send html if the query is not valid |
@@ -483,10 +494,11 private | |||
|
483 | 494 | @query.add_short_filter(field, params[field]) if params[field] |
|
484 | 495 | end |
|
485 | 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 | 499 | else |
|
488 | 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 | 502 | @query.project = @project |
|
491 | 503 | end |
|
492 | 504 | end |
@@ -30,6 +30,7 class QueriesController < ApplicationController | |||
|
30 | 30 | params[:fields].each do |field| |
|
31 | 31 | @query.add_filter(field, params[:operators][field], params[:values][field]) |
|
32 | 32 | end if params[:fields] |
|
33 | @query.group_by ||= params[:group_by] | |
|
33 | 34 | |
|
34 | 35 | if request.post? && params[:confirm] && @query.save |
|
35 | 36 | flash[:notice] = l(:notice_successful_create) |
@@ -16,12 +16,13 | |||
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class QueryColumn |
|
19 | attr_accessor :name, :sortable, :default_order | |
|
19 | attr_accessor :name, :sortable, :groupable, :default_order | |
|
20 | 20 | include Redmine::I18n |
|
21 | 21 | |
|
22 | 22 | def initialize(name, options={}) |
|
23 | 23 | self.name = name |
|
24 | 24 | self.sortable = options[:sortable] |
|
25 | self.groupable = options[:groupable] || false | |
|
25 | 26 | self.default_order = options[:default_order] |
|
26 | 27 | end |
|
27 | 28 | |
@@ -98,20 +99,20 class Query < ActiveRecord::Base | |||
|
98 | 99 | cattr_reader :operators_by_filter_type |
|
99 | 100 | |
|
100 | 101 | @@available_columns = [ |
|
101 | QueryColumn.new(:project, :sortable => "#{Project.table_name}.name"), | |
|
102 | QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"), | |
|
103 | QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"), | |
|
104 | QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'), | |
|
102 | QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true), | |
|
103 | QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true), | |
|
104 | QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true), | |
|
105 | QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc', :groupable => true), | |
|
105 | 106 | QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"), |
|
106 | 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 | 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(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc'), | |
|
110 | QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true), | |
|
111 | QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true), | |
|
111 | 112 | QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"), |
|
112 | 113 | QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"), |
|
113 | 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 | 116 | QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), |
|
116 | 117 | ] |
|
117 | 118 | cattr_reader :available_columns |
@@ -241,6 +242,11 class Query < ActiveRecord::Base | |||
|
241 | 242 | ).collect {|cf| QueryCustomFieldColumn.new(cf) } |
|
242 | 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 | 250 | def columns |
|
245 | 251 | if has_default_columns? |
|
246 | 252 | available_columns.select do |c| |
@@ -288,6 +294,24 class Query < ActiveRecord::Base | |||
|
288 | 294 | sort_criteria && sort_criteria[arg] && sort_criteria[arg].last |
|
289 | 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 | 315 | def project_statement |
|
292 | 316 | project_clauses = [] |
|
293 | 317 | if project && !@project.descendants.active.empty? |
@@ -9,8 +9,18 | |||
|
9 | 9 | <%= column_header(column) %> |
|
10 | 10 | <% end %> |
|
11 | 11 | </tr></thead> |
|
12 | <% group = false %> | |
|
12 | 13 | <tbody> |
|
13 | 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 | 24 | <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>"> |
|
15 | 25 | <td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td> |
|
16 | 26 | <td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td> |
@@ -4,9 +4,15 | |||
|
4 | 4 | |
|
5 | 5 | <% form_tag({ :controller => 'queries', :action => 'new' }, :id => 'query_form') do %> |
|
6 | 6 | <%= hidden_field_tag('project_id', @project.to_param) if @project %> |
|
7 | <div id="query_form_content"> | |
|
7 | 8 | <fieldset id="filters"><legend><%= l(:label_filter_plural) %></legend> |
|
8 | 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 | 14 | <p class="buttons"> |
|
15 | ||
|
10 | 16 | <%= link_to_remote l(:button_apply), |
|
11 | 17 | { :url => { :set_filter => 1 }, |
|
12 | 18 | :update => "content", |
@@ -23,7 +29,6 | |||
|
23 | 29 | <%= link_to l(:button_save), {}, :onclick => "$('query_form').submit(); return false;", :class => 'icon icon-save' %> |
|
24 | 30 | <% end %> |
|
25 | 31 | </p> |
|
26 | </fieldset> | |
|
27 | 32 | <% end %> |
|
28 | 33 | <% else %> |
|
29 | 34 | <div class="contextual"> |
@@ -36,6 +41,7 | |||
|
36 | 41 | <div id="query_form"></div> |
|
37 | 42 | <% html_title @query.name %> |
|
38 | 43 | <% end %> |
|
44 | ||
|
39 | 45 | <%= error_messages_for 'query' %> |
|
40 | 46 | <% if @query.valid? %> |
|
41 | 47 | <% if @issues.empty? %> |
@@ -19,6 +19,9 | |||
|
19 | 19 | <p><label for="query_default_columns"><%=l(:label_default_columns)%></label> |
|
20 | 20 | <%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns', |
|
21 | 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 | 25 | </div> |
|
23 | 26 | |
|
24 | 27 | <fieldset><legend><%= l(:label_filter_plural) %></legend> |
@@ -789,3 +789,4 bg: | |||
|
789 | 789 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
790 | 790 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
791 | 791 | setting_password_min_length: Minimum password length |
|
792 | field_group_by: Group results by |
@@ -822,3 +822,4 bs: | |||
|
822 | 822 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
823 | 823 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
824 | 824 | setting_password_min_length: Minimum password length |
|
825 | field_group_by: Group results by |
@@ -792,3 +792,4 ca: | |||
|
792 | 792 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
793 | 793 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
794 | 794 | setting_password_min_length: Minimum password length |
|
795 | field_group_by: Group results by |
@@ -795,3 +795,4 cs: | |||
|
795 | 795 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
796 | 796 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
797 | 797 | setting_password_min_length: Minimum password length |
|
798 | field_group_by: Group results by |
@@ -822,3 +822,4 da: | |||
|
822 | 822 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
823 | 823 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
824 | 824 | setting_password_min_length: Minimum password length |
|
825 | field_group_by: Group results by |
@@ -821,3 +821,4 de: | |||
|
821 | 821 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
822 | 822 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
823 | 823 | setting_password_min_length: Minimum password length |
|
824 | field_group_by: Group results by |
@@ -242,6 +242,7 en: | |||
|
242 | 242 | field_watcher: Watcher |
|
243 | 243 | field_identity_url: OpenID URL |
|
244 | 244 | field_content: Content |
|
245 | field_group_by: Group results by | |
|
245 | 246 | |
|
246 | 247 | setting_app_title: Application title |
|
247 | 248 | setting_app_subtitle: Application subtitle |
@@ -842,3 +842,4 es: | |||
|
842 | 842 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
843 | 843 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
844 | 844 | setting_password_min_length: Minimum password length |
|
845 | field_group_by: Group results by |
@@ -832,3 +832,4 fi: | |||
|
832 | 832 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
833 | 833 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
834 | 834 | setting_password_min_length: Minimum password length |
|
835 | field_group_by: Group results by |
@@ -274,6 +274,7 fr: | |||
|
274 | 274 | field_watcher: Observateur |
|
275 | 275 | field_identity_url: URL OpenID |
|
276 | 276 | field_content: Contenu |
|
277 | field_group_by: Grouper par | |
|
277 | 278 | |
|
278 | 279 | setting_app_title: Titre de l'application |
|
279 | 280 | setting_app_subtitle: Sous-titre de l'application |
@@ -821,3 +821,4 gl: | |||
|
821 | 821 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
822 | 822 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
823 | 823 | setting_password_min_length: Minimum password length |
|
824 | field_group_by: Group results by |
@@ -804,3 +804,4 he: | |||
|
804 | 804 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
805 | 805 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
806 | 806 | setting_password_min_length: Minimum password length |
|
807 | field_group_by: Group results by |
@@ -827,3 +827,4 | |||
|
827 | 827 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
828 | 828 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
829 | 829 | setting_password_min_length: Minimum password length |
|
830 | field_group_by: Group results by |
@@ -807,3 +807,4 it: | |||
|
807 | 807 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
808 | 808 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
809 | 809 | setting_password_min_length: Minimum password length |
|
810 | field_group_by: Group results by |
@@ -820,3 +820,4 ja: | |||
|
820 | 820 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
821 | 821 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
822 | 822 | setting_password_min_length: Minimum password length |
|
823 | field_group_by: Group results by |
@@ -851,3 +851,4 ko: | |||
|
851 | 851 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
852 | 852 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
853 | 853 | setting_password_min_length: Minimum password length |
|
854 | field_group_by: Group results by |
@@ -832,3 +832,4 lt: | |||
|
832 | 832 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
833 | 833 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
834 | 834 | setting_password_min_length: Minimum password length |
|
835 | field_group_by: Group results by |
@@ -777,3 +777,4 nl: | |||
|
777 | 777 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
778 | 778 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
779 | 779 | setting_password_min_length: Minimum password length |
|
780 | field_group_by: Group results by |
@@ -794,3 +794,4 | |||
|
794 | 794 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
795 | 795 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
796 | 796 | setting_password_min_length: Minimum password length |
|
797 | field_group_by: Group results by |
@@ -825,3 +825,4 pl: | |||
|
825 | 825 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
826 | 826 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
827 | 827 | setting_password_min_length: Minimum password length |
|
828 | field_group_by: Group results by |
@@ -827,3 +827,4 pt-BR: | |||
|
827 | 827 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
828 | 828 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
829 | 829 | setting_password_min_length: Minimum password length |
|
830 | field_group_by: Group results by |
@@ -813,3 +813,4 pt: | |||
|
813 | 813 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
814 | 814 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
815 | 815 | setting_password_min_length: Minimum password length |
|
816 | field_group_by: Group results by |
@@ -792,3 +792,4 ro: | |||
|
792 | 792 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
793 | 793 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
794 | 794 | setting_password_min_length: Minimum password length |
|
795 | field_group_by: Group results by |
@@ -919,3 +919,4 ru: | |||
|
919 | 919 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
920 | 920 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
921 | 921 | setting_password_min_length: Minimum password length |
|
922 | field_group_by: Group results by |
@@ -793,3 +793,4 sk: | |||
|
793 | 793 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
794 | 794 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
795 | 795 | setting_password_min_length: Minimum password length |
|
796 | field_group_by: Group results by |
@@ -791,3 +791,4 sl: | |||
|
791 | 791 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
792 | 792 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
793 | 793 | setting_password_min_length: Minimum password length |
|
794 | field_group_by: Group results by |
@@ -815,3 +815,4 | |||
|
815 | 815 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
816 | 816 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
817 | 817 | setting_password_min_length: Minimum password length |
|
818 | field_group_by: Group results by |
@@ -849,3 +849,4 sv: | |||
|
849 | 849 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
850 | 850 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
851 | 851 | setting_password_min_length: Minimum password length |
|
852 | field_group_by: Group results by |
@@ -792,3 +792,4 th: | |||
|
792 | 792 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
793 | 793 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
794 | 794 | setting_password_min_length: Minimum password length |
|
795 | field_group_by: Group results by |
@@ -828,3 +828,4 tr: | |||
|
828 | 828 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
829 | 829 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
830 | 830 | setting_password_min_length: Minimum password length |
|
831 | field_group_by: Group results by |
@@ -791,3 +791,4 uk: | |||
|
791 | 791 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
792 | 792 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
793 | 793 | setting_password_min_length: Minimum password length |
|
794 | field_group_by: Group results by |
@@ -861,3 +861,4 vi: | |||
|
861 | 861 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
862 | 862 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
863 | 863 | setting_password_min_length: Minimum password length |
|
864 | field_group_by: Group results by |
@@ -899,3 +899,4 | |||
|
899 | 899 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
900 | 900 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
901 | 901 | setting_password_min_length: Minimum password length |
|
902 | field_group_by: Group results by |
@@ -824,3 +824,4 zh: | |||
|
824 | 824 | text_wiki_page_nullify_children: Keep child pages as root pages |
|
825 | 825 | text_wiki_page_destroy_children: Delete child pages and all their descendants |
|
826 | 826 | setting_password_min_length: Minimum password length |
|
827 | field_group_by: Group results by |
@@ -108,7 +108,7 module Redmine | |||
|
108 | 108 | end |
|
109 | 109 | |
|
110 | 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 | 112 | pdf = IFPDF.new(current_language) |
|
113 | 113 | title = project ? "#{project} - #{l(:label_issue_plural)}" : "#{l(:label_issue_plural)}" |
|
114 | 114 | pdf.SetTitle(title) |
@@ -140,7 +140,18 module Redmine | |||
|
140 | 140 | # rows |
|
141 | 141 | pdf.SetFontStyle('',9) |
|
142 | 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 | 155 | pdf.Cell(15, row_height, issue.id.to_s, 0, 0, 'L', 1) |
|
145 | 156 | pdf.Cell(30, row_height, issue.tracker.name, 0, 0, 'L', 1) |
|
146 | 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 | 87 | table.list td { vertical-align: top; } |
|
88 | 88 | table.list td.id { width: 2%; text-align: center;} |
|
89 | 89 | table.list td.checkbox { width: 15px; padding: 0px;} |
|
90 | ||
|
90 | ||
|
91 | 91 | tr.project td.name a { padding-left: 16px; white-space:nowrap; } |
|
92 | 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 | 136 | table.plugins span.description { display: block; font-size: 0.9em; } |
|
137 | 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 | 142 | table.list tbody tr:hover { background-color:#ffffdd; } |
|
143 | table.list tbody tr.group:hover { background-color:inherit; } | |
|
140 | 144 | table td {padding:2px;} |
|
141 | 145 | table p {margin:0;} |
|
142 | 146 | .odd {background-color:#f6f7f8;} |
@@ -187,13 +191,17 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;} | |||
|
187 | 191 | p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; } |
|
188 | 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 | 198 | fieldset#filters, fieldset#date-range { padding: 0.7em; margin-bottom: 8px; } |
|
191 | 199 | fieldset#filters p { margin: 1.2em 0 0.8em 2px; } |
|
192 | 200 | fieldset#filters table { border-collapse: collapse; } |
|
193 | 201 | fieldset#filters table td { padding: 0; vertical-align: middle; } |
|
194 | 202 | fieldset#filters tr.filter { height: 2em; } |
|
195 | 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 | 206 | div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;} |
|
199 | 207 | div#issue-changesets .changeset { padding: 4px;} |
@@ -73,6 +73,7 queries_005: | |||
|
73 | 73 | is_public: true |
|
74 | 74 | name: Open issues by priority and tracker |
|
75 | 75 | filters: | |
|
76 | --- | |
|
76 | 77 | status_id: |
|
77 | 78 | :values: |
|
78 | 79 | - "1" |
@@ -86,4 +87,23 queries_005: | |||
|
86 | 87 | - desc |
|
87 | 88 | - - tracker |
|
88 | 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 | 109 | No newline at end of file |
@@ -161,6 +161,22 class IssuesControllerTest < Test::Unit::TestCase | |||
|
161 | 161 | assert_not_nil assigns(:issues) |
|
162 | 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 | 180 | def test_index_csv_with_project |
|
165 | 181 | get :index, :format => 'csv' |
|
166 | 182 | assert_response :success |
@@ -194,6 +210,11 class IssuesControllerTest < Test::Unit::TestCase | |||
|
194 | 210 | assert_response :success |
|
195 | 211 | assert_not_nil assigns(:issues) |
|
196 | 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 | 218 | end |
|
198 | 219 | |
|
199 | 220 | def test_index_sort |
General Comments 0
You need to be logged in to leave comments.
Login now