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.
- checksums.yaml +4 -4
- data/.github/workflows/codeql-analysis.yml +6 -6
- data/.github/workflows/linters.yml +16 -4
- data/.github/workflows/tests.yml +7 -4
- data/.gitignore +3 -0
- data/.rubocop.yml +9 -6
- data/.stylelintrc.json +12 -155
- data/README.md +28 -9
- data/app/controllers/additional_tags_controller.rb +10 -10
- data/app/controllers/issue_tags_controller.rb +3 -1
- data/app/helpers/additional_tags_helper.rb +39 -30
- data/app/helpers/additional_tags_issues_helper.rb +1 -3
- data/app/helpers/additional_tags_wiki_helper.rb +16 -10
- data/app/jobs/additional_tags_remove_unused_tag_job.rb +11 -6
- data/app/models/additional_tag.rb +98 -0
- data/app/views/additional_tags/_tag_list.html.slim +12 -6
- data/app/views/additional_tags/merge.html.slim +0 -1
- data/app/views/additional_tags/settings/_general.html.slim +7 -1
- data/app/views/additional_tags/settings/_manage_tags.html.slim +8 -1
- data/app/views/context_menus/_issues_tags.html.slim +7 -1
- data/app/views/dashboards/blocks/_issue_tags.html.slim +9 -7
- data/app/views/issue_tags/_edit_modal.html.slim +7 -7
- data/app/views/issue_tags/edit.js.erb +1 -1
- data/app/views/issues/_tags.html.slim +12 -7
- data/app/views/issues/_tags_bulk_edit.html.slim +2 -1
- data/app/views/issues/_tags_form_details.html.slim +1 -2
- data/app/views/wiki/_tags_form.html.slim +4 -0
- data/app/views/wiki/_tags_form_bottom.html.slim +1 -2
- data/app/views/wiki/_tags_show.html.slim +7 -7
- data/app/views/wiki/tag_index.html.slim +2 -2
- data/assets/javascripts/tags.js +3 -3
- data/assets/stylesheets/tags.css +41 -16
- data/config/locales/bg.yml +18 -11
- data/config/locales/cs.yml +19 -12
- data/config/locales/de.yml +19 -12
- data/config/locales/en.yml +19 -12
- data/config/locales/es.yml +19 -12
- data/config/locales/fr.yml +19 -12
- data/config/locales/it.yml +19 -12
- data/config/locales/ja.yml +18 -11
- data/config/locales/ko.yml +18 -11
- data/config/locales/pl.yml +19 -12
- data/config/locales/pt-BR.yml +18 -11
- data/config/locales/ru.yml +34 -27
- data/config/settings.yml +5 -5
- data/doc/images/additional-tags-framework.png +0 -0
- data/doc/images/additional-tags.gif +0 -0
- data/doc/images/tag-overview.png +0 -0
- data/lib/additional_tags/hooks/view_hook.rb +15 -2
- data/lib/additional_tags/patches/issue_patch.rb +53 -7
- data/lib/additional_tags/patches/issue_query_patch.rb +18 -0
- data/lib/additional_tags/patches/queries_helper_patch.rb +1 -3
- data/lib/additional_tags/patches/query_patch.rb +23 -2
- data/lib/additional_tags/patches/wiki_page_patch.rb +8 -2
- data/lib/additional_tags/plugin_version.rb +1 -1
- data/lib/additional_tags/tags.rb +35 -14
- data/lib/additional_tags.rb +5 -0
- data/package.json +8 -0
- 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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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]
|
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
|
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,
|
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]
|
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
|
data/lib/additional_tags/tags.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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.
|
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?
|
data/lib/additional_tags.rb
CHANGED
@@ -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
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.
|
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-
|
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.
|
201
|
+
rubygems_version: 3.3.26
|
199
202
|
signing_key:
|
200
203
|
specification_version: 4
|
201
204
|
summary: Redmine plugin for adding tag functionality
|