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