feature-34040.patch

Mizuki ISHIKAWA, 2022-02-15 08:09

Download (11.7 KB)

View differences:

app/helpers/queries_helper.rb
318 318

  
319 319
  def query_to_csv(items, query, options={})
320 320
    columns = query.columns
321
    journal_notes_columns = query.journal_notes_columns
321 322

  
322 323
    Redmine::Export::CSV.generate(:encoding => params[:encoding]) do |csv|
323 324
      # csv header fields
324 325
      csv << columns.map {|c| c.caption.to_s}
325 326
      # csv lines
326
      items.each do |item|
327
        csv << columns.map {|c| csv_content(c, item)}
327
      if journal_notes_columns.present?
328
        without_journal_notes_columns = query.without_journal_notes_columns
329

  
330
        items.each do |item|
331
          if item.journals_with_notes.blank?
332
            csv << without_journal_notes_columns.map {|c| csv_content(c, item)}
333
            next
334
          end
335

  
336
          item.journals_with_notes.each_with_index do |journal, index|
337
            if index == 0
338
              csv << without_journal_notes_columns.map {|c| csv_content(c, item)} + journal_notes_columns.map {|c| csv_content(c, journal)}
339
            else
340
              csv << without_journal_notes_columns.map {|c| csv_content(c, item) if c.name == :id } + journal_notes_columns.map {|c| csv_content(c, journal)}
341
            end
342
          end
343
        end
344
      else
345
        items.each do |item|
346
          csv << columns.map {|c| csv_content(c, item)}
347
        end
328 348
      end
329 349
    end
330 350
  end
......
344 364
    elsif api_request? || params[:set_filter] || !use_session ||
345 365
            session[session_key].nil? ||
346 366
            session[session_key][:project_id] != (@project ? @project.id : nil)
367
      # Replace journal_notes params
368
      if params[:journal_notes] && params[:format] == 'csv'
369
        params['c'] ||= []
370
        params['c'] += IssueQuery.journal_column_names.map(&:to_s)
371
      end
372

  
347 373
      # Give it a name, required to be valid
348 374
      @query = klass.new(:name => "_", :project => @project)
349 375
      @query.build_from_params(params, options[:defaults])
app/models/issue.rb
261 261
    @total_estimated_hours = nil
262 262
    @last_updated_by = nil
263 263
    @last_notes = nil
264
    @journals_with_notes = nil
264 265
    base_reload(*args)
265 266
  end
266 267

  
......
1165 1166
  def last_notes
1166 1167
    if @last_notes
1167 1168
      @last_notes
1169
    elsif @journals_with_notes && @journals_with_notes.first.notes
1170
      @journals_with_notes.first.notes
1168 1171
    else
1169 1172
      journals.where.not(notes: '').reorder(:id => :desc).first.try(:notes)
1170 1173
    end
1171 1174
  end
1172 1175

  
1176
  def journals_with_notes
1177
    if @journals_with_notes
1178
      @journals_with_notes
1179
    else
1180
      journals.where.not(notes: '').reorder(:id => :asc)
1181
    end
1182
  end
1183

  
1173 1184
  # Preloads relations for a collection of issues
1174 1185
  def self.load_relations(issues)
1175 1186
    if issues.any?
......
1260 1271
  end
1261 1272

  
1262 1273
  # Preloads visible last notes for a collection of issues
1263
  def self.load_visible_last_notes(issues, user=User.current)
1274
  def self.load_visible_last_notes(issues, user=User.current, has_notes=false)
1264 1275
    if issues.any?
1276
      if has_notes
1277
        issues.each do |issue|
1278
          issue.instance_variable_set(:@last_notes, issue.journals_with_notes.last.notes)
1279
        end
1280
        return
1281
      end
1282

  
1265 1283
      issue_ids = issues.map(&:id)
1266 1284
      journal_ids = Journal.joins(issue: :project).
1267 1285
        where(:journalized_type => 'Issue', :journalized_id => issue_ids).
......
1279 1297
    end
1280 1298
  end
1281 1299

  
1300
  # Preloads visible journals_with_notes for a collection of issues
1301
  def self.load_visible_journals_with_notes(issues, user=User.current)
1302
    return unless issues.any?
1303

  
1304
    issue_ids = issues.map(&:id)
1305
    journals = Journal.joins(issue: :project).
1306
      where(:journalized_type => 'Issue', :journalized_id => issue_ids).
1307
      where(Journal.visible_notes_condition(user, :skip_pre_condition => true)).
1308
      where.not(notes: '').
1309
      reorder(id: :asc)
1310

  
1311
    issues.each do |issue|
1312
      issue.instance_variable_set(:@journals_with_notes, journals.where(journalized_id: issue.id))
1313
    end
1314
  end
1315

  
1282 1316
  # Finds an issue relation given its id.
1283 1317
  def find_relation(relation_id)
1284 1318
    IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
app/models/issue_query.rb
70 70
    QueryColumn.new(:relations, :caption => :label_related_issues),
71 71
    QueryColumn.new(:attachments, :caption => :label_attachment_plural),
72 72
    QueryColumn.new(:description, :inline => false),
73
    QueryColumn.new(:last_notes, :caption => :label_last_notes, :inline => false)
73
    QueryColumn.new(:last_notes, :caption => :label_last_notes, :inline => false),
74
    QueryJournalsColumn.new(:id, :caption => :label_notes_id),
75
    QueryJournalsColumn.new(:notes, :caption => :field_notes),
76
    QueryJournalsColumn.new(:user, :caption => :label_notes_author),
77
    QueryJournalsColumn.new(:created_on, :caption => :label_notes_created_on),
78
    QueryJournalsColumn.new(:private_notes, :caption => :field_private_notes)
74 79
  ]
75 80

  
76 81
  has_many :projects, foreign_key: 'default_issue_query_id', dependent: :nullify, inverse_of: 'default_issue_query'
......
328 333
    @available_columns
329 334
  end
330 335

  
336
  def self.journal_column_names
337
    available_columns.select{|c| c.is_a?(QueryJournalsColumn)}.map(&:name)
338
  end
339

  
331 340
  def default_columns_names
332 341
    @default_columns_names ||= begin
333 342
      default_columns = Setting.issue_list_default_columns.map(&:to_sym)
......
410 419
    if has_column?(:relations)
411 420
      Issue.load_visible_relations(issues)
412 421
    end
422
    if IssueQuery.journal_column_names.any?{|name| has_column?(name)}
423
      Issue.load_visible_journals_with_notes(issues)
424
    end
413 425
    if has_column?(:last_notes)
414
      Issue.load_visible_last_notes(issues)
426
      Issue.load_visible_last_notes(issues, User.current, IssueQuery.journal_column_names.any?{|name| has_column?(name)})
415 427
    end
416 428
    issues
417 429
  rescue ::ActiveRecord::StatementInvalid => e
app/models/query.rb
62 62
    @inline
63 63
  end
64 64

  
65
  def journal_notes?
66
    false
67
  end
68

  
65 69
  def frozen?
66 70
    @frozen
67 71
  end
......
199 203
  end
200 204
end
201 205

  
206
class QueryJournalsColumn < QueryColumn
207
  def initialize(name, options={})
208
    @attribute = name
209
    @inline = false
210
    name_with_assoc = "journals.#{name}".to_sym
211
    super(name_with_assoc, options)
212
  end
213

  
214
  def value_object(object)
215
    return nil unless object.is_a?(Journal)
216

  
217
    object.send @attribute
218
  end
219

  
220
  def journal_notes?
221
    true
222
  end
223
end
224

  
202 225
class QueryFilter
203 226
  include Redmine::I18n
204 227

  
......
798 821
    columns.reject(&:inline?)
799 822
  end
800 823

  
824
  def journal_notes_columns
825
    columns.select(&:journal_notes?)
826
  end
827

  
828
  def without_journal_notes_columns
829
    columns.reject(&:journal_notes?)
830
  end
831

  
801 832
  def available_inline_columns
802 833
    available_columns.select(&:inline?)
803 834
  end
804 835

  
805 836
  def available_block_columns
806
    available_columns.reject(&:inline?)
837
    available_columns.reject(&:inline?).reject(&:journal_notes?)
807 838
  end
808 839

  
809 840
  def available_totalable_columns
810 841
    available_columns.select(&:totalable)
811 842
  end
812 843

  
844
  def available_journal_notes_columns
845
    available_columns.select(&:journal_notes?)
846
  end
847

  
813 848
  def default_columns_names
814 849
    []
815 850
  end
app/views/issues/index.html.erb
47 47
    <label><%= radio_button_tag 'c[]', '', true %> <%= l(:description_selected_columns) %></label><br />
48 48
    <label><%= radio_button_tag 'c[]', 'all_inline' %> <%= l(:description_all_columns) %></label>
49 49
  </p>
50
  <% if @query.available_block_columns.any? %>
51
    <fieldset id="csv-export-block-columns">
50
  <% if @query.available_block_columns.any? || @query.available_journal_notes_columns.any? %>
51
    <fieldset id="csv-export-other-columns">
52 52
      <legend>
53
        <%= toggle_checkboxes_link('#csv-export-block-columns input[type=checkbox]') %>
53
        <%= toggle_checkboxes_link('#csv-export-other-columns input[type=checkbox]') %>
54 54
      </legend>
55
      <% @query.available_block_columns.each do |column| %>
56
        <label><%= check_box_tag 'c[]', column.name, @query.has_column?(column), :id => nil %> <%= column.caption %></label>
55
      <% if @query.available_block_columns.any? %>
56
        <% @query.available_block_columns.each do |column| %>
57
          <label><%= check_box_tag 'c[]', column.name, @query.has_column?(column), :id => nil %> <%= column.caption %></label>
58
        <% end %>
59
      <% end %>
60
      <% if @query.available_journal_notes_columns.any? %>
61
        <label><%= check_box_tag 'journal_notes', 1, params[:journal_notes], :id => nil %> <%= l(:label_all_notes) %></label>
57 62
      <% end %>
58 63
    </fieldset>
59 64
  <% end %>
config/locales/en.yml
1122 1122
  label_my_bookmarks: My bookmarks
1123 1123
  label_assign_to_me: Assign to me
1124 1124
  label_default_query: Default query
1125
  label_all_notes: All notes
1126
  label_notes_id: Notes-#
1127
  label_notes_author: Notes author
1128
  label_notes_created_on: Notes created
1125 1129

  
1126 1130
  button_login: Login
1127 1131
  button_submit: Submit
test/functional/issues_controller_test.rb
960 960
    assert lines.detect {|line| line.include?('"MySQL, Oracle"')}
961 961
  end
962 962

  
963
  def test_index_csv_with_journal_notes_params
964
    get(
965
      :index,
966
      :params => {
967
        :format => 'csv',
968
        :c => ['subject'],
969
        :project => 'ecookbook',
970
        :journal_notes => 1
971
      }
972
    )
973
    assert_response :success
974
    lines = @response.body.chomp.split("\n")
975
    assert_equal 7, lines.first.split(',').count
976

  
977
    journal1 = Journal.find(1)
978
    assert_equal "1,Cannot print recipes,#{journal1.id},#{journal1.notes},#{journal1.user},#{format_time(journal1.created_on)},No", lines[-2]
979
    journal2 = Journal.find(2)
980
    assert_equal "1,,#{journal2.id},\"#{journal2.notes}\",#{journal2.user},#{format_time(journal2.created_on)},No", lines[-1]
981
  end
982

  
963 983
  def test_index_csv_should_format_float_custom_fields_with_csv_decimal_separator
964 984
    field =
965 985
      IssueCustomField.
test/unit/query_test.rb
1709 1709
    assert_not_nil issues.first.instance_variable_get(:@last_notes)
1710 1710
  end
1711 1711

  
1712
  def test_query_should_preload_journals_with_notes
1713
    q = IssueQuery.new(:name => '_', :column_names => [:subject, :'journals.notes'])
1714
    assert q.has_column?(:'journals.notes')
1715
    issues = q.issues
1716
    assert_not_nil issues.first.instance_variable_get(:@journals_with_notes)
1717
  end
1718

  
1712 1719
  def test_groupable_columns_should_include_custom_fields
1713 1720
    q = IssueQuery.new
1714 1721
    column = q.groupable_columns.detect {|c| c.name == :cf_1}