Feature #1448 » 0001-Add-tags-to-issues.patch
Gemfile | ||
---|---|---|
18 | 18 |
gem "nokogiri", "~> 1.8.0" |
19 | 19 |
gem "i18n", "~> 0.7.0" |
20 | 20 | |
21 |
# Tags |
|
22 |
gem 'acts-as-taggable-on', '~> 6.0' |
|
23 | ||
21 | 24 |
# Request at least rails-html-sanitizer 1.0.3 because of security advisories |
22 | 25 |
gem "rails-html-sanitizer", ">= 1.0.3" |
23 | 26 |
app/models/issue.rb | ||
---|---|---|
51 | 51 | |
52 | 52 |
acts_as_activity_provider :scope => preload(:project, :author, :tracker, :status), |
53 | 53 |
:author_key => :author_id |
54 | ||
54 |
acts_as_taggable |
|
55 | 55 |
DONE_RATIO_OPTIONS = %w(issue_field issue_status) |
56 | 56 | |
57 | 57 |
attr_accessor :deleted_attachment_ids |
... | ... | |
244 | 244 |
@total_estimated_hours = nil |
245 | 245 |
@last_updated_by = nil |
246 | 246 |
@last_notes = nil |
247 |
@tags_list = nil |
|
247 | 248 |
base_reload(*args) |
248 | 249 |
end |
249 | 250 | |
... | ... | |
459 | 460 |
'estimated_hours', |
460 | 461 |
'custom_field_values', |
461 | 462 |
'custom_fields', |
463 |
'tag_list', |
|
462 | 464 |
'lock_version', |
463 | 465 |
'notes', |
464 | 466 |
:if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user) } |
... | ... | |
1107 | 1109 |
end |
1108 | 1110 |
end |
1109 | 1111 | |
1112 |
def tags_list |
|
1113 |
if @tags_list |
|
1114 |
@tags_list |
|
1115 |
else |
|
1116 |
tag_list |
|
1117 |
end |
|
1118 |
end |
|
1119 | ||
1110 | 1120 |
# Preloads relations for a collection of issues |
1111 | 1121 |
def self.load_relations(issues) |
1112 | 1122 |
if issues.any? |
... | ... | |
1161 | 1171 |
end |
1162 | 1172 |
end |
1163 | 1173 | |
1174 |
# Preloads tags for a collection of issues |
|
1175 |
def self.load_issues_tags(issues) |
|
1176 |
if issues.any? |
|
1177 |
tags = ActsAsTaggableOn::Tag.joins(:taggings) |
|
1178 |
.select("#{ActsAsTaggableOn::Tag.table_name}.id", "#{ActsAsTaggableOn::Tag.table_name}.name", |
|
1179 |
"#{ActsAsTaggableOn::Tagging.table_name}.taggable_id") |
|
1180 |
.where(:taggings => {:taggable_type => 'Issue', :taggable_id => issues.map(&:id), :context => 'tags'}) |
|
1181 |
.sort |
|
1182 |
issues.each do |issue| |
|
1183 |
issue.instance_variable_set "@tags_list", (tags.select{|t| t.taggable_id == issue.id} || []) |
|
1184 |
end |
|
1185 |
end |
|
1186 |
end |
|
1187 | ||
1164 | 1188 |
# Returns a scope of the given issues and their descendants |
1165 | 1189 |
def self.self_and_descendants(issues) |
1166 | 1190 |
Issue.joins("JOIN #{Issue.table_name} ancestors" + |
... | ... | |
1565 | 1589 |
Tracker.none |
1566 | 1590 |
end |
1567 | 1591 |
end |
1568 | ||
1569 | 1592 |
private |
1570 | 1593 | |
1571 | 1594 |
def user_tracker_permission?(user, permission) |
app/models/issue_query.rb | ||
---|---|---|
44 | 44 |
QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), |
45 | 45 |
QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'), |
46 | 46 |
QueryColumn.new(:last_updated_by, :sortable => lambda {User.fields_for_order_statement("last_journal_user")}), |
47 |
QueryColumn.new(:tags_list, :caption => :field_tags), |
|
47 | 48 |
QueryColumn.new(:relations, :caption => :label_related_issues), |
48 | 49 |
QueryColumn.new(:attachments, :caption => :label_attachment_plural), |
49 | 50 |
QueryColumn.new(:description, :inline => false), |
... | ... | |
143 | 144 |
:values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] |
144 | 145 |
end |
145 | 146 | |
147 |
add_available_filter('tags_list', :type => :list_optional, :name => l(:field_tags), |
|
148 |
:values => lambda { tags_values }) |
|
149 | ||
146 | 150 |
add_available_filter "attachment", |
147 | 151 |
:type => :text, :name => l(:label_attachment) |
148 | 152 | |
... | ... | |
307 | 311 |
if has_column?(:last_notes) |
308 | 312 |
Issue.load_visible_last_notes(issues) |
309 | 313 |
end |
314 |
if has_column?(:tags_list) |
|
315 |
Issue.load_issues_tags(issues) |
|
316 |
end |
|
310 | 317 |
issues |
311 | 318 |
rescue ::ActiveRecord::StatementInvalid => e |
312 | 319 |
raise StatementInvalid.new(e.message) |
... | ... | |
577 | 584 |
"(#{sql})" |
578 | 585 |
end |
579 | 586 | |
587 |
def sql_for_tags_list_field(field, operator, value) |
|
588 |
case operator |
|
589 |
when '=', '!' |
|
590 |
issues = Issue.tagged_with(values_for('tags_list'), any: true) |
|
591 |
when '!*' |
|
592 |
issues = Issue.tagged_with ActsAsTaggableOn::Tag.all.map(&:to_s), exclude: true |
|
593 |
else |
|
594 |
issues = Issue.tagged_with ActsAsTaggableOn::Tag.all.map(&:to_s), any: true |
|
595 |
end |
|
596 |
compare = operator.eql?('!') ? 'NOT IN' : 'IN' |
|
597 |
issue_ids = issues.collect {|issue| issue.id }.push(0).join(',') |
|
598 | ||
599 |
"( #{ Issue.table_name }.id #{ compare } (#{ issue_ids }) )" |
|
600 |
end |
|
601 | ||
580 | 602 |
def find_assigned_to_id_filter_values(values) |
581 | 603 |
Principal.visible.where(:id => values).map {|p| [p.name, p.id.to_s]} |
582 | 604 |
end |
app/models/journal.rb | ||
---|---|---|
203 | 203 |
h[c.custom_field_id] = c.value |
204 | 204 |
h |
205 | 205 |
end |
206 |
@tag_list_before_change = journalized.tag_list |
|
206 | 207 |
end |
207 | 208 |
self |
208 | 209 |
end |
... | ... | |
272 | 273 |
end |
273 | 274 |
end |
274 | 275 |
end |
276 | ||
277 |
if @tag_list_before_change |
|
278 |
new_tags = journalized.send('tag_list') |
|
279 |
if new_tags != @tag_list_before_change |
|
280 |
add_attribute_detail('tags', @tag_list_before_change.to_s, new_tags.to_s) |
|
281 |
end |
|
282 |
end |
|
275 | 283 |
start |
276 | 284 |
end |
277 | 285 |
app/models/query.rb | ||
---|---|---|
579 | 579 |
end |
580 | 580 |
end |
581 | 581 | |
582 |
def tags_values |
|
583 |
issues_scope = Issue.visible.select("#{Issue.table_name}.id").joins(:project) |
|
584 |
issues_scope.where("#{project.project_condition(Setting.display_subprojects_issues?)}") if project |
|
585 | ||
586 |
result_scope = ActsAsTaggableOn::Tag |
|
587 |
.joins(:taggings) |
|
588 |
.select('tags.name') |
|
589 |
.group('tags.id, tags.name') |
|
590 |
.where(taggings: { taggable_type: 'Issue', taggable_id: issues_scope}) |
|
591 |
.collect {|t| [t.name, t.name]} |
|
592 | ||
593 |
result_scope |
|
594 |
end |
|
595 | ||
582 | 596 |
# Adds available filters |
583 | 597 |
def initialize_available_filters |
584 | 598 |
# implemented by sub-classes |
app/views/issues/_attributes.html.erb | ||
---|---|---|
78 | 78 |
<%= render :partial => 'issues/form_custom_fields' %> |
79 | 79 |
<% end %> |
80 | 80 | |
81 |
<p><%= f.text_field :tag_list, :size => 60, :value => @issue.tag_list.to_s %> |
|
82 | ||
81 | 83 |
<% end %> |
82 | 84 | |
83 | 85 |
<% include_calendar_headers_tags %> |
app/views/issues/show.html.erb | ||
---|---|---|
72 | 72 |
end |
73 | 73 |
end %> |
74 | 74 |
<%= render_half_width_custom_fields_rows(@issue) %> |
75 | ||
76 |
<% unless @issue.tag_list.empty? %> |
|
77 |
<div class="tags attribute"> |
|
78 |
<div class="label"> |
|
79 |
<span><%= l(:field_tags) %>:</span> |
|
80 |
</div> |
|
81 |
<div class="value"> |
|
82 |
<%= @issue.tag_list.to_s %> |
|
83 |
</div> |
|
84 |
</div> |
|
85 |
<% end %> |
|
86 | ||
75 | 87 |
<%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %> |
76 | 88 |
</div> |
77 | 89 |
config/locales/en.yml | ||
---|---|---|
378 | 378 |
field_full_width_layout: Full width layout |
379 | 379 |
field_digest: Checksum |
380 | 380 |
field_default_assigned_to: Default assignee |
381 |
field_tags: Tags |
|
381 | 382 | |
382 | 383 |
setting_app_title: Application title |
383 | 384 |
setting_welcome_text: Welcome text |
db/migrate/20180802191932_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb | ||
---|---|---|
1 |
# This migration comes from acts_as_taggable_on_engine (originally 1) |
|
2 |
if ActiveRecord.gem_version >= Gem::Version.new('5.0') |
|
3 |
class ActsAsTaggableOnMigration < ActiveRecord::Migration[4.2]; end |
|
4 |
else |
|
5 |
class ActsAsTaggableOnMigration < ActiveRecord::Migration; end |
|
6 |
end |
|
7 |
ActsAsTaggableOnMigration.class_eval do |
|
8 |
def self.up |
|
9 |
create_table :tags do |t| |
|
10 |
t.string :name |
|
11 |
end |
|
12 | ||
13 |
create_table :taggings do |t| |
|
14 |
t.references :tag |
|
15 | ||
16 |
# You should make sure that the column created is |
|
17 |
# long enough to store the required class names. |
|
18 |
t.references :taggable, polymorphic: true |
|
19 |
t.references :tagger, polymorphic: true |
|
20 | ||
21 |
# Limit is created to prevent MySQL error on index |
|
22 |
# length for MyISAM table type: http://bit.ly/vgW2Ql |
|
23 |
t.string :context, limit: 128 |
|
24 | ||
25 |
t.datetime :created_at |
|
26 |
end |
|
27 | ||
28 |
add_index :taggings, :tag_id |
|
29 |
add_index :taggings, [:taggable_id, :taggable_type, :context] |
|
30 |
end |
|
31 | ||
32 |
def self.down |
|
33 |
drop_table :taggings |
|
34 |
drop_table :tags |
|
35 |
end |
|
36 |
end |
db/migrate/20180802191933_add_missing_unique_indices.acts_as_taggable_on_engine.rb | ||
---|---|---|
1 |
# This migration comes from acts_as_taggable_on_engine (originally 2) |
|
2 |
if ActiveRecord.gem_version >= Gem::Version.new('5.0') |
|
3 |
class AddMissingUniqueIndices < ActiveRecord::Migration[4.2]; end |
|
4 |
else |
|
5 |
class AddMissingUniqueIndices < ActiveRecord::Migration; end |
|
6 |
end |
|
7 |
AddMissingUniqueIndices.class_eval do |
|
8 |
def self.up |
|
9 |
add_index :tags, :name, unique: true |
|
10 | ||
11 |
remove_index :taggings, :tag_id if index_exists?(:taggings, :tag_id) |
|
12 |
remove_index :taggings, [:taggable_id, :taggable_type, :context] |
|
13 |
add_index :taggings, |
|
14 |
[:tag_id, :taggable_id, :taggable_type, :context, :tagger_id, :tagger_type], |
|
15 |
unique: true, name: 'taggings_idx' |
|
16 |
end |
|
17 | ||
18 |
def self.down |
|
19 |
remove_index :tags, :name |
|
20 | ||
21 |
remove_index :taggings, name: 'taggings_idx' |
|
22 | ||
23 |
add_index :taggings, :tag_id unless index_exists?(:taggings, :tag_id) |
|
24 |
add_index :taggings, [:taggable_id, :taggable_type, :context] |
|
25 |
end |
|
26 |
end |
db/migrate/20180802191934_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb | ||
---|---|---|
1 |
# This migration comes from acts_as_taggable_on_engine (originally 3) |
|
2 |
if ActiveRecord.gem_version >= Gem::Version.new('5.0') |
|
3 |
class AddTaggingsCounterCacheToTags < ActiveRecord::Migration[4.2]; end |
|
4 |
else |
|
5 |
class AddTaggingsCounterCacheToTags < ActiveRecord::Migration; end |
|
6 |
end |
|
7 |
AddTaggingsCounterCacheToTags.class_eval do |
|
8 |
def self.up |
|
9 |
add_column :tags, :taggings_count, :integer, default: 0 |
|
10 | ||
11 |
ActsAsTaggableOn::Tag.reset_column_information |
|
12 |
ActsAsTaggableOn::Tag.find_each do |tag| |
|
13 |
ActsAsTaggableOn::Tag.reset_counters(tag.id, :taggings) |
|
14 |
end |
|
15 |
end |
|
16 | ||
17 |
def self.down |
|
18 |
remove_column :tags, :taggings_count |
|
19 |
end |
|
20 |
end |
db/migrate/20180802191935_add_missing_taggable_index.acts_as_taggable_on_engine.rb | ||
---|---|---|
1 |
# This migration comes from acts_as_taggable_on_engine (originally 4) |
|
2 |
if ActiveRecord.gem_version >= Gem::Version.new('5.0') |
|
3 |
class AddMissingTaggableIndex < ActiveRecord::Migration[4.2]; end |
|
4 |
else |
|
5 |
class AddMissingTaggableIndex < ActiveRecord::Migration; end |
|
6 |
end |
|
7 |
AddMissingTaggableIndex.class_eval do |
|
8 |
def self.up |
|
9 |
add_index :taggings, [:taggable_id, :taggable_type, :context] |
|
10 |
end |
|
11 | ||
12 |
def self.down |
|
13 |
remove_index :taggings, [:taggable_id, :taggable_type, :context] |
|
14 |
end |
|
15 |
end |
db/migrate/20180802191936_change_collation_for_tag_names.acts_as_taggable_on_engine.rb | ||
---|---|---|
1 |
# This migration comes from acts_as_taggable_on_engine (originally 5) |
|
2 |
# This migration is added to circumvent issue #623 and have special characters |
|
3 |
# work properly |
|
4 |
if ActiveRecord.gem_version >= Gem::Version.new('5.0') |
|
5 |
class ChangeCollationForTagNames < ActiveRecord::Migration[4.2]; end |
|
6 |
else |
|
7 |
class ChangeCollationForTagNames < ActiveRecord::Migration; end |
|
8 |
end |
|
9 |
ChangeCollationForTagNames.class_eval do |
|
10 |
def up |
|
11 |
if ActsAsTaggableOn::Utils.using_mysql? |
|
12 |
execute("ALTER TABLE tags MODIFY name varchar(255) CHARACTER SET utf8 COLLATE utf8_bin;") |
|
13 |
end |
|
14 |
end |
|
15 |
end |
db/migrate/20180802191937_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb | ||
---|---|---|
1 |
# This migration comes from acts_as_taggable_on_engine (originally 6) |
|
2 |
if ActiveRecord.gem_version >= Gem::Version.new('5.0') |
|
3 |
class AddMissingIndexesOnTaggings < ActiveRecord::Migration[4.2]; end |
|
4 |
else |
|
5 |
class AddMissingIndexesOnTaggings < ActiveRecord::Migration; end |
|
6 |
end |
|
7 |
AddMissingIndexesOnTaggings.class_eval do |
|
8 |
def change |
|
9 |
add_index :taggings, :tag_id unless index_exists? :taggings, :tag_id |
|
10 |
add_index :taggings, :taggable_id unless index_exists? :taggings, :taggable_id |
|
11 |
add_index :taggings, :taggable_type unless index_exists? :taggings, :taggable_type |
|
12 |
add_index :taggings, :tagger_id unless index_exists? :taggings, :tagger_id |
|
13 |
add_index :taggings, :context unless index_exists? :taggings, :context |
|
14 | ||
15 |
unless index_exists? :taggings, [:tagger_id, :tagger_type] |
|
16 |
add_index :taggings, [:tagger_id, :tagger_type] |
|
17 |
end |
|
18 | ||
19 |
unless index_exists? :taggings, [:taggable_id, :taggable_type, :tagger_id, :context], name: 'taggings_idy' |
|
20 |
add_index :taggings, [:taggable_id, :taggable_type, :tagger_id, :context], name: 'taggings_idy' |
|
21 |
end |
|
22 |
end |
|
23 |
end |
lib/redmine/export/pdf/issues_pdf_helper.rb | ||
---|---|---|
45 | 45 |
pdf.SetFontStyle('',8) |
46 | 46 |
pdf.RDMMultiCell(190, 5, "#{format_time(issue.created_on)} - #{issue.author}") |
47 | 47 |
pdf.ln |
48 |
|
|
48 | ||
49 | 49 |
left = [] |
50 | 50 |
left << [l(:field_status), issue.status] |
51 | 51 |
left << [l(:field_priority), issue.priority] |
52 | 52 |
left << [l(:field_assigned_to), issue.assigned_to] unless issue.disabled_core_fields.include?('assigned_to_id') |
53 | 53 |
left << [l(:field_category), issue.category] unless issue.disabled_core_fields.include?('category_id') |
54 | 54 |
left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?('fixed_version_id') |
55 |
|
|
55 | ||
56 | 56 |
right = [] |
57 | 57 |
right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date') |
58 | 58 |
right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?('due_date') |
59 | 59 |
right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?('done_ratio') |
60 | 60 |
right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?('estimated_hours') |
61 | 61 |
right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project) |
62 |
|
|
62 | ||
63 | 63 |
rows = left.size > right.size ? left.size : right.size |
64 | 64 |
while left.size < rows |
65 | 65 |
left << nil |
... | ... | |
73 | 73 |
custom_field_values.each_with_index do |custom_value, i| |
74 | 74 |
(i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value, false)] |
75 | 75 |
end |
76 |
|
|
76 | ||
77 | 77 |
if pdf.get_rtl |
78 | 78 |
border_first_top = 'RT' |
79 | 79 |
border_last_top = 'LT' |
... | ... | |
85 | 85 |
border_first = 'L' |
86 | 86 |
border_last = 'R' |
87 | 87 |
end |
88 |
|
|
88 | ||
89 | 89 |
rows = left.size > right.size ? left.size : right.size |
90 | 90 |
rows.times do |i| |
91 | 91 |
heights = [] |
... | ... | |
100 | 100 |
item = right[i] |
101 | 101 |
heights << pdf.get_string_height(60, item ? item.last.to_s : "") |
102 | 102 |
height = heights.max |
103 |
|
|
103 | ||
104 | 104 |
item = left[i] |
105 | 105 |
pdf.SetFontStyle('B',9) |
106 | 106 |
pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", (i == 0 ? border_first_top : border_first), '', 0, 0) |
107 | 107 |
pdf.SetFontStyle('',9) |
108 | 108 |
pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_last_top : border_last), '', 0, 0) |
109 |
|
|
109 | ||
110 | 110 |
item = right[i] |
111 | 111 |
pdf.SetFontStyle('B',9) |
112 | 112 |
pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", (i == 0 ? border_first_top : border_first), '', 0, 0) |
113 | 113 |
pdf.SetFontStyle('',9) |
114 | 114 |
pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_last_top : border_last), '', 0, 2) |
115 |
|
|
115 | ||
116 | 116 |
pdf.set_x(base_x) |
117 | 117 |
end |
118 |
|
|
118 | ||
119 |
tags = issue.tags_list |
|
120 |
if tags.any? |
|
121 |
pdf.SetFontStyle('B',9) |
|
122 |
pdf.RDMCell(35+155, 5, l(:field_tags), "LRT", 1) |
|
123 |
pdf.SetFontStyle('',9) |
|
124 | ||
125 |
pdf.SetFontStyle('',9) |
|
126 |
pdf.RDMwriteHTMLCell(35+155, 5, '', '', tags.to_s, '', "LRB") |
|
127 |
end |
|
128 | ||
119 | 129 |
pdf.SetFontStyle('B',9) |
120 | 130 |
pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1) |
121 | 131 |
pdf.SetFontStyle('',9) |
122 |
|
|
132 | ||
123 | 133 |
# Set resize image scale |
124 | 134 |
pdf.set_image_scale(1.6) |
125 | 135 |
text = textilizable(issue, :description, |
... | ... | |
157 | 167 |
pdf.ln |
158 | 168 |
end |
159 | 169 |
end |
160 |
|
|
170 | ||
161 | 171 |
relations = issue.relations.select { |r| r.other_issue(issue).visible? } |
162 | 172 |
unless relations.empty? |
163 | 173 |
truncate_length = (!is_cjk? ? 80 : 60) |
... | ... | |
185 | 195 |
end |
186 | 196 |
pdf.RDMCell(190,5, "", "T") |
187 | 197 |
pdf.ln |
188 |
|
|
198 | ||
189 | 199 |
if issue.changesets.any? && |
190 | 200 |
User.current.allowed_to?(:view_changesets, issue.project) |
191 | 201 |
pdf.SetFontStyle('B',9) |
... | ... | |
205 | 215 |
pdf.ln |
206 | 216 |
end |
207 | 217 |
end |
208 |
|
|
218 | ||
209 | 219 |
if assoc[:journals].present? |
210 | 220 |
pdf.SetFontStyle('B',9) |
211 | 221 |
pdf.RDMCell(190,5, l(:label_history), "B") |
... | ... | |
234 | 244 |
pdf.ln |
235 | 245 |
end |
236 | 246 |
end |
237 |
|
|
247 | ||
238 | 248 |
if issue.attachments.any? |
239 | 249 |
pdf.SetFontStyle('B',9) |
240 | 250 |
pdf.RDMCell(190,5, l(:label_attachment_plural), "B") |
... | ... | |
261 | 271 |
pdf.footer_date = format_date(User.current.today) |
262 | 272 |
pdf.set_auto_page_break(false) |
263 | 273 |
pdf.add_page("L") |
264 |
|
|
274 | ||
265 | 275 |
# Landscape A4 = 210 x 297 mm |
266 | 276 |
page_height = pdf.get_page_height # 210 |
267 | 277 |
page_width = pdf.get_page_width # 297 |
... | ... | |
269 | 279 |
right_margin = pdf.get_original_margins['right'] # 10 |
270 | 280 |
bottom_margin = pdf.get_footer_margin |
271 | 281 |
row_height = 4 |
272 |
|
|
282 | ||
273 | 283 |
# column widths |
274 | 284 |
table_width = page_width - right_margin - left_margin |
275 | 285 |
col_width = [] |
... | ... | |
277 | 287 |
col_width = calc_col_width(issues, query, table_width, pdf) |
278 | 288 |
table_width = col_width.inject(0, :+) |
279 | 289 |
end |
280 |
|
|
290 | ||
281 | 291 |
# use full width if the description or last_notes are displayed |
282 | 292 |
if table_width > 0 && (query.has_column?(:description) || query.has_column?(:last_notes)) |
283 | 293 |
col_width = col_width.map {|w| w * (page_width - right_margin - left_margin) / table_width} |
284 | 294 |
table_width = col_width.inject(0, :+) |
285 | 295 |
end |
286 |
|
|
296 | ||
287 | 297 |
# title |
288 | 298 |
pdf.SetFontStyle('B',11) |
289 | 299 |
pdf.RDMCell(190, 8, title) |
... | ... | |
317 | 327 |
end |
318 | 328 |
previous_group = group |
319 | 329 |
end |
320 |
|
|
330 | ||
321 | 331 |
# fetch row values |
322 | 332 |
col_values = fetch_row_values(issue, query, level) |
323 |
|
|
333 | ||
324 | 334 |
# make new page if it doesn't fit on the current one |
325 | 335 |
base_y = pdf.get_y |
326 | 336 |
max_height = get_issues_to_pdf_write_cells(pdf, col_values, col_width) |
... | ... | |
330 | 340 |
render_table_header(pdf, query, col_width, row_height, table_width) |
331 | 341 |
base_y = pdf.get_y |
332 | 342 |
end |
333 |
|
|
343 | ||
334 | 344 |
# write the cells on page |
335 | 345 |
issues_to_pdf_write_cells(pdf, col_values, col_width, max_height) |
336 | 346 |
pdf.set_y(base_y + max_height) |
337 |
|
|
347 | ||
338 | 348 |
if query.has_column?(:description) && issue.description? |
339 | 349 |
pdf.set_x(10) |
340 | 350 |
pdf.set_auto_page_break(true, bottom_margin) |
... | ... | |
349 | 359 |
pdf.set_auto_page_break(false) |
350 | 360 |
end |
351 | 361 |
end |
352 |
|
|
362 | ||
353 | 363 |
if issues.size == Setting.issues_export_limit.to_i |
354 | 364 |
pdf.SetFontStyle('B',10) |
355 | 365 |
pdf.RDMCell(0, row_height, '...') |
... | ... | |
379 | 389 |
value = " " * level + value |
380 | 390 |
when :attachments |
381 | 391 |
value = value.to_a.map {|a| a.filename}.join("\n") |
392 |
when :tags_list |
|
393 |
value = value.to_s |
|
382 | 394 |
end |
383 | 395 |
if value.is_a?(Date) |
384 | 396 |
format_date(value) |
test/fixtures/taggings.yml | ||
---|---|---|
1 |
--- |
|
2 |
tagging_1: |
|
3 |
tag_id: 1 |
|
4 |
taggable_id: 1 |
|
5 |
taggable_type: Issue |
|
6 |
context: tags |
|
7 |
created_at: <%= 2.days.ago.to_s(:db) %> |
|
8 |
tagging_2: |
|
9 |
tag_id: 2 |
|
10 |
taggable_id: 1 |
|
11 |
taggable_type: Issue |
|
12 |
context: tags |
|
13 |
created_at: <%= 2.days.ago.to_s(:db) %> |
|
14 |
tagging_3: |
|
15 |
tag_id: 1 |
|
16 |
taggable_id: 2 |
|
17 |
taggable_type: Issue |
|
18 |
context: tags |
|
19 |
created_at: <%= 2.days.ago.to_s(:db) %> |
test/fixtures/tags.yml | ||
---|---|---|
1 |
--- |
|
2 |
tag_001: |
|
3 |
id: 1 |
|
4 |
name: UX |
|
5 |
tag_002: |
|
6 |
id: 2 |
|
7 |
name: Backend |
|
8 |
tag_003: |
|
9 |
id: 3 |
|
10 |
name: API |
test/functional/issues_controller_test.rb | ||
---|---|---|
43 | 43 |
:journal_details, |
44 | 44 |
:queries, |
45 | 45 |
:repositories, |
46 |
:changesets |
|
46 |
:changesets, |
|
47 |
:tags, |
|
48 |
:taggings |
|
47 | 49 | |
48 | 50 |
include Redmine::I18n |
49 | 51 | |
... | ... | |
1558 | 1560 |
end |
1559 | 1561 |
end |
1560 | 1562 | |
1563 |
def test_index_with_tags_column |
|
1564 |
get :index, :params => { |
|
1565 |
:set_filter => 1, |
|
1566 |
:project_id => 1, |
|
1567 |
:c => %w(subject tags_list) |
|
1568 |
} |
|
1569 | ||
1570 |
assert_response :success |
|
1571 |
assert_select 'td.tags_list', :text => 'UX' |
|
1572 |
assert_select 'td.tags_list', :text => 'UX, Backend' |
|
1573 | ||
1574 |
get :index, :params => { |
|
1575 |
:set_filter => 1, |
|
1576 |
:project_id => 1, |
|
1577 |
:c => %w(subject tags_list), |
|
1578 |
:format => 'pdf' |
|
1579 |
} |
|
1580 |
assert_response :success |
|
1581 |
assert_equal 'application/pdf', response.content_type |
|
1582 |
end |
|
1583 | ||
1561 | 1584 |
def test_show_by_anonymous |
1562 | 1585 |
get :show, :params => { |
1563 | 1586 |
:id => 1 |
... | ... | |
2312 | 2335 |
assert_select 'a', :text => 'Delete', :count => 0 |
2313 | 2336 |
end |
2314 | 2337 | |
2338 |
def test_show_should_render_issue_tags_for_issue_with_tags |
|
2339 |
@request.session[:user_id] = 1 |
|
2340 | ||
2341 |
get :show, :params => { |
|
2342 |
:id => 1 |
|
2343 |
} |
|
2344 | ||
2345 |
assert_response :success |
|
2346 |
assert_select 'div.tags .value', :text => 'UX, Backend', :count => 1 |
|
2347 |
end |
|
2348 | ||
2349 |
def test_show_should_not_render_issue_tags_for_issue_without_tags |
|
2350 |
@request.session[:user_id] = 1 |
|
2351 | ||
2352 |
get :show, :params => { |
|
2353 |
:id => 14 |
|
2354 |
} |
|
2355 | ||
2356 |
assert_response :success |
|
2357 |
assert_select 'div.tags', 0 |
|
2358 |
end |
|
2359 | ||
2315 | 2360 |
def test_get_new |
2316 | 2361 |
@request.session[:user_id] = 2 |
2317 | 2362 |
get :new, :params => { |
... | ... | |
2338 | 2383 |
assert_select 'select[name=?]', 'issue[done_ratio]' |
2339 | 2384 |
assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Default string' |
2340 | 2385 |
assert_select 'input[name=?]', 'issue[watcher_user_ids][]' |
2386 |
assert_select 'input[name=?]', 'issue[tag_list]' |
|
2341 | 2387 |
end |
2342 | 2388 | |
2343 | 2389 |
# Be sure we don't display inactive IssuePriorities |
... | ... | |
3291 | 3337 |
assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail) |
3292 | 3338 |
end |
3293 | 3339 | |
3340 |
def test_post_create_with_tags |
|
3341 |
@request.session[:user_id] = 2 |
|
3342 | ||
3343 |
post :create, :params => { |
|
3344 |
:project_id => 1, |
|
3345 |
:issue => { |
|
3346 |
:tracker_id => 1, |
|
3347 |
:subject => 'This is a new issue with tags', |
|
3348 |
:description => 'This is the description', |
|
3349 |
:priority_id => 5, |
|
3350 |
:tag_list => 'Two, Three' |
|
3351 |
} |
|
3352 |
} |
|
3353 | ||
3354 |
issue = Issue.order('id DESC').first |
|
3355 |
assert_equal ['Two', 'Three'], issue.tag_list |
|
3356 |
end |
|
3357 | ||
3294 | 3358 |
def test_post_create_subissue |
3295 | 3359 |
@request.session[:user_id] = 2 |
3296 | 3360 | |
... | ... | |
4644 | 4708 |
:project_id => '1', |
4645 | 4709 |
:tracker_id => '2', |
4646 | 4710 |
:priority_id => '6' |
4647 | ||
4648 | 4711 |
} |
4649 | 4712 |
} |
4650 | 4713 |
end |
... | ... | |
5261 | 5324 |
assert_equal 'Original subject', issue.reload.subject |
5262 | 5325 |
end |
5263 | 5326 | |
5327 |
def test_put_update_issue_tags |
|
5328 |
@request.session[:user_id] = 1 |
|
5329 | ||
5330 |
put :update, :params => { |
|
5331 |
:id => 1, |
|
5332 |
:issue => { |
|
5333 |
:tag_list => 'Three' |
|
5334 |
} |
|
5335 |
} |
|
5336 |
assert_response 302 |
|
5337 | ||
5338 |
assert_equal ['Three'], Issue.find(1).tag_list |
|
5339 |
end |
|
5340 | ||
5264 | 5341 |
def test_get_bulk_edit |
5265 | 5342 |
@request.session[:user_id] = 2 |
5266 | 5343 |
get :bulk_edit, :params => { |
test/functional/queries_controller_test.rb | ||
---|---|---|
23 | 23 |
:members, :member_roles, :roles, |
24 | 24 |
:trackers, :issue_statuses, :issue_categories, :enumerations, :versions, |
25 | 25 |
:issues, :custom_fields, :custom_values, |
26 |
:queries |
|
26 |
:queries, :tags, :taggings
|
|
27 | 27 | |
28 | 28 |
def setup |
29 | 29 |
User.current = nil |
... | ... | |
732 | 732 |
assert_include ["Dave Lopper", "3", "active"], json |
733 | 733 |
assert_include ["Dave2 Lopper2", "5", "locked"], json |
734 | 734 |
end |
735 | ||
736 |
def test_filter_with_tags_should_return_filter_values |
|
737 |
@request.session[:user_id] = 2 |
|
738 |
get :filter, :params => { |
|
739 |
:project_id => 1, |
|
740 |
:type => 'IssueQuery', |
|
741 |
:name => 'tags_list' |
|
742 |
} |
|
743 | ||
744 |
assert_response :success |
|
745 |
assert_equal 'application/json', response.content_type |
|
746 |
json = ActiveSupport::JSON.decode(response.body) |
|
747 |
assert_equal [["UX","UX"],["Backend","Backend"]], json |
|
748 |
end |
|
735 | 749 |
end |
test/unit/issue_tags_test.rb | ||
---|---|---|
1 |
# Redmine - project management software |
|
2 |
# Copyright (C) 2006-2017 Jean-Philippe Lang |
|
3 |
# |
|
4 |
# This program is free software; you can redistribute it and/or |
|
5 |
# modify it under the terms of the GNU General Public License |
|
6 |
# as published by the Free Software Foundation; either version 2 |
|
7 |
# of the License, or (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU General Public License |
|
15 |
# along with this program; if not, write to the Free Software |
|
16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | ||
18 |
require File.expand_path('../../test_helper', __FILE__) |
|
19 | ||
20 |
class IssueTagsTest < ActiveSupport::TestCase |
|
21 |
fixtures :projects, :users, :email_addresses, :user_preferences, :members, :member_roles, :roles, |
|
22 |
:groups_users, |
|
23 |
:trackers, :projects_trackers, |
|
24 |
:enabled_modules, |
|
25 |
:issue_statuses, |
|
26 |
:issues, :journals, :journal_details, |
|
27 |
:tags, :taggings |
|
28 | ||
29 |
include Redmine::I18n |
|
30 | ||
31 |
def setup |
|
32 |
set_language_if_valid 'en' |
|
33 |
end |
|
34 | ||
35 |
def teardown |
|
36 |
User.current = nil |
|
37 |
end |
|
38 | ||
39 |
def test_issue_tag_list_should_return_an_array_of_tags |
|
40 |
assert_equal ['UX', 'Backend'], Issue.find(1).tag_list |
|
41 |
end |
|
42 | ||
43 |
def test_issue_tag_list_to_s_should_return_a_string_of_tags_delimited_by_comma |
|
44 |
assert_equal 'UX, Backend', Issue.find(1).tag_list.to_s |
|
45 |
end |
|
46 | ||
47 |
def test_add_issue_tags |
|
48 |
issue = Issue.find(2) |
|
49 |
issue.tag_list = "One, Two" |
|
50 | ||
51 |
assert issue.save! |
|
52 | ||
53 |
issue.reload |
|
54 | ||
55 |
assert_equal ['One', 'Two'], issue.tag_list |
|
56 |
end |
|
57 | ||
58 |
def test_clear_issue_tags |
|
59 |
issue = Issue.find(1) |
|
60 |
issue.tag_list = '' |
|
61 | ||
62 |
assert issue.save! |
|
63 |
assert_equal [], issue.tag_list |
|
64 |
end |
|
65 | ||
66 |
def test_update_issue_tags_should_journalize_changes |
|
67 |
issue = Issue.find(1) |
|
68 |
issue.init_journal User.find(1) |
|
69 |
issue.tag_list = "UX, API" |
|
70 | ||
71 |
assert_difference 'Journal.count', 1 do |
|
72 |
assert_difference 'JournalDetail.count', 1 do |
|
73 |
issue.save! |
|
74 |
end |
|
75 |
end |
|
76 |
issue.reload |
|
77 | ||
78 |
assert_equal ['UX', 'API'], issue.tag_list |
|
79 | ||
80 |
detail = JournalDetail.order('id DESC').first |
|
81 |
assert_equal issue, detail.journal.journalized |
|
82 |
assert_equal 'attr', detail.property |
|
83 |
assert_equal 'tags', detail.prop_key |
|
84 |
assert_equal 'UX, Backend', detail.old_value |
|
85 |
assert_equal 'UX, API', detail.value |
|
86 |
end |
|
87 | ||
88 |
def test_update_issue_tags_should_not_journalize_changes_if_tags_are_not_changed |
|
89 |
issue = Issue.find(1) |
|
90 |
issue.init_journal User.find(1) |
|
91 |
issue.tag_list = "UX, Backend" |
|
92 | ||
93 |
assert_difference 'Journal.count', 0 do |
|
94 |
assert_difference 'JournalDetail.count', 0 do |
|
95 |
issue.save! |
|
96 |
end |
|
97 |
end |
|
98 |
end |
|
99 |
end |
test/unit/lib/redmine/export/pdf/issues_pdf_test.rb | ||
---|---|---|
19 | 19 | |
20 | 20 |
class IssuesPdfHelperTest < ActiveSupport::TestCase |
21 | 21 |
fixtures :users, :projects, :roles, :members, :member_roles, |
22 |
:enabled_modules, :issues, :trackers, :enumerations |
|
22 |
:enabled_modules, :issues, :trackers, :enumerations, |
|
23 |
:tags, :taggings |
|
23 | 24 | |
24 | 25 |
include Redmine::Export::PDF::IssuesPdfHelper |
25 | 26 | |
... | ... | |
32 | 33 |
results = fetch_row_values(issue, query, 0) |
33 | 34 |
assert_equal ["2", "Add ingredients categories", "4.34"], results |
34 | 35 |
end |
36 | ||
37 |
def test_fetch_row_values_should_return_issue_tags_as_string |
|
38 |
query = IssueQuery.new(:project => Project.find(1), :name => '_') |
|
39 |
query.column_names = [:subject, :tags_list] |
|
40 | ||
41 |
results = fetch_row_values(Issue.find(1), query, 0) |
|
42 | ||
43 |
assert_equal ["1", "Cannot print recipes", "UX, Backend"], results |
|
44 |
end |
|
35 | 45 |
end |
test/unit/query_test.rb | ||
---|---|---|
30 | 30 |
:projects_trackers, |
31 | 31 |
:custom_fields_trackers, |
32 | 32 |
:workflows, :journals, |
33 |
:attachments |
|
33 |
:attachments, |
|
34 |
:tags, :taggings |
|
34 | 35 | |
35 | 36 |
INTEGER_KLASS = RUBY_VERSION >= "2.4" ? Integer : Fixnum |
36 | 37 | |
... | ... | |
821 | 822 |
end |
822 | 823 |
end |
823 | 824 | |
825 |
def test_filter_by_tags_with_operator_is |
|
826 |
query = IssueQuery.new(:name => '_') |
|
827 |
filter_name = "tags_list" |
|
828 |
assert_include filter_name, query.available_filters.keys |
|
829 | ||
830 |
query.filters = {filter_name => {:operator => '=', :values => ['UX']}} |
|
831 |
assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort |
|
832 | ||
833 |
# Should return issue tagged with any of the values |
|
834 |
query.filters = {filter_name => {:operator => '=', :values => ['UX, Backend']}} |
|
835 |
assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort |
|
836 |
end |
|
837 | ||
838 |
def test_filter_by_tags_with_operator_is_not |
|
839 |
query = IssueQuery.new(:name => '_') |
|
840 |
filter_name = "tags_list" |
|
841 |
assert_include filter_name, query.available_filters.keys |
|
842 | ||
843 |
query.filters = {filter_name => {:operator => '!', :values => ['Backend']}} |
|
844 |
issues = find_issues_with_query(query).map(&:id).sort |
|
845 | ||
846 |
# Issue tagged with Backend should not be returned |
|
847 |
assert_not_include 1, issues |
|
848 |
assert_include 2, issues |
|
849 |
# Untagged issues should be returned |
|
850 |
assert_include 5, issues |
|
851 |
end |
|
852 | ||
853 |
def test_filter_by_tags_with_operator_none |
|
854 |
query = IssueQuery.new(:name => '_') |
|
855 |
filter_name = "tags_list" |
|
856 |
assert_include filter_name, query.available_filters.keys |
|
857 | ||
858 |
query.filters = {filter_name => {:operator => '!*', :values => ['']}} |
|
859 |
issues = find_issues_with_query(query).map(&:id).sort |
|
860 | ||
861 |
# Tagged issues should not be returned |
|
862 |
assert_not_include 1, issues |
|
863 |
assert_not_include 2, issues |
|
864 | ||
865 |
# Untagged issues should be returned |
|
866 |
assert_include 5, issues |
|
867 |
end |
|
868 | ||
869 |
def test_filter_by_tags_with_operator_any |
|
870 |
query = IssueQuery.new(:name => '_') |
|
871 |
filter_name = "tags_list" |
|
872 |
assert_include filter_name, query.available_filters.keys |
|
873 | ||
874 |
query.filters = {filter_name => {:operator => '*', :values => ['']}} |
|
875 |
assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort |
|
876 |
end |
|
877 | ||
824 | 878 |
def test_user_custom_field_filtered_on_me |
825 | 879 |
User.current = User.find(2) |
826 | 880 |
cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1]) |
... | ... | |
1400 | 1454 |
assert_not_nil issues.first.instance_variable_get("@last_notes") |
1401 | 1455 |
end |
1402 | 1456 | |
1457 |
def test_query_should_preload_tags |
|
1458 |
q = IssueQuery.new(:name => '_', :column_names => [:subject, :tags_list]) |
|
1459 |
assert q.has_column?(:tags_list) |
|
1460 |
issues = q.issues |
|
1461 |
assert_not_nil issues.first.instance_variable_get("@tags_list") |
|
1462 |
end |
|
1463 | ||
1403 | 1464 |
def test_groupable_columns_should_include_custom_fields |
1404 | 1465 |
q = IssueQuery.new |
1405 | 1466 |
column = q.groupable_columns.detect {|c| c.name == :cf_1} |