diff --git a/Gemfile b/Gemfile
index 86b6b52e9..b9bdabe0d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,6 +1,6 @@
source 'http://rubygems.org'
-gem 'rails', '4.1.2'
+gem 'rails', '4.1.4'
gem 'rails-observers'
gem 'activerecord-session_store'
diff --git a/app/assets/javascripts/app/lib/bootstrap/button.js b/app/assets/javascripts/app/lib/bootstrap/button.js
index c9fdde5e4..b3e944c59 100644
--- a/app/assets/javascripts/app/lib/bootstrap/button.js
+++ b/app/assets/javascripts/app/lib/bootstrap/button.js
@@ -1,33 +1,26 @@
/* ========================================================================
- * Bootstrap: button.js v3.0.3
+ * Bootstrap: button.js v3.2.0
* http://getbootstrap.com/javascript/#buttons
* ========================================================================
- * Copyright 2013 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
-+function ($) { "use strict";
++function ($) {
+ 'use strict';
// BUTTON PUBLIC CLASS DEFINITION
// ==============================
var Button = function (element, options) {
- this.$element = $(element)
- this.options = $.extend({}, Button.DEFAULTS, options)
+ this.$element = $(element)
+ this.options = $.extend({}, Button.DEFAULTS, options)
+ this.isLoading = false
}
+ Button.VERSION = '3.2.0'
+
Button.DEFAULTS = {
loadingText: 'loading...'
}
@@ -40,30 +33,31 @@
state = state + 'Text'
- if (!data.resetText) $el.data('resetText', $el[val]())
+ if (data.resetText == null) $el.data('resetText', $el[val]())
- $el[val](data[state] || this.options[state])
+ $el[val](data[state] == null ? this.options[state] : data[state])
// push to event loop to allow forms to submit
- setTimeout(function () {
- state == 'loadingText' ?
- $el.addClass(d).attr(d, d) :
- $el.removeClass(d).removeAttr(d);
- }, 0)
+ setTimeout($.proxy(function () {
+ if (state == 'loadingText') {
+ this.isLoading = true
+ $el.addClass(d).attr(d, d)
+ } else if (this.isLoading) {
+ this.isLoading = false
+ $el.removeClass(d).removeAttr(d)
+ }
+ }, this), 0)
}
Button.prototype.toggle = function () {
- var $parent = this.$element.closest('[data-toggle="buttons"]')
var changed = true
+ var $parent = this.$element.closest('[data-toggle="buttons"]')
if ($parent.length) {
var $input = this.$element.find('input')
- if ($input.prop('type') === 'radio') {
- // see if clicking on current one
- if ($input.prop('checked') && this.$element.hasClass('active'))
- changed = false
- else
- $parent.find('.active').removeClass('active')
+ if ($input.prop('type') == 'radio') {
+ if ($input.prop('checked') && this.$element.hasClass('active')) changed = false
+ else $parent.find('.active').removeClass('active')
}
if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change')
}
@@ -75,9 +69,7 @@
// BUTTON PLUGIN DEFINITION
// ========================
- var old = $.fn.button
-
- $.fn.button = function (option) {
+ function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.button')
@@ -90,6 +82,9 @@
})
}
+ var old = $.fn.button
+
+ $.fn.button = Plugin
$.fn.button.Constructor = Button
@@ -105,11 +100,15 @@
// BUTTON DATA-API
// ===============
- $(document).on('click.bs.button.data-api', '[data-toggle^=button]', function (e) {
- var $btn = $(e.target)
- if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn')
- $btn.button('toggle')
- e.preventDefault()
- })
+ $(document)
+ .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) {
+ var $btn = $(e.target)
+ if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn')
+ Plugin.call($btn, 'toggle')
+ e.preventDefault()
+ })
+ .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) {
+ $(e.target).closest('.btn').toggleClass('focus', e.type == 'focus')
+ })
}(jQuery);
diff --git a/app/assets/javascripts/app/lib/bootstrap/collapse.js b/app/assets/javascripts/app/lib/bootstrap/collapse.js
index 1a079938e..5a524241d 100644
--- a/app/assets/javascripts/app/lib/bootstrap/collapse.js
+++ b/app/assets/javascripts/app/lib/bootstrap/collapse.js
@@ -1,24 +1,14 @@
/* ========================================================================
- * Bootstrap: collapse.js v3.0.3
+ * Bootstrap: collapse.js v3.2.0
* http://getbootstrap.com/javascript/#collapse
* ========================================================================
- * Copyright 2013 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
-+function ($) { "use strict";
++function ($) {
+ 'use strict';
// COLLAPSE PUBLIC CLASS DEFINITION
// ================================
@@ -32,6 +22,8 @@
if (this.options.toggle) this.toggle()
}
+ Collapse.VERSION = '3.2.0'
+
Collapse.DEFAULTS = {
toggle: true
}
@@ -53,7 +45,7 @@
if (actives && actives.length) {
var hasData = actives.data('bs.collapse')
if (hasData && hasData.transitioning) return
- actives.collapse('hide')
+ Plugin.call(actives, 'hide')
hasData || actives.data('bs.collapse', null)
}
@@ -61,18 +53,17 @@
this.$element
.removeClass('collapse')
- .addClass('collapsing')
- [dimension](0)
+ .addClass('collapsing')[dimension](0)
this.transitioning = 1
var complete = function () {
this.$element
.removeClass('collapsing')
- .addClass('in')
- [dimension]('auto')
+ .addClass('collapse in')[dimension]('')
this.transitioning = 0
- this.$element.trigger('shown.bs.collapse')
+ this.$element
+ .trigger('shown.bs.collapse')
}
if (!$.support.transition) return complete.call(this)
@@ -80,9 +71,8 @@
var scrollSize = $.camelCase(['scroll', dimension].join('-'))
this.$element
- .one($.support.transition.end, $.proxy(complete, this))
- .emulateTransitionEnd(350)
- [dimension](this.$element[0][scrollSize])
+ .one('bsTransitionEnd', $.proxy(complete, this))
+ .emulateTransitionEnd(350)[dimension](this.$element[0][scrollSize])
}
Collapse.prototype.hide = function () {
@@ -94,14 +84,11 @@
var dimension = this.dimension()
- this.$element
- [dimension](this.$element[dimension]())
- [0].offsetHeight
+ this.$element[dimension](this.$element[dimension]())[0].offsetHeight
this.$element
.addClass('collapsing')
- .removeClass('collapse')
- .removeClass('in')
+ .removeClass('collapse in')
this.transitioning = 1
@@ -117,7 +104,7 @@
this.$element
[dimension](0)
- .one($.support.transition.end, $.proxy(complete, this))
+ .one('bsTransitionEnd', $.proxy(complete, this))
.emulateTransitionEnd(350)
}
@@ -129,19 +116,21 @@
// COLLAPSE PLUGIN DEFINITION
// ==========================
- var old = $.fn.collapse
-
- $.fn.collapse = function (option) {
+ function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.collapse')
var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option)
+ if (!data && options.toggle && option == 'show') option = !option
if (!data) $this.data('bs.collapse', (data = new Collapse(this, options)))
if (typeof option == 'string') data[option]()
})
}
+ var old = $.fn.collapse
+
+ $.fn.collapse = Plugin
$.fn.collapse.Constructor = Collapse
@@ -157,11 +146,12 @@
// COLLAPSE DATA-API
// =================
- $(document).on('click.bs.collapse.data-api', '[data-toggle=collapse]', function (e) {
- var $this = $(this), href
+ $(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) {
+ var href
+ var $this = $(this)
var target = $this.attr('data-target')
|| e.preventDefault()
- || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7
+ || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7
var $target = $(target)
var data = $target.data('bs.collapse')
var option = data ? 'toggle' : $this.data()
@@ -169,11 +159,11 @@
var $parent = parent && $(parent)
if (!data || !data.transitioning) {
- if ($parent) $parent.find('[data-toggle=collapse][data-parent="' + parent + '"]').not($this).addClass('collapsed')
- $this[$target.hasClass('in') ? 'addClass' : 'removeClass']('collapsed')
+ if ($parent) $parent.find('[data-toggle="collapse"][data-parent="' + parent + '"]').not($this).addClass('collapsed')
+ $this.toggleClass('collapsed', $target.hasClass('in'))
}
- $target.collapse(option)
+ Plugin.call($target, option)
})
}(jQuery);
diff --git a/app/assets/javascripts/app/lib/bootstrap/dropdown.js b/app/assets/javascripts/app/lib/bootstrap/dropdown.js
index 13352ef7c..88f118c2d 100644
--- a/app/assets/javascripts/app/lib/bootstrap/dropdown.js
+++ b/app/assets/javascripts/app/lib/bootstrap/dropdown.js
@@ -1,34 +1,26 @@
/* ========================================================================
- * Bootstrap: dropdown.js v3.0.3
+ * Bootstrap: dropdown.js v3.2.0
* http://getbootstrap.com/javascript/#dropdowns
* ========================================================================
- * Copyright 2013 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
-+function ($) { "use strict";
++function ($) {
+ 'use strict';
// DROPDOWN CLASS DEFINITION
// =========================
var backdrop = '.dropdown-backdrop'
- var toggle = '[data-toggle=dropdown]'
+ var toggle = '[data-toggle="dropdown"]'
var Dropdown = function (element) {
$(element).on('click.bs.dropdown', this.toggle)
}
+ Dropdown.VERSION = '3.2.0'
+
Dropdown.prototype.toggle = function (e) {
var $this = $(this)
@@ -45,15 +37,16 @@
$('
').insertAfter($(this)).on('click', clearMenus)
}
- $parent.trigger(e = $.Event('show.bs.dropdown'))
+ var relatedTarget = { relatedTarget: this }
+ $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget))
if (e.isDefaultPrevented()) return
+ $this.trigger('focus')
+
$parent
.toggleClass('open')
- .trigger('shown.bs.dropdown')
-
- $this.focus()
+ .trigger('shown.bs.dropdown', relatedTarget)
}
return false
@@ -73,11 +66,12 @@
var isActive = $parent.hasClass('open')
if (!isActive || (isActive && e.keyCode == 27)) {
- if (e.which == 27) $parent.find(toggle).focus()
- return $this.click()
+ if (e.which == 27) $parent.find(toggle).trigger('focus')
+ return $this.trigger('click')
}
- var $items = $('[role=menu] li:not(.divider):visible a', $parent)
+ var desc = ' li:not(.divider):visible a'
+ var $items = $parent.find('[role="menu"]' + desc + ', [role="listbox"]' + desc)
if (!$items.length) return
@@ -85,19 +79,21 @@
if (e.keyCode == 38 && index > 0) index-- // up
if (e.keyCode == 40 && index < $items.length - 1) index++ // down
- if (!~index) index=0
+ if (!~index) index = 0
- $items.eq(index).focus()
+ $items.eq(index).trigger('focus')
}
- function clearMenus() {
+ function clearMenus(e) {
+ if (e && e.which === 3) return
$(backdrop).remove()
- $(toggle).each(function (e) {
+ $(toggle).each(function () {
var $parent = getParent($(this))
+ var relatedTarget = { relatedTarget: this }
if (!$parent.hasClass('open')) return
- $parent.trigger(e = $.Event('hide.bs.dropdown'))
+ $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget))
if (e.isDefaultPrevented()) return
- $parent.removeClass('open').trigger('hidden.bs.dropdown')
+ $parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget)
})
}
@@ -106,7 +102,7 @@
if (!selector) {
selector = $this.attr('href')
- selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
+ selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
}
var $parent = selector && $(selector)
@@ -118,9 +114,7 @@
// DROPDOWN PLUGIN DEFINITION
// ==========================
- var old = $.fn.dropdown
-
- $.fn.dropdown = function (option) {
+ function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.dropdown')
@@ -130,6 +124,9 @@
})
}
+ var old = $.fn.dropdown
+
+ $.fn.dropdown = Plugin
$.fn.dropdown.Constructor = Dropdown
@@ -148,7 +145,7 @@
$(document)
.on('click.bs.dropdown.data-api', clearMenus)
.on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
- .on('click.bs.dropdown.data-api' , toggle, Dropdown.prototype.toggle)
- .on('keydown.bs.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown)
+ .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle)
+ .on('keydown.bs.dropdown.data-api', toggle + ', [role="menu"], [role="listbox"]', Dropdown.prototype.keydown)
}(jQuery);
diff --git a/app/assets/javascripts/app/lib/bootstrap/modal.js b/app/assets/javascripts/app/lib/bootstrap/modal.js
index 3ead5ee88..fdefdd2c5 100644
--- a/app/assets/javascripts/app/lib/bootstrap/modal.js
+++ b/app/assets/javascripts/app/lib/bootstrap/modal.js
@@ -1,45 +1,45 @@
/* ========================================================================
- * Bootstrap: modal.js v3.0.3
+ * Bootstrap: modal.js v3.2.0
* http://getbootstrap.com/javascript/#modals
* ========================================================================
- * Copyright 2013 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
-+function ($) { "use strict";
++function ($) {
+ 'use strict';
// MODAL CLASS DEFINITION
// ======================
var Modal = function (element, options) {
- this.options = options
- this.$element = $(element)
- this.$backdrop =
- this.isShown = null
+ this.options = options
+ this.$body = $(document.body)
+ this.$element = $(element)
+ this.$backdrop =
+ this.isShown = null
+ this.scrollbarWidth = 0
- if (this.options.remote) this.$element.load(this.options.remote)
+ if (this.options.remote) {
+ this.$element
+ .find('.modal-content')
+ .load(this.options.remote, $.proxy(function () {
+ this.$element.trigger('loaded.bs.modal')
+ }, this))
+ }
}
+ Modal.VERSION = '3.2.0'
+
Modal.DEFAULTS = {
- backdrop: true
- , keyboard: true
- , show: true
+ backdrop: true,
+ keyboard: true,
+ show: true
}
Modal.prototype.toggle = function (_relatedTarget) {
- return this[!this.isShown ? 'show' : 'hide'](_relatedTarget)
+ return this.isShown ? this.hide() : this.show(_relatedTarget)
}
Modal.prototype.show = function (_relatedTarget) {
@@ -52,18 +52,24 @@
this.isShown = true
+ this.checkScrollbar()
+ this.$body.addClass('modal-open')
+
+ this.setScrollbar()
this.escape()
- this.$element.on('click.dismiss.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this))
+ this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this))
this.backdrop(function () {
var transition = $.support.transition && that.$element.hasClass('fade')
if (!that.$element.parent().length) {
- that.$element.appendTo(document.body) // don't move modals dom position
+ that.$element.appendTo(that.$body) // don't move modals dom position
}
- that.$element.show()
+ that.$element
+ .show()
+ .scrollTop(0)
if (transition) {
that.$element[0].offsetWidth // force reflow
@@ -79,11 +85,11 @@
transition ?
that.$element.find('.modal-dialog') // wait for modal to slide in
- .one($.support.transition.end, function () {
- that.$element.focus().trigger(e)
+ .one('bsTransitionEnd', function () {
+ that.$element.trigger('focus').trigger(e)
})
.emulateTransitionEnd(300) :
- that.$element.focus().trigger(e)
+ that.$element.trigger('focus').trigger(e)
})
}
@@ -98,6 +104,9 @@
this.isShown = false
+ this.$body.removeClass('modal-open')
+
+ this.resetScrollbar()
this.escape()
$(document).off('focusin.bs.modal')
@@ -105,11 +114,11 @@
this.$element
.removeClass('in')
.attr('aria-hidden', true)
- .off('click.dismiss.modal')
+ .off('click.dismiss.bs.modal')
$.support.transition && this.$element.hasClass('fade') ?
this.$element
- .one($.support.transition.end, $.proxy(this.hideModal, this))
+ .one('bsTransitionEnd', $.proxy(this.hideModal, this))
.emulateTransitionEnd(300) :
this.hideModal()
}
@@ -119,18 +128,18 @@
.off('focusin.bs.modal') // guard against infinite focus loop
.on('focusin.bs.modal', $.proxy(function (e) {
if (this.$element[0] !== e.target && !this.$element.has(e.target).length) {
- this.$element.focus()
+ this.$element.trigger('focus')
}
}, this))
}
Modal.prototype.escape = function () {
if (this.isShown && this.options.keyboard) {
- this.$element.on('keyup.dismiss.bs.modal', $.proxy(function (e) {
+ this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) {
e.which == 27 && this.hide()
}, this))
} else if (!this.isShown) {
- this.$element.off('keyup.dismiss.bs.modal')
+ this.$element.off('keydown.dismiss.bs.modal')
}
}
@@ -138,7 +147,6 @@
var that = this
this.$element.hide()
this.backdrop(function () {
- that.removeBackdrop()
that.$element.trigger('hidden.bs.modal')
})
}
@@ -149,16 +157,16 @@
}
Modal.prototype.backdrop = function (callback) {
- var that = this
+ var that = this
var animate = this.$element.hasClass('fade') ? 'fade' : ''
if (this.isShown && this.options.backdrop) {
var doAnimate = $.support.transition && animate
this.$backdrop = $('')
- .appendTo(document.body)
+ .appendTo(this.$body)
- this.$element.on('click.dismiss.modal', $.proxy(function (e) {
+ this.$element.on('mousedown.dismiss.bs.modal', $.proxy(function (e) {
if (e.target !== e.currentTarget) return
this.options.backdrop == 'static'
? this.$element[0].focus.call(this.$element[0])
@@ -173,31 +181,56 @@
doAnimate ?
this.$backdrop
- .one($.support.transition.end, callback)
+ .one('bsTransitionEnd', callback)
.emulateTransitionEnd(150) :
callback()
} else if (!this.isShown && this.$backdrop) {
this.$backdrop.removeClass('in')
- $.support.transition && this.$element.hasClass('fade')?
+ var callbackRemove = function () {
+ that.removeBackdrop()
+ callback && callback()
+ }
+ $.support.transition && this.$element.hasClass('fade') ?
this.$backdrop
- .one($.support.transition.end, callback)
+ .one('bsTransitionEnd', callbackRemove)
.emulateTransitionEnd(150) :
- callback()
+ callbackRemove()
} else if (callback) {
callback()
}
}
+ Modal.prototype.checkScrollbar = function () {
+ if (document.body.clientWidth >= window.innerWidth) return
+ this.scrollbarWidth = this.scrollbarWidth || this.measureScrollbar()
+ }
+
+ Modal.prototype.setScrollbar = function () {
+ var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10)
+ if (this.scrollbarWidth) this.$body.css('padding-right', bodyPad + this.scrollbarWidth)
+ }
+
+ Modal.prototype.resetScrollbar = function () {
+ this.$body.css('padding-right', '')
+ }
+
+ Modal.prototype.measureScrollbar = function () { // thx walsh
+ var scrollDiv = document.createElement('div')
+ scrollDiv.className = 'modal-scrollbar-measure'
+ this.$body.append(scrollDiv)
+ var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth
+ this.$body[0].removeChild(scrollDiv)
+ return scrollbarWidth
+ }
+
// MODAL PLUGIN DEFINITION
// =======================
- var old = $.fn.modal
-
- $.fn.modal = function (option, _relatedTarget) {
+ function Plugin(option, _relatedTarget) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.modal')
@@ -209,6 +242,9 @@
})
}
+ var old = $.fn.modal
+
+ $.fn.modal = Plugin
$.fn.modal.Constructor = Modal
@@ -227,20 +263,18 @@
$(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) {
var $this = $(this)
var href = $this.attr('href')
- var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7
- var option = $target.data('modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data())
+ var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7
+ var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data())
- e.preventDefault()
+ if ($this.is('a')) e.preventDefault()
- $target
- .modal(option, this)
- .one('hide', function () {
- $this.is(':visible') && $this.focus()
+ $target.one('show.bs.modal', function (showEvent) {
+ if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown
+ $target.one('hidden.bs.modal', function () {
+ $this.is(':visible') && $this.trigger('focus')
})
+ })
+ Plugin.call($target, option, this)
})
- $(document)
- .on('show.bs.modal', '.modal', function () { $(document.body).addClass('modal-open') })
- .on('hidden.bs.modal', '.modal', function () { $(document.body).removeClass('modal-open') })
-
}(jQuery);
diff --git a/app/assets/javascripts/app/lib/bootstrap/popover.js b/app/assets/javascripts/app/lib/bootstrap/popover.js
index 996962aa2..825e1b390 100644
--- a/app/assets/javascripts/app/lib/bootstrap/popover.js
+++ b/app/assets/javascripts/app/lib/bootstrap/popover.js
@@ -1,24 +1,14 @@
/* ========================================================================
- * Bootstrap: popover.js v3.0.3
+ * Bootstrap: popover.js v3.2.0
* http://getbootstrap.com/javascript/#popovers
* ========================================================================
- * Copyright 2013 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
-+function ($) { "use strict";
++function ($) {
+ 'use strict';
// POPOVER PUBLIC CLASS DEFINITION
// ===============================
@@ -29,11 +19,13 @@
if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js')
- Popover.DEFAULTS = $.extend({} , $.fn.tooltip.Constructor.DEFAULTS, {
- placement: 'right'
- , trigger: 'click'
- , content: ''
- , template: ''
+ Popover.VERSION = '3.2.0'
+
+ Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, {
+ placement: 'right',
+ trigger: 'click',
+ content: '',
+ template: ''
})
@@ -54,7 +46,9 @@
var content = this.getContent()
$tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)
- $tip.find('.popover-content')[this.options.html ? 'html' : 'text'](content)
+ $tip.find('.popover-content').empty()[ // we use append for html objects to maintain js events
+ this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text'
+ ](content)
$tip.removeClass('fade top bottom left right in')
@@ -78,7 +72,7 @@
}
Popover.prototype.arrow = function () {
- return this.$arrow = this.$arrow || this.tip().find('.arrow')
+ return (this.$arrow = this.$arrow || this.tip().find('.arrow'))
}
Popover.prototype.tip = function () {
@@ -90,19 +84,21 @@
// POPOVER PLUGIN DEFINITION
// =========================
- var old = $.fn.popover
-
- $.fn.popover = function (option) {
+ function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.popover')
var options = typeof option == 'object' && option
+ if (!data && option == 'destroy') return
if (!data) $this.data('bs.popover', (data = new Popover(this, options)))
if (typeof option == 'string') data[option]()
})
}
+ var old = $.fn.popover
+
+ $.fn.popover = Plugin
$.fn.popover.Constructor = Popover
diff --git a/app/assets/javascripts/app/lib/bootstrap/tab.js b/app/assets/javascripts/app/lib/bootstrap/tab.js
index 6b0f5f672..573b369a7 100644
--- a/app/assets/javascripts/app/lib/bootstrap/tab.js
+++ b/app/assets/javascripts/app/lib/bootstrap/tab.js
@@ -1,24 +1,14 @@
/* ========================================================================
- * Bootstrap: tab.js v3.0.3
+ * Bootstrap: tab.js v3.2.0
* http://getbootstrap.com/javascript/#tabs
* ========================================================================
- * Copyright 2013 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
-+function ($) { "use strict";
++function ($) {
+ 'use strict';
// TAB CLASS DEFINITION
// ====================
@@ -27,6 +17,8 @@
this.element = $(element)
}
+ Tab.VERSION = '3.2.0'
+
Tab.prototype.show = function () {
var $this = this.element
var $ul = $this.closest('ul:not(.dropdown-menu)')
@@ -34,7 +26,7 @@
if (!selector) {
selector = $this.attr('href')
- selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
+ selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
}
if ($this.parent('li').hasClass('active')) return
@@ -50,11 +42,11 @@
var $target = $(selector)
- this.activate($this.parent('li'), $ul)
+ this.activate($this.closest('li'), $ul)
this.activate($target, $target.parent(), function () {
$this.trigger({
- type: 'shown.bs.tab'
- , relatedTarget: previous
+ type: 'shown.bs.tab',
+ relatedTarget: previous
})
})
}
@@ -63,7 +55,7 @@
var $active = container.find('> .active')
var transition = callback
&& $.support.transition
- && $active.hasClass('fade')
+ && (($active.length && $active.hasClass('fade')) || !!container.find('> .fade').length)
function next() {
$active
@@ -87,9 +79,9 @@
callback && callback()
}
- transition ?
+ $active.length && transition ?
$active
- .one($.support.transition.end, next)
+ .one('bsTransitionEnd', next)
.emulateTransitionEnd(150) :
next()
@@ -100,9 +92,7 @@
// TAB PLUGIN DEFINITION
// =====================
- var old = $.fn.tab
-
- $.fn.tab = function ( option ) {
+ function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.tab')
@@ -112,6 +102,9 @@
})
}
+ var old = $.fn.tab
+
+ $.fn.tab = Plugin
$.fn.tab.Constructor = Tab
@@ -129,7 +122,7 @@
$(document).on('click.bs.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) {
e.preventDefault()
- $(this).tab('show')
+ Plugin.call($(this), 'show')
})
}(jQuery);
diff --git a/app/assets/javascripts/app/lib/bootstrap/tooltip.js b/app/assets/javascripts/app/lib/bootstrap/tooltip.js
index 4c848f0e2..0c3a79d0b 100644
--- a/app/assets/javascripts/app/lib/bootstrap/tooltip.js
+++ b/app/assets/javascripts/app/lib/bootstrap/tooltip.js
@@ -1,25 +1,15 @@
/* ========================================================================
- * Bootstrap: tooltip.js v3.0.3
+ * Bootstrap: tooltip.js v3.2.0
* http://getbootstrap.com/javascript/#tooltip
* Inspired by the original jQuery.tipsy by Jason Frame
* ========================================================================
- * Copyright 2013 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
-+function ($) { "use strict";
++function ($) {
+ 'use strict';
// TOOLTIP PUBLIC CLASS DEFINITION
// ===============================
@@ -35,23 +25,30 @@
this.init('tooltip', element, options)
}
+ Tooltip.VERSION = '3.2.0'
+
Tooltip.DEFAULTS = {
- animation: true
- , placement: 'top'
- , selector: false
- , template: ''
- , trigger: 'hover focus'
- , title: ''
- , delay: 0
- , html: false
- , container: false
+ animation: true,
+ placement: 'top',
+ selector: false,
+ template: '',
+ trigger: 'hover focus',
+ title: '',
+ delay: 0,
+ html: false,
+ container: false,
+ viewport: {
+ selector: 'body',
+ padding: 0
+ }
}
Tooltip.prototype.init = function (type, element, options) {
- this.enabled = true
- this.type = type
- this.$element = $(element)
- this.options = this.getOptions(options)
+ this.enabled = true
+ this.type = type
+ this.$element = $(element)
+ this.options = this.getOptions(options)
+ this.$viewport = this.options.viewport && $(this.options.viewport.selector || this.options.viewport)
var triggers = this.options.trigger.split(' ')
@@ -61,8 +58,8 @@
if (trigger == 'click') {
this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
} else if (trigger != 'manual') {
- var eventIn = trigger == 'hover' ? 'mouseenter' : 'focus'
- var eventOut = trigger == 'hover' ? 'mouseleave' : 'blur'
+ var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin'
+ var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout'
this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
@@ -83,8 +80,8 @@
if (options.delay && typeof options.delay == 'number') {
options.delay = {
- show: options.delay
- , hide: options.delay
+ show: options.delay,
+ hide: options.delay
}
}
@@ -104,7 +101,12 @@
Tooltip.prototype.enter = function (obj) {
var self = obj instanceof this.constructor ?
- obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
+ obj : $(obj.currentTarget).data('bs.' + this.type)
+
+ if (!self) {
+ self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
+ $(obj.currentTarget).data('bs.' + this.type, self)
+ }
clearTimeout(self.timeout)
@@ -119,7 +121,12 @@
Tooltip.prototype.leave = function (obj) {
var self = obj instanceof this.constructor ?
- obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
+ obj : $(obj.currentTarget).data('bs.' + this.type)
+
+ if (!self) {
+ self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
+ $(obj.currentTarget).data('bs.' + this.type, self)
+ }
clearTimeout(self.timeout)
@@ -133,16 +140,22 @@
}
Tooltip.prototype.show = function () {
- var e = $.Event('show.bs.'+ this.type)
+ var e = $.Event('show.bs.' + this.type)
if (this.hasContent() && this.enabled) {
this.$element.trigger(e)
- if (e.isDefaultPrevented()) return
+ var inDom = $.contains(document.documentElement, this.$element[0])
+ if (e.isDefaultPrevented() || !inDom) return
+ var that = this
var $tip = this.tip()
+ var tipId = this.getUID(this.type)
+
this.setContent()
+ $tip.attr('id', tipId)
+ this.$element.attr('aria-describedby', tipId)
if (this.options.animation) $tip.addClass('fade')
@@ -158,6 +171,7 @@
.detach()
.css({ top: 0, left: 0, display: 'block' })
.addClass(placement)
+ .data('bs.' + this.type, this)
this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
@@ -166,18 +180,14 @@
var actualHeight = $tip[0].offsetHeight
if (autoPlace) {
- var $parent = this.$element.parent()
-
var orgPlacement = placement
- var docScroll = document.documentElement.scrollTop || document.body.scrollTop
- var parentWidth = this.options.container == 'body' ? window.innerWidth : $parent.outerWidth()
- var parentHeight = this.options.container == 'body' ? window.innerHeight : $parent.outerHeight()
- var parentLeft = this.options.container == 'body' ? 0 : $parent.offset().left
+ var $parent = this.$element.parent()
+ var parentDim = this.getPosition($parent)
- placement = placement == 'bottom' && pos.top + pos.height + actualHeight - docScroll > parentHeight ? 'top' :
- placement == 'top' && pos.top - docScroll - actualHeight < 0 ? 'bottom' :
- placement == 'right' && pos.right + actualWidth > parentWidth ? 'left' :
- placement == 'left' && pos.left - actualWidth < parentLeft ? 'right' :
+ placement = placement == 'bottom' && pos.top + pos.height + actualHeight - parentDim.scroll > parentDim.height ? 'top' :
+ placement == 'top' && pos.top - parentDim.scroll - actualHeight < 0 ? 'bottom' :
+ placement == 'right' && pos.right + actualWidth > parentDim.width ? 'left' :
+ placement == 'left' && pos.left - actualWidth < parentDim.left ? 'right' :
placement
$tip
@@ -188,12 +198,21 @@
var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)
this.applyPlacement(calculatedOffset, placement)
- this.$element.trigger('shown.bs.' + this.type)
+
+ var complete = function () {
+ that.$element.trigger('shown.bs.' + that.type)
+ that.hoverState = null
+ }
+
+ $.support.transition && this.$tip.hasClass('fade') ?
+ $tip
+ .one('bsTransitionEnd', complete)
+ .emulateTransitionEnd(150) :
+ complete()
}
}
- Tooltip.prototype.applyPlacement = function(offset, placement) {
- var replace
+ Tooltip.prototype.applyPlacement = function (offset, placement) {
var $tip = this.tip()
var width = $tip[0].offsetWidth
var height = $tip[0].offsetHeight
@@ -209,42 +228,42 @@
offset.top = offset.top + marginTop
offset.left = offset.left + marginLeft
- $tip
- .offset(offset)
- .addClass('in')
+ // $.fn.offset doesn't round pixel values
+ // so we use setOffset directly with our own function B-0
+ $.offset.setOffset($tip[0], $.extend({
+ using: function (props) {
+ $tip.css({
+ top: Math.round(props.top),
+ left: Math.round(props.left)
+ })
+ }
+ }, offset), 0)
+
+ $tip.addClass('in')
// check to see if placing tip in new offset caused the tip to resize itself
var actualWidth = $tip[0].offsetWidth
var actualHeight = $tip[0].offsetHeight
if (placement == 'top' && actualHeight != height) {
- replace = true
offset.top = offset.top + height - actualHeight
}
- if (/bottom|top/.test(placement)) {
- var delta = 0
+ var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight)
- if (offset.left < 0) {
- delta = offset.left * -2
- offset.left = 0
+ if (delta.left) offset.left += delta.left
+ else offset.top += delta.top
- $tip.offset(offset)
+ var arrowDelta = delta.left ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight
+ var arrowPosition = delta.left ? 'left' : 'top'
+ var arrowOffsetPosition = delta.left ? 'offsetWidth' : 'offsetHeight'
- actualWidth = $tip[0].offsetWidth
- actualHeight = $tip[0].offsetHeight
- }
-
- this.replaceArrow(delta - width + actualWidth, actualWidth, 'left')
- } else {
- this.replaceArrow(actualHeight - height, actualHeight, 'top')
- }
-
- if (replace) $tip.offset(offset)
+ $tip.offset(offset)
+ this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], arrowPosition)
}
- Tooltip.prototype.replaceArrow = function(delta, dimension, position) {
- this.arrow().css(position, delta ? (50 * (1 - delta / dimension) + "%") : '')
+ Tooltip.prototype.replaceArrow = function (delta, dimension, position) {
+ this.arrow().css(position, delta ? (50 * (1 - delta / dimension) + '%') : '')
}
Tooltip.prototype.setContent = function () {
@@ -260,8 +279,11 @@
var $tip = this.tip()
var e = $.Event('hide.bs.' + this.type)
+ this.$element.removeAttr('aria-describedby')
+
function complete() {
if (that.hoverState != 'in') $tip.detach()
+ that.$element.trigger('hidden.bs.' + that.type)
}
this.$element.trigger(e)
@@ -272,18 +294,18 @@
$.support.transition && this.$tip.hasClass('fade') ?
$tip
- .one($.support.transition.end, complete)
+ .one('bsTransitionEnd', complete)
.emulateTransitionEnd(150) :
complete()
- this.$element.trigger('hidden.bs.' + this.type)
+ this.hoverState = null
return this
}
Tooltip.prototype.fixTitle = function () {
var $e = this.$element
- if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
+ if ($e.attr('title') || typeof ($e.attr('data-original-title')) != 'string') {
$e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
}
}
@@ -292,12 +314,22 @@
return this.getTitle()
}
- Tooltip.prototype.getPosition = function () {
- var el = this.$element[0]
- return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : {
- width: el.offsetWidth
- , height: el.offsetHeight
- }, this.$element.offset())
+ Tooltip.prototype.getPosition = function ($element) {
+ $element = $element || this.$element
+
+ var el = $element[0]
+ var isBody = el.tagName == 'BODY'
+ var isSvg = window.SVGElement && el instanceof window.SVGElement
+
+ var elRect = el.getBoundingClientRect ? el.getBoundingClientRect() : null
+ var elOffset = isBody ? { top: 0, left: 0 } : $element.offset()
+ var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() }
+ var outerDims = isSvg ? {} : {
+ width: isBody ? $(window).width() : $element.outerWidth(),
+ height: isBody ? $(window).height() : $element.outerHeight()
+ }
+
+ return $.extend({}, elRect, scroll, outerDims, elOffset)
}
Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {
@@ -305,6 +337,35 @@
placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } :
placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
/* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width }
+
+ }
+
+ Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) {
+ var delta = { top: 0, left: 0 }
+ if (!this.$viewport) return delta
+
+ var viewportPadding = this.options.viewport && this.options.viewport.padding || 0
+ var viewportDimensions = this.getPosition(this.$viewport)
+
+ if (/right|left/.test(placement)) {
+ var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll
+ var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight
+ if (topEdgeOffset < viewportDimensions.top) { // top overflow
+ delta.top = viewportDimensions.top - topEdgeOffset
+ } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow
+ delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset
+ }
+ } else {
+ var leftEdgeOffset = pos.left - viewportPadding
+ var rightEdgeOffset = pos.left + viewportPadding + actualWidth
+ if (leftEdgeOffset < viewportDimensions.left) { // left overflow
+ delta.left = viewportDimensions.left - leftEdgeOffset
+ } else if (rightEdgeOffset > viewportDimensions.width) { // right overflow
+ delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset
+ }
+ }
+
+ return delta
}
Tooltip.prototype.getTitle = function () {
@@ -318,12 +379,18 @@
return title
}
+ Tooltip.prototype.getUID = function (prefix) {
+ do prefix += ~~(Math.random() * 1000000)
+ while (document.getElementById(prefix))
+ return prefix
+ }
+
Tooltip.prototype.tip = function () {
- return this.$tip = this.$tip || $(this.options.template)
+ return (this.$tip = this.$tip || $(this.options.template))
}
Tooltip.prototype.arrow = function () {
- return this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')
+ return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow'))
}
Tooltip.prototype.validate = function () {
@@ -347,11 +414,20 @@
}
Tooltip.prototype.toggle = function (e) {
- var self = e ? $(e.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) : this
+ var self = this
+ if (e) {
+ self = $(e.currentTarget).data('bs.' + this.type)
+ if (!self) {
+ self = new this.constructor(e.currentTarget, this.getDelegateOptions())
+ $(e.currentTarget).data('bs.' + this.type, self)
+ }
+ }
+
self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
}
Tooltip.prototype.destroy = function () {
+ clearTimeout(this.timeout)
this.hide().$element.off('.' + this.type).removeData('bs.' + this.type)
}
@@ -359,19 +435,21 @@
// TOOLTIP PLUGIN DEFINITION
// =========================
- var old = $.fn.tooltip
-
- $.fn.tooltip = function (option) {
+ function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.tooltip')
var options = typeof option == 'object' && option
+ if (!data && option == 'destroy') return
if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
if (typeof option == 'string') data[option]()
})
}
+ var old = $.fn.tooltip
+
+ $.fn.tooltip = Plugin
$.fn.tooltip.Constructor = Tooltip
diff --git a/app/assets/javascripts/app/lib/bootstrap/transition.js b/app/assets/javascripts/app/lib/bootstrap/transition.js
index 773dbe693..83f85bf45 100644
--- a/app/assets/javascripts/app/lib/bootstrap/transition.js
+++ b/app/assets/javascripts/app/lib/bootstrap/transition.js
@@ -1,24 +1,14 @@
/* ========================================================================
- * Bootstrap: transition.js v3.0.3
+ * Bootstrap: transition.js v3.2.0
* http://getbootstrap.com/javascript/#transitions
* ========================================================================
- * Copyright 2013 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
-+function ($) { "use strict";
++function ($) {
+ 'use strict';
// CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/)
// ============================================================
@@ -27,10 +17,10 @@
var el = document.createElement('bootstrap')
var transEndEventNames = {
- 'WebkitTransition' : 'webkitTransitionEnd'
- , 'MozTransition' : 'transitionend'
- , 'OTransition' : 'oTransitionEnd otransitionend'
- , 'transition' : 'transitionend'
+ WebkitTransition : 'webkitTransitionEnd',
+ MozTransition : 'transitionend',
+ OTransition : 'oTransitionEnd otransitionend',
+ transition : 'transitionend'
}
for (var name in transEndEventNames) {
@@ -38,12 +28,15 @@
return { end: transEndEventNames[name] }
}
}
+
+ return false // explicit for ie8 ( ._.)
}
// http://blog.alexmaccaw.com/css-transitions
$.fn.emulateTransitionEnd = function (duration) {
- var called = false, $el = this
- $(this).one($.support.transition.end, function () { called = true })
+ var called = false
+ var $el = this
+ $(this).one('bsTransitionEnd', function () { called = true })
var callback = function () { if (!called) $($el).trigger($.support.transition.end) }
setTimeout(callback, duration)
return this
@@ -51,6 +44,16 @@
$(function () {
$.support.transition = transitionEnd()
+
+ if (!$.support.transition) return
+
+ $.event.special.bsTransitionEnd = {
+ bindType: $.support.transition.end,
+ delegateType: $.support.transition.end,
+ handle: function (e) {
+ if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments)
+ }
+ }
})
}(jQuery);
diff --git a/app/assets/javascripts/app/views/navigation.jst.eco b/app/assets/javascripts/app/views/navigation.jst.eco
index fe9952c15..c49a708d5 100644
--- a/app/assets/javascripts/app/views/navigation.jst.eco
+++ b/app/assets/javascripts/app/views/navigation.jst.eco
@@ -88,4 +88,4 @@
<%- @T( 'Sign in' ) %>
-->
-<% end %>
\ No newline at end of file
+<% end %>
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
index ac0ce43ba..2d19cd5e7 100644
--- a/app/assets/stylesheets/application.css
+++ b/app/assets/stylesheets/application.css
@@ -10,7 +10,6 @@
*= require ./bootstrap-tokenfield.css
*= require ./noty_theme_twitter.css
*= require ./sew.css
- *= require ./layout.css
*= require ./zzz.css
*= require ./fonts/fira-sans.css
*
diff --git a/app/assets/stylesheets/zzz.css b/app/assets/stylesheets/zzz.css
new file mode 100644
index 000000000..d21dfb661
--- /dev/null
+++ b/app/assets/stylesheets/zzz.css
@@ -0,0 +1,1045 @@
+body {
+ padding-top:78px;
+/*
+ font-size: 13px;
+*/
+ font-weight: 200;
+ font-size: 13px;
+}
+
+.glyphicon {
+ font-size: 13px;
+}
+
+.btn {
+ font-size: 12px;
+ font-weight: 400;
+}
+
+label, input, button, select, textarea {
+ font-size: 13px;
+ font-weight: 200;
+}
+
+select,
+textarea,
+input[type="text"],
+input[type="password"],
+input[type="datetime"],
+input[type="datetime-local"],
+input[type="date"],
+input[type="month"],
+input[type="time"],
+input[type="week"],
+input[type="number"],
+input[type="email"],
+input[type="url"],
+input[type="search"],
+input[type="tel"],
+input[type="color"],
+.uneditable-input {
+/*
+ margin-bottom: 1px;
+*/
+}
+
+
+.spinner {
+ float: left;
+ background:url("/assets/images/spinner.gif") no-repeat;
+ background-position: center center;
+ padding: 44px 10px 0 10px;
+ width: 10px;
+ height: 14px;
+ display: none;
+}
+
+table {
+ table-layout: fixed;
+}
+table th, table td {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.table {
+ display: table;
+}
+.table .table-row {
+ display: table-row;
+}
+.table .table-cell{
+ vertical-align: top;
+ display: table-cell;
+ border-bottom: 1px solid #eeeeee;
+}
+
+.popover {
+ min-width: 240px;
+ z-index: 1110;
+}
+.hero-two {
+ width: 100%;
+}
+.hero-two .hero-left {
+ width: 50%;
+ float: left;
+}
+.hero-two .hero-right {
+ width: 50%;
+ float: right;
+}
+
+.content-one {
+ top: 78px;
+ margin-left: 20px;
+ margin-right: 20px;
+/*
+ bottom: 0;
+ left: 0;
+ right: 0;
+ position: fixed;
+*/
+ z-index: -5000;
+}
+/*
+@media (min-width: 1024px) {
+ .content-one {
+ width: 1024px;
+ }
+}
+*/
+
+.content-fix {
+ top: 78px;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: -5000;
+}
+
+.content-fix .sidebar {
+ padding: 8px 10px 42px;
+ width: 260px;
+ height: 100%;
+ float: left;
+ border-right: 1px solid #eeeeee;
+}
+.content-fix .sidebar.nav-manage {
+ padding-left: 16px;
+}
+
+.content-fix .main {
+ z-index: -5000;
+ width: auto;
+ min-width: 760px;
+ height: 100%;
+ float: left;
+ margin-left: 264px;
+ padding-right: 16px;
+ padding-bottom: 10px;
+ padding-left: 14px;
+ position: absolute;
+
+ right: 0;
+ left: 0;
+}
+
+.content-two {
+ top: 78px;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ position: fixed;
+ z-index: -5000;
+}
+
+.content-two .sidebar {
+ padding: 8px 10px 42px;
+ width: 200px;
+ height: 100%;
+ float: left;
+ position: absolute;
+ overflow-y: auto;
+ overflow-x: hidden;
+ border-right: 1px solid #eeeeee;
+}
+.content-two .sidebar.nav-manage {
+ padding-left: 16px;
+}
+
+.content-two .sidebar .nav > li > a {
+ padding: 8px 13px;
+}
+
+.content-two .sidebar .btn {
+ padding: 5px 10px;
+ font-size: 12px;
+ line-height: 1.5;
+ border-radius: 3px;
+}
+.content-two .sidebar input,
+.content-two .sidebar select {
+ height: 30px;
+ padding: 5px 10px;
+ font-size: 12px;
+ border-radius: 3px;
+}
+
+.content-two .main {
+ width: auto;
+ min-width: 760px;
+ height: 100%;
+ float: left;
+ margin-left: 204px;
+ padding-right: 16px;
+ padding-bottom: 10px;
+ padding-left: 14px;
+ position: absolute;
+ overflow-y: auto;
+ overflow-x: hidden;
+ right: 0;
+ left: 0;
+}
+.content-two .main > .nav {
+ margin-top: 12px;
+}
+.content-two .meta {
+ display: none;
+}
+
+.panel-title {
+ font-size: 14px;
+}
+
+@media (min-width: 800px) {
+ .content-two .sidebar {
+ width: 240px;
+ }
+ .content-two .main {
+ margin-left: 244px;
+ }
+ .content-two .meta {
+ display: none;
+ }
+}
+@media (min-width: 1024px) {
+ .content-two .sidebar {
+ width: 260px;
+ }
+ .content-two .main {
+ margin-left: 264px;
+ }
+ .content-two .meta {
+ display: none;
+ }
+}
+@media (min-width: 1200px) {
+ .content-two .sidebar {
+ width: 280px;
+ }
+ .content-two .main {
+ margin-left: 284px;
+ }
+ .content-two .meta {
+ width: 140px;
+/*
+ display: table-cell;
+*/
+ display: none;
+ }
+}
+
+.content-cols {
+ display: table;
+ width: 100%;
+ height: 100%;
+ table-layout: fixed;
+ border-spacing: 0;
+}
+.content-cols .row {
+ margin: 0;
+}
+.content-cols .row2 > div {
+ display: table-cell;
+ vertical-align: top;
+ padding: 0 12px 0 12px;
+}
+.content-cols .row2 .sidebar {
+ border-right: 1px solid #eeeeee;
+ width: 160px;
+}
+.content-cols .sidebar input,
+.content-cols .sidebar select {
+ height: 30px;
+ padding: 5px 10px;
+ font-size: 12px;
+ border-radius: 3px;
+}
+.content-cols .sidebar .btn {
+ padding: 5px 10px;
+ font-size: 12px;
+ line-height: 1.5;
+ border-radius: 3px;
+}
+
+.content-cols .row2 .main {
+ width: 600px;
+}
+.content-cols .row2 .meta {
+ display: none;
+}
+
+@media (min-width: 800px) {
+ .content-cols .row2 .sidebar {
+ width: 200px;
+ }
+ .content-cols .row2 .main {
+ width: 750px;
+ }
+ .content-cols .row2 .meta {
+ display: none;
+ }
+}
+@media (min-width: 1024px) {
+ .content-cols .row2 .sidebar {
+ width: 220px;
+ }
+ .content-cols .row2 .main {
+ width: 900px;
+ }
+ .content-cols .row2 .meta {
+ display: none;
+ }
+}
+@media (min-width: 1200px) {
+ .content-cols .row2 .sidebar {
+ width: 240px;
+ }
+ .content-cols .row2 .main {
+ width: 900px;
+ }
+ .content-cols .row2 .meta {
+ width: 200px;
+ display: table-cell;
+ }
+}
+
+.content-cols .nav-bar-stacked {
+ background-color: #f7f5fa;
+}
+.content-cols .nav-bar-stacked .active a {
+ border-right: 1px solid #563d7c;
+}
+
+#global-search {
+ width: 150px;
+ border-radius: 8px;
+ background-color: #f4f4f4;
+}
+#global-search:focus {
+ width: 200px;
+ background-color: #ffffff;
+}
+
+#task {
+ position: fixed;
+ display: table;
+ width: 100%;
+ min-width: 1000px;
+ top: 46px;
+}
+
+#task > .taskbar {
+ display: table-row;
+ z-index: 1040;
+}
+
+#task > .taskbar > div {
+ padding: 0 4px 4px 4px;
+ display: table-cell;
+}
+
+#task .task {
+ max-width: 120px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ display: inline-block;
+}
+
+#task .task > a,
+#task .task > a:hover {
+ color: #ffffff;
+ text-decoration: none;
+}
+
+#task .btn-default .task > a,
+#task .btn-default .task > a:hover {
+ color: #333333;
+}
+
+#task .taskbar .btn-small {
+ padding: 3px 8px 4px;
+ margin-top: 2px;
+ font-size: 11px;
+ font-weight: 300;
+}
+
+#task .taskbar .btn-default {
+ background-color: #c3c3c3;
+ border-color: #c3c3c3;
+}
+
+#task [data-type="close"] {
+ margin-left: 5px;
+ font-size: 13px;
+ top: 1px;
+}
+
+#task .taskbar-items {
+}
+
+#task .taskbar-new {
+ text-align: right;
+ padding-right: 12px;
+}
+
+.max-size-scroll {
+ max-height: 240px;
+ overflow-y: scroll;
+}
+
+/*
+#content > *:not(.active) {
+ display: none !important;
+}
+*/
+
+h1, h2, h3, h4, h5, h6 {
+ font-weight: 200;
+}
+
+h1 {
+ font-size: 36px;
+ margin-top: 15px;
+}
+h2 {
+ font-size: 24px;
+ margin-top: 10px;
+}
+h3 {
+ font-size: 22px;
+ margin-top: 10px;
+}
+h4 {
+ font-size: 20px;
+}
+h5 {
+ font-size: 16px;
+}
+
+.form-group .help-message {
+ float: right;
+ margin-right: -23px;
+ margin-top: -23px;
+}
+.form-control {
+ height: 33px;
+ font-size: 13px;
+}
+
+.form-large {
+ width: 85%;
+ max-width: 700px;
+}
+.form-normal {
+ width: 70%;
+ max-width: 400px;
+}
+.form-small {
+ width: 50%;
+ max-width: 250px;
+}
+.form-normal fieldset {
+ margin-bottom: 10px;
+}
+.sidebar fieldset {
+ margin-bottom: 10px;
+}
+
+.form-horizontal label {
+ text-align: right;
+ float: left;
+ margin-left: 0;
+ margin-right: 0;
+ width: 130px;
+ margin-top: 7px;
+}
+.form-horizontal .form-group .checkbox label {
+ text-align: left;
+ width: 100%;
+ margin-top: 0;
+ margin-bottom: 4px;
+}
+
+/* to show checkboxes below in getting_started */
+.form-stacked .form-group .checkbox label {
+ display: block
+}
+/*
+ * bootstrap changes
+ */
+/* improve pagination if only one page is shown */
+.pagination li:only-child a {
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+}
+/* improved spacing */
+
+.pagination-count {
+ padding: 6px 6px;
+ float: left;
+ color: #999;
+ border: none;
+ line-height: 17px;
+}
+
+.page-header {
+ margin: 0 0 12px;
+ padding-bottom: 5px;
+}
+
+.page-header-title {
+ float: left;
+}
+.page-header-meta {
+ float: right;
+ padding-top: 20px;
+}
+.page-header .page-header-meta ul.pagination {
+ margin: 0 0 0 0;
+}
+
+.dropdown-menu {
+/*
+ min-width: 270px;
+*/
+}
+
+.dropdown-menu > li > a {
+ font-weight: 200;
+ max-width: 340px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.dropdown-menu .count {
+ padding-top: 1px;
+ margin-left: 10px;
+}
+
+.form-horizontal .help-inline, .form-horizontal .help-block {
+ font-size: 12px;
+ margin-top: 4px;
+}
+
+.form-horizontal .control-label {
+ width: 130px;
+}
+.form-horizontal .controls {
+ margin-left: 150px;
+}
+
+/* replace music icon with attachment */
+.icon-attachment {
+ background-position: -24px 0;
+}
+
+/*
+ *
+ */
+
+.hero-unit {
+ width: 940px;
+ margin-right: auto;
+ margin-left: auto;
+ *zoom: 1;
+ padding: 50px 60px 60px 60px;
+ margin-bottom: 30px;
+ font-size: 18px;
+ font-weight: 200;
+ line-height: 30px;
+ color: inherit;
+ background-color: #eeeeee;
+ -webkit-border-radius: 6px;
+ -moz-border-radius: 6px;
+ border-radius: 6px;
+}
+
+.hero-unit h1 {
+ font-size: 52px;
+ margin-top: 4px;
+}
+
+.hero-unit p {
+ font-size: 16px;
+ font-weight: 200;
+ line-height: 22px;
+}
+
+.hero-unit .small {
+ font-size: 12px;
+ line-height: 20px;
+ color: #999999;
+}
+
+/*
+ * removed margin of forms to not break the layout with submit buttons within area e. g. for modal dialogs
+ */
+form {
+ margin: 0 0 0 0;
+}
+
+form.form-inline select,
+form.form-inline input[type="text"],
+form.form-inline input[type="password"] {
+ width: 180px;
+ margin-right: 4px;
+}
+
+.modal-header {
+ padding: 10px 15px;
+}
+
+.modal .form-group .controls {
+ width: 65%;
+}
+
+.modal .form-group {
+ margin-bottom: 2px;
+}
+
+.modal-footer {
+ margin-top: 0;
+ padding: 14px 20px 20px;
+}
+
+.drox {
+ margin-bottom: 6px;
+}
+
+.drox-header {
+ padding: 8px 13px;
+ background-color: #f5f5f5;
+ color: #333333;
+ border-radius: 5px;
+}
+
+.drox-header .glyphicon {
+ display: none;
+}
+.drox-header:hover .glyphicon, .drox-header:focus .glyphicon {
+ display: inline-block;
+ color: #005580;
+ text-decoration: none;
+}
+.drox-header h3 {
+ font-size: 13px;
+ font-weight: 200;
+ margin-top: 2px;
+ margin-bottom: 2px;
+}
+
+.drox-body {
+ padding: 8px 13px;
+}
+
+
+.edit {
+}
+.edit .form-group {
+ margin-bottom: 2px;
+}
+.edit select, .edit input {
+ height: 30px;
+}
+.edit:focus {
+ color: #000000;
+ outline: 0;
+}
+.edit-title {
+}
+.edit-title small {
+ font-size: 12px;
+}
+
+
+.delete {
+ background:url("../assets/close.png") no-repeat;
+ text-indent: -9999em;
+ border: 0;
+ width: 40px;
+ height: 40px;
+ cursor:pointer;
+ float: right;
+ position: relative;
+ top: -15px;
+ left: 30px;
+ margin: 4px;
+ clear: right;
+}
+
+footer {
+ clear: both;
+ padding-top: 10px;
+ padding-left: 10px;
+ padding-right: 22px;
+}
+
+.can-move {
+ cursor: move;
+}
+
+.can-move-placeholder {
+ border: 1px dashed #DDD;
+ background: #EEE;
+ margin-top: 5px;
+ margin-bottom: 5px;
+}
+
+.customer_info {
+}
+
+.customer_info ul.nav > li {
+ width: 50%;
+}
+.customer_info ul.nav > li > a {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+.customer_info .thumbnail {
+ position: absolute;
+ right: 20px;
+}
+.customer_info textarea {
+ padding-left: 10px;
+ width: 100%;
+ border-color: #eee;
+}
+
+.bulk-action {
+ position: absolute;
+ width: 100%;
+}
+
+.sidebar .action {
+ padding: 6px 8px 6px;
+ position: fixed;
+ bottom: 0px;
+ background-color: #fff;
+ width: 100%;
+ left: 0px;
+ border-top: 1px solid #eeeeee;
+ width: 194px;
+ margin-left: 4px;
+}
+@media (min-width: 800px) {
+ .sidebar .action {
+ width: 234px;
+ }
+}
+@media (min-width: 1024px) {
+ .sidebar .action {
+ width: 254px;
+ }
+}
+@media (min-width: 1200px) {
+ .sidebar .action {
+ width: 274px;
+ }
+}
+
+.avatar {
+ padding-left: 6px;
+ padding-top: 4px;
+ width: 70px;
+ float: left;
+ overflow: hidden;
+}
+.avatar ul {
+ padding-left: 20px;
+}
+
+.ticket-article-view {
+ max-width: 750px;
+}
+
+.ticket-article {
+ padding: 8px 0 6px 2px;
+ min-height: 116px;
+ margin: 2px 0;
+}
+.ticket-article-item {
+ border-bottom: 1px solid #eeeeee;
+}
+.ticket-article-item:last-child {
+ border-bottom: none;
+}
+.ticket-article h4 {
+ margin-top: 2px;
+ margin-bottom: 6px;
+}
+.ticket-article.well {
+ padding: 4px 0 20px 2px;
+ margin-top: 4px;
+}
+.ticket-article-meta {
+ border-bottom: 1px solid #eeeeee;
+ padding-bottom: 10px;
+}
+.ticket-article-message {
+ padding-left: 6px;
+ padding-top: 4px;
+ margin-left: 70px;
+}
+.ticket-edit .ticket-article-message {
+ padding-right: 40px;
+}
+.message {
+ padding-top: 6px;
+ padding-bottom: 6px;
+}
+
+
+.ticket-update {
+ width: 100%;
+}
+.ticket-update .span2 {
+ width: 152px;
+ margin-right: 4px;
+}
+.ticket-update .medium {
+ width: 160px;
+}
+
+
+.show_toogle {
+ font-size: 10px;
+ line-height: 12px;
+ color: #999999;
+}
+
+.well-muted {
+ background-color: whiteSmoke;
+ border: 1px solid #eee;
+ border: 1px solid rgba(0, 0, 0, 0.05);
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
+ -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
+}
+
+.internal {
+ background-color: #f2dede;
+ border-color: #eed3d7;
+ border: 1px solid #fbeed5;
+ border-radius: 4px;
+}
+
+.merged, .merge:hover {
+ text-decoration: line-through;
+}
+
+.not-active {
+ text-decoration: line-through;
+}
+.not-active a {
+ color: #bbb;
+}
+
+.navbar {
+ min-height: 46px;
+ min-width: 1000px;
+}
+
+.navbar-form {
+ margin-top: 6px;
+ margin-bottom: 6px;
+ padding: 0;
+}
+
+.navbar-brand {
+ height: auto;
+ line-height: 18px;
+ font-weight: 200;
+ padding-top: 13px;
+ padding-bottom: 13px;
+}
+
+.navbar-nav > li > a {
+ line-height: 18px;
+ padding-top: 13px;
+ padding-bottom: 13px;
+}
+
+.navbar .bell {
+ display: none;
+}
+.navbar .bell.show {
+ display: block;
+}
+.navbar .bell.show a {
+ color: #fff;
+}
+
+.nav.navbar-nav.navbar-right {
+ padding-right: 15px;
+}
+
+.nav.nav-tabs.nav-stacked > li > a > span {
+ visibility: hidden;
+}
+.nav.nav-tabs.nav-stacked > li > a:hover > span {
+ visibility: visible;
+}
+
+.nav-sub{
+ margin-left: 12px;
+ font-size: 12px;
+}
+
+.nav.nav-pills > li > a > span.local {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ display: block;
+ margin-right: 22px;
+}
+.nav.nav-pills > li > a > span.badge {
+ margin-top: -18px;
+}
+
+.nav.nav-pills li .glyphicon {
+ display: none;
+}
+.nav.nav-pills li:hover .glyphicon, .nav li:focus .glyphicon {
+ display: inline-block;
+ text-decoration: none;
+}
+
+.customer-info {
+ width: 100%;
+ padding-top: 7px;
+}
+
+.inline-edit {
+ padding: 4px;
+}
+.inline-edit:hover {
+ padding: 3px;
+ border: 1px dotted #F92;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+}
+
+.translation {
+ border: 1px dotted #F92;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+}
+.translation:hover {
+}
+
+.translation .icon-edit {
+ display: none;
+}
+
+.translation:hover .icon-edit {
+ display: inline-block;
+}
+
+/*
+ * noty changes
+ */
+.noty_bar.noty_layout_top {
+ top: 56px;
+ position: fixed;
+ left: 50%;
+ margin-left: -300px;
+ width: 600px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+}
+
+.noty_bar.noty_theme_twitter {
+ font-size: 14px;
+ line-height: 14px;
+ text-shadow: 0 1px 0 #fff;
+ opacity: 0.9;
+}
+
+.qq-upload-icon {
+ margin: 6px 20px 0 0;
+ height: 18px;
+}
+.qq-upload-icon .glyphicon {
+ font-size: 15px;
+}
+
+.qq-upload-button {
+ display: block;
+ /*or inline-block*/
+ width: 40px;
+ padding: 8px 0 5px;
+ text-align: center;
+}
+
+.qq-upload-list {
+ text-align: left;
+ margin: 5px 10px 0;
+ padding: 0;
+ list-style: disc;
+}
+
+.qq-upload-list li {
+ font-size: 14px;
+ padding: 5px;
+ background-color: transparent;
+}
+
+.sub_attribute .control-label {
+ width: 60px;
+}
+.sub_attribute .controls {
+ margin-left: 80px;
+}
+
+.form-changed {
+ border: 1px solid #fbeed5;
+ border-radius: 4px;
+ background-color: #fcf8e3;
+}
+
+#splash {
+ background-color: #eee;
+ position: absolute;
+ width: 100%;
+ top: 0;
+ height: 100%;
+}
+#splash .logo {
+ margin-left: auto;
+ margin-right: auto;
+ margin-top: 200px;
+ width: 100px;
+}
+
diff --git a/app/controllers/sessions/collection_base.rb b/app/controllers/sessions/collection_base.rb
index 80af0495f..b3abe18e1 100644
--- a/app/controllers/sessions/collection_base.rb
+++ b/app/controllers/sessions/collection_base.rb
@@ -19,16 +19,16 @@ module ExtraCollection
def push( collections, user )
# all base stuff
- collections[ Role.to_app_model ] = Role.all
- collections[ Group.to_app_model ] = Group.all
+ #collections[ Role.to_app_model ] = Role.all
+ #collections[ Group.to_app_model ] = Group.all
- if !user.is_role('Customer')
- collections[ Organization.to_app_model ] = Organization.all
- else
- if user.organization_id
- collections[ Organization.to_app_model ] = Organization.where( :id => user.organization_id )
- end
- end
+ #if !user.is_role('Customer')
+ # collections[ Organization.to_app_model ] = Organization.all
+ #else
+ # if user.organization_id
+ # collections[ Organization.to_app_model ] = Organization.where( :id => user.organization_id )
+ # end
+ #end
end
module_function :session, :push
end
diff --git a/app/controllers/sessions/collection_ticket.rb b/app/controllers/sessions/collection_ticket.rb
index b05a02b3f..032b79a91 100644
--- a/app/controllers/sessions/collection_ticket.rb
+++ b/app/controllers/sessions/collection_ticket.rb
@@ -22,20 +22,20 @@ module ExtraCollection
def push( collections, user )
# all ticket stuff
- collections[ Ticket::StateType.to_app_model ] = Ticket::StateType.all
- collections[ Ticket::State.to_app_model ] = Ticket::State.all
- collections[ Ticket::Priority.to_app_model ] = Ticket::Priority.all
- collections[ Ticket::Article::Type.to_app_model ] = Ticket::Article::Type.all
- collections[ Ticket::Article::Sender.to_app_model ] = Ticket::Article::Sender.all
+ #collections[ Ticket::StateType.to_app_model ] = Ticket::StateType.all
+ #collections[ Ticket::State.to_app_model ] = Ticket::State.all
+ #collections[ Ticket::Priority.to_app_model ] = Ticket::Priority.all
+ #collections[ Ticket::Article::Type.to_app_model ] = Ticket::Article::Type.all
+ #collections[ Ticket::Article::Sender.to_app_model ] = Ticket::Article::Sender.all
- if !user.is_role('Customer')
+ #if !user.is_role('Customer')
# all signatures
- collections[ Signature.to_app_model ] = Signature.all
+ # collections[ Signature.to_app_model ] = Signature.all
# all email addresses
- collections[ EmailAddress.to_app_model ] = EmailAddress.all
- end
+ # collections[ EmailAddress.to_app_model ] = EmailAddress.all
+ #end
end
module_function :session, :push
diff --git a/app/models/observer/ticket/notification.rb b/app/models/observer/ticket/notification.rb
index 811bd96ff..f80218257 100644
--- a/app/models/observer/ticket/notification.rb
+++ b/app/models/observer/ticket/notification.rb
@@ -34,6 +34,7 @@ class Observer::Ticket::Notification < ActiveRecord::Observer
next if !ticket
article = ticket.articles[-1]
+ next if !article
else
raise "unknown object for notification #{event[:name]}"
end
@@ -243,7 +244,7 @@ class Observer::Ticket::Notification < ActiveRecord::Observer
:history_object => 'Ticket',
:value_from => notification_subject,
:value_to => recipient_list,
- :created_by_id => article.created_by_id || 1
+ :created_by_id => article.created_by_id ||Â 1
)
end
end
diff --git a/app/models/recent_view.rb b/app/models/recent_view.rb
index fc0f07204..c3a37ed15 100644
--- a/app/models/recent_view.rb
+++ b/app/models/recent_view.rb
@@ -37,7 +37,7 @@ class RecentView < ApplicationModel
data.delete( 'history_object_id' )
list.push data
}
- return list
+ list
end
def self.list_fulldata( user, limit = 10 )
diff --git a/app/models/ticket/overviews.rb b/app/models/ticket/overviews.rb
index ebe538e1c..94a79cd50 100644
--- a/app/models/ticket/overviews.rb
+++ b/app/models/ticket/overviews.rb
@@ -31,6 +31,7 @@ returns
# get agent overviews
role = data[:current_user].is_role( 'Agent' )
+ return if !role
Overview.where( :role_id => role.id, :active => true )
end
@@ -56,6 +57,7 @@ returns
def self.list (data)
overviews = self.all(data)
+ return if !overviews
# build up attributes hash
overview_selected = nil
diff --git a/app/models/user.rb b/app/models/user.rb
index d3a955612..b2030ed3d 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -93,7 +93,7 @@ returns
self.roles.each { |role|
return role if role.name == role_name
}
- return false
+ false
end
=begin
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 6cd4b4bff..f6dcc48f6 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -11,7 +11,7 @@
<% end %>
<%= csrf_meta_tags %>
-
+
<%= yield %>
diff --git a/lib/sessions.rb b/lib/sessions.rb
index aa3ed41c7..64cdc5739 100644
--- a/lib/sessions.rb
+++ b/lib/sessions.rb
@@ -14,7 +14,6 @@ module Sessions
@pid = @root + '/tmp/pids/sessionworker.pid'
# create global vars for threads
- @@user_threads = {}
@@client_threads = {}
=begin
@@ -458,23 +457,6 @@ returns
user = User.find( session_data[:user][:id] )
next if !user
- # start user thread
- start_user_thread = false
- if !@@user_threads[user.id]
- @@user_threads[user.id] = true
- @@user_threads[user.id] = Thread.new {
- thread_worker(user.id)
- @@user_threads[user.id] = nil
- puts "close user (#{user.id}) thread"
- }
- start_user_thread = true
- end
-
- # wait with client thread unil user thread has done some little work
- if start_user_thread
- sleep 0.5
- end
-
# start client thread
if !@@client_threads[client_id]
@@client_threads[client_id] = true
@@ -483,6 +465,7 @@ returns
@@client_threads[client_id] = nil
puts "close client (#{client_id}) thread"
}
+ sleep 0.5
end
}
@@ -493,68 +476,6 @@ returns
=begin
-check if worker for user is running
-
- Sessions.thread_worker_exists?(user)
-
-returns
-
- thread
-
-=end
-
- def self.thread_worker_exists?(user)
- @@user_threads[user.id]
- end
-
-=begin
-
-start worker for user
-
- Sessions.thread_worker(user.id)
-
-returns
-
- thread
-
-=end
-
- def self.thread_worker(user_id, try_count = 0, try_run_time = Time.now)
- puts "LOOP WORKER #{user_id} - #{try_count}"
- begin
- Sessions::Worker.new(user_id)
- rescue => e
- puts "thread_worker exited with error #{ e.inspect }"
- sleep 10
- begin
-# ActiveRecord::Base.remove_connection
-# ActiveRecord::Base.connection_pool.reap
- ActiveRecord::Base.connection_pool.release_connection
- rescue => e
- puts "Can't reconnect to database #{ e.inspect }"
- end
-
- try_run_max = 10
- try_count += 1
-
- # reset error counter if to old
- if try_run_time + ( 60 * 5 ) < Time.now
- try_count = 0
- end
- try_run_time = Time.now
-
- # restart worker again
- if try_run_max > try_count
- thread_worker(user_id, try_count, try_run_time)
- else
- raise "STOP thread_worker for user #{user_id} after #{try_run_max} tries"
- end
- end
- puts "/LOOP WORKER #{user_id} - #{try_count}"
- end
-
-=begin
-
check if thread for client_id is running
Sessions.thread_client_exists?(client_id)
@@ -586,7 +507,8 @@ returns
begin
Sessions::Client.new(client_id)
rescue => e
- puts "thread_client exited with error #{ e.inspect }"
+ puts "thread_client #{client_id} exited with error #{ e.inspect }"
+ puts e.backtrace.join("\n ")
sleep 10
begin
# ActiveRecord::Base.remove_connection
diff --git a/lib/sessions/backend/activity_stream.rb b/lib/sessions/backend/activity_stream.rb
index cb87d28c0..3a4fcb64a 100644
--- a/lib/sessions/backend/activity_stream.rb
+++ b/lib/sessions/backend/activity_stream.rb
@@ -1,38 +1,63 @@
-module Sessions::Backend::ActivityStream
+class Sessions::Backend::ActivityStream
- def self.worker( user, worker )
- cache_key = 'user_' + user.id.to_s + '_activity_stream'
- if Sessions::CacheIn.expired(cache_key)
- activity_stream = user.activity_stream( 20 )
- activity_stream_cache = Sessions::CacheIn.get( cache_key, { :re_expire => true } )
- worker.log 'notice', 'fetch activity_stream - ' + cache_key
- if activity_stream != activity_stream_cache
- worker.log 'notify', 'fetch activity_stream changed - ' + cache_key
-
- activity_stream_full = user.activity_stream( 20, true )
- Sessions::CacheIn.set( cache_key, activity_stream, { :expires_in => 0.75.minutes } )
- Sessions::CacheIn.set( cache_key + '_push', activity_stream_full )
- end
- end
+ def initialize( user, client = nil, client_id = nil )
+ @user = user
+ @client = client
+ @client_id = client_id
+ @last_change = nil
end
- def self.push( user, client )
- cache_key = 'user_' + user.id.to_s + '_activity_stream'
+ def load
- activity_stream_time = Sessions::CacheIn.get_time( cache_key, { :ignore_expire => true } )
- if activity_stream_time && client.last_change['activity_stream'] != activity_stream_time
- client.last_change['activity_stream'] = activity_stream_time
- activity_stream = Sessions::CacheIn.get( cache_key, { :ignore_expire => true } )
- client.log 'notify', "push activity_stream for user #{user.id}"
-
- # send update to browser
- r = Sessions::CacheIn.get( cache_key + '_push', { :ignore_expire => true } )
- client.send({
- :event => 'activity_stream_rebuild',
- :collection => 'activity_stream',
- :data => r,
- })
+ # get whole collection
+ activity_stream = @user.activity_stream( 25 )
+ if activity_stream && !activity_stream.first
+ return
end
+
+ if activity_stream && activity_stream.first && activity_stream.first['created_at'] == @last_change
+ return
+ end
+
+ # update last changed
+ if activity_stream && activity_stream.first
+ @last_change = activity_stream.first['created_at']
+ end
+
+ @user.activity_stream( 25, true )
+ end
+
+ def client_key
+ "as::load::#{ self.class.to_s }::#{ @user.id }::#{ @client_id }"
+ end
+
+ def push
+
+ # check timeout
+ timeout = Sessions::CacheIn.get( self.client_key )
+ return if timeout
+
+ # set new timeout
+ Sessions::CacheIn.set( self.client_key, true, { :expires_in => 0.5.minutes } )
+
+ data = self.load
+
+ return if !data||data.empty?
+
+ if !@client
+ return {
+ :event => 'activity_stream_rebuild',
+ :collection => 'activity_stream',
+ :data => data,
+ }
+ end
+
+ @client.log 'notify', "push activity_stream #{ data.first.class.to_s } for user #{ @user.id }"
+ @client.send({
+ :event => 'activity_stream_rebuild',
+ :collection => 'activity_stream',
+ :data => data,
+ })
end
end
\ No newline at end of file
diff --git a/lib/sessions/backend/collections.rb b/lib/sessions/backend/collections.rb
index d135a2afa..95b49ee85 100644
--- a/lib/sessions/backend/collections.rb
+++ b/lib/sessions/backend/collections.rb
@@ -1,71 +1,47 @@
-module Sessions::Backend::Collections
- @@last_change = {}
-
- def self.worker( user, worker )
-
- worker.log 'notice', "---user - fetch push_collection data"
-
- # get available collections
- cache_key = 'user_' + user.id.to_s + '_push_collections'
- collections = Sessions::CacheIn.get( cache_key )
- if !collections
- collections = {}
- push_collection = SessionHelper::push_collections(user)
- push_collection.each { | key, value |
- collections[ key ] = true
- }
- Sessions::CacheIn.set( cache_key, collections, { :expires_in => 2.minutes } )
- end
-
- # check all collections to push
- push_collection = {}
- collections.each { | key, v |
- cache_key = 'user_' + user.id.to_s + '_push_collections_' + key.to_s
- if Sessions::CacheIn.expired(cache_key)
- if push_collection.empty?
- push_collection = SessionHelper::push_collections(user)
- end
- push_collection_cache = Sessions::CacheIn.get( cache_key, { :re_expire => true } )
- worker.log 'notice', "---user - fetch push_collection data " + cache_key
-# if !push_collection[key] || !push_collection_cache || push_collection[key] != push_collection_cache || !push_collection[ key ].zip( push_collection_cache ).all? { |x, y| x.attributes == y.attributes }
- if !push_collection[key] || !push_collection_cache || push_collection[key] != push_collection_cache || !push_collection[ key ].zip( push_collection_cache ).all? { |x, y| return false if !x; return false if !y; x.attributes == y.attributes }
-
- worker.log 'notify', 'fetch push_collection changed - ' + cache_key
- Sessions::CacheIn.set( cache_key, push_collection[key], { :expires_in => 1.minutes } )
- end
- end
- }
+class Sessions::Backend::Collections
+ def initialize( user, client, client_id )
+ @user = user
+ @client = client
+ @client_id = client_id
+ @backends = self.backend
end
- def self.push( user, client )
-
- cache_key = 'user_' + user.id.to_s + '_push_collections'
- if !client.last_change['push_collections']
- client.last_change['push_collections'] = {}
- end
-
- collections = Sessions::CacheIn.get( cache_key ) || {}
- collections.each { | key, v |
- collection_cache_key = 'user_' + user.id.to_s + '_push_collections_' + key.to_s
- collection_time = Sessions::CacheIn.get_time( collection_cache_key, { :ignore_expire => true } )
- if collection_time && client.last_change['push_collections'][ key ] != collection_time
-
- client.last_change['push_collections'][ key ] = collection_time
- push_collections = Sessions::CacheIn.get( collection_cache_key, { :ignore_expire => true } )
-
- client.log 'notify', "push push_collections #{key} for user #{user.id}"
-
- # send update to browser
- data = {}
- data[key] = push_collections
- client.send({
- :event => 'resetCollection',
- :data => data,
- })
+ def push
+ results = []
+ @backends.each {|backend|
+ #puts "B: #{backend.inspect}"
+ result = backend.push
+ #puts "R: #{result.inspect}"
+ if result
+ results.push result
end
}
+ results
+ end
+
+ def backend
+
+ # auto population collections
+ backends = []
+
+ # load collections to deliver from external files
+ dir = File.expand_path('../../../../', __FILE__)
+ files = Dir.glob( "#{dir}/lib/sessions/backend/collections/*.rb" )
+ for file in files
+ file.gsub!("#{dir}/lib/", '')
+ file.gsub!(/\.rb$/, '')
+ next if file.classify == 'Sessions::Backend::Collections::Base'
+ #puts "LOAD #{file.classify}---"
+ #next if file == ''
+ backend = file.classify.constantize.new(@user, @client, @client_id)
+ if backend
+ backends.push backend
+ end
+ end
+
+ backends
end
end
\ No newline at end of file
diff --git a/lib/sessions/backend/collections/base.rb b/lib/sessions/backend/collections/base.rb
new file mode 100644
index 000000000..94c59712b
--- /dev/null
+++ b/lib/sessions/backend/collections/base.rb
@@ -0,0 +1,137 @@
+class Sessions::Backend::Collections::Base
+ class << self; attr_accessor :model, :is_role, :is_not_role end
+
+ def initialize( user, client = nil, client_id = nil )
+ @user = user
+ @client = client
+ @client_id = client_id
+ @last_change = nil
+ end
+
+ def collection_key
+ "collections::load::#{ self.class.to_s }::#{ @user.id }"
+ end
+
+ def load
+#puts "-LOAD--------#{self.collection_key}"
+ # check timeout
+ cache = Sessions::CacheIn.get( self.collection_key )
+ return cache if @last_change && cache
+#puts "---REAL FETCH #{@user.id}"
+ # update last changed
+ last = self.class.model.constantize.select('updated_at').order('updated_at DESC').first
+ if last
+ @last_change = last.updated_at
+ end
+
+ # if no entry exists, remember last check
+ if !@last_change
+ @last_change = Time.now
+ end
+
+ # get whole collection
+ all = self.class.model.constantize.all
+
+ # set new timeout
+ Sessions::CacheIn.set( self.collection_key, all, { :expires_in => 10.minutes } )
+
+ all
+ end
+
+ def changed?
+ # if no data has been delivered till now
+ return true if !@last_change
+
+ # check if update has been done
+ last = self.class.model.constantize.select('updated_at').order('updated_at DESC').first
+ return false if !last
+ return false if last.updated_at == @last_change
+
+ # delete collection cache
+ Sessions::CacheIn.delete( self.collection_key )
+
+ # collection has changed
+ true
+ end
+
+ def client_key
+ "collections::load::#{ self.class.to_s }::#{ @user.id }::#{ @client_id }"
+ end
+
+ def push
+
+ # check role based access
+ if self.class.is_role
+ access = nil
+ self.class.is_role.each {|role|
+ if @user.is_role(role)
+ access = true
+ end
+ }
+ return if !access
+ end
+ if self.class.is_not_role
+ self.class.is_not_role.each {|role|
+ return if @user.is_role(role)
+ }
+ end
+
+ # check timeout
+ timeout = Sessions::CacheIn.get( self.client_key )
+ return if timeout
+
+ # set new timeout
+ Sessions::CacheIn.set( self.client_key, true, { :expires_in => 10.seconds } )
+
+ return if !self.changed?
+ data = self.load
+
+ return if !data||data.empty?
+
+ # collect assets
+ assets = {}
+ data.each {|item|
+ assets = item.assets(assets)
+ }
+ if !@client
+ return {
+ :collection => {
+ data.first.class.to_app_model => data,
+ },
+ :assets => assets,
+ }
+ end
+ @client.log 'notify', "push assets for push_collection #{ data.first.class.to_s } for user #{ @user.id }"
+ @client.send({
+ :data => assets,
+ :event => [ 'loadAssets' ],
+ })
+
+ @client.log 'notify', "push push_collection #{ data.first.class.to_s } for user #{ @user.id }"
+ @client.send({
+ :event => 'resetCollection',
+ :data => {
+ data.first.class.to_app_model => data,
+ },
+ })
+ end
+
+ def self.model_set(model)
+ @model = model
+ end
+
+ def self.is_role_set(role)
+ if !@is_role
+ @is_role = []
+ end
+ @is_role.push role
+ end
+
+ def self.is_not_role_set(role)
+ if !@is_not_role
+ @is_not_role = []
+ end
+ @is_not_role.push role
+ end
+
+end
\ No newline at end of file
diff --git a/lib/sessions/backend/collections/email_address.rb b/lib/sessions/backend/collections/email_address.rb
new file mode 100644
index 000000000..de090e244
--- /dev/null
+++ b/lib/sessions/backend/collections/email_address.rb
@@ -0,0 +1,4 @@
+class Sessions::Backend::Collections::EmailAddress < Sessions::Backend::Collections::Base
+ model_set 'EmailAddress'
+ is_not_role_set 'Customer'
+end
\ No newline at end of file
diff --git a/lib/sessions/backend/collections/group.rb b/lib/sessions/backend/collections/group.rb
new file mode 100644
index 000000000..c8ed8f92f
--- /dev/null
+++ b/lib/sessions/backend/collections/group.rb
@@ -0,0 +1,3 @@
+class Sessions::Backend::Collections::Group < Sessions::Backend::Collections::Base
+ model_set 'Group'
+end
\ No newline at end of file
diff --git a/lib/sessions/backend/collections/organization.rb b/lib/sessions/backend/collections/organization.rb
new file mode 100644
index 000000000..f3d1dbd97
--- /dev/null
+++ b/lib/sessions/backend/collections/organization.rb
@@ -0,0 +1,67 @@
+class Sessions::Backend::Collections::Organization < Sessions::Backend::Collections::Base
+ model_set 'Organization'
+
+ def load
+
+ # check timeout
+ cache = Sessions::CacheIn.get( self.collection_key )
+ return cache if @last_change && cache
+
+ # update last changed
+ if !@user.is_role('Customer')
+ last = self.class.model.constantize.select('updated_at').order('updated_at DESC').first
+ if last
+ @last_change = last.updated_at
+ end
+ else
+ if @user.organization_id
+ last = Organization.where( :id => @user.organization_id ).first
+ @last_change = last.updated_at
+ end
+ end
+
+ # if no entry exists, remember last check
+ if !@last_change
+ @last_change = Time.now
+ end
+
+ # get whole collection
+ all = []
+ if !@user.is_role('Customer')
+ all = Organization.all
+ else
+ if @user.organization_id
+ all = Organization.where( :id => @user.organization_id )
+ end
+ end
+
+ # set new timeout
+ Sessions::CacheIn.set( self.collection_key, all, { :expires_in => 10.minutes } )
+
+ all
+ end
+
+ def changed?
+
+ # if no data has been delivered till now
+ return true if !@last_change
+
+ # check if update has been done
+ if !@user.is_role('Customer')
+ last = self.class.model.constantize.select('updated_at').order('updated_at DESC').first
+ else
+ if @user.organization_id
+ last = Organization.where( :id => @user.organization_id ).first
+ end
+ end
+ return false if !last
+ return false if last.updated_at == @last_change
+
+ # delete collection cache
+ Sessions::CacheIn.delete( self.collection_key )
+
+ # collection has changed
+ true
+ end
+
+end
\ No newline at end of file
diff --git a/lib/sessions/backend/collections/role.rb b/lib/sessions/backend/collections/role.rb
new file mode 100644
index 000000000..80b825f8c
--- /dev/null
+++ b/lib/sessions/backend/collections/role.rb
@@ -0,0 +1,3 @@
+class Sessions::Backend::Collections::Role < Sessions::Backend::Collections::Base
+ model_set 'Role'
+end
\ No newline at end of file
diff --git a/lib/sessions/backend/collections/signature.rb b/lib/sessions/backend/collections/signature.rb
new file mode 100644
index 000000000..60bea9ea1
--- /dev/null
+++ b/lib/sessions/backend/collections/signature.rb
@@ -0,0 +1,4 @@
+class Sessions::Backend::Collections::Signature < Sessions::Backend::Collections::Base
+ model_set 'Signature'
+ is_not_role_set 'Customer'
+end
\ No newline at end of file
diff --git a/lib/sessions/backend/collections/ticket_article_sender.rb b/lib/sessions/backend/collections/ticket_article_sender.rb
new file mode 100644
index 000000000..4b409f54a
--- /dev/null
+++ b/lib/sessions/backend/collections/ticket_article_sender.rb
@@ -0,0 +1,3 @@
+class Sessions::Backend::Collections::TicketArticleSender < Sessions::Backend::Collections::Base
+ model_set 'Ticket::Article::Sender'
+end
\ No newline at end of file
diff --git a/lib/sessions/backend/collections/ticket_article_type.rb b/lib/sessions/backend/collections/ticket_article_type.rb
new file mode 100644
index 000000000..e9b59d127
--- /dev/null
+++ b/lib/sessions/backend/collections/ticket_article_type.rb
@@ -0,0 +1,3 @@
+class Sessions::Backend::Collections::TicketArticleType < Sessions::Backend::Collections::Base
+ model_set 'Ticket::Article::Type'
+end
\ No newline at end of file
diff --git a/lib/sessions/backend/collections/ticket_priority.rb b/lib/sessions/backend/collections/ticket_priority.rb
new file mode 100644
index 000000000..110808da3
--- /dev/null
+++ b/lib/sessions/backend/collections/ticket_priority.rb
@@ -0,0 +1,3 @@
+class Sessions::Backend::Collections::TicketPriority < Sessions::Backend::Collections::Base
+ model_set 'Ticket::Priority'
+end
\ No newline at end of file
diff --git a/lib/sessions/backend/collections/ticket_state.rb b/lib/sessions/backend/collections/ticket_state.rb
new file mode 100644
index 000000000..7426be23f
--- /dev/null
+++ b/lib/sessions/backend/collections/ticket_state.rb
@@ -0,0 +1,3 @@
+class Sessions::Backend::Collections::TicketState < Sessions::Backend::Collections::Base
+ model_set 'Ticket::State'
+end
\ No newline at end of file
diff --git a/lib/sessions/backend/recent_viewed.rb b/lib/sessions/backend/recent_viewed.rb
index 672f96c9b..4b86fb0b8 100644
--- a/lib/sessions/backend/recent_viewed.rb
+++ b/lib/sessions/backend/recent_viewed.rb
@@ -1,37 +1,59 @@
-module Sessions::Backend::RecentViewed
-
- def self.worker( user, worker )
- cache_key = 'user_' + user.id.to_s + '_recent_viewed'
- if Sessions::CacheIn.expired(cache_key)
- recent_viewed = RecentView.list_fulldata( user, 6 )
- recent_viewed_cache = Sessions::CacheIn.get( cache_key, { :re_expire => true } )
- worker.log 'notice', 'fetch recent_viewed - ' + cache_key
- if recent_viewed != recent_viewed_cache
- worker.log 'notify', 'fetch recent_viewed changed - ' + cache_key
-
- recent_viewed_full = RecentView.list_fulldata( user, 6 )
- Sessions::CacheIn.set( cache_key, recent_viewed, { :expires_in => 5.seconds } )
- Sessions::CacheIn.set( cache_key + '_push', recent_viewed_full )
- end
- end
+class Sessions::Backend::RecentViewed
+ def initialize( user, client = nil, client_id = nil )
+ @user = user
+ @client = client
+ @client_id = client_id
+ @last_change = nil
end
- def self.push( user, client )
- cache_key = 'user_' + user.id.to_s + '_recent_viewed'
- recent_viewed_time = Sessions::CacheIn.get_time( cache_key, { :ignore_expire => true } )
- if recent_viewed_time && client.last_change['recent_viewed'] != recent_viewed_time
- client.last_change['recent_viewed'] = recent_viewed_time
- recent_viewed = Sessions::CacheIn.get( cache_key, { :ignore_expire => true } )
- client.log 'notify', "push recent_viewed for user #{user.id}"
+ def load
- # send update to browser
- r = Sessions::CacheIn.get( cache_key + '_push', { :ignore_expire => true } )
- client.send({
+ # get whole collection
+ recent_viewed = RecentView.list( @user, 10 )
+
+ # no data exists
+ return if !recent_viewed
+ return if recent_viewed.empty?
+
+ # no change exists
+ return if @last_change == recent_viewed
+
+ # remember last state
+ @last_change = recent_viewed
+
+ RecentView.list_fulldata( @user, 10 )
+ end
+
+ def client_key
+ "as::load::#{ self.class.to_s }::#{ @user.id }::#{ @client_id }"
+ end
+
+ def push
+
+ # check timeout
+ timeout = Sessions::CacheIn.get( self.client_key )
+ return if timeout
+
+ # set new timeout
+ Sessions::CacheIn.set( self.client_key, true, { :expires_in => 15.seconds } )
+
+ data = self.load
+
+ return if !data||data.empty?
+
+ if !@client
+ return {
:event => 'update_recent_viewed',
- :data => r,
- })
+ :data => data,
+ }
end
+
+ @client.log 'notify', "push recent_viewed for user #{ @user.id }"
+ @client.send({
+ :event => 'update_recent_viewed',
+ :data => data,
+ })
end
-end
+end
diff --git a/lib/sessions/backend/rss.rb b/lib/sessions/backend/rss.rb
index d2d44744d..a24a64f24 100644
--- a/lib/sessions/backend/rss.rb
+++ b/lib/sessions/backend/rss.rb
@@ -1,41 +1,63 @@
require 'rss'
-module Sessions::Backend::Rss
- def self.worker( user, worker )
- cache_key = 'user_' + user.id.to_s + '_rss'
- if Sessions::CacheIn.expired(cache_key)
- url = 'http://www.heise.de/newsticker/heise-atom.xml'
- rss_items = Rss.fetch( url, 8 )
- rss_items_cache = Sessions::CacheIn.get( cache_key, { :re_expire => true } )
- worker.log 'notice', 'fetch rss - ' + cache_key
- if rss_items != rss_items_cache
- worker.log 'notify', 'fetch rss changed - ' + cache_key
- Sessions::CacheIn.set( cache_key, rss_items, { :expires_in => 2.minutes } )
- Sessions::CacheIn.set( cache_key + '_push', {
- head: 'Heise ATOM',
- items: rss_items,
- })
- end
- end
+class Sessions::Backend::Rss
+
+ def initialize( user, client, client_id )
+ @user = user
+ @client = client
+ @client_id = client_id
end
- def self.push( user, client )
- cache_key = 'user_' + user.id.to_s + '_rss'
+ def collection_key
+ "rss::load::#{ self.class.to_s }::#{ @user.id }"
+ end
- rss_items_time = Sessions::CacheIn.get_time( cache_key, { :ignore_expire => true } )
- if rss_items_time && client.last_change['rss'] != rss_items_time
- client.last_change['rss'] = rss_items_time
- rss_items = Sessions::CacheIn.get( cache_key, { :ignore_expire => true } )
- client.log 'notify', "push rss for user #{user.id}"
+ def load
- # send update to browser
- r = Sessions::CacheIn.get( cache_key + '_push', { :ignore_expire => true } )
- client.send({
+ # check timeout
+ cache = Sessions::CacheIn.get( self.collection_key )
+ return cache if cache
+
+ url = 'http://www.heise.de/newsticker/heise-atom.xml'
+ rss_items = Rss.fetch( url, 8 )
+
+ # set new timeout
+ Sessions::CacheIn.set( self.collection_key, rss_items, { :expires_in => 1.hours } )
+
+ rss_items
+ end
+
+ def client_key
+ "rss::load::#{ self.class.to_s }::#{ @user.id }::#{ @client_id }"
+ end
+
+ def push
+
+ # check timeout
+ timeout = Sessions::CacheIn.get( self.client_key )
+ return if timeout
+
+ # set new timeout
+ Sessions::CacheIn.set( self.client_key, true, { :expires_in => 5.minutes } )
+
+ data = self.load
+
+ return if !data||data.empty?
+
+ if !@client
+ return {
:event => 'rss_rebuild',
:collection => 'dashboard_rss',
- :data => r,
- })
+ :data => data,
+ }
end
+
+ @client.log 'notify', "push rss for user #{@user.id}"
+ @client.send({
+ :event => 'rss_rebuild',
+ :collection => 'dashboard_rss',
+ :data => data,
+ })
end
end
\ No newline at end of file
diff --git a/lib/sessions/backend/ticket_create.rb b/lib/sessions/backend/ticket_create.rb
index 0714bed67..fb080b5de 100644
--- a/lib/sessions/backend/ticket_create.rb
+++ b/lib/sessions/backend/ticket_create.rb
@@ -1,47 +1,70 @@
-module Sessions::Backend::TicketCreate
-
- def self.worker( user, worker )
- cache_key = 'user_' + user.id.to_s + '_ticket_create_attributes'
-
- if Sessions::CacheIn.expired(cache_key)
- ticket_create_attributes = Ticket::ScreenOptions.attributes_to_change(
- :current_user_id => user.id,
- )
- ticket_create_attributes_cache = Sessions::CacheIn.get( cache_key, { :re_expire => true } )
- worker.log 'notice', 'fetch ticket_create_attributes - ' + cache_key
- if ticket_create_attributes != ticket_create_attributes_cache
- worker.log 'notify', 'fetch ticket_create_attributes changed - ' + cache_key
- Sessions::CacheIn.set( cache_key, ticket_create_attributes, { :expires_in => 2.minutes } )
- end
- end
-
+class Sessions::Backend::TicketCreate
+ def initialize( user, client = nil, client_id = nil )
+ @user = user
+ @client = client
+ @client_id = client_id
+ @last_change = nil
end
- def self.push( user, client )
- cache_key = 'user_' + user.id.to_s + '_ticket_create_attributes'
+ def load
- ticket_create_attributes_time = Sessions::CacheIn.get_time( cache_key, { :ignore_expire => true } )
- if ticket_create_attributes_time && client.last_change['ticket_create_attributes'] != ticket_create_attributes_time
- client.last_change['ticket_create_attributes'] = ticket_create_attributes_time
- create_attributes = Sessions::CacheIn.get( cache_key, { :ignore_expire => true } )
- users = {}
- create_attributes[:owner_id].each {|user_id|
- if !users[user_id]
- users[user_id] = User.user_data_full(user_id)
- end
- }
- data = {
- :users => users,
- :edit_form => create_attributes,
- }
- client.log 'notify', "push ticket_create_attributes for user #{user.id}"
+ # get whole collection
+ ticket_create_attributes = Ticket::ScreenOptions.attributes_to_change(
+ :current_user_id => @user.id,
+ )
- # send update to browser
- client.send({
+ # no data exists
+ return if !ticket_create_attributes
+
+ # no change exists
+ return if @last_change == ticket_create_attributes
+
+ # remember last state
+ @last_change = ticket_create_attributes
+
+ ticket_create_attributes
+ end
+
+ def client_key
+ "as::load::#{ self.class.to_s }::#{ @user.id }::#{ @client_id }"
+ end
+
+ def push
+
+ # check timeout
+ timeout = Sessions::CacheIn.get( self.client_key )
+ return if timeout
+
+ # set new timeout
+ Sessions::CacheIn.set( self.client_key, true, { :expires_in => 25.seconds } )
+
+ create_attributes = self.load
+
+ return if !create_attributes
+
+ users = {}
+ create_attributes[:owner_id].each {|user_id|
+ if !users[user_id]
+ users[user_id] = User.user_data_full(user_id)
+ end
+ }
+ data = {
+ :users => users,
+ :edit_form => create_attributes,
+ }
+
+ if !@client
+ return {
:collection => 'ticket_create_attributes',
- :data => data,
- })
+ :data => create_attributes,
+ }
end
+
+ @client.log 'notify', "push ticket_create for user #{ @user.id }"
+ @client.send({
+ :collection => 'ticket_create_attributes',
+ :data => create_attributes,
+ })
end
end
\ No newline at end of file
diff --git a/lib/sessions/backend/ticket_overview_index.rb b/lib/sessions/backend/ticket_overview_index.rb
index 6e8fef17f..67e89c8ad 100644
--- a/lib/sessions/backend/ticket_overview_index.rb
+++ b/lib/sessions/backend/ticket_overview_index.rb
@@ -1,38 +1,59 @@
-module Sessions::Backend::TicketOverviewIndex
-
- def self.worker( user, worker )
- cache_key = 'user_' + user.id.to_s + '_overview'
- if Sessions::CacheIn.expired(cache_key)
- overview = Ticket::Overviews.list(
- :current_user => user,
- )
- overview_cache = Sessions::CacheIn.get( cache_key, { :re_expire => true } )
- worker.log 'notice', 'fetch overview - ' + cache_key
- if overview != overview_cache
- worker.log 'notify', 'fetch overview changed - ' + cache_key
-# puts overview.inspect
-# puts '------'
-# puts overview_cache.inspect
- Sessions::CacheIn.set( cache_key, overview, { :expires_in => 4.seconds } )
- end
- end
+class Sessions::Backend::TicketOverviewIndex
+ def initialize( user, client = nil, client_id = nil )
+ @user = user
+ @client = client
+ @client_id = client_id
+ @last_change = nil
end
- def self.push( user, client )
- cache_key = 'user_' + user.id.to_s + '_overview'
- overview_time = Sessions::CacheIn.get_time( cache_key, { :ignore_expire => true } )
- if overview_time && client.last_change['overview'] != overview_time
- client.last_change['overview'] = overview_time
- overview = Sessions::CacheIn.get( cache_key, { :ignore_expire => true } )
+ def load
- client.log 'notify', "push overview for user #{user.id}"
+ # get whole collection
+ overview = Ticket::Overviews.list(
+ :current_user => @user,
+ )
- # send update to browser
- client.send({
+ # no data exists
+ return if !overview
+
+ # no change exists
+ return if @last_change == overview
+
+ # remember last state
+ @last_change = overview
+
+ overview
+ end
+
+ def client_key
+ "as::load::#{ self.class.to_s }::#{ @user.id }::#{ @client_id }"
+ end
+
+ def push
+
+ # check timeout
+ timeout = Sessions::CacheIn.get( self.client_key )
+ return if timeout
+
+ # set new timeout
+ Sessions::CacheIn.set( self.client_key, true, { :expires_in => 5.seconds } )
+
+ data = self.load
+
+ return if !data
+
+ if !@client
+ return {
:event => 'navupdate_ticket_overview',
- :data => overview,
- })
+ :data => data,
+ }
end
+
+ @client.log 'notify', "push overview_index for user #{ @user.id }"
+ @client.send({
+ :event => 'navupdate_ticket_overview',
+ :data => data,
+ })
end
end
\ No newline at end of file
diff --git a/lib/sessions/backend/ticket_overview_list.rb b/lib/sessions/backend/ticket_overview_list.rb
index 67680699f..2119b428a 100644
--- a/lib/sessions/backend/ticket_overview_list.rb
+++ b/lib/sessions/backend/ticket_overview_list.rb
@@ -1,80 +1,109 @@
-module Sessions::Backend::TicketOverviewList
-
- def self.worker( user, worker )
- overviews = Ticket::Overviews.all(
- :current_user => user,
- )
- overviews.each { |overview|
- cache_key = 'user_' + user.id.to_s + '_overview_data_' + overview.link
- if Sessions::CacheIn.expired(cache_key)
- overview_data = Ticket::Overviews.list(
- :view => overview.link,
- :current_user => user,
- :array => true,
- )
- overview_data_cache = Sessions::CacheIn.get( cache_key, { :re_expire => true } )
- worker.log 'notice', 'fetch overview_data - ' + cache_key
- if overview_data != overview_data_cache
- worker.log 'notify', 'fetch overview_data changed - ' + cache_key
- Sessions::CacheIn.set( cache_key, overview_data, { :expires_in => 5.seconds } )
- end
- end
- }
+class Sessions::Backend::TicketOverviewList
+ def initialize( user, client = nil, client_id = nil )
+ @user = user
+ @client = client
+ @client_id = client_id
+ @last_change = nil
end
- def self.push( user, client )
+ def load
+
+ # get whole collection
overviews = Ticket::Overviews.all(
- :current_user => user,
+ :current_user => @user,
)
+ result = []
overviews.each { |overview|
- cache_key = 'user_' + user.id.to_s + '_overview_data_' + overview.link
+ overview_data = Ticket::Overviews.list(
+ :view => overview.link,
+ :current_user => @user,
+ :array => true,
+ )
+ data = { :list => overview_data, :index => overview }
+ result.push data
+ }
- if !client.last_change['overview_list']
- client.last_change['overview_list'] = {}
- end
+ # no data exists
+ return if !result || result.empty?
- overview_data_time = Sessions::CacheIn.get_time( cache_key, { :ignore_expire => true } )
- if overview_data_time && client.last_change['overview_list'][overview.link] != overview_data_time
- client.last_change['overview_list'][overview.link] = overview_data_time
- overview_data = Sessions::CacheIn.get( cache_key, { :ignore_expire => true } )
- client.log 'notify', "push overview_data #{overview.link} for user #{user.id}"
- users = {}
- tickets = {}
- overview_data[:ticket_ids].each {|ticket_id|
- client.ticket( ticket_id, tickets, users )
- }
+ # no change exists
+ return if @last_change == result
- # get groups
- group_ids = []
- Group.where( :active => true ).each { |group|
- group_ids.push group.id
+ # remember last state
+ @last_change = result
+
+ result
+ end
+
+ def client_key
+ "as::load::#{ self.class.to_s }::#{ @user.id }::#{ @client_id }"
+ end
+
+ def push
+
+ # check timeout
+ timeout = Sessions::CacheIn.get( self.client_key )
+ return if timeout
+
+ # set new timeout
+ Sessions::CacheIn.set( self.client_key, true, { :expires_in => 6.seconds } )
+
+ items = self.load
+
+ return if !items
+
+ # push overviews
+ results = []
+ items.each { |item|
+
+ overview_data = item[:list]
+
+ assets = {}
+ overview_data[:ticket_ids].each {|ticket_id|
+ ticket = Ticket.find( ticket_id )
+ assets = ticket.assets(assets)
+ }
+
+ # get groups
+ group_ids = []
+ Group.where( :active => true ).each { |group|
+ group_ids.push group.id
+ }
+ agents = {}
+ Ticket::ScreenOptions.agents.each { |user|
+ agents[ user.id ] = 1
+ }
+ users = {}
+ groups_users = {}
+ groups_users[''] = []
+ group_ids.each {|group_id|
+ groups_users[ group_id ] = []
+ Group.find(group_id).users.each {|user|
+ next if !agents[ user.id ]
+ groups_users[ group_id ].push user.id
+ if !users[user.id]
+ users[user.id] = User.find(user.id)
+ assets = users[user.id].assets(assets)
+ end
}
- agents = {}
- Ticket::ScreenOptions.agents.each { |user|
- agents[ user.id ] = 1
- }
- groups_users = {}
- groups_users[''] = []
- group_ids.each {|group_id|
- groups_users[ group_id ] = []
- Group.find(group_id).users.each {|user|
- next if !agents[ user.id ]
- groups_users[ group_id ].push user.id
- if !users[user.id]
- users[user.id] = User.user_data_full(user.id)
- end
- }
+ }
+
+ if !@client
+ result = {
+ :event => 'navupdate_ticket_overview',
+ :data => item[:index],
}
+ results.push result
+ else
+
+ @client.log 'notify', "push overview_list for user #{ @user.id }"
# send update to browser
- client.send({
- :data => {
- User.to_app_model => users,
- Ticket.to_app_model => tickets,
- },
- :event => [ 'loadAssets' ]
+ @client.send({
+ :data => assets,
+ :event => [ 'loadAssets' ]
})
- client.send({
+ @client.send({
:data => {
:overview => overview_data[:overview],
:ticket_ids => overview_data[:ticket_ids],
@@ -85,10 +114,12 @@ module Sessions::Backend::TicketOverviewList
},
},
:event => [ 'ticket_overview_rebuild' ],
- :collection => 'ticket_overview_' + overview.link.to_s,
+ :collection => 'ticket_overview_' + item[:index].link.to_s,
})
end
}
+ return results if !@client
+ nil
end
end
\ No newline at end of file
diff --git a/lib/sessions/cache_in.rb b/lib/sessions/cache_in.rb
index 70a65f926..e0b6dcc61 100644
--- a/lib/sessions/cache_in.rb
+++ b/lib/sessions/cache_in.rb
@@ -4,6 +4,11 @@ module Sessions::CacheIn
@@expires_in = {}
@@expires_in_ttl = {}
+ def self.delete( key )
+ @@data.delete( key )
+ @@data_time.delete( key )
+ end
+
def self.set( key, value, params = {} )
# puts 'CacheIn.set:' + key + '-' + value.inspect
if params[:expires_in]
diff --git a/lib/sessions/client.rb b/lib/sessions/client.rb
index 7f303ab32..7d226fdab 100644
--- a/lib/sessions/client.rb
+++ b/lib/sessions/client.rb
@@ -1,13 +1,7 @@
class Sessions::Client
- attr_accessor :last_change
-
def initialize( client_id )
@client_id = client_id
- @cache_key = ''
- @data = {}
- @pushed = {}
- @last_change = {}
self.log 'notify', "---client start ws connection---"
self.fetch
self.log 'notify', "---client exiting ws connection---"
@@ -15,6 +9,18 @@ class Sessions::Client
def fetch
+ backends = [
+ 'Sessions::Backend::TicketOverviewIndex',
+ 'Sessions::Backend::TicketOverviewList',
+ 'Sessions::Backend::Collections',
+ 'Sessions::Backend::Rss',
+ 'Sessions::Backend::ActivityStream',
+ 'Sessions::Backend::RecentViewed',
+ 'Sessions::Backend::TicketCreate',
+ ]
+
+ backend_pool = []
+ user_id_last_run = nil
loop_count = 0
while true
@@ -26,83 +32,30 @@ class Sessions::Client
user = User.lookup( :id => session_data[:user][:id] )
return if !user
- # set cache key
- @cache_key = 'user_' + user.id.to_s
+ # init new backends
+ if user_id_last_run != user.id
+ user_id_last_run = user.id
+
+ # release old objects
+ backend_pool.each {|pool|
+ pool = nil
+ }
+
+ # create new pool
+ backend_pool = []
+ backends.each {|backend|
+ item = backend.constantize.new(user, self, @client_id)
+ backend_pool.push item
+ }
+ end
loop_count += 1
self.log 'notice', "---client - looking for data of user #{user.id}"
- # remember last run
- Sessions::CacheIn.set( 'last_run_' + user.id.to_s , true, { :expires_in => 20.seconds } )
-
- # verify already pushed data, send update if needed
- if !Sessions::CacheIn.get( 'pushed_users' + @client_id.to_s )
- Sessions::CacheIn.set( 'pushed_users' + @client_id.to_s , true, { :expires_in => 60.seconds } )
- if @pushed[:users]
- users = {}
- @pushed[:users].each {|user_id, user_o|
- self.user( user_id, users )
- }
- if !users.empty?
- users.each {|user_id, user_data|
- self.log 'notify', "push update of already pushed user id #{user_id}"
- }
- # send update to browser
- self.send({
- :data => {
- User.to_app_model => users,
- },
- :event => [ 'loadAssets' ],
- });
- end
- end
- end
-
- # verify already pushed data, send update if needed
- if !Sessions::CacheIn.get( 'pushed_tickets' + @client_id.to_s )
- Sessions::CacheIn.set( 'pushed_tickets' + @client_id.to_s , true, { :expires_in => 60.seconds } )
- if @pushed[:tickets]
- tickets = {}
- users = {}
- @pushed[:tickets].each {|ticket_id, ticket_data|
- self.ticket( ticket_id, tickets, users )
- }
- if !tickets.empty?
- tickets.each {|id, ticket|
- self.log 'notify', "push update of already pushed ticket id #{id}"
- }
- # send update to browser
- self.send({
- :data => {
- Ticket.to_app_model => tickets,
- User.to_app_model => users,
- },
- :event => [ 'loadAssets' ],
- });
- end
- end
- end
-
- # overview
- Sessions::Backend::TicketOverviewIndex.push( user, self )
-
- # overview_data
- Sessions::Backend::TicketOverviewList.push( user, self )
-
- # ticket_create_attributes
- Sessions::Backend::TicketCreate.push( user, self )
-
- # recent viewed
- Sessions::Backend::RecentViewed.push( user, self )
-
- # activity stream
- Sessions::Backend::ActivityStream.push( user, self )
-
- # rss
- Sessions::Backend::Rss.push( user, self )
-
- # push_collections
- Sessions::Backend::Collections.push( user, self )
+ # push messages from backends
+ backend_pool.each {|pool|
+ pool.push
+ }
self.log 'notice', "---/client-"
@@ -115,44 +68,6 @@ class Sessions::Client
end
end
- # add ticket if needed
- def ticket( ticket_id, tickets, users )
- if !@pushed[:tickets]
- @pushed[:tickets] = {}
- end
- ticket = Ticket.lookup( :id => ticket_id )
- if @pushed[:tickets][ticket_id] != ticket['updated_at']
- @pushed[:tickets][ticket_id] = ticket['updated_at']
- tickets[ticket_id] = ticket
- end
-
- # add users if needed
- self.user( ticket['owner_id'], users )
- self.user( ticket['customer_id'], users )
- self.user( ticket['created_by_id'], users )
- if ticket['updated_by_id']
- self.user( ticket['updated_by_id'], users )
- end
- end
-
- # add user if needed
- def user( user_id, users )
- if !@pushed[:users]
- @pushed[:users] = {}
- end
-
- # get user
- user = User.user_data_full( user_id )
-
- # user is already on client and not changed
- return if @pushed[:users][ user_id ] == user['updated_at']
- @pushed[:users][user_id] = user['updated_at']
-
- # user not on client or different
- self.log 'notice', 'push user ... ' + user['login']
- users[ user_id ] = user
- end
-
# send update to browser
def send( data )
Sessions.send( @client_id, data )
@@ -162,4 +77,4 @@ class Sessions::Client
return if level == 'notice'
puts "#{Time.now}:client(#{ @client_id }) #{ data }"
end
-end
+end
\ No newline at end of file
diff --git a/lib/sessions/worker.rb b/lib/sessions/worker.rb
deleted file mode 100644
index a8cb81042..000000000
--- a/lib/sessions/worker.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-class Sessions::Worker
- def initialize( user_id )
- @user_id = user_id
-
- self.log 'notify', "---user started user state"
-
- Sessions::CacheIn.set( 'last_run_' + user_id.to_s , true, { :expires_in => 20.seconds } )
-
- self.fetch( user_id )
- end
-
- def fetch(user_id)
-
- while true
- user = User.lookup( :id => user_id )
- return if !user
-
- # check if user is still with min one open connection
- if !Sessions::CacheIn.get( 'last_run_' + user.id.to_s )
- self.log 'notify', "---user - closeing thread - no open user connection"
- return
- end
-
- self.log 'notice', "---user - fetch user data"
-
- # overview
- Sessions::Backend::TicketOverviewIndex.worker( user, self )
-
- # overview lists
- Sessions::Backend::TicketOverviewList.worker( user, self )
-
- # create_attributes
- Sessions::Backend::TicketCreate.worker( user, self )
-
- # recent viewed
- Sessions::Backend::RecentViewed.worker( user, self )
-
- # activity steam
- Sessions::Backend::ActivityStream.worker( user, self )
-
- # rss
- Sessions::Backend::Rss.worker( user, self )
-
- # auto population of default collections
- Sessions::Backend::Collections.worker( user, self )
-
- self.log 'notice', "---/user-"
- sleep 1
- end
- end
-
- def log( level, data )
- return if level == 'notice'
- puts "#{Time.now}:user_id(#{ @user_id }) #{ data }"
- end
-end
\ No newline at end of file
diff --git a/script/websocket-server.rb b/script/websocket-server.rb
index 059c03b3d..9b5bb0ddc 100755
--- a/script/websocket-server.rb
+++ b/script/websocket-server.rb
@@ -175,6 +175,7 @@ EventMachine.run {
# remember ping, send pong back
elsif data['action'] == 'ping'
+ Sessions.touch(client_id)
@clients[client_id][:last_ping] = Time.now
@clients[client_id][:websocket].send( '[{"action":"pong"}]' )
diff --git a/test/unit/session_basic_test.rb b/test/unit/session_basic_test.rb
new file mode 100644
index 000000000..898731534
--- /dev/null
+++ b/test/unit/session_basic_test.rb
@@ -0,0 +1,320 @@
+# encoding: utf-8
+require 'test_helper'
+
+class SessionBasicTest < ActiveSupport::TestCase
+ test 'a cache' do
+ Sessions::CacheIn.set( 'last_run_test' , true, { :expires_in => 2.seconds } )
+ result = Sessions::CacheIn.get( 'last_run_test' )
+ assert_equal( true, result, "check 1" )
+
+ # should not be expired
+ result = Sessions::CacheIn.expired( 'last_run_test' )
+ assert_equal( false, result, "check 1 - expired" )
+
+ # should be expired
+ sleep 3
+ result = Sessions::CacheIn.expired( 'last_run_test' )
+ assert_equal( true, result, "check 1 - expired" )
+
+ # renew expire
+ result = Sessions::CacheIn.get( 'last_run_test', :re_expire => true )
+ assert_equal( true, result, "check 1 - re_expire" )
+
+ # should not be expired
+ result = Sessions::CacheIn.expired( 'last_run_test' )
+ assert_equal( false, result, "check 1 - expired" )
+
+ # ignore expired
+ sleep 3
+ result = Sessions::CacheIn.get( 'last_run_test', :ignore_expire => true )
+ assert_equal( true, result, "check 1 - ignore_expire" )
+
+ # should be expired
+ result = Sessions::CacheIn.expired( 'last_run_test' )
+ assert_equal( true, result, "check 2" )
+
+ result = Sessions::CacheIn.get( 'last_run_test' )
+ assert_equal( nil, result, "check 2" )
+
+ # check delete cache
+ Sessions::CacheIn.set( 'last_run_delete' , true, { :expires_in => 5.seconds } )
+ result = Sessions::CacheIn.get( 'last_run_delete' )
+ assert_equal( true, result, "check 1" )
+ Sessions::CacheIn.delete( 'last_run_delete' )
+ result = Sessions::CacheIn.get( 'last_run_delete' )
+ assert_equal( nil, nil, "check delete" )
+ end
+
+ test 'b collections group' do
+ require 'sessions/backend/collections/group.rb'
+
+ UserInfo.current_user_id = 1
+ user = User.lookup(:id => 1)
+ collection_client1 = Sessions::Backend::Collections::Group.new(user, false, '123-1')
+ collection_client2 = Sessions::Backend::Collections::Group.new(user, false, '234-2')
+
+ # get whole collections
+ result1 = collection_client1.push
+ assert( !result1.empty?, "check collections" )
+ sleep 1
+ result2 = collection_client2.push
+ assert( !result2.empty?, "check collections" )
+ assert_equal( result1, result2, "check collections" )
+
+ # next check should be empty
+ result1 = collection_client1.push
+ assert( !result1, "check collections - recall" )
+ sleep 1
+ result2 = collection_client2.push
+ assert( !result2, "check collections - recall" )
+
+ # change collection
+ group = Group.first
+ group.touch
+ sleep 16
+
+ # get whole collections
+ result1 = collection_client1.push
+ assert( !result1.empty?, "check collections - after touch" )
+ sleep 1
+ result2 = collection_client2.push
+ assert( !result2.empty?, "check collections - after touch" )
+ assert_equal( result1, result2, "check collections" )
+
+ # check again after touch
+ result1 = collection_client1.push
+ assert( !result1, "check collections - after touch - recall" )
+ sleep 1
+ result2 = collection_client2.push
+ assert( !result2, "check collections - after touch - recall" )
+ assert_equal( result1, result2, "check collections" )
+
+ # change collection
+ group = Group.create( :name => 'SomeGroup::' + rand(999999).to_s, :active => true )
+ sleep 12
+
+ # get whole collections
+ result1 = collection_client1.push
+ assert( !result1.empty?, "check collections - after create" )
+ sleep 1
+ result2 = collection_client2.push
+ assert( !result2.empty?, "check collections - after create" )
+ assert_equal( result1, result2, "check collections" )
+
+ # check again after create
+ sleep 14
+ result1 = collection_client1.push
+ assert( !result1, "check collections - after create - recall" )
+ sleep 1
+ result2 = collection_client2.push
+ assert( !result2, "check collections - after create - recall" )
+ assert_equal( result1, result2, "check collections" )
+
+ # change collection
+ group.destroy
+ sleep 14
+
+ # get whole collections
+ result1 = collection_client1.push
+ assert( !result1.empty?, "check collections - after destroy" )
+ sleep 1
+ result2 = collection_client2.push
+ assert( !result2.empty?, "check collections - after destroy" )
+ assert_equal( result1, result2, "check collections" )
+
+ # check again after destroy
+ sleep 12
+ result1 = collection_client1.push
+ assert( !result1, "check collections - after destroy - recall" )
+ sleep 1
+ result2 = collection_client2.push
+ assert( !result2, "check collections - after destroy - recall" )
+ assert_equal( result1, result2, "check collections" )
+ end
+
+ user = User.lookup(:id => 1)
+ roles = Role.where( :name => [ 'Agent', 'Admin'] )
+ user.roles = roles
+ user.save
+
+ test 'b collections organization' do
+ require 'sessions/backend/collections/organization.rb'
+ UserInfo.current_user_id = 1
+ Organization.destroy_all
+ user = User.lookup(:id => 1)
+
+ collection_client1 = Sessions::Backend::Collections::Organization.new(user, false, '123-1')
+ collection_client2 = Sessions::Backend::Collections::Organization.new(user, false, '234-2')
+
+ # get whole collections - should be nil, no org exists!
+ result1 = collection_client1.push
+ assert( !result1, "check collections" )
+ sleep 1
+ result2 = collection_client2.push
+ assert( !result2, "check collections" )
+ assert_equal( result1, result2, "check collections" )
+
+ # next check - should still be nil, no org exists!
+ result1 = collection_client1.push
+ assert( !result1, "check collections - recall" )
+ sleep 1
+ result2 = collection_client2.push
+ assert( !result2, "check collections - recall" )
+
+ # change collection
+ org = Organization.create( :name => 'SomeOrg::' + rand(999999).to_s, :active => true )
+ sleep 16
+
+ # get whole collections
+ result1 = collection_client1.push
+ assert( !result1.empty?, "check collections - after create" )
+ sleep 1
+ result2 = collection_client2.push
+ assert( !result2.empty?, "check collections - after create" )
+ assert_equal( result1, result2, "check collections" )
+
+ sleep 16
+
+ # next check should be empty
+ result1 = collection_client1.push
+ assert( !result1, "check collections - after create recall" )
+ result2 = collection_client2.push
+ assert( !result2, "check collections - after create recall" )
+
+ organization = Organization.first
+ organization.touch
+ sleep 16
+
+ # get whole collections
+ result1 = collection_client1.push
+ assert( !result1.empty?, "check collections - after touch" )
+ result2 = collection_client2.push
+ assert( !result1.empty?, "check collections - after touch" )
+ assert_equal( result1, result2, "check collections" )
+
+ end
+
+ test 'b rss' do
+ user = User.lookup(:id => 1)
+ collection_client1 = Sessions::Backend::Rss.new(user, false, '123-1')
+
+ # get whole collections
+ result1 = collection_client1.push
+ #puts "RSS1: #{result1.inspect}"
+ assert( !result1.empty?, "check rss" )
+ sleep 1
+
+ # next check should be empty
+ result1 = collection_client1.push
+ #puts "R1: #{result1.inspect}"
+ assert( !result1, "check rss - recall" )
+ end
+
+ test 'b activity stream' do
+
+ UserInfo.current_user_id = 1
+
+ # create users
+ roles = Role.where( :name => [ 'Agent', 'Admin'] )
+ groups = Group.all
+
+ UserInfo.current_user_id = 1
+ agent1 = User.create_or_update(
+ :login => 'activity-stream-agent-1',
+ :firstname => 'Session',
+ :lastname => 'activity stream ' + rand(99999).to_s,
+ :email => 'activity-stream-agent1@example.com',
+ :password => 'agentpw',
+ :active => true,
+ :roles => roles,
+ :groups => groups,
+ )
+ agent1.roles = roles
+ agent1.save
+
+ as_client1 = Sessions::Backend::ActivityStream.new(agent1, false, '123-1')
+
+ # get as stream
+ result1 = as_client1.push
+ assert( result1, "check as" )
+ sleep 1
+
+ # next check should be empty
+ result1 = as_client1.push
+ assert( !result1, "check as - recall" )
+
+ # next check should be empty
+ sleep 60
+ result1 = as_client1.push
+ assert( !result1, "check as - recall 2" )
+
+ agent1.update_attribute( :email, 'activity-stream-agent11@example.com' )
+ ticket = Ticket.create(:title => '12323', :group_id => 1, :priority_id => 1, :state_id => 1, :customer_id => 1 )
+
+ sleep 32
+
+ # get as stream
+ result1 = as_client1.push
+ assert( result1, "check as - recall 3" )
+ end
+
+ test 'b recent_viewed' do
+
+ user = User.lookup(:id => 1)
+ ticket = Ticket.all.last
+ RecentView.log( ticket, user )
+ recent_viewed_client1 = Sessions::Backend::RecentViewed.new(user, false, '123-1')
+
+ # get as stream
+ result1 = recent_viewed_client1.push
+ assert( result1, "check recent_viewed" )
+ sleep 1
+
+ # next check should be empty
+ result1 = recent_viewed_client1.push
+ assert( !result1, "check recent_viewed - recall" )
+
+ # next check should be empty
+ sleep 20
+ result1 = recent_viewed_client1.push
+ assert( !result1, "check recent_viewed - recall 2" )
+
+ RecentView.log( ticket, user )
+
+ sleep 20
+
+ # get as stream
+ result1 = recent_viewed_client1.push
+ assert( result1, "check recent_viewed - recall 3" )
+ end
+
+ test 'b ticket_create' do
+
+ UserInfo.current_user_id = 1
+ user = User.lookup(:id => 1)
+ ticket_create_client1 = Sessions::Backend::TicketCreate.new(user, false, '123-1')
+
+ # get as stream
+ result1 = ticket_create_client1.push
+ assert( result1, "check ticket_create" )
+ sleep 1
+
+ # next check should be empty
+ result1 = ticket_create_client1.push
+ assert( !result1, "check ticket_create - recall" )
+
+ # next check should be empty
+ sleep 10
+ result1 = ticket_create_client1.push
+ assert( !result1, "check ticket_create - recall 2" )
+
+ Group.create( :name => 'SomeTicketCreateGroup::' + rand(999999).to_s, :active => true )
+
+ sleep 26
+
+ # get as stream
+ result1 = ticket_create_client1.push
+ assert( result1, "check ticket_create - recall 3" )
+ end
+
+end
\ No newline at end of file
diff --git a/test/unit/session_basic_ticket_test.rb b/test/unit/session_basic_ticket_test.rb
new file mode 100644
index 000000000..32a19f9af
--- /dev/null
+++ b/test/unit/session_basic_ticket_test.rb
@@ -0,0 +1,62 @@
+# encoding: utf-8
+require 'test_helper'
+
+class SessionBasicTicketTest < ActiveSupport::TestCase
+ test 'b ticket_overview_index' do
+
+ UserInfo.current_user_id = 1
+ user = User.lookup(:id => 1)
+ client1 = Sessions::Backend::TicketOverviewIndex.new(user, false, '123-1')
+
+ # get as stream
+ result1 = client1.push
+ assert( result1, "check ticket_overview_index" )
+ sleep 1
+
+ # next check should be empty
+ result1 = client1.push
+ assert( !result1, "check ticket_overview_index - recall" )
+
+ # next check should be empty
+ sleep 10
+ result1 = client1.push
+ assert( !result1, "check ticket_overview_index - recall 2" )
+
+ ticket = Ticket.create( :title => '12323', :group_id => 1, :priority_id => 1, :state_id => 1, :customer_id => 1 )
+
+ sleep 10
+
+ # get as stream
+ result1 = client1.push
+ assert( result1, "check ticket_overview_index - recall 3" )
+ end
+
+ test 'b ticket_overview_list' do
+
+ UserInfo.current_user_id = 1
+ user = User.lookup(:id => 1)
+ client1 = Sessions::Backend::TicketOverviewList.new(user, false, '123-1')
+
+ # get as stream
+ result1 = client1.push
+ assert( result1, "check ticket_overview_list" )
+ sleep 1
+
+ # next check should be empty
+ result1 = client1.push
+ assert( !result1, "check ticket_overview_list - recall" )
+
+ # next check should be empty
+ sleep 10
+ result1 = client1.push
+ assert( !result1, "check ticket_overview_list - recall 2" )
+
+ ticket = Ticket.create( :title => '12323', :group_id => 1, :priority_id => 1, :state_id => 1, :customer_id => 1 )
+
+ sleep 10
+
+ # get as stream
+ result1 = client1.push
+ assert( result1, "check ticket_overview_list - recall 3" )
+ end
+end
\ No newline at end of file
diff --git a/test/unit/session_collections_test.rb b/test/unit/session_collections_test.rb
new file mode 100644
index 000000000..a5981c14c
--- /dev/null
+++ b/test/unit/session_collections_test.rb
@@ -0,0 +1,147 @@
+# encoding: utf-8
+require 'test_helper'
+
+class SessionCollectionsTest < ActiveSupport::TestCase
+
+ test 'c collections' do
+
+ UserInfo.current_user_id = 1
+
+ # create users
+ roles = Role.where( :name => [ 'Agent', 'Admin'] )
+ groups = Group.all
+
+ agent1 = User.create_or_update(
+ :login => 'session-collections-agent-1',
+ :firstname => 'Session',
+ :lastname => 'collections 1',
+ :email => 'session-collections-agent-1@example.com',
+ :password => 'agentpw',
+ :active => true,
+ :roles => roles,
+ :groups => groups,
+ )
+ agent1.roles = roles
+ agent1.save
+
+ roles = Role.where( :name => [ 'Agent' ] )
+ groups = Group.all
+
+ agent2 = User.create_or_update(
+ :login => 'session-collections-agent-2',
+ :firstname => 'Session',
+ :lastname => 'collections 2',
+ :email => 'session-collections-agent-2@example.com',
+ :password => 'agentpw',
+ :active => true,
+ :roles => roles,
+ :groups => groups,
+ )
+ agent2.roles = roles
+ agent2.save
+
+ roles = Role.where( :name => [ 'Customer'] )
+ customer1 = User.create_or_update(
+ :login => 'session-collections-customer-1',
+ :firstname => 'Session',
+ :lastname => 'collections 2',
+ :email => 'session-collections-customer-1@example.com',
+ :password => 'customerpw',
+ :active => true,
+ :roles => roles,
+ )
+ customer1.roles = roles
+ customer1.save
+
+ collection_client1 = Sessions::Backend::Collections.new(agent1, nil, 'aaa-1')
+ collection_client2 = Sessions::Backend::Collections.new(agent2, nil, 'bbb-2')
+ collection_client3 = Sessions::Backend::Collections.new(customer1, nil, 'bbb-2')
+
+ # get whole collections
+ result1 = collection_client1.push
+ assert( result1, "check collections" )
+ assert( check_if_collection_exists(result1, :Group), "check collections - after init" )
+ assert( check_if_collection_exists(result1, :Role), "check collections - after init" )
+ assert( check_if_collection_exists(result1, :Signature), "check collections - after init" )
+ assert( check_if_collection_exists(result1, :EmailAddress), "check collections - after init" )
+ sleep 1
+ result2 = collection_client2.push
+ assert( result2, "check collections" )
+ assert( check_if_collection_exists(result2, :Group), "check collections - after init" )
+ assert( check_if_collection_exists(result2, :Role), "check collections - after init" )
+ assert( check_if_collection_exists(result2, :Signature), "check collections - after init" )
+ assert( check_if_collection_exists(result2, :EmailAddress), "check collections - after init" )
+ assert_equal( result1, result2, "check collections" )
+
+ result3 = collection_client3.push
+ assert( result3, "check collections" )
+ assert( check_if_collection_exists(result3, :Group), "check collections - after init" )
+ assert( check_if_collection_exists(result3, :Role), "check collections - after init" )
+ assert( !check_if_collection_exists(result3, :Signature), "check collections - after init" )
+ assert( !check_if_collection_exists(result3, :EmailAddress), "check collections - after init" )
+
+ # next check should be empty
+ result1 = collection_client1.push
+ assert( result1.empty?, "check collections - recall" )
+ sleep 1
+ result2 = collection_client2.push
+ assert( result2.empty?, "check collections - recall" )
+ sleep 0.2
+ result3 = collection_client3.push
+ assert( result3.empty?, "check collections - recall" )
+
+ # change collection
+ group = Group.first
+ group.touch
+ sleep 16
+
+ # get whole collections
+ result1 = collection_client1.push
+ assert( result1, "check collections - after touch" )
+ assert( check_if_collection_exists(result1, :Group), "check collections - after touch" )
+ sleep 1
+ result2 = collection_client2.push
+ assert( result2, "check collections - after touch" )
+ assert( check_if_collection_exists(result2, :Group), "check collections - after touch" )
+ sleep 0.2
+ result3 = collection_client3.push
+ assert( result3, "check collections - after touch" )
+ assert( check_if_collection_exists(result3, :Group), "check collections - after touch" )
+
+ # change collection
+ org = Organization.create( :name => 'SomeOrg::' + rand(999999).to_s, :active => true )
+ sleep 16
+
+ # get whole collections
+ result1 = collection_client1.push
+ assert( result1, "check collections - after create" )
+ assert( check_if_collection_exists(result1, :Organization), "check collections - after create" )
+ sleep 0.5
+ result2 = collection_client2.push
+ assert( result2, "check collections - after create" )
+ assert( check_if_collection_exists(result2, :Organization), "check collections - after create" )
+ sleep 0.5
+ result3 = collection_client3.push
+ assert( result3, "check collections - after create" )
+ assert( !check_if_collection_exists(result3, :Organization), "check collections - after create" )
+
+ # next check should be empty
+ sleep 16
+ result1 = collection_client1.push
+ assert( result1.empty?, "check collections - recall" )
+ sleep 1
+ result2 = collection_client2.push
+ assert( result2.empty?, "check collections - recall" )
+ sleep 0.2
+ result3 = collection_client3.push
+ assert( result3.empty?, "check collections - recall" )
+ end
+
+ def check_if_collection_exists(results, collection)
+ results.each {|result|
+ return true if result && result[:collection] && result[:collection][collection]
+ }
+ nil
+ end
+
+end
\ No newline at end of file
diff --git a/test/unit/session_test.rb b/test/unit/session_enhanced_test.rb
similarity index 60%
rename from test/unit/session_test.rb
rename to test/unit/session_enhanced_test.rb
index ebcbbc2c1..50297ecf1 100644
--- a/test/unit/session_test.rb
+++ b/test/unit/session_enhanced_test.rb
@@ -1,70 +1,8 @@
# encoding: utf-8
require 'test_helper'
-class SessionTest < ActiveSupport::TestCase
- test 'a cache' do
- Sessions::CacheIn.set( 'last_run_test' , true, { :expires_in => 2.seconds } )
- result = Sessions::CacheIn.get( 'last_run_test' )
- assert_equal( true, result, "check 1" )
-
- # should not be expired
- result = Sessions::CacheIn.expired( 'last_run_test' )
- assert_equal( false, result, "check 1 - expired" )
-
- # should be expired
- sleep 3
- result = Sessions::CacheIn.expired( 'last_run_test' )
- assert_equal( true, result, "check 1 - expired" )
-
- # renew expire
- result = Sessions::CacheIn.get( 'last_run_test', :re_expire => true )
- assert_equal( true, result, "check 1 - re_expire" )
-
- # should not be expired
- result = Sessions::CacheIn.expired( 'last_run_test' )
- assert_equal( false, result, "check 1 - expired" )
-
- # ignore expired
- sleep 3
- result = Sessions::CacheIn.get( 'last_run_test', :ignore_expire => true )
- assert_equal( true, result, "check 1 - ignore_expire" )
-
- # should be expired
- result = Sessions::CacheIn.expired( 'last_run_test' )
- assert_equal( true, result, "check 2" )
-
- result = Sessions::CacheIn.get( 'last_run_test' )
- assert_equal( nil, result, "check 2" )
- end
-
-
- test 'worker' do
- # create users
- roles = Role.where( :name => [ 'Agent'] )
- groups = Group.all
-
- UserInfo.current_user_id = 1
- agent1 = User.create_or_update(
- :login => 'session-agent-1',
- :firstname => 'Session',
- :lastname => 'Agent 1',
- :email => 'session-agent1@example.com',
- :password => 'agentpw',
- :active => true,
- :roles => roles,
- :groups => groups,
- )
-
- worker = Thread.new {
- Sessions.thread_worker(agent1.id)
- }
-
- #Sessions::Backend::TicketOverviewIndex.worker()
-
- worker.exit
- end
-
- test 'z full' do
+class SessionEnhancedTest < ActiveSupport::TestCase
+ test 'a check clients and send messages' do
# create users
roles = Role.where( :name => [ 'Agent'] )
@@ -81,6 +19,8 @@ class SessionTest < ActiveSupport::TestCase
:roles => roles,
:groups => groups,
)
+ agent1.roles = roles
+ agent1.save
agent2 = User.create_or_update(
:login => 'session-agent-2',
:firstname => 'Session',
@@ -91,6 +31,8 @@ class SessionTest < ActiveSupport::TestCase
:roles => roles,
:groups => groups,
)
+ agent2.roles = roles
+ agent2.save
agent3 = User.create_or_update(
:login => 'session-agent-3',
:firstname => 'Session',
@@ -101,6 +43,8 @@ class SessionTest < ActiveSupport::TestCase
:roles => roles,
:groups => groups,
)
+ agent3.roles = roles
+ agent3.save
# create sessions
client_id1 = '1234'
@@ -194,11 +138,6 @@ class SessionTest < ActiveSupport::TestCase
sleep 5
#jobs.join
- # check worker threads
- assert( Sessions.thread_worker_exists?(agent1), "check if worker is running" )
- assert( Sessions.thread_worker_exists?(agent2), "check if worker is running" )
- assert( Sessions.thread_worker_exists?(agent3), "check if worker is running" )
-
# check client threads
assert( Sessions.thread_client_exists?(client_id1), "check if client is running" )
assert( Sessions.thread_client_exists?(client_id2), "check if client is running" )
@@ -220,13 +159,146 @@ class SessionTest < ActiveSupport::TestCase
assert( !Sessions.thread_client_exists?(client_id2), "check if client is running" )
assert( !Sessions.thread_client_exists?(client_id3), "check if client is running" )
- # check worker threads
- assert( !Sessions.thread_worker_exists?(agent1), "check if worker is running" )
- assert( !Sessions.thread_worker_exists?(agent2), "check if worker is running" )
- assert( !Sessions.thread_worker_exists?(agent3), "check if worker is running" )
-
# exit jobs
jobs.exit
end
+
+ test 'b check client and backends' do
+ # create users
+ roles = Role.where( :name => [ 'Agent'] )
+ groups = Group.all
+
+ UserInfo.current_user_id = 1
+ agent1 = User.create_or_update(
+ :login => 'session-agent-1',
+ :firstname => 'Session',
+ :lastname => 'Agent 1',
+ :email => 'session-agent1@example.com',
+ :password => 'agentpw',
+ :active => true,
+ :roles => roles,
+ :groups => groups,
+ )
+ agent1.roles = roles
+ agent1.save
+ agent2 = User.create_or_update(
+ :login => 'session-agent-2',
+ :firstname => 'Session',
+ :lastname => 'Agent 2',
+ :email => 'session-agent2@example.com',
+ :password => 'agentpw',
+ :active => true,
+ :roles => roles,
+ :groups => groups,
+ )
+ agent2.roles = roles
+ agent2.save
+ org = Organization.create( :name => 'SomeOrg::' + rand(999999).to_s, :active => true )
+
+ # create sessions
+ client_id1_0 = '1234-1'
+ client_id1_1 = '1234-2'
+ client_id2 = '123456'
+ Sessions.destory(client_id1_0)
+ Sessions.destory(client_id1_1)
+ Sessions.destory(client_id2)
+
+ # start jobs
+ jobs = Thread.new {
+ Sessions.jobs
+ }
+ sleep 5
+ Sessions.create( client_id1_0, agent1.attributes, { :type => 'websocket' } )
+ sleep 5.5
+ Sessions.create( client_id1_1, agent1.attributes, { :type => 'websocket' } )
+ sleep 1.2
+ Sessions.create( client_id2, agent2.attributes, { :type => 'ajax' } )
+
+ # check if session exists
+ assert( Sessions.session_exists?(client_id1_0), "check if session exists" )
+ assert( Sessions.session_exists?(client_id1_1), "check if session exists" )
+ assert( Sessions.session_exists?(client_id2), "check if session exists" )
+ sleep 19
+
+ # check collections
+ collections = {
+ 'Group' => true,
+ 'Organization' => true,
+ 'User' => nil,
+ }
+ check_if_collection_reset_message_exists(client_id1_0, collections, 'init')
+ check_if_collection_reset_message_exists(client_id1_1, collections, 'init')
+ check_if_collection_reset_message_exists(client_id2, collections, 'init')
+
+ collections = {
+ 'Group' => nil,
+ 'Organization' => nil,
+ 'User' => nil,
+ }
+ check_if_collection_reset_message_exists(client_id1_0, collections, 'init2')
+ check_if_collection_reset_message_exists(client_id1_1, collections, 'init2')
+ check_if_collection_reset_message_exists(client_id2, collections, 'init2')
+
+ sleep 20
+
+ collections = {
+ 'Group' => nil,
+ 'Organization' => nil,
+ 'User' => nil,
+ }
+ check_if_collection_reset_message_exists(client_id1_0, collections, 'init3')
+ check_if_collection_reset_message_exists(client_id1_1, collections, 'init3')
+ check_if_collection_reset_message_exists(client_id2, collections, 'init3')
+
+ # change collection
+ group = Group.first
+ group.touch
+
+ sleep 20
+
+ # check collections
+ collections = {
+ 'Group' => true,
+ 'Organization' => nil,
+ 'User' => nil,
+ }
+ check_if_collection_reset_message_exists(client_id1_0, collections, 'update')
+ check_if_collection_reset_message_exists(client_id1_1, collections, 'update')
+ check_if_collection_reset_message_exists(client_id2, collections, 'update')
+
+ # check if session still exists after idle cleanup
+ sleep 62
+ client_ids = Sessions.destory_idle_sessions(1)
+
+ # check client sessions
+ assert( !Sessions.session_exists?(client_id1_0), "check if session is removed" )
+ assert( !Sessions.session_exists?(client_id1_1), "check if session is removed" )
+ assert( !Sessions.session_exists?(client_id2), "check if session is removed" )
+
+ end
+
+ def check_if_collection_reset_message_exists(client_id, collections_orig, type)
+ messages = Sessions.queue(client_id)
+ #puts "cid: #{client_id}"
+ #puts "m: #{messages.inspect}"
+ collections_result = {}
+ messages.each {|message|
+ #puts ""
+ #puts "message: #{message.inspect}"
+ if message['event'] == 'resetCollection'
+ #puts "rc: "
+ if message['data']
+ message['data'].each {|key, value|
+ #puts "rc: #{key}"
+ collections_result[key] = true
+ }
+ end
+ end
+ }
+ #puts "c: #{collections_result.inspect}"
+ collections_orig.each {|key, value|
+ assert_equal( collections_orig[key], collections_result[key], "collection message for #{key} #{type}-check (client_id #{client_id})" )
+ }
+ end
end