additional_tags 1.0.1 → 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/codeql-analysis.yml +70 -0
  3. data/.github/workflows/linters.yml +6 -1
  4. data/.github/workflows/tests.yml +11 -2
  5. data/.gitignore +2 -0
  6. data/.rubocop.yml +55 -5
  7. data/.slim-lint.yml +3 -2
  8. data/README.md +16 -17
  9. data/Rakefile +2 -0
  10. data/additional_tags.gemspec +10 -6
  11. data/app/controllers/additional_tags_controller.rb +4 -1
  12. data/app/controllers/issue_tags_controller.rb +6 -4
  13. data/app/helpers/additional_tags_helper.rb +91 -52
  14. data/app/helpers/additional_tags_issues_helper.rb +15 -3
  15. data/app/helpers/additional_tags_wiki_helper.rb +4 -26
  16. data/app/jobs/additional_tags_job.rb +2 -0
  17. data/app/jobs/additional_tags_remove_unused_tag_job.rb +7 -0
  18. data/app/models/migrate_tag.rb +2 -0
  19. data/app/models/migrate_tagging.rb +2 -0
  20. data/app/models/query_tags_column.rb +7 -0
  21. data/app/views/additional_tags/_body_bottom.html.slim +19 -0
  22. data/app/views/additional_tags/_html_head.html.slim +1 -4
  23. data/app/views/additional_tags/_tag_list.html.slim +1 -1
  24. data/app/views/additional_tags/merge.html.slim +4 -3
  25. data/app/views/additional_tags/settings/_manage_tags.html.slim +24 -23
  26. data/app/views/common/_tag_summary_block.html.slim +11 -0
  27. data/app/views/context_menus/_issues_tags.html.slim +1 -1
  28. data/app/views/dashboards/blocks/_issue_tags.html.slim +58 -0
  29. data/app/views/dashboards/blocks/_issue_tags_settings.html.slim +20 -0
  30. data/app/views/issue_tags/_edit_modal.html.slim +6 -4
  31. data/app/views/issues/_tags_form.html.slim +1 -1
  32. data/assets/javascripts/tags.js +4 -3
  33. data/assets/stylesheets/tags.css +27 -13
  34. data/config/initializers/zeitwerk.rb +6 -0
  35. data/config/locales/bg.yml +5 -0
  36. data/config/locales/cs.yml +5 -0
  37. data/config/locales/de.yml +5 -0
  38. data/config/locales/en.yml +5 -0
  39. data/config/locales/es.yml +5 -0
  40. data/config/locales/fr.yml +5 -0
  41. data/config/locales/it.yml +5 -0
  42. data/config/locales/ja.yml +5 -0
  43. data/config/locales/ko.yml +5 -0
  44. data/config/locales/pl.yml +5 -0
  45. data/config/locales/pt-BR.yml +5 -0
  46. data/config/locales/ru.yml +28 -23
  47. data/config/routes.rb +2 -0
  48. data/db/migrate/20201116145429_acts_as_taggable_migration.rb +3 -1
  49. data/db/migrate/20201123093214_migrate_existing_tags.rb +5 -3
  50. data/init.rb +11 -6
  51. data/lib/additional_tags/hooks/model_hook.rb +13 -0
  52. data/lib/additional_tags/hooks/view_hook.rb +77 -0
  53. data/lib/additional_tags/patches/agile_boards_controller_patch.rb +2 -0
  54. data/lib/additional_tags/patches/agile_query_patch.rb +11 -9
  55. data/lib/additional_tags/patches/agile_versions_controller_patch.rb +2 -0
  56. data/lib/additional_tags/patches/agile_versions_query_patch.rb +3 -1
  57. data/lib/additional_tags/patches/auto_completes_controller_patch.rb +8 -7
  58. data/lib/additional_tags/patches/calendars_controller_patch.rb +2 -0
  59. data/lib/additional_tags/patches/dashboard_async_blocks_controller_patch.rb +2 -0
  60. data/lib/additional_tags/patches/dashboard_content_patch.rb +28 -0
  61. data/lib/additional_tags/patches/dashboards_controller_patch.rb +2 -0
  62. data/lib/additional_tags/patches/gantts_controller_patch.rb +2 -0
  63. data/lib/additional_tags/patches/imports_controller_patch.rb +2 -0
  64. data/lib/additional_tags/patches/issue_patch.rb +19 -20
  65. data/lib/additional_tags/patches/issue_query_patch.rb +17 -14
  66. data/lib/additional_tags/patches/issues_controller_patch.rb +2 -0
  67. data/lib/additional_tags/patches/journal_patch.rb +2 -0
  68. data/lib/additional_tags/patches/my_controller_patch.rb +2 -0
  69. data/lib/additional_tags/patches/queries_helper_patch.rb +3 -15
  70. data/lib/additional_tags/patches/query_patch.rb +83 -0
  71. data/lib/additional_tags/patches/settings_controller_patch.rb +2 -0
  72. data/lib/additional_tags/patches/time_entry_patch.rb +2 -0
  73. data/lib/additional_tags/patches/time_entry_query_patch.rb +5 -15
  74. data/lib/additional_tags/patches/time_report_patch.rb +2 -0
  75. data/lib/additional_tags/patches/timelog_controller_patch.rb +2 -0
  76. data/lib/additional_tags/patches/wiki_controller_patch.rb +3 -1
  77. data/lib/additional_tags/patches/wiki_page_patch.rb +54 -3
  78. data/lib/additional_tags/plugin_version.rb +7 -0
  79. data/lib/additional_tags/tags.rb +78 -18
  80. data/lib/additional_tags.rb +53 -74
  81. data/lib/tasks/additional_tags.rake +18 -0
  82. metadata +32 -9
  83. data/.github/workflows/brakeman.yml +0 -33
  84. data/app/views/reports/_tags_simple.html.slim +0 -11
  85. data/lib/additional_tags/hooks.rb +0 -73
  86. data/lib/additional_tags/patches/reports_controller_patch.rb +0 -32
  87. data/lib/additional_tags/version.rb +0 -3
@@ -1,15 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AdditionalTags
2
4
  module Patches
3
5
  module IssuePatch
4
6
  extend ActiveSupport::Concern
5
7
 
6
8
  included do
9
+ include Additionals::EntityMethodsGlobal
7
10
  include InstanceMethods
8
11
  acts_as_ordered_taggable
9
12
 
10
13
  before_save :prepare_save_tag_change
11
14
  before_save :sort_tag_list
12
15
 
16
+ after_commit :add_remove_unused_tags_job, on: %i[update destroy],
17
+ if: proc { AdditionalTags.setting?(:active_issue_tags) }
18
+
13
19
  alias_method :safe_attributes_without_tags=, :safe_attributes=
14
20
  alias_method :safe_attributes=, :safe_attributes_with_tags=
15
21
 
@@ -20,25 +26,24 @@ module AdditionalTags
20
26
  class_methods do
21
27
  def allowed_tags?(tags)
22
28
  allowed_tags = available_tags.map(&:name)
23
- tags.all? { |tag| allowed_tags.include?(tag) }
29
+ tags.all? { |tag| allowed_tags.include? tag }
24
30
  end
25
31
 
26
- def by_tags(project, with_subprojects: false)
27
- count_and_group_by(project: project, association: :tags, with_subprojects: with_subprojects)
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
28
37
  end
29
38
 
30
- def available_tags(options = {})
39
+ def available_tags(**options)
31
40
  options[:permission] = :view_issues
32
- tags = AdditionalTags::Tags.available_tags self, options
41
+ tags = AdditionalTags::Tags.available_tags self, **options
33
42
  return tags unless options[:open_issues_only]
34
43
 
35
44
  tags.joins("JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{table_name}.status_id")
36
45
  .where(issue_statuses: { is_closed: false })
37
46
  end
38
-
39
- def remove_unused_tags!
40
- AdditionalTagsRemoveUnusedTagJob.perform_later
41
- end
42
47
  end
43
48
 
44
49
  module InstanceMethods
@@ -62,24 +67,18 @@ module AdditionalTags
62
67
  end
63
68
  end
64
69
 
65
- send 'safe_attributes_without_tags=', attrs, user
70
+ send :safe_attributes_without_tags=, attrs, user
66
71
  end
67
72
 
73
+ # copy_from requires hash for Redmine - works with Ruby 3
74
+ # rubocop: disable Style/OptionHash
68
75
  def copy_from_with_tags(arg, options = {})
69
- copy_from_without_tags(arg, options)
76
+ copy_from_without_tags arg, options
70
77
  issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
71
78
  self.tag_list = issue.tag_list
72
79
  self
73
80
  end
74
-
75
- def tags_to_journal(old_tags, new_tags)
76
- return if current_journal.blank? || old_tags == new_tags
77
-
78
- current_journal.details << JournalDetail.new(property: 'attr',
79
- prop_key: 'tag_list',
80
- old_value: old_tags,
81
- value: new_tags)
82
- end
81
+ # rubocop: enable Style/OptionHash
83
82
 
84
83
  private
85
84
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AdditionalTags
2
4
  module Patches
3
5
  module IssueQueryPatch
@@ -5,6 +7,7 @@ module AdditionalTags
5
7
 
6
8
  included do
7
9
  include AdditionalsQuery
10
+ prepend InstanceOverwriteMethods
8
11
  include InstanceMethods
9
12
 
10
13
  alias_method :initialize_available_filters_without_tags, :initialize_available_filters
@@ -12,9 +15,20 @@ module AdditionalTags
12
15
 
13
16
  alias_method :available_columns_without_tags, :available_columns
14
17
  alias_method :available_columns, :available_columns_with_tags
18
+ end
19
+
20
+ module InstanceOverwriteMethods
21
+ def build_from_params(params, defaults = {})
22
+ super
23
+
24
+ return self if params[:tag_id].blank?
15
25
 
16
- alias_method :build_from_params_without_tags, :build_from_params
17
- alias_method :build_from_params, :build_from_params_with_tags
26
+ add_filter 'tags',
27
+ '=',
28
+ [ActsAsTaggableOn::Tag.find_by(id: params[:tag_id]).try(:name)]
29
+
30
+ self
31
+ end
18
32
  end
19
33
 
20
34
  module InstanceMethods
@@ -31,24 +45,13 @@ module AdditionalTags
31
45
  @available_columns = available_columns_without_tags
32
46
 
33
47
  if AdditionalTags.setting?(:active_issue_tags) && User.current.allowed_to?(:view_issue_tags, project, global: true)
34
- @available_columns << QueryColumn.new(:tags)
48
+ @available_columns << ::QueryTagsColumn.new
35
49
  end
36
50
  else
37
51
  available_columns_without_tags
38
52
  end
39
53
  @available_columns
40
54
  end
41
-
42
- def build_from_params_with_tags(params, defaults = {})
43
- build_from_params_without_tags params, defaults
44
- return self if params[:tag_id].blank?
45
-
46
- add_filter 'tags',
47
- '=',
48
- [ActsAsTaggableOn::Tag.find_by(id: params[:tag_id]).try(:name)]
49
-
50
- self
51
- end
52
55
  end
53
56
  end
54
57
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AdditionalTags
2
4
  module Patches
3
5
  module IssuesControllerPatch
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AdditionalTags
2
4
  module Patches
3
5
  module JournalPatch
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AdditionalTags
2
4
  module Patches
3
5
  module MyControllerPatch
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AdditionalTags
2
4
  module Patches
3
5
  module QueriesHelperPatch
@@ -8,25 +10,11 @@ module AdditionalTags
8
10
 
9
11
  alias_method :column_content_without_tags, :column_content
10
12
  alias_method :column_content, :column_content_with_tags
11
-
12
- alias_method :csv_content_without_tags, :csv_content
13
- alias_method :csv_content, :csv_content_with_tags
14
13
  end
15
14
 
16
15
  module InstanceMethods
17
- def csv_content_with_tags(column, item)
18
- if item.is_a?(Issue) && column.name == :tags ||
19
- item.is_a?(TimeEntry) && column.name == :issue_tags
20
-
21
- additional_plain_tag_list column.value_object(item)
22
- else
23
- csv_content_without_tags column, item
24
- end
25
- end
26
-
27
16
  def column_content_with_tags(column, item)
28
- if item.is_a?(Issue) && column.name == :tags ||
29
- item.is_a?(TimeEntry) && column.name == :issue_tags
17
+ if column.name == :issue_tags || item.is_a?(Issue) && column.name == :tags
30
18
  additional_tag_links column.value(item),
31
19
  tag_controller: 'issues',
32
20
  use_colors: AdditionalTags.setting?(:use_colors)
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AdditionalTags
4
+ module Patches
5
+ module QueryPatch
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ include InstanceMethods
10
+ end
11
+
12
+ module InstanceMethods
13
+ def sql_for_tags_field(field, _operator, values)
14
+ build_sql_for_tags_field klass: queried_class, operator: operator_for(field), values: values
15
+ end
16
+
17
+ def initialize_tags_filter(position: nil)
18
+ add_available_filter 'tags', order: position,
19
+ type: :list_optional,
20
+ values: -> { available_tag_values queried_class }
21
+ end
22
+
23
+ def initialize_issue_tags_filter
24
+ return unless AdditionalTags.setting?(:active_issue_tags) && User.current.allowed_to?(:view_issue_tags, project, global: true)
25
+
26
+ add_available_filter 'issue.tags',
27
+ type: :list_optional,
28
+ name: l('label_attribute_of_issue', name: l(:field_tags)),
29
+ values: -> { available_tag_values Issue }
30
+ end
31
+
32
+ def available_tag_values(klass)
33
+ klass.available_tags(project: project)
34
+ .pluck(:name)
35
+ .map { |name| [name, name] }
36
+ end
37
+
38
+ def build_subquery_for_tags_field(klass:, operator:, values:, joined_table:, joined_field:,
39
+ source_field: 'id', target_field: 'issue_id')
40
+ quoted_joined_table = self.class.connection.quote_table_name joined_table
41
+ quoted_joined_field = self.class.connection.quote_column_name joined_field
42
+ quoted_source_field = self.class.connection.quote_column_name source_field
43
+ quoted_target_field = self.class.connection.quote_column_name target_field
44
+ subsql = ActsAsTaggableOn::Tagging.joins("INNER JOIN #{quoted_joined_table}" \
45
+ " ON additional_taggings.taggable_id = #{quoted_joined_table}.#{quoted_target_field}")
46
+ .where(taggable_type: klass.name)
47
+ .where("#{self.class.connection.quote_table_name queried_table_name}.#{quoted_source_field} ="\
48
+ " #{quoted_joined_table}.#{quoted_joined_field}")
49
+ .select(1)
50
+
51
+ if %w[= !].include? operator
52
+ ids_list = klass.tagged_with(values, any: true).pluck :id
53
+ subsql = subsql.where taggable_id: ids_list
54
+ end
55
+
56
+ if %w[= *].include? operator
57
+ " EXISTS(#{subsql.to_sql})"
58
+ else
59
+ " NOT EXISTS(#{subsql.to_sql})"
60
+ end
61
+ end
62
+
63
+ def build_sql_for_tags_field(klass:, operator:, values:)
64
+ compare = ['=', '*'].include?(operator) ? 'IN' : 'NOT IN'
65
+ case operator
66
+ when '=', '!'
67
+ ids_list = klass.tagged_with(values, any: true).pluck :id
68
+ if ids_list.present?
69
+ "(#{klass.table_name}.id #{compare} (#{ids_list.join ','}))"
70
+ elsif values.present? && operator == '='
71
+ # special case: filter with deleted tag
72
+ '(1=0)'
73
+ end
74
+ else
75
+ entries = ActsAsTaggableOn::Tagging.where taggable_type: klass.name
76
+ id_table = klass.table_name
77
+ "(#{id_table}.id #{compare} (#{entries.select(:taggable_id).to_sql}))"
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AdditionalTags
2
4
  module Patches
3
5
  module SettingsControllerPatch
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AdditionalTags
2
4
  module Patches
3
5
  module TimeEntryPatch
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_dependency 'time_entry_query'
2
4
 
3
5
  module AdditionalTags
@@ -18,13 +20,7 @@ module AdditionalTags
18
20
  module InstanceMethods
19
21
  def initialize_available_filters_with_tags
20
22
  initialize_available_filters_without_tags
21
-
22
- return unless AdditionalTags.setting?(:active_issue_tags) && User.current.allowed_to?(:view_issue_tags, project, global: true)
23
-
24
- add_available_filter 'issue.tags',
25
- type: :list_optional,
26
- name: l('label_attribute_of_issue', name: l(:field_tags)),
27
- values: -> { available_issue_tags_values }
23
+ initialize_issue_tags_filter
28
24
  end
29
25
 
30
26
  def available_columns_with_tags
@@ -39,14 +35,8 @@ module AdditionalTags
39
35
  @available_columns
40
36
  end
41
37
 
42
- def sql_for_issue_tags_field(_field, operator, value)
43
- AdditionalTags.sql_for_tags_field Issue, operator, value
44
- end
45
-
46
- def available_issue_tags_values
47
- Issue.available_tags(project: project)
48
- .pluck(:name)
49
- .map { |name| [name, name] }
38
+ def sql_for_issue_tags_field(_field, operator, values)
39
+ build_sql_for_tags_field klass: Issue, operator: operator, values: values
50
40
  end
51
41
  end
52
42
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_dependency 'query'
2
4
 
3
5
  module AdditionalTags
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AdditionalTags
2
4
  module Patches
3
5
  module TimelogControllerPatch
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AdditionalTags
2
4
  module Patches
3
5
  module WikiControllerPatch
@@ -29,7 +31,7 @@ module AdditionalTags
29
31
  @tag = params[:tag]
30
32
  return super unless AdditionalTags.setting?(:active_wiki_tags) && @tag.present?
31
33
 
32
- @pages = wiki_pages_with_tag @tag, @project
34
+ @pages = WikiPage.with_tags @tag, project: @project
33
35
 
34
36
  respond_to do |format|
35
37
  format.html do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AdditionalTags
2
4
  module Patches
3
5
  module WikiPagePatch
@@ -20,10 +22,59 @@ module AdditionalTags
20
22
  "JOIN #{Project.table_name} ON wikis.project_id = #{Project.table_name}.id"]
21
23
  end
22
24
 
23
- def available_tags(options = {})
25
+ def available_tags(**options)
24
26
  options[:project_join] = project_joins
25
27
  options[:permission] = :view_wiki_pages
26
- AdditionalTags::Tags.available_tags self, options
28
+ AdditionalTags::Tags.available_tags self, **options
29
+ end
30
+
31
+ def with_tags_scope(project: nil, wiki: nil)
32
+ scope = if wiki
33
+ wiki.pages
34
+ else
35
+ scope = WikiPage.joins wiki: :project
36
+ scope = scope.where wikis: { project_id: project.id } if project
37
+ scope
38
+ end
39
+
40
+ scope = scope.visible User.current, project: project if scope.respond_to? :visible
41
+ scope
42
+ end
43
+
44
+ def with_tags(tag, project: nil, order: 'title_asc', max_entries: nil)
45
+ wiki = project&.wiki
46
+
47
+ scope = with_tags_scope wiki: wiki, project: project
48
+ scope = scope.limit max_entries if max_entries
49
+
50
+ tags = Array tag
51
+ tags.map!(&:strip)
52
+ tags.reject!(&:blank?)
53
+ return [] if tags.count.zero?
54
+
55
+ tags.map!(&:downcase)
56
+
57
+ scope = scope.where(id: tagged_with(tags, any: true).ids)
58
+ .with_updated_on
59
+
60
+ return scope if order.nil?
61
+
62
+ sorted = order.split '_'
63
+ raise 'unsupported sort order' if sorted != 2 && %w[title date].exclude?(sorted.first)
64
+
65
+ order_dir = sorted.second == 'desc' ? 'DESC' : 'ASC'
66
+
67
+ case sorted.first
68
+ when 'date'
69
+ scope.joins(:content)
70
+ .reorder("#{WikiContent.table_name}.updated_on #{order_dir}")
71
+ else
72
+ if wiki.nil?
73
+ scope.order "#{Project.table_name}.name, #{WikiPage.table_name}.title #{order_dir}"
74
+ else
75
+ scope.includes(:parent).order "#{WikiPage.table_name}.title #{order_dir}"
76
+ end
77
+ end
27
78
  end
28
79
  end
29
80
 
@@ -44,7 +95,7 @@ module AdditionalTags
44
95
  end
45
96
  end
46
97
 
47
- send 'safe_attributes_without_tags=', attrs, user
98
+ send :safe_attributes_without_tags=, attrs, user
48
99
  end
49
100
 
50
101
  private
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AdditionalTags
4
+ module PluginVersion
5
+ VERSION = '1.0.4' unless defined? VERSION
6
+ end
7
+ end
@@ -1,20 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AdditionalTags
2
4
  class Tags
3
5
  class << self
4
- def available_tags(klass, options = {})
6
+ def available_tags(klass, **options)
5
7
  user = options[:user].presence || User.current
6
8
 
7
9
  scope = ActsAsTaggableOn::Tag.where({})
8
- scope = scope.where("#{Project.table_name}.id = ?", options[:project]) if options[:project]
10
+ scope = scope.where "#{Project.table_name}.id = ?", options[:project] if options[:project]
9
11
  if options[:permission]
10
- scope = scope.where(tag_access(options[:permission], user))
12
+ scope = scope.where tag_access(options[:permission], user)
11
13
  elsif options[:visible_condition]
12
- scope = scope.where(klass.visible_condition(user))
14
+ scope = scope.where klass.visible_condition(user)
13
15
  end
14
- scope = scope.where("LOWER(#{TAG_TABLE_NAME}.name) LIKE ?", "%#{options[:name_like].downcase}%") if options[:name_like]
15
- scope = scope.where("#{TAG_TABLE_NAME}.name=?", options[:name]) if options[:name]
16
- scope = scope.where("#{TAGGING_TABLE_NAME}.taggable_id!=?", options[:exclude_id]) if options[:exclude_id]
17
- scope = scope.where(options[:where_field] => options[:where_value]) if options[:where_field].present? && options[:where_value]
16
+ scope = scope.where "LOWER(#{TAG_TABLE_NAME}.name) LIKE ?", "%#{options[:name_like].downcase}%" if options[:name_like]
17
+ scope = scope.where "#{TAG_TABLE_NAME}.name=?", options[:name] if options[:name]
18
+ scope = scope.where "#{TAGGING_TABLE_NAME}.taggable_id!=?", options[:exclude_id] if options[:exclude_id]
19
+ scope = scope.where options[:where_field] => options[:where_value] if options[:where_field].present? && options[:where_value]
18
20
 
19
21
  columns = ["#{TAG_TABLE_NAME}.id",
20
22
  "#{TAG_TABLE_NAME}.name",
@@ -24,14 +26,14 @@ module AdditionalTags
24
26
  columns << "MIN(#{TAGGING_TABLE_NAME}.created_at) AS last_created" if options[:sort_by] == 'last_created'
25
27
 
26
28
  scope.select(columns.join(', '))
27
- .joins(tag_for_joins(klass, options))
29
+ .joins(tag_for_joins(klass, **options.slice(:project_join, :project, :without_projects)))
28
30
  .group("#{TAG_TABLE_NAME}.id, #{TAG_TABLE_NAME}.name, #{TAG_TABLE_NAME}.taggings_count").having('COUNT(*) > 0')
29
31
  .order(build_order_sql(options[:sort_by], options[:order]))
30
32
  end
31
33
 
32
- def all_type_tags(klass, options = {})
33
- ActsAsTaggableOn::Tag.where({})
34
- .joins(tag_for_joins(klass, options))
34
+ def all_type_tags(klass, without_projects: false)
35
+ ActsAsTaggableOn::Tag.all
36
+ .joins(tag_for_joins(klass, without_projects: without_projects))
35
37
  .distinct
36
38
  .order("#{TAG_TABLE_NAME}.name")
37
39
  end
@@ -61,8 +63,9 @@ module AdditionalTags
61
63
  # remove old (merged) tags
62
64
  tags_to_merge.reject { |t| t.id == tag.id }.each(&:destroy)
63
65
  # remove duplicate taggings
64
- dup_scope = ActsAsTaggableOn::Tagging.where(tag_id: tag.id)
65
- valid_ids = dup_scope.group(:tag_id, :taggable_id, :taggable_type, :tagger_id, :tagger_type, :context).pluck(Arel.sql('MIN(id)'))
66
+ dup_scope = ActsAsTaggableOn::Tagging.where tag_id: tag.id
67
+ valid_ids = dup_scope.group(:tag_id, :taggable_id, :taggable_type, :tagger_id, :tagger_type, :context)
68
+ .pluck(Arel.sql('MIN(id)'))
66
69
  dup_scope.where.not(id: valid_ids).destroy_all if valid_ids.any?
67
70
  # recalc count for new tag
68
71
  ActsAsTaggableOn::Tag.reset_counters tag.id, :taggings
@@ -83,8 +86,65 @@ module AdditionalTags
83
86
  end
84
87
  end
85
88
 
89
+ def build_relation_tags(entries)
90
+ entries = Array entries
91
+ return [] if entries.none?
92
+
93
+ tags = entries.map(&:tags)
94
+ tags.flatten!
95
+
96
+ tags.uniq
97
+ end
98
+
99
+ def entity_group_by(scope:, tags:, statuses: nil, sub_groups: nil, group_id_is_bool: false)
100
+ counts = {}
101
+ tags.each do |tag|
102
+ values = { tag: tag, total: 0, total_sub_groups: 0, groups: [] }
103
+
104
+ if statuses
105
+ statuses.each do |status|
106
+ group_id = status.first
107
+ group = status.second
108
+ values[group] = status_for_tag_value scope: scope,
109
+ tag_id: tag.id,
110
+ group_id: group_id,
111
+ group_id_is_bool: group_id_is_bool
112
+ values[:groups] << { id: group_id, group: group, count: values[group] }
113
+ values[:total] += values[group]
114
+ values[:total_sub_groups] += values[group] if sub_groups&.include? group_id
115
+ end
116
+ else
117
+ values[:total] += status_for_tag_value scope: scope, tag_id: tag.id
118
+ end
119
+
120
+ values[:total_without_sub_groups] = values[:total] - values[:total_sub_groups]
121
+
122
+ counts[tag.name] = values
123
+ end
124
+
125
+ counts
126
+ end
127
+
86
128
  private
87
129
 
130
+ def status_for_tag_value(scope:, tag_id:, group_id: nil, group_id_is_bool: false)
131
+ value = if group_id_is_bool || group_id
132
+ if group_id_is_bool
133
+ if group_id
134
+ scope[[1, tag_id]] || scope[[true, tag_id]]
135
+ else
136
+ scope[[0, tag_id]] || scope[[false, tag_id]]
137
+ end
138
+ else
139
+ scope[[group_id, tag_id]]
140
+ end
141
+ else
142
+ scope[tag_id]
143
+ end
144
+
145
+ value || 0
146
+ end
147
+
88
148
  def build_order_sql(sort_by, order)
89
149
  order = order.present? && order == 'DESC' ? 'DESC' : 'ASC'
90
150
 
@@ -100,16 +160,16 @@ module AdditionalTags
100
160
  Arel.sql sql
101
161
  end
102
162
 
103
- def tag_for_joins(klass, options = {})
163
+ def tag_for_joins(klass, project_join: nil, project: nil, without_projects: false)
104
164
  table_name = klass.table_name
105
165
 
106
166
  joins = ["JOIN #{TAGGING_TABLE_NAME} ON #{TAGGING_TABLE_NAME}.tag_id = #{TAG_TABLE_NAME}.id"]
107
167
  joins << "JOIN #{table_name} " \
108
168
  "ON #{table_name}.id = #{TAGGING_TABLE_NAME}.taggable_id AND #{TAGGING_TABLE_NAME}.taggable_type = '#{klass}'"
109
169
 
110
- if options[:project_join]
111
- joins << options[:project_join]
112
- elsif options[:project] || !options[:without_projects]
170
+ if project_join
171
+ joins << project_join
172
+ elsif project || !without_projects
113
173
  joins << "JOIN #{Project.table_name} ON #{table_name}.project_id = #{Project.table_name}.id"
114
174
  end
115
175