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

View file

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

View file

@ -1,11 +1,11 @@
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
@url: @apiPath + '/overviews'
@configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, 'null': false },
{ 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: '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 },
@ -62,7 +62,7 @@ class App.Overview extends App.Model
@configure_overview = [
'name',
'link',
'role',
'role_ids',
]
@description = '''

View file

@ -7,6 +7,7 @@ class Overview < ApplicationModel
load 'overview/assets.rb'
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
store :condition
store :order

View file

@ -19,11 +19,12 @@ returns
current_user = data[:current_user]
# 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')
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
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
overviews_list = []
overviews.each { |overview|
@ -36,7 +37,7 @@ returns
# get agent overviews
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.each { |overview|
user_ids = overview.user_ids

View file

@ -196,7 +196,6 @@ class CreateTicket < ActiveRecord::Migration
add_index :ticket_counters, [:generator], unique: true
create_table :overviews do |t|
t.references :role, null: false
t.column :name, :string, limit: 250, null: false
t.column :link, :string, limit: 250, null: false
t.column :prio, :integer, null: false
@ -212,6 +211,13 @@ class CreateTicket < ActiveRecord::Migration
end
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|
t.integer :overview_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',
link: 'my_assigned',
prio: 1000,
role_id: overview_role.id,
role_ids: [overview_role.id],
condition: {
'ticket.state_id' => {
operator: 'is',
@ -3394,7 +3394,7 @@ Overview.create_if_not_exists(
name: 'Unassigned & Open',
link: 'all_unassigned',
prio: 1010,
role_id: overview_role.id,
role_ids: [overview_role.id],
condition: {
'ticket.state_id' => {
operator: 'is',
@ -3421,7 +3421,7 @@ Overview.create_if_not_exists(
name: 'My pending reached Tickets',
link: 'my_pending_reached',
prio: 1020,
role_id: overview_role.id,
role_ids: [overview_role.id],
condition: {
'ticket.state_id' => {
operator: 'is',
@ -3453,7 +3453,7 @@ Overview.create_if_not_exists(
name: 'Open',
link: 'all_open',
prio: 1030,
role_id: overview_role.id,
role_ids: [overview_role.id],
condition: {
'ticket.state_id' => {
operator: 'is',
@ -3476,7 +3476,7 @@ Overview.create_if_not_exists(
name: 'Pending reached',
link: 'all_pending_reached',
prio: 1040,
role_id: overview_role.id,
role_ids: [overview_role.id],
condition: {
'ticket.state_id' => {
operator: 'is',
@ -3504,7 +3504,7 @@ Overview.create_if_not_exists(
name: 'Escalated',
link: 'all_escalated',
prio: 1050,
role_id: overview_role.id,
role_ids: [overview_role.id],
condition: {
'ticket.escalation_at' => {
operator: 'within next (relative)',
@ -3529,7 +3529,7 @@ Overview.create_if_not_exists(
name: 'My Tickets',
link: 'my_tickets',
prio: 1100,
role_id: overview_role.id,
role_ids: [overview_role.id],
condition: {
'ticket.state_id' => {
operator: 'is',
@ -3555,7 +3555,7 @@ Overview.create_if_not_exists(
name: 'My Organization Tickets',
link: 'my_organization_tickets',
prio: 1200,
role_id: overview_role.id,
role_ids: [overview_role.id],
organization_shared: true,
condition: {
'ticket.state_id' => {

View file

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

View file

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

View file

@ -1575,7 +1575,7 @@ wait untill text in selector disabppears
browser: browser1,
data: {
name: name,
role: 'Agent',
roles: ['Agent'],
selector: {
'Priority': '1 low',
},
@ -1616,13 +1616,19 @@ wait untill text in selector disabppears
mute_log: true,
)
end
if data[:role]
select(
browser: instance,
css: '.modal select[name="role_id"]',
value: data[:role],
mute_log: true,
)
if data[:roles]
99.times do
begin
element = instance.find_elements(css: '.modal .js-selected[data-name=role_ids] .js-option:not(.is-hidden)')[0]
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
if data[:selector]
@ -1678,7 +1684,7 @@ wait untill text in selector disabppears
browser: browser1,
data: {
name: name,
role: 'Agent',
roles: ['Agent'],
selector: {
'Priority': '1 low',
},
@ -1717,13 +1723,18 @@ wait untill text in selector disabppears
mute_log: true,
)
end
if data[:role]
select(
browser: instance,
css: '.modal select[name="role_id"]',
value: data[:role],
mute_log: true,
)
if data[:roles]
99.times do
begin
element = instance.find_elements(css: '.modal .js-selected[data-name=role_ids] .js-option:not(.is-hidden)')[0]
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
if data[:selector]

View file

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

View file

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