From 065859b90cc5ef403b8f47bd5394b343e556cc4d Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 23 Mar 2016 21:48:52 +0100 Subject: upgrade: remove references to RestClient CouchRest > 1.2 does not use RestClient anymore. So we should not try to catch its errors. --- app/controllers/application_controller.rb | 2 +- app/models/token.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 079dc18..2af2f29 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -5,7 +5,7 @@ class ApplicationController < ActionController::Base before_filter :no_frame_header before_filter :language_header rescue_from StandardError, :with => :default_error_handler - rescue_from RestClient::Exception, :with => :default_error_handler + rescue_from CouchRest::Exception, :with => :default_error_handler ActiveSupport.run_load_hooks(:application_controller, self) diff --git a/app/models/token.rb b/app/models/token.rb index b398fcb..8ac32b8 100644 --- a/app/models/token.rb +++ b/app/models/token.rb @@ -59,8 +59,8 @@ class Token < CouchRest::Model::Base # So let's make sure we don't crash if they disappeared def destroy_with_rescue destroy_without_rescue - rescue RestClient::ResourceNotFound # do nothing it's gone already - rescue RestClient::Conflict # do nothing - it's been updated - #7670 + rescue CouchRest::NotFound + rescue CouchRest::Conflict # do nothing - it's been updated - #7670 end alias_method_chain :destroy, :rescue -- cgit v1.2.3 From 431ceda256c91980a7c3ac807548eb7c776a09f0 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 24 Mar 2016 11:28:56 +0100 Subject: upgrade: bootstrap 3 does not have -responsive We can also remove the backports now. :) --- app/assets/stylesheets/application.scss | 3 --- app/assets/stylesheets/backport.scss | 24 ------------------------ 2 files changed, 27 deletions(-) delete mode 100644 app/assets/stylesheets/backport.scss (limited to 'app') diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 9cd3a55..e6eeda2 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -10,9 +10,6 @@ // import bootstrap. // @import "bootstrap"; -@import "bootstrap-responsive"; -// backport bootstrap 3.2 features -@import "backport"; // // LEAP web app specific overrides diff --git a/app/assets/stylesheets/backport.scss b/app/assets/stylesheets/backport.scss deleted file mode 100644 index cadb035..0000000 --- a/app/assets/stylesheets/backport.scss +++ /dev/null @@ -1,24 +0,0 @@ -// -// Backporting styles from bootstrap 3.2 -// - - -// List options - -// Unstyled keeps list items block level, just removes default browser padding and list-style -.list-unstyled { - padding-left: 0; - list-style: none; -} - -// Inline turns list items into inline-block -.list-inline { - @extend .list-unstyled; - margin-left: -5px; - - > li { - display: inline-block; - padding-left: 5px; - padding-right: 5px; - } -} -- cgit v1.2.3 From dbd36d90a25ac075b98d95804306f57a4e43d063 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 24 Mar 2016 14:21:21 +0100 Subject: update: stop refering to bootstrap2 ie7 hack --- app/assets/stylesheets/leap.scss | 1 - 1 file changed, 1 deletion(-) (limited to 'app') diff --git a/app/assets/stylesheets/leap.scss b/app/assets/stylesheets/leap.scss index abbfc88..19491c8 100644 --- a/app/assets/stylesheets/leap.scss +++ b/app/assets/stylesheets/leap.scss @@ -57,7 +57,6 @@ [class*="-icon-"] { display: inline-block; - @include ie7-restore-right-whitespace(); vertical-align: middle; background-repeat: no-repeat; margin-top: 1px; -- cgit v1.2.3 From 7689ff40b24786c808a36e60801ab60ede89a106 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 24 Mar 2016 14:52:16 +0100 Subject: upgrade: use bootstrap3 variable names in leap.scss --- app/assets/stylesheets/leap.scss | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) (limited to 'app') diff --git a/app/assets/stylesheets/leap.scss b/app/assets/stylesheets/leap.scss index 19491c8..329fc09 100644 --- a/app/assets/stylesheets/leap.scss +++ b/app/assets/stylesheets/leap.scss @@ -91,8 +91,8 @@ // input.large { - font-size: $baseFontSize * 1.25; - line-height: $baseLineHeight * 1.5; + font-size: $font-size-base * 1.25; + line-height: $line-height-base * 1.5; } .p { @@ -119,9 +119,9 @@ input, textarea { // like a label, but with no background .label-clear { - background-color: $white; + background-color: $body-bg; text-shadow: none; - color: $black; + color: $gray-base; } // force a black icon, even if bootstrap thinks differently @@ -187,7 +187,7 @@ input, textarea { font-weight: bold; } a { - color: $textColor; + color: $text-color; } } @@ -227,9 +227,9 @@ input, textarea { box-shadow: 0 2px 4px rgba(0,0,0,.1); li.active { a, a:hover { - background-color: $linkColor; - color: $white; - border-color: darken($linkColor, 0%); + background-color: $link-color; + color: $body-bg; + border-color: darken($link-color, 0%); cursor: pointer; } } @@ -264,7 +264,7 @@ $footer-height: 100px; $footer-border-width: 1px; $footer-gutter: 20px; // vertical gap above footer $footer-combined: $footer-height + $footer-border-width + $footer-gutter; -$footer-color: $grayLighter !default; +$footer-color: $gray-lighter !default; html, body { height: 100%; @@ -312,7 +312,7 @@ html, body { // border-top: $footer-border-width solid darken($footer-color, 10%); background-color: $footer-color; a { - color: $black; + color: $gray-base; margin: 0 5px; } } -- cgit v1.2.3 From b718bea325793430d06d94570dd848fc71a57387 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 24 Mar 2016 21:45:14 +0100 Subject: upgrade: typeahead is not in bootstrap3 anymore using twitter/typeahead instead which seems to have similar properties --- app/assets/javascripts/application.js | 1 + app/assets/javascripts/typeahead.bundle.js | 2451 ++++++++++++++++++++++++++++ 2 files changed, 2452 insertions(+) create mode 100644 app/assets/javascripts/typeahead.bundle.js (limited to 'app') diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 9af373d..7888161 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -16,6 +16,7 @@ //= require bootstrap //= require rails.validations //= require rails.validations.simple_form +//= require typeahead.bundle //= require leap //= require platform //= require tickets diff --git a/app/assets/javascripts/typeahead.bundle.js b/app/assets/javascripts/typeahead.bundle.js new file mode 100644 index 0000000..bb0c8ae --- /dev/null +++ b/app/assets/javascripts/typeahead.bundle.js @@ -0,0 +1,2451 @@ +/*! + * typeahead.js 0.11.1 + * https://github.com/twitter/typeahead.js + * Copyright 2013-2015 Twitter, Inc. and other contributors; Licensed MIT + */ + +(function(root, factory) { + if (typeof define === "function" && define.amd) { + define("bloodhound", [ "jquery" ], function(a0) { + return root["Bloodhound"] = factory(a0); + }); + } else if (typeof exports === "object") { + module.exports = factory(require("jquery")); + } else { + root["Bloodhound"] = factory(jQuery); + } +})(this, function($) { + var _ = function() { + "use strict"; + return { + isMsie: function() { + return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false; + }, + isBlankString: function(str) { + return !str || /^\s*$/.test(str); + }, + escapeRegExChars: function(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + }, + isString: function(obj) { + return typeof obj === "string"; + }, + isNumber: function(obj) { + return typeof obj === "number"; + }, + isArray: $.isArray, + isFunction: $.isFunction, + isObject: $.isPlainObject, + isUndefined: function(obj) { + return typeof obj === "undefined"; + }, + isElement: function(obj) { + return !!(obj && obj.nodeType === 1); + }, + isJQuery: function(obj) { + return obj instanceof $; + }, + toStr: function toStr(s) { + return _.isUndefined(s) || s === null ? "" : s + ""; + }, + bind: $.proxy, + each: function(collection, cb) { + $.each(collection, reverseArgs); + function reverseArgs(index, value) { + return cb(value, index); + } + }, + map: $.map, + filter: $.grep, + every: function(obj, test) { + var result = true; + if (!obj) { + return result; + } + $.each(obj, function(key, val) { + if (!(result = test.call(null, val, key, obj))) { + return false; + } + }); + return !!result; + }, + some: function(obj, test) { + var result = false; + if (!obj) { + return result; + } + $.each(obj, function(key, val) { + if (result = test.call(null, val, key, obj)) { + return false; + } + }); + return !!result; + }, + mixin: $.extend, + identity: function(x) { + return x; + }, + clone: function(obj) { + return $.extend(true, {}, obj); + }, + getIdGenerator: function() { + var counter = 0; + return function() { + return counter++; + }; + }, + templatify: function templatify(obj) { + return $.isFunction(obj) ? obj : template; + function template() { + return String(obj); + } + }, + defer: function(fn) { + setTimeout(fn, 0); + }, + debounce: function(func, wait, immediate) { + var timeout, result; + return function() { + var context = this, args = arguments, later, callNow; + later = function() { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + } + }; + callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) { + result = func.apply(context, args); + } + return result; + }; + }, + throttle: function(func, wait) { + var context, args, timeout, result, previous, later; + previous = 0; + later = function() { + previous = new Date(); + timeout = null; + result = func.apply(context, args); + }; + return function() { + var now = new Date(), remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + } else if (!timeout) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }, + stringify: function(val) { + return _.isString(val) ? val : JSON.stringify(val); + }, + noop: function() {} + }; + }(); + var VERSION = "0.11.1"; + var tokenizers = function() { + "use strict"; + return { + nonword: nonword, + whitespace: whitespace, + obj: { + nonword: getObjTokenizer(nonword), + whitespace: getObjTokenizer(whitespace) + } + }; + function whitespace(str) { + str = _.toStr(str); + return str ? str.split(/\s+/) : []; + } + function nonword(str) { + str = _.toStr(str); + return str ? str.split(/\W+/) : []; + } + function getObjTokenizer(tokenizer) { + return function setKey(keys) { + keys = _.isArray(keys) ? keys : [].slice.call(arguments, 0); + return function tokenize(o) { + var tokens = []; + _.each(keys, function(k) { + tokens = tokens.concat(tokenizer(_.toStr(o[k]))); + }); + return tokens; + }; + }; + } + }(); + var LruCache = function() { + "use strict"; + function LruCache(maxSize) { + this.maxSize = _.isNumber(maxSize) ? maxSize : 100; + this.reset(); + if (this.maxSize <= 0) { + this.set = this.get = $.noop; + } + } + _.mixin(LruCache.prototype, { + set: function set(key, val) { + var tailItem = this.list.tail, node; + if (this.size >= this.maxSize) { + this.list.remove(tailItem); + delete this.hash[tailItem.key]; + this.size--; + } + if (node = this.hash[key]) { + node.val = val; + this.list.moveToFront(node); + } else { + node = new Node(key, val); + this.list.add(node); + this.hash[key] = node; + this.size++; + } + }, + get: function get(key) { + var node = this.hash[key]; + if (node) { + this.list.moveToFront(node); + return node.val; + } + }, + reset: function reset() { + this.size = 0; + this.hash = {}; + this.list = new List(); + } + }); + function List() { + this.head = this.tail = null; + } + _.mixin(List.prototype, { + add: function add(node) { + if (this.head) { + node.next = this.head; + this.head.prev = node; + } + this.head = node; + this.tail = this.tail || node; + }, + remove: function remove(node) { + node.prev ? node.prev.next = node.next : this.head = node.next; + node.next ? node.next.prev = node.prev : this.tail = node.prev; + }, + moveToFront: function(node) { + this.remove(node); + this.add(node); + } + }); + function Node(key, val) { + this.key = key; + this.val = val; + this.prev = this.next = null; + } + return LruCache; + }(); + var PersistentStorage = function() { + "use strict"; + var LOCAL_STORAGE; + try { + LOCAL_STORAGE = window.localStorage; + LOCAL_STORAGE.setItem("~~~", "!"); + LOCAL_STORAGE.removeItem("~~~"); + } catch (err) { + LOCAL_STORAGE = null; + } + function PersistentStorage(namespace, override) { + this.prefix = [ "__", namespace, "__" ].join(""); + this.ttlKey = "__ttl__"; + this.keyMatcher = new RegExp("^" + _.escapeRegExChars(this.prefix)); + this.ls = override || LOCAL_STORAGE; + !this.ls && this._noop(); + } + _.mixin(PersistentStorage.prototype, { + _prefix: function(key) { + return this.prefix + key; + }, + _ttlKey: function(key) { + return this._prefix(key) + this.ttlKey; + }, + _noop: function() { + this.get = this.set = this.remove = this.clear = this.isExpired = _.noop; + }, + _safeSet: function(key, val) { + try { + this.ls.setItem(key, val); + } catch (err) { + if (err.name === "QuotaExceededError") { + this.clear(); + this._noop(); + } + } + }, + get: function(key) { + if (this.isExpired(key)) { + this.remove(key); + } + return decode(this.ls.getItem(this._prefix(key))); + }, + set: function(key, val, ttl) { + if (_.isNumber(ttl)) { + this._safeSet(this._ttlKey(key), encode(now() + ttl)); + } else { + this.ls.removeItem(this._ttlKey(key)); + } + return this._safeSet(this._prefix(key), encode(val)); + }, + remove: function(key) { + this.ls.removeItem(this._ttlKey(key)); + this.ls.removeItem(this._prefix(key)); + return this; + }, + clear: function() { + var i, keys = gatherMatchingKeys(this.keyMatcher); + for (i = keys.length; i--; ) { + this.remove(keys[i]); + } + return this; + }, + isExpired: function(key) { + var ttl = decode(this.ls.getItem(this._ttlKey(key))); + return _.isNumber(ttl) && now() > ttl ? true : false; + } + }); + return PersistentStorage; + function now() { + return new Date().getTime(); + } + function encode(val) { + return JSON.stringify(_.isUndefined(val) ? null : val); + } + function decode(val) { + return $.parseJSON(val); + } + function gatherMatchingKeys(keyMatcher) { + var i, key, keys = [], len = LOCAL_STORAGE.length; + for (i = 0; i < len; i++) { + if ((key = LOCAL_STORAGE.key(i)).match(keyMatcher)) { + keys.push(key.replace(keyMatcher, "")); + } + } + return keys; + } + }(); + var Transport = function() { + "use strict"; + var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, sharedCache = new LruCache(10); + function Transport(o) { + o = o || {}; + this.cancelled = false; + this.lastReq = null; + this._send = o.transport; + this._get = o.limiter ? o.limiter(this._get) : this._get; + this._cache = o.cache === false ? new LruCache(0) : sharedCache; + } + Transport.setMaxPendingRequests = function setMaxPendingRequests(num) { + maxPendingRequests = num; + }; + Transport.resetCache = function resetCache() { + sharedCache.reset(); + }; + _.mixin(Transport.prototype, { + _fingerprint: function fingerprint(o) { + o = o || {}; + return o.url + o.type + $.param(o.data || {}); + }, + _get: function(o, cb) { + var that = this, fingerprint, jqXhr; + fingerprint = this._fingerprint(o); + if (this.cancelled || fingerprint !== this.lastReq) { + return; + } + if (jqXhr = pendingRequests[fingerprint]) { + jqXhr.done(done).fail(fail); + } else if (pendingRequestsCount < maxPendingRequests) { + pendingRequestsCount++; + pendingRequests[fingerprint] = this._send(o).done(done).fail(fail).always(always); + } else { + this.onDeckRequestArgs = [].slice.call(arguments, 0); + } + function done(resp) { + cb(null, resp); + that._cache.set(fingerprint, resp); + } + function fail() { + cb(true); + } + function always() { + pendingRequestsCount--; + delete pendingRequests[fingerprint]; + if (that.onDeckRequestArgs) { + that._get.apply(that, that.onDeckRequestArgs); + that.onDeckRequestArgs = null; + } + } + }, + get: function(o, cb) { + var resp, fingerprint; + cb = cb || $.noop; + o = _.isString(o) ? { + url: o + } : o || {}; + fingerprint = this._fingerprint(o); + this.cancelled = false; + this.lastReq = fingerprint; + if (resp = this._cache.get(fingerprint)) { + cb(null, resp); + } else { + this._get(o, cb); + } + }, + cancel: function() { + this.cancelled = true; + } + }); + return Transport; + }(); + var SearchIndex = window.SearchIndex = function() { + "use strict"; + var CHILDREN = "c", IDS = "i"; + function SearchIndex(o) { + o = o || {}; + if (!o.datumTokenizer || !o.queryTokenizer) { + $.error("datumTokenizer and queryTokenizer are both required"); + } + this.identify = o.identify || _.stringify; + this.datumTokenizer = o.datumTokenizer; + this.queryTokenizer = o.queryTokenizer; + this.reset(); + } + _.mixin(SearchIndex.prototype, { + bootstrap: function bootstrap(o) { + this.datums = o.datums; + this.trie = o.trie; + }, + add: function(data) { + var that = this; + data = _.isArray(data) ? data : [ data ]; + _.each(data, function(datum) { + var id, tokens; + that.datums[id = that.identify(datum)] = datum; + tokens = normalizeTokens(that.datumTokenizer(datum)); + _.each(tokens, function(token) { + var node, chars, ch; + node = that.trie; + chars = token.split(""); + while (ch = chars.shift()) { + node = node[CHILDREN][ch] || (node[CHILDREN][ch] = newNode()); + node[IDS].push(id); + } + }); + }); + }, + get: function get(ids) { + var that = this; + return _.map(ids, function(id) { + return that.datums[id]; + }); + }, + search: function search(query) { + var that = this, tokens, matches; + tokens = normalizeTokens(this.queryTokenizer(query)); + _.each(tokens, function(token) { + var node, chars, ch, ids; + if (matches && matches.length === 0) { + return false; + } + node = that.trie; + chars = token.split(""); + while (node && (ch = chars.shift())) { + node = node[CHILDREN][ch]; + } + if (node && chars.length === 0) { + ids = node[IDS].slice(0); + matches = matches ? getIntersection(matches, ids) : ids; + } else { + matches = []; + return false; + } + }); + return matches ? _.map(unique(matches), function(id) { + return that.datums[id]; + }) : []; + }, + all: function all() { + var values = []; + for (var key in this.datums) { + values.push(this.datums[key]); + } + return values; + }, + reset: function reset() { + this.datums = {}; + this.trie = newNode(); + }, + serialize: function serialize() { + return { + datums: this.datums, + trie: this.trie + }; + } + }); + return SearchIndex; + function normalizeTokens(tokens) { + tokens = _.filter(tokens, function(token) { + return !!token; + }); + tokens = _.map(tokens, function(token) { + return token.toLowerCase(); + }); + return tokens; + } + function newNode() { + var node = {}; + node[IDS] = []; + node[CHILDREN] = {}; + return node; + } + function unique(array) { + var seen = {}, uniques = []; + for (var i = 0, len = array.length; i < len; i++) { + if (!seen[array[i]]) { + seen[array[i]] = true; + uniques.push(array[i]); + } + } + return uniques; + } + function getIntersection(arrayA, arrayB) { + var ai = 0, bi = 0, intersection = []; + arrayA = arrayA.sort(); + arrayB = arrayB.sort(); + var lenArrayA = arrayA.length, lenArrayB = arrayB.length; + while (ai < lenArrayA && bi < lenArrayB) { + if (arrayA[ai] < arrayB[bi]) { + ai++; + } else if (arrayA[ai] > arrayB[bi]) { + bi++; + } else { + intersection.push(arrayA[ai]); + ai++; + bi++; + } + } + return intersection; + } + }(); + var Prefetch = function() { + "use strict"; + var keys; + keys = { + data: "data", + protocol: "protocol", + thumbprint: "thumbprint" + }; + function Prefetch(o) { + this.url = o.url; + this.ttl = o.ttl; + this.cache = o.cache; + this.prepare = o.prepare; + this.transform = o.transform; + this.transport = o.transport; + this.thumbprint = o.thumbprint; + this.storage = new PersistentStorage(o.cacheKey); + } + _.mixin(Prefetch.prototype, { + _settings: function settings() { + return { + url: this.url, + type: "GET", + dataType: "json" + }; + }, + store: function store(data) { + if (!this.cache) { + return; + } + this.storage.set(keys.data, data, this.ttl); + this.storage.set(keys.protocol, location.protocol, this.ttl); + this.storage.set(keys.thumbprint, this.thumbprint, this.ttl); + }, + fromCache: function fromCache() { + var stored = {}, isExpired; + if (!this.cache) { + return null; + } + stored.data = this.storage.get(keys.data); + stored.protocol = this.storage.get(keys.protocol); + stored.thumbprint = this.storage.get(keys.thumbprint); + isExpired = stored.thumbprint !== this.thumbprint || stored.protocol !== location.protocol; + return stored.data && !isExpired ? stored.data : null; + }, + fromNetwork: function(cb) { + var that = this, settings; + if (!cb) { + return; + } + settings = this.prepare(this._settings()); + this.transport(settings).fail(onError).done(onResponse); + function onError() { + cb(true); + } + function onResponse(resp) { + cb(null, that.transform(resp)); + } + }, + clear: function clear() { + this.storage.clear(); + return this; + } + }); + return Prefetch; + }(); + var Remote = function() { + "use strict"; + function Remote(o) { + this.url = o.url; + this.prepare = o.prepare; + this.transform = o.transform; + this.transport = new Transport({ + cache: o.cache, + limiter: o.limiter, + transport: o.transport + }); + } + _.mixin(Remote.prototype, { + _settings: function settings() { + return { + url: this.url, + type: "GET", + dataType: "json" + }; + }, + get: function get(query, cb) { + var that = this, settings; + if (!cb) { + return; + } + query = query || ""; + settings = this.prepare(query, this._settings()); + return this.transport.get(settings, onResponse); + function onResponse(err, resp) { + err ? cb([]) : cb(that.transform(resp)); + } + }, + cancelLastRequest: function cancelLastRequest() { + this.transport.cancel(); + } + }); + return Remote; + }(); + var oParser = function() { + "use strict"; + return function parse(o) { + var defaults, sorter; + defaults = { + initialize: true, + identify: _.stringify, + datumTokenizer: null, + queryTokenizer: null, + sufficient: 5, + sorter: null, + local: [], + prefetch: null, + remote: null + }; + o = _.mixin(defaults, o || {}); + !o.datumTokenizer && $.error("datumTokenizer is required"); + !o.queryTokenizer && $.error("queryTokenizer is required"); + sorter = o.sorter; + o.sorter = sorter ? function(x) { + return x.sort(sorter); + } : _.identity; + o.local = _.isFunction(o.local) ? o.local() : o.local; + o.prefetch = parsePrefetch(o.prefetch); + o.remote = parseRemote(o.remote); + return o; + }; + function parsePrefetch(o) { + var defaults; + if (!o) { + return null; + } + defaults = { + url: null, + ttl: 24 * 60 * 60 * 1e3, + cache: true, + cacheKey: null, + thumbprint: "", + prepare: _.identity, + transform: _.identity, + transport: null + }; + o = _.isString(o) ? { + url: o + } : o; + o = _.mixin(defaults, o); + !o.url && $.error("prefetch requires url to be set"); + o.transform = o.filter || o.transform; + o.cacheKey = o.cacheKey || o.url; + o.thumbprint = VERSION + o.thumbprint; + o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax; + return o; + } + function parseRemote(o) { + var defaults; + if (!o) { + return; + } + defaults = { + url: null, + cache: true, + prepare: null, + replace: null, + wildcard: null, + limiter: null, + rateLimitBy: "debounce", + rateLimitWait: 300, + transform: _.identity, + transport: null + }; + o = _.isString(o) ? { + url: o + } : o; + o = _.mixin(defaults, o); + !o.url && $.error("remote requires url to be set"); + o.transform = o.filter || o.transform; + o.prepare = toRemotePrepare(o); + o.limiter = toLimiter(o); + o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax; + delete o.replace; + delete o.wildcard; + delete o.rateLimitBy; + delete o.rateLimitWait; + return o; + } + function toRemotePrepare(o) { + var prepare, replace, wildcard; + prepare = o.prepare; + replace = o.replace; + wildcard = o.wildcard; + if (prepare) { + return prepare; + } + if (replace) { + prepare = prepareByReplace; + } else if (o.wildcard) { + prepare = prepareByWildcard; + } else { + prepare = idenityPrepare; + } + return prepare; + function prepareByReplace(query, settings) { + settings.url = replace(settings.url, query); + return settings; + } + function prepareByWildcard(query, settings) { + settings.url = settings.url.replace(wildcard, encodeURIComponent(query)); + return settings; + } + function idenityPrepare(query, settings) { + return settings; + } + } + function toLimiter(o) { + var limiter, method, wait; + limiter = o.limiter; + method = o.rateLimitBy; + wait = o.rateLimitWait; + if (!limiter) { + limiter = /^throttle$/i.test(method) ? throttle(wait) : debounce(wait); + } + return limiter; + function debounce(wait) { + return function debounce(fn) { + return _.debounce(fn, wait); + }; + } + function throttle(wait) { + return function throttle(fn) { + return _.throttle(fn, wait); + }; + } + } + function callbackToDeferred(fn) { + return function wrapper(o) { + var deferred = $.Deferred(); + fn(o, onSuccess, onError); + return deferred; + function onSuccess(resp) { + _.defer(function() { + deferred.resolve(resp); + }); + } + function onError(err) { + _.defer(function() { + deferred.reject(err); + }); + } + }; + } + }(); + var Bloodhound = function() { + "use strict"; + var old; + old = window && window.Bloodhound; + function Bloodhound(o) { + o = oParser(o); + this.sorter = o.sorter; + this.identify = o.identify; + this.sufficient = o.sufficient; + this.local = o.local; + this.remote = o.remote ? new Remote(o.remote) : null; + this.prefetch = o.prefetch ? new Prefetch(o.prefetch) : null; + this.index = new SearchIndex({ + identify: this.identify, + datumTokenizer: o.datumTokenizer, + queryTokenizer: o.queryTokenizer + }); + o.initialize !== false && this.initialize(); + } + Bloodhound.noConflict = function noConflict() { + window && (window.Bloodhound = old); + return Bloodhound; + }; + Bloodhound.tokenizers = tokenizers; + _.mixin(Bloodhound.prototype, { + __ttAdapter: function ttAdapter() { + var that = this; + return this.remote ? withAsync : withoutAsync; + function withAsync(query, sync, async) { + return that.search(query, sync, async); + } + function withoutAsync(query, sync) { + return that.search(query, sync); + } + }, + _loadPrefetch: function loadPrefetch() { + var that = this, deferred, serialized; + deferred = $.Deferred(); + if (!this.prefetch) { + deferred.resolve(); + } else if (serialized = this.prefetch.fromCache()) { + this.index.bootstrap(serialized); + deferred.resolve(); + } else { + this.prefetch.fromNetwork(done); + } + return deferred.promise(); + function done(err, data) { + if (err) { + return deferred.reject(); + } + that.add(data); + that.prefetch.store(that.index.serialize()); + deferred.resolve(); + } + }, + _initialize: function initialize() { + var that = this, deferred; + this.clear(); + (this.initPromise = this._loadPrefetch()).done(addLocalToIndex); + return this.initPromise; + function addLocalToIndex() { + that.add(that.local); + } + }, + initialize: function initialize(force) { + return !this.initPromise || force ? this._initialize() : this.initPromise; + }, + add: function add(data) { + this.index.add(data); + return this; + }, + get: function get(ids) { + ids = _.isArray(ids) ? ids : [].slice.call(arguments); + return this.index.get(ids); + }, + search: function search(query, sync, async) { + var that = this, local; + local = this.sorter(this.index.search(query)); + sync(this.remote ? local.slice() : local); + if (this.remote && local.length < this.sufficient) { + this.remote.get(query, processRemote); + } else if (this.remote) { + this.remote.cancelLastRequest(); + } + return this; + function processRemote(remote) { + var nonDuplicates = []; + _.each(remote, function(r) { + !_.some(local, function(l) { + return that.identify(r) === that.identify(l); + }) && nonDuplicates.push(r); + }); + async && async(nonDuplicates); + } + }, + all: function all() { + return this.index.all(); + }, + clear: function clear() { + this.index.reset(); + return this; + }, + clearPrefetchCache: function clearPrefetchCache() { + this.prefetch && this.prefetch.clear(); + return this; + }, + clearRemoteCache: function clearRemoteCache() { + Transport.resetCache(); + return this; + }, + ttAdapter: function ttAdapter() { + return this.__ttAdapter(); + } + }); + return Bloodhound; + }(); + return Bloodhound; +}); + +(function(root, factory) { + if (typeof define === "function" && define.amd) { + define("typeahead.js", [ "jquery" ], function(a0) { + return factory(a0); + }); + } else if (typeof exports === "object") { + module.exports = factory(require("jquery")); + } else { + factory(jQuery); + } +})(this, function($) { + var _ = function() { + "use strict"; + return { + isMsie: function() { + return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false; + }, + isBlankString: function(str) { + return !str || /^\s*$/.test(str); + }, + escapeRegExChars: function(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + }, + isString: function(obj) { + return typeof obj === "string"; + }, + isNumber: function(obj) { + return typeof obj === "number"; + }, + isArray: $.isArray, + isFunction: $.isFunction, + isObject: $.isPlainObject, + isUndefined: function(obj) { + return typeof obj === "undefined"; + }, + isElement: function(obj) { + return !!(obj && obj.nodeType === 1); + }, + isJQuery: function(obj) { + return obj instanceof $; + }, + toStr: function toStr(s) { + return _.isUndefined(s) || s === null ? "" : s + ""; + }, + bind: $.proxy, + each: function(collection, cb) { + $.each(collection, reverseArgs); + function reverseArgs(index, value) { + return cb(value, index); + } + }, + map: $.map, + filter: $.grep, + every: function(obj, test) { + var result = true; + if (!obj) { + return result; + } + $.each(obj, function(key, val) { + if (!(result = test.call(null, val, key, obj))) { + return false; + } + }); + return !!result; + }, + some: function(obj, test) { + var result = false; + if (!obj) { + return result; + } + $.each(obj, function(key, val) { + if (result = test.call(null, val, key, obj)) { + return false; + } + }); + return !!result; + }, + mixin: $.extend, + identity: function(x) { + return x; + }, + clone: function(obj) { + return $.extend(true, {}, obj); + }, + getIdGenerator: function() { + var counter = 0; + return function() { + return counter++; + }; + }, + templatify: function templatify(obj) { + return $.isFunction(obj) ? obj : template; + function template() { + return String(obj); + } + }, + defer: function(fn) { + setTimeout(fn, 0); + }, + debounce: function(func, wait, immediate) { + var timeout, result; + return function() { + var context = this, args = arguments, later, callNow; + later = function() { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + } + }; + callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) { + result = func.apply(context, args); + } + return result; + }; + }, + throttle: function(func, wait) { + var context, args, timeout, result, previous, later; + previous = 0; + later = function() { + previous = new Date(); + timeout = null; + result = func.apply(context, args); + }; + return function() { + var now = new Date(), remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + } else if (!timeout) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }, + stringify: function(val) { + return _.isString(val) ? val : JSON.stringify(val); + }, + noop: function() {} + }; + }(); + var WWW = function() { + "use strict"; + var defaultClassNames = { + wrapper: "twitter-typeahead", + input: "tt-input", + hint: "tt-hint", + menu: "tt-menu", + dataset: "tt-dataset", + suggestion: "tt-suggestion", + selectable: "tt-selectable", + empty: "tt-empty", + open: "tt-open", + cursor: "tt-cursor", + highlight: "tt-highlight" + }; + return build; + function build(o) { + var www, classes; + classes = _.mixin({}, defaultClassNames, o); + www = { + css: buildCss(), + classes: classes, + html: buildHtml(classes), + selectors: buildSelectors(classes) + }; + return { + css: www.css, + html: www.html, + classes: www.classes, + selectors: www.selectors, + mixin: function(o) { + _.mixin(o, www); + } + }; + } + function buildHtml(c) { + return { + wrapper: '', + menu: '
' + }; + } + function buildSelectors(classes) { + var selectors = {}; + _.each(classes, function(v, k) { + selectors[k] = "." + v; + }); + return selectors; + } + function buildCss() { + var css = { + wrapper: { + position: "relative", + display: "inline-block" + }, + hint: { + position: "absolute", + top: "0", + left: "0", + borderColor: "transparent", + boxShadow: "none", + opacity: "1" + }, + input: { + position: "relative", + verticalAlign: "top", + backgroundColor: "transparent" + }, + inputWithNoHint: { + position: "relative", + verticalAlign: "top" + }, + menu: { + position: "absolute", + top: "100%", + left: "0", + zIndex: "100", + display: "none" + }, + ltr: { + left: "0", + right: "auto" + }, + rtl: { + left: "auto", + right: " 0" + } + }; + if (_.isMsie()) { + _.mixin(css.input, { + backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)" + }); + } + return css; + } + }(); + var EventBus = function() { + "use strict"; + var namespace, deprecationMap; + namespace = "typeahead:"; + deprecationMap = { + render: "rendered", + cursorchange: "cursorchanged", + select: "selected", + autocomplete: "autocompleted" + }; + function EventBus(o) { + if (!o || !o.el) { + $.error("EventBus initialized without el"); + } + this.$el = $(o.el); + } + _.mixin(EventBus.prototype, { + _trigger: function(type, args) { + var $e; + $e = $.Event(namespace + type); + (args = args || []).unshift($e); + this.$el.trigger.apply(this.$el, args); + return $e; + }, + before: function(type) { + var args, $e; + args = [].slice.call(arguments, 1); + $e = this._trigger("before" + type, args); + return $e.isDefaultPrevented(); + }, + trigger: function(type) { + var deprecatedType; + this._trigger(type, [].slice.call(arguments, 1)); + if (deprecatedType = deprecationMap[type]) { + this._trigger(deprecatedType, [].slice.call(arguments, 1)); + } + } + }); + return EventBus; + }(); + var EventEmitter = function() { + "use strict"; + var splitter = /\s+/, nextTick = getNextTick(); + return { + onSync: onSync, + onAsync: onAsync, + off: off, + trigger: trigger + }; + function on(method, types, cb, context) { + var type; + if (!cb) { + return this; + } + types = types.split(splitter); + cb = context ? bindContext(cb, context) : cb; + this._callbacks = this._callbacks || {}; + while (type = types.shift()) { + this._callbacks[type] = this._callbacks[type] || { + sync: [], + async: [] + }; + this._callbacks[type][method].push(cb); + } + return this; + } + function onAsync(types, cb, context) { + return on.call(this, "async", types, cb, context); + } + function onSync(types, cb, context) { + return on.call(this, "sync", types, cb, context); + } + function off(types) { + var type; + if (!this._callbacks) { + return this; + } + types = types.split(splitter); + while (type = types.shift()) { + delete this._callbacks[type]; + } + return this; + } + function trigger(types) { + var type, callbacks, args, syncFlush, asyncFlush; + if (!this._callbacks) { + return this; + } + types = types.split(splitter); + args = [].slice.call(arguments, 1); + while ((type = types.shift()) && (callbacks = this._callbacks[type])) { + syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args)); + asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args)); + syncFlush() && nextTick(asyncFlush); + } + return this; + } + function getFlush(callbacks, context, args) { + return flush; + function flush() { + var cancelled; + for (var i = 0, len = callbacks.length; !cancelled && i < len; i += 1) { + cancelled = callbacks[i].apply(context, args) === false; + } + return !cancelled; + } + } + function getNextTick() { + var nextTickFn; + if (window.setImmediate) { + nextTickFn = function nextTickSetImmediate(fn) { + setImmediate(function() { + fn(); + }); + }; + } else { + nextTickFn = function nextTickSetTimeout(fn) { + setTimeout(function() { + fn(); + }, 0); + }; + } + return nextTickFn; + } + function bindContext(fn, context) { + return fn.bind ? fn.bind(context) : function() { + fn.apply(context, [].slice.call(arguments, 0)); + }; + } + }(); + var highlight = function(doc) { + "use strict"; + var defaults = { + node: null, + pattern: null, + tagName: "strong", + className: null, + wordsOnly: false, + caseSensitive: false + }; + return function hightlight(o) { + var regex; + o = _.mixin({}, defaults, o); + if (!o.node || !o.pattern) { + return; + } + o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ]; + regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly); + traverse(o.node, hightlightTextNode); + function hightlightTextNode(textNode) { + var match, patternNode, wrapperNode; + if (match = regex.exec(textNode.data)) { + wrapperNode = doc.createElement(o.tagName); + o.className && (wrapperNode.className = o.className); + patternNode = textNode.splitText(match.index); + patternNode.splitText(match[0].length); + wrapperNode.appendChild(patternNode.cloneNode(true)); + textNode.parentNode.replaceChild(wrapperNode, patternNode); + } + return !!match; + } + function traverse(el, hightlightTextNode) { + var childNode, TEXT_NODE_TYPE = 3; + for (var i = 0; i < el.childNodes.length; i++) { + childNode = el.childNodes[i]; + if (childNode.nodeType === TEXT_NODE_TYPE) { + i += hightlightTextNode(childNode) ? 1 : 0; + } else { + traverse(childNode, hightlightTextNode); + } + } + } + }; + function getRegex(patterns, caseSensitive, wordsOnly) { + var escapedPatterns = [], regexStr; + for (var i = 0, len = patterns.length; i < len; i++) { + escapedPatterns.push(_.escapeRegExChars(patterns[i])); + } + regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")"; + return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i"); + } + }(window.document); + var Input = function() { + "use strict"; + var specialKeyCodeMap; + specialKeyCodeMap = { + 9: "tab", + 27: "esc", + 37: "left", + 39: "right", + 13: "enter", + 38: "up", + 40: "down" + }; + function Input(o, www) { + o = o || {}; + if (!o.input) { + $.error("input is missing"); + } + www.mixin(this); + this.$hint = $(o.hint); + this.$input = $(o.input); + this.query = this.$input.val(); + this.queryWhenFocused = this.hasFocus() ? this.query : null; + this.$overflowHelper = buildOverflowHelper(this.$input); + this._checkLanguageDirection(); + if (this.$hint.length === 0) { + this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop; + } + } + Input.normalizeQuery = function(str) { + return _.toStr(str).replace(/^\s*/g, "").replace(/\s{2,}/g, " "); + }; + _.mixin(Input.prototype, EventEmitter, { + _onBlur: function onBlur() { + this.resetInputValue(); + this.trigger("blurred"); + }, + _onFocus: function onFocus() { + this.queryWhenFocused = this.query; + this.trigger("focused"); + }, + _onKeydown: function onKeydown($e) { + var keyName = specialKeyCodeMap[$e.which || $e.keyCode]; + this._managePreventDefault(keyName, $e); + if (keyName && this._shouldTrigger(keyName, $e)) { + this.trigger(keyName + "Keyed", $e); + } + }, + _onInput: function onInput() { + this._setQuery(this.getInputValue()); + this.clearHintIfInvalid(); + this._checkLanguageDirection(); + }, + _managePreventDefault: function managePreventDefault(keyName, $e) { + var preventDefault; + switch (keyName) { + case "up": + case "down": + preventDefault = !withModifier($e); + break; + + default: + preventDefault = false; + } + preventDefault && $e.preventDefault(); + }, + _shouldTrigger: function shouldTrigger(keyName, $e) { + var trigger; + switch (keyName) { + case "tab": + trigger = !withModifier($e); + break; + + default: + trigger = true; + } + return trigger; + }, + _checkLanguageDirection: function checkLanguageDirection() { + var dir = (this.$input.css("direction") || "ltr").toLowerCase(); + if (this.dir !== dir) { + this.dir = dir; + this.$hint.attr("dir", dir); + this.trigger("langDirChanged", dir); + } + }, + _setQuery: function setQuery(val, silent) { + var areEquivalent, hasDifferentWhitespace; + areEquivalent = areQueriesEquivalent(val, this.query); + hasDifferentWhitespace = areEquivalent ? this.query.length !== val.length : false; + this.query = val; + if (!silent && !areEquivalent) { + this.trigger("queryChanged", this.query); + } else if (!silent && hasDifferentWhitespace) { + this.trigger("whitespaceChanged", this.query); + } + }, + bind: function() { + var that = this, onBlur, onFocus, onKeydown, onInput; + onBlur = _.bind(this._onBlur, this); + onFocus = _.bind(this._onFocus, this); + onKeydown = _.bind(this._onKeydown, this); + onInput = _.bind(this._onInput, this); + this.$input.on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown); + if (!_.isMsie() || _.isMsie() > 9) { + this.$input.on("input.tt", onInput); + } else { + this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) { + if (specialKeyCodeMap[$e.which || $e.keyCode]) { + return; + } + _.defer(_.bind(that._onInput, that, $e)); + }); + } + return this; + }, + focus: function focus() { + this.$input.focus(); + }, + blur: function blur() { + this.$input.blur(); + }, + getLangDir: function getLangDir() { + return this.dir; + }, + getQuery: function getQuery() { + return this.query || ""; + }, + setQuery: function setQuery(val, silent) { + this.setInputValue(val); + this._setQuery(val, silent); + }, + hasQueryChangedSinceLastFocus: function hasQueryChangedSinceLastFocus() { + return this.query !== this.queryWhenFocused; + }, + getInputValue: function getInputValue() { + return this.$input.val(); + }, + setInputValue: function setInputValue(value) { + this.$input.val(value); + this.clearHintIfInvalid(); + this._checkLanguageDirection(); + }, + resetInputValue: function resetInputValue() { + this.setInputValue(this.query); + }, + getHint: function getHint() { + return this.$hint.val(); + }, + setHint: function setHint(value) { + this.$hint.val(value); + }, + clearHint: function clearHint() { + this.setHint(""); + }, + clearHintIfInvalid: function clearHintIfInvalid() { + var val, hint, valIsPrefixOfHint, isValid; + val = this.getInputValue(); + hint = this.getHint(); + valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0; + isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow(); + !isValid && this.clearHint(); + }, + hasFocus: function hasFocus() { + return this.$input.is(":focus"); + }, + hasOverflow: function hasOverflow() { + var constraint = this.$input.width() - 2; + this.$overflowHelper.text(this.getInputValue()); + return this.$overflowHelper.width() >= constraint; + }, + isCursorAtEnd: function() { + var valueLength, selectionStart, range; + valueLength = this.$input.val().length; + selectionStart = this.$input[0].selectionStart; + if (_.isNumber(selectionStart)) { + return selectionStart === valueLength; + } else if (document.selection) { + range = document.selection.createRange(); + range.moveStart("character", -valueLength); + return valueLength === range.text.length; + } + return true; + }, + destroy: function destroy() { + this.$hint.off(".tt"); + this.$input.off(".tt"); + this.$overflowHelper.remove(); + this.$hint = this.$input = this.$overflowHelper = $("
"); + } + }); + return Input; + function buildOverflowHelper($input) { + return $('').css({ + position: "absolute", + visibility: "hidden", + whiteSpace: "pre", + fontFamily: $input.css("font-family"), + fontSize: $input.css("font-size"), + fontStyle: $input.css("font-style"), + fontVariant: $input.css("font-variant"), + fontWeight: $input.css("font-weight"), + wordSpacing: $input.css("word-spacing"), + letterSpacing: $input.css("letter-spacing"), + textIndent: $input.css("text-indent"), + textRendering: $input.css("text-rendering"), + textTransform: $input.css("text-transform") + }).insertAfter($input); + } + function areQueriesEquivalent(a, b) { + return Input.normalizeQuery(a) === Input.normalizeQuery(b); + } + function withModifier($e) { + return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey; + } + }(); + var Dataset = function() { + "use strict"; + var keys, nameGenerator; + keys = { + val: "tt-selectable-display", + obj: "tt-selectable-object" + }; + nameGenerator = _.getIdGenerator(); + function Dataset(o, www) { + o = o || {}; + o.templates = o.templates || {}; + o.templates.notFound = o.templates.notFound || o.templates.empty; + if (!o.source) { + $.error("missing source"); + } + if (!o.node) { + $.error("missing node"); + } + if (o.name && !isValidName(o.name)) { + $.error("invalid dataset name: " + o.name); + } + www.mixin(this); + this.highlight = !!o.highlight; + this.name = o.name || nameGenerator(); + this.limit = o.limit || 5; + this.displayFn = getDisplayFn(o.display || o.displayKey); + this.templates = getTemplates(o.templates, this.displayFn); + this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source; + this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async; + this._resetLastSuggestion(); + this.$el = $(o.node).addClass(this.classes.dataset).addClass(this.classes.dataset + "-" + this.name); + } + Dataset.extractData = function extractData(el) { + var $el = $(el); + if ($el.data(keys.obj)) { + return { + val: $el.data(keys.val) || "", + obj: $el.data(keys.obj) || null + }; + } + return null; + }; + _.mixin(Dataset.prototype, EventEmitter, { + _overwrite: function overwrite(query, suggestions) { + suggestions = suggestions || []; + if (suggestions.length) { + this._renderSuggestions(query, suggestions); + } else if (this.async && this.templates.pending) { + this._renderPending(query); + } else if (!this.async && this.templates.notFound) { + this._renderNotFound(query); + } else { + this._empty(); + } + this.trigger("rendered", this.name, suggestions, false); + }, + _append: function append(query, suggestions) { + suggestions = suggestions || []; + if (suggestions.length && this.$lastSuggestion.length) { + this._appendSuggestions(query, suggestions); + } else if (suggestions.length) { + this._renderSuggestions(query, suggestions); + } else if (!this.$lastSuggestion.length && this.templates.notFound) { + this._renderNotFound(query); + } + this.trigger("rendered", this.name, suggestions, true); + }, + _renderSuggestions: function renderSuggestions(query, suggestions) { + var $fragment; + $fragment = this._getSuggestionsFragment(query, suggestions); + this.$lastSuggestion = $fragment.children().last(); + this.$el.html($fragment).prepend(this._getHeader(query, suggestions)).append(this._getFooter(query, suggestions)); + }, + _appendSuggestions: function appendSuggestions(query, suggestions) { + var $fragment, $lastSuggestion; + $fragment = this._getSuggestionsFragment(query, suggestions); + $lastSuggestion = $fragment.children().last(); + this.$lastSuggestion.after($fragment); + this.$lastSuggestion = $lastSuggestion; + }, + _renderPending: function renderPending(query) { + var template = this.templates.pending; + this._resetLastSuggestion(); + template && this.$el.html(template({ + query: query, + dataset: this.name + })); + }, + _renderNotFound: function renderNotFound(query) { + var template = this.templates.notFound; + this._resetLastSuggestion(); + template && this.$el.html(template({ + query: query, + dataset: this.name + })); + }, + _empty: function empty() { + this.$el.empty(); + this._resetLastSuggestion(); + }, + _getSuggestionsFragment: function getSuggestionsFragment(query, suggestions) { + var that = this, fragment; + fragment = document.createDocumentFragment(); + _.each(suggestions, function getSuggestionNode(suggestion) { + var $el, context; + context = that._injectQuery(query, suggestion); + $el = $(that.templates.suggestion(context)).data(keys.obj, suggestion).data(keys.val, that.displayFn(suggestion)).addClass(that.classes.suggestion + " " + that.classes.selectable); + fragment.appendChild($el[0]); + }); + this.highlight && highlight({ + className: this.classes.highlight, + node: fragment, + pattern: query + }); + return $(fragment); + }, + _getFooter: function getFooter(query, suggestions) { + return this.templates.footer ? this.templates.footer({ + query: query, + suggestions: suggestions, + dataset: this.name + }) : null; + }, + _getHeader: function getHeader(query, suggestions) { + return this.templates.header ? this.templates.header({ + query: query, + suggestions: suggestions, + dataset: this.name + }) : null; + }, + _resetLastSuggestion: function resetLastSuggestion() { + this.$lastSuggestion = $(); + }, + _injectQuery: function injectQuery(query, obj) { + return _.isObject(obj) ? _.mixin({ + _query: query + }, obj) : obj; + }, + update: function update(query) { + var that = this, canceled = false, syncCalled = false, rendered = 0; + this.cancel(); + this.cancel = function cancel() { + canceled = true; + that.cancel = $.noop; + that.async && that.trigger("asyncCanceled", query); + }; + this.source(query, sync, async); + !syncCalled && sync([]); + function sync(suggestions) { + if (syncCalled) { + return; + } + syncCalled = true; + suggestions = (suggestions || []).slice(0, that.limit); + rendered = suggestions.length; + that._overwrite(query, suggestions); + if (rendered < that.limit && that.async) { + that.trigger("asyncRequested", query); + } + } + function async(suggestions) { + suggestions = suggestions || []; + if (!canceled && rendered < that.limit) { + that.cancel = $.noop; + rendered += suggestions.length; + that._append(query, suggestions.slice(0, that.limit - rendered)); + that.async && that.trigger("asyncReceived", query); + } + } + }, + cancel: $.noop, + clear: function clear() { + this._empty(); + this.cancel(); + this.trigger("cleared"); + }, + isEmpty: function isEmpty() { + return this.$el.is(":empty"); + }, + destroy: function destroy() { + this.$el = $("
"); + } + }); + return Dataset; + function getDisplayFn(display) { + display = display || _.stringify; + return _.isFunction(display) ? display : displayFn; + function displayFn(obj) { + return obj[display]; + } + } + function getTemplates(templates, displayFn) { + return { + notFound: templates.notFound && _.templatify(templates.notFound), + pending: templates.pending && _.templatify(templates.pending), + header: templates.header && _.templatify(templates.header), + footer: templates.footer && _.templatify(templates.footer), + suggestion: templates.suggestion || suggestionTemplate + }; + function suggestionTemplate(context) { + return $("
").text(displayFn(context)); + } + } + function isValidName(str) { + return /^[_a-zA-Z0-9-]+$/.test(str); + } + }(); + var Menu = function() { + "use strict"; + function Menu(o, www) { + var that = this; + o = o || {}; + if (!o.node) { + $.error("node is required"); + } + www.mixin(this); + this.$node = $(o.node); + this.query = null; + this.datasets = _.map(o.datasets, initializeDataset); + function initializeDataset(oDataset) { + var node = that.$node.find(oDataset.node).first(); + oDataset.node = node.length ? node : $("
").appendTo(that.$node); + return new Dataset(oDataset, www); + } + } + _.mixin(Menu.prototype, EventEmitter, { + _onSelectableClick: function onSelectableClick($e) { + this.trigger("selectableClicked", $($e.currentTarget)); + }, + _onRendered: function onRendered(type, dataset, suggestions, async) { + this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty()); + this.trigger("datasetRendered", dataset, suggestions, async); + }, + _onCleared: function onCleared() { + this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty()); + this.trigger("datasetCleared"); + }, + _propagate: function propagate() { + this.trigger.apply(this, arguments); + }, + _allDatasetsEmpty: function allDatasetsEmpty() { + return _.every(this.datasets, isDatasetEmpty); + function isDatasetEmpty(dataset) { + return dataset.isEmpty(); + } + }, + _getSelectables: function getSelectables() { + return this.$node.find(this.selectors.selectable); + }, + _removeCursor: function _removeCursor() { + var $selectable = this.getActiveSelectable(); + $selectable && $selectable.removeClass(this.classes.cursor); + }, + _ensureVisible: function ensureVisible($el) { + var elTop, elBottom, nodeScrollTop, nodeHeight; + elTop = $el.position().top; + elBottom = elTop + $el.outerHeight(true); + nodeScrollTop = this.$node.scrollTop(); + nodeHeight = this.$node.height() + parseInt(this.$node.css("paddingTop"), 10) + parseInt(this.$node.css("paddingBottom"), 10); + if (elTop < 0) { + this.$node.scrollTop(nodeScrollTop + elTop); + } else if (nodeHeight < elBottom) { + this.$node.scrollTop(nodeScrollTop + (elBottom - nodeHeight)); + } + }, + bind: function() { + var that = this, onSelectableClick; + onSelectableClick = _.bind(this._onSelectableClick, this); + this.$node.on("click.tt", this.selectors.selectable, onSelectableClick); + _.each(this.datasets, function(dataset) { + dataset.onSync("asyncRequested", that._propagate, that).onSync("asyncCanceled", that._propagate, that).onSync("asyncReceived", that._propagate, that).onSync("rendered", that._onRendered, that).onSync("cleared", that._onCleared, that); + }); + return this; + }, + isOpen: function isOpen() { + return this.$node.hasClass(this.classes.open); + }, + open: function open() { + this.$node.addClass(this.classes.open); + }, + close: function close() { + this.$node.removeClass(this.classes.open); + this._removeCursor(); + }, + setLanguageDirection: function setLanguageDirection(dir) { + this.$node.attr("dir", dir); + }, + selectableRelativeToCursor: function selectableRelativeToCursor(delta) { + var $selectables, $oldCursor, oldIndex, newIndex; + $oldCursor = this.getActiveSelectable(); + $selectables = this._getSelectables(); + oldIndex = $oldCursor ? $selectables.index($oldCursor) : -1; + newIndex = oldIndex + delta; + newIndex = (newIndex + 1) % ($selectables.length + 1) - 1; + newIndex = newIndex < -1 ? $selectables.length - 1 : newIndex; + return newIndex === -1 ? null : $selectables.eq(newIndex); + }, + setCursor: function setCursor($selectable) { + this._removeCursor(); + if ($selectable = $selectable && $selectable.first()) { + $selectable.addClass(this.classes.cursor); + this._ensureVisible($selectable); + } + }, + getSelectableData: function getSelectableData($el) { + return $el && $el.length ? Dataset.extractData($el) : null; + }, + getActiveSelectable: function getActiveSelectable() { + var $selectable = this._getSelectables().filter(this.selectors.cursor).first(); + return $selectable.length ? $selectable : null; + }, + getTopSelectable: function getTopSelectable() { + var $selectable = this._getSelectables().first(); + return $selectable.length ? $selectable : null; + }, + update: function update(query) { + var isValidUpdate = query !== this.query; + if (isValidUpdate) { + this.query = query; + _.each(this.datasets, updateDataset); + } + return isValidUpdate; + function updateDataset(dataset) { + dataset.update(query); + } + }, + empty: function empty() { + _.each(this.datasets, clearDataset); + this.query = null; + this.$node.addClass(this.classes.empty); + function clearDataset(dataset) { + dataset.clear(); + } + }, + destroy: function destroy() { + this.$node.off(".tt"); + this.$node = $("
"); + _.each(this.datasets, destroyDataset); + function destroyDataset(dataset) { + dataset.destroy(); + } + } + }); + return Menu; + }(); + var DefaultMenu = function() { + "use strict"; + var s = Menu.prototype; + function DefaultMenu() { + Menu.apply(this, [].slice.call(arguments, 0)); + } + _.mixin(DefaultMenu.prototype, Menu.prototype, { + open: function open() { + !this._allDatasetsEmpty() && this._show(); + return s.open.apply(this, [].slice.call(arguments, 0)); + }, + close: function close() { + this._hide(); + return s.close.apply(this, [].slice.call(arguments, 0)); + }, + _onRendered: function onRendered() { + if (this._allDatasetsEmpty()) { + this._hide(); + } else { + this.isOpen() && this._show(); + } + return s._onRendered.apply(this, [].slice.call(arguments, 0)); + }, + _onCleared: function onCleared() { + if (this._allDatasetsEmpty()) { + this._hide(); + } else { + this.isOpen() && this._show(); + } + return s._onCleared.apply(this, [].slice.call(arguments, 0)); + }, + setLanguageDirection: function setLanguageDirection(dir) { + this.$node.css(dir === "ltr" ? this.css.ltr : this.css.rtl); + return s.setLanguageDirection.apply(this, [].slice.call(arguments, 0)); + }, + _hide: function hide() { + this.$node.hide(); + }, + _show: function show() { + this.$node.css("display", "block"); + } + }); + return DefaultMenu; + }(); + var Typeahead = function() { + "use strict"; + function Typeahead(o, www) { + var onFocused, onBlurred, onEnterKeyed, onTabKeyed, onEscKeyed, onUpKeyed, onDownKeyed, onLeftKeyed, onRightKeyed, onQueryChanged, onWhitespaceChanged; + o = o || {}; + if (!o.input) { + $.error("missing input"); + } + if (!o.menu) { + $.error("missing menu"); + } + if (!o.eventBus) { + $.error("missing event bus"); + } + www.mixin(this); + this.eventBus = o.eventBus; + this.minLength = _.isNumber(o.minLength) ? o.minLength : 1; + this.input = o.input; + this.menu = o.menu; + this.enabled = true; + this.active = false; + this.input.hasFocus() && this.activate(); + this.dir = this.input.getLangDir(); + this._hacks(); + this.menu.bind().onSync("selectableClicked", this._onSelectableClicked, this).onSync("asyncRequested", this._onAsyncRequested, this).onSync("asyncCanceled", this._onAsyncCanceled, this).onSync("asyncReceived", this._onAsyncReceived, this).onSync("datasetRendered", this._onDatasetRendered, this).onSync("datasetCleared", this._onDatasetCleared, this); + onFocused = c(this, "activate", "open", "_onFocused"); + onBlurred = c(this, "deactivate", "_onBlurred"); + onEnterKeyed = c(this, "isActive", "isOpen", "_onEnterKeyed"); + onTabKeyed = c(this, "isActive", "isOpen", "_onTabKeyed"); + onEscKeyed = c(this, "isActive", "_onEscKeyed"); + onUpKeyed = c(this, "isActive", "open", "_onUpKeyed"); + onDownKeyed = c(this, "isActive", "open", "_onDownKeyed"); + onLeftKeyed = c(this, "isActive", "isOpen", "_onLeftKeyed"); + onRightKeyed = c(this, "isActive", "isOpen", "_onRightKeyed"); + onQueryChanged = c(this, "_openIfActive", "_onQueryChanged"); + onWhitespaceChanged = c(this, "_openIfActive", "_onWhitespaceChanged"); + this.input.bind().onSync("focused", onFocused, this).onSync("blurred", onBlurred, this).onSync("enterKeyed", onEnterKeyed, this).onSync("tabKeyed", onTabKeyed, this).onSync("escKeyed", onEscKeyed, this).onSync("upKeyed", onUpKeyed, this).onSync("downKeyed", onDownKeyed, this).onSync("leftKeyed", onLeftKeyed, this).onSync("rightKeyed", onRightKeyed, this).onSync("queryChanged", onQueryChanged, this).onSync("whitespaceChanged", onWhitespaceChanged, this).onSync("langDirChanged", this._onLangDirChanged, this); + } + _.mixin(Typeahead.prototype, { + _hacks: function hacks() { + var $input, $menu; + $input = this.input.$input || $("
"); + $menu = this.menu.$node || $("
"); + $input.on("blur.tt", function($e) { + var active, isActive, hasActive; + active = document.activeElement; + isActive = $menu.is(active); + hasActive = $menu.has(active).length > 0; + if (_.isMsie() && (isActive || hasActive)) { + $e.preventDefault(); + $e.stopImmediatePropagation(); + _.defer(function() { + $input.focus(); + }); + } + }); + $menu.on("mousedown.tt", function($e) { + $e.preventDefault(); + }); + }, + _onSelectableClicked: function onSelectableClicked(type, $el) { + this.select($el); + }, + _onDatasetCleared: function onDatasetCleared() { + this._updateHint(); + }, + _onDatasetRendered: function onDatasetRendered(type, dataset, suggestions, async) { + this._updateHint(); + this.eventBus.trigger("render", suggestions, async, dataset); + }, + _onAsyncRequested: function onAsyncRequested(type, dataset, query) { + this.eventBus.trigger("asyncrequest", query, dataset); + }, + _onAsyncCanceled: function onAsyncCanceled(type, dataset, query) { + this.eventBus.trigger("asynccancel", query, dataset); + }, + _onAsyncReceived: function onAsyncReceived(type, dataset, query) { + this.eventBus.trigger("asyncreceive", query, dataset); + }, + _onFocused: function onFocused() { + this._minLengthMet() && this.menu.update(this.input.getQuery()); + }, + _onBlurred: function onBlurred() { + if (this.input.hasQueryChangedSinceLastFocus()) { + this.eventBus.trigger("change", this.input.getQuery()); + } + }, + _onEnterKeyed: function onEnterKeyed(type, $e) { + var $selectable; + if ($selectable = this.menu.getActiveSelectable()) { + this.select($selectable) && $e.preventDefault(); + } + }, + _onTabKeyed: function onTabKeyed(type, $e) { + var $selectable; + if ($selectable = this.menu.getActiveSelectable()) { + this.select($selectable) && $e.preventDefault(); + } else if ($selectable = this.menu.getTopSelectable()) { + this.autocomplete($selectable) && $e.preventDefault(); + } + }, + _onEscKeyed: function onEscKeyed() { + this.close(); + }, + _onUpKeyed: function onUpKeyed() { + this.moveCursor(-1); + }, + _onDownKeyed: function onDownKeyed() { + this.moveCursor(+1); + }, + _onLeftKeyed: function onLeftKeyed() { + if (this.dir === "rtl" && this.input.isCursorAtEnd()) { + this.autocomplete(this.menu.getTopSelectable()); + } + }, + _onRightKeyed: function onRightKeyed() { + if (this.dir === "ltr" && this.input.isCursorAtEnd()) { + this.autocomplete(this.menu.getTopSelectable()); + } + }, + _onQueryChanged: function onQueryChanged(e, query) { + this._minLengthMet(query) ? this.menu.update(query) : this.menu.empty(); + }, + _onWhitespaceChanged: function onWhitespaceChanged() { + this._updateHint(); + }, + _onLangDirChanged: function onLangDirChanged(e, dir) { + if (this.dir !== dir) { + this.dir = dir; + this.menu.setLanguageDirection(dir); + } + }, + _openIfActive: function openIfActive() { + this.isActive() && this.open(); + }, + _minLengthMet: function minLengthMet(query) { + query = _.isString(query) ? query : this.input.getQuery() || ""; + return query.length >= this.minLength; + }, + _updateHint: function updateHint() { + var $selectable, data, val, query, escapedQuery, frontMatchRegEx, match; + $selectable = this.menu.getTopSelectable(); + data = this.menu.getSelectableData($selectable); + val = this.input.getInputValue(); + if (data && !_.isBlankString(val) && !this.input.hasOverflow()) { + query = Input.normalizeQuery(val); + escapedQuery = _.escapeRegExChars(query); + frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i"); + match = frontMatchRegEx.exec(data.val); + match && this.input.setHint(val + match[1]); + } else { + this.input.clearHint(); + } + }, + isEnabled: function isEnabled() { + return this.enabled; + }, + enable: function enable() { + this.enabled = true; + }, + disable: function disable() { + this.enabled = false; + }, + isActive: function isActive() { + return this.active; + }, + activate: function activate() { + if (this.isActive()) { + return true; + } else if (!this.isEnabled() || this.eventBus.before("active")) { + return false; + } else { + this.active = true; + this.eventBus.trigger("active"); + return true; + } + }, + deactivate: function deactivate() { + if (!this.isActive()) { + return true; + } else if (this.eventBus.before("idle")) { + return false; + } else { + this.active = false; + this.close(); + this.eventBus.trigger("idle"); + return true; + } + }, + isOpen: function isOpen() { + return this.menu.isOpen(); + }, + open: function open() { + if (!this.isOpen() && !this.eventBus.before("open")) { + this.menu.open(); + this._updateHint(); + this.eventBus.trigger("open"); + } + return this.isOpen(); + }, + close: function close() { + if (this.isOpen() && !this.eventBus.before("close")) { + this.menu.close(); + this.input.clearHint(); + this.input.resetInputValue(); + this.eventBus.trigger("close"); + } + return !this.isOpen(); + }, + setVal: function setVal(val) { + this.input.setQuery(_.toStr(val)); + }, + getVal: function getVal() { + return this.input.getQuery(); + }, + select: function select($selectable) { + var data = this.menu.getSelectableData($selectable); + if (data && !this.eventBus.before("select", data.obj)) { + this.input.setQuery(data.val, true); + this.eventBus.trigger("select", data.obj); + this.close(); + return true; + } + return false; + }, + autocomplete: function autocomplete($selectable) { + var query, data, isValid; + query = this.input.getQuery(); + data = this.menu.getSelectableData($selectable); + isValid = data && query !== data.val; + if (isValid && !this.eventBus.before("autocomplete", data.obj)) { + this.input.setQuery(data.val); + this.eventBus.trigger("autocomplete", data.obj); + return true; + } + return false; + }, + moveCursor: function moveCursor(delta) { + var query, $candidate, data, payload, cancelMove; + query = this.input.getQuery(); + $candidate = this.menu.selectableRelativeToCursor(delta); + data = this.menu.getSelectableData($candidate); + payload = data ? data.obj : null; + cancelMove = this._minLengthMet() && this.menu.update(query); + if (!cancelMove && !this.eventBus.before("cursorchange", payload)) { + this.menu.setCursor($candidate); + if (data) { + this.input.setInputValue(data.val); + } else { + this.input.resetInputValue(); + this._updateHint(); + } + this.eventBus.trigger("cursorchange", payload); + return true; + } + return false; + }, + destroy: function destroy() { + this.input.destroy(); + this.menu.destroy(); + } + }); + return Typeahead; + function c(ctx) { + var methods = [].slice.call(arguments, 1); + return function() { + var args = [].slice.call(arguments); + _.each(methods, function(method) { + return ctx[method].apply(ctx, args); + }); + }; + } + }(); + (function() { + "use strict"; + var old, keys, methods; + old = $.fn.typeahead; + keys = { + www: "tt-www", + attrs: "tt-attrs", + typeahead: "tt-typeahead" + }; + methods = { + initialize: function initialize(o, datasets) { + var www; + datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1); + o = o || {}; + www = WWW(o.classNames); + return this.each(attach); + function attach() { + var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu, eventBus, input, menu, typeahead, MenuConstructor; + _.each(datasets, function(d) { + d.highlight = !!o.highlight; + }); + $input = $(this); + $wrapper = $(www.html.wrapper); + $hint = $elOrNull(o.hint); + $menu = $elOrNull(o.menu); + defaultHint = o.hint !== false && !$hint; + defaultMenu = o.menu !== false && !$menu; + defaultHint && ($hint = buildHintFromInput($input, www)); + defaultMenu && ($menu = $(www.html.menu).css(www.css.menu)); + $hint && $hint.val(""); + $input = prepInput($input, www); + if (defaultHint || defaultMenu) { + $wrapper.css(www.css.wrapper); + $input.css(defaultHint ? www.css.input : www.css.inputWithNoHint); + $input.wrap($wrapper).parent().prepend(defaultHint ? $hint : null).append(defaultMenu ? $menu : null); + } + MenuConstructor = defaultMenu ? DefaultMenu : Menu; + eventBus = new EventBus({ + el: $input + }); + input = new Input({ + hint: $hint, + input: $input + }, www); + menu = new MenuConstructor({ + node: $menu, + datasets: datasets + }, www); + typeahead = new Typeahead({ + input: input, + menu: menu, + eventBus: eventBus, + minLength: o.minLength + }, www); + $input.data(keys.www, www); + $input.data(keys.typeahead, typeahead); + } + }, + isEnabled: function isEnabled() { + var enabled; + ttEach(this.first(), function(t) { + enabled = t.isEnabled(); + }); + return enabled; + }, + enable: function enable() { + ttEach(this, function(t) { + t.enable(); + }); + return this; + }, + disable: function disable() { + ttEach(this, function(t) { + t.disable(); + }); + return this; + }, + isActive: function isActive() { + var active; + ttEach(this.first(), function(t) { + active = t.isActive(); + }); + return active; + }, + activate: function activate() { + ttEach(this, function(t) { + t.activate(); + }); + return this; + }, + deactivate: function deactivate() { + ttEach(this, function(t) { + t.deactivate(); + }); + return this; + }, + isOpen: function isOpen() { + var open; + ttEach(this.first(), function(t) { + open = t.isOpen(); + }); + return open; + }, + open: function open() { + ttEach(this, function(t) { + t.open(); + }); + return this; + }, + close: function close() { + ttEach(this, function(t) { + t.close(); + }); + return this; + }, + select: function select(el) { + var success = false, $el = $(el); + ttEach(this.first(), function(t) { + success = t.select($el); + }); + return success; + }, + autocomplete: function autocomplete(el) { + var success = false, $el = $(el); + ttEach(this.first(), function(t) { + success = t.autocomplete($el); + }); + return success; + }, + moveCursor: function moveCursoe(delta) { + var success = false; + ttEach(this.first(), function(t) { + success = t.moveCursor(delta); + }); + return success; + }, + val: function val(newVal) { + var query; + if (!arguments.length) { + ttEach(this.first(), function(t) { + query = t.getVal(); + }); + return query; + } else { + ttEach(this, function(t) { + t.setVal(newVal); + }); + return this; + } + }, + destroy: function destroy() { + ttEach(this, function(typeahead, $input) { + revert($input); + typeahead.destroy(); + }); + return this; + } + }; + $.fn.typeahead = function(method) { + if (methods[method]) { + return methods[method].apply(this, [].slice.call(arguments, 1)); + } else { + return methods.initialize.apply(this, arguments); + } + }; + $.fn.typeahead.noConflict = function noConflict() { + $.fn.typeahead = old; + return this; + }; + function ttEach($els, fn) { + $els.each(function() { + var $input = $(this), typeahead; + (typeahead = $input.data(keys.typeahead)) && fn(typeahead, $input); + }); + } + function buildHintFromInput($input, www) { + return $input.clone().addClass(www.classes.hint).removeData().css(www.css.hint).css(getBackgroundStyles($input)).prop("readonly", true).removeAttr("id name placeholder required").attr({ + autocomplete: "off", + spellcheck: "false", + tabindex: -1 + }); + } + function prepInput($input, www) { + $input.data(keys.attrs, { + dir: $input.attr("dir"), + autocomplete: $input.attr("autocomplete"), + spellcheck: $input.attr("spellcheck"), + style: $input.attr("style") + }); + $input.addClass(www.classes.input).attr({ + autocomplete: "off", + spellcheck: false + }); + try { + !$input.attr("dir") && $input.attr("dir", "auto"); + } catch (e) {} + return $input; + } + function getBackgroundStyles($el) { + return { + backgroundAttachment: $el.css("background-attachment"), + backgroundClip: $el.css("background-clip"), + backgroundColor: $el.css("background-color"), + backgroundImage: $el.css("background-image"), + backgroundOrigin: $el.css("background-origin"), + backgroundPosition: $el.css("background-position"), + backgroundRepeat: $el.css("background-repeat"), + backgroundSize: $el.css("background-size") + }; + } + function revert($input) { + var www, $wrapper; + www = $input.data(keys.www); + $wrapper = $input.parent().filter(www.selectors.wrapper); + _.each($input.data(keys.attrs), function(val, key) { + _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val); + }); + $input.removeData(keys.typeahead).removeData(keys.www).removeData(keys.attr).removeClass(www.classes.input); + if ($wrapper.length) { + $input.detach().insertAfter($wrapper); + $wrapper.remove(); + } + } + function $elOrNull(obj) { + var isValid, $el; + isValid = _.isJQuery(obj) || _.isElement(obj); + $el = isValid ? $(obj).first() : []; + return $el.length ? $el : null; + } + })(); +}); \ No newline at end of file -- cgit v1.2.3 From 636421bf386e813cb6a87a7499fdedc975b75900 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 24 Mar 2016 22:13:41 +0100 Subject: upgrade: add formbuilder wrapper for bootstrap Not sure if this does what we need. But for now it fixes the tests --- app/assets/javascripts/leap.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'app') diff --git a/app/assets/javascripts/leap.js b/app/assets/javascripts/leap.js index 94e602d..c8fbcf5 100644 --- a/app/assets/javascripts/leap.js +++ b/app/assets/javascripts/leap.js @@ -5,3 +5,5 @@ function alert_message(msg) { $('#messages').append('
×'+msg+'
'); } + +ClientSideValidations.formBuilders['SimpleForm::FormBuilder'].wrappers.bootstrap = ClientSideValidations.formBuilders['SimpleForm::FormBuilder'].wrappers["default"]; -- cgit v1.2.3 From 00fba5114ae2c6176c88ff0ea152576c5bff657d Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 25 Mar 2016 11:06:55 +0100 Subject: upgrade: {File,Dir}.exists? -> exist? exists? is deprecated in ruby 2.1 --- app/controllers/controller_extension/json_file.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/controllers/controller_extension/json_file.rb b/app/controllers/controller_extension/json_file.rb index 6be919a..5b5e55e 100644 --- a/app/controllers/controller_extension/json_file.rb +++ b/app/controllers/controller_extension/json_file.rb @@ -12,7 +12,7 @@ module ControllerExtension::JsonFile end def fetch_file - if File.exists?(@filename) + if File.exist?(@filename) @file = File.new(@filename) else not_found -- cgit v1.2.3 From de8f3e532d857ec64a9ba17bf9e5a4de272a6cbd Mon Sep 17 00:00:00 2001 From: Azul Date: Sat, 26 Mar 2016 17:36:37 +0100 Subject: upgrade: use bootstrap3 row and col-md-* --- app/views/common/_action_buttons.html.haml | 10 +++++----- app/views/common/_download_button.html.haml | 8 ++++---- app/views/common/_home_page_buttons.html.haml | 4 ++-- app/views/home/_content.html.haml | 6 +++--- app/views/layouts/_content.html.haml | 6 +++--- app/views/layouts/application.html.haml | 8 ++++---- app/views/sessions/new.html.haml | 4 ++-- app/views/users/new.html.haml | 4 ++-- app/views/users/show.html.haml | 2 +- 9 files changed, 26 insertions(+), 26 deletions(-) (limited to 'app') diff --git a/app/views/common/_action_buttons.html.haml b/app/views/common/_action_buttons.html.haml index 81ebf67..eb663c0 100644 --- a/app/views/common/_action_buttons.html.haml +++ b/app/views/common/_action_buttons.html.haml @@ -1,14 +1,14 @@ .home-buttons - .row-fluid.second - .login.span4 + .row.second + .login.col-md-4 %span.link= btn icon('ok-sign') + t(:login), login_path %span.info= t(:login_info, default: "") - if APP_CONFIG[:allow_registration] - .signup.span4 + .signup.col-md-4 %span.link= btn icon('user') + t(:signup), signup_path %span.info= t(:signup_info, default: "") - else - .signup.span4 - .help.span4 + .signup.col-md-4 + .help.col-md-4 %span.link= btn icon('question-sign') + t(:get_help), new_ticket_path %span.info= t(:support_info, default: "") diff --git a/app/views/common/_download_button.html.haml b/app/views/common/_download_button.html.haml index 9c26860..1278230 100644 --- a/app/views/common/_download_button.html.haml +++ b/app/views/common/_download_button.html.haml @@ -1,8 +1,8 @@ .home-buttons - .row-fluid.first - .span2 - .download.span8 + .row.first + .col-md-2 + .download.col-md-8 = btn client_download_url, type: [:large, :primary] do = big_icon('download') = t(:download_bitmask) - .span2 + .col-md-2 diff --git a/app/views/common/_home_page_buttons.html.haml b/app/views/common/_home_page_buttons.html.haml index fc6348e..cfe3734 100644 --- a/app/views/common/_home_page_buttons.html.haml +++ b/app/views/common/_home_page_buttons.html.haml @@ -1,6 +1,6 @@ = render 'common/download_button' - if local_assigns[:divider] - .row-fluid - .span12 + .row + .col-md-12 = render local_assigns[:divider] = render 'common/action_buttons' diff --git a/app/views/home/_content.html.haml b/app/views/home/_content.html.haml index d96a1e0..67e4533 100644 --- a/app/views/home/_content.html.haml +++ b/app/views/home/_content.html.haml @@ -1,12 +1,12 @@ -.row-fluid +.row %h1= t(:welcome, :provider => APP_CONFIG[:domain]) .p=t(:welcome_message_html) -.row-fluid +.row = home_page_buttons - if Rails.env == 'development' - .row-fluid + .row %hr %p = link_to "make donation", new_payment_path if APP_CONFIG[:payment].present? diff --git a/app/views/layouts/_content.html.haml b/app/views/layouts/_content.html.haml index d5c2fa3..21d2e12 100644 --- a/app/views/layouts/_content.html.haml +++ b/app/views/layouts/_content.html.haml @@ -8,12 +8,12 @@ - content = yield - if @show_navigation - .span2 + .col-md-2 = render 'layouts/navigation' - .span10 + .col-md-10 = render 'layouts/messages' = content - else - .span12 + .col-md-12 = render 'layouts/messages' = content diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 0aeda8b..e242a5f 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -15,13 +15,13 @@ #main .container-fluid - if @show_navigation && !admin? - .row-fluid + .row %h1= t(:user_control_panel) - if logged_in? - .row-fluid - .span12 + .row + .col-md-12 = render 'layouts/header' - .row-fluid + .row = render 'layouts/content' #push -# #push is used for sticky footer in bootstrap 2. remove when upgrading to bootstrap 3 diff --git a/app/views/sessions/new.html.haml b/app/views/sessions/new.html.haml index bb7e4bd..942c485 100644 --- a/app/views/sessions/new.html.haml +++ b/app/views/sessions/new.html.haml @@ -1,5 +1,5 @@ -.span1 -.span9 +.col-md-1 +.col-md-9 %h2=t :login .lead=t :login_info = render :partial => 'users/warnings' diff --git a/app/views/users/new.html.haml b/app/views/users/new.html.haml index bc0b1af..1b257d9 100644 --- a/app/views/users/new.html.haml +++ b/app/views/users/new.html.haml @@ -8,8 +8,8 @@ - form_options = {url: '/not-used', html: {id: 'new_user', class: user_form_class('form-horizontal'), style: 'display:none'}, validate: true} -.span1 -.span9 +.col-md-1 +.col-md-9 %h2=t :signup .lead=t :signup_info = render :partial => 'warnings' diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index da8e467..3bdf952 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,6 +1,6 @@ = render 'overview' .container-fluid - .row-fluid + .row %h4 To use bitmask services: = btn client_download_url, type: "primary" do %i.icon-arrow-down.icon-white -- cgit v1.2.3 From a0d9fbc4bcbd2f37e8d86d99c2e01753eecb14d7 Mon Sep 17 00:00:00 2001 From: Azul Date: Sat, 26 Mar 2016 18:02:20 +0100 Subject: upgrade bootstrap3 icons work now import bootstrap-sprockets; before importing bootstrap in the scss. bootstrap now uses glyphicons which are based on fonts. So we always should use a span. Not sure what to do with big and huge icons yet. --- app/assets/stylesheets/application.scss | 1 + app/helpers/application_helper.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index e6eeda2..856a559 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -9,6 +9,7 @@ // // import bootstrap. // +@import "bootstrap-sprockets"; @import "bootstrap"; // diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 6de5e1b..23b2752 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -19,7 +19,7 @@ module ApplicationHelper # http://twitter.github.io/bootstrap/base-css.html#icons # def icon(name, color=nil) - " ".html_safe + " ".html_safe end def big_icon(name, color=nil) -- cgit v1.2.3 From 65f67e1829666a65a3b354d4d95f9d38aa15a81e Mon Sep 17 00:00:00 2001 From: Azul Date: Sat, 26 Mar 2016 18:17:55 +0100 Subject: upgrade: fix buttons for bootstrap3 bootstrap3 now requires btn-default if no other btn option is present. download button on home page was too small in german translation --- app/assets/stylesheets/leap.scss | 2 +- app/helpers/link_helper.rb | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/assets/stylesheets/leap.scss b/app/assets/stylesheets/leap.scss index 329fc09..8c4d702 100644 --- a/app/assets/stylesheets/leap.scss +++ b/app/assets/stylesheets/leap.scss @@ -198,7 +198,7 @@ input, textarea { } .download { a.btn { - width: 15em; + width: 18em; font-weight: bold; small { font-weight: normal; diff --git a/app/helpers/link_helper.rb b/app/helpers/link_helper.rb index ddb063e..b74e1d7 100644 --- a/app/helpers/link_helper.rb +++ b/app/helpers/link_helper.rb @@ -39,6 +39,10 @@ module LinkHelper def btn(*args, &block) html_options = extract_html_options!(args, &block) type = Array(html_options.delete(:type)) + btn_opts = [:default, :primary, :success, :info, :warning, :danger, :link] + if (type & btn_opts).blank? + type << :default + end type.map! {|t| "btn-#{t}"} html_options[:class] = concat_classes(html_options[:class], 'btn', type) args[0] = t(args[0]) if args[0].is_a?(Symbol) -- cgit v1.2.3 From 70d8ffc40ef57efaf75cd51e0c1ca654dc37dd4c Mon Sep 17 00:00:00 2001 From: Azul Date: Sat, 26 Mar 2016 18:22:22 +0100 Subject: upgrade: bootstrap control-group -> form-group --- app/assets/javascripts/users.js | 2 +- app/views/users/_change_password.html.haml | 2 +- app/views/users/_change_pgp_key.html.haml | 2 +- app/views/users/_change_service_level.html.haml | 2 +- app/views/users/_contact_email.html.haml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/users.js b/app/assets/javascripts/users.js index 70d78fd..3dae526 100644 --- a/app/assets/javascripts/users.js +++ b/app/assets/javascripts/users.js @@ -6,7 +6,7 @@ "error_tag":"span", "wrapper_error_class":"error", "wrapper_tag":"div", - "wrapper_class":"control-group" + "wrapper_class":"form-group" } // diff --git a/app/views/users/_change_password.html.haml b/app/views/users/_change_password.html.haml index 425e3ee..64a4d0a 100644 --- a/app/views/users/_change_password.html.haml +++ b/app/views/users/_change_password.html.haml @@ -15,7 +15,7 @@ = f.input :login, :label => t(:username), :required => false, :input_html => {:id => :srp_username} = f.input :password, :required => false, :validate => true, :input_html => { :id => :srp_password } = f.input :password_confirmation, :required => false, :input_html => { :id => :srp_password_confirmation } - .control-group + .form-group .controls = f.submit t(:save), :class => 'btn btn-primary' diff --git a/app/views/users/_change_pgp_key.html.haml b/app/views/users/_change_pgp_key.html.haml index e465125..af3eb5e 100644 --- a/app/views/users/_change_pgp_key.html.haml +++ b/app/views/users/_change_pgp_key.html.haml @@ -8,6 +8,6 @@ = simple_form_for [:api, @user], form_options do |f| %legend= t(:advanced_options) = f.input :public_key, :as => :text, :hint => t(:use_ascii_key), :input_html => {:class => "full-width", :rows => 4} - .control-group + .form-group .controls = f.submit t(:save), :class => 'btn', :data => {"loading-text" => "Saving..."} diff --git a/app/views/users/_change_service_level.html.haml b/app/views/users/_change_service_level.html.haml index 42315a2..a2e9956 100644 --- a/app/views/users/_change_service_level.html.haml +++ b/app/views/users/_change_service_level.html.haml @@ -13,6 +13,6 @@ %p = t(:effective_service_level) = f.select :effective_service_level_code, ServiceLevel.select_options, :selected => @user.effective_service_level.id - .control-group + .form-group .controls = f.submit t(:save), :class => 'btn', :data => {"loading-text" => "Saving..."} diff --git a/app/views/users/_contact_email.html.haml b/app/views/users/_contact_email.html.haml index ad768b7..1627ff6 100644 --- a/app/views/users/_contact_email.html.haml +++ b/app/views/users/_contact_email.html.haml @@ -4,6 +4,6 @@ = f.input :contact_email, :label => false -# %p= t(:public_key) -# = f.text_area :contact_email_key, {:class => "full-width", :rows => 4} - .control-group + .form-group .controls = f.submit t(:save), :class => 'btn', :data => {"loading-text" => "Saving..."} \ No newline at end of file -- cgit v1.2.3 From 500fa2db1d593131d7cebd127c941a44bf174223 Mon Sep 17 00:00:00 2001 From: Azul Date: Sat, 26 Mar 2016 18:42:36 +0100 Subject: use icon helper from navigation helper and use content_tag inside the icon helper... html_safe is evil. --- app/helpers/application_helper.rb | 3 ++- app/helpers/navigation_helper.rb | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 23b2752..920186d 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -19,7 +19,8 @@ module ApplicationHelper # http://twitter.github.io/bootstrap/base-css.html#icons # def icon(name, color=nil) - " ".html_safe + content_tag :span, '', + class: "glyphicon glyphicon-#{name} #{color_class(color)}" end def big_icon(name, color=nil) diff --git a/app/helpers/navigation_helper.rb b/app/helpers/navigation_helper.rb index 2639246..1df840c 100644 --- a/app/helpers/navigation_helper.rb +++ b/app/helpers/navigation_helper.rb @@ -66,9 +66,9 @@ module NavigationHelper end def extract_icon!(options) - icon = options.delete(:icon) - if icon.present? - content_tag(:i, '', class: "icon-#{icon}") + name = options.delete(:icon) + if name.present? + icon name else "" end -- cgit v1.2.3 From fc066a42ec5a3271b0d476ff2c5ab771f1ab726d Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 3 May 2016 10:24:11 -0300 Subject: fix failing unit and functional tests --- app/controllers/controller_extension/fetch_user.rb | 2 +- app/models/api_monitor_user.rb | 11 +++++++++++ app/models/api_user.rb | 13 +------------ 3 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 app/models/api_monitor_user.rb (limited to 'app') diff --git a/app/controllers/controller_extension/fetch_user.rb b/app/controllers/controller_extension/fetch_user.rb index 97f92fa..632291d 100644 --- a/app/controllers/controller_extension/fetch_user.rb +++ b/app/controllers/controller_extension/fetch_user.rb @@ -22,7 +22,7 @@ module ControllerExtension::FetchUser @user = User.find(params[:user_id] || params[:id]) if current_user.is_admin? || current_user.is_monitor? if @user.nil? - not_found(t(:no_such_thing, :thing => 'user'), users_url) + not_found(t(:no_such_user), users_url) elsif current_user.is_monitor? access_denied unless @user.is_test? end diff --git a/app/models/api_monitor_user.rb b/app/models/api_monitor_user.rb new file mode 100644 index 0000000..d0fe411 --- /dev/null +++ b/app/models/api_monitor_user.rb @@ -0,0 +1,11 @@ +# +# A user that has limited admin access, to be used +# for running monitor tests against a live production +# installation. +# +class ApiMonitorUser < ApiUser + def is_monitor? + true + end +end + diff --git a/app/models/api_user.rb b/app/models/api_user.rb index 2efe1cb..c70cccb 100644 --- a/app/models/api_user.rb +++ b/app/models/api_user.rb @@ -2,17 +2,6 @@ class ApiUser < AnonymousUser end -# -# A user that has limited admin access, to be used -# for running monitor tests against a live production -# installation. -# -class ApiMonitorUser < ApiUser - def is_monitor? - true - end -end - # # Not yet supported: # @@ -20,4 +9,4 @@ end # def is_admin? # true # end -#end \ No newline at end of file +#end -- cgit v1.2.3 From 33e2a52f683697ca8489d856df90b39bfbbe7373 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 3 May 2016 11:29:45 -0300 Subject: use APP_CONFIG[config_file_paths] for provider.json This avoids overwriting the PROVIDER_JSON constant in the StaticConfigController and thus fixes test warnings. Also moved away from using instance variables in the ControllerExtension::JsonFile - instead querying the corresponding functions now - less sideeffects and easier stubbing. --- app/controllers/controller_extension/json_file.rb | 23 ++++++++++++++--------- app/controllers/static_config_controller.rb | 11 ++++------- app/controllers/v1/configs_controller.rb | 6 ++---- 3 files changed, 20 insertions(+), 20 deletions(-) (limited to 'app') diff --git a/app/controllers/controller_extension/json_file.rb b/app/controllers/controller_extension/json_file.rb index 5b5e55e..df9cf55 100644 --- a/app/controllers/controller_extension/json_file.rb +++ b/app/controllers/controller_extension/json_file.rb @@ -4,20 +4,25 @@ module ControllerExtension::JsonFile protected - def send_file - if stale?(:last_modified => @file.mtime) - response.content_type = 'application/json' - render :text => @file.read + def send_file(filename) + file = fetch_file(filename) + if file.present? + send_file_or_cache_hit(file) + else + not_found end end - def fetch_file - if File.exist?(@filename) - @file = File.new(@filename) - else - not_found + def send_file_or_cache_hit(file) + if stale?(:last_modified => file.mtime) + response.content_type = 'application/json' + render :text => file.read end end + def fetch_file(filename) + File.new(filename) if File.exist?(filename) + end + end diff --git a/app/controllers/static_config_controller.rb b/app/controllers/static_config_controller.rb index c78e006..46e7cd2 100644 --- a/app/controllers/static_config_controller.rb +++ b/app/controllers/static_config_controller.rb @@ -5,13 +5,9 @@ class StaticConfigController < ActionController::Base include ControllerExtension::JsonFile before_filter :set_minimum_client_version - before_filter :set_filename - before_filter :fetch_file - - PROVIDER_JSON = Rails.root.join('config', 'provider', 'provider.json') def provider - send_file + send_file provider_json end protected @@ -23,7 +19,8 @@ class StaticConfigController < ActionController::Base APP_CONFIG[:minimum_client_version].to_s end - def set_filename - @filename = PROVIDER_JSON + def provider_json + Rails.root.join APP_CONFIG[:config_file_paths]['provider'] end + end diff --git a/app/controllers/v1/configs_controller.rb b/app/controllers/v1/configs_controller.rb index 4a6f455..f0b284e 100644 --- a/app/controllers/v1/configs_controller.rb +++ b/app/controllers/v1/configs_controller.rb @@ -3,15 +3,13 @@ class V1::ConfigsController < ApiController before_filter :require_login, :unless => :anonymous_access_allowed? before_filter :sanitize_id, only: :show - before_filter :lookup_file, only: :show - before_filter :fetch_file, only: :show def index render json: {services: service_paths} end def show - send_file + send_file lookup_file end protected @@ -34,6 +32,6 @@ class V1::ConfigsController < ApiController def lookup_file path = APP_CONFIG[:config_file_paths][@id] not_found if path.blank? - @filename = Rails.root.join path + Rails.root.join path end end -- cgit v1.2.3 From a1b494e334406660a1f49fb7de9b043493809640 Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 13 May 2016 16:23:10 +0200 Subject: ensure invalid user ids still render the ticket form We still have strange urls requested like /pt/users/AnonymousUser.../tickets/new Not sure where they are coming from - but this should make sure we respond with sth. meaningful instead of erroring out. Conflicts: app/views/layouts/_content.html.haml --- app/views/layouts/_content.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/views/layouts/_content.html.haml b/app/views/layouts/_content.html.haml index 21d2e12..8e0bfbc 100644 --- a/app/views/layouts/_content.html.haml +++ b/app/views/layouts/_content.html.haml @@ -7,7 +7,7 @@ - else - content = yield -- if @show_navigation +- if @show_navigation && @user .col-md-2 = render 'layouts/navigation' .col-md-10 -- cgit v1.2.3 From e05a1b0f5ae40a2aa17976b3009cd563b8e4660a Mon Sep 17 00:00:00 2001 From: Azul Date: Sun, 1 May 2016 10:55:33 -0300 Subject: api: allow version bumping - bump to 2 --- app/controllers/api/certs_controller.rb | 31 +++++++ app/controllers/api/configs_controller.rb | 37 +++++++++ app/controllers/api/identities_controller.rb | 16 ++++ app/controllers/api/messages_controller.rb | 119 +++++++++++++++++++++++++++ app/controllers/api/services_controller.rb | 8 ++ app/controllers/api/sessions_controller.rb | 44 ++++++++++ app/controllers/api/smtp_certs_controller.rb | 42 ++++++++++ app/controllers/api/users_controller.rb | 83 +++++++++++++++++++ app/controllers/v1/certs_controller.rb | 31 ------- app/controllers/v1/configs_controller.rb | 37 --------- app/controllers/v1/identities_controller.rb | 16 ---- app/controllers/v1/messages_controller.rb | 119 --------------------------- app/controllers/v1/services_controller.rb | 8 -- app/controllers/v1/sessions_controller.rb | 44 ---------- app/controllers/v1/smtp_certs_controller.rb | 42 ---------- app/controllers/v1/users_controller.rb | 83 ------------------- app/views/api/sessions/new.json.erb | 3 + app/views/v1/sessions/new.json.erb | 3 - 18 files changed, 383 insertions(+), 383 deletions(-) create mode 100644 app/controllers/api/certs_controller.rb create mode 100644 app/controllers/api/configs_controller.rb create mode 100644 app/controllers/api/identities_controller.rb create mode 100644 app/controllers/api/messages_controller.rb create mode 100644 app/controllers/api/services_controller.rb create mode 100644 app/controllers/api/sessions_controller.rb create mode 100644 app/controllers/api/smtp_certs_controller.rb create mode 100644 app/controllers/api/users_controller.rb delete mode 100644 app/controllers/v1/certs_controller.rb delete mode 100644 app/controllers/v1/configs_controller.rb delete mode 100644 app/controllers/v1/identities_controller.rb delete mode 100644 app/controllers/v1/messages_controller.rb delete mode 100644 app/controllers/v1/services_controller.rb delete mode 100644 app/controllers/v1/sessions_controller.rb delete mode 100644 app/controllers/v1/smtp_certs_controller.rb delete mode 100644 app/controllers/v1/users_controller.rb create mode 100644 app/views/api/sessions/new.json.erb delete mode 100644 app/views/v1/sessions/new.json.erb (limited to 'app') diff --git a/app/controllers/api/certs_controller.rb b/app/controllers/api/certs_controller.rb new file mode 100644 index 0000000..46a84d3 --- /dev/null +++ b/app/controllers/api/certs_controller.rb @@ -0,0 +1,31 @@ +class Api::CertsController < ApiController + + before_filter :require_login, :unless => :anonymous_access_allowed? + before_filter :require_enabled + + # GET /cert + # deprecated - we actually create a new cert and that can + # be reflected in the action. GET /cert will eventually go + # away and be replaced by POST /cert + def show + create + end + + # POST /cert + def create + @cert = ClientCertificate.new(:prefix => service_level.cert_prefix) + render text: @cert.to_s, content_type: 'text/plain' + end + + protected + + def require_enabled + if !current_user.is_anonymous? && !current_user.enabled? + access_denied + end + end + + def service_level + current_user.effective_service_level + end +end diff --git a/app/controllers/api/configs_controller.rb b/app/controllers/api/configs_controller.rb new file mode 100644 index 0000000..55ceb4f --- /dev/null +++ b/app/controllers/api/configs_controller.rb @@ -0,0 +1,37 @@ +class Api::ConfigsController < ApiController + include ControllerExtension::JsonFile + + before_filter :require_login, :unless => :anonymous_access_allowed? + before_filter :sanitize_id, only: :show + + def index + render json: {services: service_paths} + end + + def show + send_file lookup_file + end + + protected + + SERVICE_IDS = { + soledad: "soledad-service", + eip: "eip-service", + smtp: "smtp-service" + } + + def service_paths + Hash[SERVICE_IDS.map{|k,v| [k,"/1/configs/#{v}.json"] } ] + end + + def sanitize_id + @id = params[:id].downcase + access_denied unless SERVICE_IDS.values.include? @id + end + + def lookup_file + path = APP_CONFIG[:config_file_paths][@id] + not_found if path.blank? + Rails.root.join path + end +end diff --git a/app/controllers/api/identities_controller.rb b/app/controllers/api/identities_controller.rb new file mode 100644 index 0000000..ab2ac00 --- /dev/null +++ b/app/controllers/api/identities_controller.rb @@ -0,0 +1,16 @@ +module Api + class IdentitiesController < ApiController + before_filter :token_authenticate + before_filter :require_monitor + + def show + @identity = Identity.find_by_address(params[:id]) + if @identity + respond_with @identity + else + render_not_found + end + end + + end +end diff --git a/app/controllers/api/messages_controller.rb b/app/controllers/api/messages_controller.rb new file mode 100644 index 0000000..a69a40a --- /dev/null +++ b/app/controllers/api/messages_controller.rb @@ -0,0 +1,119 @@ +module Api + class MessagesController < ApiController + + before_filter :require_login + + def index + if Dir.exist?(motd_dir) + if !CommonLanguages::available_code?(params[:locale]) + locale = 'en' + else + locale = params[:locale] + end + render json: motd_files_for_locale(locale) + else + render json: [] + end + end + + # disable per-user messages for now, not supported in the client + #def update + # if message = Message.find(params[:id]) + # message.mark_as_read_by(current_user) + # message.save + # render json: success(:marked_as_read) + # else + # render json: error(:not_found), status: :not_found + # end + #end + + private + + # + # returns list of messages, for example: + # + # [ + # {"id": 1, "locale": "en", "text": ""}, + # {"id": 2, "locale": "en", "text": ""} + # ] + # + # Each message is present only once, using the best choice + # for the locale. The order is determined by the id. + # + def motd_files_for_locale(locale) + files = [] + motd_files.keys.each do |id| + if motd_files[id].key?(locale) + msg_locale = locale + elsif motd_files[id].key?('en') + msg_locale = 'en' + else + msg_locale = motd_files[id].keys.first + end + files << { + "id" => id, + "locale" => msg_locale, + "text" => motd_files[id][msg_locale] + } + end + files.sort! {|a,b| a["id"].to_i <=> b["id"].to_i } + return files + end + + # + # returns messages of the day as a hash: + # { "1": {"en": "message"}, "2": {"en": "message"} } + # + def motd_files + if motd_changed? || @motd_files.nil? + @motd_files = load_motd_files + else + @motd_files + end + end + + def motd_changed? + newest = Dir.glob(File.join(motd_dir, '*.{html,md}')).collect{|file| File.mtime(file)}.max + if @timestamp.nil? + @timestamp = newest + return true + elsif @timestamp < newest + @timestamp = newest + return true + else + return false + end + end + + def load_motd_files + files = {} + Dir.glob(File.join(motd_dir, '*.{html,md}')).each do |file| + id, locale, msg = parse_motd_file(file) + next unless id + files[id] ||= {} + files[id][locale] = msg + end + files + end + + def parse_motd_file(file) + id, locale, ext = File.basename(file).split('.') + if id.nil? || locale.nil? || ext.nil? || id.to_i.to_s != id || !['md', 'html'].include?(ext) + Rails.logger.error "ERROR: Could not parse MOTD file #{file}" + return nil + end + contents = File.read(file) + if ext == "md" + msg = RDiscount.new(contents, :autolink).to_html + elsif ext == "html" + msg = File.read(file) + end + return id, locale, msg + end + + def motd_dir + File.join(APP_CONFIG['customization_directory'], 'motd') + end + + end +end diff --git a/app/controllers/api/services_controller.rb b/app/controllers/api/services_controller.rb new file mode 100644 index 0000000..da2774b --- /dev/null +++ b/app/controllers/api/services_controller.rb @@ -0,0 +1,8 @@ +class Api::ServicesController < ApiController + + before_filter :require_login, :unless => :anonymous_access_allowed? + + def show + respond_with current_user.effective_service_level + end +end diff --git a/app/controllers/api/sessions_controller.rb b/app/controllers/api/sessions_controller.rb new file mode 100644 index 0000000..c8deb7a --- /dev/null +++ b/app/controllers/api/sessions_controller.rb @@ -0,0 +1,44 @@ +module Api + class SessionsController < ApiController + + before_filter :require_login, only: :destroy + + def new + @session = Session.new + if authentication_errors + @errors = authentication_errors + render :status => 422 + end + end + + def create + logout if logged_in? + if params['A'] + authenticate! + else + @user = User.find_by_login(params['login']) + render :json => {salt: @user.salt} + end + end + + def update + authenticate! + @token = Token.create(:user_id => current_user.id) + session[:token] = @token.id + render :json => login_response + end + + def destroy + logout + head :no_content + end + + protected + + def login_response + handshake = session.delete(:handshake) || {} + handshake.to_hash.merge(:id => current_user.id, :token => @token.to_s) + end + + end +end diff --git a/app/controllers/api/smtp_certs_controller.rb b/app/controllers/api/smtp_certs_controller.rb new file mode 100644 index 0000000..d9eab7d --- /dev/null +++ b/app/controllers/api/smtp_certs_controller.rb @@ -0,0 +1,42 @@ +class Api::SmtpCertsController < ApiController + + before_filter :require_login + before_filter :require_email_account + before_filter :fetch_identity + before_filter :require_enabled + + # POST /1/smtp_cert + def create + @cert = ClientCertificate.new common_name: current_user.email_address + @identity.register_cert(@cert) + @identity.save + render text: @cert.to_s, content_type: 'text/plain' + end + + protected + + # + # Filters + # + + def require_email_account + access_denied unless service_level.provides? 'email' + end + + def require_enabled + access_denied unless current_user.enabled? + end + + def fetch_identity + @identity = current_user.identity + end + + # + # Helper methods + # + + def service_level + current_user.effective_service_level + end + +end diff --git a/app/controllers/api/users_controller.rb b/app/controllers/api/users_controller.rb new file mode 100644 index 0000000..e64d21f --- /dev/null +++ b/app/controllers/api/users_controller.rb @@ -0,0 +1,83 @@ +module Api + class UsersController < ApiController + include ControllerExtension::FetchUser + + # allow optional access to this controller using API auth tokens: + before_filter :token_authenticate + + before_filter :fetch_user, :only => [:update, :destroy] + before_filter :require_monitor, :only => [:index, :show] + before_filter :require_login, :only => [:index, :update, :destroy] + + respond_to :json + + # used for autocomplete for admins in the web ui + def index + if params[:query] + @users = User.login_starts_with(params[:query]) + respond_with @users.map(&:login).sort + else + render :json => {'error' => 'query required', 'status' => :unprocessable_entity} + end + end + + def show + if params[:login] + @user = User.find_by_login(params[:login]) + elsif params[:id] + @user = User.find(params[:id]) + end + if @user + respond_with @user + else + not_found + end + end + + def create + if current_user.is_monitor? + create_test_account + elsif APP_CONFIG[:allow_registration] + create_account + else + head :forbidden + end + end + + def update + @user.account.update params[:user] + respond_with @user + end + + def destroy + destroy_identity = current_user.is_monitor? || params[:identities] == "destroy" + @user.account.destroy(destroy_identity) + if @user == current_user + logout + end + render :json => {'success' => 'user deleted'} + end + + private + + # tester auth can only create test users. + def create_test_account + if User::is_test?(params[:user][:login]) + @user = Account.create(params[:user], :invite_required => false) + respond_with @user + else + head :forbidden + end + end + + def create_account + if APP_CONFIG[:allow_registration] + @user = Account.create(params[:user]) + respond_with @user # return ID instead? + else + head :forbidden + end + end + + end +end diff --git a/app/controllers/v1/certs_controller.rb b/app/controllers/v1/certs_controller.rb deleted file mode 100644 index ffa6e35..0000000 --- a/app/controllers/v1/certs_controller.rb +++ /dev/null @@ -1,31 +0,0 @@ -class V1::CertsController < ApiController - - before_filter :require_login, :unless => :anonymous_access_allowed? - before_filter :require_enabled - - # GET /cert - # deprecated - we actually create a new cert and that can - # be reflected in the action. GET /cert will eventually go - # away and be replaced by POST /cert - def show - create - end - - # POST /cert - def create - @cert = ClientCertificate.new(:prefix => service_level.cert_prefix) - render text: @cert.to_s, content_type: 'text/plain' - end - - protected - - def require_enabled - if !current_user.is_anonymous? && !current_user.enabled? - access_denied - end - end - - def service_level - current_user.effective_service_level - end -end diff --git a/app/controllers/v1/configs_controller.rb b/app/controllers/v1/configs_controller.rb deleted file mode 100644 index f0b284e..0000000 --- a/app/controllers/v1/configs_controller.rb +++ /dev/null @@ -1,37 +0,0 @@ -class V1::ConfigsController < ApiController - include ControllerExtension::JsonFile - - before_filter :require_login, :unless => :anonymous_access_allowed? - before_filter :sanitize_id, only: :show - - def index - render json: {services: service_paths} - end - - def show - send_file lookup_file - end - - protected - - SERVICE_IDS = { - soledad: "soledad-service", - eip: "eip-service", - smtp: "smtp-service" - } - - def service_paths - Hash[SERVICE_IDS.map{|k,v| [k,"/1/configs/#{v}.json"] } ] - end - - def sanitize_id - @id = params[:id].downcase - access_denied unless SERVICE_IDS.values.include? @id - end - - def lookup_file - path = APP_CONFIG[:config_file_paths][@id] - not_found if path.blank? - Rails.root.join path - end -end diff --git a/app/controllers/v1/identities_controller.rb b/app/controllers/v1/identities_controller.rb deleted file mode 100644 index 4efd1f5..0000000 --- a/app/controllers/v1/identities_controller.rb +++ /dev/null @@ -1,16 +0,0 @@ -module V1 - class IdentitiesController < ApiController - before_filter :token_authenticate - before_filter :require_monitor - - def show - @identity = Identity.find_by_address(params[:id]) - if @identity - respond_with @identity - else - render_not_found - end - end - - end -end diff --git a/app/controllers/v1/messages_controller.rb b/app/controllers/v1/messages_controller.rb deleted file mode 100644 index c0ca0c7..0000000 --- a/app/controllers/v1/messages_controller.rb +++ /dev/null @@ -1,119 +0,0 @@ -module V1 - class MessagesController < ApiController - - before_filter :require_login - - def index - if Dir.exist?(motd_dir) - if !CommonLanguages::available_code?(params[:locale]) - locale = 'en' - else - locale = params[:locale] - end - render json: motd_files_for_locale(locale) - else - render json: [] - end - end - - # disable per-user messages for now, not supported in the client - #def update - # if message = Message.find(params[:id]) - # message.mark_as_read_by(current_user) - # message.save - # render json: success(:marked_as_read) - # else - # render json: error(:not_found), status: :not_found - # end - #end - - private - - # - # returns list of messages, for example: - # - # [ - # {"id": 1, "locale": "en", "text": ""}, - # {"id": 2, "locale": "en", "text": ""} - # ] - # - # Each message is present only once, using the best choice - # for the locale. The order is determined by the id. - # - def motd_files_for_locale(locale) - files = [] - motd_files.keys.each do |id| - if motd_files[id].key?(locale) - msg_locale = locale - elsif motd_files[id].key?('en') - msg_locale = 'en' - else - msg_locale = motd_files[id].keys.first - end - files << { - "id" => id, - "locale" => msg_locale, - "text" => motd_files[id][msg_locale] - } - end - files.sort! {|a,b| a["id"].to_i <=> b["id"].to_i } - return files - end - - # - # returns messages of the day as a hash: - # { "1": {"en": "message"}, "2": {"en": "message"} } - # - def motd_files - if motd_changed? || @motd_files.nil? - @motd_files = load_motd_files - else - @motd_files - end - end - - def motd_changed? - newest = Dir.glob(File.join(motd_dir, '*.{html,md}')).collect{|file| File.mtime(file)}.max - if @timestamp.nil? - @timestamp = newest - return true - elsif @timestamp < newest - @timestamp = newest - return true - else - return false - end - end - - def load_motd_files - files = {} - Dir.glob(File.join(motd_dir, '*.{html,md}')).each do |file| - id, locale, msg = parse_motd_file(file) - next unless id - files[id] ||= {} - files[id][locale] = msg - end - files - end - - def parse_motd_file(file) - id, locale, ext = File.basename(file).split('.') - if id.nil? || locale.nil? || ext.nil? || id.to_i.to_s != id || !['md', 'html'].include?(ext) - Rails.logger.error "ERROR: Could not parse MOTD file #{file}" - return nil - end - contents = File.read(file) - if ext == "md" - msg = RDiscount.new(contents, :autolink).to_html - elsif ext == "html" - msg = File.read(file) - end - return id, locale, msg - end - - def motd_dir - File.join(APP_CONFIG['customization_directory'], 'motd') - end - - end -end diff --git a/app/controllers/v1/services_controller.rb b/app/controllers/v1/services_controller.rb deleted file mode 100644 index 523eb44..0000000 --- a/app/controllers/v1/services_controller.rb +++ /dev/null @@ -1,8 +0,0 @@ -class V1::ServicesController < ApiController - - before_filter :require_login, :unless => :anonymous_access_allowed? - - def show - respond_with current_user.effective_service_level - end -end diff --git a/app/controllers/v1/sessions_controller.rb b/app/controllers/v1/sessions_controller.rb deleted file mode 100644 index a343d9b..0000000 --- a/app/controllers/v1/sessions_controller.rb +++ /dev/null @@ -1,44 +0,0 @@ -module V1 - class SessionsController < ApiController - - before_filter :require_login, only: :destroy - - def new - @session = Session.new - if authentication_errors - @errors = authentication_errors - render :status => 422 - end - end - - def create - logout if logged_in? - if params['A'] - authenticate! - else - @user = User.find_by_login(params['login']) - render :json => {salt: @user.salt} - end - end - - def update - authenticate! - @token = Token.create(:user_id => current_user.id) - session[:token] = @token.id - render :json => login_response - end - - def destroy - logout - head :no_content - end - - protected - - def login_response - handshake = session.delete(:handshake) || {} - handshake.to_hash.merge(:id => current_user.id, :token => @token.to_s) - end - - end -end diff --git a/app/controllers/v1/smtp_certs_controller.rb b/app/controllers/v1/smtp_certs_controller.rb deleted file mode 100644 index 5760645..0000000 --- a/app/controllers/v1/smtp_certs_controller.rb +++ /dev/null @@ -1,42 +0,0 @@ -class V1::SmtpCertsController < ApiController - - before_filter :require_login - before_filter :require_email_account - before_filter :fetch_identity - before_filter :require_enabled - - # POST /1/smtp_cert - def create - @cert = ClientCertificate.new common_name: current_user.email_address - @identity.register_cert(@cert) - @identity.save - render text: @cert.to_s, content_type: 'text/plain' - end - - protected - - # - # Filters - # - - def require_email_account - access_denied unless service_level.provides? 'email' - end - - def require_enabled - access_denied unless current_user.enabled? - end - - def fetch_identity - @identity = current_user.identity - end - - # - # Helper methods - # - - def service_level - current_user.effective_service_level - end - -end diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb deleted file mode 100644 index 6640d10..0000000 --- a/app/controllers/v1/users_controller.rb +++ /dev/null @@ -1,83 +0,0 @@ -module V1 - class UsersController < ApiController - include ControllerExtension::FetchUser - - # allow optional access to this controller using API auth tokens: - before_filter :token_authenticate - - before_filter :fetch_user, :only => [:update, :destroy] - before_filter :require_monitor, :only => [:index, :show] - before_filter :require_login, :only => [:index, :update, :destroy] - - respond_to :json - - # used for autocomplete for admins in the web ui - def index - if params[:query] - @users = User.login_starts_with(params[:query]) - respond_with @users.map(&:login).sort - else - render :json => {'error' => 'query required', 'status' => :unprocessable_entity} - end - end - - def show - if params[:login] - @user = User.find_by_login(params[:login]) - elsif params[:id] - @user = User.find(params[:id]) - end - if @user - respond_with @user - else - not_found - end - end - - def create - if current_user.is_monitor? - create_test_account - elsif APP_CONFIG[:allow_registration] - create_account - else - head :forbidden - end - end - - def update - @user.account.update params[:user] - respond_with @user - end - - def destroy - destroy_identity = current_user.is_monitor? || params[:identities] == "destroy" - @user.account.destroy(destroy_identity) - if @user == current_user - logout - end - render :json => {'success' => 'user deleted'} - end - - private - - # tester auth can only create test users. - def create_test_account - if User::is_test?(params[:user][:login]) - @user = Account.create(params[:user], :invite_required => false) - respond_with @user - else - head :forbidden - end - end - - def create_account - if APP_CONFIG[:allow_registration] - @user = Account.create(params[:user]) - respond_with @user # return ID instead? - else - head :forbidden - end - end - - end -end diff --git a/app/views/api/sessions/new.json.erb b/app/views/api/sessions/new.json.erb new file mode 100644 index 0000000..36154b8 --- /dev/null +++ b/app/views/api/sessions/new.json.erb @@ -0,0 +1,3 @@ +{ +"errors": <%= raw @errors.to_json %> +} diff --git a/app/views/v1/sessions/new.json.erb b/app/views/v1/sessions/new.json.erb deleted file mode 100644 index 36154b8..0000000 --- a/app/views/v1/sessions/new.json.erb +++ /dev/null @@ -1,3 +0,0 @@ -{ -"errors": <%= raw @errors.to_json %> -} -- cgit v1.2.3 From 83f59164fc069f2593cf6babbc18638d9a68c9a3 Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 18 May 2016 20:21:04 +0200 Subject: features for API version 2 - keep old ones Now we test both api versions. We want this for backwards compatibility. --- app/controllers/api/configs_controller.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/controllers/api/configs_controller.rb b/app/controllers/api/configs_controller.rb index 55ceb4f..0f9b8a6 100644 --- a/app/controllers/api/configs_controller.rb +++ b/app/controllers/api/configs_controller.rb @@ -21,7 +21,11 @@ class Api::ConfigsController < ApiController } def service_paths - Hash[SERVICE_IDS.map{|k,v| [k,"/1/configs/#{v}.json"] } ] + Hash[SERVICE_IDS.map{|k,v| [k,"/#{api_version}/configs/#{v}.json"] } ] + end + + def api_version + ["1", "2"].include?(params[:version]) ? params[:version] : "2" end def sanitize_id -- cgit v1.2.3 From 0ba0eb633e8c24086405c53f3d8a8e747f3382e4 Mon Sep 17 00:00:00 2001 From: Azul Date: Sun, 22 May 2016 21:12:42 +0200 Subject: restrict user_params in user_controller Actually this should live in a service_level_controller. For now fix the security issue. --- app/controllers/users_controller.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 1404b0e..225584f 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -39,7 +39,7 @@ class UsersController < ApplicationController ## added so updating service level works, but not sure we will actually want this. also not sure that this is place to prevent user from updating own effective service level, but here as placeholder: def update - @user.update_attributes(params[:user]) unless (!admin? and params[:user][:effective_service_level]) + @user.update_attributes(user_params) if @user.valid? flash[:notice] = I18n.t(:changes_saved) end @@ -79,4 +79,11 @@ class UsersController < ApplicationController end end + def user_params + if admin? + params.require(:user).permit(:effective_service_level) + else + params.require(:user).permit(:password, :password_confirmation) + end + end end -- cgit v1.2.3 From 7f0c42a5fa6d6c1952e53b9b73bad0202f746f3e Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 23 May 2016 11:59:41 +0200 Subject: cleanup: remove service level code from users_controller There's no route to this right now and it also seems to be tested nowhere. Since i am about to split up the users_controller let's get rid of this and put it in the place we want it once we actually finish the implementation --- app/controllers/users_controller.rb | 11 +---------- app/views/users/_change_service_level.html.haml | 15 ++++++++++----- 2 files changed, 11 insertions(+), 15 deletions(-) (limited to 'app') diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 225584f..2816a64 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -8,7 +8,7 @@ class UsersController < ApplicationController before_filter :require_login, :except => [:new] before_filter :redirect_if_logged_in, :only => [:new] before_filter :require_admin, :only => [:index, :deactivate, :enable] - before_filter :fetch_user, :only => [:show, :edit, :update, :destroy, :deactivate, :enable] + before_filter :fetch_user, :only => [:show, :edit, :destroy, :deactivate, :enable] before_filter :require_registration_allowed, only: :new respond_to :html @@ -37,15 +37,6 @@ class UsersController < ApplicationController def edit end - ## added so updating service level works, but not sure we will actually want this. also not sure that this is place to prevent user from updating own effective service level, but here as placeholder: - def update - @user.update_attributes(user_params) - if @user.valid? - flash[:notice] = I18n.t(:changes_saved) - end - respond_with @user, :location => edit_user_path(@user) - end - def deactivate @user.account.disable flash[:notice] = I18n.t("actions.user_disabled_message", username: @user.username) diff --git a/app/views/users/_change_service_level.html.haml b/app/views/users/_change_service_level.html.haml index a2e9956..32ea8c0 100644 --- a/app/views/users/_change_service_level.html.haml +++ b/app/views/users/_change_service_level.html.haml @@ -1,8 +1,13 @@ --# TODO: probably won't want here, but here for now. Also, we will need way to ensure payment if they pick a non-free plan. --# --# SERVICE LEVEL --# -- if APP_CONFIG[:service_levels] +:ruby + # DISABLED! this form points to a route that does not exist. + # It's a draft for implementing service levels. + # TODO: probably won't want here, but here for now. + # We will need way to ensure payment for a non-free plan. + # + # SERVICE LEVEL + # + # +- if APP_CONFIG[:service_levels] && false - form_options = {:html => {:class => user_form_class('form-horizontal'), :id => 'update_service_level', :data => {token: session[:token]}}, :validate => true} = simple_form_for @user, form_options do |f| %legend= t(:service_level) -- cgit v1.2.3 From f47fc9d6522886cf81cfea26ec1f396219c539ba Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 23 May 2016 12:17:31 +0200 Subject: move signup from users to account_controller There was a lot of special case handling going on in the users_controller for this. Lot simpler this way. --- app/controllers/account_controller.rb | 17 +++++++++++++++++ app/controllers/users_controller.rb | 14 +------------- app/views/account/new.html.haml | 27 +++++++++++++++++++++++++++ app/views/sessions/_warnings.html.haml | 12 ++++++++++++ app/views/sessions/new.html.haml | 2 +- app/views/users/_warnings.html.haml | 12 ------------ app/views/users/new.html.haml | 27 --------------------------- 7 files changed, 58 insertions(+), 53 deletions(-) create mode 100644 app/controllers/account_controller.rb create mode 100644 app/views/account/new.html.haml create mode 100644 app/views/sessions/_warnings.html.haml delete mode 100644 app/views/users/_warnings.html.haml delete mode 100644 app/views/users/new.html.haml (limited to 'app') diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb new file mode 100644 index 0000000..ee7cca4 --- /dev/null +++ b/app/controllers/account_controller.rb @@ -0,0 +1,17 @@ +class AccountController < ApplicationController + + before_filter :require_registration_allowed + before_filter :redirect_if_logged_in + + def new + @user = User.new + end + + protected + + def require_registration_allowed + unless APP_CONFIG[:allow_registration] + redirect_to home_path + end + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2816a64..4d198b9 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -5,11 +5,9 @@ class UsersController < ApplicationController include ControllerExtension::FetchUser - before_filter :require_login, :except => [:new] - before_filter :redirect_if_logged_in, :only => [:new] + before_filter :require_login before_filter :require_admin, :only => [:index, :deactivate, :enable] before_filter :fetch_user, :only => [:show, :edit, :destroy, :deactivate, :enable] - before_filter :require_registration_allowed, only: :new respond_to :html @@ -27,10 +25,6 @@ class UsersController < ApplicationController @users = @users.limit(100) end - def new - @user = User.new - end - def show end @@ -64,12 +58,6 @@ class UsersController < ApplicationController protected - def require_registration_allowed - unless APP_CONFIG[:allow_registration] - redirect_to home_path - end - end - def user_params if admin? params.require(:user).permit(:effective_service_level) diff --git a/app/views/account/new.html.haml b/app/views/account/new.html.haml new file mode 100644 index 0000000..d40259e --- /dev/null +++ b/app/views/account/new.html.haml @@ -0,0 +1,27 @@ +-# +-# This form is handled entirely by javascript +-# Please take care when changing element ids. +-# +-# The form is hidden when no js is available +-# to prevent submission in the clear. +-# + +- form_options = {url: '/not-used', html: {id: 'new_user', class: user_form_class('form-horizontal'), style: 'display:none'}, validate: true} + +.col-md-1 +.col-md-9 + %h2=t :signup + .lead=t :signup_info + = render "sessions/warnings" + = simple_form_for(@user, form_options) do |f| + = f.input :login, :label => t(:username), :required => false, :input_html => { :id => :srp_username } + = f.input :password, :label => t(:password), :required => false, :validate => true, :input_html => { :id => :srp_password } + = f.input :password_confirmation, :label => t(:password_confirmation), :required => false, :validate => true, :input_html => { :id => :srp_password_confirmation } + + - if APP_CONFIG[:invite_required] + = f.input :invite_code, :label => t(:invite_code), :input_html => { :id => :srp_invite_code } + - else + = f.input :invite_code, :as => "hidden", :input_html => { :value => " ", :id => :srp_invite_code } + + = f.button :wrapped, cancel: home_path +-# diff --git a/app/views/sessions/_warnings.html.haml b/app/views/sessions/_warnings.html.haml new file mode 100644 index 0000000..baf80a4 --- /dev/null +++ b/app/views/sessions/_warnings.html.haml @@ -0,0 +1,12 @@ +%noscript + %div.alert.alert-error=t :js_required_html +#cookie_warning.alert.alert-error{:style => "display:none"} + =t :cookie_disabled_warning +:javascript + document.cookie = "testing=cookies_enabled; path=/"; + if(document.cookie.indexOf("testing=cookies_enabled") < 0) + { + document.getElementById('cookie_warning').style.display = 'block'; + } else { + document.getElementById('cookie_warning').style.display = 'none'; + } diff --git a/app/views/sessions/new.html.haml b/app/views/sessions/new.html.haml index 942c485..6695123 100644 --- a/app/views/sessions/new.html.haml +++ b/app/views/sessions/new.html.haml @@ -2,7 +2,7 @@ .col-md-9 %h2=t :login .lead=t :login_info - = render :partial => 'users/warnings' + = render 'warnings' = simple_form_for [:api, @session], validate: true, html: { id: :new_session, class: 'form-horizontal hidden js-show', style: "display:none;" } do |f| = f.input :login, :required => false, :label => t(:username), :input_html => { :id => :srp_username } = f.input :password, :required => false, :input_html => { :id => :srp_password } diff --git a/app/views/users/_warnings.html.haml b/app/views/users/_warnings.html.haml deleted file mode 100644 index baf80a4..0000000 --- a/app/views/users/_warnings.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -%noscript - %div.alert.alert-error=t :js_required_html -#cookie_warning.alert.alert-error{:style => "display:none"} - =t :cookie_disabled_warning -:javascript - document.cookie = "testing=cookies_enabled; path=/"; - if(document.cookie.indexOf("testing=cookies_enabled") < 0) - { - document.getElementById('cookie_warning').style.display = 'block'; - } else { - document.getElementById('cookie_warning').style.display = 'none'; - } diff --git a/app/views/users/new.html.haml b/app/views/users/new.html.haml deleted file mode 100644 index 1b257d9..0000000 --- a/app/views/users/new.html.haml +++ /dev/null @@ -1,27 +0,0 @@ --# --# This form is handled entirely by javascript --# Please take care when changing element ids. --# --# The form is hidden when no js is available --# to prevent submission in the clear. --# - -- form_options = {url: '/not-used', html: {id: 'new_user', class: user_form_class('form-horizontal'), style: 'display:none'}, validate: true} - -.col-md-1 -.col-md-9 - %h2=t :signup - .lead=t :signup_info - = render :partial => 'warnings' - = simple_form_for(@user, form_options) do |f| - = f.input :login, :label => t(:username), :required => false, :input_html => { :id => :srp_username } - = f.input :password, :label => t(:password), :required => false, :validate => true, :input_html => { :id => :srp_password } - = f.input :password_confirmation, :label => t(:password_confirmation), :required => false, :validate => true, :input_html => { :id => :srp_password_confirmation } - - - if APP_CONFIG[:invite_required] - = f.input :invite_code, :label => t(:invite_code), :input_html => { :id => :srp_invite_code } - - else - = f.input :invite_code, :as => "hidden", :input_html => { :value => " ", :id => :srp_invite_code } - - = f.button :wrapped, cancel: home_path --# -- cgit v1.2.3 From ad208ae3625e67c2551744df7906ebdda94d215e Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 23 May 2016 12:27:14 +0200 Subject: rename destroy_identity to release_handles This expresses the intent rather than the implementation. Also replace temp with query refactoring. --- app/controllers/api/users_controller.rb | 7 +++++-- app/models/account.rb | 10 ++++------ 2 files changed, 9 insertions(+), 8 deletions(-) (limited to 'app') diff --git a/app/controllers/api/users_controller.rb b/app/controllers/api/users_controller.rb index e64d21f..c79a729 100644 --- a/app/controllers/api/users_controller.rb +++ b/app/controllers/api/users_controller.rb @@ -50,8 +50,7 @@ module Api end def destroy - destroy_identity = current_user.is_monitor? || params[:identities] == "destroy" - @user.account.destroy(destroy_identity) + @user.account.destroy(release_handles) if @user == current_user logout end @@ -60,6 +59,10 @@ module Api private + def release_handles + current_user.is_monitor? || params[:identities] == "destroy" + end + # tester auth can only create test users. def create_test_account if User::is_test?(params[:user][:login]) diff --git a/app/models/account.rb b/app/models/account.rb index 7310250..d722caa 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -69,15 +69,13 @@ class Account @user.refresh_identity end - def destroy(destroy_identity=false) + def destroy(release_handles=false) return unless @user if !@user.is_tmp? - if destroy_identity == false - @user.identities.each do |id| + @user.identities.each do |id| + if release_handles == false id.orphan! - end - else - @user.identities.each do |id| + else id.destroy end end -- cgit v1.2.3 From 638acc59a241e141cf0fc9ccbf4e3c5578b98f0c Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 4 Jul 2016 20:19:21 +0200 Subject: Fix db:migrate and similar tasks We saw errors from duplicate loading of LocalEmail and LoginFormatValidation. The latter resulted in a crash. In an attempt to ensure all subclasses of Couchrest::Model::Base are loaded Couchrest::Model::Utils::Migrate requires all files in app/models. We have an extension that does the same for the engines. During this process LoginFormatValidation and LocalEmail were autoloaded when 'identity' was required. Afterwards they were required again. It looks like rails' autoload mechanism does not play nicely with require. So to make sure they are not autoloaded first move the concerns and helper classes into the lib directory and require them explicitly. --- app/models/email.rb | 31 ---------------- app/models/identity.rb | 2 ++ app/models/local_email.rb | 66 ----------------------------------- app/models/login_format_validation.rb | 21 ----------- app/models/user.rb | 2 ++ 5 files changed, 4 insertions(+), 118 deletions(-) delete mode 100644 app/models/email.rb delete mode 100644 app/models/local_email.rb delete mode 100644 app/models/login_format_validation.rb (limited to 'app') diff --git a/app/models/email.rb b/app/models/email.rb deleted file mode 100644 index 4090275..0000000 --- a/app/models/email.rb +++ /dev/null @@ -1,31 +0,0 @@ -class Email < String - include ActiveModel::Validations - - validates :email, - :format => { - :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/, #local part of email is case-sensitive, so allow uppercase letter. - :message => "needs to be a valid email address" - } - - # Make sure we can call Email.new(nil) and get an invalid email address - def initialize(s) - super(s.to_s) - end - - def to_partial_path - "emails/email" - end - - def to_param - to_s - end - - def email - self - end - - def handle - self.split('@').first - end - -end diff --git a/app/models/identity.rb b/app/models/identity.rb index f987e4e..92f8f7a 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -1,3 +1,5 @@ +require 'login_format_validation' +require 'local_email' # # Identity states: # diff --git a/app/models/local_email.rb b/app/models/local_email.rb deleted file mode 100644 index ded7baf..0000000 --- a/app/models/local_email.rb +++ /dev/null @@ -1,66 +0,0 @@ -class LocalEmail < Email - - BLACKLIST_FROM_RFC2142 = [ - 'postmaster', 'hostmaster', 'domainadmin', 'webmaster', 'www', - 'abuse', 'noc', 'security', 'usenet', 'news', 'uucp', - 'ftp', 'sales', 'marketing', 'support', 'info' - ] - - def self.domain - APP_CONFIG[:domain] - end - - validates :email, - :format => { - :with => /@#{domain}\Z/i, - :message => "needs to end in @#{domain}" - } - - validate :handle_allowed - - def initialize(s) - super - append_domain_if_needed - end - - def to_key - [handle] - end - - def domain - LocalEmail.domain - end - - protected - - def append_domain_if_needed - unless self.index('@') - self << '@' + domain - end - end - - def handle_allowed - errors.add(:handle, "is reserved.") if handle_reserved? - end - - def handle_reserved? - # *ARRAY in a case statement tests if ARRAY includes the handle. - case handle - when *APP_CONFIG[:handle_blacklist] - true - when *APP_CONFIG[:handle_whitelist] - false - when *BLACKLIST_FROM_RFC2142 - true - else - handle_in_passwd? - end - end - - def handle_in_passwd? - Etc.getpwnam(handle).present? - rescue ArgumentError - # handle was not found - return false - end -end diff --git a/app/models/login_format_validation.rb b/app/models/login_format_validation.rb deleted file mode 100644 index c1fcf70..0000000 --- a/app/models/login_format_validation.rb +++ /dev/null @@ -1,21 +0,0 @@ -module LoginFormatValidation - extend ActiveSupport::Concern - - #TODO: Probably will replace this. Playing with using it for aliases too, but won't want it connected to login field. - - included do - # Have multiple regular expression validations so we can get specific error messages: - validates :login, - :format => { :with => /\A.{2,}\z/, - :message => "Must have at least two characters"} - validates :login, - :format => { :with => /\A[a-z\d_\.-]+\z/, - :message => "Only lowercase letters, digits, . - and _ allowed."} - validates :login, - :format => { :with => /\A[a-z].*\z/, - :message => "Must begin with a lowercase letter"} - validates :login, - :format => { :with => /\A.*[a-z\d]\z/, - :message => "Must end with a letter or digit"} - end -end diff --git a/app/models/user.rb b/app/models/user.rb index cb093cf..206c0df 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,3 +1,5 @@ +require 'login_format_validation' + class User < CouchRest::Model::Base include LoginFormatValidation -- cgit v1.2.3 From 87e467530b686c41ae0b9a8fbf3ed571680bcb74 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 12 Jul 2016 17:30:02 +0200 Subject: bugfix: require local email in user model --- app/models/user.rb | 1 + 1 file changed, 1 insertion(+) (limited to 'app') diff --git a/app/models/user.rb b/app/models/user.rb index 206c0df..704700b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,4 +1,5 @@ require 'login_format_validation' +require 'local_email' class User < CouchRest::Model::Base include LoginFormatValidation -- cgit v1.2.3