Project

General

Profile

Feature #1448 » 0001-Add-tags-to-issues.patch

Marius BĂLTEANU, 2018-08-26 22:33

View differences:

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}
(6-6/12)