additional_tags 1.0.0

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 (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,40 @@
1
+ class ActsAsTaggableMigration < ActiveRecord::Migration[5.2]
2
+ def up
3
+ create_table ActsAsTaggableOn.tags_table do |t|
4
+ t.string :name, index: { unique: true }
5
+ t.integer :taggings_count, default: 0
6
+ t.timestamps
7
+ end
8
+
9
+ create_table ActsAsTaggableOn.taggings_table do |t|
10
+ t.references :tag, foreign_key: { to_table: ActsAsTaggableOn.tags_table }, index: false
11
+
12
+ # You should make sure that the column created is
13
+ # long enough to store the required class names.
14
+ t.references :taggable, polymorphic: true
15
+ t.references :tagger, polymorphic: true
16
+
17
+ # Limit is created to prevent MySQL error on index
18
+ # length for MyISAM table type: http://bit.ly/vgW2Ql
19
+ t.string :context, limit: 128
20
+
21
+ t.datetime :created_at
22
+
23
+ t.index %i[tag_id taggable_id taggable_type context tagger_id tagger_type], unique: true, name: 'ataggings_idx'
24
+ t.index %i[taggable_id taggable_type context], name: 'ataggings_taggable_context_idx'
25
+ t.index :taggable_type
26
+ t.index :context
27
+ t.index %i[tagger_id tagger_type]
28
+ t.index %i[taggable_id taggable_type tagger_id context], name: 'ataggings_idy'
29
+ end
30
+
31
+ return unless ActsAsTaggableOn::Utils.using_mysql?
32
+
33
+ execute("ALTER TABLE #{ActsAsTaggableOn.tags_table} MODIFY name varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;")
34
+ end
35
+
36
+ def down
37
+ drop_table ActsAsTaggableOn.taggings_table
38
+ drop_table ActsAsTaggableOn.tags_table
39
+ end
40
+ end
@@ -0,0 +1,39 @@
1
+ class MigrateExistingTags < ActiveRecord::Migration[5.2]
2
+ def up
3
+ return unless table_exists?(MigrateTag.table_name) && table_exists?(MigrateTagging.table_name)
4
+
5
+ excluded_taggable_types = %w[Question Contact DriveEntry]
6
+
7
+ MigrateTag.all.each do |old_tag|
8
+ ActsAsTaggableOn::Tagging.transaction do
9
+ tag = ActsAsTaggableOn::Tag.find_by name: old_tag.name
10
+ cnt = 0
11
+ old_tag.migrate_taggings.each do |tagging|
12
+ next if excluded_taggable_types.include? tagging.taggable_type
13
+
14
+ tag = ActsAsTaggableOn::Tag.create!(name: old_tag.name) if cnt.zero? && tag.nil?
15
+ context = tagging.respond_to?('context') && tagging.context.present? ? tagging.context : 'tags'
16
+
17
+ # old data can include dups
18
+ next if ActsAsTaggableOn::Tagging.exists? tag_id: tag.id,
19
+ taggable_id: tagging.taggable_id,
20
+ taggable_type: tagging.taggable_type,
21
+ context: context
22
+
23
+ ActsAsTaggableOn::Tagging.create! tag_id: tag.id,
24
+ taggable_id: tagging.taggable_id,
25
+ taggable_type: tagging.taggable_type,
26
+ context: context,
27
+ created_at: tagging.created_at
28
+ cnt += 1
29
+ end
30
+
31
+ ActsAsTaggableOn::Tag.reset_counters(tag.id, :taggings) unless tag.nil?
32
+ end
33
+ end
34
+ end
35
+
36
+ def down
37
+ # to nothing
38
+ end
39
+ end
data/init.rb ADDED
@@ -0,0 +1,31 @@
1
+ Redmine::Plugin.register :additional_tags do
2
+ name 'Additional Tags'
3
+ author 'AlphaNodes GmbH'
4
+ description 'Redmine tagging support'
5
+ version AdditionalTags::VERSION
6
+ url 'https://github.com/alphanodes/additional_tags/'
7
+ author_url 'https://alphanodes.com/'
8
+ directory __dir__
9
+
10
+ requires_redmine version_or_higher: '4.1'
11
+
12
+ settings default: Additionals.load_settings('additional_tags'),
13
+ partial: 'additional_tags/settings/settings'
14
+
15
+ project_module :issue_tracking do
16
+ permission :create_issue_tags, {}
17
+ permission :edit_issue_tags, {}
18
+ permission :view_issue_tags, {}, read: true
19
+ end
20
+
21
+ project_module :wiki do
22
+ permission :add_wiki_tags, wiki: %i[update_tags]
23
+ end
24
+
25
+ menu :admin_menu,
26
+ :tags, { controller: 'settings', action: 'plugin', id: 'additional_tags' }, caption: :field_tags
27
+ end
28
+
29
+ Rails.configuration.to_prepare do
30
+ AdditionalTags.setup
31
+ end
@@ -0,0 +1,107 @@
1
+ require 'additional_tags/version'
2
+ require 'additional_tags/tags'
3
+
4
+ module AdditionalTags
5
+ TAG_TABLE_NAME = 'additional_tags'.freeze
6
+ TAGGING_TABLE_NAME = 'additional_taggings'.freeze
7
+
8
+ class << self
9
+ def setup
10
+ raise 'Please install additionals plugin (https://github.com/alphanodes/additionals)' unless Redmine::Plugin.installed? 'additionals'
11
+
12
+ Additionals.incompatible_plugins(%w[redmine_tags
13
+ redmine_tagging
14
+ redmineup_tags], 'additional_tags')
15
+
16
+ # Patches
17
+ AutoCompletesController.include AdditionalTags::Patches::AutoCompletesControllerPatch
18
+ CalendarsController.include AdditionalTags::Patches::CalendarsControllerPatch
19
+ DashboardsController.include AdditionalTags::Patches::DashboardsControllerPatch
20
+ DashboardAsyncBlocksController.include AdditionalTags::Patches::DashboardAsyncBlocksControllerPatch
21
+ GanttsController.include AdditionalTags::Patches::GanttsControllerPatch
22
+ MyController.include AdditionalTags::Patches::MyControllerPatch
23
+ Issue.include AdditionalTags::Patches::IssuePatch
24
+ Journal.include AdditionalTags::Patches::JournalPatch
25
+ IssuesController.include AdditionalTags::Patches::IssuesControllerPatch
26
+ ImportsController.include AdditionalTags::Patches::ImportsControllerPatch
27
+ QueriesHelper.include AdditionalTags::Patches::QueriesHelperPatch
28
+ ReportsController.include AdditionalTags::Patches::ReportsControllerPatch
29
+ SettingsController.include AdditionalTags::Patches::SettingsControllerPatch
30
+ Redmine::Helpers::TimeReport.include AdditionalTags::Patches::TimeReportPatch
31
+ TimeEntry.include AdditionalTags::Patches::TimeEntryPatch
32
+ TimelogController.include AdditionalTags::Patches::TimelogControllerPatch
33
+ WikiController.include AdditionalTags::Patches::WikiControllerPatch
34
+ WikiPage.include AdditionalTags::Patches::WikiPagePatch
35
+
36
+ # because of this bug: https://www.redmine.org/issues/33290
37
+ if Additionals.redmine_database_ready? TAG_TABLE_NAME
38
+ IssueQuery.include AdditionalTags::Patches::IssueQueryPatch
39
+ TimeEntryQuery.include AdditionalTags::Patches::TimeEntryQueryPatch
40
+
41
+ if Redmine::Plugin.installed? 'redmine_agile'
42
+ AgileQuery.include AdditionalTags::Patches::AgileQueryPatch
43
+ AgileBoardsController.include AdditionalTags::Patches::AgileBoardsControllerPatch
44
+ if AGILE_VERSION_TYPE == 'PRO version'
45
+ AgileVersionsController.include AdditionalTags::Patches::AgileVersionsControllerPatch
46
+ AgileVersionsQuery.include AdditionalTags::Patches::AgileVersionsQueryPatch
47
+ end
48
+ end
49
+ end
50
+
51
+ # Hooks
52
+ require_dependency 'additional_tags/hooks'
53
+ end
54
+
55
+ # support with default setting as fall back
56
+ def setting(value)
57
+ if settings.key? value
58
+ settings[value]
59
+ else
60
+ Additionals.load_settings('additional_tags')[value]
61
+ end
62
+ end
63
+
64
+ def setting?(value)
65
+ Additionals.true? settings[value]
66
+ end
67
+
68
+ def show_sidebar_tags?
69
+ setting(:tags_sidebar).present? && setting(:tags_sidebar) != 'none'
70
+ end
71
+
72
+ def sql_for_tags_field(klass, operator, value)
73
+ compare = operator.eql?('=') ? 'IN' : 'NOT IN'
74
+ ids_list = klass.tagged_with(value).map(&:id).push(0).join(',')
75
+ "(#{klass.table_name}.id #{compare} (#{ids_list})) "
76
+ end
77
+
78
+ private
79
+
80
+ def settings
81
+ Setting[:plugin_additional_tags]
82
+ end
83
+ end
84
+
85
+ # Run the classic redmine plugin initializer after rails boot
86
+ class Plugin < ::Rails::Engine
87
+ require 'acts-as-taggable-on'
88
+
89
+ ActsAsTaggableOn.tags_table = TAG_TABLE_NAME
90
+ ActsAsTaggableOn.taggings_table = TAGGING_TABLE_NAME
91
+
92
+ config.after_initialize do
93
+ # engine_name could be used (additional_tags_plugin), but can
94
+ # create some side effencts
95
+ plugin_id = 'additional_tags'
96
+
97
+ # if plugin is already in plugins directory, use this and leave here
98
+ next if Redmine::Plugin.installed? plugin_id
99
+
100
+ # gem is used as redmine plugin
101
+ require File.expand_path '../init', __dir__
102
+ AdditionalTags.setup
103
+ Additionals::Gemify.install_assets plugin_id
104
+ Additionals::Gemify.create_plugin_hint plugin_id
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,63 @@
1
+ module AdditionalTags
2
+ class AdditionalTagsHookListener < Redmine::Hook::ViewListener
3
+ render_on :view_issues_bulk_edit_details_bottom,
4
+ partial: 'issues/tags_form_details',
5
+ locals: { tags_form: 'issues/tags_bulk_edit' }
6
+ render_on :view_issues_context_menu_end, partial: 'context_menus/issues_tags'
7
+ render_on :view_issues_form_details_bottom,
8
+ partial: 'issues/tags_form_details',
9
+ locals: { tags_form: 'issues/tags_form' }
10
+ render_on :view_issues_show_details_bottom, partial: 'issues/tags'
11
+ render_on :view_issues_sidebar_planning_bottom, partial: 'issues/tags_sidebar'
12
+ render_on :view_layouts_base_html_head, partial: 'additional_tags/html_head'
13
+ render_on :view_reports_issue_report_split_content_right, partial: 'tags_simple'
14
+ render_on :view_wiki_form_bottom, partial: 'tags_form_bottom'
15
+ render_on :view_wiki_show_bottom, partial: 'tags_show'
16
+ render_on :view_wiki_show_sidebar_bottom, partial: 'tags_sidebar'
17
+
18
+ def controller_issues_edit_before_save(context = {})
19
+ tags_journal context[:issue], context[:params]
20
+ end
21
+
22
+ def controller_issues_bulk_edit_before_save(context = {})
23
+ tags_journal context[:issue], context[:params]
24
+ end
25
+
26
+ # this hook is missing in redmine core at the moment
27
+ def view_issue_pdf_fields(context = {})
28
+ issue = context[:issue]
29
+ right = context[:right]
30
+
31
+ if AdditionalTags.setting?(:active_issue_tags) &&
32
+ User.current.allowed_to?(:view_issue_tags, issue.project)
33
+ right << [l(:field_tag_list), issue.tag_list]
34
+ end
35
+ end
36
+
37
+ # this hook is missing in redmine core at the moment
38
+ def view_wiki_pdf_buttom(context = {})
39
+ page = context[:page]
40
+ pdf = context[:pdf]
41
+
42
+ return if page.tag_list.blank?
43
+
44
+ pdf.ln 5
45
+ pdf.SetFontStyle 'B', 9
46
+ pdf.RDMCell 190, 5, l(:field_tag_list), 'B'
47
+
48
+ pdf.ln
49
+ pdf.SetFontStyle '', 8
50
+ pdf.RDMCell 190, 5, page.tag_list.join(', ')
51
+ pdf.ln
52
+ end
53
+
54
+ private
55
+
56
+ def tags_journal(issue, params)
57
+ return unless params && params[:issue] && params[:issue][:tag_list]
58
+
59
+ issue.tags_to_journal Issue.find_by(id: issue.id)&.tag_list&.to_s,
60
+ issue.tag_list.to_s
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,14 @@
1
+ module AdditionalTags
2
+ module Patches
3
+ module AgileBoardsControllerPatch
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,41 @@
1
+ module AdditionalTags
2
+ module Patches
3
+ module AgileQueryPatch
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
+ add_available_column QueryColumn.new(:tags)
14
+ end
15
+
16
+ module InstanceMethods
17
+ def sql_for_tags_field(_field, operator, value)
18
+ case operator
19
+ when '=', '!'
20
+ issues = Issue.tagged_with(value.clone)
21
+ when '!*'
22
+ issues = Issue.joins(:tags).uniq
23
+ else
24
+ issues = Issue.tagged_with(ActsAsTaggableOn::Tag.all.map(&:to_s), any: true)
25
+ end
26
+
27
+ compare = operator.include?('!') ? 'NOT IN' : 'IN'
28
+ ids_list = issues.collect(&:id).push(0).join(',')
29
+ "( #{Issue.table_name}.id #{compare} (#{ids_list}) ) "
30
+ end
31
+
32
+ def initialize_available_filters_with_tags
33
+ initialize_available_filters_without_tags
34
+
35
+ initialize_tags_filter if !available_filters.key?('tags') &&
36
+ User.current.allowed_to?(:view_issue_tags, project, global: true)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,14 @@
1
+ module AdditionalTags
2
+ module Patches
3
+ module AgileVersionsControllerPatch
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,11 @@
1
+ module AdditionalTags
2
+ module Patches
3
+ module AgileVersionsQueryPatch
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ add_available_column QueryColumn.new(:tags)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,42 @@
1
+ module AdditionalTags
2
+ module Patches
3
+ module AutoCompletesControllerPatch
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include InstanceMethods
8
+ end
9
+
10
+ module InstanceMethods
11
+ def issue_tags
12
+ suggestion_order = AdditionalTags.setting(:tags_suggestion_order) || 'name'
13
+ @name = (params[:q] || params[:term]).to_s.strip
14
+ @tags = Issue.available_tags name_like: @name,
15
+ sort_by: suggestion_order,
16
+ order: (suggestion_order == 'name' ? 'ASC' : 'DESC')
17
+
18
+ @tags = AdditionalTags::Tags.sort_tag_list @tags if suggestion_order == 'name'
19
+
20
+ render layout: false, partial: 'additional_tag_list', locals: { unsorted: true }
21
+ end
22
+
23
+ def wiki_tags
24
+ @name = params[:q].to_s
25
+ @tags = WikiPage.available_tags project: @project, name_like: @name
26
+ render layout: false, partial: 'additional_tag_list', locals: { unsorted: true }
27
+ end
28
+
29
+ def all_tags
30
+ return render_403 unless User.current.admin?
31
+
32
+ @name = params[:q].to_s
33
+ sql_for_where = "LOWER(#{ActiveRecord::Base.connection.quote_table_name(ActsAsTaggableOn.tags_table)}.name) LIKE ?"
34
+ @tags = ActsAsTaggableOn::Tag.where(sql_for_where, "%#{@name.downcase}%")
35
+ .order(name: :asc)
36
+
37
+ render layout: false, partial: 'additional_tag_list', locals: { unsorted: true }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,14 @@
1
+ module AdditionalTags
2
+ module Patches
3
+ module CalendarsControllerPatch
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,11 @@
1
+ module AdditionalTags
2
+ module Patches
3
+ module DashboardAsyncBlocksControllerPatch
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ helper :additional_tags
8
+ end
9
+ end
10
+ end
11
+ end