From aca6073ebabe209bb030954b3ca33f0ba4cd61ad Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Tue, 7 Nov 2017 14:42:51 +0100 Subject: [PATCH 001/196] Fixed issue #1618 - Chat language setting or behaviour in dutch. --- public/assets/chat/chat.coffee | 16 ++++ public/assets/chat/chat.js | 139 ++++++++++++++++++--------------- public/assets/chat/chat.min.js | 2 +- 3 files changed, 95 insertions(+), 62 deletions(-) diff --git a/public/assets/chat/chat.coffee b/public/assets/chat/chat.coffee index 23b008a31..6a3731a8f 100644 --- a/public/assets/chat/chat.coffee +++ b/public/assets/chat/chat.coffee @@ -228,6 +228,22 @@ do($ = window.jQuery, window) -> 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Si vous ne répondez pas dans les %s minutes, votre conversation avec %s va être fermée.' 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Si vous ne répondez pas dans les %s minutes, votre conversation va être fermée.' 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Nous sommes désolés, il faut plus de temps que prévu pour obtenir un emplacement vide. Veuillez réessayer ultérieurement ou nous envoyer un courriel. Je vous remercie!' + 'nl': + 'Chat with us!': 'Chat met ons!' + 'Scroll down to see new messages': 'Scrol naar beneden om nieuwe berichten te zien' + 'Online': 'Online' + 'Offline': 'Offline' + 'Connecting': 'Verbinden' + 'Connection re-established': 'Verbinding herstelt' + 'Today': 'Vandaag' + 'Send': 'Verzenden' + 'Compose your message...': 'Typ uw bericht...' + 'All colleagues are busy.': 'Alle medewerkers zijn bezet.' + 'You are on waiting list position %s.': 'U bent %s in de wachtrij.' + 'Start new conversation': 'Nieuwe conversatie starten' + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Omdat u in de laatste %s minuten niets geschreven heeft wordt de conversatie met %s gesloten.' + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Omdat u in de laatste %s minuten niets geschreven heeft is de conversatie gesloten.' + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Het spijt ons, het duurt langer dan verwacht om te antwoorden. Alstublieft probeer het later nogmaals of stuur ons een email. Hartelijk dank!' 'zh-cn': 'Chat with us!': '发起即时对话!' 'Scroll down to see new messages': '向下滚动以查看新消息' diff --git a/public/assets/chat/chat.js b/public/assets/chat/chat.js index 6183a6f63..f3e75b67b 100644 --- a/public/assets/chat/chat.js +++ b/public/assets/chat/chat.js @@ -1,64 +1,3 @@ -if (!window.zammadChatTemplates) { - window.zammadChatTemplates = {}; -} -window.zammadChatTemplates["agent"] = function (__obj) { - if (!__obj) __obj = {}; - var __out = [], __capture = function(callback) { - var out = __out, result; - __out = []; - callback.call(this); - result = __out.join(''); - __out = out; - return __safe(result); - }, __sanitize = function(value) { - if (value && value.ecoSafe) { - return value; - } else if (typeof value !== 'undefined' && value != null) { - return __escape(value); - } else { - return ''; - } - }, __safe, __objSafe = __obj.safe, __escape = __obj.escape; - __safe = __obj.safe = function(value) { - if (value && value.ecoSafe) { - return value; - } else { - if (!(typeof value !== 'undefined' && value != null)) value = ''; - var result = new String(value); - result.ecoSafe = true; - return result; - } - }; - if (!__escape) { - __escape = __obj.escape = function(value) { - return ('' + value) - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"'); - }; - } - (function() { - (function() { - if (this.agent.avatar) { - __out.push('\n\n'); - } - - __out.push('\n\n '); - - __out.push(__sanitize(this.agent.name)); - - __out.push('\n'); - - }).call(this); - - }).call(__obj); - __obj.safe = __objSafe, __obj.escape = __escape; - return __out.join(''); -}; - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, slice = [].slice, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, @@ -418,6 +357,23 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Si vous ne répondez pas dans les %s minutes, votre conversation va être fermée.', 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Nous sommes désolés, il faut plus de temps que prévu pour obtenir un emplacement vide. Veuillez réessayer ultérieurement ou nous envoyer un courriel. Je vous remercie!' }, + 'nl': { + 'Chat with us!': 'Chat met ons!', + 'Scroll down to see new messages': 'Scrol naar beneden om nieuwe berichten te zien', + 'Online': 'Online', + 'Offline': 'Offline', + 'Connecting': 'Verbinden', + 'Connection re-established': 'Verbinding herstelt', + 'Today': 'Vandaag', + 'Send': 'Verzenden', + 'Compose your message...': 'Typ uw bericht...', + 'All colleagues are busy.': 'Alle medewerkers zijn bezet.', + 'You are on waiting list position %s.': 'U bent %s in de wachtrij.', + 'Start new conversation': 'Nieuwe conversatie starten', + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Omdat u in de laatste %s minuten niets geschreven heeft wordt de conversatie met %s gesloten.', + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Omdat u in de laatste %s minuten niets geschreven heeft is de conversatie gesloten.', + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Het spijt ons, het duurt langer dan verwacht om te antwoorden. Alstublieft probeer het later nogmaals of stuur ons een email. Hartelijk dank!' + }, 'zh-cn': { 'Chat with us!': '发起即时对话!', 'Scroll down to see new messages': '向下滚动以查看新消息', @@ -1818,6 +1774,67 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); return window.ZammadChat = ZammadChat; })(window.jQuery, window); +if (!window.zammadChatTemplates) { + window.zammadChatTemplates = {}; +} +window.zammadChatTemplates["agent"] = function (__obj) { + if (!__obj) __obj = {}; + var __out = [], __capture = function(callback) { + var out = __out, result; + __out = []; + callback.call(this); + result = __out.join(''); + __out = out; + return __safe(result); + }, __sanitize = function(value) { + if (value && value.ecoSafe) { + return value; + } else if (typeof value !== 'undefined' && value != null) { + return __escape(value); + } else { + return ''; + } + }, __safe, __objSafe = __obj.safe, __escape = __obj.escape; + __safe = __obj.safe = function(value) { + if (value && value.ecoSafe) { + return value; + } else { + if (!(typeof value !== 'undefined' && value != null)) value = ''; + var result = new String(value); + result.ecoSafe = true; + return result; + } + }; + if (!__escape) { + __escape = __obj.escape = function(value) { + return ('' + value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + }; + } + (function() { + (function() { + if (this.agent.avatar) { + __out.push('\n\n'); + } + + __out.push('\n\n '); + + __out.push(__sanitize(this.agent.name)); + + __out.push('\n'); + + }).call(this); + + }).call(__obj); + __obj.safe = __objSafe, __obj.escape = __escape; + return __out.join(''); +}; + if (!window.zammadChatTemplates) { window.zammadChatTemplates = {}; } diff --git a/public/assets/chat/chat.min.js b/public/assets/chat/chat.min.js index 70fa0ee65..2130c5537 100644 --- a/public/assets/chat/chat.min.js +++ b/public/assets/chat/chat.min.js @@ -1 +1 @@ -window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.agent=function(t){t||(t={});var e=[],s=function(t){return t&&t.ecoSafe?t:void 0!==t&&null!=t?i(t):""},n=t.safe,i=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){this.agent.avatar&&(e.push('\n\n')),e.push('\n\n '),e.push(s(this.agent.name)),e.push("\n")}).call(this)}.call(t),t.safe=n,t.escape=i,e.join("")};var bind=function(t,e){return function(){return t.apply(e,arguments)}},slice=[].slice,extend=function(t,e){function s(){this.constructor=t}for(var n in e)hasProp.call(e,n)&&(t[n]=e[n]);return s.prototype=e.prototype,t.prototype=new s,t.__super__=e.prototype,t},hasProp={}.hasOwnProperty;!function(t,e){var s,n,i,o,a,r,l,h,c;c=document.getElementsByTagName("script"),r=c[c.length-1],l=r.src.match(".*://([^:/]*).*")[1],h=r.src.match("(.*)://[^:/]*.*")[1],s=function(){function e(e){this.options=t.extend({},this.defaults,e),this.log=new i({debug:this.options.debug,logPrefix:this.options.logPrefix||this.logPrefix})}return e.prototype.defaults={debug:!1},e}(),i=function(){function e(e){this.log=bind(this.log,this),this.error=bind(this.error,this),this.notice=bind(this.notice,this),this.debug=bind(this.debug,this),this.options=t.extend({},this.defaults,e)}return e.prototype.defaults={debug:!1},e.prototype.debug=function(){var t;if(t=1<=arguments.length?slice.call(arguments,0):[],this.options.debug)return this.log("debug",t)},e.prototype.notice=function(){var t;return t=1<=arguments.length?slice.call(arguments,0):[],this.log("notice",t)},e.prototype.error=function(){var t;return t=1<=arguments.length?slice.call(arguments,0):[],this.log("error",t)},e.prototype.log=function(e,s){var n,i,o,a;if(s.unshift("||"),s.unshift(e),s.unshift(this.options.logPrefix),console.log.apply(console,s),this.options.debug){for(a="",i=0,o=s.length;i"+a+"")}},e}(),o=function(t){function e(t){this.stop=bind(this.stop,this),this.start=bind(this.start,this),e.__super__.constructor.call(this,t)}return extend(e,s),e.prototype.timeoutStartedAt=null,e.prototype.logPrefix="timeout",e.prototype.defaults={debug:!1,timeout:4,timeoutIntervallCheck:.5},e.prototype.start=function(){var t,e;return this.stop(),e=new Date,t=function(t){return function(){var s;if(s=new Date-new Date(e.getTime()+1e3*t.options.timeout*60),t.log.debug("Timeout check for "+t.options.timeout+" minutes (left "+s/1e3+" sec.)"),!(s<0))return t.stop(),t.options.callback()}}(this),this.log.debug("Start timeout in "+this.options.timeout+" minutes"),this.intervallId=setInterval(t,1e3*this.options.timeoutIntervallCheck*60)},e.prototype.stop=function(){if(this.intervallId)return this.log.debug("Stop timeout of "+this.options.timeout+" minutes"),clearInterval(this.intervallId)},e}(),n=function(t){function n(t){this.ping=bind(this.ping,this),this.send=bind(this.send,this),this.reconnect=bind(this.reconnect,this),this.close=bind(this.close,this),this.connect=bind(this.connect,this),this.set=bind(this.set,this),n.__super__.constructor.call(this,t)}return extend(n,s),n.prototype.logPrefix="io",n.prototype.set=function(t){var e,s,n;s=[];for(e in t)n=t[e],s.push(this.options[e]=n);return s},n.prototype.connect=function(){return this.log.debug("Connecting to "+this.options.host),this.ws=new e.WebSocket(""+this.options.host),this.ws.onopen=function(t){return function(e){return t.log.debug("onOpen",e),t.options.onOpen(e),t.ping()}}(this),this.ws.onmessage=function(t){return function(e){var s,n,i;for(i=JSON.parse(e.data),t.log.debug("onMessage",e.data),s=0,n=i.length;sChat with us!",scrollHint:"Scroll down to see new messages",idleTimeout:6,idleTimeoutIntervallCheck:.5,inactiveTimeout:8,inactiveTimeoutIntervallCheck:.5,waitingListTimeout:4,waitingListTimeoutIntervallCheck:.5},a.prototype.logPrefix="chat",a.prototype._messageCount=0,a.prototype.isOpen=!1,a.prototype.blinkOnlineInterval=null,a.prototype.stopBlinOnlineStateTimeout=null,a.prototype.showTimeEveryXMinutes=2,a.prototype.lastTimestamp=null,a.prototype.lastAddedType=null,a.prototype.inputTimeout=null,a.prototype.isTyping=!1,a.prototype.state="offline",a.prototype.initialQueueDelay=1e4,a.prototype.translations={de:{"Chat with us!":"Chatte mit uns!","Scroll down to see new messages":"Scrolle nach unten um neue Nachrichten zu sehen",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbindung wiederhergestellt",Today:"Heute",Send:"Senden","Compose your message...":"Ihre Nachricht...","All colleagues are busy.":"Alle Kollegen sind belegt.","You are on waiting list position %s.":"Sie sind in der Warteliste an der Position %s.","Start new conversation":"Neue Konversation starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation mit %s geschlossen.","Since you didn't respond in the last %s minutes your conversation got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation geschlossen.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Es tut uns leid, es dauert länger als erwartet, um einen freien Platz zu erhalten. Bitte versuchen Sie es zu einem späteren Zeitpunkt noch einmal oder schicken Sie uns eine E-Mail. Vielen Dank!"},es:{"Chat with us!":"Chatee con nosotros!","Scroll down to see new messages":"Haga scroll hacia abajo para ver nuevos mensajes",Online:"En linea",Offline:"Desconectado",Connecting:"Conectando","Connection re-established":"Conexión restablecida",Today:"Hoy",Send:"Enviar","Compose your message...":"Escriba su mensaje...","All colleagues are busy.":"Todos los agentes están ocupados.","You are on waiting list position %s.":"Usted está en la posición %s de la lista de espera.","Start new conversation":"Iniciar nueva conversación","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Puesto que usted no respondió en los últimos %s minutos su conversación con %s se ha cerrado.","Since you didn't respond in the last %s minutes your conversation got closed.":"Puesto que usted no respondió en los últimos %s minutos su conversación se ha cerrado.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Lo sentimos, se tarda más tiempo de lo esperado para ser atendido por un agente. Inténtelo de nuevo más tarde o envíenos un correo electrónico. ¡Gracias!"},fr:{"Chat with us!":"Chattez avec nous!","Scroll down to see new messages":"Faites défiler pour lire les nouveaux messages",Online:"En-ligne",Offline:"Hors-ligne",Connecting:"Connexion en cours","Connection re-established":"Connexion rétablie",Today:"Aujourdhui",Send:"Envoyer","Compose your message...":"Composez votre message...","All colleagues are busy.":"Tous les collègues sont actuellement occupés.","You are on waiting list position %s.":"Vous êtes actuellement en %s position dans la file d'attente.","Start new conversation":"Démarrer une nouvelle conversation","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Si vous ne répondez pas dans les %s minutes, votre conversation avec %s va être fermée.","Since you didn't respond in the last %s minutes your conversation got closed.":"Si vous ne répondez pas dans les %s minutes, votre conversation va être fermée.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Nous sommes désolés, il faut plus de temps que prévu pour obtenir un emplacement vide. Veuillez réessayer ultérieurement ou nous envoyer un courriel. Je vous remercie!"},"zh-cn":{"Chat with us!":"发起即时对话!","Scroll down to see new messages":"向下滚动以查看新消息",Online:"在线",Offline:"离线",Connecting:"连接中","Connection re-established":"正在重新建立连接",Today:"今天",Send:"发送","Compose your message...":"正在输入信息...","All colleagues are busy.":"所有工作人员都在忙碌中.","You are on waiting list position %s.":"您目前的等候位置是第 %s 位.","Start new conversation":"开始新的会话","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"由于您超过 %s 分钟没有回复, 您与 %s 的会话已被关闭.","Since you didn't respond in the last %s minutes your conversation got closed.":"由于您超过 %s 分钟没有任何回复, 该对话已被关闭.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"非常抱歉, 目前需要等候更长的时间才能接入对话, 请稍后重试或向我们发送电子邮件. 谢谢!"},"zh-tw":{"Chat with us!":"開始即時對话!","Scroll down to see new messages":"向下滑動以查看新訊息",Online:"線上",Offline:"离线",Connecting:"連線中","Connection re-established":"正在重新建立連線中",Today:"今天",Send:"發送","Compose your message...":"正在輸入訊息...","All colleagues are busy.":"所有服務人員都在忙碌中.","You are on waiting list position %s.":"你目前的等候位置是第 %s 順位.","Start new conversation":"開始新的對話","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"由於你超過 %s 分鐘沒有回應, 你與 %s 的對話已被關閉.","Since you didn't respond in the last %s minutes your conversation got closed.":"由於你超過 %s 分鐘沒有任何回應, 該對話已被關閉.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"非常抱歉, 當前需要等候更長的時間方可排入對話程序, 請稍後重試或向我們寄送電子郵件. 謝謝!"}},a.prototype.sessionId=void 0,a.prototype.scrolledToBottom=!0,a.prototype.scrollSnapTolerance=10,a.prototype.richTextFormatKey={66:!0,73:!0,85:!0,83:!0},a.prototype.T=function(){var t,e,s,n,i,o;if(i=arguments[0],e=2<=arguments.length?slice.call(arguments,1):[],this.options.lang&&"en"!==this.options.lang&&(this.translations[this.options.lang]?((o=this.translations[this.options.lang])[i]||this.log.notice("Translation needed for '"+i+"'"),i=o[i]||i):this.log.notice("Translation '"+this.options.lang+"' needed!")),e)for(s=0,n=e.length;ss?e:document.body)},a.prototype.render=function(){return this.el&&t(".zammad-chat").get(0)||this.renderBase(),t("."+this.options.buttonClass).addClass(this.inactiveClass),this.setAgentOnlineState("online"),this.log.debug("widget rendered"),this.startTimeoutObservers(),this.idleTimeout.start(),this.sessionId=sessionStorage.getItem("sessionId"),this.send("chat_status_customer",{session_id:this.sessionId,url:e.location.href})},a.prototype.renderBase=function(){if(this.el=t(this.view("chat")({title:this.options.title,scrollHint:this.options.scrollHint})),this.options.target.append(this.el),this.input=this.el.find(".zammad-chat-input"),this.el.find(".js-chat-open").click(this.open),this.el.find(".js-chat-toggle").click(this.toggle),this.el.find(".zammad-chat-controls").on("submit",this.onSubmit),this.el.find(".zammad-chat-body").on("scroll",this.detectScrolledtoBottom),this.el.find(".zammad-scroll-hint").click(this.onScrollHintClick),this.input.on({keydown:this.checkForEnter,input:this.onInput}),this.input.on("keydown",function(t){return function(e){var s;if(s=!1,e.altKey||e.ctrlKey||!e.metaKey?e.altKey||!e.ctrlKey||e.metaKey||(s=!0):s=!0,s&&t.richTextFormatKey[e.keyCode]){if(e.preventDefault(),66===e.keyCode)return document.execCommand("bold"),!0;if(73===e.keyCode)return document.execCommand("italic"),!0;if(85===e.keyCode)return document.execCommand("underline"),!0;if(83===e.keyCode)return document.execCommand("strikeThrough"),!0}}}(this)),this.input.on("paste",function(s){return function(n){var i,o,a,r,l,h,c,d,u,p,m,g;if(n.stopPropagation(),n.preventDefault(),n.clipboardData)i=n.clipboardData;else if(e.clipboardData)i=e.clipboardData;else{if(!n.originalEvent.clipboardData)throw"No clipboardData support";i=n.originalEvent.clipboardData}if(h=!1,i&&i.items&&i.items[0]&&("file"!==(c=i.items[0]).kind||"image/png"!==c.type&&"image/jpeg"!==c.type||(l=c.getAsFile(),(u=new FileReader).onload=function(t){var e,n,i;return i=t.target.result,e=document.createElement("img"),e.src=i,n=function(t,n,o,a){return s.isRetina()&&(n/=2,2),i=t,e='',document.execCommand("insertHTML",!1,e)},s.resizeImage(e.src,460,"auto",2,"image/jpeg","auto",n)},u.readAsDataURL(l),h=!0)),!h){g=void 0,o=void 0;try{g=i.getData("text/html"),o="html",g&&0!==g.length||(o="text",g=i.getData("text/plain")),g&&0!==g.length||(o="text2",g=i.getData("text"))}catch(t){n=t,console.log("Sorry, can't insert markup because browser is not supporting it."),o="text3",g=i.getData("text")}return"text"!==o&&"text2"!==o&&"text3"!==o||(g="
"+g.replace(/\n/g,"
")+"
",g=g.replace(/
<\/div>/g,"

")),console.log("p",o,g),"html"===o&&(a=t("
"+g+"
"),d=!1,r=g,p=new RegExp("<(/w|w):[A-Za-z]"),r.match(p)&&(d=!0,r=r.replace(p,"")),p=new RegExp("<(/o|o):[A-Za-z]"),r.match(p)&&(d=!0,r=r.replace(p,"")),d&&(a=s.wordFilter(a)),(a=t(a)).contents().each(function(){if(8===this.nodeType)return t(this).remove()}),a.find("a, font, small, time, form, label").replaceWith(function(){return t(this).contents()}),m="div",a.find("textarea").each(function(){var e,s;return s=this.outerHTML,p=new RegExp("<"+this.tagName,"i"),e=s.replace(p,"<"+m),p=new RegExp("'),n=n.get(0),document.caretPositionFromPoint?(c=document.caretPositionFromPoint(r,l),(d=document.createRange()).setStart(c.offsetNode,c.offset),d.collapse(),d.insertNode(n)):document.caretRangeFromPoint?(d=document.caretRangeFromPoint(r,l)).insertNode(n):console.log("could not find carat")},s.resizeImage(n.src,460,"auto",2,"image/jpeg","auto",i)},a.readAsDataURL(o)}}(this)),t(e).on("beforeunload",function(t){return function(){return t.onLeaveTemporary()}}(this)),t(e).bind("hashchange",function(t){return function(){if(!t.isOpen)return t.idleTimeout.start();t.sessionId&&t.send("chat_session_notice",{session_id:t.sessionId,message:e.location.href})}}(this)),this.isFullscreen)return this.input.on({focus:this.onFocus,focusout:this.onFocusOut})},a.prototype.checkForEnter=function(t){if(!t.shiftKey&&13===t.keyCode)return t.preventDefault(),this.sendMessage()},a.prototype.send=function(t,e){return null==e&&(e={}),e.chat_id=this.options.chatId,this.io.send(t,e)},a.prototype.onWebSocketMessage=function(t){var e,s,n;for(e=0,s=t.length;e0,t(e).scrollTop(0),s)return this.log.notice("virtual keyboard shown")},a.prototype.onFocusOut=function(){},a.prototype.onTyping=function(){if(!(this.isTyping&&this.isTyping>new Date((new Date).getTime()-1500)))return this.isTyping=new Date,this.send("chat_session_typing",{session_id:this.sessionId}),this.inactiveTimeout.start()},a.prototype.onSubmit=function(t){return t.preventDefault(),this.sendMessage()},a.prototype.sendMessage=function(){var t,e;if(t=this.input.html())return this.inactiveTimeout.start(),sessionStorage.removeItem("unfinished_message"),e=this.view("message")({message:t,from:"customer",id:this._messageCount++,unreadClass:""}),this.maybeAddTimestamp(),this.el.find(".zammad-chat-message--typing").get(0)?(this.lastAddedType="typing-placeholder",this.el.find(".zammad-chat-message--typing").before(e)):(this.lastAddedType="message--customer",this.el.find(".zammad-chat-body").append(e)),this.input.html(""),this.scrollToBottom(),this.send("chat_session_message",{content:t,id:this._messageCount,session_id:this.sessionId})},a.prototype.receiveMessage=function(t){return this.inactiveTimeout.start(),this.onAgentTypingEnd(),this.maybeAddTimestamp(),this.renderMessage({message:t.message.content,id:t.id,from:"agent"}),this.scrollToBottom({showHint:!0})},a.prototype.renderMessage=function(t){return this.lastAddedType="message--"+t.from,t.unreadClass=document.hidden?" zammad-chat-message--unread":"",this.el.find(".zammad-chat-body").append(this.view("message")(t))},a.prototype.open=function(){var t;{if(!this.isOpen)return this.isOpen=!0,this.log.debug("open widget"),this.sessionId||this.showLoader(),this.el.addClass("zammad-chat-is-open"),t=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.css("bottom",-t),this.sessionId?(this.el.css("bottom",0),this.onOpenAnimationEnd()):(this.el.animate({bottom:0},500,this.onOpenAnimationEnd),this.send("chat_session_init",{url:e.location.href}));this.log.debug("widget already open, block")}},a.prototype.onOpenAnimationEnd=function(){if(this.idleTimeout.stop(),this.isFullscreen)return this.disableScrollOnRoot()},a.prototype.sessionClose=function(){return this.send("chat_session_close",{session_id:this.sessionId}),this.inactiveTimeout.stop(),this.waitingListTimeout.stop(),sessionStorage.removeItem("unfinished_message"),this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.setSessionId(void 0)},a.prototype.toggle=function(t){return this.isOpen?this.close(t):this.open(t)},a.prototype.close=function(t){var e;if(this.isOpen){if(this.initDelayId&&clearTimeout(this.initDelayId),this.sessionId)return this.log.debug("close widget"),t&&t.stopPropagation(),this.sessionClose(),this.isFullscreen&&this.enableScrollOnRoot(),e=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.animate({bottom:-e},500,this.onCloseAnimationEnd);this.log.debug("can't close widget without sessionId")}else this.log.debug("can't close widget, it's not open")},a.prototype.onCloseAnimationEnd=function(){return this.el.css("bottom",""),this.el.removeClass("zammad-chat-is-open"),this.showLoader(),this.el.find(".zammad-chat-welcome").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").addClass("zammad-chat-is-hidden"),this.isOpen=!1,this.io.reconnect()},a.prototype.onWebSocketClose=function(){if(!this.isOpen)return this.el?(this.el.removeClass("zammad-chat-is-shown"),this.el.removeClass("zammad-chat-is-loaded")):void 0},a.prototype.show=function(){if("offline"!==this.state)return this.el.addClass("zammad-chat-is-loaded"),this.el.addClass("zammad-chat-is-shown")},a.prototype.disableInput=function(){return this.input.prop("disabled",!0),this.el.find(".zammad-chat-send").prop("disabled",!0)},a.prototype.enableInput=function(){return this.input.prop("disabled",!1),this.el.find(".zammad-chat-send").prop("disabled",!1)},a.prototype.hideModal=function(){return this.el.find(".zammad-chat-modal").html("")},a.prototype.onQueueScreen=function(t){var e;if(this.setSessionId(t.session_id),e=function(e){return function(){return e.onQueue(t),e.waitingListTimeout.start()}}(this),!this.initialQueueDelay||this.onInitialQueueDelayId)return this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),e();this.onInitialQueueDelayId=setTimeout(e,this.initialQueueDelay)},a.prototype.onQueue=function(t){return this.log.notice("onQueue",t.position),this.inQueue=!0,this.el.find(".zammad-chat-modal").html(this.view("waiting")({position:t.position}))},a.prototype.onAgentTypingStart=function(){if(this.stopTypingId&&clearTimeout(this.stopTypingId),this.stopTypingId=setTimeout(this.onAgentTypingEnd,3e3),!this.el.find(".zammad-chat-message--typing").get(0)&&(this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("typingIndicator")()),this.isVisible(this.el.find(".zammad-chat-message--typing"),!0)))return this.scrollToBottom()},a.prototype.onAgentTypingEnd=function(){return this.el.find(".zammad-chat-message--typing").remove()},a.prototype.onLeaveTemporary=function(){if(this.sessionId)return this.send("chat_session_leave_temporary",{session_id:this.sessionId})},a.prototype.maybeAddTimestamp=function(){var t,e,s;if(s=Date.now(),!this.lastTimestamp||s-this.lastTimestamp>6e4*this.showTimeEveryXMinutes)return t=this.T("Today"),e=(new Date).toTimeString().substr(0,5),"timestamp"===this.lastAddedType?(this.updateLastTimestamp(t,e),this.lastTimestamp=s):(this.el.find(".zammad-chat-body").append(this.view("timestamp")({label:t,time:e})),this.lastTimestamp=s,this.lastAddedType="timestamp",this.scrollToBottom())},a.prototype.updateLastTimestamp=function(t,e){if(this.el)return this.el.find(".zammad-chat-body").find(".zammad-chat-timestamp").last().replaceWith(this.view("timestamp")({label:t,time:e}))},a.prototype.addStatus=function(t){if(this.el)return this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("status")({status:t})),this.scrollToBottom()},a.prototype.detectScrolledtoBottom=function(){var t;if(t=this.el.find(".zammad-chat-body").scrollTop()+this.el.find(".zammad-chat-body").outerHeight(),this.scrolledToBottom=Math.abs(t-this.el.find(".zammad-chat-body").prop("scrollHeight"))<=this.scrollSnapTolerance,this.scrolledToBottom)return this.el.find(".zammad-scroll-hint").addClass("is-hidden")},a.prototype.showScrollHint=function(){return this.el.find(".zammad-scroll-hint").removeClass("is-hidden"),this.el.find(".zammad-chat-body").scrollTop(this.el.find(".zammad-chat-body").scrollTop()+this.el.find(".zammad-scroll-hint").outerHeight())},a.prototype.onScrollHintClick=function(){return this.el.find(".zammad-chat-body").animate({scrollTop:this.el.find(".zammad-chat-body").prop("scrollHeight")},300)},a.prototype.scrollToBottom=function(e){var s;return s=(null!=e?e:{showHint:!1}).showHint,this.scrolledToBottom?this.el.find(".zammad-chat-body").scrollTop(t(".zammad-chat-body").prop("scrollHeight")):s?this.showScrollHint():void 0},a.prototype.destroy=function(t){return null==t&&(t={}),this.log.debug("destroy widget",t),this.setAgentOnlineState("offline"),t.remove&&this.el&&this.el.remove(),this.waitingListTimeout&&this.waitingListTimeout.stop(),this.inactiveTimeout&&this.inactiveTimeout.stop(),this.idleTimeout&&this.idleTimeout.stop(),this.io.close()},a.prototype.reconnect=function(){return this.log.notice("reconnecting"),this.disableInput(),this.lastAddedType="status",this.setAgentOnlineState("connecting"),this.addStatus(this.T("Connection lost"))},a.prototype.onConnectionReestablished=function(){return this.lastAddedType="status",this.setAgentOnlineState("online"),this.addStatus(this.T("Connection re-established"))},a.prototype.onSessionClosed=function(t){return this.addStatus(this.T("Chat closed by %s",t.realname)),this.disableInput(),this.setAgentOnlineState("offline"),this.inactiveTimeout.stop()},a.prototype.setSessionId=function(t){return this.sessionId=t,void 0===t?sessionStorage.removeItem("sessionId"):sessionStorage.setItem("sessionId",t)},a.prototype.onConnectionEstablished=function(t){return this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.inQueue=!1,t.agent&&(this.agent=t.agent),t.session_id&&this.setSessionId(t.session_id),this.el.find(".zammad-chat-body").html(""),this.el.find(".zammad-chat-agent").html(this.view("agent")({agent:this.agent})),this.enableInput(),this.hideModal(),this.el.find(".zammad-chat-welcome").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").removeClass("zammad-chat-is-hidden"),this.isFullscreen||this.input.focus(),this.setAgentOnlineState("online"),this.waitingListTimeout.stop(),this.idleTimeout.stop(),this.inactiveTimeout.start()},a.prototype.showCustomerTimeout=function(){var t;return this.el.find(".zammad-chat-modal").html(this.view("customer_timeout")({agent:this.agent.name,delay:this.options.inactiveTimeout})),t=function(){return location.reload()},this.el.find(".js-restart").click(t),this.sessionClose()},a.prototype.showWaitingListTimeout=function(){var t;return this.el.find(".zammad-chat-modal").html(this.view("waiting_list_timeout")({delay:this.options.watingListTimeout})),t=function(){return location.reload()},this.el.find(".js-restart").click(t),this.sessionClose()},a.prototype.showLoader=function(){return this.el.find(".zammad-chat-modal").html(this.view("loader")())},a.prototype.setAgentOnlineState=function(t){var e;if(this.state=t,this.el)return e=t.charAt(0).toUpperCase()+t.slice(1),this.el.find(".zammad-chat-agent-status").attr("data-status",t).text(this.T(e))},a.prototype.detectHost=function(){var t;return t="ws://","https"===h&&(t="wss://"),this.options.host=""+t+l+"/ws"},a.prototype.loadCss=function(){var t,e,s;if(this.options.cssAutoload)return(s=this.options.cssUrl)||(s=this.options.host.replace(/^wss/i,"https").replace(/^ws/i,"http").replace(/\/ws/i,""),s+="/assets/chat/chat.css"),this.log.debug("load css from '"+s+"'"),e="@import url('"+s+"');",t=document.createElement("link"),t.onload=this.onCssLoaded,t.rel="stylesheet",t.href="data:text/css,"+escape(e),document.getElementsByTagName("head")[0].appendChild(t)},a.prototype.onCssLoaded=function(){return this.socketReady?this.onReady():this.cssLoaded=!0},a.prototype.startTimeoutObservers=function(){return this.idleTimeout=new o({logPrefix:"idleTimeout",debug:this.options.debug,timeout:this.options.idleTimeout,timeoutIntervallCheck:this.options.idleTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Idle timeout reached, hide widget",new Date),t.destroy({remove:!0})}}(this)}),this.inactiveTimeout=new o({logPrefix:"inactiveTimeout",debug:this.options.debug,timeout:this.options.inactiveTimeout,timeoutIntervallCheck:this.options.inactiveTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Inactive timeout reached, show timeout screen.",new Date),t.showCustomerTimeout(),t.destroy({remove:!1})}}(this)}),this.waitingListTimeout=new o({logPrefix:"waitingListTimeout",debug:this.options.debug,timeout:this.options.waitingListTimeout,timeoutIntervallCheck:this.options.waitingListTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Waiting list timeout reached, show timeout screen.",new Date),t.showWaitingListTimeout(),t.destroy({remove:!1})}}(this)})},a.prototype.disableScrollOnRoot=function(){return this.rootScrollOffset=this.scrollRoot.scrollTop(),this.scrollRoot.css({overflow:"hidden",position:"fixed"})},a.prototype.enableScrollOnRoot=function(){return this.scrollRoot.scrollTop(this.rootScrollOffset),this.scrollRoot.css({overflow:"",position:""})},a.prototype.isVisible=function(s,n,i,o){var a,r,l,h,c,d,u,p,m,g,f,v,y,b,w,T,C,z,S,k,I,A,x,_,E,O;if(!(s.length<1))if(r=t(e),a=s.length>1?s.eq(0):s,z=a.get(0),O=r.width(),E=r.height(),o=o||"both",p=!0!==i||z.offsetWidth*z.offsetHeight,"function"==typeof z.getBoundingClientRect){if(C=z.getBoundingClientRect(),S=C.top>=0&&C.top0&&C.bottom<=E,b=C.left>=0&&C.left0&&C.right<=O,k=n?S||u:S&&u,y=n?b||T:b&&T,"both"===o)return p&&k&&y;if("vertical"===o)return p&&k;if("horizontal"===o)return p&&y}else{if(_=r.scrollTop(),I=_+E,A=r.scrollLeft(),x=A+O,w=a.offset(),d=w.top,l=d+a.height(),h=w.left,c=h+a.width(),v=!0===n?l:d,m=!0===n?d:l,g=!0===n?c:h,f=!0===n?h:c,"both"===o)return!!p&&m<=I&&v>=_&&f<=x&&g>=A;if("vertical"===o)return!!p&&m<=I&&v>=_;if("horizontal"===o)return!!p&&f<=x&&g>=A}},a.prototype.isRetina=function(){var t;return!!e.matchMedia&&((t=e.matchMedia("only screen and (min--moz-device-pixel-ratio: 1.3), only screen and (-o-min-device-pixel-ratio: 2.6/2), only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen and (min-device-pixel-ratio: 1.3), only screen and (min-resolution: 1.3dppx)"))&&t.matches||e.devicePixelRatio>1)},a.prototype.resizeImage=function(t,e,s,n,i,o,a,r){var l;return null==e&&(e="auto"),null==s&&(s="auto"),null==n&&(n=1),null==r&&(r=!0),l=new Image,l.onload=function(){var t,r,h,c,d;return h=l.width,r=l.height,console.log("ImageService","current size",h,r),"auto"===s&&"auto"===e&&(e=h,s=r),"auto"===s&&(s=r/(h/e)),"auto"===e&&(e=r/(h/s)),d=!1,e/gi,""),s=s.replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi,""),s=s.replace(/<(\/?)s>/gi,"<$1strike>"),s=s.replace(/ /gi," "),e.html(s),t("p",e).each(function(){var e,s;if(s=t(this).attr("style"),e=/mso-list:\w+ \w+([0-9]+)/.exec(s))return t(this).data("_listLevel",parseInt(e[1],10))}),n=0,i=null,t("p",e).each(function(){var e,s,o,a,r,l,h,c,d,u;if(void 0!==(e=t(this).data("_listLevel"))){if(u=t(this).text(),a="
    ",/^\s*\w+\./.test(u)&&(a=(r=/([0-9])\./.exec(u))?null!=(l=(d=parseInt(r[1],10))>1)?l:'
      ':"
        "}:"
          "),e>n&&(0===n?(t(this).before(a),i=t(this).prev()):i=t(a).appendTo(i)),e=c;s=h<=c?++o:--o)i=i.parent();return t("span:first",this).remove(),i.append("
        1. "+t(this).html()+"
        2. "),t(this).remove(),n=e}return n=0}),t("[style]",e).removeAttr("style"),t("[align]",e).removeAttr("align"),t("span",e).replaceWith(function(){return t(this).contents()}),t("span:empty",e).remove(),t("[class^='Mso']",e).removeAttr("class"),t("p:empty",e).remove(),e},a.prototype.removeAttribute=function(e){var s,n,i,o,a;if(e){for(s=t(e),i=0,o=(a=e.attributes).length;i/g,">").replace(/"/g,""")}),function(){(function(){e.push('
          \n
          \n
          \n \n \n \n \n \n
          \n
          \n
          \n
          \n \n '),e.push(this.T(this.title)),e.push('\n
          \n
          \n
          \n \n
          \n
          \n
          \n \n
          \n
          ")}).call(this)}.call(t),t.safe=n,t.escape=i,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.customer_timeout=function(t){t||(t={});var e=[],s=function(t){return t&&t.ecoSafe?t:void 0!==t&&null!=t?i(t):""},n=t.safe,i=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
          \n '),this.agent?(e.push("\n "),e.push(this.T("Since you didn't respond in the last %s minutes your conversation with %s got closed.",this.delay,this.agent)),e.push("\n ")):(e.push("\n "),e.push(this.T("Since you didn't respond in the last %s minutes your conversation got closed.",this.delay)),e.push("\n ")),e.push('\n
          \n
          "),e.push(this.T("Start new conversation")),e.push("
          \n
          ")}).call(this)}.call(t),t.safe=n,t.escape=i,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.loader=function(t){t||(t={});var e=[],s=t.safe,n=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},n||(n=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('\n \n \n \n\n'),e.push(this.T("Connecting")),e.push("")}).call(this)}.call(t),t.safe=s,t.escape=n,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.message=function(t){t||(t={});var e=[],s=function(t){return t&&t.ecoSafe?t:void 0!==t&&null!=t?i(t):""},n=t.safe,i=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
          \n "),e.push(this.message),e.push("\n
          ")}).call(this)}.call(t),t.safe=n,t.escape=i,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.status=function(t){t||(t={});var e=[],s=t.safe,n=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},n||(n=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
          \n
          \n '),e.push(this.status),e.push("\n
          \n
          ")}).call(this)}.call(t),t.safe=s,t.escape=n,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.timestamp=function(t){t||(t={});var e=[],s=function(t){return t&&t.ecoSafe?t:void 0!==t&&null!=t?i(t):""},n=t.safe,i=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
          '),e.push(s(this.label)),e.push(" "),e.push(s(this.time)),e.push("
          ")}).call(this)}.call(t),t.safe=n,t.escape=i,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.typingIndicator=function(t){t||(t={});var e=[],s=t.safe,n=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},n||(n=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
          \n \n \n \n \n \n \n \n
          ')}).call(this)}.call(t),t.safe=s,t.escape=n,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting=function(t){t||(t={});var e=[],s=t.safe,n=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},n||(n=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
          \n \n \n \n \n \n '),e.push(this.T("All colleagues are busy.")),e.push("
          \n "),e.push(this.T("You are on waiting list position %s.",this.position)),e.push("\n
          ")}).call(this)}.call(t),t.safe=s,t.escape=n,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting_list_timeout=function(t){t||(t={});var e=[],s=function(t){return t&&t.ecoSafe?t:void 0!==t&&null!=t?i(t):""},n=t.safe,i=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
          \n '),e.push(this.T("We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!")),e.push('\n
          \n
          "),e.push(this.T("Start new conversation")),e.push("
          \n
          ")}).call(this)}.call(t),t.safe=n,t.escape=i,e.join("")}; \ No newline at end of file +var bind=function(t,e){return function(){return t.apply(e,arguments)}},slice=[].slice,extend=function(t,e){function s(){this.constructor=t}for(var n in e)hasProp.call(e,n)&&(t[n]=e[n]);return s.prototype=e.prototype,t.prototype=new s,t.__super__=e.prototype,t},hasProp={}.hasOwnProperty;!function(t,e){var s,n,i,o,a,r,l,h,c;c=document.getElementsByTagName("script"),r=c[c.length-1],l=r.src.match(".*://([^:/]*).*")[1],h=r.src.match("(.*)://[^:/]*.*")[1],s=function(){function e(e){this.options=t.extend({},this.defaults,e),this.log=new i({debug:this.options.debug,logPrefix:this.options.logPrefix||this.logPrefix})}return e.prototype.defaults={debug:!1},e}(),i=function(){function e(e){this.log=bind(this.log,this),this.error=bind(this.error,this),this.notice=bind(this.notice,this),this.debug=bind(this.debug,this),this.options=t.extend({},this.defaults,e)}return e.prototype.defaults={debug:!1},e.prototype.debug=function(){var t;if(t=1<=arguments.length?slice.call(arguments,0):[],this.options.debug)return this.log("debug",t)},e.prototype.notice=function(){var t;return t=1<=arguments.length?slice.call(arguments,0):[],this.log("notice",t)},e.prototype.error=function(){var t;return t=1<=arguments.length?slice.call(arguments,0):[],this.log("error",t)},e.prototype.log=function(e,s){var n,i,o,a;if(s.unshift("||"),s.unshift(e),s.unshift(this.options.logPrefix),console.log.apply(console,s),this.options.debug){for(a="",i=0,o=s.length;i"+a+"
          ")}},e}(),o=function(t){function e(t){this.stop=bind(this.stop,this),this.start=bind(this.start,this),e.__super__.constructor.call(this,t)}return extend(e,s),e.prototype.timeoutStartedAt=null,e.prototype.logPrefix="timeout",e.prototype.defaults={debug:!1,timeout:4,timeoutIntervallCheck:.5},e.prototype.start=function(){var t,e;return this.stop(),e=new Date,t=function(t){return function(){var s;if(s=new Date-new Date(e.getTime()+1e3*t.options.timeout*60),t.log.debug("Timeout check for "+t.options.timeout+" minutes (left "+s/1e3+" sec.)"),!(s<0))return t.stop(),t.options.callback()}}(this),this.log.debug("Start timeout in "+this.options.timeout+" minutes"),this.intervallId=setInterval(t,1e3*this.options.timeoutIntervallCheck*60)},e.prototype.stop=function(){if(this.intervallId)return this.log.debug("Stop timeout of "+this.options.timeout+" minutes"),clearInterval(this.intervallId)},e}(),n=function(t){function n(t){this.ping=bind(this.ping,this),this.send=bind(this.send,this),this.reconnect=bind(this.reconnect,this),this.close=bind(this.close,this),this.connect=bind(this.connect,this),this.set=bind(this.set,this),n.__super__.constructor.call(this,t)}return extend(n,s),n.prototype.logPrefix="io",n.prototype.set=function(t){var e,s,n;s=[];for(e in t)n=t[e],s.push(this.options[e]=n);return s},n.prototype.connect=function(){return this.log.debug("Connecting to "+this.options.host),this.ws=new e.WebSocket(""+this.options.host),this.ws.onopen=function(t){return function(e){return t.log.debug("onOpen",e),t.options.onOpen(e),t.ping()}}(this),this.ws.onmessage=function(t){return function(e){var s,n,i;for(i=JSON.parse(e.data),t.log.debug("onMessage",e.data),s=0,n=i.length;sChat with us!",scrollHint:"Scroll down to see new messages",idleTimeout:6,idleTimeoutIntervallCheck:.5,inactiveTimeout:8,inactiveTimeoutIntervallCheck:.5,waitingListTimeout:4,waitingListTimeoutIntervallCheck:.5},a.prototype.logPrefix="chat",a.prototype._messageCount=0,a.prototype.isOpen=!1,a.prototype.blinkOnlineInterval=null,a.prototype.stopBlinOnlineStateTimeout=null,a.prototype.showTimeEveryXMinutes=2,a.prototype.lastTimestamp=null,a.prototype.lastAddedType=null,a.prototype.inputTimeout=null,a.prototype.isTyping=!1,a.prototype.state="offline",a.prototype.initialQueueDelay=1e4,a.prototype.translations={de:{"Chat with us!":"Chatte mit uns!","Scroll down to see new messages":"Scrolle nach unten um neue Nachrichten zu sehen",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbindung wiederhergestellt",Today:"Heute",Send:"Senden","Compose your message...":"Ihre Nachricht...","All colleagues are busy.":"Alle Kollegen sind belegt.","You are on waiting list position %s.":"Sie sind in der Warteliste an der Position %s.","Start new conversation":"Neue Konversation starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation mit %s geschlossen.","Since you didn't respond in the last %s minutes your conversation got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation geschlossen.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Es tut uns leid, es dauert länger als erwartet, um einen freien Platz zu erhalten. Bitte versuchen Sie es zu einem späteren Zeitpunkt noch einmal oder schicken Sie uns eine E-Mail. Vielen Dank!"},es:{"Chat with us!":"Chatee con nosotros!","Scroll down to see new messages":"Haga scroll hacia abajo para ver nuevos mensajes",Online:"En linea",Offline:"Desconectado",Connecting:"Conectando","Connection re-established":"Conexión restablecida",Today:"Hoy",Send:"Enviar","Compose your message...":"Escriba su mensaje...","All colleagues are busy.":"Todos los agentes están ocupados.","You are on waiting list position %s.":"Usted está en la posición %s de la lista de espera.","Start new conversation":"Iniciar nueva conversación","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Puesto que usted no respondió en los últimos %s minutos su conversación con %s se ha cerrado.","Since you didn't respond in the last %s minutes your conversation got closed.":"Puesto que usted no respondió en los últimos %s minutos su conversación se ha cerrado.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Lo sentimos, se tarda más tiempo de lo esperado para ser atendido por un agente. Inténtelo de nuevo más tarde o envíenos un correo electrónico. ¡Gracias!"},fr:{"Chat with us!":"Chattez avec nous!","Scroll down to see new messages":"Faites défiler pour lire les nouveaux messages",Online:"En-ligne",Offline:"Hors-ligne",Connecting:"Connexion en cours","Connection re-established":"Connexion rétablie",Today:"Aujourdhui",Send:"Envoyer","Compose your message...":"Composez votre message...","All colleagues are busy.":"Tous les collègues sont actuellement occupés.","You are on waiting list position %s.":"Vous êtes actuellement en %s position dans la file d'attente.","Start new conversation":"Démarrer une nouvelle conversation","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Si vous ne répondez pas dans les %s minutes, votre conversation avec %s va être fermée.","Since you didn't respond in the last %s minutes your conversation got closed.":"Si vous ne répondez pas dans les %s minutes, votre conversation va être fermée.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Nous sommes désolés, il faut plus de temps que prévu pour obtenir un emplacement vide. Veuillez réessayer ultérieurement ou nous envoyer un courriel. Je vous remercie!"},nl:{"Chat with us!":"Chat met ons!","Scroll down to see new messages":"Scrol naar beneden om nieuwe berichten te zien",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbinding herstelt",Today:"Vandaag",Send:"Verzenden","Compose your message...":"Typ uw bericht...","All colleagues are busy.":"Alle medewerkers zijn bezet.","You are on waiting list position %s.":"U bent %s in de wachtrij.","Start new conversation":"Nieuwe conversatie starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Omdat u in de laatste %s minuten niets geschreven heeft wordt de conversatie met %s gesloten.","Since you didn't respond in the last %s minutes your conversation got closed.":"Omdat u in de laatste %s minuten niets geschreven heeft is de conversatie gesloten.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Het spijt ons, het duurt langer dan verwacht om te antwoorden. Alstublieft probeer het later nogmaals of stuur ons een email. Hartelijk dank!"},"zh-cn":{"Chat with us!":"发起即时对话!","Scroll down to see new messages":"向下滚动以查看新消息",Online:"在线",Offline:"离线",Connecting:"连接中","Connection re-established":"正在重新建立连接",Today:"今天",Send:"发送","Compose your message...":"正在输入信息...","All colleagues are busy.":"所有工作人员都在忙碌中.","You are on waiting list position %s.":"您目前的等候位置是第 %s 位.","Start new conversation":"开始新的会话","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"由于您超过 %s 分钟没有回复, 您与 %s 的会话已被关闭.","Since you didn't respond in the last %s minutes your conversation got closed.":"由于您超过 %s 分钟没有任何回复, 该对话已被关闭.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"非常抱歉, 目前需要等候更长的时间才能接入对话, 请稍后重试或向我们发送电子邮件. 谢谢!"},"zh-tw":{"Chat with us!":"開始即時對话!","Scroll down to see new messages":"向下滑動以查看新訊息",Online:"線上",Offline:"离线",Connecting:"連線中","Connection re-established":"正在重新建立連線中",Today:"今天",Send:"發送","Compose your message...":"正在輸入訊息...","All colleagues are busy.":"所有服務人員都在忙碌中.","You are on waiting list position %s.":"你目前的等候位置是第 %s 順位.","Start new conversation":"開始新的對話","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"由於你超過 %s 分鐘沒有回應, 你與 %s 的對話已被關閉.","Since you didn't respond in the last %s minutes your conversation got closed.":"由於你超過 %s 分鐘沒有任何回應, 該對話已被關閉.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"非常抱歉, 當前需要等候更長的時間方可排入對話程序, 請稍後重試或向我們寄送電子郵件. 謝謝!"}},a.prototype.sessionId=void 0,a.prototype.scrolledToBottom=!0,a.prototype.scrollSnapTolerance=10,a.prototype.richTextFormatKey={66:!0,73:!0,85:!0,83:!0},a.prototype.T=function(){var t,e,s,n,i,o;if(i=arguments[0],e=2<=arguments.length?slice.call(arguments,1):[],this.options.lang&&"en"!==this.options.lang&&(this.translations[this.options.lang]?((o=this.translations[this.options.lang])[i]||this.log.notice("Translation needed for '"+i+"'"),i=o[i]||i):this.log.notice("Translation '"+this.options.lang+"' needed!")),e)for(s=0,n=e.length;ss?e:document.body)},a.prototype.render=function(){return this.el&&t(".zammad-chat").get(0)||this.renderBase(),t("."+this.options.buttonClass).addClass(this.inactiveClass),this.setAgentOnlineState("online"),this.log.debug("widget rendered"),this.startTimeoutObservers(),this.idleTimeout.start(),this.sessionId=sessionStorage.getItem("sessionId"),this.send("chat_status_customer",{session_id:this.sessionId,url:e.location.href})},a.prototype.renderBase=function(){if(this.el=t(this.view("chat")({title:this.options.title,scrollHint:this.options.scrollHint})),this.options.target.append(this.el),this.input=this.el.find(".zammad-chat-input"),this.el.find(".js-chat-open").click(this.open),this.el.find(".js-chat-toggle").click(this.toggle),this.el.find(".zammad-chat-controls").on("submit",this.onSubmit),this.el.find(".zammad-chat-body").on("scroll",this.detectScrolledtoBottom),this.el.find(".zammad-scroll-hint").click(this.onScrollHintClick),this.input.on({keydown:this.checkForEnter,input:this.onInput}),this.input.on("keydown",function(t){return function(e){var s;if(s=!1,e.altKey||e.ctrlKey||!e.metaKey?e.altKey||!e.ctrlKey||e.metaKey||(s=!0):s=!0,s&&t.richTextFormatKey[e.keyCode]){if(e.preventDefault(),66===e.keyCode)return document.execCommand("bold"),!0;if(73===e.keyCode)return document.execCommand("italic"),!0;if(85===e.keyCode)return document.execCommand("underline"),!0;if(83===e.keyCode)return document.execCommand("strikeThrough"),!0}}}(this)),this.input.on("paste",function(s){return function(n){var i,o,a,r,l,h,c,d,u,p,m,g;if(n.stopPropagation(),n.preventDefault(),n.clipboardData)i=n.clipboardData;else if(e.clipboardData)i=e.clipboardData;else{if(!n.originalEvent.clipboardData)throw"No clipboardData support";i=n.originalEvent.clipboardData}if(h=!1,i&&i.items&&i.items[0]&&("file"!==(c=i.items[0]).kind||"image/png"!==c.type&&"image/jpeg"!==c.type||(l=c.getAsFile(),(u=new FileReader).onload=function(t){var e,n,i;return i=t.target.result,e=document.createElement("img"),e.src=i,n=function(t,n,o,a){return s.isRetina()&&(n/=2,2),i=t,e='',document.execCommand("insertHTML",!1,e)},s.resizeImage(e.src,460,"auto",2,"image/jpeg","auto",n)},u.readAsDataURL(l),h=!0)),!h){g=void 0,o=void 0;try{g=i.getData("text/html"),o="html",g&&0!==g.length||(o="text",g=i.getData("text/plain")),g&&0!==g.length||(o="text2",g=i.getData("text"))}catch(t){n=t,console.log("Sorry, can't insert markup because browser is not supporting it."),o="text3",g=i.getData("text")}return"text"!==o&&"text2"!==o&&"text3"!==o||(g="
          "+g.replace(/\n/g,"
          ")+"
          ",g=g.replace(/
          <\/div>/g,"

          ")),console.log("p",o,g),"html"===o&&(a=t("
          "+g+"
          "),d=!1,r=g,p=new RegExp("<(/w|w):[A-Za-z]"),r.match(p)&&(d=!0,r=r.replace(p,"")),p=new RegExp("<(/o|o):[A-Za-z]"),r.match(p)&&(d=!0,r=r.replace(p,"")),d&&(a=s.wordFilter(a)),(a=t(a)).contents().each(function(){if(8===this.nodeType)return t(this).remove()}),a.find("a, font, small, time, form, label").replaceWith(function(){return t(this).contents()}),m="div",a.find("textarea").each(function(){var e,s;return s=this.outerHTML,p=new RegExp("<"+this.tagName,"i"),e=s.replace(p,"<"+m),p=new RegExp("'),n=n.get(0),document.caretPositionFromPoint?(c=document.caretPositionFromPoint(r,l),(d=document.createRange()).setStart(c.offsetNode,c.offset),d.collapse(),d.insertNode(n)):document.caretRangeFromPoint?(d=document.caretRangeFromPoint(r,l)).insertNode(n):console.log("could not find carat")},s.resizeImage(n.src,460,"auto",2,"image/jpeg","auto",i)},a.readAsDataURL(o)}}(this)),t(e).on("beforeunload",function(t){return function(){return t.onLeaveTemporary()}}(this)),t(e).bind("hashchange",function(t){return function(){if(!t.isOpen)return t.idleTimeout.start();t.sessionId&&t.send("chat_session_notice",{session_id:t.sessionId,message:e.location.href})}}(this)),this.isFullscreen)return this.input.on({focus:this.onFocus,focusout:this.onFocusOut})},a.prototype.checkForEnter=function(t){if(!t.shiftKey&&13===t.keyCode)return t.preventDefault(),this.sendMessage()},a.prototype.send=function(t,e){return null==e&&(e={}),e.chat_id=this.options.chatId,this.io.send(t,e)},a.prototype.onWebSocketMessage=function(t){var e,s,n;for(e=0,s=t.length;e0,t(e).scrollTop(0),s)return this.log.notice("virtual keyboard shown")},a.prototype.onFocusOut=function(){},a.prototype.onTyping=function(){if(!(this.isTyping&&this.isTyping>new Date((new Date).getTime()-1500)))return this.isTyping=new Date,this.send("chat_session_typing",{session_id:this.sessionId}),this.inactiveTimeout.start()},a.prototype.onSubmit=function(t){return t.preventDefault(),this.sendMessage()},a.prototype.sendMessage=function(){var t,e;if(t=this.input.html())return this.inactiveTimeout.start(),sessionStorage.removeItem("unfinished_message"),e=this.view("message")({message:t,from:"customer",id:this._messageCount++,unreadClass:""}),this.maybeAddTimestamp(),this.el.find(".zammad-chat-message--typing").get(0)?(this.lastAddedType="typing-placeholder",this.el.find(".zammad-chat-message--typing").before(e)):(this.lastAddedType="message--customer",this.el.find(".zammad-chat-body").append(e)),this.input.html(""),this.scrollToBottom(),this.send("chat_session_message",{content:t,id:this._messageCount,session_id:this.sessionId})},a.prototype.receiveMessage=function(t){return this.inactiveTimeout.start(),this.onAgentTypingEnd(),this.maybeAddTimestamp(),this.renderMessage({message:t.message.content,id:t.id,from:"agent"}),this.scrollToBottom({showHint:!0})},a.prototype.renderMessage=function(t){return this.lastAddedType="message--"+t.from,t.unreadClass=document.hidden?" zammad-chat-message--unread":"",this.el.find(".zammad-chat-body").append(this.view("message")(t))},a.prototype.open=function(){var t;{if(!this.isOpen)return this.isOpen=!0,this.log.debug("open widget"),this.sessionId||this.showLoader(),this.el.addClass("zammad-chat-is-open"),t=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.css("bottom",-t),this.sessionId?(this.el.css("bottom",0),this.onOpenAnimationEnd()):(this.el.animate({bottom:0},500,this.onOpenAnimationEnd),this.send("chat_session_init",{url:e.location.href}));this.log.debug("widget already open, block")}},a.prototype.onOpenAnimationEnd=function(){if(this.idleTimeout.stop(),this.isFullscreen)return this.disableScrollOnRoot()},a.prototype.sessionClose=function(){return this.send("chat_session_close",{session_id:this.sessionId}),this.inactiveTimeout.stop(),this.waitingListTimeout.stop(),sessionStorage.removeItem("unfinished_message"),this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.setSessionId(void 0)},a.prototype.toggle=function(t){return this.isOpen?this.close(t):this.open(t)},a.prototype.close=function(t){var e;if(this.isOpen){if(this.initDelayId&&clearTimeout(this.initDelayId),this.sessionId)return this.log.debug("close widget"),t&&t.stopPropagation(),this.sessionClose(),this.isFullscreen&&this.enableScrollOnRoot(),e=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.animate({bottom:-e},500,this.onCloseAnimationEnd);this.log.debug("can't close widget without sessionId")}else this.log.debug("can't close widget, it's not open")},a.prototype.onCloseAnimationEnd=function(){return this.el.css("bottom",""),this.el.removeClass("zammad-chat-is-open"),this.showLoader(),this.el.find(".zammad-chat-welcome").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").addClass("zammad-chat-is-hidden"),this.isOpen=!1,this.io.reconnect()},a.prototype.onWebSocketClose=function(){if(!this.isOpen)return this.el?(this.el.removeClass("zammad-chat-is-shown"),this.el.removeClass("zammad-chat-is-loaded")):void 0},a.prototype.show=function(){if("offline"!==this.state)return this.el.addClass("zammad-chat-is-loaded"),this.el.addClass("zammad-chat-is-shown")},a.prototype.disableInput=function(){return this.input.prop("disabled",!0),this.el.find(".zammad-chat-send").prop("disabled",!0)},a.prototype.enableInput=function(){return this.input.prop("disabled",!1),this.el.find(".zammad-chat-send").prop("disabled",!1)},a.prototype.hideModal=function(){return this.el.find(".zammad-chat-modal").html("")},a.prototype.onQueueScreen=function(t){var e;if(this.setSessionId(t.session_id),e=function(e){return function(){return e.onQueue(t),e.waitingListTimeout.start()}}(this),!this.initialQueueDelay||this.onInitialQueueDelayId)return this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),e();this.onInitialQueueDelayId=setTimeout(e,this.initialQueueDelay)},a.prototype.onQueue=function(t){return this.log.notice("onQueue",t.position),this.inQueue=!0,this.el.find(".zammad-chat-modal").html(this.view("waiting")({position:t.position}))},a.prototype.onAgentTypingStart=function(){if(this.stopTypingId&&clearTimeout(this.stopTypingId),this.stopTypingId=setTimeout(this.onAgentTypingEnd,3e3),!this.el.find(".zammad-chat-message--typing").get(0)&&(this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("typingIndicator")()),this.isVisible(this.el.find(".zammad-chat-message--typing"),!0)))return this.scrollToBottom()},a.prototype.onAgentTypingEnd=function(){return this.el.find(".zammad-chat-message--typing").remove()},a.prototype.onLeaveTemporary=function(){if(this.sessionId)return this.send("chat_session_leave_temporary",{session_id:this.sessionId})},a.prototype.maybeAddTimestamp=function(){var t,e,s;if(s=Date.now(),!this.lastTimestamp||s-this.lastTimestamp>6e4*this.showTimeEveryXMinutes)return t=this.T("Today"),e=(new Date).toTimeString().substr(0,5),"timestamp"===this.lastAddedType?(this.updateLastTimestamp(t,e),this.lastTimestamp=s):(this.el.find(".zammad-chat-body").append(this.view("timestamp")({label:t,time:e})),this.lastTimestamp=s,this.lastAddedType="timestamp",this.scrollToBottom())},a.prototype.updateLastTimestamp=function(t,e){if(this.el)return this.el.find(".zammad-chat-body").find(".zammad-chat-timestamp").last().replaceWith(this.view("timestamp")({label:t,time:e}))},a.prototype.addStatus=function(t){if(this.el)return this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("status")({status:t})),this.scrollToBottom()},a.prototype.detectScrolledtoBottom=function(){var t;if(t=this.el.find(".zammad-chat-body").scrollTop()+this.el.find(".zammad-chat-body").outerHeight(),this.scrolledToBottom=Math.abs(t-this.el.find(".zammad-chat-body").prop("scrollHeight"))<=this.scrollSnapTolerance,this.scrolledToBottom)return this.el.find(".zammad-scroll-hint").addClass("is-hidden")},a.prototype.showScrollHint=function(){return this.el.find(".zammad-scroll-hint").removeClass("is-hidden"),this.el.find(".zammad-chat-body").scrollTop(this.el.find(".zammad-chat-body").scrollTop()+this.el.find(".zammad-scroll-hint").outerHeight())},a.prototype.onScrollHintClick=function(){return this.el.find(".zammad-chat-body").animate({scrollTop:this.el.find(".zammad-chat-body").prop("scrollHeight")},300)},a.prototype.scrollToBottom=function(e){var s;return s=(null!=e?e:{showHint:!1}).showHint,this.scrolledToBottom?this.el.find(".zammad-chat-body").scrollTop(t(".zammad-chat-body").prop("scrollHeight")):s?this.showScrollHint():void 0},a.prototype.destroy=function(t){return null==t&&(t={}),this.log.debug("destroy widget",t),this.setAgentOnlineState("offline"),t.remove&&this.el&&this.el.remove(),this.waitingListTimeout&&this.waitingListTimeout.stop(),this.inactiveTimeout&&this.inactiveTimeout.stop(),this.idleTimeout&&this.idleTimeout.stop(),this.io.close()},a.prototype.reconnect=function(){return this.log.notice("reconnecting"),this.disableInput(),this.lastAddedType="status",this.setAgentOnlineState("connecting"),this.addStatus(this.T("Connection lost"))},a.prototype.onConnectionReestablished=function(){return this.lastAddedType="status",this.setAgentOnlineState("online"),this.addStatus(this.T("Connection re-established"))},a.prototype.onSessionClosed=function(t){return this.addStatus(this.T("Chat closed by %s",t.realname)),this.disableInput(),this.setAgentOnlineState("offline"),this.inactiveTimeout.stop()},a.prototype.setSessionId=function(t){return this.sessionId=t,void 0===t?sessionStorage.removeItem("sessionId"):sessionStorage.setItem("sessionId",t)},a.prototype.onConnectionEstablished=function(t){return this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.inQueue=!1,t.agent&&(this.agent=t.agent),t.session_id&&this.setSessionId(t.session_id),this.el.find(".zammad-chat-body").html(""),this.el.find(".zammad-chat-agent").html(this.view("agent")({agent:this.agent})),this.enableInput(),this.hideModal(),this.el.find(".zammad-chat-welcome").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").removeClass("zammad-chat-is-hidden"),this.isFullscreen||this.input.focus(),this.setAgentOnlineState("online"),this.waitingListTimeout.stop(),this.idleTimeout.stop(),this.inactiveTimeout.start()},a.prototype.showCustomerTimeout=function(){var t;return this.el.find(".zammad-chat-modal").html(this.view("customer_timeout")({agent:this.agent.name,delay:this.options.inactiveTimeout})),t=function(){return location.reload()},this.el.find(".js-restart").click(t),this.sessionClose()},a.prototype.showWaitingListTimeout=function(){var t;return this.el.find(".zammad-chat-modal").html(this.view("waiting_list_timeout")({delay:this.options.watingListTimeout})),t=function(){return location.reload()},this.el.find(".js-restart").click(t),this.sessionClose()},a.prototype.showLoader=function(){return this.el.find(".zammad-chat-modal").html(this.view("loader")())},a.prototype.setAgentOnlineState=function(t){var e;if(this.state=t,this.el)return e=t.charAt(0).toUpperCase()+t.slice(1),this.el.find(".zammad-chat-agent-status").attr("data-status",t).text(this.T(e))},a.prototype.detectHost=function(){var t;return t="ws://","https"===h&&(t="wss://"),this.options.host=""+t+l+"/ws"},a.prototype.loadCss=function(){var t,e,s;if(this.options.cssAutoload)return(s=this.options.cssUrl)||(s=this.options.host.replace(/^wss/i,"https").replace(/^ws/i,"http").replace(/\/ws/i,""),s+="/assets/chat/chat.css"),this.log.debug("load css from '"+s+"'"),e="@import url('"+s+"');",t=document.createElement("link"),t.onload=this.onCssLoaded,t.rel="stylesheet",t.href="data:text/css,"+escape(e),document.getElementsByTagName("head")[0].appendChild(t)},a.prototype.onCssLoaded=function(){return this.socketReady?this.onReady():this.cssLoaded=!0},a.prototype.startTimeoutObservers=function(){return this.idleTimeout=new o({logPrefix:"idleTimeout",debug:this.options.debug,timeout:this.options.idleTimeout,timeoutIntervallCheck:this.options.idleTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Idle timeout reached, hide widget",new Date),t.destroy({remove:!0})}}(this)}),this.inactiveTimeout=new o({logPrefix:"inactiveTimeout",debug:this.options.debug,timeout:this.options.inactiveTimeout,timeoutIntervallCheck:this.options.inactiveTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Inactive timeout reached, show timeout screen.",new Date),t.showCustomerTimeout(),t.destroy({remove:!1})}}(this)}),this.waitingListTimeout=new o({logPrefix:"waitingListTimeout",debug:this.options.debug,timeout:this.options.waitingListTimeout,timeoutIntervallCheck:this.options.waitingListTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Waiting list timeout reached, show timeout screen.",new Date),t.showWaitingListTimeout(),t.destroy({remove:!1})}}(this)})},a.prototype.disableScrollOnRoot=function(){return this.rootScrollOffset=this.scrollRoot.scrollTop(),this.scrollRoot.css({overflow:"hidden",position:"fixed"})},a.prototype.enableScrollOnRoot=function(){return this.scrollRoot.scrollTop(this.rootScrollOffset),this.scrollRoot.css({overflow:"",position:""})},a.prototype.isVisible=function(s,n,i,o){var a,r,l,h,c,d,u,p,m,g,f,v,y,b,w,T,C,z,S,k,I,A,x,_,E,O;if(!(s.length<1))if(r=t(e),a=s.length>1?s.eq(0):s,z=a.get(0),O=r.width(),E=r.height(),o=o||"both",p=!0!==i||z.offsetWidth*z.offsetHeight,"function"==typeof z.getBoundingClientRect){if(C=z.getBoundingClientRect(),S=C.top>=0&&C.top0&&C.bottom<=E,b=C.left>=0&&C.left0&&C.right<=O,k=n?S||u:S&&u,y=n?b||T:b&&T,"both"===o)return p&&k&&y;if("vertical"===o)return p&&k;if("horizontal"===o)return p&&y}else{if(_=r.scrollTop(),I=_+E,A=r.scrollLeft(),x=A+O,w=a.offset(),d=w.top,l=d+a.height(),h=w.left,c=h+a.width(),v=!0===n?l:d,m=!0===n?d:l,g=!0===n?c:h,f=!0===n?h:c,"both"===o)return!!p&&m<=I&&v>=_&&f<=x&&g>=A;if("vertical"===o)return!!p&&m<=I&&v>=_;if("horizontal"===o)return!!p&&f<=x&&g>=A}},a.prototype.isRetina=function(){var t;return!!e.matchMedia&&((t=e.matchMedia("only screen and (min--moz-device-pixel-ratio: 1.3), only screen and (-o-min-device-pixel-ratio: 2.6/2), only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen and (min-device-pixel-ratio: 1.3), only screen and (min-resolution: 1.3dppx)"))&&t.matches||e.devicePixelRatio>1)},a.prototype.resizeImage=function(t,e,s,n,i,o,a,r){var l;return null==e&&(e="auto"),null==s&&(s="auto"),null==n&&(n=1),null==r&&(r=!0),l=new Image,l.onload=function(){var t,r,h,c,d;return h=l.width,r=l.height,console.log("ImageService","current size",h,r),"auto"===s&&"auto"===e&&(e=h,s=r),"auto"===s&&(s=r/(h/e)),"auto"===e&&(e=r/(h/s)),d=!1,e/gi,""),s=s.replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi,""),s=s.replace(/<(\/?)s>/gi,"<$1strike>"),s=s.replace(/ /gi," "),e.html(s),t("p",e).each(function(){var e,s;if(s=t(this).attr("style"),e=/mso-list:\w+ \w+([0-9]+)/.exec(s))return t(this).data("_listLevel",parseInt(e[1],10))}),n=0,i=null,t("p",e).each(function(){var e,s,o,a,r,l,h,c,d,u;if(void 0!==(e=t(this).data("_listLevel"))){if(u=t(this).text(),a="
            ",/^\s*\w+\./.test(u)&&(a=(r=/([0-9])\./.exec(u))?null!=(l=(d=parseInt(r[1],10))>1)?l:'
              ':"
                "}:"
                  "),e>n&&(0===n?(t(this).before(a),i=t(this).prev()):i=t(a).appendTo(i)),e=c;s=h<=c?++o:--o)i=i.parent();return t("span:first",this).remove(),i.append("
                1. "+t(this).html()+"
                2. "),t(this).remove(),n=e}return n=0}),t("[style]",e).removeAttr("style"),t("[align]",e).removeAttr("align"),t("span",e).replaceWith(function(){return t(this).contents()}),t("span:empty",e).remove(),t("[class^='Mso']",e).removeAttr("class"),t("p:empty",e).remove(),e},a.prototype.removeAttribute=function(e){var s,n,i,o,a;if(e){for(s=t(e),i=0,o=(a=e.attributes).length;i/g,">").replace(/"/g,""")}),function(){(function(){this.agent.avatar&&(e.push('\n\n')),e.push('\n\n '),e.push(s(this.agent.name)),e.push("\n")}).call(this)}.call(t),t.safe=n,t.escape=i,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.chat=function(t){t||(t={});var e=[],s=function(t){return t&&t.ecoSafe?t:void 0!==t&&null!=t?i(t):""},n=t.safe,i=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
                  \n
                  \n
                  \n \n \n \n \n \n
                  \n
                  \n
                  \n
                  \n \n '),e.push(this.T(this.title)),e.push('\n
                  \n
                  \n
                  \n \n
                  \n
                  \n
                  \n \n
                  \n
                  ")}).call(this)}.call(t),t.safe=n,t.escape=i,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.customer_timeout=function(t){t||(t={});var e=[],s=function(t){return t&&t.ecoSafe?t:void 0!==t&&null!=t?i(t):""},n=t.safe,i=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
                  \n '),this.agent?(e.push("\n "),e.push(this.T("Since you didn't respond in the last %s minutes your conversation with %s got closed.",this.delay,this.agent)),e.push("\n ")):(e.push("\n "),e.push(this.T("Since you didn't respond in the last %s minutes your conversation got closed.",this.delay)),e.push("\n ")),e.push('\n
                  \n
                  "),e.push(this.T("Start new conversation")),e.push("
                  \n
                  ")}).call(this)}.call(t),t.safe=n,t.escape=i,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.loader=function(t){t||(t={});var e=[],s=t.safe,n=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},n||(n=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('\n \n \n \n\n'),e.push(this.T("Connecting")),e.push("")}).call(this)}.call(t),t.safe=s,t.escape=n,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.message=function(t){t||(t={});var e=[],s=function(t){return t&&t.ecoSafe?t:void 0!==t&&null!=t?i(t):""},n=t.safe,i=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
                  \n "),e.push(this.message),e.push("\n
                  ")}).call(this)}.call(t),t.safe=n,t.escape=i,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.status=function(t){t||(t={});var e=[],s=t.safe,n=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},n||(n=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
                  \n
                  \n '),e.push(this.status),e.push("\n
                  \n
                  ")}).call(this)}.call(t),t.safe=s,t.escape=n,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.timestamp=function(t){t||(t={});var e=[],s=function(t){return t&&t.ecoSafe?t:void 0!==t&&null!=t?i(t):""},n=t.safe,i=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
                  '),e.push(s(this.label)),e.push(" "),e.push(s(this.time)),e.push("
                  ")}).call(this)}.call(t),t.safe=n,t.escape=i,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.typingIndicator=function(t){t||(t={});var e=[],s=t.safe,n=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},n||(n=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
                  \n \n \n \n \n \n \n \n
                  ')}).call(this)}.call(t),t.safe=s,t.escape=n,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting=function(t){t||(t={});var e=[],s=t.safe,n=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},n||(n=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
                  \n \n \n \n \n \n '),e.push(this.T("All colleagues are busy.")),e.push("
                  \n "),e.push(this.T("You are on waiting list position %s.",this.position)),e.push("\n
                  ")}).call(this)}.call(t),t.safe=s,t.escape=n,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting_list_timeout=function(t){t||(t={});var e=[],s=function(t){return t&&t.ecoSafe?t:void 0!==t&&null!=t?i(t):""},n=t.safe,i=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
                  \n '),e.push(this.T("We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!")),e.push('\n
                  \n
                  "),e.push(this.T("Start new conversation")),e.push("
                  \n
                  ")}).call(this)}.call(t),t.safe=n,t.escape=i,e.join("")}; \ No newline at end of file From ab189f46ec88258d54963d0d1919d91236f535de Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Wed, 8 Nov 2017 17:14:12 +0100 Subject: [PATCH 002/196] Fixed issue #1628 - Added support for new Twitter tweet length limit of 280 chars. --- app/assets/javascripts/app/controllers/layout_ref.coffee | 2 +- .../javascripts/app/controllers/ticket_zoom/article_new.coffee | 2 +- .../app/views/layout_ref/twitter_conversation.jst.eco | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/app/controllers/layout_ref.coffee b/app/assets/javascripts/app/controllers/layout_ref.coffee index f4dc268e7..6b49ed6bc 100644 --- a/app/assets/javascripts/app/controllers/layout_ref.coffee +++ b/app/assets/javascripts/app/controllers/layout_ref.coffee @@ -2121,7 +2121,7 @@ class TwitterConversationRef extends App.ControllerContent open: 88 closed: 20 - maxTextLength: 140 + maxTextLength: 280 warningTextLength: 10 constructor: -> diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee index edb39480d..42b036c6f 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee @@ -144,7 +144,7 @@ class App.TicketZoomArticleNew extends App.Controller attributes: [] internal: false, features: ['body:limit', 'body:initials'] - maxTextLength: 140 + maxTextLength: 280 warningTextLength: 30 } if possibleArticleType['twitter direct-message'] diff --git a/app/assets/javascripts/app/views/layout_ref/twitter_conversation.jst.eco b/app/assets/javascripts/app/views/layout_ref/twitter_conversation.jst.eco index c909828a8..7761ae209 100644 --- a/app/assets/javascripts/app/views/layout_ref/twitter_conversation.jst.eco +++ b/app/assets/javascripts/app/views/layout_ref/twitter_conversation.jst.eco @@ -440,7 +440,7 @@
                  From f3ff650bfea78c042468b96a700832bfa9cda95a Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Tue, 14 Nov 2017 14:35:31 +0100 Subject: [PATCH 003/196] Split of article actions into separate backends. --- .../ticket_zoom/article_action/delete.coffee | 34 ++ .../article_action/email_reply.coffee | 135 ++++++ .../article_action/facebook_reply.coffee | 37 ++ .../article_action/internal.coffee | 40 ++ .../ticket_zoom/article_action/split.coffee | 18 + .../article_action/telegram.coffee | 45 ++ .../article_action/twitter_reply.coffee | 128 ++++++ .../ticket_zoom/article_actions.coffee | 390 +----------------- .../ticket_zoom/article_view_actions.jst.eco | 4 +- 9 files changed, 460 insertions(+), 371 deletions(-) create mode 100644 app/assets/javascripts/app/controllers/ticket_zoom/article_action/delete.coffee create mode 100644 app/assets/javascripts/app/controllers/ticket_zoom/article_action/email_reply.coffee create mode 100644 app/assets/javascripts/app/controllers/ticket_zoom/article_action/facebook_reply.coffee create mode 100644 app/assets/javascripts/app/controllers/ticket_zoom/article_action/internal.coffee create mode 100644 app/assets/javascripts/app/controllers/ticket_zoom/article_action/split.coffee create mode 100644 app/assets/javascripts/app/controllers/ticket_zoom/article_action/telegram.coffee create mode 100644 app/assets/javascripts/app/controllers/ticket_zoom/article_action/twitter_reply.coffee diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_action/delete.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/delete.coffee new file mode 100644 index 000000000..7436c15f6 --- /dev/null +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/delete.coffee @@ -0,0 +1,34 @@ +class Delete + @action: (actions, ticket, article, ui) -> + return actions if ui.permissionCheck('ticket.customer') + + if article.type.name is 'note' + user = undefined + if App.Session.get('id') == article.created_by_id + user = App.User.find(App.Session.get('id')) + if user.permission('ticket.agent') + actions.push { + name: 'delete' + type: 'delete' + icon: 'trash' + href: '#' + } + + actions + + @perform: (articleContainer, type, ticket, article, ui) -> + return true if type isnt 'delete' + + callback = -> + article = App.TicketArticle.find(article.id) + article.destroy() + + new App.ControllerConfirm( + message: 'Sure?' + callback: callback + container: ui.el.closest('.content') + ) + + true + +App.Config.set('900-Delete', Delete, 'TicketZoomArticleAction') diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_action/email_reply.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/email_reply.coffee new file mode 100644 index 000000000..eacd9cf14 --- /dev/null +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/email_reply.coffee @@ -0,0 +1,135 @@ +class EmailReply extends App.Controller + @action: (actions, ticket, article, ui) -> + return actions if ui.permissionCheck('ticket.customer') + + group = ticket.group + if group.email_address_id && (article.type.name is 'email' || article.type.name is 'web') + actions.push { + name: 'reply' + type: 'emailReply' + icon: 'reply' + href: '#' + } + recipients = [] + if article.sender.name is 'Customer' + if article.from + localRecipients = emailAddresses.parseAddressList(article.from) + if localRecipients + recipients = recipients.concat localRecipients + if article.to + localRecipients = emailAddresses.parseAddressList(article.to) + if localRecipients + recipients = recipients.concat localRecipients + if article.cc + localRecipients = emailAddresses.parseAddressList(article.cc) + if localRecipients + recipients = recipients.concat localRecipients + + # remove system addresses + localAddresses = App.EmailAddress.all() + forgeinRecipients = [] + recipientUsed = {} + for recipient in recipients + if !_.isEmpty(recipient.address) + localRecipientAddress = recipient.address.toString().toLowerCase() + if !recipientUsed[localRecipientAddress] + recipientUsed[localRecipientAddress] = true + localAddress = false + for address in localAddresses + if localRecipientAddress is address.email.toString().toLowerCase() + recipientUsed[localRecipientAddress] = true + localAddress = true + if !localAddress + forgeinRecipients.push recipient + + # check if reply all is neede + if forgeinRecipients.length > 1 + actions.push { + name: 'reply all' + type: 'emailReplyAll' + icon: 'reply-all' + href: '#' + } + if article.sender.name is 'Customer' && article.type.name is 'phone' + actions.push { + name: 'reply' + type: 'emailReply' + icon: 'reply' + href: '#' + } + if article.sender.name is 'Agent' && article.type.name is 'phone' + actions.push { + name: 'reply' + type: 'emailReply' + icon: 'reply' + href: '#' + } + + actions + + @perform: (articleContainer, type, ticket, article, ui) -> + return true if type isnt 'emailReply' && type isnt 'emailReplyAll' + + if type isnt 'emailReply' + @emailReply(true, ticket, article, ui) + + else if type isnt 'emailReplyAll' + @emailReply(false, ticket, article, ui) + + true + + @emailReply: (all = false, ticket, article, ui) -> + + # get reference article + type = App.TicketArticleType.find(article.type_id) + article_created_by = App.User.find(article.created_by_id) + email_addresses = App.EmailAddress.all() + + ui.scrollToCompose() + + # empty form + articleNew = App.Utils.getRecipientArticle(ticket, article, article_created_by, type, email_addresses, all) + + # get current body + body = ui.el.closest('.ticketZoom').find('.article-add [data-name="body"]').html() || '' + + # check if quote need to be added + signaturePosition = 'bottom' + selected = App.ClipBoard.getSelected('html') + if selected + selected = App.Utils.htmlCleanup(selected).html() + if !selected + selected = App.ClipBoard.getSelected('text') + if selected + selected = App.Utils.textCleanup(selected) + selected = App.Utils.text2html(selected) + + # full quote, if needed + if !selected && article && App.Config.get('ui_ticket_zoom_article_email_full_quote') + signaturePosition = 'top' + if article.content_type.match('html') + selected = App.Utils.textCleanup(article.body) + if article.content_type.match('plain') + selected = App.Utils.textCleanup(article.body) + selected = App.Utils.text2html(selected) + + if selected + selected = "


                  #{selected}

                  " + + # add selected text to body + body = selected + body + + articleNew.body = body + + type = App.TicketArticleType.findByAttribute(name:'email') + + App.Event.trigger('ui::ticket::setArticleType', { + ticket: ticket + type: type + article: articleNew + signaturePosition: signaturePosition + }) + + true + +App.Config.set('200-EmailReply', EmailReply, 'TicketZoomArticleAction') diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_action/facebook_reply.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/facebook_reply.coffee new file mode 100644 index 000000000..41c9ad4e8 --- /dev/null +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/facebook_reply.coffee @@ -0,0 +1,37 @@ +class FacebookReply + @action: (actions, ticket, article, ui) -> + return actions if ui.permissionCheck('ticket.customer') + + if article.type.name is 'facebook feed post' || article.type.name is 'facebook feed comment' + actions.push { + name: 'reply' + type: 'facebookFeedReply' + icon: 'reply' + href: '#' + } + + actions + + @perform: (articleContainer, type, ticket, article, ui) -> + return true if type isnt 'facebookFeedReply' + + ui.scrollToCompose() + + type = App.TicketArticleType.findByAttribute('name', 'facebook feed comment') + + articleNew = { + to: '' + cc: '' + body: '' + in_reply_to: '' + } + + App.Event.trigger('ui::ticket::setArticleType', { + ticket: ticket + type: type + article: articleNew + }) + + true + +App.Config.set('300-FacebookReply', FacebookReply, 'TicketZoomArticleAction') diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_action/internal.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/internal.coffee new file mode 100644 index 000000000..8fc1e88bd --- /dev/null +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/internal.coffee @@ -0,0 +1,40 @@ +class Internal + @action: (actions, ticket, article, ui) -> + return actions if ui.permissionCheck('ticket.customer') + + if article.internal is true + actions.push { + name: 'set to public' + type: 'public' + icon: 'lock-open' + } + else + actions.push { + name: 'set to internal' + type: 'internal' + icon: 'lock' + } + + actions + + @perform: (articleContainer, type, ticket, article, ui) -> + return true if type isnt 'internal' && type isnt 'public' + + # storage update + internal = true + if article.internal == true + internal = false + ui.lastAttributres.internal = internal + article.updateAttributes(internal: internal) + + # runtime update + if internal + articleContainer.addClass('is-internal') + else + articleContainer.removeClass('is-internal') + + ui.render() + + true + +App.Config.set('100-Internal', Internal, 'TicketZoomArticleAction') diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_action/split.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/split.coffee new file mode 100644 index 000000000..716267dd2 --- /dev/null +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/split.coffee @@ -0,0 +1,18 @@ +class Split + @action: (actions, ticket, article, ui) -> + return actions if ui.permissionCheck('ticket.customer') + + actions.push { + name: 'split' + type: 'split' + icon: 'split' + href: "#ticket/create/#{article.ticket_id}/#{article.id}" + } + actions + + @perform: (articleContainer, type, ticket, article, ui) -> + return true if type isnt 'split' + ui.navigate "#ticket/create/#{article.ticket_id}/#{article.id}" + true + +App.Config.set('700-Split', Split, 'TicketZoomArticleAction') diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_action/telegram.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/telegram.coffee new file mode 100644 index 000000000..6f18a172d --- /dev/null +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/telegram.coffee @@ -0,0 +1,45 @@ +class TelegramReply + @action: (actions, ticket, article, ui) -> + return actions if ui.permissionCheck('ticket.customer') + + if article.sender.name is 'Customer' && article.type.name is 'telegram personal-message' + actions.push { + name: 'reply' + type: 'telegramPersonalMessageReply' + icon: 'reply' + href: '#' + } + + actions + + @perform: (articleContainer, type, ticket, article, ui) -> + return true if type isnt 'telegramPersonalMessageReply' + + ui.scrollToCompose() + + # get reference article + type = App.TicketArticleType.find(article.type_id) + + articleNew = { + to: '' + cc: '' + body: '' + in_reply_to: '' + } + + if article.message_id + articleNew.in_reply_to = article.message_id + + # get current body + articleNew.body = ui.el.closest('.ticketZoom').find('.article-add [data-name="body"]').html().trim() || '' + + App.Event.trigger('ui::ticket::setArticleType', { + ticket: ticket + type: type + article: articleNew + position: 'end' + }) + + true + +App.Config.set('300-TelegramReply', TelegramReply, 'TicketZoomArticleAction') diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_action/twitter_reply.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/twitter_reply.coffee new file mode 100644 index 000000000..8b03ba249 --- /dev/null +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/twitter_reply.coffee @@ -0,0 +1,128 @@ +class TwitterReply + @action: (actions, ticket, article, ui) -> + return actions if ui.permissionCheck('ticket.customer') + + if article.type.name is 'twitter status' + actions.push { + name: 'reply' + type: 'twitterStatusReply' + icon: 'reply' + href: '#' + } + if article.type.name is 'twitter direct-message' + actions.push { + name: 'reply' + type: 'twitterDirectMessageReply' + icon: 'reply' + href: '#' + } + + actions + + @perform: (articleContainer, type, ticket, article, ui) -> + return true if type isnt 'twitterStatusReply' && type isnt 'twitterDirectMessageReply' + + if type is 'twitterStatusReply' + @twitterStatusReply(ticket, article, ui) + + else if type is 'twitterDirectMessageReply' + @twitterDirectMessageReply(ticket, article, ui) + + true + + @twitterStatusReply: (ticket, article, ui) -> + + ui.scrollToCompose() + + # get reference article + type = App.TicketArticleType.find(article.type_id) + + # empty form + articleNew = { + to: '' + cc: '' + body: '' + in_reply_to: '' + } + + if article.message_id + articleNew.in_reply_to = article.message_id + + # get current body + body = ui.el.closest('.ticketZoom').find('.article-add [data-name="body"]').html().trim() || '' + articleNew.body = body + + recipients = article.from + if article.to + if recipients + recipients += ', ' + recipients += article.to + + if recipients + recipientString = '' + recipientScreenNames = recipients.split(',') + for recipientScreenName in recipientScreenNames + if recipientScreenName + recipientScreenName = recipientScreenName.trim().toLowerCase() + + # exclude already listed screen name + exclude = false + if body && body.toLowerCase().match(recipientScreenName) + exclude = true + + # exclude own screen_name + if recipientScreenName is "@#{@ticket.preferences.channel_screen_name}".toLowerCase() + exclude = true + + if exclude is false + if recipientString isnt '' + recipientString += ' ' + recipientString += recipientScreenName + + if body + articleNew.body = "#{recipientString} #{body} " + else + articleNew.body = "#{recipientString} " + + App.Event.trigger('ui::ticket::setArticleType', { + ticket: ticket + type: type + article: articleNew + position: 'end' + }) + + @twitterDirectMessageReply: (ticket, article, ui) -> + + # get reference article + type = App.TicketArticleType.find(article.type_id) + sender = App.TicketArticleSender.find(article.sender_id) + customer = App.User.find(article.created_by_id) + + ui.scrollToCompose() + + # empty form + articleNew = { + to: '' + cc: '' + body: '' + in_reply_to: '' + } + + if article.message_id + articleNew.in_reply_to = article.message_id + + if sender.name is 'Agent' + articleNew.to = article.to + else + articleNew.to = article.from + + if !articleNew.to + articleNew.to = customer.accounts['twitter'].username || customer.accounts['twitter'].uid + + App.Event.trigger('ui::ticket::setArticleType', { + ticket: ticket + type: type + article: articleNew + }) + +App.Config.set('300-TwitterReply', TwitterReply, 'TicketZoomArticleAction') diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_actions.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_actions.coffee index 3f42ce106..fbece5315 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_actions.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_actions.coffee @@ -1,21 +1,13 @@ class App.TicketZoomArticleActions extends App.Controller events: - 'click [data-type=public]': 'publicInternal' - 'click [data-type=internal]': 'publicInternal' - 'click [data-type=emailReply]': 'emailReply' - 'click [data-type=emailReplyAll]': 'emailReplyAll' - 'click [data-type=twitterStatusReply]': 'twitterStatusReply' - 'click [data-type=twitterDirectMessageReply]': 'twitterDirectMessageReply' - 'click [data-type=facebookFeedReply]': 'facebookFeedReply' - 'click [data-type=telegramPersonalMessageReply]': 'telegramPersonalMessageReply' - 'click [data-type=delete]': 'delete' + 'click .js-ArticleAction': 'actionPerform' constructor: -> super @render() render: -> - actions = @actionRow(@article) + actions = @actionRow(@ticket, @article) if actions @html App.view('ticket_zoom/article_view_actions')( @@ -25,371 +17,31 @@ class App.TicketZoomArticleActions extends App.Controller else @html '' - publicInternal: (e) => - e.preventDefault() - articleContainer = $(e.target).closest('.ticket-article-item') - article_id = $(e.target).parents('[data-id]').data('id') - - # storage update - article = App.TicketArticle.find(article_id) - internal = true - if article.internal == true - internal = false - @lastAttributres.internal = internal - article.updateAttributes(internal: internal) - - # runntime update - if internal - articleContainer.addClass('is-internal') - else - articleContainer.removeClass('is-internal') - - @render() - - actionRow: (article) -> - if @permissionCheck('ticket.customer') - return [] - + actionRow: (ticket, article) -> + actionConfig = App.Config.get('TicketZoomArticleAction') + keys = _.keys(actionConfig).sort() actions = [] - if article.internal is true - actions = [ - { - name: 'set to public' - type: 'public' - icon: 'lock-open' - } - ] - else - actions = [ - { - name: 'set to internal' - type: 'internal' - icon: 'lock' - } - ] - #if @article.type.name is 'note' - # actions.push [] - group = @ticket.group - if group.email_address_id && (article.type.name is 'email' || article.type.name is 'web') - actions.push { - name: 'reply' - type: 'emailReply' - icon: 'reply' - href: '#' - } - recipients = [] - if article.sender.name is 'Customer' - if article.from - localRecipients = emailAddresses.parseAddressList(article.from) - if localRecipients - recipients = recipients.concat localRecipients - if article.to - localRecipients = emailAddresses.parseAddressList(article.to) - if localRecipients - recipients = recipients.concat localRecipients - if article.cc - localRecipients = emailAddresses.parseAddressList(article.cc) - if localRecipients - recipients = recipients.concat localRecipients - - # remove system addresses - localAddresses = App.EmailAddress.all() - forgeinRecipients = [] - recipientUsed = {} - for recipient in recipients - if !_.isEmpty(recipient.address) - localRecipientAddress = recipient.address.toString().toLowerCase() - if !recipientUsed[localRecipientAddress] - recipientUsed[localRecipientAddress] = true - localAddress = false - for address in localAddresses - if localRecipientAddress is address.email.toString().toLowerCase() - recipientUsed[localRecipientAddress] = true - localAddress = true - if !localAddress - forgeinRecipients.push recipient - - # check if reply all is neede - if forgeinRecipients.length > 1 - actions.push { - name: 'reply all' - type: 'emailReplyAll' - icon: 'reply-all' - href: '#' - } - if article.sender.name is 'Customer' && article.type.name is 'phone' - actions.push { - name: 'reply' - type: 'emailReply' - icon: 'reply' - href: '#' - } - if article.sender.name is 'Agent' && article.type.name is 'phone' - actions.push { - name: 'reply' - type: 'emailReply' - icon: 'reply' - href: '#' - } - if article.type.name is 'twitter status' - actions.push { - name: 'reply' - type: 'twitterStatusReply' - icon: 'reply' - href: '#' - } - if article.type.name is 'twitter direct-message' - actions.push { - name: 'reply' - type: 'twitterDirectMessageReply' - icon: 'reply' - href: '#' - } - if article.type.name is 'facebook feed post' || article.type.name is 'facebook feed comment' - actions.push { - name: 'reply' - type: 'facebookFeedReply' - icon: 'reply' - href: '#' - } - if article.sender.name is 'Customer' && article.type.name is 'telegram personal-message' - actions.push { - name: 'reply' - type: 'telegramPersonalMessageReply' - icon: 'reply' - href: '#' - } - - actions.push { - name: 'split' - type: 'split' - icon: 'split' - href: '#ticket/create/' + article.ticket_id + '/' + article.id - } - - if article.type.name is 'note' - user = undefined - if App.Session.get('id') == article.created_by_id - user = App.User.find(App.Session.get('id')) - if user.permission('ticket.agent') - actions.push { - name: 'delete' - type: 'delete' - icon: 'trash' - href: '#' - } + for key in keys + config = actionConfig[key] + if config + actions = config.action(actions, ticket, article, @) actions - facebookFeedReply: (e) => + actionPerform: (e) => e.preventDefault() - type = App.TicketArticleType.findByAttribute('name', 'facebook feed comment') - @scrollToCompose() + articleContainer = $(e.target).closest('.ticket-article-item') + type = $(e.currentTarget).attr('data-type') + ticket = App.Ticket.fullLocal(@ticket.id) + article = App.TicketArticle.fullLocal(@article.id) - # empty form - articleNew = { - to: '' - cc: '' - body: '' - in_reply_to: '' - } - - App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew } ) - - twitterStatusReply: (e) => - e.preventDefault() - - # get reference article - article_id = $(e.target).parents('[data-id]').data('id') - article = App.TicketArticle.fullLocal(article_id) - sender = App.TicketArticleSender.find(article.sender_id) - type = App.TicketArticleType.find(article.type_id) - customer = App.User.find(article.created_by_id) - - @scrollToCompose() - - # empty form - articleNew = { - to: '' - cc: '' - body: '' - in_reply_to: '' - } - - if article.message_id - articleNew.in_reply_to = article.message_id - - # get current body - body = @el.closest('.ticketZoom').find('.article-add [data-name="body"]').html().trim() || '' - articleNew.body = body - - recipients = article.from - if article.to - if recipients - recipients += ', ' - recipients += article.to - - if recipients - recipientString = '' - recipientScreenNames = recipients.split(',') - for recipientScreenName in recipientScreenNames - if recipientScreenName - recipientScreenName = recipientScreenName.trim().toLowerCase() - - # exclude already listed screen name - exclude = false - if body && body.toLowerCase().match(recipientScreenName) - exclude = true - - # exclude own screen_name - if recipientScreenName is "@#{@ticket.preferences.channel_screen_name}".toLowerCase() - exclude = true - - if exclude is false - if recipientString isnt '' - recipientString += ' ' - recipientString += recipientScreenName - - if body - articleNew.body = "#{recipientString} #{body} " - else - articleNew.body = "#{recipientString} " - - App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew, position: 'end' } ) - - twitterDirectMessageReply: (e) => - e.preventDefault() - - # get reference article - article_id = $(e.target).parents('[data-id]').data('id') - article = App.TicketArticle.fullLocal(article_id) - type = App.TicketArticleType.find(article.type_id) - sender = App.TicketArticleSender.find(article.sender_id) - customer = App.User.find(article.created_by_id) - - @scrollToCompose() - - # empty form - articleNew = { - to: '' - cc: '' - body: '' - in_reply_to: '' - } - - if article.message_id - articleNew.in_reply_to = article.message_id - - if sender.name is 'Agent' - articleNew.to = article.to - else - articleNew.to = article.from - - if !articleNew.to - articleNew.to = customer.accounts['twitter'].username || customer.accounts['twitter'].uid - - App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew } ) - - emailReplyAll: (e) => - @emailReply(e, true) - - emailReply: (e, all = false) => - e.preventDefault() - - # get reference article - article_id = $(e.target).parents('[data-id]').data('id') - article = App.TicketArticle.fullLocal(article_id) - ticket = App.Ticket.fullLocal(article.ticket_id) - type = App.TicketArticleType.find(article.type_id) - article_created_by = App.User.find(article.created_by_id) - email_addresses = App.EmailAddress.all() - - @scrollToCompose() - - # empty form - articleNew = App.Utils.getRecipientArticle(ticket, article, article_created_by, type, email_addresses, all) - - # get current body - body = @el.closest('.ticketZoom').find('.article-add [data-name="body"]').html() || '' - - # check if quote need to be added - signaturePosition = 'bottom' - selected = App.ClipBoard.getSelected('html') - if selected - selected = App.Utils.htmlCleanup(selected).html() - if !selected - selected = App.ClipBoard.getSelected('text') - if selected - selected = App.Utils.textCleanup(selected) - selected = App.Utils.text2html(selected) - - # full quote, if needed - if !selected && article && App.Config.get('ui_ticket_zoom_article_email_full_quote') - signaturePosition = 'top' - if article.content_type.match('html') - selected = App.Utils.textCleanup(article.body) - if article.content_type.match('plain') - selected = App.Utils.textCleanup(article.body) - selected = App.Utils.text2html(selected) - - if selected - selected = "


                  #{selected}

                  " - - # add selected text to body - body = selected + body - - articleNew.body = body - - type = App.TicketArticleType.findByAttribute(name:'email') - - App.Event.trigger('ui::ticket::setArticleType', { - ticket: @ticket - type: type - article: articleNew - signaturePosition: signaturePosition - }) - - telegramPersonalMessageReply: (e) => - e.preventDefault() - - # get reference article - article_id = $(e.target).parents('[data-id]').data('id') - article = App.TicketArticle.fullLocal(article_id) - sender = App.TicketArticleSender.find(article.sender_id) - type = App.TicketArticleType.find(article.type_id) - customer = App.User.find(article.created_by_id) - - @scrollToCompose() - - # empty form - articleNew = { - to: '' - cc: '' - body: '' - in_reply_to: '' - } - - if article.message_id - articleNew.in_reply_to = article.message_id - - # get current body - articleNew.body = @el.closest('.ticketZoom').find('.article-add [data-name="body"]').html().trim() || '' - - App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew, position: 'end' } ) - - delete: (e) => - e.preventDefault() - - callback = -> - article_id = $(e.target).parents('[data-id]').data('id') - article = App.TicketArticle.find(article_id) - article.destroy() - - new App.ControllerConfirm( - message: 'Sure?' - callback: callback - container: @el.closest('.content') - ) + actionConfig = App.Config.get('TicketZoomArticleAction') + keys = _.keys(actionConfig).sort() + actions = [] + for key in keys + config = actionConfig[key] + if config + return if !config.perform(articleContainer, type, ticket, article, @) scrollToCompose: => @el.closest('.content').find('.article-add').ScrollTo() diff --git a/app/assets/javascripts/app/views/ticket_zoom/article_view_actions.jst.eco b/app/assets/javascripts/app/views/ticket_zoom/article_view_actions.jst.eco index 26cd41ec2..e0301172b 100644 --- a/app/assets/javascripts/app/views/ticket_zoom/article_view_actions.jst.eco +++ b/app/assets/javascripts/app/views/ticket_zoom/article_view_actions.jst.eco @@ -1,7 +1,7 @@
                  <% for action in @actions: %> - - <%- @Icon(action.icon, 'article-action-icon') %><%- @T( action.name ) %> + + <%- @Icon(action.icon, 'article-action-icon') %><%- @T(action.name) %> <% end %>
                  \ No newline at end of file From 17e107aa0a277410a7c1d92bcfb50810d670770f Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Tue, 14 Nov 2017 15:31:15 +0100 Subject: [PATCH 004/196] Fixed this binding/proxy in callback. --- app/assets/javascripts/app/lib/app_post/_collection_base.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/app/lib/app_post/_collection_base.coffee b/app/assets/javascripts/app/lib/app_post/_collection_base.coffee index ddd758914..2e1a487b0 100644 --- a/app/assets/javascripts/app/lib/app_post/_collection_base.coffee +++ b/app/assets/javascripts/app/lib/app_post/_collection_base.coffee @@ -77,7 +77,7 @@ class App._CollectionSingletonBase callback: (data) => for counter, attr of @callbacks - callback = -> + callback = => attr.callback(data) if attr.one delete @callbacks[counter] From 70565938bada9392659affd6d7533f2a841157c8 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Wed, 15 Nov 2017 12:07:37 +0100 Subject: [PATCH 005/196] Implemented issue #1623 - Translation for form widget in Dutch. --- public/assets/form/form.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/public/assets/form/form.js b/public/assets/form/form.js index b8d0456fd..8bf176a06 100644 --- a/public/assets/form/form.js +++ b/public/assets/form/form.js @@ -124,6 +124,15 @@ $(function() { 'Attachments': 'Pièces jointes', 'Your Message...': 'Votre message...', }, + 'nl': { + 'Name': 'Naam', + 'Your Name': 'Uw naam', + 'Email': 'Email adres', + 'Your Email': 'Uw Email adres', + 'Message': 'Bericht', + 'Attachments': 'Bijlage', + 'Your Message...': 'Uw bericht.......', + }, 'zh-cn': { 'Name': '联系人', 'Your Name': '您的尊姓大名', From 601960f5fbe260d00b8ca7ce7fc579dc26739564 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Wed, 15 Nov 2017 15:06:53 +0100 Subject: [PATCH 006/196] Implemented issue #1171 - The ability to set the platform default language. --- .../controllers/_settings/area_item.coffee | 2 +- .../_settings/area_item_default_locale.coffee | 21 +++++++ .../controllers/widget/default_locale.coffee | 23 +++++++ .../widget/switch_back_to_user.coffee | 2 +- .../javascripts/app/lib/app_post/auth.coffee | 5 +- .../javascripts/app/lib/app_post/i18n.coffee | 18 ++++++ app/controllers/first_steps_controller.rb | 2 +- app/models/locale.rb | 2 +- app/models/text_module.rb | 4 +- app/models/transaction/clearbit_enrichment.rb | 2 +- app/models/transaction/notification.rb | 2 +- app/models/transaction/slack.rb | 2 +- app/models/translation.rb | 4 +- .../20171115000001_setting_default_locale.rb | 28 +++++++++ db/seeds/settings.rb | 19 ++++++ lib/calendar_subscriptions/tickets.rb | 24 ++++---- lib/notification_factory.rb | 2 +- lib/notification_factory/mailer.rb | 2 +- lib/notification_factory/renderer.rb | 2 +- lib/notification_factory/slack.rb | 2 +- ...tification_factory_mailer_template_test.rb | 60 +++++++++++++++++++ 21 files changed, 198 insertions(+), 30 deletions(-) create mode 100644 app/assets/javascripts/app/controllers/_settings/area_item_default_locale.coffee create mode 100644 app/assets/javascripts/app/controllers/widget/default_locale.coffee create mode 100644 db/migrate/20171115000001_setting_default_locale.rb diff --git a/app/assets/javascripts/app/controllers/_settings/area_item.coffee b/app/assets/javascripts/app/controllers/_settings/area_item.coffee index c7db71eb3..877caeee0 100644 --- a/app/assets/javascripts/app/controllers/_settings/area_item.coffee +++ b/app/assets/javascripts/app/controllers/_settings/area_item.coffee @@ -32,7 +32,7 @@ class App.SettingsAreaItem extends App.Controller ) new App.ControllerForm( - el: @el.find('.form-item'), + el: @el.find('.form-item') model: { configure_attributes: @configure_attributes, className: '' } autofocus: false ) diff --git a/app/assets/javascripts/app/controllers/_settings/area_item_default_locale.coffee b/app/assets/javascripts/app/controllers/_settings/area_item_default_locale.coffee new file mode 100644 index 000000000..5d22c5a59 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_settings/area_item_default_locale.coffee @@ -0,0 +1,21 @@ +class App.SettingsAreaItemDefaultLocale extends App.SettingsAreaItem + + render: => + + options = {} + locales = App.Locale.all() + for locale in locales + options[locale.locale] = locale.name + configure_attributes = [ + { name: 'locale_default', display: '', tag: 'searchable_select', null: false, class: 'input', options: options, default: @setting.state_current.value }, + ] + + @html App.view(@template)( + setting: @setting + ) + + new App.ControllerForm( + el: @el.find('.form-item') + model: { configure_attributes: configure_attributes, className: '' } + autofocus: false + ) diff --git a/app/assets/javascripts/app/controllers/widget/default_locale.coffee b/app/assets/javascripts/app/controllers/widget/default_locale.coffee new file mode 100644 index 000000000..db4437c30 --- /dev/null +++ b/app/assets/javascripts/app/controllers/widget/default_locale.coffee @@ -0,0 +1,23 @@ +class DefaultLocale extends App.Controller + constructor: -> + super + + check = => + + preferences = App.Session.get('preferences') + return if !preferences + return if !_.isEmpty(preferences.locale) + locale = App.i18n.get() + @ajax( + id: "i18n-set-user-#{locale}" + type: 'PUT' + url: "#{App.Config.get('api_path')}/users/preferences" + data: JSON.stringify(locale: locale) + processData: true + ) + + App.Event.bind('auth:login', (session) => + @delay(check, 3500, 'default_locale') + ) + +App.Config.set('default_locale', DefaultLocale, 'Widgets') diff --git a/app/assets/javascripts/app/controllers/widget/switch_back_to_user.coffee b/app/assets/javascripts/app/controllers/widget/switch_back_to_user.coffee index 9f5e3c526..612d9cbe4 100644 --- a/app/assets/javascripts/app/controllers/widget/switch_back_to_user.coffee +++ b/app/assets/javascripts/app/controllers/widget/switch_back_to_user.coffee @@ -47,4 +47,4 @@ class Widget extends App.ControllerWidgetOnDemand 800 ) -App.Config.set( 'switch_back_to_user', Widget, 'Widgets' ) +App.Config.set('switch_back_to_user', Widget, 'Widgets') diff --git a/app/assets/javascripts/app/lib/app_post/auth.coffee b/app/assets/javascripts/app/lib/app_post/auth.coffee index f46c8c6e3..7f95f2375 100644 --- a/app/assets/javascripts/app/lib/app_post/auth.coffee +++ b/app/assets/javascripts/app/lib/app_post/auth.coffee @@ -79,8 +79,7 @@ class App.Auth @_updateModelAttributes(data.models) # set locale - locale = window.navigator.userLanguage || window.navigator.language || 'en-us' - App.i18n.set(locale) + App.i18n.set(App.i18n.detectBrowserLocale()) # rebuild navbar with new navbar items App.Event.trigger('auth') @@ -120,7 +119,7 @@ class App.Auth if preferences && preferences.locale locale = preferences.locale if !locale - locale = window.navigator.userLanguage || window.navigator.language || 'en-us' + locale = App.i18n.detectBrowserLocale() App.i18n.set(locale) App.Event.trigger('auth:login', data.session) diff --git a/app/assets/javascripts/app/lib/app_post/i18n.coffee b/app/assets/javascripts/app/lib/app_post/i18n.coffee index 560562269..af8b5ef3d 100644 --- a/app/assets/javascripts/app/lib/app_post/i18n.coffee +++ b/app/assets/javascripts/app/lib/app_post/i18n.coffee @@ -80,6 +80,24 @@ class App.i18n _instance ?= new _i18nSingleton() _instance.mapTime + @detectBrowserLocale: -> + return 'en-us' if !window.navigator.userLanguage && !window.navigator.language + + if window.navigator.languages + allLocales = App.Locale.all() + for browserLocale in window.navigator.languages + for localAllLocale in allLocales + if browserLocale is localAllLocale.locale + return localAllLocale.locale + + for browserLocale in window.navigator.languages + browserLocale = browserLocale.substr(0, 2) + for localAllLocale in allLocales + if browserLocale is localAllLocale.alias + return localAllLocale.locale + + window.navigator.userLanguage || window.navigator.language || 'en-us' + class _i18nSingleton extends Spine.Module @include App.LogInclude diff --git a/app/controllers/first_steps_controller.rb b/app/controllers/first_steps_controller.rb index 096879693..0dd392be3 100644 --- a/app/controllers/first_steps_controller.rb +++ b/app/controllers/first_steps_controller.rb @@ -177,7 +177,7 @@ class FirstStepsController < ApplicationController original_user_id = UserInfo.current_user_id result = NotificationFactory::Mailer.template( template: 'test_ticket', - locale: agent.preferences[:locale] || 'en-us', + locale: agent.preferences[:locale] || Setting.get('locale_default') || 'en-us', objects: { agent: agent, customer: customer, diff --git a/app/models/locale.rb b/app/models/locale.rb index 4ce319646..fa597f189 100644 --- a/app/models/locale.rb +++ b/app/models/locale.rb @@ -24,7 +24,7 @@ returns # read used locales based on env, e. g. export Z_LOCALES='en-us:de-de' if ENV['Z_LOCALES'] - locales = Locale.where(active: true, locale: ENV['Z_LOCALES'].split(':') ) + locales = Locale.where(active: true, locale: ENV['Z_LOCALES'].split(':')) end locales end diff --git a/app/models/text_module.rb b/app/models/text_module.rb index b01d3a037..1cec59992 100644 --- a/app/models/text_module.rb +++ b/app/models/text_module.rb @@ -18,7 +18,7 @@ load text modules from online =end def self.load(locale, overwrite_existing_item = false) - raise 'Got no locale' if locale.empty? + raise 'Got no locale' if locale.blank? locale = locale.split(',').first.downcase # in case of accept_language header is given url = "https://i18n.zammad.com/api/v1/text_modules/#{locale}" @@ -67,7 +67,7 @@ push text_modules to online text_modules_to_push.push text_module end - return true if text_modules_to_push.empty? + return true if text_modules_to_push.blank? url = 'https://i18n.zammad.com/api/v1/text_modules/thanks_for_your_support' diff --git a/app/models/transaction/clearbit_enrichment.rb b/app/models/transaction/clearbit_enrichment.rb index b9c186e9b..e38f9a052 100644 --- a/app/models/transaction/clearbit_enrichment.rb +++ b/app/models/transaction/clearbit_enrichment.rb @@ -31,7 +31,7 @@ class Transaction::ClearbitEnrichment config = Setting.get('clearbit_config') return if !config - return if config['api_key'].empty? + return if config['api_key'].blank? user = User.lookup(id: @item[:object_id]) return if !user diff --git a/app/models/transaction/notification.rb b/app/models/transaction/notification.rb index 398426e0b..8c5a74539 100644 --- a/app/models/transaction/notification.rb +++ b/app/models/transaction/notification.rb @@ -231,7 +231,7 @@ class Transaction::Notification def human_changes(user, record) return {} if !@item[:changes] - locale = user.preferences[:locale] || 'en-us' + locale = user.preferences[:locale] || Setting.get('locale_default') || 'en-us' # only show allowed attributes attribute_list = ObjectManager::Attribute.by_object_as_hash('Ticket', user) diff --git a/app/models/transaction/slack.rb b/app/models/transaction/slack.rb index 124685618..aac7d393a 100644 --- a/app/models/transaction/slack.rb +++ b/app/models/transaction/slack.rb @@ -197,7 +197,7 @@ class Transaction::Slack return {} if !@item[:changes] user = User.find(1) - locale = user.preferences[:locale] || 'en-us' + locale = user.preferences[:locale] || Setting.get('locale_default') || 'en-us' # only show allowed attributes attribute_list = ObjectManager::Attribute.by_object_as_hash('Ticket', user) diff --git a/app/models/translation.rb b/app/models/translation.rb index 5a406a21a..dbb013345 100644 --- a/app/models/translation.rb +++ b/app/models/translation.rb @@ -65,7 +65,7 @@ push translations to online end end - return true if translations_to_push.empty? + return true if translations_to_push.blank? url = 'https://i18n.zammad.com/api/v1/translations/thanks_for_your_support' @@ -108,7 +108,7 @@ reset translations to origin # only push changed translations translations = Translation.where(locale: locale) translations.each do |translation| - if !translation.target_initial || translation.target_initial.empty? + if translation.target_initial.blank? translation.destroy elsif translation.target != translation.target_initial translation.target = translation.target_initial diff --git a/db/migrate/20171115000001_setting_default_locale.rb b/db/migrate/20171115000001_setting_default_locale.rb new file mode 100644 index 000000000..e91bbfa12 --- /dev/null +++ b/db/migrate/20171115000001_setting_default_locale.rb @@ -0,0 +1,28 @@ +class SettingDefaultLocale < ActiveRecord::Migration[5.1] + def up + + # return if it's a new setup + return if !Setting.find_by(name: 'system_init_done') + + Setting.create_if_not_exists( + title: 'Locale', + name: 'locale_default', + area: 'System::Base', + description: 'Defines the system default language.', + options: { + form: [ + { + name: 'locale_default', + } + ], + }, + state: 'en-us', + preferences: { + controller: 'SettingsAreaItemDefaultLocale', + permission: ['admin.system'], + }, + frontend: true + ) + end + +end diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb index 12b351599..decad0c72 100644 --- a/db/seeds/settings.rb +++ b/db/seeds/settings.rb @@ -183,6 +183,25 @@ Setting.create_or_update( state: 'relative', frontend: true ) +Setting.create_if_not_exists( + title: 'Locale', + name: 'locale_default', + area: 'System::Base', + description: 'Defines the system default language.', + options: { + form: [ + { + name: 'locale_default', + } + ], + }, + state: 'en-us', + preferences: { + controller: 'SettingsAreaItemDefaultLocale', + permission: ['admin.system'], + }, + frontend: true +) options = {} (10..99).each do |item| options[item] = item diff --git a/lib/calendar_subscriptions/tickets.rb b/lib/calendar_subscriptions/tickets.rb index 631538042..571c2c698 100644 --- a/lib/calendar_subscriptions/tickets.rb +++ b/lib/calendar_subscriptions/tickets.rb @@ -11,7 +11,7 @@ class CalendarSubscriptions::Tickets def all events_data = [] - return events_data if @preferences.empty? + return events_data if @preferences.blank? events_data += new_open events_data += pending @@ -24,7 +24,7 @@ class CalendarSubscriptions::Tickets alarm = false - return alarm if @preferences.empty? + return alarm if @preferences.blank? return alarm if !@preferences[:alarm] @preferences[:alarm] @@ -34,11 +34,11 @@ class CalendarSubscriptions::Tickets owner_ids = [] - return owner_ids if @preferences.empty? - return owner_ids if !@preferences[ method ] - return owner_ids if @preferences[ method ].empty? + return owner_ids if @preferences.blank? + return owner_ids if !@preferences[method] + return owner_ids if @preferences[method].blank? - preferences = @preferences[ method ] + preferences = @preferences[method] if preferences[:own] owner_ids = [ @user.id ] @@ -54,7 +54,7 @@ class CalendarSubscriptions::Tickets events_data = [] owner_ids = owner_ids(:new_open) - return events_data if owner_ids.empty? + return events_data if owner_ids.blank? condition = { 'ticket.owner_id' => { @@ -76,7 +76,7 @@ class CalendarSubscriptions::Tickets condition: condition, ) - user_locale = @user.preferences['locale'] || 'en' + user_locale = @user.preferences['locale'] || Setting.get('locale_default') || 'en-us' translated_ticket = Translation.translate(user_locale, 'ticket') events_data = [] @@ -101,7 +101,7 @@ class CalendarSubscriptions::Tickets events_data = [] owner_ids = owner_ids(:pending) - return events_data if owner_ids.empty? + return events_data if owner_ids.blank? condition = { 'ticket.owner_id' => { @@ -126,7 +126,7 @@ class CalendarSubscriptions::Tickets condition: condition, ) - user_locale = @user.preferences['locale'] || 'en' + user_locale = @user.preferences['locale'] || Setting.get('locale_default') || 'en-us' translated_ticket = Translation.translate(user_locale, 'ticket') customer = Translation.translate(user_locale, 'customer') @@ -165,7 +165,7 @@ class CalendarSubscriptions::Tickets events_data = [] owner_ids = owner_ids(:escalation) - return events_data if owner_ids.empty? + return events_data if owner_ids.blank? condition = { 'ticket.owner_id' => { @@ -183,7 +183,7 @@ class CalendarSubscriptions::Tickets condition: condition, ) - user_locale = @user.preferences['locale'] || 'en' + user_locale = @user.preferences['locale'] || Setting.get('locale_default') || 'en-us' translated_ticket_escalation = Translation.translate(user_locale, 'ticket escalation') customer = Translation.translate(user_locale, 'customer') diff --git a/lib/notification_factory.rb b/lib/notification_factory.rb index 26de217e3..477a9994e 100644 --- a/lib/notification_factory.rb +++ b/lib/notification_factory.rb @@ -31,7 +31,7 @@ returns template_subject = nil template_body = '' - locale = data[:locale] || 'en' + locale = data[:locale] || Setting.get('locale_default') || 'en-us' template = data[:template] format = data[:format] type = data[:type] diff --git a/lib/notification_factory/mailer.rb b/lib/notification_factory/mailer.rb index 68dec4cf8..b949f044e 100644 --- a/lib/notification_factory/mailer.rb +++ b/lib/notification_factory/mailer.rb @@ -264,7 +264,7 @@ returns end template = NotificationFactory.template_read( - locale: data[:locale] || 'en', + locale: data[:locale] || Setting.get('locale_default') || 'en-us', template: data[:template], format: 'html', type: 'mailer', diff --git a/lib/notification_factory/renderer.rb b/lib/notification_factory/renderer.rb index 680200f5e..5e700c876 100644 --- a/lib/notification_factory/renderer.rb +++ b/lib/notification_factory/renderer.rb @@ -25,7 +25,7 @@ examples how to use def initialize(objects, locale, template, escape = true) @objects = objects - @locale = locale || 'en-us' + @locale = locale || Setting.get('locale_default') || 'en-us' @template = NotificationFactory::Template.new(template, escape) @escape = escape end diff --git a/lib/notification_factory/slack.rb b/lib/notification_factory/slack.rb index da8dc3b6c..87221565a 100644 --- a/lib/notification_factory/slack.rb +++ b/lib/notification_factory/slack.rb @@ -27,7 +27,7 @@ returns end template = NotificationFactory.template_read( - locale: data[:locale] || 'en', + locale: data[:locale] || Setting.get('locale_default') || 'en-us', template: data[:template], format: 'md', type: 'slack', diff --git a/test/unit/notification_factory_mailer_template_test.rb b/test/unit/notification_factory_mailer_template_test.rb index 7889b6057..2dc38a9e8 100644 --- a/test/unit/notification_factory_mailer_template_test.rb +++ b/test/unit/notification_factory_mailer_template_test.rb @@ -201,6 +201,66 @@ class NotificationFactoryMailerTemplateTest < ActiveSupport::TestCase assert_no_match('longname', result[:body]) assert_match('Current User', result[:body]) + Setting.set('locale_default', 'de-de') + result = NotificationFactory::Mailer.template( + template: 'ticket_update', + objects: { + ticket: ticket, + article: article, + recipient: agent1, + current_user: agent_current_user, + changes: changes, + }, + ) + assert_match('Ticket aktualisiert', result[:subject]) + assert_match('Notification<b>xxx</b>', result[:body]) + assert_match('wurde von', result[:body]) + assert_match('test123', result[:body]) + assert_match('Benachrichtigungseinstellungen Verwalten', result[:body]) + assert_no_match('Your', result[:body]) + assert_no_match('longname', result[:body]) + assert_match('Current User', result[:body]) + + Setting.set('locale_default', 'not_existing') + result = NotificationFactory::Mailer.template( + template: 'ticket_update', + objects: { + ticket: ticket, + article: article, + recipient: agent1, + current_user: agent_current_user, + changes: changes, + }, + ) + assert_match('Updated Ticket', result[:subject]) + assert_match('Notification<b>xxx</b>', result[:body]) + assert_match('has been updated by', result[:body]) + assert_match('test123', result[:body]) + assert_match('Manage your notifications settings', result[:body]) + assert_no_match('Dein', result[:body]) + assert_no_match('longname', result[:body]) + assert_match('Current User', result[:body]) + + Setting.set('locale_default', 'pt-br') + result = NotificationFactory::Mailer.template( + template: 'ticket_update', + objects: { + ticket: ticket, + article: article, + recipient: agent1, + current_user: agent_current_user, + changes: changes, + }, + ) + assert_match('Chamado atualizado', result[:subject]) + assert_match('Notification<b>xxx</b>', result[:body]) + assert_match('foi atualizado por', result[:body]) + assert_match('test123', result[:body]) + assert_match('Manage your notifications settings', result[:body]) + assert_no_match('Dein', result[:body]) + assert_no_match('longname', result[:body]) + assert_match('Current User', result[:body]) + end end From c7606f66cec7f387eee85430267603d1d76d5692 Mon Sep 17 00:00:00 2001 From: Johannes Nickel Date: Wed, 15 Nov 2017 17:33:14 +0100 Subject: [PATCH 007/196] Update ISSUE_TEMPLATE.md added reference to the new community board. --- .github/ISSUE_TEMPLATE.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index b7776694b..5e7e1a1cd 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,6 +1,16 @@ @@ -35,3 +45,4 @@ Note: We always do our best. Unfortunately, sometimes the requests are too much * +Yes I'm sure this is a bug and no feature request or a general question. From cb35d2b4fb1a7b473a427834202e83fdd7a0d362 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Thu, 16 Nov 2017 22:54:31 +0100 Subject: [PATCH 008/196] Code cleanup. --- app/models/ticket/overviews.rb | 2 +- test/unit/ticket_test.rb | 40 +++++++++++++++++----------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/app/models/ticket/overviews.rb b/app/models/ticket/overviews.rb index 87e0d16fb..de060ac01 100644 --- a/app/models/ticket/overviews.rb +++ b/app/models/ticket/overviews.rb @@ -114,7 +114,7 @@ returns order_by = "tickets.#{order_by} #{overview.order[:direction]}" # check if group by exists - if overview.group_by && !overview.group_by.empty? + if overview.group_by.present? group_by = overview.group_by if !ticket_attributes.key?(group_by) group_by = if ticket_attributes.key?("#{group_by}_id") diff --git a/test/unit/ticket_test.rb b/test/unit/ticket_test.rb index 549d7130e..39a51b90f 100644 --- a/test/unit/ticket_test.rb +++ b/test/unit/ticket_test.rb @@ -3,7 +3,7 @@ require 'test_helper' class TicketTest < ActiveSupport::TestCase test 'ticket create' do - ticket = Ticket.create( + ticket = Ticket.create!( title: "some title\n äöüß", group: Group.lookup(name: 'Users'), customer_id: 2, @@ -19,7 +19,7 @@ class TicketTest < ActiveSupport::TestCase assert_equal(ticket.state.name, 'new', 'ticket.state verify') # create inbound article #1 - article_inbound1 = Ticket::Article.create( + article_inbound1 = Ticket::Article.create!( ticket_id: ticket.id, from: 'some_sender@example.com', to: 'some_recipient@example.com', @@ -44,7 +44,7 @@ class TicketTest < ActiveSupport::TestCase # create inbound article #2 travel 2.seconds - article_inbound2 = Ticket::Article.create( + article_inbound2 = Ticket::Article.create!( ticket_id: ticket.id, from: 'some_sender@example.com', to: 'some_recipient@example.com', @@ -68,7 +68,7 @@ class TicketTest < ActiveSupport::TestCase assert_nil(ticket.close_at, 'ticket.close_at verify - inbound') # create note article - article_note = Ticket::Article.create( + article_note = Ticket::Article.create!( ticket_id: ticket.id, from: 'some person', subject: "some\nnote", @@ -92,7 +92,7 @@ class TicketTest < ActiveSupport::TestCase # create outbound article travel 2.seconds - article_outbound = Ticket::Article.create( + article_outbound = Ticket::Article.create!( ticket_id: ticket.id, from: 'some_recipient@example.com', to: 'some_sender@example.com', @@ -115,7 +115,7 @@ class TicketTest < ActiveSupport::TestCase assert_nil(ticket.close_at, 'ticket.close_at verify - outbound') # create inbound article #3 - article_inbound3 = Ticket::Article.create( + article_inbound3 = Ticket::Article.create!( ticket_id: ticket.id, from: 'some_sender@example.com', to: 'some_recipient@example.com', @@ -140,7 +140,7 @@ class TicketTest < ActiveSupport::TestCase # create inbound article #4 travel 2.seconds - article_inbound4 = Ticket::Article.create( + article_inbound4 = Ticket::Article.create!( ticket_id: ticket.id, from: 'some_sender@example.com', to: 'some_recipient@example.com', @@ -192,7 +192,7 @@ class TicketTest < ActiveSupport::TestCase assert_nil(ticket.pending_time) # delete article - article_note = Ticket::Article.create( + article_note = Ticket::Article.create!( ticket_id: ticket.id, from: 'some person', subject: 'some note', @@ -218,7 +218,7 @@ class TicketTest < ActiveSupport::TestCase end test 'ticket latest change' do - ticket1 = Ticket.create( + ticket1 = Ticket.create!( title: 'latest change 1', group: Group.lookup(name: 'Users'), customer_id: 2, @@ -231,7 +231,7 @@ class TicketTest < ActiveSupport::TestCase travel 1.minute - ticket2 = Ticket.create( + ticket2 = Ticket.create!( title: 'latest change 2', group: Group.lookup(name: 'Users'), customer_id: 2, @@ -267,7 +267,7 @@ class TicketTest < ActiveSupport::TestCase ticket.save! end - ticket = Ticket.create( + ticket = Ticket.create!( title: 'pending close test', group: Group.lookup(name: 'Users'), customer_id: 2, @@ -289,7 +289,7 @@ class TicketTest < ActiveSupport::TestCase test 'ticket subject' do - ticket = Ticket.create( + ticket = Ticket.create!( title: 'subject test 1', group: Group.lookup(name: 'Users'), customer_id: 2, @@ -307,7 +307,7 @@ class TicketTest < ActiveSupport::TestCase Setting.set('ticket_hook_position', 'left') - ticket = Ticket.create( + ticket = Ticket.create!( title: 'subject test 1', group: Group.lookup(name: 'Users'), customer_id: 2, @@ -325,7 +325,7 @@ class TicketTest < ActiveSupport::TestCase Setting.set('ticket_hook_position', 'none') - ticket = Ticket.create( + ticket = Ticket.create!( title: 'subject test 1', group: Group.lookup(name: 'Users'), customer_id: 2, @@ -348,7 +348,7 @@ class TicketTest < ActiveSupport::TestCase origin_backend = Setting.get('ticket_number') Setting.set('ticket_number', 'Ticket::Number::Increment') - ticket1 = Ticket.create( + ticket1 = Ticket.create!( title: 'subject test 1234-1', group: Group.lookup(name: 'Users'), customer_id: 2, @@ -362,7 +362,7 @@ class TicketTest < ActiveSupport::TestCase assert_equal(ticket1.id, Ticket::Number.check("Re: Help [Ticket##{ticket1.number}]").id) Setting.set('ticket_number', 'Ticket::Number::Date') - ticket1 = Ticket.create( + ticket1 = Ticket.create!( title: 'subject test 1234-2', group: Group.lookup(name: 'Users'), customer_id: 2, @@ -380,7 +380,7 @@ class TicketTest < ActiveSupport::TestCase test 'article attachment helper 1' do - ticket1 = Ticket.create( + ticket1 = Ticket.create!( title: 'some article helper test1', group: Group.lookup(name: 'Users'), customer_id: 2, @@ -392,7 +392,7 @@ class TicketTest < ActiveSupport::TestCase assert(ticket1, 'ticket created') # create inbound article #1 - article1 = Ticket::Article.create( + article1 = Ticket::Article.create!( ticket_id: ticket1.id, from: 'some_sender@example.com', to: 'some_recipient@example.com', @@ -466,7 +466,7 @@ class TicketTest < ActiveSupport::TestCase test 'article attachment helper 2' do - ticket1 = Ticket.create( + ticket1 = Ticket.create!( title: 'some article helper test2', group: Group.lookup(name: 'Users'), customer_id: 2, @@ -478,7 +478,7 @@ class TicketTest < ActiveSupport::TestCase assert(ticket1, 'ticket created') # create inbound article #1 - article1 = Ticket::Article.create( + article1 = Ticket::Article.create!( ticket_id: ticket1.id, from: 'some_sender@example.com', to: 'some_recipient@example.com', From 753af9d5898d0d4a042bad76a5d380bbfbd3ebcb Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Fri, 17 Nov 2017 11:39:57 +0100 Subject: [PATCH 009/196] Small improvements for jobs model and execution of jobs. --- app/models/job.rb | 105 ++++++++++----- test/unit/job_test.rb | 303 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 364 insertions(+), 44 deletions(-) diff --git a/app/models/job.rb b/app/models/job.rb index 03f06eecf..9d5b422a1 100644 --- a/app/models/job.rb +++ b/app/models/job.rb @@ -15,46 +15,83 @@ class Job < ApplicationModel before_create :updated_matching, :update_next_run_at before_update :updated_matching, :update_next_run_at +=begin + +verify each job if needed to run (e. g. if true and times are matching) and execute it + +Job.run + +=end + def self.run + start_at = Time.zone.now jobs = Job.where(active: true, running: false) jobs.each do |job| - logger.debug "Execute job #{job.inspect}" - - next if !job.executable? - - matching = job.matching_count - if job.matching != matching - job.matching = matching - job.save - end - - next if !job.in_timeplan? - - # find tickets to change - ticket_count, tickets = Ticket.selectors(job.condition, 2_000) - - logger.debug "Job #{job.name} with #{ticket_count} tickets" - - job.processed = ticket_count || 0 - job.running = true - job.save - - if tickets - tickets.each do |ticket| - Transaction.execute(disable_notification: job.disable_notification, reset_user_id: true) do - ticket.perform_changes(job.perform, 'job') - end - end - end - - job.running = false - job.last_run_at = Time.zone.now - job.save + job.run(false, start_at) end true end - def executable? +=begin + +execute a single job if needed (e. g. if true and times are matching) + +job = Job.find(123) + +job.run + +force to run job (ignore times are matching) + +job.run(true) + +=end + + def run(force = false, start_at = Time.zone.now) + logger.debug "Execute job #{inspect}" + + if !executable?(start_at) && force == false + if next_run_at && next_run_at <= Time.zone.now + save! + end + return + end + + matching = matching_count + if self.matching != matching + self.matching = matching + save! + end + + if !in_timeplan?(start_at) && force == false + if next_run_at && next_run_at <= Time.zone.now + save! + end + return + end + + # find tickets to change + ticket_count, tickets = Ticket.selectors(condition, 2_000) + + logger.debug "Job #{name} with #{ticket_count} tickets" + + self.processed = ticket_count || 0 + self.running = true + save! + + if tickets + tickets.each do |ticket| + Transaction.execute(disable_notification: disable_notification, reset_user_id: true) do + ticket.perform_changes(perform, 'job') + end + end + end + + self.running = false + self.last_run_at = Time.zone.now + save! + end + + def executable?(start_at = Time.zone.now) return false if !active # only execute jobs, older then 1 min, to give admin posibility to change @@ -62,7 +99,7 @@ class Job < ApplicationModel # check if jobs need to be executed # ignore if job was running within last 10 min. - return false if last_run_at && last_run_at > Time.zone.now - 10.minutes + return false if last_run_at && last_run_at > start_at - 10.minutes true end diff --git a/test/unit/job_test.rb b/test/unit/job_test.rb index 7e41e925d..d92edfe59 100644 --- a/test/unit/job_test.rb +++ b/test/unit/job_test.rb @@ -11,7 +11,7 @@ class JobTest < ActiveSupport::TestCase updated_by_id: 1, created_by_id: 1, ) - ticket1 = Ticket.create( + ticket1 = Ticket.create!( title: 'job test 1', group: group1, customer_id: 2, @@ -22,7 +22,7 @@ class JobTest < ActiveSupport::TestCase created_by_id: 1, updated_by_id: 1, ) - ticket2 = Ticket.create( + ticket2 = Ticket.create!( title: 'job test 2', group: group1, customer_id: 2, @@ -33,7 +33,7 @@ class JobTest < ActiveSupport::TestCase updated_at: Time.zone.now - 1.day, updated_by_id: 1, ) - ticket3 = Ticket.create( + ticket3 = Ticket.create!( title: 'job test 3', group: group2, customer_id: 2, @@ -44,7 +44,7 @@ class JobTest < ActiveSupport::TestCase updated_at: Time.zone.now - 1.day, updated_by_id: 1, ) - ticket4 = Ticket.create( + ticket4 = Ticket.create!( title: 'job test 4', group: group2, customer_id: 2, @@ -55,7 +55,7 @@ class JobTest < ActiveSupport::TestCase updated_at: Time.zone.now - 3.days, updated_by_id: 1, ) - ticket5 = Ticket.create( + ticket5 = Ticket.create!( title: 'job test 5', group: group2, customer_id: 2, @@ -249,7 +249,7 @@ class JobTest < ActiveSupport::TestCase end - test 'case 2' do + test 'with invalid state_id' do # create ticket group1 = Group.lookup(name: 'Users') @@ -258,7 +258,7 @@ class JobTest < ActiveSupport::TestCase updated_by_id: 1, created_by_id: 1, ) - ticket1 = Ticket.create( + ticket1 = Ticket.create!( title: 'job test 1', group: group1, customer_id: 2, @@ -269,7 +269,7 @@ class JobTest < ActiveSupport::TestCase created_by_id: 1, updated_by_id: 1, ) - ticket2 = Ticket.create( + ticket2 = Ticket.create!( title: 'job test 2', group: group1, customer_id: 2, @@ -575,9 +575,8 @@ class JobTest < ActiveSupport::TestCase end - test 'case 5' do + test 'check next_run_at' do - # create jobs job1 = Job.create_or_update( name: 'Test Job1', timeplan: { @@ -644,6 +643,290 @@ class JobTest < ActiveSupport::TestCase time_now = Time.zone.parse('2016-03-17 23:51:23 UTC') next_run_at = job1.next_run_at_calculate(time_now) assert_equal('2016-03-21 00:00:00 UTC', next_run_at.to_s) + end + + test 'update next run at' do + + travel_to Time.zone.local(2017, 11, 10, 22, 0o4, 44) + + job1 = Job.create_or_update( + name: 'Test Job1', + timeplan: { + days: { + Mon: false, + Tue: false, + Wed: false, + Thu: false, + Fri: false, + Sat: true, + Sun: false, + }, + hours: { + '0' => false, + '1' => false, + '2' => false, + '3' => false, + '4' => false, + '5' => false, + '6' => false, + '7' => false, + '8' => false, + '9' => false, + '10' => false, + '11' => false, + '12' => false, + '13' => false, + '14' => false, + '15' => false, + '16' => false, + '17' => false, + '18' => false, + '19' => false, + '20' => false, + '21' => false, + '22' => false, + '23' => true, + }, + minutes: { + '0' => true, + '10' => false, + '20' => false, + '30' => false, + '40' => false, + '50' => false, + }, + }, + condition: { + 'ticket.state_id' => { 'operator' => 'is', 'value' => [Ticket::State.lookup(name: 'new').id.to_s, Ticket::State.lookup(name: 'open').id.to_s] }, + }, + perform: { + 'ticket.action' => { 'value' => 'delete' }, + }, + disable_notification: true, + last_run_at: nil, + active: true, + created_by_id: 1, + created_at: Time.zone.now, + updated_by_id: 1, + updated_at: Time.zone.now, + ) + + assert_equal('2017-11-11 23:00:00 UTC', job1.next_run_at.to_s) + assert_not(job1.last_run_at) + + travel_to Time.zone.local(2017, 11, 16, 22, 0o4, 44) + + Job.run + + job1.reload + + assert_equal('2017-11-18 23:00:00 UTC', job1.next_run_at.to_s) + assert_not(job1.last_run_at) + + travel_back + + end + + test 'execute on certain time' do + + travel_to Time.zone.local(2017, 11, 16, 22, 0o4, 44) + + group1 = Group.lookup(name: 'Users') + ticket1 = Ticket.create!( + title: 'job test 1', + group: group1, + customer_id: 2, + state: Ticket::State.lookup(name: 'new'), + priority: Ticket::Priority.lookup(name: '2 normal'), + created_by_id: 1, + updated_by_id: 1, + ) + ticket2 = Ticket.create!( + title: 'job test 2', + group: group1, + customer_id: 2, + state: Ticket::State.lookup(name: 'new'), + priority: Ticket::Priority.lookup(name: '2 normal'), + created_by_id: 1, + updated_by_id: 1, + ) + + job1 = Job.create_or_update( + name: 'Test Job1', + timeplan: { + days: { + Mon: false, + Tue: false, + Wed: false, + Thu: true, + Fri: false, + Sat: false, + Sun: false, + }, + hours: { + '0' => false, + '1' => false, + '2' => false, + '3' => false, + '4' => false, + '5' => false, + '6' => false, + '7' => false, + '8' => false, + '9' => false, + '10' => false, + '11' => false, + '12' => false, + '13' => false, + '14' => false, + '15' => false, + '16' => false, + '17' => false, + '18' => false, + '19' => false, + '20' => false, + '21' => false, + '22' => false, + '23' => true, + }, + minutes: { + '0' => true, + '10' => false, + '20' => false, + '30' => false, + '40' => false, + '50' => false, + }, + }, + condition: { + 'ticket.state_id' => { 'operator' => 'is', 'value' => [Ticket::State.lookup(name: 'new').id.to_s, Ticket::State.lookup(name: 'open').id.to_s] }, + }, + perform: { + 'ticket.action' => { 'value' => 'delete' }, + }, + disable_notification: true, + last_run_at: nil, + active: true, + created_by_id: 1, + created_at: Time.zone.now, + updated_by_id: 1, + updated_at: Time.zone.now, + ) + Job.run + + assert(Ticket.find_by(id: ticket1.id)) + assert(Ticket.find_by(id: ticket2.id)) + + travel_to Time.zone.local(2017, 11, 16, 23, 0o4, 44) + + Job.run + + assert_not(Ticket.find_by(id: ticket1.id)) + assert_not(Ticket.find_by(id: ticket2.id)) + + travel_back + end + + test 'delete based on tag' do + + # create ticket + group1 = Group.lookup(name: 'Users') + group2 = Group.create_or_update( + name: 'JobTest2', + updated_by_id: 1, + created_by_id: 1, + ) + ticket1 = Ticket.create!( + title: 'job test 1', + group: group1, + customer_id: 2, + state: Ticket::State.lookup(name: 'new'), + priority: Ticket::Priority.lookup(name: '2 normal'), + created_at: Time.zone.now - 3.days, + updated_at: Time.zone.now - 3.days, + created_by_id: 1, + updated_by_id: 1, + ) + ticket1.tag_add('spam', 1) + ticket1.tag_add('test1 ', 1) + ticket2 = Ticket.create!( + title: 'job test 2', + group: group1, + customer_id: 2, + state: Ticket::State.lookup(name: 'new'), + priority: Ticket::Priority.lookup(name: '2 normal'), + created_at: Time.zone.now - 1.day, + created_by_id: 1, + updated_at: Time.zone.now - 1.day, + updated_by_id: 1, + ) + + job1 = Job.create_or_update( + name: 'Test Job1', + timeplan: { + days: { + Mon: true, + Tue: true, + Wed: true, + Thu: true, + Fri: true, + Sat: true, + Sun: true, + }, + hours: { + 0 => true, + 1 => true, + 2 => true, + 3 => true, + 4 => true, + 5 => true, + 6 => true, + 7 => true, + 8 => true, + 9 => true, + 10 => true, + 11 => true, + 12 => true, + 13 => true, + 14 => true, + 15 => true, + 16 => true, + 17 => true, + 18 => true, + 19 => true, + 20 => true, + 21 => true, + 22 => true, + 23 => true, + }, + minutes: { + 0 => true, + 10 => true, + 20 => true, + 30 => true, + 40 => true, + 50 => true, + }, + }, + condition: { + 'ticket.tags' => { 'operator' => 'contains one', 'value' => 'spam' }, + }, + perform: { + 'ticket.action' => { 'value' => 'delete' }, + }, + disable_notification: true, + last_run_at: nil, + updated_at: Time.zone.now - 15.minutes, + active: true, + updated_by_id: 1, + created_by_id: 1, + ) + assert(job1.executable?) + assert(job1.in_timeplan?) + Job.run + + assert_not(Ticket.find_by(id: ticket1.id)) + assert(Ticket.find_by(id: ticket2.id)) end From eb5ad7d9faf658b262ce892ecd74be48f38b6572 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Fri, 17 Nov 2017 11:41:44 +0100 Subject: [PATCH 010/196] Fixed issue #1649 - Tickets are deleted but database is still the same size. --- app/models/store.rb | 13 +++-- app/models/ticket/article.rb | 13 +++++ test/unit/store_test.rb | 43 +++++++------- test/unit/ticket_article_store_empty.rb | 75 +++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 27 deletions(-) create mode 100644 test/unit/ticket_article_store_empty.rb diff --git a/app/models/store.rb b/app/models/store.rb index de3548358..76fea4961 100644 --- a/app/models/store.rb +++ b/app/models/store.rb @@ -47,7 +47,7 @@ returns data.delete('object') # store meta data - store = Store.create(data) + store = Store.create!(data) store end @@ -123,17 +123,18 @@ remove one attachment from storage =end def self.remove_item(store_id) - store = Store.find(store_id) file_id = store.store_file_id - store.destroy # check backend for references files = Store.where(store_file_id: file_id) - return if files.count != 1 - return if files.first.id != store.id + if files.count > 1 || files.first.id != store.id + store.destroy! + return true + end - Store::File.find(file_id).destroy + store.destroy! + Store::File.find(file_id).destroy! end =begin diff --git a/app/models/ticket/article.rb b/app/models/ticket/article.rb index 6a5e2cf5d..96213f2d8 100644 --- a/app/models/ticket/article.rb +++ b/app/models/ticket/article.rb @@ -18,6 +18,7 @@ class Ticket::Article < ApplicationModel store :preferences before_create :check_subject, :check_body, :check_message_id_md5 before_update :check_subject, :check_body, :check_message_id_md5 + after_destroy :store_delete sanitized_html :body @@ -316,6 +317,18 @@ returns } end + # delete attachments and mails of article + def store_delete + Store.remove( + object: 'Ticket::Article', + o_id: id, + ) + Store.remove( + object: 'Ticket::Article::Mail', + o_id: id, + ) + end + class Flag < ApplicationModel end diff --git a/test/unit/store_test.rb b/test/unit/store_test.rb index 1213768f2..134c6ae76 100644 --- a/test/unit/store_test.rb +++ b/test/unit/store_test.rb @@ -29,7 +29,7 @@ class StoreTest < ActiveSupport::TestCase assert(exists) end - test 'store attachment' do + test 'store attachment and move it between backends' do files = [ { data: 'hello world', @@ -54,7 +54,7 @@ class StoreTest < ActiveSupport::TestCase ] files.each do |file| - sha = Digest::SHA256.hexdigest( file[:data] ) + sha = Digest::SHA256.hexdigest(file[:data]) # add attachments store = Store.add( @@ -75,23 +75,23 @@ class StoreTest < ActiveSupport::TestCase assert attachments # sha check - sha_new = Digest::SHA256.hexdigest( attachments[0].content ) - assert_equal( sha, sha_new, "check file #{file[:filename]}") + sha_new = Digest::SHA256.hexdigest(attachments[0].content) + assert_equal(sha, sha_new, "check file #{file[:filename]}") # filename check - assert_equal( file[:filename], attachments[0].filename ) + assert_equal(file[:filename], attachments[0].filename) # provider check - assert_equal( 'DB', attachments[0].provider ) + assert_equal('DB', attachments[0].provider) end success = Store::File.verify assert success, 'verify ok' - Store::File.move( 'DB', 'File' ) + Store::File.move('DB', 'File') files.each do |file| - sha = Digest::SHA256.hexdigest( file[:data] ) + sha = Digest::SHA256.hexdigest(file[:data]) # get list of attachments attachments = Store.list( @@ -101,54 +101,55 @@ class StoreTest < ActiveSupport::TestCase assert attachments # sha check - sha_new = Digest::SHA256.hexdigest( attachments[0].content ) - assert_equal( sha, sha_new, "check file #{file[:filename]}") + sha_new = Digest::SHA256.hexdigest(attachments[0].content) + assert_equal(sha, sha_new, "check file #{file[:filename]}") # filename check - assert_equal( file[:filename], attachments[0].filename ) + assert_equal(file[:filename], attachments[0].filename) # provider check - assert_equal( 'File', attachments[0].provider ) + assert_equal('File', attachments[0].provider) end success = Store::File.verify assert success, 'verify ok' - Store::File.move( 'File', 'DB' ) + Store::File.move('File', 'DB') files.each do |file| - sha = Digest::SHA256.hexdigest( file[:data] ) + sha = Digest::SHA256.hexdigest(file[:data]) # get list of attachments attachments = Store.list( object: 'Test', o_id: file[:o_id], ) - assert attachments + assert(attachments) + assert_equal(attachments.count, 1) # sha check - sha_new = Digest::SHA256.hexdigest( attachments[0].content ) - assert_equal( sha, sha_new, "check file #{file[:filename]}") + sha_new = Digest::SHA256.hexdigest(attachments[0].content) + assert_equal(sha, sha_new, "check file #{file[:filename]}") # filename check - assert_equal( file[:filename], attachments[0].filename ) + assert_equal(file[:filename], attachments[0].filename) # provider check - assert_equal( 'DB', attachments[0].provider ) + assert_equal('DB', attachments[0].provider) # delete attachments success = Store.remove( object: 'Test', o_id: file[:o_id], ) - assert success + assert(success) # check attachments again attachments = Store.list( object: 'Test', o_id: file[:o_id], ) - assert !attachments[0] + assert_not(attachments[0]) end end end diff --git a/test/unit/ticket_article_store_empty.rb b/test/unit/ticket_article_store_empty.rb new file mode 100644 index 000000000..ad3cd5049 --- /dev/null +++ b/test/unit/ticket_article_store_empty.rb @@ -0,0 +1,75 @@ +# encoding: utf-8 +require 'test_helper' + +class TicketArticleStoreEmpty < ActiveSupport::TestCase + + test 'check if attachments are deleted after ticket is deleted' do + + current_count = Store.count + current_file_count = Store::File.count + current_backend_count = Store::Provider::DB.count + + email_raw_string = IO.binread('test/fixtures/mail1.box') + ticket, article, user, mail = Channel::EmailParser.new.process({}, email_raw_string) + + next_count = Store.count + next_file_count = Store::File.count + next_backend_count = Store::Provider::DB.count + + assert_equal(current_count, next_count - 2) + assert_equal(current_file_count, next_file_count - 2) + assert_equal(current_backend_count, next_backend_count - 2) + + ticket.destroy! + + after_count = Store.count + after_file_count = Store::File.count + after_backend_count = Store::Provider::DB.count + + assert_equal(current_count, after_count) + assert_equal(current_file_count, after_file_count) + assert_equal(current_backend_count, after_backend_count) + + end + + test 'check if attachments are deleted after ticket same ticket 2 times is deleted' do + + current_count = Store.count + current_file_count = Store::File.count + current_backend_count = Store::Provider::DB.count + + email_raw_string = IO.binread('test/fixtures/mail1.box') + ticket1, article1, user1, mail1 = Channel::EmailParser.new.process({}, email_raw_string) + ticket2, article2, user2, mail2 = Channel::EmailParser.new.process({}, email_raw_string) + + next_count = Store.count + next_file_count = Store::File.count + next_backend_count = Store::Provider::DB.count + + assert_equal(current_count, next_count - 4) + assert_equal(current_file_count, next_file_count - 2) + assert_equal(current_backend_count, next_backend_count - 2) + + ticket1.destroy! + + next_count = Store.count + next_file_count = Store::File.count + next_backend_count = Store::Provider::DB.count + + assert_equal(current_count, next_count - 2) + assert_equal(current_file_count, next_file_count - 2) + assert_equal(current_backend_count, next_backend_count - 2) + + ticket2.destroy! + + after_count = Store.count + after_file_count = Store::File.count + after_backend_count = Store::Provider::DB.count + + assert_equal(current_count, after_count) + assert_equal(current_file_count, after_file_count) + assert_equal(current_backend_count, after_backend_count) + + end + +end From 17ce2dcbde6b256c91f668259ecb98a879ee16a8 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Fri, 17 Nov 2017 15:39:56 +0100 Subject: [PATCH 011/196] Improved agent limit detection (do not complain if role with ticket.agent permission is assigned and agent already owns a role with ticket.agent permission). --- app/models/user.rb | 16 +++++- test/unit/role_validate_agent_limit_test.rb | 55 ++++++++++++++++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 6f09f6f10..95d280ec0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1036,8 +1036,22 @@ raise 'Minimum one user need to have admin permissions' return true if !role.with_permission?('ticket.agent') ticket_agent_role_ids = Role.joins(:permissions).where(permissions: { name: 'ticket.agent', active: true }, roles: { active: true }).pluck(:id) count = User.joins(:roles).where(roles: { id: ticket_agent_role_ids }, users: { active: true }).count + + # if new added role is a ticket.agent role if ticket_agent_role_ids.include?(role.id) - count += 1 + + # if user already has a ticket.agent role + hint = false + role_ids.each do |locale_role_id| + next if !ticket_agent_role_ids.include?(locale_role_id) + hint = true + break + end + + # user has not already a ticket.agent role + if hint == false + count += 1 + end end raise Exceptions::UnprocessableEntity, 'Agent limit exceeded, please check your account settings.' if count > Setting.get('system_agent_limit') true diff --git a/test/unit/role_validate_agent_limit_test.rb b/test/unit/role_validate_agent_limit_test.rb index 5395d0c5e..b10d8b523 100644 --- a/test/unit/role_validate_agent_limit_test.rb +++ b/test/unit/role_validate_agent_limit_test.rb @@ -2,7 +2,7 @@ require 'test_helper' class RoleValidateAgentLimit < ActiveSupport::TestCase - test 'role_validate_agent_limit' do + test 'role validate agent limit' do agent_max = User.with_permissions('ticket.agent').count UserInfo.current_user_id = 1 @@ -70,4 +70,57 @@ class RoleValidateAgentLimit < ActiveSupport::TestCase role_agent_limit_fail.destroy! Setting.set('system_agent_limit', nil) end + + test 'role validate agent limit - 1 user 2 ticket.agent roles' do + + agent_max = User.with_permissions('ticket.agent').count + UserInfo.current_user_id = 1 + Setting.set('system_agent_limit', agent_max + 1) + + permission_ticket_agent = Permission.find_by(name: 'ticket.agent') + + role_agent_limit1 = Role.create!( + name: 'agent-limit-test1', + note: 'agent-limit-test1 Role.', + permissions: [permission_ticket_agent], + active: true, + ) + role_agent_limit2 = Role.create!( + name: 'agent-limit-test2', + note: 'agent-limit-test2 Role.', + permissions: [permission_ticket_agent], + active: true, + ) + + user1 = User.create!( + firstname: 'Firstname', + lastname: 'Lastname', + email: 'some-agentlimit-role@example.com', + login: 'some-agentlimit-role@example.com', + roles: [role_agent_limit1, role_agent_limit2], + active: true, + ) + + user1.roles = Role.where(name: %w(Admin Agent)) + + user1.role_ids = [Role.find_by(name: 'Agent').id] + + user1.role_ids = [Role.find_by(name: 'Agent').id, role_agent_limit1.id] + + assert_raises(Exceptions::UnprocessableEntity) do + user2 = User.create!( + firstname: 'Firstname2', + lastname: 'Lastname2', + email: 'some-agentlimit-role-2@example.com', + login: 'some-agentlimit-role-2@example.com', + roles: [role_agent_limit1], + active: true, + ) + end + + role_agent_limit1.destroy! + role_agent_limit2.destroy! + Setting.set('system_agent_limit', nil) + end + end From 3417618798e2b98436288012e0164e6259a7ac62 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Tue, 21 Nov 2017 08:24:03 +0100 Subject: [PATCH 012/196] Improved removing of html comments. --- app/models/concerns/checks_html_sanitized.rb | 2 +- lib/html_sanitizer.rb | 9 ++++-- test/unit/html_sanitizer_test.rb | 31 ++++++++++++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/app/models/concerns/checks_html_sanitized.rb b/app/models/concerns/checks_html_sanitized.rb index 0247fc427..147125a8e 100644 --- a/app/models/concerns/checks_html_sanitized.rb +++ b/app/models/concerns/checks_html_sanitized.rb @@ -9,7 +9,7 @@ module ChecksHtmlSanitized def sanitized_html_attributes html_attributes = self.class.instance_variable_get(:@sanitized_html) || [] - return true if html_attributes.empty? + return true if html_attributes.blank? html_attributes.each do |attribute| value = send(attribute) diff --git a/lib/html_sanitizer.rb b/lib/html_sanitizer.rb index 5abb5b979..67e0b046b 100644 --- a/lib/html_sanitizer.rb +++ b/lib/html_sanitizer.rb @@ -19,6 +19,9 @@ satinize html string based on whiltelist classes_whitelist = ['js-signatureMarker'] attributes_2_css = %w(width height) + # remove html comments + string.gsub!(//m, '') + scrubber_link = Loofah::Scrubber.new do |node| # check if href is different to text @@ -64,7 +67,7 @@ satinize html string based on whiltelist urls.push match[1].to_s.strip end end - next if urls.empty? + next if urls.blank? add_link(node.content, urls, node) end end @@ -136,7 +139,7 @@ satinize html string based on whiltelist # move style attributes to css attributes attributes_2_css.each do |key| next if !node[key] - if node['style'].empty? + if node['style'].blank? node['style'] = '' else node['style'] += ';' @@ -343,7 +346,7 @@ cleanup html string: end def self.add_link(content, urls, node) - if urls.empty? + if urls.blank? text = Nokogiri::XML::Text.new(content, node.document) node.add_next_sibling(text) return diff --git a/test/unit/html_sanitizer_test.rb b/test/unit/html_sanitizer_test.rb index e29b71c8e..77d03f4c3 100644 --- a/test/unit/html_sanitizer_test.rb +++ b/test/unit/html_sanitizer_test.rb @@ -75,6 +75,37 @@ tt p://6 6.000146.0x7.147/">XSS', true), 'XSS ('), 'alert(1)') assert_equal(HtmlSanitizer.strict(''), 'http://example.com') assert_equal(HtmlSanitizer.strict('', true), 'http://example.com') + assert_equal(HtmlSanitizer.strict('
                  + +test 123 +
                  '), '
                  + +test 123 +
                  +
                  ') + end end From 9f204806db6483de644c5996463d3e81e20b4e4e Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Tue, 21 Nov 2017 08:37:09 +0100 Subject: [PATCH 013/196] Fixed issue #1661 - Users mail_delivery_failed is not removed after changing the email address. --- app/models/user.rb | 8 ++++- test/unit/user_mail_delivery_failed_test.rb | 40 +++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 test/unit/user_mail_delivery_failed_test.rb diff --git a/app/models/user.rb b/app/models/user.rb index 95d280ec0..21c32d86a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -38,7 +38,7 @@ class User < ApplicationModel load 'user/search_index.rb' include User::SearchIndex - before_validation :check_name, :check_email, :check_login, :ensure_uniq_email, :ensure_password, :ensure_roles, :ensure_identifier + 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 after_create :avatar_for_email_check @@ -945,6 +945,12 @@ returns true end + def check_mail_delivery_failed + return true if !changes || !changes['email'] + preferences.delete(:mail_delivery_failed) + true + end + def ensure_roles return true if role_ids.present? self.role_ids = Role.signup_role_ids diff --git a/test/unit/user_mail_delivery_failed_test.rb b/test/unit/user_mail_delivery_failed_test.rb new file mode 100644 index 000000000..2ff9e0b9f --- /dev/null +++ b/test/unit/user_mail_delivery_failed_test.rb @@ -0,0 +1,40 @@ +require 'test_helper' + +class UserMailDeliveryFailedTest < ActiveSupport::TestCase + setup do + + UserInfo.current_user_id = 1 + + roles = Role.where(name: 'Customer') + @customer1 = User.create_or_update( + login: 'user-mail-delivery-failed-customer1@example.com', + firstname: 'UserOutOfOffice', + lastname: 'Customer1', + email: 'user-mail-delivery-failed-customer1@example.com', + password: 'agentpw', + active: true, + roles: roles, + ) + + end + + test 'check reset of mail_delivery_failed' do + + @customer1.preferences[:mail_delivery_failed] = true + @customer1.preferences[:mail_delivery_failed_data] = Time.zone.now + @customer1.save! + @customer1.reload + + assert_equal(@customer1.preferences[:mail_delivery_failed], true) + assert(@customer1.preferences[:mail_delivery_failed_data]) + + @customer1.email = 'new-user-mail-delivery-failed-customer1@example.com' + @customer1.save! + @customer1.reload + + assert_not(@customer1.preferences[:mail_delivery_failed], true) + assert(@customer1.preferences[:mail_delivery_failed_data]) + + end + +end From 460182e663fcc7320b171fac46d7ca1f79b77553 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Tue, 21 Nov 2017 15:25:04 +0100 Subject: [PATCH 014/196] Small code cleanup. --- .../controllers/_application_controller_generic.coffee | 4 ++-- .../ticket_zoom/article_action/twitter_reply.coffee | 2 +- .../app/controllers/widget/ticket_stats.coffee | 1 + app/controllers/monitoring_controller.rb | 6 +++--- app/controllers/user_access_token_controller.rb | 6 +++--- app/models/channel/email_parser.rb | 4 ++-- app/models/package.rb | 2 +- app/models/ticket.rb | 4 +--- lib/enrichment/clearbit/organization.rb | 9 ++++----- lib/enrichment/clearbit/user.rb | 1 - 10 files changed, 18 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/app/controllers/_application_controller_generic.coffee b/app/assets/javascripts/app/controllers/_application_controller_generic.coffee index 71789f94c..20991117e 100644 --- a/app/assets/javascripts/app/controllers/_application_controller_generic.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller_generic.coffee @@ -685,8 +685,8 @@ class App.Sidebar extends App.Controller for item in @items area = localEl.filter('.sidebar[data-tab="' + item.name + '"]') if item.callback - item.callback( area.find('.sidebar-content') ) - if item.actions + item.callback(area.find('.sidebar-content')) + if !_.isEmpty(item.actions) new App.ActionRow( el: area.find('.js-actions') items: item.actions diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_action/twitter_reply.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/twitter_reply.coffee index 8b03ba249..6ce871868 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_action/twitter_reply.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/twitter_reply.coffee @@ -71,7 +71,7 @@ class TwitterReply exclude = true # exclude own screen_name - if recipientScreenName is "@#{@ticket.preferences.channel_screen_name}".toLowerCase() + if recipientScreenName is "@#{ticket.preferences.channel_screen_name}".toLowerCase() exclude = true if exclude is false diff --git a/app/assets/javascripts/app/controllers/widget/ticket_stats.coffee b/app/assets/javascripts/app/controllers/widget/ticket_stats.coffee index 7445e8294..67d8b4b5b 100644 --- a/app/assets/javascripts/app/controllers/widget/ticket_stats.coffee +++ b/app/assets/javascripts/app/controllers/widget/ticket_stats.coffee @@ -70,6 +70,7 @@ class App.TicketStats extends App.Controller render: (data) => if !data data = @data + return if !data user_total = 0 if data.user.open_ids && data.user.closed_ids diff --git a/app/controllers/monitoring_controller.rb b/app/controllers/monitoring_controller.rb index 0bdcfd6df..22f8f319f 100644 --- a/app/controllers/monitoring_controller.rb +++ b/app/controllers/monitoring_controller.rb @@ -40,7 +40,7 @@ curl http://localhost/api/v1/monitoring/health_check?token=XXX if channel.status_in == 'error' message = "Channel: #{channel.area} in " %w(host user uid).each do |key| - next if !channel.options[key] || channel.options[key].empty? + next if channel.options[key].blank? message += "key:#{channel.options[key]};" end issues.push "#{message} #{channel.last_log_in}" @@ -53,7 +53,7 @@ curl http://localhost/api/v1/monitoring/health_check?token=XXX next if channel.status_out != 'error' message = "Channel: #{channel.area} out " %w(host user uid).each do |key| - next if !channel.options[key] || channel.options[key].empty? + next if channel.options[key].blank? message += "key:#{channel.options[key]};" end issues.push "#{message} #{channel.last_log_out}" @@ -89,7 +89,7 @@ curl http://localhost/api/v1/monitoring/health_check?token=XXX token = Setting.get('monitoring_token') - if issues.empty? + if issues.blank? result = { healthy: true, message: 'success', diff --git a/app/controllers/user_access_token_controller.rb b/app/controllers/user_access_token_controller.rb index 16def0e98..8b1f5aeec 100644 --- a/app/controllers/user_access_token_controller.rb +++ b/app/controllers/user_access_token_controller.rb @@ -45,10 +45,10 @@ class UserAccessTokenController < ApplicationController if Setting.get('api_token_access') == false raise Exceptions::UnprocessableEntity, 'API token access disabled!' end - if params[:label].empty? + if params[:label].blank? raise Exceptions::UnprocessableEntity, 'Need label!' end - token = Token.create( + token = Token.create!( action: 'api', label: params[:label], persistent: true, @@ -66,7 +66,7 @@ class UserAccessTokenController < ApplicationController def destroy token = Token.find_by(action: 'api', user_id: current_user.id, id: params[:id]) raise Exceptions::UnprocessableEntity, 'Unable to find api token!' if !token - token.destroy + token.destroy! render json: {}, status: :ok end diff --git a/app/models/channel/email_parser.rb b/app/models/channel/email_parser.rb index 8b97388d6..9496572dd 100644 --- a/app/models/channel/email_parser.rb +++ b/app/models/channel/email_parser.rb @@ -615,10 +615,10 @@ returns if channel[:group_id] group = Group.lookup(id: channel[:group_id]) end - if !group || group && !group.active + if group.blank? || group.active == false group = Group.where(active: true).order('id ASC').first end - if !group + if group.blank? group = Group.first end title = mail[:subject] diff --git a/app/models/package.rb b/app/models/package.rb index f76c30326..221896a0e 100644 --- a/app/models/package.rb +++ b/app/models/package.rb @@ -44,7 +44,7 @@ returns: logger.error "File #{file['location']} is different" issues[file['location']] = 'changed' end - return nil if issues.empty? + return nil if issues.blank? issues end diff --git a/app/models/ticket.rb b/app/models/ticket.rb index d630dbce1..e930201ae 100644 --- a/app/models/ticket.rb +++ b/app/models/ticket.rb @@ -1031,9 +1031,7 @@ perform changes on ticket if key == 'ticket.action' next if value['value'].blank? next if value['value'] != 'delete' - - destroy - + destroy! next end diff --git a/lib/enrichment/clearbit/organization.rb b/lib/enrichment/clearbit/organization.rb index 7b095afa5..2477acd80 100644 --- a/lib/enrichment/clearbit/organization.rb +++ b/lib/enrichment/clearbit/organization.rb @@ -13,7 +13,6 @@ module Enrichment def synced? return false if !@config - # TODO UserInfo.current_user_id = 1 return false if !mapping? @@ -74,7 +73,7 @@ module Enrichment return false if @user.organization_id # can't create organization without name - return false if @current_changes[:name].empty? + return false if @current_changes[:name].blank? organization = create_current @@ -95,7 +94,7 @@ module Enrichment object: organization, current_changes: @current_changes, ) - organization.save + organization.save! ExternalSync.create( source: @source, @@ -127,10 +126,10 @@ module Enrichment ) organization.updated_by_id = 1 - organization.save + organization.save! @external_organization.last_payload = @payload - @external_organization.save + @external_organization.save! organization end diff --git a/lib/enrichment/clearbit/user.rb b/lib/enrichment/clearbit/user.rb index 57d39350b..85f509abe 100644 --- a/lib/enrichment/clearbit/user.rb +++ b/lib/enrichment/clearbit/user.rb @@ -12,7 +12,6 @@ module Enrichment return false if !@config return false if @local_user.email.blank? - # TODO UserInfo.current_user_id = 1 return false if !mapping? From 112edd362c9527151e4b8af94a77e7f704b419c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bauer?= Date: Tue, 21 Nov 2017 21:30:53 +0100 Subject: [PATCH 015/196] added dalli gem --- Gemfile | 1 + Gemfile.lock | 157 +++++++++++++++++++++++++++------------------------ 2 files changed, 83 insertions(+), 75 deletions(-) diff --git a/Gemfile b/Gemfile index 812e6cdf9..53e3fff1a 100644 --- a/Gemfile +++ b/Gemfile @@ -91,6 +91,7 @@ gem 'eventmachine' gem 'em-websocket' gem 'diffy' +gem 'dalli' # Gems used only for develop/test and not required # in production environments by default. diff --git a/Gemfile.lock b/Gemfile.lock index 3e2981f49..4fb822511 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -65,22 +65,22 @@ GEM addressable (2.5.2) public_suffix (>= 2.0.2, < 4.0) arel (8.0.0) - argon2 (1.1.3) + argon2 (1.1.4) ffi (~> 1.9) ffi-compiler (~> 0.1) ast (2.3.0) - autoprefixer-rails (7.1.3) + autoprefixer-rails (7.1.6) execjs biz (1.7.0) clavius (~> 1.0) tzinfo - browser (2.5.1) + browser (2.5.2) buftok (0.2.0) builder (3.2.3) - childprocess (0.7.1) + childprocess (0.8.0) ffi (~> 1.0, >= 1.0.11) clavius (1.0.3) - clearbit (0.2.7) + clearbit (0.2.8) nestful (~> 1.1.0) coderay (1.1.2) coffee-rails (4.2.2) @@ -90,11 +90,11 @@ GEM coffee-script-source execjs coffee-script-source (1.12.2) - coffeelint (1.16.0) + coffeelint (1.16.1) coffee-script execjs json - composite_primary_keys (10.0.0) + composite_primary_keys (10.0.1) activerecord (~> 5.1.0) concurrent-ruby (1.0.5) coveralls (0.8.21) @@ -105,7 +105,9 @@ GEM tins (~> 1.6) crack (0.4.3) safe_yaml (~> 1.0.0) - daemons (1.2.4) + crass (1.0.3) + daemons (1.2.5) + dalli (2.7.6) delayed_job (4.1.3) activesupport (>= 3.0, < 5.2) delayed_job_active_record (4.1.2) @@ -127,15 +129,15 @@ GEM eventmachine (>= 0.12.9) http_parser.rb (~> 0.6.0) equalizer (0.0.11) - erubi (1.6.1) + erubi (1.7.0) eventmachine (1.2.5) execjs (2.7.0) - factory_girl (4.8.0) + factory_girl (4.9.0) activesupport (>= 3.0.0) - factory_girl_rails (4.8.0) - factory_girl (~> 4.8.0) + factory_girl_rails (4.9.0) + factory_girl (~> 4.9.0) railties (>= 3.0.0) - faraday (0.11.0) + faraday (0.12.2) multipart-post (>= 1.2, < 3) faraday-http-cache (2.0.0) faraday (~> 0.8) @@ -154,7 +156,7 @@ GEM rainbow (>= 2.1) rake (>= 10.0) retriable (~> 2.1) - globalid (0.4.0) + globalid (0.4.1) activesupport (>= 4.2.0) guard (2.14.1) formatador (>= 0.2.4) @@ -174,20 +176,21 @@ GEM guard-symlink (0.1.1) guard guard-compat (~> 1.1) - hashdiff (0.3.6) + hashdiff (0.3.7) hashie (3.5.6) htmlentities (4.3.4) - http (2.2.2) + http (3.0.0) addressable (~> 2.3) http-cookie (~> 1.0) - http-form_data (~> 1.0.1) + http-form_data (>= 2.0.0.pre.pre2, < 3) http_parser.rb (~> 0.6.0) http-cookie (1.0.3) domain_name (~> 0.5) - http-form_data (1.0.3) + http-form_data (2.0.0) http_parser.rb (0.6.0) httpclient (2.8.3) - i18n (0.8.6) + i18n (0.9.1) + concurrent-ruby (~> 1.0) icalendar (2.4.1) icalendar-recurrence (1.1.2) icalendar (~> 2.0) @@ -210,25 +213,27 @@ GEM logging (2.2.2) little-plugger (~> 1.1) multi_json (~> 1.10) - loofah (2.0.3) + loofah (2.1.1) + crass (~> 1.0.2) nokogiri (>= 1.5.9) lumberjack (1.0.12) - mail (2.6.6) - mime-types (>= 1.16, < 4) + mail (2.7.0) + mini_mime (>= 0.1.1) memoizable (0.4.2) thread_safe (~> 0.3, >= 0.3.1) - method_source (0.8.2) + method_source (0.9.0) mime-types (2.99.3) + mini_mime (1.0.0) mini_portile2 (2.3.0) minitest (5.10.3) multi_json (1.12.2) multi_xml (0.6.0) multipart-post (2.0.0) - mysql2 (0.4.9) + mysql2 (0.4.10) naught (1.1.0) nenv (0.3.0) - nestful (1.1.1) - net-ldap (0.16.0) + nestful (1.1.3) + net-ldap (0.16.1) netrc (0.11.0) nio4r (2.1.0) nokogiri (1.8.1) @@ -246,7 +251,7 @@ GEM rack (>= 1.2, < 3) octokit (4.7.0) sawyer (~> 0.8.0, >= 0.5.3) - omniauth (1.6.1) + omniauth (1.7.1) hashie (>= 3.4.6, < 3.6.0) rack (>= 1.6.2, < 3) omniauth-facebook (4.0.0) @@ -280,24 +285,24 @@ GEM omniauth-weibo-oauth2 (0.4.5) omniauth (~> 1.5) omniauth-oauth2 (>= 1.4.0) - parser (2.4.0.0) - ast (~> 2.2) + parallel (1.12.0) + parser (2.4.0.2) + ast (~> 2.3) pg (0.21.0) pluginator (1.5.0) - power_assert (1.1.0) + power_assert (1.1.1) powerpack (0.1.1) - pre-commit (0.35.0) + pre-commit (0.37.0) pluginator (~> 1.5) - pry (0.10.4) + pry (0.11.3) coderay (~> 1.1.0) - method_source (~> 0.8.1) - slop (~> 3.4) - public_suffix (3.0.0) - puma (3.10.0) + method_source (~> 0.9.0) + public_suffix (3.0.1) + puma (3.11.0) rack (2.0.3) rack-livereload (0.3.16) rack - rack-test (0.7.0) + rack-test (0.8.2) rack (>= 1.0, < 3) rails (5.1.4) actioncable (= 5.1.4) @@ -327,7 +332,7 @@ GEM rainbow (2.2.2) rake raindrops (0.19.0) - rake (12.1.0) + rake (12.3.0) rb-fsevent (0.10.2) rb-inotify (0.9.10) ffi (>= 0.5.0, < 2) @@ -337,39 +342,40 @@ GEM mime-types (>= 1.16, < 3.0) netrc (~> 0.7) retriable (2.1.0) - rspec-core (3.6.0) - rspec-support (~> 3.6.0) - rspec-expectations (3.6.0) + rspec-core (3.7.0) + rspec-support (~> 3.7.0) + rspec-expectations (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.6.0) - rspec-mocks (3.6.0) + rspec-support (~> 3.7.0) + rspec-mocks (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.6.0) - rspec-rails (3.6.1) + rspec-support (~> 3.7.0) + rspec-rails (3.7.2) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) - rspec-core (~> 3.6.0) - rspec-expectations (~> 3.6.0) - rspec-mocks (~> 3.6.0) - rspec-support (~> 3.6.0) - rspec-support (3.6.0) - rubocop (0.42.0) - parser (>= 2.3.1.1, < 3.0) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-support (~> 3.7.0) + rspec-support (3.7.0) + rubocop (0.51.0) + parallel (~> 1.10) + parser (>= 2.3.3.1, < 3.0) powerpack (~> 0.1) - rainbow (>= 1.99.1, < 3.0) + rainbow (>= 2.2.2, < 3.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) - ruby-progressbar (1.8.1) + ruby-progressbar (1.9.0) ruby_dep (1.5.0) rubyzip (1.2.1) safe_yaml (1.0.4) - sass (3.5.1) + sass (3.5.3) sass-listen (~> 4.0.0) sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) - sass-rails (5.0.6) + sass-rails (5.0.7) railties (>= 4.0.0, < 6) sass (~> 3.1) sprockets (>= 2.8, < 4.0) @@ -393,7 +399,6 @@ GEM simplecov-rcov (0.2.3) simplecov (>= 0.4.1) slack-notifier (2.3.1) - slop (3.6.0) spring (2.0.2) activesupport (>= 4.2) spring-commands-rspec (1.0.4) @@ -410,7 +415,7 @@ GEM rest-client (~> 1.7, >= 1.7.3) term-ansicolor (1.6.0) tins (~> 1.0) - test-unit (3.2.5) + test-unit (3.2.6) power_assert therubyracer (0.12.3) libv8 (~> 3.16.14.15) @@ -418,18 +423,19 @@ GEM thor (0.19.4) thread_safe (0.3.6) tilt (2.0.8) - tins (1.15.0) - twitter (6.1.0) - addressable (~> 2.5) + tins (1.15.1) + twitter (6.2.0) + addressable (~> 2.3) buftok (~> 0.2.0) - equalizer (= 0.0.11) - faraday (~> 0.11.0) - http (~> 2.1) + equalizer (~> 0.0.11) + http (~> 3.0) + http-form_data (~> 2.0) http_parser.rb (~> 0.6.0) - memoizable (~> 0.4.2) - naught (~> 1.1) - simple_oauth (~> 0.3.1) - tzinfo (1.2.3) + memoizable (~> 0.4.0) + multipart-post (~> 2.0) + naught (~> 1.0) + simple_oauth (~> 0.3.0) + tzinfo (1.2.4) thread_safe (~> 0.1) uglifier (3.2.0) execjs (>= 0.3.0, < 3) @@ -437,10 +443,10 @@ GEM unf_ext unf_ext (0.0.7.4) unicode-display_width (1.3.0) - unicorn (5.3.0) + unicorn (5.3.1) kgio (~> 2.6) raindrops (~> 0.7) - valid_email2 (2.0.1) + valid_email2 (2.1.0) activemodel (>= 3.2) mail (~> 2.5) viewpoint (1.1.0) @@ -448,16 +454,16 @@ GEM logging nokogiri rubyntlm - webmock (3.0.1) + webmock (3.1.1) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff - websocket (1.2.4) + websocket (1.2.5) websocket-driver (0.6.5) websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.2) + websocket-extensions (0.1.3) writeexcel (1.0.5) - zendesk_api (1.14.4) + zendesk_api (1.16.0) faraday (~> 0.9) hashie (>= 3.5.2, < 4.0.0) inflection @@ -482,6 +488,7 @@ DEPENDENCIES composite_primary_keys coveralls daemons + dalli delayed_job_active_record diffy doorkeeper @@ -552,4 +559,4 @@ RUBY VERSION ruby 2.4.1p111 BUNDLED WITH - 1.15.4 + 1.16.0 From b9d728f890eec61ca96fe0c539e916f1bfe68dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bauer?= Date: Tue, 21 Nov 2017 22:50:03 +0100 Subject: [PATCH 016/196] replaced deprecate FactoryGirl gem with FactoryBot --- Gemfile | 4 ++-- Gemfile.lock | 24 ++++++++++++------------ spec/factories/group.rb | 4 ++-- spec/factories/import_job.rb | 2 +- spec/factories/link.rb | 2 +- spec/factories/online_notification.rb | 2 +- spec/factories/role.rb | 4 ++-- spec/factories/scheduler.rb | 4 ++-- spec/factories/signature.rb | 4 ++-- spec/factories/tag.rb | 2 +- spec/factories/ticket.rb | 4 ++-- spec/factories/ticket/article.rb | 2 +- spec/factories/token.rb | 4 ++-- spec/factories/user.rb | 4 ++-- spec/factories/vendor/net/ldap/entry.rb | 2 +- spec/support/factory_bot.rb | 3 +++ spec/support/factory_girl.rb | 3 --- spec/support/system_init.rb | 2 +- 18 files changed, 38 insertions(+), 38 deletions(-) create mode 100644 spec/support/factory_bot.rb delete mode 100644 spec/support/factory_girl.rb diff --git a/Gemfile b/Gemfile index 53e3fff1a..394f766f1 100644 --- a/Gemfile +++ b/Gemfile @@ -131,8 +131,8 @@ group :development, :test do # Setting ENV for testing purposes gem 'figaro' - # Use Factory Girl for generating random test data - gem 'factory_girl_rails' + # Use Factory Bot for generating random test data + gem 'factory_bot_rails' # mock http calls gem 'webmock' diff --git a/Gemfile.lock b/Gemfile.lock index 4fb822511..bc2c0bf51 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -97,12 +97,12 @@ GEM composite_primary_keys (10.0.1) activerecord (~> 5.1.0) concurrent-ruby (1.0.5) - coveralls (0.8.21) - json (>= 1.8, < 3) - simplecov (~> 0.14.1) - term-ansicolor (~> 1.3) - thor (~> 0.19.4) - tins (~> 1.6) + coveralls (0.7.1) + multi_json (~> 1.3) + rest-client + simplecov (>= 0.7) + term-ansicolor + thor crack (0.4.3) safe_yaml (~> 1.0.0) crass (1.0.3) @@ -132,10 +132,10 @@ GEM erubi (1.7.0) eventmachine (1.2.5) execjs (2.7.0) - factory_girl (4.9.0) + factory_bot (4.8.2) activesupport (>= 3.0.0) - factory_girl_rails (4.9.0) - factory_girl (~> 4.9.0) + factory_bot_rails (4.8.2) + factory_bot (~> 4.8.2) railties (>= 3.0.0) faraday (0.12.2) multipart-post (>= 1.2, < 3) @@ -391,7 +391,7 @@ GEM shellany (0.0.1) simple-rss (1.3.1) simple_oauth (0.3.1) - simplecov (0.14.1) + simplecov (0.15.1) docile (~> 1.1.0) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) @@ -420,7 +420,7 @@ GEM therubyracer (0.12.3) libv8 (~> 3.16.14.15) ref - thor (0.19.4) + thor (0.20.0) thread_safe (0.3.6) tilt (2.0.8) tins (1.15.1) @@ -496,7 +496,7 @@ DEPENDENCIES em-websocket eventmachine execjs - factory_girl_rails + factory_bot_rails figaro github_changelog_generator guard diff --git a/spec/factories/group.rb b/spec/factories/group.rb index 41470d89c..b2a673ee1 100644 --- a/spec/factories/group.rb +++ b/spec/factories/group.rb @@ -1,10 +1,10 @@ -FactoryGirl.define do +FactoryBot.define do sequence :test_group_name do |n| "TestGroup#{n}" end end -FactoryGirl.define do +FactoryBot.define do factory :group do name { generate(:test_group_name) } diff --git a/spec/factories/import_job.rb b/spec/factories/import_job.rb index ce4ed05e3..228093a1d 100644 --- a/spec/factories/import_job.rb +++ b/spec/factories/import_job.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :import_job do name 'Import::Test' payload {} diff --git a/spec/factories/link.rb b/spec/factories/link.rb index a9fb7195a..5365c4ade 100644 --- a/spec/factories/link.rb +++ b/spec/factories/link.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :link do link_type_id { Link::Type.find_by(name: 'normal').id } link_object_source_id { Link::Object.find_by(name: 'Ticket').id } diff --git a/spec/factories/online_notification.rb b/spec/factories/online_notification.rb index 37ef435f9..00e43c1d0 100644 --- a/spec/factories/online_notification.rb +++ b/spec/factories/online_notification.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :online_notification do object_lookup_id { ObjectLookup.by_name('Ticket') } type_lookup_id { TypeLookup.by_name('Assigned to you') } diff --git a/spec/factories/role.rb b/spec/factories/role.rb index 6a8cd742f..23d0c4463 100644 --- a/spec/factories/role.rb +++ b/spec/factories/role.rb @@ -1,10 +1,10 @@ -FactoryGirl.define do +FactoryBot.define do sequence :test_role_name do |n| "TestRole#{n}" end end -FactoryGirl.define do +FactoryBot.define do factory :role do name { generate(:test_role_name) } diff --git a/spec/factories/scheduler.rb b/spec/factories/scheduler.rb index 5b484c9a1..5f2cc2e36 100644 --- a/spec/factories/scheduler.rb +++ b/spec/factories/scheduler.rb @@ -1,10 +1,10 @@ -FactoryGirl.define do +FactoryBot.define do sequence :test_scheduler_name do |n| "Testscheduler#{n}" end end -FactoryGirl.define do +FactoryBot.define do factory :scheduler do name { generate(:test_scheduler_name) } diff --git a/spec/factories/signature.rb b/spec/factories/signature.rb index acbee4f72..15bc21f3e 100644 --- a/spec/factories/signature.rb +++ b/spec/factories/signature.rb @@ -1,10 +1,10 @@ -FactoryGirl.define do +FactoryBot.define do sequence :test_signature_name do |n| "Test signature #{n}" end end -FactoryGirl.define do +FactoryBot.define do factory :signature do name { generate(:test_signature_name) } body '#{user.firstname} #{user.lastname}'.text2html diff --git a/spec/factories/tag.rb b/spec/factories/tag.rb index 04e00c15d..3794e6f2b 100644 --- a/spec/factories/tag.rb +++ b/spec/factories/tag.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :tag do tag_object_id { Tag::Object.lookup_by_name_and_create('Ticket').id } tag_item_id { Tag::Item.lookup_by_name_and_create('blub').id } diff --git a/spec/factories/ticket.rb b/spec/factories/ticket.rb index a9161d77f..35b03275c 100644 --- a/spec/factories/ticket.rb +++ b/spec/factories/ticket.rb @@ -1,8 +1,8 @@ -FactoryGirl.define do +FactoryBot.define do factory :ticket do title 'Test Ticket' group { Group.lookup(name: 'Users') } - customer { FactoryGirl.create(:customer_user) } + customer { FactoryBot.create(:customer_user) } state { Ticket::State.lookup(name: 'new') } priority { Ticket::Priority.lookup(name: '2 normal') } updated_by_id 1 diff --git a/spec/factories/ticket/article.rb b/spec/factories/ticket/article.rb index be26f9c3f..efcd7f845 100644 --- a/spec/factories/ticket/article.rb +++ b/spec/factories/ticket/article.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :ticket_article, class: Ticket::Article do from 'factory-customer-1@example.com' to 'factory-customer-1@example.com' diff --git a/spec/factories/token.rb b/spec/factories/token.rb index bdd77e6f3..f87477ef1 100644 --- a/spec/factories/token.rb +++ b/spec/factories/token.rb @@ -1,6 +1,6 @@ -FactoryGirl.define do +FactoryBot.define do factory :token do - user_id { FactoryGirl.create(:user).id } + user_id { FactoryBot.create(:user).id } end factory :token_password_reset, parent: :token do diff --git a/spec/factories/user.rb b/spec/factories/user.rb index 8077e61be..4272f225d 100644 --- a/spec/factories/user.rb +++ b/spec/factories/user.rb @@ -1,10 +1,10 @@ -FactoryGirl.define do +FactoryBot.define do sequence :email do |n| "nicole.braun#{n}@zammad.org" end end -FactoryGirl.define do +FactoryBot.define do factory :user do login 'nicole.braun' diff --git a/spec/factories/vendor/net/ldap/entry.rb b/spec/factories/vendor/net/ldap/entry.rb index 605b368eb..5dc4fa729 100644 --- a/spec/factories/vendor/net/ldap/entry.rb +++ b/spec/factories/vendor/net/ldap/entry.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do # add custom attributes via: # mocked_entry = build(:ldap_entry) diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb new file mode 100644 index 000000000..c7890e49c --- /dev/null +++ b/spec/support/factory_bot.rb @@ -0,0 +1,3 @@ +RSpec.configure do |config| + config.include FactoryBot::Syntax::Methods +end diff --git a/spec/support/factory_girl.rb b/spec/support/factory_girl.rb deleted file mode 100644 index eec437fb3..000000000 --- a/spec/support/factory_girl.rb +++ /dev/null @@ -1,3 +0,0 @@ -RSpec.configure do |config| - config.include FactoryGirl::Syntax::Methods -end diff --git a/spec/support/system_init.rb b/spec/support/system_init.rb index 3b4bcec18..ac0c986b7 100644 --- a/spec/support/system_init.rb +++ b/spec/support/system_init.rb @@ -3,7 +3,7 @@ RSpec.configure do |config| email = 'admin@example.com' if !::User.exists?(email: email) - FactoryGirl.create(:user, + FactoryBot.create(:user, login: 'admin', firstname: 'Admin', lastname: 'Admin', From 492ca261cef2e6398d9331992a8af1d126a92be2 Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Tue, 21 Nov 2017 18:46:26 +0100 Subject: [PATCH 017/196] Fixed bug #1663 - Exchange integration tries to import user twice and fails. --- .../common/model/attributes/remote_id.rb | 2 +- .../common/model/attributes/remote_id_spec.rb | 70 +++++++++++++++++++ .../common/model/external_sync/lookup_spec.rb | 25 +++++++ 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 spec/lib/sequencer/unit/import/common/model/attributes/remote_id_spec.rb create mode 100644 spec/lib/sequencer/unit/import/common/model/external_sync/lookup_spec.rb diff --git a/lib/sequencer/unit/import/common/model/attributes/remote_id.rb b/lib/sequencer/unit/import/common/model/attributes/remote_id.rb index 412fbb773..ca2dae12c 100644 --- a/lib/sequencer/unit/import/common/model/attributes/remote_id.rb +++ b/lib/sequencer/unit/import/common/model/attributes/remote_id.rb @@ -12,7 +12,7 @@ class Sequencer def process state.provide(:remote_id) do - resource.fetch(attribute) + resource.fetch(attribute).dup.to_s.downcase end rescue KeyError => e handle_failure(e) diff --git a/spec/lib/sequencer/unit/import/common/model/attributes/remote_id_spec.rb b/spec/lib/sequencer/unit/import/common/model/attributes/remote_id_spec.rb new file mode 100644 index 000000000..b82648474 --- /dev/null +++ b/spec/lib/sequencer/unit/import/common/model/attributes/remote_id_spec.rb @@ -0,0 +1,70 @@ +require 'rails_helper' + +RSpec.describe Sequencer::Unit::Import::Common::Model::Attributes::RemoteId, sequencer: :unit do + + it 'takes remote_id from id' do + parameters = { + resource: { + id: '123abc', + } + } + + provided = process(parameters) + + expect(provided).to include(remote_id: '123abc') + end + + it 'takes remote_id from attribute method result' do + parameters = { + resource: { + other_attribute: '123abc', + } + } + + provided = process(parameters) do |instance| + expect(instance).to receive(:attribute).and_return(:other_attribute) + end + + expect(provided).to include(remote_id: '123abc') + end + + it 'converts value to a String' do + parameters = { + resource: { + id: 1337, + } + } + + provided = process(parameters) + + expect(provided).to include(remote_id: '1337') + end + + it 'downcases the value to prevent case sensivity issues with the ORM' do + parameters = { + resource: { + id: 'AbCdEfG', + } + } + + provided = process(parameters) + + expect(provided[:remote_id]).to eq(parameters[:resource][:id].downcase) + end + + it 'duplicates the value to prevent attribute changes' do + parameters = { + resource: { + id: 'this is', + } + } + + provided = process(parameters) + + expect(provided[:remote_id]).to eq(parameters[:resource][:id]) + + parameters[:resource][:id] += ' a test' + + expect(provided[:remote_id]).not_to eq(parameters[:resource][:id]) + end +end diff --git a/spec/lib/sequencer/unit/import/common/model/external_sync/lookup_spec.rb b/spec/lib/sequencer/unit/import/common/model/external_sync/lookup_spec.rb new file mode 100644 index 000000000..58c33b2bd --- /dev/null +++ b/spec/lib/sequencer/unit/import/common/model/external_sync/lookup_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' + +RSpec.describe Sequencer::Unit::Import::Common::Model::ExternalSync::Lookup, sequencer: :unit do + + it 'finds model_class instances by remote_id' do + user = create(:user) + external_sync_source = 'test' + remote_id = '1337' + + ExternalSync.create( + source: external_sync_source, + source_id: remote_id, + o_id: user.id, + object: user.class, + ) + + provided = process( + remote_id: remote_id, + model_class: user.class, + external_sync_source: external_sync_source, + ) + + expect(provided[:instance]).to eq(user) + end +end From e2dec9b0464718e68ce265d3676883c40869c620 Mon Sep 17 00:00:00 2001 From: jayki Date: Wed, 22 Nov 2017 16:33:50 +0100 Subject: [PATCH 018/196] Update functions This is needed for automated restore via Script to the latest Backup file. So the files latest* always point to the newest Backup. --- contrib/backup/functions | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contrib/backup/functions b/contrib/backup/functions index bbb2d4019..b92ec39d4 100644 --- a/contrib/backup/functions +++ b/contrib/backup/functions @@ -41,16 +41,19 @@ function backup_dir_create () { function backup_files () { echo "creating file backup..." tar -czf ${BACKUP_DIR}/${TIMESTAMP}_zammad_files.tar.gz ${ZAMMAD_DIR} + ln -sfn ${BACKUP_DIR}/${TIMESTAMP}_zammad_files.tar.gz ${BACKUP_DIR}/latest_zammad_files.tar.gz } function backup_db () { if [ "${DB_ADAPTER}" == "mysql2" ]; then echo "creating mysql backup..." mysqldump --opt --single-transaction -u${DB_USER} -p${DB_PASS} ${DB_NAME} | gzip > ${BACKUP_DIR}/${TIMESTAMP}_zammad_db.mysql.gz + ln -sfn ${BACKUP_DIR}/${TIMESTAMP}_zammad_db.mysql.gz ${BACKUP_DIR}/latest_zammad_db.mysql.gz elif [ "${DB_ADAPTER}" == "postgresql" ]; then echo "creating postgresql backup..." su -c "pg_dump -c ${DB_NAME} | gzip > /tmp/${TIMESTAMP}_zammad_db.psql.gz" postgres mv /tmp/${TIMESTAMP}_zammad_db.psql.gz ${BACKUP_DIR} + ln -sfn ${BACKUP_DIR}/${TIMESTAMP}_zammad_db.psql.gz ${BACKUP_DIR}/latest_zammad_db.psql.gz else echo "DB ADAPTER not found. if its sqlite backup is already saved in the filebackup" fi From 705487154c81599768dd5429d23434f59422b03e Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Thu, 23 Nov 2017 09:09:44 +0100 Subject: [PATCH 019/196] Applied changes for Rubocop 0.51. --- .rubocop.yml | 61 ++- Gemfile | 128 ++++--- Gemfile.lock | 15 +- Rakefile | 0 .../application_controller/authenticates.rb | 2 +- .../application_controller/handles_devices.rb | 3 +- .../application_controller/handles_errors.rb | 2 +- .../application_controller/renders_models.rb | 2 +- app/controllers/channels_email_controller.rb | 2 +- app/controllers/form_controller.rb | 25 +- app/controllers/getting_started_controller.rb | 6 +- app/controllers/import_otrs_controller.rb | 8 +- app/controllers/import_zendesk_controller.rb | 2 +- .../integration/check_mk_controller.rb | 2 + .../integration/idoit_controller.rb | 4 +- .../integration/sipgate_controller.rb | 2 + app/controllers/long_polling_controller.rb | 5 +- app/controllers/monitoring_controller.rb | 12 +- .../object_manager_attributes_controller.rb | 12 +- app/controllers/organizations_controller.rb | 8 +- app/controllers/search_controller.rb | 3 + app/controllers/sessions_controller.rb | 4 +- app/controllers/settings_controller.rb | 6 +- app/controllers/ticket_articles_controller.rb | 2 +- app/controllers/tickets_controller.rb | 25 +- .../time_accountings_controller.rb | 6 +- .../user_access_token_controller.rb | 2 +- app/controllers/user_devices_controller.rb | 2 +- app/controllers/users_controller.rb | 17 +- app/models/activity_stream.rb | 2 +- app/models/application_model/can_assets.rb | 2 +- .../application_model/can_associations.rb | 40 +- .../application_model/can_cleanup_param.rb | 4 +- .../can_creates_and_updates.rb | 20 +- .../application_model/can_touch_references.rb | 2 +- .../application_model/has_attachments.rb | 2 +- app/models/authorization.rb | 2 +- app/models/avatar.rb | 18 +- app/models/calendar.rb | 8 +- app/models/channel.rb | 3 - app/models/channel/assets.rb | 4 +- app/models/channel/driver/facebook.rb | 3 +- app/models/channel/driver/imap.rb | 4 +- app/models/channel/driver/smtp.rb | 2 +- app/models/channel/driver/twitter.rb | 23 +- app/models/channel/email_build.rb | 60 ++- app/models/channel/email_parser.rb | 77 ++-- .../channel/filter/auto_response_check.rb | 2 +- .../bounce_delivery_permanent_failed.rb | 2 +- app/models/channel/filter/follow_up_check.rb | 1 + app/models/channel/filter/identify_sender.rb | 6 +- .../channel/filter/match/email_regex.rb | 4 +- app/models/channel/filter/monitoring_base.rb | 9 +- .../channel/filter/out_of_office_check.rb | 5 +- app/models/channel/filter/trusted.rb | 2 +- app/models/chat.rb | 6 +- app/models/chat/agent.rb | 2 +- app/models/concerns/can_seed.rb | 2 +- .../concerns/has_activity_stream_log.rb | 5 +- app/models/concerns/has_history.rb | 10 +- .../concerns/has_search_index_backend.rb | 2 +- app/models/cti/caller_id.rb | 16 +- app/models/cti/log.rb | 2 +- app/models/email_address.rb | 2 +- app/models/external_sync.rb | 2 +- app/models/job.rb | 8 +- app/models/job/assets.rb | 2 +- app/models/locale.rb | 4 +- app/models/object_manager.rb | 4 +- app/models/object_manager/attribute.rb | 65 ++-- .../observer/chat/leave/background_job.rb | 2 - .../observer/organization/ref_object_touch.rb | 4 +- .../observer/sla/ticket_rebuild_escalation.rb | 4 +- .../background_job.rb | 3 +- .../ticket/article/communicate_email.rb | 2 +- .../communicate_email/background_job.rb | 4 +- .../ticket/article/communicate_facebook.rb | 2 +- .../ticket/article/communicate_twitter.rb | 2 +- .../ticket/article/fillup_from_email.rb | 2 +- .../ticket/article/fillup_from_general.rb | 2 +- .../article/fillup_from_origin_by_id.rb | 2 +- app/models/observer/ticket/article_changes.rb | 4 +- .../observer/ticket/ref_object_touch.rb | 10 +- app/models/observer/ticket/reset_new_state.rb | 2 +- app/models/observer/transaction.rb | 8 +- app/models/observer/user/geo.rb | 4 +- app/models/observer/user/ref_object_touch.rb | 6 +- app/models/organization/assets.rb | 2 +- app/models/overview.rb | 4 +- app/models/overview/assets.rb | 14 +- app/models/package.rb | 14 +- app/models/postmaster_filter.rb | 2 +- app/models/report.rb | 2 +- app/models/role/assets.rb | 4 +- app/models/scheduler.rb | 4 +- app/models/sla/assets.rb | 2 +- app/models/store.rb | 2 +- app/models/store/file.rb | 4 +- app/models/store/provider/file.rb | 9 +- app/models/taskbar.rb | 8 +- app/models/ticket.rb | 49 +-- app/models/ticket/article.rb | 17 +- app/models/ticket/article/assets.rb | 2 +- app/models/ticket/assets.rb | 2 +- app/models/ticket/escalation.rb | 14 +- app/models/ticket/overviews.rb | 2 +- app/models/ticket/priority.rb | 7 +- app/models/ticket/screen_options.rb | 2 +- app/models/ticket/search_index.rb | 2 +- app/models/ticket/state.rb | 12 +- app/models/transaction/background_job.rb | 2 +- .../transaction/cti_caller_id_detection.rb | 1 + app/models/transaction/karma.rb | 11 +- app/models/transaction/notification.rb | 8 +- app/models/transaction/slack.rb | 14 +- app/models/transaction/trigger.rb | 8 +- app/models/translation.rb | 12 +- app/models/user.rb | 31 +- app/models/user/assets.rb | 36 +- app/models/user_device.rb | 2 +- config/application.rb | 6 +- config/initializers/assets.rb | 4 +- config/initializers/core_ext.rb | 2 +- config/initializers/doorkeeper.rb | 2 +- .../initializers/filter_parameter_logging.rb | 2 +- config/initializers/html_email_style.rb | 154 ++++---- config/initializers/html_sanitizer.rb | 78 ++-- config/initializers/vendor_lib.rb | 2 +- config/routes.rb | 2 +- config/routes/auth.rb | 8 +- config/routes/message.rb | 4 +- config/routes/organization.rb | 2 +- config/routes/report.rb | 2 +- config/routes/search.rb | 4 +- config/routes/ticket.rb | 2 +- config/routes/user.rb | 4 +- db/migrate/20120101000001_create_base.rb | 28 +- db/migrate/20120101000010_create_ticket.rb | 14 +- .../20150979000001_update_timestamps.rb | 2 +- ...160217000001_object_manager_update_user.rb | 1 + ...01_organization_domain_based_assignment.rb | 1 + ...00002_ticket_number_generator_issue_427.rb | 2 +- ...3000001_fixed_admin_user_permission_920.rb | 6 +- db/migrate/20170419000001_ldap_support.rb | 2 +- db/migrate/20170531144425_foreign_keys.rb | 96 ++--- ...905140038_cti_log_preferences_migration.rb | 4 +- ...xed_twitter_ticket_article_preferences4.rb | 2 +- db/migrate/20170910000002_out_of_office2.rb | 8 +- db/migrate/20170912123300_remove_network.rb | 1 + db/seeds.rb | 3 +- db/seeds/object_manager_attributes.rb | 6 +- db/seeds/overviews.rb | 54 +-- db/seeds/roles.rb | 2 +- db/seeds/settings.rb | 8 +- db/seeds/triggers.rb | 6 +- lib/application_handle_info.rb | 5 + lib/application_lib.rb | 3 +- lib/auth/ldap.rb | 2 +- lib/auto_wizard.rb | 50 ++- lib/calendar_subscriptions.rb | 7 +- lib/calendar_subscriptions/tickets.rb | 2 +- lib/core_ext/integer.rb | 17 - lib/core_ext/nil_class.rb | 17 - lib/core_ext/open-uri.rb | 5 +- lib/core_ext/string.rb | 17 +- lib/email_helper/probe.rb | 16 +- lib/encode.rb | 2 - lib/enrichment/clearbit/user.rb | 2 +- lib/facebook.rb | 13 +- lib/fill_db.rb | 65 ++-- lib/html_sanitizer.rb | 38 +- lib/import/base_factory.rb | 6 +- lib/import/base_resource.rb | 6 +- lib/import/exchange/folder.rb | 2 +- lib/import/exchange/item_attributes.rb | 4 +- lib/import/integration_base.rb | 2 +- lib/import/ldap/user.rb | 8 +- lib/import/ldap/user_factory.rb | 2 +- lib/import/otrs.rb | 2 +- lib/import/otrs/article/attachment_factory.rb | 2 +- lib/import/otrs/article_customer.rb | 2 +- lib/import/otrs/article_customer_factory.rb | 2 +- lib/import/otrs/dynamic_field_factory.rb | 4 +- lib/import/otrs/history_factory.rb | 2 +- lib/import/otrs/import_stats.rb | 6 +- lib/import/otrs/requester.rb | 4 +- lib/import/otrs/state_factory.rb | 8 +- lib/import/otrs/sys_config_factory.rb | 4 +- lib/import/otrs/ticket.rb | 6 +- lib/import/otrs/user.rb | 4 +- lib/import/zendesk/import_stats.rb | 2 +- lib/import/zendesk/object_attribute.rb | 3 +- lib/import/zendesk/ticket/comment.rb | 2 +- .../ticket/comment/attachment_factory.rb | 2 +- lib/import/zendesk/user/group.rb | 2 +- lib/ldap.rb | 9 +- lib/ldap/group.rb | 6 +- lib/ldap/user.rb | 78 ++-- lib/mixin/required_sub_paths.rb | 2 +- lib/models.rb | 30 +- lib/notification_factory/mailer.rb | 2 +- lib/notification_factory/renderer.rb | 4 +- lib/report/ticket_moved.rb | 5 +- lib/report/ticket_reopened.rb | 2 +- lib/search_index_backend.rb | 8 +- .../common/import_job/sub_sequence/general.rb | 3 +- .../import/common/model/skip/blank/base.rb | 2 +- .../model/skip/missing_mandatory/base.rb | 2 +- .../common/model/statistics/mixin/diff.rb | 2 +- .../import/common/user/attributes/downcase.rb | 2 +- .../folder_contact/statistics/diff.rb | 2 +- lib/sequencer/unit/mixin/dynamic_attribute.rb | 1 - lib/service/image/zammad.rb | 2 +- lib/sessions.rb | 22 +- lib/sessions/backend/activity_stream.rb | 4 +- lib/sessions/event/base.rb | 3 +- lib/sessions/event/login.rb | 2 +- lib/signature_detection.rb | 6 +- lib/static_assets.rb | 24 +- lib/stats.rb | 2 +- lib/telegram.rb | 12 +- lib/tweet_base.rb | 29 +- lib/tweet_stream.rb | 2 +- lib/user_agent.rb | 10 +- lib/version.rb | 2 +- script/websocket-server.rb | 7 +- spec/factories/signature.rb | 2 +- spec/lib/auth/ldap_spec.rb | 2 +- spec/lib/import/ldap/user_factory_spec.rb | 4 +- spec/lib/import/otrs/state_factory_spec.rb | 4 +- spec/lib/import/otrs/user_spec.rb | 3 +- spec/models/concerns/has_groups_examples.rb | 16 +- spec/models/concerns/has_roles_examples.rb | 14 +- spec/models/cti/caller_id_spec.rb | 6 +- spec/models/taskbar_spec.rb | 2 +- spec/models/translation_spec.rb | 6 +- spec/rails_helper.rb | 2 +- spec/support/system_init.rb | 12 +- test/browser/aaa_getting_started_test.rb | 6 +- test/browser/aab_basic_urls_test.rb | 2 +- test/browser/aab_unit_test.rb | 2 +- test/browser/aac_basic_richtext_test.rb | 2 +- test/browser/abb_one_group_test.rb | 2 +- test/browser/admin_channel_email_test.rb | 2 +- test/browser/admin_object_manager_test.rb | 2 +- test/browser/admin_overview_test.rb | 2 +- test/browser/admin_role_test.rb | 2 +- .../agent_navigation_and_title_test.rb | 2 +- .../agent_organization_profile_test.rb | 2 +- test/browser/agent_ticket_attachment_test.rb | 2 +- ...agent_ticket_email_reply_keep_body_test.rb | 2 +- .../agent_ticket_email_signature_test.rb | 2 +- test/browser/agent_ticket_link_test.rb | 2 +- test/browser/agent_ticket_macro_test.rb | 2 +- test/browser/agent_ticket_merge_test.rb | 2 +- .../agent_ticket_online_notification_test.rb | 2 +- .../agent_ticket_overview_level0_test.rb | 2 +- .../agent_ticket_overview_level1_test.rb | 2 +- .../browser/agent_ticket_overview_tab_test.rb | 2 +- test/browser/agent_ticket_tag_test.rb | 2 +- test/browser/agent_ticket_text_module_test.rb | 4 +- .../agent_ticket_time_accounting_test.rb | 2 +- test/browser/agent_ticket_update1_test.rb | 2 +- test/browser/agent_ticket_update2_test.rb | 2 +- test/browser/agent_ticket_update3_test.rb | 2 +- .../agent_ticket_update_and_reload_test.rb | 2 +- test/browser/agent_user_manage_test.rb | 2 +- test/browser/agent_user_profile_test.rb | 2 +- test/browser/auth_test.rb | 2 +- test/browser/chat_test.rb | 2 +- test/browser/customer_ticket_create_test.rb | 2 +- test/browser/first_steps_test.rb | 2 +- test/browser/form_test.rb | 2 +- test/browser/integration_test.rb | 2 +- test/browser/keyboard_shortcuts_test.rb | 2 +- test/browser/maintenance_app_version_test.rb | 2 +- .../browser/maintenance_login_message_test.rb | 2 +- test/browser/maintenance_mode_test.rb | 2 +- .../maintenance_session_message_test.rb | 2 +- test/browser/manage_test.rb | 2 +- test/browser/monitoring_test.rb | 2 +- test/browser/preferences_language_test.rb | 2 +- .../preferences_permission_check_test.rb | 2 +- test/browser/preferences_token_access_test.rb | 2 +- test/browser/setting_test.rb | 2 +- .../signup_password_change_and_reset_test.rb | 2 +- test/browser/switch_to_user_test.rb | 2 +- test/browser/taskbar_session_test.rb | 2 +- test/browser/taskbar_task_test.rb | 2 +- test/browser/translation_test.rb | 2 +- test/browser/user_switch_cache_test.rb | 2 +- test/browser_test_helper.rb | 348 ++++++++---------- test/controllers/api_auth_controller_test.rb | 4 +- test/controllers/basic_controller_test.rb | 2 +- test/controllers/form_controller_test.rb | 2 +- .../integration_check_mk_controller_test.rb | 2 +- .../controllers/monitoring_controller_test.rb | 8 +- test/controllers/packages_controller_test.rb | 4 +- test/controllers/search_controller_test.rb | 10 +- test/controllers/settings_controller_test.rb | 4 +- test/controllers/taskbars_controller_test.rb | 2 +- ...ket_article_attachments_controller_test.rb | 4 +- .../ticket_articles_controller_test.rb | 4 +- test/controllers/tickets_controller_test.rb | 4 +- .../user_organization_controller_test.rb | 4 +- test/fixtures/seeds.rb | 4 +- .../aaa_auto_wizard_base_setup_test.rb | 2 +- test/integration/auto_wizard_browser_test.rb | 2 +- test/integration/auto_wizard_test.rb | 16 +- .../calendar_subscriptions_tickets_test.rb | 8 +- test/integration/clearbit_test.rb | 2 +- test/integration/elasticsearch_test.rb | 28 +- test/integration/email_deliver_test.rb | 2 +- test/integration/email_helper_test.rb | 4 +- test/integration/email_keep_on_server_test.rb | 2 +- test/integration/facebook_browser_test.rb | 2 +- test/integration/facebook_test.rb | 2 +- test/integration/geo_calendar_test.rb | 2 +- test/integration/geo_ip_test.rb | 2 +- test/integration/geo_location_test.rb | 2 +- test/integration/idoit_controller_test.rb | 6 +- test/integration/object_manager_test.rb | 6 +- test/integration/otrs_import_browser_test.rb | 2 +- test/integration/otrs_import_test.rb | 8 +- test/integration/package_test.rb | 2 +- test/integration/report_test.rb | 2 +- test/integration/sipgate_controller_test.rb | 4 +- test/integration/slack_test.rb | 6 +- test/integration/telegram_controller_test.rb | 2 +- test/integration/twitter_browser_test.rb | 2 +- test/integration/twitter_test.rb | 2 +- test/integration/user_agent_test.rb | 2 +- .../user_device_controller_test.rb | 4 +- .../zendesk_import_browser_test.rb | 2 +- test/integration/zendesk_import_test.rb | 14 +- test/integration_test_helper.rb | 4 +- test/test_helper.rb | 12 +- test/unit/aaa_string_test.rb | 1 - test/unit/activity_stream_test.rb | 12 +- test/unit/assets_test.rb | 30 +- test/unit/auth_test.rb | 2 +- test/unit/cache_test.rb | 2 +- test/unit/calendar_subscription_test.rb | 2 +- test/unit/calendar_test.rb | 2 +- test/unit/chat_test.rb | 4 +- test/unit/cti_caller_id_test.rb | 2 +- test/unit/db_auto_increment_test.rb | 2 +- test/unit/email_address_test.rb | 2 +- test/unit/email_build_test.rb | 48 ++- test/unit/email_parser_test.rb | 1 - test/unit/email_postmaster_test.rb | 1 - test/unit/email_process_auto_response_test.rb | 2 +- ...s_bounce_delivery_permanent_failed_test.rb | 6 +- test/unit/email_process_bounce_follow_test.rb | 6 +- .../email_process_follow_up_possible_test.rb | 2 +- test/unit/email_process_follow_up_test.rb | 6 +- .../email_process_identify_sender_max_test.rb | 2 +- test/unit/email_process_out_of_office_test.rb | 2 +- test/unit/email_process_reply_to_test.rb | 2 +- ..._sender_is_system_address_or_agent_test.rb | 2 +- ...il_process_sender_name_update_if_needed.rb | 2 +- test/unit/email_process_state_open_set.rb | 2 +- test/unit/email_process_test.rb | 1 - test/unit/email_regex_test.rb | 2 +- test/unit/email_signatur_detection_test.rb | 20 +- test/unit/history_test.rb | 2 +- test/unit/html_sanitizer_test.rb | 2 +- test/unit/integration_icinga_test.rb | 2 +- test/unit/integration_monit_test.rb | 2 +- test/unit/integration_nagios_test.rb | 2 +- test/unit/job_test.rb | 2 +- test/unit/karma_test.rb | 2 +- test/unit/migration_ror_42_to50_store_test.rb | 2 +- test/unit/model_test.rb | 10 +- ...tification_factory_mailer_template_test.rb | 6 +- test/unit/notification_factory_mailer_test.rb | 2 +- .../notification_factory_renderer_test.rb | 2 +- ...otification_factory_slack_template_test.rb | 6 +- .../notification_factory_template_test.rb | 2 +- test/unit/object_cache_test.rb | 6 +- ...object_create_update_with_ref_name_test.rb | 8 +- test/unit/object_type_lookup_test.rb | 2 +- test/unit/online_notifiaction_test.rb | 8 +- ...ganization_domain_based_assignment_test.rb | 2 +- .../organization_ref_object_touch_test.rb | 2 +- test/unit/overview_test.rb | 56 +-- test/unit/permission_test.rb | 2 +- test/unit/recent_view_test.rb | 2 +- test/unit/role_test.rb | 2 +- test/unit/role_validate_agent_limit_test.rb | 4 +- test/unit/session_basic_test.rb | 24 +- test/unit/session_basic_ticket_test.rb | 4 +- test/unit/session_collections_test.rb | 18 +- test/unit/session_enhanced_test.rb | 6 +- test/unit/stats_ticket_waiting_time_test.rb | 2 +- test/unit/store_test.rb | 12 +- test/unit/tag_test.rb | 2 +- test/unit/ticket_article_communicate_test.rb | 2 +- test/unit/ticket_article_dos_test.rb | 2 +- test/unit/ticket_article_store_empty.rb | 2 +- .../ticket_article_time_accouting_test.rb | 2 +- test/unit/ticket_article_twitter_test.rb | 2 +- ...icket_customer_organization_update_test.rb | 2 +- test/unit/ticket_escalation_test.rb | 2 +- test/unit/ticket_last_owner_update_test.rb | 2 +- test/unit/ticket_notification_test.rb | 4 +- test/unit/ticket_null_byte_test.rb | 2 +- .../ticket_overview_out_of_office_test.rb | 20 +- test/unit/ticket_overview_test.rb | 50 +-- test/unit/ticket_priority_test.rb | 2 +- test/unit/ticket_ref_object_touch_test.rb | 2 +- test/unit/ticket_selector_test.rb | 2 +- test/unit/ticket_sla_test.rb | 14 +- test/unit/ticket_state_test.rb | 2 +- test/unit/ticket_test.rb | 2 +- test/unit/ticket_trigger_test.rb | 24 +- test/unit/ticket_xss_test.rb | 2 +- test/unit/token_test.rb | 2 +- test/unit/user_ref_object_touch_test.rb | 2 +- test/unit/user_test.rb | 56 ++- test/unit/user_validate_agent_limit_test.rb | 2 +- 421 files changed, 1792 insertions(+), 1867 deletions(-) mode change 100644 => 100755 Rakefile delete mode 100644 lib/core_ext/integer.rb delete mode 100644 lib/core_ext/nil_class.rb diff --git a/.rubocop.yml b/.rubocop.yml index f9156d529..6e91d3296 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -45,29 +45,29 @@ Style/TrailingCommaInArguments: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' Enabled: false -Style/SpaceInsideParens: +Layout/SpaceInsideParens: Description: 'No spaces after ( or before ).' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces' Enabled: false -Style/SpaceAfterMethodName: +Layout/SpaceAfterMethodName: Description: >- Do not put a space between a method name and the opening parenthesis in a method definition. StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces' Enabled: false -Style/LeadingCommentSpace: +Layout/LeadingCommentSpace: Description: 'Comments should start with a space.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-space' Enabled: false -Style/MethodCallParentheses: +Style/MethodCallWithoutArgsParentheses: Description: 'Do not use parentheses for method calls with no arguments.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-args-no-parens' Enabled: false -Style/SpaceInsideBrackets: +Layout/SpaceInsideBrackets: Description: 'No spaces after [ or before ].' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces' Enabled: false @@ -83,19 +83,19 @@ Style/MethodDefParentheses: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens' Enabled: false -Style/EmptyLinesAroundClassBody: +Layout/EmptyLinesAroundClassBody: Description: "Keeps track of empty lines around class bodies." Enabled: false -Style/EmptyLinesAroundMethodBody: +Layout/EmptyLinesAroundMethodBody: Description: "Keeps track of empty lines around method bodies." Enabled: false -Style/EmptyLinesAroundBlockBody: +Layout/EmptyLinesAroundBlockBody: Description: "Keeps track of empty lines around block bodies." Enabled: false -Style/EmptyLinesAroundModuleBody: +Layout/EmptyLinesAroundModuleBody: Description: "Keeps track of empty lines around module bodies." Enabled: false @@ -143,17 +143,29 @@ Rails/HasAndBelongsToMany: # StyleGuide: 'https://github.com/bbatsov/rails-style-guide#has-many-through' Enabled: false +Rails/SkipsModelValidations: + Description: >- + Use methods that skips model validations with caution. + See reference for more information. + Reference: 'http://guides.rubyonrails.org/active_record_validations.html#skipping-validations' + Enabled: true + Exclude: + - test/**/* + Style/ClassAndModuleChildren: Description: 'Checks style of children classes and modules.' Enabled: false -Style/FileName: +Naming/FileName: Description: 'Use snake_case for source file names.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files' Enabled: true Exclude: - 'script/websocket-server.rb' +Naming/VariableNumber: + Description: 'Use the configured style when numbering variables.' + Enabled: false # 2.0 @@ -184,8 +196,23 @@ Metrics/ModuleLength: Description: 'Avoid modules longer than 100 lines of code.' Enabled: false +Metrics/BlockLength: + Enabled: false + +Lint/RescueWithoutErrorClass: + Enabled: false + +Rails/ApplicationRecord: + Enabled: false + # TODO +Rails/HasManyOrHasOneDependent: + Enabled: false + +Style/DateTime: + Enabled: false + Style/Documentation: Description: 'Document classes and non-namespace modules.' Enabled: false @@ -193,7 +220,7 @@ Style/Documentation: Lint/UselessAssignment: Enabled: false -Style/ExtraSpacing: +Layout/ExtraSpacing: Description: 'Do not use unnecessary spacing.' Enabled: false @@ -215,4 +242,14 @@ Style/NumericPredicate: AutoCorrect: false Enabled: true Exclude: - - "**/*_spec.rb" \ No newline at end of file + - "**/*_spec.rb" + +Lint/AmbiguousBlockAssociation: + Description: >- + Checks for ambiguous block association with method when param passed without + parentheses. + StyleGuide: '#syntax' + Enabled: true + Exclude: + - "**/*_spec.rb" + - "**/*_examples.rb" diff --git a/Gemfile b/Gemfile index 394f766f1..047a01bb1 100644 --- a/Gemfile +++ b/Gemfile @@ -1,112 +1,131 @@ source 'https://rubygems.org' +# core - base ruby '2.4.1' - gem 'rails', '5.1.4' -gem 'rails-observers' + +# core - rails additions gem 'activerecord-session_store' - -# Bundle edge Rails instead: -#gem 'rails', :git => 'git://github.com/rails/rails.git' - +gem 'composite_primary_keys' gem 'json' +gem 'rails-observers' -# Supported DBs +# core - application servers +gem 'puma', group: :puma +gem 'unicorn', group: :unicorn + +# core - supported ORMs gem 'activerecord-nulldb-adapter', group: :nulldb gem 'mysql2', group: :mysql gem 'pg', group: :postgres +# core - asynchrous task execution +gem 'daemons' +gem 'delayed_job_active_record' + +# core - websocket +gem 'em-websocket' +gem 'eventmachine' + +# core - password security +gem 'argon2' + +# performance - Memcached +gem 'dalli' + +# asset handling group :assets do - gem 'sass-rails' #, github: 'rails/sass-rails' + # asset handling - coffee-script gem 'coffee-rails' gem 'coffee-script-source' - gem 'sprockets' - - gem 'uglifier' + # asset handling - frontend templating gem 'eco' + + # asset handling - SASS + gem 'sass-rails' + + # asset handling - pipeline + gem 'sprockets' + gem 'uglifier' end gem 'autoprefixer-rails' +# asset handling - javascript execution for e.g. linux +gem 'execjs' +gem 'libv8' +gem 'therubyracer' + +# authentication - provider gem 'doorkeeper' gem 'oauth2' +# authentication - third party gem 'omniauth' -gem 'omniauth-oauth2' gem 'omniauth-facebook' gem 'omniauth-github' gem 'omniauth-gitlab' gem 'omniauth-google-oauth2' gem 'omniauth-linkedin-oauth2' -gem 'omniauth-twitter' gem 'omniauth-microsoft-office365' +gem 'omniauth-oauth2' +gem 'omniauth-twitter' gem 'omniauth-weibo-oauth2' -gem 'twitter' -gem 'telegramAPI' +# channels gem 'koala' -gem 'mail' -gem 'valid_email2' +gem 'telegramAPI' +gem 'twitter' + +# channels - email additions gem 'htmlentities' - +gem 'mail', '2.6.6' gem 'mime-types' +gem 'valid_email2' +# feature - business hours gem 'biz' -gem 'composite_primary_keys' -gem 'delayed_job_active_record' -gem 'daemons' - -gem 'simple-rss' - -# e. g. on linux we need a javascript execution -gem 'libv8' -gem 'execjs' -gem 'therubyracer' - -require 'erb' -require 'yaml' - -gem 'net-ldap' - -# password security -gem 'argon2' +# feature - signature diffing +gem 'diffy' +# feature - excel output gem 'writeexcel' -gem 'icalendar' -gem 'icalendar-recurrence' + +# feature - device logging gem 'browser' +# feature - iCal export +gem 'icalendar' +gem 'icalendar-recurrence' + # integrations -gem 'slack-notifier' gem 'clearbit' +gem 'net-ldap' +gem 'slack-notifier' gem 'zendesk_api' -gem 'viewpoint' -gem 'rubyntlm', git: 'https://github.com/wimm/rubyntlm.git' + +# integrations - exchange gem 'autodiscover', git: 'https://github.com/thorsteneckel/autodiscover.git' - -# event machine -gem 'eventmachine' -gem 'em-websocket' - -gem 'diffy' -gem 'dalli' +gem 'rubyntlm', git: 'https://github.com/wimm/rubyntlm.git' +gem 'viewpoint' # Gems used only for develop/test and not required # in production environments by default. group :development, :test do + # test frameworks gem 'rspec-rails' gem 'test-unit' - gem 'spring' - gem 'spring-commands-rspec' + + # test DB gem 'sqlite3' # code coverage + gem 'coveralls', require: false gem 'simplecov' gem 'simplecov-rcov' - gem 'coveralls', require: false # UI tests w/ Selenium gem 'selenium-webdriver', '2.53.4' @@ -121,9 +140,9 @@ group :development, :test do gem 'guard-symlink', require: false # code QA + gem 'coffeelint' gem 'pre-commit' gem 'rubocop' - gem 'coffeelint' # changelog generation gem 'github_changelog_generator' @@ -138,10 +157,7 @@ group :development, :test do gem 'webmock' end -gem 'puma', group: :puma -gem 'unicorn', group: :unicorn - -# load onw gem's +# load onw gems for development and testing purposes local_gemfile = File.join(File.dirname(__FILE__), 'Gemfile.local') if File.exist?(local_gemfile) eval_gemfile local_gemfile diff --git a/Gemfile.lock b/Gemfile.lock index bc2c0bf51..a5ae0d62c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -217,13 +217,12 @@ GEM crass (~> 1.0.2) nokogiri (>= 1.5.9) lumberjack (1.0.12) - mail (2.7.0) - mini_mime (>= 0.1.1) + mail (2.6.6) + mime-types (>= 1.16, < 4) memoizable (0.4.2) thread_safe (~> 0.3, >= 0.3.1) method_source (0.9.0) mime-types (2.99.3) - mini_mime (1.0.0) mini_portile2 (2.3.0) minitest (5.10.3) multi_json (1.12.2) @@ -389,7 +388,6 @@ GEM rubyzip (~> 1.0) websocket (~> 1.0) shellany (0.0.1) - simple-rss (1.3.1) simple_oauth (0.3.1) simplecov (0.15.1) docile (~> 1.1.0) @@ -399,10 +397,6 @@ GEM simplecov-rcov (0.2.3) simplecov (>= 0.4.1) slack-notifier (2.3.1) - spring (2.0.2) - activesupport (>= 4.2) - spring-commands-rspec (1.0.4) - spring (>= 0.9.1) sprockets (3.7.1) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -508,7 +502,7 @@ DEPENDENCIES json koala libv8 - mail + mail (= 2.6.6) mime-types mysql2 net-ldap @@ -535,12 +529,9 @@ DEPENDENCIES rubyntlm! sass-rails selenium-webdriver (= 2.53.4) - simple-rss simplecov simplecov-rcov slack-notifier - spring - spring-commands-rspec sprockets sqlite3 telegramAPI diff --git a/Rakefile b/Rakefile old mode 100644 new mode 100755 diff --git a/app/controllers/application_controller/authenticates.rb b/app/controllers/application_controller/authenticates.rb index 9d5703dbb..c0b1ff341 100644 --- a/app/controllers/application_controller/authenticates.rb +++ b/app/controllers/application_controller/authenticates.rb @@ -18,7 +18,7 @@ module ApplicationController::Authenticates raise Exceptions::NotAuthorized, 'Not authorized (token)!' end - return false if current_user && current_user.permissions?(key) + return false if current_user&.permissions?(key) raise Exceptions::NotAuthorized, 'Not authorized (user)!' end diff --git a/app/controllers/application_controller/handles_devices.rb b/app/controllers/application_controller/handles_devices.rb index 9bead8f45..718dae82e 100644 --- a/app/controllers/application_controller/handles_devices.rb +++ b/app/controllers/application_controller/handles_devices.rb @@ -26,8 +26,7 @@ module ApplicationController::HandlesDevices if user_device_updated_at # check if entry exists / only if write action diff = Time.zone.now - 10.minutes - method = request.method - if method == 'GET' || method == 'OPTIONS' || method == 'HEAD' + if %w[GET OPTIONS HEAD].include?(request.method) diff = Time.zone.now - 30.minutes end diff --git a/app/controllers/application_controller/handles_errors.rb b/app/controllers/application_controller/handles_errors.rb index 1ec299704..98c1728f7 100644 --- a/app/controllers/application_controller/handles_errors.rb +++ b/app/controllers/application_controller/handles_errors.rb @@ -72,7 +72,7 @@ module ApplicationController::HandlesErrors data[:error_human] = data[:error] end - if Rails.env.production? && !data[:error_human].empty? + if Rails.env.production? && data[:error_human].present? data[:error] = data.delete(:error_human) end data diff --git a/app/controllers/application_controller/renders_models.rb b/app/controllers/application_controller/renders_models.rb index a4eb681d1..09fd9db7d 100644 --- a/app/controllers/application_controller/renders_models.rb +++ b/app/controllers/application_controller/renders_models.rb @@ -146,7 +146,7 @@ module ApplicationController::RendersModels def model_references_check(object, params) generic_object = object.find(params[:id]) result = Models.references(object, generic_object.id) - return false if result.empty? + return false if result.blank? raise Exceptions::UnprocessableEntity, 'Can\'t delete, object has references.' rescue => e raise Exceptions::UnprocessableEntity, e diff --git a/app/controllers/channels_email_controller.rb b/app/controllers/channels_email_controller.rb index 633d13dfb..0dc1ac72a 100644 --- a/app/controllers/channels_email_controller.rb +++ b/app/controllers/channels_email_controller.rb @@ -226,7 +226,7 @@ class ChannelsEmailController < ApplicationController Channel.where(area: 'Email::Notification').each do |channel| active = false - if adapter =~ /^#{channel.options[:outbound][:adapter]}$/i + if adapter.match?(/^#{channel.options[:outbound][:adapter]}$/i) active = true channel.options = { outbound: { diff --git a/app/controllers/form_controller.rb b/app/controllers/form_controller.rb index 22693a203..18ab83c70 100644 --- a/app/controllers/form_controller.rb +++ b/app/controllers/form_controller.rb @@ -44,7 +44,7 @@ class FormController < ApplicationController errors['email'] = 'required' elsif params[:email] !~ /@/ errors['email'] = 'invalid' - elsif params[:email] =~ /(>|<|\||\!|"|§|'|\$|%|&|\(|\)|\?|\s|\.\.)/ + elsif params[:email].match?(/(>|<|\||\!|"|§|'|\$|%|&|\(|\)|\?|\s|\.\.)/) errors['email'] = 'invalid' end if params[:title].blank? @@ -126,19 +126,16 @@ class FormController < ApplicationController internal: false, ) - if params[:file] - - params[:file].each do |file| - Store.add( - object: 'Ticket::Article', - o_id: article.id, - data: file.read, - filename: file.original_filename, - preferences: { - 'Mime-Type' => file.content_type, - } - ) - end + params[:file]&.each do |file| + Store.add( + object: 'Ticket::Article', + o_id: article.id, + data: file.read, + filename: file.original_filename, + preferences: { + 'Mime-Type' => file.content_type, + } + ) end UserInfo.current_user_id = 1 diff --git a/app/controllers/getting_started_controller.rb b/app/controllers/getting_started_controller.rb index c074b928e..91b542d4a 100644 --- a/app/controllers/getting_started_controller.rb +++ b/app/controllers/getting_started_controller.rb @@ -66,7 +66,7 @@ curl http://localhost/api/v1/getting_started -v -u #{login}:#{password} # verify auto wizard file auto_wizard_data = AutoWizard.data - if !auto_wizard_data || auto_wizard_data.empty? + if auto_wizard_data.blank? render json: { auto_wizard: true, auto_wizard_success: false, @@ -132,7 +132,7 @@ curl http://localhost/api/v1/getting_started -v -u #{login}:#{password} end # validate organization - if !params[:organization] || params[:organization].empty? + if params[:organization].blank? messages[:organization] = 'Invalid!' else settings[:organization] = params[:organization] @@ -146,7 +146,7 @@ curl http://localhost/api/v1/getting_started -v -u #{login}:#{password} end end - if !messages.empty? + if messages.present? render json: { result: 'invalid', messages: messages, diff --git a/app/controllers/import_otrs_controller.rb b/app/controllers/import_otrs_controller.rb index 9177de868..289d9e7ca 100644 --- a/app/controllers/import_otrs_controller.rb +++ b/app/controllers/import_otrs_controller.rb @@ -26,7 +26,7 @@ class ImportOtrsController < ApplicationController if !response.success? && response.code.to_s !~ /^40.$/ message_human = '' translation_map.each do |key, message| - if response.error.to_s =~ /#{Regexp.escape(key)}/i + if response.error.to_s.match?(/#{Regexp.escape(key)}/i) message_human = message end end @@ -39,7 +39,7 @@ class ImportOtrsController < ApplicationController end result = {} - if response.body =~ /zammad migrator/ + if response.body.match?(/zammad migrator/) migrator_response = JSON.parse(response.body) @@ -86,7 +86,7 @@ class ImportOtrsController < ApplicationController message_human: migrator_response['Error'] } end - elsif response.body =~ /(otrs\sag|otrs\.com|otrs\.org)/i + elsif response.body.match?(/(otrs\sag|otrs\.com|otrs\.org)/i) result = { result: 'invalid', message_human: 'Host found, but no OTRS migrator is installed!' @@ -144,7 +144,7 @@ class ImportOtrsController < ApplicationController end result = 'ok' - if !issues.empty? + if issues.present? result = 'failed' end render json: { diff --git a/app/controllers/import_zendesk_controller.rb b/app/controllers/import_zendesk_controller.rb index 2b2f23541..28bdbd609 100644 --- a/app/controllers/import_zendesk_controller.rb +++ b/app/controllers/import_zendesk_controller.rb @@ -28,7 +28,7 @@ class ImportZendeskController < ApplicationController if !response.success? message_human = '' translation_map.each do |key, message| - if response.error.to_s =~ /#{Regexp.escape(key)}/i + if response.error.to_s.match?(/#{Regexp.escape(key)}/i) message_human = message end end diff --git a/app/controllers/integration/check_mk_controller.rb b/app/controllers/integration/check_mk_controller.rb index 4335af3a6..2c62c7fe3 100644 --- a/app/controllers/integration/check_mk_controller.rb +++ b/app/controllers/integration/check_mk_controller.rb @@ -133,6 +133,8 @@ UserAgent: #{request.env['HTTP_USER_AGENT']} if Setting.get('check_mk_token') != params[:token] raise Exceptions::UnprocessableEntity, 'Invalid token!' end + + true end end diff --git a/app/controllers/integration/idoit_controller.rb b/app/controllers/integration/idoit_controller.rb index c217de073..39ea03361 100644 --- a/app/controllers/integration/idoit_controller.rb +++ b/app/controllers/integration/idoit_controller.rb @@ -1,9 +1,9 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class Integration::IdoitController < ApplicationController - prepend_before_action -> { authentication_check(permission: ['agent.integration.idoit', 'admin.integration.idoit']) }, except: [:verify, :query, :update] + prepend_before_action -> { authentication_check(permission: ['agent.integration.idoit', 'admin.integration.idoit']) }, except: %i[verify query update] prepend_before_action -> { authentication_check(permission: ['admin.integration.idoit']) }, only: [:verify] - prepend_before_action -> { authentication_check(permission: ['ticket.agent']) }, only: [:query, :update] + prepend_before_action -> { authentication_check(permission: ['ticket.agent']) }, only: %i[query update] def verify response = ::Idoit.verify(params[:api_token], params[:endpoint], params[:client_id]) diff --git a/app/controllers/integration/sipgate_controller.rb b/app/controllers/integration/sipgate_controller.rb index 10545e891..4359c16d5 100644 --- a/app/controllers/integration/sipgate_controller.rb +++ b/app/controllers/integration/sipgate_controller.rb @@ -93,6 +93,8 @@ class Integration::SipgateController < ApplicationController xml_error('Feature not configured, please contact your admin!') return end + + true end def config_integration diff --git a/app/controllers/long_polling_controller.rb b/app/controllers/long_polling_controller.rb index 1bc6b1937..5ac74de82 100644 --- a/app/controllers/long_polling_controller.rb +++ b/app/controllers/long_polling_controller.rb @@ -18,7 +18,7 @@ class LongPollingController < ApplicationController params['data'] = {} end session_data = {} - if current_user && current_user.id + if current_user&.id session_data = { 'id' => current_user.id } end @@ -61,13 +61,12 @@ class LongPollingController < ApplicationController # check queue to send begin - # update last ping 4.times do sleep 0.25 end #sleep 1 - Sessions.touch(client_id) + Sessions.touch(client_id) # rubocop:disable Rails/SkipsModelValidations # set max loop time to 24 sec. because of 30 sec. timeout of mod_proxy count = 3 diff --git a/app/controllers/monitoring_controller.rb b/app/controllers/monitoring_controller.rb index 22f8f319f..2d30de077 100644 --- a/app/controllers/monitoring_controller.rb +++ b/app/controllers/monitoring_controller.rb @@ -1,7 +1,7 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class MonitoringController < ApplicationController - prepend_before_action -> { authentication_check(permission: 'admin.monitoring') }, except: [:health_check, :status] + prepend_before_action -> { authentication_check(permission: 'admin.monitoring') }, except: %i[health_check status] skip_before_action :verify_csrf_token =begin @@ -39,7 +39,7 @@ curl http://localhost/api/v1/monitoring/health_check?token=XXX # inbound channel if channel.status_in == 'error' message = "Channel: #{channel.area} in " - %w(host user uid).each do |key| + %w[host user uid].each do |key| next if channel.options[key].blank? message += "key:#{channel.options[key]};" end @@ -52,7 +52,7 @@ curl http://localhost/api/v1/monitoring/health_check?token=XXX # outbound channel next if channel.status_out != 'error' message = "Channel: #{channel.area} out " - %w(host user uid).each do |key| + %w[host user uid].each do |key| next if channel.options[key].blank? message += "key:#{channel.options[key]};" end @@ -60,7 +60,7 @@ curl http://localhost/api/v1/monitoring/health_check?token=XXX end # unprocessable mail check - directory = "#{Rails.root}/tmp/unprocessable_mail" + directory = Rails.root.join('tmp', 'unprocessable_mail').to_s if File.exist?(directory) count = 0 Dir.glob("#{directory}/*.eml") do |_entry| @@ -161,9 +161,7 @@ curl http://localhost/api/v1/monitoring/status?token=XXX map.each do |key, class_name| status[:counts][key] = class_name.count last = class_name.last - status[:last_created_at][key] = if last - last.created_at - end + status[:last_created_at][key] = last&.created_at end render json: status diff --git a/app/controllers/object_manager_attributes_controller.rb b/app/controllers/object_manager_attributes_controller.rb index 33e1ad304..15bace68d 100644 --- a/app/controllers/object_manager_attributes_controller.rb +++ b/app/controllers/object_manager_attributes_controller.rb @@ -98,20 +98,20 @@ class ObjectManagerAttributesController < ApplicationController private def check_params - if params[:data_type] =~ /^(boolean)$/ + if params[:data_type].match?(/^(boolean)$/) if params[:data_option][:options] + # rubocop:disable Lint/BooleanSymbol if params[:data_option][:options][:false] - params[:data_option][:options][false] = params[:data_option][:options][:false] - params[:data_option][:options].delete(:false) + params[:data_option][:options][false] = params[:data_option][:options].delete(:false) end if params[:data_option][:options][:true] - params[:data_option][:options][true] = params[:data_option][:options][:true] - params[:data_option][:options].delete(:true) + params[:data_option][:options][true] = params[:data_option][:options].delete(:true) end + # rubocop:enable Lint/BooleanSymbol end end if params[:data_option] && !params[:data_option].key?(:default) - params[:data_option][:default] = if params[:data_type] =~ /^(input|select|tree_select)$/ + params[:data_option][:default] = if params[:data_type].match?(/^(input|select|tree_select)$/) '' end end diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index 5ba533af3..d42096466 100644 --- a/app/controllers/organizations_controller.rb +++ b/app/controllers/organizations_controller.rb @@ -237,12 +237,16 @@ curl http://localhost/api/v1/organization/{id} -v -u #{login}:#{password} -H "Co params[:limit].to_i = 500 end + query = params[:query] + if query.respond_to?(:permit!) + query = query.permit!.to_h + end query_params = { - query: params[:query], + query: query, limit: params[:limit], current_user: current_user, } - if params[:role_ids] && !params[:role_ids].empty? + if params[:role_ids].present? query_params[:role_ids] = params[:role_ids] end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index af280d34a..1ec9c76f8 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -16,6 +16,9 @@ class SearchController < ApplicationController # get params query = params[:query] + if query.respond_to?(:permit!) + query = query.permit!.to_h + end limit = params[:limit] || 10 # convert objects string into array of class names diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index fbcc7ad05..d65a9711c 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,8 +1,8 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class SessionsController < ApplicationController - prepend_before_action :authentication_check, only: [:switch_to_user, :list, :delete] - skip_before_action :verify_csrf_token, only: [:create, :show, :destroy, :create_omniauth, :create_sso] + prepend_before_action :authentication_check, only: %i[switch_to_user list delete] + skip_before_action :verify_csrf_token, only: %i[create show destroy create_omniauth create_sso] # "Create" a login, aka "log the user in" def create diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index d9288d6c0..6d332edb0 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -93,11 +93,11 @@ class SettingsController < ApplicationController def keep_certain_attributes setting = Setting.find(params[:id]) - [:name, :area, :state_initial, :frontend, :options].each do |key| + %i[name area state_initial frontend options].each do |key| params.delete(key) end - if !params[:preferences].empty? - [:online_service_disable, :permission, :render].each do |key| + if params[:preferences].present? + %i[online_service_disable permission render].each do |key| params[:preferences].delete(key) end params[:preferences].merge!(setting.preferences) diff --git a/app/controllers/ticket_articles_controller.rb b/app/controllers/ticket_articles_controller.rb index bb10603da..6585a43e0 100644 --- a/app/controllers/ticket_articles_controller.rb +++ b/app/controllers/ticket_articles_controller.rb @@ -266,7 +266,7 @@ class TicketArticlesController < ApplicationController def sanitized_disposition disposition = params.fetch(:disposition, 'inline') - valid_disposition = %w(inline attachment) + valid_disposition = %w[inline attachment] return disposition if valid_disposition.include?(disposition) raise Exceptions::NotAuthorized, "Invalid disposition #{disposition} requested. Only #{valid_disposition.join(', ')} are valid." end diff --git a/app/controllers/tickets_controller.rb b/app/controllers/tickets_controller.rb index 5f289bb01..dbf33b33f 100644 --- a/app/controllers/tickets_controller.rb +++ b/app/controllers/tickets_controller.rb @@ -80,7 +80,7 @@ class TicketsController < ApplicationController # overwrite params if !current_user.permissions?('ticket.agent') - [:owner, :owner_id, :customer, :customer_id, :organization, :organization_id, :preferences].each do |key| + %i[owner owner_id customer customer_id organization organization_id preferences].each do |key| clean_params.delete(key) end clean_params[:customer_id] = current_user.id @@ -186,7 +186,7 @@ class TicketsController < ApplicationController # overwrite params if !current_user.permissions?('ticket.agent') - [:owner, :owner_id, :customer, :customer_id, :organization, :organization_id, :preferences].each do |key| + %i[owner owner_id customer customer_id organization organization_id preferences].each do |key| clean_params.delete(key) end end @@ -270,7 +270,7 @@ class TicketsController < ApplicationController .limit(6) # if we do not have open related tickets, search for any tickets - if ticket_lists.empty? + if ticket_lists.blank? ticket_lists = Ticket .where( customer_id: ticket.customer_id, @@ -389,11 +389,16 @@ class TicketsController < ApplicationController params[:limit].to_i = 100 end + query = params[:query] + if query.respond_to?(:permit!) + query = query.permit!.to_h + end + # build result list tickets = Ticket.search( + query: query, + condition: params[:condition].to_h, limit: params[:limit], - query: params[:query], - condition: params[:condition], current_user: current_user, ) @@ -435,11 +440,9 @@ class TicketsController < ApplicationController assets = {} ticket_ids = [] - if tickets - tickets.each do |ticket| - ticket_ids.push ticket.id - assets = ticket.assets(assets) - end + tickets&.each do |ticket| + ticket_ids.push ticket.id + assets = ticket.assets(assets) end # return result @@ -504,7 +507,7 @@ class TicketsController < ApplicationController # lookup open org tickets org_tickets = {} - if params[:organization_id] && !params[:organization_id].empty? + if params[:organization_id].present? organization = Organization.lookup(id: params[:organization_id]) if !organization raise "No such organization with id #{params[:organization_id]}" diff --git a/app/controllers/time_accountings_controller.rb b/app/controllers/time_accountings_controller.rb index 2c93797f6..ec353dd22 100644 --- a/app/controllers/time_accountings_controller.rb +++ b/app/controllers/time_accountings_controller.rb @@ -164,7 +164,7 @@ class TimeAccountingsController < ApplicationController ] result = [] results.each do |row| - row[:ticket].keys.each do |field| + row[:ticket].each_key do |field| next if row[:ticket][field].blank? next if !row[:ticket][field].is_a?(ActiveSupport::TimeWithZone) @@ -250,7 +250,7 @@ class TimeAccountingsController < ApplicationController customers[ticket.customer_id][:time_unit] += local_time_unit[:time_unit] end results = [] - customers.each do |_customer_id, content| + customers.each_value do |content| results.push content end @@ -326,7 +326,7 @@ class TimeAccountingsController < ApplicationController organizations[ticket.organization_id][:time_unit] += local_time_unit[:time_unit] end results = [] - organizations.each do |_customer_id, content| + organizations.each_value do |content| results.push content end diff --git a/app/controllers/user_access_token_controller.rb b/app/controllers/user_access_token_controller.rb index 8b1f5aeec..f00227db7 100644 --- a/app/controllers/user_access_token_controller.rb +++ b/app/controllers/user_access_token_controller.rb @@ -14,7 +14,7 @@ class UserAccessTokenController < ApplicationController end local_permissions = current_user.permissions local_permissions_new = {} - local_permissions.each do |key, _value| + local_permissions.each_key do |key| keys = Object.const_get('Permission').with_parents(key) keys.each do |local_key| next if local_permissions_new.key?([local_key]) diff --git a/app/controllers/user_devices_controller.rb b/app/controllers/user_devices_controller.rb index b419b336c..4b9efccc3 100644 --- a/app/controllers/user_devices_controller.rb +++ b/app/controllers/user_devices_controller.rb @@ -8,7 +8,7 @@ class UserDevicesController < ApplicationController devices_full = [] devices.each do |device| attributes = device.attributes - if device.location_details['city_name'] && !device.location_details['city_name'].empty? + if device.location_details['city_name'].present? attributes['location'] += ", #{device.location_details['city_name']}" end attributes.delete('created_at') diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index bad343c3e..c356a51ce 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,7 +1,7 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class UsersController < ApplicationController - prepend_before_action :authentication_check, except: [:create, :password_reset_send, :password_reset_verify, :image] + prepend_before_action :authentication_check, except: %i[create password_reset_send password_reset_verify image] prepend_before_action :authentication_check_only, only: [:create] # @path [GET] /users @@ -145,7 +145,7 @@ class UsersController < ApplicationController group_ids = [] role_ids = [] if count <= 2 - Role.where(name: %w(Admin Agent)).each do |role| + Role.where(name: %w[Admin Agent]).each do |role| role_ids.push role.id end Group.all().each do |group| @@ -363,12 +363,17 @@ class UsersController < ApplicationController params[:limit].to_i = 500 end + query = params[:query] + if query.respond_to?(:permit!) + query = query.permit!.to_h + end + query_params = { - query: params[:query], + query: query, limit: params[:limit], current_user: current_user, } - [:role_ids, :permissions].each do |key| + %i[role_ids permissions].each do |key| next if params[key].blank? query_params[key] = params[key] end @@ -1046,7 +1051,7 @@ curl http://localhost/api/v1/users/avatar -v -u #{login}:#{password} -H "Content def permission_check_by_permission(params) return true if current_user.permissions?('admin.user') - %i(role_ids roles).each do |key| + %i[role_ids roles].each do |key| next if !params[key] if current_user.permissions?('ticket.agent') params.delete(key) @@ -1059,7 +1064,7 @@ curl http://localhost/api/v1/users/avatar -v -u #{login}:#{password} -H "Content params[:role_ids] = Role.signup_role_ids end - %i(group_ids groups).each do |key| + %i[group_ids groups].each do |key| next if !params[key] if current_user.permissions?('ticket.agent') params.delete(key) diff --git a/app/models/activity_stream.rb b/app/models/activity_stream.rb index 957e7ff2a..882f36126 100644 --- a/app/models/activity_stream.rb +++ b/app/models/activity_stream.rb @@ -99,7 +99,7 @@ return all activity entries of an user permission_ids = user.permissions_with_child_ids group_ids = user.group_ids_access('read') - stream = if group_ids.empty? + stream = if group_ids.blank? ActivityStream.where('(permission_id IN (?) AND group_id is NULL)', permission_ids) .order('created_at DESC, id DESC') .limit(limit) diff --git a/app/models/application_model/can_assets.rb b/app/models/application_model/can_assets.rb index c66c563c7..802e6afd4 100644 --- a/app/models/application_model/can_assets.rb +++ b/app/models/application_model/can_assets.rb @@ -33,7 +33,7 @@ returns return data if !self['created_by_id'] && !self['updated_by_id'] app_model_user = User.to_app_model - %w(created_by_id updated_by_id).each do |local_user_id| + %w[created_by_id updated_by_id].each do |local_user_id| next if !self[ local_user_id ] next if data[ app_model_user ] && data[ app_model_user ][ self[ local_user_id ] ] user = User.lookup(id: self[ local_user_id ]) diff --git a/app/models/application_model/can_associations.rb b/app/models/application_model/can_associations.rb index 058087300..2a625f19e 100644 --- a/app/models/application_model/can_associations.rb +++ b/app/models/application_model/can_associations.rb @@ -109,7 +109,7 @@ returns return cache if cache attributes = self.attributes - relevant = %i(has_and_belongs_to_many has_many) + relevant = %i[has_and_belongs_to_many has_many] eager_load = [] pluck = [] keys = [] @@ -180,7 +180,7 @@ returns next if !item[:name] attributes[assoc.name.to_s].push item[:name] end - if ref.count.positive? && attributes[assoc.name.to_s].empty? + if ref.count.positive? && attributes[assoc.name.to_s].blank? attributes.delete(assoc.name.to_s) end next @@ -216,7 +216,7 @@ returns def filter_attributes(attributes) # remove forbitten attributes - %w(password token tokens token_ids).each do |item| + %w[password token tokens token_ids].each do |item| attributes.delete(item) end end @@ -237,7 +237,7 @@ returns def association_id_validation(attribute_id, value) return true if value.nil? - attributes.each do |key, _value| + attributes.each_key do |key| next if key != attribute_id # check if id is assigned @@ -339,16 +339,15 @@ returns class_object = assoc.klass lookup = nil if class_object == User - if value.instance_of?(String) - if !lookup - lookup = class_object.lookup(login: value) - end - if !lookup - lookup = class_object.lookup(email: value) - end - else + if !value.instance_of?(String) raise ArgumentError, "String is needed as ref value #{value.inspect} for '#{assoc_name}'" end + if !lookup + lookup = class_object.lookup(login: value) + end + if !lookup + lookup = class_object.lookup(email: value) + end else lookup = class_object.lookup(name: value) end @@ -367,7 +366,7 @@ returns end next if !value.instance_of?(Array) - next if value.empty? + next if value.blank? next if !value[0].instance_of?(String) # handle _ids values @@ -383,16 +382,15 @@ returns value.each do |item| lookup = nil if class_object == User - if item.instance_of?(String) - if !lookup - lookup = class_object.lookup(login: item) - end - if !lookup - lookup = class_object.lookup(email: item) - end - else + if !item.instance_of?(String) raise ArgumentError, "String is needed in array ref as ref value #{value.inspect} for '#{assoc_name}'" end + if !lookup + lookup = class_object.lookup(login: item) + end + if !lookup + lookup = class_object.lookup(email: item) + end else lookup = class_object.lookup(name: item) end diff --git a/app/models/application_model/can_cleanup_param.rb b/app/models/application_model/can_cleanup_param.rb index d6bc032e0..46b3f7999 100644 --- a/app/models/application_model/can_cleanup_param.rb +++ b/app/models/application_model/can_cleanup_param.rb @@ -43,7 +43,7 @@ returns # only use object attributes clean_params = {} - new.attributes.each do |attribute, _value| + new.attributes.each_key do |attribute| next if !data.key?(attribute.to_sym) # check reference records, referenced by _id attributes @@ -80,7 +80,7 @@ returns def filter_unused_params(data) # we do want to set this via database - [:action, :controller, :updated_at, :created_at, :updated_by_id, :created_by_id, :updated_by, :created_by].each do |key| + %i[action controller updated_at created_at updated_by_id created_by_id updated_by created_by].each do |key| data.delete(key) end diff --git a/app/models/application_model/can_creates_and_updates.rb b/app/models/application_model/can_creates_and_updates.rb index d31417821..34e7dd041 100644 --- a/app/models/application_model/can_creates_and_updates.rb +++ b/app/models/application_model/can_creates_and_updates.rb @@ -123,8 +123,8 @@ returns return record end record = new(data) - record.save - return record + record.save! + record elsif data[:name] # do lookup with == to handle case insensitive databases @@ -140,8 +140,8 @@ returns end end record = new(data) - record.save - return record + record.save! + record elsif data[:login] # do lookup with == to handle case insensitive databases @@ -157,8 +157,8 @@ returns end end record = new(data) - record.save - return record + record.save! + record elsif data[:email] # do lookup with == to handle case insensitive databases @@ -174,8 +174,8 @@ returns end end record = new(data) - record.save - return record + record.save! + record elsif data[:locale] # do lookup with == to handle case insensitive databases @@ -191,8 +191,8 @@ returns end end record = new(data) - record.save - return record + record.save! + record else raise ArgumentError, 'Need name, login, email or locale for create_or_update()' end diff --git a/app/models/application_model/can_touch_references.rb b/app/models/application_model/can_touch_references.rb index b73eeac6b..23eba980a 100644 --- a/app/models/application_model/can_touch_references.rb +++ b/app/models/application_model/can_touch_references.rb @@ -21,7 +21,7 @@ touch references by params object_class = Kernel.const_get(data[:object]) object = object_class.lookup(id: data[:o_id]) return if !object - object.touch + object.touch # rubocop:disable Rails/SkipsModelValidations rescue => e logger.error e end diff --git a/app/models/application_model/has_attachments.rb b/app/models/application_model/has_attachments.rb index c3ca0da15..eea4f1540 100644 --- a/app/models/application_model/has_attachments.rb +++ b/app/models/application_model/has_attachments.rb @@ -37,7 +37,7 @@ store attachments for this object self.attachments_buffer = attachments # update if object already exists - return if !(id && id.nonzero?) + return if !(id&.nonzero?) attachments_buffer_check end diff --git a/app/models/authorization.rb b/app/models/authorization.rb index ab0a2acdd..8e61d99b9 100644 --- a/app/models/authorization.rb +++ b/app/models/authorization.rb @@ -90,7 +90,7 @@ class Authorization < ApplicationModel def delete_user_cache return if !user - user.touch + user.touch # rubocop:disable Rails/SkipsModelValidations end end diff --git a/app/models/avatar.rb b/app/models/avatar.rb index 76456b1a7..3bc1a9984 100644 --- a/app/models/avatar.rb +++ b/app/models/avatar.rb @@ -105,16 +105,16 @@ add avatar by url # fetch image based on http url if data[:url].present? - if data[:url] =~ /^http/ + if data[:url].match?(/^http/) # check if source ist already updated within last 2 minutes - if avatar_already_exists && avatar_already_exists.source_url == data[:url] + if avatar_already_exists&.source_url == data[:url] return if avatar_already_exists.updated_at > 2.minutes.ago end # twitter workaround to get bigger avatar images # see also https://dev.twitter.com/overview/general/user-profile-images-and-banners - if data[:url] =~ %r{//pbs.twimg.com/}i + if data[:url].match?(%r{//pbs.twimg.com/}i) data[:url].sub!(/normal\.(png|jpg|gif)$/, 'bigger.\1') end @@ -134,10 +134,10 @@ add avatar by url end logger.info "Fetchd image '#{data[:url]}', http code: #{response.code}" mime_type = 'image' - if data[:url] =~ /\.png/i + if data[:url].match?(/\.png/i) mime_type = 'image/png' end - if data[:url] =~ /\.(jpg|jpeg)/i + if data[:url].match?(/\.(jpg|jpeg)/i) mime_type = 'image/jpeg' end if !data[:resize] @@ -150,10 +150,10 @@ add avatar by url data[:full][:mime_type] = mime_type # try zammad backend to find image based on email - elsif data[:url] =~ /@/ + elsif data[:url].match?(/@/) # check if source ist already updated within last 3 minutes - if avatar_already_exists && avatar_already_exists.source_url == data[:url] + if avatar_already_exists&.source_url == data[:url] return if avatar_already_exists.updated_at > 2.minutes.ago end @@ -170,8 +170,8 @@ add avatar by url # check if avatar need to be updated if data[:resize].present? && data[:resize][:content].present? record[:store_hash] = Digest::MD5.hexdigest(data[:resize][:content]) - if avatar_already_exists && avatar_already_exists.store_hash == record[:store_hash] - avatar_already_exists.touch + if avatar_already_exists&.store_hash == record[:store_hash] + avatar_already_exists.touch # rubocop:disable Rails/SkipsModelValidations return avatar_already_exists end end diff --git a/app/models/calendar.rb b/app/models/calendar.rb index 93a23fea1..9cd2a670a 100644 --- a/app/models/calendar.rb +++ b/app/models/calendar.rb @@ -83,11 +83,11 @@ returns =end def self.ical_feeds - data = YAML.load_file(Rails.root.join('config/holiday_calendars.yml')) + data = YAML.load_file(Rails.root.join('config', 'holiday_calendars.yml')) url = data['url'] data['countries'].map do |country, domain| - [(url % { domain: domain }), country] + [format(url, domain: domain), country] end.to_h end @@ -210,7 +210,7 @@ returns end def self.fetch_parse(location) - if location =~ /^http/i + if location.match?(/^http/i) result = UserAgent.get(location) if !result.success? raise result.error @@ -257,7 +257,7 @@ returns end # ignore daylight saving time entries - return if comment =~ /(daylight saving|sommerzeit|summertime)/i + return if comment.match?(/(daylight saving|sommerzeit|summertime)/i) [day, comment] end diff --git a/app/models/channel.rb b/app/models/channel.rb index d3c8a63c4..faf8752f4 100644 --- a/app/models/channel.rb +++ b/app/models/channel.rb @@ -50,7 +50,6 @@ fetch one account end begin - # we need to require each channel backend individually otherwise we get a # 'warning: toplevel constant Twitter referenced by Channel::Driver::Twitter' error e.g. # so we have to convert the channel name to the filename via Rails String.underscore @@ -94,7 +93,6 @@ stream instance of account adapter = options[:adapter] begin - # we need to require each channel backend individually otherwise we get a # 'warning: toplevel constant Twitter referenced by Channel::Driver::Twitter' error e.g. # so we have to convert the channel name to the filename via Rails String.underscore @@ -264,7 +262,6 @@ send via account result = nil begin - # we need to require each channel backend individually otherwise we get a # 'warning: toplevel constant Twitter referenced by Channel::Driver::Twitter' error e.g. # so we have to convert the channel name to the filename via Rails String.underscore diff --git a/app/models/channel/assets.rb b/app/models/channel/assets.rb index 46c044fbd..f6f7af0b3 100644 --- a/app/models/channel/assets.rb +++ b/app/models/channel/assets.rb @@ -40,7 +40,7 @@ returns end end if !access - %w(inbound outbound).each do |key| + %w[inbound outbound].each do |key| if attributes['options'] && attributes['options'][key] && attributes['options'][key]['options'] attributes['options'][key]['options'].delete('password') end @@ -51,7 +51,7 @@ returns end return data if !self['created_by_id'] && !self['updated_by_id'] - %w(created_by_id updated_by_id).each do |local_user_id| + %w[created_by_id updated_by_id].each do |local_user_id| next if !self[ local_user_id ] next if data[ User.to_app_model ] && data[ User.to_app_model ][ self[ local_user_id ] ] user = User.lookup(id: self[ local_user_id ]) diff --git a/app/models/channel/driver/facebook.rb b/app/models/channel/driver/facebook.rb index 3d46638e6..75809da88 100644 --- a/app/models/channel/driver/facebook.rb +++ b/app/models/channel/driver/facebook.rb @@ -60,8 +60,7 @@ class Channel::Driver::Facebook true end - def disconnect - end + def disconnect; end =begin diff --git a/app/models/channel/driver/imap.rb b/app/models/channel/driver/imap.rb index 5a609f957..8986753ab 100644 --- a/app/models/channel/driver/imap.rb +++ b/app/models/channel/driver/imap.rb @@ -104,7 +104,7 @@ example # sort messages by date on server (if not supported), if not fetch messages via search (first in, first out) filter = ['ALL'] if keep_on_server && check_type != 'check' && check_type != 'verify' - filter = %w(NOT SEEN) + filter = %w[NOT SEEN] end begin message_ids = @imap.sort(['DATE'], filter, 'US-ASCII') @@ -254,7 +254,7 @@ returns # verify if message is already imported via same channel, if not, import it again ticket = article.ticket - if ticket && ticket.preferences && ticket.preferences[:channel_id].present? && channel.present? + if ticket&.preferences && ticket.preferences[:channel_id].present? && channel.present? return false if ticket.preferences[:channel_id] != channel[:id] end diff --git a/app/models/channel/driver/smtp.rb b/app/models/channel/driver/smtp.rb index d678a40f0..27beb797d 100644 --- a/app/models/channel/driver/smtp.rb +++ b/app/models/channel/driver/smtp.rb @@ -27,7 +27,7 @@ class Channel::Driver::Smtp return if Setting.get('import_mode') # set smtp defaults - if !options.key?(:port) || options[:port].empty? + if !options.key?(:port) || options[:port].blank? options[:port] = 25 end if !options.key?(:ssl) diff --git a/app/models/channel/driver/twitter.rb b/app/models/channel/driver/twitter.rb index 97d72734f..406f9a9f8 100644 --- a/app/models/channel/driver/twitter.rb +++ b/app/models/channel/driver/twitter.rb @@ -119,8 +119,8 @@ returns end def disconnect - @stream_client.disconnect if @stream_client - @rest_client.disconnect if @rest_client + @stream_client&.disconnect + @rest_client&.disconnect end =begin @@ -203,12 +203,11 @@ returns stream_start rescue Twitter::Error::Unauthorized => e Rails.logger.info "Unable to stream, try #{loop_count}, error #{e.inspect}" - if loop_count < 2 - Rails.logger.info "wait for #{sleep_on_unauthorized} sec. and try it again" - sleep sleep_on_unauthorized - else + if loop_count >= 2 raise "Unable to stream, try #{loop_count}, error #{e.inspect}" end + Rails.logger.info "wait for #{sleep_on_unauthorized} sec. and try it again" + sleep sleep_on_unauthorized end end end @@ -233,7 +232,7 @@ returns filter[:replies] = 'all' end - return if filter.empty? + return if filter.blank? @stream_client.client.user(filter) do |tweet| next if tweet.class != Twitter::Tweet && tweet.class != Twitter::DirectMessage @@ -258,11 +257,9 @@ returns # check if it's mention if sync['mentions'] && sync['mentions']['group_id'].present? hit = false - if tweet.user_mentions - tweet.user_mentions.each do |user| - if user.id.to_s == @channel.options['user']['id'].to_s - hit = true - end + tweet.user_mentions&.each do |user| + if user.id.to_s == @channel.options['user']['id'].to_s + hit = true end end if hit @@ -299,7 +296,7 @@ returns next if item['term'].blank? next if item['term'] == '#' next if item['group_id'].blank? - if body =~ /#{item['term']}/ + if body.match?(/#{item['term']}/) hit = item end end diff --git a/app/models/channel/email_build.rb b/app/models/channel/email_build.rb index b0692648f..ae8f12959 100644 --- a/app/models/channel/email_build.rb +++ b/app/models/channel/email_build.rb @@ -69,7 +69,7 @@ module Channel::EmailBuild end # build email without any attachments - if !html_alternative && ( !attr[:attachments] || attr[:attachments].empty? ) + if !html_alternative && attr[:attachments].blank? mail.content_type 'text/plain; charset=UTF-8' mail.body attr[:body] return mail @@ -84,19 +84,17 @@ module Channel::EmailBuild html_container.add_part html_alternative # place to add inline attachments related to html alternative - if attr[:attachments] - attr[:attachments].each do |attachment| - next if attachment.class == Hash - next if attachment.preferences['Content-ID'].empty? - attachment = Mail::Part.new do - content_type attachment.preferences['Content-Type'] - content_id "<#{attachment.preferences['Content-ID']}>" - content_disposition attachment.preferences['Content-Disposition'] || 'inline' - content_transfer_encoding 'binary' - body attachment.content.force_encoding('BINARY') - end - html_container.add_part attachment + attr[:attachments]&.each do |attachment| + next if attachment.class == Hash + next if attachment.preferences['Content-ID'].blank? + attachment = Mail::Part.new do + content_type attachment.preferences['Content-Type'] + content_id "<#{attachment.preferences['Content-ID']}>" + content_disposition attachment.preferences['Content-Disposition'] || 'inline' + content_transfer_encoding 'binary' + body attachment.content.force_encoding('BINARY') end + html_container.add_part attachment end alternative_bodies.add_part html_container end @@ -104,23 +102,21 @@ module Channel::EmailBuild mail.add_part alternative_bodies # add attachments - if attr[:attachments] - attr[:attachments].each do |attachment| - if attachment.class == Hash - attachment['content-id'] = nil - mail.attachments[ attachment[:filename] ] = attachment - else - next if !attachment.preferences['Content-ID'].empty? - 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' - mail.attachments[attachment.filename] = { - content_disposition: "#{disposition}; filename=\"#{encoded_filename}\"", - content_type: "#{content_type}; filename=\"#{encoded_filename}\"", - content: attachment.content - } - end + attr[:attachments]&.each do |attachment| + if attachment.class == Hash + attachment['content-id'] = nil + 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' + mail.attachments[attachment.filename] = { + content_disposition: "#{disposition}; filename=\"#{encoded_filename}\"", + content_type: "#{content_type}; filename=\"#{encoded_filename}\"", + content: attachment.content + } end end mail @@ -137,7 +133,7 @@ returns =end def self.recipient_line(realname, email) - return "#{realname} <#{email}>" if realname =~ /^[A-z]+$/i + return "#{realname} <#{email}>" if realname.match?(/^[A-z]+$/i) "\"#{realname.gsub('"', '\"')}\" <#{email}>" end @@ -154,7 +150,7 @@ Check if string is a complete html document. If not, add head and css styles. # apply mail client fixes html = Channel::EmailBuild.html_mail_client_fixes(html) - return html if html =~ //i + return html if html.match?(//i) # use block form because variable html could contain backslashes and e. g. '\1' that # must not be handled as back-references for regular expressions diff --git a/app/models/channel/email_parser.rb b/app/models/channel/email_parser.rb index 9496572dd..b4092f593 100644 --- a/app/models/channel/email_parser.rb +++ b/app/models/channel/email_parser.rb @@ -94,7 +94,7 @@ class Channel::EmailParser # verify content, ignore recipients with non email address ['to', 'cc', 'delivered-to', 'x-original-to', 'envelope-to'].each do |field| next if data[field.to_sym].blank? - next if data[field.to_sym] =~ /@/ + next if data[field.to_sym].match?(/@/) data[field.to_sym] = '' end @@ -146,7 +146,7 @@ class Channel::EmailParser if mail.multipart? # html attachment/body may exists and will be converted to strict html - if mail.html_part && mail.html_part.body + if mail.html_part&.body data[:body] = mail.html_part.body.to_s data[:body] = Encode.conv(mail.html_part.charset.to_s, data[:body]) data[:body] = data[:body].html2html_strict.to_s.force_encoding('utf-8') @@ -196,17 +196,15 @@ class Channel::EmailParser end # get attachments - if mail.parts - mail.parts.each do |part| + mail.parts&.each do |part| - # protect process to work fine with spam emails, see test/fixtures/mail15.box - begin - attachs = _get_attachment(part, data[:attachments], mail) - data[:attachments].concat(attachs) - rescue - attachs = _get_attachment(part, data[:attachments], mail) - data[:attachments].concat(attachs) - end + # protect process to work fine with spam emails, see test/fixtures/mail15.box + begin + attachs = _get_attachment(part, data[:attachments], mail) + data[:attachments].concat(attachs) + rescue + attachs = _get_attachment(part, data[:attachments], mail) + data[:attachments].concat(attachs) end end @@ -306,10 +304,10 @@ class Channel::EmailParser end # ignore text/plain attachments - already shown in view - return [] if mail.text_part && mail.text_part.body.to_s == file.body.to_s + return [] if mail.text_part&.body.to_s == file.body.to_s # ignore text/html - html part, already shown in view - return [] if mail.html_part && mail.html_part.body.to_s == file.body.to_s + return [] if mail.html_part&.body.to_s == file.body.to_s # get file preferences headers_store = {} @@ -376,7 +374,7 @@ class Channel::EmailParser # generate file name based on content type if filename.blank? && headers_store['Content-Type'].present? - if headers_store['Content-Type'] =~ %r{^message/rfc822}i + if headers_store['Content-Type'].match?(%r{^message/rfc822}i) begin parser = Channel::EmailParser.new mail_local = parser.parse(file.body.to_s) @@ -406,13 +404,13 @@ class Channel::EmailParser if filename.blank? map = { 'message/delivery-status': ['txt', 'delivery-status'], - 'text/plain': %w(txt document), - 'text/html': %w(html document), - 'video/quicktime': %w(mov video), - 'image/jpeg': %w(jpg image), - 'image/jpg': %w(jpg image), - 'image/png': %w(png image), - 'image/gif': %w(gif image), + 'text/plain': %w[txt document], + 'text/html': %w[html document], + 'video/quicktime': %w[mov video], + 'image/jpeg': %w[jpg image], + 'image/jpg': %w[jpg image], + 'image/png': %w[png image], + 'image/gif': %w[gif image], } map.each do |type, ext| next if headers_store['Content-Type'] !~ /^#{Regexp.quote(type)}/i @@ -454,12 +452,12 @@ class Channel::EmailParser end # get mime type - if file.header[:content_type] && file.header[:content_type].string + if file.header[:content_type]&.string headers_store['Mime-Type'] = file.header[:content_type].string end # get charset - if file.header && file.header.charset + if file.header&.charset headers_store['Charset'] = file.header.charset end @@ -503,9 +501,8 @@ returns _process(channel, msg) rescue => e - # store unprocessable email for bug reporting - path = "#{Rails.root}/tmp/unprocessable_mail/" + path = Rails.root.join('tmp', 'unprocessable_mail') FileUtils.mkpath path md5 = Digest::MD5.hexdigest(msg) filename = "#{path}/#{md5}.eml" @@ -532,7 +529,7 @@ returns Setting.where(area: 'Postmaster::PreFilter').order(:name).each do |setting| filters[setting.name] = Kernel.const_get(Setting.get(setting.name)) end - filters.each do |_prio, backend| + filters.each_value do |backend| Rails.logger.debug "run postmaster pre filter #{backend}" begin backend.run(channel, mail) @@ -663,16 +660,14 @@ returns article.save_as_raw(msg) # store attachments - if mail[:attachments] - mail[:attachments].each do |attachment| - Store.add( - object: 'Ticket::Article', - o_id: article.id, - data: attachment[:data], - filename: attachment[:filename], - preferences: attachment[:preferences] - ) - end + mail[:attachments]&.each do |attachment| + Store.add( + object: 'Ticket::Article', + o_id: article.id, + data: attachment[:data], + filename: attachment[:filename], + preferences: attachment[:preferences] + ) end end end @@ -682,7 +677,7 @@ returns Setting.where(area: 'Postmaster::PostFilter').order(:name).each do |setting| filters[setting.name] = Kernel.const_get(Setting.get(setting.name)) end - filters.each do |_prio, backend| + filters.each_value do |backend| Rails.logger.debug "run postmaster post filter #{backend}" begin backend.run(channel, mail, ticket, article, session_user) @@ -765,7 +760,7 @@ returns def set_attributes_by_x_headers(item_object, header_name, mail, suffix = false) # loop all x-zammad-header-* headers - item_object.attributes.each do |key, _value| + item_object.attributes.each_key do |key| # ignore read only attributes next if key == 'updated_by_id' @@ -862,9 +857,9 @@ module Mail .+?(?=\=\?|$) # Plain String )/xmi).map do |matches| string, method = *matches - if method == 'b' || method == 'B' + if method == 'b' || method == 'B' # rubocop:disable Style/MultipleComparison b_value_decode(string) - elsif method == 'q' || method == 'Q' + elsif method == 'q' || method == 'Q' # rubocop:disable Style/MultipleComparison q_value_decode(string) else string diff --git a/app/models/channel/filter/auto_response_check.rb b/app/models/channel/filter/auto_response_check.rb index 588094802..ed7be0c86 100644 --- a/app/models/channel/filter/auto_response_check.rb +++ b/app/models/channel/filter/auto_response_check.rb @@ -25,7 +25,7 @@ module Channel::Filter::AutoResponseCheck message_id = mail[ 'message_id'.to_sym ] if message_id fqdn = Setting.get('fqdn') - return if message_id =~ /@#{Regexp.quote(fqdn)}/i + return if message_id.match?(/@#{Regexp.quote(fqdn)}/i) end mail[ 'x-zammad-send-auto-response'.to_sym ] = true diff --git a/app/models/channel/filter/bounce_delivery_permanent_failed.rb b/app/models/channel/filter/bounce_delivery_permanent_failed.rb index b7b535f90..7def43640 100644 --- a/app/models/channel/filter/bounce_delivery_permanent_failed.rb +++ b/app/models/channel/filter/bounce_delivery_permanent_failed.rb @@ -28,7 +28,7 @@ module Channel::Filter::BounceDeliveryPermanentFailed # get recipient of origin article, if only one - mark this user to not sent notifications anymore recipients = [] if article.sender.name == 'System' || article.sender.name == 'Agent' - %w(to cc).each do |line| + %w[to cc].each do |line| next if article[line].blank? recipients = [] begin diff --git a/app/models/channel/filter/follow_up_check.rb b/app/models/channel/filter/follow_up_check.rb index 179978187..739f69df1 100644 --- a/app/models/channel/filter/follow_up_check.rb +++ b/app/models/channel/filter/follow_up_check.rb @@ -104,5 +104,6 @@ module Channel::Filter::FollowUpCheck end end + true end end diff --git a/app/models/channel/filter/identify_sender.rb b/app/models/channel/filter/identify_sender.rb index 94cfd6139..1b67d1709 100644 --- a/app/models/channel/filter/identify_sender.rb +++ b/app/models/channel/filter/identify_sender.rb @@ -30,7 +30,7 @@ module Channel::Filter::IdentifySender # get first recipient and set customer begin to = 'raw-to'.to_sym - if mail[to] && mail[to].addrs + if mail[to]&.addrs items = mail[to].addrs items.each do |item| @@ -83,6 +83,8 @@ module Channel::Filter::IdentifySender if session_user mail[ 'x-zammad-session-user-id'.to_sym ] = session_user.id end + + true end # create to and cc user @@ -159,7 +161,7 @@ module Channel::Filter::IdentifySender role_ids = Role.signup_role_ids # fillup - %w(firstname lastname).each do |item| + %w[firstname lastname].each do |item| if data[item.to_sym].nil? data[item.to_sym] = '' end diff --git a/app/models/channel/filter/match/email_regex.rb b/app/models/channel/filter/match/email_regex.rb index 185f48be0..cf5dead02 100644 --- a/app/models/channel/filter/match/email_regex.rb +++ b/app/models/channel/filter/match/email_regex.rb @@ -9,12 +9,12 @@ module Channel::Filter::Match::EmailRegex if regexp == false match_rule_quoted = Regexp.quote(match_rule).gsub(/\\\*/, '.*') - return true if value =~ /#{match_rule_quoted}/i + return true if value.match?(/#{match_rule_quoted}/i) return false end begin - return true if value =~ /#{match_rule}/i + return true if value.match?(/#{match_rule}/i) return false rescue => e message = "Can't use regex '#{match_rule}' on '#{value}': #{e.message}" diff --git a/app/models/channel/filter/monitoring_base.rb b/app/models/channel/filter/monitoring_base.rb index 9ab171bc1..c7304fa8d 100644 --- a/app/models/channel/filter/monitoring_base.rb +++ b/app/models/channel/filter/monitoring_base.rb @@ -36,9 +36,7 @@ class Channel::Filter::MonitoringBase key = key.downcase end value = $2 - if value - value.strip! - end + value&.strip! result[key] = value end @@ -70,9 +68,9 @@ class Channel::Filter::MonitoringBase # possible event types https://mmonit.com/monit/documentation/#Setting-an-event-filter if result['state'].blank? - result['state'] = if mail[:body] =~ /\s(done|recovery|succeeded|bytes\sok|packets\sok)\s/ + result['state'] = if mail[:body].match?(/\s(done|recovery|succeeded|bytes\sok|packets\sok)\s/) 'OK' - elsif mail[:body] =~ /(instance\schanged\snot|Link\sup|Exists|Saturation\sok|Speed\sok)/ + elsif mail[:body].match?(/(instance\schanged\snot|Link\sup|Exists|Saturation\sok|Speed\sok)/) 'OK' else 'CRITICAL' @@ -132,5 +130,6 @@ class Channel::Filter::MonitoringBase return true end + true end end diff --git a/app/models/channel/filter/out_of_office_check.rb b/app/models/channel/filter/out_of_office_check.rb index 089ec785d..bed49ef9e 100644 --- a/app/models/channel/filter/out_of_office_check.rb +++ b/app/models/channel/filter/out_of_office_check.rb @@ -18,12 +18,12 @@ module Channel::Filter::OutOfOfficeCheck if mail[ 'auto-submitted'.to_sym ] # check zimbra out of office characteristics - if mail[ 'auto-submitted'.to_sym ] =~ /vacation/i + if mail[ 'auto-submitted'.to_sym ].match?(/vacation/i) mail[ 'x-zammad-out-of-office'.to_sym ] = true end # check cloud out of office characteristics - if mail[ 'auto-submitted'.to_sym ] =~ /auto-replied;\sowner-email=/i + if mail[ 'auto-submitted'.to_sym ].match?(/auto-replied;\sowner-email=/i) mail[ 'x-zammad-out-of-office'.to_sym ] = true end @@ -35,6 +35,7 @@ module Channel::Filter::OutOfOfficeCheck return end + true end end diff --git a/app/models/channel/filter/trusted.rb b/app/models/channel/filter/trusted.rb index 812c4ac68..e118dc3da 100644 --- a/app/models/channel/filter/trusted.rb +++ b/app/models/channel/filter/trusted.rb @@ -7,7 +7,7 @@ module Channel::Filter::Trusted # check if trust x-headers if !channel[:trusted] - mail.each do |key, _value| + mail.each_key do |key| next if key !~ /^x-zammad/i mail.delete(key) end diff --git a/app/models/chat.rb b/app/models/chat.rb index f1736329a..858cd76b4 100644 --- a/app/models/chat.rb +++ b/app/models/chat.rb @@ -9,7 +9,7 @@ class Chat < ApplicationModel # reconnect if session_id - chat_session = Chat::Session.find_by(session_id: session_id, state: %w(waiting running)) + chat_session = Chat::Session.find_by(session_id: session_id, state: %w[waiting running]) if chat_session if chat_session.state == 'running' @@ -126,7 +126,7 @@ class Chat < ApplicationModel end def self.active_chat_count - Chat::Session.where(state: %w(waiting running)).count + Chat::Session.where(state: %w[waiting running]).count end def self.available_agents(diff = 2.minutes) @@ -153,7 +153,7 @@ class Chat < ApplicationModel def self.seads_total(diff = 2.minutes) total = 0 - available_agents(diff).each do |_user_id, concurrent| + available_agents(diff).each_value do |concurrent| total += concurrent end total diff --git a/app/models/chat/agent.rb b/app/models/chat/agent.rb index 99ea9be48..fdcd1ff12 100644 --- a/app/models/chat/agent.rb +++ b/app/models/chat/agent.rb @@ -5,7 +5,7 @@ class Chat::Agent < ApplicationModel end def active_chat_count - Chat::Session.where(state: %w(waiting running), user_id: updated_by_id).count + Chat::Session.where(state: %w[waiting running], user_id: updated_by_id).count end def self.state(user_id, state = nil) diff --git a/app/models/concerns/can_seed.rb b/app/models/concerns/can_seed.rb index 89fd7d879..6b51f3e1a 100644 --- a/app/models/concerns/can_seed.rb +++ b/app/models/concerns/can_seed.rb @@ -17,7 +17,7 @@ module CanSeed end def seedfile - "#{Rails.root}/db/seeds/#{name.pluralize.underscore.tr('/', '_')}.rb" + Rails.root.join('db', 'seeds', "#{name.pluralize.underscore.tr('/', '_')}.rb").to_s end end end diff --git a/app/models/concerns/has_activity_stream_log.rb b/app/models/concerns/has_activity_stream_log.rb index a4a4cdee6..f07b872d0 100644 --- a/app/models/concerns/has_activity_stream_log.rb +++ b/app/models/concerns/has_activity_stream_log.rb @@ -35,12 +35,11 @@ log object update activity stream, if configured - will be executed automaticall return true if !saved_changes? ignored_attributes = self.class.instance_variable_get(:@activity_stream_attributes_ignored) || [] - ignored_attributes += %i(created_at updated_at created_by_id updated_by_id) + ignored_attributes += %i[created_at updated_at created_by_id updated_by_id] log = false - saved_changes.each do |key, _value| + saved_changes.each_key do |key| next if ignored_attributes.include?(key.to_sym) - log = true end return true if !log diff --git a/app/models/concerns/has_history.rb b/app/models/concerns/has_history.rb index a351fe16b..543550aab 100644 --- a/app/models/concerns/has_history.rb +++ b/app/models/concerns/has_history.rb @@ -40,11 +40,9 @@ log object update history with all updated attributes, if configured - will be e # new record also triggers update, so ignore new records changes = saved_changes - if history_changes_last_done - history_changes_last_done.each do |key, value| - if changes.key?(key) && changes[key] == value - changes.delete(key) - end + history_changes_last_done&.each do |key, value| + if changes.key?(key) && changes[key] == value + changes.delete(key) end end self.history_changes_last_done = changes @@ -53,7 +51,7 @@ log object update history with all updated attributes, if configured - will be e return if changes['id'] && !changes['id'][0] ignored_attributes = self.class.instance_variable_get(:@history_attributes_ignored) || [] - ignored_attributes += %i(created_at updated_at created_by_id updated_by_id) + ignored_attributes += %i[created_at updated_at created_by_id updated_by_id] changes.each do |key, value| diff --git a/app/models/concerns/has_search_index_backend.rb b/app/models/concerns/has_search_index_backend.rb index 7e4bb998b..283928e9b 100644 --- a/app/models/concerns/has_search_index_backend.rb +++ b/app/models/concerns/has_search_index_backend.rb @@ -84,7 +84,7 @@ returns def search_index_data attributes = {} - %w(name note).each do |key| + %w[name note].each do |key| next if !self[key] next if self[key].respond_to?('blank?') && self[key].blank? attributes[key] = self[key] diff --git a/app/models/cti/caller_id.rb b/app/models/cti/caller_id.rb index ad8930044..caf7beea6 100644 --- a/app/models/cti/caller_id.rb +++ b/app/models/cti/caller_id.rb @@ -108,11 +108,11 @@ returns # get caller ids caller_ids = [] attributes = record.attributes - attributes.each do |_attribute, value| + attributes.each_value do |value| next if value.class != String - next if value.empty? + next if value.blank? local_caller_ids = Cti::CallerId.extract_numbers(value) - next if local_caller_ids.empty? + next if local_caller_ids.blank? caller_ids = caller_ids.concat(local_caller_ids) end @@ -233,23 +233,23 @@ returns if user comment += user.fullname end - elsif !record.comment.empty? + elsif record.comment.present? comment += record.comment end if record.level == 'known' - if !from_comment_known.empty? + if from_comment_known.present? from_comment_known += ',' end from_comment_known += comment else - if !from_comment_maybe.empty? + if from_comment_maybe.present? from_comment_maybe += ',' end from_comment_maybe += comment end end - return [from_comment_known, preferences_known] if !from_comment_known.empty? - return ["maybe #{from_comment_maybe}", preferences_maybe] if !from_comment_maybe.empty? + return [from_comment_known, preferences_known] if from_comment_known.present? + return ["maybe #{from_comment_maybe}", preferences_maybe] if from_comment_maybe.present? nil end diff --git a/app/models/cti/log.rb b/app/models/cti/log.rb index 4f03057b4..9b043207f 100644 --- a/app/models/cti/log.rb +++ b/app/models/cti/log.rb @@ -249,7 +249,7 @@ returns assets = {} list.each do |item| next if !item.preferences - %w(from to).each do |direction| + %w[from to].each do |direction| next if !item.preferences[direction] item.preferences[direction].each do |caller_id| next if !caller_id['user_id'] diff --git a/app/models/email_address.rb b/app/models/email_address.rb index dc2affea8..0441f4431 100644 --- a/app/models/email_address.rb +++ b/app/models/email_address.rb @@ -47,7 +47,7 @@ check and if channel not exists reset configured channels for email addresses return true if email.blank? self.email = email.downcase.strip raise Exceptions::UnprocessableEntity, 'Invalid email' if email !~ /@/ - raise Exceptions::UnprocessableEntity, 'Invalid email' if email =~ /\s/ + raise Exceptions::UnprocessableEntity, 'Invalid email' if email.match?(/\s/) true end diff --git a/app/models/external_sync.rb b/app/models/external_sync.rb index b43bbe5c5..1eff33857 100644 --- a/app/models/external_sync.rb +++ b/app/models/external_sync.rb @@ -68,7 +68,7 @@ class ExternalSync < ApplicationModel break if !value storable = value.class.ancestors.any? do |ancestor| - %w(String Integer Float Bool Array).include?(ancestor.to_s) + %w[String Integer Float Bool Array].include?(ancestor.to_s) end if storable diff --git a/app/models/job.rb b/app/models/job.rb index 9d5b422a1..027a1de35 100644 --- a/app/models/job.rb +++ b/app/models/job.rb @@ -78,11 +78,9 @@ job.run(true) self.running = true save! - if tickets - tickets.each do |ticket| - Transaction.execute(disable_notification: disable_notification, reset_user_id: true) do - ticket.perform_changes(perform, 'job') - end + tickets&.each do |ticket| + Transaction.execute(disable_notification: disable_notification, reset_user_id: true) do + ticket.perform_changes(perform, 'job') end end diff --git a/app/models/job/assets.rb b/app/models/job/assets.rb index fbfd4a15f..3e0c0a656 100644 --- a/app/models/job/assets.rb +++ b/app/models/job/assets.rb @@ -36,7 +36,7 @@ returns data = assets_of_selector('condition', data) data = assets_of_selector('perform', data) end - %w(created_by_id updated_by_id).each do |local_user_id| + %w[created_by_id updated_by_id].each do |local_user_id| next if !self[ local_user_id ] next if data[ User.to_app_model ][ self[ local_user_id ] ] user = User.lookup(id: self[ local_user_id ]) diff --git a/app/models/locale.rb b/app/models/locale.rb index fa597f189..e183cd904 100644 --- a/app/models/locale.rb +++ b/app/models/locale.rb @@ -71,7 +71,7 @@ all: def self.load_from_file version = Version.get - file = Rails.root.join("config/locales-#{version}.yml") + file = Rails.root.join('config', "locales-#{version}.yml") return false if !File.exist?(file) data = YAML.load_file(file) to_database(data) @@ -107,7 +107,7 @@ all: raise "Can't load locales from #{url}" if !result raise "Can't load locales from #{url}: #{result.error}" if !result.success? - file = Rails.root.join("config/locales-#{version}.yml") + file = Rails.root.join('config', "locales-#{version}.yml") File.open(file, 'w') do |out| YAML.dump(result.data, out) end diff --git a/app/models/object_manager.rb b/app/models/object_manager.rb index e6d1b5f5a..306122924 100644 --- a/app/models/object_manager.rb +++ b/app/models/object_manager.rb @@ -11,7 +11,7 @@ list all backend managed object =end def self.list_objects - %w(Ticket TicketArticle User Organization Group) + %w[Ticket TicketArticle User Organization Group] end =begin @@ -23,7 +23,7 @@ list all frontend managed object =end def self.list_frontend_objects - %w(Ticket User Organization Group) + %w[Ticket User Organization Group] end end diff --git a/app/models/object_manager/attribute.rb b/app/models/object_manager/attribute.rb index 97183e5c1..56ae893ed 100644 --- a/app/models/object_manager/attribute.rb +++ b/app/models/object_manager/attribute.rb @@ -279,7 +279,7 @@ possible types # if data_option has changed, store it for next migration if !force - [:name, :display, :data_type, :position, :active].each do |key| + %i[name display data_type position active].each do |key| next if record[key] == data[key] data[:to_config] = true break @@ -441,7 +441,7 @@ returns: tag: item.data_type, #:null => item.null, } - if item.data_option[:permission] && item.data_option[:permission].any? + if item.data_option[:permission]&.any? next if !user hint = false item.data_option[:permission].each do |permission| @@ -459,7 +459,7 @@ returns: permission_options.each do |permission, options| if permission == '-all-' data[:screen][screen] = options - elsif user && user.permissions?(permission) + elsif user&.permissions?(permission) data[:screen][screen] = options end end @@ -535,7 +535,7 @@ returns =end def self.pending_migration? - return false if migrations.empty? + return false if migrations.blank? true end @@ -601,21 +601,21 @@ to send no browser reload event, pass false end data_type = nil - if attribute.data_type =~ /^input|select|tree_select|richtext|textarea|checkbox$/ + if attribute.data_type.match?(/^input|select|tree_select|richtext|textarea|checkbox$/) data_type = :string - elsif attribute.data_type =~ /^integer|user_autocompletion$/ + elsif attribute.data_type.match?(/^integer|user_autocompletion$/) data_type = :integer - elsif attribute.data_type =~ /^boolean|active$/ + elsif attribute.data_type.match?(/^boolean|active$/) data_type = :boolean - elsif attribute.data_type =~ /^datetime$/ + elsif attribute.data_type.match?(/^datetime$/) data_type = :datetime - elsif attribute.data_type =~ /^date$/ + elsif attribute.data_type.match?(/^date$/) data_type = :date end # change field if model.column_names.include?(attribute.name) - if attribute.data_type =~ /^input|select|tree_select|richtext|textarea|checkbox$/ + if attribute.data_type.match?(/^input|select|tree_select|richtext|textarea|checkbox$/) ActiveRecord::Migration.change_column( model.table_name, attribute.name, @@ -623,7 +623,7 @@ to send no browser reload event, pass false limit: attribute.data_option[:maxlength], null: true ) - elsif attribute.data_type =~ /^integer|user_autocompletion|datetime|date$/ + elsif attribute.data_type.match?(/^integer|user_autocompletion|datetime|date$/) ActiveRecord::Migration.change_column( model.table_name, attribute.name, @@ -631,7 +631,7 @@ to send no browser reload event, pass false default: attribute.data_option[:default], null: true ) - elsif attribute.data_type =~ /^boolean|active$/ + elsif attribute.data_type.match?(/^boolean|active$/) ActiveRecord::Migration.change_column( model.table_name, attribute.name, @@ -654,7 +654,7 @@ to send no browser reload event, pass false end # create field - if attribute.data_type =~ /^input|select|tree_select|richtext|textarea|checkbox$/ + if attribute.data_type.match?(/^input|select|tree_select|richtext|textarea|checkbox$/) ActiveRecord::Migration.add_column( model.table_name, attribute.name, @@ -662,7 +662,7 @@ to send no browser reload event, pass false limit: attribute.data_option[:maxlength], null: true ) - elsif attribute.data_type =~ /^integer|user_autocompletion$/ + elsif attribute.data_type.match?(/^integer|user_autocompletion$/) ActiveRecord::Migration.add_column( model.table_name, attribute.name, @@ -670,7 +670,7 @@ to send no browser reload event, pass false default: attribute.data_option[:default], null: true ) - elsif attribute.data_type =~ /^boolean|active$/ + elsif attribute.data_type.match?(/^boolean|active$/) ActiveRecord::Migration.add_column( model.table_name, attribute.name, @@ -678,7 +678,7 @@ to send no browser reload event, pass false default: attribute.data_option[:default], null: true ) - elsif attribute.data_type =~ /^datetime|date$/ + elsif attribute.data_type.match?(/^datetime|date$/) ActiveRecord::Migration.add_column( model.table_name, attribute.name, @@ -727,26 +727,20 @@ to send no browser reload event, pass false def check_name return if !name - if name =~ /_(id|ids)$/i || name =~ /^id$/i - raise 'Name can\'t get used, *_id and *_ids are not allowed' - elsif name =~ /\s/ - raise 'Spaces in name are not allowed' - elsif name !~ /^[a-z0-9_]+$/ - raise 'Only letters from a-z, numbers from 0-9, and _ are allowed' - elsif name !~ /[a-z]/ - raise 'At least one letters is needed' - elsif name =~ /^(destroy|true|false|integer|select|drop|create|alter|index|table|varchar|blob|date|datetime|timestamp)$/ - raise "#{name} is a reserved word, please choose a different one" + + raise 'Name can\'t get used, *_id and *_ids are not allowed' if name.match?(/_(id|ids)$/i) || name.match?(/^id$/i) + raise 'Spaces in name are not allowed' if name.match?(/\s/) + raise 'Only letters from a-z, numbers from 0-9, and _ are allowed' if !name.match?(/^[a-z0-9_]+$/) + raise 'At least one letters is needed' if !name.match?(/[a-z]/) # do not allow model method names as attributes - else - model = Kernel.const_get(object_lookup.name) - record = model.new - if record.respond_to?(name.to_sym) && !record.attributes.key?(name) - raise "#{name} is a reserved word, please choose a different one" - end - end - true + reserved_words = %w[destroy true false integer select drop create alter index table varchar blob date datetime timestamp] + raise "#{name} is a reserved word, please choose a different one" if name.match?(/^(#{reserved_words.join('|')})$/) + + record = object_lookup.name.constantize.new + return true if !record.respond_to?(name.to_sym) + return true if record.attributes.key?(name) + raise "#{name} is a reserved word, please choose a different one" end def check_editable @@ -783,7 +777,7 @@ to send no browser reload event, pass false end if data_type == 'integer' - [:min, :max].each do |item| + %i[min max].each do |item| raise "Need data_option[#{item.inspect}] param" if !data_option[item] raise "Invalid data_option[#{item.inspect}] param #{data_option[item]}" if data_option[item].to_s !~ /^\d+?$/ end @@ -817,6 +811,7 @@ to send no browser reload event, pass false raise 'Need data_option[:diff] param in days' if data_option[:diff].nil? end + true end end diff --git a/app/models/observer/chat/leave/background_job.rb b/app/models/observer/chat/leave/background_job.rb index 1e9eb9a15..1a681eb38 100644 --- a/app/models/observer/chat/leave/background_job.rb +++ b/app/models/observer/chat/leave/background_job.rb @@ -1,5 +1,3 @@ -# encoding: utf-8 - class Observer::Chat::Leave::BackgroundJob def initialize(chat_session_id, client_id, session) @chat_session_id = chat_session_id diff --git a/app/models/observer/organization/ref_object_touch.rb b/app/models/observer/organization/ref_object_touch.rb index 3571ae50b..c5e934d29 100644 --- a/app/models/observer/organization/ref_object_touch.rb +++ b/app/models/observer/organization/ref_object_touch.rb @@ -27,7 +27,7 @@ class Observer::Organization::RefObjectTouch < ActiveRecord::Observer Ticket.select('id').where(organization_id: record.id).pluck(:id).each do |ticket_id| ticket = Ticket.find(ticket_id) ticket.with_lock do - ticket.touch + ticket.touch # rubocop:disable Rails/SkipsModelValidations end end @@ -35,7 +35,7 @@ class Observer::Organization::RefObjectTouch < ActiveRecord::Observer User.select('id').where(organization_id: record.id).pluck(:id).each do |user_id| user = User.find(user_id) user.with_lock do - user.touch + user.touch # rubocop:disable Rails/SkipsModelValidations end end true diff --git a/app/models/observer/sla/ticket_rebuild_escalation.rb b/app/models/observer/sla/ticket_rebuild_escalation.rb index 37503fc11..761dfd10e 100644 --- a/app/models/observer/sla/ticket_rebuild_escalation.rb +++ b/app/models/observer/sla/ticket_rebuild_escalation.rb @@ -33,9 +33,9 @@ class Observer::Sla::TicketRebuildEscalation < ActiveRecord::Observer changed = false fields_to_check = nil fields_to_check = if record.class == Sla - %w(condition calendar_id first_response_time update_time solution_time) + %w[condition calendar_id first_response_time update_time solution_time] else - %w(timezone business_hours default ical_url public_holidays) + %w[timezone business_hours default ical_url public_holidays] end fields_to_check.each do |item| next if !record.saved_change_to_attribute(item) diff --git a/app/models/observer/sla/ticket_rebuild_escalation/background_job.rb b/app/models/observer/sla/ticket_rebuild_escalation/background_job.rb index 763844571..002d7ab2d 100644 --- a/app/models/observer/sla/ticket_rebuild_escalation/background_job.rb +++ b/app/models/observer/sla/ticket_rebuild_escalation/background_job.rb @@ -1,6 +1,5 @@ class Observer::Sla::TicketRebuildEscalation::BackgroundJob - def initialize(_sla_id) - end + def initialize(_sla_id); end def perform Cache.delete('SLA::List::Active') diff --git a/app/models/observer/ticket/article/communicate_email.rb b/app/models/observer/ticket/article/communicate_email.rb index 1f159bb09..a369706d7 100644 --- a/app/models/observer/ticket/article/communicate_email.rb +++ b/app/models/observer/ticket/article/communicate_email.rb @@ -10,7 +10,7 @@ class Observer::Ticket::Article::CommunicateEmail < ActiveRecord::Observer # only do send email if article got created via application_server (e. g. not # if article and sender type is set via *.postmaster) - return if ApplicationHandleInfo.current.split('.')[1] == 'postmaster' + return if ApplicationHandleInfo.postmaster? # if sender is customer, do not communicate return if !record.sender_id diff --git a/app/models/observer/ticket/article/communicate_email/background_job.rb b/app/models/observer/ticket/article/communicate_email/background_job.rb index 15d4d6d97..87c4ceaef 100644 --- a/app/models/observer/ticket/article/communicate_email/background_job.rb +++ b/app/models/observer/ticket/article/communicate_email/background_job.rb @@ -90,7 +90,7 @@ class Observer::Ticket::Article::CommunicateEmail::BackgroundJob # add history record recipient_list = '' - [:to, :cc].each do |key| + %i[to cc].each do |key| next if !record[key] next if record[key] == '' @@ -130,7 +130,7 @@ class Observer::Ticket::Article::CommunicateEmail::BackgroundJob if local_record.preferences['delivery_retry'] > 3 recipient_list = '' - [:to, :cc].each do |key| + %i[to cc].each do |key| next if !local_record[key] next if local_record[key] == '' diff --git a/app/models/observer/ticket/article/communicate_facebook.rb b/app/models/observer/ticket/article/communicate_facebook.rb index d318c9079..0285f7de9 100644 --- a/app/models/observer/ticket/article/communicate_facebook.rb +++ b/app/models/observer/ticket/article/communicate_facebook.rb @@ -12,7 +12,7 @@ class Observer::Ticket::Article::CommunicateFacebook < ActiveRecord::Observer # only do send email if article got created via application_server (e. g. not # if article and sender type is set via *.postmaster) - return if ApplicationHandleInfo.current.split('.')[1] == 'postmaster' + return if ApplicationHandleInfo.postmaster? # if sender is customer, do not communicate return if !record.sender_id diff --git a/app/models/observer/ticket/article/communicate_twitter.rb b/app/models/observer/ticket/article/communicate_twitter.rb index 96340826a..b05048c4b 100644 --- a/app/models/observer/ticket/article/communicate_twitter.rb +++ b/app/models/observer/ticket/article/communicate_twitter.rb @@ -10,7 +10,7 @@ class Observer::Ticket::Article::CommunicateTwitter < ActiveRecord::Observer # only do send email if article got created via application_server (e. g. not # if article and sender type is set via *.postmaster) - return if ApplicationHandleInfo.current.split('.')[1] == 'postmaster' + return if ApplicationHandleInfo.postmaster? # if sender is customer, do not communicate return if !record.sender_id diff --git a/app/models/observer/ticket/article/fillup_from_email.rb b/app/models/observer/ticket/article/fillup_from_email.rb index 1fc206ecd..c44c7fcd9 100644 --- a/app/models/observer/ticket/article/fillup_from_email.rb +++ b/app/models/observer/ticket/article/fillup_from_email.rb @@ -10,7 +10,7 @@ class Observer::Ticket::Article::FillupFromEmail < ActiveRecord::Observer # only do fill of email from if article got created via application_server (e. g. not # if article and sender type is set via *.postmaster) - return true if ApplicationHandleInfo.current.split('.')[1] == 'postmaster' + return if ApplicationHandleInfo.postmaster? # if sender is customer, do not change anything return true if !record.sender_id diff --git a/app/models/observer/ticket/article/fillup_from_general.rb b/app/models/observer/ticket/article/fillup_from_general.rb index 7c6ce9166..c4fc8d960 100644 --- a/app/models/observer/ticket/article/fillup_from_general.rb +++ b/app/models/observer/ticket/article/fillup_from_general.rb @@ -10,7 +10,7 @@ class Observer::Ticket::Article::FillupFromGeneral < ActiveRecord::Observer # only do fill of from if article got created via application_server (e. g. not # if article and sender type is set via *.postmaster) - return true if ApplicationHandleInfo.current.split('.')[1] == 'postmaster' + return if ApplicationHandleInfo.postmaster? # set from on all article types excluding email|twitter status|twitter direct-message|facebook feed post|facebook feed comment return true if !record.type_id diff --git a/app/models/observer/ticket/article/fillup_from_origin_by_id.rb b/app/models/observer/ticket/article/fillup_from_origin_by_id.rb index fa77cf3d5..b636a9ae0 100644 --- a/app/models/observer/ticket/article/fillup_from_origin_by_id.rb +++ b/app/models/observer/ticket/article/fillup_from_origin_by_id.rb @@ -10,7 +10,7 @@ class Observer::Ticket::Article::FillupFromOriginById < ActiveRecord::Observer # only do fill of from if article got created via application_server (e. g. not # if article and sender type is set via *.postmaster) - return if ApplicationHandleInfo.current.split('.')[1] == 'postmaster' + return if ApplicationHandleInfo.postmaster? # check if origin_by_id exists return if record.origin_by_id.present? diff --git a/app/models/observer/ticket/article_changes.rb b/app/models/observer/ticket/article_changes.rb index 85e721803..a62ad9e88 100644 --- a/app/models/observer/ticket/article_changes.rb +++ b/app/models/observer/ticket/article_changes.rb @@ -24,7 +24,7 @@ class Observer::Ticket::ArticleChanges < ActiveRecord::Observer # save ticket if !changed - record.ticket.touch + record.ticket.touch # rubocop:disable Rails/SkipsModelValidations return end record.ticket.save @@ -38,7 +38,7 @@ class Observer::Ticket::ArticleChanges < ActiveRecord::Observer # save ticket if !changed - record.ticket.touch + record.ticket.touch # rubocop:disable Rails/SkipsModelValidations return end record.ticket.save! diff --git a/app/models/observer/ticket/ref_object_touch.rb b/app/models/observer/ticket/ref_object_touch.rb index 242de0ed9..85d529c57 100644 --- a/app/models/observer/ticket/ref_object_touch.rb +++ b/app/models/observer/ticket/ref_object_touch.rb @@ -24,26 +24,24 @@ class Observer::Ticket::RefObjectTouch < ActiveRecord::Observer cutomer_id_changed = record.saved_changes['customer_id'] if cutomer_id_changed && cutomer_id_changed[0] != cutomer_id_changed[1] if cutomer_id_changed[0] - User.find(cutomer_id_changed[0]).touch + User.find(cutomer_id_changed[0]).touch # rubocop:disable Rails/SkipsModelValidations end end # touch new/current customer - if record.customer - record.customer.touch - end + record.customer&.touch # touch old organization if changed organization_id_changed = record.saved_changes['organization_id'] if organization_id_changed && organization_id_changed[0] != organization_id_changed[1] if organization_id_changed[0] - Organization.find(organization_id_changed[0]).touch + Organization.find(organization_id_changed[0]).touch # rubocop:disable Rails/SkipsModelValidations end end # touch new/current organization return true if !record.organization - record.organization.touch + record.organization.touch # rubocop:disable Rails/SkipsModelValidations end end diff --git a/app/models/observer/ticket/reset_new_state.rb b/app/models/observer/ticket/reset_new_state.rb index c676b0391..05e20ada3 100644 --- a/app/models/observer/ticket/reset_new_state.rb +++ b/app/models/observer/ticket/reset_new_state.rb @@ -9,7 +9,7 @@ class Observer::Ticket::ResetNewState < ActiveRecord::Observer return if Setting.get('import_mode') # only change state if not processed via postmaster - return if ApplicationHandleInfo.current.split('.')[1] == 'postmaster' + return if ApplicationHandleInfo.postmaster? # if article in internal return true if record.internal diff --git a/app/models/observer/transaction.rb b/app/models/observer/transaction.rb index 15254620b..3dfe47690 100644 --- a/app/models/observer/transaction.rb +++ b/app/models/observer/transaction.rb @@ -33,14 +33,14 @@ class Observer::Transaction < ActiveRecord::Observer sync_backends = [] Setting.where(area: 'Transaction::Backend::Sync').order(:name).each do |setting| backend = Setting.get(setting.name) - next if params[:disable] && params[:disable].include?(backend) + next if params[:disable]&.include?(backend) sync_backends.push Kernel.const_get(backend) end # get uniq objects list_objects = get_uniq_changes(list) - list_objects.each do |_object, objects| - objects.each do |_id, item| + list_objects.each_value do |objects| + objects.each_value do |item| # execute sync backends sync_backends.each do |backend| @@ -215,7 +215,7 @@ class Observer::Transaction < ActiveRecord::Observer end # do not send anything if nothing has changed - return true if real_changes.empty? + return true if real_changes.blank? changed_by_id = nil changed_by_id = if record.respond_to?('updated_by_id') diff --git a/app/models/observer/user/geo.rb b/app/models/observer/user/geo.rb index c65c00923..8cb6b9a68 100644 --- a/app/models/observer/user/geo.rb +++ b/app/models/observer/user/geo.rb @@ -16,7 +16,7 @@ class Observer::User::Geo < ActiveRecord::Observer # check if geo need to be updated def check_geo(record) - location = %w(address street zip city country) + location = %w[address street zip city country] # check if geo update is needed based on old/new location if record.id @@ -45,7 +45,7 @@ class Observer::User::Geo < ActiveRecord::Observer # update geo data of user def geo_update(record) address = '' - location = %w(address street zip city country) + location = %w[address street zip city country] location.each do |item| next if record[item].blank? if address.present? diff --git a/app/models/observer/user/ref_object_touch.rb b/app/models/observer/user/ref_object_touch.rb index 3c828cb5d..07a380ec7 100644 --- a/app/models/observer/user/ref_object_touch.rb +++ b/app/models/observer/user/ref_object_touch.rb @@ -29,7 +29,7 @@ class Observer::User::RefObjectTouch < ActiveRecord::Observer # featrue used for different propose, do not touch references if User.where(organization_id: organization_id_changed[0]).count < 100 organization = Organization.find(organization_id_changed[0]) - organization.touch + organization.touch # rubocop:disable Rails/SkipsModelValidations member_ids = organization.member_ids end end @@ -40,7 +40,7 @@ class Observer::User::RefObjectTouch < ActiveRecord::Observer # featrue used for different propose, do not touch references if User.where(organization_id: record.organization_id).count < 100 - record.organization.touch + record.organization.touch # rubocop:disable Rails/SkipsModelValidations member_ids += record.organization.member_ids end end @@ -48,7 +48,7 @@ class Observer::User::RefObjectTouch < ActiveRecord::Observer # touch old/current customer member_ids.uniq.each do |user_id| next if user_id == record.id - User.find(user_id).touch + User.find(user_id).touch # rubocop:disable Rails/SkipsModelValidations end true end diff --git a/app/models/organization/assets.rb b/app/models/organization/assets.rb index d500a5392..d0db37fe0 100644 --- a/app/models/organization/assets.rb +++ b/app/models/organization/assets.rb @@ -55,7 +55,7 @@ returns data[ app_model_organization ][ id ] = local_attributes end - %w(created_by_id updated_by_id).each do |local_user_id| + %w[created_by_id updated_by_id].each do |local_user_id| next if !self[ local_user_id ] next if data[ app_model_user ][ self[ local_user_id ] ] user = User.lookup(id: self[ local_user_id ]) diff --git a/app/models/overview.rb b/app/models/overview.rb index ffadc7cb6..c6e604323 100644 --- a/app/models/overview.rb +++ b/app/models/overview.rb @@ -45,14 +45,14 @@ class Overview < ApplicationModel local_link = local_link.parameterize(separator: '_') local_link.gsub!(/\s/, '_') local_link.gsub!(/_+/, '_') - local_link = URI.escape(local_link) + local_link = CGI.escape(local_link) if local_link.blank? local_link = id || rand(999) end check = true while check exists = Overview.find_by(link: local_link) - if exists && exists.id != id + if exists&.id != id local_link = "#{local_link}_#{rand(999)}" else check = false diff --git a/app/models/overview/assets.rb b/app/models/overview/assets.rb index 9698a07f3..15f252a66 100644 --- a/app/models/overview/assets.rb +++ b/app/models/overview/assets.rb @@ -34,19 +34,17 @@ returns end if !data[ app_model_overview ][ id ] data[ app_model_overview ][ id ] = attributes_with_association_ids - if user_ids - user_ids.each do |local_user_id| - next if data[ app_model_user ][ local_user_id ] - user = User.lookup(id: local_user_id) - next if !user - data = user.assets(data) - end + user_ids&.each do |local_user_id| + next if data[ app_model_user ][ local_user_id ] + user = User.lookup(id: local_user_id) + next if !user + data = user.assets(data) end data = assets_of_selector('condition', data) end - %w(created_by_id updated_by_id).each do |local_user_id| + %w[created_by_id updated_by_id].each do |local_user_id| next if !self[ local_user_id ] next if data[ app_model_user ][ self[ local_user_id ] ] user = User.lookup(id: self[ local_user_id ]) diff --git a/app/models/package.rb b/app/models/package.rb index 221896a0e..361108685 100644 --- a/app/models/package.rb +++ b/app/models/package.rb @@ -166,7 +166,7 @@ link files + execute migration up file = file.sub(%r{^/}, '') # ignore files - if file =~ /^README/ + if file.match?(/^README/) logger.info "NOTICE: Ignore #{file}" next end @@ -185,10 +185,9 @@ link files + execute migration up backup_file = dest.to_s + '.link_backup' if File.exist?(backup_file) raise "Can't link #{entry} -> #{dest}, destination and .link_backup already exists!" - else - logger.info "Create backup file of #{dest} -> #{backup_file}." - File.rename(dest.to_s, backup_file) end + logger.info "Create backup file of #{dest} -> #{backup_file}." + File.rename(dest.to_s, backup_file) end if File.file?(entry) @@ -528,21 +527,18 @@ execute all pending package migrations at once end # down + done = Package::Migration.find_by(name: package.underscore, version: version) if direction == 'reverse' - done = Package::Migration.find_by(name: package.underscore, version: version) next if !done logger.info "NOTICE: down package migration '#{migration}'" load "#{location}/#{migration}" classname = name.camelcase classname.constantize.down record = Package::Migration.find_by(name: package.underscore, version: version) - if record - record.destroy - end + record&.destroy # up else - done = Package::Migration.find_by(name: package.underscore, version: version) next if done logger.info "NOTICE: up package migration '#{migration}'" load "#{location}/#{migration}" diff --git a/app/models/postmaster_filter.rb b/app/models/postmaster_filter.rb index 9a5f72361..172cb32f0 100644 --- a/app/models/postmaster_filter.rb +++ b/app/models/postmaster_filter.rb @@ -10,7 +10,7 @@ class PostmasterFilter < ApplicationModel def validate_condition raise Exceptions::UnprocessableEntity, 'Min. one match rule needed!' if match.blank? - match.each do |_key, meta| + match.each_value do |meta| raise Exceptions::UnprocessableEntity, 'operator invalid, ony "contains" and "contains not" is supported' if meta['operator'].blank? || meta['operator'] !~ /^(contains|contains not)$/ raise Exceptions::UnprocessableEntity, 'value invalid/empty' if meta['value'].blank? begin diff --git a/app/models/report.rb b/app/models/report.rb index d969b8342..f2d130b8b 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -1,7 +1,7 @@ class Report def self.enabled? - !Setting.get('es_url').empty? + Setting.get('es_url').present? end def self.config diff --git a/app/models/role/assets.rb b/app/models/role/assets.rb index f046d12c7..cb42d7575 100644 --- a/app/models/role/assets.rb +++ b/app/models/role/assets.rb @@ -35,7 +35,7 @@ returns # loops, will be updated with lookup attributes later data[ app_model ][ id ] = local_attributes - local_attributes['group_ids'].each do |group_id, _access| + local_attributes['group_ids'].each_key do |group_id| group = Group.lookup(id: group_id) next if !group data = group.assets(data) @@ -44,7 +44,7 @@ returns return data if !self['created_by_id'] && !self['updated_by_id'] app_model_user = User.to_app_model - %w(created_by_id updated_by_id).each do |local_user_id| + %w[created_by_id updated_by_id].each do |local_user_id| next if !self[ local_user_id ] next if data[ app_model_user ] && data[ app_model_user ][ self[ local_user_id ] ] user = User.lookup(id: self[ local_user_id ]) diff --git a/app/models/scheduler.rb b/app/models/scheduler.rb index 2ed876d12..8397032fd 100644 --- a/app/models/scheduler.rb +++ b/app/models/scheduler.rb @@ -69,7 +69,7 @@ class Scheduler < ApplicationModel # return [nil] def self.cleanup(force: false) - if !force && caller_locations.first.label != 'threads' + if !force && caller_locations(1..1).first.label != 'threads' raise 'This method should only get called when Scheduler.threads are initialized. Use `force: true` to start anyway.' end @@ -175,7 +175,7 @@ class Scheduler < ApplicationModel ) logger.info "execute #{job.method} (try_count #{try_count})..." - eval job.method() # rubocop:disable Lint/Eval + eval job.method() # rubocop:disable Security/Eval rescue => e logger.error "execute #{job.method} (try_count #{try_count}) exited with error #{e.inspect}" diff --git a/app/models/sla/assets.rb b/app/models/sla/assets.rb index 796eb9e5c..b24d0f88d 100644 --- a/app/models/sla/assets.rb +++ b/app/models/sla/assets.rb @@ -42,7 +42,7 @@ returns end end end - %w(created_by_id updated_by_id).each do |local_user_id| + %w[created_by_id updated_by_id].each do |local_user_id| next if !self[ local_user_id ] next if data[ app_model_user ][ self[ local_user_id ] ] user = User.lookup(id: self[ local_user_id ]) diff --git a/app/models/store.rb b/app/models/store.rb index 76fea4961..45200c455 100644 --- a/app/models/store.rb +++ b/app/models/store.rb @@ -178,7 +178,7 @@ returns raise "No such file #{store_file_id}!" end if !path - path = "#{Rails.root}/tmp/#{filename}" + path = Rails.root.join('tmp', filename) end ::File.open(path, 'wb') do |handle| handle.write file.content diff --git a/app/models/store/file.rb b/app/models/store/file.rb index 9c6ae18bf..c056a4be8 100644 --- a/app/models/store/file.rb +++ b/app/models/store/file.rb @@ -90,7 +90,7 @@ in case of fixing sha hash use: store = Store.find_by(store_file_id: item.id) logger.error "STORE: #{store.inspect}" if fix_it - item.update_attribute(:sha, sha) + item.update_attribute(:sha, sha) # rubocop:disable Rails/SkipsModelValidations end end success @@ -128,7 +128,7 @@ nice move to keep system responsive adapter_target.add(content, item.sha) # update meta data - item.update_attribute(:provider, target) + item.update_attribute(:provider, target) # rubocop:disable Rails/SkipsModelValidations # remove from old provider adapter_source.delete(item.sha) diff --git a/app/models/store/provider/file.rb b/app/models/store/provider/file.rb index 82b1a48ec..3d8b4f76d 100644 --- a/app/models/store/provider/file.rb +++ b/app/models/store/provider/file.rb @@ -51,12 +51,11 @@ class Store::Provider::File end # check if dir need to be removed - base = "#{Rails.root}/storage/fs" locations = location.split('/') (0..locations.count).reverse_each do |count| local_location = locations[0, count].join('/') - break if local_location =~ %r{storage/fs/{0,4}$} - break if !Dir["#{local_location}/*"].empty? + break if local_location.match?(%r{storage/fs/{0,4}$}) + break if Dir["#{local_location}/*"].present? FileUtils.rmdir(local_location) end end @@ -65,8 +64,8 @@ class Store::Provider::File def self.get_location(sha) # generate directory - base = "#{Rails.root}/storage/fs/" - parts = [] + base = Rails.root.join('storage', 'fs').to_s + parts = [] length1 = 4 length2 = 5 length3 = 7 diff --git a/app/models/taskbar.rb b/app/models/taskbar.rb index 8aac94cee..ba4c64fe8 100644 --- a/app/models/taskbar.rb +++ b/app/models/taskbar.rb @@ -14,9 +14,9 @@ class Taskbar < ApplicationModel def state_changed? return false if state.blank? - state.each do |_key, value| + state.each_value do |value| if value.is_a? Hash - value.each do |_key1, value1| + value.each_value do |value1| next if value1.blank? return true end @@ -32,10 +32,10 @@ class Taskbar < ApplicationModel def update_last_contact return true if local_update - return true if changes.empty? + return true if changes.blank? if changes['notify'] count = 0 - changes.each do |attribute, _value| + changes.each_key do |attribute| next if attribute == 'updated_at' next if attribute == 'created_at' count += 1 diff --git a/app/models/ticket.rb b/app/models/ticket.rb index e930201ae..83aa9a5b3 100644 --- a/app/models/ticket.rb +++ b/app/models/ticket.rb @@ -300,7 +300,7 @@ returns Ticket::Article.where(ticket_id: id).each(&:touch) # quiet update of reassign of articles - Ticket::Article.where(ticket_id: id).update_all(['ticket_id = ?', data[:ticket_id]]) + Ticket::Article.where(ticket_id: id).update_all(['ticket_id = ?', data[:ticket_id]]) # rubocop:disable Rails/SkipsModelValidations # update history @@ -318,6 +318,7 @@ returns # add history to both # reassign links to the new ticket + # rubocop:disable Rails/SkipsModelValidations Link.where( link_object_source_id: Link::Object.find_by(name: 'Ticket').id, link_object_source_value: id, @@ -326,6 +327,7 @@ returns link_object_target_id: Link::Object.find_by(name: 'Ticket').id, link_object_target_value: id, ).update_all(link_object_target_value: data[:ticket_id]) + # rubocop:enable Rails/SkipsModelValidations # link tickets Link.add( @@ -346,7 +348,7 @@ returns save! # touch new ticket (to broadcast change) - target_ticket.touch + target_ticket.touch # rubocop:disable Rails/SkipsModelValidations end true end @@ -500,9 +502,13 @@ condition example bind_params = [] like = Rails.application.config.db_like + if selectors.respond_to?(:permit!) + selectors = selectors.permit!.to_h + end + # get tables to join tables = '' - selectors.each do |attribute, selector| + selectors.each_key do |attribute| selector = attribute.split(/\./) next if !selector[1] next if selector[0] == 'ticket' @@ -537,7 +543,7 @@ condition example raise "Invalid selector, operator missing #{selector.inspect}" if !selector['operator'] # validate value / allow blank but only if pre_condition exists and is not specific - if !selector.key?('value') || ((selector['value'].class == String || selector['value'].class == Array) && (selector['value'].respond_to?(:empty?) && selector['value'].empty?)) + if !selector.key?('value') || ((selector['value'].class == String || selector['value'].class == Array) && (selector['value'].respond_to?(:blank?) && selector['value'].blank?)) return nil if selector['pre_condition'].nil? return nil if selector['pre_condition'].respond_to?(:blank?) && selector['pre_condition'].blank? return nil if selector['pre_condition'] == 'specific' @@ -565,7 +571,7 @@ condition example if selector['operator'] == 'is' if selector['pre_condition'] == 'not_set' - if attributes[1] =~ /^(created_by|updated_by|owner|customer|user)_id/ + if attributes[1].match?(/^(created_by|updated_by|owner|customer|user)_id/) query += "#{attribute} IN (?)" bind_params.push 1 else @@ -573,11 +579,10 @@ condition example end elsif selector['pre_condition'] == 'current_user.id' raise "Use current_user.id in selector, but no current_user is set #{selector.inspect}" if !current_user_id + query += "#{attribute} IN (?)" if attributes[1] == 'out_of_office_replacement_id' - query += "#{attribute} IN (?)" bind_params.push User.find(current_user_id).out_of_office_agent_of.pluck(:id) else - query += "#{attribute} IN (?)" bind_params.push current_user_id end elsif selector['pre_condition'] == 'current_user.organization_id' @@ -590,11 +595,10 @@ condition example if selector['value'].nil? query += "#{attribute} IS NULL" else + query += "#{attribute} IN (?)" if attributes[1] == 'out_of_office_replacement_id' - query += "#{attribute} IN (?)" bind_params.push User.find(selector['value']).out_of_office_agent_of.pluck(:id) else - query += "#{attribute} IN (?)" bind_params.push selector['value'] end end @@ -602,18 +606,17 @@ condition example end elsif selector['operator'] == 'is not' if selector['pre_condition'] == 'not_set' - if attributes[1] =~ /^(created_by|updated_by|owner|customer|user)_id/ + if attributes[1].match?(/^(created_by|updated_by|owner|customer|user)_id/) query += "#{attribute} NOT IN (?)" bind_params.push 1 else query += "#{attribute} IS NOT NULL" end elsif selector['pre_condition'] == 'current_user.id' + query += "#{attribute} NOT IN (?)" if attributes[1] == 'out_of_office_replacement_id' - query += "#{attribute} NOT IN (?)" bind_params.push User.find(current_user_id).out_of_office_agent_of.pluck(:id) else - query += "#{attribute} NOT IN (?)" bind_params.push current_user_id end elsif selector['pre_condition'] == 'current_user.organization_id' @@ -625,11 +628,10 @@ condition example if selector['value'].nil? query += "#{attribute} IS NOT NULL" else + query += "#{attribute} NOT IN (?)" if attributes[1] == 'out_of_office_replacement_id' - query += "#{attribute} NOT IN (?)" bind_params.push User.find(selector['value']).out_of_office_agent_of.pluck(:id) else - query += "#{attribute} NOT IN (?)" bind_params.push selector['value'] end end @@ -806,7 +808,7 @@ perform changes on ticket # if the configuration contains the deletion of the ticket then # we skip all other ticket changes because they does not matter if perform['ticket.action'].present? && perform['ticket.action']['value'] == 'delete' - perform.each do |key, _value| + perform.each_key do |key| (object_name, attribute) = key.split('.', 2) next if object_name != 'ticket' next if attribute == 'action' @@ -892,17 +894,17 @@ perform changes on ticket # do not sent notifications to this recipients send_no_auto_response_reg_exp = Setting.get('send_no_auto_response_reg_exp') begin - next if recipient_email =~ /#{send_no_auto_response_reg_exp}/i + next if recipient_email.match?(/#{send_no_auto_response_reg_exp}/i) rescue => e logger.error "ERROR: Invalid regex '#{send_no_auto_response_reg_exp}' in setting send_no_auto_response_reg_exp" logger.error 'ERROR: ' + e.inspect - next if recipient_email =~ /(mailer-daemon|postmaster|abuse|root|noreply|noreply.+?|no-reply|no-reply.+?)@.+?/i + next if recipient_email.match?(/(mailer-daemon|postmaster|abuse|root|noreply|noreply.+?|no-reply|no-reply.+?)@.+?/i) end # check if notification should be send because of customer emails if item && item[:article_id] article = Ticket::Article.lookup(id: item[:article_id]) - if article && article.preferences['is-auto-response'] == true && article.from && article.from =~ /#{Regexp.quote(recipient_email)}/i + if article&.preferences&.fetch('is-auto-response', false) == true && article.from && article.from =~ /#{Regexp.quote(recipient_email)}/i logger.info "Send no trigger based notification to #{recipient_email} because of auto response tagged incoming email" next end @@ -1037,9 +1039,9 @@ perform changes on ticket # lookup pre_condition if value['pre_condition'] - if value['pre_condition'] =~ /^not_set/ + if value['pre_condition'].match?(/^not_set/) value['value'] = 1 - elsif value['pre_condition'] =~ /^current_user\./ + elsif value['pre_condition'].match?(/^current_user\./) raise 'Unable to use current_user, got no current_user_id for ticket.perform_changes' if !current_user_id value['value'] = current_user_id end @@ -1079,11 +1081,10 @@ result def get_references(ignore = []) references = [] Ticket::Article.select('in_reply_to, message_id').where(ticket_id: id).each do |article| - if !article.in_reply_to.empty? + if article.in_reply_to.present? references.push article.in_reply_to end - next if !article.message_id - next if article.message_id.empty? + next if article.message_id.blank? references.push article.message_id end ignore.each do |item| @@ -1169,7 +1170,7 @@ result current_state_type = Ticket::StateType.lookup(id: current_state.state_type_id) # in case, set pending_time to nil - return true if current_state_type.name =~ /^pending/i + return true if current_state_type.name.match?(/^pending/i) self.pending_time = nil true end diff --git a/app/models/ticket/article.rb b/app/models/ticket/article.rb index 96213f2d8..64ebcb1d9 100644 --- a/app/models/ticket/article.rb +++ b/app/models/ticket/article.rb @@ -148,7 +148,7 @@ get body as text def body_as_text return '' if !body - return body if !content_type || content_type.empty? || content_type =~ %r{text/plain}i + return body if content_type.blank? || content_type =~ %r{text/plain}i body.html2text end @@ -290,15 +290,12 @@ returns return true if body.blank? limit = 1_500_000 current_length = body.length - if body.length > limit - if ApplicationHandleInfo.current.present? && ApplicationHandleInfo.current.split('.')[1] == 'postmaster' - logger.warn "WARNING: cut string because of database length #{self.class}.body(#{limit} but is #{current_length})" - self.body = body[0, limit] - else - raise Exceptions::UnprocessableEntity, "body if article is to large, #{current_length} chars - only #{limit} allowed" - end - end - true + return true if body.length <= limit + + raise Exceptions::UnprocessableEntity, "body if article is to large, #{current_length} chars - only #{limit} allowed" if !ApplicationHandleInfo.postmaster? + + logger.warn "WARNING: cut string because of database length #{self.class}.body(#{limit} but is #{current_length})" + self.body = body[0, limit] end def history_log_attributes diff --git a/app/models/ticket/article/assets.rb b/app/models/ticket/article/assets.rb index 88c6e2fe1..e2efaf92d 100644 --- a/app/models/ticket/article/assets.rb +++ b/app/models/ticket/article/assets.rb @@ -42,7 +42,7 @@ returns data[ app_model_article ][ id ] = attributes_with_association_ids end - %w(created_by_id updated_by_id origin_by_id).each do |local_user_id| + %w[created_by_id updated_by_id origin_by_id].each do |local_user_id| next if !self[ local_user_id ] next if data[ app_model_user ] && data[ app_model_user ][ self[ local_user_id ] ] user = User.lookup(id: self[ local_user_id ]) diff --git a/app/models/ticket/assets.rb b/app/models/ticket/assets.rb index 4fdd1424d..c247949c6 100644 --- a/app/models/ticket/assets.rb +++ b/app/models/ticket/assets.rb @@ -31,7 +31,7 @@ returns if !data[ app_model_ticket ][ id ] data[ app_model_ticket ][ id ] = attributes_with_association_ids end - %w(created_by_id updated_by_id owner_id customer_id).each do |local_user_id| + %w[created_by_id updated_by_id owner_id customer_id].each do |local_user_id| next if !self[ local_user_id ] next if data[ app_model_user ] && data[ app_model_user ][ self[ local_user_id ] ] user = User.lookup(id: self[ local_user_id ]) diff --git a/app/models/ticket/escalation.rb b/app/models/ticket/escalation.rb index 66152a6a4..816d2fde0 100644 --- a/app/models/ticket/escalation.rb +++ b/app/models/ticket/escalation.rb @@ -169,13 +169,11 @@ returns # get holidays holidays = [] - if calendar.public_holidays - calendar.public_holidays.each do |day, meta| - next if !meta - next if !meta['active'] - next if meta['removed'] - holidays.push Date.parse(day) - end + calendar.public_holidays&.each do |day, meta| + next if !meta + next if !meta['active'] + next if meta['removed'] + holidays.push Date.parse(day) end config.holidays = holidays config.time_zone = calendar.timezone @@ -294,7 +292,7 @@ returns Cache.write('SLA::List::Active', sla_list, { expires_in: 1.hour }) end sla_list.each do |sla| - if !sla.condition || sla.condition.empty? + if sla.condition.blank? sla_selected = sla elsif sla.condition query_condition, bind_condition, tables = Ticket.selector2sql(sla.condition) diff --git a/app/models/ticket/overviews.rb b/app/models/ticket/overviews.rb index de060ac01..344b2e7c3 100644 --- a/app/models/ticket/overviews.rb +++ b/app/models/ticket/overviews.rb @@ -27,7 +27,7 @@ returns overviews_list = [] overviews.each do |overview| user_ids = overview.user_ids - next if !user_ids.empty? && !user_ids.include?(current_user.id) + next if user_ids.present? && !user_ids.include?(current_user.id) overviews_list.push overview end return overviews_list diff --git a/app/models/ticket/priority.rb b/app/models/ticket/priority.rb index be3a780b5..5481e226c 100644 --- a/app/models/ticket/priority.rb +++ b/app/models/ticket/priority.rb @@ -10,16 +10,16 @@ class Ticket::Priority < ApplicationModel attr_accessor :callback_loop def ensure_defaults - return if callback_loop + return true if callback_loop priorities_with_default = Ticket::Priority.where(default_create: true) - return if priorities_with_default.count == 1 + return true if priorities_with_default.count == 1 if priorities_with_default.count.zero? priority = Ticket::Priority.where(active: true).order(id: :asc).first priority.default_create = true priority.callback_loop = true priority.save! - return + return true end if priorities_with_default.count > 1 @@ -30,5 +30,6 @@ class Ticket::Priority < ApplicationModel local_priority.save! end end + true end end diff --git a/app/models/ticket/screen_options.rb b/app/models/ticket/screen_options.rb index f490b4dda..c15b2ae81 100644 --- a/app/models/ticket/screen_options.rb +++ b/app/models/ticket/screen_options.rb @@ -68,7 +68,7 @@ returns type_ids = [] if params[:ticket] - types = %w(note phone) + types = %w[note phone] if params[:ticket].group.email_address_id types.push 'email' end diff --git a/app/models/ticket/search_index.rb b/app/models/ticket/search_index.rb index dbd0ecf8a..c5ff8361b 100644 --- a/app/models/ticket/search_index.rb +++ b/app/models/ticket/search_index.rb @@ -40,7 +40,7 @@ returns article_attributes = article.search_index_attribute_lookup # remove note needed attributes - ignore = %w(message_id_md5 ticket) + ignore = %w[message_id_md5 ticket] ignore.each do |attribute| article_attributes.delete(attribute) end diff --git a/app/models/ticket/state.rb b/app/models/ticket/state.rb index 7adb18dd4..9f75ef380 100644 --- a/app/models/ticket/state.rb +++ b/app/models/ticket/state.rb @@ -36,7 +36,7 @@ returns: when :pending state_types = ['pending reminder', 'pending action'] when :work_on - state_types = %w(new open) + state_types = %w[new open] when :work_on_all state_types = ['new', 'open', 'pending reminder'] when :viewable @@ -46,13 +46,13 @@ returns: when :viewable_agent_edit state_types = ['open', 'pending reminder', 'pending action', 'closed'] when :viewable_customer_new - state_types = %w(new closed) + state_types = %w[new closed] when :viewable_customer_edit - state_types = %w(open closed) + state_types = %w[open closed] when :closed - state_types = %w(closed) + state_types = %w[closed] when :merged - state_types = %w(merged) + state_types = %w[merged] end raise "Unknown category '#{category}'" if state_types.blank? @@ -84,7 +84,7 @@ returns: def ensure_defaults return if callback_loop - %w(default_create default_follow_up).each do |default_field| + %w[default_create default_follow_up].each do |default_field| states_with_default = Ticket::State.where(default_field => true) next if states_with_default.count == 1 diff --git a/app/models/transaction/background_job.rb b/app/models/transaction/background_job.rb index f37c99846..4d92c6017 100644 --- a/app/models/transaction/background_job.rb +++ b/app/models/transaction/background_job.rb @@ -25,7 +25,7 @@ class Transaction::BackgroundJob def perform Setting.where(area: 'Transaction::Backend::Async').order(:name).each do |setting| backend = Setting.get(setting.name) - next if @params[:disable] && @params[:disable].include?(backend) + next if @params[:disable]&.include?(backend) backend = Kernel.const_get(backend) Observer::Transaction.execute_singel_backend(backend, @item, @params) end diff --git a/app/models/transaction/cti_caller_id_detection.rb b/app/models/transaction/cti_caller_id_detection.rb index 186d4f8e6..959ba4a26 100644 --- a/app/models/transaction/cti_caller_id_detection.rb +++ b/app/models/transaction/cti_caller_id_detection.rb @@ -48,6 +48,7 @@ class Transaction::CtiCallerIdDetection Cti::CallerId.build(user) end + true end end diff --git a/app/models/transaction/karma.rb b/app/models/transaction/karma.rb index 07a43b73f..c5fa1d082 100644 --- a/app/models/transaction/karma.rb +++ b/app/models/transaction/karma.rb @@ -98,6 +98,7 @@ class Transaction::Karma Karma::ActivityLog.add('ticket pending state', user, 'Ticket', ticket.id) end + true end def ticket_article_karma(user) @@ -132,11 +133,7 @@ class Transaction::Karma return false if !local_sender next if local_sender.name == 'System' - last_sender_customer = if local_sender.name == 'Customer' - true - else - false - end + last_sender_customer = local_sender.name == 'Customer' next if local_sender.name != 'Customer' last_customer_contact = local_article.created_at @@ -158,9 +155,11 @@ class Transaction::Karma end ### text module - if article.preferences[:text_module_ids] && !article.preferences[:text_module_ids].empty? + if article.preferences[:text_module_ids].present? Karma::ActivityLog.add('text module', user, 'Ticket', @item[:object_id]) end + + true end def tagging(user) diff --git a/app/models/transaction/notification.rb b/app/models/transaction/notification.rb index 8c5a74539..bbcad80d4 100644 --- a/app/models/transaction/notification.rb +++ b/app/models/transaction/notification.rb @@ -36,7 +36,7 @@ class Transaction::Notification # ignore notifications sender = Ticket::Article::Sender.lookup(id: article.sender_id) - if sender && sender.name == 'System' + if sender&.name == 'System' return if @item[:changes].blank? && article.preferences[:notification] != true if article.preferences[:notification] != true article = nil @@ -91,7 +91,7 @@ class Transaction::Notification # ignore user who changed it by him self via web if @params[:interface_handle] == 'application_server' - next if article && article.updated_by_id == user.id + next if article&.updated_by_id == user.id next if !article && @item[:user_id] == user.id end @@ -213,7 +213,7 @@ class Transaction::Notification end def add_recipient_list(ticket, user, channels, type) - return if channels.empty? + return if channels.blank? identifier = user.email if !identifier || identifier == '' identifier = user.login @@ -240,7 +240,7 @@ class Transaction::Notification @item[:changes].each do |key, value| # if no config exists, use all attributes - if !attribute_list || attribute_list.empty? + if attribute_list.blank? user_related_changes[key] = value # if config exists, just use existing attributes for user diff --git a/app/models/transaction/slack.rb b/app/models/transaction/slack.rb index aac7d393a..3c49a61a3 100644 --- a/app/models/transaction/slack.rb +++ b/app/models/transaction/slack.rb @@ -43,15 +43,15 @@ class Transaction::Slack # ignore notifications sender = Ticket::Article::Sender.lookup(id: article.sender_id) - if sender && sender.name == 'System' - return if @item[:changes].empty? + if sender&.name == 'System' + return if @item[:changes].blank? article = nil end end # ignore if no changes has been done changes = human_changes(ticket) - return if @item[:type] == 'update' && !article && (!changes || changes.empty?) + return if @item[:type] == 'update' && !article && changes.blank? # get user based notification template # if create, send create message / block update messages @@ -101,14 +101,14 @@ class Transaction::Slack if ticket.pending_time && ticket.pending_time < Time.zone.now color = '#faab00' end - elsif ticket_state_type =~ /^(new|open)$/ + elsif ticket_state_type.match?(/^(new|open)$/) color = '#faab00' elsif ticket_state_type == 'closed' color = '#38ad69' end config['items'].each do |local_config| - next if local_config['webhook'].empty? + next if local_config['webhook'].blank? # check if reminder_reached/escalation/escalation_warning is already sent today md5_webhook = Digest::MD5.hexdigest(local_config['webhook']) @@ -155,7 +155,7 @@ class Transaction::Slack end logo_url = 'https://zammad.com/assets/images/logo-200x200.png' - if !local_config['logo_url'].empty? + if local_config['logo_url'].present? logo_url = local_config['logo_url'] end @@ -206,7 +206,7 @@ class Transaction::Slack @item[:changes].each do |key, value| # if no config exists, use all attributes - if !attribute_list || attribute_list.empty? + if attribute_list.blank? user_related_changes[key] = value # if config exists, just use existing attributes for user diff --git a/app/models/transaction/trigger.rb b/app/models/transaction/trigger.rb index 24322ceb8..66e8cb468 100644 --- a/app/models/transaction/trigger.rb +++ b/app/models/transaction/trigger.rb @@ -34,7 +34,7 @@ class Transaction::Trigger else Trigger.where(active: true).order(:name) end - return if triggers.empty? + return if triggers.blank? ticket = Ticket.lookup(id: @item[:object_id]) return if !ticket @@ -51,7 +51,7 @@ class Transaction::Trigger # check if one article attribute is used one_has_changed_done = false article_selector = false - trigger.condition.each do |key, _value| + trigger.condition.each_key do |key| (object_name, attribute) = key.split('.', 2) next if object_name != 'article' next if attribute == 'id' @@ -81,7 +81,7 @@ class Transaction::Trigger end # check if we have not matching "has changed" attributes - condition.each do |_key, value| + condition.each_value do |value| next if !value next if !value['operator'] next if !value['operator']['has changed'] @@ -102,7 +102,7 @@ class Transaction::Trigger if @item[:type] == 'update' # verify if ticket condition exists - condition.each do |key, _value| + condition.each_key do |key| (object_name, attribute) = key.split('.', 2) next if object_name != 'ticket' one_has_changed_condition = true diff --git a/app/models/translation.rb b/app/models/translation.rb index dbb013345..4d1a4253a 100644 --- a/app/models/translation.rb +++ b/app/models/translation.rb @@ -168,7 +168,7 @@ get list of translations # add presorted on top presorted_list = [] - %w(yes no or Year Years Month Months Day Days Hour Hours Minute Minutes Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec January February March April May June July August September October November December Mon Tue Wed Thu Fri Sat Sun Monday Tuesday Wednesday Thursday Friday Saturday Sunday).each do |presort| + %w[yes no or Year Years Month Months Day Days Hour Hours Minute Minutes Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec January February March April May June July August September October November December Mon Tue Wed Thu Fri Sat Sun Monday Tuesday Wednesday Thursday Friday Saturday Sunday].each do |presort| list.each do |item| next if item[1] != presort presorted_list.push item @@ -227,9 +227,9 @@ all: def self.load_from_file(dedicated_locale = nil) version = Version.get - directory = Rails.root.join('config/translations') + directory = Rails.root.join('config', 'translations') locals_to_sync(dedicated_locale).each do |locale| - file = Rails.root.join("#{directory}/#{locale}-#{version}.yml") + file = Rails.root.join(directory, "#{locale}-#{version}.yml") return false if !File.exist?(file) data = YAML.load_file(file) to_database(locale, data) @@ -271,11 +271,11 @@ all: ) raise "Can't load translations from #{url}: #{result.error}" if !result.success? - directory = Rails.root.join('config/translations') + directory = Rails.root.join('config', 'translations') if !File.directory?(directory) Dir.mkdir(directory, 0o755) end - file = Rails.root.join("#{directory}/#{locale}-#{version}.yml") + file = Rails.root.join(directory, "#{locale}-#{version}.yml") File.open(file, 'w') do |out| YAML.dump(result.data, out) end @@ -359,7 +359,7 @@ Get source file at https://i18n.zammad.com/api/v1/translations_empty_translation # verify if update is needed update_needed = false - translation_raw.each do |key, _value| + translation_raw.each_key do |key| # if translation target has changes next unless translation_raw[key] != translation.target diff --git a/app/models/user.rb b/app/models/user.rb index 21c32d86a..ac6dac3cf 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -45,7 +45,7 @@ class User < ApplicationModel after_update :avatar_for_email_check after_destroy :avatar_destroy, :user_device_destroy - has_and_belongs_to_many :roles, after_add: [:cache_update, :check_notifications], after_remove: :cache_update, before_add: [:validate_agent_limit_by_role, :validate_roles], before_remove: :last_admin_check_by_role, class_name: 'Role' + has_and_belongs_to_many :roles, after_add: %i[cache_update check_notifications], after_remove: :cache_update, before_add: %i[validate_agent_limit_by_role validate_roles], before_remove: :last_admin_check_by_role, class_name: 'Role' has_and_belongs_to_many :organizations, after_add: :cache_update, after_remove: :cache_update, class_name: 'Organization' #has_many :permissions, class_name: 'Permission', through: :roles, class_name: 'Role' has_many :tokens, after_add: :cache_update, after_remove: :cache_update @@ -368,12 +368,9 @@ returns role_ids = Role.signup_role_ids url = '' - if hash['info']['urls'] - hash['info']['urls'].each do |_name, local_url| - next if !local_url - next if local_url.empty? - url = local_url - end + hash['info']['urls']&.each_value do |local_url| + next if local_url.blank? + url = local_url end create( login: hash['info']['nickname'] || hash['uid'], @@ -441,13 +438,13 @@ returns end keys.each do |local_key| list = [] - if local_key =~ /\.\*$/ + if local_key.match?(/\.\*$/) local_key.sub!('.*', '.%') permissions = ::Permission.with_parents(local_key) list = ::Permission.select('preferences').joins(:roles).where('roles.id IN (?) AND roles.active = ? AND (permissions.name IN (?) OR permissions.name LIKE ?) AND permissions.active = ?', role_ids, true, permissions, local_key, true).pluck(:preferences) else permission = ::Permission.lookup(name: local_key) - break if permission && permission.active == false + break if permission&.active == false permissions = ::Permission.with_parents(local_key) list = ::Permission.select('preferences').joins(:roles).where('roles.id IN (?) AND roles.active = ? AND permissions.name IN (?) AND permissions.active = ?', role_ids, true, permissions, true).pluck(:preferences) end @@ -472,7 +469,7 @@ returns def permissions_with_child_ids where = '' where_bind = [true] - permissions.each do |permission_name, _value| + permissions.each_key do |permission_name| where += ' OR ' if where != '' where += 'permissions.name = ? OR permissions.name LIKE ?' where_bind.push permission_name @@ -511,13 +508,13 @@ returns next if !permission permission_ids.push permission.id end - next if permission_ids.empty? + next if permission_ids.blank? Role.joins(:roles_permissions).joins(:permissions).where('permissions_roles.permission_id IN (?) AND roles.active = ? AND permissions.active = ?', permission_ids, true, true).distinct().pluck(:id).each do |role_id| role_ids.push role_id end total_role_ids.push role_ids end - return [] if total_role_ids.empty? + return [] if total_role_ids.blank? condition = '' total_role_ids.each do |_role_ids| if condition != '' @@ -792,7 +789,7 @@ returns true end - def check_notifications(o, shouldSave = true) + def check_notifications(o, should_save = true) default = Rails.configuration.preferences_default_by_permission return if !default default.deep_stringify_keys! @@ -808,7 +805,7 @@ returns return true if !has_changed - if id && shouldSave + if id && should_save save! return true end @@ -908,7 +905,7 @@ returns self.email = email.downcase.strip return true if id == 1 raise Exceptions::UnprocessableEntity, 'Invalid email' if email !~ /@/ - raise Exceptions::UnprocessableEntity, 'Invalid email' if email =~ /\s/ + raise Exceptions::UnprocessableEntity, 'Invalid email' if email.match?(/\s/) true end @@ -936,7 +933,7 @@ returns check = true while check exists = User.find_by(login: login) - if exists && exists.id != id + if exists && exists.id != id # rubocop:disable Style/SafeNavigation self.login = "#{login}#{rand(999)}" else check = false @@ -1109,7 +1106,7 @@ raise 'Minimum one user need to have admin permissions' # update user link return true if !avatar - update_column(:image, avatar.store_hash) + update_column(:image, avatar.store_hash) # rubocop:disable Rails/SkipsModelValidations cache_delete true end diff --git a/app/models/user/assets.rb b/app/models/user/assets.rb index 77dcd8473..2be614b5f 100644 --- a/app/models/user/assets.rb +++ b/app/models/user/assets.rb @@ -56,32 +56,26 @@ returns local_attributes['accounts'] = local_accounts # get roles - if local_attributes['role_ids'] - local_attributes['role_ids'].each do |role_id| - next if data[:Role] && data[:Role][role_id] - role = Role.lookup(id: role_id) - data = role.assets(data) - end + local_attributes['role_ids']&.each do |role_id| + next if data[:Role] && data[:Role][role_id] + role = Role.lookup(id: role_id) + data = role.assets(data) end # get groups - if local_attributes['group_ids'] - local_attributes['group_ids'].each do |group_id, _access| - next if data[:Group] && data[:Group][group_id] - group = Group.lookup(id: group_id) - next if !group - data = group.assets(data) - end + local_attributes['group_ids']&.each do |group_id, _access| + next if data[:Group] && data[:Group][group_id] + group = Group.lookup(id: group_id) + next if !group + data = group.assets(data) end # get organizations - if local_attributes['organization_ids'] - local_attributes['organization_ids'].each do |organization_id| - next if data[:Organization] && data[:Organization][organization_id] - organization = Organization.lookup(id: organization_id) - next if !organization - data = organization.assets(data) - end + local_attributes['organization_ids']&.each do |organization_id| + next if data[:Organization] && data[:Organization][organization_id] + organization = Organization.lookup(id: organization_id) + next if !organization + data = organization.assets(data) end data[ app_model ][ id ] = local_attributes @@ -96,7 +90,7 @@ returns end end end - %w(created_by_id updated_by_id).each do |local_user_id| + %w[created_by_id updated_by_id].each do |local_user_id| next if !self[ local_user_id ] next if data[ app_model ][ self[ local_user_id ] ] user = User.lookup(id: self[ local_user_id ]) diff --git a/app/models/user_device.rb b/app/models/user_device.rb index cecc0cc1c..4edb36d03 100644 --- a/app/models/user_device.rb +++ b/app/models/user_device.rb @@ -48,7 +48,7 @@ store new device for user if device not already known # for basic_auth|token_auth search for user agent device_exists_by_user_agent = false - if type == 'basic_auth' || type == 'token_auth' + if %w[basic_auth token_auth].include?(type) user_devices = UserDevice.where( user_id: user_id, user_agent: user_agent, diff --git a/config/application.rb b/config/application.rb index 79eef32c8..1e2444552 100644 --- a/config/application.rb +++ b/config/application.rb @@ -13,8 +13,8 @@ module Zammad # -- all .rb files in that directory are automatically loaded. # Custom directories with classes and modules you want to be autoloadable. - config.autoload_paths += %W(#{config.root}/lib) - config.eager_load_paths += %W(#{config.root}/lib) + config.autoload_paths += %W[#{config.root}/lib] + config.eager_load_paths += %W[#{config.root}/lib] # Activate observers that should always be running. # config.active_record.observers = :cacher, :garbage_collector, :forum_observer @@ -47,7 +47,7 @@ module Zammad config.api_path = '/api/v1' # define cache store - config.cache_store = :file_store, "#{Rails.root}/tmp/cache_file_store_#{Rails.env}" + config.cache_store = :file_store, Rails.root.join('tmp', "cache_file_store_#{Rails.env}") # default preferences by permission config.preferences_default_by_permission = { diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 09feafbf1..110972dae 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -8,5 +8,5 @@ Rails.application.config.assets.version = '1.0' # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. -Rails.application.config.assets.precompile += %w(application-print.css) -Rails.application.config.assets.precompile += %w(print.css) +Rails.application.config.assets.precompile += %w[application-print.css] +Rails.application.config.assets.precompile += %w[print.css] diff --git a/config/initializers/core_ext.rb b/config/initializers/core_ext.rb index 86258f99e..e4c09d57e 100644 --- a/config/initializers/core_ext.rb +++ b/config/initializers/core_ext.rb @@ -1,5 +1,5 @@ # load all core_ext extentions -Dir.glob("#{Rails.root}/lib/core_ext/**/*").each do |file| +Dir.glob( Rails.root.join('lib', 'core_ext', '**', '*') ).each do |file| if File.file?(file) require file end diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index a1bc85d76..be3dd4cd2 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -7,7 +7,7 @@ Doorkeeper.configure do # fail "Please configure doorkeeper resource_owner_authenticator block located in #{__FILE__}" # Put your resource owner authentication logic here. # Example implementation: - User.find_by_id(session[:user_id]) || redirect_to(new_user_session_url) + User.find_by(id: session[:user_id]) || redirect_to(new_user_session_url) end # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below. diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index 6c009f1e7..66d608e63 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -1,4 +1,4 @@ # Be sure to restart your server when you modify this file. # Configure sensitive parameters which will be filtered from the log file. -Rails.application.config.filter_parameters += [:password, :bind_pw] +Rails.application.config.filter_parameters += %i[password bind_pw] diff --git a/config/initializers/html_email_style.rb b/config/initializers/html_email_style.rb index b963a13dd..8561d3a7e 100644 --- a/config/initializers/html_email_style.rb +++ b/config/initializers/html_email_style.rb @@ -1,81 +1,81 @@ Rails.application.config.html_email_css_font = "font-family:'Helvetica Neue', Helvetica, Arial, Geneva, sans-serif; font-size: 12px;" -Rails.application.config.html_email_body = < - - - - - - - ###html### - +Rails.application.config.html_email_body = <<~HERE + + + + + + + + ###html### + HERE diff --git a/config/initializers/html_sanitizer.rb b/config/initializers/html_sanitizer.rb index 387fd5fdd..5d7716340 100644 --- a/config/initializers/html_sanitizer.rb +++ b/config/initializers/html_sanitizer.rb @@ -1,16 +1,16 @@ # content of this tags will also be removed -Rails.application.config.html_sanitizer_tags_remove_content = %w( +Rails.application.config.html_sanitizer_tags_remove_content = %w[ style -) +] # content of this tags will will be inserted html quoted -Rails.application.config.html_sanitizer_tags_quote_content = %w( +Rails.application.config.html_sanitizer_tags_quote_content = %w[ script -) +] # only this tags are allowed -Rails.application.config.html_sanitizer_tags_whitelist = %w( +Rails.application.config.html_sanitizer_tags_whitelist = %w[ a abbr acronym address area article aside audio b bdi bdo big blockquote br canvas caption center cite code col colgroup command @@ -20,44 +20,44 @@ Rails.application.config.html_sanitizer_tags_whitelist = %w( ol output optgroup option p pre q s samp section small span strike strong sub summary sup text table tbody td tfoot th thead time tr tt u ul var video -) +] # attributes allowed for tags Rails.application.config.html_sanitizer_attributes_whitelist = { - :all => %w(class dir lang title translate data-signature data-signature-id), - 'a' => %w(href hreflang name rel), - 'abbr' => %w(title), - 'blockquote' => %w(type cite), - 'col' => %w(span width), - 'colgroup' => %w(span width), - 'data' => %w(value), - 'del' => %w(cite datetime), - 'dfn' => %w(title), - 'img' => %w(align alt border height src srcset width style), - 'ins' => %w(cite datetime), - 'li' => %w(value), - 'ol' => %w(reversed start type), - 'table' => %w(align bgcolor border cellpadding cellspacing frame rules sortable summary width style), - 'td' => %w(abbr align axis colspan headers rowspan valign width style), - 'th' => %w(abbr align axis colspan headers rowspan scope sorted valign width style), - 'tr' => %w(width style), - 'ul' => %w(type), - 'q' => %w(cite), - 'span' => %w(style), - 'time' => %w(datetime pubdate), + :all => %w[class dir lang title translate data-signature data-signature-id], + 'a' => %w[href hreflang name rel], + 'abbr' => %w[title], + 'blockquote' => %w[type cite], + 'col' => %w[span width], + 'colgroup' => %w[span width], + 'data' => %w[value], + 'del' => %w[cite datetime], + 'dfn' => %w[title], + 'img' => %w[align alt border height src srcset width style], + 'ins' => %w[cite datetime], + 'li' => %w[value], + 'ol' => %w[reversed start type], + 'table' => %w[align bgcolor border cellpadding cellspacing frame rules sortable summary width style], + 'td' => %w[abbr align axis colspan headers rowspan valign width style], + 'th' => %w[abbr align axis colspan headers rowspan scope sorted valign width style], + 'tr' => %w[width style], + 'ul' => %w[type], + 'q' => %w[cite], + 'span' => %w[style], + 'time' => %w[datetime pubdate], } # only this css properties are allowed Rails.application.config.html_sanitizer_css_properties_whitelist = { - 'img' => %w( + 'img' => %w[ width height max-width min-width max-height min-height - ), - 'span' => %w( + ], + 'span' => %w[ color - ), - 'table' => %w( + ], + 'table' => %w[ background background-color color font-size vertical-align margin margin-top margin-right margin-bottom margin-left padding padding-top padding-right padding-bottom padding-left @@ -73,8 +73,8 @@ Rails.application.config.html_sanitizer_css_properties_whitelist = { border-right-color border-bottom-color border-left-color - ), - 'th' => %w( + ], + 'th' => %w[ background background-color color font-size vertical-align margin margin-top margin-right margin-bottom margin-left padding padding-top padding-right padding-bottom padding-left @@ -90,8 +90,8 @@ Rails.application.config.html_sanitizer_css_properties_whitelist = { border-right-color border-bottom-color border-left-color - ), - 'tr' => %w( + ], + 'tr' => %w[ background background-color color font-size vertical-align margin margin-top margin-right margin-bottom margin-left padding padding-top padding-right padding-bottom padding-left @@ -107,8 +107,8 @@ Rails.application.config.html_sanitizer_css_properties_whitelist = { border-right-color border-bottom-color border-left-color - ), - 'td' => %w( + ], + 'td' => %w[ background background-color color font-size vertical-align margin margin-top margin-right margin-bottom margin-left padding padding-top padding-right padding-bottom padding-left @@ -124,5 +124,5 @@ Rails.application.config.html_sanitizer_css_properties_whitelist = { border-right-color border-bottom-color border-left-color - ), + ], } diff --git a/config/initializers/vendor_lib.rb b/config/initializers/vendor_lib.rb index 9367719f8..73c004c7a 100644 --- a/config/initializers/vendor_lib.rb +++ b/config/initializers/vendor_lib.rb @@ -1,5 +1,5 @@ # load all vendor/lib extentions -Dir["#{Rails.root}/vendor/lib/*"].each do |file| +Dir[ Rails.root.join('vendor', 'lib', '*') ].each do |file| if File.file?(file) require file end diff --git a/config/routes.rb b/config/routes.rb index 93f9a8a50..d523fcc7f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -18,6 +18,6 @@ Rails.application.routes.draw do end end - match '*a', to: 'errors#routing', via: [:get, :post, :put, :delete] + match '*a', to: 'errors#routing', via: %i[get post put delete] end diff --git a/config/routes/auth.rb b/config/routes/auth.rb index 50a77195f..b6fc7a6ea 100644 --- a/config/routes/auth.rb +++ b/config/routes/auth.rb @@ -2,15 +2,15 @@ Zammad::Application.routes.draw do api_path = Rails.configuration.api_path # omniauth - match '/auth/:provider/callback', to: 'sessions#create_omniauth', via: [:post, :get, :puts, :delete] + match '/auth/:provider/callback', to: 'sessions#create_omniauth', via: %i[post get puts delete] # sso - match '/auth/sso', to: 'sessions#create_sso', via: [:post, :get] + match '/auth/sso', to: 'sessions#create_sso', via: %i[post get] # sessions match api_path + '/signin', to: 'sessions#create', via: :post - match api_path + '/signshow', to: 'sessions#show', via: [:get, :post] - match api_path + '/signout', to: 'sessions#destroy', via: [:get, :delete] + match api_path + '/signshow', to: 'sessions#show', via: %i[get post] + match api_path + '/signout', to: 'sessions#destroy', via: %i[get delete] match api_path + '/available', to: 'sessions#available', via: :get diff --git a/config/routes/message.rb b/config/routes/message.rb index 67b748557..6d44d39f5 100644 --- a/config/routes/message.rb +++ b/config/routes/message.rb @@ -2,7 +2,7 @@ Zammad::Application.routes.draw do api_path = Rails.configuration.api_path # messages - match api_path + '/message_send', to: 'long_polling#message_send', via: [ :get, :post ] - match api_path + '/message_receive', to: 'long_polling#message_receive', via: [ :get, :post ] + match api_path + '/message_send', to: 'long_polling#message_send', via: %i[get post] + match api_path + '/message_receive', to: 'long_polling#message_receive', via: %i[get post] end diff --git a/config/routes/organization.rb b/config/routes/organization.rb index b7c95c5d2..930a6c393 100644 --- a/config/routes/organization.rb +++ b/config/routes/organization.rb @@ -2,7 +2,7 @@ Zammad::Application.routes.draw do api_path = Rails.configuration.api_path # organizations - match api_path + '/organizations/search', to: 'organizations#search', via: [:get, :post] + match api_path + '/organizations/search', to: 'organizations#search', via: %i[get post] match api_path + '/organizations', to: 'organizations#index', via: :get match api_path + '/organizations/:id', to: 'organizations#show', via: :get match api_path + '/organizations', to: 'organizations#create', via: :post diff --git a/config/routes/report.rb b/config/routes/report.rb index 8fe454b89..5784d775b 100644 --- a/config/routes/report.rb +++ b/config/routes/report.rb @@ -4,7 +4,7 @@ Zammad::Application.routes.draw do # reports match api_path + '/reports/config', to: 'reports#reporting_config', via: :get match api_path + '/reports/generate', to: 'reports#generate', via: :post - match api_path + '/reports/sets', to: 'reports#sets', via: [:post, :get] + match api_path + '/reports/sets', to: 'reports#sets', via: %i[post get] # report_profiles match api_path + '/report_profiles', to: 'report_profiles#index', via: :get diff --git a/config/routes/search.rb b/config/routes/search.rb index fd4b0fb94..df1382f79 100644 --- a/config/routes/search.rb +++ b/config/routes/search.rb @@ -2,6 +2,6 @@ Zammad::Application.routes.draw do api_path = Rails.configuration.api_path # search - match api_path + '/search', to: 'search#search_generic', via: [:get, :post] - match api_path + '/search/:objects', to: 'search#search_generic', via: [:get, :post] + match api_path + '/search', to: 'search#search_generic', via: %i[get post] + match api_path + '/search/:objects', to: 'search#search_generic', via: %i[get post] end diff --git a/config/routes/ticket.rb b/config/routes/ticket.rb index 6dc60d9b3..0e12e66e0 100644 --- a/config/routes/ticket.rb +++ b/config/routes/ticket.rb @@ -2,7 +2,7 @@ Zammad::Application.routes.draw do api_path = Rails.configuration.api_path # tickets - match api_path + '/tickets/search', to: 'tickets#search', via: [:get, :post] + match api_path + '/tickets/search', to: 'tickets#search', via: %i[get post] match api_path + '/tickets/selector', to: 'tickets#selector', via: :post match api_path + '/tickets', to: 'tickets#index', via: :get match api_path + '/tickets/:id', to: 'tickets#show', via: :get diff --git a/config/routes/user.rb b/config/routes/user.rb index 0f66db41c..7847d30e3 100644 --- a/config/routes/user.rb +++ b/config/routes/user.rb @@ -2,8 +2,8 @@ Zammad::Application.routes.draw do api_path = Rails.configuration.api_path # users - match api_path + '/users/search', to: 'users#search', via: [:get, :post] - match api_path + '/users/recent', to: 'users#recent', via: [:get, :post] + match api_path + '/users/search', to: 'users#search', via: %i[get post] + match api_path + '/users/recent', to: 'users#recent', via: %i[get post] match api_path + '/users/password_reset', to: 'users#password_reset_send', via: :post match api_path + '/users/password_reset_verify', to: 'users#password_reset_verify', via: :post match api_path + '/users/password_change', to: 'users#password_change', via: :post diff --git a/db/migrate/20120101000001_create_base.rb b/db/migrate/20120101000001_create_base.rb index ea1212388..c74f50836 100644 --- a/db/migrate/20120101000001_create_base.rb +++ b/db/migrate/20120101000001_create_base.rb @@ -58,7 +58,7 @@ class CreateBase < ActiveRecord::Migration[4.2] add_index :users, [:phone] add_index :users, [:fax] add_index :users, [:mobile] - add_index :users, [:out_of_office, :out_of_office_start_at, :out_of_office_end_at], name: 'index_out_of_office' + add_index :users, %i[out_of_office out_of_office_start_at out_of_office_end_at], name: 'index_out_of_office' add_index :users, [:out_of_office_replacement_id] add_index :users, [:source] add_index :users, [:created_by_id] @@ -207,7 +207,7 @@ class CreateBase < ActiveRecord::Migration[4.2] t.references :user, null: false t.timestamps limit: 3, null: false end - add_index :authorizations, [:uid, :provider] + add_index :authorizations, %i[uid provider] add_index :authorizations, [:user_id] add_index :authorizations, [:username] add_foreign_key :authorizations, :users @@ -262,7 +262,7 @@ class CreateBase < ActiveRecord::Migration[4.2] t.timestamps limit: 3, null: false end add_index :tokens, :user_id - add_index :tokens, [:name, :action], unique: true + add_index :tokens, %i[name action], unique: true add_index :tokens, :created_at add_index :tokens, :persistent add_foreign_key :tokens, :users @@ -454,7 +454,7 @@ class CreateBase < ActiveRecord::Migration[4.2] t.integer :created_by_id, null: false t.timestamps limit: 3, null: false end - add_index :stores, [:store_object_id, :o_id] + add_index :stores, %i[store_object_id o_id] add_foreign_key :stores, :store_objects add_foreign_key :stores, :store_files add_foreign_key :stores, :users, column: :created_by_id @@ -481,7 +481,7 @@ class CreateBase < ActiveRecord::Migration[4.2] t.integer :created_by_id, null: false t.timestamps limit: 3, null: false end - add_index :avatars, [:o_id, :object_lookup_id] + add_index :avatars, %i[o_id object_lookup_id] add_index :avatars, [:store_hash] add_index :avatars, [:source] add_index :avatars, [:default] @@ -556,7 +556,7 @@ class CreateBase < ActiveRecord::Migration[4.2] t.timestamps limit: 3, null: false end add_index :user_devices, [:user_id] - add_index :user_devices, [:os, :browser, :location] + add_index :user_devices, %i[os browser location] add_index :user_devices, [:fingerprint] add_index :user_devices, [:updated_at] add_index :user_devices, [:created_at] @@ -587,7 +587,7 @@ class CreateBase < ActiveRecord::Migration[4.2] t.integer :updated_by_id, null: false t.timestamps limit: 3, null: false end - add_index :object_manager_attributes, [:object_lookup_id, :name], unique: true + add_index :object_manager_attributes, %i[object_lookup_id name], unique: true add_index :object_manager_attributes, [:object_lookup_id] add_foreign_key :object_manager_attributes, :object_lookups add_foreign_key :object_manager_attributes, :users, column: :created_by_id @@ -606,7 +606,7 @@ class CreateBase < ActiveRecord::Migration[4.2] t.timestamps limit: 3, null: false end - add_index :delayed_jobs, [:priority, :run_at], name: 'delayed_jobs_priority' + add_index :delayed_jobs, %i[priority run_at], name: 'delayed_jobs_priority' create_table :external_syncs do |t| t.string :source, limit: 100, null: false @@ -616,9 +616,9 @@ class CreateBase < ActiveRecord::Migration[4.2] t.text :last_payload, limit: 500.kilobytes + 1, null: true t.timestamps limit: 3, null: false end - add_index :external_syncs, [:source, :source_id], unique: true - add_index :external_syncs, [:source, :source_id, :object, :o_id], name: 'index_external_syncs_on_source_and_source_id_and_object_o_id' - add_index :external_syncs, [:object, :o_id] + add_index :external_syncs, %i[source source_id], unique: true + add_index :external_syncs, %i[source source_id object o_id], name: 'index_external_syncs_on_source_and_source_id_and_object_o_id' + add_index :external_syncs, %i[object o_id] create_table :import_jobs do |t| t.string :name, limit: 250, null: false @@ -664,9 +664,9 @@ class CreateBase < ActiveRecord::Migration[4.2] t.timestamps limit: 3, null: false end add_index :cti_caller_ids, [:caller_id] - add_index :cti_caller_ids, [:caller_id, :level] - add_index :cti_caller_ids, [:caller_id, :user_id] - add_index :cti_caller_ids, [:object, :o_id] + add_index :cti_caller_ids, %i[caller_id level] + add_index :cti_caller_ids, %i[caller_id user_id] + add_index :cti_caller_ids, %i[object o_id] add_foreign_key :cti_caller_ids, :users create_table :stats_stores do |t| diff --git a/db/migrate/20120101000010_create_ticket.rb b/db/migrate/20120101000010_create_ticket.rb index c7d5c9bbb..726eede9f 100644 --- a/db/migrate/20120101000010_create_ticket.rb +++ b/db/migrate/20120101000010_create_ticket.rb @@ -127,8 +127,8 @@ class CreateTicket < ActiveRecord::Migration[4.2] t.column :created_by_id, :integer, null: false t.timestamps limit: 3, null: false end - add_index :ticket_flags, [:ticket_id, :created_by_id] - add_index :ticket_flags, [:ticket_id, :key] + add_index :ticket_flags, %i[ticket_id created_by_id] + add_index :ticket_flags, %i[ticket_id key] add_index :ticket_flags, [:ticket_id] add_index :ticket_flags, [:created_by_id] add_foreign_key :ticket_flags, :tickets, column: :ticket_id @@ -182,7 +182,7 @@ class CreateTicket < ActiveRecord::Migration[4.2] end add_index :ticket_articles, [:ticket_id] add_index :ticket_articles, [:message_id_md5] - add_index :ticket_articles, [:message_id_md5, :type_id], name: 'index_ticket_articles_message_id_md5_type_id' + add_index :ticket_articles, %i[message_id_md5 type_id], name: 'index_ticket_articles_message_id_md5_type_id' add_index :ticket_articles, [:created_by_id] add_index :ticket_articles, [:created_at] add_index :ticket_articles, [:internal] @@ -202,8 +202,8 @@ class CreateTicket < ActiveRecord::Migration[4.2] t.column :created_by_id, :integer, null: false t.timestamps limit: 3, null: false end - add_index :ticket_article_flags, [:ticket_article_id, :created_by_id], name: 'index_ticket_article_flags_on_articles_id_and_created_by_id' - add_index :ticket_article_flags, [:ticket_article_id, :key] + add_index :ticket_article_flags, %i[ticket_article_id created_by_id], name: 'index_ticket_article_flags_on_articles_id_and_created_by_id' + add_index :ticket_article_flags, %i[ticket_article_id key] add_index :ticket_article_flags, [:ticket_article_id] add_index :ticket_article_flags, [:created_by_id] add_foreign_key :ticket_article_flags, :ticket_articles, column: :ticket_article_id @@ -346,7 +346,7 @@ class CreateTicket < ActiveRecord::Migration[4.2] t.column :link_object_target_value, :integer, null: false t.timestamps limit: 3, null: false end - add_index :links, [:link_object_source_id, :link_object_source_value, :link_object_target_id, :link_object_target_value, :link_type_id], unique: true, name: 'links_uniq_total' + add_index :links, %i[link_object_source_id link_object_source_value link_object_target_id link_object_target_value link_type_id], unique: true, name: 'links_uniq_total' add_foreign_key :links, :link_types create_table :postmaster_filters do |t| @@ -572,7 +572,7 @@ class CreateTicket < ActiveRecord::Migration[4.2] end add_index :karma_activity_logs, [:user_id] add_index :karma_activity_logs, [:created_at] - add_index :karma_activity_logs, [:o_id, :object_lookup_id] + add_index :karma_activity_logs, %i[o_id object_lookup_id] add_foreign_key :karma_activity_logs, :users add_foreign_key :karma_activity_logs, :karma_activities, column: :activity_id end diff --git a/db/migrate/20150979000001_update_timestamps.rb b/db/migrate/20150979000001_update_timestamps.rb index 075a73528..3f476ac0c 100644 --- a/db/migrate/20150979000001_update_timestamps.rb +++ b/db/migrate/20150979000001_update_timestamps.rb @@ -1,7 +1,7 @@ class UpdateTimestamps < ActiveRecord::Migration[4.2] def up # get all models - Models.all.each do |_model, value| + Models.all.each_value do |value| next if !value next if !value[:attributes] if value[:attributes].include?('changed_at') diff --git a/db/migrate/20160217000001_object_manager_update_user.rb b/db/migrate/20160217000001_object_manager_update_user.rb index e9a0f007b..5d307ae1e 100644 --- a/db/migrate/20160217000001_object_manager_update_user.rb +++ b/db/migrate/20160217000001_object_manager_update_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable Lint/BooleanSymbol class ObjectManagerUpdateUser < ActiveRecord::Migration[4.2] def up diff --git a/db/migrate/20161112000001_organization_domain_based_assignment.rb b/db/migrate/20161112000001_organization_domain_based_assignment.rb index 7ca282bc2..afce02e32 100644 --- a/db/migrate/20161112000001_organization_domain_based_assignment.rb +++ b/db/migrate/20161112000001_organization_domain_based_assignment.rb @@ -1,3 +1,4 @@ +# rubocop:disable Lint/BooleanSymbol class OrganizationDomainBasedAssignment < ActiveRecord::Migration[4.2] def up # return if it's a new setup diff --git a/db/migrate/20161117000002_ticket_number_generator_issue_427.rb b/db/migrate/20161117000002_ticket_number_generator_issue_427.rb index 05f440bcb..0200184e2 100644 --- a/db/migrate/20161117000002_ticket_number_generator_issue_427.rb +++ b/db/migrate/20161117000002_ticket_number_generator_issue_427.rb @@ -5,7 +5,7 @@ class TicketNumberGeneratorIssue427 < ActiveRecord::Migration[4.2] setting = Setting.find_by(name: 'ticket_number') setting.preferences = { - settings_included: %w(ticket_number_increment ticket_number_date), + settings_included: %w[ticket_number_increment ticket_number_date], controller: 'SettingsAreaTicketNumber', permission: ['admin.ticket'], } diff --git a/db/migrate/20170403000001_fixed_admin_user_permission_920.rb b/db/migrate/20170403000001_fixed_admin_user_permission_920.rb index 8209aaeb3..c1387387b 100644 --- a/db/migrate/20170403000001_fixed_admin_user_permission_920.rb +++ b/db/migrate/20170403000001_fixed_admin_user_permission_920.rb @@ -1,3 +1,4 @@ +# rubocop:disable Lint/BooleanSymbol class FixedAdminUserPermission920 < ActiveRecord::Migration[4.2] def up @@ -337,7 +338,10 @@ class FixedAdminUserPermission920 < ActiveRecord::Migration[4.2] display: 'Visibility', data_type: 'select', data_option: { - options: { true: 'internal', false: 'public' }, + options: { + true: 'internal', + false: 'public' + }, nulloption: false, multiple: false, null: true, diff --git a/db/migrate/20170419000001_ldap_support.rb b/db/migrate/20170419000001_ldap_support.rb index 1429e6627..5984098d0 100644 --- a/db/migrate/20170419000001_ldap_support.rb +++ b/db/migrate/20170419000001_ldap_support.rb @@ -32,7 +32,7 @@ class LdapSupport < ActiveRecord::Migration[4.2] }, state: { adapter: 'Auth::Ldap', - login_attributes: %w(login email), + login_attributes: %w[login email], }, frontend: false ) diff --git a/db/migrate/20170531144425_foreign_keys.rb b/db/migrate/20170531144425_foreign_keys.rb index 696b79134..71699646e 100644 --- a/db/migrate/20170531144425_foreign_keys.rb +++ b/db/migrate/20170531144425_foreign_keys.rb @@ -13,7 +13,7 @@ class ForeignKeys < ActiveRecord::Migration[4.2] # add missing foreign keys foreign_keys = [ # Base - [:users, :organizations], + %i[users organizations], [:users, :users, column: :created_by_id], [:users, :users, column: :updated_by_id], @@ -23,8 +23,8 @@ class ForeignKeys < ActiveRecord::Migration[4.2] [:email_addresses, :users, column: :created_by_id], [:email_addresses, :users, column: :updated_by_id], - [:groups, :signatures], - [:groups, :email_addresses], + %i[groups signatures], + %i[groups email_addresses], [:groups, :users, column: :created_by_id], [:groups, :users, column: :updated_by_id], @@ -34,29 +34,29 @@ class ForeignKeys < ActiveRecord::Migration[4.2] [:organizations, :users, column: :created_by_id], [:organizations, :users, column: :updated_by_id], - [:roles_users, :users], - [:roles_users, :roles], + %i[roles_users users], + %i[roles_users roles], - [:groups_users, :users], - [:groups_users, :groups], + %i[groups_users users], + %i[groups_users groups], - [:organizations_users, :users], - [:organizations_users, :organizations], + %i[organizations_users users], + %i[organizations_users organizations], - [:authorizations, :users], + %i[authorizations users], [:translations, :users, column: :created_by_id], [:translations, :users, column: :updated_by_id], - [:tokens, :users], + %i[tokens users], [:packages, :users, column: :created_by_id], [:packages, :users, column: :updated_by_id], - [:taskbars, :users], + %i[taskbars users], - [:tags, :tag_items], - [:tags, :tag_objects], + %i[tags tag_items], + %i[tags tag_objects], [:tags, :users, column: :created_by_id], [:recent_views, :object_lookups, column: :recent_view_object_id], @@ -64,17 +64,17 @@ class ForeignKeys < ActiveRecord::Migration[4.2] [:activity_streams, :type_lookups, column: :activity_stream_type_id], [:activity_streams, :object_lookups, column: :activity_stream_object_id], - [:activity_streams, :permissions], - [:activity_streams, :groups], + %i[activity_streams permissions], + %i[activity_streams groups], [:activity_streams, :users, column: :created_by_id], - [:histories, :history_types], - [:histories, :history_objects], - [:histories, :history_attributes], + %i[histories history_types], + %i[histories history_objects], + %i[histories history_attributes], [:histories, :users, column: :created_by_id], - [:stores, :store_objects], - [:stores, :store_files], + %i[stores store_objects], + %i[stores store_files], [:stores, :users, column: :created_by_id], [:avatars, :users, column: :created_by_id], @@ -89,13 +89,13 @@ class ForeignKeys < ActiveRecord::Migration[4.2] [:calendars, :users, column: :created_by_id], [:calendars, :users, column: :updated_by_id], - [:user_devices, :users], + %i[user_devices users], - [:object_manager_attributes, :object_lookups], + %i[object_manager_attributes object_lookups], [:object_manager_attributes, :users, column: :created_by_id], [:object_manager_attributes, :users, column: :updated_by_id], - [:cti_caller_ids, :users], + %i[cti_caller_ids users], [:stats_stores, :users, column: :created_by_id], @@ -113,12 +113,12 @@ class ForeignKeys < ActiveRecord::Migration[4.2] [:ticket_priorities, :users, column: :created_by_id], [:ticket_priorities, :users, column: :updated_by_id], - [:tickets, :groups], + %i[tickets groups], [:tickets, :users, column: :owner_id], [:tickets, :users, column: :customer_id], [:tickets, :ticket_priorities, column: :priority_id], [:tickets, :ticket_states, column: :state_id], - [:tickets, :organizations], + %i[tickets organizations], [:tickets, :users, column: :created_by_id], [:tickets, :users, column: :updated_by_id], @@ -131,7 +131,7 @@ class ForeignKeys < ActiveRecord::Migration[4.2] [:ticket_article_senders, :users, column: :created_by_id], [:ticket_article_senders, :users, column: :updated_by_id], - [:ticket_articles, :tickets], + %i[ticket_articles tickets], [:ticket_articles, :ticket_article_types, column: :type_id], [:ticket_articles, :ticket_article_senders, column: :sender_id], [:ticket_articles, :users, column: :created_by_id], @@ -141,21 +141,21 @@ class ForeignKeys < ActiveRecord::Migration[4.2] [:ticket_article_flags, :ticket_articles, column: :ticket_article_id], [:ticket_article_flags, :users, column: :created_by_id], - [:ticket_time_accountings, :tickets], - [:ticket_time_accountings, :ticket_articles], + %i[ticket_time_accountings tickets], + %i[ticket_time_accountings ticket_articles], [:ticket_time_accountings, :users, column: :created_by_id], [:overviews, :users, column: :created_by_id], [:overviews, :users, column: :updated_by_id], - [:overviews_roles, :overviews], - [:overviews_roles, :roles], + %i[overviews_roles overviews], + %i[overviews_roles roles], - [:overviews_users, :overviews], - [:overviews_users, :users], + %i[overviews_users overviews], + %i[overviews_users users], - [:overviews_groups, :overviews], - [:overviews_groups, :groups], + %i[overviews_groups overviews], + %i[overviews_groups groups], [:triggers, :users, column: :created_by_id], [:triggers, :users, column: :updated_by_id], @@ -163,26 +163,26 @@ class ForeignKeys < ActiveRecord::Migration[4.2] [:jobs, :users, column: :created_by_id], [:jobs, :users, column: :updated_by_id], - [:links, :link_types], + %i[links link_types], [:postmaster_filters, :users, column: :created_by_id], [:postmaster_filters, :users, column: :updated_by_id], - [:text_modules, :users], + %i[text_modules users], [:text_modules, :users, column: :created_by_id], [:text_modules, :users, column: :updated_by_id], - [:text_modules_groups, :text_modules], - [:text_modules_groups, :groups], + %i[text_modules_groups text_modules], + %i[text_modules_groups groups], - [:templates, :users], + %i[templates users], [:templates, :users, column: :created_by_id], [:templates, :users, column: :updated_by_id], - [:templates_groups, :templates], - [:templates_groups, :groups], + %i[templates_groups templates], + %i[templates_groups groups], - [:channels, :groups], + %i[channels groups], [:channels, :users, column: :created_by_id], [:channels, :users, column: :updated_by_id], @@ -198,12 +198,12 @@ class ForeignKeys < ActiveRecord::Migration[4.2] [:chat_topics, :users, column: :created_by_id], [:chat_topics, :users, column: :updated_by_id], - [:chat_sessions, :chats], - [:chat_sessions, :users], + %i[chat_sessions chats], + %i[chat_sessions users], [:chat_sessions, :users, column: :created_by_id], [:chat_sessions, :users, column: :updated_by_id], - [:chat_messages, :chat_sessions], + %i[chat_messages chat_sessions], [:chat_messages, :users, column: :created_by_id], [:chat_agents, :users, column: :created_by_id], @@ -212,9 +212,9 @@ class ForeignKeys < ActiveRecord::Migration[4.2] [:report_profiles, :users, column: :created_by_id], [:report_profiles, :users, column: :updated_by_id], - [:karma_users, :users], + %i[karma_users users], - [:karma_activity_logs, :users], + %i[karma_activity_logs users], [:karma_activity_logs, :karma_activities, column: :activity_id], ] diff --git a/db/migrate/20170905140038_cti_log_preferences_migration.rb b/db/migrate/20170905140038_cti_log_preferences_migration.rb index 16c0c3a67..513473a5f 100644 --- a/db/migrate/20170905140038_cti_log_preferences_migration.rb +++ b/db/migrate/20170905140038_cti_log_preferences_migration.rb @@ -42,7 +42,7 @@ class CtiLogPreferencesMigration < ActiveRecord::Migration[5.0] # check from and to keys which hold the instances preferences = {} - %w(from to).each do |direction| + %w[from to].each do |direction| next if item.preferences[direction].blank? # loop over all instances and covert them @@ -57,7 +57,7 @@ class CtiLogPreferencesMigration < ActiveRecord::Migration[5.0] end # update entry - item.update_column(:preferences, preferences) + item.update_column(:preferences, preferences) # rubocop:disable Rails/SkipsModelValidations end end end diff --git a/db/migrate/20170908000001_fixed_twitter_ticket_article_preferences4.rb b/db/migrate/20170908000001_fixed_twitter_ticket_article_preferences4.rb index 76e07c07d..77979d462 100644 --- a/db/migrate/20170908000001_fixed_twitter_ticket_article_preferences4.rb +++ b/db/migrate/20170908000001_fixed_twitter_ticket_article_preferences4.rb @@ -11,7 +11,7 @@ class FixedTwitterTicketArticlePreferences4 < ActiveRecord::Migration[5.0] article = Ticket::Article.find(article_id) next if !article.preferences changed = false - article.preferences.each do |_key, value| + article.preferences.each_value do |value| next if value.class != ActiveSupport::HashWithIndifferentAccess value.each do |sub_key, sub_level| if sub_level.class == NilClass diff --git a/db/migrate/20170910000002_out_of_office2.rb b/db/migrate/20170910000002_out_of_office2.rb index a3819e741..dae2ce062 100644 --- a/db/migrate/20170910000002_out_of_office2.rb +++ b/db/migrate/20170910000002_out_of_office2.rb @@ -15,7 +15,7 @@ class OutOfOffice2 < ActiveRecord::Migration[4.2] add_column :users, :out_of_office_end_at, :date, null: true add_column :users, :out_of_office_replacement_id, :integer, null: true - add_index :users, [:out_of_office, :out_of_office_start_at, :out_of_office_end_at], name: 'index_out_of_office' + add_index :users, %i[out_of_office out_of_office_start_at out_of_office_end_at], name: 'index_out_of_office' add_index :users, [:out_of_office_replacement_id] add_foreign_key :users, :users, column: :out_of_office_replacement_id User.reset_column_information @@ -44,9 +44,9 @@ class OutOfOffice2 < ActiveRecord::Migration[4.2] direction: 'DESC', }, view: { - d: %w(title customer group owner escalation_at), - s: %w(title customer group owner escalation_at), - m: %w(number title customer group owner escalation_at), + d: %w[title customer group owner escalation_at], + s: %w[title customer group owner escalation_at], + m: %w[number title customer group owner escalation_at], view_mode_default: 's', }, updated_by_id: 1, diff --git a/db/migrate/20170912123300_remove_network.rb b/db/migrate/20170912123300_remove_network.rb index f64f646de..fa8f29434 100644 --- a/db/migrate/20170912123300_remove_network.rb +++ b/db/migrate/20170912123300_remove_network.rb @@ -1,3 +1,4 @@ +# rubocop:disable Rails/ReversibleMigration class RemoveNetwork < ActiveRecord::Migration[5.0] # rewinds db/migrate/20120101000020_create_network.rb diff --git a/db/seeds.rb b/db/seeds.rb index 2623896cc..87dbec1e4 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 # This file should contain all the record creation needed to seed the database with its default values. # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). # @@ -13,7 +12,7 @@ Cache.clear # this is the __ordered__ list of seed files # extend only if needed - try to add your changes # to the matching one of the existing files -seeds = %w(settings user_nr_1 signatures roles permissions groups links ticket_state_types ticket_states ticket_priorities ticket_article_types ticket_article_senders macros community_user_resources overviews channels report_profiles chats object_manager_attributes schedulers triggers karma_activities) +seeds = %w[settings user_nr_1 signatures roles permissions groups links ticket_state_types ticket_states ticket_priorities ticket_article_types ticket_article_senders macros community_user_resources overviews channels report_profiles chats object_manager_attributes schedulers triggers karma_activities] # loop and require all seedfiles # files will get executed automatically diff --git a/db/seeds/object_manager_attributes.rb b/db/seeds/object_manager_attributes.rb index 41ab17826..560b3a210 100644 --- a/db/seeds/object_manager_attributes.rb +++ b/db/seeds/object_manager_attributes.rb @@ -1,3 +1,4 @@ +# rubocop:disable Lint/BooleanSymbol ObjectManager::Attribute.add( force: true, object: 'Ticket', @@ -359,7 +360,10 @@ ObjectManager::Attribute.add( display: 'Visibility', data_type: 'select', data_option: { - options: { true: 'internal', false: 'public' }, + options: { + true: 'internal', + false: 'public' + }, nulloption: false, multiple: false, null: true, diff --git a/db/seeds/overviews.rb b/db/seeds/overviews.rb index e274c533b..65db69d50 100644 --- a/db/seeds/overviews.rb +++ b/db/seeds/overviews.rb @@ -19,9 +19,9 @@ Overview.create_if_not_exists( direction: 'ASC', }, view: { - d: %w(title customer group created_at), - s: %w(title customer group created_at), - m: %w(number title customer group created_at), + d: %w[title customer group created_at], + s: %w[title customer group created_at], + m: %w[number title customer group created_at], view_mode_default: 's', }, ) @@ -46,9 +46,9 @@ Overview.create_if_not_exists( direction: 'ASC', }, view: { - d: %w(title customer group created_at), - s: %w(title customer group created_at), - m: %w(number title customer group created_at), + d: %w[title customer group created_at], + s: %w[title customer group created_at], + m: %w[number title customer group created_at], view_mode_default: 's', }, ) @@ -78,9 +78,9 @@ Overview.create_if_not_exists( direction: 'ASC', }, view: { - d: %w(title customer group created_at), - s: %w(title customer group created_at), - m: %w(number title customer group created_at), + d: %w[title customer group created_at], + s: %w[title customer group created_at], + m: %w[number title customer group created_at], view_mode_default: 's', }, ) @@ -101,9 +101,9 @@ Overview.create_if_not_exists( direction: 'ASC', }, view: { - d: %w(title customer group state owner created_at), - s: %w(title customer group state owner created_at), - m: %w(number title customer group state owner created_at), + d: %w[title customer group state owner created_at], + s: %w[title customer group state owner created_at], + m: %w[number title customer group state owner created_at], view_mode_default: 's', }, ) @@ -129,9 +129,9 @@ Overview.create_if_not_exists( direction: 'ASC', }, view: { - d: %w(title customer group owner created_at), - s: %w(title customer group owner created_at), - m: %w(number title customer group owner created_at), + d: %w[title customer group owner created_at], + s: %w[title customer group owner created_at], + m: %w[number title customer group owner created_at], view_mode_default: 's', }, ) @@ -153,9 +153,9 @@ Overview.create_if_not_exists( direction: 'ASC', }, view: { - d: %w(title customer group owner escalation_at), - s: %w(title customer group owner escalation_at), - m: %w(number title customer group owner escalation_at), + d: %w[title customer group owner escalation_at], + s: %w[title customer group owner escalation_at], + m: %w[number title customer group owner escalation_at], view_mode_default: 's', }, ) @@ -181,9 +181,9 @@ Overview.create_if_not_exists( direction: 'DESC', }, view: { - d: %w(title customer group owner escalation_at), - s: %w(title customer group owner escalation_at), - m: %w(number title customer group owner escalation_at), + d: %w[title customer group owner escalation_at], + s: %w[title customer group owner escalation_at], + m: %w[number title customer group owner escalation_at], view_mode_default: 's', }, ) @@ -209,9 +209,9 @@ Overview.create_if_not_exists( direction: 'DESC', }, view: { - d: %w(title customer state created_at), - s: %w(number title state created_at), - m: %w(number title state created_at), + d: %w[title customer state created_at], + s: %w[number title state created_at], + m: %w[number title state created_at], view_mode_default: 's', }, ) @@ -236,9 +236,9 @@ Overview.create_if_not_exists( direction: 'DESC', }, view: { - d: %w(title customer state created_at), - s: %w(number title customer state created_at), - m: %w(number title customer state created_at), + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], view_mode_default: 's', }, ) diff --git a/db/seeds/roles.rb b/db/seeds/roles.rb index a1bd66e6f..5f1a643af 100644 --- a/db/seeds/roles.rb +++ b/db/seeds/roles.rb @@ -25,7 +25,7 @@ Role.create_if_not_exists( name: 'Customer', note: 'People who create Tickets ask for help.', preferences: { - not: %w(Agent Admin), + not: %w[Agent Admin], }, default_at_signup: true, updated_by_id: 1, diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb index decad0c72..25155d67a 100644 --- a/db/seeds/settings.rb +++ b/db/seeds/settings.rb @@ -236,7 +236,7 @@ Setting.create_if_not_exists( title: 'Fully Qualified Domain Name', name: 'fqdn', area: 'System::Base', - description: 'Defines the fully qualified domain name of the system. This setting is used as a variable, #{setting.fqdn} which is found in all forms of messaging used by the application, to build links to the tickets within your system.', + description: 'Defines the fully qualified domain name of the system. This setting is used as a variable, #{setting.fqdn} which is found in all forms of messaging used by the application, to build links to the tickets within your system.', # rubocop:disable Lint/InterpolationCheck options: { form: [ { @@ -792,7 +792,7 @@ Setting.create_if_not_exists( uid: 'mail', base: 'dc=example,dc=org', always_filter: '', - always_roles: %w(Admin Agent), + always_roles: %w[Admin Agent], always_groups: ['Users'], sync_params: { firstname: 'sn', @@ -1580,7 +1580,7 @@ Setting.create_if_not_exists( }, state: 'Ticket::Number::Increment', preferences: { - settings_included: %w(ticket_number_increment ticket_number_date), + settings_included: %w[ticket_number_increment ticket_number_date], controller: 'SettingsAreaTicketNumber', permission: ['admin.ticket'], }, @@ -2056,7 +2056,7 @@ Setting.create_if_not_exists( }, ], }, - state: 'Notification Master ', + state: 'Notification Master ', # rubocop:disable Lint/InterpolationCheck preferences: { online_service_disable: true, permission: ['admin.channel_email'], diff --git a/db/seeds/triggers.rb b/db/seeds/triggers.rb index 247d38314..8b07e0c57 100644 --- a/db/seeds/triggers.rb +++ b/db/seeds/triggers.rb @@ -34,7 +34,7 @@ Trigger.create_or_update(
                  Zammad, your customer support system
                  ', 'recipient' => 'article_last_sender', - 'subject' => 'Thanks for your inquiry (#{ticket.title})', + 'subject' => 'Thanks for your inquiry (#{ticket.title})', # rubocop:disable Lint/InterpolationCheck }, }, active: true, @@ -73,7 +73,7 @@ Trigger.create_or_update(
                  Zammad, your customer support system
                  ', 'recipient' => 'article_last_sender', - 'subject' => 'Thanks for your follow up (#{ticket.title})', + 'subject' => 'Thanks for your follow up (#{ticket.title})', # rubocop:disable Lint/InterpolationCheck }, }, active: false, @@ -101,7 +101,7 @@ Trigger.create_or_update(

                  Zammad, your customer support system

                  ', 'recipient' => 'ticket_customer', - 'subject' => 'Owner has changed (#{ticket.title})', + 'subject' => 'Owner has changed (#{ticket.title})', # rubocop:disable Lint/InterpolationCheck }, }, active: false, diff --git a/lib/application_handle_info.rb b/lib/application_handle_info.rb index 854a9d8b4..78d4586c1 100644 --- a/lib/application_handle_info.rb +++ b/lib/application_handle_info.rb @@ -6,4 +6,9 @@ module ApplicationHandleInfo def self.current=(name) Thread.current[:application_handle] = name end + + def self.postmaster? + return false if current.blank? + current.split('.')[1] == 'postmaster' + end end diff --git a/lib/application_lib.rb b/lib/application_lib.rb index 53ab30f2b..7b3ace036 100644 --- a/lib/application_lib.rb +++ b/lib/application_lib.rb @@ -19,8 +19,7 @@ returns def load_adapter_by_setting(setting) adapter = Setting.get(setting) - return if !adapter - return if adapter.empty? + return if adapter.blank? # load backend load_adapter(adapter) diff --git a/lib/auth/ldap.rb b/lib/auth/ldap.rb index 475cf3be4..95fb38d2e 100644 --- a/lib/auth/ldap.rb +++ b/lib/auth/ldap.rb @@ -13,7 +13,7 @@ class Auth # get from config or fallback to login # for a list of user attributes which should # be used for logging in - login_attributes = @config[:login_attributes] || %w(login) + login_attributes = @config[:login_attributes] || %w[login] authed = login_attributes.any? do |attribute| ldap_user.valid?(user[attribute], password) diff --git a/lib/auto_wizard.rb b/lib/auto_wizard.rb index fd1267b0a..362a0819d 100644 --- a/lib/auto_wizard.rb +++ b/lib/auto_wizard.rb @@ -82,10 +82,8 @@ returns end # set Settings - if auto_wizard_hash['Settings'] - auto_wizard_hash['Settings'].each do |setting_data| - Setting.set(setting_data['name'], setting_data['value']) - end + auto_wizard_hash['Settings']&.each do |setting_data| + Setting.set(setting_data['name'], setting_data['value']) end # create Permissions/Organization @@ -103,32 +101,30 @@ returns end # create Users - if auto_wizard_hash['Users'] - auto_wizard_hash['Users'].each do |user_data| - user_data.symbolize_keys! + auto_wizard_hash['Users']&.each do |user_data| + user_data.symbolize_keys! - if admin_user.id == 1 - if !user_data[:roles] && !user_data[:role_ids] - user_data[:roles] = Role.where(name: %w(Agent Admin)) - end - if !user_data[:groups] && !user_data[:group_ids] - user_data[:groups] = Group.all - end + if admin_user.id == 1 + if !user_data[:roles] && !user_data[:role_ids] + user_data[:roles] = Role.where(name: %w[Agent Admin]) end - - created_user = User.create_or_update_with_ref(user_data) - - # use first created user as admin - next if admin_user.id != 1 - - admin_user = created_user - UserInfo.current_user_id = admin_user.id - - # fetch org logo - if admin_user.email.present? - Service::Image.organization_suggest(admin_user.email) + if !user_data[:groups] && !user_data[:group_ids] + user_data[:groups] = Group.all end end + + created_user = User.create_or_update_with_ref(user_data) + + # use first created user as admin + next if admin_user.id != 1 + + admin_user = created_user + UserInfo.current_user_id = admin_user.id + + # fetch org logo + if admin_user.email.present? + Service::Image.organization_suggest(admin_user.email) + end end # create EmailAddresses/Channels/Signatures @@ -158,7 +154,7 @@ returns def self.file_location auto_wizard_file_name = 'auto_wizard.json' - auto_wizard_file_location = "#{Rails.root}/#{auto_wizard_file_name}" + auto_wizard_file_location = Rails.root.join(auto_wizard_file_name) auto_wizard_file_location end private_class_method :file_location diff --git a/lib/calendar_subscriptions.rb b/lib/calendar_subscriptions.rb index fe7686064..1fd8d4ec1 100644 --- a/lib/calendar_subscriptions.rb +++ b/lib/calendar_subscriptions.rb @@ -15,14 +15,13 @@ class CalendarSubscriptions @preferences[ object_name ] = calendar_subscription.state_current[:value] end - return if !@user.preferences[:calendar_subscriptions] - return if @user.preferences[:calendar_subscriptions].empty? + return if @user.preferences[:calendar_subscriptions].blank? @preferences = @preferences.merge(@user.preferences[:calendar_subscriptions]) end def all events_data = [] - @preferences.each do |object_name, _sub_structure| + @preferences.each_key do |object_name| result = generic_call(object_name) events_data = events_data + result end @@ -39,7 +38,7 @@ class CalendarSubscriptions method_name ||= 'all' events_data = [] - if @preferences[ object_name ] && !@preferences[ object_name ].empty? + if @preferences[ object_name ].present? sub_class_name = object_name.to_s.capitalize object = Object.const_get("CalendarSubscriptions::#{sub_class_name}") instance = object.new(@user, @preferences[ object_name ]) diff --git a/lib/calendar_subscriptions/tickets.rb b/lib/calendar_subscriptions/tickets.rb index 571c2c698..10d788591 100644 --- a/lib/calendar_subscriptions/tickets.rb +++ b/lib/calendar_subscriptions/tickets.rb @@ -65,7 +65,7 @@ class CalendarSubscriptions::Tickets operator: 'is', value: Ticket::State.where( state_type_id: Ticket::StateType.where( - name: %w(new open), + name: %w[new open], ), ).map(&:id), }, diff --git a/lib/core_ext/integer.rb b/lib/core_ext/integer.rb deleted file mode 100644 index b3363b5f4..000000000 --- a/lib/core_ext/integer.rb +++ /dev/null @@ -1,17 +0,0 @@ -class Integer - -=begin - - result = 5.empty? - -result - - false - -=end - - def empty? - false - end - -end diff --git a/lib/core_ext/nil_class.rb b/lib/core_ext/nil_class.rb deleted file mode 100644 index 4b4a8878a..000000000 --- a/lib/core_ext/nil_class.rb +++ /dev/null @@ -1,17 +0,0 @@ -class NilClass - -=begin - - result = nil.empty? - -result - - true - -=end - - def empty? - true - end - -end diff --git a/lib/core_ext/open-uri.rb b/lib/core_ext/open-uri.rb index 703b20594..2a262b467 100644 --- a/lib/core_ext/open-uri.rb +++ b/lib/core_ext/open-uri.rb @@ -1,11 +1,12 @@ -# rubocop:disable Style/FileName +# rubocop:disable Naming/FileName +# rubocop:disable Style/CommentedKeyword if Kernel.respond_to?(:open_uri_original_open) module Kernel private # see: https://github.com/ruby/ruby/pull/1675 def open(name, *rest, &block) # :doc: - if name.respond_to?(:open) && !name.method(:open).parameters.empty? + if name.respond_to?(:open) && name.method(:open).parameters.present? name.open(*rest, &block) elsif name.respond_to?(:to_str) && %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} =~ name && diff --git a/lib/core_ext/string.rb b/lib/core_ext/string.rb index 9fda812e3..89c914bf4 100644 --- a/lib/core_ext/string.rb +++ b/lib/core_ext/string.rb @@ -37,7 +37,7 @@ class String def word_wrap(*args) options = args.extract_options! - unless args.blank? + if args.present? options[:line_width] = args[0] || 82 end options.reverse_merge!(line_width: 82) @@ -135,26 +135,26 @@ class String text.gsub!(/\<.+?\>/, '') link_compare = link.dup - if !link_compare.empty? + if link_compare.present? link.strip! link_compare.strip! link_compare.downcase! link_compare.sub!(%r{/$}, '') end text_compare = text.dup - if !text_compare.empty? + if text_compare.present? text.strip! text_compare.strip! text_compare.downcase! text_compare.sub!(%r{/$}, '') end - placeholder = if !link_compare.empty? && text_compare.empty? + placeholder = if link_compare.present? && text_compare.blank? link - elsif link_compare.empty? && !text_compare.empty? + elsif link_compare.blank? && text_compare.present? text elsif link_compare && link_compare =~ /^mailto/i text - elsif !link_compare.empty? && !text_compare.empty? && (link_compare == text_compare || link_compare == "mailto:#{text}".downcase || link_compare == "http://#{text}".downcase) + elsif link_compare.present? && text_compare.present? && (link_compare == text_compare || link_compare == "mailto:#{text}".downcase || link_compare == "http://#{text}".downcase) "######LINKEXT:#{link}/TEXT:#{text}######" elsif text !~ /^http/ "#{text} (######LINKRAW:#{link}######)" @@ -223,7 +223,7 @@ class String pre = $1 content = $2 post = $5 - if content =~ /^www/i + if content.match?(/^www/i) content = "http://#{content}" end placeholder = if content =~ /^(http|https|ftp|tel)/i @@ -239,7 +239,6 @@ class String coder = HTMLEntities.new string = coder.decode(string) rescue - # strip all & < > " string.gsub!('&', '&') string.gsub!('<', '<') @@ -439,7 +438,7 @@ class String # edv hotline schrieb: #map['word-en-de'] = "[^#{marker}].{1,250}\s(wrote|schrieb):" - map.each do |_key, regexp| + map.each_value do |regexp| begin string.sub!(/#{regexp}/) do |placeholder| placeholder = "#{marker}#{placeholder}" diff --git a/lib/email_helper/probe.rb b/lib/email_helper/probe.rb index a62ca50cb..e34937d9b 100644 --- a/lib/email_helper/probe.rb +++ b/lib/email_helper/probe.rb @@ -68,13 +68,13 @@ returns on fail # get mx records, try to find provider based on mx records mx_records = EmailHelper.mx_records(domain) domains = domains.concat(mx_records) - provider_map.each do |_provider, settings| + provider_map.each_value do |settings| domains.each do |domain_to_check| next if domain_to_check !~ /#{settings[:domain]}/i # add folder to config if needed - if !params[:folder].empty? && settings[:inbound] && settings[:inbound][:options] + if params[:folder].present? && settings[:inbound] && settings[:inbound][:options] settings[:inbound][:options][:folder] = params[:folder] end @@ -112,7 +112,7 @@ returns on fail inbound_map.each do |config| # add folder to config if needed - if !params[:folder].empty? && config[:options] + if params[:folder].present? && config[:options] config[:options][:folder] = params[:folder] end @@ -221,13 +221,11 @@ returns on fail # connection test result_inbound = {} begin - require "channel/driver/#{adapter.to_filename}" driver_class = Object.const_get("Channel::Driver::#{adapter.to_classname}") driver_instance = driver_class.new result_inbound = driver_instance.fetch(params[:options], nil, 'check') - rescue => e return { result: 'invalid', @@ -321,7 +319,6 @@ returns on fail # test connection begin - require "channel/driver/#{adapter.to_filename}" driver_class = Object.const_get("Channel::Driver::#{adapter.to_classname}") @@ -331,13 +328,12 @@ returns on fail mail, ) rescue => e - # check if sending email was ok, but mailserver rejected if !subject white_map = { 'Recipient address rejected' => true, } - white_map.each do |key, _message| + white_map.each_key do |key| next if e.message !~ /#{Regexp.escape(key)}/i @@ -363,7 +359,7 @@ returns on fail def self.invalid_field(message_backend) invalid_fields.each do |key, fields| - return fields if message_backend =~ /#{Regexp.escape(key)}/i + return fields if message_backend.match?(/#{Regexp.escape(key)}/i) end {} end @@ -388,7 +384,7 @@ returns on fail def self.translation(message_backend) translations.each do |key, message_human| - return message_human if message_backend =~ /#{Regexp.escape(key)}/i + return message_human if message_backend.match?(/#{Regexp.escape(key)}/i) end nil end diff --git a/lib/encode.rb b/lib/encode.rb index 7f7ee3733..c0ccc5ac6 100644 --- a/lib/encode.rb +++ b/lib/encode.rb @@ -13,14 +13,12 @@ module Encode # validate already existing utf8 strings if charset.casecmp('utf8').zero? || charset.casecmp('utf-8').zero? begin - # return if encoding is valid utf8 = string.dup.force_encoding('UTF-8') return utf8 if utf8.valid_encoding? # try to encode from Windows-1252 to utf8 string.encode!('UTF-8', 'Windows-1252') - rescue EncodingError => e Rails.logger.error "Bad encoding: #{string.inspect}" string = string.encode!('UTF-8', invalid: :replace, undef: :replace, replace: '?') diff --git a/lib/enrichment/clearbit/user.rb b/lib/enrichment/clearbit/user.rb index 85f509abe..6dff9d66e 100644 --- a/lib/enrichment/clearbit/user.rb +++ b/lib/enrichment/clearbit/user.rb @@ -119,7 +119,7 @@ module Enrichment def fetch if !Rails.env.production? - filename = "#{Rails.root}/test/fixtures/clearbit/#{@local_user.email}.json" + filename = Rails.root.join('test', 'fixtures', 'clearbit', "#{@local_user.email}.json") if File.exist?(filename) data = IO.binread(filename) return JSON.parse(data) if data diff --git a/lib/facebook.rb b/lib/facebook.rb index d082a3d37..70af4a206 100644 --- a/lib/facebook.rb +++ b/lib/facebook.rb @@ -142,9 +142,9 @@ result # ignore if value is already set map.each do |target, source| - next if user[target] && !user[target].empty? + next if user[target].present? new_value = tweet_user.send(source).to_s - next if !new_value || new_value.empty? + next if new_value.blank? user_data[target] = new_value end user.update!(user_data) @@ -329,12 +329,11 @@ result def from_article(article) post = nil - if article[:type] == 'facebook feed comment' - Rails.logger.debug 'Create feed comment from article...' - post = @client.put_comment(article[:in_reply_to], article[:body]) - else + if article[:type] != 'facebook feed comment' raise "Can't handle unknown facebook article type '#{article[:type]}'." end + Rails.logger.debug 'Create feed comment from article...' + post = @client.put_comment(article[:in_reply_to], article[:body]) Rails.logger.debug post.inspect @client.get_object(post['id']) end @@ -376,7 +375,7 @@ result Rails.logger.debug comments.inspect result = [] - return result if comments.empty? + return result if comments.blank? comments.each do |comment| user = to_user(comment) diff --git a/lib/fill_db.rb b/lib/fill_db.rb index 87a4e62d0..1ceabdd4f 100644 --- a/lib/fill_db.rb +++ b/lib/fill_db.rb @@ -89,7 +89,7 @@ or if you only want to create 100 tickets ActiveRecord::Base.transaction do suffix = rand(99_999).to_s organization = nil - if !organization_pool.empty? && rand(2) == 1 + if organization_pool.present? && rand(2) == 1 organization = organization_pool[ organization_pool.length - 1 ] end user = User.create_or_update( @@ -137,39 +137,38 @@ or if you only want to create 100 tickets priority_pool = Ticket::Priority.all state_pool = Ticket::State.all - if tickets && !tickets.zero? - (1..tickets).each do - ActiveRecord::Base.transaction do - customer = customer_pool[ rand(customer_pool.length - 1) ] - agent = agent_pool[ rand(agent_pool.length - 1) ] - ticket = Ticket.create!( - title: "some title äöüß#{rand(999_999)}", - group: group_pool[ rand(group_pool.length - 1) ], - customer: customer, - owner: agent, - state: state_pool[ rand(state_pool.length - 1) ], - priority: priority_pool[ rand(priority_pool.length - 1) ], - updated_by_id: agent.id, - created_by_id: agent.id, - ) + return if !tickets || tickets.zero? + (1..tickets).each do + ActiveRecord::Base.transaction do + customer = customer_pool[ rand(customer_pool.length - 1) ] + agent = agent_pool[ rand(agent_pool.length - 1) ] + ticket = Ticket.create!( + title: "some title äöüß#{rand(999_999)}", + group: group_pool[ rand(group_pool.length - 1) ], + customer: customer, + owner: agent, + state: state_pool[ rand(state_pool.length - 1) ], + priority: priority_pool[ rand(priority_pool.length - 1) ], + updated_by_id: agent.id, + created_by_id: agent.id, + ) - # create article - article = Ticket::Article.create!( - ticket_id: ticket.id, - from: customer.email, - to: 'some_recipient@example.com', - subject: "some subject#{rand(999_999)}", - message_id: "some@id-#{rand(999_999)}", - body: 'some message ...', - internal: false, - sender: Ticket::Article::Sender.where(name: 'Customer').first, - type: Ticket::Article::Type.where(name: 'phone').first, - updated_by_id: agent.id, - created_by_id: agent.id, - ) - puts " Ticket #{ticket.number} created" - sleep nice - end + # create article + article = Ticket::Article.create!( + ticket_id: ticket.id, + from: customer.email, + to: 'some_recipient@example.com', + subject: "some subject#{rand(999_999)}", + message_id: "some@id-#{rand(999_999)}", + body: 'some message ...', + internal: false, + sender: Ticket::Article::Sender.where(name: 'Customer').first, + type: Ticket::Article::Type.where(name: 'phone').first, + updated_by_id: agent.id, + created_by_id: agent.id, + ) + puts " Ticket #{ticket.number} created" + sleep nice end end end diff --git a/lib/html_sanitizer.rb b/lib/html_sanitizer.rb index 67e0b046b..4d1d73ac8 100644 --- a/lib/html_sanitizer.rb +++ b/lib/html_sanitizer.rb @@ -17,7 +17,7 @@ satinize html string based on whiltelist attributes_whitelist = Rails.configuration.html_sanitizer_attributes_whitelist css_properties_whitelist = Rails.configuration.html_sanitizer_css_properties_whitelist classes_whitelist = ['js-signatureMarker'] - attributes_2_css = %w(width height) + attributes_2_css = %w[width height] # remove html comments string.gsub!(//m, '') @@ -29,7 +29,7 @@ satinize html string based on whiltelist if node['href'].blank? node.replace node.children.to_s Loofah::Scrubber::STOP - elsif ((node.children.empty? || node.children.first.class == Nokogiri::XML::Text) && node.text.present?) || (node.children.size == 1 && node.children.first.content == node.content && node.content.present?) + elsif ((node.children.blank? || node.children.first.class == Nokogiri::XML::Text) && node.text.present?) || (node.children.size == 1 && node.children.first.content == node.content && node.content.present?) if node.text.downcase.start_with?('http', 'ftp', '//') a = Nokogiri::XML::Node.new 'a', node.document a['href'] = node['href'] @@ -54,7 +54,7 @@ satinize html string based on whiltelist end # check if text has urls which need to be clickable - if node && node.name != 'a' && node.parent && node.parent.name != 'a' && (!node.parent.parent || node.parent.parent.name != 'a') + if node&.name != 'a' && node.parent && node.parent.name != 'a' && (!node.parent.parent || node.parent.parent.name != 'a') if node.class == Nokogiri::XML::Text urls = [] node.content.scan(%r{((http|https|ftp|tel)://.+?)([[:space:]]|\.[[:space:]]|,[[:space:]]|\.$|,$|\)|\(|$)}mxi).each do |match| @@ -172,7 +172,7 @@ satinize html string based on whiltelist end # scan for invalid link content - %w(href style).each do |attribute_name| + %w[href style].each do |attribute_name| next if !node[attribute_name] href = cleanup_target(node[attribute_name]) next if href !~ /(javascript|livescript|vbscript):/i @@ -180,9 +180,9 @@ satinize html string based on whiltelist end # remove attributes if not whitelisted - node.each do |attribute, _value| + node.each do |attribute, _value| # rubocop:disable Performance/HashEachMethods attribute_name = attribute.downcase - next if attributes_whitelist[:all].include?(attribute_name) || (attributes_whitelist[node.name] && attributes_whitelist[node.name].include?(attribute_name)) + next if attributes_whitelist[:all].include?(attribute_name) || (attributes_whitelist[node.name]&.include?(attribute_name)) node.delete(attribute) end @@ -241,7 +241,7 @@ cleanup html string: def self.cleanup_replace_tags(string) #return string - tags_backlist = %w(span center) + tags_backlist = %w[span center] scrubber = Loofah::Scrubber.new do |node| next if !tags_backlist.include?(node.name) hit = false @@ -265,11 +265,11 @@ cleanup html string: def self.cleanup_structure(string, type = 'all') remove_empty_nodes = if type == 'pre' - %w(span) + %w[span] else - %w(p div span small table) + %w[p div span small table] end - remove_empty_last_nodes = %w(b i u small table) + remove_empty_last_nodes = %w[b i u small table] # remove last empty nodes and empty -not needed- parrent nodes scrubber_structure = Loofah::Scrubber.new do |node| @@ -357,7 +357,7 @@ cleanup html string: pre = $1 post = $2 - if url =~ /^www/i + if url.match?(/^www/i) url = "http://#{url}" end @@ -379,6 +379,8 @@ cleanup html string: return if post.blank? add_link(post, urls, a) end + + true end def self.html_decode(string) @@ -386,20 +388,20 @@ cleanup html string: end def self.cleanup_target(string) - string = URI.unescape(string).encode('utf-8', 'binary', invalid: :replace, undef: :replace, replace: '?') + string = CGI.unescape(string).encode('utf-8', 'binary', invalid: :replace, undef: :replace, replace: '?') string.gsub(/[[:space:]]|\t|\n|\r/, '').gsub(%r{/\*.*?\*/}, '').gsub(//, '').gsub(/\[.+?\]/, '') end def self.url_same?(url_new, url_old) - url_new = URI.unescape(url_new.to_s).encode('utf-8', 'binary', invalid: :replace, undef: :replace, replace: '?').downcase.gsub(%r{/$}, '').gsub(/[[:space:]]|\t|\n|\r/, '').strip - url_old = URI.unescape(url_old.to_s).encode('utf-8', 'binary', invalid: :replace, undef: :replace, replace: '?').downcase.gsub(%r{/$}, '').gsub(/[[:space:]]|\t|\n|\r/, '').strip + url_new = CGI.unescape(url_new.to_s).encode('utf-8', 'binary', invalid: :replace, undef: :replace, replace: '?').downcase.gsub(%r{/$}, '').gsub(/[[:space:]]|\t|\n|\r/, '').strip + url_old = CGI.unescape(url_old.to_s).encode('utf-8', 'binary', invalid: :replace, undef: :replace, replace: '?').downcase.gsub(%r{/$}, '').gsub(/[[:space:]]|\t|\n|\r/, '').strip url_new = html_decode(url_new).sub('/?', '?') url_old = html_decode(url_old).sub('/?', '?') return true if url_new == url_old - return true if "http://#{url_new}" == url_old - return true if "http://#{url_old}" == url_new - return true if "https://#{url_new}" == url_old - return true if "https://#{url_old}" == url_new + return true if url_old == "http://#{url_new}" + return true if url_new == "http://#{url_old}" + return true if url_old == "https://#{url_new}" + return true if url_new == "https://#{url_old}" false end diff --git a/lib/import/base_factory.rb b/lib/import/base_factory.rb index ed8039bd3..26d52ef43 100644 --- a/lib/import/base_factory.rb +++ b/lib/import/base_factory.rb @@ -17,11 +17,9 @@ module Import raise 'Missing import method implementation for this factory' end - def pre_import_hook(_records, *args) - end + def pre_import_hook(_records, *args); end - def post_import_hook(_record, _backend_instance, *args) - end + def post_import_hook(_record, _backend_instance, *args); end def backend_class(_record, *_args) "Import::#{module_name}".constantize diff --git a/lib/import/base_resource.rb b/lib/import/base_resource.rb index 93a57321f..04cb8efad 100644 --- a/lib/import/base_resource.rb +++ b/lib/import/base_resource.rb @@ -2,7 +2,7 @@ module Import class BaseResource include Import::Helper - attr_reader :resource, :remote_id, :errors + attr_reader :resource, :errors def initialize(resource, *args) @action = :unknown @@ -69,7 +69,7 @@ module Import def initialize_associations_states @associations = {} - %i(before after).each do |state| + %i[before after].each do |state| @associations[state] ||= {} end end @@ -236,7 +236,7 @@ module Import def handle_args(_resource, *args) return if !args return if !args.is_a?(Array) - return if args.empty? + return if args.blank? last_arg = args.last return if !last_arg.is_a?(Hash) diff --git a/lib/import/exchange/folder.rb b/lib/import/exchange/folder.rb index 7a4559f0a..3e702bc9c 100644 --- a/lib/import/exchange/folder.rb +++ b/lib/import/exchange/folder.rb @@ -28,7 +28,7 @@ module Import def all # request folders only if neccessary and store the result - @all ||= children(%i(root msgfolderroot publicfoldersroot)) + @all ||= children(%i[root msgfolderroot publicfoldersroot]) end def children(parent_identifiers) diff --git a/lib/import/exchange/item_attributes.rb b/lib/import/exchange/item_attributes.rb index bc0d19ec0..6f414af98 100644 --- a/lib/import/exchange/item_attributes.rb +++ b/lib/import/exchange/item_attributes.rb @@ -24,7 +24,7 @@ module Import def booleanize_values(properties) properties.each do |key, value| if value.is_a?(String) - next if !%w(true false).include?(value) + next if !%w[true false].include?(value) properties[key] = value == 'true' elsif value.is_a?(Hash) properties[key] = booleanize_values(value) @@ -89,7 +89,7 @@ module Import result_key = key if prefix - result_key = if %i(text id).include?(key) && ( !result[result_key] || result[result_key] == value ) + result_key = if %i[text id].include?(key) && ( !result[result_key] || result[result_key] == value ) prefix else "#{prefix}.#{key}".to_sym diff --git a/lib/import/integration_base.rb b/lib/import/integration_base.rb index 45fad7193..40380f5ec 100644 --- a/lib/import/integration_base.rb +++ b/lib/import/integration_base.rb @@ -14,7 +14,7 @@ module Import subclass.extend(Forwardable) # delegate instance methods to the generic class implementations - subclass.delegate [:identifier, :active?, :config, :display_name] => subclass + subclass.delegate %i[identifier active? config display_name] => subclass end # Defines the integration identifier used for diff --git a/lib/import/ldap/user.rb b/lib/import/ldap/user.rb index e5c607a32..aee72561b 100644 --- a/lib/import/ldap/user.rb +++ b/lib/import/ldap/user.rb @@ -86,8 +86,8 @@ module Import return true if resource[:login].blank? # skip resource if only ignored attributes are set - ignored_attributes = %i(login dn created_by_id updated_by_id active) - !resource.except(*ignored_attributes).values.any?(&:present?) + ignored_attributes = %i[login dn created_by_id updated_by_id active] + resource.except(*ignored_attributes).values.none?(&:present?) end def determine_role_ids(resource) @@ -168,7 +168,7 @@ module Import if instance.blank? checked_values = [@remote_id] - %i(login email).each do |attribute| + %i[login email].each do |attribute| check_value = resource[attribute] next if check_value.blank? next if checked_values.include?(check_value) @@ -204,7 +204,7 @@ module Import # we have to manually downcase the login and email # to avoid wrong attribute change detection - %i(login email).each do |attribute| + %i[login email].each do |attribute| next if mapped[attribute].blank? mapped[attribute] = mapped[attribute].downcase end diff --git a/lib/import/ldap/user_factory.rb b/lib/import/ldap/user_factory.rb index 3ed6f40ee..7a9c77e75 100644 --- a/lib/import/ldap/user_factory.rb +++ b/lib/import/ldap/user_factory.rb @@ -124,7 +124,7 @@ module Import def self.track_found_remote_ids(backend_instance) remote_id = backend_instance.remote_id(nil) - @deactivation_actions ||= %i(skipped failed) + @deactivation_actions ||= %i[skipped failed] if @deactivation_actions.include?(backend_instance.action) @found_lost_remote_ids.push(remote_id) else diff --git a/lib/import/otrs.rb b/lib/import/otrs.rb index 3223b7327..55e9489f9 100644 --- a/lib/import/otrs.rb +++ b/lib/import/otrs.rb @@ -140,7 +140,7 @@ module Import def import_action(remote_object, args = {}) records = Import::OTRS::Requester.load(remote_object, limit: args[:limit], offset: args[:offset], diff: args[:diff]) - if !records || records.empty? + if records.blank? log '... no more work.' return false end diff --git a/lib/import/otrs/article/attachment_factory.rb b/lib/import/otrs/article/attachment_factory.rb index c68956b4c..45e987486 100644 --- a/lib/import/otrs/article/attachment_factory.rb +++ b/lib/import/otrs/article/attachment_factory.rb @@ -67,7 +67,7 @@ module Import return true if local_attachments.count == attachments.count # get a common ground local_attachments.each(&:delete) - return true if attachments.empty? + return true if attachments.blank? false end diff --git a/lib/import/otrs/article_customer.rb b/lib/import/otrs/article_customer.rb index 1ee83c56a..d4f478730 100644 --- a/lib/import/otrs/article_customer.rb +++ b/lib/import/otrs/article_customer.rb @@ -84,7 +84,7 @@ module Import def parsed_display_name(from) parsed_address = Mail::Address.new(from) return parsed_address.display_name if parsed_address.display_name - return from if parsed_address.comments.empty? + return from if parsed_address.comments.blank? parsed_address.comments[0] rescue from diff --git a/lib/import/otrs/article_customer_factory.rb b/lib/import/otrs/article_customer_factory.rb index 18c22c931..e2db53431 100644 --- a/lib/import/otrs/article_customer_factory.rb +++ b/lib/import/otrs/article_customer_factory.rb @@ -6,7 +6,7 @@ module Import def skip?(record, *_args) return true if record['SenderType'] != 'customer' return true if record['CreatedBy'].to_i != 1 - return true if record['From'].empty? + return true if record['From'].blank? false end end diff --git a/lib/import/otrs/dynamic_field_factory.rb b/lib/import/otrs/dynamic_field_factory.rb index 4d730ce2a..549853c22 100644 --- a/lib/import/otrs/dynamic_field_factory.rb +++ b/lib/import/otrs/dynamic_field_factory.rb @@ -42,12 +42,12 @@ module Import end def supported_field_types - %w(Text TextArea Checkbox DateTime Date Dropdown Multiselect) + %w[Text TextArea Checkbox DateTime Date Dropdown Multiselect] end def skip_fields return @skip_fields if @skip_fields - @skip_fields = %w(ProcessManagementProcessID ProcessManagementActivityID ZammadMigratorChanged ZammadMigratorChangedOld) + @skip_fields = %w[ProcessManagementProcessID ProcessManagementActivityID ZammadMigratorChanged ZammadMigratorChangedOld] end end end diff --git a/lib/import/otrs/history_factory.rb b/lib/import/otrs/history_factory.rb index f0d99dbb9..03d0604dc 100644 --- a/lib/import/otrs/history_factory.rb +++ b/lib/import/otrs/history_factory.rb @@ -22,7 +22,7 @@ module Import end def supported_types - %w(NewTicket StateUpdate Move PriorityUpdate) + %w[NewTicket StateUpdate Move PriorityUpdate] end def check_supported(history) diff --git a/lib/import/otrs/import_stats.rb b/lib/import/otrs/import_stats.rb index 5929bcbee..5a9f84378 100644 --- a/lib/import/otrs/import_stats.rb +++ b/lib/import/otrs/import_stats.rb @@ -42,7 +42,7 @@ module Import end def base_total - sum_stat(%w(Queue State Priority)) + sum_stat(%w[Queue State Priority]) end def user_done @@ -50,7 +50,7 @@ module Import end def user_total - sum_stat(%w(User CustomerUser)) + sum_stat(%w[User CustomerUser]) end def ticket_done @@ -58,7 +58,7 @@ module Import end def ticket_total - sum_stat(%w(Ticket)) + sum_stat(%w[Ticket]) end def sum_stat(objects) diff --git a/lib/import/otrs/requester.rb b/lib/import/otrs/requester.rb index ef504cf71..adbcc4c27 100644 --- a/lib/import/otrs/requester.rb +++ b/lib/import/otrs/requester.rb @@ -27,7 +27,7 @@ module Import def load(object, opts = {}) @cache ||= {} - if opts.empty? && @cache[object] + if opts.blank? && @cache[object] return @cache[object] end @@ -39,7 +39,7 @@ module Import Diff: opts[:diff] ? 1 : 0 ) - return result if !opts.empty? + return result if opts.present? @cache[object] = result @cache[object] end diff --git a/lib/import/otrs/state_factory.rb b/lib/import/otrs/state_factory.rb index aaf59d66f..f62f9b8ce 100644 --- a/lib/import/otrs/state_factory.rb +++ b/lib/import/otrs/state_factory.rb @@ -65,19 +65,19 @@ module Import def update_ticket_state agent_new = ::Ticket::State.where( - state_type_id: ::Ticket::StateType.where.not(name: %w(merged removed)) + state_type_id: ::Ticket::StateType.where.not(name: %w[merged removed]) ).pluck(:id) agent_edit = ::Ticket::State.where( - state_type_id: ::Ticket::StateType.where.not(name: %w(new merged removed)) + state_type_id: ::Ticket::StateType.where.not(name: %w[new merged removed]) ).pluck(:id) customer_new = ::Ticket::State.where( - state_type_id: ::Ticket::StateType.where.not(name: %w(new closed)) + state_type_id: ::Ticket::StateType.where.not(name: %w[new closed]) ).pluck(:id) customer_edit = ::Ticket::State.where( - state_type_id: ::Ticket::StateType.where.not(name: %w(open closed)) + state_type_id: ::Ticket::StateType.where.not(name: %w[open closed]) ).pluck(:id) ticket_state_id = ::ObjectManager::Attribute.get( diff --git a/lib/import/otrs/sys_config_factory.rb b/lib/import/otrs/sys_config_factory.rb index a17abbe06..43c7ba114 100644 --- a/lib/import/otrs/sys_config_factory.rb +++ b/lib/import/otrs/sys_config_factory.rb @@ -21,7 +21,7 @@ module Import private def direct_settings - %w(HttpType SystemID Organization TicketHook) + %w[HttpType SystemID Organization TicketHook] end def direct_copy?(setting) @@ -55,7 +55,7 @@ module Import def postmaster_default?(setting) - relevant_configs = %w(PostmasterDefaultPriority PostmasterDefaultState PostmasterFollowUpState) + relevant_configs = %w[PostmasterDefaultPriority PostmasterDefaultState PostmasterFollowUpState] return false if !relevant_configs.include?(setting['Key']) map = { diff --git a/lib/import/otrs/ticket.rb b/lib/import/otrs/ticket.rb index 45fdd35c0..8af2ef7d8 100644 --- a/lib/import/otrs/ticket.rb +++ b/lib/import/otrs/ticket.rb @@ -87,7 +87,7 @@ module Import def dynamic_fields(ticket) result = {} - ticket.keys.each do |key| + ticket.each_key do |key| key_string = key.to_s @@ -144,7 +144,7 @@ module Import user_id = nil articles.each do |article| next if article['SenderType'] != 'customer' - next if article['From'].empty? + next if article['From'].blank? user = Import::OTRS::ArticleCustomer.find(article) break if !user user_id = user.id @@ -171,7 +171,7 @@ module Import def fix_close_time(ticket) return if ticket['StateType'] != 'closed' return if ticket['Closed'] - return if !ticket['Closed'].empty? + return if ticket['Closed'].present? ticket['Closed'] = ticket['Created'] end end diff --git a/lib/import/otrs/user.rb b/lib/import/otrs/user.rb index f52acdf53..a9392c890 100644 --- a/lib/import/otrs/user.rb +++ b/lib/import/otrs/user.rb @@ -38,7 +38,7 @@ module Import return false if !@local_user # only update roles if different (reduce sql statements) - if user[:role_ids] && user[:role_ids].sort == @local_user.role_ids.sort + if user[:role_ids]&.sort == @local_user.role_ids.sort user.delete(:role_ids) end @@ -143,7 +143,7 @@ module Import def groups_from_otrs_group(role_object, group) result = [] - return result if role_object['GroupIDs'].empty? + return result if role_object['GroupIDs'].blank? permissions = role_object['GroupIDs'][ group['ID'] ] return result if !permissions diff --git a/lib/import/zendesk/import_stats.rb b/lib/import/zendesk/import_stats.rb index d32b084e0..28773ba5c 100644 --- a/lib/import/zendesk/import_stats.rb +++ b/lib/import/zendesk/import_stats.rb @@ -49,7 +49,7 @@ module Import 'Automations' => 0, } - result.each do |object, _score| + result.each_key do |object| result[ object ] = statistic_count(object) end diff --git a/lib/import/zendesk/object_attribute.rb b/lib/import/zendesk/object_attribute.rb index ae8a7fd01..440f2b220 100644 --- a/lib/import/zendesk/object_attribute.rb +++ b/lib/import/zendesk/object_attribute.rb @@ -12,8 +12,7 @@ module Import private - def init_callback(_attribute) - end + def init_callback(_attribute); end def add(object, name, attribute) ObjectManager::Attribute.add( attribute_config(object, name, attribute) ) diff --git a/lib/import/zendesk/ticket/comment.rb b/lib/import/zendesk/ticket/comment.rb index 996920d56..d03f3b657 100644 --- a/lib/import/zendesk/ticket/comment.rb +++ b/lib/import/zendesk/ticket/comment.rb @@ -62,7 +62,7 @@ module Import def import_attachments(comment) attachments = comment.attachments - return if attachments.empty? + return if attachments.blank? Import::Zendesk::Ticket::Comment::AttachmentFactory.import(attachments, @local_article) end end diff --git a/lib/import/zendesk/ticket/comment/attachment_factory.rb b/lib/import/zendesk/ticket/comment/attachment_factory.rb index 4ccc1da85..167ff8192 100644 --- a/lib/import/zendesk/ticket/comment/attachment_factory.rb +++ b/lib/import/zendesk/ticket/comment/attachment_factory.rb @@ -22,7 +22,7 @@ module Import return if local_attachments.count == records.count # get a common ground local_attachments.each(&:delete) - return if records.empty? + return if records.blank? records.each(&import_block) end diff --git a/lib/import/zendesk/user/group.rb b/lib/import/zendesk/user/group.rb index afeb8fe7a..9fe4782a8 100644 --- a/lib/import/zendesk/user/group.rb +++ b/lib/import/zendesk/user/group.rb @@ -14,7 +14,7 @@ module Import def for(user) groups = [] - return groups if mapping[user.id].empty? + return groups if mapping[user.id].blank? mapping[user.id].each do |zendesk_group_id| diff --git a/lib/ldap.rb b/lib/ldap.rb index ae2d50beb..7c0623a9e 100644 --- a/lib/ldap.rb +++ b/lib/ldap.rb @@ -200,11 +200,10 @@ class Ldap method: :simple_tls, } - if !@config[:ssl_verify] - @encryption[:tls_options] = { - verify_mode: OpenSSL::SSL::VERIFY_NONE - } - end + return if @config[:ssl_verify] + @encryption[:tls_options] = { + verify_mode: OpenSSL::SSL::VERIFY_NONE + } end def handle_bind_crendentials diff --git a/lib/ldap/group.rb b/lib/ldap/group.rb index 39bfef153..a1c5419f4 100644 --- a/lib/ldap/group.rb +++ b/lib/ldap/group.rb @@ -58,7 +58,7 @@ class Ldap return {} if filter.blank? groups = {} - @ldap.search(filter, base: base_dn, attributes: %w(dn)) do |entry| + @ldap.search(filter, base: base_dn, attributes: %w[dn]) do |entry| groups[entry.dn.downcase] = entry.dn.downcase end groups @@ -80,7 +80,7 @@ class Ldap filter ||= filter() result = {} - @ldap.search(filter, attributes: %w(dn member memberuid)) do |entry| + @ldap.search(filter, attributes: %w[dn member memberuid]) do |entry| roles = mapping[entry.dn.downcase] next if roles.blank? @@ -140,7 +140,7 @@ class Ldap entry[:memberuid].collect do |uid| dn = nil - @ldap.search("(uid=#{uid})", attributes: %w(dn)) do |user| + @ldap.search("(uid=#{uid})", attributes: %w[dn]) do |user| dn = user.dn end dn diff --git a/lib/ldap/user.rb b/lib/ldap/user.rb index c10cae694..3a7b99a1d 100644 --- a/lib/ldap/user.rb +++ b/lib/ldap/user.rb @@ -11,44 +11,44 @@ class Ldap class User include Ldap::FilterLookup - BLACKLISTED = [ - :admincount, - :accountexpires, - :badpasswordtime, - :badpwdcount, - :countrycode, - :distinguishedname, - :dnshostname, - :dscorepropagationdata, - :instancetype, - :iscriticalsystemobject, - :useraccountcontrol, - :usercertificate, - :objectclass, - :objectcategory, - :objectguid, - :objectsid, - :primarygroupid, - :pwdlastset, - :lastlogoff, - :lastlogon, - :lastlogontimestamp, - :localpolicyflags, - :lockouttime, - :logoncount, - :logonhours, - :'msdfsr-computerreferencebl', - :'msds-supportedencryptiontypes', - :ridsetreferences, - :samaccounttype, - :memberof, - :serverreferencebl, - :serviceprincipalname, - :showinadvancedviewonly, - :usnchanged, - :usncreated, - :whenchanged, - :whencreated, + BLACKLISTED = %i[ + admincount + accountexpires + badpasswordtime + badpwdcount + countrycode + distinguishedname + dnshostname + dscorepropagationdata + instancetype + iscriticalsystemobject + useraccountcontrol + usercertificate + objectclass + objectcategory + objectguid + objectsid + primarygroupid + pwdlastset + lastlogoff + lastlogon + lastlogontimestamp + localpolicyflags + lockouttime + logoncount + logonhours + msdfsr-computerreferencebl + msds-supportedencryptiontypes + ridsetreferences + samaccounttype + memberof + serverreferencebl + serviceprincipalname + showinadvancedviewonly + usnchanged + usncreated + whenchanged + whencreated ].freeze # Returns the uid attribute. @@ -61,7 +61,7 @@ class Ldap # @return [String] The uid attribute. def self.uid_attribute(attributes) result = nil - %i(samaccountname userprincipalname uid dn).each do |attribute| + %i[samaccountname userprincipalname uid dn].each do |attribute| next if attributes[attribute].blank? result = attribute.to_s break diff --git a/lib/mixin/required_sub_paths.rb b/lib/mixin/required_sub_paths.rb index fa436749e..38eb22b13 100644 --- a/lib/mixin/required_sub_paths.rb +++ b/lib/mixin/required_sub_paths.rb @@ -2,7 +2,7 @@ module Mixin module RequiredSubPaths def self.included(_base) - path = caller_locations.first.path + path = caller_locations(1..1).first.path sub_path = File.join(File.dirname(path), File.basename(path, '.rb')) eager_load_recursive(sub_path) end diff --git a/lib/models.rb b/lib/models.rb index f51965948..c9b1c3a99 100644 --- a/lib/models.rb +++ b/lib/models.rb @@ -26,13 +26,13 @@ returns def self.all all = {} - dir = "#{Rails.root}/app/models/" - Dir.glob( "#{dir}**/*.rb" ) do |entry| - next if entry =~ /application_model/i - next if entry =~ %r{channel/}i - next if entry =~ %r{observer/}i - next if entry =~ %r{store/provider/}i - next if entry =~ %r{models/concerns/}i + dir = Rails.root.join('app', 'models').to_s + Dir.glob("#{dir}/**/*.rb" ) do |entry| + next if entry.match?(/application_model/i) + next if entry.match?(%r{channel/}i) + next if entry.match?(%r{observer/}i) + next if entry.match?(%r{store/provider/}i) + next if entry.match?(%r{models/concerns/}i) entry.gsub!(dir, '') entry = entry.to_classname @@ -69,7 +69,7 @@ returns def self.searchable models = [] - all.each do |model_class, _options| + all.each_key do |model_class| next if !model_class next if !model_class.respond_to? :search_preferences models.push model_class @@ -139,7 +139,7 @@ returns # find relations via reflections list.each do |model_class, model_attributes| next if !model_attributes[:reflections] - model_attributes[:reflections].each do |_reflection_key, reflection_value| + model_attributes[:reflections].each_value do |reflection_value| next if reflection_value.macro != :belongs_to col_name = "#{reflection_value.name}_id" @@ -170,7 +170,7 @@ returns # cleanup, remove models with empty references references.each do |k, v| - next if !v.empty? + next if v.present? references.delete(k) end @@ -192,8 +192,8 @@ returns def self.references_total(object_name, object_id) references = references(object_name, object_id) total = 0 - references.each do |_model, model_references| - model_references.each do |_col, count| + references.each_value do |model_references| + model_references.each_value do |count| total += count end end @@ -229,7 +229,7 @@ returns # collect items and attributes to update items_to_update = {} - attributes.each do |attribute, _count| + attributes.each_key do |attribute| Rails.logger.debug "#{object_name}: #{model}.#{attribute}->#{object_id_to_merge}->#{object_id_primary}" model_object.where("#{attribute} = ?", object_id_to_merge).each do |item| if !items_to_update[item.id] @@ -241,9 +241,7 @@ returns # update items ActiveRecord::Base.transaction do - items_to_update.each do |_id, item| - item.save! - end + items_to_update.each_value(&:save!) end end true diff --git a/lib/notification_factory/mailer.rb b/lib/notification_factory/mailer.rb index b949f044e..6472543f8 100644 --- a/lib/notification_factory/mailer.rb +++ b/lib/notification_factory/mailer.rb @@ -62,7 +62,7 @@ returns selected_group_ids = user.preferences['notification_config']['group_ids'] if selected_group_ids.is_a?(Array) hit = nil - if selected_group_ids.empty? + if selected_group_ids.blank? hit = true elsif selected_group_ids[0] == '-' && selected_group_ids.count == 1 hit = true diff --git a/lib/notification_factory/renderer.rb b/lib/notification_factory/renderer.rb index 5e700c876..0a55064cf 100644 --- a/lib/notification_factory/renderer.rb +++ b/lib/notification_factory/renderer.rb @@ -65,14 +65,14 @@ examples how to use object_name = object_methods.shift # if no object is given, just return - return "\#{no such object}" if object_name.empty? + return '#{no such object}' if object_name.blank? # rubocop:disable Lint/InterpolationCheck object_refs = @objects[object_name] || @objects[object_name.to_sym] # if object is not in avalable objects, just return return "\#{#{object_name} / no such object}" if !object_refs # if content of method is a complex datatype, just return - if object_methods.empty? && object_refs.class != String && object_refs.class != Float && object_refs.class != Integer + if object_methods.blank? && object_refs.class != String && object_refs.class != Float && object_refs.class != Integer return "\#{#{key} / no such method}" end object_methods_s = '' diff --git a/lib/report/ticket_moved.rb b/lib/report/ticket_moved.rb index 83df739d4..03f75b546 100644 --- a/lib/report/ticket_moved.rb +++ b/lib/report/ticket_moved.rb @@ -133,8 +133,8 @@ returns end def self.group_attributes(selector, params) + group_id = selector['value'] if selector['operator'] == 'is' - group_id = selector['value'] if params[:params][:type] == 'in' return { id_not_from: group_id, @@ -146,8 +146,7 @@ returns id_not_to: group_id, } end - else - group_id = selector['value'] + elsif selector['operator'] == 'is not' if params[:params][:type] == 'in' return { id_from: group_id, diff --git a/lib/report/ticket_reopened.rb b/lib/report/ticket_reopened.rb index 9046e82d9..ef4affd37 100644 --- a/lib/report/ticket_reopened.rb +++ b/lib/report/ticket_reopened.rb @@ -111,7 +111,7 @@ returns key = 'Report::TicketReopened::StateList' ticket_state_ids = Cache.get( key ) return ticket_state_ids if ticket_state_ids - ticket_state_types = Ticket::StateType.where( name: %w(closed merged removed) ) + ticket_state_types = Ticket::StateType.where( name: %w[closed merged removed] ) ticket_state_ids = [] ticket_state_types.each do |ticket_state_type| ticket_state_type.states.each do |ticket_state| diff --git a/lib/search_index_backend.rb b/lib/search_index_backend.rb index 893a5e920..3d244b16b 100644 --- a/lib/search_index_backend.rb +++ b/lib/search_index_backend.rb @@ -201,7 +201,7 @@ return search result # add * on simple query like "somephrase23" or "attribute: somephrase23" if query.present? query.strip! - if query =~ /^([[:alpha:],0-9]+|[[:alpha:],0-9]+\:\s+[[:alpha:],0-9]+)$/ + if query.match?(/^([[:alpha:],0-9]+|[[:alpha:],0-9]+\:\s+[[:alpha:],0-9]+)$/) query += '*' end end @@ -345,7 +345,7 @@ get count of tickets and tickets which match on selector def self.selector2query(selector, _current_user, aggs_interval, limit) query_must = [] query_must_not = [] - if selector && !selector.empty? + if selector.present? selector.each do |key, data| key_tmp = key.sub(/^.+?\./, '') t = {} @@ -400,10 +400,10 @@ get count of tickets and tickets which match on selector data[:query][:bool] = {} end - if !query_must.empty? + if query_must.present? data[:query][:bool][:must] = query_must end - if !query_must_not.empty? + if query_must_not.present? data[:query][:bool][:must_not] = query_must_not end diff --git a/lib/sequencer/unit/import/common/import_job/sub_sequence/general.rb b/lib/sequencer/unit/import/common/import_job/sub_sequence/general.rb index e581e1782..b0ffeafa1 100644 --- a/lib/sequencer/unit/import/common/import_job/sub_sequence/general.rb +++ b/lib/sequencer/unit/import/common/import_job/sub_sequence/general.rb @@ -55,8 +55,7 @@ class Sequencer end end - def processed(_result) - end + def processed(_result); end end end end diff --git a/lib/sequencer/unit/import/common/model/skip/blank/base.rb b/lib/sequencer/unit/import/common/model/skip/blank/base.rb index d57a10126..70a78da2b 100644 --- a/lib/sequencer/unit/import/common/model/skip/blank/base.rb +++ b/lib/sequencer/unit/import/common/model/skip/blank/base.rb @@ -31,7 +31,7 @@ class Sequencer end def relevant_blank? - !attribute_value.except(*ignore).values.any?(&:present?) + attribute_value.except(*ignore).values.none?(&:present?) end end end diff --git a/lib/sequencer/unit/import/common/model/skip/missing_mandatory/base.rb b/lib/sequencer/unit/import/common/model/skip/missing_mandatory/base.rb index cb327161e..14cf05759 100644 --- a/lib/sequencer/unit/import/common/model/skip/missing_mandatory/base.rb +++ b/lib/sequencer/unit/import/common/model/skip/missing_mandatory/base.rb @@ -32,7 +32,7 @@ class Sequencer def mandatory_missing? values = attribute_value.fetch_values(*mandatory) - !values.any?(&:present?) + values.none?(&:present?) rescue KeyError => e false end diff --git a/lib/sequencer/unit/import/common/model/statistics/mixin/diff.rb b/lib/sequencer/unit/import/common/model/statistics/mixin/diff.rb index 1dd1194c7..7411709bc 100644 --- a/lib/sequencer/unit/import/common/model/statistics/mixin/diff.rb +++ b/lib/sequencer/unit/import/common/model/statistics/mixin/diff.rb @@ -15,7 +15,7 @@ class Sequencer private def actions - %i(skipped created updated unchanged failed deactivated) + %i[skipped created updated unchanged failed deactivated] end def diff diff --git a/lib/sequencer/unit/import/common/user/attributes/downcase.rb b/lib/sequencer/unit/import/common/user/attributes/downcase.rb index c5791e062..eb0a6499c 100644 --- a/lib/sequencer/unit/import/common/user/attributes/downcase.rb +++ b/lib/sequencer/unit/import/common/user/attributes/downcase.rb @@ -10,7 +10,7 @@ class Sequencer uses :mapped def process - %i(login email).each do |attribute| + %i[login email].each do |attribute| next if mapped[attribute].blank? mapped[attribute].downcase! end diff --git a/lib/sequencer/unit/import/exchange/folder_contact/statistics/diff.rb b/lib/sequencer/unit/import/exchange/folder_contact/statistics/diff.rb index 9ed81f27f..55c127d2c 100644 --- a/lib/sequencer/unit/import/exchange/folder_contact/statistics/diff.rb +++ b/lib/sequencer/unit/import/exchange/folder_contact/statistics/diff.rb @@ -28,7 +28,7 @@ class Sequencer private def actions - %i(created updated unchanged skipped failed) + %i[created updated unchanged skipped failed] end end end diff --git a/lib/sequencer/unit/mixin/dynamic_attribute.rb b/lib/sequencer/unit/mixin/dynamic_attribute.rb index e37012d90..df7e1b707 100644 --- a/lib/sequencer/unit/mixin/dynamic_attribute.rb +++ b/lib/sequencer/unit/mixin/dynamic_attribute.rb @@ -1,4 +1,3 @@ -# rubocop:disable Lint/NestedMethodDefinition class Sequencer class Unit module Mixin diff --git a/lib/service/image/zammad.rb b/lib/service/image/zammad.rb index 22c93bcd5..dcae93943 100644 --- a/lib/service/image/zammad.rb +++ b/lib/service/image/zammad.rb @@ -13,7 +13,7 @@ class Service::Image::Zammad email.downcase! - return if email =~ /@example.com$/ + return if email.match?(/@example.com$/) # fetch image response = UserAgent.post( diff --git a/lib/sessions.rb b/lib/sessions.rb index b191013a7..95149cd26 100644 --- a/lib/sessions.rb +++ b/lib/sessions.rb @@ -55,15 +55,14 @@ returns FileUtils.mv(path_tmp, path) # send update to browser - if session && session['id'] - send( - client_id, - { - event: 'ws:login', - data: { success: true }, - } - ) - end + return if !session || session['id'].blank? + send( + client_id, + { + event: 'ws:login', + data: { success: true }, + } + ) end =begin @@ -656,9 +655,10 @@ returns # restart job again if try_run_max > try_count thread_client(client_id, try_count, try_run_time) - else - raise "STOP thread_client for client #{client_id} after #{try_run_max} tries" + return end + + raise "STOP thread_client for client #{client_id} after #{try_run_max} tries" end log('debug', "/LOOP #{client_id} - #{try_count}") end diff --git a/lib/sessions/backend/activity_stream.rb b/lib/sessions/backend/activity_stream.rb index acc463dd0..9a024c502 100644 --- a/lib/sessions/backend/activity_stream.rb +++ b/lib/sessions/backend/activity_stream.rb @@ -17,12 +17,12 @@ class Sessions::Backend::ActivityStream return end - if activity_stream && activity_stream.first && activity_stream.first['created_at'] == @last_change + if activity_stream&.first && activity_stream.first['created_at'] == @last_change return end # update last changed - if activity_stream && activity_stream.first + if activity_stream&.first @last_change = activity_stream.first['created_at'] end diff --git a/lib/sessions/event/base.rb b/lib/sessions/event/base.rb index 936b8e7a9..5c1b659f2 100644 --- a/lib/sessions/event/base.rb +++ b/lib/sessions/event/base.rb @@ -107,7 +107,6 @@ class Sessions::Event::Base # rubocop:enable Rails/Output end - def destroy - end + def destroy; end end diff --git a/lib/sessions/event/login.rb b/lib/sessions/event/login.rb index a4b42c156..2533d08b8 100644 --- a/lib/sessions/event/login.rb +++ b/lib/sessions/event/login.rb @@ -20,7 +20,7 @@ class Sessions::Event::Login < Sessions::Event::Base new_session_data = {} - if session && session.data && session.data['user_id'] + if session&.data && session.data['user_id'] new_session_data = { 'id' => session.data['user_id'], } diff --git a/lib/signature_detection.rb b/lib/signature_detection.rb index 03ed608b1..8361a62ae 100644 --- a/lib/signature_detection.rb +++ b/lib/signature_detection.rb @@ -23,7 +23,7 @@ returns string_list = [] messages.each do |message| - if message[:content_type] =~ %r{text/html}i + if message[:content_type].match?(%r{text/html}i) string_list.push message[:content].html2text(true) next end @@ -78,7 +78,7 @@ returns # define the block size without any difference # except "-" because in this case 1 line is removed to much - match_block_total = diff_string_index + (line =~ /^(\\|\+)/i ? -1 : 0) + match_block_total = diff_string_index + (line.match?(/^(\\|\+)/i) ? -1 : 0) # get string of possible signature, use only the first 10 lines match_max_content = 0 @@ -128,7 +128,7 @@ returns def self.find_signature_line(signature, string, content_type) - if content_type =~ %r{text/html}i + if content_type.match?(%r{text/html}i) string = string.html2text(true) end diff --git a/lib/static_assets.rb b/lib/static_assets.rb index aadb8342d..b9a17e133 100644 --- a/lib/static_assets.rb +++ b/lib/static_assets.rb @@ -121,12 +121,12 @@ returns end # store hash in config - if list && list[0] - file = Store.find(list[0].id) - filelocation = filename(file) - Setting.set('product_logo', filelocation) - return file - end + return if !list || !list[0] + + file = Store.find(list[0].id) + filelocation = filename(file) + Setting.set('product_logo', filelocation) + file end =begin @@ -140,13 +140,13 @@ generate filename based on Store model def self.filename(file) hash = Digest::MD5.hexdigest(file.content) extention = '' - if file.preferences['Content-Type'] =~ /jpg|jpeg/i + if file.preferences['Content-Type'].match?(/jpg|jpeg/i) extention = '.jpg' - elsif file.preferences['Content-Type'] =~ /png/i + elsif file.preferences['Content-Type'].match?(/png/i) extention = '.png' - elsif file.preferences['Content-Type'] =~ /gif/i + elsif file.preferences['Content-Type'].match?(/gif/i) extention = '.gif' - elsif file.preferences['Content-Type'] =~ /svg/i + elsif file.preferences['Content-Type'].match?(/svg/i) extention = '.svg' end "#{hash}#{extention}" @@ -163,8 +163,8 @@ sync image to fs (public/assets/images/hash.png) def self.sync file = read return if !file - path = "#{Rails.root}/public/assets/images/#{filename(file)}" - File.open( path, 'wb' ) do |f| + path = Rails.root.join('public', 'assets', 'images', filename(file)) + File.open(path, 'wb') do |f| f.puts file.content end end diff --git a/lib/stats.rb b/lib/stats.rb index ef0e9cd74..615d49720 100644 --- a/lib/stats.rb +++ b/lib/stats.rb @@ -44,7 +44,7 @@ returns # calculate average backend_average_sum = {} - user_result.each do |_user_id, data| + user_result.each_value do |data| data.each do |backend_model, backend_result| next if !backend_result.key?(:used_for_average) if !backend_average_sum[backend_model] diff --git a/lib/telegram.rb b/lib/telegram.rb index 729dee516..159e5a66e 100644 --- a/lib/telegram.rb +++ b/lib/telegram.rb @@ -35,7 +35,7 @@ returns =end def self.set_webhook(token, callback_url) - if callback_url =~ %r{^http://}i + if callback_url.match?(%r{^http://}i) raise 'webhook url need to start with https://, you use http://' end api = TelegramAPI.new(token) @@ -174,14 +174,14 @@ returns def self.message_id(params) message_id = nil - [:message, :edited_message].each do |key| + %i[message edited_message].each do |key| next if !params[key] next if !params[key][:message_id] message_id = params[key][:message_id] break end if message_id - [:message, :edited_message].each do |key| + %i[message edited_message].each do |key| next if !params[key] next if !params[key][:chat] next if !params[key][:chat][:id] @@ -279,14 +279,14 @@ returns # prepare title title = '-' - [:text, :caption].each do |area| + %i[text caption].each do |area| next if !params[:message] next if !params[:message][area] title = params[:message][area] break end if title == '-' - [:sticker, :photo, :document, :voice].each do |area| + %i[sticker photo document voice].each do |area| begin next if !params[:message] next if !params[:message][area] @@ -304,7 +304,7 @@ returns end # find ticket or create one - state_ids = Ticket::State.where(name: %w(closed merged removed)).pluck(:id) + state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id) ticket = Ticket.where(customer_id: user.id).where.not(state_id: state_ids).order(:updated_at).first if ticket diff --git a/lib/tweet_base.rb b/lib/tweet_base.rb index cc492641a..bf95d3ce4 100644 --- a/lib/tweet_base.rb +++ b/lib/tweet_base.rb @@ -12,11 +12,11 @@ class TweetBase if tweet.class == Twitter::DirectMessage Rails.logger.debug "Twitter sender for dm (#{tweet.id}): found" Rails.logger.debug tweet.sender.inspect - return tweet.sender + tweet.sender elsif tweet.class == Twitter::Tweet Rails.logger.debug "Twitter sender for tweet (#{tweet.id}): found" Rails.logger.debug tweet.user.inspect - return tweet.user + tweet.user else raise "Unknown tweet type '#{tweet.class}'" end @@ -47,9 +47,9 @@ class TweetBase # ignore if value is already set map.each do |target, source| - next if user[target] && !user[target].empty? + next if user[target].present? new_value = tweet_user.send(source).to_s - next if !new_value || new_value.empty? + next if new_value.blank? user_data[target] = new_value end user.update!(user_data) @@ -113,7 +113,7 @@ class TweetBase customer_id: user.id, state: Ticket::State.where.not( state_type_id: Ticket::StateType.where( - name: %w(closed merged removed), + name: %w[closed merged removed], ) ) ) @@ -169,16 +169,14 @@ class TweetBase article_type = 'twitter status' from = "@#{tweet.user.screen_name}" mention_ids = [] - if tweet.user_mentions - tweet.user_mentions.each do |local_user| - if !to - to = '' - else - to += ', ' - end - to += "@#{local_user.screen_name}" - mention_ids.push local_user.id + tweet.user_mentions&.each do |local_user| + if !to + to = '' + else + to += ', ' end + to += "@#{local_user.screen_name}" + mention_ids.push local_user.id end in_reply_to = tweet.in_reply_to_status_id @@ -257,7 +255,6 @@ class TweetBase ticket = existing_article.ticket else begin - # in case of streaming mode, get parent tweet via REST client if @connection_type == 'stream' client = TweetRest.new(@auth) @@ -370,7 +367,7 @@ class TweetBase def preferences_cleanup(preferences) # replace Twitter::NullObject with nill to prevent elasticsearch index issue - preferences.each do |_key, value| + preferences.each_value do |value| next if !value.is_a?(Hash) value.each do |sub_key, sub_level| if sub_level.class == NilClass diff --git a/lib/tweet_stream.rb b/lib/tweet_stream.rb index dc270932c..925d1cfa7 100644 --- a/lib/tweet_stream.rb +++ b/lib/tweet_stream.rb @@ -17,7 +17,7 @@ class TweetStream < TweetBase end def disconnect - if @client && @client.custom_connection_handle + if @client&.custom_connection_handle @client.custom_connection_handle.close end diff --git a/lib/user_agent.rb b/lib/user_agent.rb index 5f8eb98c5..1cc4bc155 100644 --- a/lib/user_agent.rb +++ b/lib/user_agent.rb @@ -271,7 +271,9 @@ returns if proxy =~ /^(.+?):(.+?)$/ proxy_host = $1 proxy_port = $2 - else + end + + if proxy_host.blank? || proxy_port.blank? raise "Invalid proxy address: #{proxy} - expect e.g. proxy.example.com:3128" end @@ -292,7 +294,7 @@ returns http.open_timeout = options[:open_timeout] || 4 http.read_timeout = options[:read_timeout] || 10 - if uri.scheme =~ /https/i + if uri.scheme.match?(/https/i) http.use_ssl = true # @TODO verify_mode should be configurable http.verify_mode = OpenSSL::SSL::VERIFY_NONE @@ -313,10 +315,10 @@ returns def self.set_params(request, params, options) if options[:json] request.add_field('Content-Type', 'application/json') - if !params.empty? + if params.present? request.body = params.to_json end - elsif !params.empty? + elsif params.present? request.set_form_data(params) end request diff --git a/lib/version.rb b/lib/version.rb index 011ba1a3d..847079ca1 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -17,7 +17,7 @@ returns def self.get begin - version = File.read("#{Rails.root}/VERSION") + version = File.read(Rails.root.join('VERSION')) version.strip! rescue => e message = e.to_s diff --git a/script/websocket-server.rb b/script/websocket-server.rb index 8882aacac..6199a089b 100755 --- a/script/websocket-server.rb +++ b/script/websocket-server.rb @@ -169,7 +169,7 @@ EventMachine.run do # check if connection not already exists next if !@clients[client_id] - Sessions.touch(client_id) + Sessions.touch(client_id) # rubocop:disable Rails/SkipsModelValidations @clients[client_id][:last_ping] = Time.now.utc.to_i # spool messages for new connects @@ -211,14 +211,14 @@ EventMachine.run do # websocket log 'notice', "Status: websocket clients: #{@clients.size}" - @clients.each do |client_id, _client| + @clients.each_key do |client_id| log 'notice', 'working...', client_id end # ajax client_list = Sessions.list clients = 0 - client_list.each do |_client_id, client| + client_list.each_value do |client| next if client[:meta][:type] == 'websocket' clients = clients + 1 end @@ -242,7 +242,6 @@ EventMachine.run do log 'notice', 'send data to client', client_id websocket_send(client_id, queue) rescue => e - log 'error', 'problem:' + e.inspect, client_id # disconnect client diff --git a/spec/factories/signature.rb b/spec/factories/signature.rb index 15bc21f3e..296992c1c 100644 --- a/spec/factories/signature.rb +++ b/spec/factories/signature.rb @@ -7,7 +7,7 @@ end FactoryBot.define do factory :signature do name { generate(:test_signature_name) } - body '#{user.firstname} #{user.lastname}'.text2html + body '#{user.firstname} #{user.lastname}'.text2html # rubocop:disable Lint/InterpolationCheck created_by_id 1 updated_by_id 1 end diff --git a/spec/lib/auth/ldap_spec.rb b/spec/lib/auth/ldap_spec.rb index d14294fe5..c3dfdc22f 100644 --- a/spec/lib/auth/ldap_spec.rb +++ b/spec/lib/auth/ldap_spec.rb @@ -30,7 +30,7 @@ RSpec.describe ::Auth::Ldap do instance = described_class.new( adapter: described_class.name, - login_attributes: %w(firstname), + login_attributes: %w[firstname], ) ldap_user = double diff --git a/spec/lib/import/ldap/user_factory_spec.rb b/spec/lib/import/ldap/user_factory_spec.rb index d0ea4b2f3..df091375b 100644 --- a/spec/lib/import/ldap/user_factory_spec.rb +++ b/spec/lib/import/ldap/user_factory_spec.rb @@ -211,7 +211,7 @@ RSpec.describe Import::Ldap::UserFactory do # activate skipping config[:unassigned_users] = 'skip_sync' config[:group_role_map] = { - 'dummy' => %w(1 2), + 'dummy' => %w[1 2], } # group user role mapping @@ -518,7 +518,7 @@ RSpec.describe Import::Ldap::UserFactory do config = { group_filter: '(objectClass=group)', group_role_map: { - group_dn => %w(1 2), + group_dn => %w[1 2], } } diff --git a/spec/lib/import/otrs/state_factory_spec.rb b/spec/lib/import/otrs/state_factory_spec.rb index 1b50b8181..2bf2806a4 100644 --- a/spec/lib/import/otrs/state_factory_spec.rb +++ b/spec/lib/import/otrs/state_factory_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Import::OTRS::StateFactory do it 'updates ObjectManager Ticket state_id and pending_time filter' do - states = %w(new open merged pending_reminder pending_auto_close_p pending_auto_close_n pending_auto_close_p closed_successful closed_unsuccessful closed_successful removed) + states = %w[new open merged pending_reminder pending_auto_close_p pending_auto_close_n pending_auto_close_p closed_successful closed_unsuccessful closed_successful removed] state_backend_param = [] states.each do |state| @@ -110,7 +110,7 @@ RSpec.describe Import::OTRS::StateFactory do context 'changing Ticket::State IDs' do let(:state_backend_param) do - states = %w(new open merged pending_reminder pending_auto_close_p pending_auto_close_n pending_auto_close_p closed_successful closed_unsuccessful closed_successful removed) + states = %w[new open merged pending_reminder pending_auto_close_p pending_auto_close_n pending_auto_close_p closed_successful closed_unsuccessful closed_successful removed] state_backend_param = [] states.each do |state| diff --git a/spec/lib/import/otrs/user_spec.rb b/spec/lib/import/otrs/user_spec.rb index 92423aef6..11f08d9bc 100644 --- a/spec/lib/import/otrs/user_spec.rb +++ b/spec/lib/import/otrs/user_spec.rb @@ -21,8 +21,7 @@ RSpec.describe Import::OTRS::User do start_import_test end - def role_delete_expecations(role_ids) - end + def role_delete_expecations(role_ids); end def load_user_json(file) json_fixture("import/otrs/user/#{file}") diff --git a/spec/models/concerns/has_groups_examples.rb b/spec/models/concerns/has_groups_examples.rb index 331bca593..07ed6dc0c 100644 --- a/spec/models/concerns/has_groups_examples.rb +++ b/spec/models/concerns/has_groups_examples.rb @@ -169,12 +169,12 @@ RSpec.shared_examples 'HasGroups' do context 'access list' do it 'lists access Group IDs' do - result = group_access_instance.group_ids_access(%w(read write)) + result = group_access_instance.group_ids_access(%w[read write]) expect(result).to include(group_read.id) end it "doesn't list for no access" do - result = group_access_instance.group_ids_access(%w(write create)) + result = group_access_instance.group_ids_access(%w[write create]) expect(result).not_to include(group_read.id) end end @@ -223,7 +223,7 @@ RSpec.shared_examples 'HasGroups' do expect do group_access_instance.group_names_access_map = { group_full.name => 'full', - group_read.name => %w(read write), + group_read.name => %w[read write], } end.to change { described_class.group_through.klass.count @@ -309,7 +309,7 @@ RSpec.shared_examples 'HasGroups' do expect do group_access_instance.group_ids_access_map = { group_full.id => 'full', - group_read.id => %w(read write), + group_read.id => %w[read write], } end.to change { described_class.group_through.klass.count @@ -523,11 +523,11 @@ RSpec.shared_examples '#group_access? call' do context 'access list' do it 'checks positive' do - expect(group_access_instance.group_access?(group_parameter, %w(read write))).to be true + expect(group_access_instance.group_access?(group_parameter, %w[read write])).to be true end it 'checks negative' do - expect(group_access_instance.group_access?(group_parameter, %w(write create))).to be false + expect(group_access_instance.group_access?(group_parameter, %w[write create])).to be false end end end @@ -547,11 +547,11 @@ RSpec.shared_examples '.group_access call' do context 'access list' do it 'lists access IDs' do - expect(described_class.group_access(group_parameter, %w(read write))).to include(group_access_instance) + expect(described_class.group_access(group_parameter, %w[read write])).to include(group_access_instance) end it 'excludes non access IDs' do - expect(described_class.group_access(group_parameter, %w(write create))).not_to include(group_access_instance) + expect(described_class.group_access(group_parameter, %w[write create])).not_to include(group_access_instance) end end end diff --git a/spec/models/concerns/has_roles_examples.rb b/spec/models/concerns/has_roles_examples.rb index cc86565e4..1d55d2412 100644 --- a/spec/models/concerns/has_roles_examples.rb +++ b/spec/models/concerns/has_roles_examples.rb @@ -182,12 +182,12 @@ RSpec.shared_examples 'HasRoles' do context 'access list' do it 'lists access Group IDs' do - result = group_access_instance.group_ids_access(%w(read write)) + result = group_access_instance.group_ids_access(%w[read write]) expect(result).to include(group_role.id) end it "doesn't list for no access" do - result = group_access_instance.group_ids_access(%w(write create)) + result = group_access_instance.group_ids_access(%w[write create]) expect(result).not_to include(group_role.id) end @@ -196,7 +196,7 @@ RSpec.shared_examples 'HasRoles' do group_role.name => 'read', } - result = group_access_instance.group_ids_access(%w(read create)) + result = group_access_instance.group_ids_access(%w[read create]) expect(result.uniq).to eq(result) end end @@ -237,11 +237,11 @@ RSpec.shared_examples '#role_access? call' do context 'access list' do it 'checks positive' do - expect(group_access_instance.role_access?(group_parameter, %w(read write))).to be true + expect(group_access_instance.role_access?(group_parameter, %w[read write])).to be true end it 'checks negative' do - expect(group_access_instance.role_access?(group_parameter, %w(write create))).to be false + expect(group_access_instance.role_access?(group_parameter, %w[write create])).to be false end end end @@ -261,11 +261,11 @@ RSpec.shared_examples '.role_access_ids call' do context 'access list' do it 'lists access IDs' do - expect(described_class.role_access_ids(group_parameter, %w(read write))).to include(group_access_instance.id) + expect(described_class.role_access_ids(group_parameter, %w[read write])).to include(group_access_instance.id) end it 'excludes non access IDs' do - expect(described_class.role_access_ids(group_parameter, %w(write create))).not_to include(group_access_instance.id) + expect(described_class.role_access_ids(group_parameter, %w[write create])).not_to include(group_access_instance.id) end end end diff --git a/spec/models/cti/caller_id_spec.rb b/spec/models/cti/caller_id_spec.rb index f7c7a5bd1..8d450ba1f 100644 --- a/spec/models/cti/caller_id_spec.rb +++ b/spec/models/cti/caller_id_spec.rb @@ -5,9 +5,9 @@ RSpec.describe Cti::CallerId do describe 'extract_numbers' do it { expect(described_class.extract_numbers("some text\ntest 123")).to eq [] } it { expect(described_class.extract_numbers('Lorem ipsum dolor sit amet, consectetuer +49 (0) 30 60 00 00 00-0 adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel.')).to eq ['4930600000000'] } - it { expect(described_class.extract_numbers("GS Oberalteich\nTelefon 09422 1000 Telefax 09422 805000\nE-Mail: ")).to eq %w(4994221000 499422805000) } - it { expect(described_class.extract_numbers('Tel +41 81 288 63 93 / +41 76 346 72 14 ...')).to eq %w(41812886393 41763467214) } - it { expect(described_class.extract_numbers("P: +1 (949) 431 0000\nF: +1 (949) 431 0001\nW: http://znuny")).to eq %w(19494310000 19494310001) } + it { expect(described_class.extract_numbers("GS Oberalteich\nTelefon 09422 1000 Telefax 09422 805000\nE-Mail: ")).to eq %w[4994221000 499422805000] } + it { expect(described_class.extract_numbers('Tel +41 81 288 63 93 / +41 76 346 72 14 ...')).to eq %w[41812886393 41763467214] } + it { expect(described_class.extract_numbers("P: +1 (949) 431 0000\nF: +1 (949) 431 0001\nW: http://znuny")).to eq %w[19494310000 19494310001] } end describe 'normalize_number' do diff --git a/spec/models/taskbar_spec.rb b/spec/models/taskbar_spec.rb index 83d5e3e58..346e246d5 100644 --- a/spec/models/taskbar_spec.rb +++ b/spec/models/taskbar_spec.rb @@ -27,7 +27,7 @@ RSpec.describe Taskbar do end it 'state' do - expect(taskbar.state.empty?).to eq(true) + expect(taskbar.state.blank?).to eq(true) end it 'check last_contact' do diff --git a/spec/models/translation_spec.rb b/spec/models/translation_spec.rb index 1c78441eb..642207422 100644 --- a/spec/models/translation_spec.rb +++ b/spec/models/translation_spec.rb @@ -99,7 +99,7 @@ RSpec.describe Translation do it 'check download of locales' do version = Version.get directory = Rails.root.join('config') - file = Rails.root.join("#{directory}/locales-#{version}.yml") + file = Rails.root.join(directory, "locales-#{version}.yml") if File.exist?(file) File.delete(file) end @@ -111,11 +111,11 @@ RSpec.describe Translation do it 'check download of translations' do version = Version.get locale = 'de-de' - directory = Rails.root.join('config/translations') + directory = Rails.root.join('config', 'translations') if File.directory?(directory) FileUtils.rm_rf(directory) end - file = Rails.root.join("#{directory}/#{locale}-#{version}.yml") + file = Rails.root.join(directory, "#{locale}-#{version}.yml") expect(File.exist?(file)).to be false Translation.fetch(locale) expect(File.exist?(file)).to be true diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 2fb7affaa..0e350a2ec 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -21,7 +21,7 @@ require 'rspec/rails' # directory. Alternatively, in the individual `*_spec.rb` files, manually # require only the support files necessary. # -Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } +Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f } # Checks for pending migration and applies them before tests are run. # If you are not using ActiveRecord, you can remove this line. diff --git a/spec/support/system_init.rb b/spec/support/system_init.rb index ac0c986b7..a7d554f83 100644 --- a/spec/support/system_init.rb +++ b/spec/support/system_init.rb @@ -4,12 +4,12 @@ RSpec.configure do |config| email = 'admin@example.com' if !::User.exists?(email: email) FactoryBot.create(:user, - login: 'admin', - firstname: 'Admin', - lastname: 'Admin', - email: email, - password: 'admin', - roles: [Role.lookup(name: 'Admin')],) + login: 'admin', + firstname: 'Admin', + lastname: 'Admin', + email: email, + password: 'admin', + roles: [Role.lookup(name: 'Admin')],) end end end diff --git a/test/browser/aaa_getting_started_test.rb b/test/browser/aaa_getting_started_test.rb index ebe5588bc..6a6857dbe 100644 --- a/test/browser/aaa_getting_started_test.rb +++ b/test/browser/aaa_getting_started_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AaaGettingStartedTest < TestCase @@ -211,7 +211,7 @@ class AaaGettingStartedTest < TestCase } accounts.push account end - if accounts.empty? + if accounts.blank? #raise "Need min. MAILBOX_AUTO1 as ENV variable like export MAILBOX_AUTO1='nicole.braun2015@gmail.com:somepass'" puts "NOTICE: Need min. MAILBOX_AUTO1 as ENV variable like export MAILBOX_AUTO1='nicole.braun2015@gmail.com:somepass'" return @@ -282,7 +282,7 @@ class AaaGettingStartedTest < TestCase } accounts.push account end - if accounts.empty? + if accounts.blank? #raise "Need min. MAILBOX_MANUAL1 as ENV variable like export MAILBOX_MANUAL1='nicole.bauer2015@yahoo.de:somepass:imap.mail.yahoo.com:smtp.mail.yahoo.com'" puts "NOTICE: Need min. MAILBOX_MANUAL1 as ENV variable like export MAILBOX_MANUAL1='nicole.bauer2015@yahoo.de:somepass:imap.mail.yahoo.com:smtp.mail.yahoo.com'" return diff --git a/test/browser/aab_basic_urls_test.rb b/test/browser/aab_basic_urls_test.rb index 83b106611..5bd5838cf 100644 --- a/test/browser/aab_basic_urls_test.rb +++ b/test/browser/aab_basic_urls_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AABBasicUrlsTest < TestCase diff --git a/test/browser/aab_unit_test.rb b/test/browser/aab_unit_test.rb index 94a960793..fab47d2de 100644 --- a/test/browser/aab_unit_test.rb +++ b/test/browser/aab_unit_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AAbUnitTest < TestCase diff --git a/test/browser/aac_basic_richtext_test.rb b/test/browser/aac_basic_richtext_test.rb index 46d276e18..c3e026954 100644 --- a/test/browser/aac_basic_richtext_test.rb +++ b/test/browser/aac_basic_richtext_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AACBasicRichtextTest < TestCase diff --git a/test/browser/abb_one_group_test.rb b/test/browser/abb_one_group_test.rb index 13de33b8a..a24cac5fb 100644 --- a/test/browser/abb_one_group_test.rb +++ b/test/browser/abb_one_group_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AgentTicketActionLevel0Test < TestCase diff --git a/test/browser/admin_channel_email_test.rb b/test/browser/admin_channel_email_test.rb index 78acf93d2..3ff92b526 100644 --- a/test/browser/admin_channel_email_test.rb +++ b/test/browser/admin_channel_email_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AdminChannelEmailTest < TestCase diff --git a/test/browser/admin_object_manager_test.rb b/test/browser/admin_object_manager_test.rb index 0e93c0cd3..691cb5a55 100644 --- a/test/browser/admin_object_manager_test.rb +++ b/test/browser/admin_object_manager_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 +# rubocop:disable Lint/BooleanSymbol require 'browser_test_helper' class AdminObjectManagerTest < TestCase diff --git a/test/browser/admin_overview_test.rb b/test/browser/admin_overview_test.rb index 94a577ea7..87b6791f5 100644 --- a/test/browser/admin_overview_test.rb +++ b/test/browser/admin_overview_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AdminOverviewTest < TestCase diff --git a/test/browser/admin_role_test.rb b/test/browser/admin_role_test.rb index 5c34baffa..da4e862b0 100644 --- a/test/browser/admin_role_test.rb +++ b/test/browser/admin_role_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AdminRoleTest < TestCase diff --git a/test/browser/agent_navigation_and_title_test.rb b/test/browser/agent_navigation_and_title_test.rb index d2b141d8c..499ca87b0 100644 --- a/test/browser/agent_navigation_and_title_test.rb +++ b/test/browser/agent_navigation_and_title_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AgentNavigationAndTitleTest < TestCase diff --git a/test/browser/agent_organization_profile_test.rb b/test/browser/agent_organization_profile_test.rb index 6c8051509..89826290d 100644 --- a/test/browser/agent_organization_profile_test.rb +++ b/test/browser/agent_organization_profile_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AgentOrganizationProfileTest < TestCase diff --git a/test/browser/agent_ticket_attachment_test.rb b/test/browser/agent_ticket_attachment_test.rb index 2e35f5f56..e309ca91c 100644 --- a/test/browser/agent_ticket_attachment_test.rb +++ b/test/browser/agent_ticket_attachment_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AgentTicketAttachmentTest < TestCase diff --git a/test/browser/agent_ticket_email_reply_keep_body_test.rb b/test/browser/agent_ticket_email_reply_keep_body_test.rb index 20f793241..3878d2e85 100644 --- a/test/browser/agent_ticket_email_reply_keep_body_test.rb +++ b/test/browser/agent_ticket_email_reply_keep_body_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AgentTicketEmailReplyKeepBodyTest < TestCase diff --git a/test/browser/agent_ticket_email_signature_test.rb b/test/browser/agent_ticket_email_signature_test.rb index 3e8ad43cb..e5cd13b5f 100644 --- a/test/browser/agent_ticket_email_signature_test.rb +++ b/test/browser/agent_ticket_email_signature_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AgentTicketEmailSignatureTest < TestCase diff --git a/test/browser/agent_ticket_link_test.rb b/test/browser/agent_ticket_link_test.rb index 60769d51d..5eb964a11 100644 --- a/test/browser/agent_ticket_link_test.rb +++ b/test/browser/agent_ticket_link_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AgentTicketLinkTest < TestCase diff --git a/test/browser/agent_ticket_macro_test.rb b/test/browser/agent_ticket_macro_test.rb index aeff466a0..94a5730d8 100644 --- a/test/browser/agent_ticket_macro_test.rb +++ b/test/browser/agent_ticket_macro_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AgentTicketMacroTest < TestCase diff --git a/test/browser/agent_ticket_merge_test.rb b/test/browser/agent_ticket_merge_test.rb index 4582aa2f0..27ed3b6eb 100644 --- a/test/browser/agent_ticket_merge_test.rb +++ b/test/browser/agent_ticket_merge_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AgentTicketMergeTest < TestCase diff --git a/test/browser/agent_ticket_online_notification_test.rb b/test/browser/agent_ticket_online_notification_test.rb index 7300e6560..db0bd9e0e 100644 --- a/test/browser/agent_ticket_online_notification_test.rb +++ b/test/browser/agent_ticket_online_notification_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AgentTicketOnlineNotificationTest < TestCase diff --git a/test/browser/agent_ticket_overview_level0_test.rb b/test/browser/agent_ticket_overview_level0_test.rb index 3385b6dca..8c1fc1e72 100644 --- a/test/browser/agent_ticket_overview_level0_test.rb +++ b/test/browser/agent_ticket_overview_level0_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AgentTicketOverviewLevel0Test < TestCase diff --git a/test/browser/agent_ticket_overview_level1_test.rb b/test/browser/agent_ticket_overview_level1_test.rb index 4200c37a3..2f6ebdc7c 100644 --- a/test/browser/agent_ticket_overview_level1_test.rb +++ b/test/browser/agent_ticket_overview_level1_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AgentTicketOverviewLevel1Test < TestCase diff --git a/test/browser/agent_ticket_overview_tab_test.rb b/test/browser/agent_ticket_overview_tab_test.rb index 80d495d26..b38a524f5 100644 --- a/test/browser/agent_ticket_overview_tab_test.rb +++ b/test/browser/agent_ticket_overview_tab_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AgentTicketOverviewTabTest < TestCase diff --git a/test/browser/agent_ticket_tag_test.rb b/test/browser/agent_ticket_tag_test.rb index 337d2ab13..1d881b5e7 100644 --- a/test/browser/agent_ticket_tag_test.rb +++ b/test/browser/agent_ticket_tag_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AgentTicketTagTest < TestCase diff --git a/test/browser/agent_ticket_text_module_test.rb b/test/browser/agent_ticket_text_module_test.rb index 4a32f5c8b..47324b771 100644 --- a/test/browser/agent_ticket_text_module_test.rb +++ b/test/browser/agent_ticket_text_module_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AgentTicketTextModuleTest < TestCase @@ -101,7 +101,7 @@ class AgentTicketTextModuleTest < TestCase data: { name: 'some name' + random, keywords: random, - content: 'some content #{ticket.customer.lastname}' + random, + content: "some content \#{ticket.customer.lastname}#{random}", }, ) diff --git a/test/browser/agent_ticket_time_accounting_test.rb b/test/browser/agent_ticket_time_accounting_test.rb index 983355938..fcfc2ef84 100644 --- a/test/browser/agent_ticket_time_accounting_test.rb +++ b/test/browser/agent_ticket_time_accounting_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AgentTicketTimeAccountingTest < TestCase diff --git a/test/browser/agent_ticket_update1_test.rb b/test/browser/agent_ticket_update1_test.rb index c758ee841..5f9be5085 100644 --- a/test/browser/agent_ticket_update1_test.rb +++ b/test/browser/agent_ticket_update1_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AgentTicketUpdate1Test < TestCase diff --git a/test/browser/agent_ticket_update2_test.rb b/test/browser/agent_ticket_update2_test.rb index 6b70eac23..6a1c0ace1 100644 --- a/test/browser/agent_ticket_update2_test.rb +++ b/test/browser/agent_ticket_update2_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AgentTicketUpdate2Test < TestCase diff --git a/test/browser/agent_ticket_update3_test.rb b/test/browser/agent_ticket_update3_test.rb index ddf3b9dd7..9dc52755b 100644 --- a/test/browser/agent_ticket_update3_test.rb +++ b/test/browser/agent_ticket_update3_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AgentTicketUpdate3Test < TestCase diff --git a/test/browser/agent_ticket_update_and_reload_test.rb b/test/browser/agent_ticket_update_and_reload_test.rb index 27a5583d1..9f8d3083d 100644 --- a/test/browser/agent_ticket_update_and_reload_test.rb +++ b/test/browser/agent_ticket_update_and_reload_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AgentTicketUpdateAndReloadTest < TestCase diff --git a/test/browser/agent_user_manage_test.rb b/test/browser/agent_user_manage_test.rb index 282dcbd38..4d2160f29 100644 --- a/test/browser/agent_user_manage_test.rb +++ b/test/browser/agent_user_manage_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AgentUserManageTest < TestCase diff --git a/test/browser/agent_user_profile_test.rb b/test/browser/agent_user_profile_test.rb index a53a28566..664a03d0f 100644 --- a/test/browser/agent_user_profile_test.rb +++ b/test/browser/agent_user_profile_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AgentUserProfileTest < TestCase diff --git a/test/browser/auth_test.rb b/test/browser/auth_test.rb index ee09c79cf..2ea4238c5 100644 --- a/test/browser/auth_test.rb +++ b/test/browser/auth_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AuthTest < TestCase diff --git a/test/browser/chat_test.rb b/test/browser/chat_test.rb index 5fa781d37..cd273d4d0 100644 --- a/test/browser/chat_test.rb +++ b/test/browser/chat_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class ChatTest < TestCase diff --git a/test/browser/customer_ticket_create_test.rb b/test/browser/customer_ticket_create_test.rb index c538d420a..e7f15f162 100644 --- a/test/browser/customer_ticket_create_test.rb +++ b/test/browser/customer_ticket_create_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class CustomerTicketCreateTest < TestCase diff --git a/test/browser/first_steps_test.rb b/test/browser/first_steps_test.rb index 2ed6827c2..2506abbda 100644 --- a/test/browser/first_steps_test.rb +++ b/test/browser/first_steps_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class FirstStepsTest < TestCase diff --git a/test/browser/form_test.rb b/test/browser/form_test.rb index 8b6f35a91..140abdb5c 100644 --- a/test/browser/form_test.rb +++ b/test/browser/form_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class FormTest < TestCase diff --git a/test/browser/integration_test.rb b/test/browser/integration_test.rb index d6bb6beb7..3154d012d 100644 --- a/test/browser/integration_test.rb +++ b/test/browser/integration_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class IntegrationTest < TestCase diff --git a/test/browser/keyboard_shortcuts_test.rb b/test/browser/keyboard_shortcuts_test.rb index 6a394e060..c266cb85d 100644 --- a/test/browser/keyboard_shortcuts_test.rb +++ b/test/browser/keyboard_shortcuts_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class KeyboardShortcutsTest < TestCase diff --git a/test/browser/maintenance_app_version_test.rb b/test/browser/maintenance_app_version_test.rb index e8d322cc8..2370bd88c 100644 --- a/test/browser/maintenance_app_version_test.rb +++ b/test/browser/maintenance_app_version_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class MaintenanceAppVersionTest < TestCase diff --git a/test/browser/maintenance_login_message_test.rb b/test/browser/maintenance_login_message_test.rb index d08077dc0..90423b034 100644 --- a/test/browser/maintenance_login_message_test.rb +++ b/test/browser/maintenance_login_message_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class MaintenanceLoginMessageTest < TestCase diff --git a/test/browser/maintenance_mode_test.rb b/test/browser/maintenance_mode_test.rb index a214a5409..f00952423 100644 --- a/test/browser/maintenance_mode_test.rb +++ b/test/browser/maintenance_mode_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class MaintenanceModeTest < TestCase diff --git a/test/browser/maintenance_session_message_test.rb b/test/browser/maintenance_session_message_test.rb index 3df03ae8f..5ea58c808 100644 --- a/test/browser/maintenance_session_message_test.rb +++ b/test/browser/maintenance_session_message_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class MaintenanceSessionMessageTest < TestCase diff --git a/test/browser/manage_test.rb b/test/browser/manage_test.rb index 631114ee4..0ecc13415 100644 --- a/test/browser/manage_test.rb +++ b/test/browser/manage_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class ManageTest < TestCase diff --git a/test/browser/monitoring_test.rb b/test/browser/monitoring_test.rb index d4e79b4db..695c4a87e 100644 --- a/test/browser/monitoring_test.rb +++ b/test/browser/monitoring_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class MonitoringTest < TestCase diff --git a/test/browser/preferences_language_test.rb b/test/browser/preferences_language_test.rb index 2dfa5ef87..59903391a 100644 --- a/test/browser/preferences_language_test.rb +++ b/test/browser/preferences_language_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class PreferencesLanguageTest < TestCase diff --git a/test/browser/preferences_permission_check_test.rb b/test/browser/preferences_permission_check_test.rb index 3723708a3..a08e69af1 100644 --- a/test/browser/preferences_permission_check_test.rb +++ b/test/browser/preferences_permission_check_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class PreferencesPermissionCheckTest < TestCase diff --git a/test/browser/preferences_token_access_test.rb b/test/browser/preferences_token_access_test.rb index 11f153440..d6d97cfee 100644 --- a/test/browser/preferences_token_access_test.rb +++ b/test/browser/preferences_token_access_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class PreferencesTokenAccessTest < TestCase diff --git a/test/browser/setting_test.rb b/test/browser/setting_test.rb index 2cbd02b40..5783bd097 100644 --- a/test/browser/setting_test.rb +++ b/test/browser/setting_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class SettingTest < TestCase diff --git a/test/browser/signup_password_change_and_reset_test.rb b/test/browser/signup_password_change_and_reset_test.rb index c72f1d598..b174f2cb7 100644 --- a/test/browser/signup_password_change_and_reset_test.rb +++ b/test/browser/signup_password_change_and_reset_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class SignupPasswordChangeAndResetTest < TestCase diff --git a/test/browser/switch_to_user_test.rb b/test/browser/switch_to_user_test.rb index 6148dcb05..6c953d111 100644 --- a/test/browser/switch_to_user_test.rb +++ b/test/browser/switch_to_user_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class SwitchToUserTest < TestCase diff --git a/test/browser/taskbar_session_test.rb b/test/browser/taskbar_session_test.rb index 7ef161e0b..f289d0d37 100644 --- a/test/browser/taskbar_session_test.rb +++ b/test/browser/taskbar_session_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class TaskbarSessionTest < TestCase diff --git a/test/browser/taskbar_task_test.rb b/test/browser/taskbar_task_test.rb index 5702f4f4b..959008134 100644 --- a/test/browser/taskbar_task_test.rb +++ b/test/browser/taskbar_task_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class TaskbarTaskTest < TestCase diff --git a/test/browser/translation_test.rb b/test/browser/translation_test.rb index c05041895..be17b75b5 100644 --- a/test/browser/translation_test.rb +++ b/test/browser/translation_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class TranslationTest < TestCase diff --git a/test/browser/user_switch_cache_test.rb b/test/browser/user_switch_cache_test.rb index 7b700c7ab..576fdbb4d 100644 --- a/test/browser/user_switch_cache_test.rb +++ b/test/browser/user_switch_cache_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class UserSwitchCache < TestCase diff --git a/test/browser_test_helper.rb b/test/browser_test_helper.rb index f8f12fb19..909503218 100644 --- a/test/browser_test_helper.rb +++ b/test/browser_test_helper.rb @@ -1,5 +1,5 @@ ENV['RAILS_ENV'] = 'test' -# rubocop:disable HandleExceptions, ClassVars, NonLocalExitFromIterator +# rubocop:disable HandleExceptions, ClassVars, NonLocalExitFromIterator, Style/GuardClause require File.expand_path('../../config/environment', __FILE__) require 'selenium-webdriver' @@ -33,7 +33,7 @@ class TestCase < Test::Unit::TestCase end def browser_support_cookies - if browser =~ /(internet_explorer|ie)/i + if browser.match?(/(internet_explorer|ie)/i) return false end true @@ -47,7 +47,7 @@ class TestCase < Test::Unit::TestCase if !@browsers @browsers = {} end - if !ENV['REMOTE_URL'] || ENV['REMOTE_URL'].empty? + if ENV['REMOTE_URL'].blank? local_browser = Selenium::WebDriver.for(browser.to_sym, profile: profile) @browsers[local_browser.hash] = local_browser browser_instance_preferences(local_browser) @@ -116,7 +116,7 @@ class TestCase < Test::Unit::TestCase def teardown return if !@browsers - @browsers.each do |_hash, local_browser| + @browsers.each_value do |local_browser| screenshot(browser: local_browser, comment: 'teardown') browser_instance_close(local_browser) end @@ -874,14 +874,12 @@ class TestCase < Test::Unit::TestCase instance = params[:browser] || @browser element = instance.find_elements(css: params[:css])[0] - if params[:css] =~ /select/ + if params[:css].match?(/select/) dropdown = Selenium::WebDriver::Support::Select.new(element) success = false - if dropdown.selected_options - dropdown.selected_options.each do |option| - if option.text == params[:value] - success = true - end + dropdown.selected_options&.each do |option| + if option.text == params[:value] + success = true end end if params[:should_not_match] @@ -901,13 +899,12 @@ class TestCase < Test::Unit::TestCase begin text = if params[:attribute] element.attribute(params[:attribute]) - elsif params[:css] =~ /(input|textarea)/i + elsif params[:css].match?(/(input|textarea)/i) element.attribute('value') else element.text end rescue => e - # just try again if !fallback return match(params, true) @@ -928,7 +925,7 @@ class TestCase < Test::Unit::TestCase if text =~ /#{params[:value]}/i match = $1 || true end - elsif text =~ /#{Regexp.quote(params[:value])}/i + elsif text.match?(/#{Regexp.quote(params[:value])}/i) match = true end @@ -1020,7 +1017,6 @@ set type of task (closeTab, closeNextInOverview, stayOnTab) cookies = instance.manage.all_cookies cookies.each do |cookie| - #puts "CCC #{cookie.inspect}" # :name=>"_zammad_session_c25832f4de2", :value=>"adc31cd21615cb0a7ab269184ec8b76f", :path=>"/", :domain=>"localhost", :expires=>nil, :secure=>false} next if cookie[:name] !~ /#{params[:name]}/i @@ -1062,7 +1058,7 @@ set type of task (closeTab, closeNextInOverview, stayOnTab) instance = params[:browser] || @browser title = instance.title - if title =~ /#{params[:value]}/i + if title.match?(/#{params[:value]}/i) assert(true, "matching '#{params[:value]}' in title '#{title}'") else raise "not matching '#{params[:value]}' in title '#{title}'" @@ -1091,11 +1087,10 @@ set type of task (closeTab, closeNextInOverview, stayOnTab) sleep 1 begin - # verify title if data[:title] title = instance.find_elements(css: '.tasks .is-active')[0].text.strip - if title =~ /#{data[:title]}/i + if title.match?(/#{data[:title]}/i) assert(true, "matching '#{data[:title]}' in title '#{title}'") else screenshot(browser: instance, comment: 'verify_task_failed') @@ -1135,7 +1130,6 @@ set type of task (closeTab, closeNextInOverview, stayOnTab) end end rescue => e - # just try again if !fallback verify_task(params, true) @@ -1228,7 +1222,7 @@ set type of task (closeTab, closeNextInOverview, stayOnTab) instance = params[:browser] || @browser params[:files].each do |file| - instance.find_elements(css: params[:css])[0].send_keys "#{Rails.root}/#{file}" + instance.find_elements(css: params[:css])[0].send_keys(Rails.root.join(file)) end sleep 2 * params[:files].count end @@ -1261,7 +1255,6 @@ set type of task (closeTab, closeNextInOverview, stayOnTab) element = instance.find_elements(css: params[:css])[0] if element #&& element.displayed? begin - # watch for selector if !params[:attribute] && !params[:value] assert(true, "'#{params[:css]}' found") @@ -1272,12 +1265,12 @@ set type of task (closeTab, closeNextInOverview, stayOnTab) else text = if params[:attribute] element.attribute(params[:attribute]) - elsif params[:css] =~ /(input|textarea)/i + elsif params[:css].match?(/(input|textarea)/i) element.attribute('value') else element.text end - if text =~ /#{params[:value]}/i + if text.match?(/#{params[:value]}/i) assert(true, "'#{params[:value]}' found in '#{text}'") sleep 0.5 return true @@ -1499,9 +1492,7 @@ wait untill text in selector disabppears instance.mouse.move_to(instance.find_elements(css: '.js-notificationsContainer .js-item:first-child')[0]) sleep 0.1 click_element = instance.find_elements(css: '.js-notificationsContainer .js-item:first-child .js-remove')[0] - if click_element - click_element.click - end + click_element&.click else break end @@ -1530,7 +1521,6 @@ wait untill text in selector disabppears begin instance.find_elements(css: '.search .js-emptySearch')[0].click rescue - # in issues with ff & selenium, sometimes exeption appears # "Element is not currently visible and so may not be interacted with" log('empty_search via js') @@ -1636,23 +1626,21 @@ wait untill text in selector disabppears end end - if data[:selector] - data[:selector].each do |key, value| - select( - browser: instance, - css: '.modal .ticket_selector .js-attributeSelector select', - value: key, - mute_log: true, - ) - sleep 0.5 - select( - browser: instance, - css: '.modal .ticket_selector .js-value select', - value: value, - deselect_all: true, - mute_log: true, - ) - end + data[:selector]&.each do |key, value| + select( + browser: instance, + css: '.modal .ticket_selector .js-attributeSelector select', + value: key, + mute_log: true, + ) + sleep 0.5 + select( + browser: instance, + css: '.modal .ticket_selector .js-value select', + value: value, + deselect_all: true, + mute_log: true, + ) end if data['order::direction'] @@ -1669,7 +1657,7 @@ wait untill text in selector disabppears 11.times do element = instance.find_elements(css: 'body')[0] text = element.text - if text =~ /#{Regexp.quote(data[:name])}/ + if text.match?(/#{Regexp.quote(data[:name])}/) assert(true, 'overview created') overview = { name: name, @@ -1742,23 +1730,21 @@ wait untill text in selector disabppears end end - if data[:selector] - data[:selector].each do |key, value| - select( - browser: instance, - css: '.modal .ticket_selector .js-attributeSelector select', - value: key, - mute_log: true, - ) - sleep 0.5 - select( - browser: instance, - css: '.modal .ticket_selector .js-value select', - value: value, - deselect_all: true, - mute_log: true, - ) - end + data[:selector]&.each do |key, value| + select( + browser: instance, + css: '.modal .ticket_selector .js-attributeSelector select', + value: key, + mute_log: true, + ) + sleep 0.5 + select( + browser: instance, + css: '.modal .ticket_selector .js-value select', + value: value, + deselect_all: true, + mute_log: true, + ) end if data['order::direction'] @@ -1775,7 +1761,7 @@ wait untill text in selector disabppears 11.times do element = instance.find_elements(css: 'body')[0] text = element.text - if text =~ /#{Regexp.quote(data[:name])}/ + if text.match?(/#{Regexp.quote(data[:name])}/) assert(true, 'overview updated') overview = { name: name, @@ -1963,24 +1949,20 @@ wait untill text in selector disabppears end end - if params[:custom_data_select] - params[:custom_data_select].each do |local_key, local_value| - select( - browser: instance, - css: ".content.active .newTicket select[name=\"#{local_key}\"]", - value: local_value, - ) - end + params[:custom_data_select]&.each do |local_key, local_value| + select( + browser: instance, + css: ".content.active .newTicket select[name=\"#{local_key}\"]", + value: local_value, + ) end - if params[:custom_data_input] - params[:custom_data_input].each do |local_key, local_value| - set( - browser: instance, - css: ".content.active .newTicket input[name=\"#{local_key}\"]", - value: local_value, - clear: true, - ) - end + params[:custom_data_input]&.each do |local_key, local_value| + set( + browser: instance, + css: ".content.active .newTicket input[name=\"#{local_key}\"]", + value: local_value, + clear: true, + ) end if data[:attachment] @@ -2005,7 +1987,7 @@ wait untill text in selector disabppears sleep 1 9.times do - if instance.current_url =~ /#{Regexp.quote('#ticket/zoom/')}/ + if instance.current_url.match?(/#{Regexp.quote('#ticket/zoom/')}/) assert(true, 'ticket created') sleep 2.5 id = instance.current_url @@ -2197,27 +2179,23 @@ wait untill text in selector disabppears ) end - if params[:custom_data_select] - params[:custom_data_select].each do |local_key, local_value| - select( - browser: instance, - css: ".active .sidebar select[name=\"#{local_key}\"]", - value: local_value, - ) - end + params[:custom_data_select]&.each do |local_key, local_value| + select( + browser: instance, + css: ".active .sidebar select[name=\"#{local_key}\"]", + value: local_value, + ) end - if params[:custom_data_input] - params[:custom_data_input].each do |local_key, local_value| - set( - browser: instance, - css: ".active .sidebar input[name=\"#{local_key}\"]", - value: local_value, - clear: true, - ) - end + params[:custom_data_input]&.each do |local_key, local_value| + set( + browser: instance, + css: ".active .sidebar input[name=\"#{local_key}\"]", + value: local_value, + clear: true, + ) end - if data[:state] || data[:group] || data[:body] || !params[:custom_data_select].empty? || !params[:custom_data_input].empty? + if data[:state] || data[:group] || data[:body] || params[:custom_data_select].present? || params[:custom_data_input].present? found = nil 9.times do @@ -2225,7 +2203,7 @@ wait untill text in selector disabppears begin text = instance.find_elements(css: '.content.active .js-reset')[0].text - if text =~ /(Discard your unsaved changes.|Verwerfen der)/ + if text.match?(/(Discard your unsaved changes.|Verwerfen der)/) found = true end rescue @@ -2304,7 +2282,7 @@ wait untill text in selector disabppears if data[:title] title = instance.find_elements(css: '.content.active .ticketZoom-header .js-objectTitle').first.text.strip - if title =~ /#{data[:title]}/i + if title.match?(/#{data[:title]}/i) assert(true, "matching '#{data[:title]}' in title '#{title}'") else raise "not matching '#{data[:title]}' in title '#{title}'" @@ -2313,33 +2291,29 @@ wait untill text in selector disabppears if data[:body] body = instance.find_elements(css: '.content.active [data-name="body"]').first.text.strip - if body =~ /#{data[:body]}/i + if body.match?(/#{data[:body]}/i) assert(true, "matching '#{data[:body]}' in body '#{body}'") else raise "not matching '#{data[:body]}' in body '#{body}'" end end - if params[:custom_data_select] - params[:custom_data_select].each do |local_key, local_value| - element = instance.find_elements(css: ".active .sidebar select[name=\"#{local_key}\"] option[selected]").first - value = element.text.strip - if value =~ /#{local_value}/i - assert(true, "matching '#{value}' in #{local_key} '#{local_value}'") - else - raise "not matching '#{value}' in #{local_key} '#{local_value}'" - end + params[:custom_data_select]&.each do |local_key, local_value| + element = instance.find_elements(css: ".active .sidebar select[name=\"#{local_key}\"] option[selected]").first + value = element.text.strip + if value.match?(/#{local_value}/i) + assert(true, "matching '#{value}' in #{local_key} '#{local_value}'") + else + raise "not matching '#{value}' in #{local_key} '#{local_value}'" end end - if params[:custom_data_input] - params[:custom_data_input].each do |local_key, local_value| - element = instance.find_elements(css: ".active .sidebar input[name=\"#{local_key}\"]").first - value = element.text.strip - if value =~ /#{local_value}/i - assert(true, "matching '#{value}' in #{local_key} '#{local_value}'") - else - raise "not matching '#{value}' in #{local_key} '#{local_value}'" - end + params[:custom_data_input]&.each do |local_key, local_value| + element = instance.find_elements(css: ".active .sidebar input[name=\"#{local_key}\"]").first + value = element.text.strip + if value.match?(/#{local_value}/i) + assert(true, "matching '#{value}' in #{local_key} '#{local_value}'") + else + raise "not matching '#{value}' in #{local_key} '#{local_value}'" end end @@ -2528,7 +2502,7 @@ wait untill text in selector disabppears #puts url.inspect #puts element.inspect end - overviews.each do |url, _value| + overviews.each_key do |url| count = instance.find_elements(css: ".content.active .sidebar a[href=\"#{url}\"] .badge")[0].text overviews[url] = count.to_i end @@ -2735,7 +2709,7 @@ wait untill text in selector disabppears 7.times do element = instance.find_elements(css: 'body')[0] text = element.text - if text =~ /#{Regexp.quote(data[:name])}/ + if text.match?(/#{Regexp.quote(data[:name])}/) assert(true, 'sla created') sleep 1 return true @@ -2802,7 +2776,7 @@ wait untill text in selector disabppears 7.times do element = instance.find_elements(css: 'body')[0] text = element.text - if text =~ /#{Regexp.quote(data[:name])}/ + if text.match?(/#{Regexp.quote(data[:name])}/) assert(true, 'text module created') sleep 1 return true @@ -2869,7 +2843,7 @@ wait untill text in selector disabppears 11.times do element = instance.find_elements(css: 'body')[0] text = element.text - if text =~ /#{Regexp.quote(data[:name])}/ + if text.match?(/#{Regexp.quote(data[:name])}/) assert(true, 'signature created') sleep 1 return true @@ -2938,30 +2912,28 @@ wait untill text in selector disabppears 11.times do element = instance.find_elements(css: 'body')[0] text = element.text - if text =~ /#{Regexp.quote(data[:name])}/ + if text.match?(/#{Regexp.quote(data[:name])}/) assert(true, 'group created') modal_disappear(browser: instance) # wait until modal has gone # add member - if data[:member] - data[:member].each do |member| - instance.find_elements(css: 'a[href="#manage"]')[0].click - sleep 1 - instance.find_elements(css: '.content.active a[href="#manage/users"]')[0].click - sleep 3 - element = instance.find_elements(css: '.content.active [name="search"]')[0] - element.clear - element.send_keys(member[:login]) - sleep 3 - #instance.find_elements(:css => '.content.active table [data-id]')[0].click - instance.execute_script('$(".content.active table [data-id] td").first().click()') - modal_ready(browser: instance) - #instance.find_elements(:css => 'label:contains(" ' + action[:name] + '")')[0].click - instance.execute_script('$(".js-groupList tr:contains(\"' + data[:name] + '\") .js-groupListItem[value=' + member[:access] + ']").prop("checked", true)') - screenshot(browser: instance, comment: 'group_create_member') - instance.find_elements(css: '.modal button.js-submit')[0].click - modal_disappear(browser: instance) - end + data[:member]&.each do |member| + instance.find_elements(css: 'a[href="#manage"]')[0].click + sleep 1 + instance.find_elements(css: '.content.active a[href="#manage/users"]')[0].click + sleep 3 + element = instance.find_elements(css: '.content.active [name="search"]')[0] + element.clear + element.send_keys(member[:login]) + sleep 3 + #instance.find_elements(:css => '.content.active table [data-id]')[0].click + instance.execute_script('$(".content.active table [data-id] td").first().click()') + modal_ready(browser: instance) + #instance.find_elements(:css => 'label:contains(" ' + action[:name] + '")')[0].click + instance.execute_script('$(".js-groupList tr:contains(\"' + data[:name] + '\") .js-groupListItem[value=' + member[:access] + ']").prop("checked", true)') + screenshot(browser: instance, comment: 'group_create_member') + instance.find_elements(css: '.modal button.js-submit')[0].click + modal_disappear(browser: instance) end end sleep 1 @@ -3048,29 +3020,27 @@ wait untill text in selector disabppears 11.times do element = instance.find_elements(css: 'body')[0] text = element.text - if text =~ /#{Regexp.quote(data[:name])}/ + if text.match?(/#{Regexp.quote(data[:name])}/) assert(true, 'role created') modal_disappear(browser: instance) # wait until modal has gone # add member - if data[:member] - data[:member].each do |login| - instance.find_elements(css: 'a[href="#manage"]')[0].click - sleep 1 - instance.find_elements(css: '.content.active a[href="#manage/users"]')[0].click - sleep 3 - element = instance.find_elements(css: '.content.active [name="search"]')[0] - element.clear - element.send_keys(login) - sleep 3 - #instance.find_elements(:css => '.content.active table [data-id]')[0].click - instance.execute_script('$(".content.active table [data-id] td").first().click()') - sleep 3 - #instance.find_elements(:css => 'label:contains(" ' + action[:name] + '")')[0].click - instance.execute_script('$(\'label:contains(" ' + data[:name] + '")\').first().click()') - instance.find_elements(css: '.modal button.js-submit')[0].click - modal_disappear(browser: instance) - end + data[:member]&.each do |login| + instance.find_elements(css: 'a[href="#manage"]')[0].click + sleep 1 + instance.find_elements(css: '.content.active a[href="#manage/users"]')[0].click + sleep 3 + element = instance.find_elements(css: '.content.active [name="search"]')[0] + element.clear + element.send_keys(login) + sleep 3 + #instance.find_elements(:css => '.content.active table [data-id]')[0].click + instance.execute_script('$(".content.active table [data-id] td").first().click()') + sleep 3 + #instance.find_elements(:css => 'label:contains(" ' + action[:name] + '")')[0].click + instance.execute_script('$(\'label:contains(" ' + data[:name] + '")\').first().click()') + instance.find_elements(css: '.modal button.js-submit')[0].click + modal_disappear(browser: instance) end end sleep 1 @@ -3164,29 +3134,27 @@ wait untill text in selector disabppears 11.times do element = instance.find_elements(css: 'body')[0] text = element.text - if text =~ /#{Regexp.quote(data[:name])}/ + if text.match?(/#{Regexp.quote(data[:name])}/) assert(true, 'role created') modal_disappear(browser: instance) # wait until modal has gone # add member - if data[:member] - data[:member].each do |login| - instance.find_elements(css: 'a[href="#manage"]')[0].click - sleep 1 - instance.find_elements(css: '.content.active a[href="#manage/users"]')[0].click - sleep 3 - element = instance.find_elements(css: '.content.active [name="search"]')[0] - element.clear - element.send_keys(login) - sleep 3 - #instance.find_elements(:css => '.content.active table [data-id]')[0].click - instance.execute_script('$(".content.active table [data-id] td").first().click()') - sleep 3 - #instance.find_elements(:css => 'label:contains(" ' + action[:name] + '")')[0].click - instance.execute_script('$(\'label:contains(" ' + data[:name] + '")\').first().click()') - instance.find_elements(css: '.modal button.js-submit')[0].click - modal_disappear(browser: instance) - end + data[:member]&.each do |login| + instance.find_elements(css: 'a[href="#manage"]')[0].click + sleep 1 + instance.find_elements(css: '.content.active a[href="#manage/users"]')[0].click + sleep 3 + element = instance.find_elements(css: '.content.active [name="search"]')[0] + element.clear + element.send_keys(login) + sleep 3 + #instance.find_elements(:css => '.content.active table [data-id]')[0].click + instance.execute_script('$(".content.active table [data-id] td").first().click()') + sleep 3 + #instance.find_elements(:css => 'label:contains(" ' + action[:name] + '")')[0].click + instance.execute_script('$(\'label:contains(" ' + data[:name] + '")\').first().click()') + instance.find_elements(css: '.modal button.js-submit')[0].click + modal_disappear(browser: instance) end end sleep 1 @@ -3332,12 +3300,14 @@ wait untill text in selector disabppears if data[:data_option] if data[:data_option][:options] if data[:data_type] == 'Boolean' + # rubocop:disable Lint/BooleanSymbol element = instance.find_elements(css: '.modal .js-valueTrue').first element.clear element.send_keys(data[:data_option][:options][:true]) element = instance.find_elements(css: '.modal .js-valueFalse').first element.clear element.send_keys(data[:data_option][:options][:false]) + # rubocop:enable Lint/BooleanSymbol else data[:data_option][:options].each do |key, value| element = instance.find_elements(css: '.modal .js-Table .js-key').last @@ -3352,14 +3322,14 @@ wait untill text in selector disabppears end end - [:default, :min, :max, :diff].each do |key| + %i[default min max diff].each do |key| next if !data[:data_option].key?(key) element = instance.find_elements(css: ".modal [name=\"data_option::#{key}\"]").first element.clear element.send_keys(data[:data_option][key]) end - [:future, :past].each do |key| + %i[future past].each do |key| next if !data[:data_option].key?(key) select( browser: instance, @@ -3388,7 +3358,7 @@ wait untill text in selector disabppears 11.times do element = instance.find_elements(css: 'body')[0] text = element.text - if text =~ /#{Regexp.quote(data[:name])}/ + if text.match?(/#{Regexp.quote(data[:name])}/) assert(true, 'object manager attribute created') sleep 1 return true @@ -3454,12 +3424,14 @@ wait untill text in selector disabppears if data[:data_option] if data[:data_option][:options] if data[:data_type] == 'Boolean' + # rubocop:disable Lint/BooleanSymbol element = instance.find_elements(css: '.modal .js-valueTrue').first element.clear element.send_keys(data[:data_option][:options][:true]) element = instance.find_elements(css: '.modal .js-valueFalse').first element.clear element.send_keys(data[:data_option][:options][:false]) + # rubocop:enable Lint/BooleanSymbol else data[:data_option][:options].each do |key, value| element = instance.find_elements(css: '.modal .js-Table .js-key').last @@ -3474,14 +3446,14 @@ wait untill text in selector disabppears end end - [:default, :min, :max, :diff].each do |key| + %i[default min max diff].each do |key| next if !data[:data_option].key?(key) element = instance.find_elements(css: ".modal [name=\"data_option::#{key}\"]").first element.clear element.send_keys(data[:data_option][key]) end - [:future, :past].each do |key| + %i[future past].each do |key| next if !data[:data_option].key?(key) select( browser: instance, @@ -3510,7 +3482,7 @@ wait untill text in selector disabppears 11.times do element = instance.find_elements(css: 'body')[0] text = element.text - if text =~ /#{Regexp.quote(data[:name])}/ + if text.match?(/#{Regexp.quote(data[:name])}/) assert(true, 'object manager attribute updated') sleep 1 return true @@ -3617,7 +3589,7 @@ wait untill text in selector disabppears assert(tags[0]) tags_found = {} - params[:tags].each do |key, _value| + params[:tags].each_key do |key| tags_found[key] = false end diff --git a/test/controllers/api_auth_controller_test.rb b/test/controllers/api_auth_controller_test.rb index 019ffd696..3bd3f35aa 100644 --- a/test/controllers/api_auth_controller_test.rb +++ b/test/controllers/api_auth_controller_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class ApiAuthControllerTest < ActionDispatch::IntegrationTest @@ -8,7 +8,7 @@ class ApiAuthControllerTest < ActionDispatch::IntegrationTest @headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json' } # create agent - roles = Role.where(name: %w(Admin Agent)) + roles = Role.where(name: %w[Admin Agent]) groups = Group.all UserInfo.current_user_id = 1 diff --git a/test/controllers/basic_controller_test.rb b/test/controllers/basic_controller_test.rb index 7c7c1bf5b..da6306be3 100644 --- a/test/controllers/basic_controller_test.rb +++ b/test/controllers/basic_controller_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class BasicControllerTest < ActionDispatch::IntegrationTest diff --git a/test/controllers/form_controller_test.rb b/test/controllers/form_controller_test.rb index a90844a70..7c14edbd8 100644 --- a/test/controllers/form_controller_test.rb +++ b/test/controllers/form_controller_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' require 'rake' diff --git a/test/controllers/integration_check_mk_controller_test.rb b/test/controllers/integration_check_mk_controller_test.rb index ae0465fe2..d14a7534a 100644 --- a/test/controllers/integration_check_mk_controller_test.rb +++ b/test/controllers/integration_check_mk_controller_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class IntegationCheckMkControllerTest < ActionDispatch::IntegrationTest diff --git a/test/controllers/monitoring_controller_test.rb b/test/controllers/monitoring_controller_test.rb index 26bfe7389..b9c6d7bdc 100644 --- a/test/controllers/monitoring_controller_test.rb +++ b/test/controllers/monitoring_controller_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class MonitoringControllerTest < ActionDispatch::IntegrationTest @@ -12,7 +12,7 @@ class MonitoringControllerTest < ActionDispatch::IntegrationTest Setting.set('monitoring_token', @token) # create agent - roles = Role.where(name: %w(Admin Agent)) + roles = Role.where(name: %w[Admin Agent]) groups = Group.all # channel cleanup @@ -24,7 +24,7 @@ class MonitoringControllerTest < ActionDispatch::IntegrationTest channel.last_log_out = nil channel.save! end - dir = "#{Rails.root}/tmp/unprocessable_mail" + dir = Rails.root.join('tmp', 'unprocessable_mail') Dir.glob("#{dir}/*.eml") do |entry| File.delete(entry) end @@ -374,7 +374,7 @@ class MonitoringControllerTest < ActionDispatch::IntegrationTest assert_equal(false, result['healthy']) assert_equal('Channel: Email::Notification out ;scheduler not running', result['message']) - dir = "#{Rails.root}/tmp/unprocessable_mail" + dir = Rails.root.join('tmp', 'unprocessable_mail') FileUtils.mkdir_p(dir) FileUtils.touch("#{dir}/test.eml") diff --git a/test/controllers/packages_controller_test.rb b/test/controllers/packages_controller_test.rb index 36d79343e..1eac66ca5 100644 --- a/test/controllers/packages_controller_test.rb +++ b/test/controllers/packages_controller_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class PackagesControllerTest < ActionDispatch::IntegrationTest @@ -8,7 +8,7 @@ class PackagesControllerTest < ActionDispatch::IntegrationTest @headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json' } # create agent - roles = Role.where(name: %w(Admin Agent)) + roles = Role.where(name: %w[Admin Agent]) groups = Group.all UserInfo.current_user_id = 1 diff --git a/test/controllers/search_controller_test.rb b/test/controllers/search_controller_test.rb index c483dab67..e444485e9 100644 --- a/test/controllers/search_controller_test.rb +++ b/test/controllers/search_controller_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' require 'rake' @@ -12,7 +12,7 @@ class SearchControllerTest < ActionDispatch::IntegrationTest @headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json' } # create agent - roles = Role.where(name: %w(Admin Agent)) + roles = Role.where(name: %w[Admin Agent]) groups = Group.all @admin = User.create_or_update( @@ -195,21 +195,21 @@ class SearchControllerTest < ActionDispatch::IntegrationTest assert_response(401) result = JSON.parse(@response.body) assert_equal(Hash, result.class) - assert_not(result.empty?) + assert_not(result.blank?) assert_equal('authentication failed', result['error']) post '/api/v1/search/user', params: params.to_json, headers: @headers assert_response(401) result = JSON.parse(@response.body) assert_equal(Hash, result.class) - assert_not(result.empty?) + assert_not(result.blank?) assert_equal('authentication failed', result['error']) post '/api/v1/search', params: params.to_json, headers: @headers assert_response(401) result = JSON.parse(@response.body) assert_equal(Hash, result.class) - assert_not(result.empty?) + assert_not(result.blank?) assert_equal('authentication failed', result['error']) end diff --git a/test/controllers/settings_controller_test.rb b/test/controllers/settings_controller_test.rb index 93249425b..5930ba0f5 100644 --- a/test/controllers/settings_controller_test.rb +++ b/test/controllers/settings_controller_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class SettingsControllerTest < ActionDispatch::IntegrationTest @@ -8,7 +8,7 @@ class SettingsControllerTest < ActionDispatch::IntegrationTest @headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json' } # create agent - roles = Role.where(name: %w(Admin Agent)) + roles = Role.where(name: %w[Admin Agent]) groups = Group.all UserInfo.current_user_id = 1 diff --git a/test/controllers/taskbars_controller_test.rb b/test/controllers/taskbars_controller_test.rb index 9c3697669..e8a8bf48b 100644 --- a/test/controllers/taskbars_controller_test.rb +++ b/test/controllers/taskbars_controller_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TaskbarsControllerTest < ActionDispatch::IntegrationTest diff --git a/test/controllers/ticket_article_attachments_controller_test.rb b/test/controllers/ticket_article_attachments_controller_test.rb index 35bbc74eb..2e4286332 100644 --- a/test/controllers/ticket_article_attachments_controller_test.rb +++ b/test/controllers/ticket_article_attachments_controller_test.rb @@ -1,11 +1,11 @@ -# encoding: utf-8 + require 'test_helper' class TicketArticleAttachmentsControllerTest < ActionDispatch::IntegrationTest setup do # create agent - roles = Role.where(name: %w(Admin Agent)) + roles = Role.where(name: %w[Admin Agent]) groups = Group.all UserInfo.current_user_id = 1 diff --git a/test/controllers/ticket_articles_controller_test.rb b/test/controllers/ticket_articles_controller_test.rb index 9119426bf..0fb8be1d0 100644 --- a/test/controllers/ticket_articles_controller_test.rb +++ b/test/controllers/ticket_articles_controller_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TicketArticlesControllerTest < ActionDispatch::IntegrationTest @@ -8,7 +8,7 @@ class TicketArticlesControllerTest < ActionDispatch::IntegrationTest @headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json' } # create agent - roles = Role.where(name: %w(Admin Agent)) + roles = Role.where(name: %w[Admin Agent]) groups = Group.all UserInfo.current_user_id = 1 diff --git a/test/controllers/tickets_controller_test.rb b/test/controllers/tickets_controller_test.rb index d63bac89e..aaf571fab 100644 --- a/test/controllers/tickets_controller_test.rb +++ b/test/controllers/tickets_controller_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TicketsControllerTest < ActionDispatch::IntegrationTest @@ -8,7 +8,7 @@ class TicketsControllerTest < ActionDispatch::IntegrationTest @headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json' } # create agent - roles = Role.where(name: %w(Admin Agent)) + roles = Role.where(name: %w[Admin Agent]) groups = Group.all UserInfo.current_user_id = 1 diff --git a/test/controllers/user_organization_controller_test.rb b/test/controllers/user_organization_controller_test.rb index 4079eba1f..b484d4faf 100644 --- a/test/controllers/user_organization_controller_test.rb +++ b/test/controllers/user_organization_controller_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' require 'rake' @@ -9,7 +9,7 @@ class UserOrganizationControllerTest < ActionDispatch::IntegrationTest @headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json' } # create agent - roles = Role.where(name: %w(Admin Agent)) + roles = Role.where(name: %w[Admin Agent]) groups = Group.all UserInfo.current_user_id = 1 diff --git a/test/fixtures/seeds.rb b/test/fixtures/seeds.rb index 8ed902c03..8f21fdf54 100644 --- a/test/fixtures/seeds.rb +++ b/test/fixtures/seeds.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + # inital data set as extention to db/seeds.rb Trigger.destroy_all @@ -30,7 +30,7 @@ User.create_or_update( email: 'admin@example.com', password: 'adminpw', active: true, - roles: Role.where(name: %w(Admin)), + roles: Role.where(name: %w[Admin]), updated_by_id: 1, created_by_id: 1, ) diff --git a/test/integration/aaa_auto_wizard_base_setup_test.rb b/test/integration/aaa_auto_wizard_base_setup_test.rb index 913583238..8f26b5169 100644 --- a/test/integration/aaa_auto_wizard_base_setup_test.rb +++ b/test/integration/aaa_auto_wizard_base_setup_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AaaAutoWizardBaseSetupTest < TestCase diff --git a/test/integration/auto_wizard_browser_test.rb b/test/integration/auto_wizard_browser_test.rb index b07485433..2a71f78a9 100644 --- a/test/integration/auto_wizard_browser_test.rb +++ b/test/integration/auto_wizard_browser_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class AutoWizardBrowserTest < TestCase diff --git a/test/integration/auto_wizard_test.rb b/test/integration/auto_wizard_test.rb index 17ef729b2..026466bc6 100644 --- a/test/integration/auto_wizard_test.rb +++ b/test/integration/auto_wizard_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class AutoWizardTest < ActiveSupport::TestCase @@ -46,7 +46,7 @@ class AutoWizardTest < ActiveSupport::TestCase assert_equal(false, AutoWizard.enabled?) # check first user roles - auto_wizard_data[:Users][0][:roles] = %w(Agent Admin) + auto_wizard_data[:Users][0][:roles] = %w[Agent Admin] auto_wizard_data[:Users].each do |local_user| user = User.find_by(login: local_user[:login]) @@ -203,11 +203,9 @@ class AutoWizardTest < ActiveSupport::TestCase auto_wizard_data[:Groups].each do |local_group| group = Group.find_by(name: local_group[:name]) assert_equal(local_group[:name], group.name) - if local_group[:users] - local_group[:users].each do |local_user_login| - local_user = User.find_by(login: local_user_login) - assert(group.user_ids.include?(local_user.id)) - end + local_group[:users]&.each do |local_user_login| + local_user = User.find_by(login: local_user_login) + assert(group.user_ids.include?(local_user.id)) end if local_group[:signature] signature = group.signature @@ -239,14 +237,14 @@ class AutoWizardTest < ActiveSupport::TestCase end def auto_wizard_file_write(data) - location = "#{Rails.root}/auto_wizard.json" + location = Rails.root.join('auto_wizard.json') file = File.new(location, 'wb') file.write(data.to_json) file.close end def auto_wizard_file_exists? - location = "#{Rails.root}/auto_wizard.json" + location = Rails.root.join('auto_wizard.json') return false if File.exist?(location) true end diff --git a/test/integration/calendar_subscriptions_tickets_test.rb b/test/integration/calendar_subscriptions_tickets_test.rb index 0816aacb8..25a9246c8 100644 --- a/test/integration/calendar_subscriptions_tickets_test.rb +++ b/test/integration/calendar_subscriptions_tickets_test.rb @@ -1,4 +1,4 @@ -## encoding: utf-8 + require 'integration_test_helper' class CalendarSubscriptionsTicketsTest < ActiveSupport::TestCase @@ -272,7 +272,7 @@ class CalendarSubscriptionsTicketsTest < ActiveSupport::TestCase event_data.each do |event| contained = false - if ical =~ /#{event[:summary]}/ + if ical.match?(/#{event[:summary]}/) contained = true end @@ -348,7 +348,7 @@ class CalendarSubscriptionsTicketsTest < ActiveSupport::TestCase event_data.each do |event| contained = false - if ical =~ /#{event[:summary]}/ + if ical.match?(/#{event[:summary]}/) contained = true end @@ -424,7 +424,7 @@ class CalendarSubscriptionsTicketsTest < ActiveSupport::TestCase event_data.each do |event| contained = false - if ical =~ /#{event[:summary]}/ + if ical.match?(/#{event[:summary]}/) contained = true end diff --git a/test/integration/clearbit_test.rb b/test/integration/clearbit_test.rb index 6e01e9ca0..1b4f42c2e 100644 --- a/test/integration/clearbit_test.rb +++ b/test/integration/clearbit_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'integration_test_helper' class ClearbitTest < ActiveSupport::TestCase diff --git a/test/integration/elasticsearch_test.rb b/test/integration/elasticsearch_test.rb index 778c4100d..961a3ff30 100644 --- a/test/integration/elasticsearch_test.rb +++ b/test/integration/elasticsearch_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'integration_test_helper' require 'rake' @@ -168,7 +168,7 @@ class ElasticsearchTest < ActiveSupport::TestCase Store.add( object: 'Ticket::Article', o_id: article1.id, - data: IO.binread("#{Rails.root}/test/fixtures/es-normal.txt"), + data: IO.binread(Rails.root.join('test', 'fixtures', 'es-normal.txt')), filename: 'es-normal.txt', preferences: {}, created_by_id: 1, @@ -229,7 +229,7 @@ class ElasticsearchTest < ActiveSupport::TestCase Store.add( object: 'Ticket::Article', o_id: article1.id, - data: IO.binread("#{Rails.root}/test/fixtures/es-normal.txt"), + data: IO.binread(Rails.root.join('test', 'fixtures', 'es-normal.txt')), filename: 'es-normal.txt', preferences: {}, created_by_id: 1, @@ -240,7 +240,7 @@ class ElasticsearchTest < ActiveSupport::TestCase Store.add( object: 'Ticket::Article', o_id: article1.id, - data: IO.binread("#{Rails.root}/test/fixtures/es-pdf1.pdf"), + data: IO.binread(Rails.root.join('test', 'fixtures', 'es-pdf1.pdf')), filename: 'es-pdf1.pdf', preferences: {}, created_by_id: 1, @@ -251,7 +251,7 @@ class ElasticsearchTest < ActiveSupport::TestCase Store.add( object: 'Ticket::Article', o_id: article1.id, - data: IO.binread("#{Rails.root}/test/fixtures/es-box1.box"), + data: IO.binread(Rails.root.join('test', 'fixtures', 'es-box1.box')), filename: 'mail1.box', preferences: {}, created_by_id: 1, @@ -262,7 +262,7 @@ class ElasticsearchTest < ActiveSupport::TestCase Store.add( object: 'Ticket::Article', o_id: article1.id, - data: IO.binread("#{Rails.root}/test/fixtures/es-too-big.txt"), + data: IO.binread(Rails.root.join('test', 'fixtures', 'es-too-big.txt')), filename: 'es-too-big.txt', preferences: {}, created_by_id: 1, @@ -332,7 +332,7 @@ class ElasticsearchTest < ActiveSupport::TestCase limit: 15, ) - assert(!result.empty?, 'result exists not') + assert(result.present?, 'result exists not') assert(result[0], 'record 1') assert(!result[1], 'record 2') assert_equal(result[0].id, ticket2.id) @@ -344,7 +344,7 @@ class ElasticsearchTest < ActiveSupport::TestCase limit: 15, ) - assert(!result.empty?, 'result exists not') + assert(result.present?, 'result exists not') assert(result[0], 'record 1') assert(!result[1], 'record 2') assert_equal(result[0].id, ticket2.id) @@ -387,7 +387,7 @@ class ElasticsearchTest < ActiveSupport::TestCase query: 'kindergarden', limit: 15, ) - assert(result.empty?, 'result should be empty') + assert(result.blank?, 'result should be empty') assert(!result[0], 'record 1') # search as @customer1 @@ -397,7 +397,7 @@ class ElasticsearchTest < ActiveSupport::TestCase limit: 15, ) - assert(!result.empty?, 'result exists not') + assert(result.present?, 'result exists not') assert(result[0], 'record 1') assert(result[1], 'record 2') assert(!result[2], 'record 3') @@ -411,7 +411,7 @@ class ElasticsearchTest < ActiveSupport::TestCase limit: 15, ) - assert(!result.empty?, 'result exists not') + assert(result.present?, 'result exists not') assert(result[0], 'record 1') assert(result[1], 'record 2') assert(!result[2], 'record 3') @@ -425,7 +425,7 @@ class ElasticsearchTest < ActiveSupport::TestCase limit: 15, ) - assert(!result.empty?, 'result exists not') + assert(result.present?, 'result exists not') assert(result[0], 'record 1') assert(!result[1], 'record 2') assert_equal(result[0].id, ticket3.id) @@ -523,7 +523,7 @@ class ElasticsearchTest < ActiveSupport::TestCase query: 'customer1', limit: 15, ) - assert(!result.empty?, 'result should not be empty') + assert(result.present?, 'result should not be empty') assert(result[0], 'record 1') assert(!result[1], 'record 2') assert_equal(result[0].id, @customer1.id) @@ -534,7 +534,7 @@ class ElasticsearchTest < ActiveSupport::TestCase query: 'customer1', limit: 15, ) - assert(result.empty?, 'result should be empty') + assert(result.blank?, 'result should be empty') assert(!result[0], 'record 1') # cleanup diff --git a/test/integration/email_deliver_test.rb b/test/integration/email_deliver_test.rb index 5b069068d..b7dff2901 100644 --- a/test/integration/email_deliver_test.rb +++ b/test/integration/email_deliver_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class EmailDeliverTest < ActiveSupport::TestCase diff --git a/test/integration/email_helper_test.rb b/test/integration/email_helper_test.rb index 745e6c130..f13802872 100644 --- a/test/integration/email_helper_test.rb +++ b/test/integration/email_helper_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class EmailHelperTest < ActiveSupport::TestCase @@ -374,7 +374,7 @@ class EmailHelperTest < ActiveSupport::TestCase assert_equal('invalid', result[:result]) # if we have to many failed logins, we need to handle another error message - if result[:message_human] && !result[:message_human].empty? + if result[:message_human].present? assert_equal('Authentication failed!', result[:message_human]) else assert_match(/Please log in with your web browser and then try again/, result[:message]) diff --git a/test/integration/email_keep_on_server_test.rb b/test/integration/email_keep_on_server_test.rb index 99d8069b9..e3003aad0 100644 --- a/test/integration/email_keep_on_server_test.rb +++ b/test/integration/email_keep_on_server_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' require 'net/imap' diff --git a/test/integration/facebook_browser_test.rb b/test/integration/facebook_browser_test.rb index c42135d5b..d2e371e6f 100644 --- a/test/integration/facebook_browser_test.rb +++ b/test/integration/facebook_browser_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class FacebookBrowserTest < TestCase diff --git a/test/integration/facebook_test.rb b/test/integration/facebook_test.rb index 769b9a523..a9cdc7808 100644 --- a/test/integration/facebook_test.rb +++ b/test/integration/facebook_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'integration_test_helper' class FacebookTest < ActiveSupport::TestCase diff --git a/test/integration/geo_calendar_test.rb b/test/integration/geo_calendar_test.rb index 9cbbe836e..8ecd6bcaa 100644 --- a/test/integration/geo_calendar_test.rb +++ b/test/integration/geo_calendar_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'integration_test_helper' class GeoIpCalendar < ActiveSupport::TestCase diff --git a/test/integration/geo_ip_test.rb b/test/integration/geo_ip_test.rb index b1eaba5e8..a2537343a 100644 --- a/test/integration/geo_ip_test.rb +++ b/test/integration/geo_ip_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'integration_test_helper' class GeoIpTest < ActiveSupport::TestCase diff --git a/test/integration/geo_location_test.rb b/test/integration/geo_location_test.rb index 88e6d1152..7fb38f17c 100644 --- a/test/integration/geo_location_test.rb +++ b/test/integration/geo_location_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'integration_test_helper' require 'webmock/minitest' diff --git a/test/integration/idoit_controller_test.rb b/test/integration/idoit_controller_test.rb index f7d376066..a970abf31 100644 --- a/test/integration/idoit_controller_test.rb +++ b/test/integration/idoit_controller_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' require 'webmock/minitest' @@ -20,7 +20,7 @@ class IdoitControllerTest < ActionDispatch::IntegrationTest client_id: '', }) groups = Group.where(name: 'Users') - roles = Role.where(name: %w(Agent)) + roles = Role.where(name: %w[Agent]) agent = User.create_or_update( login: 'idoit-agent@example.com', firstname: 'E', @@ -33,7 +33,7 @@ class IdoitControllerTest < ActionDispatch::IntegrationTest updated_by_id: 1, created_by_id: 1, ) - roles = Role.where(name: %w(Agent Admin)) + roles = Role.where(name: %w[Agent Admin]) admin = User.create_or_update( login: 'idoit-admin@example.com', firstname: 'E', diff --git a/test/integration/object_manager_test.rb b/test/integration/object_manager_test.rb index c6125a69f..47d9db91a 100644 --- a/test/integration/object_manager_test.rb +++ b/test/integration/object_manager_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 +# rubocop:disable Lint/BooleanSymbol require 'test_helper' class ObjectManagerTest < ActiveSupport::TestCase @@ -7,10 +7,10 @@ class ObjectManagerTest < ActiveSupport::TestCase test 'a object manager' do list_objects = ObjectManager.list_objects - assert_equal(%w(Ticket TicketArticle User Organization Group), list_objects) + assert_equal(%w[Ticket TicketArticle User Organization Group], list_objects) list_objects = ObjectManager.list_frontend_objects - assert_equal(%w(Ticket User Organization Group), list_objects) + assert_equal(%w[Ticket User Organization Group], list_objects) assert_equal(false, ObjectManager::Attribute.pending_migration?) diff --git a/test/integration/otrs_import_browser_test.rb b/test/integration/otrs_import_browser_test.rb index 505bea0fd..9329089e3 100644 --- a/test/integration/otrs_import_browser_test.rb +++ b/test/integration/otrs_import_browser_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class OtrsImportBrowserTest < TestCase diff --git a/test/integration/otrs_import_test.rb b/test/integration/otrs_import_test.rb index c3a839d5a..87010e203 100644 --- a/test/integration/otrs_import_test.rb +++ b/test/integration/otrs_import_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'integration_test_helper' class OtrsImportTest < ActiveSupport::TestCase @@ -33,12 +33,12 @@ class OtrsImportTest < ActiveSupport::TestCase test 'check dynamic fields' do local_objects = ObjectManager::Attribute.list_full - object_attribute_names = local_objects.reject do |local_object| - local_object[:object] != 'Ticket' + object_attribute_names = local_objects.select do |local_object| + local_object[:object] == 'Ticket' end.collect do |local_object| local_object['name'] end - expected_object_attribute_names = %w(vertriebsweg te_test sugar_crm_remote_no sugar_crm_company_selected_no sugar_crm_company_selection combine itsm_criticality customer_id itsm_impact itsm_review_required itsm_decision_result itsm_repair_start_time itsm_recovery_start_time itsm_decision_date title itsm_due_date topic_no open_exchange_ticket_number hostname ticket_free_key11 type ticket_free_text11 open_exchange_tn topic zarafa_tn group_id scom_hostname checkbox_example scom_uuid scom_state scom_service location owner_id department customer_location state_id pending_time priority_id tags) + expected_object_attribute_names = %w[vertriebsweg te_test sugar_crm_remote_no sugar_crm_company_selected_no sugar_crm_company_selection combine itsm_criticality customer_id itsm_impact itsm_review_required itsm_decision_result itsm_repair_start_time itsm_recovery_start_time itsm_decision_date title itsm_due_date topic_no open_exchange_ticket_number hostname ticket_free_key11 type ticket_free_text11 open_exchange_tn topic zarafa_tn group_id scom_hostname checkbox_example scom_uuid scom_state scom_service location owner_id department customer_location state_id pending_time priority_id tags] assert_equal(expected_object_attribute_names, object_attribute_names, 'dynamic field names') end diff --git a/test/integration/package_test.rb b/test/integration/package_test.rb index 1799e97cc..26bf7f7f2 100644 --- a/test/integration/package_test.rb +++ b/test/integration/package_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class PackageTest < ActiveSupport::TestCase diff --git a/test/integration/report_test.rb b/test/integration/report_test.rb index af1164ae8..b4f300459 100644 --- a/test/integration/report_test.rb +++ b/test/integration/report_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'integration_test_helper' require 'rake' diff --git a/test/integration/sipgate_controller_test.rb b/test/integration/sipgate_controller_test.rb index 26eecf6e6..5d252f130 100644 --- a/test/integration/sipgate_controller_test.rb +++ b/test/integration/sipgate_controller_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' require 'rexml/document' @@ -37,7 +37,7 @@ class SipgateControllerTest < ActionDispatch::IntegrationTest },) groups = Group.where(name: 'Users') - roles = Role.where(name: %w(Agent)) + roles = Role.where(name: %w[Agent]) agent = User.create_or_update( login: 'cti-agent@example.com', firstname: 'E', diff --git a/test/integration/slack_test.rb b/test/integration/slack_test.rb index 52847def9..b57d77367 100644 --- a/test/integration/slack_test.rb +++ b/test/integration/slack_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'integration_test_helper' require 'slack' @@ -34,7 +34,7 @@ class SlackTest < ActiveSupport::TestCase items = [ { group_ids: [slack_group.id], - types: %w(create update reminder_reached), + types: %w[create update reminder_reached], webhook: webhook, channel: channel, username: 'zammad bot', @@ -300,7 +300,7 @@ class SlackTest < ActiveSupport::TestCase message_count = 0 channel_history['messages'].each do |message| next if !message['text'] - if message['text'] =~ /#{search_for}/i + if message['text'].match?(/#{search_for}/i) message_count += 1 p "SUCCESS: message with #{search_for} found #{message_count} time(s)!" end diff --git a/test/integration/telegram_controller_test.rb b/test/integration/telegram_controller_test.rb index c31465ca1..b9adae6b7 100644 --- a/test/integration/telegram_controller_test.rb +++ b/test/integration/telegram_controller_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' require 'rexml/document' require 'webmock/minitest' diff --git a/test/integration/twitter_browser_test.rb b/test/integration/twitter_browser_test.rb index dab4ae94e..c0ea4d20b 100644 --- a/test/integration/twitter_browser_test.rb +++ b/test/integration/twitter_browser_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class TwitterBrowserTest < TestCase diff --git a/test/integration/twitter_test.rb b/test/integration/twitter_test.rb index c1d984f16..25c9f0d7f 100644 --- a/test/integration/twitter_test.rb +++ b/test/integration/twitter_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'integration_test_helper' class TwitterTest < ActiveSupport::TestCase diff --git a/test/integration/user_agent_test.rb b/test/integration/user_agent_test.rb index 57923906c..d3f7b19a6 100644 --- a/test/integration/user_agent_test.rb +++ b/test/integration/user_agent_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'integration_test_helper' class UserAgentTest < ActiveSupport::TestCase diff --git a/test/integration/user_device_controller_test.rb b/test/integration/user_device_controller_test.rb index a5788f44d..d3697b407 100644 --- a/test/integration/user_device_controller_test.rb +++ b/test/integration/user_device_controller_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class UserDeviceControllerTest < ActionDispatch::IntegrationTest @@ -11,7 +11,7 @@ class UserDeviceControllerTest < ActionDispatch::IntegrationTest @headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json' } # create agent - roles = Role.where(name: %w(Admin Agent)) + roles = Role.where(name: %w[Admin Agent]) groups = Group.all UserInfo.current_user_id = 1 diff --git a/test/integration/zendesk_import_browser_test.rb b/test/integration/zendesk_import_browser_test.rb index 60876e0bf..1963ac81d 100644 --- a/test/integration/zendesk_import_browser_test.rb +++ b/test/integration/zendesk_import_browser_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'browser_test_helper' class ZendeskImportBrowserTest < TestCase diff --git a/test/integration/zendesk_import_test.rb b/test/integration/zendesk_import_test.rb index 8d125fa7a..fe598f861 100644 --- a/test/integration/zendesk_import_test.rb +++ b/test/integration/zendesk_import_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'integration_test_helper' class ZendeskImportTest < ActiveSupport::TestCase @@ -155,7 +155,7 @@ class ZendeskImportTest < ActiveSupport::TestCase # check user fields test 'check user fields' do local_fields = User.column_names - copmare_fields = %w( + copmare_fields = %w[ id organization_id login @@ -193,7 +193,7 @@ class ZendeskImportTest < ActiveSupport::TestCase updated_at lieblingstier custom_dropdown - ) + ] assert_equal(copmare_fields, local_fields, 'user fields') end @@ -275,7 +275,7 @@ class ZendeskImportTest < ActiveSupport::TestCase # check organization fields test 'check organization fields' do local_fields = Organization.column_names - copmare_fields = %w( + copmare_fields = %w[ id name shared @@ -289,7 +289,7 @@ class ZendeskImportTest < ActiveSupport::TestCase updated_at api_key custom_dropdown - ) + ] assert_equal(copmare_fields, local_fields, 'organization fields') end @@ -456,7 +456,7 @@ If you\'re reading this message in your email, click the ticket number link that # check ticket fields test 'check ticket fields' do local_fields = Ticket.column_names - copmare_fields = %w( + copmare_fields = %w[ id group_id priority_id @@ -500,7 +500,7 @@ If you\'re reading this message in your email, click the ticket number link that custom_integer custom_regex custom_drop_down - ) + ] assert_equal(copmare_fields, local_fields, 'ticket fields') end diff --git a/test/integration_test_helper.rb b/test/integration_test_helper.rb index 841df1ccc..ca23a6622 100644 --- a/test/integration_test_helper.rb +++ b/test/integration_test_helper.rb @@ -15,8 +15,8 @@ class ActiveSupport::TestCase Cache.clear # load seeds - load "#{Rails.root}/db/seeds.rb" - load "#{Rails.root}/test/fixtures/seeds.rb" + load Rails.root.join('db', 'seeds.rb') + load Rails.root.join('test', 'fixtures', 'seeds.rb') setup do diff --git a/test/test_helper.rb b/test/test_helper.rb index d1064274c..51bb095e5 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -28,8 +28,8 @@ class ActiveSupport::TestCase Cache.clear # load seeds - load "#{Rails.root}/db/seeds.rb" - load "#{Rails.root}/test/fixtures/seeds.rb" + load Rails.root.join('db', 'seeds.rb') + load Rails.root.join('test', 'fixtures', 'seeds.rb') # set system mode to done / to activate Setting.set('system_init_done', true) @@ -67,14 +67,14 @@ class ActiveSupport::TestCase def email_notification_count(type, recipient) # read config file and count type & recipients - file = "#{Rails.root}/log/#{Rails.env}.log" + file = Rails.root.join('log', "#{Rails.env}.log") lines = [] IO.foreach(file) do |line| lines.push line end count = 0 lines.reverse.each do |line| - break if line =~ /\+\+\+\+NEW\+\+\+\+TEST\+\+\+\+/ + break if line.match?(/\+\+\+\+NEW\+\+\+\+TEST\+\+\+\+/) next if line !~ /Send notification \(#{type}\)/ next if line !~ /to:\s#{recipient}/ count += 1 @@ -85,14 +85,14 @@ class ActiveSupport::TestCase def email_count(recipient) # read config file and count & recipients - file = "#{Rails.root}/log/#{Rails.env}.log" + file = Rails.root.join('log', "#{Rails.env}.log") lines = [] IO.foreach(file) do |line| lines.push line end count = 0 lines.reverse.each do |line| - break if line =~ /\+\+\+\+NEW\+\+\+\+TEST\+\+\+\+/ + break if line.match?(/\+\+\+\+NEW\+\+\+\+TEST\+\+\+\+/) next if line !~ /Send email to:/ next if line !~ /to:\s'#{recipient}'/ count += 1 diff --git a/test/unit/aaa_string_test.rb b/test/unit/aaa_string_test.rb index e1c0458f1..70f2da1be 100644 --- a/test/unit/aaa_string_test.rb +++ b/test/unit/aaa_string_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 # rubocop:disable all require 'test_helper' diff --git a/test/unit/activity_stream_test.rb b/test/unit/activity_stream_test.rb index 01110299e..3b2f8cda4 100644 --- a/test/unit/activity_stream_test.rb +++ b/test/unit/activity_stream_test.rb @@ -1,10 +1,10 @@ -# encoding: utf-8 + require 'test_helper' class ActivityStreamTest < ActiveSupport::TestCase setup do - roles = Role.where(name: %w(Admin Agent)) + roles = Role.where(name: %w[Admin Agent]) groups = Group.where(name: 'Users') @admin_user = User.create_or_update( login: 'admin', @@ -83,7 +83,7 @@ class ActivityStreamTest < ActiveSupport::TestCase assert_not(stream[3]) stream = @current_user.activity_stream(4) - assert(stream.empty?) + assert(stream.blank?) # cleanup ticket.destroy! @@ -122,7 +122,7 @@ class ActivityStreamTest < ActiveSupport::TestCase assert_not(stream[2]) stream = @current_user.activity_stream(4) - assert(stream.empty?) + assert(stream.blank?) # cleanup organization.destroy! @@ -154,7 +154,7 @@ class ActivityStreamTest < ActiveSupport::TestCase assert_not(stream[1]) stream = @current_user.activity_stream(4) - assert(stream.empty?) + assert(stream.blank?) # cleanup user.destroy! @@ -201,7 +201,7 @@ class ActivityStreamTest < ActiveSupport::TestCase assert_not(stream[2]) stream = @current_user.activity_stream(4) - assert(stream.empty?) + assert(stream.blank?) # cleanup user.destroy! diff --git a/test/unit/assets_test.rb b/test/unit/assets_test.rb index 9e7914d7b..538f1c492 100644 --- a/test/unit/assets_test.rb +++ b/test/unit/assets_test.rb @@ -1,10 +1,10 @@ -# encoding: utf-8 + require 'test_helper' class AssetsTest < ActiveSupport::TestCase test 'user' do - roles = Role.where(name: %w(Agent Admin)) + roles = Role.where(name: %w[Agent Admin]) groups = Group.all org1 = Organization.create_or_update( name: 'some user org', @@ -141,7 +141,7 @@ class AssetsTest < ActiveSupport::TestCase test 'organization' do - roles = Role.where( name: %w(Agent Admin) ) + roles = Role.where( name: %w[Agent Admin] ) admin1 = User.create_or_update( login: 'admin1@example.org', firstname: 'admin1', @@ -154,7 +154,7 @@ class AssetsTest < ActiveSupport::TestCase roles: roles, ) - roles = Role.where( name: %w(Customer) ) + roles = Role.where( name: %w[Customer] ) org = Organization.create_or_update( name: 'some customer org', updated_by_id: admin1.id, @@ -278,7 +278,7 @@ class AssetsTest < ActiveSupport::TestCase def diff(o1, o2) return true if o1 == o2 - %w(updated_at created_at).each do |item| + %w[updated_at created_at].each do |item| if o1[item] o1[item] = o1[item].to_s end @@ -286,7 +286,7 @@ class AssetsTest < ActiveSupport::TestCase o2[item] = o2[item].to_s end end - return true if (o1.to_a - o2.to_a).empty? + return true if (o1.to_a - o2.to_a).blank? #puts "ERROR: difference \n1: #{o1.inspect}\n2: #{o2.inspect}\ndiff: #{(o1.to_a - o2.to_a).inspect}" false end @@ -294,7 +294,7 @@ class AssetsTest < ActiveSupport::TestCase test 'overview' do UserInfo.current_user_id = 1 - roles = Role.where(name: %w(Customer)) + roles = Role.where(name: %w[Customer]) user1 = User.create_or_update( login: 'assets_overview1@example.org', @@ -368,9 +368,9 @@ class AssetsTest < ActiveSupport::TestCase direction: 'ASC', }, view: { - d: %w(title customer group created_at), - s: %w(title customer group created_at), - m: %w(number title customer group created_at), + d: %w[title customer group created_at], + s: %w[title customer group created_at], + m: %w[number title customer group created_at], view_mode_default: 's', }, ) @@ -405,9 +405,9 @@ class AssetsTest < ActiveSupport::TestCase direction: 'ASC', }, view: { - d: %w(title customer group created_at), - s: %w(title customer group created_at), - m: %w(number title customer group created_at), + d: %w[title customer group created_at], + s: %w[title customer group created_at], + m: %w[number title customer group created_at], view_mode_default: 's', }, ) @@ -425,7 +425,7 @@ class AssetsTest < ActiveSupport::TestCase test 'sla' do UserInfo.current_user_id = 1 - roles = Role.where(name: %w(Customer)) + roles = Role.where(name: %w[Customer]) user1 = User.create_or_update( login: 'assets_sla1@example.org', @@ -491,7 +491,7 @@ class AssetsTest < ActiveSupport::TestCase test 'job' do UserInfo.current_user_id = 1 - roles = Role.where(name: %w(Customer)) + roles = Role.where(name: %w[Customer]) user1 = User.create_or_update( login: 'assets_job1@example.org', diff --git a/test/unit/auth_test.rb b/test/unit/auth_test.rb index f982e4e6e..151782666 100644 --- a/test/unit/auth_test.rb +++ b/test/unit/auth_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class AuthTest < ActiveSupport::TestCase diff --git a/test/unit/cache_test.rb b/test/unit/cache_test.rb index 37992bdaf..76b8bdc27 100644 --- a/test/unit/cache_test.rb +++ b/test/unit/cache_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class CacheTest < ActiveSupport::TestCase diff --git a/test/unit/calendar_subscription_test.rb b/test/unit/calendar_subscription_test.rb index 3e31dbab1..36292b148 100644 --- a/test/unit/calendar_subscription_test.rb +++ b/test/unit/calendar_subscription_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class CalendarSubscriptionTest < ActiveSupport::TestCase diff --git a/test/unit/calendar_test.rb b/test/unit/calendar_test.rb index 0b33fa1b2..ac57ce9cf 100644 --- a/test/unit/calendar_test.rb +++ b/test/unit/calendar_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class CalendarTest < ActiveSupport::TestCase diff --git a/test/unit/chat_test.rb b/test/unit/chat_test.rb index fe406d23b..27b428d38 100644 --- a/test/unit/chat_test.rb +++ b/test/unit/chat_test.rb @@ -1,11 +1,11 @@ -# encoding: utf-8 + require 'test_helper' class ChatTest < ActiveSupport::TestCase setup do groups = Group.all - roles = Role.where( name: %w(Agent) ) + roles = Role.where( name: %w[Agent] ) @agent1 = User.create_or_update( login: 'ticket-chat-agent1@example.com', firstname: 'Notification', diff --git a/test/unit/cti_caller_id_test.rb b/test/unit/cti_caller_id_test.rb index 94c08c078..7a899f9e7 100644 --- a/test/unit/cti_caller_id_test.rb +++ b/test/unit/cti_caller_id_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class CtiCallerIdTest < ActiveSupport::TestCase diff --git a/test/unit/db_auto_increment_test.rb b/test/unit/db_auto_increment_test.rb index 4872b33ae..0f5edcc39 100644 --- a/test/unit/db_auto_increment_test.rb +++ b/test/unit/db_auto_increment_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class DbAutoIncrementTest < ActiveSupport::TestCase diff --git a/test/unit/email_address_test.rb b/test/unit/email_address_test.rb index 261cd033e..4a6ccf588 100644 --- a/test/unit/email_address_test.rb +++ b/test/unit/email_address_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class EmailAddressTest < ActiveSupport::TestCase diff --git a/test/unit/email_build_test.rb b/test/unit/email_build_test.rb index c293bcd98..cf6585e4d 100644 --- a/test/unit/email_build_test.rb +++ b/test/unit/email_build_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class EmailBuildTest < ActiveSupport::TestCase @@ -73,21 +73,19 @@ class EmailBuildTest < ActiveSupport::TestCase assert_equal(2, data[:attachments].length) # check attachments - if data[:attachments] - data[:attachments].each do |attachment| - if attachment[:filename] == 'message.html' - assert_nil(attachment[:preferences]['Content-ID']) - assert_equal(true, attachment[:preferences]['content-alternative']) - assert_equal('text/html', attachment[:preferences]['Mime-Type']) - assert_equal('UTF-8', attachment[:preferences]['Charset']) - elsif attachment[:filename] == 'somename.png' - assert_nil(attachment[:preferences]['Content-ID']) - assert_nil(attachment[:preferences]['content-alternative']) - assert_equal('image/png', attachment[:preferences]['Mime-Type']) - assert_equal('UTF-8', attachment[:preferences]['Charset']) - else - assert(false, "invalid attachment, should not be there, #{attachment.inspect}") - end + data[:attachments]&.each do |attachment| + if attachment[:filename] == 'message.html' + assert_nil(attachment[:preferences]['Content-ID']) + assert_equal(true, attachment[:preferences]['content-alternative']) + assert_equal('text/html', attachment[:preferences]['Mime-Type']) + assert_equal('UTF-8', attachment[:preferences]['Charset']) + elsif attachment[:filename] == 'somename.png' + assert_nil(attachment[:preferences]['Content-ID']) + assert_nil(attachment[:preferences]['content-alternative']) + assert_equal('image/png', 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 @@ -127,16 +125,14 @@ class EmailBuildTest < ActiveSupport::TestCase assert_equal(1, data[:attachments].length) # check attachments - if data[:attachments] - data[:attachments].each do |attachment| - if attachment[:filename] == 'somename.png' - assert_nil(attachment[:preferences]['Content-ID']) - assert_nil(attachment[:preferences]['content-alternative']) - assert_equal('image/png', attachment[:preferences]['Mime-Type']) - assert_equal('UTF-8', attachment[:preferences]['Charset']) - else - assert(false, "invalid attachment, should not be there, #{attachment.inspect}") - end + data[:attachments]&.each do |attachment| + if attachment[:filename] == 'somename.png' + assert_nil(attachment[:preferences]['Content-ID']) + assert_nil(attachment[:preferences]['content-alternative']) + assert_equal('image/png', 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 diff --git a/test/unit/email_parser_test.rb b/test/unit/email_parser_test.rb index 1bfa8f3f6..e0bd4808e 100644 --- a/test/unit/email_parser_test.rb +++ b/test/unit/email_parser_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 # rubocop:disable all require 'test_helper' diff --git a/test/unit/email_postmaster_test.rb b/test/unit/email_postmaster_test.rb index 3893ddab3..877c8b0b3 100644 --- a/test/unit/email_postmaster_test.rb +++ b/test/unit/email_postmaster_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 # rubocop:disable all require 'test_helper' diff --git a/test/unit/email_process_auto_response_test.rb b/test/unit/email_process_auto_response_test.rb index 1d81118f6..d37c1f1d7 100644 --- a/test/unit/email_process_auto_response_test.rb +++ b/test/unit/email_process_auto_response_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 +# rubocop:disable Lint/InterpolationCheck require 'test_helper' class EmailProcessAutoResponseTest < ActiveSupport::TestCase diff --git a/test/unit/email_process_bounce_delivery_permanent_failed_test.rb b/test/unit/email_process_bounce_delivery_permanent_failed_test.rb index f62210e55..75d5b62ce 100644 --- a/test/unit/email_process_bounce_delivery_permanent_failed_test.rb +++ b/test/unit/email_process_bounce_delivery_permanent_failed_test.rb @@ -1,10 +1,10 @@ -# encoding: utf-8 +# rubocop:disable Lint/InterpolationCheck require 'test_helper' class EmailProcessBounceDeliveryPermanentFailedTest < ActiveSupport::TestCase test 'process with bounce trigger email loop check - article based blocker' do - roles = Role.where(name: %w(Customer)) + roles = Role.where(name: %w[Customer]) customer1 = User.create_or_update( login: 'ticket-bounce-trigger1@example.com', firstname: 'Notification', @@ -111,7 +111,7 @@ class EmailProcessBounceDeliveryPermanentFailedTest < ActiveSupport::TestCase end test 'process with bounce trigger email loop check - bounce based blocker' do - roles = Role.where(name: %w(Customer)) + roles = Role.where(name: %w[Customer]) customer2 = User.create_or_update( login: 'ticket-bounce-trigger2@example.com', firstname: 'Notification', diff --git a/test/unit/email_process_bounce_follow_test.rb b/test/unit/email_process_bounce_follow_test.rb index 242c95845..0deed2d91 100644 --- a/test/unit/email_process_bounce_follow_test.rb +++ b/test/unit/email_process_bounce_follow_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 +# rubocop:disable Lint/InterpolationCheck require 'test_helper' class EmailProcessBounceFollowUpTest < ActiveSupport::TestCase @@ -38,7 +38,7 @@ class EmailProcessBounceFollowUpTest < ActiveSupport::TestCase end test 'process with bounce trigger email loop check - article based blocker' do - roles = Role.where(name: %w(Customer)) + roles = Role.where(name: %w[Customer]) customer1 = User.create_or_update( login: 'ticket-bounce-trigger1@example.com', firstname: 'Notification', @@ -145,7 +145,7 @@ class EmailProcessBounceFollowUpTest < ActiveSupport::TestCase end test 'process with bounce trigger email loop check - bounce based blocker' do - roles = Role.where(name: %w(Customer)) + roles = Role.where(name: %w[Customer]) customer2 = User.create_or_update( login: 'ticket-bounce-trigger2@example.com', firstname: 'Notification', diff --git a/test/unit/email_process_follow_up_possible_test.rb b/test/unit/email_process_follow_up_possible_test.rb index 32529ceaf..46a81cf8f 100644 --- a/test/unit/email_process_follow_up_possible_test.rb +++ b/test/unit/email_process_follow_up_possible_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class EmailProcessFollowUpPossibleTest < ActiveSupport::TestCase diff --git a/test/unit/email_process_follow_up_test.rb b/test/unit/email_process_follow_up_test.rb index ccbc5c291..59c1d0e08 100644 --- a/test/unit/email_process_follow_up_test.rb +++ b/test/unit/email_process_follow_up_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class EmailProcessFollowUpTest < ActiveSupport::TestCase @@ -88,7 +88,7 @@ References: <20150830145601.30. no reference " setting_orig = Setting.get('postmaster_follow_up_search_in') - Setting.set('postmaster_follow_up_search_in', %w(body attachment references)) + Setting.set('postmaster_follow_up_search_in', %w[body attachment references]) travel 1.second ticket_p, article_p, user_p = Channel::EmailParser.new.process({}, email_raw_string_subject) @@ -251,7 +251,7 @@ Some Text" test 'process with follow up check with two external reference headers' do setting_orig = Setting.get('postmaster_follow_up_search_in') - Setting.set('postmaster_follow_up_search_in', %w(body attachment references)) + Setting.set('postmaster_follow_up_search_in', %w[body attachment references]) data1 = "From: me@example.com To: z@example.com diff --git a/test/unit/email_process_identify_sender_max_test.rb b/test/unit/email_process_identify_sender_max_test.rb index 81d0d316b..0a0efecc3 100644 --- a/test/unit/email_process_identify_sender_max_test.rb +++ b/test/unit/email_process_identify_sender_max_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class EmailProcessIdentifySenderMax < ActiveSupport::TestCase diff --git a/test/unit/email_process_out_of_office_test.rb b/test/unit/email_process_out_of_office_test.rb index 10f529e0c..2025a12c1 100644 --- a/test/unit/email_process_out_of_office_test.rb +++ b/test/unit/email_process_out_of_office_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class EmailProcessOutOfOfficeTest < ActiveSupport::TestCase diff --git a/test/unit/email_process_reply_to_test.rb b/test/unit/email_process_reply_to_test.rb index dbef68207..d0db22d1a 100644 --- a/test/unit/email_process_reply_to_test.rb +++ b/test/unit/email_process_reply_to_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class EmailProcessReplyToTest < ActiveSupport::TestCase diff --git a/test/unit/email_process_sender_is_system_address_or_agent_test.rb b/test/unit/email_process_sender_is_system_address_or_agent_test.rb index bd0b7093b..d1e7265c8 100644 --- a/test/unit/email_process_sender_is_system_address_or_agent_test.rb +++ b/test/unit/email_process_sender_is_system_address_or_agent_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class EmailProcessSenderIsSystemAddressOrAgent < ActiveSupport::TestCase diff --git a/test/unit/email_process_sender_name_update_if_needed.rb b/test/unit/email_process_sender_name_update_if_needed.rb index 553adf421..6c979f315 100644 --- a/test/unit/email_process_sender_name_update_if_needed.rb +++ b/test/unit/email_process_sender_name_update_if_needed.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class EmailProcessSenderNameUpdateIfNeeded < ActiveSupport::TestCase diff --git a/test/unit/email_process_state_open_set.rb b/test/unit/email_process_state_open_set.rb index 6ae4bf578..0347e14ca 100644 --- a/test/unit/email_process_state_open_set.rb +++ b/test/unit/email_process_state_open_set.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class EmailProcessStateOpenSet < ActiveSupport::TestCase diff --git a/test/unit/email_process_test.rb b/test/unit/email_process_test.rb index 3df6d1844..b945e0737 100644 --- a/test/unit/email_process_test.rb +++ b/test/unit/email_process_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 # rubocop:disable all require 'test_helper' diff --git a/test/unit/email_regex_test.rb b/test/unit/email_regex_test.rb index 0052fa06f..88832ef76 100644 --- a/test/unit/email_regex_test.rb +++ b/test/unit/email_regex_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class EmailRegexTest < ActiveSupport::TestCase diff --git a/test/unit/email_signatur_detection_test.rb b/test/unit/email_signatur_detection_test.rb index 8ad183d1d..37510921a 100644 --- a/test/unit/email_signatur_detection_test.rb +++ b/test/unit/email_signatur_detection_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class EmailSignaturDetectionTest < ActiveSupport::TestCase @@ -14,7 +14,7 @@ class EmailSignaturDetectionTest < ActiveSupport::TestCase fixture_messages = [] fixture_files.each do |filepath, value| - value[:content] = File.new("#{Rails.root}/test/fixtures/#{filepath}", 'r').read + value[:content] = File.new( Rails.root.join('test', 'fixtures', filepath), 'r').read fixture_messages.push value end @@ -22,7 +22,7 @@ class EmailSignaturDetectionTest < ActiveSupport::TestCase expected_signature = "\nMit freundlichen Grüßen\n\nBob Smith\nBerechtigungen und dez. Department\n________________________________\n\nMusik AG\nBerechtigungen und dez. Department (ITPBM)\nKastanien 2" assert_equal(expected_signature, signature) - fixture_files.each do |_filepath, value| + fixture_files.each_value do |value| assert_equal(value[:line], SignatureDetection.find_signature_line(signature, value[:content], value[:content_type])) end end @@ -37,7 +37,7 @@ class EmailSignaturDetectionTest < ActiveSupport::TestCase fixture_messages = [] fixture_files.each do |filepath, value| - value[:content] = File.new("#{Rails.root}/test/fixtures/#{filepath}", 'r').read + value[:content] = File.new( Rails.root.join('test', 'fixtures', filepath), 'r').read fixture_messages.push value end @@ -45,14 +45,14 @@ class EmailSignaturDetectionTest < ActiveSupport::TestCase expected_signature = "\nFreundliche Grüße\n\nGünter Lässig\nLokale Daten\n\nMusic GmbH\nBaustraße 123, 12345 Max City\nTelefon 0123 5432114\nTelefax 0123 5432139" assert_equal(expected_signature, signature) - fixture_files.each do |_filepath, value| + fixture_files.each_value do |value| assert_equal(value[:line], SignatureDetection.find_signature_line(signature, value[:content], value[:content_type])) end end test 'test case 3 - just tests' do signature = "~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nBob Smith\nABC Organisation\n\nEXAMPLE IT-Service GmbH\nDorten 5 F&E\n12345 Da / Germany\nPhone: +49 (0) 1234 567 890 / +49 (0) 1234 567 891\nFax:     +49 (0) 1234 567 892" - message = File.new("#{Rails.root}/test/fixtures/email_signature_detection/example1.html", 'r').read + message = File.new(Rails.root.join('test', 'fixtures', 'email_signature_detection', 'example1.html'), 'r').read signature_line = SignatureDetection.find_signature_line(signature, message, 'text/html') assert_equal(11, signature_line) end @@ -67,7 +67,7 @@ class EmailSignaturDetectionTest < ActiveSupport::TestCase fixture_messages = [] fixture_files.each do |filepath, value| - value[:content] = File.new("#{Rails.root}/test/fixtures/#{filepath}", 'r').read + value[:content] = File.new( Rails.root.join('test', 'fixtures', filepath), 'r').read fixture_messages.push value end @@ -84,7 +84,7 @@ class EmailSignaturDetectionTest < ActiveSupport::TestCase raw_email_header = "From: Bob.Smith@music.com\nTo: test@zammad.org\nSubject: test\n\n" # process email I - file = File.open("#{Rails.root}/test/fixtures/email_signature_detection/client_a_1.txt", 'rb') + file = File.open(Rails.root.join('test', 'fixtures', 'email_signature_detection', 'client_a_1.txt'), 'rb') raw_email = raw_email_header + file.read ticket1, article1, user1, mail = Channel::EmailParser.new.process({}, raw_email) assert(ticket1) @@ -92,7 +92,7 @@ class EmailSignaturDetectionTest < ActiveSupport::TestCase Scheduler.worker(true) # process email II - file = File.open("#{Rails.root}/test/fixtures/email_signature_detection/client_a_2.txt", 'rb') + file = File.open(Rails.root.join('test', 'fixtures', 'email_signature_detection', 'client_a_2.txt'), 'rb') raw_email = raw_email_header + file.read ticket2, article2, user2, mail = Channel::EmailParser.new.process({}, raw_email) assert(ticket2) @@ -104,7 +104,7 @@ class EmailSignaturDetectionTest < ActiveSupport::TestCase assert(user2.preferences[:signature_detection]) # process email III - file = File.open("#{Rails.root}/test/fixtures/email_signature_detection/client_a_3.txt", 'rb') + file = File.open(Rails.root.join('test', 'fixtures', 'email_signature_detection', 'client_a_3.txt'), 'rb') raw_email = raw_email_header + file.read ticket3, article3, user3, mail = Channel::EmailParser.new.process({}, raw_email) assert(ticket3) diff --git a/test/unit/history_test.rb b/test/unit/history_test.rb index afb276786..51c06e078 100644 --- a/test/unit/history_test.rb +++ b/test/unit/history_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class HistoryTest < ActiveSupport::TestCase diff --git a/test/unit/html_sanitizer_test.rb b/test/unit/html_sanitizer_test.rb index 77d03f4c3..56d81360a 100644 --- a/test/unit/html_sanitizer_test.rb +++ b/test/unit/html_sanitizer_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class HtmlSanitizerTest < ActiveSupport::TestCase diff --git a/test/unit/integration_icinga_test.rb b/test/unit/integration_icinga_test.rb index 2106bd6c4..4bf1ce357 100644 --- a/test/unit/integration_icinga_test.rb +++ b/test/unit/integration_icinga_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class IntegrationIcingaTest < ActiveSupport::TestCase diff --git a/test/unit/integration_monit_test.rb b/test/unit/integration_monit_test.rb index fafe72109..a481cc77d 100644 --- a/test/unit/integration_monit_test.rb +++ b/test/unit/integration_monit_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class IntegrationMonitTest < ActiveSupport::TestCase diff --git a/test/unit/integration_nagios_test.rb b/test/unit/integration_nagios_test.rb index 4e33aa405..9248be00e 100644 --- a/test/unit/integration_nagios_test.rb +++ b/test/unit/integration_nagios_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class IntegrationNagiosTest < ActiveSupport::TestCase diff --git a/test/unit/job_test.rb b/test/unit/job_test.rb index d92edfe59..9000e312e 100644 --- a/test/unit/job_test.rb +++ b/test/unit/job_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class JobTest < ActiveSupport::TestCase diff --git a/test/unit/karma_test.rb b/test/unit/karma_test.rb index c53bd09ed..b2659a206 100644 --- a/test/unit/karma_test.rb +++ b/test/unit/karma_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class KarmaTest < ActiveSupport::TestCase diff --git a/test/unit/migration_ror_42_to50_store_test.rb b/test/unit/migration_ror_42_to50_store_test.rb index 56778693b..09e1c273b 100644 --- a/test/unit/migration_ror_42_to50_store_test.rb +++ b/test/unit/migration_ror_42_to50_store_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + # Rails 5.0 has changed to only store and read ActiveSupport::HashWithIndifferentAccess from stores # we extended lib/core_ext/active_record/store/indifferent_coder.rb to read also ActionController::Parameters # and convert them to ActiveSupport::HashWithIndifferentAccess for migration in db/migrate/20170910000001_fixed_store_upgrade_45.rb. diff --git a/test/unit/model_test.rb b/test/unit/model_test.rb index b7c0d7777..9c5431539 100644 --- a/test/unit/model_test.rb +++ b/test/unit/model_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class ModelTest < ActiveSupport::TestCase @@ -65,7 +65,7 @@ class ModelTest < ActiveSupport::TestCase # create base groups = Group.where(name: 'Users') - roles = Role.where(name: %w(Agent Admin)) + roles = Role.where(name: %w[Agent Admin]) agent1 = User.create_or_update( login: 'model-agent1@example.com', firstname: 'Model', @@ -179,7 +179,7 @@ class ModelTest < ActiveSupport::TestCase assert(!references1['Organization']) assert(!references1['Group']) assert(!references1['UserGroup']) - assert(references1.empty?) + assert(references1.blank?) references_total1 = Models.references_total('User', agent1.id) assert_equal(references_total1, 0) @@ -211,7 +211,7 @@ class ModelTest < ActiveSupport::TestCase # verify agent2 references2 = Models.references('Organization', organization2.id) - assert(references2.empty?) + assert(references2.blank?) references_total2 = Models.references_total('Organization', organization2.id) assert_equal(references_total2, 0) @@ -221,7 +221,7 @@ class ModelTest < ActiveSupport::TestCase # verify agent1 references1 = Models.references('Organization', organization1.id) - assert(references1.empty?) + assert(references1.blank?) references_total1 = Models.references_total('Organization', organization1.id) assert_equal(references_total1, 0) diff --git a/test/unit/notification_factory_mailer_template_test.rb b/test/unit/notification_factory_mailer_template_test.rb index 2dc38a9e8..2a3e4b36b 100644 --- a/test/unit/notification_factory_mailer_template_test.rb +++ b/test/unit/notification_factory_mailer_template_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class NotificationFactoryMailerTemplateTest < ActiveSupport::TestCase @@ -158,8 +158,8 @@ class NotificationFactoryMailerTemplateTest < ActiveSupport::TestCase created_by_id: 1, ) changes = { - state: %w(aaa bbb), - group: %w(xxx yyy), + state: %w[aaa bbb], + group: %w[xxx yyy], } result = NotificationFactory::Mailer.template( template: 'ticket_update', diff --git a/test/unit/notification_factory_mailer_test.rb b/test/unit/notification_factory_mailer_test.rb index 22a0d3e7c..770911693 100644 --- a/test/unit/notification_factory_mailer_test.rb +++ b/test/unit/notification_factory_mailer_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class NotificationFactoryMailerTest < ActiveSupport::TestCase diff --git a/test/unit/notification_factory_renderer_test.rb b/test/unit/notification_factory_renderer_test.rb index d3bcffed1..74e33362c 100644 --- a/test/unit/notification_factory_renderer_test.rb +++ b/test/unit/notification_factory_renderer_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 +# rubocop:disable Lint/InterpolationCheck require 'test_helper' class NotificationFactoryRendererTest < ActiveSupport::TestCase diff --git a/test/unit/notification_factory_slack_template_test.rb b/test/unit/notification_factory_slack_template_test.rb index cb3b349f0..319198492 100644 --- a/test/unit/notification_factory_slack_template_test.rb +++ b/test/unit/notification_factory_slack_template_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class NotificationFactorySlackTemplateTest < ActiveSupport::TestCase @@ -98,8 +98,8 @@ class NotificationFactorySlackTemplateTest < ActiveSupport::TestCase created_by_id: 1, ) changes = { - state: %w(aaa bbb), - group: %w(xxx yyy), + state: %w[aaa bbb], + group: %w[xxx yyy], } result = NotificationFactory::Slack.template( template: 'ticket_update', diff --git a/test/unit/notification_factory_template_test.rb b/test/unit/notification_factory_template_test.rb index baee7c7ea..604ed0e62 100644 --- a/test/unit/notification_factory_template_test.rb +++ b/test/unit/notification_factory_template_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 +# rubocop:disable Lint/InterpolationCheck require 'test_helper' class NotificationFactoryTemplateTest < ActiveSupport::TestCase diff --git a/test/unit/object_cache_test.rb b/test/unit/object_cache_test.rb index 4ab4ee840..086210c9a 100644 --- a/test/unit/object_cache_test.rb +++ b/test/unit/object_cache_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class ObjectCacheTest < ActiveSupport::TestCase @@ -9,7 +9,7 @@ class ObjectCacheTest < ActiveSupport::TestCase created_by_id: 1, ) - roles = Role.where( name: %w(Agent Admin) ) + roles = Role.where( name: %w[Agent Admin] ) groups = Group.all user1 = User.create_or_update( login: 'object_cache1@example.org', @@ -36,7 +36,7 @@ class ObjectCacheTest < ActiveSupport::TestCase end test 'user cache' do - roles = Role.where(name: %w(Agent Admin)) + roles = Role.where(name: %w[Agent Admin]) groups = Group.all.order(:id) # be sure that minimum one admin is available diff --git a/test/unit/object_create_update_with_ref_name_test.rb b/test/unit/object_create_update_with_ref_name_test.rb index f79557371..db2b02d04 100644 --- a/test/unit/object_create_update_with_ref_name_test.rb +++ b/test/unit/object_create_update_with_ref_name_test.rb @@ -1,9 +1,9 @@ -# encoding: utf-8 + require 'test_helper' class ObjectCreateUpdateWithRefNameTest < ActiveSupport::TestCase test 'organization' do - roles = Role.where(name: %w(Agent Admin)) + roles = Role.where(name: %w[Agent Admin]) groups = Group.all user1 = User.create_or_update( login: 'object_ref_name1@example.org', @@ -90,7 +90,7 @@ class ObjectCreateUpdateWithRefNameTest < ActiveSupport::TestCase organization: 'some org update_with_ref user', updated_by_id: 1, created_by_id: 1, - roles: %w(Agent Admin), + roles: %w[Agent Admin], groups: ['Users'], ) user2 = User.create_or_update_with_ref( @@ -137,7 +137,7 @@ class ObjectCreateUpdateWithRefNameTest < ActiveSupport::TestCase organization_id: nil, updated_by_id: 1, created_by_id: 1, - roles: %w(Agent Admin), + roles: %w[Agent Admin], groups: [], ) user2 = User.create_or_update_with_ref( diff --git a/test/unit/object_type_lookup_test.rb b/test/unit/object_type_lookup_test.rb index d4643d471..f37a9c24a 100644 --- a/test/unit/object_type_lookup_test.rb +++ b/test/unit/object_type_lookup_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class ObjectTypeLookupTest < ActiveSupport::TestCase diff --git a/test/unit/online_notifiaction_test.rb b/test/unit/online_notifiaction_test.rb index a50354f11..07e669f9d 100644 --- a/test/unit/online_notifiaction_test.rb +++ b/test/unit/online_notifiaction_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class OnlineNotificationTest < ActiveSupport::TestCase @@ -347,11 +347,11 @@ class OnlineNotificationTest < ActiveSupport::TestCase Scheduler.worker(true) notifications = OnlineNotification.list_by_object('Ticket', tickets[0].id) - assert(!notifications.empty?, 'should have notifications') + assert(notifications.present?, 'should have notifications') assert(OnlineNotification.all_seen?('Ticket', tickets[0].id), 'still not seen notifications for merged ticket available') notifications = OnlineNotification.list_by_object('Ticket', tickets[1].id) - assert(!notifications.empty?, 'should have notifications') + assert(notifications.present?, 'should have notifications') assert(!OnlineNotification.all_seen?('Ticket', tickets[1].id), 'no notifications for master ticket available') # delete tickets @@ -364,7 +364,7 @@ class OnlineNotificationTest < ActiveSupport::TestCase # check if notifications for ticket still exist Scheduler.worker(true) notifications = OnlineNotification.list_by_object('Ticket', ticket_id) - assert(notifications.empty?, 'still notifications for destroyed ticket available') + assert(notifications.blank?, 'still notifications for destroyed ticket available') end end diff --git a/test/unit/organization_domain_based_assignment_test.rb b/test/unit/organization_domain_based_assignment_test.rb index bfc2cff9a..4b2e04523 100644 --- a/test/unit/organization_domain_based_assignment_test.rb +++ b/test/unit/organization_domain_based_assignment_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class OrganizationDomainBasedAssignmentTest < ActiveSupport::TestCase diff --git a/test/unit/organization_ref_object_touch_test.rb b/test/unit/organization_ref_object_touch_test.rb index f6095fd98..f327bee06 100644 --- a/test/unit/organization_ref_object_touch_test.rb +++ b/test/unit/organization_ref_object_touch_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class OrganizationRefObjectTouchTest < ActiveSupport::TestCase diff --git a/test/unit/overview_test.rb b/test/unit/overview_test.rb index 145b64951..a70b2d381 100644 --- a/test/unit/overview_test.rb +++ b/test/unit/overview_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class OverviewTest < ActiveSupport::TestCase @@ -18,9 +18,9 @@ class OverviewTest < ActiveSupport::TestCase direction: 'DESC', }, view: { - d: %w(title customer state created_at), - s: %w(number title customer state created_at), - m: %w(number title customer state created_at), + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], view_mode_default: 's', }, ) @@ -40,9 +40,9 @@ class OverviewTest < ActiveSupport::TestCase direction: 'DESC', }, view: { - d: %w(title customer state created_at), - s: %w(number title customer state created_at), - m: %w(number title customer state created_at), + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], view_mode_default: 's', }, ) @@ -62,9 +62,9 @@ class OverviewTest < ActiveSupport::TestCase direction: 'DESC', }, view: { - d: %w(title customer state created_at), - s: %w(number title customer state created_at), - m: %w(number title customer state created_at), + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], view_mode_default: 's', }, ) @@ -84,9 +84,9 @@ class OverviewTest < ActiveSupport::TestCase direction: 'DESC', }, view: { - d: %w(title customer state created_at), - s: %w(number title customer state created_at), - m: %w(number title customer state created_at), + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], view_mode_default: 's', }, ) @@ -106,9 +106,9 @@ class OverviewTest < ActiveSupport::TestCase direction: 'DESC', }, view: { - d: %w(title customer state created_at), - s: %w(number title customer state created_at), - m: %w(number title customer state created_at), + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], view_mode_default: 's', }, ) @@ -126,9 +126,9 @@ class OverviewTest < ActiveSupport::TestCase direction: 'DESC', }, view: { - d: %w(title customer state created_at), - s: %w(number title customer state created_at), - m: %w(number title customer state created_at), + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], view_mode_default: 's', }, ) @@ -150,9 +150,9 @@ class OverviewTest < ActiveSupport::TestCase direction: 'DESC', }, view: { - d: %w(title customer state created_at), - s: %w(number title customer state created_at), - m: %w(number title customer state created_at), + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], view_mode_default: 's', }, ) @@ -172,9 +172,9 @@ class OverviewTest < ActiveSupport::TestCase direction: 'DESC', }, view: { - d: %w(title customer state created_at), - s: %w(number title customer state created_at), - m: %w(number title customer state created_at), + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], view_mode_default: 's', }, ) @@ -195,9 +195,9 @@ class OverviewTest < ActiveSupport::TestCase direction: 'DESC', }, view: { - d: %w(title customer state created_at), - s: %w(number title customer state created_at), - m: %w(number title customer state created_at), + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], view_mode_default: 's', }, ) diff --git a/test/unit/permission_test.rb b/test/unit/permission_test.rb index 10d30cf77..b8ccc0268 100644 --- a/test/unit/permission_test.rb +++ b/test/unit/permission_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class PermissionTest < ActiveSupport::TestCase diff --git a/test/unit/recent_view_test.rb b/test/unit/recent_view_test.rb index 6604b87ce..aabd1fe5f 100644 --- a/test/unit/recent_view_test.rb +++ b/test/unit/recent_view_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class RecentViewTest < ActiveSupport::TestCase diff --git a/test/unit/role_test.rb b/test/unit/role_test.rb index 81bb767ef..7c03368d1 100644 --- a/test/unit/role_test.rb +++ b/test/unit/role_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class RoleTest < ActiveSupport::TestCase diff --git a/test/unit/role_validate_agent_limit_test.rb b/test/unit/role_validate_agent_limit_test.rb index b10d8b523..ded98ea6a 100644 --- a/test/unit/role_validate_agent_limit_test.rb +++ b/test/unit/role_validate_agent_limit_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class RoleValidateAgentLimit < ActiveSupport::TestCase @@ -101,7 +101,7 @@ class RoleValidateAgentLimit < ActiveSupport::TestCase active: true, ) - user1.roles = Role.where(name: %w(Admin Agent)) + user1.roles = Role.where(name: %w[Admin Agent]) user1.role_ids = [Role.find_by(name: 'Agent').id] diff --git a/test/unit/session_basic_test.rb b/test/unit/session_basic_test.rb index b3a029774..072cf492d 100644 --- a/test/unit/session_basic_test.rb +++ b/test/unit/session_basic_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class SessionBasicTest < ActiveSupport::TestCase @@ -50,7 +50,7 @@ class SessionBasicTest < ActiveSupport::TestCase test 'c session create / update' do # create users - roles = Role.where(name: %w(Agent)) + roles = Role.where(name: %w[Agent]) groups = Group.all agent1 = User.create_or_update( @@ -124,10 +124,10 @@ class SessionBasicTest < ActiveSupport::TestCase # get whole collections result1 = collection_client1.push - assert(!result1.empty?, 'check collections') + assert(result1.present?, 'check collections') sleep 0.6 result2 = collection_client2.push - assert(!result2.empty?, 'check collections') + assert(result2.present?, 'check collections') assert_equal(result1, result2, 'check collections') # next check should be empty @@ -145,10 +145,10 @@ class SessionBasicTest < ActiveSupport::TestCase # get whole collections result1 = collection_client1.push - assert(!result1.empty?, 'check collections - after touch') + assert(result1.present?, 'check collections - after touch') result2 = collection_client2.push - assert(!result2.empty?, 'check collections - after touch') + assert(result2.present?, 'check collections - after touch') assert_equal(result1, result2, 'check collections') # check again after touch @@ -168,9 +168,9 @@ class SessionBasicTest < ActiveSupport::TestCase # get whole collections result1 = collection_client1.push - assert(!result1.empty?, 'check collections - after create') + assert(result1.present?, 'check collections - after create') result2 = collection_client2.push - assert(!result2.empty?, 'check collections - after create') + assert(result2.present?, 'check collections - after create') assert_equal(result1, result2, 'check collections') # check again after create @@ -186,9 +186,9 @@ class SessionBasicTest < ActiveSupport::TestCase # get whole collections result1 = collection_client1.push - assert(!result1.empty?, 'check collections - after destroy') + assert(result1.present?, 'check collections - after destroy') result2 = collection_client2.push - assert(!result2.empty?, 'check collections - after destroy') + assert(result2.present?, 'check collections - after destroy') assert_equal(result1, result2, 'check collections') # check again after destroy @@ -203,7 +203,7 @@ class SessionBasicTest < ActiveSupport::TestCase test 'c activity stream' do # create users - roles = Role.where(name: %w(Agent Admin)) + roles = Role.where(name: %w[Agent Admin]) groups = Group.all agent1 = User.create_or_update( @@ -265,7 +265,7 @@ class SessionBasicTest < ActiveSupport::TestCase test 'c ticket_create' do # create users - roles = Role.where(name: %w(Agent Admin)) + roles = Role.where(name: %w[Agent Admin]) groups = Group.all agent1 = User.create_or_update( diff --git a/test/unit/session_basic_ticket_test.rb b/test/unit/session_basic_ticket_test.rb index 2defadf8e..20706036e 100644 --- a/test/unit/session_basic_ticket_test.rb +++ b/test/unit/session_basic_ticket_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class SessionBasicTicketTest < ActiveSupport::TestCase @@ -30,7 +30,7 @@ class SessionBasicTicketTest < ActiveSupport::TestCase groups: groups, ) Overview.destroy_all - load "#{Rails.root}/db/seeds/overviews.rb" + load Rails.root.join('db', 'seeds', 'overviews.rb') end test 'asset needed' do diff --git a/test/unit/session_collections_test.rb b/test/unit/session_collections_test.rb index 0af97e5d1..d58b01918 100644 --- a/test/unit/session_collections_test.rb +++ b/test/unit/session_collections_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class SessionCollectionsTest < ActiveSupport::TestCase @@ -8,7 +8,7 @@ class SessionCollectionsTest < ActiveSupport::TestCase UserInfo.current_user_id = 1 # create users - roles = Role.where(name: %w(Agent Admin)) + roles = Role.where(name: %w[Agent Admin]) groups = Group.all agent1 = User.create_or_update( @@ -94,12 +94,12 @@ class SessionCollectionsTest < ActiveSupport::TestCase # next check should be empty result1 = collection_client1.push - assert(result1.empty?, 'check collections - recall') + assert(result1.blank?, 'check collections - recall') travel 0.4.seconds result2 = collection_client2.push - assert(result2.empty?, 'check collections - recall') + assert(result2.blank?, 'check collections - recall') result3 = collection_client3.push - assert(result3.empty?, 'check collections - recall') + assert(result3.blank?, 'check collections - recall') # change collection group = Group.first @@ -123,11 +123,11 @@ class SessionCollectionsTest < ActiveSupport::TestCase # next check should be empty travel 0.5.seconds result1 = collection_client1.push - assert(result1.empty?, 'check collections - recall') + assert(result1.blank?, 'check collections - recall') result2 = collection_client2.push - assert(result2.empty?, 'check collections - recall') + assert(result2.blank?, 'check collections - recall') result3 = collection_client3.push - assert(result3.empty?, 'check collections - recall') + assert(result3.blank?, 'check collections - recall') travel 10.seconds Sessions.destroy_idle_sessions(3) @@ -171,7 +171,7 @@ class SessionCollectionsTest < ActiveSupport::TestCase end test 'b assets' do - roles = Role.where(name: %w(Agent Admin)) + roles = Role.where(name: %w[Agent Admin]) groups = Group.all.order(id: :asc) UserInfo.current_user_id = 2 diff --git a/test/unit/session_enhanced_test.rb b/test/unit/session_enhanced_test.rb index 680933deb..2443adeba 100644 --- a/test/unit/session_enhanced_test.rb +++ b/test/unit/session_enhanced_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class SessionEnhancedTest < ActiveSupport::TestCase @@ -331,13 +331,13 @@ class SessionEnhancedTest < ActiveSupport::TestCase #puts "rc: " next if !message['data'] - message['data'].each do |key, _value| + message['data'].each_key do |key| #puts "rc: #{key}" collections_result[key] = true end end #puts "c: #{collections_result.inspect}" - collections_orig.each do |key, _value| + collections_orig.each_key do |key| if collections_orig[key].nil? assert_nil(collections_result[key], "collection message for #{key} #{type}-check (client_id #{client_id})") else diff --git a/test/unit/stats_ticket_waiting_time_test.rb b/test/unit/stats_ticket_waiting_time_test.rb index bd4229d0c..a1842090c 100644 --- a/test/unit/stats_ticket_waiting_time_test.rb +++ b/test/unit/stats_ticket_waiting_time_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' require 'stats/ticket_waiting_time' diff --git a/test/unit/store_test.rb b/test/unit/store_test.rb index 134c6ae76..bd1732fce 100644 --- a/test/unit/store_test.rb +++ b/test/unit/store_test.rb @@ -1,11 +1,11 @@ -# encoding: utf-8 + require 'test_helper' class StoreTest < ActiveSupport::TestCase test 'store fs - get_location' do sha = 'ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73' location = Store::Provider::File.get_location(sha) - assert_equal("#{Rails.root}/storage/fs/ed70/02b4/39e9a/c845f/22357d8/22bac14/44730fbdb6016d3ec9432297b9ec9f73", location) + assert_equal(Rails.root.join('storage', 'fs', 'ed70', '02b4', '39e9a', 'c845f', '22357d8', '22bac14', '44730fbdb6016d3ec9432297b9ec9f73').to_s, location) end test 'store fs - empty dir remove' do @@ -19,13 +19,13 @@ class StoreTest < ActiveSupport::TestCase result = Store::Provider::File.delete(sha) exists = File.exist?(location) assert(!exists) - exists = File.exist?("#{Rails.root}/storage/fs/ed70/02b4") + exists = File.exist?(Rails.root.join('storage', 'fs', 'ed70', '02b4')) assert(!exists) - exists = File.exist?("#{Rails.root}/storage/fs/ed70/") + exists = File.exist?(Rails.root.join('storage', 'fs', 'ed70')) assert(!exists) - exists = File.exist?("#{Rails.root}/storage/fs/") + exists = File.exist?(Rails.root.join('storage', 'fs')) assert(exists) - exists = File.exist?("#{Rails.root}/storage/") + exists = File.exist?(Rails.root.join('storage')) assert(exists) end diff --git a/test/unit/tag_test.rb b/test/unit/tag_test.rb index c54ce4416..a1d1809d3 100644 --- a/test/unit/tag_test.rb +++ b/test/unit/tag_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TagTest < ActiveSupport::TestCase diff --git a/test/unit/ticket_article_communicate_test.rb b/test/unit/ticket_article_communicate_test.rb index 533cbe657..a4d0618f0 100644 --- a/test/unit/ticket_article_communicate_test.rb +++ b/test/unit/ticket_article_communicate_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TicketArticleCommunicateTest < ActiveSupport::TestCase diff --git a/test/unit/ticket_article_dos_test.rb b/test/unit/ticket_article_dos_test.rb index 1891bcb5b..f22ba38ec 100644 --- a/test/unit/ticket_article_dos_test.rb +++ b/test/unit/ticket_article_dos_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TicketArticleDos < ActiveSupport::TestCase diff --git a/test/unit/ticket_article_store_empty.rb b/test/unit/ticket_article_store_empty.rb index ad3cd5049..913ef2798 100644 --- a/test/unit/ticket_article_store_empty.rb +++ b/test/unit/ticket_article_store_empty.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TicketArticleStoreEmpty < ActiveSupport::TestCase diff --git a/test/unit/ticket_article_time_accouting_test.rb b/test/unit/ticket_article_time_accouting_test.rb index 4bb650c7c..54adc448c 100644 --- a/test/unit/ticket_article_time_accouting_test.rb +++ b/test/unit/ticket_article_time_accouting_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TicketArticleTimeAccoutingTest < ActiveSupport::TestCase diff --git a/test/unit/ticket_article_twitter_test.rb b/test/unit/ticket_article_twitter_test.rb index a4f743a11..d76f9bf11 100644 --- a/test/unit/ticket_article_twitter_test.rb +++ b/test/unit/ticket_article_twitter_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TicketArticleTwitter < ActiveSupport::TestCase diff --git a/test/unit/ticket_customer_organization_update_test.rb b/test/unit/ticket_customer_organization_update_test.rb index 8327048e9..7d7914d60 100644 --- a/test/unit/ticket_customer_organization_update_test.rb +++ b/test/unit/ticket_customer_organization_update_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TicketCustomerOrganizationUpdateTest < ActiveSupport::TestCase diff --git a/test/unit/ticket_escalation_test.rb b/test/unit/ticket_escalation_test.rb index a79be51ca..a963874f0 100644 --- a/test/unit/ticket_escalation_test.rb +++ b/test/unit/ticket_escalation_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TicketEscalationTest < ActiveSupport::TestCase diff --git a/test/unit/ticket_last_owner_update_test.rb b/test/unit/ticket_last_owner_update_test.rb index 80ec7478d..e4dc85bb6 100644 --- a/test/unit/ticket_last_owner_update_test.rb +++ b/test/unit/ticket_last_owner_update_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TicketLastOwnerUpdateTest < ActiveSupport::TestCase diff --git a/test/unit/ticket_notification_test.rb b/test/unit/ticket_notification_test.rb index 0288f41dd..dee8ec856 100644 --- a/test/unit/ticket_notification_test.rb +++ b/test/unit/ticket_notification_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TicketNotificationTest < ActiveSupport::TestCase @@ -25,6 +25,7 @@ class TicketNotificationTest < ActiveSupport::TestCase }, perform: { 'notification.email' => { + # rubocop:disable Lint/InterpolationCheck 'body' => '

                  Your request (Ticket##{ticket.number}) has been received and will be reviewed by our support staff.


                  To provide additional information, please reply to this email or click on the following link: @@ -34,6 +35,7 @@ class TicketNotificationTest < ActiveSupport::TestCase

                  Zammad, your customer support system

                  ', 'recipient' => 'ticket_customer', 'subject' => 'Thanks for your inquiry (#{ticket.title})', + # rubocop:enable Lint/InterpolationCheck }, }, disable_notification: true, diff --git a/test/unit/ticket_null_byte_test.rb b/test/unit/ticket_null_byte_test.rb index aa3fdbd6a..0579112bf 100644 --- a/test/unit/ticket_null_byte_test.rb +++ b/test/unit/ticket_null_byte_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TicketNullByteTest < ActiveSupport::TestCase diff --git a/test/unit/ticket_overview_out_of_office_test.rb b/test/unit/ticket_overview_out_of_office_test.rb index f1901f628..9445f944c 100644 --- a/test/unit/ticket_overview_out_of_office_test.rb +++ b/test/unit/ticket_overview_out_of_office_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TicketOverviewOutOfOfficeTest < ActiveSupport::TestCase @@ -85,9 +85,9 @@ class TicketOverviewOutOfOfficeTest < ActiveSupport::TestCase direction: 'ASC', }, view: { - d: %w(title customer group created_at), - s: %w(title customer group created_at), - m: %w(number title customer group created_at), + d: %w[title customer group created_at], + s: %w[title customer group created_at], + m: %w[number title customer group created_at], view_mode_default: 's', }, ) @@ -111,9 +111,9 @@ class TicketOverviewOutOfOfficeTest < ActiveSupport::TestCase direction: 'ASC', }, view: { - d: %w(title customer group created_at), - s: %w(title customer group created_at), - m: %w(number title customer group created_at), + d: %w[title customer group created_at], + s: %w[title customer group created_at], + m: %w[number title customer group created_at], view_mode_default: 's', }, ) @@ -140,9 +140,9 @@ class TicketOverviewOutOfOfficeTest < ActiveSupport::TestCase direction: 'DESC', }, view: { - d: %w(title customer state created_at), - s: %w(number title state created_at), - m: %w(number title state created_at), + d: %w[title customer state created_at], + s: %w[number title state created_at], + m: %w[number title state created_at], view_mode_default: 's', }, ) diff --git a/test/unit/ticket_overview_test.rb b/test/unit/ticket_overview_test.rb index 284787ee4..6ab583cab 100644 --- a/test/unit/ticket_overview_test.rb +++ b/test/unit/ticket_overview_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TicketOverviewTest < ActiveSupport::TestCase @@ -106,9 +106,9 @@ class TicketOverviewTest < ActiveSupport::TestCase direction: 'ASC', }, view: { - d: %w(title customer group created_at), - s: %w(title customer group created_at), - m: %w(number title customer group created_at), + d: %w[title customer group created_at], + s: %w[title customer group created_at], + m: %w[number title customer group created_at], view_mode_default: 's', }, ) @@ -133,9 +133,9 @@ class TicketOverviewTest < ActiveSupport::TestCase direction: 'ASC', }, view: { - d: %w(title customer group created_at), - s: %w(title customer group created_at), - m: %w(number title customer group created_at), + d: %w[title customer group created_at], + s: %w[title customer group created_at], + m: %w[number title customer group created_at], view_mode_default: 's', }, ) @@ -160,9 +160,9 @@ class TicketOverviewTest < ActiveSupport::TestCase direction: 'ASC', }, view: { - d: %w(title customer group created_at), - s: %w(title customer group created_at), - m: %w(number title customer group created_at), + d: %w[title customer group created_at], + s: %w[title customer group created_at], + m: %w[number title customer group created_at], view_mode_default: 's', }, ) @@ -187,9 +187,9 @@ class TicketOverviewTest < ActiveSupport::TestCase direction: 'ASC', }, view: { - d: %w(title customer group created_at), - s: %w(title customer group created_at), - m: %w(number title customer group created_at), + d: %w[title customer group created_at], + s: %w[title customer group created_at], + m: %w[number title customer group created_at], view_mode_default: 's', }, ) @@ -215,9 +215,9 @@ class TicketOverviewTest < ActiveSupport::TestCase direction: 'DESC', }, view: { - d: %w(title customer state created_at), - s: %w(number title state created_at), - m: %w(number title state created_at), + d: %w[title customer state created_at], + s: %w[number title state created_at], + m: %w[number title state created_at], view_mode_default: 's', }, ) @@ -242,9 +242,9 @@ class TicketOverviewTest < ActiveSupport::TestCase direction: 'DESC', }, view: { - d: %w(title customer state created_at), - s: %w(number title customer state created_at), - m: %w(number title customer state created_at), + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], view_mode_default: 's', }, ) @@ -270,9 +270,9 @@ class TicketOverviewTest < ActiveSupport::TestCase direction: 'DESC', }, view: { - d: %w(title customer state created_at), - s: %w(number title customer state created_at), - m: %w(number title customer state created_at), + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], view_mode_default: 's', }, ) @@ -294,9 +294,9 @@ class TicketOverviewTest < ActiveSupport::TestCase direction: 'DESC', }, view: { - d: %w(title customer state created_at), - s: %w(number title customer state created_at), - m: %w(number title customer state created_at), + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], view_mode_default: 's', }, ) diff --git a/test/unit/ticket_priority_test.rb b/test/unit/ticket_priority_test.rb index c6cbe4dee..92ff51064 100644 --- a/test/unit/ticket_priority_test.rb +++ b/test/unit/ticket_priority_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TicketPriorityTest < ActiveSupport::TestCase diff --git a/test/unit/ticket_ref_object_touch_test.rb b/test/unit/ticket_ref_object_touch_test.rb index 77bb2ed6c..d2957c7a5 100644 --- a/test/unit/ticket_ref_object_touch_test.rb +++ b/test/unit/ticket_ref_object_touch_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TicketRefObjectTouchTest < ActiveSupport::TestCase diff --git a/test/unit/ticket_selector_test.rb b/test/unit/ticket_selector_test.rb index 374172b09..6ba4e33e7 100644 --- a/test/unit/ticket_selector_test.rb +++ b/test/unit/ticket_selector_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TicketSelectorTest < ActiveSupport::TestCase diff --git a/test/unit/ticket_sla_test.rb b/test/unit/ticket_sla_test.rb index fa17d4eb9..83f15a403 100644 --- a/test/unit/ticket_sla_test.rb +++ b/test/unit/ticket_sla_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TicketSlaTest < ActiveSupport::TestCase @@ -141,7 +141,7 @@ class TicketSlaTest < ActiveSupport::TestCase condition: { 'ticket.priority_id' => { operator: 'is', - value: %w(1 2 3), + value: %w[1 2 3], }, }, calendar_id: calendar2.id, @@ -551,7 +551,7 @@ class TicketSlaTest < ActiveSupport::TestCase condition: { 'ticket.priority_id' => { operator: 'is', - value: %w(2), + value: %w[2], }, }, first_response_time: 60, @@ -676,7 +676,7 @@ class TicketSlaTest < ActiveSupport::TestCase condition: { 'ticket.priority_id' => { operator: 'is not', - value: %w(1 2 3), + value: %w[1 2 3], }, }, calendar_id: calendar.id, @@ -1640,7 +1640,7 @@ class TicketSlaTest < ActiveSupport::TestCase condition: { 'ticket.priority_id' => { operator: 'is', - value: %w(1 2 3), + value: %w[1 2 3], }, 'article.subject' => { operator: 'contains', @@ -1670,7 +1670,7 @@ class TicketSlaTest < ActiveSupport::TestCase condition: { 'ticket.priority_id' => { operator: 'is', - value: %w(1 2 3), + value: %w[1 2 3], }, 'ticket.title' => { operator: 'contains', @@ -1700,7 +1700,7 @@ class TicketSlaTest < ActiveSupport::TestCase condition: { 'ticket.priority_id' => { operator: 'is', - value: %w(1 2 3), + value: %w[1 2 3], }, 'ticket.title' => { operator: 'contains', diff --git a/test/unit/ticket_state_test.rb b/test/unit/ticket_state_test.rb index b4f9996e7..3e3cbf7a2 100644 --- a/test/unit/ticket_state_test.rb +++ b/test/unit/ticket_state_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TicketStateTest < ActiveSupport::TestCase diff --git a/test/unit/ticket_test.rb b/test/unit/ticket_test.rb index 39a51b90f..90dae91ea 100644 --- a/test/unit/ticket_test.rb +++ b/test/unit/ticket_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TicketTest < ActiveSupport::TestCase diff --git a/test/unit/ticket_trigger_test.rb b/test/unit/ticket_trigger_test.rb index 686827c6c..68aa9da73 100644 --- a/test/unit/ticket_trigger_test.rb +++ b/test/unit/ticket_trigger_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 +# rubocop:disable Lint/InterpolationCheck require 'test_helper' class TicketTriggerTest < ActiveSupport::TestCase @@ -189,7 +189,7 @@ class TicketTriggerTest < ActiveSupport::TestCase assert_equal('new', ticket1.state.name, 'ticket1.state verify') assert_equal('3 high', ticket1.priority.name, 'ticket1.priority verify') assert_equal(2, ticket1.articles.count, 'ticket1.articles verify') - assert_equal(%w(aa kk abc), ticket1.tag_list) + assert_equal(%w[aa kk abc], ticket1.tag_list) article1 = ticket1.articles.last assert_match('Zammad ', article1.from) assert_match('nicole.braun@zammad.org', article1.to) @@ -208,7 +208,7 @@ class TicketTriggerTest < ActiveSupport::TestCase assert_equal('new', ticket1.state.name, 'ticket1.state verify') assert_equal('2 normal', ticket1.priority.name, 'ticket1.priority verify') assert_equal(2, ticket1.articles.count, 'ticket1.articles verify') - assert_equal(%w(aa kk abc), ticket1.tag_list) + assert_equal(%w[aa kk abc], ticket1.tag_list) ticket1.state = Ticket::State.lookup(name: 'open') ticket1.save! @@ -221,7 +221,7 @@ class TicketTriggerTest < ActiveSupport::TestCase assert_equal('open', ticket1.state.name, 'ticket1.state verify') assert_equal('2 normal', ticket1.priority.name, 'ticket1.priority verify') assert_equal(2, ticket1.articles.count, 'ticket1.articles verify') - assert_equal(%w(aa kk abc), ticket1.tag_list) + assert_equal(%w[aa kk abc], ticket1.tag_list) ticket1.state = Ticket::State.lookup(name: 'new') ticket1.save! @@ -234,7 +234,7 @@ class TicketTriggerTest < ActiveSupport::TestCase assert_equal('new', ticket1.state.name, 'ticket1.state verify') assert_equal('3 high', ticket1.priority.name, 'ticket1.priority verify') assert_equal(2, ticket1.articles.count, 'ticket1.articles verify') - assert_equal(%w(aa abc), ticket1.tag_list) + assert_equal(%w[aa abc], ticket1.tag_list) ticket2 = Ticket.create( title: "some title\n äöüß", @@ -303,7 +303,7 @@ class TicketTriggerTest < ActiveSupport::TestCase assert_equal('new', ticket3.state.name, 'ticket3.state verify') assert_equal('3 high', ticket3.priority.name, 'ticket3.priority verify') assert_equal(3, ticket3.articles.count, 'ticket3.articles verify') - assert_equal(%w(aa kk abc article_create_trigger), ticket3.tag_list) + assert_equal(%w[aa kk abc article_create_trigger], ticket3.tag_list) article3 = ticket3.articles[1] assert_match('Zammad ', article3.from) assert_match('nicole.braun@zammad.org', article3.to) @@ -341,7 +341,7 @@ class TicketTriggerTest < ActiveSupport::TestCase assert_equal('new', ticket3.state.name, 'ticket3.state verify') assert_equal('3 high', ticket3.priority.name, 'ticket3.priority verify') assert_equal(4, ticket3.articles.count, 'ticket3.articles verify') - assert_equal(%w(aa abc article_create_trigger), ticket3.tag_list) + assert_equal(%w[aa abc article_create_trigger], ticket3.tag_list) Ticket::Article.create( ticket_id: ticket3.id, @@ -366,7 +366,7 @@ class TicketTriggerTest < ActiveSupport::TestCase assert_equal('new', ticket3.state.name, 'ticket3.state verify') assert_equal('3 high', ticket3.priority.name, 'ticket3.priority verify') assert_equal(5, ticket3.articles.count, 'ticket3.articles verify') - assert_equal(%w(aa abc article_create_trigger), ticket3.tag_list) + assert_equal(%w[aa abc article_create_trigger], ticket3.tag_list) Ticket::Article.create( ticket_id: ticket3.id, @@ -391,7 +391,7 @@ class TicketTriggerTest < ActiveSupport::TestCase assert_equal('new', ticket3.state.name, 'ticket3.state verify') assert_equal('3 high', ticket3.priority.name, 'ticket3.priority verify') assert_equal(7, ticket3.articles.count, 'ticket3.articles verify') - assert_equal(%w(aa abc article_create_trigger), ticket3.tag_list) + assert_equal(%w[aa abc article_create_trigger], ticket3.tag_list) end test '2 actions - create' do @@ -2959,7 +2959,7 @@ class TicketTriggerTest < ActiveSupport::TestCase perform: { 'notification.email' => { 'body' => 'some text
                  #{ticket.customer.lastname}
                  #{ticket.title}
                  #{article.body}', - 'recipient' => %w(ticket_owner article_last_sender), + 'recipient' => %w[ticket_owner article_last_sender], 'subject' => 'Thanks for your inquiry (#{ticket.title})!', }, }, @@ -2975,7 +2975,7 @@ class TicketTriggerTest < ActiveSupport::TestCase email: 'admin+owner_recipient@example.com', password: 'adminpw', active: true, - roles: Role.where(name: %w(Admin Agent)), + roles: Role.where(name: %w[Admin Agent]), updated_by_id: 1, created_by_id: 1, ) @@ -3469,7 +3469,7 @@ class TicketTriggerTest < ActiveSupport::TestCase assert_equal('new', ticket1.state.name, 'ticket1.state verify') assert_equal('3 high', ticket1.priority.name, 'ticket1.priority verify') assert_equal(2, ticket1.articles.count, 'ticket1.articles verify') - assert_equal(%w(aa kk), ticket1.tag_list) + assert_equal(%w[aa kk], ticket1.tag_list) article1 = ticket1.articles.last assert_match('Zammad ', article1.from) assert_match('nicole.braun@zammad.org', article1.to) diff --git a/test/unit/ticket_xss_test.rb b/test/unit/ticket_xss_test.rb index 7ebd33b13..a449b779b 100644 --- a/test/unit/ticket_xss_test.rb +++ b/test/unit/ticket_xss_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TicketXssTest < ActiveSupport::TestCase diff --git a/test/unit/token_test.rb b/test/unit/token_test.rb index f2c1d990f..ec1a2bf5b 100644 --- a/test/unit/token_test.rb +++ b/test/unit/token_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class TokenTest < ActiveSupport::TestCase diff --git a/test/unit/user_ref_object_touch_test.rb b/test/unit/user_ref_object_touch_test.rb index 3060cb474..c30f284b2 100644 --- a/test/unit/user_ref_object_touch_test.rb +++ b/test/unit/user_ref_object_touch_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class UserRefObjectTouchTest < ActiveSupport::TestCase diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index e04984cf8..a6842b556 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class UserTest < ActiveSupport::TestCase @@ -264,9 +264,7 @@ class UserTest < ActiveSupport::TestCase # check if user exists user = User.find_by(login: test[:create][:login]) - if user - user.destroy! - end + user&.destroy! user = User.create!(test[:create]) @@ -320,7 +318,7 @@ class UserTest < ActiveSupport::TestCase email: " #{email} ", password: 'customerpw', active: true, - roles: Role.where(name: %w(Customer)), + roles: Role.where(name: %w[Customer]), updated_by_id: 1, created_by_id: 1, ) @@ -336,7 +334,7 @@ class UserTest < ActiveSupport::TestCase email: "\u{00a0}#{email}\u{00a0}", password: 'customerpw', active: true, - roles: Role.where(name: %w(Customer)), + roles: Role.where(name: %w[Customer]), updated_by_id: 1, created_by_id: 1, ) @@ -354,7 +352,7 @@ class UserTest < ActiveSupport::TestCase email: "\u{200B}#{email}\u{200B}", password: 'customerpw', active: true, - roles: Role.where(name: %w(Customer)), + roles: Role.where(name: %w[Customer]), updated_by_id: 1, created_by_id: 1, ) @@ -372,7 +370,7 @@ class UserTest < ActiveSupport::TestCase email: "\u{200B}#{email}\u{200B}", password: 'customerpw', active: true, - roles: Role.where(name: %w(Customer)), + roles: Role.where(name: %w[Customer]), updated_by_id: 1, created_by_id: 1, ) @@ -390,7 +388,7 @@ class UserTest < ActiveSupport::TestCase email: "\u{200B}#{email}\u{200B}\u{2007}\u{2008}", password: 'customerpw', active: true, - roles: Role.where(name: %w(Customer)), + roles: Role.where(name: %w[Customer]), updated_by_id: 1, created_by_id: 1, ) @@ -413,7 +411,7 @@ class UserTest < ActiveSupport::TestCase #email: "", password: 'adminpw', active: true, - roles: Role.where(name: %w(Admin Agent)), + roles: Role.where(name: %w[Admin Agent]), updated_by_id: 1, created_by_id: 1, ) @@ -451,7 +449,7 @@ class UserTest < ActiveSupport::TestCase #email: "", password: 'adminpw', active: true, - roles: Role.where(name: %w(Admin Agent)), + roles: Role.where(name: %w[Admin Agent]), updated_by_id: 1, created_by_id: 1, ) @@ -487,7 +485,7 @@ class UserTest < ActiveSupport::TestCase email: email1, password: 'adminpw', active: true, - roles: Role.where(name: %w(Admin Agent)), + roles: Role.where(name: %w[Admin Agent]), updated_by_id: 1, created_by_id: 1, ) @@ -503,7 +501,7 @@ class UserTest < ActiveSupport::TestCase email: email1, password: 'adminpw', active: true, - roles: Role.where(name: %w(Admin Agent)), + roles: Role.where(name: %w[Admin Agent]), updated_by_id: 1, created_by_id: 1, ) @@ -516,7 +514,7 @@ class UserTest < ActiveSupport::TestCase email: email2, password: 'adminpw', active: true, - roles: Role.where(name: %w(Admin Agent)), + roles: Role.where(name: %w[Admin Agent]), updated_by_id: 1, created_by_id: 1, ) @@ -545,7 +543,7 @@ class UserTest < ActiveSupport::TestCase email: email1, password: 'adminpw', active: true, - roles: Role.where(name: %w(Admin Agent)), + roles: Role.where(name: %w[Admin Agent]), updated_by_id: 1, created_by_id: 1, ) @@ -560,7 +558,7 @@ class UserTest < ActiveSupport::TestCase email: email1, password: 'adminpw', active: true, - roles: Role.where(name: %w(Admin Agent)), + roles: Role.where(name: %w[Admin Agent]), updated_by_id: 1, created_by_id: 1, ) @@ -580,7 +578,7 @@ class UserTest < ActiveSupport::TestCase email: "admin-role#{name}@example.com", password: 'adminpw', active: true, - roles: Role.where(name: %w(Admin Agent)), + roles: Role.where(name: %w[Admin Agent]), updated_by_id: 1, created_by_id: 1, ) @@ -654,12 +652,12 @@ class UserTest < ActiveSupport::TestCase ) assert_raises(RuntimeError) do - customer3.roles = Role.where(name: %w(Customer Admin)) + customer3.roles = Role.where(name: %w[Customer Admin]) end assert_raises(RuntimeError) do - customer3.roles = Role.where(name: %w(Customer Agent)) + customer3.roles = Role.where(name: %w[Customer Agent]) end - customer3.roles = Role.where(name: %w(Admin Agent)) + customer3.roles = Role.where(name: %w[Admin Agent]) customer3.roles.each do |role| assert_not_equal(role.name, 'Customer') end @@ -760,7 +758,7 @@ class UserTest < ActiveSupport::TestCase name: 'Test3', note: 'People who create Tickets ask for help.', preferences: { - not: %w(Test1 Test2), + not: %w[Test1 Test2], }, updated_by_id: 1, created_by_id: 1 @@ -872,7 +870,7 @@ class UserTest < ActiveSupport::TestCase email: "admin-role#{name}@example.com", password: 'adminpw', active: true, - roles: Role.where(name: %w(Admin Agent)), + roles: Role.where(name: %w[Admin Agent]), updated_by_id: 1, created_by_id: 1, ) @@ -901,7 +899,7 @@ class UserTest < ActiveSupport::TestCase created_by_id: 1, ) users = User.with_permissions('not_existing') - assert(users.empty?) + assert(users.blank?) users = User.with_permissions('admin') assert_equal(admin_count + 1, users.count) @@ -949,7 +947,7 @@ class UserTest < ActiveSupport::TestCase email: "admin-role#{random}@example.com", password: 'adminpw', active: true, - roles: Role.where(name: %w(Admin Agent)), + roles: Role.where(name: %w[Admin Agent]), updated_by_id: 1, created_by_id: 1, ) @@ -962,7 +960,7 @@ class UserTest < ActiveSupport::TestCase email: "admin-role#{random}@example.com", password: 'adminpw', active: true, - roles: Role.where(name: %w(Admin Agent)), + roles: Role.where(name: %w[Admin Agent]), updated_by_id: 1, created_by_id: 1, ) @@ -975,7 +973,7 @@ class UserTest < ActiveSupport::TestCase email: "admin-role#{random}@example.com", password: 'adminpw', active: true, - roles: Role.where(name: %w(Admin Agent)), + roles: Role.where(name: %w[Admin Agent]), updated_by_id: 1, created_by_id: 1, ) @@ -983,18 +981,18 @@ class UserTest < ActiveSupport::TestCase admin_count_inital = User.with_permissions('admin').count assert_equal(3, admin_count_inital) - admin1.update!(roles: Role.where(name: %w(Agent))) + admin1.update!(roles: Role.where(name: %w[Agent])) admin_count_inital = User.with_permissions('admin').count assert_equal(2, admin_count_inital) - admin2.update!(roles: Role.where(name: %w(Agent))) + admin2.update!(roles: Role.where(name: %w[Agent])) admin_count_inital = User.with_permissions('admin').count assert_equal(1, admin_count_inital) assert_raises(Exceptions::UnprocessableEntity) do - admin3.update!(roles: Role.where(name: %w(Agent))) + admin3.update!(roles: Role.where(name: %w[Agent])) end admin_count_inital = User.with_permissions('admin').count diff --git a/test/unit/user_validate_agent_limit_test.rb b/test/unit/user_validate_agent_limit_test.rb index 2fbfa936c..34dd758a4 100644 --- a/test/unit/user_validate_agent_limit_test.rb +++ b/test/unit/user_validate_agent_limit_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'test_helper' class UserValidateAgentLimit < ActiveSupport::TestCase From b952880d5df888800a6d1cec732f2bb2f47cef88 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Thu, 23 Nov 2017 09:14:03 +0100 Subject: [PATCH 020/196] Fixed issue #1666 - Unable to open trigger in admin interface. --- app/models/trigger.rb | 3 + app/models/trigger/assets.rb | 50 +++ test/unit/assets_test.rb | 566 -------------------------- test/unit/job_assets_test.rb | 80 ++++ test/unit/organization_assets_test.rb | 157 +++++++ test/unit/overview_assets_test.rb | 136 +++++++ test/unit/sla_assets_test.rb | 71 ++++ test/unit/trigger_assets_test.rb | 77 ++++ test/unit/user_assets_test.rb | 157 +++++++ 9 files changed, 731 insertions(+), 566 deletions(-) create mode 100644 app/models/trigger/assets.rb delete mode 100644 test/unit/assets_test.rb create mode 100644 test/unit/job_assets_test.rb create mode 100644 test/unit/organization_assets_test.rb create mode 100644 test/unit/overview_assets_test.rb create mode 100644 test/unit/sla_assets_test.rb create mode 100644 test/unit/trigger_assets_test.rb create mode 100644 test/unit/user_assets_test.rb diff --git a/app/models/trigger.rb b/app/models/trigger.rb index 65bf1cbb7..93f859c99 100644 --- a/app/models/trigger.rb +++ b/app/models/trigger.rb @@ -4,6 +4,9 @@ class Trigger < ApplicationModel include ChecksConditionValidation include CanSeed + load 'trigger/assets.rb' + include Trigger::Assets + store :condition store :perform validates :name, presence: true diff --git a/app/models/trigger/assets.rb b/app/models/trigger/assets.rb new file mode 100644 index 000000000..5fd7c40aa --- /dev/null +++ b/app/models/trigger/assets.rb @@ -0,0 +1,50 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ + +class Trigger + module Assets + +=begin + +get all assets / related models for this trigger + + trigger = Trigger.find(123) + result = trigger.assets(assets_if_exists) + +returns + + result = { + :triggers => { + 123 => trigger_model_123, + 1234 => trigger_model_1234, + } + } + +=end + + def assets(data) + + app_model_overview = Trigger.to_app_model + app_model_user = User.to_app_model + + if !data[ app_model_overview ] + data[ app_model_overview ] = {} + end + if !data[ app_model_user ] + data[ app_model_user ] = {} + end + if !data[ app_model_overview ][ id ] + data[ app_model_overview ][ id ] = attributes_with_association_ids + data = assets_of_selector('condition', data) + data = assets_of_selector('perform', data) + end + %w[created_by_id updated_by_id].each do |local_user_id| + next if !self[ local_user_id ] + next if data[ app_model_user ][ self[ local_user_id ] ] + user = User.lookup(id: self[ local_user_id ]) + next if !user + data = user.assets(data) + end + data + end + end +end diff --git a/test/unit/assets_test.rb b/test/unit/assets_test.rb deleted file mode 100644 index 538f1c492..000000000 --- a/test/unit/assets_test.rb +++ /dev/null @@ -1,566 +0,0 @@ - -require 'test_helper' - -class AssetsTest < ActiveSupport::TestCase - test 'user' do - - roles = Role.where(name: %w[Agent Admin]) - groups = Group.all - org1 = Organization.create_or_update( - name: 'some user org', - updated_by_id: 1, - created_by_id: 1, - ) - - user1 = User.create_or_update( - login: 'assets1@example.org', - firstname: 'assets1', - lastname: 'assets1', - email: 'assets1@example.org', - password: 'some_pass', - active: true, - updated_by_id: 1, - created_by_id: 1, - organization_id: org1.id, - roles: roles, - groups: groups, - ) - - user2 = User.create_or_update( - login: 'assets2@example.org', - firstname: 'assets2', - lastname: 'assets2', - email: 'assets2@example.org', - password: 'some_pass', - active: true, - updated_by_id: 1, - created_by_id: 1, - roles: roles, - groups: groups, - ) - - user3 = User.create_or_update( - login: 'assets3@example.org', - firstname: 'assets3', - lastname: 'assets3', - email: 'assets3@example.org', - password: 'some_pass', - active: true, - updated_by_id: user1.id, - created_by_id: user2.id, - roles: roles, - groups: groups, - ) - user3 = User.find(user3.id) - assets = user3.assets({}) - - org1 = Organization.find(org1.id) - attributes = org1.attributes_with_association_ids - attributes.delete('user_ids') - assert( diff(attributes, assets[:Organization][org1.id]), 'check assets') - - user1 = User.find(user1.id) - attributes = user1.attributes_with_association_ids - attributes['accounts'] = {} - attributes.delete('password') - attributes.delete('token_ids') - attributes.delete('authorization_ids') - assert( diff(attributes, assets[:User][user1.id]), 'check assets' ) - - user2 = User.find(user2.id) - attributes = user2.attributes_with_association_ids - attributes['accounts'] = {} - attributes.delete('password') - attributes.delete('token_ids') - attributes.delete('authorization_ids') - assert( diff(attributes, assets[:User][user2.id]), 'check assets' ) - - user3 = User.find(user3.id) - attributes = user3.attributes_with_association_ids - attributes['accounts'] = {} - attributes.delete('password') - attributes.delete('token_ids') - attributes.delete('authorization_ids') - assert( diff(attributes, assets[:User][user3.id]), 'check assets' ) - - # touch org, check if user1 has changed - travel 2.seconds - org2 = Organization.find(org1.id) - org2.note = "some note...#{rand(9_999_999_999_999)}" - org2.save! - - attributes = org2.attributes_with_association_ids - attributes.delete('user_ids') - assert( !diff(attributes, assets[:Organization][org2.id]), 'check assets' ) - - user1_new = User.find(user1.id) - attributes = user1_new.attributes_with_association_ids - attributes['accounts'] = {} - attributes.delete('password') - attributes.delete('token_ids') - attributes.delete('authorization_ids') - assert( !diff(attributes, assets[:User][user1_new.id]), 'check assets' ) - - # check new assets lookup - assets = user3.assets({}) - attributes = org2.attributes_with_association_ids - attributes.delete('user_ids') - assert( diff(attributes, assets[:Organization][org1.id]), 'check assets') - - user1 = User.find(user1.id) - attributes = user1.attributes_with_association_ids - attributes['accounts'] = {} - attributes.delete('password') - attributes.delete('token_ids') - attributes.delete('authorization_ids') - assert( diff(attributes, assets[:User][user1.id]), 'check assets' ) - - user2 = User.find(user2.id) - attributes = user2.attributes_with_association_ids - attributes['accounts'] = {} - attributes.delete('password') - attributes.delete('token_ids') - attributes.delete('authorization_ids') - assert( diff(attributes, assets[:User][user2.id]), 'check assets' ) - - user3 = User.find(user3.id) - attributes = user3.attributes_with_association_ids - attributes['accounts'] = {} - attributes.delete('password') - attributes.delete('token_ids') - attributes.delete('authorization_ids') - assert( diff(attributes, assets[:User][user3.id]), 'check assets' ) - travel_back - - user3.destroy! - user2.destroy! - user1.destroy! - org1.destroy! - assert_not(Organization.find_by(id: org2.id)) - end - - test 'organization' do - - roles = Role.where( name: %w[Agent Admin] ) - admin1 = User.create_or_update( - login: 'admin1@example.org', - firstname: 'admin1', - lastname: 'admin1', - email: 'admin1@example.org', - password: 'some_pass', - active: true, - updated_by_id: 1, - created_by_id: 1, - roles: roles, - ) - - roles = Role.where( name: %w[Customer] ) - org = Organization.create_or_update( - name: 'some customer org', - updated_by_id: admin1.id, - created_by_id: 1, - ) - - user1 = User.create_or_update( - login: 'assets1@example.org', - firstname: 'assets1', - lastname: 'assets1', - email: 'assets1@example.org', - password: 'some_pass', - active: true, - updated_by_id: 1, - created_by_id: 1, - organization_id: org.id, - roles: roles, - ) - - user2 = User.create_or_update( - login: 'assets2@example.org', - firstname: 'assets2', - lastname: 'assets2', - email: 'assets2@example.org', - password: 'some_pass', - active: true, - updated_by_id: 1, - created_by_id: 1, - organization_id: org.id, - roles: roles, - ) - - user3 = User.create_or_update( - login: 'assets3@example.org', - firstname: 'assets3', - lastname: 'assets3', - email: 'assets3@example.org', - password: 'some_pass', - active: true, - updated_by_id: user1.id, - created_by_id: user2.id, - roles: roles, - ) - - org = Organization.find(org.id) - assets = org.assets({}) - attributes = org.attributes_with_association_ids - attributes.delete('user_ids') - assert( diff(attributes, assets[:Organization][org.id]), 'check assets' ) - - admin1 = User.find(admin1.id) - attributes = admin1.attributes_with_association_ids - attributes['accounts'] = {} - attributes.delete('password') - attributes.delete('token_ids') - attributes.delete('authorization_ids') - assert( diff(attributes, assets[:User][admin1.id]), 'check assets' ) - - user1 = User.find(user1.id) - attributes = user1.attributes_with_association_ids - attributes['accounts'] = {} - attributes.delete('password') - attributes.delete('token_ids') - attributes.delete('authorization_ids') - assert( diff(attributes, assets[:User][user1.id]), 'check assets' ) - - user2 = User.find(user2.id) - attributes = user2.attributes_with_association_ids - attributes['accounts'] = {} - attributes.delete('password') - attributes.delete('token_ids') - attributes.delete('authorization_ids') - assert( diff(attributes, assets[:User][user2.id]), 'check assets' ) - - user3 = User.find(user3.id) - attributes = user3.attributes_with_association_ids - attributes['accounts'] = {} - attributes.delete('password') - attributes.delete('token_ids') - attributes.delete('authorization_ids') - assert_nil( assets[:User][user3.id], 'check assets' ) - - # touch user 2, check if org has changed - travel 2.seconds - user_new_2 = User.find(user2.id) - user_new_2.lastname = 'assets2' - user_new_2.save! - - org_new = Organization.find(org.id) - attributes = org_new.attributes_with_association_ids - attributes.delete('user_ids') - assert( !diff(attributes, assets[:Organization][org_new.id]), 'check assets' ) - - attributes = user_new_2.attributes_with_association_ids - attributes['accounts'] = {} - attributes.delete('password') - attributes.delete('token_ids') - attributes.delete('authorization_ids') - assert( diff(attributes, assets[:User][user_new_2.id]), 'check assets' ) - - # check new assets lookup - assets = org_new.assets({}) - attributes = org_new.attributes_with_association_ids - attributes.delete('user_ids') - assert( diff(attributes, assets[:Organization][org_new.id]), 'check assets' ) - - attributes = user_new_2.attributes_with_association_ids - attributes['accounts'] = {} - attributes.delete('password') - attributes.delete('token_ids') - attributes.delete('authorization_ids') - assert( diff(attributes, assets[:User][user_new_2.id]), 'check assets' ) - travel_back - - user3.destroy! - user2.destroy! - user1.destroy! - org.destroy! - assert_not(Organization.find_by(id: org_new.id)) - end - - def diff(o1, o2) - return true if o1 == o2 - %w[updated_at created_at].each do |item| - if o1[item] - o1[item] = o1[item].to_s - end - if o2[item] - o2[item] = o2[item].to_s - end - end - return true if (o1.to_a - o2.to_a).blank? - #puts "ERROR: difference \n1: #{o1.inspect}\n2: #{o2.inspect}\ndiff: #{(o1.to_a - o2.to_a).inspect}" - false - end - - test 'overview' do - - UserInfo.current_user_id = 1 - roles = Role.where(name: %w[Customer]) - - user1 = User.create_or_update( - login: 'assets_overview1@example.org', - firstname: 'assets_overview1', - lastname: 'assets_overview1', - email: 'assets_overview1@example.org', - password: 'some_pass', - active: true, - roles: roles, - ) - user2 = User.create_or_update( - login: 'assets_overview2@example.org', - firstname: 'assets_overview2', - lastname: 'assets_overview2', - email: 'assets_overview2@example.org', - password: 'some_pass', - active: true, - roles: roles, - ) - user3 = User.create_or_update( - login: 'assets_overview3@example.org', - firstname: 'assets_overview3', - lastname: 'assets_overview3', - email: 'assets_overview3@example.org', - password: 'some_pass', - active: true, - roles: roles, - ) - user4 = User.create_or_update( - login: 'assets_overview4@example.org', - firstname: 'assets_overview4', - lastname: 'assets_overview4', - email: 'assets_overview4@example.org', - password: 'some_pass', - active: true, - roles: roles, - ) - user5 = User.create_or_update( - login: 'assets_overview5@example.org', - firstname: 'assets_overview5', - lastname: 'assets_overview5', - email: 'assets_overview5@example.org', - password: 'some_pass', - active: true, - roles: roles, - ) - - ticket_state1 = Ticket::State.find_by(name: 'new') - ticket_state2 = Ticket::State.find_by(name: 'open') - overview_role = Role.find_by(name: 'Agent') - overview = Overview.create_or_update( - name: 'my asset test', - link: 'my_asset_test', - prio: 1000, - role_ids: [overview_role.id], - user_ids: [user4.id, user5.id], - condition: { - 'ticket.state_id' => { - operator: 'is', - value: [ ticket_state1.id, ticket_state2.id ], - }, - 'ticket.owner_id' => { - operator: 'is', - pre_condition: 'specific', - value: user1.id, - value_completion: 'John Smith ' - }, - }, - order: { - by: 'created_at', - direction: 'ASC', - }, - view: { - d: %w[title customer group created_at], - s: %w[title customer group created_at], - m: %w[number title customer group created_at], - view_mode_default: 's', - }, - ) - assets = overview.assets({}) - assert(assets[:User][user1.id], 'check assets') - assert_not(assets[:User][user2.id], 'check assets') - assert_not(assets[:User][user3.id], 'check assets') - assert(assets[:User][user4.id], 'check assets') - assert(assets[:User][user5.id], 'check assets') - assert(assets[:TicketState][ticket_state1.id], 'check assets') - assert(assets[:TicketState][ticket_state2.id], 'check assets') - - overview = Overview.create_or_update( - name: 'my asset test', - link: 'my_asset_test', - prio: 1000, - role_ids: [overview_role.id], - user_ids: [user4.id], - condition: { - 'ticket.state_id' => { - operator: 'is', - value: ticket_state1.id, - }, - 'ticket.owner_id' => { - operator: 'is', - pre_condition: 'specific', - value: [user1.id, user2.id], - }, - }, - order: { - by: 'created_at', - direction: 'ASC', - }, - view: { - d: %w[title customer group created_at], - s: %w[title customer group created_at], - m: %w[number title customer group created_at], - view_mode_default: 's', - }, - ) - assets = overview.assets({}) - assert(assets[:User][user1.id], 'check assets') - assert(assets[:User][user2.id], 'check assets') - assert_not(assets[:User][user3.id], 'check assets') - assert(assets[:User][user4.id], 'check assets') - assert_not(assets[:User][user5.id], 'check assets') - assert(assets[:TicketState][ticket_state1.id], 'check assets') - assert_not(assets[:TicketState][ticket_state2.id], 'check assets') - overview.destroy! - end - - test 'sla' do - - UserInfo.current_user_id = 1 - roles = Role.where(name: %w[Customer]) - - user1 = User.create_or_update( - login: 'assets_sla1@example.org', - firstname: 'assets_sla1', - lastname: 'assets_sla1', - email: 'assets_sla1@example.org', - password: 'some_pass', - active: true, - roles: roles, - ) - user2 = User.create_or_update( - login: 'assets_sla2@example.org', - firstname: 'assets_sla2', - lastname: 'assets_sla2', - email: 'assets_sla2@example.org', - password: 'some_pass', - active: true, - roles: roles, - ) - - calendar1 = Calendar.create_or_update( - name: 'US 1', - timezone: 'America/Los_Angeles', - business_hours: { - mon: { '09:00' => '17:00' }, - tue: { '09:00' => '17:00' }, - wed: { '09:00' => '17:00' }, - thu: { '09:00' => '17:00' }, - fri: { '09:00' => '17:00' } - }, - default: true, - ical_url: nil, - updated_by_id: 1, - created_by_id: 1, - ) - ticket_state1 = Ticket::State.find_by(name: 'new') - ticket_state2 = Ticket::State.find_by(name: 'open') - sla = Sla.create_or_update( - name: 'my asset test', - calendar_id: calendar1.id, - condition: { - 'ticket.state_id' => { - operator: 'is', - value: [ ticket_state1.id, ticket_state2.id ], - }, - 'ticket.owner_id' => { - operator: 'is', - pre_condition: 'specific', - value: user1.id, - value_completion: 'John Smith ' - }, - }, - ) - assets = sla.assets({}) - assert(assets[:User][user1.id], 'check assets') - assert_not(assets[:User][user2.id], 'check assets') - assert(assets[:TicketState][ticket_state1.id], 'check assets') - assert(assets[:TicketState][ticket_state2.id], 'check assets') - assert(assets[:Calendar][calendar1.id], 'check assets') - - end - - test 'job' do - - UserInfo.current_user_id = 1 - roles = Role.where(name: %w[Customer]) - - user1 = User.create_or_update( - login: 'assets_job1@example.org', - firstname: 'assets_job1', - lastname: 'assets_job1', - email: 'assets_job1@example.org', - password: 'some_pass', - active: true, - roles: roles, - ) - user2 = User.create_or_update( - login: 'assets_job2@example.org', - firstname: 'assets_job2', - lastname: 'assets_job2', - email: 'assets_job2@example.org', - password: 'some_pass', - active: true, - roles: roles, - ) - user3 = User.create_or_update( - login: 'assets_job3@example.org', - firstname: 'assets_job3', - lastname: 'assets_job3', - email: 'assets_job3@example.org', - password: 'some_pass', - active: true, - roles: roles, - ) - - ticket_state1 = Ticket::State.find_by(name: 'new') - ticket_state2 = Ticket::State.find_by(name: 'open') - ticket_priority2 = Ticket::Priority.find_by(name: '2 normal') - job = Job.create_or_update( - name: 'my job', - timeplan: { - mon: true, - }, - condition: { - 'ticket.state_id' => { - operator: 'is', - value: [ ticket_state1.id, ticket_state2.id ], - }, - 'ticket.owner_id' => { - operator: 'is', - pre_condition: 'specific', - value: user1.id, - value_completion: 'John Smith ' - }, - }, - perform: { - 'ticket.priority_id' => { - value: ticket_priority2.id, - }, - 'ticket.owner_id' => { - pre_condition: 'specific', - value: user2.id, - value_completion: 'metest123@znuny.com ' - }, - }, - disable_notification: true, - ) - assets = job.assets({}) - assert(assets[:User][user1.id], 'check assets') - assert(assets[:User][user2.id], 'check assets') - assert_not(assets[:User][user3.id], 'check assets') - assert(assets[:TicketState][ticket_state1.id], 'check assets') - assert(assets[:TicketState][ticket_state2.id], 'check assets') - assert(assets[:TicketPriority][ticket_priority2.id], 'check assets') - - end - -end diff --git a/test/unit/job_assets_test.rb b/test/unit/job_assets_test.rb new file mode 100644 index 000000000..aff315032 --- /dev/null +++ b/test/unit/job_assets_test.rb @@ -0,0 +1,80 @@ + +require 'test_helper' + +class JobAssetsTest < ActiveSupport::TestCase + test 'assets' do + + UserInfo.current_user_id = 1 + roles = Role.where(name: %w[Customer]) + + user1 = User.create_or_update( + login: 'assets_job1@example.org', + firstname: 'assets_job1', + lastname: 'assets_job1', + email: 'assets_job1@example.org', + password: 'some_pass', + active: true, + roles: roles, + ) + user2 = User.create_or_update( + login: 'assets_job2@example.org', + firstname: 'assets_job2', + lastname: 'assets_job2', + email: 'assets_job2@example.org', + password: 'some_pass', + active: true, + roles: roles, + ) + user3 = User.create_or_update( + login: 'assets_job3@example.org', + firstname: 'assets_job3', + lastname: 'assets_job3', + email: 'assets_job3@example.org', + password: 'some_pass', + active: true, + roles: roles, + ) + + ticket_state1 = Ticket::State.find_by(name: 'new') + ticket_state2 = Ticket::State.find_by(name: 'open') + ticket_priority2 = Ticket::Priority.find_by(name: '2 normal') + job = Job.create_or_update( + name: 'my job', + timeplan: { + mon: true, + }, + condition: { + 'ticket.state_id' => { + operator: 'is', + value: [ ticket_state1.id, ticket_state2.id ], + }, + 'ticket.owner_id' => { + operator: 'is', + pre_condition: 'specific', + value: user1.id, + value_completion: 'John Smith ' + }, + }, + perform: { + 'ticket.priority_id' => { + value: ticket_priority2.id, + }, + 'ticket.owner_id' => { + pre_condition: 'specific', + value: user2.id, + value_completion: 'metest123@znuny.com ' + }, + }, + disable_notification: true, + ) + assets = job.assets({}) + assert(assets[:User][user1.id]) + assert(assets[:User][user2.id]) + assert_not(assets[:User][user3.id]) + assert(assets[:TicketState][ticket_state1.id]) + assert(assets[:TicketState][ticket_state2.id]) + assert(assets[:TicketPriority][ticket_priority2.id]) + + end + +end diff --git a/test/unit/organization_assets_test.rb b/test/unit/organization_assets_test.rb new file mode 100644 index 000000000..48c89a735 --- /dev/null +++ b/test/unit/organization_assets_test.rb @@ -0,0 +1,157 @@ + +require 'test_helper' + +class OrganizationAssetsTest < ActiveSupport::TestCase + test 'assets' do + + roles = Role.where( name: %w[Agent Admin] ) + admin1 = User.create_or_update( + login: 'admin1@example.org', + firstname: 'admin1', + lastname: 'admin1', + email: 'admin1@example.org', + password: 'some_pass', + active: true, + updated_by_id: 1, + created_by_id: 1, + roles: roles, + ) + + roles = Role.where( name: %w[Customer] ) + org = Organization.create_or_update( + name: 'some customer org', + updated_by_id: admin1.id, + created_by_id: 1, + ) + + user1 = User.create_or_update( + login: 'assets1@example.org', + firstname: 'assets1', + lastname: 'assets1', + email: 'assets1@example.org', + password: 'some_pass', + active: true, + updated_by_id: 1, + created_by_id: 1, + organization_id: org.id, + roles: roles, + ) + + user2 = User.create_or_update( + login: 'assets2@example.org', + firstname: 'assets2', + lastname: 'assets2', + email: 'assets2@example.org', + password: 'some_pass', + active: true, + updated_by_id: 1, + created_by_id: 1, + organization_id: org.id, + roles: roles, + ) + + user3 = User.create_or_update( + login: 'assets3@example.org', + firstname: 'assets3', + lastname: 'assets3', + email: 'assets3@example.org', + password: 'some_pass', + active: true, + updated_by_id: user1.id, + created_by_id: user2.id, + roles: roles, + ) + + org = Organization.find(org.id) + assets = org.assets({}) + attributes = org.attributes_with_association_ids + attributes.delete('user_ids') + assert( diff(attributes, assets[:Organization][org.id]), 'check assets' ) + + admin1 = User.find(admin1.id) + attributes = admin1.attributes_with_association_ids + attributes['accounts'] = {} + attributes.delete('password') + attributes.delete('token_ids') + attributes.delete('authorization_ids') + assert( diff(attributes, assets[:User][admin1.id]), 'check assets' ) + + user1 = User.find(user1.id) + attributes = user1.attributes_with_association_ids + attributes['accounts'] = {} + attributes.delete('password') + attributes.delete('token_ids') + attributes.delete('authorization_ids') + assert( diff(attributes, assets[:User][user1.id]), 'check assets' ) + + user2 = User.find(user2.id) + attributes = user2.attributes_with_association_ids + attributes['accounts'] = {} + attributes.delete('password') + attributes.delete('token_ids') + attributes.delete('authorization_ids') + assert( diff(attributes, assets[:User][user2.id]), 'check assets' ) + + user3 = User.find(user3.id) + attributes = user3.attributes_with_association_ids + attributes['accounts'] = {} + attributes.delete('password') + attributes.delete('token_ids') + attributes.delete('authorization_ids') + assert_nil( assets[:User][user3.id], 'check assets' ) + + # touch user 2, check if org has changed + travel 2.seconds + user_new_2 = User.find(user2.id) + user_new_2.lastname = 'assets2' + user_new_2.save! + + org_new = Organization.find(org.id) + attributes = org_new.attributes_with_association_ids + attributes.delete('user_ids') + assert( !diff(attributes, assets[:Organization][org_new.id]), 'check assets' ) + + attributes = user_new_2.attributes_with_association_ids + attributes['accounts'] = {} + attributes.delete('password') + attributes.delete('token_ids') + attributes.delete('authorization_ids') + assert( diff(attributes, assets[:User][user_new_2.id]), 'check assets' ) + + # check new assets lookup + assets = org_new.assets({}) + attributes = org_new.attributes_with_association_ids + attributes.delete('user_ids') + assert( diff(attributes, assets[:Organization][org_new.id]), 'check assets' ) + + attributes = user_new_2.attributes_with_association_ids + attributes['accounts'] = {} + attributes.delete('password') + attributes.delete('token_ids') + attributes.delete('authorization_ids') + assert( diff(attributes, assets[:User][user_new_2.id]), 'check assets' ) + travel_back + + user3.destroy! + user2.destroy! + user1.destroy! + org.destroy! + assert_not(Organization.find_by(id: org_new.id)) + end + + def diff(o1, o2) + return true if o1 == o2 + %w[updated_at created_at].each do |item| + if o1[item] + o1[item] = o1[item].to_s + end + if o2[item] + o2[item] = o2[item].to_s + end + end + return true if (o1.to_a - o2.to_a).blank? + #puts "ERROR: difference \n1: #{o1.inspect}\n2: #{o2.inspect}\ndiff: #{(o1.to_a - o2.to_a).inspect}" + false + end + +end diff --git a/test/unit/overview_assets_test.rb b/test/unit/overview_assets_test.rb new file mode 100644 index 000000000..5c25cc428 --- /dev/null +++ b/test/unit/overview_assets_test.rb @@ -0,0 +1,136 @@ + +require 'test_helper' + +class OverviewAssetsTest < ActiveSupport::TestCase + test 'assets' do + + UserInfo.current_user_id = 1 + roles = Role.where(name: %w[Customer]) + + user1 = User.create_or_update( + login: 'assets_overview1@example.org', + firstname: 'assets_overview1', + lastname: 'assets_overview1', + email: 'assets_overview1@example.org', + password: 'some_pass', + active: true, + roles: roles, + ) + user2 = User.create_or_update( + login: 'assets_overview2@example.org', + firstname: 'assets_overview2', + lastname: 'assets_overview2', + email: 'assets_overview2@example.org', + password: 'some_pass', + active: true, + roles: roles, + ) + user3 = User.create_or_update( + login: 'assets_overview3@example.org', + firstname: 'assets_overview3', + lastname: 'assets_overview3', + email: 'assets_overview3@example.org', + password: 'some_pass', + active: true, + roles: roles, + ) + user4 = User.create_or_update( + login: 'assets_overview4@example.org', + firstname: 'assets_overview4', + lastname: 'assets_overview4', + email: 'assets_overview4@example.org', + password: 'some_pass', + active: true, + roles: roles, + ) + user5 = User.create_or_update( + login: 'assets_overview5@example.org', + firstname: 'assets_overview5', + lastname: 'assets_overview5', + email: 'assets_overview5@example.org', + password: 'some_pass', + active: true, + roles: roles, + ) + + ticket_state1 = Ticket::State.find_by(name: 'new') + ticket_state2 = Ticket::State.find_by(name: 'open') + overview_role = Role.find_by(name: 'Agent') + overview = Overview.create_or_update( + name: 'my asset test', + link: 'my_asset_test', + prio: 1000, + role_ids: [overview_role.id], + user_ids: [user4.id, user5.id], + condition: { + 'ticket.state_id' => { + operator: 'is', + value: [ ticket_state1.id, ticket_state2.id ], + }, + 'ticket.owner_id' => { + operator: 'is', + pre_condition: 'specific', + value: user1.id, + value_completion: 'John Smith ' + }, + }, + order: { + by: 'created_at', + direction: 'ASC', + }, + view: { + d: %w[title customer group created_at], + s: %w[title customer group created_at], + m: %w[number title customer group created_at], + view_mode_default: 's', + }, + ) + assets = overview.assets({}) + assert(assets[:User][user1.id]) + assert_not(assets[:User][user2.id]) + assert_not(assets[:User][user3.id]) + assert(assets[:User][user4.id]) + assert(assets[:User][user5.id]) + assert(assets[:TicketState][ticket_state1.id]) + assert(assets[:TicketState][ticket_state2.id]) + + overview = Overview.create_or_update( + name: 'my asset test', + link: 'my_asset_test', + prio: 1000, + role_ids: [overview_role.id], + user_ids: [user4.id], + condition: { + 'ticket.state_id' => { + operator: 'is', + value: ticket_state1.id, + }, + 'ticket.owner_id' => { + operator: 'is', + pre_condition: 'specific', + value: [user1.id, user2.id], + }, + }, + order: { + by: 'created_at', + direction: 'ASC', + }, + view: { + d: %w[title customer group created_at], + s: %w[title customer group created_at], + m: %w[number title customer group created_at], + view_mode_default: 's', + }, + ) + assets = overview.assets({}) + assert(assets[:User][user1.id]) + assert(assets[:User][user2.id]) + assert_not(assets[:User][user3.id]) + assert(assets[:User][user4.id]) + assert_not(assets[:User][user5.id]) + assert(assets[:TicketState][ticket_state1.id]) + assert_not(assets[:TicketState][ticket_state2.id]) + overview.destroy! + end + +end diff --git a/test/unit/sla_assets_test.rb b/test/unit/sla_assets_test.rb new file mode 100644 index 000000000..7616cb851 --- /dev/null +++ b/test/unit/sla_assets_test.rb @@ -0,0 +1,71 @@ + +require 'test_helper' + +class SlaAssetsTest < ActiveSupport::TestCase + test 'assets' do + + UserInfo.current_user_id = 1 + roles = Role.where(name: %w[Customer]) + + user1 = User.create_or_update( + login: 'assets_sla1@example.org', + firstname: 'assets_sla1', + lastname: 'assets_sla1', + email: 'assets_sla1@example.org', + password: 'some_pass', + active: true, + roles: roles, + ) + user2 = User.create_or_update( + login: 'assets_sla2@example.org', + firstname: 'assets_sla2', + lastname: 'assets_sla2', + email: 'assets_sla2@example.org', + password: 'some_pass', + active: true, + roles: roles, + ) + + calendar1 = Calendar.create_or_update( + name: 'US 1', + timezone: 'America/Los_Angeles', + business_hours: { + mon: { '09:00' => '17:00' }, + tue: { '09:00' => '17:00' }, + wed: { '09:00' => '17:00' }, + thu: { '09:00' => '17:00' }, + fri: { '09:00' => '17:00' } + }, + default: true, + ical_url: nil, + updated_by_id: 1, + created_by_id: 1, + ) + ticket_state1 = Ticket::State.find_by(name: 'new') + ticket_state2 = Ticket::State.find_by(name: 'open') + sla = Sla.create_or_update( + name: 'my asset test', + calendar_id: calendar1.id, + condition: { + 'ticket.state_id' => { + operator: 'is', + value: [ ticket_state1.id, ticket_state2.id ], + }, + 'ticket.owner_id' => { + operator: 'is', + pre_condition: 'specific', + value: user1.id, + value_completion: 'John Smith ' + }, + }, + ) + assets = sla.assets({}) + assert(assets[:User][user1.id], 'check assets') + assert_not(assets[:User][user2.id], 'check assets') + assert(assets[:TicketState][ticket_state1.id], 'check assets') + assert(assets[:TicketState][ticket_state2.id], 'check assets') + assert(assets[:Calendar][calendar1.id], 'check assets') + + end + +end diff --git a/test/unit/trigger_assets_test.rb b/test/unit/trigger_assets_test.rb new file mode 100644 index 000000000..69def14fb --- /dev/null +++ b/test/unit/trigger_assets_test.rb @@ -0,0 +1,77 @@ + +require 'test_helper' + +class TriggerAssetsTest < ActiveSupport::TestCase + test 'assets' do + + UserInfo.current_user_id = 1 + roles = Role.where(name: %w[Customer]) + + user1 = User.create_or_update( + login: 'assets_trigger1@example.org', + firstname: 'assets_trigger1', + lastname: 'assets_trigger1', + email: 'assets_trigger1@example.org', + password: 'some_pass', + active: true, + roles: roles, + ) + user2 = User.create_or_update( + login: 'assets_trigger2@example.org', + firstname: 'assets_trigger2', + lastname: 'assets_trigger2', + email: 'assets_trigger2@example.org', + password: 'some_pass', + active: true, + roles: roles, + ) + user3 = User.create_or_update( + login: 'assets_trigger3@example.org', + firstname: 'assets_trigger3', + lastname: 'assets_trigger3', + email: 'assets_trigger3@example.org', + password: 'some_pass', + active: true, + roles: roles, + ) + group1 = Group.create_or_update( + name: 'group1_trigger', + active: true, + ) + + ticket_state1 = Ticket::State.find_by(name: 'new') + ticket_state2 = Ticket::State.find_by(name: 'open') + ticket_priority2 = Ticket::Priority.find_by(name: '2 normal') + trigger = Trigger.create_or_update( + name: 'my trigger', + condition: { + 'ticket.state_id' => { + operator: 'is', + value: [ ticket_state1.id ], + }, + 'ticket.owner_id' => { + operator: 'is', + pre_condition: 'specific', + value: user1.id, + value_completion: 'John Smith ' + }, + }, + perform: { + 'ticket.group_id' => { + value: group1.id.to_s, + }, + }, + disable_notification: true, + ) + assets = trigger.assets({}) + assert(assets[:User][user1.id]) + assert_not(assets[:User][user2.id]) + assert_not(assets[:User][user3.id]) + assert(assets[:TicketState][ticket_state1.id]) + assert_not(assets[:TicketState][ticket_state2.id]) + assert_not(assets[:TicketPriority]) + assert(assets[:Group][group1.id]) + + end + +end diff --git a/test/unit/user_assets_test.rb b/test/unit/user_assets_test.rb new file mode 100644 index 000000000..432cdee94 --- /dev/null +++ b/test/unit/user_assets_test.rb @@ -0,0 +1,157 @@ + +require 'test_helper' + +class UserAssetsTest < ActiveSupport::TestCase + test 'assets' do + + roles = Role.where(name: %w[Agent Admin]) + groups = Group.all + org1 = Organization.create_or_update( + name: 'some user org', + updated_by_id: 1, + created_by_id: 1, + ) + + user1 = User.create_or_update( + login: 'assets1@example.org', + firstname: 'assets1', + lastname: 'assets1', + email: 'assets1@example.org', + password: 'some_pass', + active: true, + updated_by_id: 1, + created_by_id: 1, + organization_id: org1.id, + roles: roles, + groups: groups, + ) + + user2 = User.create_or_update( + login: 'assets2@example.org', + firstname: 'assets2', + lastname: 'assets2', + email: 'assets2@example.org', + password: 'some_pass', + active: true, + updated_by_id: 1, + created_by_id: 1, + roles: roles, + groups: groups, + ) + + user3 = User.create_or_update( + login: 'assets3@example.org', + firstname: 'assets3', + lastname: 'assets3', + email: 'assets3@example.org', + password: 'some_pass', + active: true, + updated_by_id: user1.id, + created_by_id: user2.id, + roles: roles, + groups: groups, + ) + user3 = User.find(user3.id) + assets = user3.assets({}) + + org1 = Organization.find(org1.id) + attributes = org1.attributes_with_association_ids + attributes.delete('user_ids') + assert(diff(attributes, assets[:Organization][org1.id]), 'check assets') + + user1 = User.find(user1.id) + attributes = user1.attributes_with_association_ids + attributes['accounts'] = {} + attributes.delete('password') + attributes.delete('token_ids') + attributes.delete('authorization_ids') + assert(diff(attributes, assets[:User][user1.id]), 'check assets') + + user2 = User.find(user2.id) + attributes = user2.attributes_with_association_ids + attributes['accounts'] = {} + attributes.delete('password') + attributes.delete('token_ids') + attributes.delete('authorization_ids') + assert(diff(attributes, assets[:User][user2.id]), 'check assets') + + user3 = User.find(user3.id) + attributes = user3.attributes_with_association_ids + attributes['accounts'] = {} + attributes.delete('password') + attributes.delete('token_ids') + attributes.delete('authorization_ids') + assert(diff(attributes, assets[:User][user3.id]), 'check assets') + + # touch org, check if user1 has changed + travel 2.seconds + org2 = Organization.find(org1.id) + org2.note = "some note...#{rand(9_999_999_999_999)}" + org2.save! + + attributes = org2.attributes_with_association_ids + attributes.delete('user_ids') + assert_not(diff(attributes, assets[:Organization][org2.id]), 'check assets') + + user1_new = User.find(user1.id) + attributes = user1_new.attributes_with_association_ids + attributes['accounts'] = {} + attributes.delete('password') + attributes.delete('token_ids') + attributes.delete('authorization_ids') + assert_not(diff(attributes, assets[:User][user1_new.id]), 'check assets') + + # check new assets lookup + assets = user3.assets({}) + attributes = org2.attributes_with_association_ids + attributes.delete('user_ids') + assert(diff(attributes, assets[:Organization][org1.id]), 'check assets') + + user1 = User.find(user1.id) + attributes = user1.attributes_with_association_ids + attributes['accounts'] = {} + attributes.delete('password') + attributes.delete('token_ids') + attributes.delete('authorization_ids') + assert(diff(attributes, assets[:User][user1.id]), 'check assets') + + user2 = User.find(user2.id) + attributes = user2.attributes_with_association_ids + attributes['accounts'] = {} + attributes.delete('password') + attributes.delete('token_ids') + attributes.delete('authorization_ids') + assert(diff(attributes, assets[:User][user2.id]), 'check assets') + + user3 = User.find(user3.id) + attributes = user3.attributes_with_association_ids + attributes['accounts'] = {} + attributes.delete('password') + attributes.delete('token_ids') + attributes.delete('authorization_ids') + assert(diff(attributes, assets[:User][user3.id]), 'check assets') + travel_back + + user3.destroy! + user2.destroy! + user1.destroy! + org1.destroy! + assert_not(Organization.find_by(id: org2.id)) + end + + def diff(o1, o2) + return true if o1 == o2 + %w[updated_at created_at].each do |item| + if o1[item] + o1[item] = o1[item].to_s + end + if o2[item] + o2[item] = o2[item].to_s + end + end + return true if (o1.to_a - o2.to_a).blank? + #puts "ERROR: difference \n1: #{o1.inspect}\n2: #{o2.inspect}\ndiff: #{(o1.to_a - o2.to_a).inspect}" + false + end + +end From 9b0f51461b1d4e50e5b100bda09f7fd2e2e9a110 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Thu, 23 Nov 2017 20:43:01 +0100 Subject: [PATCH 021/196] Fixed issue #1671 - Add config option for intelligent customer selection of incoming emails of agents. --- app/models/channel/filter/identify_sender.rb | 25 +++--- ...mer_selection_based_on_sender_recipient.rb | 34 ++++++++ db/seeds/settings.rb | 26 ++++++ ...election_based_on_sender_recipient_test.rb | 79 +++++++++++++++++++ 4 files changed, 153 insertions(+), 11 deletions(-) create mode 100644 db/migrate/20171123000001_email_process_customer_selection_based_on_sender_recipient.rb create mode 100644 test/unit/email_process_customer_selection_based_on_sender_recipient_test.rb diff --git a/app/models/channel/filter/identify_sender.rb b/app/models/channel/filter/identify_sender.rb index 1b67d1709..7818f251f 100644 --- a/app/models/channel/filter/identify_sender.rb +++ b/app/models/channel/filter/identify_sender.rb @@ -22,9 +22,9 @@ module Channel::Filter::IdentifySender if !customer_user && mail[ 'x-zammad-customer-email'.to_sym ].present? customer_user = User.find_by(email: mail[ 'x-zammad-customer-email'.to_sym ]) end - if !customer_user - # get correct customer + # get correct customer + if !customer_user && Setting.get('postmaster_sender_is_agent_search_for_customer') == true if mail[ 'x-zammad-ticket-create-article-sender'.to_sym ] == 'Agent' # get first recipient and set customer @@ -46,18 +46,21 @@ module Channel::Filter::IdentifySender end end rescue => e - Rails.logger.error 'ERROR: SenderIsSystemAddress: ' + e.inspect + Rails.logger.error "SenderIsSystemAddress: ##{e.inspect}" end end - if !customer_user - customer_user = user_create( - login: mail[ 'x-zammad-customer-login'.to_sym ] || mail[ 'x-zammad-customer-email'.to_sym ] || mail[:from_email], - firstname: mail[ 'x-zammad-customer-firstname'.to_sym ] || mail[:from_display_name], - lastname: mail[ 'x-zammad-customer-lastname'.to_sym ], - email: mail[ 'x-zammad-customer-email'.to_sym ] || mail[:from_email], - ) - end end + + # take regular from as customer + if !customer_user + customer_user = user_create( + login: mail[ 'x-zammad-customer-login'.to_sym ] || mail[ 'x-zammad-customer-email'.to_sym ] || mail[:from_email], + firstname: mail[ 'x-zammad-customer-firstname'.to_sym ] || mail[:from_display_name], + lastname: mail[ 'x-zammad-customer-lastname'.to_sym ], + email: mail[ 'x-zammad-customer-email'.to_sym ] || mail[:from_email], + ) + end + create_recipients(mail) mail[ 'x-zammad-ticket-customer_id'.to_sym ] = customer_user.id diff --git a/db/migrate/20171123000001_email_process_customer_selection_based_on_sender_recipient.rb b/db/migrate/20171123000001_email_process_customer_selection_based_on_sender_recipient.rb new file mode 100644 index 000000000..54f04157f --- /dev/null +++ b/db/migrate/20171123000001_email_process_customer_selection_based_on_sender_recipient.rb @@ -0,0 +1,34 @@ +class EmailProcessCustomerSelectionBasedOnSenderRecipient < ActiveRecord::Migration[4.2] + def up + + # return if it's a new setup + return if !Setting.find_by(name: 'system_init_done') + + Setting.create_if_not_exists( + title: 'Customer selection based on sender and receiver list', + name: 'postmaster_sender_is_agent_search_for_customer', + area: 'Email::Base', + description: 'If the sender is an agent, set the first user in the recipient list as a customer.', + options: { + form: [ + { + display: '', + null: true, + name: 'postmaster_sender_is_agent_search_for_customer', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: true, + preferences: { + permission: ['admin.channel_email'], + }, + frontend: false + ) + end + +end diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb index 25155d67a..b2a8c8521 100644 --- a/db/seeds/settings.rb +++ b/db/seeds/settings.rb @@ -2041,6 +2041,32 @@ Setting.create_if_not_exists( frontend: false ) +Setting.create_if_not_exists( + title: 'Customer selection based on sender and receiver list', + name: 'postmaster_sender_is_agent_search_for_customer', + area: 'Email::Base', + description: 'If the sender is an agent, set the first user in the recipient list as a customer.', + options: { + form: [ + { + display: '', + null: true, + name: 'postmaster_sender_is_agent_search_for_customer', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: true, + preferences: { + permission: ['admin.channel_email'], + }, + frontend: false +) + Setting.create_if_not_exists( title: 'Notification Sender', name: 'notification_sender', diff --git a/test/unit/email_process_customer_selection_based_on_sender_recipient_test.rb b/test/unit/email_process_customer_selection_based_on_sender_recipient_test.rb new file mode 100644 index 000000000..81fcfa24d --- /dev/null +++ b/test/unit/email_process_customer_selection_based_on_sender_recipient_test.rb @@ -0,0 +1,79 @@ + +require 'test_helper' + +class EmailProcessCustomerSelectionBasedOnSenderRecipient < ActiveSupport::TestCase + + setup do + groups = Group.all + roles = Role.where(name: 'Agent') + @agent1 = User.create_or_update( + login: 'user-customer-selection-agent1@example.com', + firstname: 'UserOutOfOffice', + lastname: 'Agent1', + email: 'user-customer-selection-agent1@example.com', + password: 'agentpw', + active: true, + roles: roles, + groups: groups, + updated_by_id: 1, + created_by_id: 1, + ) + roles = Role.where(name: 'Customer') + @customer1 = User.create_or_update( + login: 'user-customer-selection-customer1@example.com', + firstname: 'UserOutOfOffice', + lastname: 'customer1', + email: 'user-customer-selection-customer1@example.com', + password: 'customerpw', + active: true, + roles: roles, + updated_by_id: 1, + created_by_id: 1, + ) + end + + test 'customer need to be customer' do + + email_raw_string = "From: #{@agent1.email} +To: #{@customer1.email} +Subject: test + +Some Text" + + ticket_p, article_p, user_p, mail = Channel::EmailParser.new.process({}, email_raw_string) + ticket = Ticket.find(ticket_p.id) + article = Ticket::Article.find(article_p.id) + assert_equal('test', ticket.title) + assert_equal('new', ticket.state.name) + assert_equal('Agent', ticket.create_article_sender.name) + assert_equal('Agent', article.sender.name) + assert_equal(@customer1.email, ticket.customer.email) + assert_equal(@customer1.firstname, ticket.customer.firstname) + assert_equal(@customer1.lastname, ticket.customer.lastname) + + end + + test 'agent need to be customer' do + + Setting.set('postmaster_sender_is_agent_search_for_customer', false) + + email_raw_string = "From: #{@agent1.email} +To: #{@customer1.email} +Subject: test + +Some Text" + + ticket_p, article_p, user_p, mail = Channel::EmailParser.new.process({}, email_raw_string) + ticket = Ticket.find(ticket_p.id) + article = Ticket::Article.find(article_p.id) + assert_equal('test', ticket.title) + assert_equal('new', ticket.state.name) + assert_equal('Agent', ticket.create_article_sender.name) + assert_equal('Agent', article.sender.name) + assert_equal(@agent1.email, ticket.customer.email) + assert_equal(@agent1.firstname, ticket.customer.firstname) + assert_equal(@agent1.lastname, ticket.customer.lastname) + + end + +end From fa63fbae8a25d35ded30dd4b40ddb039e2939bc3 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Thu, 23 Nov 2017 22:13:02 +0100 Subject: [PATCH 022/196] Added Italian and Polish translations. --- public/assets/chat/chat.coffee | 40 ++++++++ public/assets/chat/chat.js | 166 +++++++++++++++++++++------------ public/assets/chat/chat.min.js | 3 +- public/assets/form/form.js | 20 +++- 4 files changed, 165 insertions(+), 64 deletions(-) diff --git a/public/assets/chat/chat.coffee b/public/assets/chat/chat.coffee index 6a3731a8f..792c2e575 100644 --- a/public/assets/chat/chat.coffee +++ b/public/assets/chat/chat.coffee @@ -189,6 +189,7 @@ do($ = window.jQuery, window) -> 'Connection re-established': 'Verbindung wiederhergestellt' 'Today': 'Heute' 'Send': 'Senden' + 'Chat closed by %s': 'Chat beendet von %s', 'Compose your message...': 'Ihre Nachricht...' 'All colleagues are busy.': 'Alle Kollegen sind belegt.' 'You are on waiting list position %s.': 'Sie sind in der Warteliste an der Position %s.' @@ -205,6 +206,7 @@ do($ = window.jQuery, window) -> 'Connection re-established': 'Conexión restablecida' 'Today': 'Hoy' 'Send': 'Enviar' + 'Chat closed by %s': 'Chat cerrado por %s', 'Compose your message...': 'Escriba su mensaje...' 'All colleagues are busy.': 'Todos los agentes están ocupados.' 'You are on waiting list position %s.': 'Usted está en la posición %s de la lista de espera.' @@ -221,6 +223,7 @@ do($ = window.jQuery, window) -> 'Connection re-established': 'Connexion rétablie' 'Today': 'Aujourdhui' 'Send': 'Envoyer' + 'Chat closed by %s': 'Chat fermé par %s', 'Compose your message...': 'Composez votre message...' 'All colleagues are busy.': 'Tous les collègues sont actuellement occupés.' 'You are on waiting list position %s.': 'Vous êtes actuellement en %s position dans la file d\'attente.' @@ -237,6 +240,7 @@ do($ = window.jQuery, window) -> 'Connection re-established': 'Verbinding herstelt' 'Today': 'Vandaag' 'Send': 'Verzenden' + 'Chat closed by %s': 'Chat gesloten door %s', 'Compose your message...': 'Typ uw bericht...' 'All colleagues are busy.': 'Alle medewerkers zijn bezet.' 'You are on waiting list position %s.': 'U bent %s in de wachtrij.' @@ -244,6 +248,40 @@ do($ = window.jQuery, window) -> 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Omdat u in de laatste %s minuten niets geschreven heeft wordt de conversatie met %s gesloten.' 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Omdat u in de laatste %s minuten niets geschreven heeft is de conversatie gesloten.' 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Het spijt ons, het duurt langer dan verwacht om te antwoorden. Alstublieft probeer het later nogmaals of stuur ons een email. Hartelijk dank!' + 'it': + 'Chat with us!': 'Chatta con noi!' + 'Scroll down to see new messages': 'Scorrere verso il basso per vedere i nuovi messaggi' + 'Online': 'Online' + 'Offline': 'Offline' + 'Connecting': 'Collegamento' + 'Connection re-established': 'Collegamento ristabilito' + 'Today': 'Oggi' + 'Send': 'Invio' + 'Chat closed by %s': 'Conversazione chiusa da %s', + 'Compose your message...': 'Comporre il tuo messaggio...' + 'All colleagues are busy.': 'Tutti i colleghi sono occupati.' + 'You are on waiting list position %s.': 'Siete in posizione lista d\' attesa %s.' + 'Start new conversation': 'Avviare una nuova conversazione' + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Dal momento che non hai risposto negli ultimi %s minuti la tua conversazione con %s si è chiusa.' + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Dal momento che non hai risposto negli ultimi %s minuti la tua conversazione si è chiusa.' + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Ci dispiace, ci vuole più tempo come previsto per ottenere uno slot vuoto. Per favore riprova più tardi o inviaci un\' e-mail. Grazie!' + 'pl': + 'Chat with us!': 'Czatuj z nami!' + 'Scroll down to see new messages': 'Przewiń w dół, aby wyświetlić nowe wiadomości' + 'Online': 'Online' + 'Offline': 'Offline' + 'Connecting': 'Łączenie' + 'Connection re-established': 'Ponowne nawiązanie połączenia' + 'Today': 'dzisiejszy' + 'Send': 'Wyślij' + 'Chat closed by %s': 'Czat zamknięty przez %s', + 'Compose your message...': 'Utwórz swoją wiadomość...' + 'All colleagues are busy.': 'Wszyscy koledzy są zajęci.' + 'You are on waiting list position %s.': 'Na liście oczekujących znajduje się pozycja %s.' + 'Start new conversation': 'Rozpoczęcie nowej konwersacji' + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Ponieważ w ciągu ostatnich %s minut nie odpowiedziałeś, Twoja rozmowa z %s została zamknięta.' + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Ponieważ nie odpowiedziałeś w ciągu ostatnich %s minut, Twoja rozmowa została zamknięta.' + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Przykro nam, ale to trwa dłużej niż się spodziewamy. Spróbuj ponownie później lub wyślij nam wiadomość e-mail. Dziękuję!' 'zh-cn': 'Chat with us!': '发起即时对话!' 'Scroll down to see new messages': '向下滚动以查看新消息' @@ -253,6 +291,7 @@ do($ = window.jQuery, window) -> 'Connection re-established': '正在重新建立连接' 'Today': '今天' 'Send': '发送' + 'Chat closed by %s': 'Chat closed by %s', 'Compose your message...': '正在输入信息...' 'All colleagues are busy.': '所有工作人员都在忙碌中.' 'You are on waiting list position %s.': '您目前的等候位置是第 %s 位.' @@ -269,6 +308,7 @@ do($ = window.jQuery, window) -> 'Connection re-established': '正在重新建立連線中' 'Today': '今天' 'Send': '發送' + 'Chat closed by %s': 'Chat closed by %s', 'Compose your message...': '正在輸入訊息...' 'All colleagues are busy.': '所有服務人員都在忙碌中.' 'You are on waiting list position %s.': '你目前的等候位置是第 %s 順位.' diff --git a/public/assets/chat/chat.js b/public/assets/chat/chat.js index f3e75b67b..0dc5842fa 100644 --- a/public/assets/chat/chat.js +++ b/public/assets/chat/chat.js @@ -1,3 +1,64 @@ +if (!window.zammadChatTemplates) { + window.zammadChatTemplates = {}; +} +window.zammadChatTemplates["agent"] = function (__obj) { + if (!__obj) __obj = {}; + var __out = [], __capture = function(callback) { + var out = __out, result; + __out = []; + callback.call(this); + result = __out.join(''); + __out = out; + return __safe(result); + }, __sanitize = function(value) { + if (value && value.ecoSafe) { + return value; + } else if (typeof value !== 'undefined' && value != null) { + return __escape(value); + } else { + return ''; + } + }, __safe, __objSafe = __obj.safe, __escape = __obj.escape; + __safe = __obj.safe = function(value) { + if (value && value.ecoSafe) { + return value; + } else { + if (!(typeof value !== 'undefined' && value != null)) value = ''; + var result = new String(value); + result.ecoSafe = true; + return result; + } + }; + if (!__escape) { + __escape = __obj.escape = function(value) { + return ('' + value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + }; + } + (function() { + (function() { + if (this.agent.avatar) { + __out.push('\n\n'); + } + + __out.push('\n\n '); + + __out.push(__sanitize(this.agent.name)); + + __out.push('\n'); + + }).call(this); + + }).call(__obj); + __obj.safe = __objSafe, __obj.escape = __escape; + return __out.join(''); +}; + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, slice = [].slice, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, @@ -315,6 +376,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); 'Connection re-established': 'Verbindung wiederhergestellt', 'Today': 'Heute', 'Send': 'Senden', + 'Chat closed by %s': 'Chat beendet von %s', 'Compose your message...': 'Ihre Nachricht...', 'All colleagues are busy.': 'Alle Kollegen sind belegt.', 'You are on waiting list position %s.': 'Sie sind in der Warteliste an der Position %s.', @@ -332,6 +394,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); 'Connection re-established': 'Conexión restablecida', 'Today': 'Hoy', 'Send': 'Enviar', + 'Chat closed by %s': 'Chat cerrado por %s', 'Compose your message...': 'Escriba su mensaje...', 'All colleagues are busy.': 'Todos los agentes están ocupados.', 'You are on waiting list position %s.': 'Usted está en la posición %s de la lista de espera.', @@ -349,6 +412,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); 'Connection re-established': 'Connexion rétablie', 'Today': 'Aujourdhui', 'Send': 'Envoyer', + 'Chat closed by %s': 'Chat fermé par %s', 'Compose your message...': 'Composez votre message...', 'All colleagues are busy.': 'Tous les collègues sont actuellement occupés.', 'You are on waiting list position %s.': 'Vous êtes actuellement en %s position dans la file d\'attente.', @@ -366,6 +430,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); 'Connection re-established': 'Verbinding herstelt', 'Today': 'Vandaag', 'Send': 'Verzenden', + 'Chat closed by %s': 'Chat gesloten door %s', 'Compose your message...': 'Typ uw bericht...', 'All colleagues are busy.': 'Alle medewerkers zijn bezet.', 'You are on waiting list position %s.': 'U bent %s in de wachtrij.', @@ -374,6 +439,42 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Omdat u in de laatste %s minuten niets geschreven heeft is de conversatie gesloten.', 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Het spijt ons, het duurt langer dan verwacht om te antwoorden. Alstublieft probeer het later nogmaals of stuur ons een email. Hartelijk dank!' }, + 'it': { + 'Chat with us!': 'Chatta con noi!', + 'Scroll down to see new messages': 'Scorrere verso il basso per vedere i nuovi messaggi', + 'Online': 'Online', + 'Offline': 'Offline', + 'Connecting': 'Collegamento', + 'Connection re-established': 'Collegamento ristabilito', + 'Today': 'Oggi', + 'Send': 'Invio', + 'Chat closed by %s': 'Conversazione chiusa da %s', + 'Compose your message...': 'Comporre il tuo messaggio...', + 'All colleagues are busy.': 'Tutti i colleghi sono occupati.', + 'You are on waiting list position %s.': 'Siete in posizione lista d\' attesa %s.', + 'Start new conversation': 'Avviare una nuova conversazione', + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Dal momento che non hai risposto negli ultimi %s minuti la tua conversazione con %s si è chiusa.', + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Dal momento che non hai risposto negli ultimi %s minuti la tua conversazione si è chiusa.', + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Ci dispiace, ci vuole più tempo come previsto per ottenere uno slot vuoto. Per favore riprova più tardi o inviaci un\' e-mail. Grazie!' + }, + 'pl': { + 'Chat with us!': 'Czatuj z nami!', + 'Scroll down to see new messages': 'Przewiń w dół, aby wyświetlić nowe wiadomości', + 'Online': 'Online', + 'Offline': 'Offline', + 'Connecting': 'Łączenie', + 'Connection re-established': 'Ponowne nawiązanie połączenia', + 'Today': 'dzisiejszy', + 'Send': 'Wyślij', + 'Chat closed by %s': 'Czat zamknięty przez %s', + 'Compose your message...': 'Utwórz swoją wiadomość...', + 'All colleagues are busy.': 'Wszyscy koledzy są zajęci.', + 'You are on waiting list position %s.': 'Na liście oczekujących znajduje się pozycja %s.', + 'Start new conversation': 'Rozpoczęcie nowej konwersacji', + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Ponieważ w ciągu ostatnich %s minut nie odpowiedziałeś, Twoja rozmowa z %s została zamknięta.', + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Ponieważ nie odpowiedziałeś w ciągu ostatnich %s minut, Twoja rozmowa została zamknięta.', + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Przykro nam, ale to trwa dłużej niż się spodziewamy. Spróbuj ponownie później lub wyślij nam wiadomość e-mail. Dziękuję!' + }, 'zh-cn': { 'Chat with us!': '发起即时对话!', 'Scroll down to see new messages': '向下滚动以查看新消息', @@ -383,6 +484,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); 'Connection re-established': '正在重新建立连接', 'Today': '今天', 'Send': '发送', + 'Chat closed by %s': 'Chat closed by %s', 'Compose your message...': '正在输入信息...', 'All colleagues are busy.': '所有工作人员都在忙碌中.', 'You are on waiting list position %s.': '您目前的等候位置是第 %s 位.', @@ -400,6 +502,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); 'Connection re-established': '正在重新建立連線中', 'Today': '今天', 'Send': '發送', + 'Chat closed by %s': 'Chat closed by %s', 'Compose your message...': '正在輸入訊息...', 'All colleagues are busy.': '所有服務人員都在忙碌中.', 'You are on waiting list position %s.': '你目前的等候位置是第 %s 順位.', @@ -622,7 +725,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); })(this)); this.input.on('paste', (function(_this) { return function(e) { - var clipboardData, docType, error, html, htmlTmp, imageFile, imageInserted, item, match, reader, regex, replacementTag, text; + var clipboardData, docType, html, htmlTmp, imageFile, imageInserted, item, match, reader, regex, replacementTag, text; e.stopPropagation(); e.preventDefault(); clipboardData; @@ -1774,67 +1877,6 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); return window.ZammadChat = ZammadChat; })(window.jQuery, window); -if (!window.zammadChatTemplates) { - window.zammadChatTemplates = {}; -} -window.zammadChatTemplates["agent"] = function (__obj) { - if (!__obj) __obj = {}; - var __out = [], __capture = function(callback) { - var out = __out, result; - __out = []; - callback.call(this); - result = __out.join(''); - __out = out; - return __safe(result); - }, __sanitize = function(value) { - if (value && value.ecoSafe) { - return value; - } else if (typeof value !== 'undefined' && value != null) { - return __escape(value); - } else { - return ''; - } - }, __safe, __objSafe = __obj.safe, __escape = __obj.escape; - __safe = __obj.safe = function(value) { - if (value && value.ecoSafe) { - return value; - } else { - if (!(typeof value !== 'undefined' && value != null)) value = ''; - var result = new String(value); - result.ecoSafe = true; - return result; - } - }; - if (!__escape) { - __escape = __obj.escape = function(value) { - return ('' + value) - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"'); - }; - } - (function() { - (function() { - if (this.agent.avatar) { - __out.push('\n\n'); - } - - __out.push('\n\n '); - - __out.push(__sanitize(this.agent.name)); - - __out.push('\n'); - - }).call(this); - - }).call(__obj); - __obj.safe = __objSafe, __obj.escape = __escape; - return __out.join(''); -}; - if (!window.zammadChatTemplates) { window.zammadChatTemplates = {}; } diff --git a/public/assets/chat/chat.min.js b/public/assets/chat/chat.min.js index 2130c5537..c6b6915c3 100644 --- a/public/assets/chat/chat.min.js +++ b/public/assets/chat/chat.min.js @@ -1 +1,2 @@ -var bind=function(t,e){return function(){return t.apply(e,arguments)}},slice=[].slice,extend=function(t,e){function s(){this.constructor=t}for(var n in e)hasProp.call(e,n)&&(t[n]=e[n]);return s.prototype=e.prototype,t.prototype=new s,t.__super__=e.prototype,t},hasProp={}.hasOwnProperty;!function(t,e){var s,n,i,o,a,r,l,h,c;c=document.getElementsByTagName("script"),r=c[c.length-1],l=r.src.match(".*://([^:/]*).*")[1],h=r.src.match("(.*)://[^:/]*.*")[1],s=function(){function e(e){this.options=t.extend({},this.defaults,e),this.log=new i({debug:this.options.debug,logPrefix:this.options.logPrefix||this.logPrefix})}return e.prototype.defaults={debug:!1},e}(),i=function(){function e(e){this.log=bind(this.log,this),this.error=bind(this.error,this),this.notice=bind(this.notice,this),this.debug=bind(this.debug,this),this.options=t.extend({},this.defaults,e)}return e.prototype.defaults={debug:!1},e.prototype.debug=function(){var t;if(t=1<=arguments.length?slice.call(arguments,0):[],this.options.debug)return this.log("debug",t)},e.prototype.notice=function(){var t;return t=1<=arguments.length?slice.call(arguments,0):[],this.log("notice",t)},e.prototype.error=function(){var t;return t=1<=arguments.length?slice.call(arguments,0):[],this.log("error",t)},e.prototype.log=function(e,s){var n,i,o,a;if(s.unshift("||"),s.unshift(e),s.unshift(this.options.logPrefix),console.log.apply(console,s),this.options.debug){for(a="",i=0,o=s.length;i"+a+"")}},e}(),o=function(t){function e(t){this.stop=bind(this.stop,this),this.start=bind(this.start,this),e.__super__.constructor.call(this,t)}return extend(e,s),e.prototype.timeoutStartedAt=null,e.prototype.logPrefix="timeout",e.prototype.defaults={debug:!1,timeout:4,timeoutIntervallCheck:.5},e.prototype.start=function(){var t,e;return this.stop(),e=new Date,t=function(t){return function(){var s;if(s=new Date-new Date(e.getTime()+1e3*t.options.timeout*60),t.log.debug("Timeout check for "+t.options.timeout+" minutes (left "+s/1e3+" sec.)"),!(s<0))return t.stop(),t.options.callback()}}(this),this.log.debug("Start timeout in "+this.options.timeout+" minutes"),this.intervallId=setInterval(t,1e3*this.options.timeoutIntervallCheck*60)},e.prototype.stop=function(){if(this.intervallId)return this.log.debug("Stop timeout of "+this.options.timeout+" minutes"),clearInterval(this.intervallId)},e}(),n=function(t){function n(t){this.ping=bind(this.ping,this),this.send=bind(this.send,this),this.reconnect=bind(this.reconnect,this),this.close=bind(this.close,this),this.connect=bind(this.connect,this),this.set=bind(this.set,this),n.__super__.constructor.call(this,t)}return extend(n,s),n.prototype.logPrefix="io",n.prototype.set=function(t){var e,s,n;s=[];for(e in t)n=t[e],s.push(this.options[e]=n);return s},n.prototype.connect=function(){return this.log.debug("Connecting to "+this.options.host),this.ws=new e.WebSocket(""+this.options.host),this.ws.onopen=function(t){return function(e){return t.log.debug("onOpen",e),t.options.onOpen(e),t.ping()}}(this),this.ws.onmessage=function(t){return function(e){var s,n,i;for(i=JSON.parse(e.data),t.log.debug("onMessage",e.data),s=0,n=i.length;sChat with us!",scrollHint:"Scroll down to see new messages",idleTimeout:6,idleTimeoutIntervallCheck:.5,inactiveTimeout:8,inactiveTimeoutIntervallCheck:.5,waitingListTimeout:4,waitingListTimeoutIntervallCheck:.5},a.prototype.logPrefix="chat",a.prototype._messageCount=0,a.prototype.isOpen=!1,a.prototype.blinkOnlineInterval=null,a.prototype.stopBlinOnlineStateTimeout=null,a.prototype.showTimeEveryXMinutes=2,a.prototype.lastTimestamp=null,a.prototype.lastAddedType=null,a.prototype.inputTimeout=null,a.prototype.isTyping=!1,a.prototype.state="offline",a.prototype.initialQueueDelay=1e4,a.prototype.translations={de:{"Chat with us!":"Chatte mit uns!","Scroll down to see new messages":"Scrolle nach unten um neue Nachrichten zu sehen",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbindung wiederhergestellt",Today:"Heute",Send:"Senden","Compose your message...":"Ihre Nachricht...","All colleagues are busy.":"Alle Kollegen sind belegt.","You are on waiting list position %s.":"Sie sind in der Warteliste an der Position %s.","Start new conversation":"Neue Konversation starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation mit %s geschlossen.","Since you didn't respond in the last %s minutes your conversation got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation geschlossen.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Es tut uns leid, es dauert länger als erwartet, um einen freien Platz zu erhalten. Bitte versuchen Sie es zu einem späteren Zeitpunkt noch einmal oder schicken Sie uns eine E-Mail. Vielen Dank!"},es:{"Chat with us!":"Chatee con nosotros!","Scroll down to see new messages":"Haga scroll hacia abajo para ver nuevos mensajes",Online:"En linea",Offline:"Desconectado",Connecting:"Conectando","Connection re-established":"Conexión restablecida",Today:"Hoy",Send:"Enviar","Compose your message...":"Escriba su mensaje...","All colleagues are busy.":"Todos los agentes están ocupados.","You are on waiting list position %s.":"Usted está en la posición %s de la lista de espera.","Start new conversation":"Iniciar nueva conversación","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Puesto que usted no respondió en los últimos %s minutos su conversación con %s se ha cerrado.","Since you didn't respond in the last %s minutes your conversation got closed.":"Puesto que usted no respondió en los últimos %s minutos su conversación se ha cerrado.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Lo sentimos, se tarda más tiempo de lo esperado para ser atendido por un agente. Inténtelo de nuevo más tarde o envíenos un correo electrónico. ¡Gracias!"},fr:{"Chat with us!":"Chattez avec nous!","Scroll down to see new messages":"Faites défiler pour lire les nouveaux messages",Online:"En-ligne",Offline:"Hors-ligne",Connecting:"Connexion en cours","Connection re-established":"Connexion rétablie",Today:"Aujourdhui",Send:"Envoyer","Compose your message...":"Composez votre message...","All colleagues are busy.":"Tous les collègues sont actuellement occupés.","You are on waiting list position %s.":"Vous êtes actuellement en %s position dans la file d'attente.","Start new conversation":"Démarrer une nouvelle conversation","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Si vous ne répondez pas dans les %s minutes, votre conversation avec %s va être fermée.","Since you didn't respond in the last %s minutes your conversation got closed.":"Si vous ne répondez pas dans les %s minutes, votre conversation va être fermée.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Nous sommes désolés, il faut plus de temps que prévu pour obtenir un emplacement vide. Veuillez réessayer ultérieurement ou nous envoyer un courriel. Je vous remercie!"},nl:{"Chat with us!":"Chat met ons!","Scroll down to see new messages":"Scrol naar beneden om nieuwe berichten te zien",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbinding herstelt",Today:"Vandaag",Send:"Verzenden","Compose your message...":"Typ uw bericht...","All colleagues are busy.":"Alle medewerkers zijn bezet.","You are on waiting list position %s.":"U bent %s in de wachtrij.","Start new conversation":"Nieuwe conversatie starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Omdat u in de laatste %s minuten niets geschreven heeft wordt de conversatie met %s gesloten.","Since you didn't respond in the last %s minutes your conversation got closed.":"Omdat u in de laatste %s minuten niets geschreven heeft is de conversatie gesloten.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Het spijt ons, het duurt langer dan verwacht om te antwoorden. Alstublieft probeer het later nogmaals of stuur ons een email. Hartelijk dank!"},"zh-cn":{"Chat with us!":"发起即时对话!","Scroll down to see new messages":"向下滚动以查看新消息",Online:"在线",Offline:"离线",Connecting:"连接中","Connection re-established":"正在重新建立连接",Today:"今天",Send:"发送","Compose your message...":"正在输入信息...","All colleagues are busy.":"所有工作人员都在忙碌中.","You are on waiting list position %s.":"您目前的等候位置是第 %s 位.","Start new conversation":"开始新的会话","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"由于您超过 %s 分钟没有回复, 您与 %s 的会话已被关闭.","Since you didn't respond in the last %s minutes your conversation got closed.":"由于您超过 %s 分钟没有任何回复, 该对话已被关闭.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"非常抱歉, 目前需要等候更长的时间才能接入对话, 请稍后重试或向我们发送电子邮件. 谢谢!"},"zh-tw":{"Chat with us!":"開始即時對话!","Scroll down to see new messages":"向下滑動以查看新訊息",Online:"線上",Offline:"离线",Connecting:"連線中","Connection re-established":"正在重新建立連線中",Today:"今天",Send:"發送","Compose your message...":"正在輸入訊息...","All colleagues are busy.":"所有服務人員都在忙碌中.","You are on waiting list position %s.":"你目前的等候位置是第 %s 順位.","Start new conversation":"開始新的對話","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"由於你超過 %s 分鐘沒有回應, 你與 %s 的對話已被關閉.","Since you didn't respond in the last %s minutes your conversation got closed.":"由於你超過 %s 分鐘沒有任何回應, 該對話已被關閉.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"非常抱歉, 當前需要等候更長的時間方可排入對話程序, 請稍後重試或向我們寄送電子郵件. 謝謝!"}},a.prototype.sessionId=void 0,a.prototype.scrolledToBottom=!0,a.prototype.scrollSnapTolerance=10,a.prototype.richTextFormatKey={66:!0,73:!0,85:!0,83:!0},a.prototype.T=function(){var t,e,s,n,i,o;if(i=arguments[0],e=2<=arguments.length?slice.call(arguments,1):[],this.options.lang&&"en"!==this.options.lang&&(this.translations[this.options.lang]?((o=this.translations[this.options.lang])[i]||this.log.notice("Translation needed for '"+i+"'"),i=o[i]||i):this.log.notice("Translation '"+this.options.lang+"' needed!")),e)for(s=0,n=e.length;ss?e:document.body)},a.prototype.render=function(){return this.el&&t(".zammad-chat").get(0)||this.renderBase(),t("."+this.options.buttonClass).addClass(this.inactiveClass),this.setAgentOnlineState("online"),this.log.debug("widget rendered"),this.startTimeoutObservers(),this.idleTimeout.start(),this.sessionId=sessionStorage.getItem("sessionId"),this.send("chat_status_customer",{session_id:this.sessionId,url:e.location.href})},a.prototype.renderBase=function(){if(this.el=t(this.view("chat")({title:this.options.title,scrollHint:this.options.scrollHint})),this.options.target.append(this.el),this.input=this.el.find(".zammad-chat-input"),this.el.find(".js-chat-open").click(this.open),this.el.find(".js-chat-toggle").click(this.toggle),this.el.find(".zammad-chat-controls").on("submit",this.onSubmit),this.el.find(".zammad-chat-body").on("scroll",this.detectScrolledtoBottom),this.el.find(".zammad-scroll-hint").click(this.onScrollHintClick),this.input.on({keydown:this.checkForEnter,input:this.onInput}),this.input.on("keydown",function(t){return function(e){var s;if(s=!1,e.altKey||e.ctrlKey||!e.metaKey?e.altKey||!e.ctrlKey||e.metaKey||(s=!0):s=!0,s&&t.richTextFormatKey[e.keyCode]){if(e.preventDefault(),66===e.keyCode)return document.execCommand("bold"),!0;if(73===e.keyCode)return document.execCommand("italic"),!0;if(85===e.keyCode)return document.execCommand("underline"),!0;if(83===e.keyCode)return document.execCommand("strikeThrough"),!0}}}(this)),this.input.on("paste",function(s){return function(n){var i,o,a,r,l,h,c,d,u,p,m,g;if(n.stopPropagation(),n.preventDefault(),n.clipboardData)i=n.clipboardData;else if(e.clipboardData)i=e.clipboardData;else{if(!n.originalEvent.clipboardData)throw"No clipboardData support";i=n.originalEvent.clipboardData}if(h=!1,i&&i.items&&i.items[0]&&("file"!==(c=i.items[0]).kind||"image/png"!==c.type&&"image/jpeg"!==c.type||(l=c.getAsFile(),(u=new FileReader).onload=function(t){var e,n,i;return i=t.target.result,e=document.createElement("img"),e.src=i,n=function(t,n,o,a){return s.isRetina()&&(n/=2,2),i=t,e='',document.execCommand("insertHTML",!1,e)},s.resizeImage(e.src,460,"auto",2,"image/jpeg","auto",n)},u.readAsDataURL(l),h=!0)),!h){g=void 0,o=void 0;try{g=i.getData("text/html"),o="html",g&&0!==g.length||(o="text",g=i.getData("text/plain")),g&&0!==g.length||(o="text2",g=i.getData("text"))}catch(t){n=t,console.log("Sorry, can't insert markup because browser is not supporting it."),o="text3",g=i.getData("text")}return"text"!==o&&"text2"!==o&&"text3"!==o||(g="
                  "+g.replace(/\n/g,"
                  ")+"
                  ",g=g.replace(/
                  <\/div>/g,"

                  ")),console.log("p",o,g),"html"===o&&(a=t("
                  "+g+"
                  "),d=!1,r=g,p=new RegExp("<(/w|w):[A-Za-z]"),r.match(p)&&(d=!0,r=r.replace(p,"")),p=new RegExp("<(/o|o):[A-Za-z]"),r.match(p)&&(d=!0,r=r.replace(p,"")),d&&(a=s.wordFilter(a)),(a=t(a)).contents().each(function(){if(8===this.nodeType)return t(this).remove()}),a.find("a, font, small, time, form, label").replaceWith(function(){return t(this).contents()}),m="div",a.find("textarea").each(function(){var e,s;return s=this.outerHTML,p=new RegExp("<"+this.tagName,"i"),e=s.replace(p,"<"+m),p=new RegExp("'),n=n.get(0),document.caretPositionFromPoint?(c=document.caretPositionFromPoint(r,l),(d=document.createRange()).setStart(c.offsetNode,c.offset),d.collapse(),d.insertNode(n)):document.caretRangeFromPoint?(d=document.caretRangeFromPoint(r,l)).insertNode(n):console.log("could not find carat")},s.resizeImage(n.src,460,"auto",2,"image/jpeg","auto",i)},a.readAsDataURL(o)}}(this)),t(e).on("beforeunload",function(t){return function(){return t.onLeaveTemporary()}}(this)),t(e).bind("hashchange",function(t){return function(){if(!t.isOpen)return t.idleTimeout.start();t.sessionId&&t.send("chat_session_notice",{session_id:t.sessionId,message:e.location.href})}}(this)),this.isFullscreen)return this.input.on({focus:this.onFocus,focusout:this.onFocusOut})},a.prototype.checkForEnter=function(t){if(!t.shiftKey&&13===t.keyCode)return t.preventDefault(),this.sendMessage()},a.prototype.send=function(t,e){return null==e&&(e={}),e.chat_id=this.options.chatId,this.io.send(t,e)},a.prototype.onWebSocketMessage=function(t){var e,s,n;for(e=0,s=t.length;e0,t(e).scrollTop(0),s)return this.log.notice("virtual keyboard shown")},a.prototype.onFocusOut=function(){},a.prototype.onTyping=function(){if(!(this.isTyping&&this.isTyping>new Date((new Date).getTime()-1500)))return this.isTyping=new Date,this.send("chat_session_typing",{session_id:this.sessionId}),this.inactiveTimeout.start()},a.prototype.onSubmit=function(t){return t.preventDefault(),this.sendMessage()},a.prototype.sendMessage=function(){var t,e;if(t=this.input.html())return this.inactiveTimeout.start(),sessionStorage.removeItem("unfinished_message"),e=this.view("message")({message:t,from:"customer",id:this._messageCount++,unreadClass:""}),this.maybeAddTimestamp(),this.el.find(".zammad-chat-message--typing").get(0)?(this.lastAddedType="typing-placeholder",this.el.find(".zammad-chat-message--typing").before(e)):(this.lastAddedType="message--customer",this.el.find(".zammad-chat-body").append(e)),this.input.html(""),this.scrollToBottom(),this.send("chat_session_message",{content:t,id:this._messageCount,session_id:this.sessionId})},a.prototype.receiveMessage=function(t){return this.inactiveTimeout.start(),this.onAgentTypingEnd(),this.maybeAddTimestamp(),this.renderMessage({message:t.message.content,id:t.id,from:"agent"}),this.scrollToBottom({showHint:!0})},a.prototype.renderMessage=function(t){return this.lastAddedType="message--"+t.from,t.unreadClass=document.hidden?" zammad-chat-message--unread":"",this.el.find(".zammad-chat-body").append(this.view("message")(t))},a.prototype.open=function(){var t;{if(!this.isOpen)return this.isOpen=!0,this.log.debug("open widget"),this.sessionId||this.showLoader(),this.el.addClass("zammad-chat-is-open"),t=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.css("bottom",-t),this.sessionId?(this.el.css("bottom",0),this.onOpenAnimationEnd()):(this.el.animate({bottom:0},500,this.onOpenAnimationEnd),this.send("chat_session_init",{url:e.location.href}));this.log.debug("widget already open, block")}},a.prototype.onOpenAnimationEnd=function(){if(this.idleTimeout.stop(),this.isFullscreen)return this.disableScrollOnRoot()},a.prototype.sessionClose=function(){return this.send("chat_session_close",{session_id:this.sessionId}),this.inactiveTimeout.stop(),this.waitingListTimeout.stop(),sessionStorage.removeItem("unfinished_message"),this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.setSessionId(void 0)},a.prototype.toggle=function(t){return this.isOpen?this.close(t):this.open(t)},a.prototype.close=function(t){var e;if(this.isOpen){if(this.initDelayId&&clearTimeout(this.initDelayId),this.sessionId)return this.log.debug("close widget"),t&&t.stopPropagation(),this.sessionClose(),this.isFullscreen&&this.enableScrollOnRoot(),e=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.animate({bottom:-e},500,this.onCloseAnimationEnd);this.log.debug("can't close widget without sessionId")}else this.log.debug("can't close widget, it's not open")},a.prototype.onCloseAnimationEnd=function(){return this.el.css("bottom",""),this.el.removeClass("zammad-chat-is-open"),this.showLoader(),this.el.find(".zammad-chat-welcome").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").addClass("zammad-chat-is-hidden"),this.isOpen=!1,this.io.reconnect()},a.prototype.onWebSocketClose=function(){if(!this.isOpen)return this.el?(this.el.removeClass("zammad-chat-is-shown"),this.el.removeClass("zammad-chat-is-loaded")):void 0},a.prototype.show=function(){if("offline"!==this.state)return this.el.addClass("zammad-chat-is-loaded"),this.el.addClass("zammad-chat-is-shown")},a.prototype.disableInput=function(){return this.input.prop("disabled",!0),this.el.find(".zammad-chat-send").prop("disabled",!0)},a.prototype.enableInput=function(){return this.input.prop("disabled",!1),this.el.find(".zammad-chat-send").prop("disabled",!1)},a.prototype.hideModal=function(){return this.el.find(".zammad-chat-modal").html("")},a.prototype.onQueueScreen=function(t){var e;if(this.setSessionId(t.session_id),e=function(e){return function(){return e.onQueue(t),e.waitingListTimeout.start()}}(this),!this.initialQueueDelay||this.onInitialQueueDelayId)return this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),e();this.onInitialQueueDelayId=setTimeout(e,this.initialQueueDelay)},a.prototype.onQueue=function(t){return this.log.notice("onQueue",t.position),this.inQueue=!0,this.el.find(".zammad-chat-modal").html(this.view("waiting")({position:t.position}))},a.prototype.onAgentTypingStart=function(){if(this.stopTypingId&&clearTimeout(this.stopTypingId),this.stopTypingId=setTimeout(this.onAgentTypingEnd,3e3),!this.el.find(".zammad-chat-message--typing").get(0)&&(this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("typingIndicator")()),this.isVisible(this.el.find(".zammad-chat-message--typing"),!0)))return this.scrollToBottom()},a.prototype.onAgentTypingEnd=function(){return this.el.find(".zammad-chat-message--typing").remove()},a.prototype.onLeaveTemporary=function(){if(this.sessionId)return this.send("chat_session_leave_temporary",{session_id:this.sessionId})},a.prototype.maybeAddTimestamp=function(){var t,e,s;if(s=Date.now(),!this.lastTimestamp||s-this.lastTimestamp>6e4*this.showTimeEveryXMinutes)return t=this.T("Today"),e=(new Date).toTimeString().substr(0,5),"timestamp"===this.lastAddedType?(this.updateLastTimestamp(t,e),this.lastTimestamp=s):(this.el.find(".zammad-chat-body").append(this.view("timestamp")({label:t,time:e})),this.lastTimestamp=s,this.lastAddedType="timestamp",this.scrollToBottom())},a.prototype.updateLastTimestamp=function(t,e){if(this.el)return this.el.find(".zammad-chat-body").find(".zammad-chat-timestamp").last().replaceWith(this.view("timestamp")({label:t,time:e}))},a.prototype.addStatus=function(t){if(this.el)return this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("status")({status:t})),this.scrollToBottom()},a.prototype.detectScrolledtoBottom=function(){var t;if(t=this.el.find(".zammad-chat-body").scrollTop()+this.el.find(".zammad-chat-body").outerHeight(),this.scrolledToBottom=Math.abs(t-this.el.find(".zammad-chat-body").prop("scrollHeight"))<=this.scrollSnapTolerance,this.scrolledToBottom)return this.el.find(".zammad-scroll-hint").addClass("is-hidden")},a.prototype.showScrollHint=function(){return this.el.find(".zammad-scroll-hint").removeClass("is-hidden"),this.el.find(".zammad-chat-body").scrollTop(this.el.find(".zammad-chat-body").scrollTop()+this.el.find(".zammad-scroll-hint").outerHeight())},a.prototype.onScrollHintClick=function(){return this.el.find(".zammad-chat-body").animate({scrollTop:this.el.find(".zammad-chat-body").prop("scrollHeight")},300)},a.prototype.scrollToBottom=function(e){var s;return s=(null!=e?e:{showHint:!1}).showHint,this.scrolledToBottom?this.el.find(".zammad-chat-body").scrollTop(t(".zammad-chat-body").prop("scrollHeight")):s?this.showScrollHint():void 0},a.prototype.destroy=function(t){return null==t&&(t={}),this.log.debug("destroy widget",t),this.setAgentOnlineState("offline"),t.remove&&this.el&&this.el.remove(),this.waitingListTimeout&&this.waitingListTimeout.stop(),this.inactiveTimeout&&this.inactiveTimeout.stop(),this.idleTimeout&&this.idleTimeout.stop(),this.io.close()},a.prototype.reconnect=function(){return this.log.notice("reconnecting"),this.disableInput(),this.lastAddedType="status",this.setAgentOnlineState("connecting"),this.addStatus(this.T("Connection lost"))},a.prototype.onConnectionReestablished=function(){return this.lastAddedType="status",this.setAgentOnlineState("online"),this.addStatus(this.T("Connection re-established"))},a.prototype.onSessionClosed=function(t){return this.addStatus(this.T("Chat closed by %s",t.realname)),this.disableInput(),this.setAgentOnlineState("offline"),this.inactiveTimeout.stop()},a.prototype.setSessionId=function(t){return this.sessionId=t,void 0===t?sessionStorage.removeItem("sessionId"):sessionStorage.setItem("sessionId",t)},a.prototype.onConnectionEstablished=function(t){return this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.inQueue=!1,t.agent&&(this.agent=t.agent),t.session_id&&this.setSessionId(t.session_id),this.el.find(".zammad-chat-body").html(""),this.el.find(".zammad-chat-agent").html(this.view("agent")({agent:this.agent})),this.enableInput(),this.hideModal(),this.el.find(".zammad-chat-welcome").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").removeClass("zammad-chat-is-hidden"),this.isFullscreen||this.input.focus(),this.setAgentOnlineState("online"),this.waitingListTimeout.stop(),this.idleTimeout.stop(),this.inactiveTimeout.start()},a.prototype.showCustomerTimeout=function(){var t;return this.el.find(".zammad-chat-modal").html(this.view("customer_timeout")({agent:this.agent.name,delay:this.options.inactiveTimeout})),t=function(){return location.reload()},this.el.find(".js-restart").click(t),this.sessionClose()},a.prototype.showWaitingListTimeout=function(){var t;return this.el.find(".zammad-chat-modal").html(this.view("waiting_list_timeout")({delay:this.options.watingListTimeout})),t=function(){return location.reload()},this.el.find(".js-restart").click(t),this.sessionClose()},a.prototype.showLoader=function(){return this.el.find(".zammad-chat-modal").html(this.view("loader")())},a.prototype.setAgentOnlineState=function(t){var e;if(this.state=t,this.el)return e=t.charAt(0).toUpperCase()+t.slice(1),this.el.find(".zammad-chat-agent-status").attr("data-status",t).text(this.T(e))},a.prototype.detectHost=function(){var t;return t="ws://","https"===h&&(t="wss://"),this.options.host=""+t+l+"/ws"},a.prototype.loadCss=function(){var t,e,s;if(this.options.cssAutoload)return(s=this.options.cssUrl)||(s=this.options.host.replace(/^wss/i,"https").replace(/^ws/i,"http").replace(/\/ws/i,""),s+="/assets/chat/chat.css"),this.log.debug("load css from '"+s+"'"),e="@import url('"+s+"');",t=document.createElement("link"),t.onload=this.onCssLoaded,t.rel="stylesheet",t.href="data:text/css,"+escape(e),document.getElementsByTagName("head")[0].appendChild(t)},a.prototype.onCssLoaded=function(){return this.socketReady?this.onReady():this.cssLoaded=!0},a.prototype.startTimeoutObservers=function(){return this.idleTimeout=new o({logPrefix:"idleTimeout",debug:this.options.debug,timeout:this.options.idleTimeout,timeoutIntervallCheck:this.options.idleTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Idle timeout reached, hide widget",new Date),t.destroy({remove:!0})}}(this)}),this.inactiveTimeout=new o({logPrefix:"inactiveTimeout",debug:this.options.debug,timeout:this.options.inactiveTimeout,timeoutIntervallCheck:this.options.inactiveTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Inactive timeout reached, show timeout screen.",new Date),t.showCustomerTimeout(),t.destroy({remove:!1})}}(this)}),this.waitingListTimeout=new o({logPrefix:"waitingListTimeout",debug:this.options.debug,timeout:this.options.waitingListTimeout,timeoutIntervallCheck:this.options.waitingListTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Waiting list timeout reached, show timeout screen.",new Date),t.showWaitingListTimeout(),t.destroy({remove:!1})}}(this)})},a.prototype.disableScrollOnRoot=function(){return this.rootScrollOffset=this.scrollRoot.scrollTop(),this.scrollRoot.css({overflow:"hidden",position:"fixed"})},a.prototype.enableScrollOnRoot=function(){return this.scrollRoot.scrollTop(this.rootScrollOffset),this.scrollRoot.css({overflow:"",position:""})},a.prototype.isVisible=function(s,n,i,o){var a,r,l,h,c,d,u,p,m,g,f,v,y,b,w,T,C,z,S,k,I,A,x,_,E,O;if(!(s.length<1))if(r=t(e),a=s.length>1?s.eq(0):s,z=a.get(0),O=r.width(),E=r.height(),o=o||"both",p=!0!==i||z.offsetWidth*z.offsetHeight,"function"==typeof z.getBoundingClientRect){if(C=z.getBoundingClientRect(),S=C.top>=0&&C.top0&&C.bottom<=E,b=C.left>=0&&C.left0&&C.right<=O,k=n?S||u:S&&u,y=n?b||T:b&&T,"both"===o)return p&&k&&y;if("vertical"===o)return p&&k;if("horizontal"===o)return p&&y}else{if(_=r.scrollTop(),I=_+E,A=r.scrollLeft(),x=A+O,w=a.offset(),d=w.top,l=d+a.height(),h=w.left,c=h+a.width(),v=!0===n?l:d,m=!0===n?d:l,g=!0===n?c:h,f=!0===n?h:c,"both"===o)return!!p&&m<=I&&v>=_&&f<=x&&g>=A;if("vertical"===o)return!!p&&m<=I&&v>=_;if("horizontal"===o)return!!p&&f<=x&&g>=A}},a.prototype.isRetina=function(){var t;return!!e.matchMedia&&((t=e.matchMedia("only screen and (min--moz-device-pixel-ratio: 1.3), only screen and (-o-min-device-pixel-ratio: 2.6/2), only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen and (min-device-pixel-ratio: 1.3), only screen and (min-resolution: 1.3dppx)"))&&t.matches||e.devicePixelRatio>1)},a.prototype.resizeImage=function(t,e,s,n,i,o,a,r){var l;return null==e&&(e="auto"),null==s&&(s="auto"),null==n&&(n=1),null==r&&(r=!0),l=new Image,l.onload=function(){var t,r,h,c,d;return h=l.width,r=l.height,console.log("ImageService","current size",h,r),"auto"===s&&"auto"===e&&(e=h,s=r),"auto"===s&&(s=r/(h/e)),"auto"===e&&(e=r/(h/s)),d=!1,e/gi,""),s=s.replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi,""),s=s.replace(/<(\/?)s>/gi,"<$1strike>"),s=s.replace(/ /gi," "),e.html(s),t("p",e).each(function(){var e,s;if(s=t(this).attr("style"),e=/mso-list:\w+ \w+([0-9]+)/.exec(s))return t(this).data("_listLevel",parseInt(e[1],10))}),n=0,i=null,t("p",e).each(function(){var e,s,o,a,r,l,h,c,d,u;if(void 0!==(e=t(this).data("_listLevel"))){if(u=t(this).text(),a="
                    ",/^\s*\w+\./.test(u)&&(a=(r=/([0-9])\./.exec(u))?null!=(l=(d=parseInt(r[1],10))>1)?l:'
                      ':"
                        "}:"
                          "),e>n&&(0===n?(t(this).before(a),i=t(this).prev()):i=t(a).appendTo(i)),e=c;s=h<=c?++o:--o)i=i.parent();return t("span:first",this).remove(),i.append("
                        1. "+t(this).html()+"
                        2. "),t(this).remove(),n=e}return n=0}),t("[style]",e).removeAttr("style"),t("[align]",e).removeAttr("align"),t("span",e).replaceWith(function(){return t(this).contents()}),t("span:empty",e).remove(),t("[class^='Mso']",e).removeAttr("class"),t("p:empty",e).remove(),e},a.prototype.removeAttribute=function(e){var s,n,i,o,a;if(e){for(s=t(e),i=0,o=(a=e.attributes).length;i/g,">").replace(/"/g,""")}),function(){(function(){this.agent.avatar&&(e.push('\n\n')),e.push('\n\n '),e.push(s(this.agent.name)),e.push("\n")}).call(this)}.call(t),t.safe=n,t.escape=i,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.chat=function(t){t||(t={});var e=[],s=function(t){return t&&t.ecoSafe?t:void 0!==t&&null!=t?i(t):""},n=t.safe,i=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
                          \n
                          \n
                          \n \n \n \n \n \n
                          \n
                          \n
                          \n
                          \n \n '),e.push(this.T(this.title)),e.push('\n
                          \n
                          \n
                          \n \n
                          \n
                          \n
                          \n \n
                          \n
                          ")}).call(this)}.call(t),t.safe=n,t.escape=i,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.customer_timeout=function(t){t||(t={});var e=[],s=function(t){return t&&t.ecoSafe?t:void 0!==t&&null!=t?i(t):""},n=t.safe,i=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
                          \n '),this.agent?(e.push("\n "),e.push(this.T("Since you didn't respond in the last %s minutes your conversation with %s got closed.",this.delay,this.agent)),e.push("\n ")):(e.push("\n "),e.push(this.T("Since you didn't respond in the last %s minutes your conversation got closed.",this.delay)),e.push("\n ")),e.push('\n
                          \n
                          "),e.push(this.T("Start new conversation")),e.push("
                          \n
                          ")}).call(this)}.call(t),t.safe=n,t.escape=i,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.loader=function(t){t||(t={});var e=[],s=t.safe,n=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},n||(n=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('\n \n \n \n\n'),e.push(this.T("Connecting")),e.push("")}).call(this)}.call(t),t.safe=s,t.escape=n,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.message=function(t){t||(t={});var e=[],s=function(t){return t&&t.ecoSafe?t:void 0!==t&&null!=t?i(t):""},n=t.safe,i=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
                          \n "),e.push(this.message),e.push("\n
                          ")}).call(this)}.call(t),t.safe=n,t.escape=i,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.status=function(t){t||(t={});var e=[],s=t.safe,n=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},n||(n=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
                          \n
                          \n '),e.push(this.status),e.push("\n
                          \n
                          ")}).call(this)}.call(t),t.safe=s,t.escape=n,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.timestamp=function(t){t||(t={});var e=[],s=function(t){return t&&t.ecoSafe?t:void 0!==t&&null!=t?i(t):""},n=t.safe,i=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
                          '),e.push(s(this.label)),e.push(" "),e.push(s(this.time)),e.push("
                          ")}).call(this)}.call(t),t.safe=n,t.escape=i,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.typingIndicator=function(t){t||(t={});var e=[],s=t.safe,n=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},n||(n=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
                          \n \n \n \n \n \n \n \n
                          ')}).call(this)}.call(t),t.safe=s,t.escape=n,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting=function(t){t||(t={});var e=[],s=t.safe,n=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},n||(n=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
                          \n \n \n \n \n \n '),e.push(this.T("All colleagues are busy.")),e.push("
                          \n "),e.push(this.T("You are on waiting list position %s.",this.position)),e.push("\n
                          ")}).call(this)}.call(t),t.safe=s,t.escape=n,e.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting_list_timeout=function(t){t||(t={});var e=[],s=function(t){return t&&t.ecoSafe?t:void 0!==t&&null!=t?i(t):""},n=t.safe,i=t.escape;return t.safe=function(t){if(t&&t.ecoSafe)return t;void 0!==t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){e.push('
                          \n '),e.push(this.T("We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!")),e.push('\n
                          \n
                          "),e.push(this.T("Start new conversation")),e.push("
                          \n
                          ")}).call(this)}.call(t),t.safe=n,t.escape=i,e.join("")}; \ No newline at end of file +window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.agent=function(t){t||(t={});var e,n=[],s=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},i=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){this.agent.avatar&&(n.push('\n\n')),n.push('\n\n '),n.push(s(this.agent.name)),n.push("\n")}).call(this)}.call(t),t.safe=i,t.escape=o,n.join("")};var bind=function(t,e){return function(){return t.apply(e,arguments)}},slice=[].slice,extend=function(t,e){function n(){this.constructor=t}for(var s in e)hasProp.call(e,s)&&(t[s]=e[s]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},hasProp={}.hasOwnProperty;!function(t,e){var n,s,i,o,a,r,l,c,h;return h=document.getElementsByTagName("script"),r=h[h.length-1],l=r.src.match(".*://([^:/]*).*")[1],c=r.src.match("(.*)://[^:/]*.*")[1],n=function(){function e(e){this.options=t.extend({},this.defaults,e),this.log=new i({debug:this.options.debug,logPrefix:this.options.logPrefix||this.logPrefix})}return e.prototype.defaults={debug:!1},e}(),i=function(){function e(e){this.log=bind(this.log,this),this.error=bind(this.error,this),this.notice=bind(this.notice,this),this.debug=bind(this.debug,this),this.options=t.extend({},this.defaults,e)}return e.prototype.defaults={debug:!1},e.prototype.debug=function(){var t;if(t=1<=arguments.length?slice.call(arguments,0):[],this.options.debug)return this.log("debug",t)},e.prototype.notice=function(){var t;return t=1<=arguments.length?slice.call(arguments,0):[],this.log("notice",t)},e.prototype.error=function(){var t;return t=1<=arguments.length?slice.call(arguments,0):[],this.log("error",t)},e.prototype.log=function(e,n){var s,i,o,a;if(n.unshift("||"),n.unshift(e),n.unshift(this.options.logPrefix),console.log.apply(console,n),this.options.debug){for(a="",i=0,o=n.length;i"+a+"
                          ")}},e}(),o=function(t){function e(t){this.stop=bind(this.stop,this),this.start=bind(this.start,this),e.__super__.constructor.call(this,t)}return extend(e,t),e.prototype.timeoutStartedAt=null,e.prototype.logPrefix="timeout",e.prototype.defaults={debug:!1,timeout:4,timeoutIntervallCheck:.5},e.prototype.start=function(){var t,e;return this.stop(),e=new Date,t=function(t){return function(){var n;if(n=new Date-new Date(e.getTime()+1e3*t.options.timeout*60),t.log.debug("Timeout check for "+t.options.timeout+" minutes (left "+n/1e3+" sec.)"),!(n<0))return t.stop(),t.options.callback()}}(this),this.log.debug("Start timeout in "+this.options.timeout+" minutes"),this.intervallId=setInterval(t,1e3*this.options.timeoutIntervallCheck*60)},e.prototype.stop=function(){if(this.intervallId)return this.log.debug("Stop timeout of "+this.options.timeout+" minutes"),clearInterval(this.intervallId)},e}(n),s=function(t){function n(t){this.ping=bind(this.ping,this),this.send=bind(this.send,this),this.reconnect=bind(this.reconnect,this),this.close=bind(this.close,this),this.connect=bind(this.connect,this),this.set=bind(this.set,this),n.__super__.constructor.call(this,t)}return extend(n,t),n.prototype.logPrefix="io",n.prototype.set=function(t){var e,n,s;n=[];for(e in t)s=t[e],n.push(this.options[e]=s);return n},n.prototype.connect=function(){return this.log.debug("Connecting to "+this.options.host),this.ws=new e.WebSocket(""+this.options.host),this.ws.onopen=function(t){return function(e){return t.log.debug("onOpen",e),t.options.onOpen(e),t.ping()}}(this),this.ws.onmessage=function(t){return function(e){var n,s,i,o;for(o=JSON.parse(e.data),t.log.debug("onMessage",e.data),n=0,s=o.length;nChat with us!",scrollHint:"Scroll down to see new messages",idleTimeout:6,idleTimeoutIntervallCheck:.5,inactiveTimeout:8,inactiveTimeoutIntervallCheck:.5,waitingListTimeout:4,waitingListTimeoutIntervallCheck:.5},i.prototype.logPrefix="chat",i.prototype._messageCount=0,i.prototype.isOpen=!1,i.prototype.blinkOnlineInterval=null,i.prototype.stopBlinOnlineStateTimeout=null,i.prototype.showTimeEveryXMinutes=2,i.prototype.lastTimestamp=null,i.prototype.lastAddedType=null,i.prototype.inputTimeout=null,i.prototype.isTyping=!1,i.prototype.state="offline",i.prototype.initialQueueDelay=1e4,i.prototype.translations={de:{"Chat with us!":"Chatte mit uns!","Scroll down to see new messages":"Scrolle nach unten um neue Nachrichten zu sehen",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbindung wiederhergestellt",Today:"Heute",Send:"Senden","Chat closed by %s":"Chat beendet von %s","Compose your message...":"Ihre Nachricht...","All colleagues are busy.":"Alle Kollegen sind belegt.","You are on waiting list position %s.":"Sie sind in der Warteliste an der Position %s.","Start new conversation":"Neue Konversation starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation mit %s geschlossen.","Since you didn't respond in the last %s minutes your conversation got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation geschlossen.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Es tut uns leid, es dauert länger als erwartet, um einen freien Platz zu erhalten. Bitte versuchen Sie es zu einem späteren Zeitpunkt noch einmal oder schicken Sie uns eine E-Mail. Vielen Dank!"},es:{"Chat with us!":"Chatee con nosotros!","Scroll down to see new messages":"Haga scroll hacia abajo para ver nuevos mensajes",Online:"En linea",Offline:"Desconectado",Connecting:"Conectando","Connection re-established":"Conexión restablecida",Today:"Hoy",Send:"Enviar","Chat closed by %s":"Chat cerrado por %s","Compose your message...":"Escriba su mensaje...","All colleagues are busy.":"Todos los agentes están ocupados.","You are on waiting list position %s.":"Usted está en la posición %s de la lista de espera.","Start new conversation":"Iniciar nueva conversación","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Puesto que usted no respondió en los últimos %s minutos su conversación con %s se ha cerrado.","Since you didn't respond in the last %s minutes your conversation got closed.":"Puesto que usted no respondió en los últimos %s minutos su conversación se ha cerrado.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Lo sentimos, se tarda más tiempo de lo esperado para ser atendido por un agente. Inténtelo de nuevo más tarde o envíenos un correo electrónico. ¡Gracias!"},fr:{"Chat with us!":"Chattez avec nous!","Scroll down to see new messages":"Faites défiler pour lire les nouveaux messages",Online:"En-ligne",Offline:"Hors-ligne",Connecting:"Connexion en cours","Connection re-established":"Connexion rétablie",Today:"Aujourdhui",Send:"Envoyer","Chat closed by %s":"Chat fermé par %s","Compose your message...":"Composez votre message...","All colleagues are busy.":"Tous les collègues sont actuellement occupés.","You are on waiting list position %s.":"Vous êtes actuellement en %s position dans la file d'attente.","Start new conversation":"Démarrer une nouvelle conversation","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Si vous ne répondez pas dans les %s minutes, votre conversation avec %s va être fermée.","Since you didn't respond in the last %s minutes your conversation got closed.":"Si vous ne répondez pas dans les %s minutes, votre conversation va être fermée.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Nous sommes désolés, il faut plus de temps que prévu pour obtenir un emplacement vide. Veuillez réessayer ultérieurement ou nous envoyer un courriel. Je vous remercie!"},nl:{"Chat with us!":"Chat met ons!","Scroll down to see new messages":"Scrol naar beneden om nieuwe berichten te zien",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbinding herstelt",Today:"Vandaag",Send:"Verzenden","Chat closed by %s":"Chat gesloten door %s","Compose your message...":"Typ uw bericht...","All colleagues are busy.":"Alle medewerkers zijn bezet.","You are on waiting list position %s.":"U bent %s in de wachtrij.","Start new conversation":"Nieuwe conversatie starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Omdat u in de laatste %s minuten niets geschreven heeft wordt de conversatie met %s gesloten.","Since you didn't respond in the last %s minutes your conversation got closed.":"Omdat u in de laatste %s minuten niets geschreven heeft is de conversatie gesloten.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Het spijt ons, het duurt langer dan verwacht om te antwoorden. Alstublieft probeer het later nogmaals of stuur ons een email. Hartelijk dank!"},it:{"Chat with us!":"Chatta con noi!","Scroll down to see new messages":"Scorrere verso il basso per vedere i nuovi messaggi",Online:"Online",Offline:"Offline",Connecting:"Collegamento","Connection re-established":"Collegamento ristabilito",Today:"Oggi",Send:"Invio","Chat closed by %s":"Conversazione chiusa da %s","Compose your message...":"Comporre il tuo messaggio...","All colleagues are busy.":"Tutti i colleghi sono occupati.","You are on waiting list position %s.":"Siete in posizione lista d' attesa %s.","Start new conversation":"Avviare una nuova conversazione","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Dal momento che non hai risposto negli ultimi %s minuti la tua conversazione con %s si è chiusa.","Since you didn't respond in the last %s minutes your conversation got closed.":"Dal momento che non hai risposto negli ultimi %s minuti la tua conversazione si è chiusa.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Ci dispiace, ci vuole più tempo come previsto per ottenere uno slot vuoto. Per favore riprova più tardi o inviaci un' e-mail. Grazie!"},pl:{"Chat with us!":"Czatuj z nami!","Scroll down to see new messages":"Przewiń w dół, aby wyświetlić nowe wiadomości",Online:"Online",Offline:"Offline",Connecting:"Łączenie","Connection re-established":"Ponowne nawiązanie połączenia",Today:"dzisiejszy",Send:"Wyślij","Chat closed by %s":"Czat zamknięty przez %s","Compose your message...":"Utwórz swoją wiadomość...","All colleagues are busy.":"Wszyscy koledzy są zajęci.","You are on waiting list position %s.":"Na liście oczekujących znajduje się pozycja %s.","Start new conversation":"Rozpoczęcie nowej konwersacji","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Ponieważ w ciągu ostatnich %s minut nie odpowiedziałeś, Twoja rozmowa z %s została zamknięta.","Since you didn't respond in the last %s minutes your conversation got closed.":"Ponieważ nie odpowiedziałeś w ciągu ostatnich %s minut, Twoja rozmowa została zamknięta.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Przykro nam, ale to trwa dłużej niż się spodziewamy. Spróbuj ponownie później lub wyślij nam wiadomość e-mail. Dziękuję!"},"zh-cn":{"Chat with us!":"发起即时对话!","Scroll down to see new messages":"向下滚动以查看新消息",Online:"在线",Offline:"离线",Connecting:"连接中","Connection re-established":"正在重新建立连接",Today:"今天",Send:"发送","Chat closed by %s":"Chat closed by %s","Compose your message...":"正在输入信息...","All colleagues are busy.":"所有工作人员都在忙碌中.","You are on waiting list position %s.":"您目前的等候位置是第 %s 位.","Start new conversation":"开始新的会话","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"由于您超过 %s 分钟没有回复, 您与 %s 的会话已被关闭.","Since you didn't respond in the last %s minutes your conversation got closed.":"由于您超过 %s 分钟没有任何回复, 该对话已被关闭.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"非常抱歉, 目前需要等候更长的时间才能接入对话, 请稍后重试或向我们发送电子邮件. 谢谢!"},"zh-tw":{"Chat with us!":"開始即時對话!","Scroll down to see new messages":"向下滑動以查看新訊息",Online:"線上",Offline:"离线",Connecting:"連線中","Connection re-established":"正在重新建立連線中",Today:"今天",Send:"發送","Chat closed by %s":"Chat closed by %s","Compose your message...":"正在輸入訊息...","All colleagues are busy.":"所有服務人員都在忙碌中.","You are on waiting list position %s.":"你目前的等候位置是第 %s 順位.","Start new conversation":"開始新的對話","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"由於你超過 %s 分鐘沒有回應, 你與 %s 的對話已被關閉.","Since you didn't respond in the last %s minutes your conversation got closed.":"由於你超過 %s 分鐘沒有任何回應, 該對話已被關閉.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"非常抱歉, 當前需要等候更長的時間方可排入對話程序, 請稍後重試或向我們寄送電子郵件. 謝謝!"}},i.prototype.sessionId=void 0,i.prototype.scrolledToBottom=!0,i.prototype.scrollSnapTolerance=10,i.prototype.richTextFormatKey={66:!0,73:!0,85:!0,83:!0},i.prototype.T=function(){var t,e,n,s,i,o;if(i=arguments[0],e=2<=arguments.length?slice.call(arguments,1):[],this.options.lang&&"en"!==this.options.lang&&(this.translations[this.options.lang]?(o=this.translations[this.options.lang],o[i]||this.log.notice("Translation needed for '"+i+"'"),i=o[i]||i):this.log.notice("Translation '"+this.options.lang+"' needed!")),e)for(n=0,s=e.length;nn?e:document.body)},i.prototype.render=function(){return this.el&&t(".zammad-chat").get(0)||this.renderBase(),t("."+this.options.buttonClass).addClass(this.inactiveClass),this.setAgentOnlineState("online"),this.log.debug("widget rendered"),this.startTimeoutObservers(),this.idleTimeout.start(),this.sessionId=sessionStorage.getItem("sessionId"),this.send("chat_status_customer",{session_id:this.sessionId,url:e.location.href})},i.prototype.renderBase=function(){if(this.el=t(this.view("chat")({title:this.options.title,scrollHint:this.options.scrollHint})),this.options.target.append(this.el),this.input=this.el.find(".zammad-chat-input"),this.el.find(".js-chat-open").click(this.open),this.el.find(".js-chat-toggle").click(this.toggle),this.el.find(".zammad-chat-controls").on("submit",this.onSubmit),this.el.find(".zammad-chat-body").on("scroll",this.detectScrolledtoBottom),this.el.find(".zammad-scroll-hint").click(this.onScrollHintClick),this.input.on({keydown:this.checkForEnter,input:this.onInput}),this.input.on("keydown",function(t){return function(e){var n;if(n=!1,e.altKey||e.ctrlKey||!e.metaKey?e.altKey||!e.ctrlKey||e.metaKey||(n=!0):n=!0,n&&t.richTextFormatKey[e.keyCode]){if(e.preventDefault(),66===e.keyCode)return document.execCommand("bold"),!0;if(73===e.keyCode)return document.execCommand("italic"),!0;if(85===e.keyCode)return document.execCommand("underline"),!0;if(83===e.keyCode)return document.execCommand("strikeThrough"),!0}}}(this)),this.input.on("paste",function(n){return function(s){var i,o,a,r,l,c,h,d,u,p,m,g;if(s.stopPropagation(),s.preventDefault(),s.clipboardData)i=s.clipboardData;else if(e.clipboardData)i=e.clipboardData;else{if(!s.originalEvent.clipboardData)throw"No clipboardData support";i=s.originalEvent.clipboardData}if(c=!1,i&&i.items&&i.items[0]&&(h=i.items[0],"file"!==h.kind||"image/png"!==h.type&&"image/jpeg"!==h.type||(l=h.getAsFile(),u=new FileReader,u.onload=function(t){var e,s,i;return i=t.target.result,e=document.createElement("img"),e.src=i,s=function(t,s,o,a){return n.isRetina()&&(s/=2,o/=2),i=t,e='',document.execCommand("insertHTML",!1,e)},n.resizeImage(e.src,460,"auto",2,"image/jpeg","auto",s)},u.readAsDataURL(l),c=!0)),!c){g=void 0,o=void 0;try{g=i.getData("text/html"),o="html",g&&0!==g.length||(o="text",g=i.getData("text/plain")),g&&0!==g.length||(o="text2",g=i.getData("text"))}catch(f){s=f,console.log("Sorry, can't insert markup because browser is not supporting it."),o="text3",g=i.getData("text")}return"text"!==o&&"text2"!==o&&"text3"!==o||(g="
                          "+g.replace(/\n/g,"
                          ")+"
                          ",g=g.replace(/
                          <\/div>/g,"

                          ")),console.log("p",o,g),"html"===o&&(a=t("
                          "+g+"
                          "),d=!1,r=g,p=new RegExp("<(/w|w):[A-Za-z]"),r.match(p)&&(d=!0,r=r.replace(p,"")),p=new RegExp("<(/o|o):[A-Za-z]"),r.match(p)&&(d=!0,r=r.replace(p,"")),d&&(a=n.wordFilter(a)),a=t(a),a.contents().each(function(){if(8===this.nodeType)return t(this).remove()}),a.find("a, font, small, time, form, label").replaceWith(function(){return t(this).contents()}),m="div",a.find("textarea").each(function(){var e,n;return n=this.outerHTML,p=new RegExp("<"+this.tagName,"i"),e=n.replace(p,"<"+m),p=new RegExp("'),s=s.get(0),document.caretPositionFromPoint?(h=document.caretPositionFromPoint(r,l),d=document.createRange(),d.setStart(h.offsetNode,h.offset),d.collapse(),d.insertNode(s)):document.caretRangeFromPoint?(d=document.caretRangeFromPoint(r,l),d.insertNode(s)):console.log("could not find carat")},n.resizeImage(s.src,460,"auto",2,"image/jpeg","auto",i)},a.readAsDataURL(o)}}(this)),t(e).on("beforeunload",function(t){return function(){return t.onLeaveTemporary()}}(this)),t(e).bind("hashchange",function(t){return function(){return t.isOpen?void(t.sessionId&&t.send("chat_session_notice",{session_id:t.sessionId,message:e.location.href})):t.idleTimeout.start()}}(this)),this.isFullscreen)return this.input.on({focus:this.onFocus,focusout:this.onFocusOut})},i.prototype.checkForEnter=function(t){if(!t.shiftKey&&13===t.keyCode)return t.preventDefault(),this.sendMessage()},i.prototype.send=function(t,e){return null==e&&(e={}),e.chat_id=this.options.chatId,this.io.send(t,e)},i.prototype.onWebSocketMessage=function(t){var e,n,s;for(e=0,n=t.length;e0,t(e).scrollTop(0),n)return this.log.notice("virtual keyboard shown")},i.prototype.onFocusOut=function(){},i.prototype.onTyping=function(){if(!(this.isTyping&&this.isTyping>new Date((new Date).getTime()-1500)))return this.isTyping=new Date,this.send("chat_session_typing",{session_id:this.sessionId}),this.inactiveTimeout.start()},i.prototype.onSubmit=function(t){return t.preventDefault(),this.sendMessage()},i.prototype.sendMessage=function(){var t,e;if(t=this.input.html())return this.inactiveTimeout.start(),sessionStorage.removeItem("unfinished_message"),e=this.view("message")({message:t,from:"customer",id:this._messageCount++,unreadClass:""}),this.maybeAddTimestamp(),this.el.find(".zammad-chat-message--typing").get(0)?(this.lastAddedType="typing-placeholder",this.el.find(".zammad-chat-message--typing").before(e)):(this.lastAddedType="message--customer",this.el.find(".zammad-chat-body").append(e)),this.input.html(""),this.scrollToBottom(),this.send("chat_session_message",{content:t,id:this._messageCount,session_id:this.sessionId})},i.prototype.receiveMessage=function(t){return this.inactiveTimeout.start(),this.onAgentTypingEnd(),this.maybeAddTimestamp(),this.renderMessage({message:t.message.content,id:t.id,from:"agent"}),this.scrollToBottom({showHint:!0})},i.prototype.renderMessage=function(t){return this.lastAddedType="message--"+t.from,t.unreadClass=document.hidden?" zammad-chat-message--unread":"",this.el.find(".zammad-chat-body").append(this.view("message")(t))},i.prototype.open=function(){var t;return this.isOpen?void this.log.debug("widget already open, block"):(this.isOpen=!0,this.log.debug("open widget"),this.sessionId||this.showLoader(),this.el.addClass("zammad-chat-is-open"),t=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.css("bottom",-t),this.sessionId?(this.el.css("bottom",0),this.onOpenAnimationEnd()):(this.el.animate({bottom:0},500,this.onOpenAnimationEnd),this.send("chat_session_init",{url:e.location.href})))},i.prototype.onOpenAnimationEnd=function(){if(this.idleTimeout.stop(),this.isFullscreen)return this.disableScrollOnRoot()},i.prototype.sessionClose=function(){return this.send("chat_session_close",{session_id:this.sessionId}),this.inactiveTimeout.stop(),this.waitingListTimeout.stop(),sessionStorage.removeItem("unfinished_message"),this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.setSessionId(void 0)},i.prototype.toggle=function(t){return this.isOpen?this.close(t):this.open(t)},i.prototype.close=function(t){var e;return this.isOpen?(this.initDelayId&&clearTimeout(this.initDelayId),this.sessionId?(this.log.debug("close widget"),t&&t.stopPropagation(),this.sessionClose(),this.isFullscreen&&this.enableScrollOnRoot(),e=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.animate({bottom:-e},500,this.onCloseAnimationEnd)):void this.log.debug("can't close widget without sessionId")):void this.log.debug("can't close widget, it's not open")},i.prototype.onCloseAnimationEnd=function(){return this.el.css("bottom",""),this.el.removeClass("zammad-chat-is-open"),this.showLoader(),this.el.find(".zammad-chat-welcome").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").addClass("zammad-chat-is-hidden"),this.isOpen=!1,this.io.reconnect()},i.prototype.onWebSocketClose=function(){if(!this.isOpen)return this.el?(this.el.removeClass("zammad-chat-is-shown"),this.el.removeClass("zammad-chat-is-loaded")):void 0},i.prototype.show=function(){if("offline"!==this.state)return this.el.addClass("zammad-chat-is-loaded"),this.el.addClass("zammad-chat-is-shown")},i.prototype.disableInput=function(){return this.input.prop("disabled",!0),this.el.find(".zammad-chat-send").prop("disabled",!0)},i.prototype.enableInput=function(){return this.input.prop("disabled",!1),this.el.find(".zammad-chat-send").prop("disabled",!1)},i.prototype.hideModal=function(){return this.el.find(".zammad-chat-modal").html("")},i.prototype.onQueueScreen=function(t){var e;return this.setSessionId(t.session_id),e=function(e){return function(){return e.onQueue(t),e.waitingListTimeout.start()}}(this),this.initialQueueDelay&&!this.onInitialQueueDelayId?void(this.onInitialQueueDelayId=setTimeout(e,this.initialQueueDelay)):(this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),e())},i.prototype.onQueue=function(t){return this.log.notice("onQueue",t.position),this.inQueue=!0,this.el.find(".zammad-chat-modal").html(this.view("waiting")({position:t.position}))},i.prototype.onAgentTypingStart=function(){if(this.stopTypingId&&clearTimeout(this.stopTypingId),this.stopTypingId=setTimeout(this.onAgentTypingEnd,3e3),!this.el.find(".zammad-chat-message--typing").get(0)&&(this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("typingIndicator")()),this.isVisible(this.el.find(".zammad-chat-message--typing"),!0)))return this.scrollToBottom()},i.prototype.onAgentTypingEnd=function(){return this.el.find(".zammad-chat-message--typing").remove()},i.prototype.onLeaveTemporary=function(){if(this.sessionId)return this.send("chat_session_leave_temporary",{ +session_id:this.sessionId})},i.prototype.maybeAddTimestamp=function(){var t,e,n;if(n=Date.now(),!this.lastTimestamp||n-this.lastTimestamp>6e4*this.showTimeEveryXMinutes)return t=this.T("Today"),e=(new Date).toTimeString().substr(0,5),"timestamp"===this.lastAddedType?(this.updateLastTimestamp(t,e),this.lastTimestamp=n):(this.el.find(".zammad-chat-body").append(this.view("timestamp")({label:t,time:e})),this.lastTimestamp=n,this.lastAddedType="timestamp",this.scrollToBottom())},i.prototype.updateLastTimestamp=function(t,e){if(this.el)return this.el.find(".zammad-chat-body").find(".zammad-chat-timestamp").last().replaceWith(this.view("timestamp")({label:t,time:e}))},i.prototype.addStatus=function(t){if(this.el)return this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("status")({status:t})),this.scrollToBottom()},i.prototype.detectScrolledtoBottom=function(){var t;if(t=this.el.find(".zammad-chat-body").scrollTop()+this.el.find(".zammad-chat-body").outerHeight(),this.scrolledToBottom=Math.abs(t-this.el.find(".zammad-chat-body").prop("scrollHeight"))<=this.scrollSnapTolerance,this.scrolledToBottom)return this.el.find(".zammad-scroll-hint").addClass("is-hidden")},i.prototype.showScrollHint=function(){return this.el.find(".zammad-scroll-hint").removeClass("is-hidden"),this.el.find(".zammad-chat-body").scrollTop(this.el.find(".zammad-chat-body").scrollTop()+this.el.find(".zammad-scroll-hint").outerHeight())},i.prototype.onScrollHintClick=function(){return this.el.find(".zammad-chat-body").animate({scrollTop:this.el.find(".zammad-chat-body").prop("scrollHeight")},300)},i.prototype.scrollToBottom=function(e){var n;return n=(null!=e?e:{showHint:!1}).showHint,this.scrolledToBottom?this.el.find(".zammad-chat-body").scrollTop(t(".zammad-chat-body").prop("scrollHeight")):n?this.showScrollHint():void 0},i.prototype.destroy=function(t){return null==t&&(t={}),this.log.debug("destroy widget",t),this.setAgentOnlineState("offline"),t.remove&&this.el&&this.el.remove(),this.waitingListTimeout&&this.waitingListTimeout.stop(),this.inactiveTimeout&&this.inactiveTimeout.stop(),this.idleTimeout&&this.idleTimeout.stop(),this.io.close()},i.prototype.reconnect=function(){return this.log.notice("reconnecting"),this.disableInput(),this.lastAddedType="status",this.setAgentOnlineState("connecting"),this.addStatus(this.T("Connection lost"))},i.prototype.onConnectionReestablished=function(){return this.lastAddedType="status",this.setAgentOnlineState("online"),this.addStatus(this.T("Connection re-established"))},i.prototype.onSessionClosed=function(t){return this.addStatus(this.T("Chat closed by %s",t.realname)),this.disableInput(),this.setAgentOnlineState("offline"),this.inactiveTimeout.stop()},i.prototype.setSessionId=function(t){return this.sessionId=t,void 0===t?sessionStorage.removeItem("sessionId"):sessionStorage.setItem("sessionId",t)},i.prototype.onConnectionEstablished=function(t){return this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.inQueue=!1,t.agent&&(this.agent=t.agent),t.session_id&&this.setSessionId(t.session_id),this.el.find(".zammad-chat-body").html(""),this.el.find(".zammad-chat-agent").html(this.view("agent")({agent:this.agent})),this.enableInput(),this.hideModal(),this.el.find(".zammad-chat-welcome").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").removeClass("zammad-chat-is-hidden"),this.isFullscreen||this.input.focus(),this.setAgentOnlineState("online"),this.waitingListTimeout.stop(),this.idleTimeout.stop(),this.inactiveTimeout.start()},i.prototype.showCustomerTimeout=function(){var t;return this.el.find(".zammad-chat-modal").html(this.view("customer_timeout")({agent:this.agent.name,delay:this.options.inactiveTimeout})),t=function(){return location.reload()},this.el.find(".js-restart").click(t),this.sessionClose()},i.prototype.showWaitingListTimeout=function(){var t;return this.el.find(".zammad-chat-modal").html(this.view("waiting_list_timeout")({delay:this.options.watingListTimeout})),t=function(){return location.reload()},this.el.find(".js-restart").click(t),this.sessionClose()},i.prototype.showLoader=function(){return this.el.find(".zammad-chat-modal").html(this.view("loader")())},i.prototype.setAgentOnlineState=function(t){var e;if(this.state=t,this.el)return e=t.charAt(0).toUpperCase()+t.slice(1),this.el.find(".zammad-chat-agent-status").attr("data-status",t).text(this.T(e))},i.prototype.detectHost=function(){var t;return t="ws://","https"===c&&(t="wss://"),this.options.host=""+t+l+"/ws"},i.prototype.loadCss=function(){var t,e,n;if(this.options.cssAutoload)return n=this.options.cssUrl,n||(n=this.options.host.replace(/^wss/i,"https").replace(/^ws/i,"http").replace(/\/ws/i,""),n+="/assets/chat/chat.css"),this.log.debug("load css from '"+n+"'"),e="@import url('"+n+"');",t=document.createElement("link"),t.onload=this.onCssLoaded,t.rel="stylesheet",t.href="data:text/css,"+escape(e),document.getElementsByTagName("head")[0].appendChild(t)},i.prototype.onCssLoaded=function(){return this.socketReady?this.onReady():this.cssLoaded=!0},i.prototype.startTimeoutObservers=function(){return this.idleTimeout=new o({logPrefix:"idleTimeout",debug:this.options.debug,timeout:this.options.idleTimeout,timeoutIntervallCheck:this.options.idleTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Idle timeout reached, hide widget",new Date),t.destroy({remove:!0})}}(this)}),this.inactiveTimeout=new o({logPrefix:"inactiveTimeout",debug:this.options.debug,timeout:this.options.inactiveTimeout,timeoutIntervallCheck:this.options.inactiveTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Inactive timeout reached, show timeout screen.",new Date),t.showCustomerTimeout(),t.destroy({remove:!1})}}(this)}),this.waitingListTimeout=new o({logPrefix:"waitingListTimeout",debug:this.options.debug,timeout:this.options.waitingListTimeout,timeoutIntervallCheck:this.options.waitingListTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Waiting list timeout reached, show timeout screen.",new Date),t.showWaitingListTimeout(),t.destroy({remove:!1})}}(this)})},i.prototype.disableScrollOnRoot=function(){return this.rootScrollOffset=this.scrollRoot.scrollTop(),this.scrollRoot.css({overflow:"hidden",position:"fixed"})},i.prototype.enableScrollOnRoot=function(){return this.scrollRoot.scrollTop(this.rootScrollOffset),this.scrollRoot.css({overflow:"",position:""})},i.prototype.isVisible=function(n,s,i,o){var a,r,l,c,h,d,u,p,m,g,f,y,v,b,w,T,C,z,S,k,I,A,x,_,O,E;if(!(n.length<1))if(r=t(e),a=n.length>1?n.eq(0):n,z=a.get(0),E=r.width(),O=r.height(),o=o?o:"both",p=i!==!0||z.offsetWidth*z.offsetHeight,"function"==typeof z.getBoundingClientRect){if(C=z.getBoundingClientRect(),S=C.top>=0&&C.top0&&C.bottom<=O,b=C.left>=0&&C.left0&&C.right<=E,k=s?S||u:S&&u,v=s?b||T:b&&T,"both"===o)return p&&k&&v;if("vertical"===o)return p&&k;if("horizontal"===o)return p&&v}else{if(_=r.scrollTop(),I=_+O,A=r.scrollLeft(),x=A+E,w=a.offset(),d=w.top,l=d+a.height(),c=w.left,h=c+a.width(),y=s===!0?l:d,m=s===!0?d:l,g=s===!0?h:c,f=s===!0?c:h,"both"===o)return!!p&&m<=I&&y>=_&&f<=x&&g>=A;if("vertical"===o)return!!p&&m<=I&&y>=_;if("horizontal"===o)return!!p&&f<=x&&g>=A}},i.prototype.isRetina=function(){var t;return!!e.matchMedia&&(t=e.matchMedia("only screen and (min--moz-device-pixel-ratio: 1.3), only screen and (-o-min-device-pixel-ratio: 2.6/2), only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen and (min-device-pixel-ratio: 1.3), only screen and (min-resolution: 1.3dppx)"),t&&t.matches||e.devicePixelRatio>1)},i.prototype.resizeImage=function(t,e,n,s,i,o,a,r){var l;return null==e&&(e="auto"),null==n&&(n="auto"),null==s&&(s=1),null==r&&(r=!0),l=new Image,l.onload=function(){var t,r,c,h,d,u,p;return d=l.width,h=l.height,console.log("ImageService","current size",d,h),"auto"===n&&"auto"===e&&(e=d,n=h),"auto"===n&&(c=d/e,n=h/c),"auto"===e&&(c=d/n,e=h/c),p=!1,e/gi,""),n=n.replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi,""),n=n.replace(/<(\/?)s>/gi,"<$1strike>"),n=n.replace(/ /gi," "),e.html(n),t("p",e).each(function(){var e,n;if(n=t(this).attr("style"),e=/mso-list:\w+ \w+([0-9]+)/.exec(n))return t(this).data("_listLevel",parseInt(e[1],10))}),s=0,i=null,t("p",e).each(function(){var e,n,o,a,r,l,c,h,d,u;if(e=t(this).data("_listLevel"),void 0!==e){if(u=t(this).text(),a="
                            ",/^\s*\w+\./.test(u)&&(r=/([0-9])\./.exec(u),r?(d=parseInt(r[1],10),a=null!=(l=d>1)?l:'
                              ':"
                                "}):a="
                                  "),e>s&&(0===s?(t(this).before(a),i=t(this).prev()):i=t(a).appendTo(i)),e=h;n=c<=h?++o:--o)i=i.parent();return t("span:first",this).remove(),i.append("
                                1. "+t(this).html()+"
                                2. "),t(this).remove(),s=e}return s=0}),t("[style]",e).removeAttr("style"),t("[align]",e).removeAttr("align"),t("span",e).replaceWith(function(){return t(this).contents()}),t("span:empty",e).remove(),t("[class^='Mso']",e).removeAttr("class"),t("p:empty",e).remove(),e},i.prototype.removeAttribute=function(e){var n,s,i,o,a;if(e){for(n=t(e),a=e.attributes,i=0,o=a.length;i/g,">").replace(/"/g,""")}),function(){(function(){n.push('
                                  \n
                                  \n
                                  \n \n \n \n \n \n
                                  \n
                                  \n
                                  \n
                                  \n \n '),n.push(this.T(this.title)),n.push('\n
                                  \n
                                  \n
                                  \n \n
                                  \n
                                  \n
                                  \n \n
                                  \n
                                  ")}).call(this)}.call(t),t.safe=i,t.escape=o,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.customer_timeout=function(t){t||(t={});var e,n=[],s=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},i=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('
                                  \n '),this.agent?(n.push("\n "),n.push(this.T("Since you didn't respond in the last %s minutes your conversation with %s got closed.",this.delay,this.agent)),n.push("\n ")):(n.push("\n "),n.push(this.T("Since you didn't respond in the last %s minutes your conversation got closed.",this.delay)),n.push("\n ")),n.push('\n
                                  \n
                                  "),n.push(this.T("Start new conversation")),n.push("
                                  \n
                                  ")}).call(this)}.call(t),t.safe=i,t.escape=o,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.loader=function(t){t||(t={});var e,n=[],s=t.safe,i=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('\n \n \n \n\n'),n.push(this.T("Connecting")),n.push("")}).call(this)}.call(t),t.safe=s,t.escape=i,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.message=function(t){t||(t={});var e,n=[],s=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},i=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('
                                  \n "),n.push(this.message),n.push("\n
                                  ")}).call(this)}.call(t),t.safe=i,t.escape=o,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.status=function(t){t||(t={});var e,n=[],s=t.safe,i=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('
                                  \n
                                  \n '),n.push(this.status),n.push("\n
                                  \n
                                  ")}).call(this)}.call(t),t.safe=s,t.escape=i,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.timestamp=function(t){t||(t={});var e,n=[],s=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},i=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('
                                  '),n.push(s(this.label)),n.push(" "),n.push(s(this.time)),n.push("
                                  ")}).call(this)}.call(t),t.safe=i,t.escape=o,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.typingIndicator=function(t){t||(t={});var e,n=[],s=t.safe,i=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('
                                  \n \n \n \n \n \n \n \n
                                  ')}).call(this)}.call(t),t.safe=s,t.escape=i,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting=function(t){t||(t={});var e,n=[],s=t.safe,i=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('
                                  \n \n \n \n \n \n '),n.push(this.T("All colleagues are busy.")),n.push("
                                  \n "),n.push(this.T("You are on waiting list position %s.",this.position)),n.push("\n
                                  ")}).call(this)}.call(t),t.safe=s,t.escape=i,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting_list_timeout=function(t){t||(t={});var e,n=[],s=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},i=t.safe,o=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;"undefined"!=typeof t&&null!=t||(t="");var e=new String(t);return e.ecoSafe=!0,e},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('
                                  \n '),n.push(this.T("We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!")),n.push('\n
                                  \n
                                  "),n.push(this.T("Start new conversation")),n.push("
                                  \n
                                  ")}).call(this)}.call(t),t.safe=i,t.escape=o,n.join("")}; \ No newline at end of file diff --git a/public/assets/form/form.js b/public/assets/form/form.js index 8bf176a06..df238b7df 100644 --- a/public/assets/form/form.js +++ b/public/assets/form/form.js @@ -131,7 +131,25 @@ $(function() { 'Your Email': 'Uw Email adres', 'Message': 'Bericht', 'Attachments': 'Bijlage', - 'Your Message...': 'Uw bericht.......', + 'Your Message...': 'Uw bericht...', + }, + 'it': { + 'Name': 'Nome', + 'Your Name': 'Il tuo nome', + 'Email': 'E-mail', + 'Your Email': 'Il tuo indirizzo e-mail', + 'Message': 'Messaggio', + 'Attachments': 'Allegati', + 'Your Message...': 'Il tuo messaggio...', + }, + 'pl': { + 'Name': 'Nazwa', + 'Your Name': 'Imię i nazwisko', + 'Email': 'Email adres', + 'Your Email': 'Adres e-mail', + 'Message': 'Wiadomość', + 'Attachments': 'Załączniki', + 'Your Message...': 'Twoja wiadomość...', }, 'zh-cn': { 'Name': '联系人', From 6c707dc5f519c45d9d4d79723d67448704c9c788 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Thu, 23 Nov 2017 23:09:11 +0100 Subject: [PATCH 023/196] Fixed test. --- test/browser/chat_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/browser/chat_test.rb b/test/browser/chat_test.rb index cd273d4d0..f7ea8a66e 100644 --- a/test/browser/chat_test.rb +++ b/test/browser/chat_test.rb @@ -397,7 +397,7 @@ class ChatTest < TestCase watch_for( browser: customer, css: '.zammad-chat', - value: 'Chat closed by', + value: '(Chat closed by|Chat beendet von)', ) click( browser: customer, From ce1135a55c1e9b127f99bf2d5f9647128d603d2e Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Fri, 24 Nov 2017 16:54:56 +0100 Subject: [PATCH 024/196] Fixed issue #1670 - Reset customer selection in ticket create screen if input field cleared. --- .../sidebar_customer.coffee | 3 +- .../app/controllers/ticket_customer.coffee | 13 +- ..._object_organization_autocompletion.coffee | 39 +-- script/build/test_slice_tests.sh | 6 + ...et_create_reset_customer_selection_test.rb | 244 ++++++++++++++++++ test/browser/agent_user_manage_test.rb | 18 +- test/browser_test_helper.rb | 2 +- 7 files changed, 297 insertions(+), 28 deletions(-) create mode 100644 test/browser/agent_ticket_create_reset_customer_selection_test.rb diff --git a/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_customer.coffee b/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_customer.coffee index cbfd4e7e9..b4b80bdeb 100644 --- a/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_customer.coffee +++ b/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_customer.coffee @@ -1,7 +1,7 @@ class SidebarCustomer extends App.Controller sidebarItem: => return if !@permissionCheck('ticket.agent') - return if !@params.customer_id + return if _.isEmpty(@params.customer_id) { head: 'Customer' name: 'customer' @@ -18,6 +18,7 @@ class SidebarCustomer extends App.Controller showCustomer: (el) => @el = el + return if _.isEmpty(@params.customer_id) new App.WidgetUser( el: @el user_id: @params.customer_id diff --git a/app/assets/javascripts/app/controllers/ticket_customer.coffee b/app/assets/javascripts/app/controllers/ticket_customer.coffee index edb5df0c0..ba50eb0ba 100644 --- a/app/assets/javascripts/app/controllers/ticket_customer.coffee +++ b/app/assets/javascripts/app/controllers/ticket_customer.coffee @@ -18,8 +18,19 @@ class App.TicketCustomer extends App.ControllerModal onSubmit: (e) => params = @formParam(e.target) - @customer_id = params['customer_id'] + ticket = App.Ticket.find(@ticket_id) + ticket.customer_id = params['customer_id'] + errors = ticket.validate() + if !_.isEmpty(errors) + @log 'error', errors + @formValidate( + form: e.target + errors: errors + ) + return + + @customer_id = params['customer_id'] callback = => # close modal diff --git a/app/assets/javascripts/app/lib/app_post/_object_organization_autocompletion.coffee b/app/assets/javascripts/app/lib/app_post/_object_organization_autocompletion.coffee index 011dc2202..da0d8b2ee 100644 --- a/app/assets/javascripts/app/lib/app_post/_object_organization_autocompletion.coffee +++ b/app/assets/javascripts/app/lib/app_post/_object_organization_autocompletion.coffee @@ -71,7 +71,7 @@ class App.ObjectOrganizationAutocompletion extends App.Controller @open() focusInput: => - @objectSelect.focus() if not @formControl.hasClass 'focus' + @objectSelect.focus() if not @formControl.hasClass('focus') onBlur: => selectObject = @objectSelect.val() @@ -85,6 +85,9 @@ class App.ObjectOrganizationAutocompletion extends App.Controller @objectId.val("guess:#{selectObject}") @formControl.removeClass 'focus' + resetObjectSelection: => + @objectId.val('').trigger('change') + onObjectClick: (e) => objectId = $(e.currentTarget).data('object-id') @selectObject(objectId) @@ -103,23 +106,23 @@ class App.ObjectOrganizationAutocompletion extends App.Controller # Only work with the last one since its the newest one objectId = @objectId.val().split(',').pop() - return if !objectId - return if !App[@objectSingle].exists(objectId) - object = App[@objectSingle].find(objectId) - name = object.displayName() + if objectId && App[@objectSingle].exists(objectId) + object = App[@objectSingle].find(objectId) + name = object.displayName() - if @attribute.multiple - # create token - @createToken name, objectId - else - if object.email + if @attribute.multiple - # quote name for special character - if name.match(/\@|,|;|\^|\+|#|§|\$|%|&|\/|\(|\)|=|\?|!|\*|\[|\]/) - name = "\"#{name}\"" - name += " <#{object.email}>" + # create token + @createToken(name, objectId) + else + if object.email - @objectSelect.val(name) + # quote name for special character + if name.match(/\@|,|;|\^|\+|#|§|\$|%|&|\/|\(|\)|=|\?|!|\*|\[|\]/) + name = "\"#{name}\"" + name += " <#{object.email}>" + + @objectSelect.val(name) if @callback @callback(objectId) @@ -321,12 +324,16 @@ class App.ObjectOrganizationAutocompletion extends App.Controller @hideOrganizationMembers() # hide dropdown - if !query + if _.isEmpty(query) @emptyResultList() if !@attribute.disableCreateObject @recipientList.append(@buildObjectNew()) + # reset object selection + @resetObjectSelection() + return + # show dropdown if query && ( !@attribute.minLengt || @attribute.minLengt <= query.length ) @lazySearch(query) diff --git a/script/build/test_slice_tests.sh b/script/build/test_slice_tests.sh index 34e837538..559f49fa0 100755 --- a/script/build/test_slice_tests.sh +++ b/script/build/test_slice_tests.sh @@ -20,6 +20,7 @@ if [ "$LEVEL" == '1' ]; then rm test/browser/admin_role_test.rb # test/browser/agent_navigation_and_title_test.rb rm test/browser/agent_ticket_attachment_test.rb + rm test/browser/agent_ticket_create_reset_customer_selection_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,6 +79,7 @@ elif [ "$LEVEL" == '2' ]; then 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_create_reset_customer_selection_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 @@ -136,6 +138,7 @@ elif [ "$LEVEL" == '3' ]; then 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_create_reset_customer_selection_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 @@ -194,6 +197,7 @@ elif [ "$LEVEL" == '4' ]; then 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_create_reset_customer_selection_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 @@ -251,6 +255,7 @@ elif [ "$LEVEL" == '5' ]; then rm 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_create_reset_customer_selection_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 @@ -311,6 +316,7 @@ elif [ "$LEVEL" == '6' ]; then 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_create_reset_customer_selection_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 diff --git a/test/browser/agent_ticket_create_reset_customer_selection_test.rb b/test/browser/agent_ticket_create_reset_customer_selection_test.rb new file mode 100644 index 000000000..29ebe49dc --- /dev/null +++ b/test/browser/agent_ticket_create_reset_customer_selection_test.rb @@ -0,0 +1,244 @@ + +require 'browser_test_helper' + +class AgentTicketCreateResetCustomerSelectionTest < TestCase + def test_clear_customer + @browser = browser_instance + login( + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + tasks_close_all() + + click(css: 'a[href="#new"]') + click(css: 'a[href="#ticket/create"]') + sleep 2 + + exists(css: '.content.active .newTicket') + exists(css: '.content.active .tabsSidebar .sidebar[data-tab="template"]') + exists(css: '.content.active .tabsSidebar .tabsSidebar-tab.active[data-tab="template"]') + + exists_not(css: '.content.active .tabsSidebar .sidebar[data-tab="customer"]') + exists_not(css: '.content.active .tabsSidebar .tabsSidebar-tab[data-tab="customer"]') + + click(css: '.content.active .newTicket [name="customer_id_completion"]') + + # check if pulldown is open, it's not working stable via selenium + @browser.execute_script( "$('.content.active .newTicket .js-recipientDropdown').addClass('open')" ) + + set( + css: '.content.active .newTicket input[name="customer_id_completion"]', + value: 'nicole', + ) + + sleep 2 + sendkey(value: :enter) + sleep 1 + + exists(css: '.content.active .newTicket') + exists(css: '.content.active .tabsSidebar .sidebar[data-tab="template"]') + exists(css: '.content.active .tabsSidebar .tabsSidebar-tab.active[data-tab="template"]') + + exists(css: '.content.active .tabsSidebar .sidebar[data-tab="customer"]') + exists(css: '.content.active .tabsSidebar .tabsSidebar-tab[data-tab="customer"]') + + set( + css: '.content.active .newTicket input[name="customer_id_completion"]', + value: '', + ) + sendkey(value: :backspace) + + sleep 1 + + exists(css: '.content.active .newTicket') + exists(css: '.content.active .tabsSidebar .sidebar[data-tab="template"]') + exists(css: '.content.active .tabsSidebar .tabsSidebar-tab.active[data-tab="template"]') + + exists_not(css: '.content.active .tabsSidebar .sidebar[data-tab="customer"]') + exists_not(css: '.content.active .tabsSidebar .tabsSidebar-tab[data-tab="customer"]') + + set( + css: '.content.active .newTicket input[name="title"]', + value: 'some title', + ) + + set( + css: '.content.active .newTicket div[data-name="body"]', + value: 'some body', + ) + + select( + css: '.content.active .newTicket select[name="group_id"]', + value: 'Users', + ) + + click(css: '.content.active .newTicket .js-submit') + + watch_for( + css: '.content.active .newTicket .user_autocompletion.form-group.has-error', + ) + + # cleanup + tasks_close_all() + end + + def test_clear_customer_use_email + @browser = browser_instance + login( + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + tasks_close_all() + + click(css: 'a[href="#new"]') + click(css: 'a[href="#ticket/create"]') + sleep 2 + + exists(css: '.content.active .newTicket') + exists(css: '.content.active .tabsSidebar .sidebar[data-tab="template"]') + exists(css: '.content.active .tabsSidebar .tabsSidebar-tab.active[data-tab="template"]') + + exists_not(css: '.content.active .tabsSidebar .sidebar[data-tab="customer"]') + exists_not(css: '.content.active .tabsSidebar .tabsSidebar-tab[data-tab="customer"]') + + click(css: '.content.active .newTicket [name="customer_id_completion"]') + + # check if pulldown is open, it's not working stable via selenium + @browser.execute_script( "$('.content.active .newTicket .js-recipientDropdown').addClass('open')" ) + + set( + css: '.content.active .newTicket input[name="customer_id_completion"]', + value: 'nicole', + ) + + sleep 2 + sendkey(value: :enter) + sleep 1 + + exists(css: '.content.active .newTicket') + exists(css: '.content.active .tabsSidebar .sidebar[data-tab="template"]') + exists(css: '.content.active .tabsSidebar .tabsSidebar-tab.active[data-tab="template"]') + + exists(css: '.content.active .tabsSidebar .sidebar[data-tab="customer"]') + exists(css: '.content.active .tabsSidebar .tabsSidebar-tab[data-tab="customer"]') + + set( + css: '.content.active .newTicket input[name="customer_id_completion"]', + value: '', + ) + sendkey(value: :backspace) + + sleep 1 + + exists(css: '.content.active .newTicket') + exists(css: '.content.active .tabsSidebar .sidebar[data-tab="template"]') + exists(css: '.content.active .tabsSidebar .tabsSidebar-tab.active[data-tab="template"]') + + exists_not(css: '.content.active .tabsSidebar .sidebar[data-tab="customer"]') + exists_not(css: '.content.active .tabsSidebar .tabsSidebar-tab[data-tab="customer"]') + + set( + css: '.content.active .newTicket input[name="customer_id_completion"]', + value: 'somecustomer_not_existing_right_now@example.com', + ) + + set( + css: '.content.active .newTicket input[name="title"]', + value: 'some title', + ) + + set( + css: '.content.active .newTicket div[data-name="body"]', + value: 'some body', + ) + + select( + css: '.content.active .newTicket select[name="group_id"]', + value: 'Users', + ) + + click(css: '.content.active .newTicket .js-submit') + + 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: 'somecustomer_not_existing_right_now@example.com', + ) + + 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', + ) + + exists_not( + css: '.content.active .modal .user_autocompletion.form-group.has-error', + ) + + click(css: '.content.active .modal .js-submit') + + watch_for( + css: '.content.active .modal .user_autocompletion.form-group.has-error', + ) + + set( + css: '.content.active .modal input[name="customer_id_completion"]', + value: 'master', + ) + + click(css: '.content.active .modal .js-submit') + + watch_for( + css: '.content.active .modal .user_autocompletion.form-group.has-error', + ) + + set( + css: '.content.active .modal input[name="customer_id_completion"]', + value: 'master', + ) + + sendkey(value: :enter) + sleep 1 + + set( + css: '.content.active .modal input[name="customer_id_completion"]', + value: '', + ) + sendkey(value: :backspace) + sleep 1 + + click(css: '.content.active .modal .js-submit') + + watch_for( + css: '.content.active .modal .user_autocompletion.form-group.has-error', + ) + + set( + css: '.content.active .modal input[name="customer_id_completion"]', + value: 'master', + ) + + sendkey(value: :enter) + sleep 1 + + click(css: '.content.active .modal .js-submit') + #click(css: '.content.active .tabsSidebar-tabs .tabsSidebar-tab[data-tab="customer"]') + + watch_for( + css: '.content.active .tabsSidebar .sidebar[data-tab="customer"]', + value: 'master@example.com', + ) + + # cleanup + tasks_close_all() + end +end diff --git a/test/browser/agent_user_manage_test.rb b/test/browser/agent_user_manage_test.rb index 4d2160f29..4c82d1b16 100644 --- a/test/browser/agent_user_manage_test.rb +++ b/test/browser/agent_user_manage_test.rb @@ -19,17 +19,17 @@ class AgentUserManageTest < TestCase sleep 1 # create customer - click( css: 'a[href="#new"]' ) - click( css: 'a[href="#ticket/create"]' ) - click( css: '.active .newTicket [name="customer_id_completion"]' ) + click(css: 'a[href="#new"]') + click(css: 'a[href="#ticket/create"]') + click(css: '.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')" ) sleep 1 - sendkey( value: :arrow_down ) + sendkey(value: :arrow_down) sleep 0.5 - click( css: '.active .newTicket .recipientList-entry.js-objectNew' ) + click(css: '.active .newTicket .recipientList-entry.js-objectNew') sleep 1 set( @@ -45,7 +45,7 @@ class AgentUserManageTest < TestCase value: customer_user_email, ) - click( css: '.modal button.js-submit' ) + click(css: '.modal button.js-submit') sleep 4 # check is used to check selected @@ -75,8 +75,8 @@ class AgentUserManageTest < TestCase # call new ticket screen again tasks_close_all() - click( css: 'a[href="#new"]' ) - click( css: 'a[href="#ticket/create"]' ) + click(css: 'a[href="#new"]') + click(css: 'a[href="#ticket/create"]') sleep 2 match( @@ -93,7 +93,7 @@ class AgentUserManageTest < TestCase ) sleep 3 - click( css: '.active .newTicket .recipientList-entry.js-object.is-active' ) + click(css: '.active .newTicket .recipientList-entry.js-object.is-active') sleep 1 # check is used to check selected diff --git a/test/browser_test_helper.rb b/test/browser_test_helper.rb index 909503218..3f4decb00 100644 --- a/test/browser_test_helper.rb +++ b/test/browser_test_helper.rb @@ -1261,7 +1261,7 @@ set type of task (closeTab, closeNextInOverview, stayOnTab) sleep 0.5 return true - # match pn attribute + # match an attribute else text = if params[:attribute] element.attribute(params[:attribute]) From 1a9a23ce0efe06f983f9a45100aeb688d5221567 Mon Sep 17 00:00:00 2001 From: Muhammad Nuzaihan Date: Sat, 25 Nov 2017 00:49:40 +0800 Subject: [PATCH 025/196] #1674 make sure login data is downcased. included tests to check as well --- lib/import/otrs/customer_user.rb | 1 + lib/import/otrs/user.rb | 1 + .../otrs/customer_user/camel_case_login.json | 348 ++++++++++++++++++ .../import/otrs/user/camel_case_login.json | 135 +++++++ spec/lib/import/otrs/customer_user_spec.rb | 38 ++ spec/lib/import/otrs/user_spec.rb | 34 ++ 6 files changed, 557 insertions(+) create mode 100644 spec/fixtures/import/otrs/customer_user/camel_case_login.json create mode 100644 spec/fixtures/import/otrs/user/camel_case_login.json diff --git a/lib/import/otrs/customer_user.rb b/lib/import/otrs/customer_user.rb index 6e12bb4b6..0d42816dd 100644 --- a/lib/import/otrs/customer_user.rb +++ b/lib/import/otrs/customer_user.rb @@ -68,6 +68,7 @@ module Import mapped[:created_at] ||= DateTime.current mapped[:updated_at] ||= DateTime.current mapped[:email].downcase! + mapped[:login].downcase! mapped end diff --git a/lib/import/otrs/user.rb b/lib/import/otrs/user.rb index a9392c890..e6b2f2ecd 100644 --- a/lib/import/otrs/user.rb +++ b/lib/import/otrs/user.rb @@ -68,6 +68,7 @@ module Import def map(user) mapped = map_default(user) mapped[:email].downcase! + mapped[:login].downcase! mapped end diff --git a/spec/fixtures/import/otrs/customer_user/camel_case_login.json b/spec/fixtures/import/otrs/customer_user/camel_case_login.json new file mode 100644 index 000000000..71dc42eca --- /dev/null +++ b/spec/fixtures/import/otrs/customer_user/camel_case_login.json @@ -0,0 +1,348 @@ +{ + "CustomerCompanyCity": "test712259", + "Config": { + "CustomerUserEmailUniqCheck": 1, + "CustomerUserSearchListLimit": 250, + "CustomerCompanySupport": 1, + "CustomerValid": "valid_id", + "CustomerUserSearchFields": [ + "login", + "first_name", + "last_name", + "customer_id" + ], + "CustomerUserSearchPrefix": "*", + "Params": { + "Table": "customer_user", + "CaseSensitive": 0 + }, + "CustomerUserListFields": [ + "first_name", + "last_name", + "email" + ], + "Map": [ + [ + "UserTitle", + "Title", + "title", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserFirstname", + "Firstname", + "first_name", + 1, + 1, + "var", + "", + 0 + ], + [ + "UserLastname", + "Lastname", + "last_name", + 1, + 1, + "var", + "", + 0 + ], + [ + "UserLogin", + "Username", + "login", + 1, + 1, + "var", + "", + 0 + ], + [ + "UserPassword", + "Password", + "pw", + 0, + 0, + "var", + "", + 0 + ], + [ + "UserEmail", + "Email", + "email", + 1, + 1, + "var", + "", + 0 + ], + [ + "UserCustomerID", + "CustomerID", + "customer_id", + 0, + 1, + "var", + "", + 0 + ], + [ + "UserPhone", + "Phone", + "phone", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserFax", + "Fax", + "fax", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserMobile", + "Mobile", + "mobile", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserStreet", + "Street", + "street", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserZip", + "Zip", + "zip", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserCity", + "City", + "city", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserCountry", + "Country", + "country", + 1, + 0, + "var", + "", + 0 + ], + [ + "UserComment", + "Comment", + "comments", + 1, + 0, + "var", + "", + 0 + ], + [ + "ValidID", + "Valid", + "valid_id", + 0, + 1, + "int", + "", + 0 + ] + ], + "CustomerKey": "login", + "CustomerUserSearchSuffix": "*", + "Module": "Kernel::System::CustomerUser::DB", + "CacheTTL": 86400, + "Selections": {}, + "CustomerID": "customer_id", + "Name": "Database Backend", + "CustomerUserPostMasterSearchFields": [ + "email" + ], + "CustomerUserNameFields": [ + "title", + "first_name", + "last_name" + ] + }, + "UserCustomerID": "test712259", + "CustomerCompanyComment": "test712259", + "Source": "CustomerUser", + "UserTitle": "", + "CompanyConfig": { + "CustomerCompanySearchFields": [ + "customer_id", + "name" + ], + "CustomerCompanyListFields": [ + "customer_id", + "name" + ], + "Module": "Kernel::System::CustomerCompany::DB", + "CustomerCompanyKey": "customer_id", + "CustomerCompanySearchSuffix": "*", + "CacheTTL": 86400, + "CustomerCompanySearchListLimit": 250, + "CustomerCompanySearchPrefix": "", + "CustomerCompanyValid": "valid_id", + "Params": { + "Table": "customer_company", + "CaseSensitive": 0 + }, + "Map": [ + [ + "CustomerID", + "CustomerID", + "customer_id", + 0, + 1, + "var", + "", + 0 + ], + [ + "CustomerCompanyName", + "Customer", + "name", + 1, + 1, + "var", + "", + 0 + ], + [ + "CustomerCompanyStreet", + "Street", + "street", + 1, + 0, + "var", + "", + 0 + ], + [ + "CustomerCompanyZIP", + "Zip", + "zip", + 1, + 0, + "var", + "", + 0 + ], + [ + "CustomerCompanyCity", + "City", + "city", + 1, + 0, + "var", + "", + 0 + ], + [ + "CustomerCompanyCountry", + "Country", + "country", + 1, + 0, + "var", + "", + 0 + ], + [ + "CustomerCompanyURL", + "URL", + "url", + 1, + 0, + "var", + "[% Data.CustomerCompanyURL | html %]", + 0 + ], + [ + "CustomerCompanyComment", + "Comment", + "comments", + 1, + 0, + "var", + "", + 0 + ], + [ + "ValidID", + "Valid", + "valid_id", + 0, + 1, + "int", + "", + 0 + ] + ], + "Name": "Database Backend" + }, + "UserZip": null, + "UserLastname": "test669673", + "ChangeBy": "1", + "CreateTime": "2014-06-07 02:31:31", + "UserLogin": "TeSt669673", + "UserPhone": null, + "CustomerID": "test712259", + "CustomerCompanyValidID": "1", + "CustomerCompanyZIP": "test712259", + "UserCountry": null, + "UserPassword": "f8be19af2f25837a31eff9131b0e47a5173290652c04a48b49b86474d48825ee", + "ValidID": "1", + "UserRefreshTime": "0", + "UserEmail": "QA100@t-Online.de", + "UserComment": "", + "UserID": "test669673", + "UserFirstname": "test669673", + "CustomerCompanyCountry": "test712259", + "UserFax": null, + "CreateBy": "1", + "ChangeTime": "2014-06-07 02:31:31", + "UserShowTickets": "25", + "UserStreet": null, + "CustomerCompanyURL": "test712259", + "CustomerCompanyName": "test712259", + "UserMobile": null, + "CustomerCompanyStreet": "test712259", + "UserCity": null +} diff --git a/spec/fixtures/import/otrs/user/camel_case_login.json b/spec/fixtures/import/otrs/user/camel_case_login.json new file mode 100644 index 000000000..49d6d4aeb --- /dev/null +++ b/spec/fixtures/import/otrs/user/camel_case_login.json @@ -0,0 +1,135 @@ +{ + "OutOfOffice": "1", + "OutOfOfficeStartMonth": "9", + "UserStoredFilterColumns-AgentTicketLockedView": "{}", + "UserTicketOverviewSmallPageShown": "35", + "UserCreateWorkOrderNextMask": "AgentITSMWorkOrderZoom", + "OutOfOfficeEndYear": "2014", + "UserDashboardTicketGenericFilter0110-TicketEscalation": "All", + "UserDashboardPref0120-TicketNew-Columns": "{\"Columns\":{\"Changed\":0,\"CustomerID\":0,\"CustomerName\":0,\"CustomerUserID\":0,\"DynamicField_CustomerLocation\":0,\"EscalationResponseTime\":0,\"EscalationSolutionTime\":0,\"EscalationTime\":0,\"EscalationUpdateTime\":0,\"Lock\":0,\"Owner\":0,\"PendingTime\":0,\"Priority\":0,\"Responsible\":0,\"SLA\":0,\"State\":0,\"Type\":0,\"Age\":1,\"Title\":1,\"Queue\":1,\"Service\":1,\"TicketNumber\":1},\"Order\":[\"Age\",\"Title\",\"Queue\",\"Service\",\"TicketNumber\"]}", + "UserLastUsedZoomViewType": "", + "OutOfOfficeStartDay": "10", + "UserStoredFilterColumns-AgentTicketStatusView": "{}", + "UserTitle": null, + "UserLastname": "OTRS", + "UserTicketOverviewMediumPageShown": "20", + "OutOfOfficeEndDay": "12", + "CreateTime": "2014-04-28 10:53:18", + "UserTicketOverviewPreviewPageShown": "15", + "UserLogin": "rOoT@LoCaLhOsT", + "UserFilterColumnsEnabled-AgentTicketEscalationView": "[\"TicketNumber\",\"Age\",\"EscalationTime\",\"EscalationResponseTime\",\"EscalationSolutionTime\",\"EscalationUpdateTime\",\"Title\",\"State\",\"Lock\",\"Queue\",\"Owner\",\"CustomerID\"]", + "UserLanguage": "de", + "UserDashboardPref0110-TicketEscalation-Columns": "{\"Columns\":{\"Changed\":0,\"CustomerID\":0,\"CustomerUserID\":0,\"EscalationResponseTime\":0,\"EscalationSolutionTime\":0,\"EscalationTime\":0,\"EscalationUpdateTime\":0,\"Lock\":0,\"Owner\":0,\"PendingTime\":0,\"Priority\":0,\"Queue\":0,\"Responsible\":0,\"SLA\":0,\"Service\":0,\"State\":0,\"Type\":0,\"Age\":1,\"Title\":1,\"CustomerName\":1,\"TicketNumber\":1},\"Order\":[\"Age\",\"Title\",\"CustomerName\",\"TicketNumber\"]}", + "OutOfOfficeStartYear": "2014", + "UserDashboardPref0120-TicketNew-Shown": "10", + "UserFullname": "Admin OTRS", + "UserLastLoginTimestamp": "2016-08-10 19:37:44", + "UserLastLogin": "1470850664", + "UserMarkTicketUnseenRedirectURL": "Action=AgentTicketZoom;TicketID=###TicketID####1", + "AdminDynamicFieldsOverviewPageShown": "35", + "UserChangeOverviewSmallPageShown": "25", + "RoleIDs": [], + "ValidID": "1", + "UserStoredFilterColumns-AgentTicketQueue": "{}", + "UserEmail": "ROOT@loCalhosT", + "UserRefreshTime": "0", + "UserDashboardPref0130-TicketOpen-Shown": "10", + "UserTicketOverviewAgentTicketQueue": "Small", + "UserID": "1", + "UserDashboardTicketGenericColumnFiltersRealKeys0120-TicketNew": "{\"QueueIDs\":[\"1\"]}", + "wpt22": "1", + "UserMarkTicketSeenRedirectURL": "Action=AgentTicketZoom;TicketID=###TicketID####1", + "UserStoredFilterColumns-AgentTicketEscalationView": "{}", + "UserDashboardTicketGenericFilter0120-TicketNew": "MyQueues", + "UserCreateNextMask": "", + "UserFirstname": "Admin", + "UserPw": "9faaba2ab242a99bbb6992e9424386375f6757c17e6484ae570f39d9cad9f28ea", + "UserDashboardPref0110-TicketEscalation-Shown": "10", + "UserFilterColumnsEnabled-AgentTicketQueue": "[\"TicketNumber\",\"Age\",\"Title\",\"State\",\"Lock\",\"DynamicField_CustomerLocation\",\"Queue\",\"Owner\",\"CustomerID\",\"DynamicField_Hostname\"]", + "OutOfOfficeEndMonth": "9", + "ChangeTime": "2014-04-28 10:53:18", + "UserDashboardPref0130-TicketOpen-Columns": "{\"Columns\":{\"Changed\":0,\"CustomerID\":0,\"CustomerUserID\":0,\"EscalationResponseTime\":0,\"EscalationTime\":0,\"EscalationUpdateTime\":0,\"Lock\":0,\"Owner\":0,\"PendingTime\":0,\"Priority\":0,\"Queue\":0,\"Responsible\":0,\"SLA\":0,\"Service\":0,\"State\":0,\"Type\":0,\"Age\":1,\"DynamicField_CustomerLocation\":1,\"Title\":1,\"CustomerName\":1,\"EscalationSolutionTime\":1,\"TicketNumber\":1},\"Order\":[\"Age\",\"DynamicField_CustomerLocation\",\"Title\",\"CustomerName\",\"EscalationSolutionTime\",\"TicketNumber\"]}", + "UserTicketOverviewAgentTicketSearch": "Small", + "UserTicketOverviewAgentCustomerSearch": "Small", + "UserDashboardTicketGenericColumnFilters0120-TicketNew": "{\"Queue\":\"1\"}", + "GroupIDs": { + "6": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ], + "3": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ], + "7": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ], + "2": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ], + "8": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ], + "1": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ], + "4": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ], + "5": [ + "ro", + "move_into", + "create", + "note", + "owner", + "priority", + "rw" + ] + }, + "UserConfigItemOverviewSmallPageShown": "25", + "UserAuthBackend": "", + "UserTicketOverviewAgentTicketLockedView": "Small", + "UserTicketOverviewAgentTicketEscalationView": "Small", + "UserTicketOverviewAgentTicketStatusView": "Small", + "UserLoginFailed": "0" +} diff --git a/spec/lib/import/otrs/customer_user_spec.rb b/spec/lib/import/otrs/customer_user_spec.rb index 51374dd4b..5f43a51b2 100644 --- a/spec/lib/import/otrs/customer_user_spec.rb +++ b/spec/lib/import/otrs/customer_user_spec.rb @@ -158,4 +158,42 @@ RSpec.describe Import::OTRS::CustomerUser do updates_with(zammad_structure) end end + + context 'regular user with camelcase login' do + + let(:object_structure) { load_customer_json('camel_case_login') } + let(:zammad_structure) do + { + created_by_id: '1', + updated_by_id: '1', + active: true, + source: 'OTRS Import', + organization_id: 1337, + role_ids: [3], + updated_at: '2014-06-07 02:31:31', + created_at: '2014-06-07 02:31:31', + note: '', + email: 'qa100@t-online.de', + firstname: 'test669673', + lastname: 'test669673', + login: 'test669673', + password: 'f8be19af2f25837a31eff9131b0e47a5173290652c04a48b49b86474d48825ee', + phone: nil, + fax: nil, + mobile: nil, + street: nil, + zip: nil, + city: nil, + country: nil + } + end + + it 'creates' do + creates_with(zammad_structure) + end + + it 'updates' do + updates_with(zammad_structure) + end + end end diff --git a/spec/lib/import/otrs/user_spec.rb b/spec/lib/import/otrs/user_spec.rb index 11f08d9bc..fcbbe00e4 100644 --- a/spec/lib/import/otrs/user_spec.rb +++ b/spec/lib/import/otrs/user_spec.rb @@ -166,4 +166,38 @@ RSpec.describe Import::OTRS::User do updates_with(zammad_structure) end end + + context 'regular user with camel case login' do + + let(:object_structure) { load_user_json('camel_case_login') } + let(:zammad_structure) do + { + created_by_id: 1, + updated_by_id: 1, + active: true, + source: 'OTRS Import', + role_ids: [2, 1], + group_ids: ['1'], + password: '{sha2}9faaba2ab242a99bbb6992e9424386375f6757c17e6484ae570f39d9cad9f28ea', + updated_at: '2014-04-28 10:53:18', + created_at: '2014-04-28 10:53:18', + id: '1', + email: 'root@localhost', + firstname: 'Admin', + lastname: 'OTRS', + login: 'root@localhost' + } + end + + it 'creates' do + prepare_expectations + creates_with(zammad_structure) + end + + it 'updates' do + prepare_expectations + updates_with(zammad_structure) + end + end + end From 7599e8e17a5121e90f49d25a84ee8a2df02a4700 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Mon, 27 Nov 2017 11:50:57 +0100 Subject: [PATCH 026/196] Fixed issue #1681 - Unable to re-order overviews in admin interface with over 100 overviews. --- .../_application_controller_generic.coffee | 39 ++-- .../app/controllers/overview.coffee | 17 +- .../app/models/_application_model.coffee | 12 +- .../javascripts/app/models/overview.coffee | 2 +- app/controllers/overviews_controller.rb | 58 +++++- app/models/application_model/can_assets.rb | 14 +- app/models/overview.rb | 47 +++-- app/models/overview/assets.rb | 2 - config/routes/overview.rb | 1 + lib/fill_db.rb | 42 +++- test/controllers/overviews_controller_test.rb | 179 ++++++++++++++++++ test/unit/overview_test.rb | 156 ++++++++++++++- 12 files changed, 503 insertions(+), 66 deletions(-) create mode 100644 test/controllers/overviews_controller_test.rb diff --git a/app/assets/javascripts/app/controllers/_application_controller_generic.coffee b/app/assets/javascripts/app/controllers/_application_controller_generic.coffee index 20991117e..7fda56473 100644 --- a/app/assets/javascripts/app/controllers/_application_controller_generic.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller_generic.coffee @@ -148,24 +148,26 @@ class App.ControllerGenericIndex extends App.Controller return item ) - # show description button, only if content exists - showDescription = false - if App[ @genericObject ].description && !_.isEmpty(objects) - showDescription = true + if !@table - @html App.view('generic/admin/index')( - head: @pageData.objects - notes: @pageData.notes - buttons: @pageData.buttons - menus: @pageData.menus - showDescription: showDescription - ) + # show description button, only if content exists + showDescription = false + if App[ @genericObject ].description && !_.isEmpty(objects) + showDescription = true - # show description in content if no no content exists - if _.isEmpty(objects) && App[ @genericObject ].description - description = marked(App[ @genericObject ].description) - @$('.table-overview').html(description) - return + @html App.view('generic/admin/index')( + head: @pageData.objects + notes: @pageData.notes + buttons: @pageData.buttons + menus: @pageData.menus + showDescription: showDescription + ) + + # show description in content if no no content exists + if _.isEmpty(objects) && App[ @genericObject ].description + description = marked(App[ @genericObject ].description) + @$('.table-overview').html(description) + return # append content table params = _.extend( @@ -184,7 +186,10 @@ class App.ControllerGenericIndex extends App.Controller }, @pageData.tableExtend ) - new App.ControllerTable(params) + if !@table + @table = new App.ControllerTable(params) + else + @table.update(objects: objects) edit: (id, e) => e.preventDefault() diff --git a/app/assets/javascripts/app/controllers/overview.coffee b/app/assets/javascripts/app/controllers/overview.coffee index 3a8fae430..377328750 100644 --- a/app/assets/javascripts/app/controllers/overview.coffee +++ b/app/assets/javascripts/app/controllers/overview.coffee @@ -23,17 +23,22 @@ class Index extends App.ControllerSubContent ] container: @el.closest('.content') large: true - dndCallback: => + dndCallback: (e, item) => items = @el.find('table > tbody > tr') - order = [] + prios = [] prio = 0 for item in items prio += 1 id = $(item).data('id') - overview = App.Overview.find(id) - if overview.prio isnt prio - overview.prio = prio - overview.save() + prios.push [id, prio] + + @ajax( + id: 'overview_prio' + type: 'POST' + url: "#{@apiPath}/overviews_prio" + processData: true + data: JSON.stringify(prios: prios) + ) ) App.Config.set('Overview', { prio: 2300, name: 'Overviews', parent: '#manage', target: '#manage/overviews', controller: Index, permission: ['admin.overview'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/models/_application_model.coffee b/app/assets/javascripts/app/models/_application_model.coffee index cf41a2c23..660af96df 100644 --- a/app/assets/javascripts/app/models/_application_model.coffee +++ b/app/assets/javascripts/app/models/_application_model.coffee @@ -387,15 +387,17 @@ set new attributes of model (remove already available attributes) => return if _.isEmpty(@SUBSCRIPTION_COLLECTION) App.Log.debug('Model', "server notify collection change #{@className}") - @fetchFull( - -> - clear: true - ) + callback = => + @fetchFull( + -> + clear: true + ) + App.Delay.set(callback, 200, "full-#{@className}") "Collection::Subscribe::#{@className}" ) - key = @className + '-' + Math.floor( Math.random() * 99999 ) + key = "#{@className}-#{Math.floor(Math.random() * 99999)}" @SUBSCRIPTION_COLLECTION[key] = callback # fetch init collection diff --git a/app/assets/javascripts/app/models/overview.coffee b/app/assets/javascripts/app/models/overview.coffee index 85581873c..d6ad48a3c 100644 --- a/app/assets/javascripts/app/models/overview.coffee +++ b/app/assets/javascripts/app/models/overview.coffee @@ -1,5 +1,5 @@ class App.Overview extends App.Model - @configure 'Overview', 'name', 'prio', 'condition', 'order', 'group_by', 'view', 'user_ids', 'organization_shared', 'role_ids', 'order', 'group_by', 'active', 'updated_at' + @configure 'Overview', 'name', 'prio', 'condition', 'order', 'group_by', 'view', 'user_ids', 'organization_shared', 'role_ids', 'active' @extend Spine.Model.Ajax @url: @apiPath + '/overviews' @configure_attributes = [ diff --git a/app/controllers/overviews_controller.rb b/app/controllers/overviews_controller.rb index 65deb2d0f..ee7d835ca 100644 --- a/app/controllers/overviews_controller.rb +++ b/app/controllers/overviews_controller.rb @@ -30,7 +30,7 @@ Example: =begin Resource: -GET /api/v1/overviews.json +GET /api/v1/overviews Response: [ @@ -47,7 +47,7 @@ Response: ] Test: -curl http://localhost/api/v1/overviews.json -v -u #{login}:#{password} +curl http://localhost/api/v1/overviews -v -u #{login}:#{password} =end @@ -58,7 +58,7 @@ curl http://localhost/api/v1/overviews.json -v -u #{login}:#{password} =begin Resource: -GET /api/v1/overviews/#{id}.json +GET /api/v1/overviews/#{id} Response: { @@ -68,7 +68,7 @@ Response: } Test: -curl http://localhost/api/v1/overviews/#{id}.json -v -u #{login}:#{password} +curl http://localhost/api/v1/overviews/#{id} -v -u #{login}:#{password} =end @@ -79,7 +79,7 @@ curl http://localhost/api/v1/overviews/#{id}.json -v -u #{login}:#{password} =begin Resource: -POST /api/v1/overviews.json +POST /api/v1/overviews Payload: { @@ -101,7 +101,7 @@ Response: } Test: -curl http://localhost/api/v1/overviews.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X POST -d '{"name": "some_name","active": true, "note": "some note"}' +curl http://localhost/api/v1/overviews -v -u #{login}:#{password} -H "Content-Type: application/json" -X POST -d '{"name": "some_name","active": true, "note": "some note"}' =end @@ -112,7 +112,7 @@ curl http://localhost/api/v1/overviews.json -v -u #{login}:#{password} -H "Conte =begin Resource: -PUT /api/v1/overviews/{id}.json +PUT /api/v1/overviews/{id} Payload: { @@ -134,7 +134,7 @@ Response: } Test: -curl http://localhost/api/v1/overviews.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X PUT -d '{"name": "some_name","active": true, "note": "some note"}' +curl http://localhost/api/v1/overviews -v -u #{login}:#{password} -H "Content-Type: application/json" -X PUT -d '{"name": "some_name","active": true, "note": "some note"}' =end @@ -145,17 +145,55 @@ curl http://localhost/api/v1/overviews.json -v -u #{login}:#{password} -H "Conte =begin Resource: -DELETE /api/v1/overviews/{id}.json +DELETE /api/v1/overviews/{id} Response: {} Test: -curl http://localhost/api/v1/overviews/#{id}.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X DELETE +curl http://localhost/api/v1/overviews/#{id} -v -u #{login}:#{password} -H "Content-Type: application/json" -X DELETE =end def destroy model_destroy_render(Overview, params) end + +=begin + +Resource: +POST /api/v1/overviews_prio + +Payload: +{ + "prios": [ + [overview_id, prio], + [overview_id, prio], + [overview_id, prio], + [overview_id, prio], + [overview_id, prio] + ] +} + +Response: +{ + "success": true, +} + +Test: +curl http://localhost/api/v1/overviews_prio -v -u #{login}:#{password} -H "Content-Type: application/json" -X POST -d '{"prios": [ [1,1], [44,2] ]}' + +=end + + def prio + Overview.without_callback(:update, :before, :rearrangement) do + params[:prios].each do |overview_prio| + overview = Overview.find(overview_prio[0]) + next if overview.prio == overview_prio[1] + overview.prio = overview_prio[1] + overview.save! + end + end + render json: { success: true }, status: :ok + end end diff --git a/app/models/application_model/can_assets.rb b/app/models/application_model/can_assets.rb index 802e6afd4..42f2fee84 100644 --- a/app/models/application_model/can_assets.rb +++ b/app/models/application_model/can_assets.rb @@ -75,12 +75,12 @@ get assets and record_ids of selector attribute_ref_class = models[attribute_class][:reflections][reflection].klass if content['value'].instance_of?(Array) content['value'].each do |item_id| - attribute_object = attribute_ref_class.find_by(id: item_id) - if attribute_object - assets = attribute_object.assets(assets) - end + next if item_id.blank? + attribute_object = attribute_ref_class.lookup(id: item_id) + next if !attribute_object + assets = attribute_object.assets(assets) end - else + elsif content['value'].present? attribute_object = attribute_ref_class.find_by(id: content['value']) if attribute_object assets = attribute_object.assets(assets) @@ -138,11 +138,11 @@ get assets of object list require item['object'].to_filename record = Kernel.const_get(item['object']).find(item['o_id']) assets = record.assets(assets) - if item['created_by_id'] + if item['created_by_id'].present? user = User.find(item['created_by_id']) assets = user.assets(assets) end - if item['updated_by_id'] + if item['updated_by_id'].present? user = User.find(item['updated_by_id']) assets = user.assets(assets) end diff --git a/app/models/overview.rb b/app/models/overview.rb index c6e604323..3b7db3588 100644 --- a/app/models/overview.rb +++ b/app/models/overview.rb @@ -17,26 +17,47 @@ class Overview < ApplicationModel validates :name, presence: true before_create :fill_link_on_create, :fill_prio - before_update :fill_link_on_update + before_update :fill_link_on_update, :rearrangement private + def rearrangement + return true if !changes['prio'] + prio = 0 + Overview.all.order(prio: :asc, updated_at: :desc).pluck(:id).each do |overview_id| + prio += 1 + next if id == overview_id + Overview.without_callback(:update, :before, :rearrangement) do + overview = Overview.find(overview_id) + next if overview.prio == prio + overview.prio = prio + overview.save! + end + end + end + def fill_prio - return true if prio - self.prio = 9999 + return true if prio.present? + self.prio = Overview.count + 1 true end def fill_link_on_create - return true if link.present? - self.link = link_name(name) + self.link = if link.present? + link_name(link) + else + link_name(name) + end true end def fill_link_on_update - return true if !changes['name'] - return true if changes['link'] - self.link = link_name(name) + return true if !changes['name'] && !changes['link'] + self.link = if link.present? + link_name(link) + else + link_name(name) + end true end @@ -50,12 +71,16 @@ class Overview < ApplicationModel local_link = id || rand(999) end check = true + count = 0 + local_lookup_link = local_link while check - exists = Overview.find_by(link: local_link) - if exists&.id != id - local_link = "#{local_link}_#{rand(999)}" + count += 1 + exists = Overview.find_by(link: local_lookup_link) + if exists && exists.id != id # rubocop:disable Style/SafeNavigation + local_lookup_link = "#{local_link}_#{count}" else check = false + local_link = local_lookup_link end end local_link diff --git a/app/models/overview/assets.rb b/app/models/overview/assets.rb index 15f252a66..bccb2440e 100644 --- a/app/models/overview/assets.rb +++ b/app/models/overview/assets.rb @@ -40,9 +40,7 @@ returns next if !user data = user.assets(data) end - data = assets_of_selector('condition', data) - end %w[created_by_id updated_by_id].each do |local_user_id| next if !self[ local_user_id ] diff --git a/config/routes/overview.rb b/config/routes/overview.rb index 8b017738d..14038be5a 100644 --- a/config/routes/overview.rb +++ b/config/routes/overview.rb @@ -7,5 +7,6 @@ Zammad::Application.routes.draw do match api_path + '/overviews', to: 'overviews#create', via: :post match api_path + '/overviews/:id', to: 'overviews#update', via: :put match api_path + '/overviews/:id', to: 'overviews#destroy', via: :delete + match api_path + '/overviews_prio', to: 'overviews#prio', via: :post end diff --git a/lib/fill_db.rb b/lib/fill_db.rb index 1ceabdd4f..a02bd298f 100644 --- a/lib/fill_db.rb +++ b/lib/fill_db.rb @@ -10,7 +10,8 @@ fill your database with demo records customers: 1000, groups: 20, organizations: 40, - tickets: 100 + overviews: 5, + tickets: 100, ) or if you only want to create 100 tickets @@ -25,6 +26,7 @@ or if you only want to create 100 tickets customers = params[:customers] || 0 groups = params[:groups] || 0 organizations = params[:organizations] || 0 + overviews = params[:overviews] || 0 tickets = params[:tickets] || 0 puts 'load db with:' @@ -32,6 +34,7 @@ or if you only want to create 100 tickets puts " customers:#{customers}" puts " groups:#{groups}" puts " organizations:#{organizations}" + puts " overviews:#{overviews}" puts " tickets:#{tickets}" # set current user @@ -39,7 +42,7 @@ or if you only want to create 100 tickets # organizations organization_pool = [] - if organizations && !organizations.zero? + if !organizations.zero? (1..organizations).each do ActiveRecord::Base.transaction do organization = Organization.create!(name: "FillOrganization::#{rand(999_999)}", active: true) @@ -53,7 +56,7 @@ or if you only want to create 100 tickets # create agents agent_pool = [] - if agents && !agents.zero? + if !agents.zero? roles = Role.where(name: [ 'Agent']) groups_all = Group.all @@ -81,7 +84,7 @@ or if you only want to create 100 tickets # create customer customer_pool = [] - if customers && !customers.zero? + if !customers.zero? roles = Role.where(name: [ 'Customer']) groups_all = Group.all @@ -113,7 +116,7 @@ or if you only want to create 100 tickets # create groups group_pool = [] - if groups && !groups.zero? + if !groups.zero? (1..groups).each do ActiveRecord::Base.transaction do @@ -133,6 +136,35 @@ or if you only want to create 100 tickets puts " take #{group_pool.length} groups" end + # create overviews + if !overviews.zero? + (1..overviews).each do + ActiveRecord::Base.transaction do + overview = Overview.create!( + name: "Filloverview::#{rand(999_999)}", + role_ids: [Role.find_by(name: 'Agent').id], + condition: { + 'ticket.state_id' => { + operator: 'is', + value: Ticket::State.by_category(:work_on_all).pluck(:id), + }, + }, + order: { + by: 'created_at', + direction: 'ASC', + }, + view: { + d: %w[title customer group state owner created_at], + s: %w[title customer group state owner created_at], + m: %w[number title customer group state owner created_at], + view_mode_default: 's', + }, + active: true + ) + end + end + end + # create tickets priority_pool = Ticket::Priority.all state_pool = Ticket::State.all diff --git a/test/controllers/overviews_controller_test.rb b/test/controllers/overviews_controller_test.rb new file mode 100644 index 000000000..04cf81b1b --- /dev/null +++ b/test/controllers/overviews_controller_test.rb @@ -0,0 +1,179 @@ + +require 'test_helper' + +class OverviewsControllerTest < 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: 'tickets-admin', + firstname: 'Tickets', + lastname: 'Admin', + email: 'tickets-admin@example.com', + password: 'adminpw', + active: true, + roles: roles, + groups: groups, + ) + + # create agent + roles = Role.where(name: 'Agent') + @agent = User.create_or_update( + login: 'tickets-agent@example.com', + firstname: 'Tickets', + lastname: 'Agent', + email: 'tickets-agent@example.com', + password: 'agentpw', + active: true, + roles: roles, + groups: Group.all, + ) + + end + + test 'no permissions' do + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('tickets-agent', 'agentpw') + + params = { + name: 'Overview2', + link: 'my_overview', + condition: { + 'ticket.state_id' => { + operator: 'is', + value: [1, 2, 3], + }, + }, + order: { + by: 'created_at', + direction: 'DESC', + }, + view: { + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], + view_mode_default: 's', + }, + } + + post '/api/v1/overviews', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(401) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal('authentication failed', result['error']) + end + + test 'create overviews' do + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('tickets-admin', 'adminpw') + + params = { + name: 'Overview2', + link: 'my_overview', + condition: { + 'ticket.state_id' => { + operator: 'is', + value: [1, 2, 3], + }, + }, + order: { + by: 'created_at', + direction: 'DESC', + }, + view: { + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], + view_mode_default: 's', + }, + } + + post '/api/v1/overviews', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(201) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal('Overview2', result['name']) + assert_equal('my_overview', result['link']) + + post '/api/v1/overviews', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(201) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal('Overview2', result['name']) + assert_equal('my_overview_1', result['link']) + end + + test 'set mass prio' do + overview1 = Overview.create!( + name: 'Overview1', + link: 'my_overview', + condition: { + 'ticket.state_id' => { + operator: 'is', + value: [1, 2, 3], + }, + }, + order: { + by: 'created_at', + direction: 'DESC', + }, + view: { + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], + view_mode_default: 's', + }, + prio: 1, + updated_by_id: 1, + created_by_id: 1, + ) + overview2 = Overview.create!( + name: 'Overview2', + link: 'my_overview', + condition: { + 'ticket.state_id' => { + operator: 'is', + value: [1, 2, 3], + }, + }, + order: { + by: 'created_at', + direction: 'DESC', + }, + view: { + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], + view_mode_default: 's', + }, + prio: 2, + updated_by_id: 1, + created_by_id: 1, + ) + + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('tickets-admin', 'adminpw') + params = { + prios: [ + [overview2.id, 1], + [overview1.id, 2], + ] + } + post '/api/v1/overviews_prio', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal(true, result['success']) + + overview1.reload + overview2.reload + + assert_equal(2, overview1.prio) + assert_equal(1, overview2.prio) + end + +end diff --git a/test/unit/overview_test.rb b/test/unit/overview_test.rb index a70b2d381..01192ff56 100644 --- a/test/unit/overview_test.rb +++ b/test/unit/overview_test.rb @@ -28,7 +28,7 @@ class OverviewTest < ActiveSupport::TestCase overview.destroy! overview = Overview.create!( - name: 'My assigned Tickets', + name: 'My assigned Tickets 2', condition: { 'ticket.state_id' => { operator: 'is', @@ -46,7 +46,7 @@ class OverviewTest < ActiveSupport::TestCase view_mode_default: 's', }, ) - assert_equal(overview.link, 'my_assigned_tickets') + assert_equal(overview.link, 'my_assigned_tickets_2') overview.destroy! overview = Overview.create!( @@ -210,6 +210,158 @@ class OverviewTest < ActiveSupport::TestCase assert_equal(overview.link, 'my_overview2') overview.destroy! + end + test 'same url' do + UserInfo.current_user_id = 1 + + overview1 = Overview.create!( + name: 'My own assigned Tickets', + condition: { + 'ticket.state_id' => { + operator: 'is', + value: [1, 2, 3], + }, + }, + order: { + by: 'created_at', + direction: 'DESC', + }, + view: { + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], + view_mode_default: 's', + }, + ) + assert_equal(overview1.link, 'my_own_assigned_tickets') + + overview2 = Overview.create!( + name: 'My own assigned Tickets', + condition: { + 'ticket.state_id' => { + operator: 'is', + value: [1, 2, 3], + }, + }, + order: { + by: 'created_at', + direction: 'DESC', + }, + view: { + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], + view_mode_default: 's', + }, + ) + assert_equal(overview2.link, 'my_own_assigned_tickets_1') + + overview3 = Overview.create!( + name: 'My own assigned Tickets', + condition: { + 'ticket.state_id' => { + operator: 'is', + value: [1, 2, 3], + }, + }, + order: { + by: 'created_at', + direction: 'DESC', + }, + view: { + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], + view_mode_default: 's', + }, + ) + assert_equal(overview3.link, 'my_own_assigned_tickets_2') + + overview1.destroy! + overview2.destroy! + overview3.destroy! + end + + test 'priority rearrangement' do + UserInfo.current_user_id = 1 + + overview1 = Overview.create!( + name: 'Overview1', + link: 'my_overview', + condition: { + 'ticket.state_id' => { + operator: 'is', + value: [1, 2, 3], + }, + }, + order: { + by: 'created_at', + direction: 'DESC', + }, + view: { + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], + view_mode_default: 's', + }, + prio: 1, + ) + + overview2 = Overview.create!( + name: 'Overview2', + link: 'my_overview', + condition: { + 'ticket.state_id' => { + operator: 'is', + value: [1, 2, 3], + }, + }, + order: { + by: 'created_at', + direction: 'DESC', + }, + view: { + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], + view_mode_default: 's', + }, + prio: 2, + ) + + overview3 = Overview.create!( + name: 'Overview3', + link: 'my_overview', + condition: { + 'ticket.state_id' => { + operator: 'is', + value: [1, 2, 3], + }, + }, + order: { + by: 'created_at', + direction: 'DESC', + }, + view: { + d: %w[title customer state created_at], + s: %w[number title customer state created_at], + m: %w[number title customer state created_at], + view_mode_default: 's', + }, + prio: 3, + ) + + overview2.prio = 3 + overview2.save! + + overviews = Overview.all.order(prio: :asc).pluck(:id) + assert_equal(overview1.id, overviews[0]) + assert_equal(overview3.id, overviews[1]) + assert_equal(overview2.id, overviews[2]) + + overview1.destroy! + overview2.destroy! + overview3.destroy! end end From d3062592ea052578372648fca779d6ea0fdc257c Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Thu, 30 Nov 2017 09:29:18 +0100 Subject: [PATCH 027/196] Fixed issue #1513 - TimeAccounting ticket condition prevents submit of Zoom. --- .../javascripts/app/models/ticket.coffee | 20 ++ public/assets/tests/ticket_selector.js | 220 +++++++++++++++--- 2 files changed, 203 insertions(+), 37 deletions(-) diff --git a/app/assets/javascripts/app/models/ticket.coffee b/app/assets/javascripts/app/models/ticket.coffee index ac9d93df1..a197bba69 100644 --- a/app/assets/javascripts/app/models/ticket.coffee +++ b/app/assets/javascripts/app/models/ticket.coffee @@ -200,6 +200,26 @@ class App.Ticket extends App.Model result = true if objectValue.toString().match(contains_regex) else if condition.operator == 'contains not' result = true if !objectValue.toString().match(contains_regex) + else if condition.operator == 'contains all' + result = true + for loopConditionValue in conditionValue + if !_.contains(objectValue, loopConditionValue) + result = false + else if condition.operator == 'contains one' + result = false + for loopConditionValue in conditionValue + if _.contains(objectValue, loopConditionValue) + result = true + else if condition.operator == 'contains all not' + result = true + for loopObjectValue in objectValue + if _.contains(conditionValue, loopObjectValue) + result = false + else if condition.operator == 'contains one not' + result = false + for loopObjectValue in objectValue + if !_.contains(conditionValue, loopObjectValue) + result = true else if condition.operator == 'is' result = true if objectValue.toString().trim().toLowerCase() is loopConditionValue.toString().trim().toLowerCase() else if condition.operator == 'is not' diff --git a/public/assets/tests/ticket_selector.js b/public/assets/tests/ticket_selector.js index e9783afba..9254ce4ad 100644 --- a/public/assets/tests/ticket_selector.js +++ b/public/assets/tests/ticket_selector.js @@ -130,6 +130,7 @@ window.onload = function() { "vip": false, "id": 434 }, + "tags": ["tag a", "tag b"], "escalation_at": "2017-02-09T09:16:56.192Z", "last_contact_agent_at": "2017-02-09T09:16:56.192Z", "last_contact_agent_at": "2017-02-09T09:16:56.192Z", @@ -191,7 +192,7 @@ window.onload = function() { }, } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, true, result); setting = { @@ -202,7 +203,7 @@ window.onload = function() { }, } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, false, result); }; @@ -215,7 +216,7 @@ window.onload = function() { }, } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, true, result); setting = { @@ -226,7 +227,7 @@ window.onload = function() { }, } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, false, result); }; @@ -243,7 +244,7 @@ window.onload = function() { }, } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, true, result); setting = { @@ -256,7 +257,7 @@ window.onload = function() { }, } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, false, result); setting = { @@ -270,7 +271,7 @@ window.onload = function() { } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, true, result); setting = { @@ -283,7 +284,7 @@ window.onload = function() { } } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, false, result); setting = { @@ -296,7 +297,7 @@ window.onload = function() { } } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, false, result); setting = { @@ -307,7 +308,7 @@ window.onload = function() { } } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, false, result); setting = { @@ -318,7 +319,7 @@ window.onload = function() { } } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, true, result); }; @@ -335,7 +336,7 @@ window.onload = function() { }, } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, true, result); setting = { @@ -348,7 +349,7 @@ window.onload = function() { }, } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, false, result); setting = { @@ -362,7 +363,7 @@ window.onload = function() { } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, true, result); setting = { @@ -375,7 +376,7 @@ window.onload = function() { } } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, false, result); setting = { @@ -388,7 +389,7 @@ window.onload = function() { } } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, false, result); setting = { @@ -399,7 +400,7 @@ window.onload = function() { } } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, false, result); setting = { @@ -410,10 +411,148 @@ window.onload = function() { } } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, true, result); }; + var testPreConditionTags = function (key, ticket) { + App.Session.set(sessionData); + + setting = { + "condition": { + "ticket.tags": { + "operator": "contains one", + "value": "tag a", + }, + } + }; + result = App.Ticket.selector(ticket, setting['condition']); + equal(result, true, result); + + setting = { + "condition": { + "ticket.tags": { + "operator": "contains one", + "value": "tag aa", + }, + } + }; + result = App.Ticket.selector(ticket, setting['condition']); + equal(result, false, result); + + setting = { + "condition": { + "ticket.tags": { + "operator": "contains all", + "value": "tag a", + }, + } + }; + result = App.Ticket.selector(ticket, setting['condition']); + equal(result, true, result); + + setting = { + "condition": { + "ticket.tags": { + "operator": "contains all", + "value": ["tag a", "not existing"], + }, + } + }; + result = App.Ticket.selector(ticket, setting['condition']); + equal(result, false, result); + + setting = { + "condition": { + "ticket.tags": { + "operator": "contains all not", + "value": "tag a", + }, + } + }; + result = App.Ticket.selector(ticket, setting['condition']); + equal(result, false, result); + + setting = { + "condition": { + "ticket.tags": { + "operator": "contains all not", + "value": ["tag a", "tag b"], + }, + } + }; + result = App.Ticket.selector(ticket, setting['condition']); + equal(result, false, result); + + setting = { + "condition": { + "ticket.tags": { + "operator": "contains all not", + "value": ["tag a", "tag b", "tag c"], + }, + } + }; + result = App.Ticket.selector(ticket, setting['condition']); + equal(result, false, result); + + setting = { + "condition": { + "ticket.tags": { + "operator": "contains all not", + "value": ["tag c", "tag d"], + }, + } + }; + result = App.Ticket.selector(ticket, setting['condition']); + equal(result, true, result); + + setting = { + "condition": { + "ticket.tags": { + "operator": "contains one not", + "value": "tag a", + }, + } + }; + + result = App.Ticket.selector(ticket, setting['condition']); + equal(result, true, result); + + setting = { + "condition": { + "ticket.tags": { + "operator": "contains one not", + "value": ["tag a", "tag b"], + }, + } + }; + result = App.Ticket.selector(ticket, setting['condition']); + equal(result, false, result); + + setting = { + "condition": { + "ticket.tags": { + "operator": "contains one not", + "value": ["tag a", "tag c"], + }, + } + }; + result = App.Ticket.selector(ticket, setting['condition']); + equal(result, true, result); + + setting = { + "condition": { + "ticket.tags": { + "operator": "contains one not", + "value": ["tag c"], + }, + } + }; + result = App.Ticket.selector(ticket, setting['condition']); + equal(result, true, result); + + }; + var testTime = function (key, value, ticket) { valueDate = new Date(value); compareDate = new Date( valueDate.setHours( valueDate.getHours() - 1 ) ).toISOString(); @@ -425,7 +564,7 @@ window.onload = function() { }, } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, true, result); valueDate = new Date(value); @@ -438,7 +577,7 @@ window.onload = function() { }, } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, false, result); valueDate = new Date(value); @@ -451,7 +590,7 @@ window.onload = function() { }, } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, false, result); valueDate = new Date(value); @@ -464,7 +603,7 @@ window.onload = function() { }, } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, true, result); valueDate = new Date(value); @@ -478,7 +617,7 @@ window.onload = function() { }, } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, true, result); }; @@ -492,7 +631,7 @@ window.onload = function() { }, } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, expectedResult, result); }; @@ -506,7 +645,7 @@ window.onload = function() { }, } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, expectedResult, result); }; @@ -520,7 +659,7 @@ window.onload = function() { }, } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, expectedResult, result); }; @@ -534,7 +673,7 @@ window.onload = function() { }, } }; - result = App.Ticket.selector(ticket, setting['condition'] ); + result = App.Ticket.selector(ticket, setting['condition']); equal(result, expectedResult, result); }; @@ -612,7 +751,7 @@ window.onload = function() { testTimeBeforeRelative('ticket.pending_time', 1, 'hour', false, ticket); compareDate = new Date(); - compareDate.setTime( compareDate.getTime() - 60 * 60 * 2 * 1000 ); + compareDate.setTime( compareDate.getTime() - 60 * 60 * 2 * 1000); ticket.pending_time = compareDate.toISOString(); testTimeBeforeRelative('ticket.pending_time', 1, 'hour', true, ticket); @@ -621,7 +760,7 @@ window.onload = function() { testTimeBeforeRelative('ticket.pending_time', 1, 'day', false, ticket); compareDate = new Date(); - compareDate.setTime( compareDate.getTime() - 60 * 60 * 48 * 1000 ); + compareDate.setTime( compareDate.getTime() - 60 * 60 * 48 * 1000); ticket.pending_time = compareDate.toISOString(); testTimeBeforeRelative('ticket.pending_time', 1, 'day', true, ticket); @@ -630,7 +769,7 @@ window.onload = function() { testTimeBeforeRelative('ticket.pending_time', 1, 'year', false, ticket); compareDate = new Date(); - compareDate.setTime( compareDate.getTime() - 60 * 60 * 365 * 2 * 1000 ); + compareDate.setTime( compareDate.getTime() - 60 * 60 * 365 * 2 * 1000); ticket.pending_time = compareDate.toISOString(); testTimeBeforeRelative('ticket.pending_time', 1, 'year', true, ticket); @@ -643,7 +782,7 @@ window.onload = function() { testTimeAfterRelative('ticket.pending_time', 1, 'hour', false, ticket); compareDate = new Date(); - compareDate.setTime( compareDate.getTime() + 60 * 60 * 2 * 1000 ); + compareDate.setTime( compareDate.getTime() + 60 * 60 * 2 * 1000); ticket.pending_time = compareDate.toISOString(); testTimeAfterRelative('ticket.pending_time', 1, 'hour', true, ticket); @@ -652,7 +791,7 @@ window.onload = function() { testTimeAfterRelative('ticket.pending_time', 1, 'day', false, ticket); compareDate = new Date(); - compareDate.setTime( compareDate.getTime() + 60 * 60 * 48 * 1000 ); + compareDate.setTime( compareDate.getTime() + 60 * 60 * 48 * 1000); ticket.pending_time = compareDate.toISOString(); testTimeAfterRelative('ticket.pending_time', 1, 'day', true, ticket); @@ -661,7 +800,7 @@ window.onload = function() { testTimeAfterRelative('ticket.pending_time', 1, 'year', false, ticket); compareDate = new Date(); - compareDate.setTime( compareDate.getTime() + 60 * 60 * 365 * 2 * 1000 ); + compareDate.setTime( compareDate.getTime() + 60 * 60 * 365 * 2 * 1000); ticket.pending_time = compareDate.toISOString(); testTimeAfterRelative('ticket.pending_time', 1, 'year', true, ticket); @@ -672,12 +811,12 @@ window.onload = function() { // hour compareDate = new Date(); - compareDate.setTime( compareDate.getTime() - 60 * 60 * 0.5 * 1000 ); + compareDate.setTime( compareDate.getTime() - 60 * 60 * 0.5 * 1000); ticket.pending_time = compareDate.toISOString(); testTimeWithinLastRelative('ticket.pending_time', 1, 'hour', true, ticket); compareDate = new Date(); - compareDate.setTime( compareDate.getTime() - 60 * 60 * 2 * 1000 ); + compareDate.setTime( compareDate.getTime() - 60 * 60 * 2 * 1000); ticket.pending_time = compareDate.toISOString(); testTimeWithinLastRelative('ticket.pending_time', 1, 'hour', false, ticket); @@ -687,12 +826,12 @@ window.onload = function() { // hour compareDate = new Date(); - compareDate.setTime( compareDate.getTime() + 60 * 60 * 0.5 * 1000 ); + compareDate.setTime( compareDate.getTime() + 60 * 60 * 0.5 * 1000); ticket.pending_time = compareDate.toISOString(); testTimeWithinNextRelative('ticket.pending_time', 1, 'hour', true, ticket); compareDate = new Date(); - compareDate.setTime( compareDate.getTime() + 60 * 60 * 2 * 1000 ); + compareDate.setTime( compareDate.getTime() + 60 * 60 * 2 * 1000); ticket.pending_time = compareDate.toISOString(); testTimeWithinNextRelative('ticket.pending_time', 1, 'hour', false, ticket); }); @@ -778,6 +917,13 @@ window.onload = function() { testPreConditionUser('ticket.updated_by_id', '6', ticket, sessionData); }); + test("ticket tags", function() { + ticket = new App.Ticket(); + ticket.load(ticketData); + + testPreConditionTags('ticket.tags', ticket); + }); + test("article from", function() { ticket = new App.Ticket(); ticket.load(ticketData); From 8b3b90b9d069e73cbaa7e570bb9bf2cbf10c6df1 Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Thu, 30 Nov 2017 10:11:43 +0100 Subject: [PATCH 028/196] Improved Sequencer logging of removed unneeded attributes to show inspect dump instead of .to_s --- lib/sequencer/state.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sequencer/state.rb b/lib/sequencer/state.rb index 39a7107a6..8498744fb 100644 --- a/lib/sequencer/state.rb +++ b/lib/sequencer/state.rb @@ -262,7 +262,7 @@ class Sequencer remove = !attribute.will_be_used? remove ||= attribute.to <= @index if remove && attribute.will_be_used? - logger.debug("Removing unneeded attribute '#{identifier}': #{@values[identifier]}") + logger.debug("Removing unneeded attribute '#{identifier}': #{@values[identifier].inspect}") end remove end From eebe7ccdc4eabb7f90fdfde02c67959485d4fa84 Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Thu, 30 Nov 2017 10:12:28 +0100 Subject: [PATCH 029/196] Fixed bug: Exchange sync does not show current import state statistic. --- .../unit/import/common/import_job/statistics/store.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/sequencer/unit/import/common/import_job/statistics/store.rb b/lib/sequencer/unit/import/common/import_job/statistics/store.rb index 33aa8a9e8..78207a631 100644 --- a/lib/sequencer/unit/import/common/import_job/statistics/store.rb +++ b/lib/sequencer/unit/import/common/import_job/statistics/store.rb @@ -23,8 +23,12 @@ class Sequencer def store? return true if import_job.updated_at.blank? + next_update_at < Time.zone.now + end + + def next_update_at # update every 10 seconds to reduce DB load - import_job.updated_at > Time.zone.now + 10.seconds + import_job.updated_at + 10.seconds end end end From b61e98206b3a940bf06897f8f3523e20ecf736ae Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Thu, 30 Nov 2017 11:12:58 +0100 Subject: [PATCH 030/196] Fixed issue #1571 - Improve i-doit filtering (without type). --- .../javascripts/app/controllers/idoit_object_selector.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/app/controllers/idoit_object_selector.coffee b/app/assets/javascripts/app/controllers/idoit_object_selector.coffee index d2b7f2065..8c108c358 100644 --- a/app/assets/javascripts/app/controllers/idoit_object_selector.coffee +++ b/app/assets/javascripts/app/controllers/idoit_object_selector.coffee @@ -44,6 +44,8 @@ class App.IdoitObjectSelector extends App.ControllerModal '' search: (filter) => + if _.isEmpty(filter.type) + delete filter.type if _.isEmpty(filter.title) delete filter.title else From adc2564fa6cb1255ee5f175c1bde5a842cf256ff Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Thu, 30 Nov 2017 11:33:47 +0100 Subject: [PATCH 031/196] Removed debug log. --- .../app/controllers/_application_controller_generic.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/app/controllers/_application_controller_generic.coffee b/app/assets/javascripts/app/controllers/_application_controller_generic.coffee index 7fda56473..00ae69725 100644 --- a/app/assets/javascripts/app/controllers/_application_controller_generic.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller_generic.coffee @@ -1175,7 +1175,6 @@ class App.ObserverController extends App.Controller if @globalRerender @bind('ui:rerender', => @lastAttributres = undefined - console.log('aaaa', @model, @template) @maybeRender(App[@model].fullLocal(@object_id)) ) From 88da94e80d6695deb7be4dfe4209ebcb38982259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bauer?= Date: Sun, 3 Dec 2017 14:33:31 +0100 Subject: [PATCH 032/196] changed servername in nginx config --- contrib/nginx/zammad.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/nginx/zammad.conf b/contrib/nginx/zammad.conf index c039880a8..c635dba6d 100644 --- a/contrib/nginx/zammad.conf +++ b/contrib/nginx/zammad.conf @@ -2,7 +2,7 @@ # this is the nginx config for zammad # -upstream zammad { +upstream zammad-railsserver { server localhost:3000; } @@ -44,7 +44,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 300; - proxy_pass http://zammad; + proxy_pass http://zammad-railsserver; gzip on; gzip_types text/plain text/xml text/css image/svg+xml application/javascript application/x-javascript application/json application/xml; From f62225dbdecdd8169b17fb19caa2f998b5993527 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Sun, 3 Dec 2017 17:32:33 +0100 Subject: [PATCH 033/196] Improved white listing for mail probing. --- lib/email_helper/probe.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/email_helper/probe.rb b/lib/email_helper/probe.rb index a62ca50cb..bf308fb59 100644 --- a/lib/email_helper/probe.rb +++ b/lib/email_helper/probe.rb @@ -336,6 +336,7 @@ returns on fail if !subject white_map = { 'Recipient address rejected' => true, + 'Sender address rejected: Domain not found' => true, } white_map.each do |key, _message| From 5ba83da6f535810213efc77d094b2bdf3c6bf13a Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Mon, 4 Dec 2017 01:24:58 +0100 Subject: [PATCH 034/196] Fixed issue #599 - Elasticsearchs mapper-attachments plugin has been deprecated, use ingest-attachment now. --- app/models/ticket/search_index.rb | 8 +- .../20171203000001_setting_es_pipeline.rb | 16 +++ db/seeds/settings.rb | 9 ++ lib/search_index_backend.rb | 124 ++++++++++++++++-- lib/tasks/search_index_es.rake | 92 ++++++++++--- test/integration/elasticsearch_test.rb | 16 +-- 6 files changed, 223 insertions(+), 42 deletions(-) create mode 100644 db/migrate/20171203000001_setting_es_pipeline.rb diff --git a/app/models/ticket/search_index.rb b/app/models/ticket/search_index.rb index dbd0ecf8a..3a2e6bcaf 100644 --- a/app/models/ticket/search_index.rb +++ b/app/models/ticket/search_index.rb @@ -40,7 +40,7 @@ returns article_attributes = article.search_index_attribute_lookup # remove note needed attributes - ignore = %w(message_id_md5 ticket) + ignore = %w[message_id_md5 ticket] ignore.each do |attribute| article_attributes.delete(attribute) end @@ -51,10 +51,8 @@ returns end # lookup attachments + article_attributes['attachment'] = [] article.attachments.each do |attachment| - if !article_attributes['attachment'] - article_attributes['attachment'] = [] - end # check file size next if !attachment.content @@ -70,7 +68,7 @@ returns data = { '_name' => attachment.filename, - '_content' => Base64.encode64(attachment.content) + '_content' => Base64.encode64(attachment.content).delete("\n") } article_attributes['attachment'].push data end diff --git a/db/migrate/20171203000001_setting_es_pipeline.rb b/db/migrate/20171203000001_setting_es_pipeline.rb new file mode 100644 index 000000000..bf2af922c --- /dev/null +++ b/db/migrate/20171203000001_setting_es_pipeline.rb @@ -0,0 +1,16 @@ +class SettingEsPipeline < ActiveRecord::Migration[5.1] + def up + + # return if it's a new setup + return if !Setting.find_by(name: 'system_init_done') + Setting.create_if_not_exists( + title: 'Elasticsearch Pipeline Name', + name: 'es_pipeline', + area: 'SearchIndex::Elasticsearch', + description: 'Define pipeline name for Elasticsearch.', + state: '', + preferences: { online_service_disable: true }, + frontend: false + ) + end +end \ No newline at end of file diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb index decad0c72..d908f8f16 100644 --- a/db/seeds/settings.rb +++ b/db/seeds/settings.rb @@ -2286,6 +2286,15 @@ Setting.create_if_not_exists( preferences: { online_service_disable: true }, frontend: false ) +Setting.create_if_not_exists( + title: 'Elasticsearch Pipeline Name', + name: 'es_pipeline', + area: 'SearchIndex::Elasticsearch', + description: 'Define pipeline name for Elasticsearch.', + state: '', + preferences: { online_service_disable: true }, + frontend: false +) Setting.create_if_not_exists( title: 'Import Mode', diff --git a/lib/search_index_backend.rb b/lib/search_index_backend.rb index 893a5e920..3717fa273 100644 --- a/lib/search_index_backend.rb +++ b/lib/search_index_backend.rb @@ -4,6 +4,102 @@ class SearchIndexBackend =begin +info about used search index machine + + SearchIndexBackend.info + +=end + + def self.info + url = Setting.get('es_url').to_s + Rails.logger.info "# curl -X GET \"#{url}\"" + response = UserAgent.get( + url, + {}, + { + json: true, + open_timeout: 8, + read_timeout: 12, + user: Setting.get('es_user'), + password: Setting.get('es_password'), + } + ) + Rails.logger.info "# #{response.code}" + raise "Unable to process GET at #{url}\n#{response.inspect}" if !response.success? + response.data + end + +=begin + +update processors + + SearchIndexBackend.processors( + _ingest/pipeline/attachment: { + description: 'Extract attachment information from arrays', + processors: [ + { + foreach: { + field: 'ticket.articles.attachments', + processor: { + attachment: { + target_field: '_ingest._value.attachment', + field: '_ingest._value.data' + } + } + } + } + ] + } + ) + +=end + + def self.processors(data) + data.each do |key, items| + url = "#{Setting.get('es_url')}/#{key}" + + items.each do |item| + if item[:action] == 'delete' + #Rails.logger.info "# curl -X DELETE \"#{url}\"" + #response = UserAgent.delete( + # url, + # { + # json: true, + # open_timeout: 8, + # read_timeout: 12, + # user: Setting.get('es_user'), + # password: Setting.get('es_password'), + # } + #) + #Rails.logger.info "# #{response.code}" + #next if response.success? + #raise "Unable to process DELETE at #{url}\n#{response.inspect}" + next + end + Rails.logger.info "# curl -X PUT \"#{url}\" \\" + Rails.logger.debug "-d '#{data.to_json}'" + item.delete(:action) + response = UserAgent.put( + url, + item, + { + json: true, + open_timeout: 8, + read_timeout: 12, + user: Setting.get('es_user'), + password: Setting.get('es_password'), + } + ) + Rails.logger.info "# #{response.code}" + next if response.success? + raise "Unable to process PUT at #{url}\n#{response.inspect}" + end + end + true + end + +=begin + create/update/delete index SearchIndexBackend.index( @@ -51,8 +147,8 @@ create/update/delete index data[:data], { json: true, - open_timeout: 5, - read_timeout: 20, + open_timeout: 8, + read_timeout: 12, user: Setting.get('es_user'), password: Setting.get('es_password'), } @@ -83,8 +179,8 @@ add new object to search index data, { json: true, - open_timeout: 5, - read_timeout: 20, + open_timeout: 8, + read_timeout: 16, user: Setting.get('es_user'), password: Setting.get('es_password'), } @@ -113,8 +209,8 @@ remove whole data from index response = UserAgent.delete( url, { - open_timeout: 5, - read_timeout: 14, + open_timeout: 8, + read_timeout: 16, user: Setting.get('es_user'), password: Setting.get('es_password'), } @@ -166,7 +262,7 @@ return search result def self.search_by_index(query, limit = 10, index = nil, query_extention = {}) return [] if !query - url = build_url() + url = build_url return if !url url += if index if index.class == Array @@ -201,7 +297,7 @@ return search result # add * on simple query like "somephrase23" or "attribute: somephrase23" if query.present? query.strip! - if query =~ /^([[:alpha:],0-9]+|[[:alpha:],0-9]+\:\s+[[:alpha:],0-9]+)$/ + if query.match?(/^([[:alpha:],0-9]+|[[:alpha:],0-9]+\:\s+[[:alpha:],0-9]+)$/) query += '*' end end @@ -294,7 +390,7 @@ get count of tickets and tickets which match on selector def self.selectors(index = nil, selectors = nil, limit = 10, current_user = nil, aggs_interval = nil) raise 'no selectors given' if !selectors - url = build_url() + url = build_url return if !url url += if index if index.class == Array @@ -345,7 +441,7 @@ get count of tickets and tickets which match on selector def self.selector2query(selector, _current_user, aggs_interval, limit) query_must = [] query_must_not = [] - if selector && !selector.empty? + if selector.present? selector.each do |key, data| key_tmp = key.sub(/^.+?\./, '') t = {} @@ -439,10 +535,14 @@ return true if backend is configured index = "#{Setting.get('es_index')}_#{Rails.env}" url = Setting.get('es_url') url = if type + url_pipline = Setting.get('es_pipeline') + if url_pipline.present? + url_pipline = "?pipeline=#{url_pipline}" + end if o_id - "#{url}/#{index}/#{type}/#{o_id}" + "#{url}/#{index}/#{type}/#{o_id}#{url_pipline}" else - "#{url}/#{index}/#{type}" + "#{url}/#{index}/#{type}#{url_pipline}" end else "#{url}/#{index}" diff --git a/lib/tasks/search_index_es.rake b/lib/tasks/search_index_es.rake index c24416669..add30bf3a 100644 --- a/lib/tasks/search_index_es.rake +++ b/lib/tasks/search_index_es.rake @@ -14,29 +14,87 @@ namespace :searchindex do task :create, [:opts] => :environment do |_t, _args| - # create indexes - puts 'create indexes...' - SearchIndexBackend.index( - action: 'create', - data: { - mappings: { - Ticket: { - _source: { excludes: [ 'article.attachment' ] }, - properties: { - article: { - type: 'nested', - include_in_parent: true, - properties: { - attachment: { - type: 'attachment', + # es with mapper-attachments plugin + number = SearchIndexBackend.info['version']['number'].to_s + if number =~ /^[2-4]\./ || number =~ /^5\.[0-5]\./ + + # create indexes + puts 'create indexes...' + SearchIndexBackend.index( + action: 'create', + data: { + mappings: { + Ticket: { + _source: { excludes: [ 'article.attachment' ] }, + properties: { + article: { + type: 'nested', + include_in_parent: true, + properties: { + attachment: { + type: 'attachment', + } } } } } } } - } - ) + ) + Setting.set('es_pipeline', '') + + # es with ingest-attachment plugin + else + + # create indexes + puts 'create indexes...' + SearchIndexBackend.index( + action: 'create', + data: { + mappings: { + Ticket: { + _source: { excludes: [ 'article.attachment' ] }, + } + } + } + ) + + # update processors + pipeline = 'zammad-attachment' + Setting.set('es_pipeline', pipeline) + SearchIndexBackend.processors( + "_ingest/pipeline/#{pipeline}": [ + { + action: 'delete', + }, + { + action: 'create', + description: 'Extract zammad-attachment information from arrays', + processors: [ + { + foreach: { + field: 'article', + ignore_failure: true, + processor: { + foreach: { + field: '_ingest._value.attachment', + ignore_failure: true, + processor: { + attachment: { + target_field: '_ingest._value', + field: '_ingest._value._content', + ignore_failure: true, + } + } + } + } + } + } + ] + } + ] + ) + end end diff --git a/test/integration/elasticsearch_test.rb b/test/integration/elasticsearch_test.rb index 778c4100d..e17a8ba49 100644 --- a/test/integration/elasticsearch_test.rb +++ b/test/integration/elasticsearch_test.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 + require 'integration_test_helper' require 'rake' @@ -168,7 +168,7 @@ class ElasticsearchTest < ActiveSupport::TestCase Store.add( object: 'Ticket::Article', o_id: article1.id, - data: IO.binread("#{Rails.root}/test/fixtures/es-normal.txt"), + data: IO.binread(Rails.root.join('test', 'fixtures', 'es-normal.txt')), filename: 'es-normal.txt', preferences: {}, created_by_id: 1, @@ -194,9 +194,9 @@ class ElasticsearchTest < ActiveSupport::TestCase assert(attributes['article'][0]['attachment'][0]) assert_not(attributes['article'][0]['attachment'][1]) assert_equal('es-normal.txt', attributes['article'][0]['attachment'][0]['_name']) - assert_equal("c29tZSBub3JtYWwgdGV4dDY2Cg==\n", attributes['article'][0]['attachment'][0]['_content']) + assert_equal('c29tZSBub3JtYWwgdGV4dDY2Cg==', attributes['article'][0]['attachment'][0]['_content']) - ticket1.destroy + ticket1.destroy! # execute background jobs Scheduler.worker(true) @@ -229,7 +229,7 @@ class ElasticsearchTest < ActiveSupport::TestCase Store.add( object: 'Ticket::Article', o_id: article1.id, - data: IO.binread("#{Rails.root}/test/fixtures/es-normal.txt"), + data: IO.binread(Rails.root.join('test', 'fixtures', 'es-normal.txt')), filename: 'es-normal.txt', preferences: {}, created_by_id: 1, @@ -240,7 +240,7 @@ class ElasticsearchTest < ActiveSupport::TestCase Store.add( object: 'Ticket::Article', o_id: article1.id, - data: IO.binread("#{Rails.root}/test/fixtures/es-pdf1.pdf"), + data: IO.binread(Rails.root.join('test', 'fixtures', 'es-pdf1.pdf')), filename: 'es-pdf1.pdf', preferences: {}, created_by_id: 1, @@ -251,7 +251,7 @@ class ElasticsearchTest < ActiveSupport::TestCase Store.add( object: 'Ticket::Article', o_id: article1.id, - data: IO.binread("#{Rails.root}/test/fixtures/es-box1.box"), + data: IO.binread(Rails.root.join('test', 'fixtures', 'es-box1.box')), filename: 'mail1.box', preferences: {}, created_by_id: 1, @@ -262,7 +262,7 @@ class ElasticsearchTest < ActiveSupport::TestCase Store.add( object: 'Ticket::Article', o_id: article1.id, - data: IO.binread("#{Rails.root}/test/fixtures/es-too-big.txt"), + data: IO.binread(Rails.root.join('test', 'fixtures', 'es-too-big.txt')), filename: 'es-too-big.txt', preferences: {}, created_by_id: 1, From 68b85efc24e834f65ff56bea0a3644c44ee5fe0c Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Mon, 4 Dec 2017 01:34:27 +0100 Subject: [PATCH 035/196] Applied rubocop. --- db/migrate/20171203000001_setting_es_pipeline.rb | 2 +- lib/tasks/search_index_es.rake | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/db/migrate/20171203000001_setting_es_pipeline.rb b/db/migrate/20171203000001_setting_es_pipeline.rb index bf2af922c..2d7b9b667 100644 --- a/db/migrate/20171203000001_setting_es_pipeline.rb +++ b/db/migrate/20171203000001_setting_es_pipeline.rb @@ -13,4 +13,4 @@ class SettingEsPipeline < ActiveRecord::Migration[5.1] frontend: false ) end -end \ No newline at end of file +end diff --git a/lib/tasks/search_index_es.rake b/lib/tasks/search_index_es.rake index add30bf3a..e92ecd180 100644 --- a/lib/tasks/search_index_es.rake +++ b/lib/tasks/search_index_es.rake @@ -13,13 +13,13 @@ namespace :searchindex do end task :create, [:opts] => :environment do |_t, _args| + puts 'create indexes...' # es with mapper-attachments plugin number = SearchIndexBackend.info['version']['number'].to_s if number =~ /^[2-4]\./ || number =~ /^5\.[0-5]\./ # create indexes - puts 'create indexes...' SearchIndexBackend.index( action: 'create', data: { @@ -47,7 +47,6 @@ namespace :searchindex do else # create indexes - puts 'create indexes...' SearchIndexBackend.index( action: 'create', data: { From f3cbe7f10e0ee231de5aace3a46fe63b1dc9c0eb Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Mon, 4 Dec 2017 01:48:41 +0100 Subject: [PATCH 036/196] Added ability to delete elasticsearch processors first (for cleanup). --- lib/search_index_backend.rb | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/search_index_backend.rb b/lib/search_index_backend.rb index 2fdcdbd97..6547e491f 100644 --- a/lib/search_index_backend.rb +++ b/lib/search_index_backend.rb @@ -60,21 +60,20 @@ update processors items.each do |item| if item[:action] == 'delete' - #Rails.logger.info "# curl -X DELETE \"#{url}\"" - #response = UserAgent.delete( - # url, - # { - # json: true, - # open_timeout: 8, - # read_timeout: 12, - # user: Setting.get('es_user'), - # password: Setting.get('es_password'), - # } - #) - #Rails.logger.info "# #{response.code}" - #next if response.success? - #raise "Unable to process DELETE at #{url}\n#{response.inspect}" - next + Rails.logger.info "# curl -X DELETE \"#{url}\"" + response = UserAgent.delete( + url, + { + json: true, + open_timeout: 8, + read_timeout: 12, + user: Setting.get('es_user'), + password: Setting.get('es_password'), + } + ) + Rails.logger.info "# #{response.code}" + next if response.success? + raise "Unable to process DELETE at #{url}\n#{response.inspect}" end Rails.logger.info "# curl -X PUT \"#{url}\" \\" Rails.logger.debug "-d '#{data.to_json}'" From a08d5f1b519fe11bc2a1c26c96801c0b78e2739b Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Mon, 4 Dec 2017 02:12:55 +0100 Subject: [PATCH 037/196] Improved error handling. --- lib/search_index_backend.rb | 1 + lib/tasks/search_index_es.rake | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/search_index_backend.rb b/lib/search_index_backend.rb index 6547e491f..d909b8ed6 100644 --- a/lib/search_index_backend.rb +++ b/lib/search_index_backend.rb @@ -12,6 +12,7 @@ info about used search index machine def self.info url = Setting.get('es_url').to_s + return if url.blank? Rails.logger.info "# curl -X GET \"#{url}\"" response = UserAgent.get( url, diff --git a/lib/tasks/search_index_es.rake b/lib/tasks/search_index_es.rake index e92ecd180..7a68ab5c6 100644 --- a/lib/tasks/search_index_es.rake +++ b/lib/tasks/search_index_es.rake @@ -16,8 +16,12 @@ namespace :searchindex do puts 'create indexes...' # es with mapper-attachments plugin - number = SearchIndexBackend.info['version']['number'].to_s - if number =~ /^[2-4]\./ || number =~ /^5\.[0-5]\./ + info = SearchIndexBackend.info + number = nil + if info.present? + number = info['version']['number'].to_s + end + if number.blank? || number =~ /^[2-4]\./ || number =~ /^5\.[0-5]\./ # create indexes SearchIndexBackend.index( From 486802770b9cb70548cada0f40b958cc528a873c Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Mon, 4 Dec 2017 08:00:00 +0100 Subject: [PATCH 038/196] Added option to use multiple session instances. --- .../app/controllers/ticket_overview.coffee | 1 - lib/sessions.rb | 64 +++++-- lib/sessions/client.rb | 8 +- lib/sessions/node.rb | 169 ++++++++++++++++++ 4 files changed, 223 insertions(+), 19 deletions(-) create mode 100644 lib/sessions/node.rb diff --git a/app/assets/javascripts/app/controllers/ticket_overview.coffee b/app/assets/javascripts/app/controllers/ticket_overview.coffee index de9d4df17..40f9b7a7e 100644 --- a/app/assets/javascripts/app/controllers/ticket_overview.coffee +++ b/app/assets/javascripts/app/controllers/ticket_overview.coffee @@ -956,7 +956,6 @@ class Table extends App.Controller ticketListShow = [] for ticket in tickets ticketListShow.push App.Ticket.find(ticket.id) - console.log('overview', overview) @overview = App.Overview.find(overview.id) @table.update( overviewAttributes: @overview.view.s diff --git a/lib/sessions.rb b/lib/sessions.rb index 95149cd26..58f1dcf0b 100644 --- a/lib/sessions.rb +++ b/lib/sessions.rb @@ -109,8 +109,11 @@ returns =end def self.session_exists?(client_id) - client_ids = sessions - client_ids.include? client_id.to_s + session_dir = "#{@path}/#{client_id}" + return false if !File.exist?(session_dir) + session_file = "#{session_dir}/session" + return false if !File.exist?(session_file) + true end =begin @@ -247,14 +250,14 @@ returns data = nil # if no session dir exists, session got destoried - if !File.exist? session_dir + if !File.exist?(session_dir) destroy(client_id) - log('debug', "missing session directory for '#{client_id}', remove session.") + log('debug', "missing session directory #{session_dir} for '#{client_id}', remove session.") return end # if only session file is missing, then it's an error behavior - if !File.exist? session_file + if !File.exist?(session_file) destroy(client_id) log('error', "missing session file for '#{client_id}', remove session.") return @@ -558,16 +561,47 @@ remove all session and spool messages data end - def self.jobs + def self.jobs(node_id = nil) # just make sure that spool path exists if !File.exist?(@path) FileUtils.mkpath @path end + # dispatch sessions + if node_id && node_id.zero? + loop do + + # nodes + nodes_stats = Sessions::Node.stats + + client_ids = sessions + client_ids.each do |client_id| + + # ask nodes for nodes + next if nodes_stats[client_id] + + # assigne to node + Sessions::Node.session_assigne(client_id) + end + sleep 1 + end + end + Thread.abort_on_exception = true loop do - client_ids = sessions + + if node_id + + # register node + Sessions::Node.register(node_id) + + # watch for assigned sessions + client_ids = Sessions::Node.sessions_by(node_id) + else + client_ids = sessions + end + client_ids.each do |client_id| # connection already open, ignore @@ -586,7 +620,7 @@ remove all session and spool messages @@client_threads[client_id] = true @@client_threads[client_id] = Thread.new do - thread_client(client_id) + thread_client(client_id, 0, Time.now.utc, node_id) @@client_threads[client_id] = nil log('debug', "close client (#{client_id}) thread") if ActiveRecord::Base.connection.owner == Thread.current @@ -629,10 +663,10 @@ returns =end - def self.thread_client(client_id, try_count = 0, try_run_time = Time.now.utc) - log('debug', "LOOP #{client_id} - #{try_count}") + def self.thread_client(client_id, try_count = 0, try_run_time = Time.now.utc, node_id) + log('debug', "LOOP #{node_id}.#{client_id} - #{try_count}") begin - Sessions::Client.new(client_id) + Sessions::Client.new(client_id, node_id) rescue => e log('error', "thread_client #{client_id} exited with error #{e.inspect}") log('error', e.backtrace.join("\n ") ) @@ -654,13 +688,11 @@ returns # restart job again if try_run_max > try_count - thread_client(client_id, try_count, try_run_time) - return + thread_client(client_id, try_count, try_run_time, node_id) end - - raise "STOP thread_client for client #{client_id} after #{try_run_max} tries" + raise "STOP thread_client for client #{node_id}.#{client_id} after #{try_run_max} tries" end - log('debug', "/LOOP #{client_id} - #{try_count}") + log('debug', "/LOOP #{node_id}.#{client_id} - #{try_count}") end def self.symbolize_keys(hash) diff --git a/lib/sessions/client.rb b/lib/sessions/client.rb index 264b4a49f..7258e19c0 100644 --- a/lib/sessions/client.rb +++ b/lib/sessions/client.rb @@ -1,7 +1,8 @@ class Sessions::Client - def initialize(client_id) + def initialize(client_id, node_id) @client_id = client_id + @node_id = node_id log '---client start ws connection---' fetch log '---client exiting ws connection---' @@ -22,6 +23,9 @@ class Sessions::Client loop_count = 0 loop do + # check if session still exists + return if !Sessions.session_exists?(@client_id) + # get connection user session_data = Sessions.get(@client_id) return if !session_data @@ -71,6 +75,6 @@ class Sessions::Client end def log(msg) - Rails.logger.debug "client(#{@client_id}) #{msg}" + Rails.logger.debug "client(#{@node_id}.#{@client_id}) #{msg}" end end diff --git a/lib/sessions/node.rb b/lib/sessions/node.rb new file mode 100644 index 000000000..f023e95c1 --- /dev/null +++ b/lib/sessions/node.rb @@ -0,0 +1,169 @@ +module Sessions::Node + + # get application root directory + @root = Dir.pwd.to_s + if @root.blank? || @root == '/' + @root = Rails.root + end + + # get working directories + @path = "#{@root}/tmp/session_node_#{Rails.env}" + + def self.session_assigne(client_id, force = false) + + # get available nodes + nodes = Sessions::Node.registered + session_count = {} + + nodes.each do |node| + count = Sessions::Node.sessions_by(node['node_id'], force).count + session_count[node['node_id']] = count + end + + # search for lowest session count + node_id = nil + node_count = nil + session_count.each do |local_node_id, count| + next if !node_count.nil? && count > node_count + node_count = count + node_id = local_node_id + end + + # assigne session + Rails.logger.info "Assigne session to node #{node_id} (#{client_id})" + Sessions::Node.sessions_for(node_id, client_id) + + # write node status file + node_id + end + + def self.cleanup + FileUtils.rm_rf @path + end + + def self.registered + path = "#{@path}/*.status" + nodes = [] + files = Dir.glob(path) + files.each do |filename| + File.open(filename, 'rb') do |file| + file.flock(File::LOCK_SH) + content = file.read + file.flock(File::LOCK_UN) + begin + data = JSON.parse(content) + nodes.push data + rescue => e + Rails.logger.error "can't parse status file #{filename}, #{e.inspect}" + #to_delete.push "#{path}/#{entry}" + #next + end + end + end + nodes + end + + def self.register(node_id) + if !File.exist?(@path) + FileUtils.mkpath @path + end + + status_file = "#{@path}/#{node_id}.status" + + # write node status file + data = { + updated_at_human: Time.now.utc, + updated_at: Time.now.utc.to_i, + node_id: node_id.to_s, + pid: $PROCESS_ID, + } + content = data.to_json + + # store session data in session file + File.open(status_file, 'wb') do |file| + file.write content + end + + end + + def self.stats + # read node sessions + path = "#{@path}/*.session" + + sessions = {} + files = Dir.glob(path) + files.each do |filename| + File.open(filename, 'rb') do |file| + file.flock(File::LOCK_SH) + content = file.read + file.flock(File::LOCK_UN) + begin + next if content.blank? + data = JSON.parse(content) + next if data.blank? + next if data['client_id'].blank? + sessions[data['client_id']] = data['node_id'] + rescue => e + Rails.logger.error "can't parse session file #{filename}, #{e.inspect}" + #to_delete.push "#{path}/#{entry}" + #next + end + end + end + sessions + end + + def self.sessions_for(node_id, client_id) + if !File.exist?(@path) + FileUtils.mkpath @path + end + + status_file = "#{@path}/#{node_id}.#{client_id}.session" + + # write node status file + data = { + updated_at_human: Time.now.utc, + updated_at: Time.now.utc.to_i, + node_id: node_id.to_s, + client_id: client_id.to_s, + pid: $PROCESS_ID, + } + content = data.to_json + + # store session data in session file + File.open(status_file, 'wb') do |file| + file.write content + end + + end + + def self.sessions_by(node_id, force = false) + + # read node sessions + path = "#{@path}/#{node_id}.*.session" + + sessions = [] + files = Dir.glob(path) + files.each do |filename| + File.open(filename, 'rb') do |file| + file.flock(File::LOCK_SH) + content = file.read + file.flock(File::LOCK_UN) + begin + next if content.blank? + data = JSON.parse(content) + next if data.blank? + next if data['client_id'].blank? + next if !Sessions.session_exists?(data['client_id']) && force == false + sessions.push data['client_id'] + rescue => e + Rails.logger.error "can't parse session file #{filename}, #{e.inspect}" + #to_delete.push "#{path}/#{entry}" + #next + end + end + end + sessions + end + +end From 3bdb3dacb250d1926c2dd07ef2f6ee923d3b4971 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Mon, 4 Dec 2017 08:05:31 +0100 Subject: [PATCH 039/196] Applied rubocop. --- lib/sessions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sessions.rb b/lib/sessions.rb index 58f1dcf0b..f432ab7cd 100644 --- a/lib/sessions.rb +++ b/lib/sessions.rb @@ -569,7 +569,7 @@ remove all session and spool messages end # dispatch sessions - if node_id && node_id.zero? + if node_id&.zero? loop do # nodes From bb2537b49b0fa6eb1b5ff71225fc735e60f1914d Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Mon, 4 Dec 2017 09:10:49 +0100 Subject: [PATCH 040/196] Implemented issue #1689 - Enable state "merged" for admin overviews, triggers and jobs. --- .../_ui_element/select_search.coffee | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 app/assets/javascripts/app/controllers/_ui_element/select_search.coffee diff --git a/app/assets/javascripts/app/controllers/_ui_element/select_search.coffee b/app/assets/javascripts/app/controllers/_ui_element/select_search.coffee new file mode 100644 index 000000000..2b53dd6d1 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_ui_element/select_search.coffee @@ -0,0 +1,35 @@ +# coffeelint: disable=camel_case_classes +class App.UiElement.select_search extends App.UiElement.ApplicationUiElement + @render: (attribute, params) -> + + # set multiple option + if attribute.multiple + attribute.multiple = 'multiple' + else + attribute.multiple = '' + + delete attribute.filter + + # build options list based on config + @getConfigOptionList(attribute, params) + + # build options list based on relation + @getRelationOptionList(attribute, params) + + # add null selection if needed + @addNullOption(attribute, params) + + # sort attribute.options + @sortOptions(attribute, params) + + # finde selected/checked item of list + @selectedOptions(attribute, params) + + # disable item of list + @disabledOptions(attribute, params) + + # filter attributes + @filterOption(attribute, params) + + # return item + $( App.view('generic/select')(attribute: attribute) ) From 84ed5ee4511bcf5212c8a811f966d1afe63b9c4e Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Mon, 4 Dec 2017 15:14:56 +0100 Subject: [PATCH 041/196] Improved issue #1591 - Improved js snipped for form channel (use div, not button for non modal forms). --- .../app/controllers/_channel/form.coffee | 4 ++++ .../javascripts/app/views/channel/form.jst.eco | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/app/controllers/_channel/form.coffee b/app/assets/javascripts/app/controllers/_channel/form.coffee index 96cca32d3..2edaa3cdc 100644 --- a/app/assets/javascripts/app/controllers/_channel/form.coffee +++ b/app/assets/javascripts/app/controllers/_channel/form.coffee @@ -67,11 +67,15 @@ class App.ChannelForm extends App.ControllerSubContent # rebuild preview params.test = true if params.modal + @$('.js-modal').removeClass('hide') + @$('.js-inlineForm').addClass('hide') @$('.js-formInline').addClass('hide') @$('.js-formBtn').removeClass('hide') @$('.js-formBtn').ZammadForm(params) @$('.js-formBtn').text('Feedback') else + @$('.js-modal').addClass('hide') + @$('.js-inlineForm').removeClass('hide') @$('.js-formBtn').addClass('hide') @$('.js-formInline').removeClass('hide') @$('.js-formInline').ZammadForm(params) diff --git a/app/assets/javascripts/app/views/channel/form.jst.eco b/app/assets/javascripts/app/views/channel/form.jst.eco index 011c10a8c..40101bd29 100644 --- a/app/assets/javascripts/app/views/channel/form.jst.eco +++ b/app/assets/javascripts/app/views/channel/form.jst.eco @@ -132,7 +132,7 @@

                                  <%- @T('You need to add the following Javascript code snippet to your web page') %>:

                                  -
                                  <button id="feedback-form">Feedback</button>
                                  +  
                                  <button id="feedback-form">Feedback</button>
                                   
                                   <script id="zammad_form_script" src="<%= @baseurl %>/assets/form/form.js"></script>
                                   
                                  @@ -144,3 +144,16 @@ $(function() {
                                   });
                                   </script>
                                  + +
                                  <div id="feedback-form">form will be placed in here</div>
                                  +
                                  +<script id="zammad_form_script" src="<%= @baseurl %>/assets/form/form.js"></script>
                                  +
                                  +<script>
                                  +$(function() {
                                  +  $('#feedback-form').ZammadForm({
                                  +
                                  +  });
                                  +});
                                  +</script>
                                  + \ No newline at end of file From de55dbeb3330a655a46733b3054fbc3b0f6030cf Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Mon, 4 Dec 2017 15:20:56 +0100 Subject: [PATCH 042/196] =?UTF-8?q?Fixed=20issue=20#1604=20-=20Webform=20i?= =?UTF-8?q?sn=C2=B4t=20available=20|=20401=20Unauthorized.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/form_controller.rb | 2 +- test/controllers/form_controller_test.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/form_controller.rb b/app/controllers/form_controller.rb index 18ab83c70..bea472d77 100644 --- a/app/controllers/form_controller.rb +++ b/app/controllers/form_controller.rb @@ -238,7 +238,7 @@ class FormController < ApplicationController def enabled? return true if params[:test] && current_user && current_user.permissions?('admin.channel_formular') - return true if Setting.get('form_ticket_create') && Setting.get('customer_ticket_create') + return true if Setting.get('form_ticket_create') response_access_deny false end diff --git a/test/controllers/form_controller_test.rb b/test/controllers/form_controller_test.rb index 7c14edbd8..98ea10cee 100644 --- a/test/controllers/form_controller_test.rb +++ b/test/controllers/form_controller_test.rb @@ -245,8 +245,8 @@ class FormControllerTest < ActionDispatch::IntegrationTest end test '06 - customer_ticket_create false disables form' do - Setting.set('form_ticket_create', true) - Setting.set('customer_ticket_create', false) + Setting.set('form_ticket_create', false) + Setting.set('customer_ticket_create', true) fingerprint = SecureRandom.hex(40) From 75ac4252f3d868459923d09ebaede9d0041f2bfc Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Tue, 5 Dec 2017 15:49:59 +0100 Subject: [PATCH 043/196] Implemented issue #195 - Take over attachment on ticket split. --- .../controllers/_ui_element/richtext.coffee | 71 +++++++++---------- .../controllers/agent_ticket_create.coffee | 33 +++++---- .../app/controllers/layout_ref.coffee | 20 +++--- .../ticket_zoom/article_new.coffee | 67 +++++++++-------- .../app/views/generic/attachment_item.jst.eco | 6 +- app/controllers/ticket_articles_controller.rb | 13 ++-- app/controllers/tickets_controller.rb | 22 +++++- 7 files changed, 128 insertions(+), 104 deletions(-) diff --git a/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee b/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee index f3cba6f7b..387a35a8f 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee @@ -1,8 +1,7 @@ # coffeelint: disable=camel_case_classes class App.UiElement.richtext - @render: (attribute) -> - - item = $( App.view('generic/richtext')( attribute: attribute ) ) + @render: (attribute, params) -> + item = $( App.view('generic/richtext')(attribute: attribute) ) item.find('[contenteditable]').ce( mode: attribute.type maxlength: attribute.maxlength @@ -15,42 +14,42 @@ class App.UiElement.richtext new App[plugin.controller](params) if attribute.upload - item.append( $( App.view('generic/attachment')( attribute: attribute ) ) ) + @attachments = [] + item.append( $( App.view('generic/attachment')(attribute: attribute) ) ) - renderAttachment = (file) => - item.find('.attachments').append( App.view('generic/attachment_item')( - fileName: file.filename - fileSize: App.Utils.humanFileSize(file.size) - store_id: file.store_id - )) - item.on( - 'click' - "[data-id=#{file.store_id}]", (e) => - @attachments = _.filter( - @attachments, - (item) -> - return if item.id isnt file.store_id - item - ) - store_id = $(e.currentTarget).data('id') + renderFile = (file) => + item.find('.attachments').append(App.view('generic/attachment_item')(file)) + @attachments.push file - # delete attachment from storage - App.Ajax.request( - type: 'DELETE' - url: "#{App.Config.get('api_path')}/ticket_attachment_upload" - data: JSON.stringify(store_id: store_id), - processData: false - ) + if params && params.attachments + for file in params.attachments + renderFile(file) - # remove attachment from dom - element = $(e.currentTarget).closest('.attachments') - $(e.currentTarget).closest('.attachment').remove() - # empty .attachment (remove spaces) to keep css working, thanks @mrflix :-o - if element.find('.attachment').length == 0 - element.empty() + # remove items + item.find('.attachments').on('click', '.js-delete', (e) => + id = $(e.currentTarget).data('id') + @attachments = _.filter( + @attachments, + (item) -> + return if item.id.toString() is id.toString() + item ) - @attachments = [] + # delete attachment from storage + App.Ajax.request( + type: 'DELETE' + url: "#{App.Config.get('api_path')}/ticket_attachment_upload" + data: JSON.stringify(id: id), + processData: false + ) + + # remove attachment from dom + element = $(e.currentTarget).closest('.attachments') + $(e.currentTarget).closest('.attachment').remove() + if element.find('.attachment').length == 0 + element.empty() + ) + @progressBar = item.find('.attachmentUpload-progressBar') @progressText = item.find('.js-percentage') @attachmentPlaceholder = item.find('.attachmentPlaceholder') @@ -84,7 +83,6 @@ class App.UiElement.richtext # Called after received response from the server onCompleted: (response) => response = JSON.parse(response) - @attachments.push response.data @attachmentPlaceholder.removeClass('hide') @attachmentUpload.addClass('hide') @@ -93,7 +91,7 @@ class App.UiElement.richtext @progressBar.width(parseInt(0) + '%') @progressText.text('') - renderAttachment(response.data) + renderFile(response.data) item.find('input').val('') App.Log.debug 'UiElement.richtext', 'upload complete', response.data @@ -111,4 +109,5 @@ class App.UiElement.richtext ) ) App.Delay.set(u, 100, undefined, 'form_upload') + item diff --git a/app/assets/javascripts/app/controllers/agent_ticket_create.coffee b/app/assets/javascripts/app/controllers/agent_ticket_create.coffee index 617c83f0c..732e9e67a 100644 --- a/app/assets/javascripts/app/controllers/agent_ticket_create.coffee +++ b/app/assets/javascripts/app/controllers/agent_ticket_create.coffee @@ -14,6 +14,8 @@ class App.TicketCreate extends App.Controller # define default type @default_type = 'phone-in' + @formId = App.ControllerForm.formId() + # remember split info if exists @split = '' if @ticket_id && @article_id @@ -158,7 +160,7 @@ class App.TicketCreate extends App.Controller # get data / in case also ticket data for split buildScreen: (params) => - if !params.ticket_id && !params.article_id + if _.isEmpty(params.ticket_id) && _.isEmpty(params.article_id) if !_.isEmpty(params.customer_id) @render(options: { customer_id: params.customer_id }) return @@ -173,6 +175,7 @@ class App.TicketCreate extends App.Controller data: ticket_id: params.ticket_id article_id: params.article_id + form_id: @formId processData: true success: (data, status, xhr) => @@ -194,6 +197,9 @@ class App.TicketCreate extends App.Controller else t.body = App.Utils.text2html(a.body) + # add attachments + t.attachments = data.attachments + # render page @render(options: t) ) @@ -206,18 +212,15 @@ class App.TicketCreate extends App.Controller params = template.options else if App.TaskManager.get(@task_key) && !_.isEmpty(App.TaskManager.get(@task_key).state) params = App.TaskManager.get(@task_key).state + if !_.isEmpty(params['form_id']) + @formId = params['form_id'] - if params['form_id'] - @form_id = params['form_id'] - else - @form_id = App.ControllerForm.formId() - - @html App.view('agent_ticket_create')( + @html(App.view('agent_ticket_create')( head: 'New Ticket' agent: @permissionCheck('ticket.agent') admin: @permissionCheck('admin') - form_id: @form_id - ) + form_id: @formId + )) signatureChanges = (params, attribute, attributes, classname, form, ui) => if attribute && attribute.name is 'group_id' @@ -272,7 +275,7 @@ class App.TicketCreate extends App.Controller } new App.ControllerForm( el: @$('.ticket-form-top') - form_id: @form_id + form_id: @formId model: App.Ticket screen: 'create_top' events: @@ -288,14 +291,14 @@ class App.TicketCreate extends App.Controller new App.ControllerForm( el: @$('.article-form-top') - form_id: @form_id + form_id: @formId model: App.TicketArticle screen: 'create_top' params: params ) new App.ControllerForm( el: @$('.ticket-form-middle') - form_id: @form_id + form_id: @formId model: App.Ticket screen: 'create_middle' events: @@ -310,7 +313,7 @@ class App.TicketCreate extends App.Controller ) new App.ControllerForm( el: @$('.ticket-form-bottom') - form_id: @form_id + form_id: @formId model: App.Ticket screen: 'create_bottom' events: @@ -420,7 +423,7 @@ class App.TicketCreate extends App.Controller body: params.body type_id: type.id sender_id: sender.id - form_id: @form_id + form_id: @formId content_type: 'text/html' } else @@ -432,7 +435,7 @@ class App.TicketCreate extends App.Controller body: params.body type_id: type.id sender_id: sender.id - form_id: @form_id + form_id: @formId content_type: 'text/html' } diff --git a/app/assets/javascripts/app/controllers/layout_ref.coffee b/app/assets/javascripts/app/controllers/layout_ref.coffee index 6b49ed6bc..07ee4bafa 100644 --- a/app/assets/javascripts/app/controllers/layout_ref.coffee +++ b/app/assets/javascripts/app/controllers/layout_ref.coffee @@ -349,7 +349,7 @@ class LayoutRefCommunicationReply extends App.ControllerContent file = @uploadQueue.shift() # console.log "working of", file, "from", @uploadQueue - @fakeUpload file.name, file.size, @workOfUploadQueue + @fakeUpload(file.name, file.size, @workOfUploadQueue) humanFileSize: (size) -> i = Math.floor( Math.log(size) / Math.log(1024) ) @@ -363,27 +363,27 @@ class LayoutRefCommunicationReply extends App.ControllerContent @attachmentPlaceholder.removeClass('hide') @attachmentUpload.addClass('hide') - fakeUpload: (fileName, fileSize, callback) -> + fakeUpload: (filename, size, callback) -> @attachmentPlaceholder.addClass('hide') @attachmentUpload.removeClass('hide') progress = 0 - duration = fileSize / 1024 + duration = size / 1024 for i in [0..100] setTimeout @updateUploadProgress, i*duration/100 , i setTimeout (=> callback() - @renderAttachment(fileName, fileSize) + @renderAttachment(filename, size) ), duration - renderAttachment: (fileName, fileSize) => - @attachments.push([fileName, fileSize]) - @attachmentsHolder.append App.view('generic/attachment_item') - fileName: fileName - fileSize: @humanFileSize(fileSize) - + renderAttachment: (filename, size) => + @attachments.push([filename, size]) + @attachmentsHolder.append(App.view('generic/attachment_item') + filename: filename + size: @humanFileSize(size) + ) App.Config.set( 'layout_ref/communication_reply/:content', LayoutRefCommunicationReply, 'Routes' ) diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee index 42b036c6f..e2c854bae 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee @@ -245,7 +245,7 @@ class App.TicketZoomArticleNew extends App.Controller controller = new App.ControllerForm( el: @$('.recipients') model: - configure_attributes: configure_attributes, + configure_attributes: configure_attributes ) @$('[data-name="body"]').ce({ @@ -255,12 +255,13 @@ class App.TicketZoomArticleNew extends App.Controller }) html5Upload.initialize( - uploadUrl: App.Config.get('api_path') + '/ticket_attachment_upload', - dropContainer: @$('.article-add').get(0), - cancelContainer: @cancelContainer, - inputField: @$('.article-attachment input').get(0), - key: 'File', - data: { form_id: @form_id }, + uploadUrl: App.Config.get('api_path') + '/ticket_attachment_upload' + dropContainer: @$('.article-add').get(0) + cancelContainer: @cancelContainer + inputField: @$('.article-attachment input').get(0) + key: 'File' + data: + form_id: @form_id maxSimultaneousUploads: 1, onFileAdded: (file) => @@ -303,6 +304,8 @@ class App.TicketZoomArticleNew extends App.Controller ) ) + @bindAttachmentDelete() + # show text module UI if !@permissionCheck('ticket.customer') textModule = new App.WidgetTextModule( @@ -737,33 +740,29 @@ class App.TicketZoomArticleNew extends App.Controller @articleNewEdit.parent().removeClass('is-dropTarget') if @dragEventCounter is 0 renderAttachment: (file) => - @attachmentsHolder.append App.view('generic/attachment_item') - fileName: file.filename - fileSize: @humanFileSize( file.size ) - store_id: file.store_id - @attachmentsHolder.on( - 'click' - "[data-id=#{file.store_id}]", (e) => - @attachments = _.filter( - @attachments, - (item) -> - return if item.id isnt file.store_id - item - ) - store_id = $(e.currentTarget).data('id') + @attachmentsHolder.append(App.view('generic/attachment_item')(file)) - # delete attachment from storage - App.Ajax.request( - type: 'DELETE' - url: App.Config.get('api_path') + '/ticket_attachment_upload' - data: JSON.stringify(store_id: store_id) - processData: false - ) + bindAttachmentDelete: => + @attachmentsHolder.on('click', '.js-delete', (e) => + id = $(e.currentTarget).data('id') + @attachments = _.filter( + @attachments, + (item) -> + return if item.id.toString() is id.toString() + item + ) - # remove attachment from dom - element = $(e.currentTarget).closest('.attachments') - $(e.currentTarget).closest('.attachment').remove() - # empty .attachment (remove spaces) to keep css working, thanks @mrflix :-o - if element.find('.attachment').length == 0 - element.empty() + # delete attachment from storage + App.Ajax.request( + type: 'DELETE' + url: App.Config.get('api_path') + '/ticket_attachment_upload' + data: JSON.stringify(id: id) + processData: false + ) + + # remove attachment from dom + element = $(e.currentTarget).closest('.attachments') + $(e.currentTarget).closest('.attachment').remove() + if element.find('.attachment').length == 0 + element.empty() ) diff --git a/app/assets/javascripts/app/views/generic/attachment_item.jst.eco b/app/assets/javascripts/app/views/generic/attachment_item.jst.eco index dfa927c95..d4de0ce4a 100644 --- a/app/assets/javascripts/app/views/generic/attachment_item.jst.eco +++ b/app/assets/javascripts/app/views/generic/attachment_item.jst.eco @@ -1,7 +1,7 @@
                                  -
                                  <%= @fileName %>
                                  -
                                  <%= @fileSize %>
                                  -
                                  +
                                  <%= @filename %>
                                  +
                                  <%= @humanFileSize(@size) %>
                                  +
                                  <%- @Icon('diagonal-cross') %><%- @T('Delete File') %>
                                  \ No newline at end of file diff --git a/app/controllers/ticket_articles_controller.rb b/app/controllers/ticket_articles_controller.rb index 6585a43e0..10468c8b5 100644 --- a/app/controllers/ticket_articles_controller.rb +++ b/app/controllers/ticket_articles_controller.rb @@ -150,13 +150,16 @@ class TicketArticlesController < ApplicationController # DELETE /ticket_attachment_upload def ticket_attachment_upload_delete - if params[:store_id] - Store.remove_item(params[:store_id]) + + if params[:id].present? + Store.remove_item(params[:id]) render json: { success: true, } return - elsif params[:form_id] + end + + if params[:form_id].present? Store.remove( object: 'UploadCache', o_id: params[:form_id], @@ -167,7 +170,7 @@ class TicketArticlesController < ApplicationController return end - render json: { message: 'No such store_id or form_id!' }, status: :unprocessable_entity + render json: { message: 'No such id or form_id!' }, status: :unprocessable_entity end # POST /ticket_attachment_upload @@ -198,7 +201,7 @@ class TicketArticlesController < ApplicationController render json: { success: true, data: { - store_id: store.id, + id: store.id, filename: file.original_filename, size: store.size, } diff --git a/app/controllers/tickets_controller.rb b/app/controllers/tickets_controller.rb index dbf33b33f..9b471d5ee 100644 --- a/app/controllers/tickets_controller.rb +++ b/app/controllers/tickets_controller.rb @@ -357,8 +357,28 @@ class TicketsController < ApplicationController article = Ticket::Article.find(params[:article_id]) assets = article.assets(assets) + attachments = [] + if params[:form_id].present? + attachments = Store.list( + object: 'UploadCache', + o_id: params[:form_id], + ).to_a + article.attachments.each do |attachment| + next if attachment.preferences['Content-ID'].present? + file = Store.add( + object: 'UploadCache', + o_id: params[:form_id], + data: attachment.content, + filename: attachment.filename, + preferences: attachment.preferences, + ) + attachments.push file + end + end + render json: { - assets: assets + assets: assets, + attachments: attachments, } end From 712acdfe2af4ab022b708947b27e1b071b3c320b Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Tue, 5 Dec 2017 16:12:52 +0100 Subject: [PATCH 044/196] Fixed issue #573 - email forward of article (like regular email client forward - e. g. forward customers message to third party contact). --- .../app/controllers/ticket_zoom.coffee | 1 + .../article_action/email_reply.coffee | 77 ++++++++++++++++++- .../ticket_zoom/article_new.coffee | 8 ++ .../ticket_zoom/article_view.coffee | 2 + app/controllers/ticket_articles_controller.rb | 37 +++++++++ config/routes/ticket.rb | 1 + 6 files changed, 122 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/app/controllers/ticket_zoom.coffee b/app/assets/javascripts/app/controllers/ticket_zoom.coffee index 094a15f30..d05db0448 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom.coffee @@ -461,6 +461,7 @@ class App.TicketZoom extends App.Controller ui: @ highligher: @highligher ticket_article_ids: @ticket_article_ids + form_id: @form_id ) new App.TicketCustomerAvatar( diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_action/email_reply.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/email_reply.coffee index eacd9cf14..0d1cd8666 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_action/email_reply.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/email_reply.coffee @@ -10,6 +10,13 @@ class EmailReply extends App.Controller icon: 'reply' href: '#' } + actions.push { + name: 'forward' + type: 'emailForward' + #icon: 'forward' + icon: 'info' + href: '#' + } recipients = [] if article.sender.name is 'Customer' if article.from @@ -57,6 +64,13 @@ class EmailReply extends App.Controller icon: 'reply' href: '#' } + actions.push { + name: 'forward' + type: 'emailForward' + #icon: 'forward' + icon: 'info' + href: '#' + } if article.sender.name is 'Agent' && article.type.name is 'phone' actions.push { name: 'reply' @@ -64,17 +78,27 @@ class EmailReply extends App.Controller icon: 'reply' href: '#' } + actions.push { + name: 'forward' + type: 'emailForward' + #icon: 'forward' + icon: 'info' + href: '#' + } actions @perform: (articleContainer, type, ticket, article, ui) -> - return true if type isnt 'emailReply' && type isnt 'emailReplyAll' + return true if type isnt 'emailReply' && type isnt 'emailReplyAll' && type isnt 'emailForward' - if type isnt 'emailReply' + if type is 'emailReply' + @emailReply(false, ticket, article, ui) + + else if type is 'emailReplyAll' @emailReply(true, ticket, article, ui) - else if type isnt 'emailReplyAll' - @emailReply(false, ticket, article, ui) + else if type is 'emailForward' + @emailForward(ticket, article, ui) true @@ -132,4 +156,49 @@ class EmailReply extends App.Controller true + @emailForward: (ticket, article, ui) -> + + ui.scrollToCompose() + + signaturePosition = 'top' + body = '' + if article.content_type.match('html') + body = App.Utils.textCleanup(article.body) + if article.content_type.match('plain') + body = App.Utils.textCleanup(article.body) + body = App.Utils.text2html(body) + + body = "
                                  ---Begin forwarded message:---

                                  #{body}

                                  " + + articleNew = {} + articleNew.body = body + + type = App.TicketArticleType.findByAttribute(name:'email') + + App.Event.trigger('ui::ticket::setArticleType', { + ticket: ticket + type: type + article: articleNew + signaturePosition: signaturePosition + }) + + # add attachments to form + App.Ajax.request( + id: "ticket_attachment_clone#{ui.form_id}" + type: 'POST' + url: "#{App.Config.get('api_path')}/ticket_attachment_upload_clone_by_article/#{article.id}" + data: JSON.stringify(form_id: ui.form_id) + processData: true + success: (data, status, xhr) -> + return if _.isEmpty(data.attachments) + App.Event.trigger('ui::ticket::addArticleAttachent', { + ticket: ticket + article: article + attachments: data.attachments + form_id: ui.form_id + }) + ) + + true + App.Config.set('200-EmailReply', EmailReply, 'TicketZoomArticleAction') diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee index e2c854bae..3eaf3dce9 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee @@ -70,6 +70,14 @@ class App.TicketZoomArticleNew extends App.Controller @textarea.focus() ) + # add article attachment + @bind('ui::ticket::addArticleAttachent', (data) => + return if data.ticket.id.toString() isnt @ticket_id.toString() + return if _.isEmpty(data.attachments) + for file in data.attachments + @renderAttachment(file) + ) + # reset new article screen @bind('ui::ticket::taskReset', (data) => return if data.ticket_id.toString() isnt @ticket_id.toString() diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_view.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_view.coffee index d66ed3c15..d70282853 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_view.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_view.coffee @@ -20,6 +20,7 @@ class App.TicketZoomArticleView extends App.Controller el: el ui: @ui highligher: @highligher + form_id: @form_id ) if !@ticketArticleInsertByIndex(index, el) all.push el @@ -193,6 +194,7 @@ class ArticleViewItem extends App.ObserverController ticket: @ticket article: article lastAttributres: @lastAttributres + form_id: @form_id ) # set see more diff --git a/app/controllers/ticket_articles_controller.rb b/app/controllers/ticket_articles_controller.rb index 10468c8b5..415ee4dcb 100644 --- a/app/controllers/ticket_articles_controller.rb +++ b/app/controllers/ticket_articles_controller.rb @@ -208,6 +208,43 @@ class TicketArticlesController < ApplicationController } end + # POST /ticket_attachment_upload_clone_by_article + def ticket_attachment_upload_clone_by_article + + article = Ticket::Article.find(params[:article_id]) + access!(article.ticket, 'read') + + raise Exceptions::NotAuthorized, 'Need form_id to attach attachmeints.' if params[:form_id].blank? + + existing_attachments = Store.list( + object: 'UploadCache', + o_id: params[:form_id], + ).to_a + new_attachments = [] + article.attachments.each do |new_attachment| + next if new_attachment.preferences['Content-ID'].present? + already_added = false + existing_attachments.each do |local_attachment| + next if local_attachment.filename != new_attachment.filename || local_attachment.size != new_attachment.size + already_added = true + break + end + next if already_added == true + file = Store.add( + object: 'UploadCache', + o_id: params[:form_id], + data: new_attachment.content, + filename: new_attachment.filename, + preferences: new_attachment.preferences, + ) + new_attachments.push file + end + + render json: { + attachments: new_attachments, + } + end + # GET /ticket_attachment/:ticket_id/:article_id/:id def attachment ticket = Ticket.lookup(id: params[:ticket_id]) diff --git a/config/routes/ticket.rb b/config/routes/ticket.rb index 0e12e66e0..fef82d720 100644 --- a/config/routes/ticket.rb +++ b/config/routes/ticket.rb @@ -44,6 +44,7 @@ Zammad::Application.routes.draw do match api_path + '/ticket_attachment/:ticket_id/:article_id/:id', to: 'ticket_articles#attachment', via: :get match api_path + '/ticket_attachment_upload', to: 'ticket_articles#ticket_attachment_upload_add', via: :post match api_path + '/ticket_attachment_upload', to: 'ticket_articles#ticket_attachment_upload_delete', via: :delete + match api_path + '/ticket_attachment_upload_clone_by_article/:article_id', to: 'ticket_articles#ticket_attachment_upload_clone_by_article', via: :post match api_path + '/ticket_article_plain/:id', to: 'ticket_articles#article_plain', via: :get end From 63ff844f383aa36db1e2e0da4f980581c3976481 Mon Sep 17 00:00:00 2001 From: Felix Niklas Date: Mon, 28 Aug 2017 23:31:26 +0200 Subject: [PATCH 045/196] Code cleanup. --- .../article_action/email_reply.coffee | 20 +++---- .../ticket_zoom/article_view_actions.jst.eco | 2 +- app/assets/stylesheets/zammad.scss | 12 +++++ .../clones_ticket_article_attachments.rb | 37 +++++++++++++ app/controllers/ticket_articles_controller.rb | 30 +---------- app/controllers/tickets_controller.rb | 24 ++------- ...ket_article_attachments_controller_test.rb | 53 +++++++++++++++++++ 7 files changed, 119 insertions(+), 59 deletions(-) create mode 100644 app/controllers/concerns/clones_ticket_article_attachments.rb diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_action/email_reply.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/email_reply.coffee index 0d1cd8666..c1b65742a 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_action/email_reply.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/email_reply.coffee @@ -10,13 +10,6 @@ class EmailReply extends App.Controller icon: 'reply' href: '#' } - actions.push { - name: 'forward' - type: 'emailForward' - #icon: 'forward' - icon: 'info' - href: '#' - } recipients = [] if article.sender.name is 'Customer' if article.from @@ -57,6 +50,15 @@ class EmailReply extends App.Controller icon: 'reply-all' href: '#' } + + actions.push { + name: 'forward' + type: 'emailForward' + #icon: 'forward' + icon: 'line-right-arrow' + href: '#' + } + if article.sender.name is 'Customer' && article.type.name is 'phone' actions.push { name: 'reply' @@ -68,7 +70,7 @@ class EmailReply extends App.Controller name: 'forward' type: 'emailForward' #icon: 'forward' - icon: 'info' + icon: 'line-right-arrow' href: '#' } if article.sender.name is 'Agent' && article.type.name is 'phone' @@ -82,7 +84,7 @@ class EmailReply extends App.Controller name: 'forward' type: 'emailForward' #icon: 'forward' - icon: 'info' + icon: 'line-right-arrow' href: '#' } diff --git a/app/assets/javascripts/app/views/ticket_zoom/article_view_actions.jst.eco b/app/assets/javascripts/app/views/ticket_zoom/article_view_actions.jst.eco index e0301172b..e4cd4695e 100644 --- a/app/assets/javascripts/app/views/ticket_zoom/article_view_actions.jst.eco +++ b/app/assets/javascripts/app/views/ticket_zoom/article_view_actions.jst.eco @@ -1,7 +1,7 @@
                                  <% for action in @actions: %> - <%- @Icon(action.icon, 'article-action-icon') %><%- @T(action.name) %> + <%- @Icon(action.icon, 'article-action-icon') %><%- @T(action.name) %> <% end %>
                                  \ No newline at end of file diff --git a/app/assets/stylesheets/zammad.scss b/app/assets/stylesheets/zammad.scss index 6678c2d2a..b7014b403 100644 --- a/app/assets/stylesheets/zammad.scss +++ b/app/assets/stylesheets/zammad.scss @@ -4985,6 +4985,18 @@ footer { fill: currentColor; } + .article-action-name { + @media screen and (max-width: 1080px) { + display: none; + } + + @media screen and (max-width: 1358px) { + .main:not(.is-closed) & { + display: none; + } + } + } + .article-add { position: relative; z-index: 1; // fixed chrome 49 + flex issue, not shown article diff --git a/app/controllers/concerns/clones_ticket_article_attachments.rb b/app/controllers/concerns/clones_ticket_article_attachments.rb new file mode 100644 index 000000000..da8248030 --- /dev/null +++ b/app/controllers/concerns/clones_ticket_article_attachments.rb @@ -0,0 +1,37 @@ +module ClonesTicketArticleAttachments + extend ActiveSupport::Concern + + private + + def article_attachments_clone(article) + raise Exceptions::UnprocessableEntity, 'Need form_id to attach attachments to new form.' if params[:form_id].blank? + + existing_attachments = Store.list( + object: 'UploadCache', + o_id: params[:form_id], + ) + attachments = [] + article.attachments.each do |new_attachment| + next if new_attachment.preferences['Content-ID'].present? + next if new_attachment.preferences['content-alternative'] == true + already_added = false + existing_attachments.each do |existing_attachment| + next if existing_attachment.filename != new_attachment.filename || existing_attachment.size != new_attachment.size + already_added = true + break + end + next if already_added == true + file = Store.add( + object: 'UploadCache', + o_id: params[:form_id], + data: new_attachment.content, + filename: new_attachment.filename, + preferences: new_attachment.preferences, + ) + attachments.push file + end + + attachments + end + +end diff --git a/app/controllers/ticket_articles_controller.rb b/app/controllers/ticket_articles_controller.rb index 415ee4dcb..cf4e5c98d 100644 --- a/app/controllers/ticket_articles_controller.rb +++ b/app/controllers/ticket_articles_controller.rb @@ -2,6 +2,7 @@ class TicketArticlesController < ApplicationController include CreatesTicketArticles + include ClonesTicketArticleAttachments prepend_before_action :authentication_check @@ -210,38 +211,11 @@ class TicketArticlesController < ApplicationController # POST /ticket_attachment_upload_clone_by_article def ticket_attachment_upload_clone_by_article - article = Ticket::Article.find(params[:article_id]) access!(article.ticket, 'read') - raise Exceptions::NotAuthorized, 'Need form_id to attach attachmeints.' if params[:form_id].blank? - - existing_attachments = Store.list( - object: 'UploadCache', - o_id: params[:form_id], - ).to_a - new_attachments = [] - article.attachments.each do |new_attachment| - next if new_attachment.preferences['Content-ID'].present? - already_added = false - existing_attachments.each do |local_attachment| - next if local_attachment.filename != new_attachment.filename || local_attachment.size != new_attachment.size - already_added = true - break - end - next if already_added == true - file = Store.add( - object: 'UploadCache', - o_id: params[:form_id], - data: new_attachment.content, - filename: new_attachment.filename, - preferences: new_attachment.preferences, - ) - new_attachments.push file - end - render json: { - attachments: new_attachments, + attachments: article_attachments_clone(article), } end diff --git a/app/controllers/tickets_controller.rb b/app/controllers/tickets_controller.rb index 9b471d5ee..df8b29ae1 100644 --- a/app/controllers/tickets_controller.rb +++ b/app/controllers/tickets_controller.rb @@ -2,6 +2,7 @@ class TicketsController < ApplicationController include CreatesTicketArticles + include ClonesTicketArticleAttachments include TicketStats prepend_before_action :authentication_check @@ -353,32 +354,13 @@ class TicketsController < ApplicationController access!(ticket, 'read') assets = ticket.assets({}) - # get related articles article = Ticket::Article.find(params[:article_id]) + access!(article.ticket, 'read') assets = article.assets(assets) - attachments = [] - if params[:form_id].present? - attachments = Store.list( - object: 'UploadCache', - o_id: params[:form_id], - ).to_a - article.attachments.each do |attachment| - next if attachment.preferences['Content-ID'].present? - file = Store.add( - object: 'UploadCache', - o_id: params[:form_id], - data: attachment.content, - filename: attachment.filename, - preferences: attachment.preferences, - ) - attachments.push file - end - end - render json: { assets: assets, - attachments: attachments, + attachments: article_attachments_clone(article), } end diff --git a/test/controllers/ticket_article_attachments_controller_test.rb b/test/controllers/ticket_article_attachments_controller_test.rb index 2e4286332..90acadf91 100644 --- a/test/controllers/ticket_article_attachments_controller_test.rb +++ b/test/controllers/ticket_article_attachments_controller_test.rb @@ -141,4 +141,57 @@ class TicketArticleAttachmentsControllerTest < ActionDispatch::IntegrationTest end + test '01.02 test attachments for split' do + headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json' } + + email_raw_string = IO.binread('test/fixtures/mail24.box') + ticket_p, article_p, user_p = Channel::EmailParser.new.process({}, email_raw_string) + + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('tickets-agent@example.com', 'agentpw') + get "/api/v1/ticket_split", params: { form_id: '1234-2', ticket_id: ticket_p.id, article_id: article_p.id }, headers: headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert(result['assets']) + assert_equal(result['attachments'].class, Array) + assert_equal(result['attachments'].count, 1) + assert_equal(result['attachments'][0]['filename'], 'rulesets-report.csv') + + end + + test '01.03 test attachments for forward' do + headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json' } + + email_raw_string = IO.binread('test/fixtures/mail8.box') + ticket_p, article_p, user_p = Channel::EmailParser.new.process({}, email_raw_string) + + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('tickets-agent@example.com', 'agentpw') + post "/api/v1/ticket_attachment_upload_clone_by_article/#{article_p.id}", params: {}, headers: headers.merge('Authorization' => credentials) + assert_response(422) + result = JSON.parse(@response.body) + assert_equal(result.class, Hash) + assert(result['error'], 'Need form_id to attach attachments to new form') + + post "/api/v1/ticket_attachment_upload_clone_by_article/#{article_p.id}", params: { form_id: '1234-1' }.to_json, headers: headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(result['attachments'].class, Array) + assert(result['attachments'].blank?) + + email_raw_string = IO.binread('test/fixtures/mail24.box') + ticket_p, article_p, user_p = Channel::EmailParser.new.process({}, email_raw_string) + + post "/api/v1/ticket_attachment_upload_clone_by_article/#{article_p.id}", params: { form_id: '1234-2' }.to_json, headers: headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(result['attachments'].class, Array) + assert_equal(result['attachments'].count, 1) + assert_equal(result['attachments'][0]['filename'], 'rulesets-report.csv') + + post "/api/v1/ticket_attachment_upload_clone_by_article/#{article_p.id}", params: { form_id: '1234-2' }.to_json, headers: headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(result['attachments'].class, Array) + assert(result['attachments'].blank?) + end + end From 18329a13790c9284709213885e8f3456d325eab6 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Wed, 6 Dec 2017 00:53:00 +0100 Subject: [PATCH 046/196] Applied rubocop. --- test/controllers/ticket_article_attachments_controller_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/controllers/ticket_article_attachments_controller_test.rb b/test/controllers/ticket_article_attachments_controller_test.rb index 90acadf91..abee9e162 100644 --- a/test/controllers/ticket_article_attachments_controller_test.rb +++ b/test/controllers/ticket_article_attachments_controller_test.rb @@ -148,7 +148,7 @@ class TicketArticleAttachmentsControllerTest < ActionDispatch::IntegrationTest ticket_p, article_p, user_p = Channel::EmailParser.new.process({}, email_raw_string) credentials = ActionController::HttpAuthentication::Basic.encode_credentials('tickets-agent@example.com', 'agentpw') - get "/api/v1/ticket_split", params: { form_id: '1234-2', ticket_id: ticket_p.id, article_id: article_p.id }, headers: headers.merge('Authorization' => credentials) + get '/api/v1/ticket_split', params: { form_id: '1234-2', ticket_id: ticket_p.id, article_id: article_p.id }, headers: headers.merge('Authorization' => credentials) assert_response(200) result = JSON.parse(@response.body) assert(result['assets']) From c74e0dac462253b9b585b6ff727c9c574e258ed2 Mon Sep 17 00:00:00 2001 From: Felix Niklas Date: Mon, 28 Aug 2017 23:30:35 +0200 Subject: [PATCH 047/196] forward icon, adjust ticket action icons height # Conflicts: # public/assets/images/icons.svg - prettify svg output --- app/assets/stylesheets/svg-dimensions.css | 15 +- contrib/icon-sprite.sketch | Bin 206391 -> 207059 bytes gulpfile.js | 6 +- public/assets/images/icons.svg | 682 +++++++++++++++++++++- public/assets/images/icons/forward.svg | 12 + public/assets/images/icons/lock-open.svg | 4 +- public/assets/images/icons/lock.svg | 4 +- public/assets/images/icons/reply-all.svg | 6 +- public/assets/images/icons/reply.svg | 6 +- public/assets/images/icons/split.svg | 6 +- 10 files changed, 719 insertions(+), 22 deletions(-) create mode 100644 public/assets/images/icons/forward.svg diff --git a/app/assets/stylesheets/svg-dimensions.css b/app/assets/stylesheets/svg-dimensions.css index 3fa27c806..9f063d0e8 100644 --- a/app/assets/stylesheets/svg-dimensions.css +++ b/app/assets/stylesheets/svg-dimensions.css @@ -23,6 +23,7 @@ .icon-facebook-button { width: 29px; height: 24px; } .icon-facebook { width: 17px; height: 17px; } .icon-form { width: 17px; height: 17px; } +.icon-forward { width: 16px; height: 17px; } .icon-full-logo { width: 175px; height: 50px; } .icon-github-button { width: 29px; height: 24px; } .icon-gitlab-button { width: 29px; height: 24px; } @@ -37,8 +38,8 @@ .icon-linkedin-button { width: 29px; height: 24px; } .icon-list { width: 16px; height: 16px; } .icon-loading { width: 16px; height: 16px; } -.icon-lock-open { width: 16px; height: 16px; } -.icon-lock { width: 16px; height: 16px; } +.icon-lock-open { width: 16px; height: 17px; } +.icon-lock { width: 16px; height: 17px; } .icon-logo { width: 42px; height: 36px; } .icon-logotype { width: 91px; height: 15px; } .icon-long-arrow-right { width: 11px; height: 11px; } @@ -56,7 +57,6 @@ .icon-note { width: 16px; height: 16px; } .icon-oauth2-button { width: 29px; height: 24px; } .icon-office365-button { width: 29px; height: 24px; } -.icon-weibo-button { width: 29px; height: 24px; } .icon-one-ticket { width: 48px; height: 10px; } .icon-organization { width: 16px; height: 16px; } .icon-outbound-calls { width: 17px; height: 17px; } @@ -74,13 +74,13 @@ .icon-received-calls { width: 17px; height: 17px; } .icon-reload { width: 16px; height: 16px; } .icon-reopening { width: 68px; height: 47px; } -.icon-reply-all { width: 16px; height: 16px; } -.icon-reply { width: 16px; height: 16px; } +.icon-reply-all { width: 16px; height: 17px; } +.icon-reply { width: 16px; height: 17px; } .icon-report { width: 20px; height: 20px; } .icon-searchdetail { width: 18px; height: 14px; } .icon-signout { width: 15px; height: 19px; } .icon-small-dot { width: 16px; height: 16px; } -.icon-split { width: 16px; height: 16px; } +.icon-split { width: 16px; height: 17px; } .icon-status-modified-outer-circle { width: 16px; height: 16px; } .icon-status { width: 16px; height: 16px; } .icon-stopwatch { width: 77px; height: 83px; } @@ -97,5 +97,6 @@ .icon-unmute { width: 16px; height: 16px; } .icon-user { width: 16px; height: 16px; } .icon-web { width: 17px; height: 17px; } +.icon-weibo-button { width: 29px; height: 24px; } .icon-zoom-in { width: 20px; height: 20px; } -.icon-zoom-out { width: 20px; height: 20px; } +.icon-zoom-out { width: 20px; height: 20px; } \ No newline at end of file diff --git a/contrib/icon-sprite.sketch b/contrib/icon-sprite.sketch index 2a6135cec3f0976d4d4583547bb1df26aa5f1491..751f95348e611c155b1ffcc9bbcea610b3846dab 100644 GIT binary patch delta 178824 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

                                  !PXYveP)1s7OyZ*E&xiJ9rsw`R%`Nx?&3t|#nv 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�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 z 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 delta 178076 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

                                  diff --git a/app/assets/javascripts/app/views/report/download_list.jst.eco b/app/assets/javascripts/app/views/report/download_list.jst.eco deleted file mode 100644 index f22dc8983..000000000 --- a/app/assets/javascripts/app/views/report/download_list.jst.eco +++ /dev/null @@ -1,24 +0,0 @@ -<%- @Icon('download') %><%- @T('Download %s record(s)', @count) %> -
                                  - - - - - - - - - - - -<% for ticket in @tickets: %> - - - - - - - -<% end %> - -
                                  <%- @T('Number') %><%- @T('Title') %><%- @T('State') %><%- @T('Group') %><%- @T('Created') %>
                                  target="_blank"<% end %> href="<%= @url %><%= ticket.id %>"><%- @P(ticket, 'number') %><%- @P(ticket, 'title') %><%- @P(ticket, 'state') %><%- @P(ticket, 'group') %><%- @P(ticket, 'created_at') %>
                                  diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb index 01c36af9f..6cba59c42 100644 --- a/app/controllers/reports_controller.rb +++ b/app/controllers/reports_controller.rb @@ -211,22 +211,26 @@ class ReportsController < ApplicationController row = 2 result[:ticket_ids].each do |ticket_id| - ticket = Ticket.lookup(id: ticket_id) - row += 1 - worksheet.write(row, 0, ticket.number) - worksheet.write(row, 1, ticket.title) - worksheet.write(row, 2, ticket.state.name) - worksheet.write(row, 3, ticket.priority.name) - worksheet.write(row, 4, ticket.group.name) - worksheet.write(row, 5, ticket.owner.fullname) - worksheet.write(row, 6, ticket.customer.fullname) - worksheet.write(row, 7, ticket.try(:organization).try(:name)) - worksheet.write(row, 8, ticket.create_article_type.name) - worksheet.write(row, 9, ticket.create_article_sender.name) - worksheet.write(row, 10, ticket.tag_list.join(',')) - worksheet.write(row, 11, ticket.created_at) - worksheet.write(row, 12, ticket.updated_at) - worksheet.write(row, 13, ticket.close_at) + begin + ticket = Ticket.lookup(id: ticket_id) + row += 1 + worksheet.write(row, 0, ticket.number) + worksheet.write(row, 1, ticket.title) + worksheet.write(row, 2, ticket.state.name) + worksheet.write(row, 3, ticket.priority.name) + worksheet.write(row, 4, ticket.group.name) + worksheet.write(row, 5, ticket.owner.fullname) + worksheet.write(row, 6, ticket.customer.fullname) + worksheet.write(row, 7, ticket.try(:organization).try(:name)) + worksheet.write(row, 8, ticket.create_article_type.name) + worksheet.write(row, 9, ticket.create_article_sender.name) + worksheet.write(row, 10, ticket.tag_list.join(',')) + worksheet.write(row, 11, ticket.created_at) + worksheet.write(row, 12, ticket.updated_at) + worksheet.write(row, 13, ticket.close_at) + rescue => e + Rails.logger.error "SKIP: #{e.message}" + end end workbook.close diff --git a/lib/report/base.rb b/lib/report/base.rb index ada98a0f1..b43801452 100644 --- a/lib/report/base.rb +++ b/lib/report/base.rb @@ -10,7 +10,7 @@ class Report::Base # :selector def self.history_count(params) - history_object = History::Object.lookup( name: params[:object] ) + history_object = History::Object.lookup(name: params[:object]) query, bind_params, tables = Ticket.selector2sql(params[:selector]) diff --git a/lib/report/ticket_generic_time.rb b/lib/report/ticket_generic_time.rb index 0eaba33e2..5372f1166 100644 --- a/lib/report/ticket_generic_time.rb +++ b/lib/report/ticket_generic_time.rb @@ -135,8 +135,8 @@ returns field: params[:params][:field], } - limit = 1000 - if !params[:sheet] + limit = 6000 + if params[:sheet].blank? limit = 100 end @@ -146,6 +146,7 @@ returns end result = SearchIndexBackend.selectors(['Ticket'], selector, limit, nil, aggs_interval) + return result if params[:sheet].present? assets = {} result[:ticket_ids].each do |ticket_id| ticket_full = Ticket.find(ticket_id) diff --git a/lib/report/ticket_moved.rb b/lib/report/ticket_moved.rb index 03f75b546..88e4de29e 100644 --- a/lib/report/ticket_moved.rb +++ b/lib/report/ticket_moved.rb @@ -123,6 +123,7 @@ returns } local_params = defaults.merge(local_params) result = history(local_params) + return result if params[:sheet].present? assets = {} result[:ticket_ids].each do |ticket_id| ticket_full = Ticket.find(ticket_id) diff --git a/lib/report/ticket_reopened.rb b/lib/report/ticket_reopened.rb index ef4affd37..9b7f7d5a6 100644 --- a/lib/report/ticket_reopened.rb +++ b/lib/report/ticket_reopened.rb @@ -98,6 +98,7 @@ returns end: params[:range_end], selector: params[:selector] ) + return result if params[:sheet].present? assets = {} result[:ticket_ids].each do |ticket_id| ticket_full = Ticket.find(ticket_id) diff --git a/public/assets/tests/table_extended.js b/public/assets/tests/table_extended.js index 63a8c1476..082861c2f 100644 --- a/public/assets/tests/table_extended.js +++ b/public/assets/tests/table_extended.js @@ -1646,4 +1646,63 @@ test('table new - initial list', function() { equal(el.find('tbody > tr:nth-child(4) > td').length, 0, 'check row 3') + $('#table').append('

                                  table with data 11

                                  ') + var el = $('#table-new11') + + 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', + note: 'some note 2', + active: false, + created_at: '2014-06-10T10:17:34.000Z', + }, + ], {clear: true}) + + var table = new App.ControllerTable({ + el: el, + overviewAttributes: ['name', 'created_at', 'active'], + model: App.TicketPriority, + objects: App.TicketPriority.search({sortBy:'name', order: 'ASC'}), + checkbox: false, + radio: false, + frontendTimeUpdateExecute: false, + }) + //equal(el.find('table').length, 0, 'row count') + //table.render() + equal(el.find('table > thead > tr').length, 1, 'row count') + equal(el.find('table > thead > tr > th:nth-child(1)').text().trim(), 'Name', 'check header') + equal(el.find('table > thead > tr > th:nth-child(2)').text().trim(), 'Erstellt', 'check header') + equal(el.find('table > thead > tr > th:nth-child(3)').text().trim(), 'Aktiv', 'check header') + equal(el.find('tbody > tr:nth-child(1) > td').length, 3, 'check row 1') + equal(el.find('tbody > tr:nth-child(1) > td:first').text().trim(), '1 niedrig', 'check row 1') + equal(el.find('tbody > tr:nth-child(1) > td:nth-child(2)').text().trim(), '', 'check row 1') + equal(el.find('tbody > tr:nth-child(1) > td:nth-child(3)').text().trim(), 'true', 'check row 1') + equal(el.find('tbody > tr:nth-child(2) > td').length, 3, 'check row 2') + equal(el.find('tbody > tr:nth-child(2) > td:first').text().trim(), '2 normal', 'check row 2') + equal(el.find('tbody > tr:nth-child(2) > td:nth-child(2)').text().trim(), '', 'check row 2') + equal(el.find('tbody > tr:nth-child(3) > td').length, 0, 'check row 3') + + result = table.update({sync: true, objects: App.TicketPriority.search({sortBy:'name', order: 'ASC'})}) + equal(result[0], 'noChanges') + + equal(el.find('table > thead > tr').length, 1, 'row count') + equal(el.find('table > thead > tr > th:nth-child(1)').text().trim(), 'Name', 'check header') + equal(el.find('table > thead > tr > th:nth-child(2)').text().trim(), 'Erstellt', 'check header') + equal(el.find('table > thead > tr > th:nth-child(3)').text().trim(), 'Aktiv', 'check header') + equal(el.find('tbody > tr:nth-child(1) > td').length, 3, 'check row 1') + equal(el.find('tbody > tr:nth-child(1) > td:first').text().trim(), '1 niedrig', 'check row 1') + equal(el.find('tbody > tr:nth-child(1) > td:nth-child(2)').text().trim(), '', 'check row 1') + equal(el.find('tbody > tr:nth-child(1) > td:nth-child(3)').text().trim(), 'true', 'check row 1') + equal(el.find('tbody > tr:nth-child(2) > td').length, 3, 'check row 2') + equal(el.find('tbody > tr:nth-child(2) > td:first').text().trim(), '2 normal', 'check row 2') + equal(el.find('tbody > tr:nth-child(2) > td:nth-child(2)').text().trim(), '', 'check row 2') + }) From 09af88a578e56106ae160b8761ffbb24edcab920 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Tue, 12 Dec 2017 10:15:07 +0100 Subject: [PATCH 062/196] Fixed usage to twitter initials setting. --- .../app/controllers/ticket_zoom/article_new.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee index 3eaf3dce9..d8bc066ba 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee @@ -151,7 +151,7 @@ class App.TicketZoomArticleNew extends App.Controller icon: 'twitter' attributes: [] internal: false, - features: ['body:limit', 'body:initials'] + features: attributes maxTextLength: 280 warningTextLength: 30 } @@ -164,7 +164,7 @@ class App.TicketZoomArticleNew extends App.Controller icon: 'twitter' attributes: ['to'] internal: false, - features: ['body:limit', 'body:initials'] + features: attributes maxTextLength: 10000 warningTextLength: 500 } From d0a0dc7d37394c0b2db7142dad45d58af4d87503 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Tue, 12 Dec 2017 12:18:17 +0100 Subject: [PATCH 063/196] Show ticket edit form as disable if agent has read only permission. --- .gitlab-ci.yml | 2 ++ .../app/controllers/_application_controller_form.coffee | 3 +++ .../app/controllers/ticket_zoom/sidebar_ticket.coffee | 5 +++-- app/assets/javascripts/app/models/ticket.coffee | 9 +++++++++ app/models/ticket/screen_options.rb | 2 +- 5 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2e89ebccc..366486644 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -342,6 +342,7 @@ test:integration:zendesk_mysql: - rake db:migrate - ruby -I test/ test/integration/zendesk_import_test.rb - rake db:drop + allow_failure: true test:integration:zendesk_postgresql: stage: test @@ -354,6 +355,7 @@ test:integration:zendesk_postgresql: - rake db:migrate - ruby -I test/ test/integration/zendesk_import_test.rb - rake db:drop + allow_failure: true test:integration:otrs_5_mysql: stage: test diff --git a/app/assets/javascripts/app/controllers/_application_controller_form.coffee b/app/assets/javascripts/app/controllers/_application_controller_form.coffee index bc82db034..91fdcbdb1 100644 --- a/app/assets/javascripts/app/controllers/_application_controller_form.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller_form.coffee @@ -86,6 +86,9 @@ class App.ControllerForm extends App.Controller for attribute in @attributes attribute_count = attribute_count + 1 + if @isDisabled == true + attribute.disabled = true + # add item item = @formGenItem(attribute, className, fieldset, attribute_count) item.appendTo(fieldset) diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee index cb656f976..c2c950076 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee @@ -20,8 +20,9 @@ class Edit extends App.ObserverController handlers: [ @ticketFormChanges ] - filter: @formMeta.filter - params: defaults + filter: @formMeta.filter + params: defaults + isDisabled: !ticket.editable() #bookmarkable: true ) diff --git a/app/assets/javascripts/app/models/ticket.coffee b/app/assets/javascripts/app/models/ticket.coffee index a197bba69..463e400c9 100644 --- a/app/assets/javascripts/app/models/ticket.coffee +++ b/app/assets/javascripts/app/models/ticket.coffee @@ -244,3 +244,12 @@ class App.Ticket extends App.Model throw "Unknown operator: #{condition.operator}" result + + editable: -> + group_ids = App.Session.get('group_ids') + if _.isEmpty(group_ids[@group_id]) + return false + else if group_ids[@group_id] && !_.include(group_ids[@group_id], 'edit') && !_.include(group_ids[@group_id], 'full') + return false + true + diff --git a/app/models/ticket/screen_options.rb b/app/models/ticket/screen_options.rb index c15b2ae81..f51aeaf31 100644 --- a/app/models/ticket/screen_options.rb +++ b/app/models/ticket/screen_options.rb @@ -90,7 +90,7 @@ returns filter[:group_id] = [] groups = if params[:current_user].permissions?('ticket.agent') - params[:current_user].groups_access('create') + params[:current_user].groups_access(%w[create edit]) else Group.where(active: true) end From 97a5cd3cf5a81dd9651952613e1bb477dd98e1f6 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Wed, 13 Dec 2017 12:17:57 +0100 Subject: [PATCH 064/196] Improved max. agent limit check. --- app/models/role.rb | 8 ++++---- app/models/user.rb | 6 +++--- test/unit/role_validate_agent_limit_test.rb | 20 ++++++++++++++++++-- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/app/models/role.rb b/app/models/role.rb index 5130be108..130c691d4 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -185,7 +185,7 @@ returns def last_admin_check_admin_count admin_role_ids = Role.joins(:permissions).where(permissions: { name: ['admin', 'admin.user'], active: true }, roles: { active: true }).where.not(id: id).pluck(:id) - User.joins(:roles).where(roles: { id: admin_role_ids }, users: { active: true }).count + User.joins(:roles).where(roles: { id: admin_role_ids }, users: { active: true }).distinct().count end def validate_agent_limit_by_attributes @@ -194,8 +194,8 @@ returns return true if active != true return true if !with_permission?('ticket.agent') ticket_agent_role_ids = Role.joins(:permissions).where(permissions: { name: 'ticket.agent', active: true }, roles: { active: true }).pluck(:id) - currents = User.joins(:roles).where(roles: { id: ticket_agent_role_ids }, users: { active: true }).pluck(:id) - news = User.joins(:roles).where(roles: { id: id }, users: { active: true }).pluck(:id) + currents = User.joins(:roles).where(roles: { id: ticket_agent_role_ids }, users: { active: true }).distinct().pluck(:id) + news = User.joins(:roles).where(roles: { id: id }, users: { active: true }).distinct().pluck(:id) count = currents.concat(news).uniq.count raise Exceptions::UnprocessableEntity, 'Agent limit exceeded, please check your account settings.' if count > Setting.get('system_agent_limit') true @@ -208,7 +208,7 @@ returns return true if permission.name != 'ticket.agent' ticket_agent_role_ids = Role.joins(:permissions).where(permissions: { name: 'ticket.agent' }, roles: { active: true }).pluck(:id) ticket_agent_role_ids.push(id) - count = User.joins(:roles).where(roles: { id: ticket_agent_role_ids }, users: { active: true }).count + count = User.joins(:roles).where(roles: { id: ticket_agent_role_ids }, users: { active: true }).distinct().count raise Exceptions::UnprocessableEntity, 'Agent limit exceeded, please check your account settings.' if count > Setting.get('system_agent_limit') true end diff --git a/app/models/user.rb b/app/models/user.rb index ac6dac3cf..42c6bbc31 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1018,7 +1018,7 @@ raise 'Minimum one user need to have admin permissions' def last_admin_check_admin_count admin_role_ids = Role.joins(:permissions).where(permissions: { name: ['admin', 'admin.user'], active: true }, roles: { active: true }).pluck(:id) - User.joins(:roles).where(roles: { id: admin_role_ids }, users: { active: true }).count - 1 + User.joins(:roles).where(roles: { id: admin_role_ids }, users: { active: true }).distinct().count - 1 end def validate_agent_limit_by_attributes @@ -1027,7 +1027,7 @@ raise 'Minimum one user need to have admin permissions' return true if active != true return true if !permissions?('ticket.agent') ticket_agent_role_ids = Role.joins(:permissions).where(permissions: { name: 'ticket.agent', active: true }, roles: { active: true }).pluck(:id) - count = User.joins(:roles).where(roles: { id: ticket_agent_role_ids }, users: { active: true }).count + 1 + count = User.joins(:roles).where(roles: { id: ticket_agent_role_ids }, users: { active: true }).distinct().count + 1 raise Exceptions::UnprocessableEntity, 'Agent limit exceeded, please check your account settings.' if count > Setting.get('system_agent_limit') true end @@ -1038,7 +1038,7 @@ raise 'Minimum one user need to have admin permissions' return true if role.active != true return true if !role.with_permission?('ticket.agent') ticket_agent_role_ids = Role.joins(:permissions).where(permissions: { name: 'ticket.agent', active: true }, roles: { active: true }).pluck(:id) - count = User.joins(:roles).where(roles: { id: ticket_agent_role_ids }, users: { active: true }).count + count = User.joins(:roles).where(roles: { id: ticket_agent_role_ids }, users: { active: true }).distinct().count # if new added role is a ticket.agent role if ticket_agent_role_ids.include?(role.id) diff --git a/test/unit/role_validate_agent_limit_test.rb b/test/unit/role_validate_agent_limit_test.rb index ded98ea6a..4292e596b 100644 --- a/test/unit/role_validate_agent_limit_test.rb +++ b/test/unit/role_validate_agent_limit_test.rb @@ -73,9 +73,9 @@ class RoleValidateAgentLimit < ActiveSupport::TestCase test 'role validate agent limit - 1 user 2 ticket.agent roles' do - agent_max = User.with_permissions('ticket.agent').count + current_agent_max = User.with_permissions('ticket.agent').count + 1 UserInfo.current_user_id = 1 - Setting.set('system_agent_limit', agent_max + 1) + Setting.set('system_agent_limit', current_agent_max) permission_ticket_agent = Permission.find_by(name: 'ticket.agent') @@ -107,6 +107,8 @@ class RoleValidateAgentLimit < ActiveSupport::TestCase user1.role_ids = [Role.find_by(name: 'Agent').id, role_agent_limit1.id] + user1.role_ids = [Role.find_by(name: 'Agent').id, role_agent_limit1.id, role_agent_limit2.id] + assert_raises(Exceptions::UnprocessableEntity) do user2 = User.create!( firstname: 'Firstname2', @@ -118,6 +120,20 @@ class RoleValidateAgentLimit < ActiveSupport::TestCase ) end + assert_equal(current_agent_max, User.with_permissions('ticket.agent').count) + + current_agent_max = User.with_permissions('ticket.agent').count + 1 + Setting.set('system_agent_limit', current_agent_max) + + user3 = User.create!( + firstname: 'Firstname', + lastname: 'Lastname', + email: 'some-agentlimit-role-3@example.com', + login: 'some-agentlimit-role-3@example.com', + role_ids: [Role.find_by(name: 'Agent').id], + active: true, + ) + role_agent_limit1.destroy! role_agent_limit2.destroy! Setting.set('system_agent_limit', nil) From 504bc58d47f733dc5da2714546ab0671b2f567e2 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Wed, 13 Dec 2017 18:38:21 +0100 Subject: [PATCH 065/196] Fixed issue #1710 - Unable to login with Office365. --- app/models/avatar.rb | 51 +++++++++++++------ db/migrate/20120101000001_create_base.rb | 2 +- ...3000001_change_authorization_token_size.rb | 10 ++++ 3 files changed, 46 insertions(+), 17 deletions(-) create mode 100644 db/migrate/20171213000001_change_authorization_token_size.rb diff --git a/app/models/avatar.rb b/app/models/avatar.rb index 3bc1a9984..fb4b2793b 100644 --- a/app/models/avatar.rb +++ b/app/models/avatar.rb @@ -105,22 +105,41 @@ add avatar by url # fetch image based on http url if data[:url].present? - if data[:url].match?(/^http/) + if data[:url].class == Tempfile + logger.info "Reading image from tempfile '#{data[:url].inspect}'" + content = data[:url].read + filename = data[:url].path + mime_type = 'image' + if filename.match?(/\.png/i) + mime_type = 'image/png' + end + if filename.match?(/\.(jpg|jpeg)/i) + mime_type = 'image/jpeg' + end + data[:resize] ||= {} + data[:resize][:content] = content + data[:resize][:mime_type] = mime_type + data[:full] ||= {} + data[:full][:content] = content + data[:full][:mime_type] = mime_type + + elsif data[:url].to_s.match?(/^http/) + url = data[:url].to_s # check if source ist already updated within last 2 minutes - if avatar_already_exists&.source_url == data[:url] + if avatar_already_exists&.source_url == url return if avatar_already_exists.updated_at > 2.minutes.ago end # twitter workaround to get bigger avatar images # see also https://dev.twitter.com/overview/general/user-profile-images-and-banners - if data[:url].match?(%r{//pbs.twimg.com/}i) - data[:url].sub!(/normal\.(png|jpg|gif)$/, 'bigger.\1') + if url.match?(%r{//pbs.twimg.com/}i) + url.sub!(/normal\.(png|jpg|gif)$/, 'bigger.\1') end # fetch image response = UserAgent.get( - data[:url], + url, {}, { open_timeout: 4, @@ -129,20 +148,19 @@ add avatar by url }, ) if !response.success? - logger.info "Can't fetch '#{data[:url]}' (maybe no avatar available), http code: #{response.code}" + logger.info "Can't fetch '#{url}' (maybe no avatar available), http code: #{response.code}" return end - logger.info "Fetchd image '#{data[:url]}', http code: #{response.code}" + logger.info "Fetchd image '#{url}', http code: #{response.code}" mime_type = 'image' - if data[:url].match?(/\.png/i) + if url.match?(/\.png/i) mime_type = 'image/png' end - if data[:url].match?(/\.(jpg|jpeg)/i) + if url.match?(/\.(jpg|jpeg)/i) mime_type = 'image/jpeg' end - if !data[:resize] - data[:resize] = {} - end + + data[:resize] ||= {} data[:resize][:content] = response.body data[:resize][:mime_type] = mime_type data[:full] ||= {} @@ -150,15 +168,16 @@ add avatar by url data[:full][:mime_type] = mime_type # try zammad backend to find image based on email - elsif data[:url].match?(/@/) + elsif data[:url].to_s.match?(/@/) + url = data[:url].to_s # check if source ist already updated within last 3 minutes - if avatar_already_exists&.source_url == data[:url] + if avatar_already_exists&.source_url == url return if avatar_already_exists.updated_at > 2.minutes.ago end # fetch image - image = Service::Image.user(data[:url]) + image = Service::Image.user(url) return if !image data[:resize] ||= {} data[:resize] = image @@ -337,7 +356,7 @@ returns: store_hash: hash, ) return if !avatar - file = Store.find(avatar.store_resize_id) + Store.find(avatar.store_resize_id) end =begin diff --git a/db/migrate/20120101000001_create_base.rb b/db/migrate/20120101000001_create_base.rb index c74f50836..01834da3c 100644 --- a/db/migrate/20120101000001_create_base.rb +++ b/db/migrate/20120101000001_create_base.rb @@ -201,7 +201,7 @@ class CreateBase < ActiveRecord::Migration[4.2] create_table :authorizations do |t| t.string :provider, limit: 250, null: false t.string :uid, limit: 250, null: false - t.string :token, limit: 250, null: true + t.string :token, limit: 2500, null: true t.string :secret, limit: 250, null: true t.string :username, limit: 250, null: true t.references :user, null: false diff --git a/db/migrate/20171213000001_change_authorization_token_size.rb b/db/migrate/20171213000001_change_authorization_token_size.rb new file mode 100644 index 000000000..34a71e23f --- /dev/null +++ b/db/migrate/20171213000001_change_authorization_token_size.rb @@ -0,0 +1,10 @@ +class ChangeAuthorizationTokenSize < ActiveRecord::Migration[5.1] + def up + # return if it's a new setup to avoid running the migration + return if !Setting.find_by(name: 'system_init_done') + + change_column :authorizations, :token, :string, limit: 2500 + + end + +end From 5ebeb51e2ec9596edf7f4c2e88d4ac1658ff6393 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Wed, 13 Dec 2017 19:20:37 +0100 Subject: [PATCH 066/196] Fixed issue #1711 - Zammad Api for idoit.object_ids broken. --- app/models/ticket/search.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/ticket/search.rb b/app/models/ticket/search.rb index aa1bea338..b6d91e64a 100644 --- a/app/models/ticket/search.rb +++ b/app/models/ticket/search.rb @@ -99,7 +99,7 @@ returns end # try search index backend - if !condition && SearchIndexBackend.enabled? + if condition.blank? && SearchIndexBackend.enabled? query_extention = {} query_extention['bool'] = {} query_extention['bool']['must'] = [] From e460c99cadb63591b1f154f45e7ff1f7cc0d8c57 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Thu, 14 Dec 2017 14:19:24 +0100 Subject: [PATCH 067/196] Introduced REST expand=true/false/1/0, full=true/false/1/0 and all=true/false/1/0 options. Improved controller tests. @hanneshal --- .gitlab-ci.yml | 6 +- .../javascripts/app/controllers/users.coffee | 4 +- app/controllers/application_controller.rb | 1 + .../has_response_extentions.rb | 31 + .../application_controller/renders_models.rb | 28 +- app/controllers/applications_controller.rb | 2 +- .../online_notifications_controller.rb | 4 +- app/controllers/organizations_controller.rb | 15 +- app/controllers/slas_controller.rb | 2 +- app/controllers/ticket_articles_controller.rb | 20 +- app/controllers/tickets_controller.rb | 40 +- app/controllers/users_controller.rb | 90 +- app/models/application_model/can_assets.rb | 2 +- app/models/ticket/search.rb | 2 +- db/seeds/community_user_resources.rb | 42 +- .../organization_controller_test.rb | 513 ++++++++++ test/controllers/tickets_controller_test.rb | 351 ++++++- test/controllers/user_controller_test.rb | 960 ++++++++++++++++++ test/integration/report_test.rb | 13 +- 19 files changed, 2015 insertions(+), 111 deletions(-) create mode 100644 app/controllers/application_controller/has_response_extentions.rb create mode 100644 test/controllers/organization_controller_test.rb create mode 100644 test/controllers/user_controller_test.rb diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 366486644..2b5140aa4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -310,7 +310,8 @@ test:integration:es_mysql: - ruby -I test/ test/controllers/search_controller_test.rb - ruby -I test/ test/integration/report_test.rb - ruby -I test/ test/controllers/form_controller_test.rb - - ruby -I test/ test/controllers/user_organization_controller_test.rb + - ruby -I test/ test/controllers/user_controller_test.rb + - ruby -I test/ test/controllers/organization_controller_test.rb - rake db:drop test:integration:es_postgresql: @@ -328,7 +329,8 @@ test:integration:es_postgresql: - ruby -I test/ test/controllers/search_controller_test.rb - ruby -I test/ test/integration/report_test.rb - ruby -I test/ test/controllers/form_controller_test.rb - - ruby -I test/ test/controllers/user_organization_controller_test.rb + - ruby -I test/ test/controllers/user_controller_test.rb + - ruby -I test/ test/controllers/organization_controller_test.rb - rake db:drop test:integration:zendesk_mysql: diff --git a/app/assets/javascripts/app/controllers/users.coffee b/app/assets/javascripts/app/controllers/users.coffee index 07b893a58..27b464580 100644 --- a/app/assets/javascripts/app/controllers/users.coffee +++ b/app/assets/javascripts/app/controllers/users.coffee @@ -145,7 +145,7 @@ class Index extends App.ControllerSubContent query: @query limit: 140 role_ids: role_ids - full: 1 + full: true processData: true, success: (data, status, xhr) => App.Collection.loadAssets(data.assets) @@ -167,7 +167,7 @@ class Index extends App.ControllerSubContent data: limit: 50 role_ids: role_ids - full: 1 + full: true processData: true success: (data, status, xhr) => App.Collection.loadAssets(data.assets) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1d37f3f60..a2a6a6343 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -10,6 +10,7 @@ class ApplicationController < ActionController::Base include ApplicationController::ChecksMaintainance include ApplicationController::RendersModels include ApplicationController::HasUser + include ApplicationController::HasResponseExtentions include ApplicationController::PreventsCsrf include ApplicationController::LogsHttpAccess include ApplicationController::ChecksAccess diff --git a/app/controllers/application_controller/has_response_extentions.rb b/app/controllers/application_controller/has_response_extentions.rb new file mode 100644 index 000000000..284573cb6 --- /dev/null +++ b/app/controllers/application_controller/has_response_extentions.rb @@ -0,0 +1,31 @@ +module ApplicationController::HasResponseExtentions + extend ActiveSupport::Concern + + private + + def response_expand? + return true if params[:expand] == true + return true if params[:expand] == 'true' + return true if params[:expand] == 1 + return true if params[:expand] == '1' + + false + end + + def response_full? + return true if params[:full] == true + return true if params[:full] == 'true' + return true if params[:full] == 1 + return true if params[:full] == '1' + false + end + + def response_all? + return true if params[:all] == true + return true if params[:all] == 'true' + return true if params[:all] == 1 + return true if params[:all] == '1' + false + end + +end diff --git a/app/controllers/application_controller/renders_models.rb b/app/controllers/application_controller/renders_models.rb index 09fd9db7d..d6be04207 100644 --- a/app/controllers/application_controller/renders_models.rb +++ b/app/controllers/application_controller/renders_models.rb @@ -18,11 +18,16 @@ module ApplicationController::RendersModels # set relations generic_object.associations_from_param(params) - if params[:expand] + if response_expand? render json: generic_object.attributes_with_association_names, status: :created return end + if response_full? + render json: generic_object.class.full(generic_object.id), status: :created + return + end + model_create_render_item(generic_object) end @@ -47,11 +52,16 @@ module ApplicationController::RendersModels generic_object.associations_from_param(params) end - if params[:expand] + if response_expand? render json: generic_object.attributes_with_association_names, status: :ok return end + if response_full? + render json: generic_object.class.full(generic_object.id), status: :ok + return + end + model_update_render_item(generic_object) end @@ -71,20 +81,18 @@ module ApplicationController::RendersModels def model_show_render(object, params) - if params[:expand] + if response_expand? generic_object = object.find(params[:id]) render json: generic_object.attributes_with_association_names, status: :ok return end - if params[:full] - generic_object_full = object.full(params[:id]) - render json: generic_object_full, status: :ok + if response_full? + render json: object.full(params[:id]), status: :ok return end - generic_object = object.find(params[:id]) - model_show_render_item(generic_object) + model_show_render_item(object.find(params[:id])) end def model_show_render_item(generic_object) @@ -109,7 +117,7 @@ module ApplicationController::RendersModels object.all.order(id: 'ASC').offset(offset).limit(limit) end - if params[:expand] + if response_expand? list = [] generic_objects.each do |generic_object| list.push generic_object.attributes_with_association_names @@ -118,7 +126,7 @@ module ApplicationController::RendersModels return end - if params[:full] + if response_full? assets = {} item_ids = [] generic_objects.each do |item| diff --git a/app/controllers/applications_controller.rb b/app/controllers/applications_controller.rb index 6cf408d1b..e9f52145c 100644 --- a/app/controllers/applications_controller.rb +++ b/app/controllers/applications_controller.rb @@ -5,7 +5,7 @@ class ApplicationsController < ApplicationController def index all = Doorkeeper::Application.all - if params[:full] + if response_full? assets = {} item_ids = [] all.each do |item| diff --git a/app/controllers/online_notifications_controller.rb b/app/controllers/online_notifications_controller.rb index 3b499453c..a7fd3cf44 100644 --- a/app/controllers/online_notifications_controller.rb +++ b/app/controllers/online_notifications_controller.rb @@ -47,7 +47,7 @@ curl http://localhost/api/v1/online_notifications.json -v -u #{login}:#{password =end def index - if params[:full] + if response_full? render json: OnlineNotification.list_full(current_user, 200) return end @@ -149,7 +149,7 @@ curl http://localhost/api/v1/online_notifications/mark_all_as_read -v -u #{login notifications = OnlineNotification.list(current_user, 200) notifications.each do |notification| if !notification['seen'] - OnlineNotification.seen( id: notification['id'] ) + OnlineNotification.seen(id: notification['id']) end end render json: {}, status: :ok diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index d42096466..959bc175e 100644 --- a/app/controllers/organizations_controller.rb +++ b/app/controllers/organizations_controller.rb @@ -69,7 +69,7 @@ curl http://localhost/api/v1/organizations -v -u #{login}:#{password} organizations = Organization.all.order(id: 'ASC').offset(offset).limit(per_page) end - if params[:expand] + if response_expand? list = [] organizations.each do |organization| list.push organization.attributes_with_association_names @@ -78,7 +78,7 @@ curl http://localhost/api/v1/organizations -v -u #{login}:#{password} return end - if params[:full] + if response_full? assets = {} item_ids = [] organizations.each do |item| @@ -91,6 +91,7 @@ curl http://localhost/api/v1/organizations -v -u #{login}:#{password} }, status: :ok return end + list = [] organizations.each do |organization| list.push organization.attributes_with_association_ids @@ -126,15 +127,15 @@ curl http://localhost/api/v1/organizations/#{id} -v -u #{login}:#{password} raise Exceptions::NotAuthorized if params[:id].to_i != current_user.organization_id end - if params[:expand] + if response_expand? organization = Organization.find(params[:id]).attributes_with_association_names render json: organization, status: :ok return end - if params[:full] + if response_full? full = Organization.full(params[:id]) - render json: full + render json: full, status: :ok return end @@ -259,7 +260,7 @@ curl http://localhost/api/v1/organization/{id} -v -u #{login}:#{password} -H "Co organization_all = organization_all[offset, params[:per_page].to_i] || [] end - if params[:expand] + if response_expand? list = [] organization_all.each do |organization| list.push organization.attributes_with_association_names @@ -281,7 +282,7 @@ curl http://localhost/api/v1/organization/{id} -v -u #{login}:#{password} -H "Co return end - if params[:full] + if response_full? organization_ids = [] assets = {} organization_all.each do |organization| diff --git a/app/controllers/slas_controller.rb b/app/controllers/slas_controller.rb index 574738ee5..09df65ff6 100644 --- a/app/controllers/slas_controller.rb +++ b/app/controllers/slas_controller.rb @@ -48,7 +48,7 @@ curl http://localhost/api/v1/slas.json -v -u #{login}:#{password} def index - if params[:full] + if response_full? # calendars assets = {} diff --git a/app/controllers/ticket_articles_controller.rb b/app/controllers/ticket_articles_controller.rb index cf4e5c98d..786748a5c 100644 --- a/app/controllers/ticket_articles_controller.rb +++ b/app/controllers/ticket_articles_controller.rb @@ -17,13 +17,13 @@ class TicketArticlesController < ApplicationController article = Ticket::Article.find(params[:id]) access!(article, 'read') - if params[:expand] + if response_expand? result = article.attributes_with_association_names render json: result, status: :ok return end - if params[:full] + if response_full? full = Ticket::Article.full(params[:id]) render json: full return @@ -39,7 +39,7 @@ class TicketArticlesController < ApplicationController articles = [] - if params[:expand] + if response_expand? ticket.articles.each do |article| # ignore internal article if customer is requesting @@ -52,7 +52,7 @@ class TicketArticlesController < ApplicationController return end - if params[:full] + if response_full? assets = {} record_ids = [] ticket.articles.each do |article| @@ -66,7 +66,7 @@ class TicketArticlesController < ApplicationController render json: { record_ids: record_ids, assets: assets, - } + }, status: :ok return end @@ -76,7 +76,7 @@ class TicketArticlesController < ApplicationController next if article.internal == true && current_user.permissions?('ticket.customer') articles.push article.attributes_with_association_names end - render json: articles + render json: articles, status: :ok end # POST /articles @@ -85,13 +85,13 @@ class TicketArticlesController < ApplicationController access!(ticket, 'create') article = article_create(ticket, params) - if params[:expand] + if response_expand? result = article.attributes_with_association_names render json: result, status: :created return end - if params[:full] + if response_full? full = Ticket::Article.full(params[:id]) render json: full, status: :created return @@ -114,13 +114,13 @@ class TicketArticlesController < ApplicationController article.update!(clean_params) - if params[:expand] + if response_expand? result = article.attributes_with_association_names render json: result, status: :ok return end - if params[:full] + if response_full? full = Ticket::Article.full(params[:id]) render json: full, status: :ok return diff --git a/app/controllers/tickets_controller.rb b/app/controllers/tickets_controller.rb index df8b29ae1..01703bc91 100644 --- a/app/controllers/tickets_controller.rb +++ b/app/controllers/tickets_controller.rb @@ -24,7 +24,7 @@ class TicketsController < ApplicationController access_condition = Ticket.access_condition(current_user, 'read') tickets = Ticket.where(access_condition).order(id: 'ASC').offset(offset).limit(per_page) - if params[:expand] + if response_expand? list = [] tickets.each do |ticket| list.push ticket.attributes_with_association_names @@ -33,7 +33,7 @@ class TicketsController < ApplicationController return end - if params[:full] + if response_full? assets = {} item_ids = [] tickets.each do |item| @@ -55,19 +55,19 @@ class TicketsController < ApplicationController ticket = Ticket.find(params[:id]) access!(ticket, 'read') - if params[:expand] + if response_expand? result = ticket.attributes_with_association_names render json: result, status: :ok return end - if params[:full] + if response_full? full = Ticket.full(params[:id]) render json: full return end - if params[:all] + if response_all? render json: ticket_all(ticket) return end @@ -163,18 +163,24 @@ class TicketsController < ApplicationController end end - if params[:expand] + if response_expand? result = ticket.reload.attributes_with_association_names render json: result, status: :created return end - if params[:all] - render json: ticket_all(ticket.reload) + if response_full? + full = Ticket.full(ticket.id) + render json: full, status: :created return end - render json: ticket.reload, status: :created + if response_all? + render json: ticket_all(ticket.reload), status: :created + return + end + + render json: ticket.reload.attributes_with_association_ids, status: :created end # PUT /api/v1/tickets/1 @@ -199,18 +205,24 @@ class TicketsController < ApplicationController end end - if params[:expand] + if response_expand? result = ticket.reload.attributes_with_association_names render json: result, status: :ok return end - if params[:all] - render json: ticket_all(ticket.reload) + if response_full? + full = Ticket.full(params[:id]) + render json: full, status: :ok return end - render json: ticket.reload, status: :ok + if response_all? + render json: ticket_all(ticket.reload), status: :ok + return + end + + render json: ticket.reload.attributes_with_association_ids, status: :ok end # DELETE /api/v1/tickets/1 @@ -410,7 +422,7 @@ class TicketsController < ApplicationController tickets = tickets[offset, params[:per_page].to_i] || [] end - if params[:expand] + if response_expand? list = [] tickets.each do |ticket| list.push ticket.attributes_with_association_names diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index c356a51ce..687a14f22 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -32,7 +32,7 @@ class UsersController < ApplicationController User.all.order(id: 'ASC').offset(offset).limit(per_page) end - if params[:expand] + if response_expand? list = [] users.each do |user| list.push user.attributes_with_association_names @@ -41,7 +41,7 @@ class UsersController < ApplicationController return end - if params[:full] + if response_full? assets = {} item_ids = [] users.each do |item| @@ -78,18 +78,24 @@ class UsersController < ApplicationController user = User.find(params[:id]) access!(user, 'read') - if params[:expand] + if response_expand? result = user.attributes_with_association_names - elsif params[:full] - result = { - id: params[:id], - assets: user.assets({}), - } - else - result = user.attributes_with_association_ids result.delete('password') + render json: result + return end + if response_full? + result = { + id: user.id, + assets: user.assets({}), + } + render json: result + return + end + + result = user.attributes_with_association_ids + result.delete('password') render json: result end @@ -198,7 +204,7 @@ class UsersController < ApplicationController end # send inviteation if needed / only if session exists - if params[:invite] && current_user + if params[:invite].present? && current_user token = Token.create(action: 'PasswordReset', user_id: user.id) NotificationFactory::Mailer.notification( template: 'user_invite', @@ -212,7 +218,7 @@ class UsersController < ApplicationController end # send email verify - if params[:signup] && !current_user + if params[:signup].present? && !current_user result = User.signup_new_token(user) NotificationFactory::Mailer.notification( template: 'signup', @@ -221,15 +227,25 @@ class UsersController < ApplicationController ) end - if params[:expand] - user = User.find(user.id).attributes_with_association_names + if response_expand? + user = user.reload.attributes_with_association_names + user.delete('password') render json: user, status: :created return end - user_new = User.find(user.id).attributes_with_association_ids - user_new.delete('password') - render json: user_new, status: :created + if response_full? + result = { + id: user.id, + assets: user.assets({}), + } + render json: result, status: :created + return + end + + user = user.reload.attributes_with_association_ids + user.delete('password') + render json: user, status: :created end # @path [PUT] /users/{id} @@ -269,18 +285,27 @@ class UsersController < ApplicationController if current_user.permissions?(['admin.user', 'ticket.agent']) && (params[:organization_ids] || params[:organizations]) user.associations_from_param(organization_ids: params[:organization_ids], organizations: params[:organizations]) end - - if params[:expand] - user = User.find(user.id).attributes_with_association_names - render json: user, status: :ok - return - end end - # get new data - user_new = User.find(user.id).attributes_with_association_ids - user_new.delete('password') - render json: user_new, status: :ok + if response_expand? + user = user.reload.attributes_with_association_names + user.delete('password') + render json: user, status: :ok + return + end + + if response_full? + result = { + id: user.id, + assets: user.assets({}), + } + render json: result, status: :ok + return + end + + user = user.reload.attributes_with_association_ids + user.delete('password') + render json: user, status: :ok end # @path [DELETE] /users/{id} @@ -311,13 +336,14 @@ class UsersController < ApplicationController # @response_message 401 Invalid session. def me - if params[:expand] + if response_expand? user = current_user.attributes_with_association_names + user.delete('password') render json: user, status: :ok return end - if params[:full] + if response_full? full = User.full(current_user.id) render json: full return @@ -387,7 +413,7 @@ class UsersController < ApplicationController user_all = user_all[offset, params[:per_page].to_i] || [] end - if params[:expand] + if response_expand? list = [] user_all.each do |user| list.push user.attributes_with_association_names @@ -413,7 +439,7 @@ class UsersController < ApplicationController return end - if params[:full] + if response_full? user_ids = [] assets = {} user_all.each do |user| @@ -467,7 +493,7 @@ class UsersController < ApplicationController end # build result list - if !params[:full] + if !response_full? users = [] user_all.each do |user| realname = user.firstname.to_s + ' ' + user.lastname.to_s diff --git a/app/models/application_model/can_assets.rb b/app/models/application_model/can_assets.rb index 42f2fee84..e0e084a31 100644 --- a/app/models/application_model/can_assets.rb +++ b/app/models/application_model/can_assets.rb @@ -109,7 +109,7 @@ return object and assets object = find(id) assets = object.assets({}) { - id: id, + id: object.id, assets: assets, } end diff --git a/app/models/ticket/search.rb b/app/models/ticket/search.rb index b6d91e64a..ef8f487dc 100644 --- a/app/models/ticket/search.rb +++ b/app/models/ticket/search.rb @@ -94,7 +94,7 @@ returns limit = params[:limit] || 12 current_user = params[:current_user] full = false - if params[:full] || !params.key?(:full) + if params[:full] == true || params[:full] == 'true' || !params.key?(:full) full = true end diff --git a/db/seeds/community_user_resources.rb b/db/seeds/community_user_resources.rb index a8c4aff12..12e467557 100644 --- a/db/seeds/community_user_resources.rb +++ b/db/seeds/community_user_resources.rb @@ -16,29 +16,31 @@ user_community = User.create_or_update( UserInfo.current_user_id = user_community.id -ticket = Ticket.create!( - group_id: Group.find_by(name: 'Users').id, - customer_id: User.find_by(login: 'nicole.braun@zammad.org').id, - title: 'Welcome to Zammad!', -) -Ticket::Article.create!( - ticket_id: ticket.id, - type_id: Ticket::Article::Type.find_by(name: 'phone').id, - sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id, - from: 'Zammad Feedback ', - body: 'Welcome! +if Ticket.count.zero? + ticket = Ticket.create!( + group_id: Group.find_by(name: 'Users').id, + customer_id: User.find_by(login: 'nicole.braun@zammad.org').id, + title: 'Welcome to Zammad!', + ) + Ticket::Article.create!( + ticket_id: ticket.id, + type_id: Ticket::Article::Type.find_by(name: 'phone').id, + sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id, + from: 'Zammad Feedback ', + body: 'Welcome! -Thank you for choosing Zammad. + Thank you for choosing Zammad. -You will find updates and patches at https://zammad.org/. Online -documentation is available at https://zammad.org/documentation. Get -involved (discussions, contributing, ...) at https://zammad.org/participate. + You will find updates and patches at https://zammad.org/. Online + documentation is available at https://zammad.org/documentation. Get + involved (discussions, contributing, ...) at https://zammad.org/participate. -Regards, + Regards, -Your Zammad Team -', - internal: false, -) + Your Zammad Team + ', + internal: false, + ) +end UserInfo.current_user_id = 1 diff --git a/test/controllers/organization_controller_test.rb b/test/controllers/organization_controller_test.rb new file mode 100644 index 000000000..1ba5f2849 --- /dev/null +++ b/test/controllers/organization_controller_test.rb @@ -0,0 +1,513 @@ + +require 'test_helper' +require 'rake' + +class OrganizationControllerTest < 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 + + @backup_admin = User.create_or_update( + login: 'backup-admin', + firstname: 'Backup', + lastname: 'Agent', + email: 'backup-admin@example.com', + password: 'adminpw', + active: true, + roles: roles, + groups: groups, + ) + + @admin = User.create_or_update( + login: 'rest-admin', + firstname: 'Rest', + lastname: 'Agent', + email: 'rest-admin@example.com', + password: 'adminpw', + active: true, + roles: roles, + groups: groups, + ) + + # create agent + roles = Role.where(name: 'Agent') + @agent = User.create_or_update( + login: 'rest-agent@example.com', + firstname: 'Rest', + lastname: 'Agent', + email: 'rest-agent@example.com', + password: 'agentpw', + active: true, + roles: roles, + groups: groups, + ) + + # create customer without org + roles = Role.where(name: 'Customer') + @customer_without_org = User.create_or_update( + login: 'rest-customer1@example.com', + firstname: 'Rest', + lastname: 'Customer1', + email: 'rest-customer1@example.com', + password: 'customer1pw', + active: true, + roles: roles, + ) + + # create orgs + @organization = Organization.create_or_update( + name: 'Rest Org', + ) + @organization2 = Organization.create_or_update( + name: 'Rest Org #2', + ) + @organization3 = Organization.create_or_update( + name: 'Rest Org #3', + ) + + # create customer with org + @customer_with_org = User.create_or_update( + login: 'rest-customer2@example.com', + firstname: 'Rest', + lastname: 'Customer2', + email: 'rest-customer2@example.com', + password: 'customer2pw', + active: true, + roles: roles, + organization_id: @organization.id, + ) + + # configure es + if ENV['ES_URL'].present? + #fail "ERROR: Need ES_URL - hint ES_URL='http://127.0.0.1:9200'" + Setting.set('es_url', ENV['ES_URL']) + + # Setting.set('es_url', 'http://127.0.0.1:9200') + # Setting.set('es_index', 'estest.local_zammad') + # Setting.set('es_user', 'elasticsearch') + # Setting.set('es_password', 'zammad') + + if ENV['ES_INDEX_RAND'].present? + ENV['ES_INDEX'] = "es_index_#{rand(999_999_999)}" + end + if ENV['ES_INDEX'].blank? + raise "ERROR: Need ES_INDEX - hint ES_INDEX='estest.local_zammad'" + end + Setting.set('es_index', ENV['ES_INDEX']) + + travel 1.minute + + # drop/create indexes + Rake::Task.clear + Zammad::Application.load_tasks + #Rake::Task["searchindex:drop"].execute + #Rake::Task["searchindex:create"].execute + Rake::Task['searchindex:rebuild'].execute + + # execute background jobs + Scheduler.worker(true) + + sleep 6 + end + + UserInfo.current_user_id = nil + end + + test 'organization index with agent' do + + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('rest-agent@example.com', 'agentpw') + + # index + get '/api/v1/organizations', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(result.class, Array) + assert_equal(result[0]['member_ids'].class, Array) + assert(result.length >= 3) + + get '/api/v1/organizations?limit=40&page=1&per_page=2', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + organizations = Organization.order(:id).limit(2) + assert_equal(organizations[0].id, result[0]['id']) + assert_equal(organizations[0].member_ids, result[0]['member_ids']) + assert_equal(organizations[1].id, result[1]['id']) + assert_equal(organizations[1].member_ids, result[1]['member_ids']) + assert_equal(2, result.count) + + get '/api/v1/organizations?limit=40&page=2&per_page=2', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + organizations = Organization.order(:id).limit(4) + assert_equal(organizations[2].id, result[0]['id']) + assert_equal(organizations[2].member_ids, result[0]['member_ids']) + assert_equal(organizations[3].id, result[1]['id']) + assert_equal(organizations[3].member_ids, result[1]['member_ids']) + + assert_equal(2, result.count) + + # show/:id + get "/api/v1/organizations/#{@organization.id}", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(result.class, Hash) + assert_equal(result['member_ids'].class, Array) + assert_not(result['members']) + assert_equal(result['name'], 'Rest Org') + + get "/api/v1/organizations/#{@organization2.id}", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(result.class, Hash) + assert_equal(result['member_ids'].class, Array) + assert_not(result['members']) + assert_equal(result['name'], 'Rest Org #2') + + # search as agent + Scheduler.worker(true) + get "/api/v1/organizations/search?query=#{CGI.escape('Zammad')}", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + assert_equal('Zammad Foundation', result[0]['name']) + assert(result[0]['member_ids']) + assert_not(result[0]['members']) + + get "/api/v1/organizations/search?query=#{CGI.escape('Zammad')}&expand=true", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + assert_equal('Zammad Foundation', result[0]['name']) + assert(result[0]['member_ids']) + assert(result[0]['members']) + + get "/api/v1/organizations/search?query=#{CGI.escape('Zammad')}&label=true", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + assert_equal('Zammad Foundation', result[0]['label']) + assert_equal('Zammad Foundation', result[0]['value']) + assert_not(result[0]['member_ids']) + assert_not(result[0]['members']) + end + + test 'organization index with customer1' do + + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('rest-customer1@example.com', 'customer1pw') + + # index + get '/api/v1/organizations', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(result.class, Array) + assert_equal(result.length, 0) + + # show/:id + get "/api/v1/organizations/#{@organization.id}", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(result.class, Hash) + assert_nil(result['name']) + + get "/api/v1/organizations/#{@organization2.id}", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(result.class, Hash) + assert_nil(result['name']) + + # search + Scheduler.worker(true) + get "/api/v1/organizations/search?query=#{CGI.escape('Zammad')}", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(401) + end + + test 'organization index with customer2' do + + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('rest-customer2@example.com', 'customer2pw') + + # index + get '/api/v1/organizations', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(result.class, Array) + assert_equal(result.length, 1) + + # show/:id + get "/api/v1/organizations/#{@organization.id}", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(result.class, Hash) + assert_equal(result['name'], 'Rest Org') + + get "/api/v1/organizations/#{@organization2.id}", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(401) + result = JSON.parse(@response.body) + assert_equal(result.class, Hash) + assert_nil(result['name']) + + # search + Scheduler.worker(true) + get "/api/v1/organizations/search?query=#{CGI.escape('Zammad')}", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(401) + end + + test '04.01 organization show and response format' do + organization = Organization.create_or_update( + name: 'Rest Org NEW', + members: [@customer_without_org], + updated_by_id: @admin.id, + created_by_id: @admin.id, + ) + + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('rest-admin@example.com', 'adminpw') + get "/api/v1/organizations/#{organization.id}", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal(organization.id, result['id']) + assert_equal(organization.name, result['name']) + assert_not(result['members']) + assert_equal([@customer_without_org.id], result['member_ids']) + assert_equal(@admin.id, result['updated_by_id']) + assert_equal(@admin.id, result['created_by_id']) + + get "/api/v1/organizations/#{organization.id}?expand=true", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal(organization.id, result['id']) + assert_equal(organization.name, result['name']) + assert(result['members']) + assert_equal([@customer_without_org.id], result['member_ids']) + assert_equal(@admin.id, result['updated_by_id']) + assert_equal(@admin.id, result['created_by_id']) + + get "/api/v1/organizations/#{organization.id}?expand=false", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal(organization.id, result['id']) + assert_equal(organization.name, result['name']) + assert_not(result['members']) + assert_equal([@customer_without_org.id], result['member_ids']) + assert_equal(@admin.id, result['updated_by_id']) + assert_equal(@admin.id, result['created_by_id']) + + get "/api/v1/organizations/#{organization.id}?full=true", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + + assert_equal(Hash, result.class) + assert_equal(organization.id, result['id']) + assert(result['assets']) + assert(result['assets']['Organization']) + assert(result['assets']['Organization'][organization.id.to_s]) + assert_equal(organization.id, result['assets']['Organization'][organization.id.to_s]['id']) + assert_equal(organization.name, result['assets']['Organization'][organization.id.to_s]['name']) + assert_equal(organization.member_ids, result['assets']['Organization'][organization.id.to_s]['member_ids']) + assert_not(result['assets']['Organization'][organization.id.to_s]['members']) + + get "/api/v1/organizations/#{organization.id}?full=false", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal(organization.id, result['id']) + assert_equal(organization.name, result['name']) + assert_not(result['members']) + assert_equal([@customer_without_org.id], result['member_ids']) + assert_equal(@admin.id, result['updated_by_id']) + assert_equal(@admin.id, result['created_by_id']) + end + + test '04.02 organization index and response format' do + organization = Organization.create_or_update( + name: 'Rest Org NEW', + members: [@customer_without_org], + updated_by_id: @admin.id, + created_by_id: @admin.id, + ) + + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('rest-admin@example.com', 'adminpw') + get '/api/v1/organizations', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + assert_equal(Hash, result[0].class) + assert_equal(organization.id, result.last['id']) + assert_equal(organization.name, result.last['name']) + assert_not(result.last['members']) + assert_equal(organization.member_ids, result.last['member_ids']) + assert_equal(@admin.id, result.last['updated_by_id']) + assert_equal(@admin.id, result.last['created_by_id']) + + get '/api/v1/organizations?expand=true', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + assert_equal(Hash, result[0].class) + assert_equal(organization.id, result.last['id']) + assert_equal(organization.name, result.last['name']) + assert_equal(organization.member_ids, result.last['member_ids']) + assert_equal(organization.members.pluck(:login), [@customer_without_org.login]) + assert_equal(@admin.id, result.last['updated_by_id']) + assert_equal(@admin.id, result.last['created_by_id']) + + get '/api/v1/organizations?expand=false', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + assert_equal(Hash, result[0].class) + assert_equal(organization.id, result.last['id']) + assert_equal(organization.name, result.last['name']) + assert_not(result.last['members']) + assert_equal(organization.member_ids, result.last['member_ids']) + assert_equal(@admin.id, result.last['updated_by_id']) + assert_equal(@admin.id, result.last['created_by_id']) + + get '/api/v1/organizations?full=true', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + + assert_equal(Hash, result.class) + assert_equal(Array, result['record_ids'].class) + assert_equal(1, result['record_ids'][0]) + assert_equal(organization.id, result['record_ids'].last) + assert(result['assets']) + assert(result['assets']['Organization']) + assert(result['assets']['Organization'][organization.id.to_s]) + assert_equal(organization.id, result['assets']['Organization'][organization.id.to_s]['id']) + assert_equal(organization.name, result['assets']['Organization'][organization.id.to_s]['name']) + assert_equal(organization.member_ids, result['assets']['Organization'][organization.id.to_s]['member_ids']) + assert_not(result['assets']['Organization'][organization.id.to_s]['members']) + + get '/api/v1/organizations?full=false', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + assert_equal(Hash, result[0].class) + assert_equal(organization.id, result.last['id']) + assert_equal(organization.name, result.last['name']) + assert_not(result.last['members']) + assert_equal(organization.member_ids, result.last['member_ids']) + assert_equal(@admin.id, result.last['updated_by_id']) + assert_equal(@admin.id, result.last['created_by_id']) + end + + test '04.03 ticket create and response format' do + params = { + name: 'Rest Org NEW', + members: [@customer_without_org.login], + } + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('rest-admin@example.com', 'adminpw') + + post '/api/v1/organizations', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(201) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + + organization = Organization.find(result['id']) + assert_equal(organization.name, result['name']) + assert_equal(organization.member_ids, result['member_ids']) + assert_not(result['members']) + assert_equal(@admin.id, result['updated_by_id']) + assert_equal(@admin.id, result['created_by_id']) + + params[:name] = 'Rest Org NEW #2' + post '/api/v1/organizations?expand=true', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(201) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + + organization = Organization.find(result['id']) + assert_equal(organization.name, result['name']) + assert_equal(organization.member_ids, result['member_ids']) + assert_equal(organization.members.pluck(:login), result['members']) + assert_equal(@admin.id, result['updated_by_id']) + assert_equal(@admin.id, result['created_by_id']) + + params[:name] = 'Rest Org NEW #3' + post '/api/v1/organizations?full=true', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(201) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + + organization = Organization.find(result['id']) + assert(result['assets']) + assert(result['assets']['Organization']) + assert(result['assets']['Organization'][organization.id.to_s]) + assert_equal(organization.id, result['assets']['Organization'][organization.id.to_s]['id']) + assert_equal(organization.name, result['assets']['Organization'][organization.id.to_s]['name']) + assert_equal(organization.member_ids, result['assets']['Organization'][organization.id.to_s]['member_ids']) + assert_not(result['assets']['Organization'][organization.id.to_s]['members']) + + end + + test '04.04 ticket update and response formats' do + organization = Organization.create_or_update( + name: 'Rest Org NEW', + members: [@customer_without_org], + updated_by_id: @admin.id, + created_by_id: @admin.id, + ) + + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('rest-admin@example.com', 'adminpw') + + params = { + name: 'a update name #1', + } + put "/api/v1/organizations/#{organization.id}", params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + + organization = Organization.find(result['id']) + assert_equal(params[:name], result['name']) + assert_equal(organization.member_ids, result['member_ids']) + assert_not(result['members']) + assert_equal(@admin.id, result['updated_by_id']) + assert_equal(@admin.id, result['created_by_id']) + + params = { + name: 'a update name #2', + } + put "/api/v1/organizations/#{organization.id}?expand=true", params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + + organization = Organization.find(result['id']) + assert_equal(params[:name], result['name']) + assert_equal(organization.member_ids, result['member_ids']) + assert_equal(organization.members.pluck(:login), [@customer_without_org.login]) + assert_equal(@admin.id, result['updated_by_id']) + assert_equal(@admin.id, result['created_by_id']) + + params = { + name: 'a update name #3', + } + put "/api/v1/organizations/#{organization.id}?full=true", params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + + organization = Organization.find(result['id']) + assert(result['assets']) + assert(result['assets']['Organization']) + assert(result['assets']['Organization'][organization.id.to_s]) + assert_equal(organization.id, result['assets']['Organization'][organization.id.to_s]['id']) + assert_equal(params[:name], result['assets']['Organization'][organization.id.to_s]['name']) + assert_equal(organization.member_ids, result['assets']['Organization'][organization.id.to_s]['member_ids']) + assert_not(result['assets']['Organization'][organization.id.to_s]['members']) + + end + +end diff --git a/test/controllers/tickets_controller_test.rb b/test/controllers/tickets_controller_test.rb index aaf571fab..4d4ad6dce 100644 --- a/test/controllers/tickets_controller_test.rb +++ b/test/controllers/tickets_controller_test.rb @@ -47,7 +47,7 @@ class TicketsControllerTest < ActionDispatch::IntegrationTest active: true, roles: roles, ) - + UserInfo.current_user_id = nil end test '01.01 ticket create with agent - missing group' do @@ -1107,4 +1107,353 @@ AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO assert_equal('Not authorized (admin permission required)!', result['error']) end + test '04.01 ticket show and response format' do + title = "ticket testagent#{rand(999_999_999)}" + ticket = Ticket.create!( + title: title, + group: Group.lookup(name: 'Users'), + customer_id: @customer_without_org.id, + state: Ticket::State.lookup(name: 'new'), + priority: Ticket::Priority.lookup(name: '2 normal'), + updated_by_id: @agent.id, + created_by_id: @agent.id, + ) + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('tickets-agent@example.com', 'agentpw') + get "/api/v1/tickets/#{ticket.id}", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal(ticket.id, result['id']) + assert_equal(ticket.title, result['title']) + assert_not(result['group']) + assert_not(result['priority']) + assert_not(result['owner']) + assert_equal(ticket.customer_id, result['customer_id']) + assert_equal(@agent.id, result['updated_by_id']) + assert_equal(@agent.id, result['created_by_id']) + + get "/api/v1/tickets/#{ticket.id}?expand=true", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal(ticket.id, result['id']) + assert_equal(ticket.title, result['title']) + assert_equal(ticket.customer_id, result['customer_id']) + assert_equal(ticket.group.name, result['group']) + assert_equal(ticket.priority.name, result['priority']) + assert_equal(ticket.owner.login, result['owner']) + assert_equal(@agent.id, result['updated_by_id']) + assert_equal(@agent.id, result['created_by_id']) + + get "/api/v1/tickets/#{ticket.id}?expand=false", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal(ticket.id, result['id']) + assert_equal(ticket.title, result['title']) + assert_not(result['group']) + assert_not(result['priority']) + assert_not(result['owner']) + assert_equal(ticket.customer_id, result['customer_id']) + assert_equal(@agent.id, result['updated_by_id']) + assert_equal(@agent.id, result['created_by_id']) + + get "/api/v1/tickets/#{ticket.id}?full=true", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + + assert_equal(Hash, result.class) + assert_equal(ticket.id, result['id']) + assert(result['assets']) + assert(result['assets']['Ticket']) + assert(result['assets']['Ticket'][ticket.id.to_s]) + assert_equal(ticket.id, result['assets']['Ticket'][ticket.id.to_s]['id']) + assert_equal(ticket.title, result['assets']['Ticket'][ticket.id.to_s]['title']) + assert_equal(ticket.customer_id, result['assets']['Ticket'][ticket.id.to_s]['customer_id']) + + assert(result['assets']['User']) + assert(result['assets']['User'][@agent.id.to_s]) + assert_equal(@agent.id, result['assets']['User'][@agent.id.to_s]['id']) + assert_equal(@agent.firstname, result['assets']['User'][@agent.id.to_s]['firstname']) + assert_equal(@agent.lastname, result['assets']['User'][@agent.id.to_s]['lastname']) + + assert(result['assets']['User']) + assert(result['assets']['User'][@customer_without_org.id.to_s]) + assert_equal(@customer_without_org.id, result['assets']['User'][@customer_without_org.id.to_s]['id']) + assert_equal(@customer_without_org.firstname, result['assets']['User'][@customer_without_org.id.to_s]['firstname']) + assert_equal(@customer_without_org.lastname, result['assets']['User'][@customer_without_org.id.to_s]['lastname']) + + get "/api/v1/tickets/#{ticket.id}?full=false", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal(ticket.id, result['id']) + assert_equal(ticket.title, result['title']) + assert_not(result['group']) + assert_not(result['priority']) + assert_not(result['owner']) + assert_equal(ticket.customer_id, result['customer_id']) + assert_equal(@agent.id, result['updated_by_id']) + assert_equal(@agent.id, result['created_by_id']) + end + + test '04.02 ticket index and response format' do + title = "ticket testagent#{rand(999_999_999)}" + ticket = Ticket.create!( + title: title, + group: Group.lookup(name: 'Users'), + customer_id: @customer_without_org.id, + state: Ticket::State.lookup(name: 'new'), + priority: Ticket::Priority.lookup(name: '2 normal'), + updated_by_id: @agent.id, + created_by_id: @agent.id, + ) + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('tickets-agent@example.com', 'agentpw') + get '/api/v1/tickets', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + + assert_equal(Array, result.class) + assert_equal(Hash, result[0].class) + assert_equal(1, result[0]['id']) + assert_equal(ticket.id, result[1]['id']) + assert_equal(ticket.title, result[1]['title']) + assert_not(result[1]['group']) + assert_not(result[1]['priority']) + assert_not(result[1]['owner']) + assert_equal(ticket.customer_id, result[1]['customer_id']) + assert_equal(@agent.id, result[1]['updated_by_id']) + assert_equal(@agent.id, result[1]['created_by_id']) + + get '/api/v1/tickets?expand=true', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + assert_equal(Hash, result[0].class) + assert_equal(1, result[0]['id']) + assert_equal(ticket.id, result[1]['id']) + assert_equal(ticket.title, result[1]['title']) + assert_equal(ticket.customer_id, result[1]['customer_id']) + assert_equal(ticket.group.name, result[1]['group']) + assert_equal(ticket.priority.name, result[1]['priority']) + assert_equal(ticket.owner.login, result[1]['owner']) + assert_equal(@agent.id, result[1]['updated_by_id']) + assert_equal(@agent.id, result[1]['created_by_id']) + + get '/api/v1/tickets?expand=false', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + assert_equal(Hash, result[0].class) + assert_equal(1, result[0]['id']) + assert_equal(ticket.id, result[1]['id']) + assert_equal(ticket.title, result[1]['title']) + assert_not(result[1]['group']) + assert_not(result[1]['priority']) + assert_not(result[1]['owner']) + assert_equal(ticket.customer_id, result[1]['customer_id']) + assert_equal(@agent.id, result[1]['updated_by_id']) + assert_equal(@agent.id, result[1]['created_by_id']) + + get '/api/v1/tickets?full=true', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + + assert_equal(Hash, result.class) + assert_equal(Array, result['record_ids'].class) + assert_equal(1, result['record_ids'][0]) + assert_equal(ticket.id, result['record_ids'][1]) + assert(result['assets']) + assert(result['assets']['Ticket']) + assert(result['assets']['Ticket'][ticket.id.to_s]) + assert_equal(ticket.id, result['assets']['Ticket'][ticket.id.to_s]['id']) + assert_equal(ticket.title, result['assets']['Ticket'][ticket.id.to_s]['title']) + assert_equal(ticket.customer_id, result['assets']['Ticket'][ticket.id.to_s]['customer_id']) + + assert(result['assets']['User']) + assert(result['assets']['User'][@agent.id.to_s]) + assert_equal(@agent.id, result['assets']['User'][@agent.id.to_s]['id']) + assert_equal(@agent.firstname, result['assets']['User'][@agent.id.to_s]['firstname']) + assert_equal(@agent.lastname, result['assets']['User'][@agent.id.to_s]['lastname']) + + assert(result['assets']['User']) + assert(result['assets']['User'][@customer_without_org.id.to_s]) + assert_equal(@customer_without_org.id, result['assets']['User'][@customer_without_org.id.to_s]['id']) + assert_equal(@customer_without_org.firstname, result['assets']['User'][@customer_without_org.id.to_s]['firstname']) + assert_equal(@customer_without_org.lastname, result['assets']['User'][@customer_without_org.id.to_s]['lastname']) + + get '/api/v1/tickets?full=false', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + assert_equal(Hash, result[0].class) + assert_equal(1, result[0]['id']) + assert_equal(ticket.id, result[1]['id']) + assert_equal(ticket.title, result[1]['title']) + assert_not(result[1]['group']) + assert_not(result[1]['priority']) + assert_not(result[1]['owner']) + assert_equal(ticket.customer_id, result[1]['customer_id']) + assert_equal(@agent.id, result[1]['updated_by_id']) + assert_equal(@agent.id, result[1]['created_by_id']) + end + + test '04.03 ticket create and response format' do + title = "ticket testagent#{rand(999_999_999)}" + params = { + title: title, + group: 'Users', + customer_id: @customer_without_org.id, + state: 'new', + priority: '2 normal', + article: { + body: 'some test 123', + }, + } + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('tickets-agent@example.com', 'agentpw') + + post '/api/v1/tickets', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(201) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + + ticket = Ticket.find(result['id']) + assert_equal(ticket.state_id, result['state_id']) + assert_not(result['state']) + assert_equal(ticket.priority_id, result['priority_id']) + assert_not(result['priority']) + assert_equal(ticket.group_id, result['group_id']) + assert_not(result['group']) + assert_equal(title, result['title']) + assert_equal(@customer_without_org.id, result['customer_id']) + assert_equal(@agent.id, result['updated_by_id']) + assert_equal(@agent.id, result['created_by_id']) + + post '/api/v1/tickets?expand=true', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(201) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + + ticket = Ticket.find(result['id']) + assert_equal(ticket.state_id, result['state_id']) + assert_equal(ticket.state.name, result['state']) + assert_equal(ticket.priority_id, result['priority_id']) + assert_equal(ticket.priority.name, result['priority']) + assert_equal(ticket.group_id, result['group_id']) + assert_equal(ticket.group.name, result['group']) + assert_equal(title, result['title']) + assert_equal(@customer_without_org.id, result['customer_id']) + assert_equal(@agent.id, result['updated_by_id']) + assert_equal(@agent.id, result['created_by_id']) + + post '/api/v1/tickets?full=true', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(201) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + + ticket = Ticket.find(result['id']) + assert(result['assets']) + assert(result['assets']['Ticket']) + assert(result['assets']['Ticket'][ticket.id.to_s]) + assert_equal(ticket.id, result['assets']['Ticket'][ticket.id.to_s]['id']) + assert_equal(title, result['assets']['Ticket'][ticket.id.to_s]['title']) + assert_equal(ticket.customer_id, result['assets']['Ticket'][ticket.id.to_s]['customer_id']) + + assert(result['assets']['User']) + assert(result['assets']['User'][@agent.id.to_s]) + assert_equal(@agent.id, result['assets']['User'][@agent.id.to_s]['id']) + assert_equal(@agent.firstname, result['assets']['User'][@agent.id.to_s]['firstname']) + assert_equal(@agent.lastname, result['assets']['User'][@agent.id.to_s]['lastname']) + + assert(result['assets']['User']) + assert(result['assets']['User'][@customer_without_org.id.to_s]) + assert_equal(@customer_without_org.id, result['assets']['User'][@customer_without_org.id.to_s]['id']) + assert_equal(@customer_without_org.firstname, result['assets']['User'][@customer_without_org.id.to_s]['firstname']) + assert_equal(@customer_without_org.lastname, result['assets']['User'][@customer_without_org.id.to_s]['lastname']) + + end + + test '04.04 ticket update and response formats' do + title = "ticket testagent#{rand(999_999_999)}" + ticket = Ticket.create!( + title: title, + group: Group.lookup(name: 'Users'), + customer_id: @customer_without_org.id, + state: Ticket::State.lookup(name: 'new'), + priority: Ticket::Priority.lookup(name: '2 normal'), + updated_by_id: @agent.id, + created_by_id: @agent.id, + ) + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('tickets-agent@example.com', 'agentpw') + + params = { + title: 'a update ticket #1', + } + put "/api/v1/tickets/#{ticket.id}", params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + + ticket = Ticket.find(result['id']) + assert_equal(ticket.state_id, result['state_id']) + assert_not(result['state']) + assert_equal(ticket.priority_id, result['priority_id']) + assert_not(result['priority']) + assert_equal(ticket.group_id, result['group_id']) + assert_not(result['group']) + assert_equal('a update ticket #1', result['title']) + assert_equal(@customer_without_org.id, result['customer_id']) + assert_equal(@agent.id, result['updated_by_id']) + assert_equal(@agent.id, result['created_by_id']) + + params = { + title: 'a update ticket #2', + } + put "/api/v1/tickets/#{ticket.id}?expand=true", params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + + ticket = Ticket.find(result['id']) + assert_equal(ticket.state_id, result['state_id']) + assert_equal(ticket.state.name, result['state']) + assert_equal(ticket.priority_id, result['priority_id']) + assert_equal(ticket.priority.name, result['priority']) + assert_equal(ticket.group_id, result['group_id']) + assert_equal(ticket.group.name, result['group']) + assert_equal('a update ticket #2', result['title']) + assert_equal(@customer_without_org.id, result['customer_id']) + assert_equal(@agent.id, result['updated_by_id']) + assert_equal(@agent.id, result['created_by_id']) + + params = { + title: 'a update ticket #3', + } + put "/api/v1/tickets/#{ticket.id}?full=true", params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + + ticket = Ticket.find(result['id']) + assert(result['assets']) + assert(result['assets']['Ticket']) + assert(result['assets']['Ticket'][ticket.id.to_s]) + assert_equal(ticket.id, result['assets']['Ticket'][ticket.id.to_s]['id']) + assert_equal('a update ticket #3', result['assets']['Ticket'][ticket.id.to_s]['title']) + assert_equal(ticket.customer_id, result['assets']['Ticket'][ticket.id.to_s]['customer_id']) + + assert(result['assets']['User']) + assert(result['assets']['User'][@agent.id.to_s]) + assert_equal(@agent.id, result['assets']['User'][@agent.id.to_s]['id']) + assert_equal(@agent.firstname, result['assets']['User'][@agent.id.to_s]['firstname']) + assert_equal(@agent.lastname, result['assets']['User'][@agent.id.to_s]['lastname']) + + assert(result['assets']['User']) + assert(result['assets']['User'][@customer_without_org.id.to_s]) + assert_equal(@customer_without_org.id, result['assets']['User'][@customer_without_org.id.to_s]['id']) + assert_equal(@customer_without_org.firstname, result['assets']['User'][@customer_without_org.id.to_s]['firstname']) + assert_equal(@customer_without_org.lastname, result['assets']['User'][@customer_without_org.id.to_s]['lastname']) + + end + end diff --git a/test/controllers/user_controller_test.rb b/test/controllers/user_controller_test.rb new file mode 100644 index 000000000..1ff94a176 --- /dev/null +++ b/test/controllers/user_controller_test.rb @@ -0,0 +1,960 @@ + +require 'test_helper' +require 'rake' + +class UserControllerTest < 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 + + @backup_admin = User.create_or_update( + login: 'backup-admin', + firstname: 'Backup', + lastname: 'Agent', + email: 'backup-admin@example.com', + password: 'adminpw', + active: true, + roles: roles, + groups: groups, + ) + + @admin = User.create_or_update( + login: 'rest-admin', + firstname: 'Rest', + lastname: 'Agent', + email: 'rest-admin@example.com', + password: 'adminpw', + active: true, + roles: roles, + groups: groups, + ) + + # create agent + roles = Role.where(name: 'Agent') + @agent = User.create_or_update( + login: 'rest-agent@example.com', + firstname: 'Rest', + lastname: 'Agent', + email: 'rest-agent@example.com', + password: 'agentpw', + active: true, + roles: roles, + groups: groups, + ) + + # create customer without org + roles = Role.where(name: 'Customer') + @customer_without_org = User.create_or_update( + login: 'rest-customer1@example.com', + firstname: 'Rest', + lastname: 'Customer1', + email: 'rest-customer1@example.com', + password: 'customer1pw', + active: true, + roles: roles, + ) + + # create orgs + @organization = Organization.create_or_update( + name: 'Rest Org', + ) + @organization2 = Organization.create_or_update( + name: 'Rest Org #2', + ) + @organization3 = Organization.create_or_update( + name: 'Rest Org #3', + ) + + # create customer with org + @customer_with_org = User.create_or_update( + login: 'rest-customer2@example.com', + firstname: 'Rest', + lastname: 'Customer2', + email: 'rest-customer2@example.com', + password: 'customer2pw', + active: true, + roles: roles, + organization_id: @organization.id, + ) + + # configure es + if ENV['ES_URL'].present? + #fail "ERROR: Need ES_URL - hint ES_URL='http://127.0.0.1:9200'" + Setting.set('es_url', ENV['ES_URL']) + + # Setting.set('es_url', 'http://127.0.0.1:9200') + # Setting.set('es_index', 'estest.local_zammad') + # Setting.set('es_user', 'elasticsearch') + # Setting.set('es_password', 'zammad') + + if ENV['ES_INDEX_RAND'].present? + ENV['ES_INDEX'] = "es_index_#{rand(999_999_999)}" + end + if ENV['ES_INDEX'].blank? + raise "ERROR: Need ES_INDEX - hint ES_INDEX='estest.local_zammad'" + end + Setting.set('es_index', ENV['ES_INDEX']) + + travel 1.minute + + # drop/create indexes + Rake::Task.clear + Zammad::Application.load_tasks + #Rake::Task["searchindex:drop"].execute + #Rake::Task["searchindex:create"].execute + Rake::Task['searchindex:rebuild'].execute + + # execute background jobs + Scheduler.worker(true) + + sleep 6 + end + UserInfo.current_user_id = nil + + end + + test 'user create tests - no user' do + + post '/api/v1/signshow', params: {}, headers: @headers + + # create user with disabled feature + Setting.set('user_create_account', false) + token = @response.headers['CSRF-TOKEN'] + + # token based on form + params = { email: 'some_new_customer@example.com', authenticity_token: token } + post '/api/v1/users', params: params.to_json, headers: @headers + assert_response(422) + result = JSON.parse(@response.body) + assert(result['error']) + assert_equal('Feature not enabled!', result['error']) + + # token based on headers + headers = @headers.merge('X-CSRF-Token' => token) + params = { email: 'some_new_customer@example.com' } + post '/api/v1/users', params: params.to_json, headers: headers + assert_response(422) + result = JSON.parse(@response.body) + assert(result['error']) + assert_equal('Feature not enabled!', result['error']) + + Setting.set('user_create_account', true) + + # no signup param with enabled feature + params = { email: 'some_new_customer@example.com' } + post '/api/v1/users', params: params.to_json, headers: headers + assert_response(422) + result = JSON.parse(@response.body) + assert(result['error']) + assert_equal('Only signup with not authenticate user possible!', result['error']) + + # already existing user with enabled feature + params = { email: 'rest-customer1@example.com', signup: true } + post '/api/v1/users', params: params.to_json, headers: headers + assert_response(422) + result = JSON.parse(@response.body) + assert(result['error']) + assert_equal('Email address is already used for other user.', result['error']) + + # email missing with enabled feature + params = { firstname: 'some firstname', signup: true } + post '/api/v1/users', params: params.to_json, headers: headers + assert_response(422) + result = JSON.parse(@response.body) + assert(result['error']) + assert_equal('Attribute \'email\' required!', result['error']) + + # email missing with enabled feature + params = { firstname: 'some firstname', signup: true } + post '/api/v1/users', params: params.to_json, headers: headers + assert_response(422) + result = JSON.parse(@response.body) + assert(result['error']) + assert_equal('Attribute \'email\' required!', result['error']) + + # create user with enabled feature (take customer role) + params = { firstname: 'Me First', lastname: 'Me Last', email: 'new_here@example.com', signup: true } + post '/api/v1/users', params: params.to_json, headers: headers + assert_response(201) + result = JSON.parse(@response.body) + assert(result) + + assert_equal('Me First', result['firstname']) + assert_equal('Me Last', result['lastname']) + assert_equal('new_here@example.com', result['login']) + assert_equal('new_here@example.com', result['email']) + user = User.find(result['id']) + assert_not(user.role?('Admin')) + assert_not(user.role?('Agent')) + assert(user.role?('Customer')) + + # create user with admin role (not allowed for signup, take customer role) + role = Role.lookup(name: 'Admin') + params = { firstname: 'Admin First', lastname: 'Admin Last', email: 'new_admin@example.com', role_ids: [ role.id ], signup: true } + post '/api/v1/users', params: params.to_json, headers: headers + assert_response(201) + result = JSON.parse(@response.body) + assert(result) + user = User.find(result['id']) + assert_not(user.role?('Admin')) + assert_not(user.role?('Agent')) + assert(user.role?('Customer')) + + # create user with agent role (not allowed for signup, take customer role) + role = Role.lookup(name: 'Agent') + params = { firstname: 'Agent First', lastname: 'Agent Last', email: 'new_agent@example.com', role_ids: [ role.id ], signup: true } + post '/api/v1/users', params: params.to_json, headers: headers + assert_response(201) + result = JSON.parse(@response.body) + assert(result) + user = User.find(result['id']) + assert_not(user.role?('Admin')) + assert_not(user.role?('Agent')) + assert(user.role?('Customer')) + + # no user (because of no session) + get '/api/v1/users', params: {}, headers: headers + assert_response(401) + result = JSON.parse(@response.body) + assert_equal('authentication failed', result['error']) + + # me + get '/api/v1/users/me', params: {}, headers: headers + assert_response(401) + result = JSON.parse(@response.body) + assert_equal('authentication failed', result['error']) + end + + test 'auth tests - not existing user' do + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('not_existing@example.com', 'adminpw') + + # me + get '/api/v1/users/me', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(401) + result = JSON.parse(@response.body) + assert_equal('authentication failed', result['error']) + + get '/api/v1/users', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(401) + result = JSON.parse(@response.body) + assert_equal('authentication failed', result['error']) + end + + test 'auth tests - username auth, wrong pw' do + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('rest-admin', 'not_existing') + + get '/api/v1/users', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(401) + result = JSON.parse(@response.body) + assert_equal('authentication failed', result['error']) + end + + test 'auth tests - email auth, wrong pw' do + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('rest-admin@example.com', 'not_existing') + + get '/api/v1/users', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(401) + result = JSON.parse(@response.body) + assert_equal('authentication failed', result['error']) + end + + test 'auth tests - username auth' do + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('rest-admin', 'adminpw') + + get '/api/v1/users', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert(result) + end + + test 'auth tests - email auth' do + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('rest-admin@example.com', 'adminpw') + + get '/api/v1/users', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert(result) + end + + test 'user index and create with admin' do + + # email auth + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('rest-admin@example.com', 'adminpw') + + # me + get '/api/v1/users/me', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert(result) + assert_equal(result['email'], 'rest-admin@example.com') + + # index + get '/api/v1/users', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert(result) + + # index + get '/api/v1/users', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert(result) + assert_equal(result.class, Array) + assert(result.length >= 3) + + # show/:id + get "/api/v1/users/#{@agent.id}", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert(result) + assert_equal(result.class, Hash) + assert_equal(result['email'], 'rest-agent@example.com') + + get "/api/v1/users/#{@customer_without_org.id}", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert(result) + assert_equal(result.class, Hash) + assert_equal(result['email'], 'rest-customer1@example.com') + + # create user with admin role + role = Role.lookup(name: 'Admin') + params = { firstname: 'Admin First', lastname: 'Admin Last', email: 'new_admin_by_admin@example.com', role_ids: [ role.id ] } + post '/api/v1/users', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(201) + result = JSON.parse(@response.body) + assert(result) + user = User.find(result['id']) + assert(user.role?('Admin')) + assert_not(user.role?('Agent')) + assert_not(user.role?('Customer')) + assert_equal('new_admin_by_admin@example.com', result['login']) + assert_equal('new_admin_by_admin@example.com', result['email']) + + # create user with agent role + role = Role.lookup(name: 'Agent') + params = { firstname: 'Agent First', lastname: 'Agent Last', email: 'new_agent_by_admin1@example.com', role_ids: [ role.id ] } + post '/api/v1/users', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(201) + result = JSON.parse(@response.body) + assert(result) + user = User.find(result['id']) + assert_not(user.role?('Admin')) + assert(user.role?('Agent')) + assert_not(user.role?('Customer')) + assert_equal('new_agent_by_admin1@example.com', result['login']) + assert_equal('new_agent_by_admin1@example.com', result['email']) + + role = Role.lookup(name: 'Agent') + params = { firstname: 'Agent First', email: 'new_agent_by_admin2@example.com', role_ids: [ role.id ] } + post '/api/v1/users', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(201) + result = JSON.parse(@response.body) + assert(result) + user = User.find(result['id']) + assert_not(user.role?('Admin')) + assert(user.role?('Agent')) + assert_not(user.role?('Customer')) + assert_equal('new_agent_by_admin2@example.com', result['login']) + assert_equal('new_agent_by_admin2@example.com', result['email']) + assert_equal('Agent', result['firstname']) + assert_equal('First', result['lastname']) + + role = Role.lookup(name: 'Agent') + params = { firstname: 'Agent First', email: 'new_agent_by_admin2@example.com', role_ids: [ role.id ] } + post '/api/v1/users', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(422) + result = JSON.parse(@response.body) + assert(result) + assert_equal('Email address is already used for other user.', result['error']) + + # missing required attributes + params = { note: 'some note' } + post '/api/v1/users', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(422) + result = JSON.parse(@response.body) + assert(result) + assert_equal('Minimum one identifier (login, firstname, lastname, phone or email) for user is required.', result['error']) + + # invalid email + params = { firstname: 'newfirstname123', email: 'some_what', note: 'some note' } + post '/api/v1/users', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(422) + result = JSON.parse(@response.body) + assert(result) + assert_equal('Invalid email', result['error']) + + # with valid attributes + params = { firstname: 'newfirstname123', note: 'some note' } + post '/api/v1/users', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(201) + result = JSON.parse(@response.body) + assert(result) + user = User.find(result['id']) + assert_not(user.role?('Admin')) + assert_not(user.role?('Agent')) + assert(user.role?('Customer')) + assert(result['login'].start_with?('auto-')) + assert_equal('', result['email']) + assert_equal('newfirstname123', result['firstname']) + assert_equal('', result['lastname']) + end + + test 'user index and create with agent' do + + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('rest-agent@example.com', 'agentpw') + + # me + get '/api/v1/users/me', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert(result) + assert_equal(result['email'], 'rest-agent@example.com') + + # index + get '/api/v1/users', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert(result) + + # index + get '/api/v1/users', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert(result) + assert_equal(result.class, Array) + assert(result.length >= 3) + + get '/api/v1/users?limit=40&page=1&per_page=2', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + users = User.order(:id).limit(2) + assert_equal(users[0].id, result[0]['id']) + assert_equal(users[1].id, result[1]['id']) + assert_equal(2, result.count) + + get '/api/v1/users?limit=40&page=2&per_page=2', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + users = User.order(:id).limit(4) + assert_equal(users[2].id, result[0]['id']) + assert_equal(users[3].id, result[1]['id']) + assert_equal(2, result.count) + + # create user with admin role + firstname = "First test#{rand(999_999_999)}" + role = Role.lookup(name: 'Admin') + params = { firstname: "Admin#{firstname}", lastname: 'Admin Last', email: 'new_admin_by_agent@example.com', role_ids: [ role.id ] } + post '/api/v1/users', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(201) + result_user1 = JSON.parse(@response.body) + assert(result_user1) + user = User.find(result_user1['id']) + assert_not(user.role?('Admin')) + assert_not(user.role?('Agent')) + assert(user.role?('Customer')) + assert_equal('new_admin_by_agent@example.com', result_user1['login']) + assert_equal('new_admin_by_agent@example.com', result_user1['email']) + + # create user with agent role + role = Role.lookup(name: 'Agent') + params = { firstname: "Agent#{firstname}", lastname: 'Agent Last', email: 'new_agent_by_agent@example.com', role_ids: [ role.id ] } + post '/api/v1/users', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(201) + result_user1 = JSON.parse(@response.body) + assert(result_user1) + user = User.find(result_user1['id']) + assert_not(user.role?('Admin')) + assert_not(user.role?('Agent')) + assert(user.role?('Customer')) + assert_equal('new_agent_by_agent@example.com', result_user1['login']) + assert_equal('new_agent_by_agent@example.com', result_user1['email']) + + # create user with customer role + role = Role.lookup(name: 'Customer') + params = { firstname: "Customer#{firstname}", lastname: 'Customer Last', email: 'new_customer_by_agent@example.com', role_ids: [ role.id ] } + post '/api/v1/users', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(201) + result_user1 = JSON.parse(@response.body) + assert(result_user1) + user = User.find(result_user1['id']) + assert_not(user.role?('Admin')) + assert_not(user.role?('Agent')) + assert(user.role?('Customer')) + assert_equal('new_customer_by_agent@example.com', result_user1['login']) + assert_equal('new_customer_by_agent@example.com', result_user1['email']) + + # search as agent + Scheduler.worker(true) + sleep 2 # let es time to come ready + get "/api/v1/users/search?query=#{CGI.escape("Customer#{firstname}")}", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + + assert_equal(result_user1['id'], result[0]['id']) + assert_equal("Customer#{firstname}", result[0]['firstname']) + assert_equal('Customer Last', result[0]['lastname']) + assert(result[0]['role_ids']) + assert_not(result[0]['roles']) + + get "/api/v1/users/search?query=#{CGI.escape("Customer#{firstname}")}&expand=true", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + assert_equal(result_user1['id'], result[0]['id']) + assert_equal("Customer#{firstname}", result[0]['firstname']) + assert_equal('Customer Last', result[0]['lastname']) + assert(result[0]['role_ids']) + assert(result[0]['roles']) + + get "/api/v1/users/search?query=#{CGI.escape("Customer#{firstname}")}&label=true", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + assert_equal(result_user1['id'], result[0]['id']) + assert_equal("Customer#{firstname} Customer Last ", result[0]['label']) + assert_equal("Customer#{firstname} Customer Last ", result[0]['value']) + assert_not(result[0]['role_ids']) + assert_not(result[0]['roles']) + + role = Role.find_by(name: 'Agent') + get "/api/v1/users/search?query=#{CGI.escape("Customer#{firstname}")}&role_ids=#{role.id}&label=true", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + assert_equal(0, result.count) + + role = Role.find_by(name: 'Customer') + get "/api/v1/users/search?query=#{CGI.escape("Customer#{firstname}")}&role_ids=#{role.id}&label=true", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + assert_equal(result_user1['id'], result[0]['id']) + assert_equal("Customer#{firstname} Customer Last ", result[0]['label']) + assert_equal("Customer#{firstname} Customer Last ", result[0]['value']) + assert_not(result[0]['role_ids']) + assert_not(result[0]['roles']) + + permission = Permission.find_by(name: 'ticket.agent') + get "/api/v1/users/search?query=#{CGI.escape("Customer#{firstname}")}&permissions=#{permission.name}&label=true", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + assert_equal(0, result.count) + + permission = Permission.find_by(name: 'ticket.customer') + get "/api/v1/users/search?query=#{CGI.escape("Customer#{firstname}")}&permissions=#{permission.name}&label=true", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + assert_equal(result_user1['id'], result[0]['id']) + assert_equal("Customer#{firstname} Customer Last ", result[0]['label']) + assert_equal("Customer#{firstname} Customer Last ", result[0]['value']) + assert_not(result[0]['role_ids']) + assert_not(result[0]['roles']) + end + + test 'user index and create with customer1' do + + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('rest-customer1@example.com', 'customer1pw') + + # me + get '/api/v1/users/me', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert(result) + assert_equal(result['email'], 'rest-customer1@example.com') + + # index + get '/api/v1/users', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(result.class, Array) + assert_equal(result.length, 1) + + # show/:id + get "/api/v1/users/#{@customer_without_org.id}", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(result.class, Hash) + assert_equal(result['email'], 'rest-customer1@example.com') + + get "/api/v1/users/#{@customer_with_org.id}", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(401) + result = JSON.parse(@response.body) + assert_equal(result.class, Hash) + assert(result['error']) + + # create user with admin role + role = Role.lookup(name: 'Admin') + params = { firstname: 'Admin First', lastname: 'Admin Last', email: 'new_admin_by_customer1@example.com', role_ids: [ role.id ] } + post '/api/v1/users', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(401) + + # create user with agent role + role = Role.lookup(name: 'Agent') + params = { firstname: 'Agent First', lastname: 'Agent Last', email: 'new_agent_by_customer1@example.com', role_ids: [ role.id ] } + post '/api/v1/users', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(401) + + # search + Scheduler.worker(true) + get "/api/v1/users/search?query=#{CGI.escape('First')}", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(401) + end + + test 'user index with customer2' do + + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('rest-customer2@example.com', 'customer2pw') + + # me + get '/api/v1/users/me', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert(result) + assert_equal(result['email'], 'rest-customer2@example.com') + + # index + get '/api/v1/users', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(result.class, Array) + assert_equal(result.length, 1) + + # show/:id + get "/api/v1/users/#{@customer_with_org.id}", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(result.class, Hash) + assert_equal(result['email'], 'rest-customer2@example.com') + + get "/api/v1/users/#{@customer_without_org.id}", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(401) + result = JSON.parse(@response.body) + assert_equal(result.class, Hash) + assert(result['error']) + + # search + Scheduler.worker(true) + get "/api/v1/users/search?query=#{CGI.escape('First')}", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(401) + end + + test '04.01 users show and response format' do + roles = Role.where(name: 'Customer') + organization = Organization.first + user = User.create!( + login: 'rest-customer3@example.com', + firstname: 'Rest', + lastname: 'Customer3', + email: 'rest-customer3@example.com', + password: 'customer3pw', + active: true, + organization: organization, + roles: roles, + updated_by_id: @admin.id, + created_by_id: @admin.id, + ) + + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('rest-admin@example.com', 'adminpw') + get "/api/v1/users/#{user.id}", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal(user.id, result['id']) + assert_equal(user.firstname, result['firstname']) + assert_not(result['organization']) + assert_equal(user.organization_id, result['organization_id']) + assert_not(result['password']) + assert_equal(user.role_ids, result['role_ids']) + assert_equal(@admin.id, result['updated_by_id']) + assert_equal(@admin.id, result['created_by_id']) + + get "/api/v1/users/#{user.id}?expand=true", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal(user.id, result['id']) + assert_equal(user.firstname, result['firstname']) + assert_equal(user.organization_id, result['organization_id']) + assert_equal(user.organization.name, result['organization']) + assert_equal(user.role_ids, result['role_ids']) + assert_not(result['password']) + assert_equal(@admin.id, result['updated_by_id']) + assert_equal(@admin.id, result['created_by_id']) + + get "/api/v1/users/#{user.id}?expand=false", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal(user.id, result['id']) + assert_equal(user.firstname, result['firstname']) + assert_not(result['organization']) + assert_equal(user.organization_id, result['organization_id']) + assert_not(result['password']) + assert_equal(user.role_ids, result['role_ids']) + assert_equal(@admin.id, result['updated_by_id']) + assert_equal(@admin.id, result['created_by_id']) + + get "/api/v1/users/#{user.id}?full=true", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + + assert_equal(Hash, result.class) + assert_equal(user.id, result['id']) + assert(result['assets']) + assert(result['assets']['User']) + assert(result['assets']['User'][user.id.to_s]) + assert_equal(user.id, result['assets']['User'][user.id.to_s]['id']) + assert_equal(user.firstname, result['assets']['User'][user.id.to_s]['firstname']) + assert_equal(user.organization_id, result['assets']['User'][user.id.to_s]['organization_id']) + assert_equal(user.role_ids, result['assets']['User'][user.id.to_s]['role_ids']) + + get "/api/v1/users/#{user.id}?full=false", params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + assert_equal(user.id, result['id']) + assert_equal(user.firstname, result['firstname']) + assert_not(result['organization']) + assert_equal(user.organization_id, result['organization_id']) + assert_not(result['password']) + assert_equal(user.role_ids, result['role_ids']) + assert_equal(@admin.id, result['updated_by_id']) + assert_equal(@admin.id, result['created_by_id']) + end + + test '04.02 user index and response format' do + roles = Role.where(name: 'Customer') + organization = Organization.first + user = User.create!( + login: 'rest-customer3@example.com', + firstname: 'Rest', + lastname: 'Customer3', + email: 'rest-customer3@example.com', + password: 'customer3pw', + active: true, + organization: organization, + roles: roles, + updated_by_id: @admin.id, + created_by_id: @admin.id, + ) + + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('rest-admin@example.com', 'adminpw') + get '/api/v1/users', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + assert_equal(Hash, result[0].class) + assert_equal(user.id, result.last['id']) + assert_equal(user.lastname, result.last['lastname']) + assert_not(result.last['organization']) + assert_equal(user.role_ids, result.last['role_ids']) + assert_equal(user.organization_id, result.last['organization_id']) + assert_not(result.last['password']) + assert_equal(@admin.id, result.last['updated_by_id']) + assert_equal(@admin.id, result.last['created_by_id']) + + get '/api/v1/users?expand=true', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + assert_equal(Hash, result[0].class) + assert_equal(user.id, result.last['id']) + assert_equal(user.lastname, result.last['lastname']) + assert_equal(user.organization_id, result.last['organization_id']) + assert_equal(user.organization.name, result.last['organization']) + assert_not(result.last['password']) + assert_equal(@admin.id, result.last['updated_by_id']) + assert_equal(@admin.id, result.last['created_by_id']) + + get '/api/v1/users?expand=false', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + assert_equal(Hash, result[0].class) + assert_equal(user.id, result.last['id']) + assert_equal(user.lastname, result.last['lastname']) + assert_not(result.last['organization']) + assert_equal(user.role_ids, result.last['role_ids']) + assert_equal(user.organization_id, result.last['organization_id']) + assert_not(result.last['password']) + assert_equal(@admin.id, result.last['updated_by_id']) + assert_equal(@admin.id, result.last['created_by_id']) + + get '/api/v1/users?full=true', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + + assert_equal(Hash, result.class) + assert_equal(Array, result['record_ids'].class) + assert_equal(1, result['record_ids'][0]) + assert_equal(user.id, result['record_ids'].last) + assert(result['assets']) + assert(result['assets']['User']) + assert(result['assets']['User'][user.id.to_s]) + assert_equal(user.id, result['assets']['User'][user.id.to_s]['id']) + assert_equal(user.lastname, result['assets']['User'][user.id.to_s]['lastname']) + assert_equal(user.organization_id, result['assets']['User'][user.id.to_s]['organization_id']) + assert_not(result['assets']['User'][user.id.to_s]['password']) + + get '/api/v1/users?full=false', params: {}, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Array, result.class) + assert_equal(Hash, result[0].class) + assert_equal(user.id, result.last['id']) + assert_equal(user.lastname, result.last['lastname']) + assert_not(result.last['organization']) + assert_equal(user.role_ids, result.last['role_ids']) + assert_equal(user.organization_id, result.last['organization_id']) + assert_not(result.last['password']) + assert_equal(@admin.id, result.last['updated_by_id']) + assert_equal(@admin.id, result.last['created_by_id']) + end + + test '04.03 ticket create and response format' do + organization = Organization.first + params = { + firstname: 'newfirstname123', + note: 'some note', + organization: organization.name, + } + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('rest-admin@example.com', 'adminpw') + + post '/api/v1/users', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(201) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + + user = User.find(result['id']) + assert_equal(user.firstname, result['firstname']) + assert_equal(user.organization_id, result['organization_id']) + assert_not(result['organization']) + assert_not(result['password']) + assert_equal(@admin.id, result['updated_by_id']) + assert_equal(@admin.id, result['created_by_id']) + + post '/api/v1/users?expand=true', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(201) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + + user = User.find(result['id']) + assert_equal(user.firstname, result['firstname']) + assert_equal(user.organization_id, result['organization_id']) + assert_equal(user.organization.name, result['organization']) + assert_not(result['password']) + assert_equal(@admin.id, result['updated_by_id']) + assert_equal(@admin.id, result['created_by_id']) + + post '/api/v1/users?full=true', params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(201) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + + user = User.find(result['id']) + assert(result['assets']) + assert(result['assets']['User']) + assert(result['assets']['User'][user.id.to_s]) + assert_equal(user.id, result['assets']['User'][user.id.to_s]['id']) + assert_equal(user.firstname, result['assets']['User'][user.id.to_s]['firstname']) + assert_equal(user.lastname, result['assets']['User'][user.id.to_s]['lastname']) + assert_not(result['assets']['User'][user.id.to_s]['password']) + + assert(result['assets']['User'][@admin.id.to_s]) + assert_equal(@admin.id, result['assets']['User'][@admin.id.to_s]['id']) + assert_equal(@admin.firstname, result['assets']['User'][@admin.id.to_s]['firstname']) + assert_equal(@admin.lastname, result['assets']['User'][@admin.id.to_s]['lastname']) + assert_not(result['assets']['User'][@admin.id.to_s]['password']) + + end + + test '04.04 ticket update and response formats' do + roles = Role.where(name: 'Customer') + organization = Organization.first + user = User.create!( + login: 'rest-customer3@example.com', + firstname: 'Rest', + lastname: 'Customer3', + email: 'rest-customer3@example.com', + password: 'customer3pw', + active: true, + organization: organization, + roles: roles, + updated_by_id: @admin.id, + created_by_id: @admin.id, + ) + + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('rest-admin@example.com', 'adminpw') + + params = { + firstname: 'a update firstname #1', + } + put "/api/v1/users/#{user.id}", params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + + user = User.find(result['id']) + assert_equal(user.lastname, result['lastname']) + assert_equal(params[:firstname], result['firstname']) + assert_equal(user.organization_id, result['organization_id']) + assert_not(result['organization']) + assert_not(result['password']) + assert_equal(@admin.id, result['updated_by_id']) + assert_equal(@admin.id, result['created_by_id']) + + params = { + firstname: 'a update firstname #2', + } + put "/api/v1/users/#{user.id}?expand=true", params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + + user = User.find(result['id']) + assert_equal(user.lastname, result['lastname']) + assert_equal(params[:firstname], result['firstname']) + assert_equal(user.organization_id, result['organization_id']) + assert_equal(user.organization.name, result['organization']) + assert_not(result['password']) + assert_equal(@admin.id, result['updated_by_id']) + assert_equal(@admin.id, result['created_by_id']) + + params = { + firstname: 'a update firstname #3', + } + put "/api/v1/users/#{user.id}?full=true", params: params.to_json, headers: @headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(Hash, result.class) + + user = User.find(result['id']) + assert(result['assets']) + assert(result['assets']['User']) + assert(result['assets']['User'][user.id.to_s]) + assert_equal(user.id, result['assets']['User'][user.id.to_s]['id']) + assert_equal(params[:firstname], result['assets']['User'][user.id.to_s]['firstname']) + assert_equal(user.lastname, result['assets']['User'][user.id.to_s]['lastname']) + assert_not(result['assets']['User'][user.id.to_s]['password']) + + assert(result['assets']['User'][@admin.id.to_s]) + assert_equal(@admin.id, result['assets']['User'][@admin.id.to_s]['id']) + assert_equal(@admin.firstname, result['assets']['User'][@admin.id.to_s]['firstname']) + assert_equal(@admin.lastname, result['assets']['User'][@admin.id.to_s]['lastname']) + assert_not(result['assets']['User'][@admin.id.to_s]['password']) + + end + +end diff --git a/test/integration/report_test.rb b/test/integration/report_test.rb index b4f300459..7c27b5f85 100644 --- a/test/integration/report_test.rb +++ b/test/integration/report_test.rb @@ -136,8 +136,8 @@ class ReportTest < ActiveSupport::TestCase state: Ticket::State.lookup(name: 'closed'), priority: Ticket::Priority.lookup(name: '2 normal'), close_at: '2015-10-28 11:30:00 UTC', - created_at: '2015-10-28 10:30:00 UTC', - updated_at: '2015-10-28 10:30:00 UTC', + created_at: '2015-10-28 10:30:01 UTC', + updated_at: '2015-10-28 10:30:01 UTC', updated_by_id: 1, created_by_id: 1, ) @@ -151,8 +151,8 @@ class ReportTest < ActiveSupport::TestCase internal: false, sender: Ticket::Article::Sender.where(name: 'Customer').first, type: Ticket::Article::Type.where(name: 'email').first, - created_at: '2015-10-28 10:30:00 UTC', - updated_at: '2015-10-28 10:30:00 UTC', + created_at: '2015-10-28 10:30:01 UTC', + updated_at: '2015-10-28 10:30:01 UTC', updated_by_id: 1, created_by_id: 1, ) @@ -735,12 +735,11 @@ class ReportTest < ActiveSupport::TestCase params: { field: 'created_at' }, ) assert(result) - assert_equal(@ticket7.id, result[:ticket_ids][0].to_i) assert_equal(@ticket6.id, result[:ticket_ids][1].to_i) assert_equal(@ticket5.id, result[:ticket_ids][2].to_i) - assert_equal(@ticket3.id, result[:ticket_ids][3].to_i) - assert_equal(@ticket4.id, result[:ticket_ids][4].to_i) + assert_equal(@ticket4.id, result[:ticket_ids][3].to_i) + assert_equal(@ticket3.id, result[:ticket_ids][4].to_i) assert_equal(@ticket2.id, result[:ticket_ids][5].to_i) assert_equal(@ticket1.id, result[:ticket_ids][6].to_i) assert_nil(result[:ticket_ids][7]) From 34c14a22b5c6430da865a9ec66517a67363857a8 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Thu, 14 Dec 2017 21:05:59 +0100 Subject: [PATCH 068/196] Push users.preferences to elasticsearch to enable searches for certain features. --- app/models/user.rb | 3 +-- app/models/user/search_index.rb | 5 ----- test/integration/elasticsearch_test.rb | 3 +++ 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 42c6bbc31..90609c1ad 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -72,8 +72,7 @@ class User < ApplicationModel :image, :image_source, :source, - :login_failed, - :preferences + :login_failed def ignore_search_indexing?(_action) # ignore internal user diff --git a/app/models/user/search_index.rb b/app/models/user/search_index.rb index c28485f17..80376e7f4 100644 --- a/app/models/user/search_index.rb +++ b/app/models/user/search_index.rb @@ -50,11 +50,6 @@ returns def search_index_data attributes = {} self.attributes.each do |key, value| - next if key == 'created_at' - next if key == 'updated_at' - next if key == 'created_by_id' - next if key == 'updated_by_id' - next if key == 'preferences' next if key == 'password' next if !value next if value.respond_to?('blank?') && value.blank? diff --git a/test/integration/elasticsearch_test.rb b/test/integration/elasticsearch_test.rb index e0b6ff4e0..94d4991bc 100644 --- a/test/integration/elasticsearch_test.rb +++ b/test/integration/elasticsearch_test.rb @@ -114,6 +114,7 @@ class ElasticsearchTest < ActiveSupport::TestCase assert_equal('E', attributes['firstname']) assert_equal('S', attributes['lastname']) assert_equal('es-agent@example.com', attributes['email']) + assert(attributes['preferences']) assert_not(attributes['password']) assert_not(attributes['organization']) @@ -121,6 +122,7 @@ class ElasticsearchTest < ActiveSupport::TestCase assert_equal('E', attributes['firstname']) assert_equal('S', attributes['lastname']) assert_equal('es-agent@example.com', attributes['email']) + assert(attributes['preferences']) assert_not(attributes['password']) assert_not(attributes['organization']) @@ -128,6 +130,7 @@ class ElasticsearchTest < ActiveSupport::TestCase assert_equal('ES', attributes['firstname']) assert_equal('Customer1', attributes['lastname']) assert_equal('es-customer1@example.com', attributes['email']) + assert(attributes['preferences']) assert_not(attributes['password']) assert_equal('Customer Organization Update', attributes['organization']) From db650b9fbb54c270f4e62796d49bb860bb7cc4ff Mon Sep 17 00:00:00 2001 From: Umar Sheikh Date: Fri, 15 Dec 2017 17:57:15 +0500 Subject: [PATCH 069/196] add integer and tree_select types to Triggers Management(New Trigger) form under conditions for effected objects --- .../app/controllers/_ui_element/ticket_selector.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee b/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee index a88901169..b913cd181 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee @@ -22,8 +22,10 @@ class App.UiElement.ticket_selector '^timestamp$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)'] '^date$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)'] 'boolean$': ['is', 'is not'] + 'integer$': ['is', 'is not'] '^radio$': ['is', 'is not'] '^select$': ['is', 'is not'] + '^tree_select$': ['is', 'is not'] '^input$': ['contains', 'contains not'] '^textarea$': ['contains', 'contains not'] '^tag$': ['contains all', 'contains one', 'contains all not', 'contains one not'] @@ -34,8 +36,10 @@ class App.UiElement.ticket_selector '^timestamp$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'has changed'] '^date$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'has changed'] 'boolean$': ['is', 'is not', 'has changed'] + 'integer$': ['is', 'is not', 'has changed'] '^radio$': ['is', 'is not', 'has changed'] '^select$': ['is', 'is not', 'has changed'] + '^tree_select$': ['is', 'is not', 'has changed'] '^input$': ['contains', 'contains not', 'has changed'] '^textarea$': ['contains', 'contains not', 'has changed'] '^tag$': ['contains all', 'contains one', 'contains all not', 'contains one not'] From 8ac7356184bc3cec2efaaac3484ca49029b43652 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Fri, 15 Dec 2017 14:58:41 +0100 Subject: [PATCH 070/196] Small refactoring of search index backend. --- lib/report/ticket_generic_time.rb | 2 +- lib/search_index_backend.rb | 38 +++++------ lib/tasks/search_index_es.rake | 109 +++++++++++++++++++++--------- 3 files changed, 95 insertions(+), 54 deletions(-) diff --git a/lib/report/ticket_generic_time.rb b/lib/report/ticket_generic_time.rb index 5372f1166..3869f4e79 100644 --- a/lib/report/ticket_generic_time.rb +++ b/lib/report/ticket_generic_time.rb @@ -30,7 +30,7 @@ returns } selector = params[:selector].clone - if params[:params] && params[:params][:selector] + if params[:params].present? && params[:params][:selector].present? selector = selector.merge(params[:params][:selector]) end diff --git a/lib/search_index_backend.rb b/lib/search_index_backend.rb index d909b8ed6..b5887c512 100644 --- a/lib/search_index_backend.rb +++ b/lib/search_index_backend.rb @@ -74,6 +74,7 @@ update processors ) Rails.logger.info "# #{response.code}" next if response.success? + next if response.code.to_s == '404' raise "Unable to process DELETE at #{url}\n#{response.inspect}" end Rails.logger.info "# curl -X PUT \"#{url}\" \\" @@ -133,7 +134,7 @@ create/update/delete index def self.index(data) url = build_url(data[:name]) - return if !url + return if url.blank? if data[:action] && data[:action] == 'delete' return SearchIndexBackend.remove(data[:name]) @@ -169,7 +170,7 @@ add new object to search index def self.add(type, data) url = build_url(type, data['id']) - return if !url + return if url.blank? Rails.logger.info "# curl -X POST \"#{url}\" \\" Rails.logger.debug "-d '#{data.to_json}'" @@ -202,7 +203,7 @@ remove whole data from index def self.remove(type, o_id = nil) url = build_url(type, o_id) - return if !url + return if url.blank? Rails.logger.info "# curl -X DELETE \"#{url}\"" @@ -217,7 +218,8 @@ remove whole data from index ) Rails.logger.info "# #{response.code}" return true if response.success? - #Rails.logger.info "NOTICE: can't delete index #{url}: " + response.inspect + return true if response.code.to_s == '400' + Rails.logger.info "NOTICE: can't delete index #{url}: " + response.inspect false end @@ -247,7 +249,7 @@ return search result =end def self.search(query, limit = 10, index = nil, query_extention = {}) - return [] if !query + return [] if query.blank? if index.class == Array ids = [] index.each do |local_index| @@ -260,10 +262,10 @@ return search result end def self.search_by_index(query, limit = 10, index = nil, query_extention = {}) - return [] if !query + return [] if query.blank? url = build_url - return if !url + return if url.blank? url += if index if index.class == Array "/#{index.join(',')}/_search" @@ -287,12 +289,8 @@ return search result ] data['query'] = query_extention || {} - if !data['query']['bool'] - data['query']['bool'] = {} - end - if !data['query']['bool']['must'] - data['query']['bool']['must'] = [] - end + data['query']['bool'] ||= {} + data['query']['bool']['must'] ||= [] # add * on simple query like "somephrase23" or "attribute: somephrase23" if query.present? @@ -391,7 +389,7 @@ get count of tickets and tickets which match on selector raise 'no selectors given' if !selectors url = build_url - return if !url + return if url.blank? url += if index if index.class == Array "/#{index.join(',')}/_search" @@ -425,7 +423,7 @@ get count of tickets and tickets which match on selector end Rails.logger.debug response.data.to_json - if !aggs_interval || !aggs_interval[:interval] + if aggs_interval.blank? || aggs_interval[:interval].blank? ticket_ids = [] response.data['hits']['hits'].each do |item| ticket_ids.push item['_id'] @@ -471,8 +469,8 @@ get count of tickets and tickets which match on selector } # add aggs to filter - if aggs_interval - if aggs_interval[:interval] + if aggs_interval.present? + if aggs_interval[:interval].present? data[:size] = 0 data[:aggs] = { time_buckets: { @@ -492,9 +490,7 @@ get count of tickets and tickets which match on selector query_must.push r end - if !data[:query][:bool] - data[:query][:bool] = {} - end + data[:query][:bool] ||= {} if query_must.present? data[:query][:bool][:must] = query_must @@ -504,7 +500,7 @@ get count of tickets and tickets which match on selector end # add sort - if aggs_interval && aggs_interval[:field] && !aggs_interval[:interval] + if aggs_interval.present? && aggs_interval[:field].present? && aggs_interval[:interval].blank? sort = [] sort[0] = {} sort[0][aggs_interval[:field]] = { diff --git a/lib/tasks/search_index_es.rake b/lib/tasks/search_index_es.rake index 7a68ab5c6..5014ac0bf 100644 --- a/lib/tasks/search_index_es.rake +++ b/lib/tasks/search_index_es.rake @@ -5,15 +5,17 @@ namespace :searchindex do task :drop, [:opts] => :environment do |_t, _args| # drop indexes - puts 'drop indexes...' + print 'drop indexes...' SearchIndexBackend.index( action: 'delete', ) + puts 'done' + Rake::Task['searchindex:drop_pipeline'].execute end task :create, [:opts] => :environment do |_t, _args| - puts 'create indexes...' + print 'create indexes...' # es with mapper-attachments plugin info = SearchIndexBackend.info @@ -45,6 +47,7 @@ namespace :searchindex do } } ) + puts 'done' Setting.set('es_pipeline', '') # es with ingest-attachment plugin @@ -61,44 +64,86 @@ namespace :searchindex do } } ) + puts 'done' + end - # update processors - pipeline = 'zammad-attachment' + Rake::Task['searchindex:create_pipeline'].execute + end + + task :create_pipeline, [:opts] => :environment do |_t, _args| + + # es with mapper-attachments plugin + info = SearchIndexBackend.info + number = nil + if info.present? + number = info['version']['number'].to_s + end + next if number.blank? || number =~ /^[2-4]\./ || number =~ /^5\.[0-5]\./ + + # update processors + pipeline = Setting.get('es_pipeline') + if pipeline.blank? + pipeline = "zammad#{rand(999_999_999_999)}" Setting.set('es_pipeline', pipeline) - SearchIndexBackend.processors( - "_ingest/pipeline/#{pipeline}": [ - { - action: 'delete', - }, - { - action: 'create', - description: 'Extract zammad-attachment information from arrays', - processors: [ - { - foreach: { - field: 'article', - ignore_failure: true, - processor: { - foreach: { - field: '_ingest._value.attachment', - ignore_failure: true, - processor: { - attachment: { - target_field: '_ingest._value', - field: '_ingest._value._content', - ignore_failure: true, - } + end + print 'create pipeline (pipeline)... ' + SearchIndexBackend.processors( + "_ingest/pipeline/#{pipeline}": [ + { + action: 'delete', + }, + { + action: 'create', + description: 'Extract zammad-attachment information from arrays', + processors: [ + { + foreach: { + field: 'article', + ignore_failure: true, + processor: { + foreach: { + field: '_ingest._value.attachment', + ignore_failure: true, + processor: { + attachment: { + target_field: '_ingest._value', + field: '_ingest._value._content', + ignore_failure: true, } } } } } - ] - } - ] - ) - end + } + ] + } + ] + ) + puts 'done' + end + task :drop_pipeline, [:opts] => :environment do |_t, _args| + + # es with mapper-attachments plugin + info = SearchIndexBackend.info + number = nil + if info.present? + number = info['version']['number'].to_s + end + next if number.blank? || number =~ /^[2-4]\./ || number =~ /^5\.[0-5]\./ + + # update processors + pipeline = Setting.get('es_pipeline') + next if pipeline.blank? + print 'delete pipeline (pipeline)... ' + SearchIndexBackend.processors( + "_ingest/pipeline/#{pipeline}": [ + { + action: 'delete', + }, + ] + ) + puts 'done' end task :reload, [:opts] => :environment do |_t, _args| From 6cd203a050cdb10654092850e6f230d6ff79d1e5 Mon Sep 17 00:00:00 2001 From: Felix Niklas Date: Sat, 16 Dec 2017 13:57:12 +0100 Subject: [PATCH 071/196] Chat: fix open/close icon and status alignment --- public/assets/chat/chat.css | 7 +++++-- public/assets/chat/chat.scss | 9 +++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/public/assets/chat/chat.css b/public/assets/chat/chat.css index 255fa5b7a..81d219049 100644 --- a/public/assets/chat/chat.css +++ b/public/assets/chat/chat.css @@ -71,8 +71,11 @@ height: 100%; width: 3.4em; text-align: center; - line-height: 3.5em; + line-height: 3.4em; cursor: pointer; } + .zammad-chat-header-icon:before { + content: ""; + display: inline-block; } .zammad-chat-header-icon-open, .zammad-chat-header-icon-close { @@ -107,7 +110,7 @@ font-weight: bold; } .zammad-chat-agent-status { - margin: 0 1em; + margin: 0.25em 1em; display: inline-block; line-height: 2em; padding: 0 .7em; diff --git a/public/assets/chat/chat.scss b/public/assets/chat/chat.scss index 64f0c4cca..2c85d78fc 100644 --- a/public/assets/chat/chat.scss +++ b/public/assets/chat/chat.scss @@ -80,8 +80,13 @@ height: 100%; width: 3.4em; text-align: center; - line-height: 3.5em; + line-height: 3.4em; cursor: pointer; + + &:before { // force the icon to align to center + content: ""; + display: inline-block; + } } .zammad-chat-header-icon-open, @@ -125,7 +130,7 @@ } .zammad-chat-agent-status { - margin: 0 1em; + margin: 0.25em 1em; display: inline-block; line-height: 2em; padding: 0 .7em; From 9c54b3382d2287efc4552bcf13e83fe8fc45d325 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Mon, 18 Dec 2017 04:36:56 +0100 Subject: [PATCH 072/196] Added support to search for chat sessions. Added set name and tags for chats. --- .../javascripts/app/controllers/chat.coffee | 174 ++++++++++++------ .../javascripts/app/controllers/search.coffee | 1 + .../app/models/chat_sessions.coffee | 32 ++++ .../views/customer_chat/chat_window.jst.eco | 23 ++- .../customer_chat/chat_window_info.jst.eco | 17 -- app/controllers/search_controller.rb | 17 +- app/models/chat/session.rb | 11 ++ app/models/chat/session/assets.rb | 56 ++++++ app/models/chat/session/search.rb | 80 ++++++++ app/models/chat/session/search_index.rb | 36 ++++ lib/sessions/event/base.rb | 18 +- lib/sessions/event/chat_session_update.rb | 35 ++++ test/unit/model_test.rb | 4 +- 13 files changed, 417 insertions(+), 87 deletions(-) create mode 100644 app/assets/javascripts/app/models/chat_sessions.coffee delete mode 100644 app/assets/javascripts/app/views/customer_chat/chat_window_info.jst.eco create mode 100644 app/models/chat/session/assets.rb create mode 100644 app/models/chat/session/search.rb create mode 100644 app/models/chat/session/search_index.rb create mode 100644 lib/sessions/event/chat_session_update.rb diff --git a/app/assets/javascripts/app/controllers/chat.coffee b/app/assets/javascripts/app/controllers/chat.coffee index 2966e3b4c..8eb974089 100644 --- a/app/assets/javascripts/app/controllers/chat.coffee +++ b/app/assets/javascripts/app/controllers/chat.coffee @@ -35,7 +35,7 @@ class App.CustomerChat extends App.Controller active_agent_ids: [] @render() - @on 'layout-has-changed', @propagateLayoutChange + @on('layout-has-changed', @propagateLayoutChange) # update navbar on new status @bind('chat_status_agent', (data) => @@ -163,6 +163,11 @@ class App.CustomerChat extends App.Controller @title 'Customer Chat', true @navupdate '#customer_chat' + if params.session_id && App.ChatSession.exists(params.session_id) + session = App.ChatSession.find(params.session_id) + @addChat(session) + @navigate '#customer_chat' + active: (state) => return @shown if state is undefined @shown = state @@ -264,10 +269,11 @@ class App.CustomerChat extends App.Controller addChat: (session) -> return if @chatWindows[session.session_id] - chat = new ChatWindow + chat = new ChatWindow( session: session removeCallback: @removeChat messageCallback: @updateNavMenu + ) @workspace.append chat.el chat.render() @@ -289,7 +295,7 @@ class App.CustomerChat extends App.Controller propagateLayoutChange: (event) => # adjust scroll position on layoutChange for session_id, chat of @chatWindows - chat.trigger 'layout-changed' + chat.trigger('layout-changed') acceptChat: => return if @windowCount() >= @maxChatWindows @@ -324,19 +330,6 @@ class App.CustomerChat extends App.Controller currentPosition: => @$('.main').scrollTop() -class CustomerChatRouter extends App.ControllerPermanent - requiredPermission: 'chat.agent' - constructor: (params) -> - super - - App.TaskManager.execute( - key: 'CustomerChat' - controller: 'CustomerChat' - params: {} - show: true - persistent: true - ) - class ChatWindow extends App.Controller className: 'chat-window' @@ -348,6 +341,8 @@ class ChatWindow extends App.Controller 'click .js-close': 'close' 'click .js-disconnect': 'disconnect' 'click .js-scrollHint': 'onScrollHintClick' + 'click .js-info': 'toggleMeta' + 'submit .js-metaForm': 'sendMetaForm' elements: '.js-customerChatInput': 'input' @@ -355,8 +350,11 @@ class ChatWindow extends App.Controller '.js-close': 'closeButton' '.js-disconnect': 'disconnectButton' '.js-body': 'body' + '.js-meta': 'meta' + '.js-name': 'metaName' '.js-scrollHolder': 'scrollHolder' '.js-scrollHint': 'scrollHint' + '.js-metaForm': 'metaForm' sounds: message: new Audio('assets/sounds/chat_message.mp3') @@ -374,9 +372,11 @@ class ChatWindow extends App.Controller @scrollSnapTolerance = 10 # pixels @chat = App.Chat.find(@session.chat_id) - @name = "#{@chat.displayName()} ##{@session.id}" + @name = @chat.displayName() + if @session && !_.isEmpty(@session.name) + @name = @session.name - @on 'layout-change', @onLayoutChange + @on('layout-change', @onLayoutChange) @bind('chat_session_typing', (data) => return if data.session_id isnt @session.session_id @@ -413,12 +413,44 @@ class ChatWindow extends App.Controller onLayoutChange: => @scrollToBottom() - render: -> - @html App.view('customer_chat/chat_window') - name: @name + toggleMeta: => + if @meta.hasClass('hidden') + @showMeta() + else + @hideMeta() - @el.one 'transitionend', @onTransitionend - @scrollHolder.scroll @detectScrolledtoBottom + hideMeta: => + @body.removeClass('hidden') + @meta.addClass('hidden') + @sendMetaForm() + + showMeta: => + @body.addClass('hidden') + @meta.removeClass('hidden') + + sendMetaForm: (e) => + if e + e.preventDefault() + params = @formParam(@metaForm) + + App.WebSocket.send( + event:'chat_session_update' + data: + session_id: @session.session_id + name: params.name + tags: params.tags + ) + + @metaName.text(params.name) + + render: -> + @html App.view('customer_chat/chat_window')( + name: @name + session: @session + ) + + @el.one('transitionend', @onTransitionend) + @scrollHolder.scroll(@detectScrolledtoBottom) # force repaint @el.prop('offsetHeight') @@ -426,18 +458,24 @@ class ChatWindow extends App.Controller # @addMessage 'Hello. My name is Roger, how can I help you?', 'agent' if @session + + # set chat to offline if state is already closed + activeChat = true + if @session.state is 'closed' + activeChat = false + if @session && @session.preferences && @session.preferences.url - @addNoticeMessage(@session.preferences.url) + @addNoticeMessage(@session.preferences.url, undefined, activeChat) if @session.messages for message in @session.messages if message.created_by_id - @addMessage message.content, 'agent' + @addMessage(message.content, 'agent', false, activeChat) else - @addMessage message.content, 'customer' + @addMessage(message.content, 'customer', false, activeChat) # send init reply - if !@session.messages || _.isEmpty(@session.messages) + if activeChat && _.isEmpty(@session.messages) preferences = @Session.get('preferences') if preferences.chat && preferences.chat.phrase phrases = preferences.chat.phrase[@session.chat_id] @@ -447,20 +485,9 @@ class ChatWindow extends App.Controller @input.html(phrase) @sendMessage(1600) - @$('.js-info').popover( - trigger: 'hover' - html: true - animation: false - delay: 0 - placement: 'bottom' - container: 'body' # place in body do prevent it from animating - title: -> - App.i18n.translateContent('Details') - content: => - App.view('customer_chat/chat_window_info')( - session: @session - ) - ) + # set chat to offline if state is already closed + if !activeChat + @goOffline() # show text module UI new App.WidgetTextModule( @@ -470,6 +497,18 @@ class ChatWindow extends App.Controller config: App.Config.all() ) + configureAttributesOutbound = [ + { name: 'name', display: 'Name', tag: 'input', null: true, }, + { name: 'tags', display: 'Tags', tag: 'tag', null: true, }, + ] + new App.ControllerForm( + el: @$('.js-metaForm') + model: + configure_attributes: configureAttributesOutbound + className: '' + params: @session + ) + focus: => @input.focus() @@ -498,7 +537,8 @@ class ChatWindow extends App.Controller @goOffline() close: => - @el.one 'transitionend', { callback: @release }, @onTransitionend + @sendMetaForm() + @el.one('transitionend', { callback: @release }, @onTransitionend) @el.removeClass('is-open') if @removeCallback @removeCallback(@session.session_id) @@ -577,7 +617,8 @@ class ChatWindow extends App.Controller ) @delay(send, delay) - @addMessage content, 'agent' + @hideMeta() + @addMessage(content, 'agent') @input.html('') updateModified: (state) => @@ -614,18 +655,19 @@ class ChatWindow extends App.Controller @messageCallback(@session.session_id) @unreadMessagesCounter = 0 - addMessage: (message, sender, isNew) => - @maybeAddTimestamp() + addMessage: (message, sender, isNew, useMaybeAddTimestamp = true) => + @maybeAddTimestamp() if useMaybeAddTimestamp @lastAddedType = sender - @body.append App.view('customer_chat/chat_message') + @body.append App.view('customer_chat/chat_message')( message: message sender: sender isNew: isNew timestamp: Date.now() + ) - @scrollToBottom showHint: true + @scrollToBottom(showHint: true) showWritingLoader: => if !@isTyping @@ -667,33 +709,37 @@ class ChatWindow extends App.Controller @lastAddedType = 'timestamp' addTimestamp: (label, time) => - @body.append App.view('customer_chat/chat_timestamp') + @body.append App.view('customer_chat/chat_timestamp')( label: label time: time + ) updateLastTimestamp: (label, time) -> @body .find('.js-timestamp') .last() - .replaceWith App.view('customer_chat/chat_timestamp') + .replaceWith App.view('customer_chat/chat_timestamp')( label: label time: time + ) - addStatusMessage: (message, args) -> - @maybeAddTimestamp() + addStatusMessage: (message, args, useMaybeAddTimestamp = true) -> + @maybeAddTimestamp() if useMaybeAddTimestamp - @body.append App.view('customer_chat/chat_status_message') + @body.append App.view('customer_chat/chat_status_message')( message: message args: args + ) @scrollToBottom() - addNoticeMessage: (message, args) -> - @maybeAddTimestamp() + addNoticeMessage: (message, args, useMaybeAddTimestamp = true) -> + @maybeAddTimestamp() if useMaybeAddTimestamp - @body.append App.view('customer_chat/chat_notice_message') + @body.append App.view('customer_chat/chat_notice_message')( message: message args: args + ) @scrollToBottom() @@ -784,6 +830,24 @@ class Setting extends App.ControllerModal msg: App.i18n.translateContent(data.message) ) +class CustomerChatRouter extends App.ControllerPermanent + requiredPermission: 'chat.agent' + constructor: (params) -> + super + + # cleanup params + clean_params = + session_id: params.session_id + + App.TaskManager.execute( + key: 'CustomerChat' + controller: 'CustomerChat' + params: clean_params + show: true + persistent: true + ) + App.Config.set('customer_chat', CustomerChatRouter, 'Routes') +App.Config.set('customer_chat/session/:session_id', CustomerChatRouter, 'Routes') App.Config.set('CustomerChat', { controller: 'CustomerChat', permission: ['chat.agent'] }, 'permanentTask') App.Config.set('CustomerChat', { prio: 1200, parent: '', name: 'Customer Chat', target: '#customer_chat', key: 'CustomerChat', shown: false, permission: ['chat.agent'], class: 'chat' }, 'NavBar') diff --git a/app/assets/javascripts/app/controllers/search.coffee b/app/assets/javascripts/app/controllers/search.coffee index 9a6f45052..67e9c009a 100644 --- a/app/assets/javascripts/app/controllers/search.coffee +++ b/app/assets/javascripts/app/controllers/search.coffee @@ -79,6 +79,7 @@ class App.Search extends App.Controller @tabs = [] for model in App.Config.get('models_searchable') + model = model.replace(/::/, '') tab = name: model model: model diff --git a/app/assets/javascripts/app/models/chat_sessions.coffee b/app/assets/javascripts/app/models/chat_sessions.coffee new file mode 100644 index 000000000..d32fccd18 --- /dev/null +++ b/app/assets/javascripts/app/models/chat_sessions.coffee @@ -0,0 +1,32 @@ +class App.ChatSession extends App.Model + @configure 'ChatSession', 'name', 'note' + @extend Spine.Model.Ajax + @url: @apiPath + '/chat_sessions' + + @configure_attributes = [ + { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, 'null': false } + { name: 'state', display: 'State', readonly: 1 } + { name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 } + { name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 } + { name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 } + { name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 } + ] + + @configure_overview = [ + 'name', + 'state', + 'created_at', + ] + + uiUrl: -> + "#customer_chat/session/#{@id}" + + searchResultAttributes: -> + displayName = '' + if !_.isEmpty(@name) + displayName = @displayName() + display: "##{@id} #{displayName}" + id: @id + class: 'chat_session chat_session-popover' + url: @uiUrl() + icon: 'chat' diff --git a/app/assets/javascripts/app/views/customer_chat/chat_window.jst.eco b/app/assets/javascripts/app/views/customer_chat/chat_window.jst.eco index 1fef2ed9b..de2f271da 100644 --- a/app/assets/javascripts/app/views/customer_chat/chat_window.jst.eco +++ b/app/assets/javascripts/app/views/customer_chat/chat_window.jst.eco @@ -7,9 +7,7 @@
                                  - <%= @name %>
                                  -
                                  <%- @Icon('info') %>
                                  -
                                  + <%= @name %> #<%= @session.id %>
                                  <%- @T('disconnect') %>
                                  @@ -24,6 +22,25 @@
                                  +
                                  diff --git a/app/assets/javascripts/app/views/customer_chat/chat_window_info.jst.eco b/app/assets/javascripts/app/views/customer_chat/chat_window_info.jst.eco deleted file mode 100644 index a7c3b1ff3..000000000 --- a/app/assets/javascripts/app/views/customer_chat/chat_window_info.jst.eco +++ /dev/null @@ -1,17 +0,0 @@ -
                                  -
                                    -<% if @session: %> -
                                  • <%- @T('Created at') %>: <%- @Ttimestamp(@session.created_at) %> -<% end %> -<% if @session && @session.preferences: %> - <% if @session.preferences.geo_ip: %> -
                                  • GeoIP: <%= @session.preferences.geo_ip.country_name %> <%= @session.preferences.geo_ip.city_name %> - <% end %> - <% if @session.preferences.remote_ip: %> -
                                  • IP: <%= @session.preferences.remote_ip %> - <% end %> - <% if @session.preferences.dns_name: %> -
                                  • DNS: <%= @session.preferences.dns_name %> - <% end %> -<% end %> -
                                  \ No newline at end of file diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 1ec9c76f8..fad61f23c 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -33,9 +33,10 @@ class SearchController < ApplicationController objects_in_order = [] objects_in_order_hash = {} objects.each do |object| - preferences = object.constantize.search_preferences(current_user) + local_class = object.constantize + preferences = local_class.search_preferences(current_user) next if !preferences - objects_in_order_hash[preferences[:prio]] = object + objects_in_order_hash[preferences[:prio]] = local_class end objects_in_order_hash.keys.sort.reverse_each do |prio| objects_in_order.push objects_in_order_hash[prio] @@ -64,16 +65,18 @@ class SearchController < ApplicationController items = SearchIndexBackend.search(query, limit, objects_with_direct_search_index) items.each do |item| require item[:type].to_filename - record = Kernel.const_get(item[:type]).lookup(id: item[:id]) + local_class = Kernel.const_get(item[:type]) + record = local_class.lookup(id: item[:id]) next if !record assets = record.assets(assets) + item[:type] = local_class.to_app_model.to_s result.push item end end # e. g. do ticket query by Ticket class to handle ticket permissions objects_without_direct_search_index.each do |object| - object_result = search_generic_backend(object, query, limit, current_user, assets) + object_result = search_generic_backend(object.constantize, query, limit, current_user, assets) if object_result.present? result = result.concat(object_result) end @@ -83,7 +86,7 @@ class SearchController < ApplicationController result_in_order = [] objects_in_order.each do |object| result.each do |item| - next if item[:type] != object + next if item[:type] != object.to_app_model.to_s item[:id] = item[:id].to_i result_in_order.push item end @@ -110,7 +113,7 @@ class SearchController < ApplicationController private def search_generic_backend(object, query, limit, current_user, assets) - found_objects = object.constantize.search( + found_objects = object.search( query: query, limit: limit, current_user: current_user, @@ -119,7 +122,7 @@ class SearchController < ApplicationController found_objects.each do |found_object| item = { id: found_object.id, - type: found_object.class.to_s + type: found_object.class.to_app_model.to_s } result.push item assets = found_object.assets(assets) diff --git a/app/models/chat/session.rb b/app/models/chat/session.rb index 70cd0d818..cf83beea4 100644 --- a/app/models/chat/session.rb +++ b/app/models/chat/session.rb @@ -1,4 +1,15 @@ class Chat::Session < ApplicationModel + include HasSearchIndexBackend + include HasTags + + extend Chat::Session::Search + load 'chat/session/search_index.rb' + include Chat::Session::SearchIndex + load 'chat/session/assets.rb' + include Chat::Session::Assets + + has_many :messages, class_name: 'Chat::Message', foreign_key: 'chat_session_id' + before_create :generate_session_id store :preferences diff --git a/app/models/chat/session/assets.rb b/app/models/chat/session/assets.rb new file mode 100644 index 000000000..6410e2692 --- /dev/null +++ b/app/models/chat/session/assets.rb @@ -0,0 +1,56 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +module Chat::Session::Assets + +=begin + +get all assets / related models for this chat + + chat = Chat::Session.find(123) + result = Chat::Session.assets(assets_if_exists) + +returns + + result = { + users: { + 123: user_model_123, + 1234: user_model_1234, + }, + chat_sessions: [ chat_session_model1 ] + } + +=end + + def assets(data) + + app_model_chat_session = Chat::Session.to_app_model + app_model_chat = Chat.to_app_model + app_model_user = User.to_app_model + + data[ app_model_chat_session ] ||= {} + + if !data[ app_model_chat_session ][ id ] + data[ app_model_chat_session ][ id ] = attributes_with_association_ids + data[ app_model_chat_session ][ id ]['messages'] = [] + messages.each do |message| + data[ app_model_chat_session ][ id ]['messages'].push message.attributes + end + data[ app_model_chat_session ][ id ]['tags'] = tag_list + end + + if !data[ app_model_chat ] || !data[ app_model_chat ][ chat_id ] + chat = Chat.lookup(id: chat_id) + if chat + data = chat.assets(data) + end + end + + %w[created_by_id updated_by_id].each do |local_user_id| + next if !self[ local_user_id ] + next if data[ app_model_user ] && data[ app_model_user ][ self[ local_user_id ] ] + user = User.lookup(id: self[ local_user_id ]) + next if !user + data = user.assets(data) + end + data + end +end diff --git a/app/models/chat/session/search.rb b/app/models/chat/session/search.rb new file mode 100644 index 000000000..87d12f1c9 --- /dev/null +++ b/app/models/chat/session/search.rb @@ -0,0 +1,80 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ + +class Chat::Session + module Search + +=begin + +search organizations preferences + + result = Chat::Session.search_preferences(user_model) + +returns if user has permissions to search + + result = { + prio: 1000, + direct_search_index: true + } + +returns if user has no permissions to search + + result = false + +=end + + def search_preferences(current_user) + return false if Setting.get('chat') != true || !current_user.permissions?('chat.agent') + { + prio: 900, + direct_search_index: true, + } + end + +=begin + +search organizations + + result = Chat::Session.search( + current_user: User.find(123), + query: 'search something', + limit: 15, + ) + +returns + + result = [organization_model1, organization_model2] + +=end + + def search(params) + + # get params + query = params[:query] + limit = params[:limit] || 10 + current_user = params[:current_user] + + # enable search only for agents and admins + return [] if !search_preferences(current_user) + + # try search index backend + if SearchIndexBackend.enabled? + items = SearchIndexBackend.search(query, limit, 'Chat::Session') + chat_sessions = [] + items.each do |item| + chat_session = Chat::Session.lookup(id: item[:id]) + next if !chat_session + chat_sessions.push chat_session + end + return chat_sessions + end + + # fallback do sql query + # - stip out * we already search for *query* - + query.delete! '*' + chat_sessions = Chat::Session.where( + 'name LIKE ?', "%#{query}%" + ).order('name').limit(limit).to_a + chat_sessions + end + end +end diff --git a/app/models/chat/session/search_index.rb b/app/models/chat/session/search_index.rb new file mode 100644 index 000000000..b81196722 --- /dev/null +++ b/app/models/chat/session/search_index.rb @@ -0,0 +1,36 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +module Chat::Session::SearchIndex + +=begin + +lookup name of ref. objects + + chat_session = Chat::Session.find(123) + result = chat_session.search_index_attribute_lookup + +returns + + attributes # object with lookup data + +=end + + def search_index_attribute_lookup + attributes = super + return if !attributes + + attributes[:tag] = tag_list + + messages = Chat::Message.where(chat_session_id: id) + attributes['messages'] = [] + messages.each do |message| + + # lookup attributes of ref. objects (normally name and note) + message_attributes = message.search_index_attribute_lookup + + attributes['messages'].push message_attributes + end + + attributes + end + +end diff --git a/lib/sessions/event/base.rb b/lib/sessions/event/base.rb index 5c1b659f2..1b02e6bfe 100644 --- a/lib/sessions/event/base.rb +++ b/lib/sessions/event/base.rb @@ -49,7 +49,7 @@ class Sessions::Event::Base true end - def permission_check(key, event) + def current_user_id if !@session error = { event: "#{event}_error", @@ -60,7 +60,7 @@ class Sessions::Event::Base Sessions.send(@client_id, error) return end - if !@session['id'] + if @session['id'].blank? error = { event: "#{event}_error", data: { @@ -70,7 +70,13 @@ class Sessions::Event::Base Sessions.send(@client_id, error) return end - user = User.lookup(id: @session['id']) + @session['id'] + end + + def current_user + user_id = current_user_id + return if !user_id + user = User.find_by(id: user_id) if !user error = { event: "#{event}_error", @@ -81,6 +87,12 @@ class Sessions::Event::Base Sessions.send(@client_id, error) return end + user + end + + def permission_check(key, event) + user = current_user + return if !user if !user.permissions?(key) error = { event: "#{event}_error", diff --git a/lib/sessions/event/chat_session_update.rb b/lib/sessions/event/chat_session_update.rb new file mode 100644 index 000000000..0f93dfc31 --- /dev/null +++ b/lib/sessions/event/chat_session_update.rb @@ -0,0 +1,35 @@ +class Sessions::Event::ChatSessionUpdate < Sessions::Event::ChatBase + + def run + return super if super + return if !check_chat_session_exists + return if !permission_check('chat.agent', 'chat') + chat_session = current_chat_session + + if @payload['data']['name'] != chat_session.name + chat_session.name = @payload['data']['name'] + chat_session.save! + end + + if @payload['data']['tags'] + new_tags = @payload['data']['tags'].split(',') + + new_tags.each(&:strip!) + + tags = chat_session.tag_list + new_tags.each do |new_tag| + next if new_tag.blank? + next if tags.include?(new_tag) + chat_session.tag_add(new_tag, current_user_id) + end + + tags.each do |tag| + next if new_tags.include?(tag) + chat_session.tag_remove(tag, current_user_id) + end + end + + nil + end + +end diff --git a/test/unit/model_test.rb b/test/unit/model_test.rb index 9c5431539..d31e69fbb 100644 --- a/test/unit/model_test.rb +++ b/test/unit/model_test.rb @@ -243,8 +243,8 @@ class ModelTest < ActiveSupport::TestCase assert(searchable.include?(Ticket)) assert(searchable.include?(User)) assert(searchable.include?(Organization)) - assert_equal(3, searchable.count) - + assert(searchable.include?(Chat::Session)) + assert_equal(4, searchable.count) end end From fe2c77df9fa8765fa447da4fdca644dc3c82b212 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Mon, 18 Dec 2017 04:49:08 +0100 Subject: [PATCH 073/196] Just update chat window title if title exists. --- app/assets/javascripts/app/controllers/chat.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/app/controllers/chat.coffee b/app/assets/javascripts/app/controllers/chat.coffee index 8eb974089..091c771b2 100644 --- a/app/assets/javascripts/app/controllers/chat.coffee +++ b/app/assets/javascripts/app/controllers/chat.coffee @@ -441,7 +441,8 @@ class ChatWindow extends App.Controller tags: params.tags ) - @metaName.text(params.name) + if !_.isEmpty(params.name) + @metaName.text(params.name) render: -> @html App.view('customer_chat/chat_window')( From 9805fa176a30416c5e7c4250c127f875ab7ffea1 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Tue, 19 Dec 2017 09:08:58 +0100 Subject: [PATCH 074/196] Enabled tests, Zendesk infrastructure is working again. --- .gitlab-ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2b5140aa4..45e50bd9c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -344,7 +344,6 @@ test:integration:zendesk_mysql: - rake db:migrate - ruby -I test/ test/integration/zendesk_import_test.rb - rake db:drop - allow_failure: true test:integration:zendesk_postgresql: stage: test @@ -357,7 +356,6 @@ test:integration:zendesk_postgresql: - rake db:migrate - ruby -I test/ test/integration/zendesk_import_test.rb - rake db:drop - allow_failure: true test:integration:otrs_5_mysql: stage: test From aef865a916bd2dfcf771e1d2d7a7392dc3003c0b Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Wed, 20 Dec 2017 10:55:54 +0100 Subject: [PATCH 075/196] Fixed issue #1723 - Wrong ticket number count in preview. --- .../_settings/area_ticket_number.coffee | 2 +- test/unit/ticket_number_test.rb | 106 ++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 test/unit/ticket_number_test.rb diff --git a/app/assets/javascripts/app/controllers/_settings/area_ticket_number.coffee b/app/assets/javascripts/app/controllers/_settings/area_ticket_number.coffee index 70ccfb563..dcbd9f6f6 100644 --- a/app/assets/javascripts/app/controllers/_settings/area_ticket_number.coffee +++ b/app/assets/javascripts/app/controllers/_settings/area_ticket_number.coffee @@ -42,7 +42,7 @@ class App.SettingsAreaTicketNumber extends App.Controller number = "#{App.Config.get('ticket_hook')}#{App.Config.get('system_id')}" counter = '1' if paramsItem.min_size - minSize = parseInt(paramsItem.min_size) + minSize = parseInt(paramsItem.min_size) - "#{App.Config.get('system_id')}".length if paramsItem.checksum minSize -= 1 if minSize > 1 diff --git a/test/unit/ticket_number_test.rb b/test/unit/ticket_number_test.rb new file mode 100644 index 000000000..a72019bff --- /dev/null +++ b/test/unit/ticket_number_test.rb @@ -0,0 +1,106 @@ + +require 'test_helper' + +class TicketNumberTest < ActiveSupport::TestCase + test 'number' do + Setting.set('ticket_number_increment', { checksum: false, min_size: 5 }) + Setting.set('system_id', 1) + + number = Ticket::Number.generate + assert_equal(number.to_s.length, 5) + + Setting.set('ticket_number_increment', { checksum: false, min_size: 10 }) + Setting.set('system_id', 1) + + number = Ticket::Number.generate + assert_equal(number.to_s.length, 10) + + Setting.set('ticket_number_increment', { checksum: true, min_size: 5 }) + Setting.set('system_id', 1) + + number = Ticket::Number.generate + assert_equal(number.to_s.length, 5) + + Setting.set('ticket_number_increment', { checksum: true, min_size: 10 }) + Setting.set('system_id', 1) + + number = Ticket::Number.generate + assert_equal(number.to_s.length, 10) + + Setting.set('ticket_number_increment', { checksum: false, min_size: 5 }) + Setting.set('system_id', 88) + + number = Ticket::Number.generate + assert_equal(number.to_s.length, 5) + + Setting.set('ticket_number_increment', { checksum: false, min_size: 10 }) + Setting.set('system_id', 88) + + number = Ticket::Number.generate + assert_equal(number.to_s.length, 10) + + Setting.set('ticket_number_increment', { checksum: true, min_size: 5 }) + Setting.set('system_id', 88) + + number = Ticket::Number.generate + assert_equal(number.to_s.length, 5) + + Setting.set('ticket_number_increment', { checksum: true, min_size: 10 }) + Setting.set('system_id', 88) + + number = Ticket::Number.generate + assert_equal(number.to_s.length, 10) + + 150.times do + number = Ticket::Number.generate + assert_equal(number.to_s.length, 10) + end + + end + + test 'date' do + Setting.set('ticket_number', 'Ticket::Number::Date') + Setting.set('ticket_number_date', { checksum: false }) + Setting.set('system_id', 1) + system_id = Setting.get('system_id') + number_prefix = "#{Time.zone.now.strftime('%Y%m%d')}#{system_id}" + + number = Ticket::Number.generate + assert_equal(number.to_s.length, 13) + assert_match(/#{number_prefix}/, number.to_s) + + Setting.set('ticket_number_date', { checksum: false }) + Setting.set('system_id', 88) + + number = Ticket::Number.generate + system_id = Setting.get('system_id') + number_prefix = "#{Time.zone.now.strftime('%Y%m%d')}#{system_id}" + assert_equal(number.to_s.length, 14) + assert_match(/#{number_prefix}/, number.to_s) + + Setting.set('ticket_number_date', { checksum: true }) + Setting.set('system_id', 1) + + number = Ticket::Number.generate + system_id = Setting.get('system_id') + number_prefix = "#{Time.zone.now.strftime('%Y%m%d')}#{system_id}" + assert_equal(number.to_s.length, 14) + assert_match(/#{number_prefix}/, number.to_s) + + Setting.set('ticket_number_date', { checksum: true }) + Setting.set('system_id', 88) + + number = Ticket::Number.generate + system_id = Setting.get('system_id') + number_prefix = "#{Time.zone.now.strftime('%Y%m%d')}#{system_id}" + assert_equal(number.to_s.length, 15) + assert_match(/#{number_prefix}/, number.to_s) + + 150.times do + number = Ticket::Number.generate + assert_equal(number.to_s.length, 15) + end + + end + +end From e9f4b5af086a439020d975fa9a5893a2dbfc1fe2 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Wed, 20 Dec 2017 17:15:40 +0100 Subject: [PATCH 076/196] Improved error handling of email processing with invalid email addresses. --- app/models/channel/filter/identify_sender.rb | 15 ++-- test/unit/email_process_test.rb | 72 +++++++++++++++++++- 2 files changed, 80 insertions(+), 7 deletions(-) diff --git a/app/models/channel/filter/identify_sender.rb b/app/models/channel/filter/identify_sender.rb index 7818f251f..9b271c556 100644 --- a/app/models/channel/filter/identify_sender.rb +++ b/app/models/channel/filter/identify_sender.rb @@ -95,16 +95,19 @@ module Channel::Filter::IdentifySender max_count = 40 current_count = 0 ['raw-to', 'raw-cc'].each do |item| - next if !mail[item.to_sym] + next if mail[item.to_sym].blank? begin - next if !mail[item.to_sym].addrs items = mail[item.to_sym].addrs + next if items.blank? items.each do |address_data| - next if address_data.address.blank? + email_address = address_data.address + next if email_address.blank? + next if email_address !~ /@/ + next if email_address.match?(/\s/) user_create( firstname: address_data.display_name, lastname: '', - email: address_data.address, + email: email_address, ) current_count += 1 return false if current_count == max_count @@ -126,6 +129,8 @@ module Channel::Filter::IdentifySender display_name = $1 end next if address.blank? + next if address !~ /@/ + next if address.match?(/\s/) user_create( firstname: display_name, lastname: '', @@ -176,7 +181,7 @@ module Channel::Filter::IdentifySender data[:updated_by_id] = 1 data[:created_by_id] = 1 - user = User.create(data) + user = User.create!(data) user.update!( updated_by_id: user.id, created_by_id: user.id, diff --git a/test/unit/email_process_test.rb b/test/unit/email_process_test.rb index b945e0737..a16cbd090 100644 --- a/test/unit/email_process_test.rb +++ b/test/unit/email_process_test.rb @@ -203,6 +203,74 @@ Some Text", ], }, }, + { + data: "From: sender@example.com +To: some_new_customer423@example.com +Cc: some recipient , max +Subject: abc some subject2 + +Some Text", + success: true, + result: { + 0 => { + priority: '2 normal', + title: 'abc some subject2', + }, + 1 => { + body: 'Some Text', + sender: 'Customer', + type: 'email', + internal: false, + }, + }, + verify: { + users: [ + { + firstname: 'max', + lastname: '', + fullname: 'max', + email: 'somebody_else@example.com', + }, + { + firstname: '', + lastname: '', + fullname: 'some_new_customer423@example.com', + email: 'some_new_customer423@example.com', + }, + ], + }, + }, + { + data: "From: sender@example.com +To: some_new_customer424@example.com +Subject: abc some subject3 +Reply-To: some user + +Some Text", + success: true, + result: { + 0 => { + priority: '2 normal', + title: 'abc some subject3', + }, + 1 => { + body: 'Some Text', + sender: 'Customer', + type: 'email', + internal: false, + }, + }, + verify: { + users: [ + { + firstname: '', + lastname: '', + fullname: 'some_new_customer424@example.com', + email: 'some_new_customer424@example.com', + }, + ], + }, + }, { data: "From: me@example.com To: Alexander Ha , @@ -2812,9 +2880,9 @@ Some Text', # verify if users are created if file[:verify][:users] file[:verify][:users].each { |user_result| - user = User.where(email: user_result[:email]).first + user = User.where(email: user_result[:email].downcase).first if !user - assert(false, "No user '#{user_result[:email]}' found!") + assert(false, "No user '#{user_result[:email].downcase}' found!") return end user_result.each { |key, value| From 6a288edc3fba63316311b9b7b21df1475f750fb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bauer?= Date: Fri, 22 Dec 2017 10:21:39 +0100 Subject: [PATCH 077/196] changed issue template --- .github/ISSUE_TEMPLATE.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 6ed32484e..58570b4f9 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,13 +1,15 @@ test
                                  " //var should = "
                                  test
                                  " - var should = "test" + var should = "test" var result = App.Utils.htmlCleanup($(source)) equal(result.html(), should, source) source = "" - should = "test" + should = "test" result = App.Utils.htmlCleanup(source) equal(result.html(), should, source) @@ -546,6 +546,11 @@ test("htmlCleanup", function() { result = App.Utils.htmlCleanup($(source)) equal(result.html(), should, source) + source = "

                                  some link to somewhere

                                  " + should = "some link to somewhere" + result = App.Utils.htmlCleanup($(source)) + equal(result.html(), should, source) + source = "

                                  some link to somewhere

                                  " should = "

                                  some link to somewhere

                                  " result = App.Utils.htmlCleanup($(source)) diff --git a/test/unit/aaa_string_test.rb b/test/unit/aaa_string_test.rb index 70f2da1be..f7b61ab6e 100644 --- a/test/unit/aaa_string_test.rb +++ b/test/unit/aaa_string_test.rb @@ -644,7 +644,11 @@ Men-----------------------' assert_equal(result, html.html2html_strict) html = 'http://what-different.example.com' - result = "http://example.com (http://what-different.example.com)" + result = "http://what-different.example.com" + assert_equal(result, html.html2html_strict) + + html = 'http://what-different.example.com' + result = "http://what-different.example.com" assert_equal(result, html.html2html_strict) html = 'http://EXAMPLE.com' @@ -676,7 +680,7 @@ Men-----------------------' assert_equal(result, html.html2html_strict) html = "" - result = 'http://example.com/?abc=123&123=abc' + result = '' assert_equal(result, html.html2html_strict) html = '

                                  https://wiki.lab.example.com/doku.php?id=xxxx:start&#ldap

                                  ' @@ -721,7 +725,7 @@ Men-----------------------' assert_equal(result, html.html2html_strict) html = "Damit Sie keinen Tag versäumen, empfehlen wir Ihnen den Link des Adventkalenders in
                                        Ihrer Lesezeichen-Symbolleiste zu ergänzen.

                                   " - result = "Damit Sie keinen Tag versäumen, empfehlen wir Ihnen den Link des Adventkalenders (http://newsletters.cylex.de/) in
                                  Ihrer Lesezeichen-Symbolleiste zu ergänzen.
                                  " + result = "Damit Sie keinen Tag versäumen, empfehlen wir Ihnen den Link des Adventkalenders in
                                  Ihrer Lesezeichen-Symbolleiste zu ergänzen.
                                  " assert_equal(result, html.html2html_strict) html = 'Hello Mr Smith,' @@ -955,18 +959,15 @@ html.html2html_strict assert_equal(result, html.html2html_strict) html = '

                                  ' - #result = '

                                  http://www.example.com/

                                  ' - result = '

                                  http://www.example.com/

                                  ' + result = '

                                  ' assert_equal(result, html.html2html_strict) html = '

                                  ' - #result = '

                                  http://www.example.com/?wm=mail

                                  ' - result = '

                                  http://www.example.com/?wm=mail

                                  ' + result = '

                                  ' assert_equal(result, html.html2html_strict) html = '

                                  ' - #result = '

                                  http://www.example.com/?wm=mail

                                  ' - result = '

                                  http://www.example.com/?wm=mail

                                  ' + result = '

                                  ' assert_equal(result, html.html2html_strict) html = '
                                  Wir brauchen also die Instanz example.zammad.com, kann die aber nicht mehr nutzen.

                                  Bitte um Freischaltung.


                                  ' @@ -980,7 +981,7 @@ html.html2html_strict assert_equal(result, html.html2html_strict) html = '' - result = "" + result = '' assert_equal(result, html.html2html_strict) html = '

                                   

                                  20-29
                                  200
                                  -1
                                  201
                                  country
                                  Target (gross)
                                  Remaining Recruits
                                  Total Recruits
                                  ' @@ -1010,7 +1011,7 @@ html.html2html_strict assert_equal(result, html.html2html_strict) html = '
                                3. Luxemburg
                                4. ' - result = '
                                5. Luxemburg (http://business-catalogs.example.com/ODtpbGs5MWIzbjUyYzExLTA4Yy06Mmg7N3AvL3R0bmFvY3B0LXlhbW9sc2Nhb3NnYy5lL3RpbXJlZi9lbS9ycnJuaWFpZXMsdGxnY25pLGUsdXJ0b3NVTGVpNWZ8fGZh)
                                6. ' + result = '
                                7. Luxemburg
                                8. ' assert_equal(result, html.html2html_strict) end diff --git a/test/unit/email_parser_test.rb b/test/unit/email_parser_test.rb index e0bd4808e..46575969e 100644 --- a/test/unit/email_parser_test.rb +++ b/test/unit/email_parser_test.rb @@ -42,7 +42,7 @@ Old programmers never die. They just branch to a new address. }, { data: IO.binread('test/fixtures/mail3.box'), - body_md5: '4681e5d8ee07ea0b53dfeaf5789c5a00', + body_md5: '0b6eb998e8903ba69a3528dedb5a5476', params: { from: '"Günther John | Example GmbH" ', from_email: 'k.guenther@example.com', @@ -50,7 +50,7 @@ Old programmers never die. They just branch to a new address. subject: 'Ticket Templates', content_type: 'text/html', body: "
                                  -

                                  Hallo Martin,

                                   

                                  ich möchte mich gern für den Beta-Test für die Ticket Templates unter XXXX 2.4 anmelden.

                                   

                                   

                                  Mit freundlichen Grüßen

                                  John Günther

                                   

                                  example.com (http://www.GeoFachDatenServer.de) – profitieren Sie vom umfangreichen Daten-Netzwerk

                                   

                                  _ __ ___ ____________________________ ___ __ _

                                   

                                  Example GmbH

                                  Some What

                                   

                                  Sitz: Someware-Straße 9, XXXXX Someware

                                   

                                  M: +49 (0) XXX XX XX 70

                                  T: +49 (0) XXX XX XX 22

                                  F: +49 (0) XXX XX XX 11

                                  W: http://www.example.de

                                   

                                  Geschäftsführer: John Smith

                                  HRB XXXXXX AG Someware

                                  St.-Nr.: 112/107/05858

                                   

                                  ISO 9001:2008 Zertifiziert -Qualitätsstandard mit Zukunft

                                  _ __ ___ ____________________________ ___ __ _

                                   

                                  Diese Information ist ausschließlich für den Adressaten bestimmt und kann vertrauliche oder gesetzlich geschützte Informationen enthalten. Wenn Sie nicht der bestimmungsgemäße Adressat sind, unterrichten Sie bitte den Absender und vernichten Sie diese Mail. Anderen als dem bestimmungsgemäßen Adressaten ist es untersagt, diese E-Mail zu lesen, zu speichern, weiterzuleiten oder ihren Inhalt auf welche Weise auch immer zu verwenden.

                                   

                                  +

                                  Hallo Martin,

                                   

                                  ich möchte mich gern für den Beta-Test für die Ticket Templates unter XXXX 2.4 anmelden.

                                   

                                   

                                  Mit freundlichen Grüßen

                                  John Günther

                                   

                                  example.com – profitieren Sie vom umfangreichen Daten-Netzwerk

                                   

                                  _ __ ___ ____________________________ ___ __ _

                                   

                                  Example GmbH

                                  Some What

                                   

                                  Sitz: Someware-Straße 9, XXXXX Someware

                                   

                                  M: +49 (0) XXX XX XX 70

                                  T: +49 (0) XXX XX XX 22

                                  F: +49 (0) XXX XX XX 11

                                  W: http://www.example.de

                                   

                                  Geschäftsführer: John Smith

                                  HRB XXXXXX AG Someware

                                  St.-Nr.: 112/107/05858

                                   

                                  ISO 9001:2008 Zertifiziert -Qualitätsstandard mit Zukunft

                                  _ __ ___ ____________________________ ___ __ _

                                   

                                  Diese Information ist ausschließlich für den Adressaten bestimmt und kann vertrauliche oder gesetzlich geschützte Informationen enthalten. Wenn Sie nicht der bestimmungsgemäße Adressat sind, unterrichten Sie bitte den Absender und vernichten Sie diese Mail. Anderen als dem bestimmungsgemäßen Adressaten ist es untersagt, diese E-Mail zu lesen, zu speichern, weiterzuleiten oder ihren Inhalt auf welche Weise auch immer zu verwenden.

                                   

                                  Von: Fritz Bauer [mailto:me@example.com]
                                  Gesendet: Donnerstag, 3. Mai 2012 11:51
                                  An: John Smith
                                  Cc: Smith, John Marian; johnel.fratczak@example.com; ole.brei@example.com; Günther John | Example GmbH; bkopon@example.com; john.heisterhagen@team.example.com; sven.rocked@example.com; michael.house@example.com; tgutzeit@example.com
                                  Betreff: Re: OTRS::XXX Erweiterung - Anhänge an CI's

                                   

                                  Hallo,

                                   

                                  ich versuche an den Punkten anzuknüpfen.

                                   

                                  a) LDAP Muster Konfigdatei

                                   

                                  @@ -115,14 +115,14 @@ Liebe Grüße! }, { data: IO.binread('test/fixtures/mail6.box'), - body_md5: 'a05afcf7de7be17e74f191a58974f682', + body_md5: '849105bdee623b4314b4c3daa2495471', params: { from: '"Hans BÄKOSchönland" ', from_email: 'me@bogen.net', from_display_name: 'Hans BÄKOSchönland', subject: 'utf8: 使って / ISO-8859-1: Priorität" / cp-1251: Сергей Углицких', content_type: 'text/html', - body: "

                                  this is a test



                                  Compare Cable, DSL or Satellite plans: As low as $2.95. (http://localhost/8HMZENUS/2737??PS=)

                                  Test1:–
                                  Test2:&
                                  Test3:∋
                                  Test4:&
                                  Test5:=", + body: "

                                  this is a test



                                  Compare Cable, DSL or Satellite plans: As low as $2.95.

                                  Test1:–
                                  Test2:&
                                  Test3:∋
                                  Test4:&
                                  Test5:=", }, }, #

                                  @@ -320,7 +320,7 @@ Managing Director: Martin Edenhofer }, { data: IO.binread('test/fixtures/mail11.box'), - body_md5: 'b211c9c28282ad0dd3fccbbf37d9928d', + body_md5: '260a815b0a7897e4219d210010008202', attachments: [ { md5: '08660cd33ce8c64b95bcf0207ff6c4d6', @@ -340,26 +340,29 @@ Managing Director: Martin Edenhofer

                                  -http://newsletters.cylex.de/ref/www.cylex.de/sid-105/uid-4134001/lid-2/http://web2.cylex.de/advent2012?b2b

                                  Lieber CYLEX Eintragsinhaber,

                                  das Jahr neigt sich dem Ende und die besinnliche Zeit beginnt laut Kalender mit dem
                                  1. Advent. Und wie immer wird es in der vorweihnachtlichen Zeit meist beruflich und privat
                                  so richtig schön hektisch.

                                  Um Ihre Weihnachtsstimmung in Schwung zu bringen kommen wir nun mit unserem Adventskalender ins Spiel. Denn 24 Tage werden Sie unsere netten Geschichten, Rezepte und Gewinnspiele sowie ausgesuchte Geschenktipps und Einkaufsgutscheine online begleiten. Damit lässt sich Ihre Freude auf das Fest garantiert mit jedem Tag steigern.

                                  +

                                  Lieber CYLEX Eintragsinhaber,

                                  das Jahr neigt sich dem Ende und die besinnliche Zeit beginnt laut Kalender mit dem
                                  1. Advent. Und wie immer wird es in der vorweihnachtlichen Zeit meist beruflich und privat
                                  so richtig schön hektisch.

                                  Um Ihre Weihnachtsstimmung in Schwung zu bringen kommen wir nun mit unserem Adventskalender ins Spiel. Denn 24 Tage werden Sie unsere netten Geschichten, Rezepte und Gewinnspiele sowie ausgesuchte Geschenktipps und Einkaufsgutscheine online begleiten. Damit lässt sich Ihre Freude auf das Fest garantiert mit jedem Tag steigern.

                                  Einen gemütlichen Start in die Adventszeit wünscht Ihnen -http://newsletters.cylex.de/ref/www.cylex.de/sid-105/uid-4134001/lid-1/http://web2.cylex.de/advent2012?b2b +

                                  Ihr CYLEX Team

                                  -P.S. Damit Sie keinen Tag versäumen, empfehlen wir Ihnen den Link des Adventkalenders (http://newsletters.cylex.de/ref/www.cylex.de/sid-105/uid-4134001/lid-3/http://web2.cylex.de/advent2012?b2b) in
                                  Ihrer Lesezeichen-Symbolleiste zu ergänzen.

                                   

                                  +P.S. Damit Sie keinen Tag versäumen, empfehlen wir Ihnen den Link des Adventkalenders in
                                  Ihrer Lesezeichen-Symbolleiste zu ergänzen.

                                   

                                  +
                                  serviceteam@cylex.de
                                  +Homepage
                                  +Newsletter abbestellen +
                                  Impressum
                                  S.C. CYLEX INTERNATIONAL S.N.C.
                                  Sat. Palota 119/A RO 417516 Palota Romania
                                  Tel.: +49 208/62957-0 |
                                  Geschäftsführer: Francisc Osvald
                                  Handelsregister: J05/1591/2009
                                  USt.IdNr.: RO26332771
                                  -
                                  serviceteam@cylex.de
                                  Homepage (http://newsletters.cylex.de/ref/www.cylex.de/sid-105/uid-4134001/lid-98/http://web2.cylex.de/Homepage/Home.asp)
                                  Newsletter abbestellen (http://newsletters.cylex.de/ref/www.cylex.de/sid-105/uid-4134001/lid-99/http://newsletters.cylex.de/unsubscribe.aspx?uid=4134001&d=www.cylex.de&e=enjoy@znuny.com&sc=3009&l=d)
                                  ", @@ -506,7 +509,7 @@ Managing Director: Martin Edenhofer }, { data: IO.binread('test/fixtures/mail20.box'), - body_md5: '7cdfb67ce7bf914fa0a5b85f0a365fdc', + body_md5: '56ad8d02f4c7641fd2bb8ebf484d36d7', params: { from: 'Health and Care-Mall ', from_email: 'drugs-cheapest8@sicor.com', @@ -520,7 +523,7 @@ Managing Director: Martin Edenhofer óû5aHw5³½IΨµÁxG⌊o8KHCmς9-Ö½23QgñV6UAD¿ùAX←t¨Lf7⊕®Ir²r½TLA5pYJhjV gPnãM36V®E89RUDΤÅ©ÈI9æsàCΘYEϒAfg∗bT¡1∫rIoiš¦O5oUIN±IsæSعPp Ÿÿq1FΧ⇑eGOz⌈F³R98y§ 74”lTr8r§HÐæuØEÛPËq VmkfB∫SKNElst4S∃Á8üTðG°í lY9åPu×8>RÒ¬⊕ΜIÙzÙCC4³ÌQEΡºSè!XgŒs. -çγ⇓BcwspC L I C K H E R Eëe3¸ ! (http://pxmzcgy.storeprescription.ru?zz=fkxffti)Calm dylan for school today.
                                  Closing the nursery with you down. Here and made the mess. Maybe the oï from under his mother. Song of course beth touched his pants.
                                  When someone who gave up from here. Feel of god knows what. +çγ⇓BcwspC L I C K H E R Eëe3¸ !Calm dylan for school today.
                                  Closing the nursery with you down. Here and made the mess. Maybe the oï from under his mother. Song of course beth touched his pants.
                                  When someone who gave up from here. Feel of god knows what. TBϖ∃M5T5ΕEf2û–N¶ÁvΖ'®⇓∝5SÐçË5 Χ0jΔHbAgþE—2i6A2lD⇑LGjÓnTOy»¦Hëτ9’:Their mother and tugged it seemed like @@ -595,7 +598,7 @@ Managing Director: Martin Edenhofer }, { data: IO.binread('test/fixtures/mail21.box'), - body_md5: '380ca2bca1d7e013abd4109459a06fac', + body_md5: '7cb50fe6b37420fe9aea61eb5badc25a', params: { from: 'Viagra Super Force Online ', from_email: 'pharmacy_affordable1@ertelecom.ru', @@ -734,14 +737,14 @@ end }, { data: IO.binread('test/fixtures/mail29.box'), - body_md5: 'f18cceddc06b60f5cdf2d39a556ab1f2', + body_md5: '0637f48a0979e479efec07120a2bb700', params: { from: 'Example Sales ', from_email: 'sales@example.com', from_display_name: 'Example Sales', subject: 'Example licensing information: No channel available', to: 'info@znuny.inc', - body: 'Dear Mr. Edenhofer,

                                  We want to keep you updated on TeamViewer licensing shortages on a regular basis.

                                  We would like to inform you that since the last message on 25-Nov-2014 there have been temporary session channel exceedances which make it impossible to establish more sessions. Since the last e-mail this has occurred in a total of 1 cases.

                                  Additional session channels can be added at any time. Please visit our TeamViewer Online Shop (https://www.teamviewer.com/en/licensing/update.aspx?channel=D842CS9BF85-P1009645N-348785E76E) for pricing information.

                                  Thank you - and again all the best with TeamViewer!

                                  Best regards,

                                  Your TeamViewer Team

                                  P.S.: You receive this e-mail because you are listed in our database as person who ordered a TeamViewer license. Please click here (http://www.teamviewer.com/en/company/unsubscribe.aspx?id=1009645&ident=E37682EAC65E8CA6FF36074907D8BC14) to unsubscribe from further e-mails.

                                  -----------------------------
                                  + body: 'Dear Mr. Edenhofer,

                                  We want to keep you updated on TeamViewer licensing shortages on a regular basis.

                                  We would like to inform you that since the last message on 25-Nov-2014 there have been temporary session channel exceedances which make it impossible to establish more sessions. Since the last e-mail this has occurred in a total of 1 cases.

                                  Additional session channels can be added at any time. Please visit our TeamViewer Online Shop for pricing information.

                                  Thank you - and again all the best with TeamViewer!

                                  Best regards,

                                  Your TeamViewer Team

                                  P.S.: You receive this e-mail because you are listed in our database as person who ordered a TeamViewer license. Please click here to unsubscribe from further e-mails.

                                  -----------------------------
                                  www.teamviewer.com

                                  TeamViewer GmbH * Jahnstr. 30 * 73037 Göppingen * Germany
                                  Tel. 07161 60692 50 * Fax 07161 60692 79

                                  Registration AG Ulm HRB 534075 * General Manager Holger Felgner' }, @@ -937,7 +940,7 @@ end }, { data: IO.binread('test/fixtures/mail43.box'), - body_md5: 'a3f7ff5e1876fdbf051c38649b4c9668', + body_md5: '1a4620c40f25a8e238769e56dcdcd373', params: { from: 'Paula ', from_email: 'databases.en@example.com', @@ -946,32 +949,32 @@ end to: 'info@example.ch', cc: nil, body: " - +
                                  -
                                  http://business-catalogs.example.com/ODtpbGs5MWIzbjUyYzExLTA4Yy06Mmg7N3AvL3R0bmFvY3B0LXlhbW9sc2Nhb3NnYy5lL3RpbXJlZi9lbS9ycnJuaWFpZXMsdGxnaWVpLGUzZHx4bnxlZWY=
                                  Geben Sie diese Information an den Direktor oder den für Marketing und Umsatzsteigerung verantwortlichen Mitarbeiter Ihrer Firma weiter! + +
                                  Geben Sie diese Information an den Direktor oder den für Marketing und Umsatzsteigerung verantwortlichen Mitarbeiter Ihrer Firma weiter!

                                  Hallo,

                                  • Sie suchen nach Möglichkeiten, den Umsatz Ihre Firma zu steigern?
                                  • Sie brauchen neue Geschäftskontakte?
                                  • Sie sind es leid, Kontaktdaten manuell zu erfassen?
                                  • Ihr Kontaktdatenanbieter ist zu teuer oder Sie sind mit seinen Dienstleistungen unzufrieden?
                                  • -
                                  • Sie möchten Ihre Kontaktinformationen gern effizienter auf dem neuesten Stand halten?


                                  Bei uns können Sie mit nur wenigen Clicks Geschäftskontakte verschiedener Länder erwerben.

                                  Dies ist eine schnelle und bequeme Methode, um Daten zu einem vernünftigen Preis zu erhalten.

                                  Alle Daten werden ständig aktualisiertm so dass Sie sich keine Sorgen machen müssen.

                                   

                                  http://business-catalogs.example.com/ODtpbGs5MWIzbjUyYzExLTA4Yy06Mmg7N3AvL3R0LnNzdXJobGZzZWVsdGEtLm10cmVzb2YvY2VtL2xpZ25pYWlnaV9hbC9zOG1lOXgyOTdzZW1hL2VlL2xwZWxheHB4Q18ubXhzfEhsODh8Y2M= http://business-catalogs.example.com/ODtpbGs5MWIzbjUyYzExLTA4Yy06Mmg7N3AvL3R0bmFvY3B0LXlhbW9sc2Nhb3NnYy5lL3RpbXJlZi9lbS9ycnJuaWFpZXMsdGxnaWVpLGUzZHx4bnxlZWY=

                                  XLS-Muster herunterladen (http://business-catalogs.example.com/ODtpbGs5MWIzbjUyYzExLTA4Yy06Mmg7N3AvL3R0LnNzdXJobGZzZWVsdGEtLm10cmVzb2YvY2VtL2xpZ25pYWlnaV9hbC9zOG1lOXgyOTdzZW1hL2VlL2xwZWxheHB4Q18ubXhzfEhsODh8Y2M=)

                                  Datenbank bestellen (http://business-catalogs.example.com/ODtpbGs5MWIzbjUyYzExLTA4Yy06Mmg7N3AvL3R0bmFvY3B0LXlhbW9sc2Nhb3NnYy5lL3RpbXJlZi9lbS9ycnJuaWFpZXMsdGxnaWVpLGUzZHx4bnxlZWY=)

                                  Die Anmeldung ist absolut kostenlos und unverbindlich. Sie können die Kataloge gemäß Ihren eigenen Kriterien filtern und ein kostenloses Datenmuster bestellen, sobald Sie sich angemeldet haben.

                                  Wir haben Datenbanken der folgenden Länder: -

                                  Anwendungsmöglichkeiten für Geschäftskontakte

                                  • +
                                  • Sie möchten Ihre Kontaktinformationen gern effizienter auf dem neuesten Stand halten?


                                  Bei uns können Sie mit nur wenigen Clicks Geschäftskontakte verschiedener Länder erwerben.

                                  Dies ist eine schnelle und bequeme Methode, um Daten zu einem vernünftigen Preis zu erhalten.

                                  Alle Daten werden ständig aktualisiertm so dass Sie sich keine Sorgen machen müssen.

                                   

                                  XLS-Muster herunterladen

                                  Datenbank bestellen

                                  Die Anmeldung ist absolut kostenlos und unverbindlich. Sie können die Kataloge gemäß Ihren eigenen Kriterien filtern und ein kostenloses Datenmuster bestellen, sobald Sie sich angemeldet haben.

                                  Wir haben Datenbanken der folgenden Länder: +

                                  Anwendungsmöglichkeiten für Geschäftskontakte

                                  • Newsletter senden - Senden von Werbung per E-Mail (besonders effizient).
                                  • Telemarketing - Telefonwerbung.
                                  • @@ -983,10 +986,10 @@ end Marktforschung - Telefonumfragen zur Erforschung Ihrer Produkte oder Dienstleistungen.

                                   

                                  Sie können Abschnitte wählen (filtern) Empfänger gemäß Tätigkeitsbereichen und Standort der Firmen, um die Effizienz Ihrer Werbemaßnahmen zu erhöhen.

                                   

                                  Für jeden Kauf von 2016-11-05 23:59:59 wir gewähren 30% Rabatt RABATTCODE: WZ2124DD -

                                  Bestellen Sie online bei:

                                  company-catalogs.com (http://business-catalogs.example.com/ODtpbGs5MWIzbjUyYzExLTA4Yy06Mmg7N3AvL3R0bmFvY3B0LXlhbW9sc2Nhb3NnYy5lL3RpbXJlZi9lbS9ycnJuaWFpZXMsdGxnaWVpLGUzZHx4bnxlZWY=)

                                  Für weitere Informationen:

                                  E-Mail: databases.en@example.com
                                  Telefon: +370-52-071554 (languages: EN, PL, RU, LT)


                                  Bestellen Sie online bei:

                                  company-catalogs.com

                                  Für weitere Informationen:

                                  E-Mail: databases.en@example.com
                                  Telefon: +370-52-071554 (languages: EN, PL, RU, LT)


                                  -
                                  Unsubscribe from newsletter: Click here (http://business-catalogs.example.com/c2JudXVlcmNic2I4MWk7MTgxOTMyNS1jMmMtNzA=)", +
                                  Unsubscribe from newsletter: Click here", }, }, { diff --git a/test/unit/email_process_test.rb b/test/unit/email_process_test.rb index a16cbd090..f1f69b467 100644 --- a/test/unit/email_process_test.rb +++ b/test/unit/email_process_test.rb @@ -422,7 +422,7 @@ Some Text",
                                  9õhH3ÿoIÚõ´GÿiH±6u-û◊NQ4ùäU¹awAq¹JLZμÒIicgT1ζ2Y7⊆t 63‘Mñ36EßÝ→DAå†I048CvJ9A↑3iTc4ÉIΥvXO50ñNÁFJSð­r 154F1HPOÀ£CRxZp tLîT9öXH1b3Es±W mNàBg3õEbPŒSúfτTóY4 sUÖPÒζΔRFkcIÕ1™CÓZ3EΛRq!Cass is good to ask what that
                                  86ËÏuÕC L I C K H E R E28M (http://piufup.medicatingsafemart.ru)Luke had been thinking about that.
                                  Shannon said nothing in fact they. Matt placed the sofa with amy smiled. Since the past him with more. Maybe he checked the phone. Neither did her name only. Ryan then went inside matt.
                                  Maybe we can have anything you sure.
                                  86ËÏuÕC L I C K H E R E28MLuke had been thinking about that.
                                  Shannon said nothing in fact they. Matt placed the sofa with amy smiled. Since the past him with more. Maybe he checked the phone. Neither did her name only. Ryan then went inside matt.
                                  Maybe we can have anything you sure.
                                  á•XMYÍÅEE£ÓN°kP'dÄÅS4⌉d √p¨HΣ>jE4y4ACüûLì“vT∧4tHXÆX: @@ -498,10 +498,10 @@ Some Text",
                                  -http://www.avast.com/ + -

                                  ?????? ?????????????????? ???????????????? ???? ?????????????? ?? ???????????????????????? ???? ?????????????????? avast! Antivirus (http://www.avast.com/) ???????????? ??????????????.

                                  +

                                  ?????? ?????????????????? ???????????????? ???? ?????????????? ?? ???????????????????????? ???? ?????????????????? avast! Antivirus ???????????? ??????????????.

                                  ", diff --git a/test/unit/html_sanitizer_test.rb b/test/unit/html_sanitizer_test.rb index 56d81360a..f441499a9 100644 --- a/test/unit/html_sanitizer_test.rb +++ b/test/unit/html_sanitizer_test.rb @@ -46,19 +46,19 @@ class HtmlSanitizerTest < ActiveSupport::TestCase assert_equal(HtmlSanitizer.strict(''), '') assert_equal(HtmlSanitizer.strict('
                                  '), '
                                  ') assert_equal(HtmlSanitizer.strict('
                                  '), '
                                  ') - assert_equal(HtmlSanitizer.strict('test'), 'test (/some/path)') - assert_equal(HtmlSanitizer.strict('test'), 'test (https://some/path)') - assert_equal(HtmlSanitizer.strict('test', true), 'test (https://some/path)') + assert_equal(HtmlSanitizer.strict('test'), 'test') + assert_equal(HtmlSanitizer.strict('test'), 'test') + assert_equal(HtmlSanitizer.strict('test', true), 'test') assert_equal(HtmlSanitizer.strict(''), '') assert_equal(HtmlSanitizer.strict(''), '') assert_equal(HtmlSanitizer.strict(' +ADw-SCRIPT+AD4-alert(\'XSS\');+ADw-/SCRIPT+AD4-'), ' +ADw-SCRIPT+AD4-alert(\'XSS\');+ADw-/SCRIPT+AD4-') assert_equal(HtmlSanitizer.strict(''), '') assert_equal(HtmlSanitizer.strict('XSS'), 'XSS (http://66.000146.0x7.147/)') +tt p://6 6.000146.0x7.147/">XSS'), 'XSS') assert_equal(HtmlSanitizer.strict('XSS', true), 'XSS (http://66.000146.0x7.147/)') - assert_equal(HtmlSanitizer.strict('XSS'), 'XSS (//www.google.com/)') - assert_equal(HtmlSanitizer.strict('XSS', true), 'XSS (//www.google.com/)') +tt p://6 6.000146.0x7.147/">XSS', true), 'XSS') + assert_equal(HtmlSanitizer.strict('XSS'), 'XSS') + assert_equal(HtmlSanitizer.strict('XSS', true), 'XSS') assert_equal(HtmlSanitizer.strict('
                                  '), 'X') assert_equal(HtmlSanitizer.strict('CLICKME'), 'CLICKME') assert_equal(HtmlSanitizer.strict('CLICKME'), 'CLICKME') @@ -73,8 +73,8 @@ tt p://6 6.000146.0x7.147/">XSS', true), 'XSS (XXX'), 'XXX') assert_equal(HtmlSanitizer.strict('XXX', true), 'XXX') assert_equal(HtmlSanitizer.strict(''), 'alert(1)') - assert_equal(HtmlSanitizer.strict(''), 'http://example.com') - assert_equal(HtmlSanitizer.strict('', true), 'http://example.com') + assert_equal(HtmlSanitizer.strict(''), '') + assert_equal(HtmlSanitizer.strict('', true), '') assert_equal(HtmlSanitizer.strict('