diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb index 77a5779985..e4c4fd61e6 100644 --- a/app/helpers/queries_helper.rb +++ b/app/helpers/queries_helper.rb @@ -318,13 +318,33 @@ module QueriesHelper def query_to_csv(items, query, options={}) columns = query.columns + journal_notes_columns = query.journal_notes_columns Redmine::Export::CSV.generate(:encoding => params[:encoding]) do |csv| # csv header fields csv << columns.map {|c| c.caption.to_s} # csv lines - items.each do |item| - csv << columns.map {|c| csv_content(c, item)} + if journal_notes_columns.present? + without_journal_notes_columns = query.without_journal_notes_columns + + items.each do |item| + if item.journals_with_notes.blank? + csv << without_journal_notes_columns.map {|c| csv_content(c, item)} + next + end + + item.journals_with_notes.each_with_index do |journal, index| + if index == 0 + csv << without_journal_notes_columns.map {|c| csv_content(c, item)} + journal_notes_columns.map {|c| csv_content(c, journal)} + else + csv << without_journal_notes_columns.map {|c| csv_content(c, item) if c.name == :id } + journal_notes_columns.map {|c| csv_content(c, journal)} + end + end + end + else + items.each do |item| + csv << columns.map {|c| csv_content(c, item)} + end end end end @@ -344,6 +364,12 @@ module QueriesHelper elsif api_request? || params[:set_filter] || !use_session || session[session_key].nil? || session[session_key][:project_id] != (@project ? @project.id : nil) + # Replace journal_notes params + if params[:journal_notes] && params[:format] == 'csv' + params['c'] ||= [] + params['c'] += IssueQuery.journal_column_names.map(&:to_s) + end + # Give it a name, required to be valid @query = klass.new(:name => "_", :project => @project) @query.build_from_params(params, options[:defaults]) diff --git a/app/models/issue.rb b/app/models/issue.rb index 02aaff33be..d1d88b1b89 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -261,6 +261,7 @@ class Issue < ActiveRecord::Base @total_estimated_hours = nil @last_updated_by = nil @last_notes = nil + @journals_with_notes = nil base_reload(*args) end @@ -1165,11 +1166,21 @@ class Issue < ActiveRecord::Base def last_notes if @last_notes @last_notes + elsif @journals_with_notes && @journals_with_notes.first.notes + @journals_with_notes.first.notes else journals.where.not(notes: '').reorder(:id => :desc).first.try(:notes) end end + def journals_with_notes + if @journals_with_notes + @journals_with_notes + else + journals.where.not(notes: '').reorder(:id => :asc) + end + end + # Preloads relations for a collection of issues def self.load_relations(issues) if issues.any? @@ -1260,8 +1271,15 @@ class Issue < ActiveRecord::Base end # Preloads visible last notes for a collection of issues - def self.load_visible_last_notes(issues, user=User.current) + def self.load_visible_last_notes(issues, user=User.current, has_notes=false) if issues.any? + if has_notes + issues.each do |issue| + issue.instance_variable_set(:@last_notes, issue.journals_with_notes.last.notes) + end + return + end + issue_ids = issues.map(&:id) journal_ids = Journal.joins(issue: :project). where(:journalized_type => 'Issue', :journalized_id => issue_ids). @@ -1279,6 +1297,22 @@ class Issue < ActiveRecord::Base end end + # Preloads visible journals_with_notes for a collection of issues + def self.load_visible_journals_with_notes(issues, user=User.current) + return unless issues.any? + + issue_ids = issues.map(&:id) + journals = Journal.joins(issue: :project). + where(:journalized_type => 'Issue', :journalized_id => issue_ids). + where(Journal.visible_notes_condition(user, :skip_pre_condition => true)). + where.not(notes: ''). + reorder(id: :asc) + + issues.each do |issue| + issue.instance_variable_set(:@journals_with_notes, journals.where(journalized_id: issue.id)) + end + end + # Finds an issue relation given its id. def find_relation(relation_id) IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id) diff --git a/app/models/issue_query.rb b/app/models/issue_query.rb index 5a3e14474d..ae54ee953d 100644 --- a/app/models/issue_query.rb +++ b/app/models/issue_query.rb @@ -70,7 +70,12 @@ class IssueQuery < Query QueryColumn.new(:relations, :caption => :label_related_issues), QueryColumn.new(:attachments, :caption => :label_attachment_plural), QueryColumn.new(:description, :inline => false), - QueryColumn.new(:last_notes, :caption => :label_last_notes, :inline => false) + QueryColumn.new(:last_notes, :caption => :label_last_notes, :inline => false), + QueryJournalsColumn.new(:id, :caption => :label_notes_id), + QueryJournalsColumn.new(:notes, :caption => :field_notes), + QueryJournalsColumn.new(:user, :caption => :label_notes_author), + QueryJournalsColumn.new(:created_on, :caption => :label_notes_created_on), + QueryJournalsColumn.new(:private_notes, :caption => :field_private_notes) ] has_many :projects, foreign_key: 'default_issue_query_id', dependent: :nullify, inverse_of: 'default_issue_query' @@ -328,6 +333,10 @@ class IssueQuery < Query @available_columns end + def self.journal_column_names + available_columns.select{|c| c.is_a?(QueryJournalsColumn)}.map(&:name) + end + def default_columns_names @default_columns_names ||= begin default_columns = Setting.issue_list_default_columns.map(&:to_sym) @@ -410,8 +419,11 @@ class IssueQuery < Query if has_column?(:relations) Issue.load_visible_relations(issues) end + if IssueQuery.journal_column_names.any?{|name| has_column?(name)} + Issue.load_visible_journals_with_notes(issues) + end if has_column?(:last_notes) - Issue.load_visible_last_notes(issues) + Issue.load_visible_last_notes(issues, User.current, IssueQuery.journal_column_names.any?{|name| has_column?(name)}) end issues rescue ::ActiveRecord::StatementInvalid => e diff --git a/app/models/query.rb b/app/models/query.rb index 0186cc379a..ad2d60e76c 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -62,6 +62,10 @@ class QueryColumn @inline end + def journal_notes? + false + end + def frozen? @frozen end @@ -199,6 +203,25 @@ class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn end end +class QueryJournalsColumn < QueryColumn + def initialize(name, options={}) + @attribute = name + @inline = false + name_with_assoc = "journals.#{name}".to_sym + super(name_with_assoc, options) + end + + def value_object(object) + return nil unless object.is_a?(Journal) + + object.send @attribute + end + + def journal_notes? + true + end +end + class QueryFilter include Redmine::I18n @@ -798,18 +821,30 @@ class Query < ActiveRecord::Base columns.reject(&:inline?) end + def journal_notes_columns + columns.select(&:journal_notes?) + end + + def without_journal_notes_columns + columns.reject(&:journal_notes?) + end + def available_inline_columns available_columns.select(&:inline?) end def available_block_columns - available_columns.reject(&:inline?) + available_columns.reject(&:inline?).reject(&:journal_notes?) end def available_totalable_columns available_columns.select(&:totalable) end + def available_journal_notes_columns + available_columns.select(&:journal_notes?) + end + def default_columns_names [] end diff --git a/app/views/issues/index.html.erb b/app/views/issues/index.html.erb index b05b6ccf2d..ac21024727 100644 --- a/app/views/issues/index.html.erb +++ b/app/views/issues/index.html.erb @@ -47,13 +47,18 @@

- <% if @query.available_block_columns.any? %> -
+ <% if @query.available_block_columns.any? || @query.available_journal_notes_columns.any? %> +
- <%= toggle_checkboxes_link('#csv-export-block-columns input[type=checkbox]') %> + <%= toggle_checkboxes_link('#csv-export-other-columns input[type=checkbox]') %> - <% @query.available_block_columns.each do |column| %> - + <% if @query.available_block_columns.any? %> + <% @query.available_block_columns.each do |column| %> + + <% end %> + <% end %> + <% if @query.available_journal_notes_columns.any? %> + <% end %>
<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 2378e56d5b..f78aabe3ee 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1122,6 +1122,10 @@ en: label_my_bookmarks: My bookmarks label_assign_to_me: Assign to me label_default_query: Default query + label_all_notes: All notes + label_notes_id: Notes-# + label_notes_author: Notes author + label_notes_created_on: Notes created button_login: Login button_submit: Submit diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index f4c269c7e3..76e839d0dd 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -960,6 +960,26 @@ class IssuesControllerTest < Redmine::ControllerTest assert lines.detect {|line| line.include?('"MySQL, Oracle"')} end + def test_index_csv_with_journal_notes_params + get( + :index, + :params => { + :format => 'csv', + :c => ['subject'], + :project => 'ecookbook', + :journal_notes => 1 + } + ) + assert_response :success + lines = @response.body.chomp.split("\n") + assert_equal 7, lines.first.split(',').count + + journal1 = Journal.find(1) + assert_equal "1,Cannot print recipes,#{journal1.id},#{journal1.notes},#{journal1.user},#{format_time(journal1.created_on)},No", lines[-2] + journal2 = Journal.find(2) + assert_equal "1,,#{journal2.id},\"#{journal2.notes}\",#{journal2.user},#{format_time(journal2.created_on)},No", lines[-1] + end + def test_index_csv_should_format_float_custom_fields_with_csv_decimal_separator field = IssueCustomField. diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb index f27e8c41f9..cf29895bb3 100644 --- a/test/unit/query_test.rb +++ b/test/unit/query_test.rb @@ -1709,6 +1709,13 @@ class QueryTest < ActiveSupport::TestCase assert_not_nil issues.first.instance_variable_get(:@last_notes) end + def test_query_should_preload_journals_with_notes + q = IssueQuery.new(:name => '_', :column_names => [:subject, :'journals.notes']) + assert q.has_column?(:'journals.notes') + issues = q.issues + assert_not_nil issues.first.instance_variable_get(:@journals_with_notes) + end + def test_groupable_columns_should_include_custom_fields q = IssueQuery.new column = q.groupable_columns.detect {|c| c.name == :cf_1}