Overviews with multi role support.

This commit is contained in:
Martin Edenhofer 2017-04-19 21:54:04 +02:00
parent 8aed86bbf0
commit f0f4f278c3
13 changed files with 171 additions and 121 deletions

View file

@ -190,9 +190,8 @@ class App.ControllerTable extends App.Controller
attribute.displayWidth = value attribute.displayWidth = value
@headers.push attribute @headers.push attribute
else else
# e.g. column: owner_id # e.g. column: owner_id or owner_ids
rowWithoutId = item + '_id' if attributeName is "#{item}_id" || attributeName is "#{item}_ids"
if attributeName is rowWithoutId
headerFound = true headerFound = true
if @headerWidth[attribute.name] if @headerWidth[attribute.name]
attribute.displayWidth = @headerWidth[attribute.name] * @availableWidth attribute.displayWidth = @headerWidth[attribute.name] * @availableWidth
@ -350,7 +349,7 @@ class App.ControllerTable extends App.Controller
for headerName in @headers for headerName in @headers
if !hit if !hit
position += 1 position += 1
if headerName.name is name || headerName.name is "#{name}_id" if headerName.name is name || headerName.name is "#{name}_id" || headerName.name is "#{name}_ids"
hit = true hit = true
if hit if hit

View file

@ -179,9 +179,14 @@ class App extends Spine.Controller
return '-' if item is undefined return '-' if item is undefined
return '-' if item is '' return '-' if item is ''
return item if item is null return item if item is null
result = item result = ''
items = [item]
if _.isArray(item)
items = item
# lookup relation # lookup relation
for item in items
resultLocal = item
if attributeConfig.relation || valueRef if attributeConfig.relation || valueRef
if valueRef if valueRef
item = valueRef item = valueRef
@ -193,21 +198,21 @@ class App extends Spine.Controller
if typeof item is 'object' if typeof item is 'object'
isObject = true isObject = true
if item.displayNameLong if item.displayNameLong
result = item.displayNameLong() resultLocal = item.displayNameLong()
else if item.displayName else if item.displayName
result = item.displayName() resultLocal = item.displayName()
else else
result = item.name resultLocal = item.name
# execute callback on content # execute callback on content
if attributeConfig.callback if attributeConfig.callback
result = attributeConfig.callback(result, attributeConfig) resultLocal = attributeConfig.callback(resultLocal, attributeConfig)
# text2html in textarea view # text2html in textarea view
isHtmlEscape = false isHtmlEscape = false
if attributeConfig.tag is 'textarea' if attributeConfig.tag is 'textarea'
isHtmlEscape = true isHtmlEscape = true
result = App.Utils.text2html(result) resultLocal = App.Utils.text2html(resultLocal)
# remember, html snippets are already escaped # remember, html snippets are already escaped
else if attributeConfig.tag is 'richtext' else if attributeConfig.tag is 'richtext'
@ -215,49 +220,53 @@ class App extends Spine.Controller
# fillup options # fillup options
if !_.isEmpty(attributeConfig.options) if !_.isEmpty(attributeConfig.options)
if attributeConfig.options[result] if attributeConfig.options[resultLocal]
result = attributeConfig.options[result] resultLocal = attributeConfig.options[resultLocal]
# transform boolean # transform boolean
if attributeConfig.tag is 'boolean' if attributeConfig.tag is 'boolean'
if result is true if resultLocal is true
result = 'yes' resultLocal = 'yes'
else if result is false else if resultLocal is false
result = 'no' resultLocal = 'no'
# translate content # translate content
if attributeConfig.translate || (isObject && item.translate && item.translate()) if attributeConfig.translate || (isObject && item.translate && item.translate())
isHtmlEscape = true isHtmlEscape = true
result = App.i18n.translateContent(result) resultLocal = App.i18n.translateContent(resultLocal)
# transform date # transform date
if attributeConfig.tag is 'date' if attributeConfig.tag is 'date'
isHtmlEscape = true isHtmlEscape = true
result = App.i18n.translateDate(result) resultLocal = App.i18n.translateDate(resultLocal)
# transform input tel|url to make it clickable # transform input tel|url to make it clickable
if attributeConfig.tag is 'input' if attributeConfig.tag is 'input'
if attributeConfig.type is 'tel' if attributeConfig.type is 'tel'
result = "<a href=\"#{App.Utils.phoneify(result)}\">#{App.Utils.htmlEscape(result)}</a>" resultLocal = "<a href=\"#{App.Utils.phoneify(resultLocal)}\">#{App.Utils.htmlEscape(resultLocal)}</a>"
else if attributeConfig.type is 'url' else if attributeConfig.type is 'url'
result = App.Utils.linkify(result) resultLocal = App.Utils.linkify(resultLocal)
else else
result = App.Utils.htmlEscape(result) resultLocal = App.Utils.htmlEscape(resultLocal)
isHtmlEscape = true isHtmlEscape = true
# use pretty time for datetime # use pretty time for datetime
else if attributeConfig.tag is 'datetime' else if attributeConfig.tag is 'datetime'
isHtmlEscape = true isHtmlEscape = true
timestamp = App.i18n.translateTimestamp(result) timestamp = App.i18n.translateTimestamp(resultLocal)
escalation = false escalation = false
cssClass = attributeConfig.class || '' cssClass = attributeConfig.class || ''
if cssClass.match 'escalation' if cssClass.match 'escalation'
escalation = true escalation = true
humanTime = App.PrettyDate.humanTime(result, escalation) humanTime = App.PrettyDate.humanTime(resultLocal, escalation)
result = "<time class=\"humanTimeFromNow #{cssClass}\" data-time=\"#{result}\" title=\"#{timestamp}\">#{humanTime}</time>" resultLocal = "<time class=\"humanTimeFromNow #{cssClass}\" data-time=\"#{resultLocal}\" title=\"#{timestamp}\">#{humanTime}</time>"
if !isHtmlEscape && typeof result is 'string' if !isHtmlEscape && typeof resultLocal is 'string'
result = App.Utils.htmlEscape(result) resultLocal = App.Utils.htmlEscape(resultLocal)
if !_.isEmpty(result)
result += ', '
result += resultLocal
result result

View file

@ -1,11 +1,11 @@
class App.Overview extends App.Model class App.Overview extends App.Model
@configure 'Overview', 'name', 'prio', 'condition', 'order', 'group_by', 'view', 'user_ids', 'organization_shared', 'role_id', 'order', 'group_by', 'active', 'updated_at' @configure 'Overview', 'name', 'prio', 'condition', 'order', 'group_by', 'view', 'user_ids', 'organization_shared', 'role_ids', 'order', 'group_by', 'active', 'updated_at'
@extend Spine.Model.Ajax @extend Spine.Model.Ajax
@url: @apiPath + '/overviews' @url: @apiPath + '/overviews'
@configure_attributes = [ @configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, 'null': false }, { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, 'null': false },
{ name: 'link', display: 'Link', readonly: 1 }, { name: 'link', display: 'Link', readonly: 1 },
{ name: 'role_id', display: 'Available for Role', tag: 'select', multiple: false, nulloption: true, null: false, relation: 'Role', translate: true }, { name: 'role_ids', display: 'Available for Role', tag: 'column_select', multiple: true, nulloption: true, null: false, relation: 'Role', translate: true },
{ name: 'user_ids', display: 'Available for User', tag: 'column_select', multiple: true, nulloption: false, null: true, relation: 'User', sortBy: 'firstname' }, { name: 'user_ids', display: 'Available for User', tag: 'column_select', multiple: true, nulloption: false, 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: 'organization_shared', display: 'Only available for Users with shared Organization', tag: 'select', options: { true: 'yes', false: 'no' }, default: false, null: true },
{ name: 'condition', display: 'Conditions for shown Tickets', tag: 'ticket_selector', null: false }, { name: 'condition', display: 'Conditions for shown Tickets', tag: 'ticket_selector', null: false },
@ -62,7 +62,7 @@ class App.Overview extends App.Model
@configure_overview = [ @configure_overview = [
'name', 'name',
'link', 'link',
'role', 'role_ids',
] ]
@description = ''' @description = '''

View file

@ -7,6 +7,7 @@ class Overview < ApplicationModel
load 'overview/assets.rb' load 'overview/assets.rb'
include Overview::Assets include Overview::Assets
has_and_belongs_to_many :roles, after_add: :cache_update, after_remove: :cache_update, class_name: 'Role'
has_and_belongs_to_many :users, after_add: :cache_update, after_remove: :cache_update has_and_belongs_to_many :users, after_add: :cache_update, after_remove: :cache_update
store :condition store :condition
store :order store :order

View file

@ -19,11 +19,12 @@ returns
current_user = data[:current_user] current_user = data[:current_user]
# get customer overviews # get customer overviews
role_ids = User.joins(:roles).where(users: { id: current_user.id, active: true }, roles: { active: true }).pluck('roles.id')
if current_user.permissions?('ticket.customer') if current_user.permissions?('ticket.customer')
overviews = if current_user.organization_id && current_user.organization.shared overviews = if current_user.organization_id && current_user.organization.shared
Overview.where(role_id: current_user.role_ids, active: true).order(:prio) Overview.joins(:roles).where(overviews_roles: { role_id: role_ids }, overviews: { active: true }).distinct('overview.id').order(:prio)
else else
Overview.where(role_id: current_user.role_ids, organization_shared: false, active: true).order(:prio) Overview.joins(:roles).where(overviews_roles: { role_id: role_ids }, overviews: { active: true, organization_shared: false }).distinct('overview.id').order(:prio)
end end
overviews_list = [] overviews_list = []
overviews.each { |overview| overviews.each { |overview|
@ -36,7 +37,7 @@ returns
# get agent overviews # get agent overviews
return [] if !current_user.permissions?('ticket.agent') return [] if !current_user.permissions?('ticket.agent')
overviews = Overview.where(role_id: current_user.role_ids, active: true).order(:prio) overviews = Overview.joins(:roles).where(overviews_roles: { role_id: role_ids }, overviews: { active: true }).distinct('overview.id').order(:prio)
overviews_list = [] overviews_list = []
overviews.each { |overview| overviews.each { |overview|
user_ids = overview.user_ids user_ids = overview.user_ids

View file

@ -196,7 +196,6 @@ class CreateTicket < ActiveRecord::Migration
add_index :ticket_counters, [:generator], unique: true add_index :ticket_counters, [:generator], unique: true
create_table :overviews do |t| create_table :overviews do |t|
t.references :role, null: false
t.column :name, :string, limit: 250, null: false t.column :name, :string, limit: 250, null: false
t.column :link, :string, limit: 250, null: false t.column :link, :string, limit: 250, null: false
t.column :prio, :integer, null: false t.column :prio, :integer, null: false
@ -212,6 +211,13 @@ class CreateTicket < ActiveRecord::Migration
end end
add_index :overviews, [:name] add_index :overviews, [:name]
create_table :overviews_roles, id: false do |t|
t.integer :overview_id
t.integer :role_id
end
add_index :overviews_roles, [:overview_id]
add_index :overviews_roles, [:role_id]
create_table :overviews_users, id: false do |t| create_table :overviews_users, id: false do |t|
t.integer :overview_id t.integer :overview_id
t.integer :user_id t.integer :user_id

View file

@ -0,0 +1,23 @@
class OverviewRoleIds < ActiveRecord::Migration
def up
# return if it's a new setup
return if !Setting.find_by(name: 'system_init_done')
create_table :overviews_roles, id: false do |t|
t.integer :overview_id
t.integer :role_id
end
add_index :overviews_roles, [:overview_id]
add_index :overviews_roles, [:role_id]
Overview.connection.schema_cache.clear!
Overview.reset_column_information
Overview.all.each { |overview|
next if overview.role_id.blank?
overview.role_ids = [overview.role_id]
overview.save!
}
remove_column :overviews, :role_id
end
end

View file

@ -3367,7 +3367,7 @@ Overview.create_if_not_exists(
name: 'My assigned Tickets', name: 'My assigned Tickets',
link: 'my_assigned', link: 'my_assigned',
prio: 1000, prio: 1000,
role_id: overview_role.id, role_ids: [overview_role.id],
condition: { condition: {
'ticket.state_id' => { 'ticket.state_id' => {
operator: 'is', operator: 'is',
@ -3394,7 +3394,7 @@ Overview.create_if_not_exists(
name: 'Unassigned & Open', name: 'Unassigned & Open',
link: 'all_unassigned', link: 'all_unassigned',
prio: 1010, prio: 1010,
role_id: overview_role.id, role_ids: [overview_role.id],
condition: { condition: {
'ticket.state_id' => { 'ticket.state_id' => {
operator: 'is', operator: 'is',
@ -3421,7 +3421,7 @@ Overview.create_if_not_exists(
name: 'My pending reached Tickets', name: 'My pending reached Tickets',
link: 'my_pending_reached', link: 'my_pending_reached',
prio: 1020, prio: 1020,
role_id: overview_role.id, role_ids: [overview_role.id],
condition: { condition: {
'ticket.state_id' => { 'ticket.state_id' => {
operator: 'is', operator: 'is',
@ -3453,7 +3453,7 @@ Overview.create_if_not_exists(
name: 'Open', name: 'Open',
link: 'all_open', link: 'all_open',
prio: 1030, prio: 1030,
role_id: overview_role.id, role_ids: [overview_role.id],
condition: { condition: {
'ticket.state_id' => { 'ticket.state_id' => {
operator: 'is', operator: 'is',
@ -3476,7 +3476,7 @@ Overview.create_if_not_exists(
name: 'Pending reached', name: 'Pending reached',
link: 'all_pending_reached', link: 'all_pending_reached',
prio: 1040, prio: 1040,
role_id: overview_role.id, role_ids: [overview_role.id],
condition: { condition: {
'ticket.state_id' => { 'ticket.state_id' => {
operator: 'is', operator: 'is',
@ -3504,7 +3504,7 @@ Overview.create_if_not_exists(
name: 'Escalated', name: 'Escalated',
link: 'all_escalated', link: 'all_escalated',
prio: 1050, prio: 1050,
role_id: overview_role.id, role_ids: [overview_role.id],
condition: { condition: {
'ticket.escalation_at' => { 'ticket.escalation_at' => {
operator: 'within next (relative)', operator: 'within next (relative)',
@ -3529,7 +3529,7 @@ Overview.create_if_not_exists(
name: 'My Tickets', name: 'My Tickets',
link: 'my_tickets', link: 'my_tickets',
prio: 1100, prio: 1100,
role_id: overview_role.id, role_ids: [overview_role.id],
condition: { condition: {
'ticket.state_id' => { 'ticket.state_id' => {
operator: 'is', operator: 'is',
@ -3555,7 +3555,7 @@ Overview.create_if_not_exists(
name: 'My Organization Tickets', name: 'My Organization Tickets',
link: 'my_organization_tickets', link: 'my_organization_tickets',
prio: 1200, prio: 1200,
role_id: overview_role.id, role_ids: [overview_role.id],
organization_shared: true, organization_shared: true,
condition: { condition: {
'ticket.state_id' => { 'ticket.state_id' => {

View file

@ -17,7 +17,7 @@ class AdminOverviewTest < TestCase
overview_create( overview_create(
data: { data: {
name: name, name: name,
role: 'Agent', roles: ['Agent'],
selector: { selector: {
'Priority' => '1 low', 'Priority' => '1 low',
}, },
@ -29,7 +29,7 @@ class AdminOverviewTest < TestCase
overview_update( overview_update(
data: { data: {
name: name, name: name,
role: 'Agent', roles: ['Agent'],
selector: { selector: {
'State' => 'new', 'State' => 'new',
}, },

View file

@ -29,7 +29,7 @@ class AgentTicketOverviewLevel1Test < TestCase
browser: browser1, browser: browser1,
data: { data: {
name: name1, name: name1,
role: 'Agent', roles: ['Agent'],
selector: { selector: {
'Priority' => '1 low', 'Priority' => '1 low',
}, },
@ -40,7 +40,7 @@ class AgentTicketOverviewLevel1Test < TestCase
browser: browser1, browser: browser1,
data: { data: {
name: name2, name: name2,
role: 'Agent', roles: ['Agent'],
selector: { selector: {
'Priority' => '3 high', 'Priority' => '3 high',
}, },

View file

@ -1575,7 +1575,7 @@ wait untill text in selector disabppears
browser: browser1, browser: browser1,
data: { data: {
name: name, name: name,
role: 'Agent', roles: ['Agent'],
selector: { selector: {
'Priority': '1 low', 'Priority': '1 low',
}, },
@ -1616,13 +1616,19 @@ wait untill text in selector disabppears
mute_log: true, mute_log: true,
) )
end end
if data[:role]
select( if data[:roles]
browser: instance, 99.times do
css: '.modal select[name="role_id"]', begin
value: data[:role], element = instance.find_elements(css: '.modal .js-selected[data-name=role_ids] .js-option:not(.is-hidden)')[0]
mute_log: true, break if !element
) element.click
sleep 0.1
end
end
data[:roles].each { |role|
instance.execute_script("$(\".modal [data-name=role_ids] .js-pool .js-option:not(.is-hidden):contains('#{role}')\").first().click()")
}
end end
if data[:selector] if data[:selector]
@ -1678,7 +1684,7 @@ wait untill text in selector disabppears
browser: browser1, browser: browser1,
data: { data: {
name: name, name: name,
role: 'Agent', roles: ['Agent'],
selector: { selector: {
'Priority': '1 low', 'Priority': '1 low',
}, },
@ -1717,13 +1723,18 @@ wait untill text in selector disabppears
mute_log: true, mute_log: true,
) )
end end
if data[:role] if data[:roles]
select( 99.times do
browser: instance, begin
css: '.modal select[name="role_id"]', element = instance.find_elements(css: '.modal .js-selected[data-name=role_ids] .js-option:not(.is-hidden)')[0]
value: data[:role], break if !element
mute_log: true, element.click
) sleep 0.1
end
end
data[:roles].each { |role|
instance.execute_script("$(\".modal [data-name=role_ids] .js-pool .js-option:not(.is-hidden):contains('#{role}')\").first().click()")
}
end end
if data[:selector] if data[:selector]

View file

@ -351,7 +351,7 @@ class AssetsTest < ActiveSupport::TestCase
name: 'my asset test', name: 'my asset test',
link: 'my_asset_test', link: 'my_asset_test',
prio: 1000, prio: 1000,
role_id: overview_role.id, role_ids: [overview_role.id],
user_ids: [user4.id, user5.id], user_ids: [user4.id, user5.id],
condition: { condition: {
'ticket.state_id' => { 'ticket.state_id' => {
@ -389,7 +389,7 @@ class AssetsTest < ActiveSupport::TestCase
name: 'my asset test', name: 'my asset test',
link: 'my_asset_test', link: 'my_asset_test',
prio: 1000, prio: 1000,
role_id: overview_role.id, role_ids: [overview_role.id],
user_ids: [user4.id], user_ids: [user4.id],
condition: { condition: {
'ticket.state_id' => { 'ticket.state_id' => {

View file

@ -105,7 +105,7 @@ class TicketOverviewTest < ActiveSupport::TestCase
name: 'My assigned Tickets', name: 'My assigned Tickets',
link: 'my_assigned', link: 'my_assigned',
prio: 1000, prio: 1000,
role_id: overview_role.id, role_ids: [overview_role.id],
condition: { condition: {
'ticket.state_id' => { 'ticket.state_id' => {
operator: 'is', operator: 'is',
@ -132,7 +132,7 @@ class TicketOverviewTest < ActiveSupport::TestCase
name: 'Unassigned & Open', name: 'Unassigned & Open',
link: 'all_unassigned', link: 'all_unassigned',
prio: 1010, prio: 1010,
role_id: overview_role.id, role_ids: [overview_role.id],
condition: { condition: {
'ticket.state_id' => { 'ticket.state_id' => {
operator: 'is', operator: 'is',
@ -158,7 +158,7 @@ class TicketOverviewTest < ActiveSupport::TestCase
name: 'My Tickets 2', name: 'My Tickets 2',
link: 'my_tickets_2', link: 'my_tickets_2',
prio: 1020, prio: 1020,
role_id: overview_role.id, role_ids: [overview_role.id],
user_ids: [agent2.id], user_ids: [agent2.id],
condition: { condition: {
'ticket.state_id' => { 'ticket.state_id' => {
@ -185,7 +185,7 @@ class TicketOverviewTest < ActiveSupport::TestCase
name: 'My Tickets only with Note', name: 'My Tickets only with Note',
link: 'my_tickets_onyl_with_note', link: 'my_tickets_onyl_with_note',
prio: 1030, prio: 1030,
role_id: overview_role.id, role_ids: [overview_role.id],
user_ids: [agent1.id], user_ids: [agent1.id],
condition: { condition: {
'article.type_id' => { 'article.type_id' => {
@ -214,7 +214,7 @@ class TicketOverviewTest < ActiveSupport::TestCase
name: 'My Tickets', name: 'My Tickets',
link: 'my_tickets', link: 'my_tickets',
prio: 1100, prio: 1100,
role_id: overview_role.id, role_ids: [overview_role.id],
condition: { condition: {
'ticket.state_id' => { 'ticket.state_id' => {
operator: 'is', operator: 'is',
@ -240,7 +240,7 @@ class TicketOverviewTest < ActiveSupport::TestCase
name: 'My Organization Tickets', name: 'My Organization Tickets',
link: 'my_organization_tickets', link: 'my_organization_tickets',
prio: 1200, prio: 1200,
role_id: overview_role.id, role_ids: [overview_role.id],
organization_shared: true, organization_shared: true,
condition: { condition: {
'ticket.state_id' => { 'ticket.state_id' => {
@ -267,7 +267,7 @@ class TicketOverviewTest < ActiveSupport::TestCase
name: 'My Organization Tickets (open)', name: 'My Organization Tickets (open)',
link: 'my_organization_tickets_open', link: 'my_organization_tickets_open',
prio: 1200, prio: 1200,
role_id: overview_role.id, role_ids: [overview_role.id],
user_ids: [customer2.id], user_ids: [customer2.id],
organization_shared: true, organization_shared: true,
condition: { condition: {
@ -297,7 +297,7 @@ class TicketOverviewTest < ActiveSupport::TestCase
name: 'Not Shown Admin', name: 'Not Shown Admin',
link: 'not_shown_admin', link: 'not_shown_admin',
prio: 9900, prio: 9900,
role_id: overview_role.id, role_ids: [overview_role.id],
condition: { condition: {
'ticket.state_id' => { 'ticket.state_id' => {
operator: 'is', operator: 'is',