Compare commits

...

329 Commits

Author SHA1 Message Date
Martin Edenhofer
ee6c8413c9 Update for 2.4.1. 2018-06-05 17:32:06 +02:00
André Bauer
0e376a32a3 added ubuntu 18.04 on packager.io 2018-05-25 11:33:21 +02:00
Mantas
a989921b15 Follow up for #1990 improves tests 2018-05-25 12:04:32 +03:00
Martin Edenhofer
1c4e55d53e Fixed issue #1957 - "Create new Customer" form delayed display. 2018-05-25 09:40:53 +02:00
Thorsten Eckel
a7aca59c73 Fixed issue #2040: Zendesk import: To/From extraction for system comments fails. 2018-05-24 14:50:22 +02:00
Thorsten Eckel
e0f616846f Fixed issue #2039: Import fails for ObjectManager Attribute with non-ASCII name. 2018-05-24 14:50:17 +02:00
Martin Edenhofer
1a8df8e61f Added test for model binding after relogin. 2018-05-24 14:28:10 +02:00
Thorsten Eckel
84110b7d14 Follow up: Fixes #2014 - Import job (LDAP/Exchange) hangs on scheduler restart. 2018-05-23 19:20:02 +02:00
Thorsten Eckel
0b5b32a7c0 Fixes #2014 - Import job (LDAP/Exchange) hangs on scheduler restart. 2018-05-23 19:01:13 +02:00
Ryan Lue
c46b95927a Revert Rubocop version bump and add exception to linting violation in ticket_spec 2018-05-23 17:12:04 +08:00
Ryan Lue
6440b6095f Merge branch 'stable' of git.znuny.com:zammad/zammad into stable 2018-05-23 16:43:33 +08:00
Ryan Lue
11448d599c Add /doc to .gitignore 2018-05-23 16:42:51 +08:00
Ryan Lue
7f0d74b815 Update Rubocop 2018-05-23 16:41:39 +08:00
Thorsten Eckel
8fb7d9903f Fixed issue #2037: Restart of processes form application context fails. 2018-05-23 10:23:34 +02:00
Ryan Lue
2067423c3d Reformat ticket_spec to avoid calling rubocop:disable/enable 2018-05-17 14:13:05 +08:00
Ryan Lue
2b2a7095e3 Fix regression from refactoring NotificationFactory::Template#to_s 2018-05-17 14:13:05 +08:00
Ryan Lue
1e967a02b5 Fix #2001 (Use non-bang gsub in NotificationFactory:Template) 2018-05-17 14:13:05 +08:00
Martin Edenhofer
ff4e43659f Fixes issue #1971 - Admin UI does not allow to select Timezone in "Calendars" options. 2018-05-11 10:16:36 +02:00
Mantas
c3d5c611ec Fixes #1990 missing CC autocomplete 2018-05-08 15:53:25 +03:00
Martin Edenhofer
d6eeff4ce2 Fixes issue #1995 - Missing DB index on article columns queried by ticket waiting time report backend. 2018-05-08 14:22:59 +02:00
Martin Edenhofer
ba57b907b4 Fixes issue #1996 - Use of wrong recipient if sender is also an agent (verify all recipient in to of article of there is a local address somewhere - not only the first one). 2018-05-07 17:25:58 +03:00
Thorsten Eckel
8834f3f526 Fixed URL required for mocking X-On-Behalf-Of header. 2018-05-03 10:10:07 +02:00
Thorsten Eckel
5fc7c2ea3c Fixed flanky test by using same value for check as for stored instead of dynamic. 2018-04-30 09:28:18 +02:00
Martin Edenhofer
23a480d2a6 Fixes issue #1980 - ObjectManager: New select attributes with empty selection will lead block UI. 2018-04-26 23:27:58 +02:00
Martin Edenhofer
7e3a10af26 Updated test with new realtime data. 2018-04-25 17:23:42 +02:00
Martin Edenhofer
15ae7aedcb Fixes #1974 - If fingerprint is an integer background job for verifying will fail. 2018-04-25 17:21:36 +02:00
Martin Edenhofer
330d9f1173 Fixed issue #1669 - Ticket templates disappear after logout/login in same browser session. 2018-04-23 15:26:11 +02:00
Thorsten Eckel
ee551209d4 Fixed issue #1969: Password reset: No such file or directory @ rb_sysopen - app/views/mailer/application_wrapper.html.erb . 2018-04-23 09:00:10 +02:00
Thorsten Eckel
0086c46e56 Fixed issue #1332: CalendarimportSync-Errors in Outlook 2016:
As described in https://github.com/icalendar/icalendar/issues/116
2018-04-22 20:16:02 +02:00
Thorsten Eckel
3890e3b4eb Fixed issue of unnecessary "Unable to get asset for 'article':..." logging. 2018-04-20 13:50:14 +02:00
Thorsten Eckel
e2664a647c Fixed issue #1642: Incorrect redirect_uri during OAuth2 access token request breaks integrations. 2018-04-20 11:18:53 +02:00
Ryan Lue
426fce7160 Add wait before retrying failed jobs (Fix #1950) 2018-04-19 16:28:00 +08:00
Muhammad Nuzaihan
020611f671 improve on icinga detection of recovery alerts for autoclose function #1907 2018-04-16 23:47:58 +08:00
Muhammad Nuzaihan
ed1b6f3b6f improve on detection of mime-type via Store object. fixes #1919 2018-04-16 23:30:51 +08:00
Martin Edenhofer
0640aa2222 Follow up fix for issue #1952 - New ticket attributes is also show for customers (which should not). 2018-04-16 02:43:49 +02:00
Martin Edenhofer
ba06ec871d Fixed issue #1952 - New ticket attributes is also show for customers (which should not). 2018-04-16 02:42:56 +02:00
Thorsten Eckel
9a64a36cd0 Fixed bug: Changes to Setting 'html_email_css_font' are only reflected after restarting Zammad. 2018-04-13 13:43:35 +02:00
Martin Edenhofer
41c091bd78 Fixed issue #1933 - Can't find organization anymore after index rebuild. 2018-04-13 09:15:40 +02:00
Martin Edenhofer
3cf1217bea Updated md5 sum of delivered avatar images. 2018-04-12 14:31:53 +02:00
Martin Edenhofer
48e44e92b5 Fixed issue #1337 - "Signin detected from a new device" Spam. 2018-04-12 13:24:50 +02:00
Martin Edenhofer
29e2749dd3 Fixed issue #1920 - Sometimes notifications are note marked as seen if somebody else already closed the ticketed. 2018-04-10 13:49:25 +02:00
Martin Edenhofer
e30a000504 Log message if of every processed email (to have a better debugging possibility in general). 2018-04-10 13:48:09 +02:00
Thorsten Eckel
ef4ab4e79d Fixed issue #1936: Generated chat widget code misses required option chatId. 2018-04-10 11:57:12 +02:00
Martin Edenhofer
dea0f28755 Fixed issue #1923 - Pulldowns are not sorted currently. 2018-04-05 06:13:37 +02:00
Martin Edenhofer
399c166e50 Allow postmaster filter and signatures to clone. 2018-04-05 06:13:20 +02:00
Thorsten Eckel
f6002bf8cb Follow up 8c5f87c9d4: Improved detection of ES attachment plugin error and output message. 2018-04-04 18:23:03 +02:00
Martin Edenhofer
53de7302e7 Fixed issue #1917. Only images in new note are lost on submit. 2018-04-04 10:56:42 +02:00
Thorsten Eckel
4423b44a13 Fixed issue #1905: Stored Exchange remote_id in login causes sync to update wrong user. 2018-04-03 17:52:31 +02:00
Thorsten Eckel
0a91e6dba3 Refactoring: Provide of :mapped is already handled in ::Sequencer::Unit::Import::Common::Mapping::Mixin::ProvideMapped. 2018-04-03 17:52:23 +02:00
Thorsten Eckel
d8ee543c0d Fixed issue #1660 - ObjectManager Attribute of type Tree-Select mixes up children. 2018-04-03 17:52:12 +02:00
Thorsten Eckel
3a822db8ae Improved RSpec system_init_done helper for DB migrations
Since DB migrations usually always check for system_init_done this is the new default behavior for all examples in a `type: :db_migration` spec. The default state is that Zammad is already initialized. This can be changed by passing `false` as a value to the meta tag `system_init_done`. See `spec/support/db_migration.rb` for more information.
2018-04-03 17:51:22 +02:00
Thorsten Eckel
e7f4f831be Follow up - Fixed issue #1838 - Follow up issue #1752: Unable to migrate old object manager attribute fields. 2018-04-03 17:51:14 +02:00
Thorsten Eckel
3849e04b54 Fixed issue #1913 and #1914: Given template parameters for rendering ticket create view are not displayed. 2018-04-03 17:10:03 +02:00
Martin Edenhofer
c63ad1ff50 Followup fix for issue #1752 - tree got lost after update. 2018-03-29 03:08:44 +02:00
Martin Edenhofer
4c615a968f Updated for 2.4.0. 2018-03-29 00:22:28 +02:00
Martin Edenhofer
b1ab4d6eb9 Merge branch 'develop' 2018-03-28 12:25:45 +02:00
Martin Edenhofer
51247a305f Merge branch 'develop' 2018-03-28 09:27:33 +02:00
Martin Edenhofer
ee1353113f Merge branch 'develop' 2018-03-06 08:24:10 +01:00
Martin Edenhofer
fe89a3a960 Merge branch 'develop' 2018-02-02 21:12:38 +01:00
Martin Edenhofer
5f504fab9c Merge branch 'develop' 2018-01-31 14:13:45 +01:00
Martin Edenhofer
db5d4de8ad Merge branch 'develop' 2018-01-30 01:26:30 +01:00
Martin Edenhofer
efd4c38f99 Merge branch 'develop' 2018-01-30 00:35:17 +01:00
Martin Edenhofer
d13ff15956 Merge branch 'develop' 2017-12-20 10:56:40 +01:00
Martin Edenhofer
d4c5d81373 Merge branch 'develop' 2017-12-06 17:10:31 +01:00
Martin Edenhofer
66c0975877 Merge branch 'develop' 2017-12-06 16:51:16 +01:00
Martin Edenhofer
76efb008a8 Merge branch 'develop' 2017-12-06 15:39:41 +01:00
Martin Edenhofer
abc0541244 Merge branch 'develop' 2017-12-06 09:56:14 +01:00
Martin Edenhofer
c9f8022f83 Merge branch 'develop' 2017-11-30 11:33:56 +01:00
Martin Edenhofer
dfc757968e Merge branch 'develop' 2017-11-30 09:32:27 +01:00
Martin Edenhofer
52bc45c0f8 Merge branch 'develop' 2017-11-20 12:03:21 +01:00
Martin Edenhofer
cbc8ba5467 Merge branch 'develop' 2017-11-14 23:03:43 +01:00
Martin Edenhofer
5f071e7d7f Merge branch 'develop' 2017-11-07 08:19:41 +01:00
Martin Edenhofer
1dcd6a243c Merge branch 'develop' 2017-11-06 03:03:07 +01:00
Martin Edenhofer
679deb1cf3 Merge branch 'develop' 2017-11-03 08:14:33 +01:00
Martin Edenhofer
6f6ac44978 Merge branch 'develop' 2017-10-30 13:35:44 +01:00
Martin Edenhofer
0bb341cb7e Merge branch 'develop' 2017-10-25 14:05:59 +02:00
Martin Edenhofer
6c122e046b Merge branch 'develop' 2017-10-25 10:15:04 +02:00
Martin Edenhofer
57c2dad0a3 Merge branch 'develop' 2017-10-23 03:19:56 +02:00
Martin Edenhofer
faefd529b4 Merge branch 'develop' 2017-10-09 02:27:20 +02:00
Martin Edenhofer
1481a12035 Merge branch 'develop' 2017-10-09 01:37:42 +02:00
Martin Edenhofer
2bf0635e0d Merge branch 'develop' 2017-09-27 11:02:30 +02:00
Martin Edenhofer
2211274bc5 Merge branch 'develop' 2017-09-12 16:39:39 +02:00
Martin Edenhofer
c229c42838 Merge branch 'develop' 2017-09-11 06:45:49 +02:00
Martin Edenhofer
a3c71f6558 Merge branch 'develop' 2017-09-10 19:43:13 +02:00
Thorsten Eckel
db8b85f767 Merge branch 'develop' 2017-09-06 15:01:15 +02:00
Martin Edenhofer
f1664c3841 Merge branch 'develop' 2017-09-05 21:54:52 +02:00
Martin Edenhofer
151c298056 Merge branch 'develop' 2017-08-31 09:05:09 +02:00
Martin Edenhofer
38f51ce6b2 Merge branch 'develop' 2017-08-28 11:44:51 +02:00
Martin Edenhofer
434f3e963e Merge branch 'develop' 2017-08-25 05:38:22 +02:00
Martin Edenhofer
b853dd1597 Merge branch 'develop' 2017-08-24 01:00:34 +02:00
Martin Edenhofer
3b5b6f612f Merge branch 'develop' 2017-08-23 17:16:20 +02:00
Martin Edenhofer
962043c520 Merge branch 'develop' 2017-08-22 14:58:32 +02:00
Martin Edenhofer
8394be87ed Merge branch 'develop' 2017-08-14 11:32:42 +02:00
Martin Edenhofer
5b547ceb3c Merge branch 'develop' 2017-08-13 11:55:41 +02:00
Martin Edenhofer
789ee08485 Merge branch 'develop' 2017-08-11 12:08:58 +02:00
Martin Edenhofer
490937859a Merge branch 'develop' 2017-07-29 13:03:58 +02:00
Martin Edenhofer
dfc306a2ac Merge branch 'develop' 2017-07-28 13:57:37 +02:00
Martin Edenhofer
dea06015a1 Merge branch 'develop' 2017-07-28 11:48:50 +02:00
Martin Edenhofer
2711e3392d Merge branch 'develop' 2017-07-27 06:38:29 +02:00
Martin Edenhofer
f5e1fe0c2f Merge branch 'develop' 2017-07-24 15:26:00 +02:00
Martin Edenhofer
dbbb4ac7a1 Merge branch 'develop' 2017-07-21 15:09:34 +02:00
Martin Edenhofer
8ec2ffdde1 Merge branch 'develop' 2017-07-20 17:22:54 +02:00
Martin Edenhofer
b76664e7ae Merge branch 'develop' 2017-07-20 15:58:47 +02:00
Martin Edenhofer
704c537749 Merge branch 'develop' 2017-07-20 15:00:37 +02:00
Martin Edenhofer
633c99bb72 Merge branch 'develop' 2017-07-19 11:07:56 +02:00
Martin Edenhofer
44051d8023 Merge branch 'develop' 2017-07-16 18:15:24 +02:00
Martin Edenhofer
50dc143c1b Merge branch 'develop' 2017-07-14 15:58:16 +02:00
Martin Edenhofer
6d44275869 Merge branch 'develop' 2017-07-13 20:53:59 +02:00
Martin Edenhofer
b1b3bf6d3c Merge branch 'develop' 2017-07-12 13:30:01 +02:00
Martin Edenhofer
77081ecd4d Merge branch 'develop' 2017-07-04 16:05:04 +02:00
Martin Edenhofer
b823b960d8 Merge branch 'develop' 2017-06-30 08:32:40 +02:00
Martin Edenhofer
e08f634125 Merge branch 'develop' 2017-06-21 09:10:41 +02:00
Martin Edenhofer
ab5e8d9338 Merge branch 'develop' 2017-06-19 20:01:15 +02:00
Martin Edenhofer
9a29ea8f14 Merge branch 'develop' 2017-06-13 02:40:32 +02:00
Martin Edenhofer
cf87e00b21 Merge branch 'develop' 2017-06-13 01:57:29 +02:00
Martin Edenhofer
6233b0a9ec Merge branch 'develop' 2017-06-01 18:32:42 +02:00
Martin Edenhofer
04b1bc638c Merge branch 'develop' 2017-05-29 06:57:52 +02:00
Martin Edenhofer
a78938f7c2 Merge branch 'develop' 2017-05-23 11:05:51 +02:00
Martin Edenhofer
1d23308454 Merge branch 'develop' 2017-05-23 10:30:03 +02:00
Martin Edenhofer
6bc9c00ab8 Merge branch 'develop' 2017-05-16 15:41:06 +02:00
Martin Edenhofer
bc131742be Merge branch 'develop' 2017-05-16 14:22:22 +02:00
Martin Edenhofer
5a12aa0167 Merge branch 'develop' 2017-05-16 01:47:49 +02:00
Martin Edenhofer
b3ab9d0055 Merge branch 'develop' 2017-05-16 00:57:14 +02:00
Martin Edenhofer
1bde8f91bc Merge branch 'develop' 2017-05-15 15:31:15 +02:00
Martin Edenhofer
47ef1b7e5d Merge branch 'develop' 2017-05-11 23:54:32 +02:00
Martin Edenhofer
88c7816f5c Merge branch 'develop' 2017-05-08 17:09:55 +02:00
Martin Edenhofer
82339c6c18 Merge branch 'develop' 2017-05-08 16:56:10 +02:00
Martin Edenhofer
04a609c3a8 Merge branch 'develop' 2017-05-05 15:23:04 +02:00
Martin Edenhofer
60430139aa Merge branch 'develop' 2017-05-02 10:45:06 +02:00
Martin Edenhofer
7d1c655ee3 Merge branch 'develop' 2017-04-28 16:50:31 +02:00
Martin Edenhofer
e34c1c9b79 Merge branch 'develop' 2017-04-28 12:48:40 +02:00
Thorsten Eckel
3c430b35ef Fixed issue #906 - Doorkeeper tokens should be refreshable but aren't. 2017-04-24 13:58:03 +02:00
Rolf Schmidt
7ad3c70311 Fixed issue #975 - Exception in OTRS Import. 2017-04-24 11:58:07 +02:00
Martin Edenhofer
c270bc1e67 Merge branch 'develop' 2017-04-21 14:36:50 +02:00
Thorsten Eckel
3d19bfdcf7 Fixed issue #912 - Long Twitter direct messages are shown as link after 140 chars. 2017-04-21 13:58:33 +02:00
Martin Edenhofer
dc4a55cc5c Merge branch 'develop' 2017-04-21 12:38:25 +02:00
Martin Edenhofer
6e8886277d Merge branch 'develop' 2017-04-21 06:17:09 +02:00
Martin Edenhofer
74e96e92b2 Merge branch 'develop' 2017-04-20 00:22:13 +02:00
Martin Edenhofer
287b1f0d01 Merge branch 'develop' 2017-04-18 09:57:19 +02:00
Martin Edenhofer
feee46e29b Merge branch 'develop' 2017-04-08 08:32:45 +02:00
Martin Edenhofer
991efe36e4 Merge branch 'develop' 2017-04-05 22:23:05 +02:00
Martin Edenhofer
a2aacb20fa Merge branch 'develop' 2017-03-31 16:20:21 +02:00
Martin Edenhofer
592e978062 Merge branch 'develop' 2017-03-31 14:01:20 +02:00
Martin Edenhofer
a4769c8bc7 Merge branch 'develop' 2017-03-27 14:49:54 +02:00
Martin Edenhofer
d1378d53f1 Merge branch 'develop' 2017-03-25 14:44:10 +01:00
Martin Edenhofer
f0b91d3269 Merge branch 'develop' 2017-03-23 21:10:03 +01:00
Martin Edenhofer
48c77dd688 Merge branch 'develop' 2017-03-21 16:06:51 +01:00
Martin Edenhofer
d86d904395 Merge branch 'develop' 2017-03-21 10:05:58 +01:00
Martin Edenhofer
5efc455f0c Merge branch 'develop' 2017-03-18 23:16:57 +01:00
Martin Edenhofer
be3bbeba20 Merged develop into master. 2017-03-18 13:45:42 +01:00
Martin Edenhofer
31ab1b614c Merge branch 'develop' 2017-03-17 08:09:20 +01:00
Martin Edenhofer
4bc5fa4693 Merge branch 'develop' 2017-03-14 19:24:36 +01:00
Martin Edenhofer
418a64f401 Merge branch 'develop' 2017-03-14 13:33:37 +01:00
Martin Edenhofer
abe46270a6 Merge branch 'develop' 2017-03-03 10:54:22 +01:00
Martin Edenhofer
1249a77150 Merge branch 'develop' 2017-03-03 09:46:01 +01:00
Martin Edenhofer
cfa6b9ed16 Merge branch 'develop' 2017-02-24 23:19:06 +01:00
Rolf Schmidt
aa2303f999 Merge branch 'develop' 2017-02-24 09:27:43 +01:00
Martin Edenhofer
d5616684f8 Merge branch 'develop' 2017-02-22 07:12:36 +01:00
Martin Edenhofer
0f75ba57bd Merge branch 'develop' 2017-02-15 17:42:20 +01:00
Martin Edenhofer
c83675bbba Merge branch 'develop' 2017-02-15 13:39:29 +01:00
Martin Edenhofer
17d16d405c Merge branch 'develop' 2017-02-06 11:48:59 +01:00
Martin Edenhofer
76795ca302 Merge branch 'develop' 2017-02-06 00:16:24 +01:00
Martin Edenhofer
5d145bd09f Merge branch 'develop' 2017-02-05 19:26:16 +01:00
Martin Edenhofer
82471552a8 Merge branch 'develop' 2017-02-01 23:24:18 +01:00
Rolf Schmidt
dc60574129 Merge branch 'develop' 2017-01-23 15:05:52 +01:00
Martin Edenhofer
d33a96dee2 Merge branch 'develop' 2017-01-19 06:18:19 +01:00
Martin Edenhofer
0098d4ab82 Merge branch 'develop' 2017-01-17 15:57:42 +01:00
Martin Edenhofer
bb03b5eab8 Merge branch 'develop' 2017-01-17 12:41:03 +01:00
Martin Edenhofer
17eb0f84ec Merge branch 'develop' 2017-01-17 08:56:18 +01:00
Martin Edenhofer
56d0523c1f Merge branch 'develop' 2017-01-04 15:21:24 +01:00
Martin Edenhofer
9fa18758bc Merge branch 'develop' 2016-12-21 14:12:06 +01:00
Martin Edenhofer
d2f2c55b06 Merge branch 'develop' 2016-12-21 08:50:52 +01:00
Martin Edenhofer
f4a409bd6e Merge branch 'develop' 2016-12-21 00:13:59 +01:00
Martin Edenhofer
971b724d4c Merge branch 'develop' 2016-12-18 12:45:13 +01:00
Martin Edenhofer
3263c6f2ba Merge branch 'develop' 2016-12-16 15:49:33 +01:00
Martin Edenhofer
359b026535 Merge branch 'develop' 2016-12-15 17:16:38 +01:00
Martin Edenhofer
c63518fd3e Merge branch 'develop' 2016-12-14 16:06:09 +01:00
Martin Edenhofer
24ba3cff44 Merge branch 'develop' 2016-12-06 13:29:23 +01:00
Martin Edenhofer
f7066e1995 Merge branch 'develop' 2016-12-06 12:23:13 +01:00
Martin Edenhofer
6844499309 Merge branch 'develop' 2016-11-25 16:59:16 +01:00
Martin Edenhofer
afa3e47eab Merge branch 'develop' 2016-11-25 14:22:00 +01:00
Martin Edenhofer
5f3e9298f1 Merge branch 'develop' 2016-11-24 16:03:45 +01:00
Martin Edenhofer
e4dd303357 Merge branch 'develop' 2016-11-21 17:07:30 +01:00
Martin Edenhofer
2fa06eee3d Merge branch 'develop' 2016-11-21 08:58:54 +01:00
Martin Edenhofer
dc0ac03334 Merge branch 'develop' 2016-11-19 16:42:10 +01:00
Martin Edenhofer
c5a1d69ec5 Merge branch 'develop' 2016-11-19 16:35:07 +01:00
Martin Edenhofer
9891981f92 Merge branch 'develop' 2016-11-17 15:12:22 +01:00
Martin Edenhofer
2c721f71e7 Merge branch 'develop' 2016-11-17 10:03:34 +01:00
Martin Edenhofer
0b4a4be0b7 Merge branch 'develop' 2016-11-16 23:21:05 +01:00
Martin Edenhofer
b13bb2fcaf Added change log. 2016-11-14 08:37:51 +01:00
Martin Edenhofer
d9ba369fcf Merge branch 'develop' 2016-11-14 08:36:58 +01:00
Martin Edenhofer
3adece0db5 Merge branch 'develop' 2016-11-14 00:03:57 +01:00
Martin Edenhofer
7101ffc277 Merge branch 'develop' 2016-11-12 16:44:43 +01:00
Martin Edenhofer
19a7a439b8 Merge branch 'develop' 2016-11-11 19:29:56 +01:00
Martin Edenhofer
e728320241 Merge branch 'develop' 2016-10-30 07:08:08 +01:00
Martin Edenhofer
1bc17afc53 Merge branch 'master' of git.znuny.com:zammad/zammad 2016-10-30 07:07:39 +01:00
Thorsten Eckel
ee5c58f53a Fixed bug: Zip archive doesn't contain sub folder content. 2016-10-19 08:25:51 +02:00
Martin Edenhofer
9bdf6d7b81 Merge branch 'develop' 2016-10-19 07:50:08 +02:00
Thorsten Eckel
5754fdccdd Working on CI tag/branch pushing, syncing, deployment issue. 2016-10-19 07:32:05 +02:00
Thorsten Eckel
4901050d57 Working on issue not syncing to github correctly. 2016-10-19 07:25:18 +02:00
Martin Edenhofer
7980c8cb93 Merge branch 'develop' 2016-10-19 07:21:10 +02:00
Martin Edenhofer
7e7088388d Merge branch 'develop' 2016-10-17 16:43:54 +02:00
Martin Edenhofer
2f6e52d63d Merge branch 'develop' 2016-09-28 16:08:28 +02:00
Martin Edenhofer
bde8babb43 Merge branch 'develop' 2016-09-28 08:53:02 +02:00
Martin Edenhofer
d6d68fb363 Merge branch 'develop' 2016-09-19 07:47:06 +02:00
Martin Edenhofer
814ffed0f0 Merge branch 'develop' 2016-09-15 22:11:57 +02:00
Thorsten Eckel
b013fb8cbc Merge branch 'private-otrs_import_df' 2016-09-13 12:06:56 +02:00
Martin Edenhofer
fbd8e63592 Merge branch 'develop' 2016-09-13 08:55:38 +02:00
Martin Edenhofer
e8ad3965d7 Removed build badged. 2016-09-10 16:47:33 +02:00
Martin Edenhofer
e00981ee23 Merge branch 'master' of git.znuny.com:zammad/zammad 2016-09-10 00:05:18 +02:00
Martin Edenhofer
4e74c6beda Merge branch 'develop' 2016-09-10 00:04:28 +02:00
Thorsten Eckel
2d09e556d7 Added deployment of archives. 2016-09-07 15:31:28 +02:00
Martin Edenhofer
8cd39398d6 Merge branch 'develop' 2016-09-06 16:07:33 +02:00
Martin Edenhofer
94c616ffe3 Merge branch 'develop' 2016-08-25 10:00:36 +02:00
Martin Edenhofer
57649bb888 Merge branch 'develop' 2016-08-23 09:49:40 +02:00
Martin Edenhofer
7e9442ec4a Merge branch 'develop' 2016-08-21 21:38:00 +02:00
Martin Edenhofer
521e6265fd Merge branch 'develop' 2016-08-19 13:58:26 +02:00
Martin Edenhofer
a42a9689fc Merge branch 'develop' 2016-08-19 13:48:46 +02:00
Martin Edenhofer
d9ffb8001d Merge branch 'develop' 2016-07-28 13:39:08 +02:00
Martin Edenhofer
ec86d26ae6 Merge branch 'develop' 2016-07-24 23:52:04 +02:00
Martin Edenhofer
e00efc821a Merge branch 'develop' 2016-07-18 19:35:51 +02:00
Martin Edenhofer
f04be8ca93 Merge branch 'develop' 2016-07-12 20:50:32 +02:00
Martin Edenhofer
c7ff6a323a Merge branch 'develop' 2016-07-08 11:28:43 +02:00
Martin Edenhofer
34833416bc Merge branch 'develop' 2016-06-30 09:50:09 +02:00
Martin Edenhofer
d7a6e28d81 Merge branch 'develop' 2016-06-29 00:18:44 +02:00
Martin Edenhofer
c13531d720 Merge branch 'develop' 2016-06-27 11:28:22 +02:00
Martin Edenhofer
4a40cced7d Merge branch 'develop' 2016-06-26 08:07:04 +02:00
Martin Edenhofer
450de10cc7 Merge branch 'develop' 2016-06-23 08:24:56 +02:00
Martin Edenhofer
5e587a455c Merge branch 'develop' 2016-06-22 08:54:24 +02:00
Martin Edenhofer
04aef9b258 Merge branch 'develop' 2016-06-20 15:52:43 +02:00
Martin Edenhofer
9b72557d13 Merge branch 'develop' 2016-06-15 06:58:36 +02:00
Martin Edenhofer
a5e0bc30c1 Merge branch 'develop' 2016-06-09 13:40:27 +02:00
Martin Edenhofer
458c166e9f Merge branch 'develop' 2016-06-08 16:43:29 +02:00
Martin Edenhofer
3373e5d141 Merge branch 'develop' 2016-06-08 10:48:12 +02:00
Martin Edenhofer
8f6b8de617 Merge branch 'develop' 2016-06-08 07:42:19 +02:00
Martin Edenhofer
22b21a82e7 Merge branch 'develop' 2016-06-07 07:02:25 +02:00
Martin Edenhofer
b4151b299f Merge branch 'develop' 2016-06-04 02:23:14 +02:00
Martin Edenhofer
e969fca8c4 Merge branch 'develop' 2016-06-03 07:08:59 +02:00
Martin Edenhofer
e59993a5d6 Merge branch 'develop' 2016-05-31 08:31:56 +02:00
Martin Edenhofer
3adc9d4cf1 Merge branch 'develop' 2016-05-30 13:21:46 +02:00
Martin Edenhofer
c1153250cb Merge branch 'develop' 2016-05-30 00:00:52 +02:00
Martin Edenhofer
7718dc28d3 Merge branch 'develop'
# Conflicts:
#	README.md
2016-05-27 17:30:22 +02:00
Martin Edenhofer
1894580ae6 Updated badge url. 2016-05-27 17:28:26 +02:00
Martin Edenhofer
11df714bbf Merge branch 'develop' 2016-05-20 15:18:36 +02:00
Martin Edenhofer
72adee37a8 Merge branch 'develop' 2016-05-20 06:10:19 +02:00
Martin Edenhofer
0e8f460d73 Merge branch 'develop' 2016-05-06 16:25:09 +02:00
Martin Edenhofer
4e62d5d0d2 Merge branch 'develop' 2016-05-06 09:40:16 +02:00
Martin Edenhofer
004e5ba457 Merge branch 'develop' 2016-05-04 12:13:34 +02:00
Martin Edenhofer
f1a8a8da6f Merge branch 'develop' 2016-05-03 16:01:03 +02:00
Martin Edenhofer
bfb44d1189 Merge branch 'develop' 2016-05-01 14:18:26 +02:00
Martin Edenhofer
d64afd8d97 Merge branch 'develop' 2016-04-14 17:47:03 +02:00
Martin Edenhofer
97cfdb9c4c Merge branch 'develop' 2016-04-12 08:25:06 +02:00
Martin Edenhofer
bb336f63f1 Merge branch 'develop' 2016-03-25 18:23:12 +01:00
Martin Edenhofer
80da08f447 Merge branch 'develop' 2016-03-23 09:31:02 +01:00
Martin Edenhofer
a6db2b7c90 Merge branch 'develop' 2016-03-17 17:45:04 +01:00
Martin Edenhofer
e1c3f83324 Merge branch 'develop' 2016-03-17 10:41:20 +01:00
Martin Edenhofer
8545a44ad1 Merge branch 'develop' 2016-03-16 11:17:22 +01:00
Martin Edenhofer
cfbe7510a2 Merge branch 'develop' 2016-03-16 09:10:05 +01:00
Martin Edenhofer
1479c32edb Merge branch 'develop' 2016-03-15 00:34:36 +01:00
Martin Edenhofer
a9d428bf5a Merge branch 'develop' 2016-03-11 10:38:15 +01:00
Martin Edenhofer
3a62eb11da Merge branch 'develop' 2016-03-04 07:07:55 +01:00
Martin Edenhofer
2df5f81029 Merge branch 'develop' 2016-03-03 03:42:27 +01:00
Martin Edenhofer
b88c23bc92 Merge branch 'develop' 2016-02-28 00:16:49 +01:00
Martin Edenhofer
4144fbaf5b Merge branch 'develop' 2016-02-25 23:46:10 +01:00
Martin Edenhofer
86ddc93f52 Merge branch 'develop' 2016-02-23 14:29:01 +01:00
Martin Edenhofer
8a1b583535 Merge branch 'develop' 2016-02-23 01:38:39 +01:00
Martin Edenhofer
7623628b86 Merge branch 'develop' 2016-02-12 08:53:57 +01:00
Martin Edenhofer
f753bc203f Merge branch 'develop' 2016-02-11 13:25:39 +01:00
Martin Edenhofer
ff9ba10705 Merge branch 'develop' 2016-02-11 02:11:06 +01:00
Martin Edenhofer
fdc25bdc26 Merge branch 'develop' 2016-02-09 01:36:03 +01:00
Martin Edenhofer
d154a587ad Merge branch 'develop' 2016-02-04 12:55:30 +01:00
Martin Edenhofer
a9cc4f037b Merge branch 'develop' 2016-02-04 10:41:41 +01:00
Martin Edenhofer
50d6679189 Merge branch 'develop' 2016-02-03 13:19:51 +01:00
Martin Edenhofer
b82cf4e1d6 Merge branch 'develop' 2016-01-29 15:19:23 +01:00
Martin Edenhofer
fa6eaa6710 Merge branch 'develop'
# Conflicts:
#	README.md
2016-01-26 19:59:34 +01:00
Martin Edenhofer
2ca17ac1bd Updated build status url. 2016-01-26 13:57:24 +01:00
Martin Edenhofer
862ecd7d36 Merge branch 'develop' 2016-01-22 14:47:53 +01:00
Martin Edenhofer
e38b72593b Merge branch 'develop' 2016-01-12 11:14:24 +01:00
Martin Edenhofer
81883cae8f Revert "update terminal start script"
This reverts commit 63770c9cbf.
2016-01-12 11:13:40 +01:00
Felix Niklas
63770c9cbf update terminal start script 2016-01-12 10:58:46 +01:00
Martin Edenhofer
aaf5da55a0 Merge branch 'develop' 2015-12-28 11:33:58 +01:00
Martin Edenhofer
8455d9ae17 Merge branch 'develop' 2015-12-21 16:03:31 +01:00
Martin Edenhofer
839a8e8eb4 Merge branch 'develop' 2015-12-13 15:26:35 +01:00
Martin Edenhofer
7aa3f6eaee Updated build url. 2015-12-10 09:44:16 +01:00
Martin Edenhofer
219abc69a3 Merge branch 'develop' 2015-12-10 09:21:59 +01:00
Martin Edenhofer
595822c928 Merge branch 'develop' 2015-12-02 02:27:40 +01:00
Martin Edenhofer
32112937b1 Merge branch 'develop' 2015-12-02 01:41:10 +01:00
Martin Edenhofer
a0889c5b99 Merge branch 'develop' 2015-12-01 22:03:52 +01:00
Martin Edenhofer
4bc281bfb0 Merge branch 'develop' 2015-12-01 20:04:51 +01:00
Martin Edenhofer
e12ec607d7 Merge branch 'develop' 2015-11-30 15:11:25 +01:00
Martin Edenhofer
545f393293 Merge branch 'develop' 2015-11-30 01:24:11 +01:00
Martin Edenhofer
4efa152e93 Merge branch 'master' of github.com:martini/zammad 2015-11-24 03:39:17 +01:00
Martin Edenhofer
0ebda4f32a Merge branch 'develop'
Conflicts:
	README.md
	script/websocket-server.rb
2015-11-24 03:38:55 +01:00
Martin Edenhofer
b94f54799f Merge branch 'develop' of http://ci.lab.zammad.com/core/zammad 2015-11-24 03:07:38 +01:00
Martin Edenhofer
99f0b4e2e2 Merge branch 'develop' 2015-11-19 14:27:39 +01:00
Martin Edenhofer
53cb906733 Merge branch 'develop' 2015-11-01 01:49:02 +01:00
Martin Edenhofer
2c31fe8a84 Merge branch 'develop' 2015-10-31 23:40:10 +01:00
Martin Edenhofer
6c5e673a2c Merge branch 'develop' 2015-10-02 15:31:54 +02:00
Martin Edenhofer
574b891a33 Merge branch 'develop' 2015-09-28 02:12:31 +02:00
Martin Edenhofer
9ef1d3f58b Merge branch 'develop'
Conflicts:
	extras/zammad.spec
2015-09-28 01:24:03 +02:00
Martin Edenhofer
8b3f432746 Merge branch 'develop' 2015-09-01 03:15:03 +02:00
Martin Edenhofer
14a9df261e Merge branch 'develop' 2015-08-31 15:11:27 +02:00
Martin Edenhofer
5620a2847b Merge branch 'develop' 2015-08-31 12:42:04 +02:00
Martin Edenhofer
13aaae3835 Merge branch 'develop' 2015-08-30 09:19:02 +02:00
Martin Edenhofer
2d4fcd3a4f Merge branch 'develop' 2015-08-30 08:22:27 +02:00
Martin Edenhofer
f4f70b0bc2 Merge branch 'develop' 2015-08-30 01:36:18 +02:00
Martin Edenhofer
1d279bb88f Merge branch 'develop' 2015-08-30 01:32:10 +02:00
Martin Edenhofer
fe4757cbf9 Merge branch 'develop' 2015-08-21 17:03:28 +02:00
me
1d476f163e Use also mysql on test. 2015-08-21 16:53:38 +02:00
Martin Edenhofer
513701b6d2 Merge branch 'develop'
Conflicts:
	README.md
	script/websocket-server.rb
2015-08-11 15:17:14 +02:00
Thorsten Eckel
1a1b661714 Fixed typo. 2015-07-16 09:50:18 +02:00
Roy Kaldung
6081a98279 Update README.md 2015-01-12 12:39:26 +01:00
Roy Kaldung
157991c33d Update README.md 2015-01-12 12:38:11 +01:00
Roy Kaldung
ef67be9170 We like to know if it's starting or stopping 2014-11-06 17:24:45 +01:00
Martin Edenhofer
39f3819303 Merge branch 'develop' 2014-11-06 16:13:39 +01:00
Martin Edenhofer
7a676bd563 Merge branch 'develop' into 'master', goto next level.
Conflicts:
	Gemfile
	README.md
	extras/zammad.spec
2014-10-05 20:42:39 +02:00
Roy Kaldung
80c54daa03 Delete httpd.spec 2014-05-02 12:47:51 +02:00
Roy Kaldung
99c88cde62 Delete apr.spec 2014-05-02 12:47:41 +02:00
Roy Kaldung
17d113ca20 removed old file 2014-05-02 12:47:18 +02:00
Martin Edenhofer
2fbf4324a7 Update Gemfile 2014-02-22 14:46:25 +01:00
Roy Kaldung
b46c390d30 minor change to test GitHub hook (automatic deploy) 2013-12-04 18:22:21 +01:00
Martin Edenhofer
3c3651bc52 Merge branch 'develop'
Conflicts:
	app/controllers/tickets_controller.rb
2013-09-20 13:52:53 +02:00
Roy Kaldung
abeeb5e66d Updated Build Status image URL 2013-08-23 21:00:55 +02:00
rkaldung
09d61ce4c8 added dependency 2013-08-14 20:38:09 +02:00
rkaldung
cc1c4495b2 Merge branch 'master' of github.com:martini/zammad 2013-08-14 20:27:15 +02:00
rkaldung
7b1ec4d17f modified install section 2013-08-14 20:26:25 +02:00
rkaldung
99b3201ccf added initial spec for rpm 2013-08-14 20:20:55 +02:00
Roy Kaldung
d4205d9ba6 removed Windows ;-) 2013-08-14 20:08:23 +02:00
Roy Kaldung
8d57633390 minor changes on comments 2013-08-05 19:18:52 +02:00
101 changed files with 3230 additions and 532 deletions

3
.gitignore vendored
View File

@@ -29,6 +29,9 @@
/config/locales*.yml
/config/translations/*.yml
# Ignore generated documentation
/doc
# Ignore coverage stuff
/coverage

View File

@@ -51,7 +51,7 @@ test:rspec:mysql:
- rake db:create
- rake db:migrate
- rake db:seed
- rspec
- bundle exec rspec
- rake db:drop
test:rspec:postgresql:
@@ -64,7 +64,7 @@ test:rspec:postgresql:
- rake db:create
- rake db:migrate
- rake db:seed
- rspec
- bundle exec rspec
- rake db:drop
test:unit:mysql:
@@ -480,8 +480,8 @@ test:browser:integration:api_client_ruby:
- git clone git@github.com:zammad/zammad-api-client-ruby.git || script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 1 1
- cd zammad-api-client-ruby
- bundle install --jobs 8
- export TEST_URL=http://$IP:$BROWSER_PORT
- rspec || (cd .. && script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 1 1)
- export TEST_URL=http://$IP:$BROWSER_PORT/
- bundle exec rspec || (cd .. && script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 1 1)
- cd .. && script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 0 1
test:browser:integration:api_client_php:

View File

@@ -28,6 +28,12 @@ targets:
- elasticsearch
- nginx|apache2
- postgresql|mysql-server|mariadb-server|sqlite
ubuntu-18.04:
dependencies:
- curl
- elasticsearch
- nginx|apache2
- postgresql|mysql-server|mariadb-server|sqlite
sles-12:
dependencies:
- curl

View File

@@ -1,16 +1,71 @@
# Change Log
## [2.4.0](https://github.com/zammad/zammad/tree/2.4.0) (2018-xx-xx)
## [2.4.1](https://github.com/zammad/zammad/tree/2.4.1) (2018-06-05)
[Full Changelog](https://github.com/zammad/zammad/compare/2.4.0...2.4.1)
**Implemented enhancements:**
- Missing CC autocomplete / type ahead function in ticket create screen \(\#1018286\) [\#1990](https://github.com/zammad/zammad/issues/1990) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
**Fixed bugs:**
- "Create new Customer" form delayed display [\#1957](https://github.com/zammad/zammad/issues/1957) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Zendesk import: To/From extraction for system comments fails [\#2040](https://github.com/zammad/zammad/issues/2040) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Import fails for Object Attribute with non-ASCII name [\#2039](https://github.com/zammad/zammad/issues/2039) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Import job \(LDAP/Exchange\) hangs on scheduler restart [\#2014](https://github.com/zammad/zammad/issues/2014) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Scheduler destroys variables in text \(after a while\) [\#2001](https://github.com/zammad/zammad/issues/2001) [[bug](https://github.com/zammad/zammad/labels/bug)] [[notification](https://github.com/zammad/zammad/labels/notification)] [[trigger](https://github.com/zammad/zammad/labels/trigger)]
- Admin UI does not allow to select Timezone in "Calendars" options [\#1971](https://github.com/zammad/zammad/issues/1971) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Ticket waiting time report backend creates high CPU load with script/scheduler.rb [\#1995](https://github.com/zammad/zammad/issues/1995) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Use of wrong recipient if sender is also an agent [\#1996](https://github.com/zammad/zammad/issues/1996) [[bug](https://github.com/zammad/zammad/labels/bug)]
- ObjectManager: New select attributes with empty selection will lead block UI [\#1980](https://github.com/zammad/zammad/issues/1980) [[bug](https://github.com/zammad/zammad/labels/bug)]
- If fingerprint is an integer background job for verifying will fail [\#1974](https://github.com/zammad/zammad/issues/1974) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Ticket templates disappear after logout/login in same browser session [\#1669](https://github.com/zammad/zammad/issues/1669) [[bug](https://github.com/zammad/zammad/labels/bug)] [[ticket templates](https://github.com/zammad/zammad/labels/ticket%20templates)]
- CalendarimportSync-Errors in Outlook 2016 [\#1332](https://github.com/zammad/zammad/issues/1332) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Generic OAuth2 authentication: incorrect redirect\_uri during access token request, does not match authorization request due to query parameters being appended [\#1642](https://github.com/zammad/zammad/issues/1642) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Failing scheduler job retries to fast without any time offset [\#1950](https://github.com/zammad/zammad/issues/1950) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Use mime type of attachments if present for outgoing emails [\#1919](https://github.com/zammad/zammad/issues/1919) [[bug](https://github.com/zammad/zammad/labels/bug)]
- New ticket attributes is also show for customers \(which should not\). [\#1952](https://github.com/zammad/zammad/issues/1952) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Can't find organization anymore after index rebuild [\#1933](https://github.com/zammad/zammad/issues/1933) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Sometimes notifications are not marked as seen if somebody else already closed the ticket [\#1920](https://github.com/zammad/zammad/issues/1920) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Generated chat widget code misses required option chatId [\#1936](https://github.com/zammad/zammad/issues/1936) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Pulldowns are not sorted currently [\#1923](https://github.com/zammad/zammad/issues/1923) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Only images in new note are lost on submit [\#1917](https://github.com/zammad/zammad/issues/1917) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Stored Exchange remote\_id in login causes sync to update wrong user [\#1905](https://github.com/zammad/zammad/issues/1905) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Ticket-Property of type Tree-Select mixes up children [\#1660](https://github.com/zammad/zammad/issues/1660) [[bug](https://github.com/zammad/zammad/labels/bug)]
## [2.4.0](https://github.com/zammad/zammad/tree/2.4.0) (2018-03-29)
[Full Changelog](https://github.com/zammad/zammad/compare/2.3.0...2.4.0)
**Implemented enhancements:**
- Autocomplete Email-Address-Fields [\#338](https://github.com/zammad/zammad/issues/338) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
- API: Ticket creation - wrong user in notifcations, activity stream [\#1805](https://github.com/zammad/zammad/issues/1805) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
- Admin interface: Generic clone function for objects
- RichText: Adjustment of the image size which were inserted from the clipboard
- Admin users can be disabled by Agents [\#1794](https://github.com/zammad/zammad/issues/1794) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Failing Scheduler Delayed::Job - jobs are not listed in the monitoring result [\#1866](https://github.com/zammad/zammad/issues/1866) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
- Reset owner of ticket on followup if owner is invalid or has no access to group anymore. [\#1816](https://github.com/zammad/zammad/issues/1816) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
**Fixed bugs:**
- Clearing the entered value in a not required ticket field [\#1882](https://github.com/zammad/zammad/issues/1882)
- Pending till date not displayed correctly? [\#1716](https://github.com/zammad/zammad/issues/1716) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Login fails after fast logout right after previous login [\#1859](https://github.com/zammad/zammad/issues/1859) [[bug](https://github.com/zammad/zammad/labels/bug)]
- customer field in customer\_ticket\_new when customer is logged in [\#1856](https://github.com/zammad/zammad/issues/1856) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Direct access to organization tickets possible even though shared is deactivated [\#1857](https://github.com/zammad/zammad/issues/1857) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Can't create email-account with local MTA as outgoing server [\#1852](https://github.com/zammad/zammad/issues/1852) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Role - Group cant be unchecked [\#1488](https://github.com/zammad/zammad/issues/1488) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Can't un-link i-doit Objects in a Ticket [\#1733](https://github.com/zammad/zammad/issues/1733) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Issues in the rights-management [\#1810](https://github.com/zammad/zammad/issues/1810) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Sometimes pending time is show as first ticket attribute [\#949](https://github.com/zammad/zammad/issues/949) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Taskbar item race condition causes JS exception [\#1841](https://github.com/zammad/zammad/issues/1841) [[bug](https://github.com/zammad/zammad/labels/bug)]
- The column widths of a table are shifted after manual change and use of pagination. [\#1829](https://github.com/zammad/zammad/issues/1829) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Auto assignment of unassigned tickets at first open time [\#1825](https://github.com/zammad/zammad/issues/1825) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
- Boolean default option in object manager not selecting when it is already saved [\#1792](https://github.com/zammad/zammad/issues/1792)
- Ticket shown multiple times in overview [\#1769](https://github.com/zammad/zammad/issues/1769) [[bug](https://github.com/zammad/zammad/labels/bug)]
- JS error on creating new customer [\#1813](https://github.com/zammad/zammad/issues/1813) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Customer response via note does not trigger escalation [\#1322](https://github.com/zammad/zammad/issues/1322) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Reporting: profile condition is ignored [\#1809](https://github.com/zammad/zammad/issues/1809) [[bug](https://github.com/zammad/zammad/labels/bug)]
- LDAP authentication doen't work in 2.3.x [\#1795](https://github.com/zammad/zammad/issues/1795) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Missing create/add customer from ticket [\#1522](https://github.com/zammad/zammad/issues/1522) [[bug](https://github.com/zammad/zammad/labels/bug)]
- XSS issue in ticket overview [\#1869](https://github.com/zammad/zammad/issues/1869) [[bug](https://github.com/zammad/zammad/labels/bug)]
\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*

View File

@@ -676,6 +676,7 @@ class App.ControllerModal extends App.Controller
closeOnAnyClick: false
initalFormParams: {}
initalFormParamsIgnore: false
showTrySupport: false
showTryMax: 10
showTrydelay: 1000
@@ -722,7 +723,7 @@ class App.ControllerModal extends App.Controller
content = @contentInline
else
content = @content()
modal = $(App.view('modal')
modal = $(App.view('modal')(
head: @head
headPrefix: @headPrefix
message: @message
@@ -734,7 +735,7 @@ class App.ControllerModal extends App.Controller
buttonClass: @buttonClass
centerButtons: @centerButtons
leftButtons: @leftButtons
)
))
modal.find('.modal-body').html(content)
if !@initRenderingDone
@initRenderingDone = true
@@ -750,7 +751,7 @@ class App.ControllerModal extends App.Controller
@el
render: =>
if @modalAlreadyExists() && @showTryCount <= @showTryMax
if @showTrySupport is true && @modalAlreadyExists() && @showTryCount <= @showTryMax
@showDelayed()
return
@@ -827,7 +828,7 @@ class App.ControllerModal extends App.Controller
localOnClosed: (e) =>
@onClosed(e)
$('.modal').remove()
@el.modal('remove')
onClosed: (e) ->
# do nothing
@@ -851,6 +852,8 @@ class App.ControllerModal extends App.Controller
@onSubmit(e)
class App.SessionMessage extends App.ControllerModal
showTrySupport: true
onCancel: (e) =>
if @forceReload
@windowReload(e)

View File

@@ -443,6 +443,8 @@ class App.ControllerForm extends App.Controller
if item.type is 'boolean'
if value is ''
value = undefined
else if value is undefined
value = false
else if value is 'true'
value = true
else if value is 'false'
@@ -463,21 +465,31 @@ class App.ControllerForm extends App.Controller
# verify if we have not checked checkboxes
uncheckParam = {}
lookupForm.find('input[type=checkbox]').each( (index) ->
checked = $(@).attr('checked')
type = $(@).data('field-type')
checked = $(@).prop('checked')
name = $(@).attr('name')
if name && !checked && (!(name of param) || param[name] is '')
if name && !checked && !(name of param)
if !(name of uncheckParam)
uncheckParam[name] = undefined
if type is 'boolean'
uncheckParam[name] = false
else
uncheckParam[name] = undefined
else
uncheckParam[name] = []
true
)
# verify if we have not checked radios
lookupForm.find('input[type=radio]').each( (index) ->
checked = $(@).attr('checked')
type = $(@).data('field-type')
checked = $(@).prop('checked')
name = $(@).attr('name')
if name && !checked && !(name of param)
uncheckParam[name] = undefined
if type is 'boolean'
uncheckParam[name] = false
else
uncheckParam[name] = undefined
true
)
# apply empty checkboxes & radio values to params

View File

@@ -3,6 +3,7 @@ class App.ControllerGenericNew extends App.ControllerModal
buttonCancel: true
buttonSubmit: true
headPrefix: 'New'
showTrySupport: true
content: =>
@head = @pageData.head || @pageData.object
@@ -236,6 +237,7 @@ class App.ControllerGenericDescription extends App.ControllerModal
class App.ControllerModalLoading extends App.Controller
className: 'modal fade'
showTrySupport: true
constructor: ->
super
@@ -322,6 +324,7 @@ class App.ControllerErrorModal extends App.ControllerModal
head: 'Error'
#small: true
#shown: true
showTrySupport: true
content: ->
@message

View File

@@ -348,7 +348,7 @@ class App.ChannelChat extends App.ControllerSubContent
params[key] = value
paramString = ''
for key, value of params
if !_.isEmpty(value)
if _.isNumber(value) || _.isBoolean(value) || !_.isEmpty(value)
if paramString != ''
# coffeelint: disable=no_unnecessary_double_quotes
paramString += ",\n"

View File

@@ -454,9 +454,9 @@ class ConnectionWizard extends App.WizardModal
for key in ['source', 'dest']
if !_.isArray(attributes[key])
attributes[key] = [attributes[key]]
attributes_local =
item_id: 'login'
length = attributes.source.length-1
attributes_local = {}
length = attributes.source.length-1
for count in [0..length]
if attributes.source[count] && attributes.dest[count]
attributes_local[attributes.source[count]] = attributes.dest[count]
@@ -465,18 +465,8 @@ class ConnectionWizard extends App.WizardModal
@tryShow()
buildRowsUserMap: (user_attribute_map) =>
# show static login row
userUidDisplayValue = @wizardConfig.wizardData.backend_attributes['item_id']
el = [
$(App.view('integration/ldap_user_attribute_row_read_only')(
key: userUidDisplayValue,
value: 'Login'
))
]
el = []
for source, dest of user_attribute_map
continue if source == 'item_id'
continue if !(source of @wizardConfig.wizardData.backend_attributes)
el.push @buildRowUserAttribute(source, dest)
el

View File

@@ -6,7 +6,7 @@ class App.UiElement.ApplicationUiElement
# skip sorting if it is disabled by config
return if attribute.sortBy == null
return if !attribute.options
return if _.isEmpty(attribute.options)
# arrays can only get ordered
if _.isArray(attribute.options)
@@ -43,7 +43,7 @@ class App.UiElement.ApplicationUiElement
attribute.options[''] = '-'
@getConfigOptionList: (attribute) ->
return if !attribute.options
return if _.isEmpty(attribute.options)
selection = attribute.options
attribute.options = []
if _.isArray(selection)
@@ -54,6 +54,7 @@ class App.UiElement.ApplicationUiElement
else
order = _.sortBy(
_.keys(selection), (item) ->
return '' if !selection[item] || !selection[item].toString
selection[item].toString().toLowerCase()
)
for key in order
@@ -69,7 +70,7 @@ class App.UiElement.ApplicationUiElement
@getRelationOptionList: (attribute, params) ->
# build options list based on relation
return if !attribute.relation
return if _.isEmpty(attribute.relation)
return if !App[attribute.relation]
attribute.options = []
@@ -175,7 +176,7 @@ class App.UiElement.ApplicationUiElement
# execute filter
@filterOption: (attribute) ->
return if !attribute.filter
return if !attribute.options
return if _.isEmpty(attribute.options)
return if typeof attribute.filter isnt 'function'
App.Log.debug 'ControllerForm', '_filterOption:filter-function'
@@ -218,10 +219,16 @@ class App.UiElement.ApplicationUiElement
value = valueOrigin
if value is null || value is undefined
value = ''
recordValue = record.value
if recordValue is null || recordValue is undefined
recordValue = ''
recordName = record.name
if recordName is null || recordName is undefined
recordName = ''
if typeof value is 'string' || typeof value is 'number' || typeof value is 'boolean'
if record.value.toString() is value.toString() || record.name.toString() is value.toString()
if recordValue.toString() is value.toString() || recordName.toString() is value.toString()
return true
else if ( value && record.value && _.include(value, record.value) ) || ( value && record.name && _.include(value, record.name) )
else if ( value && recordValue && _.include(value, recordValue) ) || ( value && recordName && _.include(value, recordName) )
return true
false

View File

@@ -134,6 +134,17 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
init = false
if params && !params.id
init = true
data = objects[object]
if init
for role, screenOptions of data
for screen, options of screenOptions
for key, defaultValue of options
params.screens ||= {}
params.screens[screen] ||= {}
params.screens[screen][role] ||= {}
params.screens[screen][role][key] = defaultValue
item = $(App.view('object_manager/screens')(
attribute: attribute
data: objects[object]

View File

@@ -33,6 +33,7 @@ class App.TicketCreate extends App.Controller
@bind('ui:rerender', =>
return if !@authenticateCheck()
@renderQueue()
@tokanice()
)
# listen to rerender sidebars
@@ -55,6 +56,7 @@ class App.TicketCreate extends App.Controller
changeFormType: (e) =>
type = $(e.currentTarget).data('type')
@setFormTypeInUi(type)
@tokanice()
setFormTypeInUi: (type) =>
@@ -220,8 +222,7 @@ class App.TicketCreate extends App.Controller
renderQueue: (template = {}) =>
localeRender = =>
@render(options: template)
@render()
@render(template)
App.QueueManager.add(@queueKey, localeRender)
return if !@formMeta
App.QueueManager.run(@queueKey)
@@ -348,6 +349,11 @@ class App.TicketCreate extends App.Controller
# update taskbar with new meta data
App.TaskManager.touch(@taskKey)
@tokanice()
tokanice: ->
App.Utils.tokaniceEmails('.content.active input[name=cc]')
localUserInfo: (e) =>
return if !@sidebarWidget
params = App.ControllerForm.params($(e.target).closest('form'))

View File

@@ -12,15 +12,18 @@ class Index extends App.ControllerSubContent
super
@subscribeId = App.Calendar.subscribe(@render)
callback = (data) =>
App.Config.set('ical_feeds', data.ical_feeds)
App.Config.set('timezones', data.timezones)
@stopLoading()
@render()
@startLoading()
App.Calendar.fetchFull(
callback
clear: true
@ajax(
id: 'calendar_index'
type: 'GET'
url: @apiPath + '/calendars_init'
processData: true
success: (data, status, xhr) =>
App.Config.set('ical_feeds', data.ical_feeds)
App.Config.set('timezones', data.timezones)
App.Collection.loadAssets(data.assets)
@stopLoading()
@render()
)
render: =>
@@ -59,7 +62,7 @@ class Index extends App.ControllerSubContent
if !_.isEmpty(calendars)
showDescription = true
else
description = marked(App[ @genericObject ].description)
description = marked(App.Calendar.description)
@html App.view('calendar/index')(
calendars: calendars

View File

@@ -1,9 +1,8 @@
# coffeelint: disable=duplicate_key
treeParams = (e, params) ->
tree = []
lastLevel = 0
lastLevels = []
valueLevels = []
previousValueLevels = []
$(e.target).closest('.modal').find('.js-treeTable .js-key').each( ->
$element = $(@)
@@ -19,20 +18,19 @@ treeParams = (e, params) ->
lastLevels[level-1].children.push item
else
console.log('ERROR', item)
if level is 0
valueLevels = []
else if lastLevel is level
valueLevels.pop()
else if lastLevel > level
down = lastLevel - level
for count in [1..down]
valueLevels.pop()
if lastLevel <= level
valueLevels.push name
valueLevels = []
# add previous level values
if level > 0
for previousLevel in [0..level - 1]
valueLevels.push(previousValueLevels[previousLevel])
# add current level value
valueLevels.push(name)
item.value = valueLevels.join('::')
lastLevels[level] = item
lastLevel = level
previousValueLevels = valueLevels
)
if tree[0]

View File

@@ -102,22 +102,7 @@ class App.TicketZoomArticleNew extends App.Controller
)
tokanice: ->
source = "#{App.Config.get('api_path')}/users/search"
a = ->
$('.content.active .js-to, .js-cc, js-bcc').tokenfield(
createTokensOnBlur: true
autocomplete: {
source: source
minLength: 2
},
).on('tokenfield:createtoken', (e) ->
if !e.attrs.value.match(/@/) || e.attrs.value.match(/\s/)
e.preventDefault()
return false
e.attrs.label = e.attrs.value
true
)
App.Delay.set(a, 500, undefined, 'tags')
App.Utils.tokaniceEmails('.content.active .js-to, .js-cc, js-bcc')
setPossibleArticleTypes: =>
actionConfig = App.Config.get('TicketZoomArticleAction')

View File

@@ -1,9 +1,9 @@
class App.WidgetTemplate extends App.Controller
events:
'click [data-type=template_save]': 'create'
'click [data-type=template_select]': 'select'
'click [data-type=template_delete]': 'delete'
'click .templates-welcome .create': 'showManage'
'click .templates-manage .js-save': 'create'
'click .templates-manage .js-apply': 'select'
'click .templates-manage .js-delete': 'delete'
'click .templates-welcome .js-create': 'showManage'
constructor: ->
super
@@ -15,12 +15,12 @@ class App.WidgetTemplate extends App.Controller
render: =>
@configure_attributes = [
{ name: 'template_id', display: '', tag: 'select', multiple: false, null: true, nulloption: true, relation: 'Template', default: @template_id },
{ name: 'id', display: '', tag: 'select', multiple: false, null: true, nulloption: true, relation: 'Template', default: @id },
]
template = {}
if @template_id && App.Template.exists(@template_id)
template = App.Template.find(@template_id)
if @id && App.Template.exists(@id)
template = App.Template.find(@id)
# insert data
@html App.view('widget/template')(
@@ -57,11 +57,11 @@ class App.WidgetTemplate extends App.Controller
params = @formParam(e.target)
# check if template is selected
return if !params['template_id']
return if !params['id']
template = App.Template.find(params['template_id'])
template = App.Template.find(params['id'])
if confirm('Sure?')
@template_id = false
@id = false
template.destroy()
select: (e) =>
@@ -71,12 +71,12 @@ class App.WidgetTemplate extends App.Controller
params = @formParam(e.target)
# check if template is selected
return if !params['template_id']
return if !params['id']
# remember template (to select it after rerender)
@template_id = params['template_id']
@id = params['id']
template = App.Template.find(params['template_id'])
template = App.Template.find(params['id'])
App.Event.trigger 'ticket_create_rerender', template.attributes()
create: (e) =>
@@ -85,7 +85,7 @@ class App.WidgetTemplate extends App.Controller
# get params
form = @formParam($(e.target).closest('.content').find('.ticket-create'))
params = @formParam(e.target)
name = params['template_name']
name = params['name']
return if !name
template = App.Template.findByAttribute('name', name)
@@ -93,7 +93,7 @@ class App.WidgetTemplate extends App.Controller
template = new App.Template
template.load(
name: params['template_name']
name: params['name']
options: form
)
@@ -107,7 +107,7 @@ class App.WidgetTemplate extends App.Controller
ui = @
template.save(
done: ->
ui.template_id = @id
ui.id = @id
fail: =>
@log 'error', 'save failed!'

View File

@@ -977,7 +977,9 @@ class App.Utils
if !_.isEmpty(article.to)
recipients = App.Utils.parseAddressListLocal(article.to)
if recipients && recipients[0]
recipientIsLocal = isLocalAddress(recipients[0])
for localRecipient in recipients
recipientIsLocal = isLocalAddress(localRecipient)
break if recipientIsLocal is true
# sender is local
if senderIsLocal
@@ -1041,3 +1043,23 @@ class App.Utils
articleNew.cc = addAddresses(article.cc, articleNew.cc)
articleNew
# apply email token field with autocompletion
@tokaniceEmails: (selector) ->
source = "#{App.Config.get('api_path')}/users/search"
a = ->
$(selector).tokenfield(
createTokensOnBlur: true
autocomplete: {
source: source
minLength: 2
},
).on('tokenfield:createtoken', (e) ->
if !e.attrs.value.match(/@/) || e.attrs.value.match(/\s/)
e.preventDefault()
return false
e.attrs.label = e.attrs.value
true
)
App.Delay.set(a, 500, undefined, 'tags')

View File

@@ -540,7 +540,7 @@
// for validation, do not retrun empty content by empty tags
text_plain = this.$element.text().trim()
if ( !text_plain || text_plain == '' ) {
if ( (!text_plain || text_plain == '') && !this.$element.find('img').get(0) ) {
return text_plain
}
return this.$element.html().trim()

View File

@@ -182,6 +182,13 @@
})
}
// me - 2018-05-24
// cleanup element on hide - cleanup dom with old modal dialogs
Modal.prototype.remove = function () {
console.log('remove', this.$element)
this.$element.remove()
}
Modal.prototype.removeBackdrop = function () {
this.$backdrop && this.$backdrop.remove()
this.$backdrop = null

View File

@@ -237,8 +237,8 @@ set new attributes of model (remove already available attributes)
attributesNew = {}
if screen
for attribute in attributes
if attribute && attribute.screen && attribute.screen[ screen ] && !_.isEmpty(attribute.screen[ screen ])
for item, value of attribute.screen[ screen ]
if attribute && attribute.screen && attribute.screen[screen] && (!_.isEmpty(attribute.screen[screen]) && (attribute.screen[screen].shown is true || attribute.screen[screen].shown is undefined))
for item, value of attribute.screen[screen]
attribute[item] = value
attributesNew[ attribute.name ] = attribute
@@ -406,10 +406,12 @@ set new attributes of model (remove already available attributes)
clear = true
if param.clear is true || param.clear is false
clear = param.clear
if !@initFetchActive
if !@initFetchActives && @count() is 0
@initFetchActive = true
@one 'refresh', (collection) ->
@one('refresh', (collection) =>
@initFetchActive = false
callback(collection)
)
@fetchFull(
->
clear: clear
@@ -867,5 +869,7 @@ set new attributes of model (remove already available attributes)
@configure_attributes = $.extend(true, [], @org_configure_attributes)
@resetCallbacks: ->
@SUBSCRIPTION_ITEM = {}
@SUBSCRIPTION_COLLECTION = {}
if @SUBSCRIPTION_ITEM
@SUBSCRIPTION_ITEM = {}
if @SUBSCRIPTION_COLLECTION
@SUBSCRIPTION_COLLECTION = {}

View File

@@ -5,7 +5,7 @@ class App.Calendar extends App.Model
@configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false },
{ name: 'timezone', display: 'Time zone', tag: 'timezone', null: false }
{ name: 'timezone', display: 'Time zone', tag: 'timezone', null: false }
{ name: 'business_hours', display: 'Business Hours', tag: 'business_hours', null: true }
{ name: 'ical_url', display: 'Holidays iCalendar Feed', tag: 'ical_feed', placeholder: 'http://example.com/public_holidays.ical', null: true }
{ name: 'public_holidays',display: 'Holidays', tag: 'holiday_selector', null: true }

View File

@@ -20,3 +20,4 @@ class App.PostmasterFilter extends App.Model
@configure_overview = [
'name',
]
@configure_clone = true

View File

@@ -33,3 +33,4 @@ class App.Signature extends App.Model
@configure_overview = [
'name',
]
@configure_clone = true

View File

@@ -1,7 +1,7 @@
<div class="dropdown-toggle" data-toggle="dropdown">
<input
class="searchableSelect-shadow form-control js-shadow"
id="<%= @attribute.id %>"
<% if @attribute.id: %>id="<%= @attribute.id %>"<% end %>
name="<%= @attribute.name %>"
<%= @attribute.required %>
<%= @attribute.autofocus %>

View File

@@ -1,6 +0,0 @@
<tr class="js-entry">
<td style="max-width: 240px" class="settings-list-row-control">
<div class="u-textTruncate"><%= @key %></div>
<td class="settings-list-row-control">
<%= @value %>
<td class="settings-list-row-control">

View File

@@ -1,5 +1,6 @@
<div class="js-data">
<div class="js-dataType"></div>
<div class="js-dataMap" style="padding: 30px 0 0 30px;"></div>
<div class="js-dataScreens" style="padding: 30px 0 0 30px;"></div>
<div class="" style="padding-bottom: 10px"><%- @T('Here you define which authorization has access to the attribute.') %></div>
<div class="js-dataScreens"></div>
</div>

View File

@@ -1,32 +0,0 @@
<div>
<div class="js-top"></div>
<div class="js-middle" style="padding-left: 70px;">
<div class="js-input">input settings</div>
<div class="js-textarea">textarea settings</div>
<div class="js-datetime">date time settings</div>
<div class="js-date">date settings</div>
<div class="js-select">select settings</div>
<div class="js-tree_selection">tree selection settings</div>
<div class="js-checkbox">checkbox settings</div>
<div class="js-boolean">boolean settings</div>
<div class="js-richtext">richtext settings</div>
</div>
<div class="boolean form-group">
<div class="horizontal">
<label for=""><%- @T('Screens') %> <span>*</span></label>
</div>
<div class="controls">
1
2
3
</diV
</div>
<div class="js-bottom"></div>
</div>

View File

@@ -2,7 +2,7 @@
<table class="settings-list" style="width: 100%;">
<thead>
<tr>
<th><%- @T('Role') %>
<th><%- @T('Permissions') %>
<th><%- @T('Screen') %>
<th style="width: 50%;"><%- @T('Options') %>
</thead>
@@ -20,11 +20,10 @@
<%= screen %>
<td class="settings-list-row-control">
<% for key, defaultValue of options: %>
<%- @T(key) %>: <input class="js-boolean" name="screens::<%= screen %>::<%= role %>::<%= key %>" type="checkbox" <% if (@init && defaultValue is true) || (@params && @params.screens && @params.screens[screen] && @params.screens[screen][role] && @params.screens[screen][role][key] is true) : %>checked<% end %> value="true">
<%- @T(key) %>: <input class="js-boolean" name="screens::<%= screen %>::<%= role %>::<%= key %>" type="checkbox" <% if @params && @params.screens && @params.screens[screen] && @params.screens[screen][role] && @params.screens[screen][role][key] is true: %>checked<% end %> value="true">
<% end %>
<% end %>
<% end %>
</tbody>
</table>
</div>

View File

@@ -5,18 +5,18 @@
<div id="form-template"></div>
</div>
<div class="horizontal">
<button type="submit" class="btn btn--action" data-type="template_delete"><%- @T('Delete') %></button>
<button type="submit" class="btn btn--action align-right" data-type="template_select"><%- @T('Apply') %></button>
<button type="submit" class="btn btn--action js-delete"><%- @T('Delete') %></button>
<button type="submit" class="btn btn--action align-right js-apply"><%- @T('Apply') %></button>
</div>
</form>
<hr>
<form>
<div class="form-group">
<label class="" for="template_name"><%- @T('Save as template') %></label>
<input type="text" name="template_name" id="template_name" class="form-control" value="<%= @template.name %>"/>
<input type="text" name="name" id="template_name" class="form-control js-name" value="<%= @template.name %>"/>
</div>
<div class="horizontal">
<button type="submit" class="btn btn--action align-right" data-type="template_save"><%- @T('Save') %></button>
<button type="submit" class="btn btn--action align-right js-save"><%- @T('Save') %></button>
</div>
</form>
</div>
@@ -25,5 +25,5 @@
<p><%- @T('No template created yet.') %></p>
<p><%- @T('With templates you can pre-fill ticket attributes.') %></p>
<p><%- @T('Choose attributes and then save them as a new template.') %></p>
<p><a class="create u-clickable"><%- @T('Save new template') %></a></p>
<p><a class="js-create u-clickable"><%- @T('Save new template') %></a></p>
</div>

View File

@@ -47,6 +47,7 @@ module ApplicationController::HandlesDevices
raise Exceptions::UnprocessableEntity, 'Need fingerprint param!'
end
if params[:fingerprint]
UserDevice.fingerprint_validation(params[:fingerprint])
session[:user_device_fingerprint] = params[:fingerprint]
end
end

View File

@@ -3,26 +3,28 @@
class CalendarsController < ApplicationController
prepend_before_action { authentication_check(permission: 'admin.calendar') }
def index
# calendars
def init
assets = {}
calendar_ids = []
record_ids = []
Calendar.all.order(:name, :created_at).each do |calendar|
calendar_ids.push calendar.id
record_ids.push calendar.id
assets = calendar.assets(assets)
end
ical_feeds = Calendar.ical_feeds
timezones = Calendar.timezones
render json: {
calendar_ids: calendar_ids,
record_ids: record_ids,
ical_feeds: ical_feeds,
timezones: timezones,
assets: assets,
}, status: :ok
end
def index
model_index_render(Calendar, params)
end
def show
model_show_render(Calendar, params)
end

View File

@@ -4,6 +4,7 @@ class FormController < ApplicationController
skip_before_action :verify_csrf_token
before_action :cors_preflight_check_execute
after_action :set_access_control_headers_execute
skip_before_action :user_device_check
def configuration
return if !enabled?

View File

@@ -127,7 +127,8 @@ class ObjectManagerAttributesController < ApplicationController
permitted[:data_option][:null] = true
end
if !permitted[:data_option][:options].is_a?(Hash)
if !permitted[:data_option][:options].is_a?(Hash) &&
!permitted[:data_option][:options].is_a?(Array)
permitted[:data_option][:options] = {}
end

View File

@@ -63,6 +63,7 @@ get assets and record_ids of selector
begin
attribute_class = attribute[0].to_classname.constantize
rescue => e
next if attribute[0] == 'article'
logger.error "Unable to get asset for '#{attribute[0]}': #{e.inspect}"
next
end

View File

@@ -105,13 +105,13 @@ module Channel::EmailBuild
attr[:attachments]&.each do |attachment|
if attachment.class == Hash
attachment['content-id'] = nil
mail.attachments[ attachment[:filename] ] = attachment
mail.attachments[attachment[:filename]] = attachment
else
next if attachment.preferences['Content-ID'].present?
filename = attachment.filename
encoded_filename = Mail::Encodings.decode_encode filename, :encode
disposition = attachment.preferences['Content-Disposition'] || 'attachment'
content_type = attachment.preferences['Content-Type'] || 'application/octet-stream'
content_type = attachment.preferences['Content-Type'] || attachment.preferences['Mime-Type'] || 'application/octet-stream'
mail.attachments[attachment.filename] = {
content_disposition: "#{disposition}; filename=\"#{encoded_filename}\"",
content_type: "#{content_type}; filename=\"#{encoded_filename}\"",
@@ -152,9 +152,13 @@ Check if string is a complete html document. If not, add head and css styles.
return html if html.match?(/<html>/i)
html_email_body = File.read(Rails.root.join('app', 'views', 'mailer', 'application_wrapper.html.erb').to_s)
html_email_body.gsub!('###html_email_css_font###', Setting.get('html_email_css_font'))
# use block form because variable html could contain backslashes and e. g. '\1' that
# must not be handled as back-references for regular expressions
Rails.configuration.html_email_body.sub('###html###') { html }
html_email_body.sub('###html###') { html }
end
=begin

View File

@@ -525,6 +525,8 @@ returns
# parse email
mail = parse(msg)
Rails.logger.info "Process email with msgid '#{mail[:message_id]}'"
# run postmaster pre filter
UserInfo.current_user_id = 1
filters = {}

View File

@@ -45,7 +45,7 @@ class Channel::Filter::MonitoringBase
# icinga - get state by body - new templates
if result['state'].blank?
if mail[:body] =~ /==>.*\sis\s(.+?)\!\s+?<==/
if mail[:body] =~ /.+?\sis\s(.+?)\!/
result['state'] = $1
end
end

View File

@@ -5,6 +5,8 @@ class ImportJob < ApplicationModel
store :payload
store :result
scope :running, -> { where(finished_at: nil, dry_run: false).where.not(started_at: nil) }
# Starts the import backend class based on the name attribute.
# Import backend class is initialized with the current instance.
# Logs the start and end time (if ended successfully) and logs

View File

@@ -0,0 +1,25 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class Observer::Ticket::EscalationUpdate < ActiveRecord::Observer
observe 'ticket'
def after_create(record)
_check(record)
end
def after_update(record)
_check(record)
end
private
def _check(record)
# return if we run import mode
return false if Setting.get('import_mode')
return false if !record.saved_changes?
record.escalation_calculation
end
end

View File

@@ -1,6 +1,7 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class Scheduler < ApplicationModel
extend ::Mixin::StartFinishLogger
# rubocop:disable Style/ClassVars
@@jobs_started = {}
@@ -120,18 +121,38 @@ class Scheduler < ApplicationModel
raise 'This method should only get called when Scheduler.threads are initialized. Use `force: true` to start anyway.'
end
Delayed::Job.all.each do |job|
cleanup_delayed(job)
start_time = Time.zone.now
cleanup_delayed_jobs(start_time)
cleanup_import_jobs(start_time)
end
# Checks for locked delayed jobs and tries to reschedule or destroy each of them.
#
# @param [ActiveSupport::TimeWithZone] after the time the cleanup was started
#
# @example
# Scheduler.cleanup_delayed_jobs(TimeZone.now)
#
# return [nil]
def self.cleanup_delayed_jobs(after)
log_start_finish(:info, "Cleanup of left over locked delayed jobs #{after}") do
Delayed::Job.where('updated_at < ?', after).where.not(locked_at: nil).each do |job|
log_start_finish(:info, "Checking left over delayed job #{job.inspect}") do
cleanup_delayed(job)
end
end
end
end
# Checks if the given job can be rescheduled or destroys it. Logs the action as warn.
# Works only for locked jobs. Jobs that are not locked are ignored and
# Checks if the given delayed job can be rescheduled or destroys it. Logs the action as warn.
# Works only for locked delayed jobs. Delayed jobs that are not locked are ignored and
# should get destroyed directly.
# Checks the delayed job object for a method called .reschedule?. The memthod is called
# with the delayed job as a parameter. The result value is expected as a Boolean. If the
# result is true the lock gets removed and the delayed job gets rescheduled. If the return
# value is false it will get destroyed which is the default behaviour.
# Checks the Delayed::Job instance for a method called .reschedule?. The method is called
# with the Delayed::Job instance as a parameter. The result value is expected to be a Boolean.
# If the result is true the lock gets removed and the delayed job gets rescheduled.
# If the return value is false it will get destroyed which is the default behaviour.
#
# @param [Delayed::Job] job the job that should get checked for destroying/rescheduling.
#
@@ -175,6 +196,33 @@ class Scheduler < ApplicationModel
Rails.logger.warn "#{action} locked delayed job: #{job_name}"
end
# Checks for killed import jobs and marks them as finished and adds a note.
#
# @param [ActiveSupport::TimeWithZone] after the time the cleanup was started
#
# @example
# Scheduler.cleanup_import_jobs(TimeZone.now)
#
# return [nil]
def self.cleanup_import_jobs(after)
log_start_finish(:info, "Cleanup of left over import jobs #{after}") do
error = 'Interrupted by scheduler restart. Please restart manually or wait till next execution time.'.freeze
# we need to exclude jobs that were updated at or since we started
# cleaning up (via the #reschedule? call) because they might
# were started `.delay`-ed and are flagged for restart
ImportJob.running.where('updated_at < ?', after).each do |job|
job.update!(
finished_at: after,
result: {
error: error
}
)
end
end
end
def self.start_job(job)
# start job and return thread handle
@@ -245,6 +293,7 @@ class Scheduler < ApplicationModel
# restart job again
if try_run_max > try_count
sleep(try_count) # wait between retries (see https://github.com/zammad/zammad/issues/1950)
_start_job(job, try_count, try_run_time)
else
# release thread lock and remove thread handle

View File

@@ -23,9 +23,7 @@ class Ticket < ApplicationModel
store :preferences
before_create :check_generate, :check_defaults, :check_title, :set_default_state, :set_default_priority
after_create :check_escalation_update
before_update :check_defaults, :check_title, :reset_pending_time, :check_owner_active
after_update :check_escalation_update
validates :group_id, presence: true
@@ -1188,11 +1186,6 @@ result
true
end
def check_escalation_update
escalation_calculation
true
end
def set_default_state
return true if state_id
default_ticket_state = Ticket::State.find_by(default_create: true)

View File

@@ -40,8 +40,8 @@ class User < ApplicationModel
include User::SearchIndex
before_validation :check_name, :check_email, :check_login, :check_mail_delivery_failed, :ensure_uniq_email, :ensure_password, :ensure_roles, :ensure_identifier
before_create :check_preferences_default, :validate_ooo, :domain_based_assignment, :set_locale
before_update :check_preferences_default, :validate_ooo, :reset_login_failed, :validate_agent_limit_by_attributes, :last_admin_check_by_attribute
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
after_update :avatar_for_email_check
before_destroy :avatar_destroy, :user_device_destroy, :cit_caller_id_destroy, :task_destroy
@@ -1006,6 +1006,22 @@ returns
true
end
def validate_preferences
return true if !changes
return true if !changes['preferences']
return true if preferences.blank?
return true if !preferences[:notification_sound]
return true if !preferences[:notification_sound][:enabled]
if preferences[:notification_sound][:enabled] == 'true'
preferences[:notification_sound][:enabled] = true
elsif preferences[:notification_sound][:enabled] == 'false'
preferences[:notification_sound][:enabled] = false
end
class_name = preferences[:notification_sound][:enabled].class.to_s
raise Exceptions::UnprocessableEntity, "preferences.notification_sound.enabled need to be an boolean, but it was a #{class_name}" if class_name != 'TrueClass' && class_name != 'FalseClass'
true
end
=begin
checks if the current user is the last one with admin permissions.

View File

@@ -5,6 +5,9 @@ class UserDevice < ApplicationModel
store :location_details
validates :name, presence: true
before_create :fingerprint_validation
before_update :fingerprint_validation
=begin
store new device for user if device not already known
@@ -34,7 +37,8 @@ store new device for user if device not already known
# find device by fingerprint
device_exists_by_fingerprint = false
if fingerprint
if fingerprint.present?
UserDevice.fingerprint_validation(fingerprint)
user_devices = UserDevice.where(
user_id: user_id,
fingerprint: fingerprint,
@@ -221,4 +225,24 @@ delete device devices of user
def self.remove(user_id)
UserDevice.where(user_id: user_id).destroy_all
end
=begin
check fingerprint string
UserDevice.fingerprint_validation(fingerprint)
=end
def self.fingerprint_validation(fingerprint)
return true if fingerprint.blank?
raise Exceptions::UnprocessableEntity, "fingerprint is #{fingerprint.to_s.length} chars but can only be 160 chars!" if fingerprint.to_s.length > 160
true
end
private
def fingerprint_validation
UserDevice.fingerprint_validation(fingerprint)
end
end

View File

@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<style type="text/css">
body {
###html_email_css_font###;
}
img {
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
a img {
border: none;
}
table td {
border-collapse: collapse;
}
table {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
border: none;
table-layout: auto;
display: block;
width: 100%;
overflow: auto;
word-break: keep-all;
}
table,
pre,
blockquote {
margin: 0 0 16px;
}
td, th {
padding: 7px 12px;
border: 1px solid hsl(0,0%,87%);
}
th {
font-weight: bold;
text-align: center;
}
tbody tr:nth-child(even) {
background: hsl(0,0%,97%);
}
col {
width: auto;
}
p {
margin: 0;
}
code {
border: none;
background: hsl(0,0%,97%);
white-space: pre-wrap;
}
blockquote {
padding: 8px 12px;
border-left: 5px solid #eee;
}
pre {
padding: 12px 15px;
font-size: 13px;
line-height: 1.45;
background: hsl(0,0%,97%);
white-space: pre-wrap;
border-radius: 3px;
border: none;
overflow: auto;
}
</style>
</head>
<body style="###html_email_css_font###">###html###</body>
</html>

View File

@@ -0,0 +1,16 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/model_binding.js"></script>
<style type="text/css">
body {
padding-top: 0px;
}
</style>
<script type="text/javascript">
</script>
<div id="qunit" class="u-dontfold"></div>

View File

@@ -2,6 +2,9 @@ require File.expand_path('../boot', __FILE__)
require 'rails/all'
# DO NOT REMOVE THIS LINE - see issue #2037
Bundler.setup
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
@@ -35,6 +38,7 @@ module Zammad
'observer::_ticket::_ref_object_touch',
'observer::_ticket::_online_notification_seen',
'observer::_ticket::_stats_reopen',
'observer::_ticket::_escalation_update',
'observer::_tag::_ticket_history',
'observer::_user::_ref_object_touch',
'observer::_user::_ticket_organization',

View File

@@ -1,83 +0,0 @@
if ActiveRecord::Base.connection.table_exists?( Setting.table_name )
html_email_css_font = Setting.get('html_email_css_font')
Rails.application.config.html_email_body = <<~HERE
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<style type="text/css">
body {
#{html_email_css_font};
}
img {
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
a img {
border: none;
}
table td {
border-collapse: collapse;
}
table {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
border: none;
table-layout: auto;
display: block;
width: 100%;
overflow: auto;
word-break: keep-all;
}
table,
pre,
blockquote {
margin: 0 0 16px;
}
td, th {
padding: 7px 12px;
border: 1px solid hsl(0,0%,87%);
}
th {
font-weight: bold;
text-align: center;
}
tbody tr:nth-child(even) {
background: hsl(0,0%,97%);
}
col {
width: auto;
}
p {
margin: 0;
}
code {
border: none;
background: hsl(0,0%,97%);
white-space: pre-wrap;
}
blockquote {
padding: 8px 12px;
border-left: 5px solid #eee;
}
pre {
padding: 12px 15px;
font-size: 13px;
line-height: 1.45;
background: hsl(0,0%,97%);
white-space: pre-wrap;
border-radius: 3px;
border: none;
overflow: auto;
}
</style>
</head>
<body style="#{html_email_css_font}">###html###</body>
</html>
HERE
end

View File

@@ -50,3 +50,11 @@ Rails.application.config.middleware.use OmniAuth::Builder do
provider :weibo_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database'
end
# This fixes issue #1642 and is required for setups in which Zammad is used
# with a reverse proxy (like e.g. NGINX) handling the HTTPS stuff.
# This leads to the generation of a wrong redirect_uri because Rack detects a
# HTTP request which breaks OAuth2.
OmniAuth.config.full_host = proc {
"#{Setting.get('http_type')}://#{Setting.get('fqdn')}"
}

View File

@@ -2,6 +2,7 @@ Zammad::Application.routes.draw do
api_path = Rails.configuration.api_path
# calendars
match api_path + '/calendars_init', to: 'calendars#init', via: :get
match api_path + '/calendars', to: 'calendars#index', via: :get
match api_path + '/calendars/:id', to: 'calendars#show', via: :get
match api_path + '/calendars', to: 'calendars#create', via: :post

View File

@@ -3,6 +3,7 @@ Zammad::Application.routes.draw do
match '/tests_core', to: 'tests#core', via: :get
match '/tests_ui', to: 'tests#ui', via: :get
match '/tests_model', to: 'tests#model', via: :get
match '/tests_model_binding', to: 'tests#model_binding', via: :get
match '/tests_model_ui', to: 'tests#model_ui', via: :get
match '/tests_form', to: 'tests#form', via: :get
match '/tests_form_tree_select', to: 'tests#form_tree_select', via: :get

View File

@@ -21,12 +21,13 @@ class CheckForObjectAttributes < ActiveRecord::Migration[5.1]
end
def fix_nil_data_option(attribute)
return if attribute[:data_option].is_a?(Hash)
return if attribute[:data_option].is_a?(Hash) || attribute[:data_option][:options].is_a?(Array)
attribute[:data_option] = {}
end
def fix_options(attribute)
return if attribute[:data_option][:options].is_a?(Hash)
return if attribute[:data_option][:options].is_a?(Array)
attribute[:data_option][:options] = {}
end

View File

@@ -1,36 +0,0 @@
class CleanupUserPreferencesNotificationSound < ActiveRecord::Migration[5.1]
def up
User.with_permissions('ticket.agent').each do |user|
local_to_h!(user.preferences)
user.save!
end
User.with_permissions('ticket.agent').each do |user|
next if !user.preferences
next if !user.preferences[:notification_sound]
next if !user.preferences[:notification_sound][:enabled]
if user.preferences[:notification_sound][:enabled] == 'true'
user.preferences[:notification_sound][:enabled] = true
user.save!
next
end
next if user.preferences[:notification_sound][:enabled] != 'false'
user.preferences[:notification_sound][:enabled] = false
user.save!
next
end
end
def local_to_h!(value)
if value.class == ActionController::Parameters
value = value.permit!.to_h
end
if value.class == Hash || value.class == ActiveSupport::HashWithIndifferentAccess
value.each_key do |local_key|
value[local_key] = local_to_h!(value[local_key])
end
end
value
end
end

View File

@@ -0,0 +1,44 @@
class Issue1660FixTreeSelectConfigurations < ActiveRecord::Migration[5.1]
def change
# return if it's a new setup
return if !Setting.find_by(name: 'system_init_done')
return if attributes.blank?
attributes.each do |attribute|
next if attribute.data_option.blank?
next if attribute.data_option[:options].blank?
fixed_options = fix(attribute.data_option[:options])
attribute.data_option[:options] = fixed_options
attribute.save!
end
end
private
def attributes
@attributes ||= ObjectManager::Attribute.where(data_type: 'tree_select')
end
def fix(options, namespace = nil)
options.tap do |ref|
ref.each do |option|
option_namespace = Array(namespace.dup)
option_namespace.push(option['name'])
option['value'] = option_namespace.join('::')
next if option['children'].blank?
option['children'] = fix(option['children'], option_namespace)
end
end
end
end

View File

@@ -0,0 +1,17 @@
class Issue1905ExchangeLoginFromRemoteId < ActiveRecord::Migration[5.1]
def change
# return if it's a new setup
return if !Setting.find_by(name: 'system_init_done')
config = Import::Exchange.config
return if config.blank?
return if config[:attributes].blank?
return if config[:attributes][:item_id].blank?
return if config[:attributes][:item_id] != 'login'
config[:attributes].delete(:item_id)
Import::Exchange.config = config
end
end

View File

@@ -0,0 +1,57 @@
class CleanupUserPreferencesNotificationSound2 < ActiveRecord::Migration[5.1]
def local_to_h!(value)
if value.class == ActionController::Parameters
value = value.permit!.to_h
end
if value.class == Hash || value.class == ActiveSupport::HashWithIndifferentAccess
value.each_key do |local_key|
value[local_key] = local_to_h!(value[local_key])
end
end
value
end
def local_clear_preferences(user)
return false if !user
return false if !user.preferences
return false if !user.preferences[:notification_sound]
return false if !user.preferences[:notification_sound][:enabled]
if user.preferences[:notification_sound][:enabled] == 'true'
user.preferences[:notification_sound][:enabled] = true
user.save!
return true
end
return false if user.preferences[:notification_sound][:enabled] != 'false'
user.preferences[:notification_sound][:enabled] = false
user.save!
true
end
def up
User.with_permissions('ticket.agent').each do |user|
local_to_h!(user.preferences)
user.save!
end
items = SearchIndexBackend.search('preferences.notification_sound.enabled:*', 3000, 'User') || []
items.each do |item|
next if !item[:id]
user = User.find_by(id: item[:id])
local_to_h!(user.preferences)
local_clear_preferences(user)
end
Organization.all.each do |organization|
organization.members.each do |user|
local_to_h!(user.preferences)
local_clear_preferences(user)
end
end
Delayed::Job.all.each do |job|
Delayed::Worker.new.run(job)
end
end
end

View File

@@ -55,8 +55,8 @@ class CalendarSubscriptions
events_data.each do |event_data|
cal.event do |e|
e.dtstart = event_data[:dtstart]
e.dtend = event_data[:dtend]
e.dtstart = Icalendar::Values::DateTime.new(event_data[:dtstart], 'tzid' => 'UTC')
e.dtend = Icalendar::Values::DateTime.new(event_data[:dtend], 'tzid' => 'UTC')
if event_data[:alarm]
e.alarm do |a|
a.action = 'DISPLAY'

View File

@@ -18,42 +18,34 @@ examples how to use
def to_s
strip_html
@template
end
def strip_html
# some browsers start adding HTML tags
# fixes https://github.com/zammad/zammad/issues/385
@template.gsub!(/\#\{\s*t\((.+?)\)\s*\}/m) do
@template.gsub(/\#\{\s*t\((.+?)\)\s*\}/m) do
content = $1
if content =~ /^'(.+?)'$/
"<%= t \"#{strip_content($1)}\", #{@escape} %>"
%(<%= t "#{strip_content($1)}", #{@escape} %>)
else
"<%= t d\"#{strip_variable(content)}\", #{@escape} %>"
%(<%= t d"#{strip_variable(content)}", #{@escape} %>)
end
end
@template.gsub!(/\#\{\s*config\.(.+?)\s*\}/m) do
"<%= c \"#{strip_variable($1)}\", #{@escape} %>"
end
@template.gsub!(/\#\{(.*?)\}/m) do
"<%= d \"#{strip_variable($1)}\", #{@escape} %>"
end.gsub(/\#\{\s*config\.(.+?)\s*\}/m) do
%(<%= c "#{strip_variable($1)}", #{@escape} %>)
end.gsub(/\#\{(.*?)\}/m) do
%(<%= d "#{strip_variable($1)}", #{@escape} %>)
end
end
def strip_content(string)
return string if !string
string.gsub!(/\t|\r|\n/, '')
string.gsub!(/"/, '\"')
string
string&.gsub(/\t|\r|\n/, '')
&.gsub(/"/, '\"')
end
def strip_variable(string)
return string if !string
string.gsub!(/\t|\r|\n|"|'|§|;/, '')
string.gsub!(/\s*/, '')
string.gsub!(/<.+?>/, '')
string
string&.gsub(/\t|\r|\n|"|'|§|;/, '')
&.gsub(/\s*/, '')
&.gsub(/<.+?>/, '')
end
end

View File

@@ -606,12 +606,12 @@ return true if backend is configured
message = if response&.error&.match?('Connection refused')
"Elasticsearch is not reachable, probably because it's not running or even installed."
elsif url.end_with?('pipeline/zammad-attachment') && response.code == 400
elsif url.end_with?('pipeline/zammad-attachment', 'pipeline=zammad-attachment') && response.code == 400
'The installed attachment plugin could not handle the request payload. Ensure that the correct attachment plugin is installed (5.6 => ingest-attachment, 2.4 - 5.5 => mapper-attachments).'
else
'Check the response and payload for detailed information: '
end
"#{prefix}#{message}#{suffix}"
"#{prefix} #{message}#{suffix}"
end
end

View File

@@ -8,7 +8,8 @@ class Sequencer
[
'Import::Exchange::FolderContact::RemoteId',
'Import::Common::RemoteId::CaseSensitive',
'Import::Exchange::FolderContact::Mapping',
'Import::Exchange::FolderContact::Mapping::FromConfig',
'Import::Exchange::FolderContact::Mapping::Login',
'Import::Common::Model::Skip::Blank::Mapped',
'Common::ModelClass::User',
'Import::Exchange::FolderContact::ExternalSyncSource',

View File

@@ -7,7 +7,6 @@ class Sequencer
include ::Sequencer::Unit::Import::Common::Mapping::Mixin::ProvideMapped
uses :resource
provides :mapped
def process
provide_mapped do

View File

@@ -12,6 +12,7 @@ class Sequencer
# model_nos
# model_name
# model_name
# model_name
without_double_underscores.gsub(/_id(s?)$/, '_no\1')
end
@@ -20,6 +21,7 @@ class Sequencer
# model_ids
# model_name
# model_name
# model_name
without_spaces_and_slashes.gsub(/_{2,}/, '_')
end
@@ -28,7 +30,17 @@ class Sequencer
# model_ids
# model___name
# model_name
unsanitized_name.gsub(%r{[\s\/]}, '_').underscore
# model_name
transliterated.gsub(%r{[\s\/]}, '_').underscore
end
def transliterated
# Model ID
# Model IDs
# Model / Name
# Model Name
# Model Name
::ActiveSupport::Inflector.transliterate(unsanitized_name, '_'.freeze)
end
def unsanitized_name
@@ -36,6 +48,9 @@ class Sequencer
# Model IDs
# Model / Name
# Model Name
# rubocop:disable Style/AsciiComments
# Mödel Nâmé
# rubocop:enable Style/AsciiComments
raise 'Missing implementation for unsanitized_name method'
end
end

View File

@@ -1,27 +0,0 @@
class Sequencer
class Unit
module Import
module Exchange
module FolderContact
class Mapping < Sequencer::Unit::Import::Common::Mapping::FlatKeys
uses :import_job
private
def mapping
from_import_job || ::Import::Exchange.config[:attributes]
end
def from_import_job
return if !state.provided?(:import_job)
payload = import_job.payload
return if payload.blank?
payload[:ews_attributes]
end
end
end
end
end
end
end

View File

@@ -0,0 +1,29 @@
class Sequencer
class Unit
module Import
module Exchange
module FolderContact
module Mapping
class FromConfig < Sequencer::Unit::Import::Common::Mapping::FlatKeys
uses :import_job
private
def mapping
from_import_job || ::Import::Exchange.config[:attributes]
end
def from_import_job
return if !state.provided?(:import_job)
payload = import_job.payload
return if payload.blank?
payload[:ews_attributes]
end
end
end
end
end
end
end
end

View File

@@ -0,0 +1,25 @@
class Sequencer
class Unit
module Import
module Exchange
module FolderContact
module Mapping
class Login < Sequencer::Unit::Import::Common::Mapping::FlatKeys
include ::Sequencer::Unit::Import::Common::Mapping::Mixin::ProvideMapped
uses :remote_id
def process
provide_mapped do
{
login: remote_id
}
end
end
end
end
end
end
end
end
end

View File

@@ -9,9 +9,12 @@ class Sequencer
uses :resource
def value
method_name = resource.via.channel.to_sym
return if !respond_to?(method_name, true)
send(method_name)
return if private_methods(false).exclude?(value_method_name)
send(value_method_name)
end
def value_method_name
@value_method_name ||= resource.via.channel.to_sym
end
end
end

View File

@@ -7,12 +7,14 @@ class Stats::TicketWaitingTime
# get users groups
group_ids = user.group_ids_access('full')
own_waiting = Ticket.where(
'owner_id = ? AND group_id IN (?) AND updated_at > ?', user.id, group_ids, Time.zone.today
)
all_waiting = Ticket.where(
'group_id IN (?) AND updated_at > ?', group_ids, Time.zone.today
)
own_waiting = []
all_waiting = []
Ticket.where('group_id IN (?) AND updated_at > ?', group_ids.sort, Time.zone.today).pluck(:id, :owner_id).each do |ticket|
all_waiting.push ticket[0]
if ticket[1] == user.id
own_waiting.push ticket[0]
end
end
handling_time = calculate_average(own_waiting, Time.zone.today)
if handling_time.positive?
@@ -51,20 +53,24 @@ class Stats::TicketWaitingTime
result
end
def self.calculate_average(tickets, start_time)
def self.calculate_average(ticket_ids, start_time)
average_time = 0
count_articles = 0
last_ticket_id = nil
count_time = nil
tickets.each do |ticket|
count_time = 0
ticket.articles.joins(:type).where('ticket_articles.created_at > ? AND ticket_articles.internal = ? AND ticket_article_types.communication = ?', start_time, false, true).each do |article|
if article.sender.name == 'Customer'
count_time = article.created_at.to_i
elsif count_time.positive?
average_time += article.created_at.to_i - count_time
count_articles += 1
count_time = 0
end
Ticket::Article.joins(:type).joins(:sender).where('ticket_articles.ticket_id IN (?) AND ticket_articles.created_at > ? AND ticket_articles.internal = ? AND ticket_article_types.communication = ?', ticket_ids, start_time, false, true).order(:ticket_id, :created_at).pluck(:created_at, :sender_id, :ticket_id, :id).each do |article|
if last_ticket_id != article[2]
last_ticket_id = article[2]
count_time = 0
end
sender = Ticket::Article::Sender.lookup(id: article[1])
if sender.name == 'Customer'
count_time = article[0].to_i
elsif count_time.positive?
average_time += article[0].to_i - count_time
count_articles += 1
count_time = 0
end
end

View File

@@ -26,6 +26,8 @@ test("form elements check", function() {
datetime1: Date.parse('2015-01-11T12:40:00Z'),
checkbox1: [],
checkbox2: '1',
boolean1: true,
boolean2: false,
}
new App.ControllerForm({
el: el,
@@ -49,6 +51,9 @@ test("form elements check", function() {
{ name: 'datetime2', display: 'Datetime2', tag: 'datetime', null: false, default: defaults['datetime2'] },
{ name: 'checkbox1', display: 'Checkbox1', tag: 'checkbox', null: false, default: defaults['checkbox1'], options: { a: 'AA', b: 'BB' } },
{ name: 'checkbox2', display: 'Checkbox2', tag: 'checkbox', null: false, default: defaults['checkbox2'], options: { 1: '11' } },
{ name: 'boolean1', display: 'Boolean1', tag: 'boolean', null: false, default: defaults['boolean1'] },
{ name: 'boolean2', display: 'Boolean2', tag: 'boolean', null: false, default: defaults['boolean2'] },
{ name: 'boolean3', display: 'Boolean3', tag: 'boolean', null: false, default: defaults['boolean3'] },
]
},
autofocus: true
@@ -108,6 +113,9 @@ test("form elements check", function() {
equal(el.find('[name="checkbox1"]').last().is(":checked"), false)
equal(el.find('[name="checkbox2"]').is(":checked"), true)
equal(el.find('[name="boolean1"]').val(), 'true')
equal(el.find('[name="boolean1"]').val(), 'true')
equal(el.find('[name="boolean2"]').val(), 'false')
});
test("form params check", function() {
@@ -147,6 +155,8 @@ test("form params check", function() {
checkbox3: 'd',
radiobox1: undefined,
radiobox2: 'a',
boolean1: true,
boolean2: false,
}
new App.ControllerForm({
el: el,
@@ -192,6 +202,9 @@ test("form params check", function() {
{ name: 'checkbox4', display: 'Checkbox4', tag: 'checkbox', null: false, default: defaults['checkbox4'], options: { aa: 'AA', bb: 'BB' } },
{ name: 'radiobox1', display: 'Radiobox1', tag: 'radio', null: false, default: defaults['radiobox1'], options: { a: 'AA', b: 'BB' } },
{ name: 'radiobox2', display: 'Radiobox2', tag: 'radio', null: false, default: defaults['radiobox2'], options: { a: '11' } },
{ name: 'boolean1', display: 'Boolean1', tag: 'boolean', null: false, default: defaults['boolean1'] },
{ name: 'boolean2', display: 'Boolean2', tag: 'boolean', null: false, default: defaults['boolean2'] },
{ name: 'boolean3', display: 'Boolean3', tag: 'boolean', null: false, default: defaults['boolean3'] },
],
},
params: defaults,
@@ -296,6 +309,9 @@ test("form params check", function() {
checkbox4: [],
radiobox1: undefined,
radiobox2: 'a',
boolean1: true,
boolean2: false,
boolean3: true,
}
deepEqual(params, test_params, 'form param check')
@@ -995,5 +1011,290 @@ test("form params check direct", function() {
//console.log('params', params)
//console.log('test_params', test_params)
deepEqual(params, test_params, 'form param check')
});
test("object manager form 1", function() {
$('#forms').append('<hr><h1>object manager 1</h1><form id="form11"></form>')
var el = $('#form11')
var defaults = {}
new App.ControllerForm({
el: el,
model: {
configure_attributes: [
{ name: 'data_type', display: 'Format', tag: 'object_manager_attribute', null: false },
],
},
params: $.extend(defaults, { object: 'Ticket' }),
autofocus: true
});
var params = App.ControllerForm.params(el)
var test_params = {
data_option: {
default: "",
maxlength: 120,
type: "text"
},
data_type: "input",
screens: {
create_middle: {
"ticket.agent": {
shown: true,
required: false,
},
"ticket.customer": {
shown: true,
required: false,
}
},
edit: {
"ticket.agent": {
shown: true,
required: false,
},
"ticket.customer": {
shown: true,
required: false,
}
}
}
}
deepEqual(params, test_params, 'form param check')
el.find('[name=data_type]').val('datetime').trigger('change')
params = App.ControllerForm.params(el)
var test_params = {
data_option: {
diff: 24,
future: true,
past: true
},
data_type: "datetime",
screens: {
create_middle: {
"ticket.agent": {
shown: true,
required: false,
},
"ticket.customer": {
shown: true,
required: false,
}
},
edit: {
"ticket.agent": {
shown: true,
required: false,
},
"ticket.customer": {
shown: true,
required: false,
}
}
}
}
deepEqual(params, test_params, 'form param check')
});
test("object manager form 2", function() {
$('#forms').append('<hr><h1>object manager 2</h1><form id="form12"></form>')
var el = $('#form12')
var defaults = {
id: 123,
data_option: {
default: "",
maxlength: 120,
type: "text"
},
data_type: "input",
screens: {
create_middle: {
"ticket.agent": {
shown: true,
required: false,
},
},
edit: {
"ticket.agent": {
shown: true,
required: false,
},
}
}
}
new App.ControllerForm({
el: el,
model: {
configure_attributes: [
{ name: 'data_type', display: 'Format', tag: 'object_manager_attribute', null: false },
],
},
params: $.extend(defaults, { object: 'Ticket' }),
autofocus: true
});
var params = App.ControllerForm.params(el)
var test_params = {
data_option: {
default: "",
maxlength: 120,
type: "text"
},
data_type: "input",
screens: {
create_middle: {
"ticket.agent": {
shown: true,
required: false,
},
"ticket.customer": {
shown: false,
required: false,
}
},
edit: {
"ticket.agent": {
shown: true,
required: false,
},
"ticket.customer": {
shown: false,
required: false,
}
}
}
}
deepEqual(params, test_params, 'form param check')
});
test("object manager form 3", function() {
$('#forms').append('<hr><h1>object manager 3</h1><form id="form13"></form>')
var el = $('#form13')
var defaults = {}
new App.ControllerForm({
el: el,
model: {
configure_attributes: [
{ name: 'data_type', display: 'Format', tag: 'object_manager_attribute', null: false },
],
},
params: $.extend(defaults, { object: 'Ticket' }),
autofocus: true
});
var params = App.ControllerForm.params(el)
var test_params = {
data_option: {
default: "",
maxlength: 120,
type: "text"
},
data_type: "input",
screens: {
create_middle: {
"ticket.agent": {
shown: true,
required: false,
},
"ticket.customer": {
shown: true,
required: false,
}
},
edit: {
"ticket.agent": {
shown: true,
required: false,
},
"ticket.customer": {
shown: true,
required: false,
}
}
}
}
deepEqual(params, test_params, 'form param check')
el.find('[name="screens::create_middle::ticket.customer::shown"]').click()
el.find('[name="screens::edit::ticket.customer::shown"]').click()
params = App.ControllerForm.params(el)
test_params = {
data_option: {
default: "",
maxlength: 120,
type: "text"
},
data_type: "input",
screens: {
create_middle: {
"ticket.agent": {
shown: true,
required: false,
},
"ticket.customer": {
shown: false,
required: false,
}
},
edit: {
"ticket.agent": {
shown: true,
required: false,
},
"ticket.customer": {
shown: false,
required: false,
}
}
}
}
deepEqual(params, test_params, 'form param check')
});
test("form select with empty option list", function() {
$('#forms').append('<hr><h1>form select with empty option list</h1><form id="form14"></form>')
var el = $('#form14')
var defaults = {}
new App.ControllerForm({
el: el,
model: {
configure_attributes: [
{ name: 'select1', display: 'Select1', tag: 'select', null: true, default: '', options: {}, relation: '', maxlength: 255 },
{ name: 'select2', display: 'Select2', tag: 'select', null: true, default: '', options: {}, relation: '', maxlength: 255, nulloption: true },
{ name: 'select3', display: 'Select3', tag: 'select', null: true, default: '', options: { undefined: 'A', null: 'B'} },
{ name: 'select4', display: 'Select4', tag: 'select', null: true, default: '', options: { 'A': undefined, 'B': null} },
{ name: 'select5', display: 'Select5', tag: 'select', null: true, default: 'A', options: { 'A': undefined, 'B': null} },
{ name: 'select6', display: 'Select6', tag: 'select', null: true, default: undefined, options: { 'A': undefined, 'B': null} },
],
},
params: defaults,
autofocus: true
});
params = App.ControllerForm.params(el)
test_params = {
select2: '',
select3: 'undefined',
select4: 'B',
select5: 'A',
select6: 'B',
}
deepEqual(params, test_params)
});

View File

@@ -2,14 +2,8 @@
// form
test('form checks', function() {
// use unsorted order to check if the frontend is sorting correctly
App.TicketPriority.refresh([
{
id: 1,
name: '1 low',
note: 'some note 1',
active: true,
created_at: '2014-06-10T11:17:34.000Z',
},
{
id: 2,
name: '2 normal',
@@ -38,6 +32,13 @@ test('form checks', function() {
active: false,
created_at: '2014-06-10T10:17:56.000Z',
},
{
id: 1,
name: '1 low',
note: 'some note 1',
active: true,
created_at: '2014-06-10T11:17:34.000Z',
},
])
App.TicketState.refresh([
@@ -145,8 +146,8 @@ test('form checks', function() {
el: el,
model: {
configure_attributes: [
{ name: 'priority1_id', display: 'Priroity1', tag: 'select', relation: 'TicketPriority', null: true },
{ name: 'priority2_id', display: 'Priroity2', tag: 'select', multiple: true, relation: 'TicketPriority', null: true },
{ 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: 'escalation_times', display: 'Times', tag: 'sla_times', null: true },
{ name: 'working_hours', display: 'Hours', tag: 'business_hours', null: true },

View File

@@ -2603,6 +2603,45 @@ test('check getRecipientArticle format', function() {
verify = App.Utils.getRecipientArticle(ticket, article, agent, article.type, email_addresses, false)
deepEqual(verify, result)
agent = {
login: 'login',
firstname: 'firstname',
lastname: 'lastname',
email: 'agent@example.com',
}
ticket = {
customer: agent,
}
article = {
message_id: 'message_id20',
created_by: agent,
type: {
name: 'email',
},
sender: {
name: 'Agent',
},
from: 'Agent <Agent@Example.com>',
to: 'somebodyelse@example.com, Zammad <zammad@example.com>',
cc: '',
}
result = {
to: 'agent@example.com',
cc: '',
body: '',
in_reply_to: 'message_id20',
}
email_addresses = [
{
email: 'zammad@example.com',
},
{
email: 'zammad2@example.com',
}
]
verify = App.Utils.getRecipientArticle(ticket, article, agent, article.type, email_addresses, false)
deepEqual(verify, result)
});
}

View File

@@ -0,0 +1,52 @@
window.onload = function() {
test("model bindings and rebinding", function() {
var callback_count1 = 0
var callbacks = App.Template._callbacks
Object.keys(callbacks).forEach(function(key) {
callback_count1 = callback_count1 + callbacks[key].length
});
equal(callback_count1, 2)
equal(App.Template.SUBSCRIPTION_COLLECTION, undefined)
App.Template.clearInMemory()
equal(App.Template.SUBSCRIPTION_COLLECTION, undefined)
var callback_count2 = 0
callbacks = App.Template._callbacks
Object.keys(callbacks).forEach(function(key) {
callback_count2 = callback_count2 + callbacks[key].length
});
equal(callback_count2, callback_count1)
var render = function() {}
var subscribe_id = App.Template.subscribe(render, {initFetch: true})
ok(_.isObject(App.Template.SUBSCRIPTION_COLLECTION))
ok(!_.isEmpty(App.Template.SUBSCRIPTION_COLLECTION))
var callback_count3 = 0
callbacks = App.Template._callbacks
Object.keys(callbacks).forEach(function(key) {
callback_count3 = callback_count3 + callbacks[key].length
});
equal(callback_count3, 6)
App.Template.clearInMemory()
ok(_.isObject(App.Template.SUBSCRIPTION_COLLECTION))
ok(_.isEmpty(App.Template.SUBSCRIPTION_COLLECTION))
var callback_count4 = 0
callbacks = App.Template._callbacks
Object.keys(callbacks).forEach(function(key) {
callback_count4 = callback_count4 + callbacks[key].length
});
equal(callback_count4, callback_count3)
});
}

View File

@@ -53,22 +53,22 @@ if [ "$RDB" == 'postgresql' ]; then
echo " pool: 50" >> $DBFILE
echo " timeout: 5000" >> $DBFILE
echo " encoding: utf8" >> $DBFILE
echo " username: root" >> $DBFILE
echo " password: pass" >> $DBFILE
echo " username: gitlab-runner" >> $DBFILE
echo " password:" >> $DBFILE
echo "test:" >> $DBFILE
echo " adapter: postgresql" >> $DBFILE
echo " database: $DB_TEST" >> $DBFILE
echo " pool: 50" >> $DBFILE
echo " timeout: 5000" >> $DBFILE
echo " encoding: utf8" >> $DBFILE
echo " username: root" >> $DBFILE
echo " password: pass" >> $DBFILE
echo " username: gitlab-runner" >> $DBFILE
echo " password:" >> $DBFILE
echo "development:" >> $DBFILE
echo " adapter: postgresql" >> $DBFILE
echo " database: $DB_DEVELOPMENT" >> $DBFILE
echo " pool: 50" >> $DBFILE
echo " timeout: 5000" >> $DBFILE
echo " encoding: utf8" >> $DBFILE
echo " username: root" >> $DBFILE
echo " password: pass" >> $DBFILE
echo " username: gitlab-runner" >> $DBFILE
echo " password:" >> $DBFILE
fi

View File

@@ -15,14 +15,18 @@ if [ "$LEVEL" == '1' ]; then
# test/browser/aac_basic_richtext_test.rb
rm test/browser/abb_one_group_test.rb
rm test/browser/admin_channel_email_test.rb
rm test/browser/admin_calendar_sla_test.rb
rm test/browser/admin_object_manager_test.rb
rm test/browser/admin_object_manager_tree_select_test.rb
rm test/browser/admin_overview_test.rb
rm test/browser/admin_role_test.rb
# test/browser/agent_navigation_and_title_test.rb
# test/browser/agent_organization_profile_test.rb
rm test/browser/agent_ticket_attachment_test.rb
rm test/browser/agent_ticket_auto_assignment_test.rb
rm test/browser/agent_ticket_create_cc_tokenizer_test.rb
rm test/browser/agent_ticket_create_reset_customer_selection_test.rb
rm test/browser/agent_ticket_create_template_test.rb
rm test/browser/agent_ticket_email_reply_keep_body_test.rb
rm test/browser/agent_ticket_email_signature_test.rb
rm test/browser/agent_ticket_link_test.rb
@@ -78,14 +82,18 @@ elif [ "$LEVEL" == '2' ]; then
rm test/browser/aac_basic_richtext_test.rb
# test/browser/abb_one_group_test.rb
rm test/browser/admin_channel_email_test.rb
rm test/browser/admin_calendar_sla_test.rb
rm test/browser/admin_object_manager_test.rb
rm test/browser/admin_object_manager_tree_select_test.rb
rm test/browser/admin_overview_test.rb
#rm test/browser/admin_role_test.rb
rm test/browser/agent_navigation_and_title_test.rb
rm test/browser/agent_organization_profile_test.rb
rm test/browser/agent_ticket_attachment_test.rb
rm test/browser/agent_ticket_auto_assignment_test.rb
rm test/browser/agent_ticket_create_cc_tokenizer_test.rb
rm test/browser/agent_ticket_create_reset_customer_selection_test.rb
rm test/browser/agent_ticket_create_template_test.rb
rm test/browser/agent_ticket_email_reply_keep_body_test.rb
rm test/browser/agent_ticket_email_signature_test.rb
rm test/browser/agent_ticket_link_test.rb
@@ -141,14 +149,18 @@ elif [ "$LEVEL" == '3' ]; then
rm test/browser/aac_basic_richtext_test.rb
# test/browser/abb_one_group_test.rb
rm test/browser/admin_channel_email_test.rb
rm test/browser/admin_calendar_sla_test.rb
rm test/browser/admin_object_manager_test.rb
rm test/browser/admin_object_manager_tree_select_test.rb
rm test/browser/admin_overview_test.rb
rm test/browser/admin_role_test.rb
rm test/browser/agent_navigation_and_title_test.rb
rm test/browser/agent_organization_profile_test.rb
# test/browser/agent_ticket_attachment_test.rb
# test/browser/agent_ticket_auto_assignment_test.rb
#rm test/browser/agent_ticket_create_cc_tokenizer_test.rb
# test/browser/agent_ticket_create_reset_customer_selection_test.rb
# test/browser/agent_ticket_create_template_test.rb
# test/browser/agent_ticket_email_reply_keep_body_test.rb
# test/browser/agent_ticket_email_signature_test.rb
# test/browser/agent_ticket_link_test.rb
@@ -204,14 +216,18 @@ elif [ "$LEVEL" == '4' ]; then
rm test/browser/aac_basic_richtext_test.rb
# test/browser/abb_one_group_test.rb
rm test/browser/admin_channel_email_test.rb
rm test/browser/admin_calendar_sla_test.rb
rm test/browser/admin_object_manager_test.rb
rm test/browser/admin_object_manager_tree_select_test.rb
rm test/browser/admin_overview_test.rb
rm test/browser/admin_role_test.rb
rm test/browser/agent_navigation_and_title_test.rb
rm test/browser/agent_organization_profile_test.rb
rm test/browser/agent_ticket_attachment_test.rb
rm test/browser/agent_ticket_auto_assignment_test.rb
rm test/browser/agent_ticket_create_cc_tokenizer_test.rb
rm test/browser/agent_ticket_create_reset_customer_selection_test.rb
rm test/browser/agent_ticket_create_template_test.rb
rm test/browser/agent_ticket_email_reply_keep_body_test.rb
rm test/browser/agent_ticket_email_signature_test.rb
rm test/browser/agent_ticket_link_test.rb
@@ -266,14 +282,18 @@ elif [ "$LEVEL" == '5' ]; then
rm test/browser/aac_basic_richtext_test.rb
# test/browser/abb_one_group_test.rb
# test/browser/admin_channel_email_test.rb
# test/browser/admin_calendar_sla_test.rb
# test/browser/admin_object_manager_test.rb
# test/browser/admin_object_manager_tree_select_test.rb
# test/browser/admin_overview_test.rb
rm test/browser/admin_role_test.rb
rm test/browser/agent_navigation_and_title_test.rb
rm test/browser/agent_organization_profile_test.rb
rm test/browser/agent_ticket_attachment_test.rb
rm test/browser/agent_ticket_auto_assignment_test.rb
rm test/browser/agent_ticket_create_cc_tokenizer_test.rb
rm test/browser/agent_ticket_create_reset_customer_selection_test.rb
rm test/browser/agent_ticket_create_template_test.rb
rm test/browser/agent_ticket_email_reply_keep_body_test.rb
rm test/browser/agent_ticket_email_signature_test.rb
rm test/browser/agent_ticket_link_test.rb
@@ -331,14 +351,18 @@ elif [ "$LEVEL" == '6' ]; then
rm test/browser/aac_basic_richtext_test.rb
rm test/browser/abb_one_group_test.rb
rm test/browser/admin_channel_email_test.rb
rm test/browser/admin_calendar_sla_test.rb
rm test/browser/admin_object_manager_test.rb
rm test/browser/admin_object_manager_tree_select_test.rb
rm test/browser/admin_overview_test.rb
rm test/browser/admin_role_test.rb
rm test/browser/agent_navigation_and_title_test.rb
rm test/browser/agent_organization_profile_test.rb
rm test/browser/agent_ticket_attachment_test.rb
rm test/browser/agent_ticket_auto_assignment_test.rb
rm test/browser/agent_ticket_create_cc_tokenizer_test.rb
rm test/browser/agent_ticket_create_reset_customer_selection_test.rb
rm test/browser/agent_ticket_create_template_test.rb
rm test/browser/agent_ticket_email_reply_keep_body_test.rb
rm test/browser/agent_ticket_email_signature_test.rb
rm test/browser/agent_ticket_link_test.rb

View File

@@ -2,9 +2,7 @@ require 'rails_helper'
RSpec.describe CheckForObjectAttributes, type: :db_migration do
it 'performs no action for new systems' do
system_init_done(false)
it 'performs no action for new systems', system_init_done: false do
migrate do |instance|
expect(instance).not_to receive(:attributes)
end
@@ -13,8 +11,6 @@ RSpec.describe CheckForObjectAttributes, type: :db_migration do
context 'valid [:data_option]' do
it 'does not change converted text attribute' do
system_init_done
attribute = create(:object_manager_attribute_text)
expect do
@@ -25,8 +21,6 @@ RSpec.describe CheckForObjectAttributes, type: :db_migration do
end
it 'does not change select attribute' do
system_init_done
attribute = create(:object_manager_attribute_select)
expect do
@@ -35,13 +29,21 @@ RSpec.describe CheckForObjectAttributes, type: :db_migration do
attribute.reload.data_option
}
end
it 'does not change tree_select attribute' do
attribute = create(:object_manager_attribute_tree_select)
expect do
migrate
end.not_to change {
attribute.reload.data_option
}
end
end
context '[:data_option]' do
it 'ensures an empty Hash' do
system_init_done
attribute = create(:object_manager_attribute_text, data_option: nil)
migrate
attribute.reload
@@ -53,8 +55,6 @@ RSpec.describe CheckForObjectAttributes, type: :db_migration do
context '[:data_option][:options]' do
it 'ensures an empty Hash' do
system_init_done
attribute = create(:object_manager_attribute_text, data_option: {})
migrate
attribute.reload
@@ -63,8 +63,6 @@ RSpec.describe CheckForObjectAttributes, type: :db_migration do
end
it 'converts String to Hash' do
system_init_done
wrong = {
default: '',
options: '',
@@ -84,8 +82,6 @@ RSpec.describe CheckForObjectAttributes, type: :db_migration do
context '[:data_option][:relation]' do
it 'ensures an empty String' do
system_init_done
wrong = {
default: '',
options: {},
@@ -100,8 +96,6 @@ RSpec.describe CheckForObjectAttributes, type: :db_migration do
end
it 'converts Hash to String' do
system_init_done
wrong = {
default: '',
options: {},

View File

@@ -0,0 +1,224 @@
require 'rails_helper'
RSpec.describe Issue1660FixTreeSelectConfigurations, type: :db_migration do
it 'corrects broken data_option options' do
# as provided in issue #1775
expected = [
{
'name' => 'Blaak',
'value' => 'Blaak',
'children' => [
{
'name' => 'BL.-1',
'value' => 'Blaak::BL.-1',
'children' => [
{
'name' => 'BL.-1.3',
'value' => 'Blaak::BL.-1::BL.-1.3',
},
{
'name' => 'BL.-1.19',
'value' => 'Blaak::BL.-1::BL.-1.19',
}
]
},
{
'name' => 'BL.0',
'value' => 'Blaak::BL.0',
'children' => [
{
'name' => 'BL.00.10a',
'value' => 'Blaak::BL.0::BL.00.10a',
},
{
'name' => 'BL.00.7',
'value' => 'Blaak::BL.0::BL.00.7',
},
{
'name' => 'BL.01.18',
'value' => 'Blaak::BL.0::BL.01.18',
},
{
'name' => 'BL.00.12',
'value' => 'Blaak::BL.0::BL.00.12',
},
{
'name' => 'BL.e0.3',
'value' => 'Blaak::BL.0::BL.e0.3',
},
]
},
{
'name' => 'BL.1',
'value' => 'Blaak::BL.1',
'children' => [
{
'name' => 'BL.01.i',
'value' => 'Blaak::BL.1::BL.01.i',
},
]
},
{
'name' => 'BL.2',
'value' => 'Blaak::BL.2',
'children' => [
{
'name' => 'BL.02.2',
'value' => 'Blaak::BL.2::BL.02.2',
},
{
'name' => 'BL.02.4',
'value' => 'Blaak::BL.2::BL.02.4',
},
{
'name' => 'BL.02.5',
'value' => 'Blaak::BL.2::BL.02.5',
},
{
'name' => 'BL.02.6',
'value' => 'Blaak::BL.2::BL.02.6',
},
{
'name' => 'BL.02.7',
'value' => 'Blaak::BL.2::BL.02.7',
},
]
},
],
}
]
broken = [
{
'name' => 'Blaak',
'value' => 'Blaak',
'children' => [
{
'name' => 'BL.-1',
'value' => 'Blaak::BL.-1',
'children' => [
{
'name' => 'BL.-1.3',
'value' => 'Blaak::BL.2::BL.-1.3',
},
{
'name' => 'BL.-1.19',
'value' => 'Blaak::BL.2::BL.-1.19',
}
]
},
{
'name' => 'BL.0',
'value' => 'Blaak::BL.0',
'children' => [
{
'name' => 'BL.00.10a',
'value' => 'Blaak::BL.2::BL.00.10a',
},
{
'name' => 'BL.00.7',
'value' => 'Blaak::BL.2::BL.00.7',
},
{
'name' => 'BL.01.18',
'value' => 'Blaak::BL.2::BL.01.18',
},
{
'name' => 'BL.00.12',
'value' => 'Blaak::BL.2::BL.00.12',
},
{
'name' => 'BL.e0.3',
'value' => 'Blaak::BL.2::BL.e0.3',
},
]
},
{
'name' => 'BL.1',
'value' => 'Blaak::BL.1',
'children' => [
{
'name' => 'BL.01.i',
'value' => 'Blaak::BL.2::BL.01.i',
},
]
},
{
'name' => 'BL.2',
'value' => 'Blaak::BL.2',
'children' => [
{
'name' => 'BL.02.2',
'value' => 'Blaak::BL.2::BL.02.2',
},
{
'name' => 'BL.02.4',
'value' => 'Blaak::BL.2::BL.02.4',
},
{
'name' => 'BL.02.5',
'value' => 'Blaak::BL.2::BL.02.5',
},
{
'name' => 'BL.02.6',
'value' => 'Blaak::BL.2::BL.02.6',
},
{
'name' => 'BL.02.7',
'value' => 'Blaak::BL.2::BL.02.7',
},
]
},
],
}
]
attribute = create(:object_manager_attribute_tree_select, data_option: { options: broken })
expect do
migrate
end.to change {
attribute.reload.data_option[:options]
}
expect(attribute.data_option[:options]).to eq(expected)
end
it 'performs no action for new systems', system_init_done: false do
migrate do |instance|
expect(instance).not_to receive(:attributes)
end
end
it 'skips blank data_option' do
attribute = create(:object_manager_attribute_tree_select, data_option: {})
expect do
migrate
end.not_to change {
attribute.reload.data_option
}
end
it 'skips blank data_option options' do
attribute = create(:object_manager_attribute_tree_select, data_option: { options: [] })
expect do
migrate
end.not_to change {
attribute.reload.data_option[:options]
}
end
it 'does not change correct data_option options' do
attribute = create(:object_manager_attribute_tree_select)
expect do
migrate
end.not_to change {
attribute.reload.data_option[:options]
}
end
end

View File

@@ -0,0 +1,82 @@
require 'rails_helper'
RSpec.describe Issue1905ExchangeLoginFromRemoteId, type: :db_migration do
let(:backend) { ::Import::Exchange }
it 'removes :item_id from attributes' do
invalid_config = {
attributes: {
item_id: 'login',
some: 'other',
}
}
valid_config = ActiveSupport::HashWithIndifferentAccess.new(
attributes: {
some: 'other',
}
)
expect(backend).to receive(:config).and_return(invalid_config)
allow(backend).to receive(:config).and_call_original
migrate
expect(backend.config).to eq(valid_config)
end
context 'no changes' do
it 'performs no action for new systems', system_init_done: false do
expect(backend).not_to receive(:config)
migrate
end
shared_examples 'irrelevant config' do
it 'does not change the config' do
expect(backend).to receive(:config).and_return(config)
expect(backend).not_to receive(:config=)
migrate
end
end
context 'blank config' do
let(:config) { nil }
it_behaves_like 'irrelevant config'
end
context 'blank attributes' do
let(:config) do
{
some: 'config'
}
end
it_behaves_like 'irrelevant config'
end
context 'blank attribute :item_id' do
let(:config) do
{
attributes: {
some: 'mapping'
}
}
end
it_behaves_like 'irrelevant config'
end
context 'attribute :item_id not mapping to login' do
let(:config) do
{
attributes: {
item_id: 'other_local_attribute'
}
}
end
it_behaves_like 'irrelevant config'
end
end
end

View File

@@ -72,4 +72,115 @@ FactoryBot.define do
}
end
end
factory :object_manager_attribute_tree_select, parent: :object_manager_attribute do
data_type 'tree_select'
data_option do
{
'options' => [
{
'name' => 'Incident',
'value' => 'Incident',
'children' => [
{
'name' => 'Hardware',
'value' => 'Incident::Hardware',
'children' => [
{
'name' => 'Monitor',
'value' => 'Incident::Hardware::Monitor'
},
{
'name' => 'Mouse',
'value' => 'Incident::Hardware::Mouse'
},
{
'name' => 'Keyboard',
'value' => 'Incident::Hardware::Keyboard'
}
]
},
{
'name' => 'Softwareproblem',
'value' => 'Incident::Softwareproblem',
'children' => [
{
'name' => 'CRM',
'value' => 'Incident::Softwareproblem::CRM'
},
{
'name' => 'EDI',
'value' => 'Incident::Softwareproblem::EDI'
},
{
'name' => 'SAP',
'value' => 'Incident::Softwareproblem::SAP',
'children' => [
{
'name' => 'Authentication',
'value' => 'Incident::Softwareproblem::SAP::Authentication'
},
{
'name' => 'Not reachable',
'value' => 'Incident::Softwareproblem::SAP::Not reachable'
}
]
},
{
'name' => 'MS Office',
'value' => 'Incident::Softwareproblem::MS Office',
'children' => [
{
'name' => 'Excel',
'value' => 'Incident::Softwareproblem::MS Office::Excel'
},
{
'name' => 'PowerPoint',
'value' => 'Incident::Softwareproblem::MS Office::PowerPoint'
},
{
'name' => 'Word',
'value' => 'Incident::Softwareproblem::MS Office::Word'
},
{
'name' => 'Outlook',
'value' => 'Incident::Softwareproblem::MS Office::Outlook'
}
]
}
]
}
]
},
{
'name' => 'Service request',
'value' => 'Service request',
'children' => [
{
'name' => 'New software requirement',
'value' => 'Service request::New software requirement'
},
{
'name' => 'New hardware',
'value' => 'Service request::New hardware'
},
{
'name' => 'Consulting',
'value' => 'Service request::Consulting'
}
]
},
{
'name' => 'Change request',
'value' => 'Change request'
}
],
'default' => '',
'null' => true,
'relation' => '',
'maxlength' => 255,
'nulloption' => true,
}
end
end
end

View File

@@ -11,7 +11,7 @@ RSpec.describe Sequencer::Unit::Import::Common::ObjectAttribute::SanitizedName,
context 'sanitizes' do
it 'whitespaces' do
it 'replaces whitespaces' do
provided = process do |instance|
expect(instance).to receive(:unsanitized_name).and_return('model name')
end
@@ -19,7 +19,7 @@ RSpec.describe Sequencer::Unit::Import::Common::ObjectAttribute::SanitizedName,
expect(provided[:sanitized_name]).to eq('model_name')
end
it 'dashes' do
it 'replaces dashes' do
provided = process do |instance|
expect(instance).to receive(:unsanitized_name).and_return('model-name')
end
@@ -27,7 +27,7 @@ RSpec.describe Sequencer::Unit::Import::Common::ObjectAttribute::SanitizedName,
expect(provided[:sanitized_name]).to eq('model_name')
end
it 'ids suffix' do
it 'replaces ids suffix' do
provided = process do |instance|
expect(instance).to receive(:unsanitized_name).and_return('Model Ids')
end
@@ -35,12 +35,20 @@ RSpec.describe Sequencer::Unit::Import::Common::ObjectAttribute::SanitizedName,
expect(provided[:sanitized_name]).to eq('model_nos')
end
it 'id suffix' do
it 'replaces id suffix' do
provided = process do |instance|
expect(instance).to receive(:unsanitized_name).and_return('Model Id')
end
expect(provided[:sanitized_name]).to eq('model_no')
end
it 'replaces non-ASCII characters' do
provided = process do |instance|
expect(instance).to receive(:unsanitized_name).and_return('Ærøskøbing Ät Mödél')
end
expect(provided[:sanitized_name]).to eq('a_eroskobing_at_model')
end
end
end

View File

@@ -0,0 +1,39 @@
require 'rails_helper'
RSpec.describe Sequencer::Unit::Import::Zendesk::Ticket::Comment::SourceBased, sequencer: :unit do
before(:all) do
described_class.class_eval do
private
def email
'test@example.com'
end
end
end
def parameters_for_channel(channel)
{
resource: double(
via: double(
channel: channel
)
)
}
end
context 'for resource.via.channel attribute' do
it 'provides from existing method' do
provided = process(parameters_for_channel('email'))
expect(provided[:source_based]).to eq('test@example.com')
end
it 'provides nil for non existing method' do
provided = process(parameters_for_channel('system'))
expect(provided[:source_based]).to be_nil
end
end
end

View File

@@ -98,56 +98,24 @@ RSpec.describe Scheduler do
described_class.cleanup
end
it 'keeps unlocked Delayed::Job-s' do
# meta :)
described_class.delay.cleanup
context 'Delayed::Job' do
expect do
simulate_threads_call
end.not_to change {
Delayed::Job.count
}
end
context 'locked Delayed::Job' do
it 'gets destroyed' do
it 'keeps unlocked' do
# meta :)
described_class.delay.cleanup
# lock job (simluates interrupted scheduler task)
locked_job = Delayed::Job.last
locked_job.update!(locked_at: Time.zone.now)
expect do
simulate_threads_call
end.to change {
end.not_to change {
Delayed::Job.count
}.by(-1)
}
end
context 'respond to reschedule?' do
context 'locked' do
it 'gets rescheduled for positive responses' do
SpecSpace::DelayedJobBackend.reschedule = true
SpecSpace::DelayedJobBackend.delay.start
# lock job (simluates interrupted scheduler task)
locked_job = Delayed::Job.last
locked_job.update!(locked_at: Time.zone.now)
expect do
simulate_threads_call
end.to not_change {
Delayed::Job.count
}.and change {
Delayed::Job.last.locked_at
}
end
it 'gets destroyed for negative responses' do
SpecSpace::DelayedJobBackend.reschedule = false
SpecSpace::DelayedJobBackend.delay.start
it 'gets destroyed' do
# meta :)
described_class.delay.cleanup
# lock job (simluates interrupted scheduler task)
locked_job = Delayed::Job.last
@@ -159,6 +127,78 @@ RSpec.describe Scheduler do
Delayed::Job.count
}.by(-1)
end
context 'respond to reschedule?' do
it 'gets rescheduled for positive responses' do
SpecSpace::DelayedJobBackend.reschedule = true
SpecSpace::DelayedJobBackend.delay.start
# lock job (simluates interrupted scheduler task)
locked_job = Delayed::Job.last
locked_job.update!(locked_at: Time.zone.now)
expect do
simulate_threads_call
end.to not_change {
Delayed::Job.count
}.and change {
Delayed::Job.last.locked_at
}
end
it 'gets destroyed for negative responses' do
SpecSpace::DelayedJobBackend.reschedule = false
SpecSpace::DelayedJobBackend.delay.start
# lock job (simluates interrupted scheduler task)
locked_job = Delayed::Job.last
locked_job.update!(locked_at: Time.zone.now)
expect do
simulate_threads_call
end.to change {
Delayed::Job.count
}.by(-1)
end
end
end
end
context 'ImportJob' do
context 'affected job' do
let(:job) { create(:import_job, started_at: 5.minutes.ago) }
it 'finishes stuck jobs' do
expect do
simulate_threads_call
end.to change {
job.reload.finished_at
}
end
it 'adds an error message to the result' do
expect do
simulate_threads_call
end.to change {
job.reload.result[:error]
}
end
end
it "doesn't change jobs added after stop" do
job = create(:import_job)
expect do
simulate_threads_call
end.not_to change {
job.reload
}
end
end
end

View File

@@ -175,7 +175,7 @@ RSpec.describe Ticket do
describe '#perform_changes' do
it 'performes a ticket state change on a ticket' do
it 'performs a ticket state change on a ticket' do
source_ticket = create(:ticket)
changes = {
@@ -188,7 +188,7 @@ RSpec.describe Ticket do
expect(source_ticket.state.name).to eq('closed')
end
it 'performes a ticket deletion on a ticket' do
it 'performs a ticket deletion on a ticket' do
source_ticket = create(:ticket)
changes = {
@@ -201,6 +201,25 @@ RSpec.describe Ticket do
expect(ticket_with_source_ids).to match_array([])
end
# Regression test for https://github.com/zammad/zammad/issues/2001
it 'does not modify its arguments' do
trigger = Trigger.new(
perform: {
# rubocop:disable Lint/InterpolationCheck
'notification.email' => {
body: "Hello \#{ticket.customer.firstname} \#{ticket.customer.lastname},",
recipient: %w[article_last_sender ticket_owner ticket_customer ticket_agents],
subject: "Autoclose (\#{ticket.title})"
}
# rubocop:enable Lint/InterpolationCheck
}
)
expect { Ticket.first.perform_changes(trigger.perform, 'trigger', {}, 1) }
.to not_change { trigger.perform['notification.email'][:body] }
.and not_change { trigger.perform['notification.email'][:subject] }
end
end
describe '#selectors' do

View File

@@ -27,6 +27,27 @@ module DbMigrationHelper
instance.migrate(direction)
end
end
def self.included(base)
# Execute in RSpec class context
base.class_exec do
# This method simulates a system that is is already initialized
# aka `Setting.exists?(name: 'system_init_done')`
# It's possible to simulate a not yet initialized system by adding the
# meta tag `system_init_done` to `false` to the needing example:
#
# @example
# it 'does stuff in an unitialized system', system_init_done: false do
#
before(:each) do |example|
initialized = example.metadata.fetch(:system_init_done, true)
system_init_done(initialized)
end
end
end
end
RSpec.configure do |config|

View File

@@ -1,5 +1,11 @@
module SystemInitDoneHelper
def system_init_done(state = true)
# generally allow all calls to Setting.find_by to avoid
# RSpec errors where a different Setting is accessed
allow(Setting).to receive(:find_by).and_call_original
# just mock the Setting check for `system_init_done`
# and return the given parameter value
expect(Setting).to receive(:find_by).with(name: 'system_init_done').and_return(state)
end
end

View File

@@ -28,6 +28,13 @@ class AAbUnitTest < TestCase
value: '0',
)
location(url: browser_url + '/tests_model_binding')
sleep 8
match(
css: '.result .failed',
value: '0',
)
location(url: browser_url + '/tests_model_ui')
sleep 8
match(

View File

@@ -0,0 +1,84 @@
require 'browser_test_helper'
class AdminCalendarSlaTest < TestCase
def test_calendar
@browser = browser_instance
login(
username: 'master@example.com',
password: 'test',
url: browser_url,
)
tasks_close_all()
calendar_name = "ZZZ some calendar #{rand(99_999_999)}"
sla_name = "ZZZ some sla #{rand(99_999_999)}"
timezone = 'Europe/Berlin'
timezone_verify = 'Europe/Berlin (GMT+2)'
calendar_create(
data: {
name: calendar_name,
timezone: timezone,
}
)
# got to maintanance
click(css: '[href="#manage"]')
click(css: '[href="#system/maintenance"]')
watch_for(
css: '.content.active',
value: 'Enable or disable the maintenance mode',
timeout: 4,
)
# go back
click(css: '[href="#manage"]')
click(css: '[href="#manage/calendars"]')
watch_for(
css: '.content.active',
value: calendar_name,
timeout: 4,
)
logout()
login(
username: 'master@example.com',
password: 'test',
)
# check if admin exists
click(css: '[href="#manage"]')
click(css: '[href="#manage/calendars"]')
watch_for(
css: '.content.active',
value: calendar_name,
timeout: 4,
)
#@browser.execute_script('$(\'.content.active table tr td:contains(" ' + data[:name] + '")\').first().click()')
@browser.execute_script('$(\'.content.active .main .js-edit\').last().click()')
modal_ready(browser: @browser)
watch_for(
css: '.content.active .modal input[name=name]',
value: calendar_name,
timeout: 4,
)
watch_for(
css: '.content.active .modal input.js-input',
value: Regexp.quote(timezone_verify),
timeout: 4,
)
modal_close()
sla_create(
data: {
name: sla_name,
calendar: "#{calendar_name} - #{timezone}",
first_response_time_in_text: 61
},
)
end
end

View File

@@ -0,0 +1,119 @@
require 'browser_test_helper'
class AdminObjectManagerTreeSelectTest < TestCase
def test_basic_a
@browser = browser_instance
login(
username: 'master@example.com',
password: 'test',
url: browser_url,
)
tasks_close_all()
object_manager_attribute_create(
data: {
name: 'browser_test_tree_select1',
display: 'Browser Test TreeSelect1',
data_type: 'Tree Select',
data_option: {
options: {
'Incident' => {
'Hardware' => {
'Monitor' => {},
'Mouse' => {},
'Keyboard' => {},
},
'Softwareproblem' => {
'CRM' => {},
'EDI' => {},
'SAP' => {
'Authentication' => {},
'Not reachable' => {},
},
'MS Office' => {
'Excel' => {},
'PowerPoint' => {},
'Word' => {},
'Outlook' => {},
},
},
},
'Service request' => {
'New software requirement' => {},
'New hardware' => {},
'Consulting' => {},
},
'Change request' => {},
},
},
},
)
watch_for(
css: '.content.active',
value: 'Database Update required',
)
click(css: '.content.active .tab-pane.active div.js-execute')
watch_for(
css: '.modal',
value: 'restart',
)
watch_for_disappear(
css: '.modal',
timeout: 120,
)
sleep 5
watch_for(
css: '.content.active',
)
# discard new attribute
click(css: 'a[href="#manage"]')
click(css: 'a[href="#system/object_manager"]')
watch_for(
css: '.content.active table',
value: 'browser_test_tree_select1',
)
match_not(
css: '.content.active',
value: 'Database Update required',
)
object_manager_attribute_delete(
data: {
name: 'browser_test_tree_select1',
},
)
watch_for(
css: '.content.active',
value: 'Database Update required',
)
watch_for(
css: '.content.active table',
value: 'browser_test_tree_select1',
)
click(css: '.content.active .tab-pane.active div.js-execute')
watch_for(
css: '.modal',
value: 'restart',
)
watch_for_disappear(
css: '.modal',
timeout: 120,
)
sleep 5
watch_for(
css: '.content.active',
)
match_not(
css: '.content.active',
value: 'Database Update required',
)
match_not(
css: '.content.active table',
value: 'browser_test_tree_select1',
)
end
end

View File

@@ -0,0 +1,33 @@
require 'browser_test_helper'
# Regression test for UI bugfix
# https://github.com/zammad/zammad/issues/1990
#
# Ensure that CC field when creating a new ticket is autocompleting user emails
class AgentTicketCreateCcTokenizerTest < TestCase
def test_tokenizer
@browser = browser_instance
login(
username: 'agent1@example.com',
password: 'test',
url: browser_url,
)
tasks_close_all()
click(
css: 'a[href="#ticket/create"]'
)
email_out_css = '.content.active li[data-type=email-out]'
watch_for(
css: email_out_css
)
click(css: email_out_css)
token_verify('.content.active input[name=cc]', 'test@example.com')
end
end

View File

@@ -0,0 +1,97 @@
require 'browser_test_helper'
# Regression test for UI bugfix
# https://github.com/zammad/zammad/issues/1669
#
# After creating a new ticket template, logging out, and logging back in,
# ensure that the template selection menu still contains the new entry.
class AgentTicketCreateTemplateTest < TestCase
def test_ticket_template_creation
@browser = browser_instance
login(
username: 'agent1@example.com',
password: 'test',
url: browser_url,
)
tasks_close_all()
click(
css: 'a[href="#ticket/create"]'
)
watch_for(
css: '.active .templates-welcome',
displayed: true
)
set(
css: 'input[name="title"]',
value: 'my first ticket'
)
click(
css: '.active .templates-welcome .js-create'
)
watch_for(
css: '.active .templates-manage',
displayed: true,
timeout: 3,
)
exists_not(
css: '.active .templates-manage select[name="id"] > option:not([value=""])'
)
# save new template
set(
css: '.active .templates-manage .js-name',
value: 'test template'
)
click(
css: '.active .templates-manage .js-save'
)
exists(
css: '.active .templates-manage select[name="id"] > option:not([value=""])',
displayed: true
)
# check if relogin temlates are still available
logout
login(
username: 'agent1@example.com',
password: 'test',
)
click(
css: '.navigation > .tasks > a.task'
)
exists(
css: '.active .templates-manage',
displayed: true
)
exists(
css: '.active .templates-manage select[name="id"] > option:not([value=""])',
displayed: true
)
# apply new tempalte
tasks_close_all()
click(
css: 'a[href="#ticket/create"]'
)
watch_for(
css: '.active .templates-manage',
displayed: true,
timeout: 3,
)
select(
css: '.active .templates-manage select[name="id"]',
value: 'test template',
)
click(
css: '.active .templates-manage .js-apply'
)
exists(
css: '.active .newTicket input[name="title"]',
value: 'my first ticket'
)
end
end

View File

@@ -2,7 +2,7 @@
require 'browser_test_helper'
class AgentUserManageTest < TestCase
def test_agent_user
def test_agent_customer_ticket_create
customer_user_email = 'customer-test-' + rand(999_999).to_s + '@example.com'
firstname = 'Customer Firstname'
lastname = 'Customer Lastname'
@@ -16,58 +16,66 @@ class AgentUserManageTest < TestCase
)
tasks_close_all()
sleep 1
# create customer
click(css: 'a[href="#new"]', only_if_exists: true)
click(css: 'a[href="#ticket/create"]')
click(css: '.active .newTicket [name="customer_id_completion"]')
watch_for(
css: '.content.active .newTicket',
timeout: 1,
)
click(css: '.content.active .newTicket [name="customer_id_completion"]')
# check if pulldown is open, it's not working stable via selenium
@browser.execute_script( "$('.active .newTicket .js-recipientDropdown').addClass('open')" )
@browser.execute_script("$('.active .newTicket .js-recipientDropdown').addClass('open')")
sleep 1
sendkey(value: :arrow_down)
sleep 0.5
click(css: '.active .newTicket .recipientList-entry.js-objectNew')
sleep 1
click(css: '.content.active .newTicket .recipientList-entry.js-objectNew')
watch_for(
css: '.content.active .modal',
timeout: 1,
)
set(
css: '.modal input[name="firstname"]',
css: '.content.active .modal input[name="firstname"]',
value: firstname,
)
set(
css: '.modal input[name="lastname"]',
css: '.content.active .modal input[name="lastname"]',
value: lastname,
)
set(
css: '.modal input[name="email"]',
css: '.content.active .modal input[name="email"]',
value: customer_user_email,
)
click(css: '.modal button.js-submit')
click(css: '.content.active .modal button.js-submit')
sleep 4
# check is used to check selected
match(
css: '.active input[name="customer_id"]',
css: '.content.active .newTicket input[name="customer_id"]',
value: '^\d+$',
no_quote: true,
)
match(
css: '.active input[name="customer_id_completion"]',
css: '.content.active .newTicket input[name="customer_id_completion"]',
value: firstname,
)
match(
css: '.active input[name="customer_id_completion"]',
css: '.content.active .newTicket input[name="customer_id_completion"]',
value: lastname,
)
match(
css: '.active input[name="customer_id_completion"]',
css: '.content.active .newTicket input[name="customer_id_completion"]',
value: customer_user_email,
)
match(
css: '.active input[name="customer_id_completion"]',
css: '.content.active .newTicket input[name="customer_id_completion"]',
value: fullname,
)
sleep 4
@@ -77,47 +85,162 @@ class AgentUserManageTest < TestCase
click(css: 'a[href="#new"]', only_if_exists: true)
click(css: 'a[href="#ticket/create"]')
sleep 2
watch_for(
css: '.content.active .newTicket',
timeout: 1,
)
match(
css: '.active input[name="customer_id"]',
css: '.content.active .newTicket input[name="customer_id"]',
value: '',
)
match(
css: '.active input[name="customer_id_completion"]',
css: '.content.active .newTicket input[name="customer_id_completion"]',
value: '',
)
set(
css: '.active .newTicket input[name="customer_id_completion"]',
css: '.content.active .newTicket input[name="customer_id_completion"]',
value: customer_user_email,
)
sleep 3
click(css: '.active .newTicket .recipientList-entry.js-object.is-active')
click(css: '.content.active .newTicket .recipientList-entry.js-object.is-active')
sleep 1
# check is used to check selected
match(
css: '.active input[name="customer_id"]',
css: '.content.active .newTicket input[name="customer_id"]',
value: '^\d+$',
no_quote: true,
)
match(
css: '.active input[name="customer_id_completion"]',
css: '.content.active .newTicket input[name="customer_id_completion"]',
value: firstname,
)
match(
css: '.active input[name="customer_id_completion"]',
css: '.content.active .newTicket input[name="customer_id_completion"]',
value: lastname,
)
match(
css: '.active input[name="customer_id_completion"]',
css: '.content.active .newTicket input[name="customer_id_completion"]',
value: customer_user_email,
)
match(
css: '.active input[name="customer_id_completion"]',
css: '.content.active .newTicket input[name="customer_id_completion"]',
value: fullname,
)
end
def test_agent_customer_ticket_zoom
customer_user_email = 'customer-test-' + rand(999_999).to_s + '@example.com'
firstname = 'Customer Firstname'
lastname = 'Customer Lastname'
fullname = "#{firstname} #{lastname} <#{customer_user_email}>"
@browser = browser_instance
login(
username: 'agent1@example.com',
password: 'test',
url: browser_url,
)
tasks_close_all()
ticket_create(
data: {
customer: 'nico',
group: 'Users',
title: 'some changes',
body: 'some body',
},
)
watch_for(
css: '.content.active .ticketZoom-header .ticket-number',
value: '\d',
)
click(css: '.content.active .tabsSidebar-tabs .tabsSidebar-tab[data-tab="customer"]')
match(
css: '.content.active .tabsSidebar .sidebar[data-tab="customer"]',
value: 'Nicole Braun',
)
click(css: '.content.active .tabsSidebar .sidebar[data-tab="customer"] .js-actions')
click(css: '.content.active .tabsSidebar .sidebar[data-tab="customer"] .js-actions li[data-type="customer-change"]')
watch_for(
css: '.content.active .modal',
)
click(css: '.content.active .modal [name="customer_id_completion"]')
# check if pulldown is open, it's not working stable via selenium
@browser.execute_script("$('.active .modal .js-recipientDropdown').addClass('open')")
sleep 1
sendkey(value: :arrow_down)
sleep 0.5
click(css: '.content.active .modal .recipientList-entry.js-objectNew')
watch_for(
css: '.content.active .modal input[name="firstname"]',
timeout: 1,
)
set(
css: '.content.active .modal input[name="firstname"]',
value: firstname,
)
set(
css: '.content.active .modal input[name="lastname"]',
value: lastname,
)
set(
css: '.content.active .modal input[name="email"]',
value: customer_user_email,
)
# there are 2 models, take the correct one
#click(css: '.content.active .modal button.js-submit')
@browser.execute_script("$('.content.active .modal input[name=\"firstname\"]').closest('form').find('button.js-submit').click()")
# check is used to check selected
watch_for(
css: '.content.active .modal input[name="customer_id"]',
value: '^\d+$',
no_quote: true,
)
match(
css: '.content.active .modal input[name="customer_id_completion"]',
value: firstname,
)
match(
css: '.content.active .modal input[name="customer_id_completion"]',
value: lastname,
)
match(
css: '.content.active .modal input[name="customer_id_completion"]',
value: customer_user_email,
)
match(
css: '.content.active .modal input[name="customer_id_completion"]',
value: fullname,
)
click(css: '.content.active .modal button.js-submit')
watch_for_disappear(
css: '.content.active .modal',
)
watch_for(
css: '.content.active .tabsSidebar .sidebar[data-tab="customer"]',
value: customer_user_email,
timeout: 4,
)
end
end

View File

@@ -44,9 +44,7 @@ class TestCase < Test::Unit::TestCase
end
def browser_instance
if !@browsers
@browsers = {}
end
@browsers ||= {}
if ENV['REMOTE_URL'].blank?
local_browser = Selenium::WebDriver.for(browser.to_sym, profile: profile)
@browsers[local_browser.hash] = local_browser
@@ -136,7 +134,7 @@ class TestCase < Test::Unit::TestCase
browser: browser1,
username: 'someuser',
password: 'somepassword',
url: 'some url', # optional
url: 'some url', # optional, in case of aleady opened brower a reload is done because url is called again
remember_me: true, # optional
auto_wizard: false, # optional, in case of auto wizard, skip login
success: false, #optional
@@ -487,6 +485,26 @@ class TestCase < Test::Unit::TestCase
screenshot(browser: instance, comment: 'scroll_to_after')
end
=begin
modal_close(
browser: browser1,
)
=end
def modal_close(params = {})
switch_window_focus(params)
log('modal_close', params)
instance = params[:browser] || @browser
element = instance.find_elements(css: '.modal .js-close')[0]
raise "No such modal to close #{params.inspect}" if !element
element.click
end
=begin
modal_ready(
@@ -1251,7 +1269,9 @@ set type of task (closeTab, closeNextInOverview, stayOnTab)
watch_for(
browser: browser1,
css: '#content .text-1',
container: element # optional, defaults to browser, must exist at the time of dispatch
css: '#content .text-1', # xpath or css required
xpath: '/content[contains(@class,".text-1")]', # xpath or css required
value: 'some text',
attribute: 'some_attribute' # optional
timeout: 16, # in sec, default 16
@@ -1263,7 +1283,15 @@ set type of task (closeTab, closeNextInOverview, stayOnTab)
switch_window_focus(params)
log('watch_for', params)
instance = params[:browser] || @browser
browser = params[:browser] || @browser
instance = params[:container] || browser
selector = params[:css] || params[:xpath]
selector_type = if params.key?(:css)
:css
elsif params.key?(:xpath)
:xpath
end
timeout = 16
if params[:timeout]
@@ -1272,12 +1300,12 @@ set type of task (closeTab, closeNextInOverview, stayOnTab)
loops = timeout.to_i * 2
text = ''
(1..loops).each do
element = instance.find_elements(css: params[:css])[0]
element = instance.find_elements(selector_type => selector)[0]
if element #&& element.displayed?
begin
# watch for selector
if !params[:attribute] && !params[:value]
assert(true, "'#{params[:css]}' found")
assert(true, "'#{selector}' found")
sleep 0.5
return true
@@ -1285,7 +1313,7 @@ set type of task (closeTab, closeNextInOverview, stayOnTab)
else
text = if params[:attribute]
element.attribute(params[:attribute])
elsif params[:css].match?(/(input|textarea)/i)
elsif selector.match?(/(input|textarea)/i)
element.attribute('value')
else
element.text
@@ -1302,9 +1330,9 @@ set type of task (closeTab, closeNextInOverview, stayOnTab)
end
sleep 0.5
end
screenshot(browser: instance, comment: 'watch_for_failed')
screenshot(browser: browser, comment: 'watch_for_failed')
if !params[:attribute] && !params[:value]
raise "'#{params[:css]}' not found"
raise "'#{selector}' not found"
end
raise "'#{params[:value]}' not found in '#{text}'"
end
@@ -2717,12 +2745,72 @@ wait untill text in selector disabppears
assert(true, 'user created')
end
=begin
calendar_create(
browser: browser2,
data: {
name: 'some calendar' + random,
first_response_time_in_text: 61
},
)
=end
def calendar_create(params = {})
switch_window_focus(params)
log('calendar_create', params)
instance = params[:browser] || @browser
data = params[:data]
click(
browser: instance,
css: 'a[href="#manage"]',
mute_log: true,
)
click(
browser: instance,
css: '.content.active a[href="#manage/calendars"]',
mute_log: true,
)
sleep 4
click(
browser: instance,
css: '.content.active a.js-new',
mute_log: true,
)
modal_ready(browser: instance)
element = instance.find_elements(css: '.content.active .modal input[name=name]')[0]
element.clear
element.send_keys(data[:name])
element = instance.find_elements(css: '.content.active .modal .js-input')[0]
element.clear
element.send_keys(data[:timezone])
element.send_keys(:enter)
instance.find_elements(css: '.modal button.js-submit')[0].click
modal_disappear(browser: instance)
7.times do
element = instance.find_elements(css: 'body')[0]
text = element.text
if text.match?(/#{Regexp.quote(data[:name])}/)
assert(true, 'calendar created')
sleep 1
return true
end
sleep 1
end
screenshot(browser: instance, comment: 'calendar_create_failed')
raise 'calendar creation failed'
end
=begin
sla_create(
browser: browser2,
data: {
name: 'some sla' + random,
calendar: 'some calendar name',
first_response_time_in_text: 61
},
)
@@ -2755,6 +2843,11 @@ wait untill text in selector disabppears
element = instance.find_elements(css: '.modal input[name=name]')[0]
element.clear
element.send_keys(data[:name])
if data[:calendar].present?
element = instance.find_elements(css: '.modal select[name="calendar_id"]')[0]
dropdown = Selenium::WebDriver::Support::Select.new(element)
dropdown.select_by(:text, data[:calendar])
end
element = instance.find_elements(css: '.modal input[name=first_response_time_in_text]')[0]
element.clear
element.send_keys(data[:first_response_time_in_text])
@@ -3362,6 +3455,11 @@ wait untill text in selector disabppears
element.clear
element.send_keys(data[:data_option][:options][:false])
# rubocop:enable Lint/BooleanSymbol
elsif data[:data_type] == 'Tree Select'
add_tree_options(
instance: instance,
options: data[:data_option][:options],
)
else
data[:data_option][:options].each do |key, value|
element = instance.find_elements(css: '.modal .js-Table .js-key').last
@@ -3695,4 +3793,80 @@ wait untill text in selector disabppears
return if params[:mute_log]
puts "#{Time.zone.now}/#{method}: #{params.inspect}"
end
private
def add_tree_options(instance:, options:)
# first level entries have to get added in regular order
options.each_key.with_index do |option, index|
if index != 0
element = instance.find_elements(css: '.modal .js-treeTable .js-addRow')[index - 1]
element.click
end
element = instance.find_elements(css: '.modal .js-treeTable .js-key')[index]
element.clear
element.send_keys(option)
end
add_sub_tree_recursion(
instance: instance,
options: options,
)
end
def add_sub_tree_recursion(instance:, options:, offset: 0)
options.each_value.inject(offset) do |child_offset, children|
child_offset += 1
# put your recursion glasses on 8-)
add_sub_tree_options(
instance: instance,
options: children,
offset: child_offset,
)
end
end
def add_sub_tree_options(instance:, options:, offset:)
# sub level entries have to get added in reversed order
level_options = options.to_a.reverse.to_h.keys
level_options.each do |option|
# sub level entries have to get added via 'add child row' link
click_index = offset - 1
element = instance.find_elements(css: '.modal .js-treeTable .js-addChild')[click_index]
element.click
element = instance.find_elements(css: '.modal .js-treeTable .js-key')[offset]
element.clear
element.send_keys(option)
sleep 0.25
end
add_sub_tree_recursion(
instance: instance,
options: options,
offset: offset,
)
end
def token_verify(css, value)
original_element = @browser.find_element(:css, css)
elem = original_element.find_element(xpath: '../input[contains(@class, "token-input")]')
elem.send_keys value
elem.send_keys :enter
watch_for(
xpath: '../*/span[contains(@class,"token-label")]',
value: value,
container: original_element
)
end
end

View File

@@ -0,0 +1,90 @@
require 'test_helper'
class CalendarControllerTest < ActionDispatch::IntegrationTest
setup do
# set accept header
@headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json' }
# create agent
roles = Role.where(name: %w[Admin Agent])
groups = Group.all
UserInfo.current_user_id = 1
@admin = User.create_or_update(
login: 'calendar-admin',
firstname: 'Packages',
lastname: 'Admin',
email: 'calendar-admin@example.com',
password: 'adminpw',
active: true,
roles: roles,
groups: groups,
)
end
test '01 calendar index with nobody' do
get '/api/v1/calendars', params: {}, headers: @headers
assert_response(401)
result = JSON.parse(@response.body)
assert_equal(Hash, result.class)
assert_equal('authentication failed', result['error'])
get '/api/v1/calendars_init', params: {}, headers: @headers
assert_response(401)
result = JSON.parse(@response.body)
assert_equal(Hash, result.class)
assert_equal('authentication failed', result['error'])
end
test '02 calendar index with admin' do
credentials = ActionController::HttpAuthentication::Basic.encode_credentials('calendar-admin@example.com', 'adminpw')
# index
get '/api/v1/calendars', params: {}, headers: @headers.merge('Authorization' => credentials)
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(Array, result.class)
assert(result)
assert_equal(1, result.count)
get '/api/v1/calendars?expand=true', params: {}, headers: @headers.merge('Authorization' => credentials)
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(Array, result.class)
assert(result)
assert_equal(1, result.count)
get '/api/v1/calendars?full=true', params: {}, headers: @headers.merge('Authorization' => credentials)
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(Hash, result.class)
assert(result)
assert(result['record_ids'])
assert_equal(1, result['record_ids'].count)
assert(result['assets'])
assert(result['assets'].present?)
# index
get '/api/v1/calendars_init', params: {}, headers: @headers.merge('Authorization' => credentials)
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(Hash, result.class)
assert(result['record_ids'])
assert(result['ical_feeds'])
assert_equal('Denmark', result['ical_feeds']['http://www.google.com/calendar/ical/da.danish%23holiday%40group.v.calendar.google.com/public/basic.ics'])
assert_equal('Austria', result['ical_feeds']['http://www.google.com/calendar/ical/de.austrian%23holiday%40group.v.calendar.google.com/public/basic.ics'])
assert(result['timezones'])
assert_equal(2, result['timezones']['Africa/Johannesburg'])
assert_equal(-8, result['timezones']['America/Sitka'])
assert(result['assets'])
end
end

View File

@@ -0,0 +1,70 @@
require 'test_helper'
class SlaControllerTest < ActionDispatch::IntegrationTest
setup do
# set accept header
@headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json' }
# create agent
roles = Role.where(name: %w[Admin Agent])
groups = Group.all
UserInfo.current_user_id = 1
@admin = User.create_or_update(
login: 'sla-admin',
firstname: 'Packages',
lastname: 'Admin',
email: 'sla-admin@example.com',
password: 'adminpw',
active: true,
roles: roles,
groups: groups,
)
end
test '01 sla index with nobody' do
get '/api/v1/slas', params: {}, headers: @headers
assert_response(401)
result = JSON.parse(@response.body)
assert_equal(Hash, result.class)
assert_equal('authentication failed', result['error'])
end
test '02 sla index with admin' do
credentials = ActionController::HttpAuthentication::Basic.encode_credentials('sla-admin@example.com', 'adminpw')
get '/api/v1/slas', params: {}, headers: @headers.merge('Authorization' => credentials)
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(Array, result.class)
assert(result)
assert_equal(0, result.count)
get '/api/v1/slas?expand=true', params: {}, headers: @headers.merge('Authorization' => credentials)
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(Array, result.class)
assert(result)
assert_equal(0, result.count)
get '/api/v1/slas?full=true', params: {}, headers: @headers.merge('Authorization' => credentials)
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(Hash, result.class)
assert(result)
assert(result['record_ids'])
assert(result['record_ids'].blank?)
assert(result['assets'])
assert(result['assets']['Calendar'].present?)
assert(result['assets'].present?)
end
end

View File

@@ -269,8 +269,10 @@ class ClearbitTest < ActiveSupport::TestCase
assert_equal('', customer6.firstname)
assert_equal('', customer6.lastname)
assert_equal('', customer6.note)
assert_equal('http://clearbit.com', customer6.web)
assert_equal('3030 16th St, San Francisco, CA 94103, USA', customer6.address)
assert_equal('', customer6.web)
#assert_equal('http://clearbit.com', customer6.web)
assert_equal('San Francisco, CA 94103, USA', customer6.address)
#assert_equal('3030 16th St, San Francisco, CA 94103, USA', customer6.address)
#assert_equal('San Francisco, CA, USA', customer6.address)
organization6 = Organization.find_by(name: 'APIHub, Inc')

View File

@@ -428,7 +428,7 @@ class MonitoringControllerTest < ActionDispatch::IntegrationTest
assert(result['message'])
assert(result['issues'])
assert_equal(false, result['healthy'])
assert_equal("Channel: Email::Notification out ;unprocessable mails: 1;scheduler not running;Failed to run import backend 'Import::Ldap'. Cause: Some bad error;Stuck import backend 'Import::Ldap' detected. Last update: #{15.minutes.ago}", result['message'])
assert_equal("Channel: Email::Notification out ;unprocessable mails: 1;scheduler not running;Failed to run import backend 'Import::Ldap'. Cause: Some bad error;Stuck import backend 'Import::Ldap' detected. Last update: #{stuck_updated_at_timestamp}", result['message'])
Setting.set('ldap_integration', false)
end

View File

@@ -41,6 +41,9 @@ class UserDeviceControllerTest < ActionDispatch::IntegrationTest
ENV['TEST_REMOTE_IP'] = '5.9.62.170' # de
ENV['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:46.0) Gecko/20100101 Firefox/46.0'
ENV['SWITCHED_FROM_USER_ID'] = nil
UserDevice.destroy_all
end
test '01 - index with nobody' do
@@ -173,6 +176,15 @@ class UserDeviceControllerTest < ActionDispatch::IntegrationTest
test '04 - login index with admin with fingerprint - II' do
UserDevice.create!(
user_id: @admin.id,
name: 'test 1',
location: 'some location',
user_agent: 'some user agent',
ip: '127.0.0.1',
fingerprint: 'fingerprintI',
)
params = { fingerprint: 'my_finger_print_II', username: 'user-device-admin', password: 'adminpw' }
post '/api/v1/signin', params: params.to_json, headers: @headers
assert_response(201)
@@ -180,13 +192,13 @@ class UserDeviceControllerTest < ActionDispatch::IntegrationTest
Scheduler.worker(true)
assert_equal(3, UserDevice.where(user_id: @admin.id).count)
assert_equal(2, UserDevice.where(user_id: @admin.id).count)
assert_equal(1, email_notification_count('user_device_new', @admin.email))
assert_equal(0, email_notification_count('user_device_new_location', @admin.email))
assert_equal(result.class, Hash)
assert_not(result['error'])
assert(result['config'])
assert('my_finger_print_II', controller.session[:user_device_fingerprint])
assert('my_finger_print_III', controller.session[:user_device_fingerprint])
get '/api/v1/users', params: params.to_json, headers: @headers
assert_response(200)
@@ -195,7 +207,7 @@ class UserDeviceControllerTest < ActionDispatch::IntegrationTest
Scheduler.worker(true)
assert_equal(3, UserDevice.where(user_id: @admin.id).count)
assert_equal(2, UserDevice.where(user_id: @admin.id).count)
assert_equal(1, email_notification_count('user_device_new', @admin.email))
assert_equal(0, email_notification_count('user_device_new_location', @admin.email))
@@ -210,7 +222,7 @@ class UserDeviceControllerTest < ActionDispatch::IntegrationTest
Scheduler.worker(true)
assert_equal(3, UserDevice.where(user_id: @admin.id).count)
assert_equal(2, UserDevice.where(user_id: @admin.id).count)
assert_equal(1, email_notification_count('user_device_new', @admin.email))
assert_equal(0, email_notification_count('user_device_new_location', @admin.email))
@@ -223,7 +235,7 @@ class UserDeviceControllerTest < ActionDispatch::IntegrationTest
Scheduler.worker(true)
assert_equal(4, UserDevice.where(user_id: @admin.id).count)
assert_equal(3, UserDevice.where(user_id: @admin.id).count)
assert_equal(1, email_notification_count('user_device_new', @admin.email))
assert_equal(1, email_notification_count('user_device_new_location', @admin.email))
@@ -234,6 +246,16 @@ class UserDeviceControllerTest < ActionDispatch::IntegrationTest
test '05 - login index with admin with fingerprint - II' do
UserDevice.add(
ENV['HTTP_USER_AGENT'],
ENV['TEST_REMOTE_IP'],
@admin.id,
'my_finger_print_II',
'session', # session|basic_auth|token_auth|sso
)
assert_equal(1, UserDevice.where(user_id: @admin.id).count)
params = { fingerprint: 'my_finger_print_II', username: 'user-device-admin', password: 'adminpw' }
post '/api/v1/signin', params: params.to_json, headers: @headers
assert_response(201)
@@ -241,7 +263,7 @@ class UserDeviceControllerTest < ActionDispatch::IntegrationTest
Scheduler.worker(true)
assert_equal(4, UserDevice.where(user_id: @admin.id).count)
assert_equal(1, UserDevice.where(user_id: @admin.id).count)
assert_equal(0, email_notification_count('user_device_new', @admin.email))
assert_equal(0, email_notification_count('user_device_new_location', @admin.email))
assert_equal(result.class, Hash)
@@ -252,9 +274,19 @@ class UserDeviceControllerTest < ActionDispatch::IntegrationTest
test '06 - login index with admin with basic auth' do
ENV['HTTP_USER_AGENT'] = 'curl 1.2.3'
ENV['HTTP_USER_AGENT'] = 'curl 1.0.0'
UserDevice.add(
ENV['HTTP_USER_AGENT'],
'127.0.0.1',
@admin.id,
'',
'basic_auth', # session|basic_auth|token_auth|sso
)
assert_equal(1, UserDevice.where(user_id: @admin.id).count)
credentials = ActionController::HttpAuthentication::Basic.encode_credentials('user-device-admin', 'adminpw')
ENV['HTTP_USER_AGENT'] = 'curl 1.2.3'
params = {}
get '/api/v1/users', params: params.to_json, headers: @headers.merge('Authorization' => credentials)
assert_response(200)
@@ -262,7 +294,7 @@ class UserDeviceControllerTest < ActionDispatch::IntegrationTest
Scheduler.worker(true)
assert_equal(5, UserDevice.where(user_id: @admin.id).count)
assert_equal(2, UserDevice.where(user_id: @admin.id).count)
assert_equal(1, email_notification_count('user_device_new', @admin.email))
assert_equal(0, email_notification_count('user_device_new_location', @admin.email))
assert_equal(result.class, Array)
@@ -276,7 +308,7 @@ class UserDeviceControllerTest < ActionDispatch::IntegrationTest
Scheduler.worker(true)
assert_equal(5, UserDevice.where(user_id: @admin.id).count)
assert_equal(2, UserDevice.where(user_id: @admin.id).count)
assert_equal(1, email_notification_count('user_device_new', @admin.email))
assert_equal(0, email_notification_count('user_device_new_location', @admin.email))
assert_equal(result.class, Array)
@@ -294,7 +326,7 @@ class UserDeviceControllerTest < ActionDispatch::IntegrationTest
Scheduler.worker(true)
assert_equal(5, UserDevice.where(user_id: @admin.id).count)
assert_equal(2, UserDevice.where(user_id: @admin.id).count)
assert_equal(1, email_notification_count('user_device_new', @admin.email))
assert_equal(0, email_notification_count('user_device_new_location', @admin.email))
assert_equal(result.class, Array)
@@ -307,6 +339,17 @@ class UserDeviceControllerTest < ActionDispatch::IntegrationTest
test '07 - login index with admin with basic auth' do
ENV['HTTP_USER_AGENT'] = 'curl 1.2.3'
UserDevice.add(
ENV['HTTP_USER_AGENT'],
ENV['TEST_REMOTE_IP'],
@admin.id,
'',
'basic_auth', # session|basic_auth|token_auth|sso
)
assert_equal(1, UserDevice.where(user_id: @admin.id).count)
credentials = ActionController::HttpAuthentication::Basic.encode_credentials('user-device-admin', 'adminpw')
params = {}
@@ -316,7 +359,7 @@ class UserDeviceControllerTest < ActionDispatch::IntegrationTest
Scheduler.worker(true)
assert_equal(5, UserDevice.where(user_id: @admin.id).count)
assert_equal(1, UserDevice.where(user_id: @admin.id).count)
assert_equal(0, email_notification_count('user_device_new', @admin.email))
assert_equal(0, email_notification_count('user_device_new_location', @admin.email))
assert_equal(result.class, Array)
@@ -348,11 +391,20 @@ class UserDeviceControllerTest < ActionDispatch::IntegrationTest
test '09 - login index with agent with basic auth' do
ENV['HTTP_USER_AGENT'] = 'curl 1.2.3'
UserDevice.add(
ENV['HTTP_USER_AGENT'],
ENV['TEST_REMOTE_IP'],
@agent.id,
'',
'basic_auth', # session|basic_auth|token_auth|sso
)
assert_equal(1, UserDevice.where(user_id: @agent.id).count)
assert_equal(0, email_notification_count('user_device_new', @agent.email))
assert_equal(0, email_notification_count('user_device_new_location', @agent.email))
ENV['HTTP_USER_AGENT'] = 'curl 1.2.3'
credentials = ActionController::HttpAuthentication::Basic.encode_credentials('user-device-agent', 'agentpw')
params = {}
@@ -371,7 +423,7 @@ class UserDeviceControllerTest < ActionDispatch::IntegrationTest
test '10 - login with switched_from_user_id' do
assert_equal(1, UserDevice.where(user_id: @agent.id).count)
assert_equal(0, UserDevice.where(user_id: @agent.id).count)
assert_equal(0, email_notification_count('user_device_new', @agent.email))
assert_equal(0, email_notification_count('user_device_new_location', @agent.email))
@@ -384,7 +436,7 @@ class UserDeviceControllerTest < ActionDispatch::IntegrationTest
Scheduler.worker(true)
assert_equal(1, UserDevice.where(user_id: @agent.id).count)
assert_equal(0, UserDevice.where(user_id: @agent.id).count)
assert_equal(0, email_notification_count('user_device_new', @agent.email))
assert_equal(0, email_notification_count('user_device_new_location', @agent.email))
assert_equal(result.class, Hash)
@@ -394,7 +446,7 @@ class UserDeviceControllerTest < ActionDispatch::IntegrationTest
Scheduler.worker(true)
assert_equal(1, UserDevice.where(user_id: @agent.id).count)
assert_equal(0, UserDevice.where(user_id: @agent.id).count)
assert_equal(0, email_notification_count('user_device_new', @agent.email))
assert_equal(0, email_notification_count('user_device_new_location', @agent.email))
@@ -408,7 +460,7 @@ class UserDeviceControllerTest < ActionDispatch::IntegrationTest
Scheduler.worker(true)
assert_equal(1, UserDevice.where(user_id: @agent.id).count)
assert_equal(0, UserDevice.where(user_id: @agent.id).count)
assert_equal(0, email_notification_count('user_device_new', @agent.email))
assert_equal(0, email_notification_count('user_device_new_location', @agent.email))
ENV['USER_DEVICE_UPDATED_AT'] = nil
@@ -424,9 +476,85 @@ class UserDeviceControllerTest < ActionDispatch::IntegrationTest
# ip reset
ENV['TEST_REMOTE_IP'] = '5.9.62.170' # de
assert_equal(1, UserDevice.where(user_id: @agent.id).count)
assert_equal(0, UserDevice.where(user_id: @agent.id).count)
assert_equal(0, email_notification_count('user_device_new', @agent.email))
assert_equal(0, email_notification_count('user_device_new_location', @agent.email))
end
test '11 - login with invalid fingerprint' do
assert_equal(0, UserDevice.where(user_id: @admin.id).count)
assert_equal(0, email_notification_count('user_device_new', @admin.email))
assert_equal(0, email_notification_count('user_device_new_location', @admin.email))
params = { fingerprint: 'to_long_1234567890to_long_1234567890to_long_1234567890to_long_1234567890to_long_1234567890to_long_1234567890to_long_1234567890to_long_1234567890to_long_1234567890to_long_1234567890to_long_1234567890', username: 'user-device-admin', password: 'adminpw' }
post '/api/v1/signin', params: params.to_json, headers: @headers
assert_response(422)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_equal('fingerprint is 198 chars but can only be 160 chars!', result['error'])
assert_not(result['config'])
assert_not(controller.session[:user_device_fingerprint])
Scheduler.worker(true)
assert_equal(0, UserDevice.where(user_id: @admin.id).count)
assert_equal(0, email_notification_count('user_device_new', @admin.email))
assert_equal(0, email_notification_count('user_device_new_location', @admin.email))
end
test '12 - login with integer as fingerprint' do
assert_equal(0, UserDevice.where(user_id: @admin.id).count)
assert_equal(0, email_notification_count('user_device_new', @admin.email))
assert_equal(0, email_notification_count('user_device_new_location', @admin.email))
params = { fingerprint: 123_456_789, username: 'user-device-admin', password: 'adminpw' }
post '/api/v1/signin', params: params.to_json, headers: @headers
assert_response(201)
result = JSON.parse(@response.body)
assert(123_456_789, controller.session[:user_device_fingerprint])
Scheduler.worker(true)
assert_equal(1, UserDevice.where(user_id: @admin.id).count)
assert_equal(0, email_notification_count('user_device_new', @admin.email))
assert_equal(0, email_notification_count('user_device_new_location', @admin.email))
assert_equal(result.class, Hash)
assert_nil(result['error'])
end
test '13 - login form controller - check no user device logging' do
Setting.set('form_ticket_create', true)
assert_equal(0, UserDevice.where(user_id: @admin.id).count)
assert_equal(0, email_notification_count('user_device_new', @admin.email))
assert_equal(0, email_notification_count('user_device_new_location', @admin.email))
credentials = ActionController::HttpAuthentication::Basic.encode_credentials('user-device-admin', 'adminpw')
params = {
fingerprint: 'long_1234567890long_1234567890long_1234567890long_1234567890long_1234567890long_1234567890long_1234567890long_1234567890long_1234567890long_1234567890long_1234567890'
}
post '/api/v1/form_config', params: params.to_json, headers: @headers.merge('Authorization' => credentials)
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(result.class, Hash)
assert_not(result['error'])
assert(result['endpoint'])
assert_not(controller.session[:user_device_fingerprint])
Scheduler.worker(true)
assert_equal(0, UserDevice.where(user_id: @admin.id).count)
assert_equal(0, email_notification_count('user_device_new', @admin.email))
assert_equal(0, email_notification_count('user_device_new_location', @admin.email))
end
end

View File

@@ -49,8 +49,8 @@ class EmailBuildTest < ActiveSupport::TestCase
{
'Mime-Type' => 'image/png',
:content => 'xxx',
:filename => 'somename.png',
},
:filename => 'somename.png'
}
],
)
@@ -103,8 +103,8 @@ class EmailBuildTest < ActiveSupport::TestCase
{
'Mime-Type' => 'image/png',
:content => 'xxx',
:filename => 'somename.png',
},
:filename => 'somename.png'
}
],
)
@@ -114,6 +114,7 @@ class EmailBuildTest < ActiveSupport::TestCase
>'
assert_equal(should, mail.text_part.body.to_s)
assert_nil(mail.html_part)
assert_equal('image/png; filename=somename.png', mail.attachments[0].content_type)
parser = Channel::EmailParser.new
data = parser.parse(mail.to_s)
@@ -137,6 +138,88 @@ class EmailBuildTest < ActiveSupport::TestCase
end
end
test 'plain email + attachment check 2' do
ticket1 = Ticket.create!(
title: 'some article helper test1',
group: Group.lookup(name: 'Users'),
customer_id: 2,
state: Ticket::State.lookup(name: 'new'),
priority: Ticket::Priority.lookup(name: '2 normal'),
updated_by_id: 1,
created_by_id: 1,
)
assert(ticket1, 'ticket created')
# create inbound article #1
article1 = Ticket::Article.create!(
ticket_id: ticket1.id,
from: 'some_sender@example.com',
to: 'some_recipient@example.com',
subject: 'some subject',
message_id: 'some@id',
content_type: 'text/html',
body: 'some message article helper test1 <div><img style="width: 85.5px; height: 49.5px" src="cid:15.274327094.140938@zammad.example.com">asdasd<img src="cid:15.274327094.140939@zammad.example.com"><br>',
internal: false,
sender: Ticket::Article::Sender.find_by(name: 'Customer'),
type: Ticket::Article::Type.find_by(name: 'email'),
updated_by_id: 1,
created_by_id: 1,
)
store1 = Store.add(
object: 'Ticket::Article',
o_id: article1.id,
data: 'content_file1_normally_should_be_an_ics_calendar_file',
filename: 'schedule.ics',
preferences: {
'Mime-Type' => 'text/calendar'
},
created_by_id: 1,
)
text = '> Welcome!
>
> Thank you for installing Zammad. äöüß
>'
mail = Channel::EmailBuild.build(
from: 'sender@example.com',
to: 'recipient@example.com',
body: text,
attachments: [
store1
],
)
should = '> Welcome!
>
> Thank you for installing Zammad. äöüß
>'
assert_equal(should, mail.text_part.body.to_s)
assert_nil(mail.html_part)
assert_equal('text/calendar; filename=schedule.ics', mail.attachments[0].content_type)
parser = Channel::EmailParser.new
data = parser.parse(mail.to_s)
# check body
assert_equal(should, data[:body])
# check count of attachments, 2
assert_equal(1, data[:attachments].length)
# check attachments
data[:attachments]&.each do |attachment|
if attachment[:filename] == 'schedule.ics'
assert(attachment[:preferences]['Content-ID'])
assert_nil(attachment[:preferences]['content-alternative'])
assert_equal('text/calendar', attachment[:preferences]['Mime-Type'])
assert_equal('UTF-8', attachment[:preferences]['Charset'])
else
assert(false, "invalid attachment, should not be there, #{attachment.inspect}")
end
end
end
test 'plain email + without attachment check' do
text = '> Welcome!
>

View File

@@ -663,4 +663,87 @@ Comment: [] =
assert_equal(ticket_count, Ticket.count)
end
test 'icinga email autoclose' do
Setting.set('icinga_sender', 'zaihan@example.com')
email_raw_string = 'Return-Path: <support@example.com>
Received: from 04747418efb9 ([175.137.28.47])
by smtp.example.com with ESMTPSA id r14sm6448824pfa.163.2018.04.03.10.10.59
for <support@example.com>
(version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128);
Tue, 03 Apr 2018 10:11:00 -0700 (PDT)
From: "zaihan@example.com" <zaihan@example.com>
X-Google-Original-From: "zaihan@example.com" <zaihan@example.com>
Message-ID: <301139.467253478-sendEmail@04747418efb9>
To: "support@example.com" <support@example.com>
Subject: PROBLEM - Awesell - is DOWN
Date: Tue, 3 Apr 2018 17:11:04 +0000
X-Mailer: sendEmail-1.56
MIME-Version: 1.0
Content-Type: multipart/related; boundary="----MIME delimiter for sendEmail-587258.191387267"
This is a multi-part message in MIME format. To properly display this message you need a MIME-Version 1.0 compliant Email program.
------MIME delimiter for sendEmail-587258.191387267
Content-Type: text/plain;
charset="iso-8859-1"
Content-Transfer-Encoding: 7bit
***** Host Monitoring on 04747418efb9 *****
Awesell is DOWN!
Info: PING CRITICAL - Packet loss = 100%
When: 2018-04-03 17:11:04 +0000
Host: Awesell
IPv4: 192.168.1.8
------MIME delimiter for sendEmail-587258.191387267--'
ticket_0, article_p, user_p, mail = Channel::EmailParser.new.process({}, email_raw_string)
assert_equal('new', ticket_0.state.name)
assert(ticket_0.preferences)
assert(ticket_0.preferences['icinga'])
assert_equal('DOWN', ticket_0.preferences['icinga']['state'])
email_raw_string = 'Return-Path: <support@example.com>
Received: from 04747418efb9 ([175.137.28.47])
by smtp.example.com with ESMTPSA id b73sm6127782pga.62.2018.04.03.10.31.00
for <support@example.com>
(version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128);
Tue, 03 Apr 2018 10:31:01 -0700 (PDT)
From: "zaihan@example.com" <zaihan@example.com>
X-Google-Original-From: "zaihan@example.com" <zaihan@example.com>
Message-ID: <601339.882795827-sendEmail@04747418efb9>
To: "support@example.com" <support@example.com>
Subject: RECOVERY - Awesell - is UP
Date: Tue, 3 Apr 2018 17:31:05 +0000
X-Mailer: sendEmail-1.56
MIME-Version: 1.0
Content-Type: multipart/related; boundary="----MIME delimiter for sendEmail-322998.239033954"
This is a multi-part message in MIME format. To properly display this message you need a MIME-Version 1.0 compliant Email program.
------MIME delimiter for sendEmail-322998.239033954
Content-Type: text/plain;
charset="iso-8859-1"
Content-Transfer-Encoding: 7bit
***** Host Monitoring on 04747418efb9 *****
Awesell is UP!
Info: PING OK - Packet loss = 68%, RTA = 0.59 ms
When: 2018-04-03 17:31:05 +0000
Host: Awesell
IPv4: 192.168.1.8
------MIME delimiter for sendEmail-322998.239033954--
'
ticket_1, article_p, user_p, mail = Channel::EmailParser.new.process({}, email_raw_string)
assert_equal('closed', ticket_1.state.name)
assert(ticket_1.preferences)
assert(ticket_1.preferences['icinga'])
assert_equal('DOWN', ticket_1.preferences['icinga']['state'])
end
end

View File

@@ -35,6 +35,57 @@ class OnlineNotificationTest < ActiveSupport::TestCase
created_by_id: 1
)
@customer_user = User.lookup(email: 'nicole.braun@zammad.org')
calendar1 = Calendar.create_or_update(
name: 'EU 1 - test',
timezone: 'Europe/Berlin',
business_hours: {
mon: {
active: true,
timeframes: [ ['00:00', '23:59'] ]
},
tue: {
active: true,
timeframes: [ ['00:00', '23:59'] ]
},
wed: {
active: true,
timeframes: [ ['00:00', '23:59'] ]
},
thu: {
active: true,
timeframes: [ ['00:00', '23:59'] ]
},
fri: {
active: true,
timeframes: [ ['00:00', '23:59'] ]
},
sat: {
active: true,
timeframes: [ ['00:00', '23:59'] ]
},
sun: {
active: true,
timeframes: [ ['00:00', '23:59'] ]
},
},
default: true,
ical_url: nil,
updated_by_id: 1,
created_by_id: 1,
)
sla1 = Sla.create_or_update(
name: 'test sla 1',
condition: {},
first_response_time: 20,
update_time: 60,
solution_time: 120,
calendar_id: calendar1.id,
updated_by_id: 1,
created_by_id: 1,
)
end
test 'ticket notification' do

View File

@@ -5,9 +5,49 @@ require 'stats/ticket_waiting_time'
class StatsTicketWaitingTimeTest < ActiveSupport::TestCase
test 'single ticket' do
ticket1 = Ticket.create(
group1 = Group.create!(
name: 'Group 1',
active: true,
email_address: EmailAddress.first,
created_by_id: 1,
updated_by_id: 1,
)
roles = Role.where(name: 'Agent')
user1 = User.create!(
login: 'assets_stats1@example.org',
firstname: 'assets_stats1',
lastname: 'assets_stats1',
email: 'assets_stats1@example.org',
password: 'some_pass',
active: true,
groups: [group1],
roles: roles,
created_by_id: 1,
updated_by_id: 1,
)
user2 = User.create!(
login: 'assets_stats2@example.org',
firstname: 'assets_stats2',
lastname: 'assets_stats2',
email: 'assets_sla2@example.org',
password: 'some_pass',
active: true,
groups: [group1],
roles: roles,
created_by_id: 1,
updated_by_id: 1,
)
result = Stats::TicketWaitingTime.generate(user1)
assert_equal(0, result[:handling_time])
assert_equal('supergood', result[:state])
assert_equal(0, result[:average_per_agent])
assert_equal(0, result[:percent])
ticket1 = Ticket.create!(
title: 'com test 1',
group: Group.lookup(name: 'Users'),
group: group1,
customer_id: 2,
state: Ticket::State.lookup(name: 'new'),
priority: Ticket::Priority.lookup(name: '2 normal'),
@@ -16,7 +56,7 @@ class StatsTicketWaitingTimeTest < ActiveSupport::TestCase
)
# communication 1: waiting time 2 hours (BUT too old yesterday)
Ticket::Article.create(
Ticket::Article.create!(
ticket_id: ticket1.id,
from: 'a@example.com',
to: 'a@example.com',
@@ -31,7 +71,7 @@ class StatsTicketWaitingTimeTest < ActiveSupport::TestCase
created_at: '2017-04-12 08:00',
updated_at: '2017-04-12 08:00',
)
Ticket::Article.create(
Ticket::Article.create!(
ticket_id: ticket1.id,
from: 'a@example.com',
to: 'a@example.com',
@@ -48,7 +88,7 @@ class StatsTicketWaitingTimeTest < ActiveSupport::TestCase
)
# communication 2: waiting time 2 hours
Ticket::Article.create(
Ticket::Article.create!(
ticket_id: ticket1.id,
from: 'a@example.com',
to: 'a@example.com',
@@ -63,7 +103,7 @@ class StatsTicketWaitingTimeTest < ActiveSupport::TestCase
created_at: '2017-04-13 08:00',
updated_at: '2017-04-13 08:00',
)
Ticket::Article.create(
Ticket::Article.create!(
ticket_id: ticket1.id,
from: 'a@example.com',
to: 'a@example.com',
@@ -80,7 +120,7 @@ class StatsTicketWaitingTimeTest < ActiveSupport::TestCase
)
# communication 3: waiting time 4 hours
Ticket::Article.create(
Ticket::Article.create!(
ticket_id: ticket1.id,
from: 'a@example.com',
to: 'a@example.com',
@@ -95,7 +135,7 @@ class StatsTicketWaitingTimeTest < ActiveSupport::TestCase
created_at: '2017-04-13 11:00',
updated_at: '2017-04-13 11:00',
)
Ticket::Article.create(
Ticket::Article.create!(
ticket_id: ticket1.id,
from: 'a@example.com',
to: 'a@example.com',
@@ -112,7 +152,7 @@ class StatsTicketWaitingTimeTest < ActiveSupport::TestCase
)
# communication 4: INVALID waiting time 1 hour (because internal)
Ticket::Article.create(
Ticket::Article.create!(
ticket_id: ticket1.id,
from: 'a@example.com',
to: 'a@example.com',
@@ -127,7 +167,7 @@ class StatsTicketWaitingTimeTest < ActiveSupport::TestCase
created_at: '2017-04-13 15:00',
updated_at: '2017-04-13 15:00',
)
Ticket::Article.create(
Ticket::Article.create!(
ticket_id: ticket1.id,
from: 'a@example.com',
to: 'a@example.com',
@@ -143,13 +183,83 @@ class StatsTicketWaitingTimeTest < ActiveSupport::TestCase
updated_at: '2017-04-13 15:10',
)
average_time = Stats::TicketWaitingTime.calculate_average([ticket1, ticket1], '2017-04-13 00:00:00')
ticket2 = Ticket.create!(
title: 'com test 2',
group: group1,
customer_id: 2,
state: Ticket::State.lookup(name: 'new'),
priority: Ticket::Priority.lookup(name: '2 normal'),
updated_by_id: 1,
created_by_id: 1,
)
average_time = Stats::TicketWaitingTime.calculate_average([ticket1.id, ticket2.id], '2017-04-13 00:00:00')
expected_average_time = 60 * 60 * 2 # for communication 2
expected_average_time += 60 * 60 * 4 # for communication 3
expected_average_time = expected_average_time / 2 # for average
travel_to Time.zone.local(2017, 0o4, 13, 23, 0o0, 44)
assert_equal(expected_average_time, average_time)
result = Stats::TicketWaitingTime.generate(user1)
assert_equal(0, result[:handling_time])
assert_equal('supergood', result[:state])
assert_equal(180, result[:average_per_agent])
assert_equal(0.0, result[:percent])
ticket3 = Ticket.create!(
title: 'com test 3',
group: group1,
customer_id: 2,
owner: user1,
state: Ticket::State.lookup(name: 'new'),
priority: Ticket::Priority.lookup(name: '2 normal'),
updated_by_id: 1,
created_by_id: 1,
)
# communication 1: waiting time 2 hours (BUT too old yesterday)
Ticket::Article.create!(
ticket_id: ticket3.id,
from: 'a@example.com',
to: 'a@example.com',
subject: 'com test 3',
message_id: 'some@id_com_1',
body: 'some message 123',
internal: false,
sender: Ticket::Article::Sender.find_by(name: 'Customer'),
type: Ticket::Article::Type.find_by(name: 'email'),
updated_by_id: 1,
created_by_id: 1,
created_at: '2017-04-13 08:00',
updated_at: '2017-04-13 08:00',
)
Ticket::Article.create!(
ticket_id: ticket3.id,
from: 'a@example.com',
to: 'a@example.com',
subject: 'com test 3',
message_id: 'some@id_com_1',
body: 'some message 123',
internal: false,
sender: Ticket::Article::Sender.find_by(name: 'Agent'),
type: Ticket::Article::Type.find_by(name: 'email'),
updated_by_id: 1,
created_by_id: 1,
created_at: '2017-04-13 09:00',
updated_at: '2017-04-13 09:00',
)
result = Stats::TicketWaitingTime.generate(user1)
assert_equal(60, result[:handling_time])
assert_equal('supergood', result[:state])
assert_equal(140, result[:average_per_agent])
assert_equal(1.0, result[:percent])
travel_back
end
end

View File

@@ -114,12 +114,24 @@ class TicketEscalationTest < ActiveSupport::TestCase
ticket.save!
assert_not(ticket.has_changes_to_save?)
assert(ticket.escalation_at)
assert_equal(ticket_escalation_at.to_s, ticket.escalation_at.to_s)
ticket.title = 'some value 123-1'
ticket.save!
assert_not(ticket.has_changes_to_save?)
assert(ticket.escalation_at)
assert_not_equal(ticket_escalation_at.to_s, ticket.escalation_at.to_s)
sla.destroy!
calendar.destroy!
ticket.save!
assert_not(ticket.has_changes_to_save?)
assert(ticket.escalation_at)
ticket.title = 'some value 123-2'
ticket.save!
assert_not(ticket.has_changes_to_save?)
assert_not(ticket.escalation_at)

View File

@@ -315,4 +315,18 @@ class UserDeviceTest < ActiveSupport::TestCase
end
test 'invalid fingerprint size' do
assert_raises(Exceptions::UnprocessableEntity) do
UserDevice.add(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.107 Safari/537.36',
'91.115.248.231',
@agent.id,
'fingerprint1234fingerprint1234fingerprint1234fingerprint1234fingerprint1234fingerprint1234fingerprint1234fingerprint1234fingerprint1234fingerprint1234fingerprint1234',
'session',
)
end
end
end

Some files were not shown because too many files have changed in this diff Show More