diff --git a/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee b/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee
index ca8455b2e..2af9cdc45 100644
--- a/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee
+++ b/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee
@@ -285,6 +285,36 @@ class App.ControllerForm extends App.Controller
else if attribute.tag is 'textarea'
item = $( App.view('generic/textarea')( attribute: attribute ) )
+ # tag
+ else if attribute.tag is 'tag'
+ item = $( App.view('generic/input')( attribute: attribute ) )
+ a = =>
+ siteUpdate = (reorder) =>
+ container = document.getElementById( attribute.id + "_tagsinput" )
+ if reorder
+ $('#' + attribute.id + "_tagsinput" ).height( 20 )
+ height = container.scrollHeight
+ $('#' + attribute.id + "_tagsinput" ).height( height - 16 )
+
+ onAddTag = =>
+ siteUpdate()
+
+ onRemoveTag = =>
+ siteUpdate(true)
+
+ w = $('#' + attribute.id).width()
+ h = $('#' + attribute.id).height()
+ $('#' + attribute.id).tagsInput(
+ width: w + 'px'
+# height: (h + 30 )+ 'px'
+ onAddTag: onAddTag
+ onRemoveTag: onRemoveTag
+ )
+ siteUpdate(true)
+
+ @delay( a, 600 )
+
+
# autocompletion
else if attribute.tag is 'autocompletion'
item = $( App.view('generic/autocompletion')( attribute: attribute ) )
diff --git a/app/assets/javascripts/app/controllers/agent_ticket_create.js.coffee b/app/assets/javascripts/app/controllers/agent_ticket_create.js.coffee
index cb5b66c0b..d4fd34e2c 100644
--- a/app/assets/javascripts/app/controllers/agent_ticket_create.js.coffee
+++ b/app/assets/javascripts/app/controllers/agent_ticket_create.js.coffee
@@ -103,6 +103,7 @@ class Index extends App.Controller
{ name: 'customer_id', display: 'Customer', tag: 'autocompletion', type: 'text', limit: 200, null: false, relation: 'User', class: 'span7', autocapitalize: false, help: 'Select the customer of the Ticket or create one.', link: '»', callback: @localUserInfo },
{ name: 'group_id', display: 'Group', tag: 'select', multiple: false, null: false, filter: @edit_form, nulloption: true, relation: 'Group', default: defaults['group_id'], class: 'span7', },
{ name: 'owner_id', display: 'Owner', tag: 'select', multiple: false, null: true, filter: @edit_form, nulloption: true, relation: 'User', default: defaults['owner_id'], class: 'span7', },
+ { name: 'tags', display: 'Tags', tag: 'tag', type: 'text', null: true, default: defaults['tags'], class: 'span7', },
{ name: 'subject', display: 'Subject', tag: 'input', type: 'text', limit: 200, null: false, default: defaults['subject'], class: 'span7', },
{ name: 'body', display: 'Text', tag: 'textarea', rows: 6, null: false, default: defaults['body'], class: 'span7', },
{ name: 'ticket_state_id', display: 'State', tag: 'select', multiple: false, null: false, filter: @edit_form, relation: 'TicketState', default: defaults['ticket_state_id'], translate: true, class: 'medium' },
diff --git a/app/assets/javascripts/app/controllers/agent_ticket_zoom.js.coffee b/app/assets/javascripts/app/controllers/agent_ticket_zoom.js.coffee
index a214408da..be012847a 100644
--- a/app/assets/javascripts/app/controllers/agent_ticket_zoom.js.coffee
+++ b/app/assets/javascripts/app/controllers/agent_ticket_zoom.js.coffee
@@ -188,6 +188,14 @@ class Index extends App.Controller
zoom: @
)
+ # start tag controller
+ if !@isRole('Customer')
+ new App.TagWidget(
+ el: @el.find('#tag_info')
+ object_type: 'Ticket'
+ object: @ticket
+ )
+
# start link info controller
if !@isRole('Customer')
new App.LinkInfo(
diff --git a/app/assets/javascripts/app/controllers/tag_widget.js.coffee b/app/assets/javascripts/app/controllers/tag_widget.js.coffee
new file mode 100644
index 000000000..bb84f6337
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/tag_widget.js.coffee
@@ -0,0 +1,69 @@
+$ = jQuery.sub()
+
+class App.TagWidget extends App.Controller
+ constructor: ->
+ super
+ @load()
+
+ load: =>
+ App.Com.ajax(
+ id: 'tags_' + @object.id + '_' + @object_type
+ type: 'GET'
+ url: '/api/tags'
+ data:
+ object: @object_type
+ o_id: @object.id
+ processData: true
+ success: (data, status, xhr) =>
+ @render(data.tags)
+ )
+
+ render: (tags) =>
+
+ # insert data
+ @html App.view('tag_widget')(
+ tags: tags || [],
+ )
+ @el.find('#tags').tagsInput(
+ width: '150px'
+ defaultText: App.i18n.translateContent('add a Tag')
+ onAddTag: @onAddTag
+ onRemoveTag: @onRemoveTag
+# height: '65px'
+ )
+ @delay @siteUpdate, 100
+
+# @el.find('#tags').elastic()
+
+ onAddTag: (item) =>
+ App.Com.ajax(
+ type: 'GET',
+ url: '/api/tags/add',
+ data:
+ object: @object_type,
+ o_id: @object.id,
+ item: item
+ processData: true,
+ success: (data, status, xhr) =>
+ @siteUpdate()
+ )
+
+ onRemoveTag: (item) =>
+ App.Com.ajax(
+ type: 'GET'
+ url: '/api/tags/remove'
+ data:
+ object: @object_type
+ o_id: @object.id
+ item: item
+ processData: true
+ success: (data, status, xhr) =>
+ @siteUpdate(true)
+ )
+
+ siteUpdate: (reorder) =>
+ container = document.getElementById("tags_tagsinput")
+ if reorder
+ $('#tags_tagsinput').height( 20 )
+ height = container.scrollHeight
+ $('#tags_tagsinput').height( height - 10 )
diff --git a/app/assets/javascripts/app/controllers/template_widget.js.coffee b/app/assets/javascripts/app/controllers/template_widget.js.coffee
index fe430a4d0..730ef243f 100644
--- a/app/assets/javascripts/app/controllers/template_widget.js.coffee
+++ b/app/assets/javascripts/app/controllers/template_widget.js.coffee
@@ -35,7 +35,7 @@ class App.TemplateUI extends App.Controller
template = App.Collection.find( 'Template', @template_id )
# insert data
- @html App.view('template')(
+ @html App.view('template_widget')(
template: template,
)
new App.ControllerForm(
diff --git a/app/assets/javascripts/app/controllers/text_module_widget.js.coffee b/app/assets/javascripts/app/controllers/text_module_widget.js.coffee
index be1981428..678cbc06a 100644
--- a/app/assets/javascripts/app/controllers/text_module_widget.js.coffee
+++ b/app/assets/javascripts/app/controllers/text_module_widget.js.coffee
@@ -132,7 +132,7 @@ class App.TextModuleUI extends App.Controller
)
# insert data
- @html App.view('text_module')(
+ @html App.view('text_module_widget')(
search: @search,
)
diff --git a/app/assets/javascripts/app/models/ticket.js.coffee b/app/assets/javascripts/app/models/ticket.js.coffee
index 0bfc24510..144ef34b6 100644
--- a/app/assets/javascripts/app/models/ticket.js.coffee
+++ b/app/assets/javascripts/app/models/ticket.js.coffee
@@ -1,5 +1,5 @@
class App.Ticket extends App.Model
- @configure 'Ticket', 'number', 'title', 'group_id', 'owner_id', 'customer_id', 'ticket_state_id', 'ticket_priority_id', 'article'
+ @configure 'Ticket', 'number', 'title', 'group_id', 'owner_id', 'customer_id', 'ticket_state_id', 'ticket_priority_id', 'article', 'tags'
@extend Spine.Model.Ajax
@url: '/api/tickets'
@configure_attributes = [
diff --git a/app/assets/javascripts/app/views/agent_ticket_zoom.jst.eco b/app/assets/javascripts/app/views/agent_ticket_zoom.jst.eco
index a764da0cb..79fc14a8c 100644
--- a/app/assets/javascripts/app/views/agent_ticket_zoom.jst.eco
+++ b/app/assets/javascripts/app/views/agent_ticket_zoom.jst.eco
@@ -93,6 +93,7 @@
diff --git a/app/assets/javascripts/app/views/tag_widget.jst.eco b/app/assets/javascripts/app/views/tag_widget.jst.eco
new file mode 100644
index 000000000..212a25351
--- /dev/null
+++ b/app/assets/javascripts/app/views/tag_widget.jst.eco
@@ -0,0 +1,4 @@
+
+
<%- @T( 'Tags' ) %>
+
+
\ No newline at end of file
diff --git a/app/assets/javascripts/app/views/template.jst.eco b/app/assets/javascripts/app/views/template_widget.jst.eco
similarity index 100%
rename from app/assets/javascripts/app/views/template.jst.eco
rename to app/assets/javascripts/app/views/template_widget.jst.eco
diff --git a/app/assets/javascripts/app/views/text_module.jst.eco b/app/assets/javascripts/app/views/text_module_widget.jst.eco
similarity index 100%
rename from app/assets/javascripts/app/views/text_module.jst.eco
rename to app/assets/javascripts/app/views/text_module_widget.jst.eco
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
index 1f9a1f5ff..76ff0b609 100644
--- a/app/assets/stylesheets/application.css
+++ b/app/assets/stylesheets/application.css
@@ -5,8 +5,9 @@
*= require_self
*= require ./bootstrap.css
*= require ./fileuploader.css
- *= require ./ui-lightness/jquery-ui-1.8.18.custom.css
+ *= require ./ui-lightness/jquery-ui-1.8.23.custom.css
*= require ./jquery.noty.css
+ *= require ./jquery.tagsinput.css
*= require ./noty_theme_twitter.css
*= require ./zzz.css
*
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
new file mode 100644
index 000000000..a0d2178f1
--- /dev/null
+++ b/app/controllers/tags_controller.rb
@@ -0,0 +1,57 @@
+class TagsController < ApplicationController
+ before_filter :authentication_check
+
+ # GET /api/tags
+ def index
+ list = Tag.list()
+
+ # return result
+ render :json => {
+ :tags => list,
+ }
+ end
+
+ # GET /api/tags
+ def list
+ list = Tag.tag_list(
+ :object => params[:object],
+ :o_id => params[:o_id],
+ )
+
+ # return result
+ render :json => {
+ :tags => list,
+ }
+ end
+
+ # POST /api/tag/add
+ def add
+ success = Tag.tag_add(
+ :object => params[:object],
+ :o_id => params[:o_id],
+ :item => params[:item],
+ :created_by_id => current_user.id,
+ );
+ if success
+ render :json => success, :status => :created
+ else
+ render :json => success.errors, :status => :unprocessable_entity
+ end
+ end
+
+ # DELETE /api/tag/remove
+ def remove
+ success = Tag.tag_remove(
+ :object => params[:object],
+ :o_id => params[:o_id],
+ :item => params[:item],
+ :created_by_id => current_user.id,
+ );
+ if success
+ render :json => success, :status => :created
+ else
+ render :json => success.errors, :status => :unprocessable_entity
+ end
+ end
+
+end
diff --git a/app/controllers/tickets_controller.rb b/app/controllers/tickets_controller.rb
index 13333363f..103123d39 100644
--- a/app/controllers/tickets_controller.rb
+++ b/app/controllers/tickets_controller.rb
@@ -36,6 +36,19 @@ class TicketsController < ApplicationController
return
end
+ # create tags if given
+ if params[:tags] && !params[:tags].empty?
+ tags = params[:tags].split /,/
+ tags.each {|tag|
+ Tag.tag_add(
+ :object => 'Ticket',
+ :o_id => @ticket.id,
+ :item => tag,
+ :created_by_id => current_user.id,
+ )
+ }
+ end
+
# create article if given
if params[:article]
@article = Ticket::Article.new(params[:article])
diff --git a/app/models/history.rb b/app/models/history.rb
index 242a06e4e..0eb1528c4 100644
--- a/app/models/history.rb
+++ b/app/models/history.rb
@@ -58,7 +58,7 @@ class History < ActiveRecord::Base
history_object = self.history_object_lookup( requested_object )
history = History.where( :history_object_id => history_object.id ).
where( :o_id => requested_object_id ).
- where( :history_type_id => History::Type.where( :name => ['created', 'updated', 'notification', 'email'] ) ).
+ where( :history_type_id => History::Type.where( :name => ['created', 'updated', 'notification', 'email', 'added', 'removed'] ) ).
order('created_at ASC, id ASC')
else
history_object_requested = self.history_object_lookup( requested_object )
@@ -69,7 +69,7 @@ class History < ActiveRecord::Base
requested_object_id,
history_object_related.id,
requested_object_id,
- History::Type.where( :name => ['created', 'updated', 'notification', 'email'] )
+ History::Type.where( :name => ['created', 'updated', 'notification', 'email', 'added', 'removed'] )
).
order('created_at ASC, id ASC')
end
diff --git a/app/models/observer/tag/ticket_history.rb b/app/models/observer/tag/ticket_history.rb
new file mode 100644
index 000000000..f729986e3
--- /dev/null
+++ b/app/models/observer/tag/ticket_history.rb
@@ -0,0 +1,37 @@
+require 'history'
+
+class Observer::Tag::TicketHistory < ActiveRecord::Observer
+ include UserInfo
+ observe 'tag'
+
+ def after_create(record)
+
+ # just process ticket object tags
+ return true if record.tag_object.name != 'Ticket';
+
+ # add ticket history
+ History.history_create(
+ :o_id => record.o_id,
+ :history_type => 'added',
+ :history_object => 'Ticket',
+ :history_attribute => 'Tag',
+ :value_from => record.tag_item.name,
+ :created_by_id => current_user_id || record.created_by_id || 1
+ )
+ end
+ def after_destroy(record)
+
+ # just process ticket object tags
+ return true if record.tag_object.name != 'Ticket';
+
+ # add ticket history
+ History.history_create(
+ :o_id => record.o_id,
+ :history_type => 'removed',
+ :history_object => 'Ticket',
+ :history_attribute => 'Tag',
+ :value_from => record.tag_item.name,
+ :created_by_id => current_user_id || record.created_by_id || 1
+ )
+ end
+end
\ No newline at end of file
diff --git a/app/models/tag.rb b/app/models/tag.rb
new file mode 100644
index 000000000..242f4fccd
--- /dev/null
+++ b/app/models/tag.rb
@@ -0,0 +1,133 @@
+class Tag < ApplicationModel
+ belongs_to :tag_object, :class_name => 'Tag::Object'
+ belongs_to :tag_item, :class_name => 'Tag::Item'
+
+ @@cache_item = {}
+ @@cache_object = {}
+
+ def self.tag_add(data)
+
+ # lookups
+ if data[:object]
+ tag_object_id = self.tag_object_lookup( data[:object] )
+ end
+ if data[:item]
+ tag_item_id = self.tag_item_lookup( data[:item] )
+ end
+
+ # create history
+ Tag.create(
+ :tag_object_id => tag_object_id,
+ :tag_item_id => tag_item_id,
+ :o_id => data[:o_id],
+ :created_by_id => data[:created_by_id]
+ )
+ return true
+ end
+
+ def self.tag_remove(data)
+
+ # lookups
+ if data[:object]
+ tag_object_id = self.tag_object_lookup( data[:object] )
+ end
+ if data[:item]
+ tag_item_id = self.tag_item_lookup( data[:item] )
+ end
+
+ # create history
+ result = Tag.where(
+ :tag_object_id => tag_object_id,
+ :tag_item_id => tag_item_id,
+ :o_id => data[:o_id],
+ )
+ result.each { |item|
+ item.destroy
+ }
+ return true
+ end
+
+ def self.tag_list( data )
+ tag_object_id_requested = self.tag_object_lookup( data[:object] )
+ tag_search = Tag.where(
+ :tag_object_id => tag_object_id_requested,
+ :o_id => data[:o_id],
+ )
+ tags = []
+ tag_search.each {|tag|
+ tags.push self.tag_item_lookup_id( tag.tag_item_id )
+ }
+ return tags
+ end
+
+ private
+
+ def self.tag_item_lookup_id( id )
+
+ # use cache
+ return @@cache_item[ id ] if @@cache_item[ id ]
+
+ # lookup
+ tag_item = Tag::Item.find(id)
+ @@cache_item[ id ] = tag_item.name
+ return tag_item.name
+ end
+
+ def self.tag_item_lookup( name )
+
+ # use cache
+ return @@cache_item[ name ] if @@cache_item[ name ]
+
+ # lookup
+ tag_item = Tag::Item.where( :name => name ).first
+ if tag_item
+ @@cache_item[ name ] = tag_item.id
+ return tag_item.id
+ end
+
+ # create
+ tag_item = Tag::Item.create(
+ :name => name
+ )
+ @@cache_item[ name ] = tag_item.id
+ return tag_item.id
+ end
+
+ def self.tag_object_lookup_id( id )
+
+ # use cache
+ return @@cache_object[ id ] if @@cache_object[ id ]
+
+ # lookup
+ tag_object = Tag::Object.find(id)
+ @@cache_object[ id ] = tag_object.name
+ return tag_object.name
+ end
+
+ def self.tag_object_lookup( name )
+
+ # use cache
+ return @@cache_object[ name ] if @@cache_object[ name ]
+
+ # lookup
+ tag_object = Tag::Object.where( :name => name ).first
+ if tag_object
+ @@cache_object[ name ] = tag_object.id
+ return tag_object.id
+ end
+
+ # create
+ tag_object = Tag::Object.create(
+ :name => name
+ )
+ @@cache_object[ name ] = tag_object.id
+ return tag_object.id
+ end
+
+ class Object < ActiveRecord::Base
+ end
+
+ class Item < ActiveRecord::Base
+ end
+
+end
diff --git a/config/routes/tag.rb b/config/routes/tag.rb
new file mode 100644
index 000000000..e6a615ace
--- /dev/null
+++ b/config/routes/tag.rb
@@ -0,0 +1,11 @@
+module ExtraRoutes
+ def add(map)
+
+ # links
+ map.match '/api/tags', :to => 'tags#list'
+ map.match '/api/tags/add', :to => 'tags#add'
+ map.match '/api/tags/remove', :to => 'tags#remove'
+
+ end
+ module_function :add
+end
\ No newline at end of file
diff --git a/db/migrate/20121117121944_tags_create.rb b/db/migrate/20121117121944_tags_create.rb
new file mode 100644
index 000000000..7f7a6da54
--- /dev/null
+++ b/db/migrate/20121117121944_tags_create.rb
@@ -0,0 +1,28 @@
+class TagsCreate < ActiveRecord::Migration
+ def up
+ create_table :tags do |t|
+ t.references :tag_item, :null => false
+ t.references :tag_object, :null => false
+ t.column :o_id, :integer, :null => false
+ t.column :created_by_id, :integer, :null => false
+ t.timestamps
+ end
+ add_index :tags, [:o_id]
+ add_index :tags, [:tag_object_id]
+
+ create_table :tag_objects do |t|
+ t.column :name, :string, :limit => 250, :null => false
+ t.timestamps
+ end
+ add_index :tag_objects, [:name], :unique => true
+
+ create_table :tag_items do |t|
+ t.column :name, :string, :limit => 250, :null => false
+ t.timestamps
+ end
+ add_index :tag_items, [:name], :unique => true
+ end
+
+ def down
+ end
+end
diff --git a/test/unit/tag_test.rb b/test/unit/tag_test.rb
new file mode 100644
index 000000000..df870f5c2
--- /dev/null
+++ b/test/unit/tag_test.rb
@@ -0,0 +1,99 @@
+# encoding: utf-8
+require 'test_helper'
+
+class TagTest < ActiveSupport::TestCase
+ test 'tags' do
+ tests = [
+
+ # test 1
+ {
+ :tag_add => {
+ :item => 'tag1',
+ :object => 'Object1',
+ :o_id => 123,
+ :created_by_id => 1
+ },
+ :verify => {
+ :object => 'Object1',
+ :items => {
+ 'tag1' => true,
+ 'tag2' => false,
+ },
+ },
+ },
+
+ # test 2
+ {
+ :tag_add => {
+ :item => 'tag2',
+ :object => 'Object1',
+ :o_id => 123,
+ :created_by_id => 1
+ },
+ :verify => {
+ :object => 'Object1',
+ :items => {
+ 'tag1' => true,
+ 'tag2' => true,
+ },
+ },
+ },
+
+ # test 2
+ {
+ :tag_add => {
+ :item => 'tagöäüß1',
+ :object => 'Object2',
+ :o_id => 123,
+ :created_by_id => 1
+ },
+ :verify => {
+ :object => 'Object2',
+ :items => {
+ 'tagöäüß1' => true,
+ 'tag2' => false,
+ },
+ },
+ },
+
+ # test 4
+ {
+ :tag_add => {
+ :item => 'tagöäüß2',
+ :object => 'Object2',
+ :o_id => 123,
+ :created_by_id => 1
+ },
+ :verify => {
+ :object => 'Object2',
+ :items => {
+ 'tagöäüß1' => true,
+ 'tagöäüß2' => true,
+ 'tagöäüß3' => false,
+ },
+ },
+ },
+
+ ]
+ tests.each { |test|
+ success = Tag.tag_add( test[:tag_add] )
+ assert( success, "Tag.tag_add successful")
+ list = Tag.tag_list( test[:tag_add] )
+ test[:verify][:items].each {|key, value|
+ if value == true
+ assert( list.include?( key ), "Tag verify #{ test[:tag_add][:item] }")
+ else
+ assert( !list.include?( key ), "Tag verify #{ test[:tag_add][:item] }")
+ end
+ }
+ }
+
+ # delete tags
+ tests.each { |test|
+ success = Tag.tag_remove( test[:tag_add] )
+ assert( success, "Tag.tag_remove successful")
+ list = Tag.tag_list( test[:tag_add] )
+ assert( !list.include?( test[:tag_add][:item] ), "Tag entry destroyed")
+ }
+ end
+end
\ No newline at end of file