Init version of tags.

This commit is contained in:
Martin Edenhofer 2012-11-18 12:06:55 +01:00
parent 998a7f55d4
commit 4aae8cb3e2
20 changed files with 498 additions and 6 deletions

View file

@ -285,6 +285,36 @@ class App.ControllerForm extends App.Controller
else if attribute.tag is 'textarea' else if attribute.tag is 'textarea'
item = $( App.view('generic/textarea')( attribute: attribute ) ) 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 # autocompletion
else if attribute.tag is 'autocompletion' else if attribute.tag is 'autocompletion'
item = $( App.view('generic/autocompletion')( attribute: attribute ) ) item = $( App.view('generic/autocompletion')( attribute: attribute ) )

View file

@ -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: '<a href="" class="customer_new">&raquo;</a>', callback: @localUserInfo }, { 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: '<a href="" class="customer_new">&raquo;</a>', 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: '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: '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: '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: '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' }, { 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' },

View file

@ -188,6 +188,14 @@ class Index extends App.Controller
zoom: @ zoom: @
) )
# start tag controller
if !@isRole('Customer')
new App.TagWidget(
el: @el.find('#tag_info')
object_type: 'Ticket'
object: @ticket
)
# start link info controller # start link info controller
if !@isRole('Customer') if !@isRole('Customer')
new App.LinkInfo( new App.LinkInfo(

View file

@ -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 )

View file

@ -35,7 +35,7 @@ class App.TemplateUI extends App.Controller
template = App.Collection.find( 'Template', @template_id ) template = App.Collection.find( 'Template', @template_id )
# insert data # insert data
@html App.view('template')( @html App.view('template_widget')(
template: template, template: template,
) )
new App.ControllerForm( new App.ControllerForm(

View file

@ -132,7 +132,7 @@ class App.TextModuleUI extends App.Controller
) )
# insert data # insert data
@html App.view('text_module')( @html App.view('text_module_widget')(
search: @search, search: @search,
) )

View file

@ -1,5 +1,5 @@
class App.Ticket extends App.Model 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 @extend Spine.Model.Ajax
@url: '/api/tickets' @url: '/api/tickets'
@configure_attributes = [ @configure_attributes = [

View file

@ -93,6 +93,7 @@
<div class="span3"> <div class="span3">
<div id="customer_info"></div> <div id="customer_info"></div>
<div id="action_info"></div> <div id="action_info"></div>
<div id="tag_info"></div>
<div id="link_info"></div> <div id="link_info"></div>
<div id="text_module"></div> <div id="text_module"></div>
</div> </div>

View file

@ -0,0 +1,4 @@
<div class="well">
<h3><%- @T( 'Tags' ) %></h3>
<input type="text" name="tags" id="tags" class="span2" value="<% for tag in @tags: %><%= tag %>,<% end %>"/>
</div>

View file

@ -5,8 +5,9 @@
*= require_self *= require_self
*= require ./bootstrap.css *= require ./bootstrap.css
*= require ./fileuploader.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.noty.css
*= require ./jquery.tagsinput.css
*= require ./noty_theme_twitter.css *= require ./noty_theme_twitter.css
*= require ./zzz.css *= require ./zzz.css
* *

View file

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

View file

@ -36,6 +36,19 @@ class TicketsController < ApplicationController
return return
end 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 # create article if given
if params[:article] if params[:article]
@article = Ticket::Article.new(params[:article]) @article = Ticket::Article.new(params[:article])

View file

@ -58,7 +58,7 @@ class History < ActiveRecord::Base
history_object = self.history_object_lookup( requested_object ) history_object = self.history_object_lookup( requested_object )
history = History.where( :history_object_id => history_object.id ). history = History.where( :history_object_id => history_object.id ).
where( :o_id => requested_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') order('created_at ASC, id ASC')
else else
history_object_requested = self.history_object_lookup( requested_object ) history_object_requested = self.history_object_lookup( requested_object )
@ -69,7 +69,7 @@ class History < ActiveRecord::Base
requested_object_id, requested_object_id,
history_object_related.id, history_object_related.id,
requested_object_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') order('created_at ASC, id ASC')
end end

View file

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

133
app/models/tag.rb Normal file
View file

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

11
config/routes/tag.rb Normal file
View file

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

View file

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

99
test/unit/tag_test.rb Normal file
View file

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