0001-Adds-tags-to-issues.patch

Marius BALTEANU, 2019-09-13 00:16

Download (28.6 KB)

View differences:

Gemfile
15 15
gem "i18n", "~> 1.6.0"
16 16
gem "rbpdf", "~> 1.20.0"
17 17

  
18
# Tags
19
gem 'acts-as-taggable-on', '~> 6.0'
20

  
18 21
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
19 22
gem 'tzinfo-data', platforms: [:mingw, :x64_mingw, :mswin]
20 23

  
app/models/issue.rb
53 53

  
54 54
  acts_as_activity_provider :scope => preload(:project, :author, :tracker, :status),
55 55
                            :author_key => :author_id
56
  acts_as_ordered_taggable
56 57

  
57 58
  DONE_RATIO_OPTIONS = %w(issue_field issue_status)
58 59

  
......
248 249
    @total_estimated_hours = nil
249 250
    @last_updated_by = nil
250 251
    @last_notes = nil
252
    @tags = nil
251 253
    base_reload(*args)
252 254
  end
253 255

  
......
468 470
    'estimated_hours',
469 471
    'custom_field_values',
470 472
    'custom_fields',
473
    'tag_list',
471 474
    'lock_version',
472 475
    'notes',
473 476
    :if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user) }
......
1101 1104
    end
1102 1105
  end
1103 1106

  
1107
  def tags
1108
    if @tags
1109
      @tags
1110
    else
1111
      tag_list
1112
    end
1113
  end
1114

  
1104 1115
  # Preloads relations for a collection of issues
1105 1116
  def self.load_relations(issues)
1106 1117
    if issues.any?
......
1155 1166
    end
1156 1167
  end
1157 1168

  
1169
  # Preloads tags for a collection of issues
1170
  def self.load_issues_tags(issues)
1171
    if issues.any?
1172
      tags = ActsAsTaggableOn::Tag.joins(:taggings)
1173
      .select("#{ActsAsTaggableOn::Tag.table_name}.id", "#{ActsAsTaggableOn::Tag.table_name}.name",
1174
        "#{ActsAsTaggableOn::Tagging.table_name}.taggable_id")
1175
      .where(:taggings => {:taggable_type => 'Issue', :taggable_id => issues.map(&:id), :context => 'tags'})
1176
      .order("#{ActsAsTaggableOn::Tagging.table_name}.id")
1177

  
1178
      issues.each do |issue|
1179
        issue.instance_variable_set "@tags", (tags.select{|t| t.taggable_id == issue.id} || [])
1180
      end
1181
    end
1182
  end
1183

  
1158 1184
  # Returns a scope of the given issues and their descendants
1159 1185
  def self.self_and_descendants(issues)
1160 1186
    Issue.joins("JOIN #{Issue.table_name} ancestors" +
app/models/issue_query.rb
49 49
    TimestampQueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc', :groupable => true),
50 50
    TimestampQueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc', :groupable => true),
51 51
    QueryColumn.new(:last_updated_by, :sortable => lambda {User.fields_for_order_statement("last_journal_user")}),
52
    QueryColumn.new(:tags, :caption => :field_tags),
52 53
    QueryColumn.new(:relations, :caption => :label_related_issues),
53 54
    QueryColumn.new(:attachments, :caption => :label_attachment_plural),
54 55
    QueryColumn.new(:description, :inline => false),
......
163 164
        :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
164 165
    end
165 166

  
167
    add_available_filter('tags', :type => :list_optional, :name => l(:field_tags),
168
      :values => lambda { tags_values })
169

  
166 170
    add_available_filter "attachment",
167 171
      :type => :text, :name => l(:label_attachment)
168 172

  
......
337 341
    if has_column?(:last_notes)
338 342
      Issue.load_visible_last_notes(issues)
339 343
    end
344
    if has_column?(:tags)
345
      Issue.load_issues_tags(issues)
346
    end
340 347
    issues
341 348
  rescue ::ActiveRecord::StatementInvalid => e
342 349
    raise StatementInvalid.new(e.message)
......
641 648
    sql_for_field(field, operator, value, Project.table_name, "status")
642 649
  end
643 650

  
651
  def sql_for_tags_field(field, operator, value)
652
    case operator
653
      when '=', '!'
654
        issues = Issue.tagged_with(values_for('tags'), any: true)
655
      when '!*'
656
        issues = Issue.tagged_with ActsAsTaggableOn::Tag.all.map(&:to_s), exclude: true
657
      else
658
        issues = Issue.tagged_with ActsAsTaggableOn::Tag.all.map(&:to_s), any: true
659
    end
660

  
661
    compare = operator.eql?('!') ? 'NOT IN' : 'IN'
662
    issue_ids = issues.collect {|issue| issue.id }.push(0).join(',')
663

  
664
    "( #{ Issue.table_name }.id #{ compare } (#{ issue_ids }) )"
665
  end
666

  
644 667
  def find_assigned_to_id_filter_values(values)
645 668
    Principal.visible.where(:id => values).map {|p| [p.name, p.id.to_s]}
646 669
  end
app/models/journal.rb
192 192
        h[c.custom_field_id] = c.value
193 193
        h
194 194
      end
195

  
196
      @tags_before_change = journalized.tags
195 197
    end
196 198
    self
197 199
  end
......
261 263
        end
262 264
      end
263 265
    end
266

  
267
    if @tags_before_change
268
      new_tags = journalized.send('tags')
269
      if new_tags != @tags_before_change
270
        add_attribute_detail('tags', @tags_before_change.to_s, new_tags.to_s)
271
      end
272
    end
264 273
    start
265 274
  end
266 275

  
app/models/query.rb
614 614
    ]
615 615
  end
616 616

  
617
  def tags_values
618
    issues_scope = Issue.visible.select("#{Issue.table_name}.id").joins(:project)
619
    issues_scope.where("#{project.project_condition(Setting.display_subprojects_issues?)}") if project
620

  
621
    result_scope = ActsAsTaggableOn::Tag
622
      .joins(:taggings)
623
      .select('tags.name')
624
      .group('tags.id, tags.name')
625
      .where(taggings: { taggable_type: 'Issue', taggable_id: issues_scope})
626
      .collect {|t| [t.name, t.name]}
627

  
628
    result_scope
629
  end
630

  
617 631
  # Adds available filters
618 632
  def initialize_available_filters
619 633
    # 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.tags.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.tags.empty? %>
77
  <div class="tags attribute">
78
    <div class="label">
79
      <span><%= l(:field_tags) %>:</span>
80
    </div>
81
    <div class="value">
82
      <%= @issue.tags.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
389 389
  field_recently_used_projects: Number of recently used projects in jump box
390 390
  field_history_default_tab: Issue's history default tab
391 391
  field_unique_id: Unique ID
392
  field_tags: Tags
392 393

  
393 394
  setting_app_title: Application title
394 395
  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
116 116
            pdf.set_x(base_x)
117 117
          end
118 118

  
119
          tags = issue.tags
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.RDMwriteFormattedCell(35+155, 5, '', '', tags.to_s, '', "LRB")
127
          end
119 128
          pdf.SetFontStyle('B',9)
120 129
          pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1)
121 130
          pdf.SetFontStyle('',9)
......
379 388
                value = "  " * level + value
380 389
              when :attachments
381 390
                value = value.to_a.map {|a| a.filename}.join("\n")
391
              when :tags
392
                value = value.to_s
382 393
              end
383 394
              if value.is_a?(Date)
384 395
                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: <%= 3.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: <%= 1.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
46 46
           :queries,
47 47
           :repositories,
48 48
           :changesets,
49
           :watchers
49
           :watchers,
50
           :tags,
51
           :taggings
50 52

  
51 53
  include Redmine::I18n
52 54

  
......
1704 1706
    end
1705 1707
  end
1706 1708

  
1709
  def  test_index_with_tags_column
1710
    get :index, :params => {
1711
        :set_filter => 1,
1712
        :project_id => 1,
1713
        :c => %w(subject tags)
1714
      }
1715

  
1716
      assert_response :success
1717
      assert_select 'td.tags', :text => 'UX'
1718
      assert_select 'td.tags', :text => 'UX, Backend'
1719

  
1720
      get :index, :params => {
1721
          :set_filter => 1,
1722
          :project_id => 1,
1723
          :c => %w(subject tags),
1724
          :format => 'pdf'
1725
        }
1726
      assert_response :success
1727
      assert_equal 'application/pdf', response.content_type
1728
  end
1729

  
1707 1730
  def test_show_by_anonymous
1708 1731
    get :show, :params => {
1709 1732
        :id => 1
......
2579 2602
    end
2580 2603
  end
2581 2604

  
2605
  def test_show_should_render_issue_tags_for_issue_with_tags
2606
    @request.session[:user_id] = 1
2607

  
2608
    get :show, :params => {
2609
        :id => 1
2610
      }
2611

  
2612
    assert_select 'div.tags .value', :text => 'UX, Backend', :count => 1
2613
  end
2614

  
2615
  def test_show_should_not_render_issue_tags_for_issue_without_tags
2616
    @request.session[:user_id] = 1
2617

  
2618
    get :show, :params => {
2619
        :id => 14
2620
      }
2621

  
2622
    assert_response :success
2623
    assert_select 'div.tags', 0
2624
  end
2625

  
2582 2626
  def test_get_new
2583 2627
    @request.session[:user_id] = 2
2584 2628
    get :new, :params => {
......
2605 2649
      assert_select 'select[name=?]', 'issue[done_ratio]'
2606 2650
      assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Default string'
2607 2651
      assert_select 'input[name=?]', 'issue[watcher_user_ids][]'
2652
      assert_select 'input[name=?]', 'issue[tag_list]'
2608 2653
    end
2609 2654

  
2610 2655
    # Be sure we don't display inactive IssuePriorities
......
3612 3657
    assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
3613 3658
  end
3614 3659

  
3660
  def test_post_create_with_tags
3661
    @request.session[:user_id] = 2
3662

  
3663
    post :create, :params => {
3664
        :project_id => 1,
3665
        :issue => {
3666
          :tracker_id => 1,
3667
          :subject => 'This is a new issue with tags',
3668
          :description => 'This is the description',
3669
          :priority_id => 5,
3670
          :tag_list => 'One, Two'
3671
        }
3672
      }
3673

  
3674
    issue = Issue.order('id DESC').first
3675
    assert_equal ['One', 'Two'], issue.tag_list
3676
  end
3677

  
3615 3678
  def test_post_create_subissue
3616 3679
    @request.session[:user_id] = 2
3617 3680

  
......
4977 5040
                :project_id => '1',
4978 5041
                :tracker_id => '2',
4979 5042
                :priority_id => '6'
4980

  
4981 5043
              }
4982 5044
            }
4983 5045
        end
......
5609 5671
    assert_equal 2, issue.reload.assigned_to_id
5610 5672
  end
5611 5673

  
5674
  def test_put_update_issue_tags
5675
    @request.session[:user_id] = 1
5676

  
5677
    put :update, :params => {
5678
        :id => 1,
5679
        :issue => {
5680
          :tag_list => 'Three'
5681
        }
5682
      }
5683
    assert_response 302
5684

  
5685
    assert_equal ['Three'], Issue.find(1).tag_list
5686
  end
5687

  
5612 5688
  def test_get_bulk_edit
5613 5689
    @request.session[:user_id] = 2
5614 5690
    get :bulk_edit, :params => {
test/functional/queries_controller_test.rb
25 25
           :members, :member_roles, :roles,
26 26
           :trackers, :issue_statuses, :issue_categories, :enumerations, :versions,
27 27
           :issues, :custom_fields, :custom_values,
28
           :queries
28
           :queries, :tags, :taggings
29 29

  
30 30
  def setup
31 31
    User.current = nil
......
738 738
    assert_include ["Dave Lopper", "3", "active"], json
739 739
    assert_include ["Dave2 Lopper2", "5", "locked"], json
740 740
  end
741

  
742
  def test_filter_with_tags_should_return_filter_values
743
    @request.session[:user_id] = 2
744
    get :filter, :params => {
745
        :project_id => 1,
746
        :type => 'IssueQuery',
747
        :name => 'tags'
748
      }
749

  
750
    assert_response :success
751
    assert_equal 'application/json', response.content_type
752
    json = ActiveSupport::JSON.decode(response.body)
753
    assert_equal [["UX","UX"],["Backend","Backend"]], json
754
  end
741 755
end
test/unit/issue_tags_test.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-2017  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
require File.expand_path('../../test_helper', __FILE__)
21

  
22
class IssueTagsTest < ActiveSupport::TestCase
23
  fixtures :projects, :users, :email_addresses, :user_preferences, :members, :member_roles, :roles,
24
           :groups_users,
25
           :trackers, :projects_trackers,
26
           :enabled_modules,
27
           :issue_statuses,
28
           :issues, :journals, :journal_details,
29
           :tags, :taggings
30

  
31
  include Redmine::I18n
32

  
33
  def setup
34
    set_language_if_valid 'en'
35
  end
36

  
37
  def teardown
38
    User.current = nil
39
  end
40

  
41
  def test_issue_tag_list_should_return_an_array_of_tags
42
    assert_equal ['UX', 'Backend'], Issue.find(1).tags
43
  end
44

  
45
  def test_add_issue_tags
46
    issue = Issue.find(2)
47
    issue.tag_list = "One, Two"
48

  
49
    assert issue.save
50

  
51
    issue.reload
52
    assert_equal ['One', 'Two'], issue.tag_list
53
  end
54

  
55
  def test_clear_issue_tags
56
    issue = Issue.find(1)
57
    issue.tag_list = ''
58

  
59
    assert issue.save!
60
    assert_equal [], issue.tag_list
61
  end
62

  
63
  def test_update_issue_tags_should_journalize_changes
64
    issue = Issue.find(1)
65
    issue.init_journal User.find(1)
66
    issue.tag_list = "UX, API"
67

  
68
    assert_difference 'Journal.count', 1 do
69
      assert_difference 'JournalDetail.count', 1 do
70
        issue.save!
71
      end
72
    end
73
    issue.reload
74

  
75
    assert_equal ['UX', 'API'], issue.tag_list
76

  
77
    detail = JournalDetail.order('id DESC').first
78

  
79
    assert_equal issue, detail.journal.journalized
80
    assert_equal 'attr', detail.property
81
    assert_equal 'tags', detail.prop_key
82
    assert_equal 'UX, API', detail.value
83
    assert_equal 'UX, Backend', detail.old_value
84
  end
85

  
86
  def test_update_issue_tags_should_not_journalize_changes_if_tags_are_not_changed
87
    issue = Issue.find(1)
88
    issue.init_journal User.find(1)
89
    issue.tag_list = "UX, Backend"
90

  
91
    assert_difference 'Journal.count', 0 do
92
      assert_difference 'JournalDetail.count', 0 do
93
        issue.save!
94
      end
95
    end
96
  end
97
end
test/unit/lib/redmine/export/pdf/issues_pdf_test.rb
21 21

  
22 22
class IssuesPdfHelperTest < ActiveSupport::TestCase
23 23
  fixtures :users, :projects, :roles, :members, :member_roles,
24
           :enabled_modules, :issues, :trackers, :enumerations
24
           :enabled_modules, :issues, :trackers, :enumerations,
25
           :tags, :taggings
25 26

  
26 27
  include Redmine::Export::PDF::IssuesPdfHelper
27 28

  
......
36 37
    results = fetch_row_values(issue, query, 0)
37 38
    assert_equal ["2", "Add ingredients categories", "4.34"], results
38 39
  end
40

  
41
  def test_fetch_row_values_should_return_issue_tags_as_string
42
    query = IssueQuery.new(:project => Project.find(1), :name => '_')
43
    query.column_names = [:subject, :tags]
44

  
45
    results = fetch_row_values(Issue.find(1), query, 0)
46

  
47
    # tags is the third column
48
    tags = results[2]
49
    assert_match 'Backend', tags
50
    assert_match 'UX', tags
51
  end
39 52
end
test/unit/query_test.rb
30 30
           :projects_trackers,
31 31
           :custom_fields_trackers,
32 32
           :workflows, :journals,
33
           :attachments, :time_entries
33
           :attachments, :time_entries,
34
           :tags, :taggings
34 35

  
35 36
  INTEGER_KLASS = RUBY_VERSION >= "2.4" ? Integer : Fixnum
36 37

  
......
887 888
    end
888 889
  end
889 890

  
891
  def test_filter_by_tags_with_operator_is
892
    query = IssueQuery.new(:name => '_')
893
    filter_name = "tags"
894
    assert_include filter_name, query.available_filters.keys
895

  
896
    query.filters = {filter_name => {:operator => '=', :values => ['UX']}}
897
    assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
898

  
899
    # Should return issue tagged with any of the values
900
    query.filters = {filter_name => {:operator => '=', :values => ['UX, Backend']}}
901
    assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
902
  end
903

  
904
  def test_filter_by_tags_with_operator_is_not
905
    query = IssueQuery.new(:name => '_')
906
    filter_name = "tags"
907
    assert_include filter_name, query.available_filters.keys
908

  
909
    query.filters = {filter_name => {:operator => '!', :values => ['Backend']}}
910
    issues = find_issues_with_query(query).map(&:id).sort
911

  
912
    # Issue tagged with Backend should not be returned
913
    assert_not_include 1, issues
914
    assert_include 2, issues
915
    # Untagged issues should be returned
916
    assert_include 5, issues
917
  end
918

  
919
  def test_filter_by_tags_with_operator_none
920
    query = IssueQuery.new(:name => '_')
921
    filter_name = "tags"
922
    assert_include filter_name, query.available_filters.keys
923

  
924
    query.filters = {filter_name => {:operator => '!*', :values => ['']}}
925
    issues = find_issues_with_query(query).map(&:id).sort
926

  
927
    # Tagged issues should not be returned
928
    assert_not_include 1, issues
929
    assert_not_include 2, issues
930

  
931
    # Untagged issues should be returned
932
    assert_include 5, issues
933
  end
934

  
935
  def test_filter_by_tags_with_operator_any
936
    query = IssueQuery.new(:name => '_')
937
    filter_name = "tags"
938
    assert_include filter_name, query.available_filters.keys
939

  
940
    query.filters = {filter_name => {:operator => '*', :values => ['']}}
941
    assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
942
  end
943

  
890 944
  def test_user_custom_field_filtered_on_me
891 945
    User.current = User.find(2)
892 946
    cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
......
1466 1520
    assert_not_nil issues.first.instance_variable_get("@last_notes")
1467 1521
  end
1468 1522

  
1523
  def test_query_should_preload_tags
1524
    q = IssueQuery.new(:name => '_', :column_names => [:subject, :tags])
1525
    assert q.has_column?(:tags)
1526
    issues = q.issues
1527
    assert_not_nil issues.first.instance_variable_get("@tags")
1528
  end
1529

  
1469 1530
  def test_groupable_columns_should_include_custom_fields
1470 1531
    q = IssueQuery.new
1471 1532
    column = q.groupable_columns.detect {|c| c.name == :cf_1}
1472
-