additional_tags 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/codeql-analysis.yml +6 -6
  3. data/.github/workflows/linters.yml +16 -4
  4. data/.github/workflows/tests.yml +7 -4
  5. data/.gitignore +3 -0
  6. data/.rubocop.yml +9 -6
  7. data/.stylelintrc.json +12 -155
  8. data/README.md +28 -9
  9. data/app/controllers/additional_tags_controller.rb +10 -10
  10. data/app/controllers/issue_tags_controller.rb +3 -1
  11. data/app/helpers/additional_tags_helper.rb +39 -30
  12. data/app/helpers/additional_tags_issues_helper.rb +1 -3
  13. data/app/helpers/additional_tags_wiki_helper.rb +16 -10
  14. data/app/jobs/additional_tags_remove_unused_tag_job.rb +11 -6
  15. data/app/models/additional_tag.rb +98 -0
  16. data/app/views/additional_tags/_tag_list.html.slim +12 -6
  17. data/app/views/additional_tags/merge.html.slim +0 -1
  18. data/app/views/additional_tags/settings/_general.html.slim +7 -1
  19. data/app/views/additional_tags/settings/_manage_tags.html.slim +8 -1
  20. data/app/views/context_menus/_issues_tags.html.slim +7 -1
  21. data/app/views/dashboards/blocks/_issue_tags.html.slim +9 -7
  22. data/app/views/issue_tags/_edit_modal.html.slim +7 -7
  23. data/app/views/issue_tags/edit.js.erb +1 -1
  24. data/app/views/issues/_tags.html.slim +12 -7
  25. data/app/views/issues/_tags_bulk_edit.html.slim +2 -1
  26. data/app/views/issues/_tags_form_details.html.slim +1 -2
  27. data/app/views/wiki/_tags_form.html.slim +4 -0
  28. data/app/views/wiki/_tags_form_bottom.html.slim +1 -2
  29. data/app/views/wiki/_tags_show.html.slim +7 -7
  30. data/app/views/wiki/tag_index.html.slim +2 -2
  31. data/assets/javascripts/tags.js +3 -3
  32. data/assets/stylesheets/tags.css +41 -16
  33. data/config/locales/bg.yml +18 -11
  34. data/config/locales/cs.yml +19 -12
  35. data/config/locales/de.yml +19 -12
  36. data/config/locales/en.yml +19 -12
  37. data/config/locales/es.yml +19 -12
  38. data/config/locales/fr.yml +19 -12
  39. data/config/locales/it.yml +19 -12
  40. data/config/locales/ja.yml +18 -11
  41. data/config/locales/ko.yml +18 -11
  42. data/config/locales/pl.yml +19 -12
  43. data/config/locales/pt-BR.yml +18 -11
  44. data/config/locales/ru.yml +34 -27
  45. data/config/settings.yml +5 -5
  46. data/doc/images/additional-tags-framework.png +0 -0
  47. data/doc/images/additional-tags.gif +0 -0
  48. data/doc/images/tag-overview.png +0 -0
  49. data/lib/additional_tags/hooks/view_hook.rb +15 -2
  50. data/lib/additional_tags/patches/issue_patch.rb +53 -7
  51. data/lib/additional_tags/patches/issue_query_patch.rb +18 -0
  52. data/lib/additional_tags/patches/queries_helper_patch.rb +1 -3
  53. data/lib/additional_tags/patches/query_patch.rb +23 -2
  54. data/lib/additional_tags/patches/wiki_page_patch.rb +8 -2
  55. data/lib/additional_tags/plugin_version.rb +1 -1
  56. data/lib/additional_tags/tags.rb +35 -14
  57. data/lib/additional_tags.rb +5 -0
  58. data/package.json +8 -0
  59. metadata +6 -3
@@ -13,6 +13,8 @@ module AdditionalTags
13
13
  before_save :prepare_save_tag_change
14
14
  before_save :sort_tag_list
15
15
 
16
+ validate :validate_tags, if: proc { AdditionalTags.setting?(:active_issue_tags) }
17
+
16
18
  after_commit :add_remove_unused_tags_job, on: %i[update destroy],
17
19
  if: proc { AdditionalTags.setting?(:active_issue_tags) }
18
20
 
@@ -29,21 +31,58 @@ module AdditionalTags
29
31
  tags.all? { |tag| allowed_tags.include? tag }
30
32
  end
31
33
 
32
- def group_by_status_with_tags(project = nil)
33
- visible(User.current, project: project).joins(:status)
34
- .joins(:tags)
35
- .group(:is_closed, 'tag_id')
36
- .count
34
+ def group_by_status_with_tags(project = nil, user = User.current)
35
+ scope = if project && Setting.display_subprojects_issues?
36
+ visible(user).where(AdditionalTags::Tags.subproject_sql(project))
37
+ else
38
+ visible user, project: project
39
+ end
40
+
41
+ scope.joins(:status)
42
+ .joins(:tags)
43
+ .group(:is_closed, 'tag_id')
44
+ .count
37
45
  end
38
46
 
39
47
  def available_tags(**options)
40
- options[:permission] = :view_issues
48
+ options[:permission] ||= :view_issue_tags
41
49
  tags = AdditionalTags::Tags.available_tags self, **options
42
50
  return tags unless options[:open_issues_only]
43
51
 
44
52
  tags.joins("JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{table_name}.status_id")
45
53
  .where(issue_statuses: { is_closed: false })
46
54
  end
55
+
56
+ def common_tag_list_from_issues(ids)
57
+ common_tags = ActsAsTaggableOn::Tag.joins(:taggings)
58
+ .select(
59
+ "#{ActiveRecord::Base.connection.quote_table_name ActsAsTaggableOn.tags_table}.id",
60
+ "#{ActiveRecord::Base.connection.quote_table_name ActsAsTaggableOn.tags_table}.name"
61
+ )
62
+ .where(taggings: { taggable_type: 'Issue', taggable_id: ids })
63
+ .group(
64
+ "#{ActiveRecord::Base.connection.quote_table_name ActsAsTaggableOn.tags_table}.id",
65
+ "#{ActiveRecord::Base.connection.quote_table_name ActsAsTaggableOn.tags_table}.name"
66
+ )
67
+ .having('count(*) = ?', ids.count).to_a
68
+
69
+ ActsAsTaggableOn::TagList.new common_tags
70
+ end
71
+
72
+ def load_visible_tags(issues, user = User.current)
73
+ return if issues.blank?
74
+
75
+ available_projects = Project.where(AdditionalTags::Tags.visible_condition(user)).ids
76
+
77
+ issues.each do |issue|
78
+ tags = if available_projects.include? issue.project_id
79
+ issue.tags
80
+ else
81
+ []
82
+ end
83
+ issue.instance_variable_set :@visible_tags, tags
84
+ end
85
+ end
47
86
  end
48
87
 
49
88
  module InstanceMethods
@@ -62,7 +101,7 @@ module AdditionalTags
62
101
  tags = attrs.delete :tag_list
63
102
  tags = Array(tags).reject(&:empty?)
64
103
 
65
- Additionals.debug "tags: #{tags.inspect} - project: #{project&.id} - access: #{user.allowed_to? :create_issue_tags, project}"
104
+ # Additionals.debug "tags: #{tags.inspect} - project: #{project&.id} - access: #{user.allowed_to? :create_issue_tags, project}"
66
105
 
67
106
  if user.allowed_to?(:create_issue_tags, project) ||
68
107
  user.allowed_to?(:edit_issue_tags, project) && Issue.allowed_tags?(tags)
@@ -88,6 +127,13 @@ module AdditionalTags
88
127
 
89
128
  self.tag_list = AdditionalTags::Tags.sort_tags tags
90
129
  end
130
+
131
+ def validate_tags
132
+ return if !User.current.allowed_to?(:create_issue_tags, project) &&
133
+ !(User.current.allowed_to?(:edit_issue_tags, project) && Issue.allowed_tags?(tags))
134
+
135
+ errors.add :tag_list, :invalid_mutually_exclusive_tags unless AdditionalTag.valid_mutually_exclusive_tag tag_list
136
+ end
91
137
  end
92
138
  end
93
139
  end
@@ -15,6 +15,9 @@ module AdditionalTags
15
15
 
16
16
  alias_method :available_columns_without_tags, :available_columns
17
17
  alias_method :available_columns, :available_columns_with_tags
18
+
19
+ alias_method :issues_without_tags, :issues
20
+ alias_method :issues, :issues_with_tags
18
21
  end
19
22
 
20
23
  module InstanceOverwriteMethods
@@ -29,9 +32,24 @@ module AdditionalTags
29
32
 
30
33
  self
31
34
  end
35
+
36
+ def sql_for_tags_field(field, _operator, values)
37
+ build_sql_for_tags_field_with_permission klass: queried_class,
38
+ operator: operator_for(field),
39
+ values: values,
40
+ permission: :view_issue_tags
41
+ end
32
42
  end
33
43
 
34
44
  module InstanceMethods
45
+ def issues_with_tags(**options)
46
+ issues = issues_without_tags(**options)
47
+ return issues unless has_column? :tags
48
+
49
+ Issue.load_visible_tags issues
50
+ issues
51
+ end
52
+
35
53
  def initialize_available_filters_with_tags
36
54
  initialize_available_filters_without_tags
37
55
 
@@ -15,9 +15,7 @@ module AdditionalTags
15
15
  module InstanceMethods
16
16
  def column_content_with_tags(column, item)
17
17
  if column.name == :issue_tags || item.is_a?(Issue) && column.name == :tags
18
- additional_tag_links column.value(item),
19
- tag_controller: 'issues',
20
- use_colors: AdditionalTags.setting?(:use_colors)
18
+ additional_tag_links item.instance_variable_get(:@visible_tags), tag_controller: 'issues'
21
19
  else
22
20
  column_content_without_tags column, item
23
21
  end
@@ -11,7 +11,9 @@ module AdditionalTags
11
11
 
12
12
  module InstanceMethods
13
13
  def sql_for_tags_field(field, _operator, values)
14
- build_sql_for_tags_field klass: queried_class, operator: operator_for(field), values: values
14
+ build_sql_for_tags_field klass: queried_class,
15
+ operator: operator_for(field),
16
+ values: values
15
17
  end
16
18
 
17
19
  def initialize_tags_filter(position: nil)
@@ -44,7 +46,7 @@ module AdditionalTags
44
46
  subsql = ActsAsTaggableOn::Tagging.joins("INNER JOIN #{quoted_joined_table}" \
45
47
  " ON additional_taggings.taggable_id = #{quoted_joined_table}.#{quoted_target_field}")
46
48
  .where(taggable_type: klass.name)
47
- .where("#{self.class.connection.quote_table_name queried_table_name}.#{quoted_source_field} ="\
49
+ .where("#{self.class.connection.quote_table_name queried_table_name}.#{quoted_source_field} =" \
48
50
  " #{quoted_joined_table}.#{quoted_joined_field}")
49
51
  .select(1)
50
52
 
@@ -60,6 +62,25 @@ module AdditionalTags
60
62
  end
61
63
  end
62
64
 
65
+ # NOTE: should be used, if tags required permission check
66
+ def build_sql_for_tags_field_with_permission(klass:, operator:, values:, permission:)
67
+ compare = ['=', '*'].include?(operator) ? 'in' : 'not_in'
68
+ case operator
69
+ when '=', '!'
70
+ ids_list = klass.tagged_with(values, any: true).ids
71
+ # special case: filter with deleted tag
72
+ return '(1=0)' if ids_list.blank? && values.present? && operator == '='
73
+ else
74
+ allowed_projects = Project.where(Project.allowed_to_condition(User.current, permission))
75
+ .select(:id)
76
+ ids_list = klass.tagged_with(klass.available_tags(skip_pre_condition: true), any: true)
77
+ .where(project_id: allowed_projects).ids
78
+ end
79
+
80
+ "(#{klass.arel_table[:id].send(compare, ids_list).to_sql})"
81
+ end
82
+
83
+ # NOTE: should be used, if tags do not require permission check
63
84
  def build_sql_for_tags_field(klass:, operator:, values:)
64
85
  compare = ['=', '*'].include?(operator) ? 'IN' : 'NOT IN'
65
86
  case operator
@@ -13,6 +13,7 @@ module AdditionalTags
13
13
  alias_method :safe_attributes_without_tags=, :safe_attributes=
14
14
  alias_method :safe_attributes=, :safe_attributes_with_tags=
15
15
 
16
+ validate :validate_tags
16
17
  before_save :sort_tag_list
17
18
  end
18
19
 
@@ -24,7 +25,7 @@ module AdditionalTags
24
25
 
25
26
  def available_tags(**options)
26
27
  options[:project_join] = project_joins
27
- options[:permission] = :view_wiki_pages
28
+ options[:permission] ||= :view_wiki_pages
28
29
  AdditionalTags::Tags.available_tags self, **options
29
30
  end
30
31
 
@@ -41,10 +42,11 @@ module AdditionalTags
41
42
  scope
42
43
  end
43
44
 
44
- def with_tags(tag, project: nil, order: 'title_asc', max_entries: nil)
45
+ def with_tags(tag, project: nil, order: 'title_asc', max_entries: nil, exclude_page: nil)
45
46
  wiki = project&.wiki
46
47
 
47
48
  scope = with_tags_scope wiki: wiki, project: project
49
+ scope = scope.where.not id: exclude_page.id if exclude_page
48
50
  scope = scope.limit max_entries if max_entries
49
51
 
50
52
  tags = Array tag
@@ -105,6 +107,10 @@ module AdditionalTags
105
107
 
106
108
  self.tag_list = AdditionalTags::Tags.sort_tags tag_list
107
109
  end
110
+
111
+ def validate_tags
112
+ errors.add :tag_list, :invalid_mutually_exclusive_tags unless AdditionalTag.valid_mutually_exclusive_tag tag_list
113
+ end
108
114
  end
109
115
  end
110
116
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module AdditionalTags
4
4
  module PluginVersion
5
- VERSION = '1.0.5' unless defined? VERSION
5
+ VERSION = '1.0.7' unless defined? VERSION
6
6
  end
7
7
  end
@@ -3,13 +3,27 @@
3
3
  module AdditionalTags
4
4
  class Tags
5
5
  class << self
6
+ def visible_condition(user, **options)
7
+ permission = options[:permission] || :view_issue_tags
8
+ skip_pre_condition = options[:skip_pre_condition] || true
9
+
10
+ tag_access permission, user, skip_pre_condition: skip_pre_condition
11
+ end
12
+
6
13
  def available_tags(klass, **options)
7
14
  user = options[:user].presence || User.current
8
15
 
9
16
  scope = ActsAsTaggableOn::Tag.where({})
10
- scope = scope.where "#{Project.table_name}.id = ?", options[:project] if options[:project]
17
+ if options[:project]
18
+ scope = if Setting.display_subprojects_issues?
19
+ scope.where subproject_sql(options[:project])
20
+ else
21
+ scope.where projects: { id: options[:project] }
22
+ end
23
+ end
24
+
11
25
  if options[:permission]
12
- scope = scope.where tag_access(options[:permission], user)
26
+ scope = scope.where tag_access(options[:permission], user, skip_pre_condition: options[:skip_pre_condition])
13
27
  elsif options[:visible_condition]
14
28
  scope = scope.where klass.visible_condition(user)
15
29
  end
@@ -18,14 +32,7 @@ module AdditionalTags
18
32
  scope = scope.where "#{TAGGING_TABLE_NAME}.taggable_id!=?", options[:exclude_id] if options[:exclude_id]
19
33
  scope = scope.where options[:where_field] => options[:where_value] if options[:where_field].present? && options[:where_value]
20
34
 
21
- columns = ["#{TAG_TABLE_NAME}.id",
22
- "#{TAG_TABLE_NAME}.name",
23
- "#{TAG_TABLE_NAME}.taggings_count",
24
- "COUNT(DISTINCT #{TAGGING_TABLE_NAME}.taggable_id) AS count"]
25
-
26
- columns << "MIN(#{TAGGING_TABLE_NAME}.created_at) AS last_created" if options[:sort_by] == 'last_created'
27
-
28
- scope.select(columns.to_list)
35
+ scope.select(table_columns(options[:sort_by]))
29
36
  .joins(tag_for_joins(klass, **options.slice(:project_join, :project, :without_projects)))
30
37
  .group("#{TAG_TABLE_NAME}.id, #{TAG_TABLE_NAME}.name, #{TAG_TABLE_NAME}.taggings_count").having('COUNT(*) > 0')
31
38
  .order(build_order_sql(options[:sort_by], options[:order]))
@@ -49,8 +56,7 @@ module AdditionalTags
49
56
  end
50
57
 
51
58
  def remove_unused_tags
52
- ActsAsTaggableOn::Tag.left_outer_joins(:taggings)
53
- .where(taggings: { id: nil })
59
+ ActsAsTaggableOn::Tag.where.missing(:taggings)
54
60
  .each(&:destroy)
55
61
  end
56
62
 
@@ -126,8 +132,23 @@ module AdditionalTags
126
132
  counts
127
133
  end
128
134
 
135
+ def subproject_sql(project)
136
+ "#{Project.table_name}.lft >= #{project.lft} " \
137
+ "AND #{Project.table_name}.rgt <= #{project.rgt}"
138
+ end
139
+
129
140
  private
130
141
 
142
+ def table_columns(sort_by)
143
+ columns = ["#{TAG_TABLE_NAME}.id",
144
+ "#{TAG_TABLE_NAME}.name",
145
+ "#{TAG_TABLE_NAME}.taggings_count",
146
+ "COUNT(DISTINCT #{TAGGING_TABLE_NAME}.taggable_id) AS count"]
147
+
148
+ columns << "MIN(#{TAGGING_TABLE_NAME}.created_at) AS last_created" if sort_by == 'last_created'
149
+ columns.to_list
150
+ end
151
+
131
152
  def status_for_tag_value(scope:, tag_id:, group_id: nil, group_id_is_bool: false)
132
153
  value = if group_id_is_bool || group_id
133
154
  if group_id_is_bool
@@ -177,11 +198,11 @@ module AdditionalTags
177
198
  joins
178
199
  end
179
200
 
180
- def tag_access(permission, user)
201
+ def tag_access(permission, user, skip_pre_condition: false)
181
202
  projects_allowed = if permission.nil?
182
203
  Project.visible.ids
183
204
  else
184
- Project.where(Project.allowed_to_condition(user, permission)).ids
205
+ Project.where(Project.allowed_to_condition(user, permission, skip_pre_condition: skip_pre_condition)).ids
185
206
  end
186
207
 
187
208
  if projects_allowed.present?
@@ -14,6 +14,11 @@ module AdditionalTags
14
14
  setting(:tags_sidebar).present? && setting(:tags_sidebar) != 'none'
15
15
  end
16
16
 
17
+ # color is used by default (if setting is missing, too)
18
+ def use_colors?
19
+ setting(:tags_color_theme).to_s != '0'
20
+ end
21
+
17
22
  private
18
23
 
19
24
  def setup
data/package.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "dependencies": {},
3
+ "devDependencies": {
4
+ "eslint": "^8.0.0",
5
+ "stylelint": "^14.0.0",
6
+ "stylelint-config-standard": "^29.0.0"
7
+ }
8
+ }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: additional_tags
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.5
4
+ version: 1.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - AlphaNodes
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-06-25 00:00:00.000000000 Z
11
+ date: 2022-12-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: acts-as-taggable-on
@@ -92,6 +92,7 @@ files:
92
92
  - app/helpers/additional_tags_wiki_helper.rb
93
93
  - app/jobs/additional_tags_job.rb
94
94
  - app/jobs/additional_tags_remove_unused_tag_job.rb
95
+ - app/models/additional_tag.rb
95
96
  - app/models/migrate_tag.rb
96
97
  - app/models/migrate_tagging.rb
97
98
  - app/models/query_tags_column.rb
@@ -143,6 +144,7 @@ files:
143
144
  - db/migrate/20201123093214_migrate_existing_tags.rb
144
145
  - doc/images/additional-tags-framework.png
145
146
  - doc/images/additional-tags.gif
147
+ - doc/images/tag-overview.png
146
148
  - init.rb
147
149
  - lib/additional_tags.rb
148
150
  - lib/additional_tags/hooks/model_hook.rb
@@ -175,6 +177,7 @@ files:
175
177
  - lib/additional_tags/plugin_version.rb
176
178
  - lib/additional_tags/tags.rb
177
179
  - lib/tasks/additional_tags.rake
180
+ - package.json
178
181
  homepage: https://github.com/alphanodes/additional_tags
179
182
  licenses:
180
183
  - GPL-2.0
@@ -195,7 +198,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
195
198
  - !ruby/object:Gem::Version
196
199
  version: '0'
197
200
  requirements: []
198
- rubygems_version: 3.3.7
201
+ rubygems_version: 3.3.26
199
202
  signing_key:
200
203
  specification_version: 4
201
204
  summary: Redmine plugin for adding tag functionality