additional_tags 1.0.5 → 1.0.7

Sign up to get free protection for your applications and to get access to all the features.
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