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,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