Compare commits
60 Commits
develop
...
stable-2.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d159b40979 | ||
|
|
d89d86d8d1 | ||
|
|
07ed45cb3a | ||
|
|
b745fb212f | ||
|
|
967ab56fc4 | ||
|
|
3a4ca7a125 | ||
|
|
3eecf52c50 | ||
|
|
5222705750 | ||
|
|
df402111cb | ||
|
|
8e6a63d8da | ||
|
|
0364225839 | ||
|
|
cdaff7906b | ||
|
|
d57116dada | ||
|
|
e19be91338 | ||
|
|
bed3e54db6 | ||
|
|
2b80635f28 | ||
|
|
a42a370b59 | ||
|
|
32b0228e11 | ||
|
|
5c07513271 | ||
|
|
cc1f959c35 | ||
|
|
faa84251fc | ||
|
|
2e141632b8 | ||
|
|
43784b0e8b | ||
|
|
9b670dfae0 | ||
|
|
040e7bbead | ||
|
|
e8dcd7f850 | ||
|
|
aa1743f17d | ||
|
|
355bc82a0b | ||
|
|
70084b538b | ||
|
|
8732f5aeca | ||
|
|
ced0d8d05c | ||
|
|
aeaa3a04d5 | ||
|
|
4bb9400a69 | ||
|
|
d8ce602906 | ||
|
|
49bcb62036 | ||
|
|
5a2f2afe8d | ||
|
|
c1c9a79eb7 | ||
|
|
37734495cc | ||
|
|
16f7edc7b9 | ||
|
|
9486025111 | ||
|
|
04ad7f1dcd | ||
|
|
0f9073acbd | ||
|
|
7ea396d043 | ||
|
|
23879bca1b | ||
|
|
c0d5584a1c | ||
|
|
1ea788c7a9 | ||
|
|
6d23dae9c3 | ||
|
|
b5dd2caa72 | ||
|
|
ea425d5062 | ||
|
|
54deb6b47d | ||
|
|
22add101d9 | ||
|
|
5a7232900c | ||
|
|
e6f2290336 | ||
|
|
4df1330cd3 | ||
|
|
63bedc53f9 | ||
|
|
39e96dd338 | ||
|
|
c3a3ed7bec | ||
|
|
cf1a8455dd | ||
|
|
bb08ba0353 | ||
|
|
5930e2addd |
54
CHANGELOG.md
54
CHANGELOG.md
@@ -1,16 +1,50 @@
|
||||
# Change Log
|
||||
|
||||
## [2.6.0](https://github.com/zammad/zammad/tree/2.6.0) (2018-xx-xx)
|
||||
[Full Changelog](https://github.com/zammad/zammad/compare/2.5.0...2.6.0)
|
||||
## [2.6.1](https://github.com/zammad/zammad/tree/2.6.1) (2018-10-25)
|
||||
[Full Changelog](https://github.com/zammad/zammad/compare/2.6.0...2.6.1)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
|
||||
|
||||
- O365/Office365 authentication missing given- and surname [2281](https://github.com/zammad/zammad/issues/2281) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
|
||||
- Italian and Spanish translations for notifications. [2270](https://github.com/zammad/zammad/issues/2270) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
|
||||
|
||||
|
||||
\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
|
||||
- `rake searchindex:rebuild` is not working after Elasticsearch 2.4 upgrade to Elasticsearch 5.6 "Limit of total fields [1000] in index" [2297](https://github.com/zammad/zammad/issues/2297) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Performance issue in bulk action selection in overview [2279](https://github.com/zammad/zammad/issues/2279) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend](https://github.com/zammad/zammad/labels/frontend)] [[overviews](https://github.com/zammad/zammad/labels/overviews)] [[regression](https://github.com/zammad/zammad/labels/regression)]
|
||||
- Note is not shown for customer / organisations if it's empty. [2277](https://github.com/zammad/zammad/issues/2277) [[bug](https://github.com/zammad/zammad/labels/bug)] [[regression](https://github.com/zammad/zammad/labels/regression)]
|
||||
- Bulk Form javascript error when checking item [2273](https://github.com/zammad/zammad/issues/2273) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend](https://github.com/zammad/zammad/labels/frontend)] [[needs verification](https://github.com/zammad/zammad/labels/needs verification)] [[overviews](https://github.com/zammad/zammad/labels/overviews)] [[regression](https://github.com/zammad/zammad/labels/regression)]
|
||||
- Permission issue: response templates only usable when admin is also agent [2285](https://github.com/zammad/zammad/issues/2285) [[admin area](https://github.com/zammad/zammad/labels/admin area)] [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Records in Reporting not updated when single ActiveRecord can not be found [2246](https://github.com/zammad/zammad/issues/2246) [[bug](https://github.com/zammad/zammad/labels/bug)] [[reporting](https://github.com/zammad/zammad/labels/reporting)]
|
||||
- Users deactivated by the LDAP sync get updated on every run [2256](https://github.com/zammad/zammad/issues/2256) [[LDAP](https://github.com/zammad/zammad/labels/LDAP)] [[bug](https://github.com/zammad/zammad/labels/bug)] [[integration](https://github.com/zammad/zammad/labels/integration)]
|
||||
- Search with id:123 does not work anymore [2195](https://github.com/zammad/zammad/issues/2195) [[bug](https://github.com/zammad/zammad/labels/bug)] [[search](https://github.com/zammad/zammad/labels/search)]
|
||||
- Invalid date causes errors [2173](https://github.com/zammad/zammad/issues/2173) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend](https://github.com/zammad/zammad/labels/frontend)]
|
||||
- Check the correct hash array if the agent is present [2243](https://github.com/zammad/zammad/pull/2243) [[bug](https://github.com/zammad/zammad/labels/bug)] [[needs verification](https://github.com/zammad/zammad/labels/needs verification)]
|
||||
- Boolean object set to false is not visible [2233](https://github.com/zammad/zammad/issues/2233) [[bug](https://github.com/zammad/zammad/labels/bug)] [[object manager attribute](https://github.com/zammad/zammad/labels/object manager attribute)]
|
||||
- Unable to process spam email `"ERROR: #<Exceptions::UnprocessableEntity: Invalid email>"` [2254](https://github.com/zammad/zammad/issues/2254) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Unable to process email `"ERROR: #<NoMethodError: undefined method `match' for nil:NilClass>"` [2252](https://github.com/zammad/zammad/issues/2252) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Naming an attribute "attribute" causes ActiveRecord failure [2236](https://github.com/zammad/zammad/issues/2236) [[bug](https://github.com/zammad/zammad/labels/bug)] [[object manager attribute](https://github.com/zammad/zammad/labels/object manager attribute)]
|
||||
- Bulk action should not be shown if user has no change permissions [2026](https://github.com/zammad/zammad/issues/2026) [[bug](https://github.com/zammad/zammad/labels/bug)] [[bulk](https://github.com/zammad/zammad/labels/bulk)] [[frontend](https://github.com/zammad/zammad/labels/frontend)] [[overviews](https://github.com/zammad/zammad/labels/overviews)]
|
||||
- Unable to process spam mail with invalid date field [2245](https://github.com/zammad/zammad/issues/2245) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Overview setting isn't applied on submit [2235](https://github.com/zammad/zammad/issues/2235) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend](https://github.com/zammad/zammad/labels/frontend)]
|
||||
- Wrong Recent viewed tickets list [2194](https://github.com/zammad/zammad/issues/2194) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend](https://github.com/zammad/zammad/labels/frontend)]
|
||||
- LDAP/Exchange UTF-8 Status Code 500 [2140](https://github.com/zammad/zammad/issues/2140) [[LDAP](https://github.com/zammad/zammad/labels/LDAP)] [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)]
|
||||
- Unprocessable email ERROR: #<ArgumentError: invalid byte sequence in UTF-8> [2227](https://github.com/zammad/zammad/issues/2227) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Printed documents do not contain the whole ticket content, if it gets too much [2162](https://github.com/zammad/zammad/issues/2162) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Unable to process email (Encoding::ConverterNotFoundError: code converter not found (Windows-1258 to UTF-8)) [2224](https://github.com/zammad/zammad/issues/2224) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Email IMAP fetch stops working if mailserver is once unreachable [1861](https://github.com/zammad/zammad/issues/1861) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Unable to use exchange sync (stack level too deep (SystemStackError)) [2220](https://github.com/zammad/zammad/issues/2220) [[bug](https://github.com/zammad/zammad/labels/bug)] [[integration](https://github.com/zammad/zammad/labels/integration)]
|
||||
- Exchange import fails if folder names can't be converted to Unicode [2152](https://github.com/zammad/zammad/issues/2152) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)] [[waiting for feedback](https://github.com/zammad/zammad/labels/waiting for feedback)]
|
||||
- CTI Caller Log blocks user deletion/destroy and fails [2218](https://github.com/zammad/zammad/issues/2218) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Unprocessable Mail if sender contains http.net@ [2199](https://github.com/zammad/zammad/issues/2199) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Call log shows only orga infos on event, not on site load [2075](https://github.com/zammad/zammad/issues/2075) [[CTI](https://github.com/zammad/zammad/labels/CTI)] [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend](https://github.com/zammad/zammad/labels/frontend)] [[needs verification](https://github.com/zammad/zammad/labels/needs verification)]
|
||||
- Unable to modify tree_select attributes with fresh 2.6 [2206](https://github.com/zammad/zammad/issues/2206) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Order of relation select fields (Group, State etc.) values is broken [2209](https://github.com/zammad/zammad/issues/2209) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend](https://github.com/zammad/zammad/labels/frontend)]
|
||||
- Max size of tabs is not recognised [2204](https://github.com/zammad/zammad/issues/2204) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Unable to process email `EncodingError: could not find a valid input encoding` [2200](https://github.com/zammad/zammad/issues/2200) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Bulk-Action: Not possible to change owner [1864](https://github.com/zammad/zammad/issues/1864) [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend](https://github.com/zammad/zammad/labels/frontend)]
|
||||
- Incorrect reset after deleting single object-selection [2020](https://github.com/zammad/zammad/issues/2020) [[bug](https://github.com/zammad/zammad/labels/bug)] [[object manager attribute](https://github.com/zammad/zammad/labels/object manager attribute)]
|
||||
- Puma performance is going worst if users with phone attribute are updated or new tickets are created. [2193](https://github.com/zammad/zammad/issues/2193) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Sometimes automatisation is not executed [2191](https://github.com/zammad/zammad/issues/2191) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Problem while updating to 2.6: Mysql2::Error: Invalid default value for 'start_at' [2183](https://github.com/zammad/zammad/issues/2183) [[bug](https://github.com/zammad/zammad/labels/bug)] [[update](https://github.com/zammad/zammad/labels/update)]
|
||||
- LDAP / Exchange update (save!) records unnecessarily [2187](https://github.com/zammad/zammad/issues/2187) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)]
|
||||
- Overview not showing unassigned tickets "if not defined" (e. g. for owner but also for select or input fields) [2171](https://github.com/zammad/zammad/issues/2171) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Incorrect encoding of email messages in HTML format (Windows-1250) [2167](https://github.com/zammad/zammad/issues/2167) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
|
||||
6
Gemfile
6
Gemfile
@@ -80,7 +80,7 @@ gem 'twitter'
|
||||
|
||||
# channels - email additions
|
||||
gem 'htmlentities'
|
||||
gem 'mail', '>= 2.7.1.rc1'
|
||||
gem 'mail', '2.6.6'
|
||||
gem 'mime-types'
|
||||
gem 'rchardet', '>= 1.8.0'
|
||||
gem 'valid_email2'
|
||||
@@ -163,6 +163,10 @@ group :development, :test do
|
||||
|
||||
# mock http calls
|
||||
gem 'webmock'
|
||||
|
||||
# record and replay TCP/HTTP transactions
|
||||
gem 'tcr'
|
||||
gem 'vcr'
|
||||
end
|
||||
|
||||
# Want to extend Zammad with additional gems?
|
||||
|
||||
41
Gemfile.lock
41
Gemfile.lock
@@ -220,15 +220,14 @@ GEM
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
lumberjack (1.0.12)
|
||||
mail (2.7.1.rc1)
|
||||
mini_mime (>= 0.1.1)
|
||||
mail (2.6.6)
|
||||
mime-types (>= 1.16, < 4)
|
||||
memoizable (0.4.2)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
method_source (0.9.0)
|
||||
mime-types (3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2016.0521)
|
||||
mini_mime (1.0.0)
|
||||
mini_portile2 (2.3.0)
|
||||
minitest (5.11.3)
|
||||
multi_json (1.12.2)
|
||||
@@ -241,7 +240,7 @@ GEM
|
||||
net-ldap (0.16.1)
|
||||
netrc (0.11.0)
|
||||
nio4r (2.3.0)
|
||||
nokogiri (1.8.4)
|
||||
nokogiri (1.8.5)
|
||||
mini_portile2 (~> 2.3.0)
|
||||
nori (2.6.0)
|
||||
notiffany (0.1.1)
|
||||
@@ -318,7 +317,7 @@ GEM
|
||||
rack (2.0.5)
|
||||
rack-livereload (0.3.16)
|
||||
rack
|
||||
rack-test (1.0.0)
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rails (5.1.5)
|
||||
actioncable (= 5.1.5)
|
||||
@@ -358,23 +357,23 @@ GEM
|
||||
mime-types (>= 1.16, < 4.0)
|
||||
netrc (~> 0.8)
|
||||
retriable (2.1.0)
|
||||
rspec-core (3.7.0)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-expectations (3.7.0)
|
||||
rspec-core (3.8.0)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-expectations (3.8.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-mocks (3.7.0)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-mocks (3.8.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-rails (3.7.2)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-rails (3.8.0)
|
||||
actionpack (>= 3.0)
|
||||
activesupport (>= 3.0)
|
||||
railties (>= 3.0)
|
||||
rspec-core (~> 3.7.0)
|
||||
rspec-expectations (~> 3.7.0)
|
||||
rspec-mocks (~> 3.7.0)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-support (3.7.0)
|
||||
rspec-core (~> 3.8.0)
|
||||
rspec-expectations (~> 3.8.0)
|
||||
rspec-mocks (~> 3.8.0)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-support (3.8.0)
|
||||
rubocop (0.54.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.5)
|
||||
@@ -384,7 +383,7 @@ GEM
|
||||
unicode-display_width (~> 1.0, >= 1.0.1)
|
||||
ruby-progressbar (1.9.0)
|
||||
ruby_dep (1.5.0)
|
||||
rubyzip (1.2.1)
|
||||
rubyzip (1.2.2)
|
||||
safe_yaml (1.0.4)
|
||||
sass (3.5.3)
|
||||
sass-listen (~> 4.0.0)
|
||||
@@ -422,6 +421,7 @@ GEM
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
sqlite3 (1.3.13)
|
||||
tcr (0.2.0)
|
||||
telegramAPI (1.4.2)
|
||||
rest-client (~> 2.0, >= 2.0.2)
|
||||
telephone_number (1.3.0)
|
||||
@@ -461,6 +461,7 @@ GEM
|
||||
valid_email2 (2.1.0)
|
||||
activemodel (>= 3.2)
|
||||
mail (~> 2.5)
|
||||
vcr (4.0.0)
|
||||
viewpoint (1.1.0)
|
||||
httpclient
|
||||
logging
|
||||
@@ -519,7 +520,7 @@ DEPENDENCIES
|
||||
json
|
||||
koala
|
||||
libv8
|
||||
mail (>= 2.7.1.rc1)
|
||||
mail (= 2.6.6)
|
||||
mime-types
|
||||
mysql2
|
||||
net-ldap
|
||||
@@ -556,6 +557,7 @@ DEPENDENCIES
|
||||
slack-notifier
|
||||
sprockets
|
||||
sqlite3
|
||||
tcr
|
||||
telegramAPI
|
||||
telephone_number
|
||||
test-unit
|
||||
@@ -564,6 +566,7 @@ DEPENDENCIES
|
||||
uglifier
|
||||
unicorn
|
||||
valid_email2
|
||||
vcr
|
||||
viewpoint
|
||||
webmock
|
||||
writeexcel
|
||||
|
||||
@@ -412,6 +412,12 @@ class App.ControllerTable extends App.Controller
|
||||
return if !@bindCheckbox
|
||||
return if !@bindCheckbox.events
|
||||
return if _.isEmpty(clicks)
|
||||
|
||||
# If a select_all callback exists, then trigger it once insteading of triggering the callback once for each checkbox
|
||||
if @bindCheckbox.select_all
|
||||
@bindCheckbox.select_all(clicks[0]..., e)
|
||||
return
|
||||
|
||||
for event, callback of @bindCheckbox.events
|
||||
if event == 'click' || event == 'change'
|
||||
for click in clicks
|
||||
@@ -1007,4 +1013,4 @@ class App.ControllerTable extends App.Controller
|
||||
for position in [0..array1.length-1]
|
||||
if array1[position] isnt array2[position]
|
||||
return position
|
||||
true
|
||||
true
|
||||
|
||||
@@ -73,7 +73,7 @@ class Index extends App.ControllerSubContent
|
||||
user_group_config = false
|
||||
|
||||
groups = []
|
||||
group_ids = App.User.find(@Session.get('id')).all_group_ids()
|
||||
group_ids = App.User.find(@Session.get('id')).allGroupIds()
|
||||
if group_ids
|
||||
for group_id in group_ids
|
||||
group = App.Group.find(group_id)
|
||||
|
||||
@@ -37,6 +37,7 @@ class App.UiElement.datetime extends App.UiElement.basedate
|
||||
@buildTimestamp: (currentInput) ->
|
||||
timestamp = "#{currentInput[0]}T#{currentInput[1]}:00.000Z"
|
||||
time = new Date( Date.parse(timestamp) )
|
||||
return '' if isNaN time
|
||||
time.setMinutes( time.getMinutes() + time.getTimezoneOffset() )
|
||||
@log 'setNewTime', time.toString()
|
||||
time.toISOString().replace(/\d\d\.\d\d\dZ$/, '00.000Z')
|
||||
|
||||
@@ -8,6 +8,9 @@ class App.UiElement.select extends App.UiElement.ApplicationUiElement
|
||||
else
|
||||
attribute.multiple = ''
|
||||
|
||||
# add deleted historical options if required
|
||||
@addDeletedOptions(attribute, params)
|
||||
|
||||
# build options list based on config
|
||||
@getConfigOptionList(attribute, params)
|
||||
|
||||
@@ -31,3 +34,20 @@ class App.UiElement.select extends App.UiElement.ApplicationUiElement
|
||||
|
||||
# return item
|
||||
$( App.view('generic/select')(attribute: attribute) )
|
||||
|
||||
# 1. If attribute.value is not among the current options, then search within historical options
|
||||
# 2. If attribute.value is not among current and historical options, then add the value itself as an option
|
||||
@addDeletedOptions: (attribute) ->
|
||||
return if !_.isEmpty(attribute.relation) # do not apply for attributes with relation, relations will fill options automatically
|
||||
value = attribute.value
|
||||
return if !value
|
||||
return if _.isArray(value)
|
||||
return if !attribute.options
|
||||
return if !_.isObject(attribute.options)
|
||||
return if value of attribute.options
|
||||
return if value in (temp for own prop, temp of attribute.options)
|
||||
|
||||
if attribute.historical_options && value of attribute.historical_options
|
||||
attribute.options[value] = attribute.historical_options[value]
|
||||
else
|
||||
attribute.options[value] = value
|
||||
|
||||
@@ -25,6 +25,13 @@ class App.CTI extends App.Controller
|
||||
@delay(delay, 500, 'cti_list_push_render')
|
||||
'cti_list_push'
|
||||
)
|
||||
@bind('cti_event', (data) =>
|
||||
return if data.state isnt 'newCall'
|
||||
return if data.direction isnt 'in'
|
||||
return if @switch() isnt true
|
||||
@notify(data)
|
||||
'cti_event'
|
||||
)
|
||||
@bind('auth', (data) =>
|
||||
@meta.counter = 0
|
||||
)
|
||||
@@ -71,6 +78,7 @@ class App.CTI extends App.Controller
|
||||
# render new caller list
|
||||
if data.list
|
||||
@list = data.list
|
||||
@updateNavMenu()
|
||||
if @renderDone
|
||||
@renderCallerLog()
|
||||
return
|
||||
@@ -145,8 +153,8 @@ class App.CTI extends App.Controller
|
||||
if item.comment
|
||||
item.state_human += ", #{item.comment}"
|
||||
|
||||
if item.start && item.end
|
||||
item.duration = format((Date.parse(item.end) - Date.parse(item.start))/1000)
|
||||
if item.start_at && item.end_at
|
||||
item.duration = format((Date.parse(item.end_at) - Date.parse(item.start_at))/1000)
|
||||
|
||||
diff_in_min = ((Date.now() - Date.parse(item.created_at)) / 1000) / 60
|
||||
if diff_in_min > 1
|
||||
@@ -198,9 +206,9 @@ class App.CTI extends App.Controller
|
||||
counter: =>
|
||||
count = 0
|
||||
for item in @list
|
||||
if item.state is 'hangup' && !item.done
|
||||
if !item.done
|
||||
count++
|
||||
@meta.counter + count
|
||||
@meta.counter = count
|
||||
|
||||
switch: (state = undefined) =>
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
class App.TestController1 extends App.Controller
|
||||
constructor: ->
|
||||
super
|
||||
@showState = false
|
||||
@hideState = false
|
||||
@activeState = false
|
||||
@showState = false
|
||||
@hideState = false
|
||||
@activeState = false
|
||||
@changedState ||= false
|
||||
@render()
|
||||
|
||||
meta: ->
|
||||
@@ -21,6 +22,9 @@ class App.TestController1 extends App.Controller
|
||||
@activeState = state
|
||||
@render()
|
||||
|
||||
changed: =>
|
||||
@changedState
|
||||
|
||||
render: ->
|
||||
@html "<div>some test controller message:'#{@message}',show:'#{@showState}',hide:'#{@hideState}',active:'#{@activeState}'</div>"
|
||||
|
||||
|
||||
@@ -1,3 +1,47 @@
|
||||
ValidUsersForTicketSelectionMethods =
|
||||
validUsersForTicketSelection: ->
|
||||
items = $('.content.active .table-overview .table').find('[name="bulk"]:checked')
|
||||
|
||||
# we want to display all users for which we can assign the tickets directly
|
||||
# for this we need to get the groups of all selected tickets
|
||||
# after we got those we need to check which users are available in all groups
|
||||
# users that are not in all groups can't get the tickets assigned
|
||||
ticket_ids = _.map(items, (el) -> $(el).val() )
|
||||
ticket_group_ids = _.map(App.Ticket.findAll(ticket_ids), (ticket) -> ticket.group_id)
|
||||
users = @usersInGroups(ticket_group_ids)
|
||||
|
||||
# get the list of possible groups for the current user
|
||||
# from the TicketCreateCollection
|
||||
# (filled for e.g. the TicketCreation or TicketZoom assignment)
|
||||
# and order them by name
|
||||
group_ids = _.keys(@formMeta?.dependencies?.group_id)
|
||||
groups = App.Group.findAll(group_ids)
|
||||
groups_sorted = _.sortBy(groups, (group) -> group.name)
|
||||
|
||||
# get the number of visible users per group
|
||||
# from the TicketCreateCollection
|
||||
# (filled for e.g. the TicketCreation or TicketZoom assignment)
|
||||
for group in groups
|
||||
group.valid_users_count = @formMeta?.dependencies?.group_id?[group.id]?.owner_id.length || 0
|
||||
|
||||
{
|
||||
users: users
|
||||
groups: groups_sorted
|
||||
}
|
||||
|
||||
usersInGroups: (group_ids) ->
|
||||
ids_by_group = _.chain(@formMeta?.dependencies?.group_id)
|
||||
.pick(group_ids)
|
||||
.values()
|
||||
.map( (e) -> e.owner_id)
|
||||
.value()
|
||||
|
||||
# Underscore's intersection doesn't work when chained
|
||||
ids_in_all_groups = _.intersection(ids_by_group...)
|
||||
|
||||
users = App.User.findAll(ids_in_all_groups)
|
||||
_.sortBy(users, (user) -> user.firstname)
|
||||
|
||||
class App.TicketOverview extends App.Controller
|
||||
className: 'overviews'
|
||||
activeFocus: 'nav'
|
||||
@@ -25,6 +69,8 @@ class App.TicketOverview extends App.Controller
|
||||
'mouseenter .js-batch-hover-target': 'highlightBatchEntry'
|
||||
'mouseleave .js-batch-hover-target': 'unhighlightBatchEntry'
|
||||
|
||||
@include ValidUsersForTicketSelectionMethods
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
@batchSupport = @permissionCheck('ticket.agent')
|
||||
@@ -540,19 +586,6 @@ class App.TicketOverview extends App.Controller
|
||||
.velocity({ opacity: [0.5, 1] }, { duration: 120 })
|
||||
.velocity({ opacity: [1, 0.5] }, { duration: 60, delay: 40 })
|
||||
|
||||
usersInGroups: (group_ids) ->
|
||||
ids_by_group = _.chain(@formMeta?.dependencies?.group_id)
|
||||
.pick(group_ids)
|
||||
.values()
|
||||
.map( (e) -> e.owner_id)
|
||||
.value()
|
||||
|
||||
# Underscore's intersection doesn't work when chained
|
||||
ids_in_all_groups = _.intersection(ids_by_group...)
|
||||
|
||||
users = App.User.findAll(ids_in_all_groups)
|
||||
_.sortBy(users, (user) -> user.firstname)
|
||||
|
||||
render: ->
|
||||
elLocal = $(App.view('ticket_overview/index')())
|
||||
|
||||
@@ -604,33 +637,8 @@ class App.TicketOverview extends App.Controller
|
||||
@renderOptionsMacros()
|
||||
|
||||
renderOptionsGroups: =>
|
||||
items = @el.find('[name="bulk"]:checked')
|
||||
|
||||
# we want to display all users for which we can assign the tickets directly
|
||||
# for this we need to get the groups of all selected tickets
|
||||
# after we got those we need to check which users are available in all groups
|
||||
# users that are not in all groups can't get the tickets assigned
|
||||
ticket_ids = _.map(items, (el) -> $(el).val() )
|
||||
ticket_group_ids = _.map(App.Ticket.findAll(ticket_ids), (ticket) -> ticket.group_id)
|
||||
users = @usersInGroups(ticket_group_ids)
|
||||
|
||||
# get the list of possible groups for the current user
|
||||
# from the TicketCreateCollection
|
||||
# (filled for e.g. the TicketCreation or TicketZoom assignment)
|
||||
# and order them by name
|
||||
group_ids = _.keys(@formMeta?.dependencies?.group_id)
|
||||
groups = App.Group.findAll(group_ids)
|
||||
groups_sorted = _.sortBy(groups, (group) -> group.name)
|
||||
|
||||
# get the number of visible users per group
|
||||
# from the TicketCreateCollection
|
||||
# (filled for e.g. the TicketCreation or TicketZoom assignment)
|
||||
for group in groups
|
||||
group.valid_users_count = @formMeta?.dependencies?.group_id?[group.id]?.owner_id.length || 0
|
||||
|
||||
@batchAssignInner.html $(App.view('ticket_overview/batch_overlay_user_group')(
|
||||
users: users
|
||||
groups: groups_sorted
|
||||
@validUsersForTicketSelection()
|
||||
))
|
||||
|
||||
renderOptionsMacros: =>
|
||||
@@ -1086,10 +1094,11 @@ class Table extends App.Controller
|
||||
id: object.organization_id
|
||||
value
|
||||
callbackCheckbox = (id, checked, e) =>
|
||||
if @$('table').find('input[name="bulk"]:checked').length == 0
|
||||
@bulkForm.hide()
|
||||
else
|
||||
if @shouldShowBulkForm()
|
||||
@bulkForm.render()
|
||||
@bulkForm.show()
|
||||
else
|
||||
@bulkForm.hide()
|
||||
|
||||
if @lastChecked && e.shiftKey
|
||||
# check items in a row
|
||||
@@ -1163,6 +1172,7 @@ class Table extends App.Controller
|
||||
bindCheckbox:
|
||||
events:
|
||||
'click': callbackCheckbox
|
||||
select_all: callbackCheckbox
|
||||
)
|
||||
|
||||
# start user popups
|
||||
@@ -1183,11 +1193,11 @@ class Table extends App.Controller
|
||||
|
||||
# show/hide bulk action
|
||||
@$('.table-overview').delegate('input[name="bulk"], input[name="bulk_all"]', 'change', (e) =>
|
||||
if @$('.table-overview').find('input[name="bulk"]:checked').length == 0
|
||||
if @shouldShowBulkForm()
|
||||
@bulkForm.show()
|
||||
else
|
||||
@bulkForm.hide()
|
||||
@bulkForm.reset()
|
||||
else
|
||||
@bulkForm.show()
|
||||
)
|
||||
|
||||
# deselect bulk_all if one item is uncheck observ
|
||||
@@ -1207,6 +1217,17 @@ class Table extends App.Controller
|
||||
bulkAll.prop('indeterminate', true)
|
||||
)
|
||||
|
||||
shouldShowBulkForm: =>
|
||||
items = @$('table').find('input[name="bulk"]:checked')
|
||||
return false if items.length == 0
|
||||
|
||||
ticket_ids = _.map(items, (el) -> $(el).val() )
|
||||
ticket_group_ids = _.map(App.Ticket.findAll(ticket_ids), (ticket) -> ticket.group_id)
|
||||
ticket_group_ids = _.uniq(ticket_group_ids)
|
||||
allowed_group_ids = App.User.find(@Session.get('id')).allGroupIds('change')
|
||||
allowed_group_ids = _.map(allowed_group_ids, (id_string) -> parseInt(id_string, 10) )
|
||||
_.every(ticket_group_ids, (id) -> id in allowed_group_ids)
|
||||
|
||||
viewmode: (e) =>
|
||||
e.preventDefault()
|
||||
@view_mode = $(e.target).data('mode')
|
||||
@@ -1233,6 +1254,8 @@ class BulkForm extends App.Controller
|
||||
'click .js-confirm': 'confirm'
|
||||
'click .js-cancel': 'reset'
|
||||
|
||||
@include ValidUsersForTicketSelectionMethods
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
|
||||
@@ -1266,6 +1289,12 @@ class BulkForm extends App.Controller
|
||||
|
||||
handlers = @Config.get('TicketZoomFormHandler')
|
||||
|
||||
for attribute in @configure_attributes_ticket
|
||||
continue if attribute.name != 'owner_id'
|
||||
{users, groups} = @validUsersForTicketSelection()
|
||||
options = _.map(users, (user) -> {value: user.id, name: user.displayName()} )
|
||||
attribute.possible_groups_owners = options
|
||||
|
||||
new App.ControllerForm(
|
||||
el: @$('#form-ticket-bulk')
|
||||
model:
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
class OwnerFormHandlerDependencies
|
||||
|
||||
# central method, is getting called on every ticket form change
|
||||
@run: (params, attribute, attributes, classname, form, ui) ->
|
||||
return if 'group_id' not of params
|
||||
return if 'owner_id' not of params
|
||||
|
||||
owner_attribute = _.find(attributes, (o) -> o.name == 'owner_id')
|
||||
return if !owner_attribute
|
||||
return if 'possible_groups_owners' not of owner_attribute
|
||||
|
||||
# fetch contents using User relation if a Group has been selected, otherwise render possible_groups_owners
|
||||
if params.group_id
|
||||
owner_attribute.relation = 'User'
|
||||
delete owner_attribute['options']
|
||||
else
|
||||
owner_attribute.options = owner_attribute.possible_groups_owners
|
||||
delete owner_attribute['relation']
|
||||
|
||||
# replace new option list
|
||||
owner_attribute.default = params[owner_attribute.name]
|
||||
owner_attribute.newValue = params[owner_attribute.name]
|
||||
newElement = ui.formGenItem(owner_attribute, classname, form)
|
||||
form.find('select[name="owner_id"]').closest('.form-group').replaceWith(newElement)
|
||||
|
||||
App.Config.set('150-ticketFormChanges', OwnerFormHandlerDependencies, 'TicketZoomFormHandler')
|
||||
@@ -25,12 +25,20 @@ class App.WidgetOrganization extends App.Controller
|
||||
if nameNew of organization
|
||||
name = nameNew
|
||||
|
||||
# add to show if value exists
|
||||
if ( organization[name] || attributeConfig.tag is 'richtext' ) && attributeConfig.shown
|
||||
# do not show name since it's already shown via diplayName()
|
||||
continue if name is 'name'
|
||||
|
||||
# do not show firstname and lastname / already show via diplayName()
|
||||
if name isnt 'name'
|
||||
organizationData.push attributeConfig
|
||||
# do not show if configured to be not shown
|
||||
continue if !attributeConfig.shown
|
||||
|
||||
# Fix for issue #2277 - note is not shown for customer/organisations if it's empty
|
||||
# Always show for these two conditions:
|
||||
# 1. the attribute exists and is not empty
|
||||
# 2. it is a richtext note field
|
||||
continue if ( !organization[name]? || organization[name] is '' ) && attributeConfig.tag isnt 'richtext'
|
||||
|
||||
# add to show if all checks passed
|
||||
organizationData.push attributeConfig
|
||||
|
||||
# insert userData
|
||||
@html App.view('widget/organization')(
|
||||
|
||||
@@ -27,12 +27,20 @@ class App.WidgetUser extends App.Controller
|
||||
if nameNew of user
|
||||
name = nameNew
|
||||
|
||||
# add to show if value exists
|
||||
if ( user[name] || attributeConfig.tag is 'richtext' ) && attributeConfig.shown
|
||||
# do not show firstname and lastname since they are already shown via diplayName()
|
||||
continue if name is 'firstname' || name is 'lastname' || name is 'organization'
|
||||
|
||||
# do not show firstname and lastname / already show via displayName()
|
||||
if name isnt 'firstname' && name isnt 'lastname' && name isnt 'organization'
|
||||
userData.push attributeConfig
|
||||
# do not show if configured to be not shown
|
||||
continue if !attributeConfig.shown
|
||||
|
||||
# Fix for issue #2277 - note is not shown for customer/organisations if it's empty
|
||||
# Always show for these two conditions:
|
||||
# 1. the attribute exists and is not empty
|
||||
# 2. it is a richtext note field
|
||||
continue if ( !user[name]? || user[name] is '' ) && attributeConfig.tag isnt 'richtext'
|
||||
|
||||
# add to show if all checks passed
|
||||
userData.push attributeConfig
|
||||
|
||||
if user.preferences
|
||||
items = []
|
||||
|
||||
@@ -83,6 +83,18 @@ class App.TaskManager
|
||||
return if !_instance
|
||||
_instance.preferencesTrigger(key)
|
||||
|
||||
@tasksAutoCleanupDelayTime: (key) ->
|
||||
return if !_instance
|
||||
if !key
|
||||
return _instance.tasksAutoCleanupDelayTime
|
||||
_instance.tasksAutoCleanupDelayTime = key
|
||||
|
||||
@tasksAutoCleanupTaskMax: (key) ->
|
||||
return if !_instance
|
||||
if !key
|
||||
return _instance.maxTaskCount
|
||||
_instance.maxTaskCount = key
|
||||
|
||||
class _taskManagerSingleton extends App.Controller
|
||||
@include App.LogInclude
|
||||
|
||||
@@ -108,9 +120,11 @@ class _taskManagerSingleton extends App.Controller
|
||||
@tasksToUpdate = {}
|
||||
@tasksPreferences = {}
|
||||
@tasksPreferencesCallbacks = {}
|
||||
@tasksAutoCleanupDelayTime = 12000
|
||||
@activeTaskHistory = []
|
||||
@queue = []
|
||||
@queueRunning = false
|
||||
@maxTaskCount = 30
|
||||
|
||||
all: ->
|
||||
|
||||
@@ -599,7 +613,7 @@ class _taskManagerSingleton extends App.Controller
|
||||
tasksAutoCleanupDelay: =>
|
||||
delay = =>
|
||||
@tasksAutoCleanup()
|
||||
App.Delay.set(delay, 12000, 'task-autocleanup', undefined, true)
|
||||
App.Delay.set(delay, @tasksAutoCleanupDelayTime, 'task-autocleanup', undefined, true)
|
||||
|
||||
tasksAutoCleanup: =>
|
||||
|
||||
@@ -607,13 +621,19 @@ class _taskManagerSingleton extends App.Controller
|
||||
currentTaskCount = =>
|
||||
Object.keys(@allTasksByKey).length
|
||||
|
||||
maxTaskCount = 30
|
||||
if currentTaskCount() > maxTaskCount
|
||||
for task in App.Taskbar.search(sortBy:'updated_at', order:'ASC')
|
||||
if currentTaskCount() > maxTaskCount
|
||||
if currentTaskCount() > @maxTaskCount
|
||||
if @offlineModus
|
||||
tasks = @all()
|
||||
else
|
||||
tasks = App.Taskbar.search(sortBy:'updated_at', order:'ASC')
|
||||
for task in tasks
|
||||
if currentTaskCount() > @maxTaskCount
|
||||
if !task.active
|
||||
if _.isEmpty(task.state) || (_.isEmpty(task.state.ticket) && _.isEmpty(task.state.article))
|
||||
@log 'notice', "More then #{maxTaskCount} tasks open, close oldest untouched task #{task.key}"
|
||||
worker = App.TaskManager.worker(task.key)
|
||||
if worker
|
||||
if worker.changed && worker.changed()
|
||||
continue
|
||||
@log 'notice', "More then #{@maxTaskCount} tasks open, close oldest untouched task #{task.key}"
|
||||
@remove(task.key)
|
||||
|
||||
tasksInitial: =>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class App.Overview extends App.Model
|
||||
@configure 'Overview', 'name', 'prio', 'condition', 'order', 'group_by', 'group_direction', 'view', 'user_ids', 'organization_shared', 'role_ids', 'active'
|
||||
@configure 'Overview', 'name', 'prio', 'condition', 'order', 'group_by', 'group_direction', 'view', 'user_ids', 'organization_shared', 'out_of_office', 'role_ids', 'active'
|
||||
@extend Spine.Model.Ajax
|
||||
@url: @apiPath + '/overviews'
|
||||
@configure_attributes = [
|
||||
|
||||
@@ -249,7 +249,7 @@ class App.Ticket extends App.Model
|
||||
user_id = App.Session.get('id')
|
||||
return true if user_id is @customer_id
|
||||
return false if !App.User.exists(user_id)
|
||||
group_ids = App.User.find(user_id).all_group_ids(permission)
|
||||
group_ids = App.User.find(user_id).allGroupIds(permission)
|
||||
for local_group_id in group_ids
|
||||
if local_group_id.toString() is @group_id.toString()
|
||||
return true
|
||||
|
||||
@@ -254,7 +254,19 @@ class App.User extends App.Model
|
||||
return access if access
|
||||
false
|
||||
|
||||
all_group_ids: (permission = 'full') ->
|
||||
###
|
||||
|
||||
Returns a list of all groups for which the user is permitted to perform the given permission key
|
||||
|
||||
user = App.User.find(3)
|
||||
result = user.allGroupIds('change') # access to a given permission key
|
||||
|
||||
returns
|
||||
|
||||
["1", "2"]
|
||||
|
||||
###
|
||||
allGroupIds: (permission = 'full') ->
|
||||
group_ids = []
|
||||
user_group_ids = App.Session.get('group_ids')
|
||||
if user_group_ids
|
||||
|
||||
@@ -25,24 +25,28 @@
|
||||
<% for caller_id in item.preferences.from: %>
|
||||
<% if caller_id.user_id && App.User.exists(caller_id.user_id): %>
|
||||
<% shown = true %>
|
||||
<% user = App.User.find(caller_id.user_id) %>
|
||||
<% user = App.User.fullLocal(caller_id.user_id) %>
|
||||
<% if caller_id.level isnt 'known': %><%- @T('maybe') %> <% end %>
|
||||
<span class="user-popover" data-id="<%- user.id %>"><%= user.displayNameLong() %></span><br>
|
||||
<% else if caller_id.comment: %>
|
||||
<% else if !_.isEmpty(caller_id.comment): %>
|
||||
<% shown = true %>
|
||||
<%- @T('maybe') %> <%= caller_id.comment %><br>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if !shown && item.from_comment: %>
|
||||
<% if !shown && !_.isEmpty(item.from_comment): %>
|
||||
<% shown = true %>
|
||||
<% if item.from_comment: %><%= item.from_comment %><% end %>
|
||||
<%= item.from_comment %>
|
||||
<br>
|
||||
<% end %>
|
||||
<% if shown: %>
|
||||
<small><%= item.from_pretty %></small>
|
||||
<% else: %>
|
||||
<span class="js-userNew u-clickable" href="#"><%= item.from_pretty %></span>
|
||||
<% if !_.isEmpty(item.from_pretty): %>
|
||||
<span class="js-userNew u-clickable" href="#"><%= item.from_pretty %></span>
|
||||
<% else: %>
|
||||
<span><%= item.from %></span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</td>
|
||||
<td>
|
||||
@@ -51,7 +55,7 @@
|
||||
<% for caller_id in item.preferences.to: %>
|
||||
<% if caller_id.user_id && App.User.exists(caller_id.user_id): %>
|
||||
<% shown = true %>
|
||||
<% user = App.User.find(caller_id.user_id) %>
|
||||
<% user = App.User.fullLocal(caller_id.user_id) %>
|
||||
<% if caller_id.level isnt 'known': %><%- @T('maybe') %> <% end %>
|
||||
<span class="user-popover" data-id="<%- user.id %>"><%= user.displayNameLong() %></span><br>
|
||||
<% else if caller_id.comment: %>
|
||||
@@ -60,15 +64,19 @@
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if !shown && item.to_comment: %>
|
||||
<% if !shown && !_.isEmpty(item.to_comment): %>
|
||||
<% shown = true %>
|
||||
<% if item.to_comment: %><%= item.to_comment %><% end %>
|
||||
<%= item.to_comment %>
|
||||
<br>
|
||||
<% end %>
|
||||
<% if shown: %>
|
||||
<small><%= item.to_pretty %></small>
|
||||
<% else: %>
|
||||
<%= item.to_pretty %>
|
||||
<% if !_.isEmpty(item.to_pretty): %>
|
||||
<%= item.to_pretty %>
|
||||
<% else: %>
|
||||
<%= item.to %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</td>
|
||||
<td style="vertical-align: middle">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
|
||||
<% for row in @organizationData: %>
|
||||
<% if @organization[row.name] || row.name is 'note': %>
|
||||
<% if @organization[row.name]? || row.name is 'note': %>
|
||||
<div class="sidebar-block">
|
||||
<% if row.tag isnt 'richtext': %>
|
||||
<label><%- @T(row.display) %></label>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<h3 title="<%- @Ti('Name') %>"><%= @user.displayName() %></h3>
|
||||
</div>
|
||||
<% for row in @userData: %>
|
||||
<% if @user[row.name] || row.name is 'note': %>
|
||||
<% if @user[row.name]? || row.name is 'note': %>
|
||||
<div class="sidebar-block">
|
||||
<label><%- @T(row.display) %></label>
|
||||
<% if row.tag isnt 'richtext': %>
|
||||
|
||||
@@ -11,6 +11,29 @@
|
||||
border-bottom: 1px solid hsl(0,0%,90%);
|
||||
}
|
||||
|
||||
html {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
body,
|
||||
.content {
|
||||
background: none;
|
||||
color: black;
|
||||
}
|
||||
|
||||
// Disable flex to allow multi page prints in Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=939897
|
||||
body,
|
||||
#app,
|
||||
#app > .content.active,
|
||||
.tabsSidebar-holder,
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#app {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Dashboard
|
||||
@@ -54,9 +77,85 @@
|
||||
}
|
||||
|
||||
.tabsSidebar-sidebarSpacer {
|
||||
margin-right: 0;
|
||||
margin: 0;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.article-meta .article-meta-key {
|
||||
display: none;
|
||||
.textBubble-content {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.tabsSidebar-tabsSpacer {
|
||||
padding: 0 !important;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.ticket-article,
|
||||
.article-new {
|
||||
max-width: none;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.ticketZoom {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.textBubble {
|
||||
padding: 20px;
|
||||
border: 1px solid !important;
|
||||
background: white !important;
|
||||
}
|
||||
|
||||
.bubble-arrow:after {
|
||||
background: white !important;
|
||||
border-color: black !important;
|
||||
}
|
||||
|
||||
.article-meta {
|
||||
color: black;
|
||||
background: none;
|
||||
border: 1px dashed hsl(240,2%,60%);
|
||||
padding: 11px 10px 1px;
|
||||
|
||||
&.top {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
border-top-width: 0;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
&-clip {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
.article-content {
|
||||
color: inherit;
|
||||
|
||||
a[href]:after {
|
||||
content: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.article-content-meta {
|
||||
opacity: 1 !important;
|
||||
display: block !important;
|
||||
transform: none !important;
|
||||
position: static;
|
||||
}
|
||||
|
||||
.attachments.attachments--list {
|
||||
border-color: currentColor !important;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border: 1px solid black;
|
||||
}
|
||||
@@ -69,7 +69,7 @@ class SessionsController < ApplicationController
|
||||
user_id = session[:user_id]
|
||||
end
|
||||
|
||||
if !user_id
|
||||
if !user_id || !User.exists?(user_id)
|
||||
# get models
|
||||
models = SessionHelper.models()
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ curl http://localhost/api/v1/text_modules.json -v -u #{login}:#{password}
|
||||
=end
|
||||
|
||||
def index
|
||||
permission_check('ticket.agent')
|
||||
permission_check(['admin.text_module', 'ticket.agent'])
|
||||
model_index_render(TextModule, params)
|
||||
end
|
||||
|
||||
@@ -71,7 +71,7 @@ curl http://localhost/api/v1/text_modules/#{id}.json -v -u #{login}:#{password}
|
||||
=end
|
||||
|
||||
def show
|
||||
permission_check('ticket.agent')
|
||||
permission_check(['admin.text_module', 'ticket.agent'])
|
||||
model_show_render(TextModule, params)
|
||||
end
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ class TimeAccountingsController < ApplicationController
|
||||
end
|
||||
end
|
||||
end
|
||||
if !customers[local_time_unit[:agent_id]]
|
||||
if !agents[local_time_unit[:agent_id]]
|
||||
agent_user = User.lookup(id: local_time_unit[:agent_id])
|
||||
agent = '-'
|
||||
if agent_user
|
||||
|
||||
@@ -20,17 +20,25 @@ class Authorization < ApplicationModel
|
||||
)
|
||||
|
||||
# update username of auth entry if empty
|
||||
if !auth.username && hash['info']['nickname']
|
||||
if !auth.username && hash['info']['nickname'].present?
|
||||
auth.update!(
|
||||
username: hash['info']['nickname'],
|
||||
)
|
||||
end
|
||||
|
||||
# update image if needed
|
||||
if hash['info']['image']
|
||||
user = User.find(auth.user_id)
|
||||
# update firstname/lastname if needed
|
||||
user = User.find(auth.user_id)
|
||||
if user.firstname.blank? && user.lastname.blank?
|
||||
if hash['info']['first_name'].present? && hash['info']['last_name'].present?
|
||||
user.firstname = hash['info']['first_name']
|
||||
user.lastname = hash['info']['last_name']
|
||||
elsif hash['info']['display_name'].present?
|
||||
user.firstname = hash['info']['display_name']
|
||||
end
|
||||
end
|
||||
|
||||
# save/update avatar
|
||||
# update image if needed
|
||||
if hash['info']['image'].present?
|
||||
avatar = Avatar.add(
|
||||
object: 'User',
|
||||
o_id: user.id,
|
||||
@@ -40,13 +48,14 @@ class Authorization < ApplicationModel
|
||||
updated_by_id: user.id,
|
||||
created_by_id: user.id,
|
||||
)
|
||||
|
||||
# update user link
|
||||
if avatar && user.image != avatar.store_hash
|
||||
user.image = avatar.store_hash
|
||||
user.save
|
||||
end
|
||||
end
|
||||
|
||||
if user.changed?
|
||||
user.save
|
||||
end
|
||||
end
|
||||
auth
|
||||
end
|
||||
|
||||
@@ -123,7 +123,7 @@ add avatar by url
|
||||
data[:full][:content] = content
|
||||
data[:full][:mime_type] = mime_type
|
||||
|
||||
elsif data[:url].to_s.match?(/^http/)
|
||||
elsif data[:url].to_s.match?(%r{^https?://})
|
||||
url = data[:url].to_s
|
||||
|
||||
# check if source ist already updated within last 2 minutes
|
||||
@@ -168,7 +168,7 @@ add avatar by url
|
||||
data[:full][:mime_type] = mime_type
|
||||
|
||||
# try zammad backend to find image based on email
|
||||
elsif data[:url].to_s.match?(/@/)
|
||||
elsif data[:url].to_s.match?(URI::MailTo::EMAIL_REGEXP)
|
||||
url = data[:url].to_s
|
||||
|
||||
# check if source ist already updated within last 3 minutes
|
||||
@@ -179,9 +179,7 @@ add avatar by url
|
||||
# fetch image
|
||||
image = Service::Image.user(url)
|
||||
return if !image
|
||||
data[:resize] ||= {}
|
||||
data[:resize] = image
|
||||
data[:full] ||= {}
|
||||
data[:full] = image
|
||||
end
|
||||
end
|
||||
|
||||
@@ -134,7 +134,7 @@ returns
|
||||
=end
|
||||
|
||||
def self.streamable?
|
||||
true
|
||||
false
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
@@ -70,15 +70,16 @@ class Channel::EmailParser
|
||||
=end
|
||||
|
||||
def parse(msg)
|
||||
mail = Mail.new(msg.utf8_encode)
|
||||
mail = Mail.new(msg.force_encoding('binary'))
|
||||
|
||||
headers = message_header_hash(mail)
|
||||
body = message_body_hash(mail)
|
||||
message_attributes = [
|
||||
{ mail_instance: mail },
|
||||
message_header_hash(mail),
|
||||
message_body_hash(mail),
|
||||
self.class.sender_attributes(mail),
|
||||
headers,
|
||||
body,
|
||||
self.class.sender_attributes(headers),
|
||||
]
|
||||
|
||||
message_attributes.reduce({}.with_indifferent_access, &:merge)
|
||||
end
|
||||
|
||||
@@ -340,8 +341,8 @@ returns
|
||||
end
|
||||
|
||||
def self.sender_attributes(from)
|
||||
if from.is_a?(Mail::Message)
|
||||
from = SENDER_FIELDS.map { |f| from.header[f] }.compact
|
||||
if from.is_a?(HashWithIndifferentAccess)
|
||||
from = SENDER_FIELDS.map { |f| from[f] }.compact
|
||||
.map(&:to_utf8).reject(&:blank?)
|
||||
.partition { |address| address.match?(EMAIL_REGEX) }
|
||||
.flatten.first
|
||||
@@ -476,12 +477,14 @@ process unprocessable_mails (tmp/unprocessable_mail/*.eml) again
|
||||
|
||||
def message_header_hash(mail)
|
||||
imported_fields = mail.header.fields.map do |f|
|
||||
value = begin
|
||||
f.to_utf8
|
||||
rescue NameError # handle bug #1238 in Mail 2.7.1.rc1
|
||||
'' # swap out for commented line below once upgrade is available
|
||||
end
|
||||
|
||||
begin
|
||||
value = f.to_utf8
|
||||
if value.blank?
|
||||
value = f.raw_value.to_utf8
|
||||
end
|
||||
rescue
|
||||
value = f.raw_value.to_utf8(fallback: :read_as_sanitized_binary)
|
||||
end
|
||||
[f.name.downcase, value]
|
||||
end.to_h
|
||||
|
||||
@@ -492,10 +495,15 @@ process unprocessable_mails (tmp/unprocessable_mail/*.eml) again
|
||||
.transform_values { |v| v.match?(EMAIL_REGEX) ? v : '' }
|
||||
h.merge!(validated_recipients)
|
||||
|
||||
h['date'] = Time.zone.parse(mail.date.to_s) || imported_fields['date']
|
||||
h['message_id'] = imported_fields['message-id']
|
||||
h['subject'] = imported_fields['subject']&.sub(/^=\?us-ascii\?Q\?(.+)\?=$/, '\1')
|
||||
h['x-any-recipient'] = validated_recipients.values.select(&:present?).join(', ')
|
||||
|
||||
begin
|
||||
h['date'] = Time.zone.parse(mail.date.to_s) || imported_fields['date']
|
||||
rescue
|
||||
h['date'] = nil
|
||||
end
|
||||
end
|
||||
|
||||
[imported_fields, raw_fields, custom_fields].reduce({}.with_indifferent_access, &:merge)
|
||||
@@ -570,7 +578,7 @@ process unprocessable_mails (tmp/unprocessable_mail/*.eml) again
|
||||
|
||||
def get_attachments(file, attachments, mail)
|
||||
return file.parts.map { |p| get_attachments(p, attachments, mail) } if file.parts.any?
|
||||
return [] if [mail.text_part, mail.html_part].include?(file)
|
||||
return [] if [mail.text_part&.body&.encoded, mail.html_part&.body&.encoded].include?(file.body.encoded)
|
||||
|
||||
# get file preferences
|
||||
headers_store = {}
|
||||
@@ -597,15 +605,29 @@ process unprocessable_mails (tmp/unprocessable_mail/*.eml) again
|
||||
# get filename from content-disposition
|
||||
|
||||
# workaround for: NoMethodError: undefined method `filename' for #<Mail::UnstructuredField:0x007ff109e80678>
|
||||
filename = file.header[:content_disposition].try(:filename)
|
||||
begin
|
||||
filename = file.header[:content_disposition].try(:filename)
|
||||
rescue
|
||||
begin
|
||||
if file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})="(.+?)"/i
|
||||
filename = $3
|
||||
elsif file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})='(.+?)'/i
|
||||
filename = $3
|
||||
elsif file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})=(.+?);/i
|
||||
filename = $3
|
||||
end
|
||||
rescue
|
||||
Rails.logger.debug { 'Unable to get filename' }
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
if file.header[:content_disposition].to_s =~ /filename="(.+?)"/i
|
||||
filename = $1
|
||||
elsif file.header[:content_disposition].to_s =~ /filename='(.+?)'/i
|
||||
filename = $1
|
||||
elsif file.header[:content_disposition].to_s =~ /filename=(.+?);/i
|
||||
filename = $1
|
||||
if file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})="(.+?)"/i
|
||||
filename = $3
|
||||
elsif file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})='(.+?)'/i
|
||||
filename = $3
|
||||
elsif file.header[:content_disposition].to_s =~ /(filename|name)(\*{0,1})=(.+?);/i
|
||||
filename = $3
|
||||
end
|
||||
rescue
|
||||
Rails.logger.debug { 'Unable to get filename' }
|
||||
@@ -613,12 +635,12 @@ process unprocessable_mails (tmp/unprocessable_mail/*.eml) again
|
||||
|
||||
# as fallback, use raw values
|
||||
if filename.blank?
|
||||
if headers_store['Content-Disposition'].to_s =~ /filename="(.+?)"/i
|
||||
filename = $1
|
||||
elsif headers_store['Content-Disposition'].to_s =~ /filename='(.+?)'/i
|
||||
filename = $1
|
||||
elsif headers_store['Content-Disposition'].to_s =~ /filename=(.+?);/i
|
||||
filename = $1
|
||||
if headers_store['Content-Disposition'].to_s =~ /(filename|name)(\*{0,1})="(.+?)"/i
|
||||
filename = $3
|
||||
elsif headers_store['Content-Disposition'].to_s =~ /(filename|name)(\*{0,1})='(.+?)'/i
|
||||
filename = $3
|
||||
elsif headers_store['Content-Disposition'].to_s =~ /(filename|name)(\*{0,1})=(.+?);/i
|
||||
filename = $3
|
||||
end
|
||||
end
|
||||
|
||||
@@ -652,9 +674,9 @@ process unprocessable_mails (tmp/unprocessable_mail/*.eml) again
|
||||
|
||||
# e. g. Content-Type: video/quicktime; name="Video.MOV";
|
||||
if filename.blank?
|
||||
['name="(.+?)"(;|$)', "name='(.+?)'(;|$)", 'name=(.+?)(;|$)'].each do |regexp|
|
||||
['(filename|name)(\*{0,1})="(.+?)"(;|$)', '(filename|name)(\*{0,1})=\'(.+?)\'(;|$)', '(filename|name)(\*{0,1})=(.+?)(;|$)'].each do |regexp|
|
||||
if headers_store['Content-Type'] =~ /#{regexp}/i
|
||||
filename = $1
|
||||
filename = $3
|
||||
break
|
||||
end
|
||||
end
|
||||
@@ -743,7 +765,11 @@ module Mail
|
||||
# workaround to get content of no parseable headers - in most cases with non 7 bit ascii signs
|
||||
class Field
|
||||
def raw_value
|
||||
value = @raw_value.try(:utf8_encode)
|
||||
begin
|
||||
value = @raw_value.try(:utf8_encode)
|
||||
rescue
|
||||
value = @raw_value.utf8_encode(fallback: :read_as_sanitized_binary)
|
||||
end
|
||||
return value if value.blank?
|
||||
value.sub(/^.+?:(\s|)/, '')
|
||||
end
|
||||
@@ -784,4 +810,19 @@ module Mail
|
||||
end.join('')
|
||||
end
|
||||
end
|
||||
|
||||
# issue#348 - IMAP mail fetching stops because of broken spam email (e. g. broken Content-Transfer-Encoding value see test/fixtures/mail43.box)
|
||||
# https://github.com/zammad/zammad/issues/348
|
||||
class Body
|
||||
def decoded
|
||||
if !Encodings.defined?(encoding)
|
||||
#raise UnknownEncodingType, "Don't know how to decode #{encoding}, please call #encoded and decode it yourself."
|
||||
Rails.logger.info "UnknownEncodingType: Don't know how to decode #{encoding}!"
|
||||
raw_source
|
||||
else
|
||||
Encodings.get_encoding(encoding).decode(raw_source)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -143,63 +143,57 @@ module Channel::Filter::IdentifySender
|
||||
end
|
||||
end
|
||||
|
||||
def self.user_create(data, role_ids = nil)
|
||||
data[:email] += '@local' if !data[:email].match?(/@/)
|
||||
data[:email] = cleanup_email(data[:email])
|
||||
user = User.find_by(email: data[:email]) ||
|
||||
User.find_by(login: data[:email])
|
||||
def self.user_create(attrs, role_ids = nil)
|
||||
populate_attributes!(attrs, role_ids: role_ids)
|
||||
|
||||
# check if firstname or lastname need to be updated
|
||||
if user
|
||||
if user.firstname.blank? && user.lastname.blank?
|
||||
if data[:firstname].present?
|
||||
data[:firstname] = cleanup_name(data[:firstname])
|
||||
user.update!(
|
||||
firstname: data[:firstname]
|
||||
)
|
||||
end
|
||||
end
|
||||
return user
|
||||
if (user = User.find_by('email = :email OR login = :email', attrs))
|
||||
user.update!(attrs.slice(:firstname)) if user.firstname.blank? && user.lastname.blank? && attrs[:firstname].present?
|
||||
elsif (user = User.create!(attrs))
|
||||
user.update!(updated_by_id: user.id, created_by_id: user.id)
|
||||
end
|
||||
|
||||
# create new user
|
||||
role_ids ||= Role.signup_role_ids
|
||||
|
||||
# fillup
|
||||
%w[firstname lastname].each do |item|
|
||||
if data[item.to_sym].nil?
|
||||
data[item.to_sym] = ''
|
||||
end
|
||||
data[item.to_sym] = cleanup_name(data[item.to_sym])
|
||||
end
|
||||
data[:password] = ''
|
||||
data[:active] = true
|
||||
data[:role_ids] = role_ids
|
||||
data[:updated_by_id] = 1
|
||||
data[:created_by_id] = 1
|
||||
|
||||
user = User.create!(data)
|
||||
user.update!(
|
||||
updated_by_id: user.id,
|
||||
created_by_id: user.id,
|
||||
)
|
||||
user
|
||||
end
|
||||
|
||||
def self.cleanup_name(string)
|
||||
string.strip!
|
||||
string.delete!('"')
|
||||
string.gsub!(/^'/, '')
|
||||
string.gsub!(/'$/, '')
|
||||
string.gsub!(/.+?\s\(.+?\)$/, '')
|
||||
string
|
||||
def self.populate_attributes!(attrs, **extras)
|
||||
if attrs[:email].match?(/\S\s+\S/) || attrs[:email].match?(/^<|>$/)
|
||||
attrs[:preferences] = { mail_delivery_failed: true,
|
||||
mail_delivery_failed_reason: 'invalid email',
|
||||
mail_delivery_failed_data: Time.zone.now }
|
||||
end
|
||||
|
||||
attrs.merge!(
|
||||
email: sanitize_email(attrs[:email]),
|
||||
firstname: sanitize_name(attrs[:firstname]),
|
||||
lastname: sanitize_name(attrs[:lastname]),
|
||||
password: '',
|
||||
active: true,
|
||||
role_ids: extras[:role_ids] || Role.signup_role_ids,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1
|
||||
)
|
||||
end
|
||||
|
||||
def self.cleanup_email(string)
|
||||
string = string.downcase
|
||||
string.strip!
|
||||
string.delete!('"')
|
||||
string
|
||||
def self.sanitize_name(string)
|
||||
return '' if string.nil?
|
||||
|
||||
string.strip
|
||||
.delete('"')
|
||||
.gsub(/^'/, '')
|
||||
.gsub(/'$/, '')
|
||||
.gsub(/.+?\s\(.+?\)$/, '')
|
||||
end
|
||||
|
||||
def self.sanitize_email(string)
|
||||
string += '@local' if !string.include?('@')
|
||||
|
||||
string.downcase
|
||||
.strip
|
||||
.delete('"')
|
||||
.delete(' ') # see https://github.com/zammad/zammad/issues/2254
|
||||
.sub(/^<|>$/, '') # see https://github.com/zammad/zammad/issues/2254
|
||||
.sub(/\A'(.*)'\z/, '\1') # see https://github.com/zammad/zammad/issues/2154
|
||||
.gsub(/\s/, '') # see https://github.com/zammad/zammad/issues/2198
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -106,6 +106,10 @@ returns
|
||||
# set user id
|
||||
user_id = record[:created_by_id]
|
||||
if model == User
|
||||
if record.destroyed?
|
||||
Cti::CallerId.where(user_id: user_id).destroy_all
|
||||
return
|
||||
end
|
||||
user_id = record.id
|
||||
end
|
||||
return if !user_id
|
||||
@@ -121,9 +125,31 @@ returns
|
||||
caller_ids = caller_ids.concat(local_caller_ids)
|
||||
end
|
||||
|
||||
# store caller ids
|
||||
Cti::CallerId.where(object: model.to_s, o_id: record.id).destroy_all
|
||||
caller_ids.each do |caller_id|
|
||||
# search for caller ids to keep
|
||||
caller_ids_to_add = []
|
||||
existing_record_ids = Cti::CallerId.where(object: model.to_s, o_id: record.id).pluck(:id)
|
||||
caller_ids.uniq.each do |caller_id|
|
||||
existing_record_id = Cti::CallerId.where(
|
||||
object: model.to_s,
|
||||
o_id: record.id,
|
||||
caller_id: caller_id,
|
||||
level: level,
|
||||
user_id: user_id,
|
||||
).pluck(:id)
|
||||
if existing_record_id[0]
|
||||
existing_record_ids.delete(existing_record_id[0])
|
||||
next
|
||||
end
|
||||
caller_ids_to_add.push caller_id
|
||||
end
|
||||
|
||||
# delete not longer existing caller ids
|
||||
existing_record_ids.each do |record_id|
|
||||
Cti::CallerId.destroy(record_id)
|
||||
end
|
||||
|
||||
# create new caller ids
|
||||
caller_ids_to_add.each do |caller_id|
|
||||
Cti::CallerId.maybe_add(
|
||||
caller_id: caller_id,
|
||||
level: level,
|
||||
@@ -259,10 +285,13 @@ returns
|
||||
end
|
||||
|
||||
def update_cti_logs
|
||||
return if object != 'User'
|
||||
UpdateCtiLogsByCallerJob.perform_later(caller_id)
|
||||
end
|
||||
|
||||
def update_cti_logs_with_fg_optimization
|
||||
return if object != 'User'
|
||||
return if level != 'known'
|
||||
UpdateCtiLogsByCallerJob.perform_now(caller_id, limit: 20)
|
||||
UpdateCtiLogsByCallerJob.perform_later(caller_id, limit: 40, offset: 20)
|
||||
end
|
||||
|
||||
@@ -4,13 +4,11 @@ module Cti
|
||||
|
||||
store :preferences
|
||||
|
||||
after_create :push_event, :push_caller_list
|
||||
after_update :push_event, :push_caller_list
|
||||
after_destroy :push_event, :push_caller_list
|
||||
after_commit :push_incoming_call, :push_caller_list_update
|
||||
|
||||
=begin
|
||||
|
||||
Cti::Log.create(
|
||||
Cti::Log.create!(
|
||||
direction: 'in',
|
||||
from: '007',
|
||||
from_comment: 'AAA',
|
||||
@@ -21,7 +19,7 @@ module Cti
|
||||
state: 'newCall',
|
||||
)
|
||||
|
||||
Cti::Log.create(
|
||||
Cti::Log.create!(
|
||||
direction: 'in',
|
||||
from: '007',
|
||||
from_comment: '',
|
||||
@@ -32,7 +30,7 @@ module Cti
|
||||
state: 'answer',
|
||||
)
|
||||
|
||||
Cti::Log.create(
|
||||
Cti::Log.create!(
|
||||
direction: 'in',
|
||||
from: '009',
|
||||
from_comment: '',
|
||||
@@ -45,13 +43,13 @@ module Cti
|
||||
|
||||
example data, can be used for demo
|
||||
|
||||
Cti::Log.create(
|
||||
Cti::Log.create!(
|
||||
direction: 'in',
|
||||
from: '4930609854180',
|
||||
from_comment: 'Franz Bauer',
|
||||
to: '4930609811111',
|
||||
to_comment: 'Bob Smith',
|
||||
call_id: '00001',
|
||||
call_id: '435452113',
|
||||
comment: '',
|
||||
state: 'newCall',
|
||||
done: false,
|
||||
@@ -69,13 +67,13 @@ example data, can be used for demo
|
||||
}
|
||||
)
|
||||
|
||||
Cti::Log.create(
|
||||
Cti::Log.create!(
|
||||
direction: 'out',
|
||||
from: '4930609854180',
|
||||
from_comment: 'Franz Bauer',
|
||||
to: '4930609811111',
|
||||
to_comment: 'Bob Smith',
|
||||
call_id: '00002',
|
||||
call_id: rand(999_999_999),
|
||||
comment: '',
|
||||
state: 'newCall',
|
||||
preferences: {
|
||||
@@ -92,13 +90,13 @@ example data, can be used for demo
|
||||
}
|
||||
)
|
||||
|
||||
Cti::Log.create(
|
||||
Cti::Log.create!(
|
||||
direction: 'in',
|
||||
from: '4930609854180',
|
||||
from_comment: 'Franz Bauer',
|
||||
to: '4930609811111',
|
||||
to_comment: 'Bob Smith',
|
||||
call_id: '00003',
|
||||
call_id: rand(999_999_999),
|
||||
comment: '',
|
||||
state: 'answer',
|
||||
preferences: {
|
||||
@@ -115,13 +113,13 @@ example data, can be used for demo
|
||||
}
|
||||
)
|
||||
|
||||
Cti::Log.create(
|
||||
Cti::Log.create!(
|
||||
direction: 'in',
|
||||
from: '4930609854180',
|
||||
from_comment: 'Franz Bauer',
|
||||
to: '4930609811111',
|
||||
to_comment: 'Bob Smith',
|
||||
call_id: '00004',
|
||||
call_id: rand(999_999_999),
|
||||
comment: '',
|
||||
state: 'hangup',
|
||||
comment: 'normalClearing',
|
||||
@@ -140,13 +138,13 @@ example data, can be used for demo
|
||||
}
|
||||
)
|
||||
|
||||
Cti::Log.create(
|
||||
Cti::Log.create!(
|
||||
direction: 'in',
|
||||
from: '4930609854180',
|
||||
from_comment: 'Franz Bauer',
|
||||
to: '4930609811111',
|
||||
to_comment: 'Bob Smith',
|
||||
call_id: '00005',
|
||||
call_id: rand(999_999_999),
|
||||
comment: '',
|
||||
state: 'hangup',
|
||||
start_at: Time.zone.now - 15.seconds,
|
||||
@@ -165,13 +163,13 @@ example data, can be used for demo
|
||||
}
|
||||
)
|
||||
|
||||
Cti::Log.create(
|
||||
Cti::Log.create!(
|
||||
direction: 'in',
|
||||
from: '4930609854180',
|
||||
from_comment: 'Franz Bauer',
|
||||
to: '4930609811111',
|
||||
to_comment: '',
|
||||
call_id: '00006',
|
||||
call_id: rand(999_999_999),
|
||||
comment: '',
|
||||
state: 'hangup',
|
||||
start_at: Time.zone.now - 15.seconds,
|
||||
@@ -190,13 +188,13 @@ example data, can be used for demo
|
||||
}
|
||||
)
|
||||
|
||||
Cti::Log.create(
|
||||
Cti::Log.create!(
|
||||
direction: 'in',
|
||||
from: '4930609854180',
|
||||
from_comment: 'Franz Bauer',
|
||||
to: '4930609811111',
|
||||
to_comment: 'Bob Smith',
|
||||
call_id: '00007',
|
||||
call_id: rand(999_999_999),
|
||||
comment: '',
|
||||
state: 'hangup',
|
||||
start_at: Time.zone.now - 15.seconds,
|
||||
@@ -215,11 +213,11 @@ example data, can be used for demo
|
||||
}
|
||||
)
|
||||
|
||||
Cti::Log.create(
|
||||
Cti::Log.create!(
|
||||
direction: 'in',
|
||||
from: '4930609854180',
|
||||
to: '4930609811112',
|
||||
call_id: '00008',
|
||||
call_id: rand(999_999_999),
|
||||
comment: '',
|
||||
state: 'hangup',
|
||||
start_at: Time.zone.now - 20.seconds,
|
||||
@@ -236,14 +234,14 @@ example data, can be used for demo
|
||||
returns
|
||||
|
||||
{
|
||||
list: [...]
|
||||
assets: {...}
|
||||
list: [log_record1, log_record2, log_record3],
|
||||
assets: {...},
|
||||
}
|
||||
|
||||
=end
|
||||
|
||||
def self.log
|
||||
list = Cti::Log.order('created_at DESC, id DESC').limit(60)
|
||||
list = Cti::Log.log_records
|
||||
|
||||
# add assets
|
||||
assets = list.map(&:preferences)
|
||||
@@ -259,6 +257,20 @@ returns
|
||||
}
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
Cti::Log.log_records
|
||||
|
||||
returns
|
||||
|
||||
[log_record1, log_record2, log_record3]
|
||||
|
||||
=end
|
||||
|
||||
def self.log_records
|
||||
Cti::Log.order('created_at DESC, id DESC').limit(60)
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
processes a incoming event
|
||||
@@ -269,7 +281,7 @@ Cti::Log.process(
|
||||
'user' => 'user 1',
|
||||
'from' => '4912347114711',
|
||||
'to' => '4930600000000',
|
||||
'callId' => '4991155921769858278-1', # or call_id
|
||||
'callId' => '43545211', # or call_id
|
||||
'direction' => 'in',
|
||||
)
|
||||
|
||||
@@ -287,6 +299,7 @@ Cti::Log.process(
|
||||
from_comment = nil
|
||||
to_comment = nil
|
||||
preferences = nil
|
||||
done = true
|
||||
if params['direction'] == 'in'
|
||||
to_comment = user
|
||||
from_comment, preferences = CallerId.get_comment_preferences(params['from'], 'from')
|
||||
@@ -297,6 +310,9 @@ Cti::Log.process(
|
||||
|
||||
case event
|
||||
when 'newCall'
|
||||
if params['direction'] == 'in'
|
||||
done = false
|
||||
end
|
||||
create(
|
||||
direction: params['direction'],
|
||||
from: params['from'],
|
||||
@@ -308,6 +324,7 @@ Cti::Log.process(
|
||||
state: event,
|
||||
initialized_at: Time.zone.now,
|
||||
preferences: preferences,
|
||||
done: done,
|
||||
)
|
||||
when 'answer'
|
||||
log = find_by(call_id: call_id)
|
||||
@@ -318,16 +335,19 @@ Cti::Log.process(
|
||||
if user
|
||||
log.to_comment = user
|
||||
end
|
||||
log.done = true
|
||||
log.comment = comment
|
||||
log.save
|
||||
when 'hangup'
|
||||
log = find_by(call_id: call_id)
|
||||
raise "No such call_id #{call_id}" if !log
|
||||
if params['direction'] == 'in' && log.state == 'newCall'
|
||||
log.done = false
|
||||
end
|
||||
if params['direction'] == 'in' && log.to_comment == 'voicemail'
|
||||
log.done = false
|
||||
log.done = done
|
||||
if params['direction'] == 'in'
|
||||
if log.state == 'newCall'
|
||||
log.done = false
|
||||
elsif log.to_comment == 'voicemail'
|
||||
log.done = false
|
||||
end
|
||||
end
|
||||
log.state = 'hangup'
|
||||
log.end_at = Time.zone.now
|
||||
@@ -343,11 +363,14 @@ Cti::Log.process(
|
||||
end
|
||||
end
|
||||
|
||||
def push_event
|
||||
def push_incoming_call
|
||||
return true if destroyed?
|
||||
return true if state != 'newCall'
|
||||
return true if direction != 'in'
|
||||
|
||||
# send notify about event
|
||||
users = User.with_permissions('cti.agent')
|
||||
users.each do |user|
|
||||
|
||||
# send notify about event
|
||||
Sessions.send_to(
|
||||
user.id,
|
||||
{
|
||||
@@ -356,23 +379,29 @@ Cti::Log.process(
|
||||
},
|
||||
)
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
def push_caller_list
|
||||
list = Cti::Log.log
|
||||
def self.push_caller_list_update?(record)
|
||||
list_ids = Cti::Log.log_records.pluck(:id)
|
||||
return true if list_ids.include?(record.id)
|
||||
false
|
||||
end
|
||||
|
||||
def push_caller_list_update
|
||||
return false if !Cti::Log.push_caller_list_update?(self)
|
||||
|
||||
# send notify on create/update/delete
|
||||
users = User.with_permissions('cti.agent')
|
||||
users.each do |user|
|
||||
|
||||
# send notify on create/update/delete
|
||||
Sessions.send_to(
|
||||
user.id,
|
||||
{
|
||||
event: 'cti_list_push',
|
||||
data: list,
|
||||
},
|
||||
)
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
@@ -24,8 +24,9 @@ Job.run
|
||||
|
||||
def self.run
|
||||
start_at = Time.zone.now
|
||||
jobs = Job.where(active: true, running: false)
|
||||
jobs = Job.where(active: true)
|
||||
jobs.each do |job|
|
||||
next if !job.executable?
|
||||
job.run(false, start_at)
|
||||
end
|
||||
true
|
||||
@@ -75,6 +76,7 @@ job.run(true)
|
||||
|
||||
self.processed = ticket_count || 0
|
||||
self.running = true
|
||||
self.last_run_at = Time.zone.now
|
||||
save!
|
||||
|
||||
tickets&.each do |ticket|
|
||||
@@ -94,6 +96,9 @@ job.run(true)
|
||||
# only execute jobs, older then 1 min, to give admin posibility to change
|
||||
return false if updated_at > Time.zone.now - 1.minute
|
||||
|
||||
# check if job got stuck
|
||||
return false if running == true && last_run_at && Time.zone.now - 1.day < last_run_at
|
||||
|
||||
# check if jobs need to be executed
|
||||
# ignore if job was running within last 10 min.
|
||||
return false if last_run_at && last_run_at > start_at - 10.minutes
|
||||
|
||||
@@ -621,6 +621,12 @@ to send no browser reload event, pass false
|
||||
# config changes
|
||||
if attribute.to_config
|
||||
execute_config_count += 1
|
||||
if attribute.data_type == 'select' && attribute.data_option[:options]
|
||||
historical_options = attribute.data_option[:historical_options] || {}
|
||||
historical_options.update(attribute.data_option[:options])
|
||||
historical_options.update(attribute.data_option_new[:options])
|
||||
attribute.data_option_new[:historical_options] = historical_options
|
||||
end
|
||||
attribute.data_option = attribute.data_option_new
|
||||
attribute.data_option_new = {}
|
||||
attribute.to_config = false
|
||||
@@ -628,6 +634,10 @@ to send no browser reload event, pass false
|
||||
next if !attribute.to_create && !attribute.to_migrate && !attribute.to_delete
|
||||
end
|
||||
|
||||
if attribute.data_type == 'select' && attribute.data_option[:options]
|
||||
attribute.data_option[:historical_options] = attribute.data_option[:options]
|
||||
end
|
||||
|
||||
data_type = nil
|
||||
if attribute.data_type.match?(/^input|select|tree_select|richtext|textarea|checkbox$/)
|
||||
data_type = :string
|
||||
@@ -868,6 +878,13 @@ is certain attribute used by triggers, overviews or schedulers
|
||||
reserved_words = %w[destroy true false integer select drop create alter index table varchar blob date datetime timestamp]
|
||||
raise "#{name} is a reserved word, please choose a different one" if name.match?(/^(#{reserved_words.join('|')})$/)
|
||||
|
||||
# fixes issue #2236 - Naming an attribute "attribute" causes ActiveRecord failure
|
||||
begin
|
||||
ObjectLookup.by_id(object_lookup_id).constantize.instance_method_already_implemented? name
|
||||
rescue ActiveRecord::DangerousAttributeError => e
|
||||
raise "#{name} is a reserved word, please choose a different one"
|
||||
end
|
||||
|
||||
record = object_lookup.name.constantize.new
|
||||
return true if !record.respond_to?(name.to_sym)
|
||||
raise "#{name} already exists!" if record.attributes.key?(name) && new_record?
|
||||
|
||||
@@ -13,24 +13,16 @@ class RecentView < ApplicationModel
|
||||
after_destroy :notify_clients
|
||||
|
||||
def self.log(object, o_id, user)
|
||||
|
||||
# access check
|
||||
return if !access(object, o_id, user)
|
||||
|
||||
# lookups
|
||||
object_lookup_id = ObjectLookup.by_name(object)
|
||||
|
||||
# create entry
|
||||
record = {
|
||||
o_id: o_id,
|
||||
recent_view_object_id: object_lookup_id.to_i,
|
||||
created_by_id: user.id,
|
||||
}
|
||||
RecentView.create(record)
|
||||
RecentView.create(o_id: o_id,
|
||||
recent_view_object_id: ObjectLookup.by_name(object),
|
||||
created_by_id: user.id)
|
||||
end
|
||||
|
||||
def self.log_destroy(requested_object, requested_object_id)
|
||||
return if requested_object == 'RecentView'
|
||||
|
||||
RecentView.where(recent_view_object_id: ObjectLookup.by_name(requested_object))
|
||||
.where(o_id: requested_object_id)
|
||||
.destroy_all
|
||||
@@ -41,43 +33,34 @@ class RecentView < ApplicationModel
|
||||
end
|
||||
|
||||
def self.list(user, limit = 10, object_name = nil)
|
||||
recent_views = if !object_name
|
||||
RecentView.select('o_id, recent_view_object_id, MAX(created_at) as created_at, MAX(id) as id, created_by_id')
|
||||
.group(:o_id, :recent_view_object_id, :created_by_id)
|
||||
.where(created_by_id: user.id)
|
||||
.limit(limit)
|
||||
elsif object_name == 'Ticket'
|
||||
state_ids = Ticket::State.by_category(:viewable_agent_new).pluck(:id)
|
||||
local_recent_views = RecentView.select('o_id, recent_view_object_id, MAX(created_at) as created_at, MAX(id) as id, created_by_id')
|
||||
.group(:o_id, :recent_view_object_id, :created_by_id)
|
||||
.where(created_by_id: user.id, recent_view_object_id: ObjectLookup.by_name(object_name))
|
||||
.limit(limit + 10)
|
||||
clear_list = []
|
||||
local_recent_views.each do |item|
|
||||
ticket = Ticket.find_by(id: item.o_id)
|
||||
next if !ticket
|
||||
next if !state_ids.include?(ticket.state_id)
|
||||
clear_list.push item
|
||||
break if clear_list.count == limit
|
||||
end
|
||||
clear_list
|
||||
else
|
||||
RecentView.select('o_id, recent_view_object_id, MAX(created_at) as created_at, MAX(id) as id, created_by_id')
|
||||
.group(:o_id, :recent_view_object_id, :created_by_id)
|
||||
.where(created_by_id: user.id, recent_view_object_id: ObjectLookup.by_name(object_name))
|
||||
.limit(limit)
|
||||
end
|
||||
recent_views = RecentView.select('o_id, ' \
|
||||
'recent_view_object_id, ' \
|
||||
'created_by_id, ' \
|
||||
'MAX(created_at) as created_at, ' \
|
||||
'MAX(id) as id')
|
||||
.group(:o_id, :recent_view_object_id, :created_by_id)
|
||||
.where(created_by_id: user.id)
|
||||
.order('MAX(created_at) DESC, MAX(id) DESC')
|
||||
.limit(limit)
|
||||
|
||||
list = []
|
||||
recent_views.each do |item|
|
||||
|
||||
# access check
|
||||
next if !access(ObjectLookup.by_id(item['recent_view_object_id']), item['o_id'], user)
|
||||
|
||||
# add to result list
|
||||
list.push item
|
||||
if object_name.present?
|
||||
recent_views = recent_views.where(recent_view_object_id: ObjectLookup.by_name(object_name))
|
||||
end
|
||||
list
|
||||
|
||||
# hide merged / removed tickets in Ticket Merge dialog
|
||||
if object_name == 'Ticket'
|
||||
recent_views = recent_views.limit(limit * 2)
|
||||
|
||||
viewable_ticket_ids = Ticket.where('id IN (?) AND state_id in (?)',
|
||||
recent_views.map(&:o_id),
|
||||
Ticket::State.by_category(:viewable_agent_new).pluck(:id))
|
||||
.pluck(:id)
|
||||
|
||||
recent_views = recent_views.select { |rv| viewable_ticket_ids.include?(rv.o_id) }
|
||||
.first(limit)
|
||||
end
|
||||
|
||||
recent_views.select { |rv| access(ObjectLookup.by_id(rv.recent_view_object_id), rv.o_id, user) }
|
||||
end
|
||||
|
||||
def notify_clients
|
||||
@@ -91,19 +74,12 @@ class RecentView < ApplicationModel
|
||||
end
|
||||
|
||||
def self.access(object, o_id, user)
|
||||
|
||||
# check if object exists
|
||||
begin
|
||||
return if !Kernel.const_get(object)
|
||||
record = Kernel.const_get(object).lookup(id: o_id)
|
||||
return if !record
|
||||
rescue
|
||||
return
|
||||
end
|
||||
|
||||
# check permission
|
||||
return if !record.respond_to?(:access?)
|
||||
record.access?(user, 'read')
|
||||
object.to_s
|
||||
.constantize
|
||||
.try(:lookup, { id: o_id })
|
||||
.try(:access?, user, 'read')
|
||||
rescue NameError
|
||||
false
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
@@ -233,7 +233,9 @@ class Scheduler < ApplicationModel
|
||||
|
||||
# start loop for periods equal or under 5 minutes
|
||||
if job.period && job.period <= 5.minutes
|
||||
loop_count = 0
|
||||
loop do
|
||||
loop_count += 1
|
||||
_start_job(job)
|
||||
job = Scheduler.lookup(id: job.id)
|
||||
|
||||
@@ -246,6 +248,9 @@ class Scheduler < ApplicationModel
|
||||
# exit if there is no loop period defined
|
||||
break if !job.period
|
||||
|
||||
# only do a certain amount of loops in this thread
|
||||
break if loop_count == 1800
|
||||
|
||||
# wait until next run
|
||||
sleep job.period
|
||||
end
|
||||
|
||||
@@ -51,7 +51,7 @@ get config setting
|
||||
|
||||
def self.get(name)
|
||||
load
|
||||
@@current[name]
|
||||
@@current[name].deep_dup # prevents accidental modification of settings in console
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
@@ -465,7 +465,7 @@ condition example
|
||||
value: '2015-10-17T06:00:00.000Z',
|
||||
},
|
||||
'ticket.created_at' => {
|
||||
operator: 'within next (relative)', # before,within,in,after
|
||||
operator: 'within next (relative)', # within next, within last, after, before
|
||||
range: 'day', # minute|hour|day|month|year
|
||||
value: '25',
|
||||
},
|
||||
@@ -544,9 +544,12 @@ condition example
|
||||
raise "Invalid selector #{selector_raw.inspect}" if !selector_raw.respond_to?(:key?)
|
||||
selector = selector_raw.stringify_keys
|
||||
raise "Invalid selector, operator missing #{selector.inspect}" if !selector['operator']
|
||||
raise "Invalid selector, operator #{selector['operator']} is invalid #{selector.inspect}" if selector['operator'] !~ /^(is|is\snot|contains|contains\s(not|all|one|all\snot|one\snot)|(after|before)\s\(absolute\)|(within\snext|within\slast|after|before)\s\(relative\))$/
|
||||
|
||||
# validate value / allow blank but only if pre_condition exists and is not specific
|
||||
if !selector.key?('value') || ((selector['value'].class == String || selector['value'].class == Array) && (selector['value'].respond_to?(:blank?) && selector['value'].blank?))
|
||||
if !selector.key?('value') ||
|
||||
(selector['value'].class == Array && selector['value'].respond_to?(:blank?) && selector['value'].blank?) ||
|
||||
(selector['operator'] =~ /^contains/ && selector['value'].respond_to?(:blank?) && selector['value'].blank?)
|
||||
return nil if selector['pre_condition'].nil?
|
||||
return nil if selector['pre_condition'].respond_to?(:blank?) && selector['pre_condition'].blank?
|
||||
return nil if selector['pre_condition'] == 'specific'
|
||||
@@ -598,10 +601,18 @@ condition example
|
||||
if selector['value'].nil?
|
||||
query += "#{attribute} IS NULL"
|
||||
else
|
||||
query += "#{attribute} IN (?)"
|
||||
if attributes[1] == 'out_of_office_replacement_id'
|
||||
query += "#{attribute} IN (?)"
|
||||
bind_params.push User.find(selector['value']).out_of_office_agent_of.pluck(:id)
|
||||
else
|
||||
if selector['value'].class != Array
|
||||
selector['value'] = [selector['value']]
|
||||
end
|
||||
query += if selector['value'].include?('')
|
||||
"(#{attribute} IN (?) OR #{attribute} IS NULL)"
|
||||
else
|
||||
"#{attribute} IN (?)"
|
||||
end
|
||||
bind_params.push selector['value']
|
||||
end
|
||||
end
|
||||
@@ -631,10 +642,18 @@ condition example
|
||||
if selector['value'].nil?
|
||||
query += "#{attribute} IS NOT NULL"
|
||||
else
|
||||
query += "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
|
||||
if attributes[1] == 'out_of_office_replacement_id'
|
||||
bind_params.push User.find(selector['value']).out_of_office_agent_of.pluck(:id)
|
||||
query += "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
|
||||
else
|
||||
if selector['value'].class != Array
|
||||
selector['value'] = [selector['value']]
|
||||
end
|
||||
query += if selector['value'].include?('')
|
||||
"(#{attribute} IS NOT NULL AND #{attribute} NOT IN (?))"
|
||||
else
|
||||
"(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
|
||||
end
|
||||
bind_params.push selector['value']
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,7 +19,8 @@ class User < ApplicationModel
|
||||
has_many :authorizations, after_add: :cache_update, after_remove: :cache_update
|
||||
belongs_to :organization, inverse_of: :members
|
||||
|
||||
before_validation :check_name, :check_email, :check_login, :check_mail_delivery_failed, :ensure_uniq_email, :ensure_password, :ensure_roles, :ensure_identifier
|
||||
before_validation :check_name, :check_email, :check_login, :ensure_uniq_email, :ensure_password, :ensure_roles, :ensure_identifier
|
||||
before_validation :check_mail_delivery_failed, on: :update
|
||||
before_create :check_preferences_default, :validate_preferences, :validate_ooo, :domain_based_assignment, :set_locale
|
||||
before_update :check_preferences_default, :validate_preferences, :validate_ooo, :reset_login_failed, :validate_agent_limit_by_attributes, :last_admin_check_by_attribute
|
||||
after_create :avatar_for_email_check
|
||||
@@ -355,26 +356,30 @@ returns
|
||||
|
||||
def self.create_from_hash!(hash)
|
||||
|
||||
role_ids = Role.signup_role_ids
|
||||
url = ''
|
||||
hash['info']['urls']&.each_value do |local_url|
|
||||
next if local_url.blank?
|
||||
url = local_url
|
||||
end
|
||||
begin
|
||||
create!(
|
||||
data = {
|
||||
login: hash['info']['nickname'] || hash['uid'],
|
||||
firstname: hash['info']['name'],
|
||||
firstname: hash['info']['name'] || hash['info']['display_name'],
|
||||
email: hash['info']['email'],
|
||||
image_source: hash['info']['image'],
|
||||
web: url,
|
||||
address: hash['info']['location'],
|
||||
note: hash['info']['description'],
|
||||
source: hash['provider'],
|
||||
role_ids: role_ids,
|
||||
role_ids: Role.signup_role_ids,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
}
|
||||
if hash['info']['first_name'].present? && hash['info']['last_name'].present?
|
||||
data[:firstname] = hash['info']['first_name']
|
||||
data[:lastname] = hash['info']['last_name']
|
||||
end
|
||||
create!(data)
|
||||
rescue => e
|
||||
logger.error e
|
||||
raise Exceptions::UnprocessableEntity, e.message
|
||||
@@ -1153,15 +1158,15 @@ raise 'Minimum one user need to have admin permissions'
|
||||
end
|
||||
|
||||
def destroy_longer_required_objects
|
||||
Authorization.where(user_id: id).destroy_all
|
||||
Avatar.remove('User', id)
|
||||
Cti::CallerId.where(user_id: id).destroy_all
|
||||
Taskbar.where(user_id: id).destroy_all
|
||||
Karma::ActivityLog.where(user_id: id).destroy_all
|
||||
Karma::User.where(user_id: id).destroy_all
|
||||
OnlineNotification.where(user_id: id).destroy_all
|
||||
RecentView.where(created_by_id: id).destroy_all
|
||||
UserDevice.remove(id)
|
||||
::Authorization.where(user_id: id).destroy_all
|
||||
::Avatar.remove('User', id)
|
||||
::Cti::CallerId.where(user_id: id).destroy_all
|
||||
::Taskbar.where(user_id: id).destroy_all
|
||||
::Karma::ActivityLog.where(user_id: id).destroy_all
|
||||
::Karma::User.where(user_id: id).destroy_all
|
||||
::OnlineNotification.where(user_id: id).destroy_all
|
||||
::RecentView.where(created_by_id: id).destroy_all
|
||||
::UserDevice.remove(id)
|
||||
true
|
||||
end
|
||||
|
||||
|
||||
11
app/views/mailer/password_change/es.html.erb
Normal file
11
app/views/mailer/password_change/es.html.erb
Normal file
@@ -0,0 +1,11 @@
|
||||
La contraseña para #{config.product_name} ha sido cambiada
|
||||
|
||||
<div>Buenos días #{user.firstname},</div>
|
||||
<br>
|
||||
<div>la contraseña para acceder a #{config.product_name} cuenta #{user.login}</b> ha cambiado recientemente. </div>
|
||||
<br>
|
||||
<div>¿Esta actividad es desconocida para vosotros? En este caso, ponerse en contacto con su administrador de sistema.</div>
|
||||
<br>
|
||||
<div>Siempre atentamente </div>
|
||||
<br>
|
||||
<div>El equipo #{config.product_name} </div>
|
||||
11
app/views/mailer/password_change/it.html.erb
Normal file
11
app/views/mailer/password_change/it.html.erb
Normal file
@@ -0,0 +1,11 @@
|
||||
La password per #{config.product_name} è stata modificata
|
||||
|
||||
<div>Buongiorno #{user.firstname},</div>
|
||||
<br>
|
||||
<div>la password per l'accesso a #{config.product_name} account #{user.login}</b> è stata modificata di recente. </div>
|
||||
<br>
|
||||
<div>Questa attività vi è sconosciuta? In questo caso, contattare l'amministratore di sistema.</div>
|
||||
<br>
|
||||
<div>Sempre cordiali saluti </div>
|
||||
<br>
|
||||
<div>Il team #{config.product_name} </div>
|
||||
18
app/views/mailer/password_reset/es.html.erb
Normal file
18
app/views/mailer/password_reset/es.html.erb
Normal file
@@ -0,0 +1,18 @@
|
||||
Restablecer contraseña para #{config.product_name}
|
||||
|
||||
<div>Buenos días #{user.firstname},</div>
|
||||
<br>
|
||||
<div>hemo recibido una solicitud de restablecimiento de contraseña para #{config.product_name} cuenta <b>#{user.login}</b>.</div>
|
||||
<br>
|
||||
<div>Si desea restablecer su contraseña, clicar sobre el link abajo (o copiar la URL en el browser
|
||||
insertar):</div>
|
||||
<br>
|
||||
<div><a href="#{config.http_type}://#{config.fqdn}/#password_reset_verify/#{token.name}">#{config.http_type}://#{config.fqdn}/#password_reset_verify/#{token.name}</a></div>
|
||||
<br>
|
||||
<div>Este link abre a una página donde puede cambiar su contraseña.</div>
|
||||
<br>
|
||||
<div>Si no desea restablecer su contraseña puede ignorar este mensaje. La contraseña no se cambia.</div>
|
||||
<br>
|
||||
<div>Siempre atentamente </div>
|
||||
<br>
|
||||
<div>El equipo #{config.product_name} </div>
|
||||
18
app/views/mailer/password_reset/it.html.erb
Normal file
18
app/views/mailer/password_reset/it.html.erb
Normal file
@@ -0,0 +1,18 @@
|
||||
Reimpostare password per #{config.product_name}
|
||||
|
||||
<div>Buongiorno #{user.firstname},</div>
|
||||
<br>
|
||||
<div>abbiamo ricevuto una richiesta di reimpostazione password per #{config.product_name} account <b>#{user.login}</b>.</div>
|
||||
<br>
|
||||
<div>Se desidera reimpostare la password, fare un clic sul collegamento sotto (o copiare l'URL nel browser
|
||||
inserire):</div>
|
||||
<br>
|
||||
<div><a href="#{config.http_type}://#{config.fqdn}/#password_reset_verify/#{token.name}">#{config.http_type}://#{config.fqdn}/#password_reset_verify/#{token.name}</a></div>
|
||||
<br>
|
||||
<div>Questo link porta a una pagina nella quale è possibile modificare la password.</div>
|
||||
<br>
|
||||
<div>Se non si desidera reimpostare la password, ignorare questo messaggio. La password rimane invariata.</div>
|
||||
<br>
|
||||
<div>Sempre cordiali saluti</div>
|
||||
<br>
|
||||
<div>Il team #{config.product_name} </div>
|
||||
11
app/views/mailer/signup/es.html.erb
Normal file
11
app/views/mailer/signup/es.html.erb
Normal file
@@ -0,0 +1,11 @@
|
||||
Confirma #{config.product_name} cuenta, #{user.firstname} #{user.lastname}
|
||||
|
||||
<div>Buenos días #{user.firstname},</div>
|
||||
<br>
|
||||
<div>confirma la dirección de correo electrónico para completar la registración para #{config.product_name}. Cliccar sobre el link abajo.</div>
|
||||
<br>
|
||||
<div><a href="#{config.http_type}://#{config.fqdn}/#email_verify/#{token.name}">#{config.http_type}://#{config.fqdn}/#email_verify/#{token.name}</a></div>
|
||||
<br>
|
||||
<div>Siempre atentamente </div>
|
||||
<br>
|
||||
<div>El equipo #{config.product_name} </div>
|
||||
11
app/views/mailer/signup/it.html.erb
Normal file
11
app/views/mailer/signup/it.html.erb
Normal file
@@ -0,0 +1,11 @@
|
||||
Conferma #{config.product_name} account, #{user.firstname} #{user.lastname}
|
||||
|
||||
<div>Buongiorno #{user.firstname},</div>
|
||||
<br>
|
||||
<div>confermare l'indirizzo email per completare la registrazione su #{config.product_name}. Cliccare sul link riportato sotto.</div>
|
||||
<br>
|
||||
<div><a href="#{config.http_type}://#{config.fqdn}/#email_verify/#{token.name}">#{config.http_type}://#{config.fqdn}/#email_verify/#{token.name}</a></div>
|
||||
<br>
|
||||
<div>Sempre cordiali saluti</div>
|
||||
<br>
|
||||
<div>Il team #{config.product_name} </div>
|
||||
9
app/views/mailer/test_ticket/es.html.erb
Normal file
9
app/views/mailer/test_ticket/es.html.erb
Normal file
@@ -0,0 +1,9 @@
|
||||
Boleto de prueba!
|
||||
|
||||
<div>Buenos días #{agent.firstname},</div>
|
||||
<br>
|
||||
<div>Este es un <b>ticket de prueba </b>. Soy un cliente y necesito ayuda! :)</div>
|
||||
<br>
|
||||
<div>#{customer.fullname}</div>
|
||||
<br>
|
||||
<div>El Proyecto Zammad </div>
|
||||
9
app/views/mailer/test_ticket/it.html.erb
Normal file
9
app/views/mailer/test_ticket/it.html.erb
Normal file
@@ -0,0 +1,9 @@
|
||||
Biglietto di prova!
|
||||
|
||||
<div>Cear #{agent.firstname},</div>,</div>.
|
||||
<br>
|
||||
<div>Questo è un <b>test ticket</b>. Sono un cliente e ho bisogno di aiuto! :)</div>
|
||||
<br>
|
||||
<div>#{customer.fullname}</div></div>
|
||||
<br>
|
||||
<div>Il Progetto Zammad</div>
|
||||
24
app/views/mailer/ticket_create/es.html.erb
Normal file
24
app/views/mailer/ticket_create/es.html.erb
Normal file
@@ -0,0 +1,24 @@
|
||||
Nuevo Ticket (#{ticket.title})
|
||||
|
||||
<div>Buenos días #{recipient.firstname},</div>
|
||||
<br>
|
||||
<div>Se ha creado un nuevo ticket (#{ticket.title}) con "<b>#{current_user.longname}</b>".</div>
|
||||
<br>
|
||||
<div>
|
||||
#{t('Group')}: #{ticket.group.name}<br>
|
||||
#{t('Owner')}: #{ticket.owner.fullname}<br>
|
||||
#{t('State')}: #{t(ticket.state.name)}<br>
|
||||
</div>
|
||||
<br>
|
||||
<% if @objects[:article] %>
|
||||
<div>
|
||||
#{t('Information')}:
|
||||
<blockquote type="cite">
|
||||
#{article.body_as_html}
|
||||
</blockquote>
|
||||
</div>
|
||||
<% end %>
|
||||
<br>
|
||||
<div>
|
||||
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
|
||||
</div>
|
||||
24
app/views/mailer/ticket_create/it.html.erb
Normal file
24
app/views/mailer/ticket_create/it.html.erb
Normal file
@@ -0,0 +1,24 @@
|
||||
Nuovo Ticket (#{ticket.title})
|
||||
|
||||
<div>Buongiorno #{recipient.firstname},</div>
|
||||
<br>
|
||||
<div>Un nuovo ticket (#{ticket.title}) è stato creato da "<b>##{current_user.longname}</b>".</div>.
|
||||
<br>
|
||||
<div>
|
||||
#{t('Group')}: #{ticket.group.name}<br>
|
||||
#{t('Owner')}: #{ticket.owner.fullname}<br>
|
||||
#{t('State')}: #{t(ticket.state.name)}<br>
|
||||
</div>
|
||||
<br>
|
||||
<% if @objects[:article] %>
|
||||
<div>
|
||||
#{t('Information')}:
|
||||
<blockquote type="cite">
|
||||
#{article.body_as_html}
|
||||
</blockquote>
|
||||
</div>
|
||||
<% end %>
|
||||
<br>
|
||||
<div>
|
||||
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
|
||||
</div>
|
||||
18
app/views/mailer/ticket_escalation/es.html.erb
Normal file
18
app/views/mailer/ticket_escalation/es.html.erb
Normal file
@@ -0,0 +1,18 @@
|
||||
El ticket está escalado (#{ticket.title})
|
||||
|
||||
<div>Buenos días #{recipient.firstname},</div>
|
||||
<br>
|
||||
<div>Un ticket (#{ticket.title}) de "<b>#{ticket.customer.longname}</b>" se incrementa desde "#{ticket.escalation_at}"!</div>
|
||||
<br>
|
||||
<% if @objects[:article] %>
|
||||
<div>
|
||||
#{t('Information')}:
|
||||
<blockquote type="cite">
|
||||
#{article.body_as_html}
|
||||
</blockquote>
|
||||
</div>
|
||||
<% end %>
|
||||
<br>
|
||||
<div>
|
||||
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
|
||||
</div>
|
||||
18
app/views/mailer/ticket_escalation/it.html.erb
Normal file
18
app/views/mailer/ticket_escalation/it.html.erb
Normal file
@@ -0,0 +1,18 @@
|
||||
Il ticket si è intensificato (#{ticket.title})
|
||||
|
||||
<div>Buongiorno #{recipient.firstname},</div>
|
||||
<br>
|
||||
<div>Un ticket (#{ticket.title}) da "<b>#{ticket.customer.longname}</b>" si è intensificato a "#{ticket.escalation_at}"!</div>.
|
||||
<br>
|
||||
<% if @objects[:article] %>
|
||||
<div>
|
||||
#{t('Information')}:
|
||||
<blockquote type="cite">
|
||||
#{article.body_as_html}
|
||||
</blockquote>
|
||||
</div>
|
||||
<% end %>
|
||||
<br>
|
||||
<div>
|
||||
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
|
||||
</div>
|
||||
18
app/views/mailer/ticket_escalation_warning/es.html.erb
Normal file
18
app/views/mailer/ticket_escalation_warning/es.html.erb
Normal file
@@ -0,0 +1,18 @@
|
||||
El ticket escalará (#{ticket.title})
|
||||
|
||||
<div>Buenos días #{recipient.firstname},</div>
|
||||
<br>
|
||||
<div>Un ticket (#{ticket.title}) de "<b>#{ticket.customer.longname}</b>" escalará a "#{ticket.escalation_at}"!</div>
|
||||
<br>
|
||||
<% if @objects[:article] %>
|
||||
<div>
|
||||
#{t('Information')}:
|
||||
<blockquote type="cite">
|
||||
#{article.body_as_html}
|
||||
</blockquote>
|
||||
</div>
|
||||
<% end %>
|
||||
<br>
|
||||
<div>
|
||||
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
|
||||
</div>
|
||||
18
app/views/mailer/ticket_escalation_warning/it.html.erb
Normal file
18
app/views/mailer/ticket_escalation_warning/it.html.erb
Normal file
@@ -0,0 +1,18 @@
|
||||
Il ticket intensificarsi (#{ticket.title})
|
||||
|
||||
<div>Buongiorno #{recipient.firstname},</div>
|
||||
<br>
|
||||
<div>Un ticket (#{ticket.title}) da "<b>#{ticket.customer.longname}</b>" intensificarsi a "#{ticket.escalation_at}"!</div>!
|
||||
<br>
|
||||
<% if @objects[:article] %>
|
||||
<div>
|
||||
#{t('Information')}:
|
||||
<blockquote type="cite">
|
||||
#{article.body_as_html}
|
||||
</blockquote>
|
||||
</div>
|
||||
<% end %>
|
||||
<br>
|
||||
<div>
|
||||
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
|
||||
</div>
|
||||
18
app/views/mailer/ticket_reminder_reached/es.html.erb
Normal file
18
app/views/mailer/ticket_reminder_reached/es.html.erb
Normal file
@@ -0,0 +1,18 @@
|
||||
Recordatorio alcanzado (#{ticket.title})
|
||||
|
||||
<div>Buenos días #{recipient.firstname},</div>
|
||||
<br>
|
||||
<div>Un ticket necesita atención, recordatorio alcanzado para (#{ticket.title}) con el cliente "<b>#{ticket.customer.longname}</b>".</div>
|
||||
<br>
|
||||
<% if @objects[:article] %>
|
||||
<div>
|
||||
#{t('Information')}:
|
||||
<blockquote type="cite">
|
||||
#{article.body_as_html}
|
||||
</blockquote>
|
||||
</div>
|
||||
<% end %>
|
||||
<br>
|
||||
<div>
|
||||
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
|
||||
</div>
|
||||
18
app/views/mailer/ticket_reminder_reached/it.html.erb
Normal file
18
app/views/mailer/ticket_reminder_reached/it.html.erb
Normal file
@@ -0,0 +1,18 @@
|
||||
Promemoria raggiunto (#{ticket.title})
|
||||
|
||||
<div>Buongiorno #{recipient.firstname},</div>
|
||||
<br>
|
||||
<div>Un biglietto ha bisogno di attenzione, promemoria raggiunto per (#{ticket.title}) con il cliente "<b>#{ticket.customer.longname}</b>".</div>
|
||||
<br>
|
||||
<% if @objects[:article] %>
|
||||
<div>
|
||||
#{t('Information')}:
|
||||
<blockquote type="cite">
|
||||
#{article.body_as_html}
|
||||
</blockquote>
|
||||
</div>
|
||||
<% end %>
|
||||
<br>
|
||||
<div>
|
||||
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
|
||||
</div>
|
||||
29
app/views/mailer/ticket_update/es.html.erb
Normal file
29
app/views/mailer/ticket_update/es.html.erb
Normal file
@@ -0,0 +1,29 @@
|
||||
Ticket actualizado (#{ticket.title})
|
||||
|
||||
<div>Buenos días #{recipient.firstname},</div>
|
||||
<br>
|
||||
<div>
|
||||
Ticket (#{ticket.title}) ha sido actualizado por "<b>#{current_user.longname}</b>".
|
||||
</div>
|
||||
<br>
|
||||
<% if @objects[:changes].present? %>
|
||||
<div>
|
||||
#{t('Changes')}:<br>
|
||||
<% @objects[:changes].each do |key, value| %>
|
||||
<%= t key %>: <%= h value[0] %> -> <%= h value[1] %><br>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<br>
|
||||
<% if @objects[:article] %>
|
||||
<div>
|
||||
#{t('Information')}:
|
||||
<blockquote type="cite">
|
||||
#{article.body_as_html}
|
||||
</blockquote>
|
||||
</div>
|
||||
<% end %>
|
||||
<br>
|
||||
<div>
|
||||
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
|
||||
</div>
|
||||
29
app/views/mailer/ticket_update/it.html.erb
Normal file
29
app/views/mailer/ticket_update/it.html.erb
Normal file
@@ -0,0 +1,29 @@
|
||||
Biglietto aggiornato (#{ticket.title})
|
||||
|
||||
<div>Buongiorno #{recipient.firstname},</div>
|
||||
<br>
|
||||
<div>
|
||||
Ticket (#{ticket.title}) è stato aggiornato da "<b>#{current_user.longname}</b>".
|
||||
</div>
|
||||
<br>
|
||||
<% if @objects[:changes].present? %>
|
||||
<div>
|
||||
#{t('Changes')}:<br>
|
||||
<% @objects[:changes].each do |key, value| %>
|
||||
<%= t key %>: <%= h value[0] %> -> <%= h value[1] %><br>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<br>
|
||||
<% if @objects[:article] %>
|
||||
<div>
|
||||
#{t('Information')}:
|
||||
<blockquote type="cite">
|
||||
#{article.body_as_html}
|
||||
</blockquote>
|
||||
</div>
|
||||
<% end %>
|
||||
<br>
|
||||
<div>
|
||||
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
|
||||
</div>
|
||||
22
app/views/mailer/user_device_new/es.html.erb
Normal file
22
app/views/mailer/user_device_new/es.html.erb
Normal file
@@ -0,0 +1,22 @@
|
||||
#{config.product_name}-login detectado por un nuevo dispositivo
|
||||
|
||||
<div>Buenos días #{user.firstname},</div>
|
||||
<br>
|
||||
<div>parece haber accedido a <b>un nuevo dispositivo</b> para "#{user_device.created_at}" a:</div>
|
||||
<br>
|
||||
<div>
|
||||
Su dispositivo: #{user_device.name}<br>
|
||||
Su posición (relativa): #{user_device.location}<br>
|
||||
Su dirección IP: #{user_device.ip}<br>
|
||||
</div>
|
||||
<br>
|
||||
<div>El dispositivo se ha agregado a la lista de dispositivos conocidos, esta lista se puede ver aquí:</div>
|
||||
<br>
|
||||
<div>#{config.http_type}://#{config.fqdn}/#profile/devices</div>
|
||||
<br>
|
||||
<div>En caso contrario, eliminar el dispositivo de la lista, cambiar la contraseña de la cuenta y contactar su administrador. Alguien podría
|
||||
tener acceso no autorizado a su cuenta.</div>
|
||||
<br>
|
||||
<div>Siempre atentamente </div>
|
||||
<br>
|
||||
<div>El equipo #{config.product_name} </div>
|
||||
21
app/views/mailer/user_device_new/it.html.erb
Normal file
21
app/views/mailer/user_device_new/it.html.erb
Normal file
@@ -0,0 +1,21 @@
|
||||
#{config.product_name}-login rilevato da un nuovo dispositivo
|
||||
|
||||
<div>Buongiorno #{user.firstname},</div>
|
||||
<br>
|
||||
<div>sembra che abbia eseguito l'accesso a <b>un nuovo dispositivo</b> per "#{user_device.created_at}" a:</div>
|
||||
<br>
|
||||
<div>
|
||||
Il suo dispositivo: #{user_device.name}<br>
|
||||
La sua posizione (relativa): #{user_device.location}<br>
|
||||
Il suo indirizzo IP: #{user_device.ip}<br>
|
||||
</div>
|
||||
<br>
|
||||
<div>Il dispositivo è stato aggiunto all'elenco dei dispositivi conosciuti, questo elenco può essere visualizzato qui:</div>
|
||||
<br>
|
||||
<div>#{config.http_type}://#{config.fqdn}/#profile/devices</div>
|
||||
<br>
|
||||
<div>In caso contrario, rimuovere il dispositivo dall'elenco, modificare la password dell'account e contattare l'amministratore. Qualcuno potrebbe avere accesso non autorizzato al suo account.</div>
|
||||
<br>
|
||||
<div>Sempre cordiali saluti</div>
|
||||
<br>
|
||||
<div>Il team #{config.product_name} </div>
|
||||
21
app/views/mailer/user_device_new_location/es.html.erb
Normal file
21
app/views/mailer/user_device_new_location/es.html.erb
Normal file
@@ -0,0 +1,21 @@
|
||||
#{config.product_name}-acceso desde otro país
|
||||
|
||||
<div>Buenos días #{user.firstname},</div>
|
||||
<br>
|
||||
<div>parece haber accedido a las "#{user_device.created_at}" desde un <b>dispositivo conocido desde un otro país</b>:</div>
|
||||
<br>
|
||||
<div>
|
||||
Su dispositivo: #{user_device.name}<br>
|
||||
Su posición (relativa): #{user_device.location}<br>
|
||||
Su dirección IP: #{user_device.ip}<br>
|
||||
</div>
|
||||
<br>
|
||||
<div>El nuevo país se ha agregado a la lista de dispositivos conocidos, esta lista se puede ver aquí:</div>
|
||||
<br>
|
||||
<div>#{config.http_type}://#{config.fqdn}/#profile/devices</div>
|
||||
<br>
|
||||
<div>En caso contrario, eliminar la nueva ubicación de la lista, cambiar la contraseña de la cuenta y contactar su administrador. Alguien puede tener acceso no autorizado a su cuenta.</div>
|
||||
<br>
|
||||
<div>Siempre atentamente </div>
|
||||
<br>
|
||||
<div>El equipo #{config.product_name} </div>
|
||||
21
app/views/mailer/user_device_new_location/it.html.erb
Normal file
21
app/views/mailer/user_device_new_location/it.html.erb
Normal file
@@ -0,0 +1,21 @@
|
||||
#{config.product_name}-accesso da un altro paese
|
||||
|
||||
<div>Buongiorno #{user.firstname},</div>
|
||||
<br>
|
||||
<div>sembra che abbia eseguito l'accesso alle "#{user_device.created_at}" da un <b>dispositivo conosciuto da un altro paese</b>:</div>
|
||||
<br>
|
||||
<div>
|
||||
Il suo dispositivo: #{user_device.name}<br>
|
||||
La sua posizione (relativa): #{user_device.location}<br>
|
||||
Il suo indirizzo IP: #{user_device.ip}<br>
|
||||
</div>
|
||||
<br>
|
||||
<div>Il nuovo paese è stato aggiunto all'elenco dei dispositivi conosciuti, questo elenco può essere visualizzato qui:</div>
|
||||
<br>
|
||||
<div>#{config.http_type}://#{config.fqdn}/#profile/devices</div>
|
||||
<br>
|
||||
<div>In caso contrario, rimuovere la nuova posizione dall'elenco, modificare la password dell'account e contattare l'amministratore. Qualcuno potrebbe avere accesso non autorizzato al suo account.</div>
|
||||
<br>
|
||||
<div>Sempre cordiali saluti</div>
|
||||
<br>
|
||||
<div>Il team #{config.product_name} </div>
|
||||
15
app/views/mailer/user_invite/es.html.erb
Normal file
15
app/views/mailer/user_invite/es.html.erb
Normal file
@@ -0,0 +1,15 @@
|
||||
Invitación a #{config.product_name} a través de #{config.fqdn}
|
||||
|
||||
<div>Buenos días #{user.firstname},</div>
|
||||
<br>
|
||||
<div>A mi (#{current_user.firstname} #{current_user.lastname}) me gustaría invitar a #{config.product_name} - nuestra plataforma Usersupport/Ticket.</div>
|
||||
<br>
|
||||
<div>Para iniciar sesión <a href="#{config.http_type}://#{config.fqdn}/#password_reset_verify/#{token.name}">configure aquí</a> la contraseña.</div>
|
||||
<br>
|
||||
<div>Gracias,</div>
|
||||
<br>
|
||||
<div>#{current_user.firstname} #{current_user.lastname}</div>
|
||||
<br>
|
||||
<div>Siempre atentamente </div>
|
||||
<br>
|
||||
<div>El equipo #{config.product_name} </div>
|
||||
15
app/views/mailer/user_invite/it.html.erb
Normal file
15
app/views/mailer/user_invite/it.html.erb
Normal file
@@ -0,0 +1,15 @@
|
||||
Invitato a #{config.product_name} tramite #{config.fqdn}
|
||||
|
||||
<div>Buongiorno #{user.firstname},</div>
|
||||
<br>
|
||||
<div>Io (#{current_user.firstname} #{current_user.lastname}) vorrei invitarvi a #{config.product_name} - la nostra piattaforma Usersupport/Ticket.</div>
|
||||
<br>
|
||||
<div>Per accedere <a href="#{config.http_type}://#{config.fqdn}/#password_reset_verify/#{token.name}">impostare qui</a> la password.</div>
|
||||
<br>
|
||||
<div>Grazie,</div>
|
||||
<br>
|
||||
<div>#{current_user.firstname} #{current_user.lastname}</div>
|
||||
<br>
|
||||
<div>Sempre cordiali saluti</div>
|
||||
<br>
|
||||
<div>Il team #{config.product_name} </div>
|
||||
@@ -1,5 +1,6 @@
|
||||
class AddConfidentialToDoorkeeperApplication < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
return if ActiveRecord::Base.connection.column_exists?(:oauth_applications, :confidential)
|
||||
add_column(
|
||||
:oauth_applications,
|
||||
:confidential,
|
||||
|
||||
@@ -67,10 +67,19 @@ class CtiGenericApi < ActiveRecord::Migration[5.1]
|
||||
frontend: false
|
||||
)
|
||||
|
||||
add_column :cti_logs, :queue, :string, limit: 250, null: true
|
||||
add_column :cti_logs, :initialized_at, :string, limit: 250, null: true
|
||||
add_column :cti_logs, :duration_waiting_time, :integer, null: true
|
||||
add_column :cti_logs, :duration_talking_time, :integer, null: true
|
||||
add_column :cti_logs, :queue, :string, limit: 250, null: true if !column_exists?(:cti_logs, :queue)
|
||||
add_column :cti_logs, :initialized_at, :string, limit: 250, null: true if !column_exists?(:cti_logs, :initialized_at)
|
||||
add_column :cti_logs, :duration_waiting_time, :integer, null: true if !column_exists?(:cti_logs, :duration_waiting_time)
|
||||
add_column :cti_logs, :duration_talking_time, :integer, null: true if !column_exists?(:cti_logs, :duration_talking_time)
|
||||
|
||||
# fixes issue #2183 - Mysql2::Error: Invalid default value for 'start_at'
|
||||
if ActiveRecord::Base.connection_config[:adapter] == 'mysql2'
|
||||
# disable the MySQL strict_mode for the current connection
|
||||
execute("SET sql_mode = ''")
|
||||
|
||||
change_column_default :cti_logs, :start, '0000-00-00 00:00:00'
|
||||
change_column_default :cti_logs, :end, '0000-00-00 00:00:00'
|
||||
end
|
||||
|
||||
rename_column :cti_logs, :start, :start_at
|
||||
rename_column :cti_logs, :end, :end_at
|
||||
|
||||
15
db/migrate/20180911064647_issue_2140_reset_ldap_config.rb
Normal file
15
db/migrate/20180911064647_issue_2140_reset_ldap_config.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
class Issue2140ResetLdapConfig < ActiveRecord::Migration[5.1]
|
||||
def up
|
||||
# return if it's a new setup
|
||||
return if !Setting.find_by(name: 'system_init_done')
|
||||
|
||||
ldap_config = Setting.get('ldap_config')
|
||||
|
||||
# finish if LDAP config isn't broken
|
||||
ldap_config.to_json
|
||||
rescue Encoding::UndefinedConversionError
|
||||
ldap_config[:wizardData].delete(:backend_user_attributes)
|
||||
|
||||
Setting.set('ldap_config', ldap_config)
|
||||
end
|
||||
end
|
||||
@@ -476,11 +476,11 @@ class String
|
||||
def utf8_encode!(**options)
|
||||
return force_encoding('utf-8') if dup.force_encoding('utf-8').valid_encoding?
|
||||
|
||||
viable_encodings(try_first: options[:from]).each do |e|
|
||||
viable_encodings(try_first: options[:from]).each do |enc|
|
||||
begin
|
||||
return encode!('utf-8', e)
|
||||
rescue Encoding::UndefinedConversionError
|
||||
nil
|
||||
return encode!('utf-8', enc)
|
||||
rescue EncodingError => e
|
||||
Rails.logger.error { e.inspect }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -387,7 +387,7 @@ cleanup html string:
|
||||
end
|
||||
|
||||
uri.to_s
|
||||
rescue URI::Error
|
||||
rescue
|
||||
url
|
||||
end
|
||||
|
||||
|
||||
@@ -42,21 +42,14 @@ module Import
|
||||
end
|
||||
|
||||
def display_path(folder)
|
||||
display_name = folder.display_name
|
||||
return display_name if !folder.parent_folder_id
|
||||
|
||||
parent_folder = find(folder.parent_folder_id)
|
||||
return display_name if !parent_folder
|
||||
|
||||
display_name = folder.display_name.utf8_encode(fallback: :read_as_sanitized_binary)
|
||||
parent_folder = id_folder_map[folder.parent_folder_id]
|
||||
return display_name if !parent_folder
|
||||
|
||||
# recursive
|
||||
parent_folder_path = display_path(parent_folder)
|
||||
return display_name if parent_folder.blank?
|
||||
|
||||
"#{parent_folder_path} -> #{display_name}"
|
||||
"#{display_path(parent_folder)} -> #{display_name}"
|
||||
rescue Viewpoint::EWS::EwsError
|
||||
folder.display_name
|
||||
display_name
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -120,38 +120,34 @@ class Ldap
|
||||
# #=> {:dn=>"dn (e. g. CN=Administrator,CN=Users,DC=domain,DC=tld)", ...}
|
||||
#
|
||||
# @return [Hash{Symbol=>String}] The available User attributes as key and the name and an example as value.
|
||||
def attributes(filter: nil, base_dn: nil)
|
||||
|
||||
filter ||= filter()
|
||||
|
||||
attributes = {}
|
||||
known_attributes = BLACKLISTED.dup
|
||||
lookup_counter = 1
|
||||
|
||||
@ldap.search(filter, base: base_dn) do |entry|
|
||||
new_attributes = entry.attribute_names - known_attributes
|
||||
|
||||
if new_attributes.blank?
|
||||
lookup_counter += 1
|
||||
# check max 50 entries with
|
||||
# the same attributes in a row
|
||||
break if lookup_counter == 50
|
||||
next
|
||||
end
|
||||
|
||||
new_attributes.each do |attribute|
|
||||
value = entry[attribute]
|
||||
next if value.blank?
|
||||
next if value[0].blank?
|
||||
|
||||
example_value = value[0].force_encoding('UTF-8').utf8_encode(fallback: :read_as_sanitized_binary)
|
||||
attributes[attribute] = "#{attribute} (e. g. #{example_value})"
|
||||
end
|
||||
|
||||
known_attributes.concat(new_attributes)
|
||||
def attributes(custom_filter: nil, base_dn: nil)
|
||||
@attributes ||= begin
|
||||
attributes = {}.with_indifferent_access
|
||||
lookup_counter = 0
|
||||
|
||||
# collect sample attributes
|
||||
@ldap.search(custom_filter || filter, base: base_dn) do |entry|
|
||||
pre_merge_count = attributes.count
|
||||
|
||||
attributes.reverse_merge!(entry.to_h
|
||||
.except(*BLACKLISTED)
|
||||
.transform_values(&:first)
|
||||
.compact)
|
||||
|
||||
# check max 50 entries with the same attributes in a row
|
||||
lookup_counter = (pre_merge_count < attributes.count ? 0 : lookup_counter.next)
|
||||
break if lookup_counter >= 50
|
||||
end
|
||||
|
||||
# format sample values for presentation
|
||||
attributes.each do |name, value|
|
||||
attributes[name] = if value.encoding == Encoding.find('ascii-8bit')
|
||||
"#{name} (binary data)"
|
||||
else
|
||||
"#{name} (e.g., #{value.utf8_encode})"
|
||||
end
|
||||
end
|
||||
end
|
||||
attributes
|
||||
end
|
||||
|
||||
# The active filter of the instance. If none give on initialization an automatic lookup is performed.
|
||||
|
||||
@@ -165,8 +165,10 @@ returns
|
||||
return result if params[:sheet].present?
|
||||
assets = {}
|
||||
result[:ticket_ids].each do |ticket_id|
|
||||
ticket_full = Ticket.find(ticket_id)
|
||||
assets = ticket_full.assets(assets)
|
||||
suppress(ActiveRecord::RecordNotFound) do
|
||||
ticket_full = Ticket.find(ticket_id)
|
||||
assets = ticket_full.assets(assets)
|
||||
end
|
||||
end
|
||||
result[:assets] = assets
|
||||
result
|
||||
|
||||
@@ -342,6 +342,7 @@ return search result
|
||||
'query_string' => {
|
||||
'query' => append_wildcard_to_simple_query(query),
|
||||
'default_operator' => 'AND',
|
||||
'analyze_wildcard' => true,
|
||||
}
|
||||
}
|
||||
data['query']['bool']['must'].push condition
|
||||
@@ -717,10 +718,10 @@ return true if backend is configured
|
||||
result
|
||||
end
|
||||
|
||||
# add * on simple query like "somephrase23" or "attribute: somephrase23"
|
||||
# add * on simple query like "somephrase23"
|
||||
def self.append_wildcard_to_simple_query(query)
|
||||
query.strip!
|
||||
query += '*' if query.match?(/^([[:alnum:]._]+|[[:alnum:]]+\:\s*[[:alnum:]._]+)$/)
|
||||
query += '*' if !query.match?(/:/)
|
||||
query
|
||||
end
|
||||
end
|
||||
|
||||
@@ -27,13 +27,15 @@ class Sequencer
|
||||
|
||||
# always returns true
|
||||
def log_associations_error
|
||||
return true if %i[failed deactivated].include?(action)
|
||||
return true if %i[skipped failed deactivated].include?(action)
|
||||
|
||||
logger.error { 'associations cannot be nil' } if associations.nil?
|
||||
true
|
||||
end
|
||||
|
||||
def register_changes
|
||||
return if !(action == :unchanged && changes.any?)
|
||||
|
||||
logger.debug { "Changed instance associations: #{changes.inspect}" }
|
||||
state.provide(:action, :updated)
|
||||
end
|
||||
|
||||
@@ -6,11 +6,14 @@ class Sequencer
|
||||
module Common
|
||||
module Model
|
||||
class Save < Sequencer::Unit::Base
|
||||
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::Skip::Action
|
||||
include ::Sequencer::Unit::Import::Common::Model::Mixin::HandleFailure
|
||||
|
||||
uses :instance, :dry_run
|
||||
uses :instance, :action, :dry_run
|
||||
provides :instance
|
||||
|
||||
skip_action :skipped, :failed, :unchanged
|
||||
|
||||
def process
|
||||
return if dry_run
|
||||
return if instance.blank?
|
||||
|
||||
@@ -10,7 +10,7 @@ class Sequencer
|
||||
|
||||
skip_any_action
|
||||
|
||||
uses :resource, :dn_roles, :ldap_config, :mapped
|
||||
uses :dn_roles, :ldap_config, :mapped, :instance
|
||||
provides :action
|
||||
|
||||
def process
|
||||
@@ -25,9 +25,7 @@ class Sequencer
|
||||
# if unassigned users should not get skipped
|
||||
return if ldap_config[:unassigned_users] != 'skip_sync'
|
||||
|
||||
instance = state.optional(:instance)
|
||||
|
||||
if instance.present?
|
||||
if instance&.active
|
||||
# deactivate instance if role assignment is lost
|
||||
instance.update!(active: false)
|
||||
state.provide(:action, :deactivated)
|
||||
|
||||
@@ -24,6 +24,9 @@ namespace :searchindex do
|
||||
number = info['version']['number'].to_s
|
||||
end
|
||||
|
||||
settings = {
|
||||
'index.mapping.total_fields.limit': 2000,
|
||||
}
|
||||
mapping = {}
|
||||
Models.searchable.each do |local_object|
|
||||
mapping.merge!(get_mapping_properties_object(local_object))
|
||||
@@ -33,7 +36,8 @@ namespace :searchindex do
|
||||
SearchIndexBackend.index(
|
||||
action: 'create',
|
||||
data: {
|
||||
mappings: mapping
|
||||
mappings: mapping,
|
||||
settings: settings,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -93,6 +93,8 @@ test('form checks', function() {
|
||||
priority1_id: '1',
|
||||
priority2_id: ['1', '2'],
|
||||
priority3_id: '2',
|
||||
priority4_id: '2',
|
||||
priority5_id: '1',
|
||||
working_hours: {
|
||||
mon: {
|
||||
active: true,
|
||||
@@ -146,9 +148,11 @@ test('form checks', function() {
|
||||
el: el,
|
||||
model: {
|
||||
configure_attributes: [
|
||||
{ name: 'priority1_id', display: 'Priroity1', tag: 'select', relation: 'TicketPriority', null: true, options: {} },
|
||||
{ name: 'priority2_id', display: 'Priroity2', tag: 'select', multiple: true, relation: 'TicketPriority', null: true, options: {} },
|
||||
{ name: 'priority3_id', display: 'Priroity3', tag: 'select', relation: 'TicketPriority', null: true },
|
||||
{ name: 'priority1_id', display: 'Priroity1 (with active selection)', tag: 'select', relation: 'TicketPriority', null: true, options: {} },
|
||||
{ name: 'priority2_id', display: 'Priroity2 (with active and inactive selection)', tag: 'select', multiple: true, relation: 'TicketPriority', null: true, options: {} },
|
||||
{ name: 'priority3_id', display: 'Priroity3 (with inactive selection)', tag: 'select', relation: 'TicketPriority', null: true, options: {} },
|
||||
{ name: 'priority4_id', display: 'Priroity4 (with inactive selection)', tag: 'select', multiple: true, relation: 'TicketPriority', null: true, options: {} },
|
||||
{ name: 'priority5_id', display: 'Priroity5 (with active selection)', tag: 'select', multiple: true, relation: 'TicketPriority', null: true, options: {} },
|
||||
{ name: 'escalation_times', display: 'Times', tag: 'sla_times', null: true },
|
||||
{ name: 'working_hours', display: 'Hours', tag: 'business_hours', null: true },
|
||||
]
|
||||
@@ -161,6 +165,8 @@ test('form checks', function() {
|
||||
priority1_id: '1',
|
||||
priority2_id: ['1', '2'],
|
||||
priority3_id: '2',
|
||||
priority4_id: '2',
|
||||
priority5_id: '1',
|
||||
first_response_time: '150',
|
||||
first_response_time_in_text: '02:30',
|
||||
solution_time: '',
|
||||
@@ -219,6 +225,36 @@ test('form checks', function() {
|
||||
equal(el.find('[name="priority1_id"] option').length, 3)
|
||||
equal(el.find('[name="priority2_id"] option').length, 4)
|
||||
equal(el.find('[name="priority3_id"] option').length, 4)
|
||||
equal(el.find('[name="priority4_id"] option').length, 4)
|
||||
equal(el.find('[name="priority5_id"] option').length, 3)
|
||||
|
||||
// check priority1_id selection order
|
||||
equal(el.find('[name="priority1_id"] option:nth-child(1)').text(), '1 low')
|
||||
equal(el.find('[name="priority1_id"] option:nth-child(2)').text(), '3 high')
|
||||
equal(el.find('[name="priority1_id"] option:nth-child(3)').text(), '4 very high')
|
||||
|
||||
// check priority2_id selection order
|
||||
equal(el.find('[name="priority2_id"] option:nth-child(1)').text(), '1 low')
|
||||
equal(el.find('[name="priority2_id"] option:nth-child(2)').text(), '2 normal')
|
||||
equal(el.find('[name="priority2_id"] option:nth-child(3)').text(), '3 high')
|
||||
equal(el.find('[name="priority2_id"] option:nth-child(4)').text(), '4 very high')
|
||||
|
||||
// check priority3_id selection order
|
||||
equal(el.find('[name="priority3_id"] option:nth-child(1)').text(), '1 low')
|
||||
equal(el.find('[name="priority3_id"] option:nth-child(2)').text(), '2 normal')
|
||||
equal(el.find('[name="priority3_id"] option:nth-child(3)').text(), '3 high')
|
||||
equal(el.find('[name="priority3_id"] option:nth-child(4)').text(), '4 very high')
|
||||
|
||||
// check priority4_id selection order
|
||||
equal(el.find('[name="priority4_id"] option:nth-child(1)').text(), '1 low')
|
||||
equal(el.find('[name="priority4_id"] option:nth-child(2)').text(), '2 normal')
|
||||
equal(el.find('[name="priority4_id"] option:nth-child(3)').text(), '3 high')
|
||||
equal(el.find('[name="priority4_id"] option:nth-child(4)').text(), '4 very high')
|
||||
|
||||
// check priority5_id selection order
|
||||
equal(el.find('[name="priority5_id"] option:nth-child(1)').text(), '1 low')
|
||||
equal(el.find('[name="priority5_id"] option:nth-child(2)').text(), '3 high')
|
||||
equal(el.find('[name="priority5_id"] option:nth-child(3)').text(), '4 very high')
|
||||
|
||||
// change sla times
|
||||
el.find('[name="first_response_time_in_text"]').val('0:30').trigger('blur')
|
||||
@@ -229,6 +265,8 @@ test('form checks', function() {
|
||||
priority1_id: '1',
|
||||
priority2_id: ['1', '2'],
|
||||
priority3_id: '2',
|
||||
priority4_id: '2',
|
||||
priority5_id: '1',
|
||||
working_hours: {
|
||||
mon: {
|
||||
active: true,
|
||||
|
||||
@@ -309,6 +309,10 @@ test( "datetime selector check", function() {
|
||||
}
|
||||
deepEqual(params, test_params, 'params check')
|
||||
|
||||
// Regression test for issue #2173 - Invalid date causes errors
|
||||
el.find('.js-datepicker').datepicker('setDate', '01/01/99999')
|
||||
el.find('.js-datepicker').datepicker('setDate', '01/01/1ABCDEFG')
|
||||
el.find('.js-datepicker').datepicker('setDate', '01/01/1äöüß')
|
||||
});
|
||||
|
||||
test( "date selector check", function() {
|
||||
|
||||
@@ -40,7 +40,6 @@ test( "taskbar basic tests", function() {
|
||||
deepEqual(task.state, 'abc')
|
||||
deepEqual(task.params, { "a": 12 })
|
||||
|
||||
|
||||
App.TaskManager.execute({
|
||||
key: 'TestKey2',
|
||||
controller: 'TestController1',
|
||||
@@ -258,12 +257,60 @@ test( "taskbar basic tests", function() {
|
||||
equal(App.TaskManager.nextTaskUrl(), false)
|
||||
equal(App.TaskManager.nextTaskUrl(), false)
|
||||
|
||||
// destroy task bar
|
||||
App.TaskManager.reset()
|
||||
// check max tabs
|
||||
var times = 5;
|
||||
App.TaskManager.tasksAutoCleanupDelayTime(200)
|
||||
App.TaskManager.tasksAutoCleanupTaskMax(3)
|
||||
|
||||
// check if any taskar exists
|
||||
equal($('#taskbars .content').length, 0, "check available active contents")
|
||||
for(var i=0; i < times; i++){
|
||||
App.TaskManager.execute({
|
||||
key: 'TestKeyLoop' + i,
|
||||
controller: 'TestController1',
|
||||
params: {
|
||||
message: "#" + i,
|
||||
},
|
||||
show: true,
|
||||
persistent: false,
|
||||
})
|
||||
}
|
||||
equal(App.TaskManager.all().length, 5)
|
||||
|
||||
})
|
||||
|
||||
App.Delay.set(function() {
|
||||
test( "taskbar check max tabs 2", function() {
|
||||
|
||||
equal(App.TaskManager.all().length, 3)
|
||||
|
||||
var times = 5;
|
||||
for(var i=0; i < times; i++){
|
||||
App.TaskManager.execute({
|
||||
key: 'TestKeyLoop2' + i,
|
||||
controller: 'TestController1',
|
||||
params: {
|
||||
message: "#" + i,
|
||||
changedState: true
|
||||
},
|
||||
show: true,
|
||||
persistent: false,
|
||||
})
|
||||
}
|
||||
equal(App.TaskManager.all().length, 8)
|
||||
|
||||
})
|
||||
}, 1000);
|
||||
|
||||
App.Delay.set(function() {
|
||||
test( "taskbar check max tabs 5", function() {
|
||||
|
||||
equal(App.TaskManager.all().length, 5)
|
||||
|
||||
// destroy task bar
|
||||
App.TaskManager.reset()
|
||||
|
||||
// check if any taskar exists
|
||||
equal($('#taskbars .content').length, 0, "check available active contents")
|
||||
})
|
||||
}, 2000);
|
||||
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@ if echo $CI_BUILD_REF_NAME | grep private; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if echo $CI_BUILD_REF_NAME | grep cherry-pick-; then
|
||||
echo 'sync no cherry-pick branches'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git checkout $CI_BUILD_REF_NAME
|
||||
if [ "$CI_BUILD_REF_NAME" != "$CI_BUILD_TAG" ]; then
|
||||
git pull --rebase origin $CI_BUILD_REF_NAME
|
||||
|
||||
35
spec/db/migrate/issue_2140_reset_ldap_config_spec.rb
Normal file
35
spec/db/migrate/issue_2140_reset_ldap_config_spec.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Issue2140ResetLdapConfig, type: :db_migration do
|
||||
before { Setting.set('ldap_config', config) }
|
||||
|
||||
context 'when LDAP config isn’t broken' do
|
||||
let(:config) do
|
||||
{ 'wizardData' =>
|
||||
{ 'backend_user_attributes' =>
|
||||
{ 'foo' => 'bar' },
|
||||
'user_attributes' =>
|
||||
{ 'baz' => 'qux' } } }.with_indifferent_access
|
||||
end
|
||||
|
||||
it 'makes no changes' do
|
||||
expect { migrate }.not_to change { Setting.get('ldap_config') }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when LDAP config is broken' do
|
||||
let(:config) do
|
||||
{ 'wizardData' =>
|
||||
{ 'backend_user_attributes' =>
|
||||
{ 'foo' => "\u0001\u0001\u0004€" },
|
||||
'user_attributes' =>
|
||||
{ 'baz' => 'qux' } } }.with_indifferent_access
|
||||
end
|
||||
|
||||
it 'removes the offending backend_user_attributes sub-hash' do
|
||||
expect { migrate }
|
||||
.to change { Setting.get('ldap_config') }
|
||||
.to(config.tap { |c| c[:wizardData].delete(:backend_user_attributes) })
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,9 +1,28 @@
|
||||
FactoryBot.define do
|
||||
factory :recent_view do
|
||||
recent_view_object_id { ObjectLookup.by_name('User') }
|
||||
o_id 1
|
||||
created_by_id 1
|
||||
created_at Time.zone.now
|
||||
updated_at Time.zone.now
|
||||
transient do
|
||||
type { :ticket }
|
||||
user_role { :agent }
|
||||
end
|
||||
|
||||
recent_view_object_id { ObjectLookup.by_name(type.to_s.camelcase) }
|
||||
|
||||
# select a random record of the given object class
|
||||
o_id do
|
||||
random_function = case ActiveRecord::Base.connection_config[:adapter]
|
||||
when 'mysql2'
|
||||
'RAND'
|
||||
when 'postgresql'
|
||||
'RANDOM'
|
||||
end
|
||||
|
||||
type.to_s.camelcase.constantize.order("#{random_function}()").first.id
|
||||
end
|
||||
|
||||
# assign to an existing user, if possible
|
||||
created_by_id do
|
||||
User.find { |u| u.role?(user_role.capitalize) }&.id ||
|
||||
create("#{user_role}_user").id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
7
spec/factories/ticket/time_accounting.rb
Normal file
7
spec/factories/ticket/time_accounting.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
FactoryBot.define do
|
||||
factory :ticket_time_accounting, class: Ticket::TimeAccounting do
|
||||
ticket
|
||||
time_unit { rand(100) }
|
||||
created_by_id 1
|
||||
end
|
||||
end
|
||||
76
spec/lib/import/exchange/folder_spec.rb
Normal file
76
spec/lib/import/exchange/folder_spec.rb
Normal file
@@ -0,0 +1,76 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Import::Exchange::Folder do
|
||||
# see https://github.com/zammad/zammad/issues/2152
|
||||
describe '#display_path (#2152)' do
|
||||
let(:subject) { described_class.new(ews_connection) }
|
||||
let(:ews_connection) { Viewpoint::EWSClient.new(endpoint, user, pass) }
|
||||
let(:endpoint) { 'https://exchange.example.com/EWS/Exchange.asmx' }
|
||||
let(:user) { 'user@example.com' }
|
||||
let(:pass) { 'password' }
|
||||
let(:grandchild_of_root) { ews_connection.get_folder_by_name('Inbox') }
|
||||
let(:child_of_root) { ews_connection.get_folder(grandchild_of_root.parent_folder_id) }
|
||||
|
||||
around do |example|
|
||||
cassette_name = example.description.gsub(/[^0-9A-Za-z.\-]+/, '_')
|
||||
VCR.use_cassette("lib/import/exchange/folder/#{cassette_name}") { example.run }
|
||||
end
|
||||
|
||||
context 'when server returns valid UTF-8' do
|
||||
context 'and target folder is in root directory' do
|
||||
it 'returns the display name of the folder' do
|
||||
expect(subject.display_path(child_of_root))
|
||||
.to eq('Top of Information Store')
|
||||
end
|
||||
end
|
||||
|
||||
context 'and target folder is in subfolder of root' do
|
||||
it 'returns the full path from root to target' do
|
||||
expect(subject.display_path(grandchild_of_root))
|
||||
.to eq('Top of Information Store -> Inbox')
|
||||
end
|
||||
end
|
||||
|
||||
context 'and walking up directory tree raises EwsError' do
|
||||
it 'returns the partial path from error to target folder' do
|
||||
allow(subject)
|
||||
.to receive(:id_folder_map).with(any_args).and_raise(Viewpoint::EWS::EwsError)
|
||||
|
||||
expect(subject.display_path(grandchild_of_root))
|
||||
.to eq('Inbox')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when server returns invalid UTF-8' do
|
||||
context 'and target folder is in root directory' do
|
||||
it 'returns the display name of the folder in valid UTF-8' do
|
||||
allow(child_of_root)
|
||||
.to receive(:display_name).and_return('你好'.b)
|
||||
|
||||
expect { subject.display_path(child_of_root).to_json }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'and target folder is in subfolder of root' do
|
||||
it 'returns the full path from root to target in valid UTF-8' do
|
||||
allow(grandchild_of_root)
|
||||
.to receive(:display_name).and_return('你好'.b)
|
||||
|
||||
expect { subject.display_path(grandchild_of_root).to_json }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'and walking up directory tree raises EwsError' do
|
||||
it 'returns the partial path from error to target folder in valid UTF-8' do
|
||||
allow(grandchild_of_root)
|
||||
.to receive(:display_name).and_return('你好'.b)
|
||||
allow(subject)
|
||||
.to receive(:id_folder_map).with(any_args).and_raise(Viewpoint::EWS::EwsError)
|
||||
|
||||
expect { subject.display_path(grandchild_of_root).to_json }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,6 +2,7 @@ require 'rails_helper'
|
||||
# rails autoloading issue
|
||||
require 'ldap'
|
||||
require 'ldap/user'
|
||||
require 'tcr/net/ldap'
|
||||
|
||||
RSpec.describe Ldap::User do
|
||||
|
||||
@@ -195,4 +196,51 @@ RSpec.describe Ldap::User do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Each of these test cases depends on
|
||||
# sample TCP transmission data recorded with TCR,
|
||||
# stored in test/data/tcr_cassettes.
|
||||
context 'on mocked LDAP connections' do
|
||||
around do |example|
|
||||
cassette_name = example.description.gsub(/[^0-9A-Za-z.\-]+/, '_')
|
||||
|
||||
begin
|
||||
original_tcr_format = TCR.configuration.format
|
||||
TCR.configuration.format = 'marshal'
|
||||
TCR.use_cassette("lib/ldap/user/#{cassette_name}") { example.run }
|
||||
ensure
|
||||
TCR.configuration.format = original_tcr_format
|
||||
end
|
||||
end
|
||||
|
||||
describe '#attributes' do
|
||||
let(:subject) { described_class.new(config, ldap: ldap) }
|
||||
let(:ldap) { Ldap.new(config) }
|
||||
let(:config) do
|
||||
{ 'host_url' => 'ldap://localhost',
|
||||
'options' => { 'dc=example,dc=org' => 'dc=example,dc=org' },
|
||||
'option' => 'dc=example,dc=org',
|
||||
'base_dn' => 'dc=example,dc=org',
|
||||
'bind_user' => 'cn=admin,dc=example,dc=org',
|
||||
'bind_pw' => 'admin' }.with_indifferent_access
|
||||
end
|
||||
|
||||
# see https://github.com/zammad/zammad/issues/2140
|
||||
#
|
||||
# This method grabs sample values of user attributes on the LDAP server.
|
||||
# It used to coerce ALL values to Unicode strings, including binary attributes
|
||||
# (e.g., usersmimecertificate / msexchmailboxsecuritydescriptor),
|
||||
# which led to valid Unicode gibberish (e.g., "\u0001\u0001\u0004...")
|
||||
#
|
||||
# When saving these values to the database,
|
||||
# ActiveRecord::Store would convert them to binary (ASCII-8BIT) strings,
|
||||
# which would then break #to_json with an Encoding::UndefinedConversion error.
|
||||
it 'skips binary attributes (#2140)' do
|
||||
Setting.set('ldap_config', subject.attributes)
|
||||
|
||||
expect { Setting.get('ldap_config').to_json }
|
||||
.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
40
spec/lib/report/ticket_generic_time_spec.rb
Normal file
40
spec/lib/report/ticket_generic_time_spec.rb
Normal file
@@ -0,0 +1,40 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Report::TicketGenericTime do
|
||||
|
||||
=begin
|
||||
|
||||
result = Report::TicketGenericTime.items(
|
||||
range_start: '2015-01-01T00:00:00Z',
|
||||
range_end: '2015-12-31T23:59:59Z',
|
||||
selector: selector, # ticket selector to get only a collection of tickets
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
|
||||
returns
|
||||
|
||||
{
|
||||
count: 123,
|
||||
ticket_ids: [4,5,1,5,0,51,5,56,7,4],
|
||||
assets: assets,
|
||||
}
|
||||
|
||||
=end
|
||||
|
||||
describe 'items' do
|
||||
|
||||
# Regression test for issue #2246 - Records in Reporting not updated when single ActiveRecord can not be found
|
||||
it 'correctly handles missing tickets' do
|
||||
class_double('SearchIndexBackend', selectors: { ticket_ids: [-1] } ).as_stubbed_const
|
||||
|
||||
expect do
|
||||
described_class.items(
|
||||
range_start: '2015-01-01T00:00:00Z',
|
||||
range_end: '2015-12-31T23:59:59Z',
|
||||
selector: {}, # ticket selector to get only a collection of tickets
|
||||
params: { field: 'created_at' },
|
||||
)
|
||||
end.to_not raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -56,6 +56,20 @@ RSpec.describe Sequencer::Unit::Import::Common::Model::Associations::Assign, seq
|
||||
let(:instance) { create(:user) }
|
||||
let(:associations) { nil }
|
||||
|
||||
context 'and `action == :skipped`' do
|
||||
let(:action) { :skipped }
|
||||
|
||||
it 'makes no changes' do
|
||||
allow(Rails.logger).to receive(:error).and_call_original
|
||||
|
||||
provided = process(parameters)
|
||||
|
||||
expect(Rails.logger).not_to have_received(:error)
|
||||
expect(provided).to include(action: action)
|
||||
expect(instance.changed?).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'and `action == :failed`' do
|
||||
let(:action) { :failed }
|
||||
|
||||
|
||||
47
spec/lib/sequencer/unit/import/common/model/save_spec.rb
Normal file
47
spec/lib/sequencer/unit/import/common/model/save_spec.rb
Normal file
@@ -0,0 +1,47 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Sequencer::Unit::Import::Common::Model::Save, sequencer: :unit do
|
||||
let(:user) { instance_double('User') }
|
||||
|
||||
before { allow(user).to receive(:save!) }
|
||||
|
||||
context 'for action: :created' do
|
||||
it 'calls #save!' do
|
||||
process(action: :created, instance: user, dry_run: false)
|
||||
|
||||
expect(user).to have_received(:save!)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for action: :updated' do
|
||||
it 'calls #save!' do
|
||||
process(action: :updated, instance: user, dry_run: false)
|
||||
|
||||
expect(user).to have_received(:save!)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for action: :unchanged' do
|
||||
it 'avoids calling #save!' do
|
||||
process(action: :unchanged, instance: user, dry_run: false)
|
||||
|
||||
expect(user).not_to have_received(:save!)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for action: :skipped' do
|
||||
it 'avoids calling #save!' do
|
||||
process(action: :skipped, instance: user, dry_run: false)
|
||||
|
||||
expect(user).not_to have_received(:save!)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for action: :failed' do
|
||||
it 'avoids calling #save!' do
|
||||
process(action: :failed, instance: user, dry_run: false)
|
||||
|
||||
expect(user).not_to have_received(:save!)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,98 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Sequencer::Unit::Import::Ldap::User::Attributes::RoleIds::Unassigned, sequencer: :unit do
|
||||
let(:subject) { process(parameters) }
|
||||
|
||||
let(:parameters) do
|
||||
{ resource: resource,
|
||||
dn_roles: dn_roles,
|
||||
ldap_config: ldap_config,
|
||||
mapped: mapped,
|
||||
instance: instance }
|
||||
end
|
||||
|
||||
let(:resource) do
|
||||
{ dn: 'uid=jane,ou=People,dc=example,dc=org',
|
||||
uid: 'jane_doe',
|
||||
sn: 'Doe',
|
||||
givenname: 'Jane' }.with_indifferent_access
|
||||
end
|
||||
|
||||
# this sequencer unit only runs when this parameter is PRESENT
|
||||
let(:dn_roles) do
|
||||
{ 'uid=john,ou=people,dc=example,dc=org' => [1] }.with_indifferent_access
|
||||
end
|
||||
|
||||
# this sequencer unit only runs when this parameter's "unassigned_users" key is "skip_sync"
|
||||
let(:ldap_config) do
|
||||
{ unassigned_users: 'skip_sync' }.with_indifferent_access
|
||||
end
|
||||
|
||||
# this sequencer unit only runs when this parameter's "role_ids" key is BLANK
|
||||
let(:mapped) do
|
||||
{ firstname: 'Jane',
|
||||
lastname: 'Doe',
|
||||
login: 'jane_doe' }.with_indifferent_access
|
||||
end
|
||||
|
||||
let(:instance) { create(:agent_user, mapped) }
|
||||
|
||||
context 'when user exists from previous import' do
|
||||
context 'and is active' do
|
||||
before { instance.update(active: true) }
|
||||
|
||||
it 'deactivates user (with action: :deactivated)' do
|
||||
expect(subject).to include(action: :deactivated)
|
||||
expect(instance.reload.active).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'and is inactive' do
|
||||
before { instance.update(active: false) }
|
||||
|
||||
it 'skips user (with action: :skipped)' do
|
||||
expect(subject).to include(action: :skipped)
|
||||
expect(instance.reload.active).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has not been imported yet' do
|
||||
let(:instance) { nil }
|
||||
|
||||
it 'skips user (with action: :skipped)' do
|
||||
expect(subject).to include(action: :skipped)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'parameters' do
|
||||
before { allow(instance).to receive(:update).and_call_original }
|
||||
|
||||
context 'without :dn_roles (hash map of LDAP DNs ↔ Zammad role IDs for ALL USERS)' do
|
||||
before { parameters[:dn_roles].clear }
|
||||
|
||||
it 'skips user (with NO action)' do
|
||||
expect(subject).to include(action: nil)
|
||||
expect(instance).not_to have_received(:update)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with :mapped[:role_ids] (hash map of LDAP attributes ↔ Zammad attributes for GIVEN USER)' do
|
||||
before { parameters[:mapped].merge!(role_ids: [2]) }
|
||||
|
||||
it 'skips user (with NO action)' do
|
||||
expect(subject).to include(action: nil)
|
||||
expect(instance).not_to have_received(:update)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when :ldap_config[:unassigned_users] != "skip_sync"' do
|
||||
before { parameters[:ldap_config].merge!(unassigned_users: 'sigup_roles') }
|
||||
|
||||
it 'skips user (with NO action)' do
|
||||
expect(subject).to include(action: nil)
|
||||
expect(instance).not_to have_received(:update)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -48,5 +48,35 @@ RSpec.describe Channel::EmailParser, type: :model do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# see https://github.com/zammad/zammad/issues/2254
|
||||
context 'when sender address contains > (#2254)' do
|
||||
let(:mail_file) { Rails.root.join('test', 'data', 'mail', 'mail076.box') }
|
||||
let(:sender_email) { 'millionslotteryspaintransfer@example.com' }
|
||||
|
||||
it 'removes them before creating a new user' do
|
||||
expect { described_class.new.process({}, raw_mail) }
|
||||
.to change { User.where(email: sender_email).count }.to(1)
|
||||
end
|
||||
|
||||
it 'marks new user email as invalid' do
|
||||
described_class.new.process({}, raw_mail)
|
||||
|
||||
expect(User.find_by(email: sender_email).preferences)
|
||||
.to include('mail_delivery_failed' => true)
|
||||
.and include('mail_delivery_failed_reason' => 'invalid email')
|
||||
.and include('mail_delivery_failed_data' => a_kind_of(ActiveSupport::TimeWithZone))
|
||||
end
|
||||
end
|
||||
|
||||
# see https://github.com/zammad/zammad/issues/2224
|
||||
context 'when header specifies Windows-1258 charset (#2224)' do
|
||||
let(:mail_file) { Rails.root.join('test', 'data', 'mail', 'mail072.box') }
|
||||
|
||||
it 'does not raise Encoding::ConverterNotFoundError' do
|
||||
expect { described_class.new.process({}, raw_mail) }
|
||||
.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
31
spec/models/object_manager/attribute_spec.rb
Normal file
31
spec/models/object_manager/attribute_spec.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ObjectManager::Attribute, type: :model do
|
||||
describe 'check name' do
|
||||
it 'rejects ActiveRecord reserved word "attribute"' do
|
||||
expect do
|
||||
ObjectManager::Attribute.add attributes_for :object_manager_attribute_text, name: 'attribute'
|
||||
end.to raise_error 'attribute is a reserved word, please choose a different one'
|
||||
end
|
||||
|
||||
it 'rejects Zammad reserved word "table"' do
|
||||
expect do
|
||||
ObjectManager::Attribute.add attributes_for :object_manager_attribute_text, name: 'table'
|
||||
end.to raise_error 'table is a reserved word, please choose a different one'
|
||||
end
|
||||
|
||||
it 'accepts duplicate attribute name on the same types (editing an existing attribute)' do
|
||||
attribute = attributes_for :object_manager_attribute_select
|
||||
ObjectManager::Attribute.add attribute
|
||||
expect do
|
||||
ObjectManager::Attribute.add attribute
|
||||
end.to_not raise_error
|
||||
end
|
||||
|
||||
it 'accepts valid attribute names' do
|
||||
expect do
|
||||
ObjectManager::Attribute.add attributes_for :object_manager_attribute_text
|
||||
end.to_not raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
179
spec/models/recent_view_spec.rb
Normal file
179
spec/models/recent_view_spec.rb
Normal file
@@ -0,0 +1,179 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe RecentView, type: :model do
|
||||
let(:admin) { create(:admin_user) }
|
||||
let(:agent) { create(:agent_user) }
|
||||
let(:customer) { create(:customer_user) }
|
||||
let(:ticket) { create(:ticket, owner: owner, customer: customer) }
|
||||
let(:tickets) { create_list(:ticket, 15, owner: owner, customer: customer) }
|
||||
let(:owner) { admin }
|
||||
|
||||
describe '::list' do
|
||||
it 'returns a sample of recently viewed objects (e.g., tickets/users/organizations)' do
|
||||
tickets.each { |t| described_class.log('Ticket', t.id, admin) }
|
||||
|
||||
expect(described_class.list(admin).map(&:o_id)).to include(*tickets.last(10).map(&:id))
|
||||
end
|
||||
|
||||
it 'returns up to 10 results by default' do
|
||||
tickets.each { |t| described_class.log('Ticket', t.id, admin) }
|
||||
|
||||
expect(described_class.list(admin).length).to eq(10)
|
||||
end
|
||||
|
||||
context 'with a `limit` argument (optional)' do
|
||||
it 'returns up to that number of results' do
|
||||
tickets.each { |t| described_class.log('Ticket', t.id, admin) }
|
||||
|
||||
expect(described_class.list(admin, 12).length).to eq(12)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an `object_name` argument (optional)' do
|
||||
it 'includes only the specified model class' do
|
||||
described_class.log('Ticket', ticket.id, admin)
|
||||
described_class.log('Organization', 1, admin)
|
||||
|
||||
expect(described_class.list(admin, 10, 'Organization').length).to eq(1)
|
||||
end
|
||||
|
||||
it 'does not include merged tickets in results' do
|
||||
described_class.log('Ticket', ticket.id, admin)
|
||||
ticket.update(state: Ticket::State.find_by(name: 'merged'))
|
||||
|
||||
expect(described_class.list(admin, 10, 'Ticket').length).to eq(0)
|
||||
end
|
||||
|
||||
it 'does not include removed tickets in results' do
|
||||
described_class.log('Ticket', ticket.id, admin)
|
||||
ticket.update(state: Ticket::State.find_by(name: 'removed'))
|
||||
|
||||
expect(described_class.list(admin, 10, 'Ticket').length).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not include duplicate results' do
|
||||
5.times { described_class.log('Ticket', ticket.id, admin) }
|
||||
|
||||
expect(described_class.list(admin).length).to eq(1)
|
||||
end
|
||||
|
||||
it 'does not include deleted tickets in results' do
|
||||
described_class.log('Ticket', ticket.id, admin)
|
||||
ticket.destroy
|
||||
|
||||
expect(described_class.list(admin).length).to eq(0)
|
||||
end
|
||||
|
||||
describe 'access privileges' do
|
||||
context 'when given user is agent' do
|
||||
let(:owner) { agent }
|
||||
|
||||
it 'includes own tickets in results' do
|
||||
described_class.log('Ticket', ticket.id, agent)
|
||||
|
||||
expect(described_class.list(agent).length).to eq(1)
|
||||
end
|
||||
|
||||
it 'does not include other agents’ tickets in results' do
|
||||
described_class.log('Ticket', ticket.id, agent)
|
||||
ticket.update(owner: User.first)
|
||||
|
||||
expect(described_class.list(agent).length).to eq(0)
|
||||
end
|
||||
|
||||
it 'includes any organizations in results' do
|
||||
agent.update(organization: nil)
|
||||
described_class.log('Organization', 1, agent)
|
||||
|
||||
expect(described_class.list(agent).length).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when given user is customer' do
|
||||
it 'includes own tickets in results' do
|
||||
described_class.log('Ticket', ticket.id, customer)
|
||||
|
||||
expect(described_class.list(customer).length).to eq(1)
|
||||
end
|
||||
|
||||
it 'does not include other customers’ tickets in results' do
|
||||
described_class.log('Ticket', ticket.id, customer)
|
||||
ticket.update(customer: User.first)
|
||||
|
||||
expect(described_class.list(customer).length).to eq(0)
|
||||
end
|
||||
|
||||
it 'includes own organization in results' do
|
||||
customer.update(organization: Organization.first)
|
||||
described_class.log('Organization', 1, customer)
|
||||
|
||||
expect(described_class.list(customer).length).to eq(1)
|
||||
end
|
||||
|
||||
it 'does not include other organizations in results' do
|
||||
customer.update(organization: Organization.first)
|
||||
described_class.log('Organization', 1, customer)
|
||||
customer.update(organization: nil)
|
||||
|
||||
expect(described_class.list(customer).length).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '::user_log_destroy' do
|
||||
it 'deletes all RecentView records for a given user' do
|
||||
create_list(:recent_view, 10, created_by_id: admin.id)
|
||||
|
||||
expect { described_class.user_log_destroy(admin) }
|
||||
.to change { described_class.exists?(created_by_id: admin.id) }.to(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe '::log' do
|
||||
let(:viewed_object) { ticket }
|
||||
let(:viewed_object_class_id) { ObjectLookup.by_name(viewed_object.class.name) }
|
||||
|
||||
it 'wraps RecentView.create' do
|
||||
expect do
|
||||
described_class.log(viewed_object.class.name, viewed_object.id, admin)
|
||||
end.to change { RecentView.count }.by(1)
|
||||
end
|
||||
|
||||
describe 'access privileges' do
|
||||
let(:owner) { agent }
|
||||
|
||||
it 'does not create RecentView for records the given user cannot read' do
|
||||
ticket.update(owner: User.first, # read access may come from ticket ownership,
|
||||
customer: User.first, # customer status,
|
||||
organization: nil) # organization's 'shared' status,
|
||||
agent.update(groups: []) # and membership in the Ticket's group
|
||||
|
||||
expect do
|
||||
described_class.log(viewed_object.class.name, viewed_object.id, agent)
|
||||
end.not_to change { RecentView.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when given an invalid object' do
|
||||
it 'does not create RecentView for non-existent record' do
|
||||
expect do
|
||||
described_class.log('User', 99_999_999, admin)
|
||||
end.not_to change { RecentView.count }
|
||||
end
|
||||
|
||||
it 'does not create RecentView for instance of non-ObjectLookup class' do
|
||||
expect do
|
||||
described_class.log('Overview', 1, admin)
|
||||
end.not_to change { RecentView.count }
|
||||
end
|
||||
|
||||
it 'does not create RecentView for instance of non-existent class' do
|
||||
expect do
|
||||
described_class.log('NonExistentClass', 1, admin)
|
||||
end.not_to change { RecentView.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -31,7 +31,7 @@ ActiveRecord::Migration.maintain_test_schema!
|
||||
Package::Migration.linked
|
||||
|
||||
# allow requests to Zammad webservices
|
||||
WebMock.disable_net_connect!(allow: /zammad\.com/)
|
||||
WebMock.disable_net_connect!(allow: /zammad\.com|exchange.example.com/)
|
||||
|
||||
RSpec.configure do |config|
|
||||
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
|
||||
|
||||
25
spec/requests/time_accounting_spec.rb
Normal file
25
spec/requests/time_accounting_spec.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Time Accounting API endpoints', type: :request do
|
||||
let(:admin) { create(:admin_user) }
|
||||
let(:customer) { create(:customer_user) }
|
||||
let(:year) { Time.current.year }
|
||||
let(:month) { Time.current.month }
|
||||
|
||||
describe '/api/v1/time_accounting/log/by_ticket' do
|
||||
context 'when requesting a JSON response' do
|
||||
# see https://github.com/zammad/zammad/pull/2243
|
||||
context 'and logs exist for work performed by an agent who is also the customer of the ticket (#2243)' do
|
||||
let(:ticket) { create(:ticket, customer: admin) }
|
||||
let!(:time_log) { create(:ticket_time_accounting, ticket: ticket, created_by_id: admin.id) }
|
||||
|
||||
it 'responds with a non-nil value for each :agent key' do
|
||||
authenticated_as(admin)
|
||||
get "/api/v1/time_accounting/log/by_ticket/#{year}/#{month}", as: :json
|
||||
|
||||
expect(json_response.first).not_to include('agent' => nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
5
spec/support/tcr.rb
Normal file
5
spec/support/tcr.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
TCR.configure do |config|
|
||||
config.cassette_library_dir = 'test/data/tcr_cassettes'
|
||||
config.hook_tcp_ports = [389] # LDAP
|
||||
config.format = 'yaml'
|
||||
end
|
||||
5
spec/support/vcr.rb
Normal file
5
spec/support/vcr.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
VCR.configure do |config|
|
||||
config.cassette_library_dir = 'test/data/vcr_cassettes'
|
||||
config.hook_into :webmock
|
||||
config.allow_http_connections_when_no_cassette = true
|
||||
end
|
||||
@@ -5,10 +5,11 @@ class AAbUnitTest < TestCase
|
||||
def test_core
|
||||
@browser = browser_instance
|
||||
location(url: browser_url + '/tests_core')
|
||||
sleep 5
|
||||
watch_for(
|
||||
css: '.result',
|
||||
value: 'Tests completed',
|
||||
timeout: 8,
|
||||
timeout: 4,
|
||||
)
|
||||
match(
|
||||
css: '.result .failed',
|
||||
@@ -30,10 +31,11 @@ class AAbUnitTest < TestCase
|
||||
)
|
||||
|
||||
location(url: browser_url + '/tests_model')
|
||||
sleep 5
|
||||
watch_for(
|
||||
css: '.result',
|
||||
value: 'Tests completed',
|
||||
timeout: 8,
|
||||
timeout: 3,
|
||||
)
|
||||
match(
|
||||
css: '.result .failed',
|
||||
@@ -77,10 +79,11 @@ class AAbUnitTest < TestCase
|
||||
def test_form
|
||||
@browser = browser_instance
|
||||
location(url: browser_url + '/tests_form')
|
||||
sleep 5
|
||||
watch_for(
|
||||
css: '.result',
|
||||
value: 'Tests completed',
|
||||
timeout: 8,
|
||||
timeout: 2,
|
||||
)
|
||||
match(
|
||||
css: '.result .failed',
|
||||
@@ -212,10 +215,11 @@ class AAbUnitTest < TestCase
|
||||
)
|
||||
|
||||
location(url: browser_url + '/tests_taskbar')
|
||||
sleep 5
|
||||
watch_for(
|
||||
css: '.result',
|
||||
value: 'Tests completed',
|
||||
timeout: 8,
|
||||
timeout: 3,
|
||||
)
|
||||
match(
|
||||
css: '.result .failed',
|
||||
|
||||
@@ -347,19 +347,9 @@ class AdminObjectManagerTest < TestCase
|
||||
name: 'browser_test7',
|
||||
},
|
||||
)
|
||||
click(css: '.content.active .tab-pane.active div.js-execute')
|
||||
watch_for(
|
||||
css: '.modal',
|
||||
value: 'restart',
|
||||
)
|
||||
watch_for_disappear(
|
||||
css: '.modal',
|
||||
timeout: 7.minutes,
|
||||
)
|
||||
sleep 5
|
||||
watch_for(
|
||||
css: '.content.active',
|
||||
)
|
||||
sleep 1
|
||||
object_manager_attribute_migrate
|
||||
|
||||
match_not(
|
||||
css: '.content.active',
|
||||
value: 'Database Update required',
|
||||
@@ -670,5 +660,269 @@ class AdminObjectManagerTest < TestCase
|
||||
unsorted_options = select_element.find_elements(xpath: './*').map(&:text).reject { |x| x == '-' }
|
||||
log unsorted_options.inspect
|
||||
assert_equal options, unsorted_options
|
||||
|
||||
object_manager_attribute_delete(
|
||||
data: {
|
||||
name: 'select_attributes_sorting_test',
|
||||
},
|
||||
)
|
||||
object_manager_attribute_migrate
|
||||
end
|
||||
|
||||
def test_deleted_select_attributes
|
||||
@browser = browser_instance
|
||||
login(
|
||||
username: 'master@example.com',
|
||||
password: 'test',
|
||||
url: browser_url,
|
||||
)
|
||||
|
||||
options = Hash[ %w[äöü cat delete dog ß].map { |x| [x, "#{x.capitalize} Display"] } ]
|
||||
options_no_dog = options.except('dog')
|
||||
options_no_dog_no_delete = options_no_dog.except('delete')
|
||||
|
||||
tasks_close_all()
|
||||
|
||||
object_manager_attribute_create(
|
||||
data: {
|
||||
name: 'select_attributes_delete_test',
|
||||
display: 'Select Attributes Delete Test',
|
||||
data_type: 'Select',
|
||||
data_option: {
|
||||
options: options,
|
||||
},
|
||||
},
|
||||
)
|
||||
object_manager_attribute_migrate
|
||||
|
||||
ticket = ticket_create(
|
||||
data: {
|
||||
customer: 'nico',
|
||||
group: 'Users',
|
||||
title: 'select_attributes_delete_test',
|
||||
body: 'select_attributes_delete_test',
|
||||
},
|
||||
custom_data_select: {
|
||||
select_attributes_delete_test: 'Delete Display',
|
||||
},
|
||||
disable_group_check: true,
|
||||
)
|
||||
|
||||
watch_for(
|
||||
css: '.content.active select[name="select_attributes_delete_test"]',
|
||||
)
|
||||
|
||||
# confirm that all options and their display values are there and are in the correct order
|
||||
select_element = @browser.find_elements(css: '.content.active select[name="select_attributes_delete_test"]')[0]
|
||||
unsorted_options = select_element.find_elements(xpath: './*').map { |o| o.attribute('value') }.reject { |x| x == '' }
|
||||
assert_equal options.keys, unsorted_options
|
||||
unsorted_display_options = select_element.find_elements(xpath: './*').map(&:text).reject { |x| x == '-' }
|
||||
assert_equal options.values, unsorted_display_options
|
||||
|
||||
# confirm that the "delete" option is selected and that its display text is indeed "Delete Display"
|
||||
selected_option = select_element.find_elements(css: 'option:checked')[0]
|
||||
assert_equal 'delete', selected_option.attribute('value')
|
||||
assert_equal 'Delete Display', selected_option.text
|
||||
|
||||
object_manager_attribute_update(
|
||||
data: {
|
||||
name: 'select_attributes_delete_test',
|
||||
data_option: {
|
||||
options: options_no_dog_no_delete,
|
||||
},
|
||||
},
|
||||
)
|
||||
object_manager_attribute_migrate
|
||||
|
||||
# open the previously created ticket and verify its attribute selection
|
||||
click(
|
||||
xpath: '//a/div[contains(text(),"select_attributes_delete_test")]',
|
||||
)
|
||||
# confirm that all options and their display values are there and are in the correct order
|
||||
select_element = @browser.find_elements(css: '.content.active select[name="select_attributes_delete_test"]')[0]
|
||||
unsorted_options = select_element.find_elements(xpath: './*').map { |o| o.attribute('value') }.reject { |x| x == '' }
|
||||
assert_equal options_no_dog.keys, unsorted_options
|
||||
unsorted_display_options = select_element.find_elements(xpath: './*').map(&:text).reject { |x| x == '-' }
|
||||
assert_equal options_no_dog.values, unsorted_display_options
|
||||
|
||||
# confirm that the "delete" option is still selected and that its display text is still indeed "Delete Display"
|
||||
selected_option = select_element.find_elements(css: 'option:checked')[0]
|
||||
assert_equal 'delete', selected_option.attribute('value')
|
||||
assert_equal 'Delete Display', selected_option.text
|
||||
|
||||
# create a new ticket and check that the deleted options no longer appear
|
||||
click(
|
||||
css: 'a[href="#ticket/create"]',
|
||||
mute_log: true,
|
||||
)
|
||||
|
||||
watch_for(
|
||||
css: 'select[name="select_attributes_delete_test"]',
|
||||
)
|
||||
|
||||
select_element = @browser.find_elements(css: 'select[name="select_attributes_delete_test"]')[0]
|
||||
unsorted_options = select_element.find_elements(xpath: './*').map { |o| o.attribute('value') }.reject { |x| x == '' }
|
||||
assert_equal options_no_dog_no_delete.keys, unsorted_options
|
||||
unsorted_display_options = select_element.find_elements(xpath: './*').map(&:text).reject { |x| x == '-' }
|
||||
assert_equal options_no_dog_no_delete.values, unsorted_display_options
|
||||
|
||||
object_manager_attribute_delete(
|
||||
data: {
|
||||
name: 'select_attributes_delete_test',
|
||||
},
|
||||
)
|
||||
object_manager_attribute_migrate
|
||||
end
|
||||
|
||||
# verify fix for issue #2233 - Boolean object set to false is not visible
|
||||
# verify fix for issue #2277 - Note is not shown for customer / organisations if it's empty
|
||||
def test_false_boolean_attributes_gets_displayed_for_organizations
|
||||
@browser = browser_instance
|
||||
login(
|
||||
username: 'master@example.com',
|
||||
password: 'test',
|
||||
url: browser_url,
|
||||
)
|
||||
tasks_close_all()
|
||||
|
||||
object_manager_attribute_create(
|
||||
data: {
|
||||
object: 'Organization',
|
||||
name: 'bool_test',
|
||||
display: 'bool_test',
|
||||
data_type: 'Boolean',
|
||||
data_option: {
|
||||
options: {
|
||||
# rubocop:disable Lint/BooleanSymbol
|
||||
true: 'YES',
|
||||
false: 'NO',
|
||||
# rubocop:enable Lint/BooleanSymbol
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
object_manager_attribute_create(
|
||||
data: {
|
||||
object: 'Organization',
|
||||
name: 'text_test',
|
||||
display: 'text_test',
|
||||
data_type: 'Text',
|
||||
},
|
||||
)
|
||||
object_manager_attribute_migrate
|
||||
|
||||
ticket_open_by_title(title: 'select')
|
||||
|
||||
click( css: '.content.active .tabsSidebar-tab[data-tab="organization"]' )
|
||||
click( css: '.content.active .sidebar[data-tab="organization"] .js-actions .dropdown-toggle' )
|
||||
click( css: '.content.active .sidebar[data-tab="organization"] .js-actions [data-type="organization-edit"]' )
|
||||
|
||||
modal_ready
|
||||
select(css: '.content.active .modal select[name="bool_test"]', value: 'NO')
|
||||
click( css: '.content.active .modal .js-submit' )
|
||||
modal_disappear
|
||||
|
||||
watch_for(
|
||||
css: '.content.active .sidebar[data-tab="organization"] .sidebar-content',
|
||||
value: 'bool_test',
|
||||
)
|
||||
match_not(
|
||||
css: '.content.active .sidebar[data-tab="organization"] .sidebar-content',
|
||||
value: 'text_test',
|
||||
)
|
||||
match(
|
||||
css: '.content.active .sidebar[data-tab="organization"] .sidebar-content',
|
||||
value: 'note',
|
||||
)
|
||||
|
||||
object_manager_attribute_delete(
|
||||
data: {
|
||||
object: 'Organization',
|
||||
name: 'bool_test',
|
||||
},
|
||||
)
|
||||
object_manager_attribute_delete(
|
||||
data: {
|
||||
object: 'Organization',
|
||||
name: 'text_test',
|
||||
},
|
||||
)
|
||||
object_manager_attribute_migrate
|
||||
end
|
||||
|
||||
# verify fix for issue #2233 - Boolean object set to false is not visible
|
||||
# verify fix for issue #2277 - Note is not shown for customer / organisations if it's empty
|
||||
def test_false_boolean_attributes_gets_displayed_for_users
|
||||
@browser = browser_instance
|
||||
login(
|
||||
username: 'master@example.com',
|
||||
password: 'test',
|
||||
url: browser_url,
|
||||
)
|
||||
tasks_close_all()
|
||||
|
||||
object_manager_attribute_create(
|
||||
data: {
|
||||
object: 'User',
|
||||
name: 'bool_test',
|
||||
display: 'bool_test',
|
||||
data_type: 'Boolean',
|
||||
data_option: {
|
||||
options: {
|
||||
# rubocop:disable Lint/BooleanSymbol
|
||||
true: 'YES',
|
||||
false: 'NO',
|
||||
# rubocop:enable Lint/BooleanSymbol
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
object_manager_attribute_create(
|
||||
data: {
|
||||
object: 'User',
|
||||
name: 'text_test',
|
||||
display: 'text_test',
|
||||
data_type: 'Text',
|
||||
},
|
||||
)
|
||||
object_manager_attribute_migrate
|
||||
|
||||
ticket_open_by_title(title: 'select')
|
||||
|
||||
click( css: '.content.active .tabsSidebar-tab[data-tab="customer"]' )
|
||||
click( css: '.content.active .sidebar[data-tab="customer"] .js-actions .dropdown-toggle' )
|
||||
click( css: '.content.active .sidebar[data-tab="customer"] .js-actions [data-type="customer-edit"]' )
|
||||
|
||||
modal_ready
|
||||
select(css: '.content.active .modal select[name="bool_test"]', value: 'NO')
|
||||
click( css: '.content.active .modal .js-submit' )
|
||||
modal_disappear
|
||||
|
||||
watch_for(
|
||||
css: '.content.active .sidebar[data-tab="customer"] .sidebar-content',
|
||||
value: 'bool_test',
|
||||
)
|
||||
match_not(
|
||||
css: '.content.active .sidebar[data-tab="customer"] .sidebar-content',
|
||||
value: 'text_test',
|
||||
)
|
||||
match(
|
||||
css: '.content.active .sidebar[data-tab="customer"] .sidebar-content',
|
||||
value: 'note',
|
||||
)
|
||||
|
||||
object_manager_attribute_delete(
|
||||
data: {
|
||||
object: 'User',
|
||||
name: 'bool_test',
|
||||
},
|
||||
)
|
||||
object_manager_attribute_delete(
|
||||
data: {
|
||||
object: 'User',
|
||||
name: 'text_test',
|
||||
},
|
||||
)
|
||||
object_manager_attribute_migrate
|
||||
end
|
||||
end
|
||||
|
||||
@@ -116,4 +116,94 @@ class AdminObjectManagerTreeSelectTest < TestCase
|
||||
)
|
||||
end
|
||||
|
||||
# verify the fix for issue #2206 - Unable to modify tree_select attributes with fresh 2.6
|
||||
def test_modify_tree_select_attributes
|
||||
@browser = instance = browser_instance
|
||||
login(
|
||||
username: 'master@example.com',
|
||||
password: 'test',
|
||||
url: browser_url,
|
||||
)
|
||||
tasks_close_all()
|
||||
|
||||
object_manager_attribute_create(
|
||||
data: {
|
||||
name: 'browser_test_tree_select2',
|
||||
display: 'Browser Test TreeSelect2',
|
||||
data_type: 'Tree Select',
|
||||
data_option: {
|
||||
options: {
|
||||
'Incident' => {
|
||||
'Hardware' => {
|
||||
'Monitor' => {},
|
||||
'Mouse' => {},
|
||||
},
|
||||
},
|
||||
'Service request' => {
|
||||
'New software requirement' => {},
|
||||
'New hardware' => {},
|
||||
},
|
||||
'Change request' => {},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
object_manager_attribute_migrate
|
||||
|
||||
# open the newly created tree_select and add some new options
|
||||
object_manager_attribute_update(
|
||||
data: {
|
||||
name: 'browser_test_tree_select2',
|
||||
},
|
||||
do_not_submit: true,
|
||||
)
|
||||
|
||||
# add two new first level entries
|
||||
2.times do |i|
|
||||
instance.find_elements(css: '.modal .js-treeTable .js-key').last.click
|
||||
|
||||
element = instance.find_elements(css: '.modal .js-treeTable .js-key').last
|
||||
element.clear
|
||||
element.send_keys("new tree option #{i}")
|
||||
end
|
||||
|
||||
click(
|
||||
css: '.modal button.js-submit'
|
||||
)
|
||||
modal_disappear
|
||||
|
||||
object_manager_attribute_migrate
|
||||
|
||||
# open the tree select again and check that the newly added options are there
|
||||
watch_for(
|
||||
css: '.content.active table',
|
||||
value: 'browser_test_tree_select2',
|
||||
)
|
||||
object_manager_attribute_update(
|
||||
data: {
|
||||
name: 'browser_test_tree_select2',
|
||||
},
|
||||
do_not_submit: true,
|
||||
)
|
||||
2.times do |i|
|
||||
exists(
|
||||
css: '.modal .js-treeTable',
|
||||
value: "new tree option #{i}",
|
||||
)
|
||||
end
|
||||
modal_close
|
||||
|
||||
# clean up and confirm the deletion of newly created attributes
|
||||
object_manager_attribute_delete(
|
||||
data: {
|
||||
name: 'browser_test_tree_select2',
|
||||
},
|
||||
)
|
||||
object_manager_attribute_migrate
|
||||
|
||||
match_not(
|
||||
css: '.content.active table',
|
||||
value: 'browser_test_tree_select2',
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -125,4 +125,59 @@ class AdminOverviewTest < TestCase
|
||||
get_location( css: "td[title='#{title}']").y]
|
||||
end.sort_by { |x| x[1] }.map { |x| x[0] }
|
||||
end
|
||||
|
||||
# verify fix for issue #2235 - Overview setting isn't applied on submit
|
||||
def test_overview_toggle_out_of_office_setting
|
||||
@browser = browser_instance
|
||||
login(
|
||||
username: 'master@example.com',
|
||||
password: 'test',
|
||||
url: browser_url,
|
||||
)
|
||||
tasks_close_all()
|
||||
|
||||
out_of_office_css = '.content.active .modal select[name="out_of_office"]'
|
||||
first_overview_css = '.content.active tr[data-id="1"] td'
|
||||
|
||||
click( css: 'a[href="#manage"]' )
|
||||
click( css: '.content.active a[href="#manage/overviews"]' )
|
||||
|
||||
# round 1, open the overview and set out_of_office to true
|
||||
click( css: first_overview_css )
|
||||
modal_ready
|
||||
watch_for(
|
||||
css: out_of_office_css,
|
||||
value: 'no',
|
||||
)
|
||||
select(
|
||||
css: out_of_office_css,
|
||||
value: 'yes',
|
||||
)
|
||||
click( css: '.content.active .modal .js-submit' )
|
||||
modal_disappear
|
||||
|
||||
# round 2, open the overview and set out_of_office back to false
|
||||
click( css: first_overview_css )
|
||||
modal_ready
|
||||
watch_for(
|
||||
css: out_of_office_css,
|
||||
value: 'yes',
|
||||
)
|
||||
select(
|
||||
css: out_of_office_css,
|
||||
value: 'no',
|
||||
)
|
||||
click( css: '.content.active .modal .js-submit' )
|
||||
modal_disappear
|
||||
|
||||
# round 3, open the overview and confirm that it's still false
|
||||
click( css: first_overview_css )
|
||||
modal_ready
|
||||
watch_for(
|
||||
css: out_of_office_css,
|
||||
value: 'no',
|
||||
)
|
||||
click( css: '.content.active .modal .js-submit' )
|
||||
modal_disappear
|
||||
end
|
||||
end
|
||||
|
||||
@@ -365,4 +365,286 @@ class AgentTicketOverviewLevel0Test < TestCase
|
||||
# cleanup
|
||||
tasks_close_all()
|
||||
end
|
||||
|
||||
# verify correct behaviour for issue #1864 - Bulk-Action: Not possible to change owner
|
||||
def test_bulk_owner_change
|
||||
@browser = browser_instance
|
||||
login(
|
||||
username: 'master@example.com',
|
||||
password: 'test',
|
||||
url: browser_url,
|
||||
)
|
||||
tasks_close_all()
|
||||
|
||||
# test bulk action
|
||||
|
||||
# create new ticket
|
||||
ticket1 = ticket_create(
|
||||
data: {
|
||||
customer: 'nico',
|
||||
group: 'Users',
|
||||
title: 'overview owner change test #1',
|
||||
body: 'overview owner change #1',
|
||||
}
|
||||
)
|
||||
ticket2 = ticket_create(
|
||||
data: {
|
||||
customer: 'nico',
|
||||
group: 'Users',
|
||||
title: 'overview owner change #2',
|
||||
body: 'overview owner change #2',
|
||||
}
|
||||
)
|
||||
|
||||
overview_open(
|
||||
link: '#ticket/view/all_unassigned',
|
||||
)
|
||||
|
||||
watch_for(
|
||||
css: '.content.active',
|
||||
value: 'overview owner change #2',
|
||||
timeout: 8,
|
||||
)
|
||||
|
||||
# remember current overview count
|
||||
overview_counter_before = overview_counter()
|
||||
|
||||
# select both via bulk action
|
||||
click(
|
||||
css: '.content.active table tr td input[value="' + ticket1[:id] + '"] + .icon-checkbox.icon-unchecked',
|
||||
fast: true,
|
||||
)
|
||||
|
||||
# scroll to reply - needed for chrome
|
||||
scroll_to(
|
||||
position: 'top',
|
||||
css: '.content.active table tr td input[value="' + ticket2[:id] + '"] + .icon-checkbox.icon-unchecked',
|
||||
)
|
||||
click(
|
||||
css: '.content.active table tr td input[value="' + ticket2[:id] + '"] + .icon-checkbox.icon-unchecked',
|
||||
fast: true,
|
||||
)
|
||||
|
||||
exists(
|
||||
css: '.content.active table tr td input[value="' + ticket1[:id] + '"][type="checkbox"]:checked',
|
||||
)
|
||||
exists(
|
||||
css: '.content.active table tr td input[value="' + ticket2[:id] + '"][type="checkbox"]:checked',
|
||||
)
|
||||
|
||||
select(
|
||||
css: '.content.active .bulkAction [name="owner_id"]',
|
||||
value: 'Test Master Agent',
|
||||
)
|
||||
|
||||
select(
|
||||
css: '.content.active .bulkAction [name="state_id"]',
|
||||
value: 'closed',
|
||||
)
|
||||
|
||||
click(
|
||||
css: '.content.active .bulkAction .js-confirm',
|
||||
)
|
||||
click(
|
||||
css: '.content.active .bulkAction .js-submit',
|
||||
)
|
||||
|
||||
watch_for_disappear(
|
||||
css: '.content.active table tr td input[value="' + ticket2[:id] + '"]',
|
||||
timeout: 12,
|
||||
)
|
||||
|
||||
exists_not(
|
||||
css: '.content.active table tr td input[value="' + ticket1[:id] + '"]',
|
||||
)
|
||||
exists_not(
|
||||
css: '.content.active table tr td input[value="' + ticket2[:id] + '"]',
|
||||
)
|
||||
|
||||
# get new overview count
|
||||
overview_counter_new = overview_counter()
|
||||
assert_equal(overview_counter_before['#ticket/view/all_unassigned'] - 2, overview_counter_new['#ticket/view/all_unassigned'])
|
||||
|
||||
# cleanup
|
||||
tasks_close_all()
|
||||
end
|
||||
|
||||
# verify fix for issue #2026 - Bulk action should not be shown if user has no change permissions
|
||||
def test_no_bulk_action_when_missing_change_permission
|
||||
@browser = browser_instance
|
||||
login(
|
||||
username: 'master@example.com',
|
||||
password: 'test',
|
||||
url: browser_url,
|
||||
)
|
||||
tasks_close_all()
|
||||
|
||||
# create new group
|
||||
group_create(
|
||||
data: {
|
||||
name: 'some group2',
|
||||
},
|
||||
)
|
||||
|
||||
role_edit(
|
||||
data: {
|
||||
name: 'Agent',
|
||||
group_permissions: { 2 => ['full'],
|
||||
3 => ['full'], }
|
||||
},
|
||||
)
|
||||
|
||||
user_edit(
|
||||
data: {
|
||||
login: 'master@example.com',
|
||||
permissions: { 1 => ['full'],
|
||||
2 => ['full'],
|
||||
3 => ['full'], }
|
||||
},
|
||||
)
|
||||
|
||||
user_create(
|
||||
data: {
|
||||
firstname: 'Tester',
|
||||
lastname: 'Agent 2',
|
||||
email: 'agent2@example.com',
|
||||
password: 'test',
|
||||
role: 'Agent',
|
||||
permissions: { 1 => %w[read create overview] }
|
||||
},
|
||||
)
|
||||
|
||||
# create new tickets
|
||||
can_change_ticket = ticket_create(
|
||||
data: {
|
||||
customer: 'nico',
|
||||
group: 'some group2',
|
||||
title: 'overview test #5',
|
||||
body: 'overview test #5',
|
||||
}
|
||||
)
|
||||
cannot_change_ticket = ticket_create(
|
||||
data: {
|
||||
customer: 'nico',
|
||||
group: 'Users',
|
||||
title: 'overview test #6',
|
||||
body: 'overview test #6',
|
||||
}
|
||||
)
|
||||
|
||||
logout() # logout as master@example.com then login as agent2@example.com
|
||||
login(
|
||||
username: 'agent2@example.com',
|
||||
password: 'test',
|
||||
url: browser_url,
|
||||
)
|
||||
tasks_close_all()
|
||||
|
||||
# open Overview menu tab
|
||||
click(
|
||||
css: '.js-menu .js-overviewsMenuItem',
|
||||
)
|
||||
|
||||
# enable full overviews
|
||||
execute(
|
||||
js: '$(".content.active .sidebar").css("display", "block")',
|
||||
)
|
||||
|
||||
# click Unassigned & Open tab
|
||||
click(
|
||||
css: '.content.active [href="#ticket/view/all_unassigned"]',
|
||||
)
|
||||
|
||||
watch_for(
|
||||
css: '.content.active',
|
||||
value: 'overview test #6',
|
||||
timeout: 8,
|
||||
)
|
||||
|
||||
# first select the ticket that we have change rights to
|
||||
check(
|
||||
css: '.content.active table tr td input[value="' + can_change_ticket[:id] + '"]',
|
||||
)
|
||||
|
||||
# check that the bulk action form appears
|
||||
exists(
|
||||
displayed: true,
|
||||
css: '.content.active .bulkAction',
|
||||
)
|
||||
|
||||
# then select the ticket that we do not have change rights to
|
||||
scroll_to(
|
||||
position: 'top',
|
||||
css: '.content.active table tr td input[value="' + cannot_change_ticket[:id] + '"] + .icon-checkbox.icon-unchecked',
|
||||
)
|
||||
check(
|
||||
css: '.content.active table tr td input[value="' + cannot_change_ticket[:id] + '"]',
|
||||
)
|
||||
|
||||
# check that the bulk action form disappears
|
||||
exists(
|
||||
displayed: false,
|
||||
css: '.content.active .bulkAction',
|
||||
)
|
||||
|
||||
# de-select the ticket that we do not have change rights to
|
||||
uncheck(
|
||||
css: '.content.active table tr td input[value="' + cannot_change_ticket[:id] + '"]',
|
||||
fast: true,
|
||||
)
|
||||
|
||||
# check that the bulk action form appears again
|
||||
exists(
|
||||
displayed: true,
|
||||
css: '.content.active .bulkAction',
|
||||
)
|
||||
|
||||
# de-select the ticket that we have change rights to
|
||||
uncheck(
|
||||
css: '.content.active table tr td input[value="' + can_change_ticket[:id] + '"]',
|
||||
fast: true,
|
||||
)
|
||||
|
||||
# check that the bulk action form disappears again
|
||||
exists(
|
||||
displayed: false,
|
||||
css: '.content.active .bulkAction',
|
||||
)
|
||||
|
||||
# cleanup
|
||||
tasks_close_all()
|
||||
logout() # logout as agent2@example.com and then login as master@example.com to clean up tickets
|
||||
login(
|
||||
username: 'master@example.com',
|
||||
password: 'test',
|
||||
url: browser_url,
|
||||
)
|
||||
tasks_close_all()
|
||||
|
||||
# open ticket by search
|
||||
ticket_open_by_search(
|
||||
number: cannot_change_ticket[:number],
|
||||
)
|
||||
sleep 1
|
||||
|
||||
# close ticket
|
||||
ticket_update(
|
||||
data: {
|
||||
state: 'closed',
|
||||
}
|
||||
)
|
||||
|
||||
# open ticket by search
|
||||
ticket_open_by_search(
|
||||
number: can_change_ticket[:number],
|
||||
)
|
||||
sleep 1
|
||||
|
||||
# close ticket
|
||||
ticket_update(
|
||||
data: {
|
||||
state: 'closed',
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user