├── Gemfile
├── lang
├── en.yml
└── ru.yml
├── config
├── configuration-travis.yml
├── routes.rb
├── database-postgresql-travis.yml
├── database-mysql-travis.yml
└── locales
│ ├── ja.yml
│ ├── nl.yml
│ ├── ru.yml
│ ├── zh.yml
│ └── en.yml
├── doc
├── tagging_1.PNG
├── tagging_2.PNG
├── tagging_3.PNG
├── tagging_4.PNG
├── tagging_5.PNG
├── tagging_6.PNG
├── tagging_7.PNG
├── tagging_8.PNG
└── tagging_9.PNG
├── app
├── views
│ ├── issue_tags
│ │ ├── _form.erb
│ │ └── edit.erb
│ ├── tagging
│ │ ├── _taglinks.erb
│ │ ├── _tagcloud.erb
│ │ ├── _tagcloud_search.erb
│ │ ├── _issue_tagcloud.erb
│ │ ├── _tagtab.erb
│ │ └── _settings.html.erb
│ ├── reports
│ │ └── _simple_tags.html.erb
│ └── issues
│ │ ├── index_with_tags.api.rsb
│ │ └── show_with_tags.api.rsb
├── models
│ ├── wiki_page_tag.rb
│ └── issue_tag.rb
├── controllers
│ └── issue_tags_controller.rb
└── helpers
│ └── tagging_helper.rb
├── lib
├── tagging_plugin
│ ├── context_helper.rb
│ ├── tags_helper.rb
│ ├── api_template_handler_patch.rb
│ ├── tagging_patches.rb
│ └── tagging_hooks.rb
├── tasks
│ ├── test.rake
│ └── reconfigure.rake
├── redmine_tagging.rb
└── redmine_tagging
│ └── patches
│ ├── project_patch.rb
│ ├── queries_helper_patch.rb
│ ├── application_controller_patch.rb
│ ├── wiki_page_patch.rb
│ ├── query_patch.rb
│ └── issue_patch.rb
├── db
└── migrate
│ ├── 20161031000008_fix_tags_with_backslash.rb
│ ├── 20161031000004_fix_tagging_contexts.rb
│ ├── 20161031000001_create_views.rb
│ ├── 20161031000002_scrub_body.rb
│ └── 20161031000006_fix_broken_issue_and_wiki_contexts.rb
├── assets
├── stylesheets
│ └── tagging.css
└── javascripts
│ ├── jquery.tagcloud.js
│ ├── toggle_tags.js
│ └── tag.js
├── test
├── functional
│ ├── gantts_controller_test.rb
│ ├── calendars_controller_test.rb
│ ├── issue_tags_controller_test.rb
│ └── issues_controller_test.rb
├── test_helper.rb
├── unit
│ └── redmine_tagging
│ │ └── project_test.rb
└── integration
│ └── tagging_test.rb
├── .travis.yml
├── init.rb
└── README.md
/Gemfile:
--------------------------------------------------------------------------------
1 | gem 'acts-as-taggable-on', '~> 4.0'
2 |
--------------------------------------------------------------------------------
/lang/en.yml:
--------------------------------------------------------------------------------
1 | # English strings go here
2 | my_label: "My label"
3 |
--------------------------------------------------------------------------------
/lang/ru.yml:
--------------------------------------------------------------------------------
1 | # Russian strings go here
2 | my_label: "Моя метка"
3 |
--------------------------------------------------------------------------------
/config/configuration-travis.yml:
--------------------------------------------------------------------------------
1 | default:
2 | email_delivery:
3 | delivery_method: :test
--------------------------------------------------------------------------------
/doc/tagging_1.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Restream/redmine_tagging/HEAD/doc/tagging_1.PNG
--------------------------------------------------------------------------------
/doc/tagging_2.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Restream/redmine_tagging/HEAD/doc/tagging_2.PNG
--------------------------------------------------------------------------------
/doc/tagging_3.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Restream/redmine_tagging/HEAD/doc/tagging_3.PNG
--------------------------------------------------------------------------------
/doc/tagging_4.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Restream/redmine_tagging/HEAD/doc/tagging_4.PNG
--------------------------------------------------------------------------------
/doc/tagging_5.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Restream/redmine_tagging/HEAD/doc/tagging_5.PNG
--------------------------------------------------------------------------------
/doc/tagging_6.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Restream/redmine_tagging/HEAD/doc/tagging_6.PNG
--------------------------------------------------------------------------------
/doc/tagging_7.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Restream/redmine_tagging/HEAD/doc/tagging_7.PNG
--------------------------------------------------------------------------------
/doc/tagging_8.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Restream/redmine_tagging/HEAD/doc/tagging_8.PNG
--------------------------------------------------------------------------------
/doc/tagging_9.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Restream/redmine_tagging/HEAD/doc/tagging_9.PNG
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | RedmineApp::Application.routes.draw do
2 | resources :issue_tags, only: [:destroy]
3 | end
4 |
--------------------------------------------------------------------------------
/app/views/issue_tags/_form.erb:
--------------------------------------------------------------------------------
1 | <%= error_messages_for 'tags' %>
2 |
3 |
4 |
<%= f.text_field :title, size: 30, required: true %>
5 |
6 |
--------------------------------------------------------------------------------
/config/database-postgresql-travis.yml:
--------------------------------------------------------------------------------
1 | # http://about.travis-ci.org/docs/user/database-setup/#PostgreSQL
2 | test:
3 | adapter: postgresql
4 | database: redmine
5 | username: postgres
6 |
--------------------------------------------------------------------------------
/config/database-mysql-travis.yml:
--------------------------------------------------------------------------------
1 | # http://about.travis-ci.org/docs/user/database-setup/#MySQL
2 | test:
3 | adapter: mysql2
4 | database: redmine
5 | username: travis
6 | encoding: utf8
7 |
--------------------------------------------------------------------------------
/app/views/tagging/_taglinks.erb:
--------------------------------------------------------------------------------
1 |
2 | | <%= l(:field_tags) %>: |
3 |
4 | <% tags.each do |tag| %>
5 | <%= link_to_project_tag_filter(@project, tag) %>
6 | <% end %>
7 | |
8 |
9 |
--------------------------------------------------------------------------------
/lib/tagging_plugin/context_helper.rb:
--------------------------------------------------------------------------------
1 | module TaggingPlugin
2 | module ContextHelper
3 | class << self
4 | def context_for(project)
5 | "context_" + project.identifier.gsub('-', '_')
6 | end
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/views/issue_tags/edit.erb:
--------------------------------------------------------------------------------
1 | <%=l(:label_issue_category)%>
2 |
3 | <% labelled_tabular_form_for :issue_tag, @tag, url: { action: 'update', id: @tag } do |f| %>
4 | <%= render partial: 'issue_tags/form', locals: { f: f } %>
5 | <%= submit_tag l(:button_save) %>
6 | <% end %>
7 |
--------------------------------------------------------------------------------
/config/locales/ja.yml:
--------------------------------------------------------------------------------
1 | # Japanese strings go here for Rails i18n
2 | ja:
3 | field_tags: "タグ"
4 | tagging_wiki_pages_inline: "Wikiページのタグをインラインで編集する"
5 | tagging_issues_inline: "チケットのタグをインラインで編集する"
6 | tagging_sidebar_tagcloud: "チケットのサイドバーでタグクラウドを表示する"
7 | tagging_dynamic_font_size: "タグの使用頻度に基づいてタグクラウドのフォントサイズを変更する"
8 |
--------------------------------------------------------------------------------
/db/migrate/20161031000008_fix_tags_with_backslash.rb:
--------------------------------------------------------------------------------
1 | class FixTagsWithBackslash < ActiveRecord::Migration
2 | def up
3 | ActsAsTaggableOn::Tag.where("name like ?", '%\\\%').find_each do |tag|
4 | tag.name = tag.name.gsub("\\", '/')
5 | tag.save
6 | end
7 | end
8 |
9 | def down
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/views/tagging/_tagcloud.erb:
--------------------------------------------------------------------------------
1 | <%= l(:field_tags) %>
2 |
3 |
4 | <%
5 | tag_cloud_in_project(@project) do |tag, factor|
6 | options = {:style => "font-size: #{10 * factor + 9}pt" }
7 | %>
8 | <%= link_to_project_tag_filter(@project, tag, {}, options) %>
9 | <% end %>
10 |
11 |
--------------------------------------------------------------------------------
/lib/tasks/test.rake:
--------------------------------------------------------------------------------
1 | namespace :redmine_tagging do
2 |
3 | desc 'Runs the plugins tests.'
4 | desc 'Runs the redmine_tagging plugin tests'
5 | Rake::TestTask.new :test do |t|
6 | t.libs << 'test'
7 | t.verbose = true
8 | t.warning = false
9 | t.pattern = 'plugins/redmine_tagging/test/**/*_test.rb'
10 | end
11 |
12 | end
--------------------------------------------------------------------------------
/config/locales/nl.yml:
--------------------------------------------------------------------------------
1 | # English strings go here for Rails i18n
2 | nl:
3 | field_tags: "Tags"
4 | tagging_wiki_pages_inline: "Tag bewerking in text van wiki paginas"
5 | tagging_issues_inline: "Tag bewerking in text van issues"
6 | tagging_sidebar_tagcloud: "Show tagcloud in issues sidebar"
7 | tagging_dynamic_font_size: "Font size in tag cloud based on tag use"
8 |
--------------------------------------------------------------------------------
/app/models/wiki_page_tag.rb:
--------------------------------------------------------------------------------
1 | class WikiPageTag < ActiveRecord::Base
2 | self.table_name = :wiki_page_tags
3 | self.primary_key = :id
4 |
5 | belongs_to :wiki_page
6 |
7 | def readonly?
8 | return true
9 | end
10 |
11 | # Prevent objects from being destroyed
12 | def before_destroy
13 | raise ActiveRecord::ReadOnlyRecord
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/config/locales/ru.yml:
--------------------------------------------------------------------------------
1 | # Russian strings go here for Rails i18n
2 | ru:
3 | field_tags: "Теги"
4 | tagging_wiki_pages_inline: "Использовать теги из текста страниц вики"
5 | tagging_issues_inline: "Использовать теги из описаний задач"
6 | detach: "Отделить"
7 | notice_successful_detached: "Тег успешно отвязан от всех тикетов проекта"
8 | tags_in_project: "Теги в проекте"
9 |
--------------------------------------------------------------------------------
/lib/redmine_tagging.rb:
--------------------------------------------------------------------------------
1 | module RedmineTagging
2 | end
3 |
4 | require 'redmine_tagging/patches/issue_patch'
5 | require 'redmine_tagging/patches/project_patch'
6 | require 'redmine_tagging/patches/query_patch'
7 | require 'redmine_tagging/patches/wiki_page_patch'
8 | require 'redmine_tagging/patches/queries_helper_patch'
9 | require 'redmine_tagging/patches/application_controller_patch'
10 |
--------------------------------------------------------------------------------
/app/views/tagging/_tagcloud_search.erb:
--------------------------------------------------------------------------------
1 | <%= l(:field_tags) %>
2 |
3 |
4 | <%
5 | tag_cloud_in_project(@project) do |tag, factor|
6 | options = {:style => "font-size: #{10 * factor + 9}pt" }
7 | %>
8 | <%= link_to(tag, {:controller => "search", :action => "index", :id => @project, :q => "\"#{tag_without_sharp(tag)}\"", :wiki_pages => true }, options) %>
9 | <% end %>
10 |
11 |
--------------------------------------------------------------------------------
/app/views/tagging/_issue_tagcloud.erb:
--------------------------------------------------------------------------------
1 | <%= t(:tags_in_project) %>
2 | —
3 | <%
4 | tag_cloud_in_project(@project) do |tag, factor|
5 | style = "font-size: #{10 * factor + 9}pt"
6 | %>
7 |
10 | <%= tag %>
11 |
12 | <% end %>
13 |
14 |
--------------------------------------------------------------------------------
/lib/tagging_plugin/tags_helper.rb:
--------------------------------------------------------------------------------
1 | module TaggingPlugin
2 | module TagsHelper
3 | class << self
4 | def from_string(tags_string)
5 | tags_string.split(/[#"'\s,\\]+/) \
6 | .select { |tag| tag.length > 0 } \
7 | .map { |tag| "##{tag}" } \
8 | .join(', ')
9 | end
10 |
11 | def to_string(tags)
12 | tags.sort_by { |t| t.downcase }.map{ |tag| tag.gsub(/^#/, '') }.join(' ')
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/config/locales/zh.yml:
--------------------------------------------------------------------------------
1 | # Simplified Chinese strings go here for Rails i18n
2 | # Author: Steven.W
3 | # Based on file: en.yml
4 |
5 | zh:
6 | field_tags: "标签"
7 | tagging_wiki_pages_inline: "在WIKI页中,内嵌标签编辑"
8 | tagging_issues_inline: "在问题中,内嵌标签编辑"
9 | tagging_sidebar_tagcloud: "在问题侧边栏内显示标签云"
10 | tagging_dynamic_font_size: "根据标签的使用情况,自动调整标签云中的标签字体大小"
11 | notice_successful_detached: "标签已拆分"
12 | tagging_tab_label: "标签"
13 | label_tagname: "标签名称"
14 | field_tag: "标签主题"
15 |
16 | tags: "标签"
17 | append_tags: "追加标签,而非重写标签"
18 | detach: "拆分"
--------------------------------------------------------------------------------
/db/migrate/20161031000004_fix_tagging_contexts.rb:
--------------------------------------------------------------------------------
1 | class FixTaggingContexts < ActiveRecord::Migration
2 |
3 | PREFIX = "context_"
4 |
5 | def up
6 | ActsAsTaggableOn::Tagging.where("context not like ?", PREFIX + "%").find_each do |tagging|
7 | tagging.context = PREFIX + tagging.context
8 | tagging.save
9 | end
10 | end
11 |
12 | def down
13 | ActsAsTaggableOn::Tagging.where("context like ?", PREFIX + "%").find_each do |tagging|
14 | tagging.context = tagging.context[PREFIX.length..-1]
15 | tagging.save
16 | end
17 | end
18 | end
19 |
20 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # English strings go here for Rails i18n
2 | en:
3 | field_tags: "Tags"
4 | tagging_wiki_pages_inline: "Inline editing of tags for wiki pages"
5 | tagging_issues_inline: "Inline editing of tags for issues"
6 | tagging_sidebar_tagcloud: "Show tagcloud in issues sidebar"
7 | tagging_dynamic_font_size: "Font size in tag cloud based on tag use"
8 | notice_successful_detached: "Tag successfully detached"
9 | tagging_tab_label: "Tags"
10 | label_tagname: "Tag name"
11 | field_tag: "Tag title"
12 |
13 | tags: "Tags"
14 | append_tags: "Append tags instead of rewrite"
15 | detach: "Detach"
16 | tags_in_project: "Project tags"
17 |
--------------------------------------------------------------------------------
/lib/redmine_tagging/patches/project_patch.rb:
--------------------------------------------------------------------------------
1 | require_dependency 'project'
2 |
3 | module RedmineTagging::Patches::ProjectPatch
4 | extend ActiveSupport::Concern
5 |
6 | def tags
7 | ActsAsTaggableOn::Tag.
8 | joins(:taggings).
9 | joins(<<-SQL
10 | inner join #{Issue.table_name} issues on
11 | issues.project_id = #{ActiveRecord::Base::sanitize(id)} and
12 | issues.id = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id
13 | SQL
14 | ).order(:name).uniq
15 | end
16 |
17 | end
18 |
19 | unless Project.included_modules.include? RedmineTagging::Patches::ProjectPatch
20 | Project.send :include, RedmineTagging::Patches::ProjectPatch
21 | end
22 |
--------------------------------------------------------------------------------
/lib/redmine_tagging/patches/queries_helper_patch.rb:
--------------------------------------------------------------------------------
1 | module RedmineTagging::Patches::QueriesHelperPatch
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | alias_method_chain :column_content, :tags
6 | end
7 |
8 | def column_content_with_tags(column, issue)
9 | value = column.value(issue)
10 |
11 | if array_of_issue_tags?(value)
12 | links = value.map do |issue_tag|
13 | link_to_project_tag_filter(@project, issue_tag.tag)
14 | end
15 | links.join(', ')
16 | else
17 | column_content_without_tags(column, issue)
18 | end
19 | end
20 |
21 | def array_of_issue_tags?(value)
22 | value.class.name == 'Array' && value.first.class.name == 'IssueTag'
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/assets/stylesheets/tagging.css:
--------------------------------------------------------------------------------
1 | .tagMatches {
2 | margin-left: 10px;
3 | }
4 |
5 | .tagMatches ._tag_suggestion {
6 | padding: 1px;
7 | margin-left: 1px;
8 | background-color: #0000AB;
9 | color: #fff;
10 | cursor: pointer;
11 | }
12 |
13 | #cloud_trigger, #cloud_content a {
14 | cursor: pointer;
15 | text-decoration: none;
16 | }
17 |
18 | #cloud_trigger {
19 | font-weight: bold;
20 | border-bottom: 1px dashed;
21 | }
22 |
23 | #cloud_content a {
24 | margin: 3px;
25 | line-height: normal;
26 | }
27 |
28 | #cloud_content .selected {
29 | background: #0000AB;
30 | color: #fff;
31 | }
32 |
33 | #issue_tags {
34 | width: 90%;
35 | }
36 |
37 | .filter select[name="v[tags][]"][multiple] {
38 | min-height: 200px;
39 | }
40 |
--------------------------------------------------------------------------------
/app/views/tagging/_tagtab.erb:
--------------------------------------------------------------------------------
1 | <%
2 | tags = @project.tags
3 | %>
4 |
5 | <% if tags.any? %>
6 |
7 |
8 | | <%= l(:label_tagname) %> |
9 | |
10 |
11 |
12 | <% for issue_tag in tags %>
13 |
14 | |
15 | <%= link_to_project_tag_filter(@project, issue_tag.name) %>
16 | |
17 |
18 | <%= link_to(l(:detach), {:controller => 'issue_tags', :action => 'destroy', :id => issue_tag.id,
19 | :project_id => @project.id },
20 | :method => :delete,
21 | :class => 'icon icon-del') %>
22 | |
23 |
24 | <% end %>
25 |
26 |
27 | <% else %>
28 | <%= l(:label_no_data) %>
29 | <% end %>
30 |
--------------------------------------------------------------------------------
/app/views/tagging/_settings.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= content_tag(:label, l(:tagging_wiki_pages_inline)) %>
3 | <%= check_box_tag("settings[wiki_pages_inline]", "1", Setting.plugin_redmine_tagging[:wiki_pages_inline] == "1") %>
4 |
5 |
6 | <%= content_tag(:label, l(:tagging_issues_inline)) %>
7 | <%= check_box_tag("settings[issues_inline]", "1", Setting.plugin_redmine_tagging[:issues_inline] == "1") %>
8 |
9 |
10 | <%= content_tag(:label, l(:tagging_sidebar_tagcloud)) %>
11 | <%= check_box_tag("settings[sidebar_tagcloud]", "1", Setting.plugin_redmine_tagging[:sidebar_tagcloud] == "1") %>
12 |
13 |
14 | <%= content_tag(:label, l(:tagging_dynamic_font_size)) %>
15 | <%= check_box_tag("settings[dynamic_font_size]", "1", Setting.plugin_redmine_tagging[:dynamic_font_size] == "1") %>
16 |
17 |
--------------------------------------------------------------------------------
/test/functional/gantts_controller_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/../test_helper'
2 |
3 | class GanttsControllerTest < ActionController::TestCase
4 | fixtures :projects,
5 | :users,
6 | :roles,
7 | :members,
8 | :member_roles,
9 | :trackers,
10 | :projects_trackers,
11 | :enabled_modules,
12 | :issue_statuses,
13 | :issues,
14 | :enumerations,
15 | :custom_fields,
16 | :custom_values,
17 | :custom_fields_trackers
18 |
19 | def setup
20 | @request.session[:user_id] = 2
21 |
22 | @some_tags = '#1, #2, #3,#4,#5'
23 |
24 | @project_with_tags = Project.find(1)
25 |
26 | @issue_with_tags = Issue.find(1)
27 | @issue_with_tags.tags_to_update = @some_tags
28 | @issue_with_tags.save!
29 | end
30 |
31 | def test_can_show_gantt
32 | get :show, project_id: @project_with_tags.id
33 | assert_response :success
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/test/functional/calendars_controller_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/../test_helper'
2 |
3 | class CalendarsControllerTest < ActionController::TestCase
4 | fixtures :projects,
5 | :users,
6 | :roles,
7 | :members,
8 | :member_roles,
9 | :trackers,
10 | :projects_trackers,
11 | :enabled_modules,
12 | :issue_statuses,
13 | :issues,
14 | :enumerations,
15 | :custom_fields,
16 | :custom_values,
17 | :custom_fields_trackers
18 |
19 | def setup
20 | @request.session[:user_id] = 2
21 |
22 | @some_tags = '#1, #2, #3,#4,#5'
23 |
24 | @project_with_tags = Project.find(1)
25 |
26 | @issue_with_tags = Issue.find(1)
27 | @issue_with_tags.tags_to_update = @some_tags
28 | @issue_with_tags.save!
29 | end
30 |
31 | def test_can_show_calendar
32 | get :show, project_id: @project_with_tags.id
33 | assert_response :success
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # Load the normal Rails helper
2 | require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper')
3 |
4 | class ActiveSupport::TestCase
5 | def log_user(login, password)
6 | User.anonymous
7 | get '/login'
8 | assert_equal nil, session[:user_id]
9 | assert_response :success
10 | assert_template 'account/login'
11 | post '/login', username: login, password: password
12 | assert_equal login, User.find(session[:user_id]).login
13 | end
14 |
15 | def setup_wiki_page_with_tags(test_tags)
16 | public_project = Project.find(1)
17 |
18 | Wiki.create!(project_id: public_project.id, start_page: 'test_page')
19 | public_project.reload
20 |
21 | public_project.wiki.pages << WikiPage.new(title: 'some_wiki_page')
22 | page = public_project.wiki.pages.first
23 | page.tags_to_update = test_tags
24 | page.content = WikiContent.new(text: 'content')
25 | page.save!
26 | page
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/db/migrate/20161031000001_create_views.rb:
--------------------------------------------------------------------------------
1 | class CreateViews < ActiveRecord::Migration
2 | def up
3 | execute 'DROP VIEW IF EXISTS issue_tags'
4 |
5 | execute <<-SQL
6 | CREATE VIEW issue_tags AS
7 | SELECT
8 | taggings.id AS id,
9 | tags.name AS tag,
10 | taggings.taggable_id AS issue_id
11 | FROM
12 | taggings
13 | JOIN tags ON taggings.tag_id = tags.id
14 | WHERE
15 | taggable_type = 'Issue'
16 | SQL
17 |
18 | execute 'DROP VIEW IF EXISTS wiki_page_tags'
19 |
20 | execute <<-SQL
21 | CREATE VIEW wiki_page_tags AS
22 | SELECT
23 | taggings.id,
24 | tags.name AS tag,
25 | taggings.taggable_id AS wiki_page_id
26 | FROM
27 | taggings
28 | JOIN tags ON taggings.tag_id = tags.id
29 | WHERE
30 | taggable_type = 'WikiPage'
31 | SQL
32 | end
33 |
34 | def down
35 | execute 'DROP VIEW issue_tags'
36 | execute 'DROP VIEW wiki_page_tags'
37 | end
38 | end
39 |
40 |
--------------------------------------------------------------------------------
/lib/redmine_tagging/patches/application_controller_patch.rb:
--------------------------------------------------------------------------------
1 | require_dependency 'application_controller'
2 |
3 | module RedmineTagging::Patches::ApplicationControllerPatch
4 | extend ActiveSupport::Concern
5 |
6 | included do
7 | before_filter :include_tagging_helper
8 | before_filter :patch_queries_helper
9 | end
10 |
11 | # A way to make plugin helpers available in all views
12 | def include_tagging_helper
13 | unless _helpers.included_modules.include? TaggingHelper
14 | self.class.helper TaggingHelper
15 | end
16 | true
17 | end
18 |
19 | def patch_queries_helper
20 | if _helpers.included_modules.include?(QueriesHelper) &&
21 | !_helpers.included_modules.include?(RedmineTagging::Patches::QueriesHelperPatch)
22 | _helpers.send :include, RedmineTagging::Patches::QueriesHelperPatch
23 | end
24 | true
25 | end
26 | end
27 |
28 | unless ApplicationController.included_modules.include? RedmineTagging::Patches::ApplicationControllerPatch
29 | ApplicationController.send :include, RedmineTagging::Patches::ApplicationControllerPatch
30 | end
31 |
--------------------------------------------------------------------------------
/app/controllers/issue_tags_controller.rb:
--------------------------------------------------------------------------------
1 | class IssueTagsController < ApplicationController
2 | unloadable
3 |
4 | model_object ActsAsTaggableOn::Tag
5 | before_filter :find_model_object
6 | before_filter :find_project_by_project_id
7 |
8 | def destroy
9 | tag = @object
10 |
11 | context = TaggingPlugin::ContextHelper.context_for(@project)
12 |
13 | tag.taggings.where(context: context).find_each do |tagging|
14 | if tagging.taggable_type = "Issue"
15 | affected_issue = Issue.find(tagging.taggable_id)
16 | affected_issue.init_journal(User.current)
17 | issue_tags = affected_issue.tag_list_on(context)
18 | affected_issue.tags_to_update = issue_tags.select { |t| t != tag.name }
19 | affected_issue.save
20 | else
21 | tagging.destroy
22 | end
23 | end
24 |
25 | tag.taggings.reload
26 |
27 | if tag.taggings.empty?
28 | tag.destroy
29 | end
30 |
31 | flash[:notice] = l(:notice_successful_detached)
32 | redirect_to controller: 'projects', action: 'settings', tab: 'tags', id: @project
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/test/unit/redmine_tagging/project_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/../../test_helper'
2 |
3 | class RedmineTagging::ProjectTest < ActiveSupport::TestCase
4 | fixtures :projects, :trackers, :issue_statuses, :issues,
5 | :enumerations, :users, :issue_categories,
6 | :projects_trackers,
7 | :roles,
8 | :member_roles,
9 | :members
10 |
11 | def test_find_all_tags_for_project
12 | project1 = Project.find(1)
13 |
14 | issue = project1.issues.find(1)
15 | issue.tags_to_update = '4, 2'
16 | issue.save!
17 |
18 | issue = project1.issues.find(2)
19 | issue.tags_to_update = '3, 1'
20 | issue.save!
21 |
22 | project2 = Project.find(3)
23 |
24 | issue = project2.issues.find(5)
25 | issue.tags_to_update = '8, 6'
26 | issue.save!
27 |
28 | issue = project2.issues.find(13)
29 | issue.tags_to_update = '7, 5'
30 | issue.save!
31 |
32 | tags1 = project1.tags.map(&:name).join(',')
33 | tags2 = project2.tags.map(&:name).join(',')
34 |
35 | assert_equal '1,2,3,4', tags1
36 | assert_equal '5,6,7,8', tags2
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/db/migrate/20161031000002_scrub_body.rb:
--------------------------------------------------------------------------------
1 | class ScrubBody < ActiveRecord::Migration
2 | def up
3 | Issue.where("description like '%{{tag(%'").each {|issue|
4 | issue.description = issue.description.gsub(/[{]{2}tag[(][^)]*[)][}]{2}/i, '')
5 | issue.save!
6 | }
7 |
8 | WikiContent.where("text like '%{{tag(%'").each {|content|
9 | content.text = content.text.gsub(/[{]{2}tag[(][^)]*[)][}]{2}/i, '')
10 | content.save!
11 | }
12 | WikiContent::Version.where("data like '%{{tag(%'").each {|content|
13 | content.data = content.data.gsub(/[{]{2}tag[(][^)]*[)][}]{2}/i, '')
14 | content.save!
15 | }
16 | end
17 |
18 | contexts = Project.where(nil).collect{|p| p.identifier}
19 | contexts << contexts.collect{|c| c.gsub('-', '_')}
20 | contexts.flatten!
21 |
22 | ActsAsTaggableOn::Tag.where(
23 | "not name like '#%' and id in (select tag_id from taggings where taggable_type in ('WikiPage', 'Issue') and context in (?))",
24 | contexts
25 | ).each do |tag|
26 | tag.name = "##{tag.name}"
27 | tag.save
28 | end
29 |
30 | def down
31 | # noop
32 | end
33 | end
34 |
35 |
--------------------------------------------------------------------------------
/app/models/issue_tag.rb:
--------------------------------------------------------------------------------
1 | class IssueTag < ActiveRecord::Base
2 | self.table_name = :issue_tags
3 | self.primary_key = :id
4 |
5 | belongs_to :issue
6 |
7 | def readonly?
8 | return true
9 | end
10 |
11 | def title
12 | tag.gsub(/^#/, '')
13 | end
14 | alias_method :name, :title
15 |
16 | def project
17 | # self.issue.project
18 | Issue.find(issue_id).project
19 | end
20 |
21 | # Prevent objects from being destroyed
22 | def before_destroy
23 | raise ActiveRecord::ReadOnlyRecord
24 | end
25 |
26 | def self.by_issue_status(project = nil)
27 | project_condition = project.nil? ? '' : "and i.project_id=#{project.id}"
28 |
29 | ActiveRecord::Base.connection.select_all(
30 | "select
31 | s.id as status_id,
32 | s.is_closed as closed,
33 | t.id as tag,
34 | count(i.id) as total
35 | from
36 | #{Issue.table_name} i, #{IssueStatus.table_name} s,
37 | #{ActsAsTaggableOn::Tagging.table_name} ti, #{ActsAsTaggableOn::Tag.table_name} t
38 | where
39 | i.status_id=s.id
40 | and ti.taggable_id=i.id
41 | and ti.tag_id=t.id
42 | #{project_condition}
43 | group
44 | by s.id, s.is_closed, t.id")
45 | end
46 |
47 | def to_s
48 | name
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/lib/tagging_plugin/api_template_handler_patch.rb:
--------------------------------------------------------------------------------
1 | module TaggingPlugin
2 | module ApiTemplateHandlerPatch
3 | class << self
4 | def included(base)
5 | base.send(:extend, ClassMethods)
6 | base.class_eval do
7 | class << self
8 | alias_method_chain :call, :api_replacement
9 | end
10 | end
11 | end
12 | end
13 |
14 | module ClassMethods
15 | def call_with_api_replacement(template)
16 | template = replace_if(template, /views\/issues\/index.api.rsb$/, 'app/views/issues/index_with_tags.api.rsb')
17 | template = replace_if(template, /views\/issues\/show.api.rsb$/, 'app/views/issues/show_with_tags.api.rsb')
18 | call_without_api_replacement(template)
19 | end
20 |
21 | def replace_if(template, regexp, new_path)
22 | if template.identifier =~ regexp
23 | new_template_filename = File.join(__dir__, '../..', new_path)
24 | source = File.open(new_template_filename).read
25 | identifier = template.identifier
26 | handler = template.handler
27 | template = ActionView::Template.new(source, identifier, handler, {})
28 | end
29 | template
30 | end
31 | end
32 | end
33 | end
34 |
35 | Redmine::Views::ApiTemplateHandler.send(:include, TaggingPlugin::ApiTemplateHandlerPatch)
36 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 |
3 | services:
4 | - mysql
5 | - postgresql
6 |
7 | rvm:
8 | - 2.3.1
9 |
10 | gemfile:
11 | - $REDMINE_PATH/Gemfile
12 |
13 | env:
14 | - REDMINE_VER=3.1.7 DB=mysql
15 | - REDMINE_VER=3.2.4 DB=mysql
16 | - REDMINE_VER=3.3.1 DB=mysql
17 | - REDMINE_VER=3.1.7 DB=postgresql
18 | - REDMINE_VER=3.2.4 DB=postgresql
19 | - REDMINE_VER=3.3.1 DB=postgresql
20 |
21 | before_install:
22 | - export PLUGIN_NAME=redmine_tagging
23 | - export REDMINE_PATH=$HOME/redmine
24 | - svn co http://svn.redmine.org/redmine/tags/$REDMINE_VER $REDMINE_PATH
25 | - ln -s $TRAVIS_BUILD_DIR $REDMINE_PATH/plugins/$PLUGIN_NAME
26 | - cp config/database-$DB-travis.yml $REDMINE_PATH/config/database.yml
27 | - cp config/configuration-travis.yml $REDMINE_PATH/config/configuration.yml
28 | - echo "config.active_record.schema_format = :sql" > $REDMINE_PATH/config/additional_environment.rb
29 | - cd $REDMINE_PATH
30 |
31 | before_script:
32 | - export RAILS_ENV=test
33 | - bundle exec rake db:create
34 | - bundle exec rake db:migrate
35 | - bundle exec rake acts_as_taggable_on_engine:install:migrations
36 | - bundle exec rake db:migrate
37 | - bundle exec rake redmine:plugins:migrate
38 | - bundle exec rake db:structure:dump
39 |
40 | script:
41 | - export RUBYOPT="-W0"
42 | - bundle exec rake redmine:plugins:test
43 |
44 |
--------------------------------------------------------------------------------
/app/views/reports/_simple_tags.html.erb:
--------------------------------------------------------------------------------
1 | <% if @statuses.empty? or rows.empty? %>
2 | <%=l(:label_no_data)%>
3 | <% else %>
4 |
5 |
6 | |
7 | <%=l(:label_open_issues_plural)%> |
8 | <%=l(:label_closed_issues_plural)%> |
9 | <%=l(:label_total)%> |
10 |
11 |
12 | <% for tag in rows %>
13 | ">
14 | |
15 | <%= link_to_project_tag_filter(@project, tag.name) %>
16 | |
17 |
18 | <%= link_to_project_tag_filter(
19 | @project,
20 | tag.name,
21 | :status => "o",
22 | :title => aggregate(data, { field_name => tag.id, "closed" => 0 }))
23 | %>
24 | |
25 |
26 | <%= link_to_project_tag_filter(
27 | @project,
28 | tag.name,
29 | :status => "c",
30 | :title => aggregate(data, { field_name => tag.id, "closed" => 1 }))
31 | %>
32 | |
33 |
34 | <%= link_to_project_tag_filter(
35 | @project,
36 | tag.name,
37 | :status => "*",
38 | :title => aggregate(data, { field_name => tag.id }))
39 | %>
40 | |
41 |
42 | <% end %>
43 |
44 |
45 | <% end
46 | reset_cycle %>
47 |
--------------------------------------------------------------------------------
/lib/redmine_tagging/patches/wiki_page_patch.rb:
--------------------------------------------------------------------------------
1 | require_dependency 'wiki_page'
2 |
3 | module RedmineTagging::Patches::WikiPagePatch
4 | extend ActiveSupport::Concern
5 |
6 | included do
7 | unloadable
8 |
9 | attr_writer :tags_to_update
10 |
11 | has_many :wiki_page_tags
12 |
13 | acts_as_taggable
14 |
15 | before_save :update_tags
16 |
17 | if Redmine::VERSION::MAJOR < 3
18 | searchable_options[:columns] << "#{WikiPageTag.table_name}.tag"
19 | searchable_options[:include] ||= []
20 | searchable_options[:include] << :wiki_page_tags
21 | else
22 | searchable_options[:columns] << "#{WikiPageTag.table_name}.tag"
23 |
24 | original_scope = searchable_options[:scope] || self
25 |
26 | searchable_options[:scope] = ->(*args) {
27 | (original_scope.respond_to?(:call) ?
28 | original_scope.call(*args) :
29 | original_scope
30 | ).includes :wiki_page_tags
31 | }
32 | end
33 | end
34 |
35 | private
36 |
37 | def update_tags
38 | if @tags_to_update
39 | project_context = TaggingPlugin::ContextHelper.context_for(project)
40 | set_tag_list_on(project_context, @tags_to_update)
41 | end
42 |
43 | true
44 | end
45 |
46 | end
47 |
48 | unless WikiPage.included_modules.include? RedmineTagging::Patches::WikiPagePatch
49 | WikiPage.send :include, RedmineTagging::Patches::WikiPagePatch
50 | end
51 |
--------------------------------------------------------------------------------
/db/migrate/20161031000006_fix_broken_issue_and_wiki_contexts.rb:
--------------------------------------------------------------------------------
1 | class FixBrokenIssueAndWikiContexts < ActiveRecord::Migration
2 | def self.remove_tagging(tagging)
3 | tag = tagging.tag
4 | tagging.destroy
5 | tag.destroy unless tag.taggings.any?
6 | end
7 |
8 | def self.up
9 |
10 | ActsAsTaggableOn::Tagging.where("taggable_type = ?", "Issue").find_each do |tagging|
11 | if tagged_issue = Issue.find_by_id(tagging.taggable_id)
12 | context_should_be = TaggingPlugin::ContextHelper.context_for(tagged_issue.project)
13 | if tagging.context != context_should_be
14 | tagging.context = context_should_be
15 | tagging.save
16 | end
17 | else
18 | remove_tagging(tagging)
19 | end
20 | end
21 |
22 | ActsAsTaggableOn::Tagging.where("taggable_type = ?", "WikiPage").find_each do |tagging|
23 | if tagged_page = WikiPage.find_by_id(tagging.taggable_id)
24 | project = tagged_page.wiki.project
25 | context_should_be = TaggingPlugin::ContextHelper.context_for(project)
26 | if tagging.context != context_should_be
27 | tagging.context = context_should_be
28 | tagging.save
29 | end
30 | else
31 | remove_tagging(tagging)
32 | end
33 | end
34 | end
35 |
36 | def self.down
37 | end
38 |
39 | def up
40 | self.class.up
41 | end
42 |
43 | def down
44 | self.class.down
45 | end
46 | end
47 |
48 |
--------------------------------------------------------------------------------
/lib/tagging_plugin/tagging_patches.rb:
--------------------------------------------------------------------------------
1 | module TaggingPlugin
2 |
3 | module ProjectsHelperPatch
4 | def self.included(base) # :nodoc:
5 | base.send(:include, InstanceMethods)
6 |
7 | base.class_eval do
8 | alias_method_chain :project_settings_tabs, :tags_tab
9 | end
10 | end
11 |
12 | module InstanceMethods
13 | def project_settings_tabs_with_tags_tab
14 | tabs = project_settings_tabs_without_tags_tab
15 | tabs << { name: 'tags', partial: 'tagging/tagtab', label: :tagging_tab_label }
16 | return tabs
17 | end
18 | end
19 | end
20 |
21 | module WikiControllerPatch
22 | def self.included(base) # :nodoc:
23 | base.send(:include, InstanceMethods)
24 |
25 | base.class_eval do
26 | unloadable
27 |
28 | alias_method_chain :update, :tags
29 | end
30 | end
31 |
32 | module InstanceMethods
33 | def update_with_tags
34 | if params[:wiki_page]
35 | if tags = params[:wiki_page][:tags]
36 | tags = TagsHelper.from_string(tags)
37 | @page.tags_to_update = tags
38 | end
39 | end
40 | update_without_tags
41 | end
42 | end
43 | end
44 | end
45 |
46 | WikiController.send(:include, TaggingPlugin::WikiControllerPatch) unless WikiController.included_modules.include? TaggingPlugin::WikiControllerPatch
47 | ProjectsHelper.send(:include, TaggingPlugin::ProjectsHelperPatch) unless ProjectsHelper.included_modules.include? TaggingPlugin::ProjectsHelperPatch
48 |
--------------------------------------------------------------------------------
/test/functional/issue_tags_controller_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/../test_helper'
2 |
3 | class IssueTagsControllerTest < ActionController::TestCase
4 | fixtures :projects,
5 | :users,
6 | :roles,
7 | :members,
8 | :member_roles,
9 | :trackers,
10 | :projects_trackers,
11 | :enabled_modules,
12 | :issue_statuses,
13 | :issues,
14 | :enumerations,
15 | :custom_fields,
16 | :custom_values,
17 | :custom_fields_trackers
18 |
19 | def setup
20 | @request.session[:user_id] = 2
21 | @some_tags = '#1, #2, #3,#4,#5'
22 |
23 | @project_with_tags = Project.find(1)
24 | @another_project = Project.find(2)
25 |
26 | @issue_with_tags = Issue.find(1)
27 | @issue_with_tags.tags_to_update = @some_tags
28 | @issue_with_tags.save!
29 | end
30 |
31 | def test_should_destroy_issue_tag
32 | @tag_id = ActsAsTaggableOn::Tag.find_by_name('#4').id
33 |
34 | delete 'destroy', project_id: @project_with_tags.id, id: @tag_id
35 | assert_response :redirect
36 |
37 | tag_rem = ActsAsTaggableOn::Tag.count
38 | assert_equal 4, tag_rem
39 | end
40 |
41 | def test_should_destroy_issue_tag_in_case_of_changed_project
42 | @issue_with_tags.project_id = @another_project.id
43 | @issue_with_tags.save!
44 | @issue_with_tags.reload
45 |
46 | @tag_id = ActsAsTaggableOn::Tag.find_by_name('#4').id
47 | delete 'destroy', project_id: @another_project.id, id: @tag_id
48 | assert_response :redirect
49 |
50 | tag_rem = ActsAsTaggableOn::Tag.count
51 | assert_equal 4, tag_rem
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/app/helpers/tagging_helper.rb:
--------------------------------------------------------------------------------
1 | module TaggingHelper
2 | def link_to_project_tag_filter(project, tag, options = {}, html_options = {})
3 | options.reverse_merge!({
4 | status: 'o',
5 | title: tag
6 | })
7 |
8 | opts = {
9 | 'set_filter' => 1,
10 | 'f' => ['tags', 'status_id'],
11 | 'op[tags]' => '=',
12 | 'op[status_id]' => options[:status],
13 | 'v[tags][]' => tag_without_sharp(tag),
14 | 'v[status_id][]' => 1
15 | }
16 |
17 | if project
18 | link_to(options[:title], project_issues_path(project, opts), html_options)
19 | else
20 | link_to(options[:title], issues_path(opts), html_options)
21 | end
22 | end
23 |
24 | def tag_without_sharp(tag)
25 | tag.to_s.gsub /^\s*#/, ''
26 | end
27 |
28 | def tag_cloud_in_project(project, &each_tag)
29 | tags = {}
30 |
31 | if project
32 | context = TaggingPlugin::ContextHelper.context_for(project)
33 |
34 | Issue.tag_counts_on(context).each do |tag|
35 | tags[tag.name] = tag.count
36 | end
37 |
38 | WikiPage.tag_counts_on(context).each do |tag|
39 | tags[tag.name] = tags[tag.name].to_i + tag.count
40 | end
41 | else
42 | Issue.all_tag_counts.each do |tag|
43 | tags[tag.name] = tag.count
44 | end
45 |
46 | WikiPage.all_tag_counts.each do |tag|
47 | tags[tag.name] = tags[tag.name].to_i + tag.count
48 | end
49 | end
50 |
51 | tags = tags.reject {|key,value| value == 0 }
52 |
53 | if tags.size > 0
54 | min_max = tags.values.minmax
55 | distance = min_max[1] - min_max[0]
56 |
57 | dynamic_fonts_enabled = (Setting.plugin_redmine_tagging[:dynamic_font_size] == "1")
58 |
59 | tags.keys.sort_by { |t| t.downcase }.each do |tag|
60 | if dynamic_fonts_enabled && (distance != 0)
61 | count = tags[tag]
62 | factor = (count - min_max[0]).to_f / distance
63 | else
64 | factor = 0.0
65 | end
66 |
67 | each_tag.call(tag, factor)
68 | end
69 | end
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/app/views/issues/index_with_tags.api.rsb:
--------------------------------------------------------------------------------
1 | api.array :issues, api_meta(:total_count => @issue_count, :offset => @offset, :limit => @limit) do
2 | @issues.each do |issue|
3 | api.issue do
4 | api.id issue.id
5 | api.project(:id => issue.project_id, :name => issue.project.name) unless issue.project.nil?
6 | api.tracker(:id => issue.tracker_id, :name => issue.tracker.name) unless issue.tracker.nil?
7 | api.status(:id => issue.status_id, :name => issue.status.name) unless issue.status.nil?
8 | api.priority(:id => issue.priority_id, :name => issue.priority.name) unless issue.priority.nil?
9 | api.author(:id => issue.author_id, :name => issue.author.name) unless issue.author.nil?
10 | api.assigned_to(:id => issue.assigned_to_id, :name => issue.assigned_to.name) unless issue.assigned_to.nil?
11 | api.category(:id => issue.category_id, :name => issue.category.name) unless issue.category.nil?
12 | api.fixed_version(:id => issue.fixed_version_id, :name => issue.fixed_version.name) unless issue.fixed_version.nil?
13 | api.parent(:id => issue.parent_id) unless issue.parent.nil?
14 |
15 | api.subject issue.subject
16 | api.description issue.description
17 | api.start_date issue.start_date
18 | api.due_date issue.due_date
19 | api.done_ratio issue.done_ratio
20 | api.estimated_hours issue.estimated_hours
21 |
22 | render_api_custom_values issue.custom_field_values, api
23 |
24 | api.created_on issue.created_on
25 | api.updated_on issue.updated_on
26 | api.closed_on issue.closed_on
27 |
28 | api.array :tags do
29 | issue.issue_tags.each do |issue_tag|
30 | api.tag(:id => issue_tag.tag[1..-1])
31 | end
32 | end
33 |
34 | api.array :relations do
35 | issue.relations.each do |relation|
36 | api.relation(:id => relation.id, :issue_id => relation.issue_from_id, :issue_to_id => relation.issue_to_id, :relation_type => relation.relation_type, :delay => relation.delay)
37 | end
38 | end if include_in_api_response?('relations')
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/assets/javascripts/jquery.tagcloud.js:
--------------------------------------------------------------------------------
1 | (function($) {
2 |
3 | $.fn.tagcloud = function(options) {
4 | var opts = $.extend({}, $.fn.tagcloud.defaults, options);
5 | tagWeights = this.map(function(){
6 | return $(this).attr("rel");
7 | })
8 | tagWeights = jQuery.makeArray(tagWeights).sort(compareWeights);
9 | lowest = tagWeights[0];
10 | highest = tagWeights.pop();
11 | range = highest - lowest;
12 | if(range == 0) {range = 1};
13 | // Sizes
14 | if (opts.size) {
15 | fontIncr = (opts.size.end - opts.size.start)/range;
16 | }
17 | // Colors
18 | if (opts.color) {
19 | colorIncr = colorIncrement (opts.color, range);
20 | }
21 | return this.each(function() {
22 | weighting = $(this).attr("rel") - lowest;
23 | if (opts.size) {
24 | $(this).css({"font-size": opts.size.start + (weighting * fontIncr) + opts.size.unit});
25 | }
26 | if (opts.color) {
27 | $(this).css({"color": tagColor(opts.color, colorIncr, weighting)});
28 | }
29 | })
30 | }
31 |
32 | $.fn.tagcloud.defaults = {
33 | size: {start: 14, end: 18, unit: "pt"}
34 | };
35 |
36 | // Converts hex to an RGB array
37 | function toRGB (code) {
38 | if (code.length == 4) {
39 | code = jQuery.map(/\w+/.exec(code), function(el) {return el + el }).join("");
40 | }
41 | hex = /(\w{2})(\w{2})(\w{2})/.exec(code);
42 | return [parseInt("0x" + hex[1]), parseInt("0x" + hex[2]), parseInt("0x" + hex[3])];
43 | }
44 |
45 | // Converts an RGB array to hex
46 | function toHex (ary) {
47 | return "#" + jQuery.map(ary, function(i) {
48 | hex = i.toString(16);
49 | hex = (hex.length == 1) ? "0" + hex : hex;
50 | return hex;
51 | }).join("");
52 | }
53 |
54 | function colorIncrement (color, range) {
55 | return jQuery.map(toRGB(color.end), function(n, i) {
56 | return (n - toRGB(color.start)[i])/range;
57 | });
58 | }
59 |
60 | function tagColor (color, increment, weighting) {
61 | rgb = jQuery.map(toRGB(color.start), function(n, i) {
62 | ref = Math.round(n + (increment[i] * weighting));
63 | if (ref > 255) {
64 | ref = 255;
65 | } else {
66 | if (ref < 0) {
67 | ref = 0;
68 | }
69 | }
70 | return ref;
71 | });
72 | return toHex(rgb);
73 | }
74 |
75 | function compareWeights(a, b)
76 | {
77 | return a - b;
78 | }
79 |
80 |
81 | })(jQuery);
82 |
--------------------------------------------------------------------------------
/lib/tasks/reconfigure.rake:
--------------------------------------------------------------------------------
1 | namespace :redmine do
2 | namespace :tagging do
3 | desc "Reconfigure for inline/separate tag editing"
4 | task :reconfigure => :environment do
5 |
6 | if Setting.plugin_redmine_tagging[:issues_inline] == "1"
7 | puts "Adding inline tags to issues"
8 |
9 | Issue.find_each do |issue|
10 | tag_context = TaggingPlugin::ContextHelper.context_for(issue.project)
11 | tags = issue.tag_list_on(tag_context) \
12 | .map {|tag| tag.gsub(/^#/, '') } \
13 | .sort_by { |t| t.downcase } \
14 | .join(', ')
15 |
16 | next if tags.blank? && issue.description.blank?
17 |
18 | tags = "{{tag(#{tags})}}"
19 |
20 | issue.description = '' if issue.description.blank?
21 | issue.description = issue.description.gsub(/[{]{2}tag[(][^)]*[)][}]{2}/i, tags)
22 | issue.description += "\n\n#{tags}" unless issue.description =~ /[{]{2}tag[(][^)]*[)][}]{2}/i
23 |
24 | issue.save!
25 | end
26 | else
27 | puts "Removing inline tags from issues"
28 | Issue.where("description like '%{{tag(%'").each {|issue|
29 | next if issue.description.blank?
30 |
31 | issue.description = issue.description.gsub(/[{]{2}tag[(][^)]*[)][}]{2}/i, '')
32 | issue.save!
33 | }
34 | end
35 |
36 | if Setting.plugin_redmine_tagging[:wiki_pages_inline] == "1"
37 | puts "Adding inline tags to wikis"
38 |
39 | WikiContent.find(:all).each {|content|
40 | tag_context = TaggingPlugin::ContextHelper.context_for(content.page.wiki.project)
41 | tags = content.page.tag_list_on(tag_context) \
42 | .map { |tag| tag.gsub(/^#/, '') } \
43 | .sort_by { |t| t.downcase } \
44 | .join(', ')
45 |
46 | next if tags.blank? && content.text.blank?
47 |
48 | tags = "{{tag(#{tags})}}"
49 |
50 | content.text = '' if content.text.blank?
51 | content.text = content.text.gsub(/[{]{2}tag[(][^)]*[)][}]{2}/i, tags)
52 | content.text += "\n\n#{tags}" unless content.text =~ /[{]{2}tag[(][^)]*[)][}]{2}/i
53 |
54 | content.save!
55 | }
56 | else
57 | puts "Removing inline tags from wikis"
58 |
59 | WikiContent.where("text like '%{{tag(%'").each {|content|
60 | next if content.text.blank?
61 |
62 | content.text = content.text.gsub(/[{]{2}tag[(][^)]*[)][}]{2}/i, '')
63 | content.save!
64 | }
65 | end
66 |
67 | end
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/assets/javascripts/toggle_tags.js:
--------------------------------------------------------------------------------
1 | // IE compatibility
2 | if (!Array.prototype.indexOf) {
3 | Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) {
4 | "use strict"
5 | if (this == null) {
6 | throw new TypeError()
7 | }
8 | var t = Object(this)
9 | var len = t.length >>> 0
10 | if (len === 0) {
11 | return -1
12 | }
13 | var n = 0
14 | if (arguments.length > 1) {
15 | n = Number(arguments[1]);
16 | if (n != n) { // shortcut for verifying if it's NaN
17 | n = 0
18 | } else if (n != 0 && n != Infinity && n != -Infinity) {
19 | n = (n > 0 || -1) * Math.floor(Math.abs(n));
20 | }
21 | }
22 | if (n >= len) {
23 | return -1
24 | }
25 | var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0);
26 | for (; k < len; k++) {
27 | if (k in t && t[k] === searchElement) {
28 | return k
29 | }
30 | }
31 | return -1
32 | }
33 | }
34 |
35 | (function ($) {
36 | $.fn.toggleCloudViaFor = function(cloud_trigger, tag_container) {
37 | var tag_cloud = $(this)
38 |
39 | cloud_trigger.click(function(e) {
40 | tag_cloud.toggle()
41 | e.preventDefault()
42 | })
43 |
44 | var tag_items = tag_cloud.children()
45 |
46 | $(tag_items).click(function(e) {
47 | var tag_value = $(this).attr("data-tag")
48 | $(this).toggleTagFor(tag_value, tag_container)
49 |
50 | show_selected_tags_from(tag_container, tag_cloud)
51 |
52 | e.preventDefault()
53 | })
54 |
55 | tag_cloud.toggle(false)
56 | show_selected_tags_from(tag_container, tag_cloud)
57 |
58 | tag_container.keyup(function(e) {
59 | show_selected_tags_from(tag_container, tag_cloud)
60 | })
61 |
62 | tag_container.change(function(e) {
63 | show_selected_tags_from(tag_container, tag_cloud)
64 | })
65 | }
66 |
67 | function show_selected_tags_from(tag_container, tag_cloud) {
68 | var current_tags = tags_to_array(tag_container.attr("value"))
69 | update_selected_tags(current_tags, tag_cloud)
70 | }
71 |
72 | function update_selected_tags(selected_tags, tag_cloud) {
73 | tag_cloud.children().each(function(index, tag_child) {
74 | var tag_value = $(tag_child).attr("data-tag")
75 | if(selected_tags.indexOf(tag_value.toLowerCase()) == -1)
76 | $(tag_child).removeClass("selected")
77 | else {
78 | $(tag_child).addClass("selected")
79 | }
80 | })
81 | }
82 |
83 | $.fn.toggleTagFor = function(tag_name, tag_container) {
84 | var content_tags = tags_to_array(tag_container.attr("value"))
85 | var this_tag_index = content_tags.indexOf(tag_name.toLowerCase())
86 |
87 | if(this_tag_index == -1) {
88 | content_tags.push(tag_name)
89 | } else {
90 | content_tags.splice(this_tag_index, 1)
91 | }
92 |
93 | tag_container.attr("value", content_tags.join(" "))
94 | }
95 |
96 | function tags_to_array(tags) {
97 | if (!tags) tags = '';
98 | dirty_items = tags.split(/[,#\s]+/)
99 |
100 | dirty_items = $.map(dirty_items, function(val, i){
101 | return val.toLowerCase()
102 | })
103 |
104 | return $.grep(dirty_items, function(val, i){
105 | return (val.length > 0)
106 | })
107 | }
108 | })(jQuery)
109 |
--------------------------------------------------------------------------------
/lib/redmine_tagging/patches/query_patch.rb:
--------------------------------------------------------------------------------
1 | require_dependency 'query'
2 |
3 | module RedmineTagging::Patches::QueryPatch
4 | extend ActiveSupport::Concern
5 |
6 | included do
7 | unloadable # Send unloadable so it will not be unloaded in development
8 |
9 | alias_method_chain :available_filters, :tags
10 | alias_method_chain :sql_for_field, :tags
11 |
12 | tag_query_column = QueryColumn.new(:issue_tags, :caption => :field_tags)
13 | add_available_column(tag_query_column)
14 | end
15 |
16 | def available_filters_with_tags
17 | unless @available_tag_filter
18 | @available_filters = available_filters_without_tags
19 | @available_tag_filter = available_tags_filter
20 | @available_filters.merge!(@available_tag_filter)
21 | end
22 | @available_filters
23 | end
24 |
25 | def available_tags_filter
26 | if project.nil?
27 | tags = ActsAsTaggableOn::Tag.where(
28 | "id in (select tag_id from taggings where taggable_type = 'Issue')"
29 | )
30 | else
31 | context = TaggingPlugin::ContextHelper.context_for(project)
32 | tags = ActsAsTaggableOn::Tag.where(
33 | "id in (select tag_id from taggings where taggable_type = 'Issue' and context = ?)",
34 | context
35 | )
36 | end
37 | tags = tags.sort_by { |t| t.name.downcase }.map do |tag|
38 | [tag_without_sharp(tag), tag_without_sharp(tag)]
39 | end
40 | field = 'tags'
41 | options = {
42 | type: :list_optional,
43 | values: tags,
44 | name: l(:field_tags),
45 | order: 21,
46 | }
47 | filter = ActiveSupport::OrderedHash.new
48 | filter[field] = QueryFilter.new(field, options)
49 | filter
50 | end
51 |
52 | def sql_for_field_with_tags(field, operator, v, db_table, db_field, is_custom_filter = false)
53 | if field == 'tags'
54 | tagging_sql(field, operator)
55 | else
56 | sql_for_field_without_tags(field, operator, v, db_table, db_field, is_custom_filter)
57 | end
58 | end
59 |
60 | def tagging_sql(field, operator)
61 | case operator
62 | when '!*'
63 | tagging_sql_none
64 | when '*'
65 | tagging_sql_any
66 | when '!'
67 | tagging_sql_not_equal(field)
68 | else
69 | tagging_sql_equal(field)
70 | end
71 | end
72 |
73 | def tagging_sql_none
74 | "(#{Issue.table_name}.id NOT IN (select taggable_id from taggings where taggable_type='Issue'))"
75 | end
76 |
77 | def tagging_sql_any
78 | "(#{Issue.table_name}.id IN (select taggable_id from taggings where taggable_type='Issue'))"
79 | end
80 |
81 | def tagging_sql_not_equal(field)
82 | sql = tagging_sql_equal(field)
83 | "(not #{sql})"
84 | end
85 |
86 | def tagging_sql_equal(field)
87 | selected_values = values_for(field).map { |tag| tag_with_sharp(tag).downcase }
88 | sql = selected_values.collect { |val| "'#{ActiveRecord::Base.connection.quote_string(val.gsub('\'', ''))}'" }.join(',')
89 | "(#{Issue.table_name}.id in (select taggable_id from taggings join tags on tags.id = taggings.tag_id where taggable_type='Issue' and lower(tags.name) in (#{sql})))"
90 | end
91 |
92 | def tag_without_sharp(tag)
93 | tag.to_s.gsub /^\s*#/, ''
94 | end
95 |
96 | def tag_with_sharp(tag)
97 | '#' + tag_without_sharp(tag)
98 | end
99 | end
100 |
101 | unless Query.included_modules.include? RedmineTagging::Patches::QueryPatch
102 | Query.send :include, RedmineTagging::Patches::QueryPatch
103 | end
104 |
--------------------------------------------------------------------------------
/init.rb:
--------------------------------------------------------------------------------
1 | require 'redmine'
2 |
3 | Redmine::Plugin.register :redmine_tagging do
4 | name 'Redmine Tagging Plugin'
5 | author 'Restream'
6 | description 'This plugin adds tagging features to Redmine.'
7 | version '0.1.6'
8 |
9 | settings default: {
10 | dynamic_font_size: '1',
11 | sidebar_tagcloud: '1',
12 | wiki_pages_inline: '0',
13 | issues_inline: '0'
14 | },
15 | partial: 'tagging/settings'
16 |
17 | Redmine::WikiFormatting::Macros.register do
18 | desc 'Wiki/Issues tagcloud'
19 | macro :tagcloud do |obj, args|
20 | args, options = extract_macro_options(args, :parent)
21 |
22 | return if params[:controller] == 'mailer'
23 |
24 | if obj
25 | if obj.is_a? WikiContent
26 | project = obj.page.wiki.project
27 | else
28 | project = obj.project
29 | end
30 | else
31 | project = Project.visible.where(identifier: params[:project_id]).first
32 | end
33 |
34 | if project # this may be an attempt to render tag cloud when deleting wiki page
35 | if [WikiContent, WikiContent::Version, NilClass].include?(obj.class)
36 | render partial: 'tagging/tagcloud_search', project: project
37 | elsif [Journal, Issue].include?(obj.class)
38 | render partial: 'tagging/tagcloud', project: project
39 | end
40 | end
41 | end
42 | end
43 |
44 | Redmine::WikiFormatting::Macros.register do
45 | desc 'Wiki/Issues tag'
46 | macro :tag do |obj, args|
47 | if obj.is_a?(WikiContent) && Setting.plugin_redmine_tagging[:wiki_pages_inline] == '1'
48 | inline = true
49 | elsif obj.is_a?(Issue) && Setting.plugin_redmine_tagging[:issues_inline] == '1'
50 | inline = true
51 | else
52 | inline = false
53 | end
54 |
55 | if inline
56 | args, options = extract_macro_options(args, :parent)
57 | tags = args.collect{|a| a.split(/[#"'\s,]+/)}.flatten.select{|tag| !tag.blank?}.collect{|tag| "##{tag}" }.uniq
58 | tags.sort_by! { |t| t.downcase }
59 |
60 | if obj.is_a? WikiContent
61 | obj = obj.page
62 | project = obj.wiki.project
63 | else
64 | project = obj.project
65 | end
66 |
67 | context = TaggingPlugin::ContextHelper.context_for(project)
68 | tags_present = obj.tag_list_on(context).sort_by { |t| t.downcase }.join(',')
69 | new_tags = tags.join(',')
70 | if tags_present != new_tags
71 | obj.tags_to_update = new_tags
72 | obj.save
73 | end
74 |
75 | taglinks = tags.collect do |tag|
76 | search_url = {
77 | controller: 'search',
78 | action: 'index',
79 | id: project,
80 | q: "\"#{tag}\""
81 | }
82 |
83 | search_url.merge!(obj.is_a?(WikiPage) ? { wiki_pages: true, issues: false } : { wiki_pages: false, issues: true })
84 | link_to(tag, search_url)
85 | end.join(' ')
86 |
87 | raw("#{taglinks}
")
88 | else
89 | ''
90 | end
91 | end
92 | end
93 | end
94 |
95 | ActionDispatch::Callbacks.to_prepare do
96 | require 'tagging_plugin/tagging_patches'
97 | require 'tagging_plugin/api_template_handler_patch'
98 | require 'redmine_tagging'
99 | require File.expand_path('../app/helpers/tagging_helper', __FILE__)
100 | ActionView::Base.send :include, TaggingHelper
101 | end
102 |
103 | require_dependency 'tagging_plugin/tagging_hooks'
104 |
--------------------------------------------------------------------------------
/app/views/issues/show_with_tags.api.rsb:
--------------------------------------------------------------------------------
1 | api.issue do
2 | api.id @issue.id
3 | api.project(:id => @issue.project_id, :name => @issue.project.name) unless @issue.project.nil?
4 | api.tracker(:id => @issue.tracker_id, :name => @issue.tracker.name) unless @issue.tracker.nil?
5 | api.status(:id => @issue.status_id, :name => @issue.status.name) unless @issue.status.nil?
6 | api.priority(:id => @issue.priority_id, :name => @issue.priority.name) unless @issue.priority.nil?
7 | api.author(:id => @issue.author_id, :name => @issue.author.name) unless @issue.author.nil?
8 | api.assigned_to(:id => @issue.assigned_to_id, :name => @issue.assigned_to.name) unless @issue.assigned_to.nil?
9 | api.category(:id => @issue.category_id, :name => @issue.category.name) unless @issue.category.nil?
10 | api.fixed_version(:id => @issue.fixed_version_id, :name => @issue.fixed_version.name) unless @issue.fixed_version.nil?
11 | api.parent(:id => @issue.parent_id) unless @issue.parent.nil?
12 |
13 | api.subject @issue.subject
14 | api.description @issue.description
15 | api.start_date @issue.start_date
16 | api.due_date @issue.due_date
17 | api.done_ratio @issue.done_ratio
18 | api.estimated_hours @issue.estimated_hours
19 | api.spent_hours(@issue.spent_hours) if User.current.allowed_to?(:view_time_entries, @project)
20 |
21 | render_api_custom_values @issue.custom_field_values, api
22 |
23 | api.created_on @issue.created_on
24 | api.updated_on @issue.updated_on
25 | api.closed_on @issue.closed_on
26 |
27 | api.array :tags do
28 | @issue.issue_tags.each do |issue_tag|
29 | api.tag(:id => issue_tag.tag[1..-1])
30 | end
31 | end
32 |
33 | render_api_issue_children(@issue, api) if include_in_api_response?('children')
34 |
35 | api.array :attachments do
36 | @issue.attachments.each do |attachment|
37 | render_api_attachment(attachment, api)
38 | end
39 | end if include_in_api_response?('attachments')
40 |
41 | api.array :relations do
42 | @relations.each do |relation|
43 | api.relation(:id => relation.id, :issue_id => relation.issue_from_id, :issue_to_id => relation.issue_to_id, :relation_type => relation.relation_type, :delay => relation.delay)
44 | end
45 | end if include_in_api_response?('relations') && @relations.present?
46 |
47 | api.array :changesets do
48 | @issue.changesets.each do |changeset|
49 | api.changeset :revision => changeset.revision do
50 | api.user(:id => changeset.user_id, :name => changeset.user.name) unless changeset.user.nil?
51 | api.comments changeset.comments
52 | api.committed_on changeset.committed_on
53 | end
54 | end
55 | end if include_in_api_response?('changesets') && User.current.allowed_to?(:view_changesets, @project)
56 |
57 | api.array :journals do
58 | @journals.each do |journal|
59 | api.journal :id => journal.id do
60 | api.user(:id => journal.user_id, :name => journal.user.name) unless journal.user.nil?
61 | api.notes journal.notes
62 | api.created_on journal.created_on
63 | api.array :details do
64 | journal.details.each do |detail|
65 | api.detail :property => detail.property, :name => detail.prop_key do
66 | api.old_value detail.old_value
67 | api.new_value detail.value
68 | end
69 | end
70 | end
71 | end
72 | end
73 | end if include_in_api_response?('journals')
74 |
75 | api.array :watchers do
76 | @issue.watcher_users.each do |user|
77 | api.user :id => user.id, :name => user.name
78 | end
79 | end if include_in_api_response?('watchers') && User.current.allowed_to?(:view_issue_watchers, @issue.project)
80 | end
81 |
--------------------------------------------------------------------------------
/lib/redmine_tagging/patches/issue_patch.rb:
--------------------------------------------------------------------------------
1 | require_dependency 'issue'
2 |
3 | module RedmineTagging::Patches::IssuePatch
4 | extend ActiveSupport::Concern
5 |
6 | included do
7 | unloadable
8 |
9 | attr_writer :tags_to_update
10 |
11 | before_save :update_tags
12 | acts_as_taggable
13 |
14 | after_save :cleanup_tags
15 |
16 | has_many :issue_tags
17 |
18 | alias_method_chain :create_journal, :tags
19 | alias_method_chain :init_journal, :tags
20 | alias_method_chain :copy_from, :tags
21 |
22 | if Redmine::VERSION::MAJOR < 3
23 | searchable_options[:columns] << "#{IssueTag.table_name}.tag"
24 | searchable_options[:include] ||= []
25 | searchable_options[:include] << :issue_tags
26 | else
27 | searchable_options[:columns] << "#{IssueTag.table_name}.tag"
28 |
29 | original_scope = searchable_options[:scope] || self
30 |
31 | searchable_options[:scope] = ->(*args) {
32 | (original_scope.respond_to?(:call) ?
33 | original_scope.call(*args) :
34 | original_scope
35 | ).includes :issue_tags
36 | }
37 | end
38 | end
39 |
40 | def create_journal_with_tags
41 | if @current_journal
42 | tag_context = TaggingPlugin::ContextHelper.context_for(project)
43 | before = @issue_tags_before_change
44 | after = TaggingPlugin::TagsHelper.to_string(tag_list_on(tag_context))
45 | unless before == after
46 | @current_journal.details << JournalDetail.new(
47 | property: 'attr',
48 | prop_key: 'tags',
49 | old_value: before,
50 | value: after)
51 | end
52 | end
53 | create_journal_without_tags
54 | end
55 |
56 | def init_journal_with_tags(user, notes = "")
57 | unless project.nil?
58 | tag_context = TaggingPlugin::ContextHelper.context_for(project)
59 | @issue_tags_before_change = TaggingPlugin::TagsHelper.to_string(tag_list_on(tag_context))
60 | end
61 | init_journal_without_tags(user, notes)
62 | end
63 |
64 | def tags
65 | issue_tags.map(&:to_s).join(' ')
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_ctx = issue.tag_list_ctx
72 | self
73 | end
74 |
75 | def tag_list_ctx
76 | tag_context = TaggingPlugin::ContextHelper.context_for(project)
77 | tag_list_on(tag_context)
78 | end
79 |
80 | def tag_list_ctx=(new_list)
81 | tag_context = TaggingPlugin::ContextHelper.context_for(project)
82 | set_tag_list_on(tag_context, new_list)
83 | end
84 |
85 | private
86 |
87 | def update_tags
88 | project_context = TaggingPlugin::ContextHelper.context_for(project)
89 |
90 | # Fix context if project changed
91 | if project_id_changed? && !new_record?
92 | @new_project_id = project_id
93 |
94 | taggings.update_all(context: project_context)
95 | end
96 |
97 | if @tags_to_update
98 | set_tag_list_on(project_context, @tags_to_update)
99 | end
100 |
101 | true
102 | end
103 |
104 | def cleanup_tags
105 | if @new_project_id
106 | context = TaggingPlugin::ContextHelper.context_for(project)
107 | ActsAsTaggableOn::Tagging.where(
108 | 'context != ? AND taggable_id = ? AND taggable_type = ?', context, id, 'Issue'
109 | ).delete_all
110 | end
111 | true
112 | end
113 | end
114 |
115 | unless Issue.included_modules.include? RedmineTagging::Patches::IssuePatch
116 | Issue.send :include, RedmineTagging::Patches::IssuePatch
117 | end
118 |
--------------------------------------------------------------------------------
/test/functional/issues_controller_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/../test_helper'
2 |
3 | class IssuesControllerTest < ActionController::TestCase
4 | fixtures :projects,
5 | :users,
6 | :roles,
7 | :members,
8 | :member_roles,
9 | :trackers,
10 | :projects_trackers,
11 | :enabled_modules,
12 | :issue_statuses,
13 | :issues,
14 | :enumerations,
15 | :custom_fields,
16 | :custom_values,
17 | :custom_fields_trackers
18 |
19 | def setup
20 | @request.session[:user_id] = 2
21 |
22 | @some_tags = '#1, #2, #3,#4,#5'
23 |
24 | @project_with_tags = Project.find(1)
25 | @another_project = Project.find(2)
26 |
27 | @issue_with_tags = Issue.find(1)
28 | @issue_with_tags.tags_to_update = @some_tags
29 | @issue_with_tags.save!
30 |
31 | @issue_without_tags = Issue.find(2)
32 |
33 | @issues_to_bulk_edit = [@issue_with_tags, @issue_without_tags]
34 | end
35 |
36 | def test_can_index_issues_when_custom_fields_available
37 | IssueCustomField.create!(
38 | name: 'cfield',
39 | default_value: 'ok',
40 | is_filter: true,
41 | field_format: 'string',
42 | is_for_all: true
43 | )
44 |
45 | get :index
46 | assert_response :success
47 | end
48 |
49 | def test_can_index_api
50 | get :index, format: 'json'
51 | assert_response :success
52 | end
53 |
54 | def test_can_show_api
55 | get :show, id: @issue_with_tags.id, format: 'xml'
56 | assert_response :success
57 | end
58 |
59 | def test_index_api_rsb_should_not_raise_in_project_issues
60 | get :index, project_id: @project_with_tags.id
61 | end
62 |
63 | def test_can_show_api_for_some_project
64 | get :index, format: 'json', project_id: @project_with_tags.id
65 | assert_response :success
66 | end
67 |
68 | def test_bulk_update_with_project_change_should_success
69 | tag_input = '"1 2 \\\\ 3 cool/tag 777 '
70 |
71 | put :bulk_update, ids: @issues_to_bulk_edit.map(&:id),
72 | issue: { project_id: @another_project.id, tags: tag_input },
73 | append_tags: 'on'
74 |
75 | assert_response :redirect
76 |
77 | @issue_with_tags.reload
78 | assert_equal @another_project.id, @issue_with_tags.project_id, 'Project should be changed'
79 |
80 | another_project_context = TaggingPlugin::ContextHelper.context_for(@another_project)
81 | tags = @issue_with_tags.tags_on(another_project_context)
82 | assert_equal %w(#1 #2 #3 #4 #5 #777 #cool/tag), tags.map(&:name).sort
83 | end
84 |
85 | def test_bulk_update_without_project_change_should_success
86 | tag_input = '"1 2 \\\\ 3 cool/tag 777 '
87 | put :bulk_update, { ids: @issues_to_bulk_edit.map(&:id), issue: { tags: tag_input }, 'append_tags' => 'on' }
88 | assert_response :redirect
89 |
90 | @issue_with_tags.reload
91 |
92 | project_context = TaggingPlugin::ContextHelper.context_for(@project_with_tags)
93 | tags = @issue_with_tags.tags_on(project_context)
94 | assert_equal %w(#1 #2 #3 #4 #5 #777 #cool/tag), tags.map(&:name).sort
95 | end
96 |
97 | def test_bulk_update_without_tags_field_should_not_drop_tags
98 | put :bulk_update, { :ids => @issues_to_bulk_edit.map(&:id), issue: { status_id: 1 } }
99 | assert_response :redirect
100 |
101 | @issue_with_tags.reload
102 |
103 | project_context = TaggingPlugin::ContextHelper.context_for(@project_with_tags)
104 | tags = @issue_with_tags.tags_on(project_context)
105 | assert_equal %w(#1 #2 #3 #4 #5), tags.map(&:name).sort
106 | end
107 |
108 | def test_update_without_tags_field_should_not_drop_tags
109 | put :update, id: @issue_with_tags.id, issue: { status_id: 1 }
110 | assert_response :redirect
111 |
112 | @issue_with_tags.reload
113 |
114 | project_context = TaggingPlugin::ContextHelper.context_for(@project_with_tags)
115 | tags = @issue_with_tags.tags_on(project_context)
116 | assert_equal %w(#1 #2 #3 #4 #5), tags.map(&:name).sort
117 | end
118 | end
119 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Redmine Tagging Plugin
2 |
3 | [](https://travis-ci.org/Restream/redmine_tagging)
4 | [](https://codeclimate.com/github/Restream/redmine_tagging)
5 |
6 | This plugin adds useful tagging features to Redmine:
7 |
8 | * Tag cloud in the sidebar
9 | * Tag suggestion and autocomplete
10 | * Redmine search integration (possibility to search for #tag to find wikis/issues)
11 | * Issue filters based on tags
12 | * Batch assignment and detachment of tags
13 | * Logging tag changes
14 |
15 | The initial authors of the plugin are [Emiliano Heyns](mailto:emiliano.heyns@gmail.com) and [Vladimir Kiselev](https://github.com/nettsundere).
16 |
17 | ## Installation
18 |
19 | *These installation instructions are based on Redmine 2.6.0. For instructions for previous versions, see [Redmine wiki](http://www.redmine.org/projects/redmine/wiki/Plugins).*
20 |
21 | 1. To install the plugin
22 | * Download the .ZIP archive, extract files and copy the plugin directory into #{REDMINE_ROOT}/plugins.
23 |
24 | Or
25 |
26 | * Change you current directory to your Redmine root directory:
27 |
28 | cd {REDMINE_ROOT}
29 |
30 | Copy the plugin from GitHub using the following commands:
31 |
32 | git clone https://github.com/Restream/redmine_tagging.git plugins/redmine_tagging
33 |
34 | 2. Update the Gemfile.lock file by running the following commands:
35 |
36 | bundle install
37 |
38 | 3. Run the migrations generator to create tables for tags and associations:
39 |
40 | bundle exec rake acts_as_taggable_on_engine:install:migrations RAILS_ENV=production
41 |
42 | 4. Run the following commands to upgrade your database (make a database backup before):
43 |
44 | bundle exec rake db:migrate RAILS_ENV=production
45 | bundle exec rake redmine:plugins:migrate RAILS_ENV=production
46 |
47 | 5. Restart Redmine.
48 |
49 | Now you should be able to see the plugin in **Administration > Plugins**.
50 |
51 | ### For MySql users
52 | You can circumvent at any time the problem of special characters [issue 623](https://github.com/mbleigh/acts-as-taggable-on/issues/623) by setting in an initializer file:
53 |
54 | ```ruby
55 | ActsAsTaggableOn.force_binary_collation = true
56 | ```
57 |
58 | Or by running this rake task:
59 |
60 | ```shell
61 | bundle exec rake acts_as_taggable_on_engine:tag_names:collate_bin
62 | ```
63 |
64 | See the [configuration](https://github.com/mbleigh/acts-as-taggable-on#configuration) section in acts-as-taggable-on gem for more details.
65 |
66 | ## Usage
67 |
68 | The plugin enables you to add tags to wiki and issue pages using either the **Tags** field or inline tags. To switch between these two modes, you should enable or disable the corresponding check boxes in the plugin settings.
69 |
70 | To switch to inline tag editing, go to **Administration > Plugins**, click **Configure**, select the corresponding check boxes and click **Apply**.
71 | 
72 |
73 | After changing the settings, run the following command:
74 |
75 | bundle exec rake redmine:tagging:reconfigure RAILS_ENV=production
76 |
77 | Failure to do so will result in loss of data (tags) when switching to another tagging mode.
78 |
79 | Inline tags can be added using the following syntax:
80 |
81 | {{tag(tag_name)}}
82 |
83 | 
84 |
85 | Note that inline tags are saved when the object body is rendered. That's why if you want to remove all tags from an object, you must first add `{{tag}}` to its body to actually clear the tags. After that you can remove `{{tag}}` from the object body.
86 |
87 | Adding `{{tagcloud}}` will generate a tag cloud, which will be displayed in the sidebar.
88 | 
89 |
90 | The most often used tags are displayed in a larger font.
91 |
92 | By default, inline tag editing is disabled. In this mode, you can type tags into the **Tags** field to add them to an issue or wiki page. You can use spaces or commas as tag separators.
93 | 
94 |
95 | You can click the **Project tags** link below the **Tags** field to view all the project tags and select the required ones.
96 | 
97 |
98 | The autocomplete feature will suggest the available tags as you start typing the tag name in the **Tags** field.
99 | 
100 |
101 | All tags added to project issues are displayed on the **Tags** tab of the project settings. To detach a tag from an issue, click the **Detach** link.
102 | 
103 |
104 | Tags can be used to search for issues and create issue filters:
105 | 
106 | 
107 |
108 | ## Maintainers
109 |
110 | Danil Tashkinov, [github.com/nodecarter](https://github.com/nodecarter)
111 |
112 | ## Thanks to
113 |
114 | * https://github.com/jkraemer
115 |
--------------------------------------------------------------------------------
/test/integration/tagging_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/../test_helper'
2 |
3 | class TaggingTest < ActionDispatch::IntegrationTest
4 | fixtures :projects,
5 | :users,
6 | :roles,
7 | :members,
8 | :member_roles,
9 | :trackers,
10 | :projects_trackers,
11 | :enabled_modules,
12 | :issue_statuses,
13 | :issues,
14 | :enumerations,
15 | :custom_fields,
16 | :custom_values,
17 | :custom_fields_trackers
18 |
19 | def setup
20 | Mailer.stubs(:deliver_mail).returns(true)
21 |
22 | log_user('admin', 'admin')
23 | User.any_instance.stubs(:allowed_to?).returns(true)
24 |
25 | @some_tags = '#1, #2, #3,#4,#5'
26 |
27 | @project_with_tags = Project.find(1)
28 | @another_project = Project.find(2)
29 |
30 | @issue_with_tags = Issue.find(1)
31 | @issue_with_tags.tags_to_update = @some_tags
32 | @issue_with_tags.save!
33 |
34 | @issue_without_tags = Issue.find(2)
35 |
36 | @wiki_page_with_tags = setup_wiki_page_with_tags(@some_tags)
37 | @wiki_page_with_tags_content = @wiki_page_with_tags.content
38 |
39 | @project_with_wiki_tags = @wiki_page_with_tags.project
40 |
41 | @another_project_context = TaggingPlugin::ContextHelper.context_for(@another_project)
42 | @project_with_tags_context = TaggingPlugin::ContextHelper.context_for(@project_with_tags)
43 |
44 | @project_with_wiki_tags_context = TaggingPlugin::ContextHelper.context_for(@project_with_wiki_tags)
45 | end
46 |
47 | def test_should_create_issue_tags_from_input
48 | Setting.plugin_redmine_tagging[:issues_inline] = '0'
49 |
50 | @new_issue_attrs = {
51 | 'project_id' => @project_with_tags.id,
52 | 'priority_id' => @issue_with_tags.priority_id,
53 | 'subject' => 'new_issue',
54 | 'tags' => '10 11 12'
55 | }
56 |
57 | get_via_redirect(new_project_issue_path(@project_with_tags))
58 | assert_response :success
59 | post_via_redirect(issues_path, issue: @new_issue_attrs)
60 | assert_response :success
61 |
62 | new_issue = Issue.find_by_subject('new_issue')
63 | assert_equal 3, new_issue.taggings.size
64 | assert_equal [@project_with_tags_context], new_issue.taggings.map(&:context).uniq
65 | end
66 |
67 | def test_should_update_issue_tags_from_input
68 | Setting.plugin_redmine_tagging[:issues_inline] = '0'
69 |
70 | issue_attrs = @issue_with_tags.attributes
71 |
72 | issue_attrs['project_id'] = @another_project.id
73 | issue_attrs['tracker'] = @another_project.trackers.first
74 | issue_attrs['tags'] = '10 11 12'
75 |
76 | get_via_redirect(edit_issue_path(@issue_with_tags))
77 | assert_response :success
78 | put_via_redirect(issue_path(@issue_with_tags), issue: issue_attrs)
79 | assert_response :success
80 | get_via_redirect(issue_path(@issue_with_tags))
81 | assert_response :success
82 |
83 | @issue_with_tags.reload
84 | assert_equal 3, @issue_with_tags.taggings.size
85 | assert_equal [@another_project_context], @issue_with_tags.taggings.map(&:context).uniq
86 | end
87 |
88 | def test_should_create_inline_issue_tags
89 | Setting.plugin_redmine_tagging[:issues_inline] = '1'
90 |
91 | @new_issue_attrs = {
92 | 'project_id' => @project_with_tags.id,
93 | 'priority_id' => @issue_with_tags.priority_id,
94 | 'subject' => 'new_issue',
95 | 'description' => '{{tag(10 11 12)}}'
96 | }
97 |
98 | get_via_redirect(new_project_issue_path(@project_with_tags))
99 | assert_response :success
100 | post_via_redirect(issues_path, issue: @new_issue_attrs)
101 | assert_response :success
102 |
103 | new_issue = Issue.find_by_subject('new_issue')
104 | assert_equal 3, new_issue.taggings.size
105 | assert_equal [@project_with_tags_context], new_issue.taggings.map(&:context).uniq
106 | end
107 |
108 | def test_should_update_inline_issue_tags
109 | Setting.plugin_redmine_tagging[:issues_inline] = '1'
110 |
111 | issue_attrs = @issue_with_tags.attributes
112 |
113 | issue_attrs['project_id'] = @another_project.id
114 | issue_attrs['tracker'] = @another_project.trackers.first
115 | issue_attrs['description'] = '{{tag(6)}} {{tag(7 8)}}'
116 |
117 | get_via_redirect(edit_issue_path(@issue_with_tags))
118 | assert_response :success
119 | put_via_redirect(issue_path(@issue_with_tags), issue: issue_attrs)
120 | assert_response :success
121 | get_via_redirect(issue_path(@issue_with_tags))
122 | assert_response :success
123 |
124 | @issue_with_tags.reload
125 | assert_equal 2, @issue_with_tags.taggings.count
126 | assert_equal [@another_project_context], @issue_with_tags.taggings.map(&:context).uniq
127 | end
128 |
129 | def test_should_generate_wiki_tagcloud
130 | edit_page_path = edit_wiki_cpath(@project_with_wiki_tags, 'newpage')
131 | page_path = wiki_cpath(@project_with_wiki_tags, 'newpage')
132 |
133 | page_content = @wiki_page_with_tags_content.attributes.merge(
134 | 'text' => '{{tag(11)}} {{tag(14 15)}} {{tagcloud}}'
135 | )
136 |
137 | page_attrs = @wiki_page_with_tags.attributes
138 |
139 | get_via_redirect(edit_page_path)
140 | assert_response :success
141 | put_via_redirect(page_path, wiki_page: page_attrs, content: page_content)
142 | assert_response :success
143 | get_via_redirect(page_path)
144 | assert_response :success
145 | end
146 |
147 | def test_should_create_wiki_page_tags_from_input
148 | Setting.plugin_redmine_tagging[:wiki_pages_inline] = '0'
149 |
150 | edit_page_path = edit_wiki_cpath(@project_with_wiki_tags, 'newpage')
151 | page_path = wiki_cpath(@project_with_wiki_tags, 'newpage')
152 |
153 | page_content = @wiki_page_with_tags_content.attributes
154 | page_attrs = @wiki_page_with_tags.attributes
155 | page_attrs['title'] = 'Newpage'
156 | page_attrs['tags'] = '10 11 12'
157 |
158 | get_via_redirect(edit_page_path)
159 | assert_response :success
160 | put_via_redirect(page_path, wiki_page: page_attrs, content: page_content)
161 | assert_response :success
162 | get_via_redirect(page_path)
163 | assert_response :success
164 |
165 | new_page = WikiPage.find_by_title('Newpage')
166 | assert_equal 3, new_page.taggings.size
167 | assert_equal [@project_with_wiki_tags_context], new_page.taggings.map(&:context).uniq
168 | end
169 |
170 | def test_should_update_wiki_page_tags_from_input
171 | Setting.plugin_redmine_tagging[:wiki_pages_inline] = '0'
172 |
173 | edit_page_path = edit_wiki_cpath(@project_with_wiki_tags, @wiki_page_with_tags.title)
174 | page_path = wiki_cpath(@project_with_wiki_tags, @wiki_page_with_tags.title)
175 | page_content = @wiki_page_with_tags_content.attributes
176 | page_attrs = @wiki_page_with_tags.attributes
177 | page_attrs['tags'] = '10 11 12'
178 |
179 | get_via_redirect(edit_page_path)
180 | assert_response :success
181 | put_via_redirect(page_path, wiki_page: page_attrs, content: page_content)
182 | assert_response :success
183 | get_via_redirect(page_path)
184 | assert_response :success
185 |
186 | @wiki_page_with_tags.reload
187 | assert_equal 3, @wiki_page_with_tags.taggings.size
188 | assert_equal [@project_with_wiki_tags_context], @wiki_page_with_tags.taggings.map(&:context).uniq
189 | end
190 |
191 | def test_should_create_inline_wiki_page_tags
192 | Setting.plugin_redmine_tagging[:wiki_pages_inline] = '1'
193 |
194 | edit_page_path = edit_wiki_cpath(@project_with_wiki_tags, 'newpage')
195 | page_path = wiki_cpath(@project_with_wiki_tags, 'newpage')
196 |
197 | page_content = @wiki_page_with_tags_content.attributes.merge(
198 | 'text' => '{{tag(11)}} {{tag(14 15)}}'
199 | )
200 |
201 | page_attrs = @wiki_page_with_tags.attributes.merge(
202 | 'title' => 'Newpage'
203 | )
204 |
205 | get_via_redirect(edit_page_path)
206 | assert_response :success
207 | put_via_redirect(page_path, wiki_page: page_attrs, content: page_content)
208 | assert_response :success
209 | get_via_redirect(page_path)
210 | assert_response :success
211 |
212 | new_page = WikiPage.find_by_title('Newpage')
213 | assert_equal 2, new_page.taggings.size
214 | assert_equal [@project_with_wiki_tags_context], new_page.taggings.map(&:context).uniq
215 | end
216 |
217 | def test_should_update_inline_wiki_page_tags
218 | Setting.plugin_redmine_tagging[:wiki_pages_inline] = '1'
219 |
220 | edit_page_path = edit_wiki_cpath(@project_with_wiki_tags, @wiki_page_with_tags.title)
221 | page_path = wiki_cpath(@project_with_wiki_tags, @wiki_page_with_tags.title)
222 |
223 | page_content = @wiki_page_with_tags_content.attributes.merge(
224 | 'text' => '{{tag(11)}} {{tag(14 15)}}'
225 | )
226 |
227 | page_attrs = @wiki_page_with_tags.attributes
228 |
229 | get_via_redirect(edit_page_path)
230 | assert_response :success
231 | put_via_redirect(page_path, wiki_page: page_attrs, content: page_content)
232 | assert_response :success
233 | get_via_redirect(page_path)
234 | assert_response :success
235 |
236 | @wiki_page_with_tags.reload
237 | assert_equal 2, @wiki_page_with_tags.taggings.size
238 | assert_equal [@project_with_wiki_tags_context], @wiki_page_with_tags.taggings.map(&:context).uniq
239 | end
240 |
241 | def wiki_cpath(project, page)
242 | project_wiki_page_path(project, page)
243 | rescue NoMethodError
244 | project_wiki_path(project, page)
245 | end
246 |
247 | def edit_wiki_cpath(project, page)
248 | edit_project_wiki_page_path(project, page)
249 | rescue NoMethodError
250 | edit_project_wiki_path(project, page)
251 | end
252 | end
253 |
--------------------------------------------------------------------------------
/lib/tagging_plugin/tagging_hooks.rb:
--------------------------------------------------------------------------------
1 | module TaggingPlugin
2 | module Hooks
3 | class LayoutHook < Redmine::Hook::ViewListener
4 |
5 | def view_issues_sidebar_planning_bottom(context = {})
6 | sidebar_tagcloud? ? render_partial_to_string(context, 'tagging/tagcloud') : ''
7 | end
8 |
9 | def view_wiki_sidebar_bottom(context = {})
10 | sidebar_tagcloud? ? render_partial_to_string(context, 'tagging/tagcloud_search') : ''
11 | end
12 |
13 | def view_layouts_base_html_head(context = {})
14 | tagging_stylesheet = stylesheet_link_tag 'tagging', plugin: 'redmine_tagging'
15 |
16 | unless ((sidebar_tagcloud? &&
17 | context[:controller].is_a?(WikiController)) ||
18 | (context[:controller].is_a?(IssuesController) &&
19 | context[:controller].action_name == 'bulk_edit'))
20 | return tagging_stylesheet
21 | end
22 |
23 | sidebar_tags = if sidebar_tagcloud?
24 | tag_cloud = render_partial_to_string(context, 'tagging/tagcloud_search')
25 | "$('#sidebar').append(\"#{escape_javascript(tag_cloud)}\")"
26 | else
27 | ''
28 | end
29 |
30 | <<-TAGS
31 | #{ tagging_stylesheet }
32 | #{ javascript_include_tag 'toggle_tags', plugin: 'redmine_tagging' }
33 |
41 | TAGS
42 | end
43 |
44 | def view_issues_show_details_bottom(context = {})
45 | return '' if issues_inline_tags?
46 |
47 | issue = context[:issue]
48 | tag_context = ContextHelper.context_for(issue.project)
49 | tags = issue.tag_list_on(tag_context).sort_by { |t| t.downcase }
50 |
51 | render_partial_to_string(context, 'tagging/taglinks', tags: tags)
52 | end
53 |
54 | def view_issues_form_details_bottom(context={})
55 | return '' if issues_inline_tags?
56 |
57 | issue = context[:issue]
58 | result = ''
59 |
60 | if context[:request].params[:issue] # update form
61 | tags = context[:request].params[:issue][:tags]
62 | result += issue_tag_field context[:form], tags
63 | else
64 | tag_context = ContextHelper.context_for(issue.project)
65 | tags = issue.tag_list_on(tag_context) \
66 | .sort_by { |t| t.downcase } \
67 | .map { |tag| tag.gsub(/^#/, '') } \
68 | .join(' ')
69 | result += issue_tag_field context[:form], tags
70 | end
71 |
72 | unless context[:request].xhr?
73 | result += javascript_include_tag 'tag', plugin: 'redmine_tagging'
74 | result += javascript_include_tag 'toggle_tags', plugin: 'redmine_tagging'
75 | end
76 |
77 | result + issue_cloud_javascript(context)
78 | end
79 |
80 | def controller_issues_bulk_edit_before_save(context={})
81 | return if issues_inline_tags? || !has_tags_in_params?(context[:params])
82 |
83 | tags = context[:params]['issue']['tags'].to_s
84 | issue = context[:issue]
85 |
86 | if context[:params]['append_tags']
87 | tag_context = if issue.project_id_changed?
88 | ContextHelper.context_for(Project.find(issue.project_id_was))
89 | else
90 | ContextHelper.context_for(issue.project)
91 | end
92 |
93 | old_tags = issue.tags_on(tag_context)
94 |
95 | tags += ' ' + TagsHelper.to_string(old_tags.map(&:name)) if old_tags.present?
96 | end
97 |
98 | issue.tags_to_update = TagsHelper.from_string(tags)
99 | end
100 |
101 | def controller_issues_edit_before_save(context={})
102 | return if issues_inline_tags?
103 | return unless has_tags_in_params?(context[:params])
104 |
105 | issue = context[:issue]
106 | tags = context[:params]['issue']['tags'].to_s
107 |
108 | tags = TagsHelper.from_string(tags)
109 | issue.tags_to_update = tags
110 | end
111 |
112 | alias_method :controller_issues_new_before_save, :controller_issues_edit_before_save
113 |
114 | # wikis have no view hooks
115 | def view_layouts_base_content(context={})
116 | return '' if wiki_pages_inline_tags?
117 |
118 | return '' unless context[:controller].is_a?(WikiController)
119 |
120 | request = context[:request]
121 |
122 | return '' unless request.parameters
123 |
124 | project = Project.find_by_identifier(request.parameters['project_id'])
125 | return '' unless project
126 |
127 | page = project.wiki.find_page(request.parameters['id'])
128 |
129 | tag_context = ContextHelper.context_for(project)
130 | tags = ''
131 |
132 | if page && request.parameters['action'] == 'index'
133 | tags = page.tag_list_on(tag_context).sort_by { |t| t.downcase }.map do |tag|
134 | link_to(tag, {
135 | controller: 'search',
136 | action: 'index',
137 | project_id: project,
138 | q: tag_without_sharp(tag),
139 | wiki_pages: true,
140 | issues: true })
141 | end.join(' ')
142 |
143 | tags = "#{ l(:field_tags) }:
#{ tags }
" if tags
144 | end
145 |
146 | action = request.parameters['action']
147 |
148 | if action == 'edit' || (!page && action == 'show')
149 | if page
150 | tags = TagsHelper.to_string(page.tag_list_on(tag_context))
151 | else
152 | tags = ""
153 | end
154 |
155 | tags = ""
156 |
157 | ac = ActsAsTaggableOn::Tag.where(
158 | "id in (select tag_id from taggings where taggable_type in ('WikiPage', 'Issue') and context = ?)",
159 | tag_context
160 | ).collect { |tag| tag.name }
161 |
162 | ac = ac.collect { |tag| "'#{escape_javascript(tag.gsub(/^#/, ''))}'" }.join(', ')
163 |
164 | tags += javascript_include_tag 'tag', plugin: 'redmine_tagging'
165 |
166 | tags += <<-generatedscript
167 |
173 | generatedscript
174 | end
175 |
176 | return tags
177 | end
178 |
179 | def view_issues_bulk_edit_details_bottom(context = {})
180 | <<-HTML
181 |
182 |
183 | #{ text_field_tag 'issue[tags]', '', size: 18 }
184 |
185 | #{ l(:append_tags) }
186 |
187 |
188 | #{ render_partial_to_string(context, 'tagging/issue_tagcloud') }
189 |
190 | HTML
191 | end
192 |
193 | def view_reports_issue_report_split_content_right(context={})
194 | project_context = ContextHelper.context_for(context[:project])
195 | tags = ActsAsTaggableOn::Tagging.find_all_by_context(project_context).map(&:tag).uniq
196 | tags_by_status = IssueTag.by_issue_status(context[:project])
197 |
198 | <<-HTML
199 | #{ l(:field_tags) }
200 | #{ render_partial_to_string(context, 'reports/simple_tags',
201 | data: tags_by_status, field_name: 'tag', rows: tags) }
202 | HTML
203 | end
204 |
205 | private
206 |
207 | def has_tags_in_params?(params)
208 | params && params['issue'] && params['issue']['tags']
209 | end
210 |
211 | def issues_inline_tags?
212 | Setting.plugin_redmine_tagging[:issues_inline] == '1'
213 | end
214 |
215 | def wiki_pages_inline_tags?
216 | Setting.plugin_redmine_tagging[:wiki_pages_inline] == '1'
217 | end
218 |
219 | def sidebar_tagcloud?
220 | Setting.plugin_redmine_tagging[:sidebar_tagcloud] == '1'
221 | end
222 |
223 | def render_partial_to_string(context, partial_name, options = {})
224 | context[:controller].send :render_to_string,
225 | partial: partial_name,
226 | locals: context.merge(options)
227 | end
228 |
229 | def issue_tag_field(form, tags = '')
230 | '' + form.text_field(:tags, autocomplete: 'off', value: tags) + '
'
231 | end
232 |
233 | def issue_cloud_javascript(context)
234 | tag_context = ContextHelper.context_for(context[:issue].project)
235 | ac = ActsAsTaggableOn::Tag.where(
236 | "id in (select tag_id from taggings where taggable_type in ('WikiPage', 'Issue') and context = ?)",
237 | tag_context)
238 | ac = ac.map { |tag| "'#{escape_javascript(tag.to_s.gsub(/^\s*#/, ''))}'" }.join(', ')
239 |
240 | cloud = render_partial_to_string(context, 'tagging/issue_tagcloud')
241 |
242 | <<-generatedscript
243 |
254 | generatedscript
255 | end
256 |
257 | end
258 | end
259 | end
260 |
--------------------------------------------------------------------------------
/assets/javascripts/tag.js:
--------------------------------------------------------------------------------
1 | /*
2 | @author: remy sharp / http://remysharp.com
3 | @url: http://remysharp.com/2007/12/28/jquery-tag-suggestion/
4 | @usage: setGlobalTags(['javascript', 'jquery', 'java', 'json']); // applied tags to be used for all implementations
5 | $('input.tags').tagSuggest(options);
6 |
7 | The selector is the element that the user enters their tag list
8 | @params:
9 | matchClass - class applied to the suggestions, defaults to 'tagMatches'
10 | tagContainer - the type of element uses to contain the suggestions, defaults to 'span'
11 | tagWrap - the type of element the suggestions a wrapped in, defaults to 'span'
12 | sort - boolean to force the sorted order of suggestions, defaults to false
13 | url - optional url to get suggestions if setGlobalTags isn't used. Must return array of suggested tags
14 | tags - optional array of tags specific to this instance of element matches
15 | delay - optional sets the delay between keyup and the request - can help throttle ajax requests, defaults to zero delay
16 | separator - optional separator string, defaults to ' ' (Brian J. Cardiff)
17 | @license: Creative Commons License - ShareAlike http://creativecommons.org/licenses/by-sa/3.0/
18 | @version: 1.4
19 | @changes: fixed filtering to ajax hits
20 | */
21 |
22 | (function ($) {
23 | var globalTags = [];
24 |
25 | // creates a public function within our private code.
26 | // tags can either be an array of strings OR
27 | // array of objects containing a 'tag' attribute
28 | window.setGlobalTags = function(tags /* array */) {
29 | globalTags = getTags(tags);
30 | };
31 |
32 | function getTags(tags) {
33 | var tag, i, goodTags = [];
34 | for (i = 0; i < tags.length; i++) {
35 | tag = tags[i];
36 | if (typeof tags[i] == 'object') {
37 | tag = tags[i].tag;
38 | }
39 | goodTags.push(tag.toLowerCase());
40 | }
41 |
42 | return goodTags;
43 | }
44 |
45 | $.fn.tagSuggest = function (options) {
46 | var defaults = {
47 | 'matchClass' : 'tagMatches',
48 | 'tagContainer' : 'div',
49 | 'tagWrap' : 'span',
50 | 'sort' : true,
51 | 'tags' : null,
52 | 'url' : null,
53 | 'delay' : 0,
54 | 'separator' : ' '
55 | };
56 |
57 | var i, tag, userTags = [], settings = $.extend({}, defaults, options);
58 |
59 | if (settings.tags) {
60 | userTags = getTags(settings.tags);
61 | } else {
62 | userTags = globalTags;
63 | }
64 |
65 | return this.each(function () {
66 | var tagsElm = $(this);
67 | var elm = this;
68 | var matches, fromTab = false;
69 | var suggestionsShow = false;
70 | var workingTags = [];
71 | var currentTag = {"position": 0, tag: ""};
72 | var tagMatches = document.createElement(settings.tagContainer);
73 |
74 | function showSuggestionsDelayed(el, key) {
75 | if (settings.delay) {
76 | if (elm.timer) clearTimeout(elm.timer);
77 | elm.timer = setTimeout(function () {
78 | showSuggestions(el, key);
79 | }, settings.delay);
80 | } else {
81 | showSuggestions(el, key);
82 | }
83 | }
84 |
85 | function showSuggestions(el, key) {
86 | workingTags = el.value.split(settings.separator);
87 | matches = [];
88 | var i, html = '', chosenTags = {}, tagSelected = false;
89 |
90 | // we're looking to complete the tag on currentTag.position (to start with)
91 | currentTag = { position: currentTags.length-1, tag: '' };
92 |
93 | for (i = 0; i < currentTags.length && i < workingTags.length; i++) {
94 | if (!tagSelected &&
95 | currentTags[i].toLowerCase() != workingTags[i].toLowerCase()) {
96 | currentTag = { position: i, tag: workingTags[i].toLowerCase() };
97 | tagSelected = true;
98 | }
99 | // lookup for filtering out chosen tags
100 | chosenTags[currentTags[i].toLowerCase()] = true;
101 | }
102 |
103 | if (currentTag.tag) {
104 | // collect potential tags
105 | if (settings.url) {
106 | $.ajax({
107 | 'url' : settings.url,
108 | 'dataType' : 'json',
109 | 'data' : { 'tag' : currentTag.tag },
110 | 'async' : false, // wait until this is ajax hit is complete before continue
111 | 'success' : function (m) {
112 | matches = m;
113 | }
114 | });
115 | } else {
116 | for (i = 0; i < userTags.length; i++) {
117 | if (userTags[i].indexOf(currentTag.tag) === 0) {
118 | matches.push(userTags[i]);
119 | }
120 | }
121 | }
122 |
123 | matches = $.grep(matches, function (v, i) {
124 | return !chosenTags[v.toLowerCase()];
125 | });
126 |
127 | if (settings.sort) {
128 | matches = matches.sort();
129 | }
130 |
131 | for (i = 0; i < matches.length; i++) {
132 | html += '<' + settings.tagWrap + ' class="_tag_suggestion">' + matches[i] + '' + settings.tagWrap + '> ';
133 | }
134 |
135 | tagMatches.html(html);
136 | suggestionsShow = !!(matches.length);
137 | } else {
138 | hideSuggestions();
139 | }
140 | }
141 |
142 | function hideSuggestions() {
143 | tagMatches.empty();
144 | matches = [];
145 | suggestionsShow = false;
146 | }
147 |
148 | function setSelection() {
149 | var v = tagsElm.val();
150 |
151 | // tweak for hintted elements
152 | // http://remysharp.com/2007/01/25/jquery-tutorial-text-box-hints/
153 | if (v == tagsElm.attr('title') && tagsElm.is('.hint')) v = '';
154 |
155 | currentTags = v.split(settings.separator);
156 | hideSuggestions();
157 | }
158 |
159 | function chooseTag(tag) {
160 | var i, index;
161 | for (i = 0; i < currentTags.length; i++) {
162 | if (currentTags[i].toLowerCase() != workingTags[i].toLowerCase()) {
163 | index = i;
164 | break;
165 | }
166 | }
167 |
168 | if (index == workingTags.length - 1) tag = tag + settings.separator;
169 |
170 | workingTags[i] = tag;
171 |
172 | tagsElm.val(workingTags.join(settings.separator));
173 | tagsElm.blur().focus();
174 | tagsElm.change();
175 | setSelection();
176 | }
177 |
178 | function handleKeys(ev) {
179 | fromTab = false;
180 | var type = ev.type;
181 | var resetSelection = false;
182 |
183 | switch (ev.keyCode) {
184 | case 37: // ignore cases (arrow keys)
185 | case 38:
186 | case 39:
187 | case 40: {
188 | hideSuggestions();
189 | return true;
190 | }
191 | case 224:
192 | case 17:
193 | case 16:
194 | case 18: {
195 | return true;
196 | }
197 |
198 | case 8: {
199 | // delete - hide selections if we're empty
200 | if (this.value == '') {
201 | hideSuggestions();
202 | setSelection();
203 | return true;
204 | } else {
205 | type = 'keyup'; // allow drop through
206 | showSuggestionsDelayed(this, ev.charCode);
207 | }
208 | break;
209 | }
210 |
211 | case 9: // return and tab
212 | case 13: {
213 | if (suggestionsShow) {
214 | // complete
215 | chooseTag(matches[0]);
216 |
217 | fromTab = true;
218 | return false;
219 | } else {
220 | return true;
221 | }
222 | }
223 | case 27: {
224 | hideSuggestions();
225 | setSelection();
226 | return true;
227 | }
228 | case 32: {
229 | setSelection();
230 | return true;
231 | }
232 | }
233 |
234 | if (type == 'keyup') {
235 | switch (ev.charCode) {
236 | case 9:
237 | case 13: {
238 | return true;
239 | }
240 | }
241 |
242 | if (resetSelection) {
243 | setSelection();
244 | }
245 | showSuggestionsDelayed(this, ev.charCode);
246 | }
247 | }
248 |
249 | tagsElm.after(tagMatches).keypress(handleKeys).keyup(handleKeys).blur(function () {
250 | if (fromTab == true || suggestionsShow) { // tweak to support tab selection for Opera & IE
251 | fromTab = false;
252 | tagsElm.focus();
253 | }
254 | });
255 |
256 | // replace with jQuery version
257 | tagMatches = $(tagMatches).click(function (ev) {
258 | if (ev.target.nodeName == settings.tagWrap.toUpperCase() && $(ev.target).is('._tag_suggestion')) {
259 | chooseTag(ev.target.innerHTML);
260 | }
261 | }).addClass(settings.matchClass);
262 |
263 | // initialise
264 | setSelection();
265 | });
266 | };
267 | })(jQuery);
268 |
--------------------------------------------------------------------------------