Feature #34040 » feature-34040.patch
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} |