diff --git a/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.js.coffee
index 6eca4740a..c870b258d 100644
--- a/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.js.coffee
+++ b/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.js.coffee
@@ -1,111 +1,58 @@
class App.UiElement.ticket_selector
- @render: (attribute, params = {}) ->
+ @defaults: ->
+ defaults = ['ticket.state_id']
- # list of attributes
groups =
- tickets:
+ ticket:
name: 'Ticket'
model: 'Ticket'
- users:
+ customer:
name: 'Customer'
model: 'User'
- organizations:
+ organization:
name: 'Organization'
model: 'Organization'
- elements =
- tickets:
- title:
- tag: 'input'
- operator: ['contains', 'contains not']
- number:
- tag: 'input'
- operator: ['contains', 'contains not']
- group_id:
- relation: 'Group'
- tag: 'select'
- multible: true
- operator: ['is', 'is not']
- priority_id:
- relation: 'Priority'
- tag: 'select'
- multible: true
- operator: ['is', 'is not']
- state_id:
- relation: 'State'
- tag: 'select'
- multible: true
- operator: ['is', 'is not']
- owner_id:
- tag: 'user_selection'
- relation: 'User'
- operator: ['is', 'is not']
- customer_id:
- tag: 'user_selection'
- relation: 'User'
- operator: ['is', 'is not']
- organization_id:
- tag: ''
- relation: 'Organization'
- operator: ['is', 'is not']
- tag:
- tag: 'tag'
- multible: true
- operator: ['is', 'is not']
- created_at:
- tag: 'timestamp'
- operator: ['before', 'after']
- updated_at:
- tag: 'timestamp'
- operator: ['before', 'after']
- escalation_time:
- tag: 'timestamp'
- operator: ['before', 'after']
- users:
- firstname:
- tag: 'input'
- operator: ['contains', 'contains not']
- lastname:
- tag: 'input'
- operator: ['contains', 'contains not']
- email:
- tag: 'input'
- operator: ['contains', 'contains not']
- login:
- tag: 'input'
- operator: ['contains', 'contains not']
- created_at:
- tag: 'time_selector_enhanced'
- operator: ['before', 'after']
- updated_at:
- tag: 'time_selector_enhanced'
- operator: ['before', 'after']
- organizations:
- name:
- tag: 'input'
- operator: ['contains', 'contains not']
- shared:
- tag: 'boolean'
- operator: ['is', 'is not']
- created_at:
- tag: 'time_selector_enhanced'
- operator: ['before', 'after']
- updated_at:
- tag: 'time_selector_enhanced'
- operator: ['before', 'after']
+ operators_type =
+ '^datetime$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)']
+ '^timestamp$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)']
+ 'boolean$': ['is', 'is not']
+ '^input$': ['contains', 'contains not']
+ '^textarea$': ['contains', 'contains not']
+
+ operators_name =
+ '_id$': ['is', 'is not']
+ '_ids$': ['is', 'is not']
# megre config
+ elements = {}
for groupKey, groupMeta of groups
- for elementKey, elementGroup of elements
- if elementKey is groupKey
- configure_attributes = App[groupMeta.model].configure_attributes
- for attributeName, attributeConfig of elementGroup
- for attribute in configure_attributes
- if attribute.name is attributeName
- attributeConfig.config = attribute
+ for row in App[groupMeta.model].configure_attributes
+
+ # ignore passwords and relations
+ if row.type isnt 'password' && row.name.substr(row.name.length-4,4) isnt '_ids'
+ config = _.clone(row)
+ for operatorRegEx, operator of operators_type
+ myRegExp = new RegExp(operatorRegEx, 'i')
+ if config.tag && config.tag.match(myRegExp)
+ config.operator = operator
+ elements["#{groupKey}.#{config.name}"] = config
+ for operatorRegEx, operator of operators_name
+ myRegExp = new RegExp(operatorRegEx, 'i')
+ if config.name && config.name.match(myRegExp)
+ config.operator = operator
+ elements["#{groupKey}.#{config.name}"] = config
+ [defaults, groups, elements]
+
+ @render: (attribute, params = {}) ->
+
+ [defaults, groups, elements] = @defaults()
selector = @buildAttributeSelector(groups, elements)
+ search = =>
+ @preview(item)
+
# return item
item = $( App.view('generic/ticket_selector')( attribute: attribute ) )
item.find('.js-attributeSelector').prepend(selector)
@@ -116,12 +63,14 @@ class App.UiElement.ticket_selector
elementClone = element.clone(true)
element.after(elementClone)
elementClone.find('.js-attributeSelector select').trigger('change')
+ @preview(item)
)
# remove filter
item.find('.js-remove').bind('click', (e) =>
$(e.target).closest('.js-filterElement').remove()
@rebuildAttributeSelectors(item)
+ @preview(item)
)
# change filter
@@ -158,9 +107,20 @@ class App.UiElement.ticket_selector
if selectorExists
item.find('.js-filterElement').first().remove()
+ else
+ for default_row in defaults
+
+ # get selector rows
+ elementFirst = item.find('.js-filterElement').first()
+ elementLast = item.find('.js-filterElement').last()
+
+ # clone, rebuild and append
+ elementClone = elementFirst.clone(true)
+ @rebuildAttributeSelectors(item, elementClone, default_row)
+ elementLast.after(elementClone)
+ item.find('.js-filterElement').first().remove()
+
# bind for preview
- search = =>
- @preview(item)
item.on('change', 'select.form-control', (e) =>
App.Delay.set(
search,
@@ -168,7 +128,7 @@ class App.UiElement.ticket_selector
'preview',
)
)
- item.on('keyup', 'input.form-control', (e) =>
+ item.on('change keyup', 'input.form-control', (e) =>
App.Delay.set(
search,
600,
@@ -200,13 +160,6 @@ class App.UiElement.ticket_selector
ticket_ids: ticket_ids
)
- @getElementConfig: (groupAndAttribute, elements) ->
- for elementGroup, elementConfig of elements
- for elementKey, elementItem of elementConfig
- if "#{elementGroup}.#{elementKey}" is groupAndAttribute
- return elementItem
- false
-
@buildValue: (elementFull, elementRow, groupAndAttribute, elements, value) ->
# do nothing if item already exists
@@ -214,16 +167,32 @@ class App.UiElement.ticket_selector
return if elementRow.find("[name=\"#{name}\"]").get(0)
# build new item
- attributeConfig = @getElementConfig(groupAndAttribute, elements)
+ attributeConfig = elements[groupAndAttribute]
+ config = _.clone(attributeConfig)
+
+ # force to use auto compition on user lookup
+ if config.relation is 'User'
+ config.tag = 'user_autocompletion'
+
+ # render ui element
item = ''
- if attributeConfig && attributeConfig.config && App.UiElement[attributeConfig.config.tag]
- config = _.clone(attributeConfig.config)
+ if config && App.UiElement[config.tag]
config['name'] = name
config['value'] = value
if 'multiple' of config
config.multiple = true
config.nulloption = false
- item = App.UiElement[attributeConfig.config.tag].render(config, {})
+ if config.tag is 'checkbox'
+ config.tag = 'select'
+ #config.type = 'datetime-local'
+ #if config.tag is 'datetime'
+ # config.tag = 'input'
+ # config.type = 'datetime-local'
+ tagSearch = "#{config.tag}_search"
+ if App.UiElement[tagSearch]
+ item = App.UiElement[tagSearch].render(config, {})
+ else
+ item = App.UiElement[config.tag].render(config, {})
elementRow.find('.js-value').html(item)
@buildAttributeSelector: (groups, elements) ->
@@ -233,13 +202,12 @@ class App.UiElement.ticket_selector
selection.closest('select').append("")
optgroup = selection.find("optgroup.js-#{groupKey}")
for elementKey, elementGroup of elements
- if elementKey is groupKey
- for attributeName, attributeConfig of elementGroup
- if attributeConfig.config && attributeConfig.config.display
- displayName = App.i18n.translateInline(attributeConfig.config.display)
- else
- displayName = App.i18n.translateInline(attributeName)
- optgroup.append("")
+ spacer = elementKey.split(/\./)
+ if spacer[0] is groupKey
+ attributeConfig = elements[elementKey]
+ if attributeConfig.operator
+ displayName = App.i18n.translateInline(attributeConfig.display)
+ optgroup.append("")
selection
@rebuildAttributeSelectors: (elementFull, elementRow, groupAndAttribute) ->
@@ -267,14 +235,16 @@ class App.UiElement.ticket_selector
@buildOperator: (elementFull, elementRow, groupAndAttribute, elements, current_operator) ->
selection = $("")
- attributeConfig = @getElementConfig(groupAndAttribute, elements)
- for operator in attributeConfig.operator
- operatorName = App.i18n.translateInline(operator)
- selected = ''
- if current_operator is operator
- selected = 'selected="selected"'
- selection.append("")
- selection
+
+ attributeConfig = elements[groupAndAttribute]
+ if attributeConfig.operator
+ for operator in attributeConfig.operator
+ operatorName = App.i18n.translateInline(operator)
+ selected = ''
+ if current_operator is operator
+ selected = 'selected="selected"'
+ selection.append("")
+ selection
@rebuildOperater: (elementFull, elementRow, groupAndAttribute, elements, current_operator) ->
return if !groupAndAttribute
@@ -290,16 +260,43 @@ class App.UiElement.ticket_selector
@humanText: (condition) ->
none = App.i18n.translateContent('No filter.')
return [none] if _.isEmpty(condition)
+ [defaults, groups, elements] = @defaults()
rules = []
- for position of condition.attribute
+ for attribute, meta of condition
+
+ objectAttribute = attribute.split(/\./)
# get stored params
- groupAndAttribute = condition.attribute[position]
- if condition[groupAndAttribute]
+ if meta && objectAttribute[1]
selectorExists = true
- operator = condition[groupAndAttribute].operator
- value = condition[groupAndAttribute].value
- rules.push "#{App.i18n.translateContent('Where')} #{App.i18n.translateContent(groupAndAttribute)} #{App.i18n.translateContent(operator)} #{App.i18n.translateContent(value)}."
+ operator = meta.operator
+ value = meta.value
+ model = toCamelCase(objectAttribute[0])
+ modelAttribute = objectAttribute[1]
+
+ config = elements[attribute]
+
+ if modelAttribute.substr(modelAttribute.length-4,4) is '_ids'
+ modelAttribute = modelAttribute.substr(0, modelAttribute.length-4)
+ if modelAttribute.substr(modelAttribute.length-3,3) is '_id'
+ modelAttribute = modelAttribute.substr(0, modelAttribute.length-3)
+ valueHuman = []
+ if _.isArray(value)
+ for data in value
+ r = @humanTextLookup(config, data)
+ valueHuman.push r
+ else
+ valueHuman.push @humanTextLookup(config, value)
+ rules.push "#{App.i18n.translateContent('Where')} #{App.i18n.translateContent(model)} -> #{App.i18n.translateContent(toCamelCase(modelAttribute))} #{App.i18n.translateContent(operator)} #{valueHuman}."
return [none] if _.isEmpty(rules)
- rules
\ No newline at end of file
+ rules
+
+ @humanTextLookup: (config, value) ->
+ return value if !App[config.relation]
+ return value if !App[config.relation].exists(value)
+ data = App[config.relation].fullLocal(value)
+ return value if !data
+ if data.displayName
+ return App.i18n.translateContent( data.displayName() )
+ valueHuman.push App.i18n.translateContent( data.name )
diff --git a/app/assets/javascripts/app/controllers/overview.js.coffee b/app/assets/javascripts/app/controllers/overview.js.coffee
index f65589a26..b182f664d 100644
--- a/app/assets/javascripts/app/controllers/overview.js.coffee
+++ b/app/assets/javascripts/app/controllers/overview.js.coffee
@@ -22,6 +22,7 @@ class Index extends App.ControllerContent
{ name: 'New Overview', 'data-type': 'new', class: 'btn--success' }
]
container: @el.closest('.content')
+ large: true,
)
App.Config.set( 'Overview', { prio: 2300, name: 'Overviews', parent: '#manage', target: '#manage/overviews', controller: Index, role: ['Admin'] }, 'NavBarAdmin' )
\ No newline at end of file
diff --git a/app/assets/javascripts/app/models/overview.js.coffee b/app/assets/javascripts/app/models/overview.js.coffee
index 9fc733d37..759a99741 100644
--- a/app/assets/javascripts/app/models/overview.js.coffee
+++ b/app/assets/javascripts/app/models/overview.js.coffee
@@ -1,16 +1,15 @@
class App.Overview extends App.Model
- @configure 'Overview', 'name', 'link', 'prio', 'condition', 'order', 'group_by', 'view', 'user_id', 'organization_shared', 'role_id', 'order', 'group_by', 'active', 'updated_at'
+ @configure 'Overview', 'name', 'prio', 'condition', 'order', 'group_by', 'view', 'user_id', 'organization_shared', 'role_id', 'order', 'group_by', 'active', 'updated_at'
@extend Spine.Model.Ajax
@url: @apiPath + '/overviews'
@configure_attributes = [
- { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, 'null': false, 'class': 'span4' },
- { name: 'link', display: 'URL', tag: 'input', type: 'text', limit: 100, 'null': false, 'class': 'span4' },
- { name: 'role_id', display: 'Available for Role', tag: 'select', multiple: false, nulloption: true, null: false, relation: 'Role', translate: true, class: 'span4' },
- { name: 'user_id', display: 'Available for User', tag: 'select', multiple: true, nulloption: true, null: true, relation: 'User', sortBy: 'firstname', class: 'span4' },
- { name: 'organization_shared', display: 'Only available for Users with shared Organization', tag: 'select', options: { true: 'yes', false: 'no' }, default: false, null: true, 'class': 'span4' },
-# { name: 'content', display: 'Content', tag: 'textarea', limit: 250, 'null': false, 'class': 'span4' },
- { name: 'condition', display: 'Conditions for shown Tickets', tag: 'ticket_attribute_selection', null: true, class: 'span4' },
- { name: 'prio', display: 'Prio', tag: 'input', type: 'text', limit: 10, 'null': false, 'class': 'span4' },
+ { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, 'null': false },
+ { name: 'role_id', display: 'Available for Role', tag: 'select', multiple: false, nulloption: true, null: false, relation: 'Role', translate: true },
+ { name: 'user_id', display: 'Available for User', tag: 'select', multiple: true, nulloption: true, null: true, relation: 'User', sortBy: 'firstname' },
+ { name: 'organization_shared', display: 'Only available for Users with shared Organization', tag: 'select', options: { true: 'yes', false: 'no' }, default: false, null: true },
+# { name: 'content', display: 'Content', tag: 'textarea', limit: 250, 'null': false },
+ { name: 'condition', display: 'Conditions for shown Tickets', tag: 'ticket_selector', null: false },
+ { name: 'prio', display: 'Prio', tag: 'input', type: 'text', limit: 10, 'null': false },
{
name: 'view::s'
display: 'Attributes'
diff --git a/app/assets/javascripts/app/models/ticket.js.coffee b/app/assets/javascripts/app/models/ticket.js.coffee
index 587e913e9..85d021aab 100644
--- a/app/assets/javascripts/app/models/ticket.js.coffee
+++ b/app/assets/javascripts/app/models/ticket.js.coffee
@@ -11,18 +11,18 @@ class App.Ticket extends App.Model
{ name: 'title', display: 'Title', tag: 'input', type: 'text', limit: 100, null: false, parentClass: 'noTruncate' },
{ name: 'state_id', display: 'State', tag: 'select', multiple: false, null: false, relation: 'TicketState', default: 'new', style: 'width: 12%', edit: true, customer: true, },
{ name: 'priority_id', display: 'Priority', tag: 'select', multiple: false, null: false, relation: 'TicketPriority', default: '2 normal', style: 'width: 12%', edit: true, customer: true, },
+ { name: 'article_count', display: 'Article#', style: 'width: 12%' },
+ { name: 'escalation_time', display: 'Escalation', tag: 'datetime', null: true, style: 'width: 12%', class: 'escalation', parentClass: 'noTruncate' },
{ name: 'last_contact', display: 'Last contact', tag: 'datetime', null: true, style: 'width: 12%', parentClass: 'noTruncate' },
{ name: 'last_contact_agent', display: 'Last contact (Agent)', tag: 'datetime', null: true, style: 'width: 12%', parentClass: 'noTruncate' },
{ name: 'last_contact_customer', display: 'Last contact (Customer)', tag: 'datetime', null: true, style: 'width: 12%', parentClass: 'noTruncate' },
{ name: 'first_response', display: 'First response', tag: 'datetime', null: true, style: 'width: 12%', parentClass: 'noTruncate' },
{ name: 'close_time', display: 'Close time', tag: 'datetime', null: true, style: 'width: 12%', parentClass: 'noTruncate' },
{ name: 'pending_time', display: 'Pending Time', tag: 'datetime', null: true, style: 'width: 12%', parentClass: 'noTruncate' },
- { name: 'escalation_time', display: 'Escalation', tag: 'datetime', null: true, style: 'width: 12%', class: 'escalation', parentClass: 'noTruncate' },
- { name: 'article_count', display: 'Article#', style: 'width: 12%' },
- { name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
- { name: 'created_at', display: 'Created', tag: 'datetime', style: 'width: 120px', readonly: 1, parentClass: 'noTruncate' },
- { name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
- { name: 'updated_at', display: 'Updated', tag: 'datetime', style: 'width: 120px', readonly: 1, parentClass: 'noTruncate' },
+ { name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
+ { name: 'created_at', display: 'Created at', tag: 'datetime', style: 'width: 120px', readonly: 1, parentClass: 'noTruncate' },
+ { name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
+ { name: 'updated_at', display: 'Updated at', tag: 'datetime', style: 'width: 120px', readonly: 1, parentClass: 'noTruncate' },
]
uiUrl: ->
diff --git a/app/assets/javascripts/app/models/user.js.coffee b/app/assets/javascripts/app/models/user.js.coffee
index d22484a4a..b71bd496e 100644
--- a/app/assets/javascripts/app/models/user.js.coffee
+++ b/app/assets/javascripts/app/models/user.js.coffee
@@ -9,15 +9,7 @@ class App.User extends App.Model
{ name: 'firstname', display: 'Firstname', tag: 'input', type: 'text', limit: 100, null: false, signup: true, info: true, invite_agent: true },
{ name: 'lastname', display: 'Lastname', tag: 'input', type: 'text', limit: 100, null: false, signup: true, info: true, invite_agent: true },
{ name: 'email', display: 'Email', tag: 'input', type: 'email', limit: 100, null: false, signup: true, info: true, invite_agent: true },
- { name: 'web', display: 'Web', tag: 'input', type: 'url', limit: 100, null: true, signup: false, info: true },
- { name: 'phone', display: 'Phone', tag: 'input', type: 'phone', limit: 100, null: true, signup: false, info: true },
- { name: 'mobile', display: 'Mobile', tag: 'input', type: 'phone', limit: 100, null: true, signup: false, info: true },
- { name: 'fax', display: 'Fax', tag: 'input', type: 'phone', limit: 100, null: true, signup: false, info: true },
{ name: 'organization_id', display: 'Organization', tag: 'select', multiple: false, nulloption: true, null: true, relation: 'Organization', signup: false, info: true },
- { name: 'department', display: 'Department', tag: 'input', type: 'text', limit: 200, null: true, signup: false, info: true },
- { name: 'street', display: 'Street', tag: 'input', type: 'text', limit: 100, null: true, signup: false, info: true },
- { name: 'zip', display: 'Zip', tag: 'input', type: 'text', limit: 100, null: true, signup: false, info: true },
- { name: 'city', display: 'City', tag: 'input', type: 'text', limit: 100, null: true, signup: false, info: true },
{ name: 'password', display: 'Password', tag: 'input', type: 'password', limit: 50, null: true, autocomplete: 'off', signup: true, },
{ name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, null: true, info: true },
{ name: 'role_ids', display: 'Roles', tag: 'checkbox', multiple: true, null: false, relation: 'Role' },
diff --git a/app/assets/javascripts/app/views/generic/ticket_attribute_manage.jst.eco b/app/assets/javascripts/app/views/generic/ticket_attribute_manage.jst.eco
deleted file mode 100644
index d288d36f9..000000000
--- a/app/assets/javascripts/app/views/generic/ticket_attribute_manage.jst.eco
+++ /dev/null
@@ -1,5 +0,0 @@
-
\ No newline at end of file
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index b68c7c063..c68a94c38 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -208,6 +208,13 @@ function underscored (str) {
return str.trim().replace(/([a-z\d])([A-Z]+)/g, '$1_$2').replace(/[-\s]+/g, '_').toLowerCase();
}
+function toCamelCase (str) {
+ return str
+ .replace(/\s(.)/g, function($1) { return $1.toUpperCase(); })
+ .replace(/\s/g, '')
+ .replace(/^(.)/, function($1) { return $1.toUpperCase(); });
+};
+
jQuery.event.special.remove = {
remove: function(e) {
if (e.handler) e.handler();
diff --git a/app/controllers/slas_controller.rb b/app/controllers/slas_controller.rb
index b76bbf7f0..a26c37d3c 100644
--- a/app/controllers/slas_controller.rb
+++ b/app/controllers/slas_controller.rb
@@ -60,9 +60,33 @@ curl http://localhost/api/v1/slas.json -v -u #{login}:#{password}
# slas
sla_ids = []
+ models = Models.all
Sla.all.order(:name).each {|sla|
sla_ids.push sla.id
assets = sla.assets(assets)
+
+ # get assets of condition
+ sla.condition.each {|item, content|
+ attribute = item.split(/\./)
+ next if !attribute[1]
+ attribute_class = attribute[0].to_classname.constantize
+ reflection = attribute[1].sub(/_id$/, '')
+ reflection = reflection.to_sym
+ next if !models[attribute_class]
+ next if !models[attribute_class][:reflections]
+ next if !models[attribute_class][:reflections][reflection]
+ next if !models[attribute_class][:reflections][reflection].klass
+ attribute_ref_class = models[attribute_class][:reflections][reflection].klass
+ if content['value'].class == Array
+ content['value'].each {|item_id|
+ attribute_object = attribute_ref_class.find_by(id: item_id)
+ assets = attribute_object.assets(assets)
+ }
+ else
+ attribute_object = attribute_ref_class.find_by(id: content['value'])
+ assets = attribute_object.assets(assets)
+ end
+ }
}
render json: {
diff --git a/app/controllers/tickets_controller.rb b/app/controllers/tickets_controller.rb
index 90e918d39..f05d81a54 100644
--- a/app/controllers/tickets_controller.rb
+++ b/app/controllers/tickets_controller.rb
@@ -389,8 +389,14 @@ class TicketsController < ApplicationController
if params[:user_id]
user = User.find( params[:user_id] )
condition = {
- 'tickets.state_id' => Ticket::State.by_category('open'),
- 'tickets.customer_id' => user.id,
+ 'tickets.state_id' => {
+ operator: 'is',
+ value: Ticket::State.by_category('open').map(&:id),
+ },
+ 'tickets.customer_id' => {
+ operator: 'is',
+ value: user.id,
+ },
}
user_tickets_open = Ticket.search(
limit: limit,
@@ -401,8 +407,14 @@ class TicketsController < ApplicationController
# lookup closed user tickets
condition = {
- 'tickets.state_id' => Ticket::State.by_category('closed'),
- 'tickets.customer_id' => user.id,
+ 'tickets.state_id' => {
+ operator: 'is',
+ value: Ticket::State.by_category('closed').map(&:id),
+ },
+ 'tickets.customer_id' => {
+ operator: 'is',
+ value: user.id,
+ },
}
user_tickets_closed = Ticket.search(
limit: limit,
@@ -451,8 +463,14 @@ class TicketsController < ApplicationController
if params[:organization_id] && !params[:organization_id].empty?
condition = {
- 'tickets.state_id' => Ticket::State.by_category('open'),
- 'tickets.organization_id' => params[:organization_id],
+ 'tickets.state_id' => {
+ operator: 'is',
+ value: Ticket::State.by_category('open').map(&:id),
+ },
+ 'tickets.organization_id' => {
+ operator: 'is',
+ value: params[:organization_id],
+ },
}
org_tickets_open = Ticket.search(
limit: limit,
@@ -463,8 +481,14 @@ class TicketsController < ApplicationController
# lookup closed org tickets
condition = {
- 'tickets.state_id' => Ticket::State.by_category('closed'),
- 'tickets.organization_id' => params[:organization_id],
+ 'tickets.state_id' => {
+ operator: 'is',
+ value: Ticket::State.by_category('closed').map(&:id),
+ },
+ 'tickets.organization_id' => {
+ operator: 'is',
+ value: params[:organization_id],
+ },
}
org_tickets_closed = Ticket.search(
limit: limit,
diff --git a/app/models/overview.rb b/app/models/overview.rb
index 4a4b4fcf5..c2e04b1f8 100644
--- a/app/models/overview.rb
+++ b/app/models/overview.rb
@@ -6,5 +6,19 @@ class Overview < ApplicationModel
store :view
validates :name, presence: true
validates :prio, presence: true
- validates :link, presence: true
+
+ before_create :fill_link
+ before_update :fill_link
+
+ private
+
+ # fill link
+ def fill_link
+ return true if link && !link.empty?
+ self.link = name.downcase
+ link.gsub!(/\s/, '_')
+ link.gsub!(/[^0-9a-z]/i, '_')
+ link.gsub!(/_+/, '_')
+ end
+
end
diff --git a/app/models/ticket.rb b/app/models/ticket.rb
index 867dd1f5e..3845fb2c0 100644
--- a/app/models/ticket.rb
+++ b/app/models/ticket.rb
@@ -270,9 +270,10 @@ returns
def self.selectors(selectors, limit = 10)
return if !selectors
- query, bind_params = _selectors(selectors)
- ticket_count = Ticket.where(query, *bind_params).count
- tickets = Ticket.where(query, *bind_params).limit(limit)
+ query, bind_params, tables = _selectors(selectors)
+ return [] if !query
+ ticket_count = Ticket.where(query, *bind_params).joins(tables).count
+ tickets = Ticket.where(query, *bind_params).joins(tables).limit(limit)
[ticket_count, tickets]
end
@@ -281,6 +282,15 @@ returns
query = ''
bind_params = []
+ tables = []
+ selectors.each {|attribute, selector|
+ selector = attribute.split(/\./)
+ next if !selector[1]
+ next if selector[0] == 'ticket'
+ next if tables.include?(selector[0])
+ tables.push selector[0].to_sym
+ }
+
selectors.each {|attribute, selector|
if query != ''
query += ' AND '
@@ -289,7 +299,9 @@ returns
next if !selector.respond_to?(:key?)
next if !selector['operator']
return nil if !selector['value']
- return nil if selector['value'].respond_to?(:key?) && selector['value'].empty?
+ return nil if selector['value'].respond_to?(:empty?) && selector['value'].empty?
+ attributes = attribute.split(/\./)
+ attribute = "#{attributes[0]}s.#{attributes[1]}"
if selector['operator'] == 'is'
query += "#{attribute} IN (?)"
bind_params.push selector['value']
@@ -304,14 +316,23 @@ returns
query += "#{attribute} NOT LIKE (?)"
value = "%#{selector['value']}%"
bind_params.push value
- elsif selector['operator'] == 'before'
- query += "#{attribute} <= (?)"
+ elsif selector['operator'] == 'before (absolute)'
+ query += "#{attribute} <= ?"
bind_params.push selector['value']
+ elsif selector['operator'] == 'after (absolute)'
+ query += "#{attribute} >= ?"
+ bind_params.push selector['value']
+ elsif selector['operator'] == 'before (relative)'
+ query += "#{attribute} <= ?"
+ bind_params.push Time.zone.now - selector['value'].to_i.minutes
+ elsif selector['operator'] == 'after (relative)'
+ query += "#{attribute} >= ?"
+ bind_params.push Time.zone.now + selector['value'].to_i.minutes
else
fail "Invalid operator '#{selector['operator']}' for '#{selector['value'].inspect}'"
end
}
- [query, bind_params]
+ [query, bind_params, tables]
end
private
diff --git a/app/models/ticket/overviews.rb b/app/models/ticket/overviews.rb
index de346ddc6..c9b7eca7d 100644
--- a/app/models/ticket/overviews.rb
+++ b/app/models/ticket/overviews.rb
@@ -39,16 +39,16 @@ returns
selected overview by user
result = Ticket::Overviews.list(
- :current_user => User.find(123),
- :view => 'some_view_url',
+ current_user: User.find(123),
+ view: 'some_view_url',
)
returns
result = {
- :tickets => tickets, # [ticket1, ticket2, ticket3]
- :tickets_count => tickets_count, # count of tickets
- :overview => overview_selected_raw, # overview attributes
+ tickets: tickets, # [ticket1, ticket2, ticket3]
+ tickets_count: tickets_count, # count of tickets
+ overview: overview_selected_raw, # overview attributes
}
=end
@@ -71,18 +71,29 @@ returns
end
# replace e.g. 'current_user.id' with current_user.id
- overview.condition.each { |item, value|
+ overview.condition.each { |attribute, content|
+ next if !content
+ next if !content.respond_to?(:key?)
+ next if !content['value']
+ next if content['value'].class != String && content['value'].class != Array
- next if !value
- next if value.class.to_s != 'String'
+ if content['value'].class == String
+ parts = content['value'].split( '.', 2 )
+ next if !parts[0]
+ next if !parts[1]
+ next if parts[0] != 'current_user'
+ overview.condition[attribute]['value'] = data[:current_user][parts[1].to_sym]
+ next
+ end
- parts = value.split( '.', 2 )
-
- next if !parts[0]
- next if !parts[1]
- next if parts[0] != 'current_user'
-
- overview.condition[item] = data[:current_user][parts[1].to_sym]
+ content['value'].each {|item|
+ next if item.class != String
+ parts = item.split('.', 2)
+ next if !parts[0]
+ next if !parts[1]
+ next if parts[0] != 'current_user'
+ item = data[:current_user][parts[1].to_sym]
+ }
}
}
@@ -90,37 +101,8 @@ returns
fail "No such view '#{data[:view]}'"
end
- # sortby
- # prio
- # state
- # group
- # customer
-
- # order
- # asc
- # desc
-
- # groupby
- # prio
- # state
- # group
- # customer
-
- # all = attributes[:myopenassigned]
- # all.merge( { :group_id => groups } )
-
- # @tickets = Ticket.where(:group_id => groups, attributes[:myopenassigned] ).limit(params[:limit])
# get only tickets with permissions
- if data[:current_user].role?('Customer')
- group_ids = Group.select( 'groups.id' )
- .where( 'groups.active = ?', true )
- .map( &:id )
- else
- group_ids = Group.select( 'groups.id' ).joins(:users)
- .where( 'groups_users.user_id = ?', [ data[:current_user].id ] )
- .where( 'groups.active = ?', true )
- .map( &:id )
- end
+ access_condition = Ticket.access_condition( data[:current_user] )
# overview meta for navbar
if !overview_selected
@@ -129,8 +111,10 @@ returns
result = []
overviews.each { |overview|
+ query_condition, bind_condition = Ticket._selectors(overview.condition)
+
# get count
- count = Ticket.where( group_id: group_ids ).where( _condition( overview.condition ) ).count()
+ count = Ticket.where( access_condition ).where( query_condition, *bind_condition ).count()
# get meta info
all = {
@@ -151,9 +135,12 @@ returns
if overview_selected.group_by && !overview_selected.group_by.empty?
order_by = overview_selected.group_by + '_id, ' + order_by
end
- tickets = Ticket.select( 'id' )
- .where( group_id: group_ids )
- .where( _condition( overview_selected.condition ) )
+
+ query_condition, bind_condition = Ticket._selectors(overview_selected.condition)
+
+ tickets = Ticket.select('id')
+ .where( access_condition )
+ .where( query_condition, *bind_condition )
.order( order_by )
.limit( 500 )
@@ -162,9 +149,7 @@ returns
ticket_ids.push ticket.id
}
- tickets_count = Ticket.where( group_id: group_ids )
- .where( _condition( overview_selected.condition ) )
- .count()
+ tickets_count = Ticket.where( access_condition ).where( query_condition, *bind_condition ).count()
return {
ticket_ids: ticket_ids,
@@ -175,15 +160,12 @@ returns
# get tickets for overview
data[:start_page] ||= 1
- tickets = Ticket.where( group_id: group_ids )
- .where( _condition( overview_selected.condition ) )
+ query_condition, bind_condition = Ticket._selectors(overview_selected.condition)
+ tickets = Ticket.where( access_condition )
+ .where( query_condition, *bind_condition )
.order( overview_selected[:order][:by].to_s + ' ' + overview_selected[:order][:direction].to_s )
- # .limit( overview_selected.view[ data[:view_mode].to_sym ][:per_page] )
- # .offset( overview_selected.view[ data[:view_mode].to_sym ][:per_page].to_i * ( data[:start_page].to_i - 1 ) )
- tickets_count = Ticket.where( group_id: group_ids )
- .where( _condition( overview_selected.condition ) )
- .count()
+ tickets_count = Ticket.where( access_condition ).where( query_condition, *bind_condition ).count()
{
tickets: tickets,
@@ -192,64 +174,4 @@ returns
}
end
- private
-
- def self._condition(condition)
- sql = ''
- bind = [nil]
- condition.each {|key, value|
- if sql != ''
- sql += ' AND '
- end
- if value.class == Array
- sql += " #{key} IN (?)"
- bind.push value
- elsif value.class == Hash || value.class == ActiveSupport::HashWithIndifferentAccess
- time = Time.zone.now
- if value['area'] == 'minute'
- if value['direction'] == 'last'
- time -= value['count'].to_i * 60
- else
- time += value['count'].to_i * 60
- end
- elsif value['area'] == 'hour'
- if value['direction'] == 'last'
- time -= value['count'].to_i * 60 * 60
- else
- time += value['count'].to_i * 60 * 60
- end
- elsif value['area'] == 'day'
- if value['direction'] == 'last'
- time -= value['count'].to_i * 60 * 60 * 24
- else
- time += value['count'].to_i * 60 * 60 * 24
- end
- elsif value['area'] == 'month'
- if value['direction'] == 'last'
- time -= value['count'].to_i * 60 * 60 * 24 * 31
- else
- time += value['count'].to_i * 60 * 60 * 24 * 31
- end
- elsif value['area'] == 'year'
- if value['direction'] == 'last'
- time -= value['count'].to_i * 60 * 60 * 24 * 365
- else
- time += value['count'].to_i * 60 * 60 * 24 * 365
- end
- end
- if value['direction'] == 'last'
- sql += " #{key} > ?"
- bind.push time
- else
- sql += " #{key} < ?"
- bind.push time
- end
- else
- sql += " #{key} = ?"
- bind.push value
- end
- }
- bind[0] = sql
- bind
- end
end
diff --git a/app/models/ticket/search.rb b/app/models/ticket/search.rb
index e045b3c79..b46848a48 100644
--- a/app/models/ticket/search.rb
+++ b/app/models/ticket/search.rb
@@ -59,14 +59,20 @@ search tickets via database
result = Ticket.search(
current_user: User.find(123),
condition: {
- 'tickets.owner_id' => user.id,
- 'tickets.state_id' => Ticket::State.where(
- state_type_id: Ticket::StateType.where(
- name: [
- 'pending reminder',
- 'pending action',
- ],
- ),
+ 'tickets.owner_id' => {
+ operator: 'is',
+ value: user.id,
+ },
+ 'tickets.state_id' => {
+ operator: 'is',
+ value: Ticket::State.where(
+ state_type_id: Ticket::StateType.where(
+ name: [
+ 'pending reminder',
+ 'pending action',
+ ],
+ ).map(&:id),
+ },
),
},
limit: 15,
@@ -154,9 +160,10 @@ returns
.order('`tickets`.`created_at` DESC')
.limit(limit)
else
+ query_condition, bind_condition = _selectors(params[:condition])
tickets_all = Ticket.select('DISTINCT(tickets.id)')
.where(access_condition)
- .where(params[:condition])
+ .where(query_condition, *bind_condition)
.order('`tickets`.`created_at` DESC')
.limit(limit)
end
diff --git a/db/migrate/20150973000001_update_overview3.rb b/db/migrate/20150973000001_update_overview3.rb
new file mode 100644
index 000000000..c35efdb5a
--- /dev/null
+++ b/db/migrate/20150973000001_update_overview3.rb
@@ -0,0 +1,218 @@
+class UpdateOverview3 < ActiveRecord::Migration
+ def up
+ UserInfo.current_user_id = 1
+ overview_role = Role.where( name: 'Agent' ).first
+ Overview.create_or_update(
+ name: 'My assigned Tickets',
+ link: 'my_assigned',
+ prio: 1000,
+ role_id: overview_role.id,
+ condition: {
+ 'ticket.state_id' => {
+ operator: 'is',
+ value: [ 1, 2, 3, 7 ],
+ },
+ 'ticket.owner_id' => {
+ operator: 'is',
+ value: 'current_user.id',
+ },
+ },
+ order: {
+ by: 'created_at',
+ direction: 'ASC',
+ },
+ view: {
+ d: %w(title customer group created_at),
+ s: %w(title customer group created_at),
+ m: %w(number title customer group created_at),
+ view_mode_default: 's',
+ },
+ )
+
+ Overview.create_or_update(
+ name: 'My pending reached Tickets',
+ link: 'my_pending_reached',
+ prio: 1010,
+ role_id: overview_role.id,
+ condition: {
+ 'ticket.state_id' => {
+ operator: 'is',
+ value: 3,
+ },
+ 'ticket.owner_id' => {
+ operator: 'is',
+ value: 'current_user.id',
+ },
+ 'ticket.pending_time' => {
+ operator: 'after (relative)',
+ value: '1',
+ },
+ },
+ order: {
+ by: 'created_at',
+ direction: 'ASC',
+ },
+ view: {
+ d: %w(title customer group created_at),
+ s: %w(title customer group created_at),
+ m: %w(number title customer group created_at),
+ view_mode_default: 's',
+ },
+ )
+
+ Overview.create_or_update(
+ name: 'Unassigned & Open Tickets',
+ link: 'all_unassigned',
+ prio: 1020,
+ role_id: overview_role.id,
+ condition: {
+ 'ticket.state_id' => {
+ operator: 'is',
+ value: [1, 2, 3],
+ },
+ 'ticket.owner_id' => {
+ operator: 'is',
+ value: 1,
+ },
+ },
+ order: {
+ by: 'created_at',
+ direction: 'ASC',
+ },
+ view: {
+ d: %w(title customer group created_at),
+ s: %w(title customer group created_at),
+ m: %w(number title customer group created_at),
+ view_mode_default: 's',
+ },
+ )
+
+ Overview.create_or_update(
+ name: 'All Open Tickets',
+ link: 'all_open',
+ prio: 1030,
+ role_id: overview_role.id,
+ condition: {
+ 'ticket.state_id' => {
+ operator: 'is',
+ value: [1, 2, 3],
+ },
+ },
+ order: {
+ by: 'created_at',
+ direction: 'ASC',
+ },
+ view: {
+ d: %w(title customer group state owner created_at),
+ s: %w(title customer group state owner created_at),
+ m: %w(number title customer group state owner created_at),
+ view_mode_default: 's',
+ },
+ )
+
+ Overview.create_or_update(
+ name: 'All pending reached Tickets',
+ link: 'all_pending_reached',
+ prio: 1035,
+ role_id: overview_role.id,
+ condition: {
+ 'ticket.state_id' => {
+ operator: 'is',
+ value: [3],
+ },
+ 'ticket.pending_time' => {
+ operator: 'after (relative)',
+ value: 1,
+ },
+ },
+ order: {
+ by: 'created_at',
+ direction: 'ASC',
+ },
+ view: {
+ d: %w(title customer group owner created_at),
+ s: %w(title customer group owner created_at),
+ m: %w(number title customer group owner created_at),
+ view_mode_default: 's',
+ },
+ )
+
+ Overview.create_or_update(
+ name: 'Escalated Tickets',
+ link: 'all_escalated',
+ prio: 1040,
+ role_id: overview_role.id,
+ condition: {
+ 'ticket.escalation_time' => {
+ operator: 'before (relative)',
+ value: 5,
+ },
+ },
+ order: {
+ by: 'escalation_time',
+ direction: 'ASC',
+ },
+ view: {
+ d: %w(title customer group owner escalation_time),
+ s: %w(title customer group owner escalation_time),
+ m: %w(number title customer group owner escalation_time),
+ view_mode_default: 's',
+ },
+ )
+
+ overview_role = Role.where( name: 'Customer' ).first
+ Overview.create_or_update(
+ name: 'My Tickets',
+ link: 'my_tickets',
+ prio: 1000,
+ role_id: overview_role.id,
+ condition: {
+ 'ticket.state_id' => {
+ operator: 'is',
+ value: [ 1, 2, 3, 4, 6 ],
+ },
+ 'ticket.customer_id' => {
+ operator: 'is',
+ value: 'current_user.id',
+ },
+ },
+ order: {
+ by: 'created_at',
+ direction: 'DESC',
+ },
+ view: {
+ d: %w(title customer state created_at),
+ s: %w(number title state created_at),
+ m: %w(number title state created_at),
+ view_mode_default: 's',
+ },
+ )
+ Overview.create_or_update(
+ name: 'My Organization Tickets',
+ link: 'my_organization_tickets',
+ prio: 1100,
+ role_id: overview_role.id,
+ organization_shared: true,
+ condition: {
+ 'ticket.state_id' => {
+ operator: 'is',
+ value: [ 1, 2, 3, 4, 6 ],
+ },
+ 'ticket.organization_id' => {
+ operator: 'is',
+ value: 'current_user.organization_id',
+ },
+ },
+ order: {
+ by: 'created_at',
+ direction: 'DESC',
+ },
+ view: {
+ d: %w(title customer state created_at),
+ s: %w(number title customer state created_at),
+ m: %w(number title customer state created_at),
+ view_mode_default: 's',
+ },
+ )
+ end
+end
diff --git a/db/seeds.rb b/db/seeds.rb
index 91fd2c55c..ee830cb76 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -1575,8 +1575,14 @@ Overview.create_if_not_exists(
prio: 1000,
role_id: overview_role.id,
condition: {
- 'tickets.state_id' => [ 1, 2, 3, 7 ],
- 'tickets.owner_id' => 'current_user.id',
+ 'ticket.state_id' => {
+ operator: 'is',
+ value: [ 1, 2, 3, 7 ],
+ },
+ 'ticket.owner_id' => {
+ operator: 'is',
+ value: current_user.id,
+ },
},
order: {
by: 'created_at',
@@ -1596,9 +1602,18 @@ Overview.create_if_not_exists(
prio: 1010,
role_id: overview_role.id,
condition: {
- 'tickets.state_id' => [3],
- 'tickets.owner_id' => 'current_user.id',
- 'tickets.pending_time' => { 'direction' => 'before', 'count' => 1, 'area' => 'minute' },
+ 'ticket.state_id' => {
+ operator: 'is',
+ value: 3,
+ },
+ 'ticket.owner_id' => {
+ operator: 'is',
+ value: 'current_user.id',
+ },
+ 'ticket.pending_time' => {
+ operator: 'after (relative)',
+ value: '1',
+ },
},
order: {
by: 'created_at',
@@ -1618,8 +1633,14 @@ Overview.create_if_not_exists(
prio: 1020,
role_id: overview_role.id,
condition: {
- 'tickets.state_id' => [1, 2, 3],
- 'tickets.owner_id' => 1,
+ 'ticket.state_id' => {
+ operator: 'is',
+ value: [1, 2, 3],
+ },
+ 'ticket.owner_id' => {
+ operator: 'is',
+ value: 1,
+ },
},
order: {
by: 'created_at',
@@ -1639,7 +1660,10 @@ Overview.create_if_not_exists(
prio: 1030,
role_id: overview_role.id,
condition: {
- 'tickets.state_id' => [1, 2, 3],
+ 'ticket.state_id' => {
+ operator: 'is',
+ value: [1, 2, 3],
+ },
},
order: {
by: 'created_at',
@@ -1659,8 +1683,14 @@ Overview.create_if_not_exists(
prio: 1035,
role_id: overview_role.id,
condition: {
- 'tickets.state_id' => [3],
- 'tickets.pending_time' => { 'direction' => 'before', 'count' => 1, 'area' => 'minute' },
+ 'ticket.state_id' => {
+ operator: 'is',
+ value: [3],
+ },
+ 'ticket.pending_time' => {
+ operator: 'after (relative)',
+ value: 1,
+ },
},
order: {
by: 'created_at',
@@ -1680,7 +1710,10 @@ Overview.create_if_not_exists(
prio: 1040,
role_id: overview_role.id,
condition: {
- 'tickets.escalation_time' => { 'direction' => 'before', 'count' => 5, 'area' => 'minute' },
+ 'ticket.escalation_time' => {
+ operator: 'before (relative)',
+ value: 5,
+ },
},
order: {
by: 'escalation_time',
@@ -1701,8 +1734,14 @@ Overview.create_if_not_exists(
prio: 1000,
role_id: overview_role.id,
condition: {
- 'tickets.state_id' => [ 1, 2, 3, 4, 6 ],
- 'tickets.customer_id' => 'current_user.id',
+ 'ticket.state_id' => {
+ operator: 'is',
+ value: [ 1, 2, 3, 4, 6 ],
+ },
+ 'ticket.customer_id' => {
+ operator: 'is',
+ value: 'current_user.id',
+ },
},
order: {
by: 'created_at',
@@ -1722,8 +1761,14 @@ Overview.create_if_not_exists(
role_id: overview_role.id,
organization_shared: true,
condition: {
- 'tickets.state_id' => [ 1, 2, 3, 4, 6 ],
- 'tickets.organization_id' => 'current_user.organization_id',
+ 'ticket.state_id' => {
+ operator: 'is',
+ value: [ 1, 2, 3, 4, 6 ],
+ },
+ 'ticket.organization_id' => {
+ operator: 'is',
+ value: 'current_user.organization_id',
+ },
},
order: {
by: 'created_at',
diff --git a/lib/calendar_subscriptions/tickets.rb b/lib/calendar_subscriptions/tickets.rb
index ec74342d8..99a910ca7 100644
--- a/lib/calendar_subscriptions/tickets.rb
+++ b/lib/calendar_subscriptions/tickets.rb
@@ -46,12 +46,18 @@ class CalendarSubscriptions::Tickets
return events_data if owner_ids.empty?
condition = {
- 'tickets.owner_id' => owner_ids,
- 'tickets.state_id' => Ticket::State.where(
- state_type_id: Ticket::StateType.where(
- name: %w(new open),
- ),
- ),
+ 'tickets.owner_id' => {
+ operator: 'is',
+ value: owner_ids,
+ },
+ 'tickets.state_id' => {
+ operator: 'is',
+ value: Ticket::State.where(
+ state_type_id: Ticket::StateType.where(
+ name: %w(new open),
+ ),
+ ).map(&:id),
+ },
}
tickets = Ticket.search(
@@ -87,15 +93,21 @@ class CalendarSubscriptions::Tickets
return events_data if owner_ids.empty?
condition = {
- 'tickets.owner_id' => owner_ids,
- 'tickets.state_id' => Ticket::State.where(
- state_type_id: Ticket::StateType.where(
- name: [
- 'pending reminder',
- 'pending action',
- ],
- ),
- ),
+ 'tickets.owner_id' => {
+ operator: 'is',
+ value: owner_ids,
+ },
+ 'tickets.state_id' => {
+ operator: 'is',
+ value: Ticket::State.where(
+ state_type_id: Ticket::StateType.where(
+ name: [
+ 'pending reminder',
+ 'pending action',
+ ],
+ ),
+ ).map(&:id),
+ },
}
tickets = Ticket.search(
@@ -137,9 +149,16 @@ class CalendarSubscriptions::Tickets
owner_ids = owner_ids(:escalation)
return events_data if owner_ids.empty?
- condition = [
- 'tickets.owner_id IN (?) AND tickets.escalation_time IS NOT NULL', owner_ids
- ]
+ condition = {
+ 'tickets.owner_id' => {
+ operator: 'is',
+ value: owner_ids,
+ },
+ 'tickets.escalation_time' => {
+ operator: 'is not',
+ value: nil,
+ }
+ }
tickets = Ticket.search(
current_user: @user,