From d825cc288b5c41487421e2d6523ae98b7b826d83 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Thu, 17 Sep 2015 20:39:51 +0200 Subject: [PATCH] Moved to new query condition syntax to support is, is not, contains, contains not, ... --- .../_ui_element/ticket_selector.js.coffee | 253 +++++++++--------- .../app/controllers/overview.js.coffee | 1 + .../javascripts/app/models/overview.js.coffee | 17 +- .../javascripts/app/models/ticket.js.coffee | 12 +- .../javascripts/app/models/user.js.coffee | 8 - .../generic/ticket_attribute_manage.jst.eco | 5 - app/assets/javascripts/application.js | 7 + app/controllers/slas_controller.rb | 24 ++ app/controllers/tickets_controller.rb | 40 ++- app/models/overview.rb | 16 +- app/models/ticket.rb | 35 ++- app/models/ticket/overviews.rb | 160 +++-------- app/models/ticket/search.rb | 25 +- db/migrate/20150973000001_update_overview3.rb | 218 +++++++++++++++ db/seeds.rb | 75 ++++-- lib/calendar_subscriptions/tickets.rb | 55 ++-- 16 files changed, 618 insertions(+), 333 deletions(-) delete mode 100644 app/assets/javascripts/app/views/generic/ticket_attribute_manage.jst.eco create mode 100644 db/migrate/20150973000001_update_overview3.rb 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,