additional_tags 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/brakeman.yml +38 -0
  3. data/.github/workflows/linters.yml +41 -0
  4. data/.github/workflows/tests.yml +139 -0
  5. data/.gitignore +8 -0
  6. data/.rubocop.yml +75 -0
  7. data/.slim-lint.yml +25 -0
  8. data/.stylelintrc.json +163 -0
  9. data/LICENSE +339 -0
  10. data/README.md +126 -0
  11. data/Rakefile +11 -0
  12. data/additional_tags.gemspec +28 -0
  13. data/app/controllers/additional_tags_controller.rb +80 -0
  14. data/app/controllers/issue_tags_controller.rb +48 -0
  15. data/app/helpers/additional_tags_helper.rb +197 -0
  16. data/app/helpers/additional_tags_issues_helper.rb +43 -0
  17. data/app/helpers/additional_tags_wiki_helper.rb +17 -0
  18. data/app/jobs/additional_tags_job.rb +3 -0
  19. data/app/jobs/additional_tags_remove_unused_tag_job.rb +5 -0
  20. data/app/models/migrate_tag.rb +4 -0
  21. data/app/models/migrate_tagging.rb +5 -0
  22. data/app/views/additional_tags/_html_head.html.slim +6 -0
  23. data/app/views/additional_tags/_tag_list.html.slim +35 -0
  24. data/app/views/additional_tags/context_menu.html.slim +16 -0
  25. data/app/views/additional_tags/edit.html.slim +14 -0
  26. data/app/views/additional_tags/merge.html.slim +17 -0
  27. data/app/views/additional_tags/settings/_general.html.slim +45 -0
  28. data/app/views/additional_tags/settings/_manage_tags.html.slim +35 -0
  29. data/app/views/additional_tags/settings/_settings.html.slim +9 -0
  30. data/app/views/auto_completes/_additional_tag_list.slim +1 -0
  31. data/app/views/context_menus/_issues_tags.html.slim +10 -0
  32. data/app/views/issue_tags/_edit_modal.html.slim +30 -0
  33. data/app/views/issue_tags/edit.js.erb +2 -0
  34. data/app/views/issues/_tags.html.slim +8 -0
  35. data/app/views/issues/_tags_bulk_edit.html.slim +7 -0
  36. data/app/views/issues/_tags_form.html.slim +11 -0
  37. data/app/views/issues/_tags_form_details.html.slim +7 -0
  38. data/app/views/issues/_tags_sidebar.html.slim +5 -0
  39. data/app/views/reports/_tags_simple.html.slim +11 -0
  40. data/app/views/wiki/_tags_form.html.slim +7 -0
  41. data/app/views/wiki/_tags_form_bottom.html.slim +5 -0
  42. data/app/views/wiki/_tags_show.html.slim +9 -0
  43. data/app/views/wiki/_tags_sidebar.html.slim +4 -0
  44. data/app/views/wiki/tag_index.html.slim +21 -0
  45. data/assets/javascripts/tags.js +19 -0
  46. data/assets/stylesheets/tags.css +119 -0
  47. data/config/locales/bg.yml +33 -0
  48. data/config/locales/cs.yml +33 -0
  49. data/config/locales/de.yml +33 -0
  50. data/config/locales/en.yml +33 -0
  51. data/config/locales/es.yml +33 -0
  52. data/config/locales/fr.yml +33 -0
  53. data/config/locales/it.yml +33 -0
  54. data/config/locales/ja.yml +33 -0
  55. data/config/locales/ko.yml +33 -0
  56. data/config/locales/pl.yml +33 -0
  57. data/config/locales/pt-BR.yml +33 -0
  58. data/config/locales/ru.yml +33 -0
  59. data/config/routes.rb +31 -0
  60. data/config/settings.yml +9 -0
  61. data/db/migrate/20201116145429_acts_as_taggable_migration.rb +40 -0
  62. data/db/migrate/20201123093214_migrate_existing_tags.rb +39 -0
  63. data/doc/images/additional-tags-framework.png +0 -0
  64. data/doc/images/additional-tags.gif +0 -0
  65. data/init.rb +31 -0
  66. data/lib/additional_tags.rb +107 -0
  67. data/lib/additional_tags/hooks.rb +63 -0
  68. data/lib/additional_tags/patches/agile_boards_controller_patch.rb +14 -0
  69. data/lib/additional_tags/patches/agile_query_patch.rb +41 -0
  70. data/lib/additional_tags/patches/agile_versions_controller_patch.rb +14 -0
  71. data/lib/additional_tags/patches/agile_versions_query_patch.rb +11 -0
  72. data/lib/additional_tags/patches/auto_completes_controller_patch.rb +42 -0
  73. data/lib/additional_tags/patches/calendars_controller_patch.rb +14 -0
  74. data/lib/additional_tags/patches/dashboard_async_blocks_controller_patch.rb +11 -0
  75. data/lib/additional_tags/patches/dashboards_controller_patch.rb +11 -0
  76. data/lib/additional_tags/patches/gantts_controller_patch.rb +14 -0
  77. data/lib/additional_tags/patches/imports_controller_patch.rb +12 -0
  78. data/lib/additional_tags/patches/issue_patch.rb +95 -0
  79. data/lib/additional_tags/patches/issue_query_patch.rb +55 -0
  80. data/lib/additional_tags/patches/issues_controller_patch.rb +14 -0
  81. data/lib/additional_tags/patches/journal_patch.rb +18 -0
  82. data/lib/additional_tags/patches/my_controller_patch.rb +14 -0
  83. data/lib/additional_tags/patches/queries_helper_patch.rb +40 -0
  84. data/lib/additional_tags/patches/reports_controller_patch.rb +32 -0
  85. data/lib/additional_tags/patches/settings_controller_patch.rb +12 -0
  86. data/lib/additional_tags/patches/time_entry_patch.rb +19 -0
  87. data/lib/additional_tags/patches/time_entry_query_patch.rb +53 -0
  88. data/lib/additional_tags/patches/time_report_patch.rb +46 -0
  89. data/lib/additional_tags/patches/timelog_controller_patch.rb +14 -0
  90. data/lib/additional_tags/patches/wiki_controller_patch.rb +63 -0
  91. data/lib/additional_tags/patches/wiki_page_patch.rb +39 -0
  92. data/lib/additional_tags/tags.rb +134 -0
  93. data/lib/additional_tags/version.rb +3 -0
  94. metadata +177 -0
@@ -0,0 +1,11 @@
1
+ module AdditionalTags
2
+ module Patches
3
+ module DashboardsControllerPatch
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ helper :additional_tags
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ module AdditionalTags
2
+ module Patches
3
+ module GanttsControllerPatch
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ helper :additional_tags
8
+ helper :additional_tags_issues
9
+
10
+ include AdditionalTagsHelper
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ module AdditionalTags
2
+ module Patches
3
+ module ImportsControllerPatch
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ helper :additional_tags
8
+ helper :additional_tags_issues
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,95 @@
1
+ module AdditionalTags
2
+ module Patches
3
+ module IssuePatch
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include InstanceMethods
8
+ acts_as_ordered_taggable
9
+
10
+ before_save :prepare_save_tag_change
11
+ before_save :sort_tag_list
12
+
13
+ alias_method :safe_attributes_without_tags=, :safe_attributes=
14
+ alias_method :safe_attributes=, :safe_attributes_with_tags=
15
+
16
+ alias_method :copy_from_without_tags, :copy_from
17
+ alias_method :copy_from, :copy_from_with_tags
18
+ end
19
+
20
+ class_methods do
21
+ def allowed_tags?(tags)
22
+ allowed_tags = available_tags.map(&:name)
23
+ tags.all? { |tag| allowed_tags.include?(tag) }
24
+ end
25
+
26
+ def by_tags(project, with_subprojects: false)
27
+ count_and_group_by(project: project, association: :tags, with_subprojects: with_subprojects)
28
+ end
29
+
30
+ def available_tags(options = {})
31
+ options[:permission] = :view_issues
32
+ tags = AdditionalTags::Tags.available_tags self, options
33
+ return tags unless options[:open_issues_only]
34
+
35
+ tags.joins("JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{table_name}.status_id")
36
+ .where(issue_statuses: { is_closed: false })
37
+ end
38
+
39
+ def remove_unused_tags!
40
+ AdditionalTagsRemoveUnusedTagJob.perform_later
41
+ end
42
+ end
43
+
44
+ module InstanceMethods
45
+ # tag_list_changed? is broken for after_save
46
+ # tag_list_changed? is not working here, too!
47
+ def prepare_save_tag_change
48
+ return unless defined?(tag_list) && defined?(tag_list_was) && !tag_list_was.nil?
49
+
50
+ @prepare_save_tag_change ||= tag_list != tag_list_was
51
+ end
52
+
53
+ def safe_attributes_with_tags=(attrs, user = User.current)
54
+ if attrs && attrs[:tag_list]
55
+ tags = attrs.delete :tag_list
56
+ tags = Array(tags).reject(&:empty?)
57
+
58
+ if user.allowed_to?(:create_issue_tags, project) ||
59
+ user.allowed_to?(:edit_issue_tags, project) && Issue.allowed_tags?(tags)
60
+ attrs[:tag_list] = tags
61
+ self.tag_list = tags
62
+ end
63
+ end
64
+
65
+ send 'safe_attributes_without_tags=', attrs, user
66
+ end
67
+
68
+ def copy_from_with_tags(arg, options = {})
69
+ copy_from_without_tags(arg, options)
70
+ issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
71
+ self.tag_list = issue.tag_list
72
+ self
73
+ 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
83
+
84
+ private
85
+
86
+ def sort_tag_list
87
+ tags = tag_list.reject(&:empty?)
88
+ return unless tags.present? && tag_list_changed?
89
+
90
+ self.tag_list = AdditionalTags::Tags.sort_tags tags
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,55 @@
1
+ module AdditionalTags
2
+ module Patches
3
+ module IssueQueryPatch
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include AdditionalsQuery
8
+ include InstanceMethods
9
+
10
+ alias_method :initialize_available_filters_without_tags, :initialize_available_filters
11
+ alias_method :initialize_available_filters, :initialize_available_filters_with_tags
12
+
13
+ alias_method :available_columns_without_tags, :available_columns
14
+ alias_method :available_columns, :available_columns_with_tags
15
+
16
+ alias_method :build_from_params_without_tags, :build_from_params
17
+ alias_method :build_from_params, :build_from_params_with_tags
18
+ end
19
+
20
+ module InstanceMethods
21
+ def initialize_available_filters_with_tags
22
+ initialize_available_filters_without_tags
23
+
24
+ initialize_tags_filter if !available_filters.key?('tags') &&
25
+ AdditionalTags.setting?(:active_issue_tags) &&
26
+ User.current.allowed_to?(:view_issue_tags, project, global: true)
27
+ end
28
+
29
+ def available_columns_with_tags
30
+ if @available_columns.nil?
31
+ @available_columns = available_columns_without_tags
32
+
33
+ if AdditionalTags.setting?(:active_issue_tags) && User.current.allowed_to?(:view_issue_tags, project, global: true)
34
+ @available_columns << QueryColumn.new(:tags)
35
+ end
36
+ else
37
+ available_columns_without_tags
38
+ end
39
+ @available_columns
40
+ 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
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,14 @@
1
+ module AdditionalTags
2
+ module Patches
3
+ module IssuesControllerPatch
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ helper :additional_tags
8
+ helper :additional_tags_issues
9
+
10
+ include AdditionalTagsHelper
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+ module AdditionalTags
2
+ module Patches
3
+ module JournalPatch
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ prepend InstanceOverwriteMethods
8
+ end
9
+
10
+ module InstanceOverwriteMethods
11
+ def visible_details(user = User.current)
12
+ details = super
13
+ details.reject { |x| x.prop_key == 'tag_list' && !user.allowed_to?(:view_issue_tags, project) }
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ module AdditionalTags
2
+ module Patches
3
+ module MyControllerPatch
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ helper :additional_tags
8
+ helper :additional_tags_issues
9
+
10
+ include AdditionalTagsHelper
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,40 @@
1
+ module AdditionalTags
2
+ module Patches
3
+ module QueriesHelperPatch
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include InstanceMethods
8
+
9
+ alias_method :column_content_without_tags, :column_content
10
+ 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
+ end
15
+
16
+ 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
+ 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
30
+ additional_tag_links column.value(item),
31
+ tag_controller: 'issues',
32
+ use_colors: AdditionalTags.setting?(:use_colors)
33
+ else
34
+ column_content_without_tags column, item
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,32 @@
1
+ module AdditionalTags
2
+ module Patches
3
+ module ReportsControllerPatch
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ prepend InstanceOverwriteMethods
8
+ end
9
+
10
+ module InstanceOverwriteMethods
11
+ def issue_report
12
+ @tags = AdditionalTags::Tags.sort_tag_list Issue.available_tags(project_id: @project.id)
13
+ @issues_by_tags = Issue.by_tags @project, with_subprojects: Setting.display_subprojects_issues?
14
+ super
15
+ end
16
+
17
+ def issue_report_details
18
+ if params[:detail] == 'tag' &&
19
+ AdditionalTags.setting?(:active_issue_tags) &&
20
+ User.current.allowed_to?(:view_issue_tags, @project)
21
+ @field = 'tag_id'
22
+ @rows = AdditionalTags::Tags.sort_tag_list Issue.available_tags(project_id: @project.id)
23
+ @data = Issue.by_tags @project, with_subprojects: Setting.display_subprojects_issues?
24
+ @report_title = l :field_tags
25
+ else
26
+ super
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,12 @@
1
+ module AdditionalTags
2
+ module Patches
3
+ module SettingsControllerPatch
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ helper :additional_tags
8
+ helper :additional_tags_issues
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ module AdditionalTags
2
+ module Patches
3
+ module TimeEntryPatch
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include InstanceMethods
8
+ end
9
+
10
+ module InstanceMethods
11
+ def issue_tags
12
+ return [] if issue.nil?
13
+
14
+ issue.tags
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,53 @@
1
+ require_dependency 'time_entry_query'
2
+
3
+ module AdditionalTags
4
+ module Patches
5
+ module TimeEntryQueryPatch
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ include InstanceMethods
10
+
11
+ alias_method :initialize_available_filters_without_tags, :initialize_available_filters
12
+ alias_method :initialize_available_filters, :initialize_available_filters_with_tags
13
+
14
+ alias_method :available_columns_without_tags, :available_columns
15
+ alias_method :available_columns, :available_columns_with_tags
16
+ end
17
+
18
+ module InstanceMethods
19
+ def initialize_available_filters_with_tags
20
+ 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,
26
+ name: l('label_attribute_of_issue', name: l(:field_tags)),
27
+ values: -> { available_issue_tags_values }
28
+ end
29
+
30
+ def available_columns_with_tags
31
+ if @available_columns.nil?
32
+ @available_columns = available_columns_without_tags
33
+ if AdditionalTags.setting?(:active_issue_tags) && User.current.allowed_to?(:view_issue_tags, project, global: true)
34
+ @available_columns << QueryColumn.new(:issue_tags)
35
+ end
36
+ else
37
+ available_columns_without_tags
38
+ end
39
+ @available_columns
40
+ end
41
+
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
+ .map { |c| [c.name, c.name] }
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,46 @@
1
+ require_dependency 'query'
2
+
3
+ module AdditionalTags
4
+ module Patches
5
+ module TimeReportPatch
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ include InstanceMethods
10
+
11
+ alias_method :load_available_criteria_without_tags, :load_available_criteria
12
+ alias_method :load_available_criteria, :load_available_criteria_with_tags
13
+ end
14
+
15
+ module InstanceMethods
16
+ def load_available_criteria_with_tags
17
+ return @load_available_criteria_with_tags if @load_available_criteria_with_tags
18
+
19
+ @load_available_criteria_with_tags = load_available_criteria_without_tags
20
+ @load_available_criteria_with_tags['tags'] = { sql: "#{ActsAsTaggableOn.tags_table}.id",
21
+ klass: ActsAsTaggableOn::Tag,
22
+ joins: additional_tags_join,
23
+ label: :field_tags }
24
+ @load_available_criteria_with_tags
25
+ end
26
+
27
+ private
28
+
29
+ def additional_tags_join
30
+ time_entry_table = Arel::Table.new TimeEntry.table_name
31
+ issues_table = Arel::Table.new Issue.table_name, as: :issues_time_entries
32
+ taggings_table = Arel::Table.new ActsAsTaggableOn.taggings_table
33
+ tags_table = Arel::Table.new ActsAsTaggableOn.tags_table
34
+
35
+ time_entry_table.join(issues_table)
36
+ .on(issues_table[:id].eq(time_entry_table[:issue_id]))
37
+ .join(taggings_table)
38
+ .on(taggings_table[:taggable_id].eq(issues_table[:id]).and(taggings_table[:taggable_type].eq('Issue')))
39
+ .join(tags_table)
40
+ .on(tags_table[:id].eq(taggings_table[:tag_id]))
41
+ .join_sources
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end