summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorAzul <azul@riseup.net>2016-10-20 14:39:33 +0200
committerAzul <azul@riseup.net>2016-10-20 14:39:33 +0200
commitb97daaed9b513006ace7e8eb5232a2211e965e77 (patch)
treee27002e8368e92410e5d4af2a945260c2ea6e2d1 /app
parentc6c4d9fd10b8ca8e24889112727e44c9bf68dd60 (diff)
parent6eb2dae802e5453e2a4361ab28f614cce9294f4c (diff)
Merge remote-tracking branch 'origin/develop'
We'll only use the master branch for development from now on.
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/Avatar_Pic.pngbin0 -> 4298 bytes
-rw-r--r--app/assets/javascripts/application.js1
-rw-r--r--app/assets/javascripts/leap.js2
-rw-r--r--app/assets/javascripts/typeahead.bundle.js2451
-rw-r--r--app/assets/javascripts/users.js2
-rw-r--r--app/assets/stylesheets/application.scss5
-rw-r--r--app/assets/stylesheets/backport.scss24
-rw-r--r--app/assets/stylesheets/leap.scss23
-rw-r--r--app/assets/stylesheets/twitter.scss78
-rw-r--r--app/controllers/account_controller.rb19
-rw-r--r--app/controllers/account_settings_controller.rb0
-rw-r--r--app/controllers/api/certs_controller.rb (renamed from app/controllers/v1/certs_controller.rb)2
-rw-r--r--app/controllers/api/configs_controller.rb (renamed from app/controllers/v1/configs_controller.rb)14
-rw-r--r--app/controllers/api/identities_controller.rb (renamed from app/controllers/v1/identities_controller.rb)4
-rw-r--r--app/controllers/api/messages_controller.rb (renamed from app/controllers/v1/messages_controller.rb)2
-rw-r--r--app/controllers/api/services_controller.rb (renamed from app/controllers/v1/services_controller.rb)4
-rw-r--r--app/controllers/api/sessions_controller.rb (renamed from app/controllers/v1/sessions_controller.rb)3
-rw-r--r--app/controllers/api/smtp_certs_controller.rb (renamed from app/controllers/v1/smtp_certs_controller.rb)2
-rw-r--r--app/controllers/api/users_controller.rb (renamed from app/controllers/v1/users_controller.rb)19
-rw-r--r--app/controllers/api_controller.rb1
-rw-r--r--app/controllers/application_controller.rb13
-rw-r--r--app/controllers/controller_extension/fetch_user.rb2
-rw-r--r--app/controllers/controller_extension/json_file.rb23
-rw-r--r--app/controllers/errors_controller.rb2
-rw-r--r--app/controllers/home_controller.rb2
-rw-r--r--app/controllers/pages_controller.rb2
-rw-r--r--app/controllers/sessions_controller.rb3
-rw-r--r--app/controllers/static_config_controller.rb11
-rw-r--r--app/controllers/users_controller.rb28
-rw-r--r--app/helpers/application_helper.rb3
-rw-r--r--app/helpers/link_helper.rb4
-rw-r--r--app/helpers/navigation_helper.rb6
-rw-r--r--app/helpers/twitter_helper.rb88
-rw-r--r--app/models/account.rb10
-rw-r--r--app/models/api_monitor_user.rb11
-rw-r--r--app/models/api_user.rb13
-rw-r--r--app/models/email.rb31
-rw-r--r--app/models/identity.rb2
-rw-r--r--app/models/local_email.rb66
-rw-r--r--app/models/login_format_validation.rb21
-rw-r--r--app/models/session.rb2
-rw-r--r--app/models/temporary_user.rb93
-rw-r--r--app/models/token.rb4
-rw-r--r--app/models/user.rb14
-rw-r--r--app/views/account/new.html.haml (renamed from app/views/users/new.html.haml)10
-rw-r--r--app/views/api/sessions/new.json.erb (renamed from app/views/v1/sessions/new.json.erb)0
-rw-r--r--app/views/common/_action_buttons.html.haml10
-rw-r--r--app/views/common/_download_button.html.haml8
-rw-r--r--app/views/common/_home_page_buttons.html.haml4
-rw-r--r--app/views/home/_content.html.haml43
-rw-r--r--app/views/home/_provider_message.html.haml2
-rw-r--r--app/views/layouts/_content.html.haml2
-rw-r--r--app/views/layouts/application.html.haml8
-rw-r--r--app/views/sessions/_warnings.html.haml (renamed from app/views/users/_warnings.html.haml)0
-rw-r--r--app/views/sessions/new.html.haml6
-rw-r--r--app/views/twitter/_index.html.erb34
-rw-r--r--app/views/users/_change_password.html.haml2
-rw-r--r--app/views/users/_change_pgp_key.html.haml2
-rw-r--r--app/views/users/_change_service_level.html.haml17
-rw-r--r--app/views/users/_contact_email.html.haml2
-rw-r--r--app/views/users/show.html.haml2
61 files changed, 2880 insertions, 382 deletions
diff --git a/app/assets/images/Avatar_Pic.png b/app/assets/images/Avatar_Pic.png
new file mode 100644
index 0000000..b5eebc8
--- /dev/null
+++ b/app/assets/images/Avatar_Pic.png
Binary files differ
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/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('<div class="alert alert-error"><a class="close" data-dismiss="alert">×</a><span>'+msg+'</span></div>');
}
+
+ClientSideValidations.formBuilders['SimpleForm::FormBuilder'].wrappers.bootstrap = ClientSideValidations.formBuilders['SimpleForm::FormBuilder'].wrappers["default"];
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: '<span class="' + c.wrapper + '"></span>',
+ menu: '<div class="' + c.menu + '"></div>'
+ };
+ }
+ 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 = $("<div>");
+ }
+ });
+ return Input;
+ function buildOverflowHelper($input) {
+ return $('<pre aria-hidden="true"></pre>').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 = $("<div>");
+ }
+ });
+ 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 $("<div>").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 : $("<div>").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 = $("<div>");
+ _.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 || $("<div>");
+ $menu = this.menu.$node || $("<div>");
+ $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
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/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 9cd3a55..f42044b 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -9,15 +9,14 @@
//
// import bootstrap.
//
+@import "bootstrap-sprockets";
@import "bootstrap";
-@import "bootstrap-responsive";
-// backport bootstrap 3.2 features
-@import "backport";
//
// LEAP web app specific overrides
//
@import "leap";
+@import "twitter";
// And finally bootswatch style itself
// @import "bootswatch/cerulean/bootswatch";
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;
- }
-}
diff --git a/app/assets/stylesheets/leap.scss b/app/assets/stylesheets/leap.scss
index abbfc88..8c4d702 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;
@@ -92,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 {
@@ -120,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
@@ -188,7 +187,7 @@ input, textarea {
font-weight: bold;
}
a {
- color: $textColor;
+ color: $text-color;
}
}
@@ -199,7 +198,7 @@ input, textarea {
}
.download {
a.btn {
- width: 15em;
+ width: 18em;
font-weight: bold;
small {
font-weight: normal;
@@ -228,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;
}
}
@@ -265,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%;
@@ -313,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;
}
}
diff --git a/app/assets/stylesheets/twitter.scss b/app/assets/stylesheets/twitter.scss
new file mode 100644
index 0000000..4419918
--- /dev/null
+++ b/app/assets/stylesheets/twitter.scss
@@ -0,0 +1,78 @@
+.twitter {
+ position: relative;
+}
+
+.twitter_header {
+ font-size: 16px;
+ text-align: left;
+ margin-bottom: 55px;
+ padding: 10px 8px;
+}
+
+.twitter_id {
+ position: absolute;
+}
+
+.twitter_image_frame {
+ display: block;
+ width: 40px;
+ height: 40px;
+ overflow: hidden;
+ position: absolute;
+ left: 0;
+ top: 0;
+ // background-image: url(/Avatar_Pic.png);
+ }
+
+.twitter_image_frame > img {
+ display: block;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 30;
+ width: 100%;
+ margin: auto;
+ }
+
+.twitter_name {
+ padding-left: 55px;
+ line-height: 20px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-size: 16px;
+}
+
+// Body = displays tweets
+.twitter_list {
+ box-sizing: border-box;
+}
+
+.tweet {
+ border-top-style: solid;
+ border-color: lightgrey;
+ padding: 10px 8px;
+}
+
+.tweet_text {
+box-sizing: border-box;
+}
+
+.tweet_text_date {
+ text-align: right;
+ padding-top: 4px;
+ font-size: 12px ;
+}
+
+.twitter_footer {
+ border-top-style: solid;
+ border-color: lightgrey;
+ padding: 10px 8px;
+ font-style: italic;
+ font-size: 12px;
+}
+
+.more_tweets_link {
+ text-align: right;
+ font-size: 12px;
+}
diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb
new file mode 100644
index 0000000..42e8983
--- /dev/null
+++ b/app/controllers/account_controller.rb
@@ -0,0 +1,19 @@
+class AccountController < ApplicationController
+
+ before_filter :require_registration_allowed
+ before_filter :redirect_if_logged_in
+
+ respond_to :html
+
+ 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/account_settings_controller.rb b/app/controllers/account_settings_controller.rb
deleted file mode 100644
index e69de29..0000000
--- a/app/controllers/account_settings_controller.rb
+++ /dev/null
diff --git a/app/controllers/v1/certs_controller.rb b/app/controllers/api/certs_controller.rb
index ffa6e35..46a84d3 100644
--- a/app/controllers/v1/certs_controller.rb
+++ b/app/controllers/api/certs_controller.rb
@@ -1,4 +1,4 @@
-class V1::CertsController < ApiController
+class Api::CertsController < ApiController
before_filter :require_login, :unless => :anonymous_access_allowed?
before_filter :require_enabled
diff --git a/app/controllers/v1/configs_controller.rb b/app/controllers/api/configs_controller.rb
index 4a6f455..0f9b8a6 100644
--- a/app/controllers/v1/configs_controller.rb
+++ b/app/controllers/api/configs_controller.rb
@@ -1,17 +1,15 @@
-class V1::ConfigsController < ApiController
+class Api::ConfigsController < ApiController
include ControllerExtension::JsonFile
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
@@ -23,7 +21,11 @@ class V1::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
@@ -34,6 +36,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
diff --git a/app/controllers/v1/identities_controller.rb b/app/controllers/api/identities_controller.rb
index 4efd1f5..de4910a 100644
--- a/app/controllers/v1/identities_controller.rb
+++ b/app/controllers/api/identities_controller.rb
@@ -1,8 +1,10 @@
-module V1
+module Api
class IdentitiesController < ApiController
before_filter :token_authenticate
before_filter :require_monitor
+ respond_to :json
+
def show
@identity = Identity.find_by_address(params[:id])
if @identity
diff --git a/app/controllers/v1/messages_controller.rb b/app/controllers/api/messages_controller.rb
index c0ca0c7..a69a40a 100644
--- a/app/controllers/v1/messages_controller.rb
+++ b/app/controllers/api/messages_controller.rb
@@ -1,4 +1,4 @@
-module V1
+module Api
class MessagesController < ApiController
before_filter :require_login
diff --git a/app/controllers/v1/services_controller.rb b/app/controllers/api/services_controller.rb
index 523eb44..58e129c 100644
--- a/app/controllers/v1/services_controller.rb
+++ b/app/controllers/api/services_controller.rb
@@ -1,7 +1,9 @@
-class V1::ServicesController < ApiController
+class Api::ServicesController < ApiController
before_filter :require_login, :unless => :anonymous_access_allowed?
+ respond_to :json
+
def show
respond_with current_user.effective_service_level
end
diff --git a/app/controllers/v1/sessions_controller.rb b/app/controllers/api/sessions_controller.rb
index a343d9b..178f86e 100644
--- a/app/controllers/v1/sessions_controller.rb
+++ b/app/controllers/api/sessions_controller.rb
@@ -1,7 +1,8 @@
-module V1
+module Api
class SessionsController < ApiController
before_filter :require_login, only: :destroy
+ respond_to :json
def new
@session = Session.new
diff --git a/app/controllers/v1/smtp_certs_controller.rb b/app/controllers/api/smtp_certs_controller.rb
index 5760645..d9eab7d 100644
--- a/app/controllers/v1/smtp_certs_controller.rb
+++ b/app/controllers/api/smtp_certs_controller.rb
@@ -1,4 +1,4 @@
-class V1::SmtpCertsController < ApiController
+class Api::SmtpCertsController < ApiController
before_filter :require_login
before_filter :require_email_account
diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/api/users_controller.rb
index 6640d10..709e076 100644
--- a/app/controllers/v1/users_controller.rb
+++ b/app/controllers/api/users_controller.rb
@@ -1,4 +1,4 @@
-module V1
+module Api
class UsersController < ApiController
include ControllerExtension::FetchUser
@@ -28,12 +28,20 @@ module V1
@user = User.find(params[:id])
end
if @user
- respond_with @user
+ respond_with user_response
else
not_found
end
end
+ def user_response
+ @user.to_hash.tap do |user_hash|
+ if @user == current_user
+ user_hash['is_admin'] = @user.is_admin?
+ end
+ end
+ end
+
def create
if current_user.is_monitor?
create_test_account
@@ -50,8 +58,7 @@ module V1
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 +67,10 @@ module V1
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/controllers/api_controller.rb b/app/controllers/api_controller.rb
index 70b3cac..95c8f57 100644
--- a/app/controllers/api_controller.rb
+++ b/app/controllers/api_controller.rb
@@ -1,7 +1,6 @@
class ApiController < ApplicationController
skip_before_filter :verify_authenticity_token
- respond_to :json
protected
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 079dc18..8d08a2c 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -4,13 +4,24 @@ class ApplicationController < ActionController::Base
before_filter :no_cache_header
before_filter :no_frame_header
before_filter :language_header
+
+ # UPGRADE: this won't be needed in Rails 5 anymore as it's the default
+ # behavior if a template is present but a different format would be
+ # rendered and that template is not present
+ before_filter :verify_request_format!, if: :mime_types_specified
+
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)
protected
+ def mime_types_specified
+ mimes = collect_mimes_from_class_level
+ mimes.present?
+ end
+
def default_error_handler(exc)
respond_to do |format|
format.json { render_json_error(exc) }
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/controllers/controller_extension/json_file.rb b/app/controllers/controller_extension/json_file.rb
index 6be919a..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.exists?(@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/errors_controller.rb b/app/controllers/errors_controller.rb
index d869ab5..80c270f 100644
--- a/app/controllers/errors_controller.rb
+++ b/app/controllers/errors_controller.rb
@@ -1,5 +1,7 @@
# We render http errors ourselves so we can customize them
class ErrorsController < ApplicationController
+ respond_to :html
+
# 404
def not_found
render status: 404
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 1d62178..86c36e9 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -1,6 +1,8 @@
class HomeController < ApplicationController
layout 'home'
+ respond_to :html
+
def index
if logged_in?
redirect_to current_user
diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb
index e0f39e3..b9c601a 100644
--- a/app/controllers/pages_controller.rb
+++ b/app/controllers/pages_controller.rb
@@ -2,7 +2,9 @@
# Render static pages
#
+
class PagesController < ApplicationController
+ respond_to :html
def show
@show_navigation = false
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 34d4f53..18e5216 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -1,6 +1,7 @@
class SessionsController < ApplicationController
before_filter :redirect_if_logged_in, :only => [:new]
+ respond_to :html, :json
def new
@session = Session.new
@@ -16,7 +17,7 @@ class SessionsController < ApplicationController
end
#
- # Warden will catch all 401s and run this instead:
+ # Warden will catch all 401s and triggers this action:
#
def unauthenticated
login_required
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/users_controller.rb b/app/controllers/users_controller.rb
index 1404b0e..0a0f551 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, :update, :destroy, :deactivate, :enable]
- before_filter :require_registration_allowed, only: :new
+ before_filter :fetch_user, :except => [:index]
respond_to :html
@@ -27,25 +25,12 @@ class UsersController < ApplicationController
@users = @users.limit(100)
end
- def new
- @user = User.new
- end
-
def show
end
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(params[:user]) unless (!admin? and params[:user][:effective_service_level])
- 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)
@@ -73,10 +58,11 @@ class UsersController < ApplicationController
protected
- def require_registration_allowed
- unless APP_CONFIG[:allow_registration]
- redirect_to home_path
+ def user_params
+ if admin?
+ params.require(:user).permit(:effective_service_level)
+ else
+ params.require(:user).permit(:password, :password_confirmation)
end
end
-
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 6de5e1b..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)
- "<i class=\"icon-#{name} #{color_class(color)}\"></i> ".html_safe
+ content_tag :span, '',
+ class: "glyphicon glyphicon-#{name} #{color_class(color)}"
end
def big_icon(name, color=nil)
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)
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
diff --git a/app/helpers/twitter_helper.rb b/app/helpers/twitter_helper.rb
new file mode 100644
index 0000000..7ce28be
--- /dev/null
+++ b/app/helpers/twitter_helper.rb
@@ -0,0 +1,88 @@
+module TwitterHelper
+ def twitter_enabled
+ if Rails.application.secrets.twitter
+ Rails.application.secrets.twitter['enabled'] == true
+ end
+ end
+
+ def twitter_client
+ Twitter::REST::Client.new do |config|
+ config.bearer_token = Rails.application.secrets.twitter['bearer_token']
+ end
+ end
+
+ def twitter_handle
+ Rails.application.secrets.twitter['twitter_handle']
+ end
+
+ def twitter_user_info
+ $twitter_user_info ||= []
+ end
+
+ def update_twitter_info
+ twitter_user_info[0] = Time.now
+ twitter_user_info[1] = twitter_client.user(twitter_handle).name
+ twitter_user_info[2] = twitter_client.user_timeline(twitter_handle, {:count => 200}).select{ |tweet| tweet.text.start_with?('RT','@')==false}
+ if twitter_user_info[2] == nil
+ error_handling
+ twitter_user_info[3] = "The twitter handle does not exist or the account's tweets are protected. Please change the privacy settings accordingly or contact your provider-admin."
+ end
+ rescue Twitter::Error::BadRequest
+ error_handling
+ twitter_user_info[3] = "The request for displaying tweets is invalid or cannot be otherwise served."
+ rescue Twitter::Error::Unauthorized
+ error_handling
+ twitter_user_info[3] = "Your bearer-token is invalid or the account's tweets are protected and cannot be displayed. Please change the privacy settings of the corresponding account, check your bearer-token in the secrets-file or contact your provider-admin to have the tweets shown."
+ rescue Twitter::Error::Forbidden
+ error_handling
+ twitter_user_info[3] = "The request for displaying tweets is understood, but it has been refused or access is not allowed."
+ rescue Twitter::Error::NotAcceptable
+ error_handling
+ twitter_user_info[3] = "An invalid format is specified in the request for displaying tweets."
+ rescue Twitter::Error::TooManyRequests
+ error_handling
+ twitter_user_info[3] = "The rate-limit for accessing the tweets is reached. You should be able to display tweets in a couple of minutes."
+ rescue Twitter::Error::NotFound
+ error_handling
+ twitter_user_info[3] = "The twitter hanlde does not exist."
+ rescue Twitter::Error
+ error_handling
+ twitter_user_info[3] = "An error occured while fetching the tweets."
+ end
+
+ def error_handling
+ twitter_user_info[2] = []
+ twitter_user_info
+ end
+
+ def cached_info
+ if twitter_user_info[0] == nil
+ update_twitter_info
+ else
+ if Time.now > twitter_user_info[0] + 15.minutes
+ update_twitter_info
+ end
+ end
+ twitter_user_info
+ end
+
+ def twitter_name
+ cached_info[1]
+ end
+
+ def num_of_tweets
+ 3
+ end
+
+ def tweets
+ cached_info[2].take(num_of_tweets)
+ end
+
+ def error_message
+ cached_info[3]
+ end
+
+ def all_tweets_count
+ twitter_user_info[2].count
+ end
+end
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
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
@@ -3,21 +3,10 @@ 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:
#
#class ApiAdminUser < ApiUser
# def is_admin?
# true
# end
-#end \ No newline at end of file
+#end
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/session.rb b/app/models/session.rb
index 0d7e10e..21e4dc6 100644
--- a/app/models/session.rb
+++ b/app/models/session.rb
@@ -1,3 +1,5 @@
+require 'login_format_validation'
+
class Session < SRP::Session
include ActiveModel::Validations
include LoginFormatValidation
diff --git a/app/models/temporary_user.rb b/app/models/temporary_user.rb
deleted file mode 100644
index 2afae15..0000000
--- a/app/models/temporary_user.rb
+++ /dev/null
@@ -1,93 +0,0 @@
-#
-# For users with login '*test_user*', we don't want to store these documents in
-# the main users db. This is because we create and destroy a lot of test
-# users. This weirdness of using a different db for some users breaks a lot of
-# things, such as associations. However, this is OK for now since we don't need
-# those for running the frequent nagios tests.
-#
-# This module is included in user.rb. This will only work if it is included
-# after designs are defined, otherwise, the design definition will overwrite
-# find_by_login().
-#
-
-module TemporaryUser
- extend ActiveSupport::Concern
- include CouchRest::Model::DatabaseMethod
-
- USER_DB = 'users'
- TMP_USER_DB = 'tmp_users'
- TMP_LOGIN = 'tmp_user' # created and deleted frequently
- TEST_LOGIN = 'test_user' # created, rarely deleted
-
- included do
- use_database_method :db_name
-
- # since the original find_by_login is dynamically created with
- # instance_eval, it appears that we also need to use instance eval to
- # override it.
- instance_eval <<-EOS, __FILE__, __LINE__ + 1
- def find_by_login(*args)
- if args.grep(/^#{TMP_LOGIN}/).any?
- by_login.database(tmp_database).key(*args).first()
- else
- by_login.key(*args).first()
- end
- end
- EOS
- end
-
- module ClassMethods
- def get(id, db = database)
- super(id, db) || super(id, tmp_database)
- end
- alias :find :get
-
- # calls db_name(TMP_LOGIN), then creates a CouchRest::Database
- # from the name
- def tmp_database
- choose_database(TMP_LOGIN)
- end
-
- def db_name(login=nil)
- if !login.nil? && login.include?(TMP_LOGIN)
- TMP_USER_DB
- else
- USER_DB
- end
- end
-
- # create the tmp db if it doesn't exist.
- # requires admin access.
- def create_tmp_database!
- design_doc.sync!(tmp_database.tap{|db|db.create!})
- end
-
- def is_tmp?(login)
- !login.nil? && login =~ /^#{TMP_LOGIN}/
- end
-
- def is_test?(login)
- !login.nil? && (login =~ /^#{TMP_LOGIN}/ || login =~ /^#{TEST_LOGIN}/)
- end
- end
-
- #
- # this gets called each and every time a User object needs to
- # access the database.
- #
- def db_name
- self.class.db_name(self.login)
- end
-
- # returns true if this User instance is stored in tmp db.
- def is_tmp?
- self.class.is_tmp?(self.login)
- end
-
- # returns true if this user is used for testing purposes
- # (either a temporary or long lived)
- def is_test?
- self.class.is_test?(self.login)
- end
-
-end
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
diff --git a/app/models/user.rb b/app/models/user.rb
index cb093cf..9cebbca 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,8 +1,10 @@
+require 'login_format_validation'
+require 'local_email'
+require 'temporary_user'
+
class User < CouchRest::Model::Base
include LoginFormatValidation
- use_database :users
-
property :login, String, :accessible => true
property :password_verifier, String, :accessible => true
property :password_salt, String, :accessible => true
@@ -71,12 +73,16 @@ class User < CouchRest::Model::Base
end
def to_json(options={})
+ to_hash.to_json(options)
+ end
+
+ def to_hash()
{
:login => self.login,
:ok => self.valid?,
:id => self.id,
- :enabled => self.enabled?
- }.to_json(options)
+ :enabled => self.enabled?,
+ }
end
def salt
diff --git a/app/views/users/new.html.haml b/app/views/account/new.html.haml
index bc0b1af..d40259e 100644
--- a/app/views/users/new.html.haml
+++ b/app/views/account/new.html.haml
@@ -1,18 +1,18 @@
-#
-# This form is handled entirely by javascript
-# Please take care when changing element ids.
--#
--# The form is hidden when no js is available
+-#
+-# 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}
-.span1
-.span9
+.col-md-1
+.col-md-9
%h2=t :signup
.lead=t :signup_info
- = render :partial => 'warnings'
+ = 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 }
diff --git a/app/views/v1/sessions/new.json.erb b/app/views/api/sessions/new.json.erb
index 36154b8..36154b8 100644
--- a/app/views/v1/sessions/new.json.erb
+++ b/app/views/api/sessions/new.json.erb
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..c7902b1 100644
--- a/app/views/home/_content.html.haml
+++ b/app/views/home/_content.html.haml
@@ -1,12 +1,31 @@
-.row-fluid
- %h1= t(:welcome, :provider => APP_CONFIG[:domain])
- .p=t(:welcome_message_html)
-
-.row-fluid
- = home_page_buttons
-
- - if Rails.env == 'development'
- .row-fluid
- %hr
- %p
- = link_to "make donation", new_payment_path if APP_CONFIG[:payment].present?
+- if twitter_enabled == true
+ .col-md-8
+ .row
+ %h1= t(:welcome, :provider => APP_CONFIG[:domain])
+ .p=t(:welcome_message_html)
+
+ .row
+ = home_page_buttons
+ .row
+ = render 'home/provider_message'
+
+ .col-md-1
+
+ .col-md-3
+ .row
+ = render 'twitter/index'
+- else
+ .row
+ %h1= t(:welcome, :provider => APP_CONFIG[:domain])
+ .p=t(:welcome_message_html)
+
+ .row
+ = home_page_buttons
+ .row
+ = render 'home/provider_message'
+
+ - if Rails.env == 'development'
+ .row
+ %hr
+ %p
+ = link_to "make donation", new_payment_path if APP_CONFIG[:payment].present?
diff --git a/app/views/home/_provider_message.html.haml b/app/views/home/_provider_message.html.haml
new file mode 100644
index 0000000..928215a
--- /dev/null
+++ b/app/views/home/_provider_message.html.haml
@@ -0,0 +1,2 @@
+-# please overwrite me in your customization files at
+-# config/customization/views/home/_provider_message.html.haml
diff --git a/app/views/layouts/_content.html.haml b/app/views/layouts/_content.html.haml
index 07f9189..b72b7c1 100644
--- a/app/views/layouts/_content.html.haml
+++ b/app/views/layouts/_content.html.haml
@@ -8,7 +8,7 @@
- content = yield
- if @show_navigation && @user
- .span2
+ .col-md-2
= render 'layouts/navigation'
.span10
= render 'layouts/messages'
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/users/_warnings.html.haml b/app/views/sessions/_warnings.html.haml
index baf80a4..baf80a4 100644
--- a/app/views/users/_warnings.html.haml
+++ b/app/views/sessions/_warnings.html.haml
diff --git a/app/views/sessions/new.html.haml b/app/views/sessions/new.html.haml
index bb7e4bd..6695123 100644
--- a/app/views/sessions/new.html.haml
+++ b/app/views/sessions/new.html.haml
@@ -1,8 +1,8 @@
-.span1
-.span9
+.col-md-1
+.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/twitter/_index.html.erb b/app/views/twitter/_index.html.erb
new file mode 100644
index 0000000..6e592c7
--- /dev/null
+++ b/app/views/twitter/_index.html.erb
@@ -0,0 +1,34 @@
+<% if twitter_enabled == true %>
+ <div class="twitter">
+
+ <div class="twitter_header">
+ <div class="twitter_id">
+ <div class="twitter_image_frame"><%= image_tag("Avatar_Pic.png") %></div>
+ <div class="twitter_name"><%= twitter_name%><br><a href="https://twitter.com/<%= twitter_handle %>">@<%= twitter_handle %></a></div>
+ </div>
+ </div>
+
+ <div class="twitter_list">
+ <%if tweets == [] then%><%= error_message %><% end %>
+ <% tweets.each do |e| %>
+ <div class="tweet">
+ <div class="tweet_text"><%= sanitize(e.text) %>
+ </div>
+ <div class="tweet_text_date">tweeted on <% t = e.created_at%> <%= t.strftime("%m/%d/%y").to_s %>
+ </div>
+ </div>
+ <% end %>
+ <%if all_tweets_count > num_of_tweets then%>
+ <div class="tweet">
+ <div class="more_tweets_link"><a href="https://twitter.com/<%= twitter_handle %>">Show more tweets...</a>
+ </div>
+ </div>
+ <% end %>
+ </div>
+
+ <div class="twitter_footer">
+
+ <p>This feed uses a Ruby interface to access the Twitter API. Within LEAP Twitter does not track you.</p>
+ </div>
+</div>
+<% end %>
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..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)
@@ -13,6 +18,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
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