Moved to new query condition syntax to support is, is not, contains, contains not, ...

This commit is contained in:
Martin Edenhofer 2015-09-17 20:39:51 +02:00
parent 3d47199ec5
commit d825cc288b
16 changed files with 618 additions and 333 deletions

View file

@ -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 label=\"#{displayName}\" class=\"js-#{groupKey}\"></optgroup>")
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("<option value=\"#{groupKey}.#{attributeName}\">#{displayName}</option>")
spacer = elementKey.split(/\./)
if spacer[0] is groupKey
attributeConfig = elements[elementKey]
if attributeConfig.operator
displayName = App.i18n.translateInline(attributeConfig.display)
optgroup.append("<option value=\"#{elementKey}\">#{displayName}</option>")
selection
@rebuildAttributeSelectors: (elementFull, elementRow, groupAndAttribute) ->
@ -267,14 +235,16 @@ class App.UiElement.ticket_selector
@buildOperator: (elementFull, elementRow, groupAndAttribute, elements, current_operator) ->
selection = $("<select class=\"form-control\" name=\"condition::#{groupAndAttribute}::operator\"></select>")
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("<option value=\"#{operator}\" #{selected}>#{operatorName}</option>")
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("<option value=\"#{operator}\" #{selected}>#{operatorName}</option>")
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')} <b>#{App.i18n.translateContent(groupAndAttribute)}</b> #{App.i18n.translateContent(operator)} <b>#{App.i18n.translateContent(value)}</b>."
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')} <b>#{App.i18n.translateContent(model)} -> #{App.i18n.translateContent(toCamelCase(modelAttribute))}</b> #{App.i18n.translateContent(operator)} <b>#{valueHuman}</b>."
return [none] if _.isEmpty(rules)
rules
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 )

View file

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

View file

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

View file

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

View file

@ -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' },

View file

@ -1,5 +0,0 @@
<div class="sub_attribute well">
<div class="ticket_attribute_item"></div>
<hr>
<div class="ticket_attribute_list"></div>
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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