From 3e24860581462ac9c5cb1e99bb5a5fd5528880e3 Mon Sep 17 00:00:00 2001 From: Ashish Date: Sat, 13 Jun 2020 09:46:24 +0530 Subject: [PATCH 1/2] Add Feature to view users status wise - Add Filter.js v2.1.0 for client side filtering and rendering of users - Remove StreamTable - Add navigation-bar, select list and pagination for Users - Update UI for User Index template - Add scope for all user status --- app/assets/javascripts/light/StreamTable.js | 545 ------ app/assets/javascripts/light/application.js | 2 + app/assets/javascripts/light/filter.js | 1725 +++++++++++++++++ app/assets/javascripts/light/users.js.coffee | 60 +- app/models/light/user.rb | 10 +- .../light/newsletters/_newsletters.html.haml | 3 +- .../light/newsletters/_top_navbar.html.haml | 3 - app/views/light/users/index.html.haml | 96 +- 8 files changed, 1832 insertions(+), 612 deletions(-) delete mode 100644 app/assets/javascripts/light/StreamTable.js create mode 100644 app/assets/javascripts/light/filter.js diff --git a/app/assets/javascripts/light/StreamTable.js b/app/assets/javascripts/light/StreamTable.js deleted file mode 100644 index bc334d5..0000000 --- a/app/assets/javascripts/light/StreamTable.js +++ /dev/null @@ -1,545 +0,0 @@ -/* - * StreamTable.js - * version: 1.1.1 (17/12/2013) - * - * Licensed under the MIT: - * http://www.opensource.org/licenses/mit-license.php - * - * Copyright 2013 Jiren Patel[ jiren@joshsoftware.com ] - * - * Dependency: - * jQuery(v1.8 >=) - */ - -(function(window, $) { - - 'use strict'; - - var StreamTable = function(container, opts, data) { - return new _StreamTable(container, opts, data); - }; - - StreamTable.VERSION = '1.1.0'; - - $.fn.stream_table = function (opts, data) { - var $this = $(this); - if ($this.data('st')) return; - $this.data('st', new _StreamTable($this.selector, opts, data)); - }; - - window.StreamTable = StreamTable; - - var _StreamTable = function(container, opts, data) { - this.data = []; - this.main_container = container; - this.$container = $(container); - this.opts = opts; - this.view = this.opts.view; - this.text_index = []; - this.last_search_result = []; - this.last_search_text = ''; - this.current_page = 0; - this.textFunc = null; - this.stream_after = (this.opts.stream_after || 2)*1000; - this.timer = null; - this.opts.callbacks = this.opts.callbacks || {}; - - if (!this.view) $.error('Add view function in options.'); - - if (this.$container.get(0).tagName == 'TABLE') this.$container = this.$container.find('tbody'); - - this.initPagination(this.opts.pagination || {}); - this.addSearchBox(); - this.addPerPage(); - this.has_sorting = $(this.main_container + ' [data-sort]').length > 0 ? true : false; - - if (this.has_sorting) { - this.sorting_opts = {}; - this.records_index = []; - this.last_search_record_index = []; - } - - if (data) { - data = this.addData(data); - this.render(0); - } - - this.bindEvents(); - this.bindSortingEvents(); - this.streamData(this.stream_after); - } - - var _F = _StreamTable.prototype; - - _F.getIndex = function(){ - return this.last_search_text.length > 0 ? this.last_search_record_index : this.records_index - }; - - _F.getData = function(){ - return this.last_search_text.length > 0 ? this.last_search_result : this.data; - }; - - _F.dataLength = function(){ - return this.has_sorting ? this.getIndex().length : this.getData().length; - } - - _F.initPagination = function(opts){ - this.paging_opts = $.extend({ - span: 5, - prev_text: '«', - next_text: '«', - per_page_select: true, - per_page_opts: [10,25,50] - }, opts); - - var p_classes = ['st_pagination']; - - if (opts.container_class){ - p_classes = [].concat.apply(p_classes, [opts.container_class]) - } - - this.paging_opts.per_page = this.paging_opts.per_page_opts[0] || 10; - this.paging_opts.container_class = p_classes.join(' '); - this.paging_opts.ul_class = ['pagination', opts.ul_class].join(' '); - this.paging_opts.per_page_class = ['st_per_page', opts.per_page_class].join(' '); - this.opts.pagination = this.paging_opts; - - var html = '
'; - - if(this.paging_opts.container){ - $(this.paging_opts.container).html(html); - }else{ - $(this.main_container).after(html); - } - - this.$pagination = $('.' + p_classes.join('.')); - }; - - _F.bindEvents = function(){ - var _self = this, - search_box = this.opts.search_box; - - $(search_box).on('keyup', function(e){ - _self.search($(this).val()); - }); - - $(search_box).on('keypress', function(e){ - if ( e.keyCode == 13 ) return false; - }); - - if (_self.paging_opts.per_page_select){ - $(_self.paging_opts.per_page_select).on('change', function(){ - _self.renderByPerPage($(this).val()); - }); - } - - _self.$pagination.on('click', 'a', function(e){ - var $this = $(this), page = parseInt($this.text()), current_page; - - if (page.toString() == 'NaN'){ - if ($this.hasClass('prev')) page = 'prev'; - else if ($this.hasClass('next')) page = 'next'; - else if ($this.hasClass('first')) page = 1; - else if ($this.hasClass('last')) page = _self.pageCount(); - } - - current_page = _self.paginate(page); - if (current_page >= 0) { - $('.st_pagination .active').removeClass('active'); - $('.st_pagination li[data-page='+ current_page +']').addClass('active'); - } - - return false; - }); - - }; - - _F.addSearchBox = function(){ - if (this.opts.search_box) return; - $(this.main_container).before(''); - this.opts.search_box = '#st_search'; - }; - - _F._makeTextFunc = function(record){ - var fields = this.opts.fields, cond_str = [], textFunc, is_array = false; - - if(typeof fields == 'function'){ - textFunc = fields; - } else if (record.constructor == Object){ - fields = fields || Object.keys(record) - - for (var i = 0, l = fields.length; i < l; i++){ - cond_str.push("d."+ fields[i]); - } - eval("textFunc = function(d) { return (" + cond_str.join(" + ' ' + ") + "); }"); - }else{ - if (fields){ - for(var i = 0, l = fields.length; i < l ; i++){ - cond_str.push("d["+ fields[i] + "]"); - } - eval("textFunc = function(d) { return (" + cond_str.join(" + ' ' + ") + "); }"); - }else{ - textFunc = function(d) { - return d.join(' '); - } - } - } - - return textFunc; - }; - - _F.buildTextIndex = function(data){ - var i = 0, l = data.length; - - if (!this.textFunc) { - this.textFunc = this._makeTextFunc(data[0]); - } - - for(i; i < l; i++){ - this.text_index.push(this.textFunc(data[i]).toUpperCase()); - } - }; - - _F.render = function(page){ - var i = (page * this.paging_opts.per_page), - l = (i + this.paging_opts.per_page), - eles = [], - index, - d = this.has_sorting ? this.getIndex() : this.getData(); - - if (d.length < l) l = d.length; - - if (this.has_sorting){ - for (i; i < l; i++){ - eles.push(this.view(this.data[d[i]], (i+1))); - } - }else{ - for (i; i < l; i++){ - eles.push(this.view(d[i], (i+1))); - } - } - - this.$container.html(eles); - }; - - _F.clearAndBuildTextIndex = function(data){ - this.text_index = [] - this.buildTextIndex(data) - }; - - _F.search = function(text){ - var q = $.trim(text), count = 0; - - if (q == this.last_search_text) return; - - this.last_search_text = q; - - if(q.length == 0 ){ - this.render(0); - }else{ - this.searchInData(q); - this.render(0); - } - - this.current_page = 0; - this.renderPagination(this.pageCount(), this.current_page); - this.execCallbacks('pagination'); - }; - - _F.searchInData = function(text){ - var result = [], - i = 0, - l = this.text_index.length, - t = text.toUpperCase(), - d = this.has_sorting ? this.records_index : this.data; - - if(this.has_sorting){ - for (i; i < l; i++){ - if (this.text_index[i].indexOf(t) != -1) result.push(i); - } - this.last_search_record_index = result - }else{ - for (i; i < l; i++){ - if (this.text_index[i].indexOf(t) != -1) result.push(this.data[i]); - } - this.last_search_result = result - } - - }; - - _F.addData = function(data){ - data = this.execCallbacks('before_add', data) || data; - - if (data.length){ - var i = this.data.length, l = data.length + i; - - this.buildTextIndex(data); - this.data = this.data.concat(data); - - if(this.has_sorting){ - for(i; i < l; i++){ - this.records_index.push(i); - } - } - - if (this.last_search_text.length > 0){ - this.searchInData(this.last_search_text); - } - - if (this.opts.auto_sorting && this.current_sorting){ - this.sort(this.current_sorting); - } - - this.render(this.current_page); - this.renderPagination(this.pageCount(), this.current_page); - this.execCallbacks('after_add', data); - this.execCallbacks('pagination'); - } - - return data; - }; - - _F.fetchData = function(){ - var _self = this, params = {q: this.last_search_text} - - if (this.opts.fetch_data_limit) { - params['limit'] = this.opts.fetch_data_limit; - params['offset'] = this.data.length; - } - - $.getJSON(this.opts.data_url, params).done(function(data){ - data = _self.addData(data); - - if (params.limit != null && (!data || !data.length ) ) { - _self.stopStreaming(); - }else{ - _self.setStreamInterval(); - } - - }).fail(function(e){ - _self.stopStreaming(); - }); - }; - - _F.setStreamInterval = function(){ - var _self = this; - if(_self.opts.stop_streaming == true) return; - - _self.timer = setTimeout(function(){ - _self.fetchData(); - }, _self.stream_after); - }; - - _F.stopStreaming = function(){ - this.opts.stop_streaming = true; - if (this.timer) clearTimeout(this.timer); - }; - - _F.streamData = function(time){ - if (!this.opts.data_url) return; - var _self = this, timer; - - _self.setStreamInterval(); - - if(!_self.opts.fetch_data_limit) _self.stopStreaming(); - }; - - _F.pageCount = function(){ - return Math.ceil(this.dataLength()/this.paging_opts.per_page); - }; - - //Render table rows for given page - _F.paginate = function(page){ - var page_count = this.pageCount(); - - if(page == 'prev'){ - page = this.current_page - 1; - }else if (page == 'next'){ - page = this.current_page + 1; - }else { - page = page - 1; - } - - if (page == this.current_page || page < 0 || page >= page_count) return; - - this.render(page); - this.current_page = page; - - if (this.paging_opts.span <= page_count) this.renderPagination(page_count, this.current_page); - - this.execCallbacks('pagination'); - - return this.current_page; - }; - - // Render Pagination call after new data added or search - _F.renderPagination = function(page_count, current_page){ - var i = 0, - l = page_count, - links = [ ''); - this.$pagination.html(links.join('')); - }; - - _F.addPerPage = function(){ - var per_page_select = this.paging_opts.per_page_select, html, arr; - - if (per_page_select === false || typeof per_page_select == 'string') return; - this.paging_opts.per_page_select = '.st_per_page'; - - html = [''); - $(this.main_container).before(html.join('')); - }; - - _F.renderByPerPage = function(per_page){ - if (this.paging_opts.per_page == per_page) return; - - this.paging_opts.per_page = parseInt(per_page); - this.current_page = 0; - this.render(0) - this.renderPagination(this.pageCount(), 0); - this.execCallbacks('pagination'); - }; - - _F.execCallbacks = function(type, args){ - var callback = this.opts.callbacks[type]; - - if (!callback) return; - - if (type == 'pagination'){ - var f = this.paging_opts.per_page * this.current_page; - args = { - from: (f + 1), - to: (this.paging_opts.per_page + f), - total: this.dataLength(), - page: this.current_page - } - - if (args['total'] == 0) args['from'] = 0; - if (args['to'] > args['total']) args['to'] = args['total']; - } - - return callback.call(this, args); - }; - - _F.bindSortingEvents = function(){ - var _self = this; - - $(this.main_container + ' [data-sort]').each(function(i){ - var $el = $(this) - ,arr = $el.data('sort').split(':') - ,data = { dir: arr[1] || 'asc', - type: arr[2] || 'string', - field: arr[0] }; - - _self.sorting_opts[data.field] = {dir: data.dir, type: data.type, field: data.field } - - $el.on('click', data, function(e){ - var $this = $(this); - - $this.addClass(e.data.dir); - _self.current_sorting = {dir: e.data.dir, type: e.data.type, field: e.data.field}; - _self.sort(e.data); - _self.render(_self.current_page); - - e.data.dir = e.data.dir == 'asc' ? 'desc' : 'asc'; - $(this).removeClass(e.data.dir); - - if(_self.opts.callbacks['after_sort']) - _self.execCallbacks('after_sort'); - }); - - //Start sorting initialy. - if(i == 0 && _self.opts.auto_sorting) { - $el.trigger('click'); - } - }); - }; - - _F.sort = function(options){ - options.order = options.dir == 'asc' ? 1 : -1; - - return this.getIndex().sort(this._sortingFunc(this.data, options)); - }; - - _F._sortingFunc = function(data, options){ - var field = options.field, order = options.order, type = options.type; - - //return this.sortingFuntions[type]; - - if (type == 'number'){ - return function(i, j){ - return (data[i][field] - data[j][field]) * order; - } - } - - return function(i, j){ - var t1 = data[i][field].toLowerCase() - ,t2 = data[j][field].toLowerCase(); - - if (t1 < t2) return (-1 * order); - if (t1 > t2) return (1 * order); - return 0; - } - }; - - _F.clear = function(){ - if (this.opts.search_box) { $(this.opts.search_box).html('')}; - $(this.main_container).html(''); - }; - - StreamTable.extend = function (name, f ) { - _StreamTable.prototype[name] = function () { - return f.apply( this, arguments ); - }; - }; - -})(this, window.jQuery) - -//In IE indexOf method not define. -if (!Array.prototype.indexOf) { - Array.prototype.indexOf = function(obj, start) { - for (var i = (start || 0), j = this.length; i < j; i++) { - if (this[i] === obj) { return i; } - } - return -1; - } -} - -if (!Object.keys) { - Object.keys = function(obj){ - var f, fields = []; - for(f in obj) fields.push(f); - return fields; - } -} - diff --git a/app/assets/javascripts/light/application.js b/app/assets/javascripts/light/application.js index cea679e..537c834 100644 --- a/app/assets/javascripts/light/application.js +++ b/app/assets/javascripts/light/application.js @@ -16,6 +16,8 @@ //= require bootstrap //= require bootstrap-datepicker //= require select2 +//= require filter +//= require json_query // require bootstrap-datepicker-rails //= require redactor-rails //= require redactor-rails/plugins diff --git a/app/assets/javascripts/light/filter.js b/app/assets/javascripts/light/filter.js new file mode 100644 index 0000000..43291ac --- /dev/null +++ b/app/assets/javascripts/light/filter.js @@ -0,0 +1,1725 @@ +/* + * filter.js + * 2.1.0 (2018-02-18) + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + * Copyright 2011-2018 Jiren Patel[jirenpatel@gmail.com] + * + * Dependency: + * jQuery(v1.9 >=) + */ + + /* + * JsonQuery + * 0.0.2 (2015-08-06) + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + * Copyright 2011-2015 Jiren Patel[jirenpatel@gmail.com] + * + */ + + (function(window) { + + 'use strict'; + + var JsonQuery = function(records, opts){ + return new _JsonQuery(records, opts || {}); + }; + + window.JsonQuery = JsonQuery; + + JsonQuery.VERSION = '0.0.2' + + JsonQuery.Config = { + id: 'id', + latitude: 'latitude', + longitude: 'longitude', + date_regx: /^\d{4}-\d{2}-\d{2}$/ + } + + JsonQuery.blankClone = function(jq, records){ + return new _JsonQuery(records, { + getterFns: jq.getterFns, + schema: jq.schema, + id: jq.id, + latitude: jq.latitude, + longitude: jq.longitude + }) + } + + var Config = JsonQuery.Config; + + var each = function(objs, callback, context){ + if (objs.length === +objs.length) { + for (var i = 0, l = objs.length; i < l; i++) { + callback.call(context, objs[i], i); + } + }else{ + for (var key in objs) { + if (hasOwnProperty.call(objs, key)) { + callback.call(context, objs[key], key); + } + } + } + }; + + var eachWithBreak = function(objs, callback, context){ + for (var i = 0, l = objs.length; i < l; i++) { + if(callback.call(context, objs[i], i) === false){ + return; + } + } + }; + + + var _JsonQuery = function(records, opts){ + this.records = records || []; + this.getterFns = opts.getterFns || {}; + this.lat = opts.latitude || Config.latitude; + this.lng = opts.longitude || Config.longitude; + this.id = opts.id; + + if(opts.schema){ + this.schema = opts.schema; + } + + if(this.records.length && !this.schema){ + initSchema(this, records[0], opts.schema); + } + }; + + var JQ = _JsonQuery.prototype; + + var initSchema = function(context, record, hasSchema){ + context.schema = {}; + + if(!context.id){ + context.id = record._id ? '_id' : Config.id; + } + + if(!hasSchema){ + buildSchema.call(context, record); + buildPropGetters.call(context, record); + } + }; + + var getDataType = function(val){ + if(val == null){ + return 'String'; + } + + /* + * @info Fix for IE 10 & 11 + * @bug Invalid calling object + */ + var type = Object.prototype.toString.call(val).slice(8, -1); + + if(type == 'String' && val.match(Config.date_regx)){ + return 'Date'; + } + + return type; + }; + + var parseValue = function(type, value){ + if(!value && value != 0){ + return value; + } + + if(type == 'String'){ + return String(value); + }else if(type == 'Number'){ + return Number(value) + }else if(type == 'Boolean'){ + return (value == 'true' || value == true || value == '1') ? true : false; + }else if(type == 'Date'){ + return new Date(value) + }else{ + return value + } + } + + var buildSchema = function(obj, parentField){ + var field, dataType, fullPath, fieldValue; + + for(field in obj){ + fieldValue = obj[field]; + dataType = getDataType(fieldValue); + + fullPath = parentField ? (parentField + '.' + field) : field; + this.schema[fullPath] = dataType; + + if(dataType == 'Object'){ + buildSchema.call(this, fieldValue, fullPath); + }else if(dataType == 'Array'){ + + if(['Object', 'Array'].indexOf(getDataType(fieldValue[0])) > -1){ + buildSchema.call(this, obj[field][0], fullPath); + }else{ + this.schema[fullPath] = getDataType(fieldValue[0]); + } + } + } + }; + + var parseDate = function(dates){ + if(dates.constructor.name == 'Array'){ + return dates.map(function(d){ return (d ? new Date(d) : null ) }); + } + return (dates ? new Date(dates) : null); + }; + + var buildPropGetters = function(record){ + var selector, type, val; + + for(selector in this.schema){ + type = this.schema[selector]; + + try{ + if(!this.getterFns[selector]){ + this.getterFns[selector] = buildGetPropFn.call(this, selector, type); + } + + //Remap if it is array + val = this.getterFns[selector](record); + if(getDataType(val) == 'Array'){ + this.schema[selector] = 'Array'; + } + }catch(err){ + console.log("Error while generating getter function for selector : " + selector + " NOTE: Define manually"); + } + } + }; + + var countArrHierarchy = function(schema, nestedPath){ + var lastArr = 0, + arrCount = 0, + path, + pathLength = nestedPath.length - 1; + + for(var i = nestedPath.length - 1; i >= 0; i--){ + path = nestedPath.slice(0, i + 1).join('.'); + + if(schema[path] == 'Array' && i < pathLength){ + lastArr = i; + arrCount = arrCount + 1; + } + } + return (arrCount > 1 ? (lastArr + 1) : -1); + }; + + var buildGetPropFn = function(field, type){ + var accessPath = '', + nestedPath = field.split('.'), + path, + lastArr = countArrHierarchy(this.schema, nestedPath), + prefix, + accessFnBody; + + for(var i = nestedPath.length - 1; i >= 0; i--){ + path = nestedPath.slice(0, i + 1).join('.'); + prefix = "['" + nestedPath[i] +"']"; + + if(this.schema[path] == 'Array'){ + if(lastArr == i){ + accessPath = prefix + (accessPath.length ? ".every(function(r" + i +"){ objs.push(r" + i + accessPath + ")})" : ''); + }else{ + accessPath = prefix + (accessPath.length ? ".map(function(r" + i +"){ return r" + i + accessPath + "})" : ''); + } + }else{ + accessPath = prefix + accessPath; + } + } + + if(lastArr > -1){ + accessFnBody = 'var objs = []; obj' + accessPath + ';' + (this.schema['path'] == 'Date' ? 'return parseDate(objs)' : 'return objs;'); + }else{ + accessFnBody = 'return ' + (this.schema['path'] == 'Date' ? 'parseDate(obj'+ accessPath +');' : 'obj'+ accessPath +';') ; + } + + return new Function('obj', accessFnBody); + }; + + JQ.operators = { + eq: function(v1, v2){ return v1 == v2}, + ne: function(v1, v2){ return v1 != v2}, + lt: function(v1, v2){ return v1 < v2}, + lte: function(v1, v2){ return v1 <= v2}, + gt: function(v1, v2){ return v1 > v2}, + gte: function(v1, v2){ return v1 >= v2}, + in: function(v1, v2){ return v2.indexOf(v1) > -1}, + ni: function(v1, v2){ return v2.indexOf(v1) == -1}, + li: function(v, regx) { return regx.test(v)}, + bt: function(v1, v2){ return (v1 >= v2[0] && v1 <= v2[1])} + }; + + JQ.addOperator = function(name, fn){ + this.operators[name] = fn; + }; + + // rVal = Record Value + // cVal = Condition Value + var arrayMatcher = function(rVal, cVal, cFn){ + var i = 0, l = rVal.length; + + for(i; i < l; i++){ + if(cFn(rVal[i], cVal)) return true; + } + }; + + JQ.addCondition = function(name, func){ + this.operators[name] = func; + }; + + JQ.getCriteria = function(criteria){ + var fieldCondition = criteria.split('.$'); + + return { + field: fieldCondition[0], + operator: fieldCondition[1] || 'eq' + }; + }; + + JQ.setGetterFn = function(field, fn){ + this.getterFns[field] = fn; + }; + + JQ.addRecords = function(records){ + if(!records || !records.length){ + return false; + } + + if(getDataType(records) == 'Array'){ + this.records = this.records.concat(records); + }else{ + this.records.push(records); + } + + if(!this.schema){ + initSchema(this, records[0]); + } + + return true; + }; + + JQ._findAll = function(records, qField, cVal, cOpt){ + var result = [], + cFn, + rVal, + qFn = this.getterFns[qField], arrayCFn; + + if(cOpt == 'li' && typeof cVal == 'string'){ + cVal = new RegExp(cVal); + } + + cFn = this.operators[cOpt]; + + if(this.schema[qField] == 'Array'){ + arrayCFn = cFn; + cFn = arrayMatcher; + } + + each(records, function(v){ + rVal = qFn(v); + + if(cFn(rVal, cVal, arrayCFn)) { + result.push(v); + } + }); + + return result; + }; + + JQ.find = function(field, value){ + var result, qFn; + + if(!value){ + value = field; + field = this.id; + } + + qFn = this.getterFns[field]; + + eachWithBreak(this.records, function(r){ + if(qFn(r) == value){ + result = r; + return false; + } + }); + + return result; + }; + + each(['where', 'or', 'groupBy', 'select', 'pluck', 'limit', 'offset', 'order', 'uniq', 'near'], function(c){ + JQ[c] = function(query){ + var q = new Query(this, this.records); + q[c].apply(q, arguments); + return q; + }; + }); + + each(['update_all', 'destroy_all'], function(c){ + JQ[c] = function(query){ + var q = new Query(this, this.records); + return q[c].apply(q, arguments); + }; + }); + + each(['count', 'first', 'last', 'all'], function(c){ + Object.defineProperty(JQ, c, { + get: function(){ + return (new Query(this, this.records))[c]; + } + }); + }); + + var compareObj = function(obj1, obj2, fields){ + for(var i = 0, l = fields.length; i < l; i++){ + if(this.getterFns[fields[i]](obj1) !== this.getterFns[fields[i]](obj2)){ + return false; + } + } + + return true; + }; + + var execWhere = function(query, records){ + var q, criteria, result; + + for(q in query){ + criteria = this.jQ.getCriteria(q); + result = this.jQ._findAll(result || records, criteria.field, query[q], criteria.operator); + } + + return result; + }; + + var execGroupBy = function(field, records){ + var fn = this.jQ.getterFns[field], v, result = {}, i = 0, l = records.length; + + each(records, function(r){ + v = fn(r); + (result[v] || (result[v] = [])).push(r); + }); + + return result; + }; + + var execOrder = function(orders, records){ + var fn, + direction, + _records = records.slice(0); + + for(var i = 0, l = orders.length; i < l; i++){ + fn = this.jQ.getterFns[orders[i].field], + direction = orders[i].direction == 'asc' ? 1 : -1; + + _records.sort(function(r1,r2){ + var a = fn(r1), b = fn(r2); + + return (a < b ? -1 : a > b ? 1 : 0)*direction; + }) + } + + return _records; + }; + + var execSelect = function(fields, records){ + var self = this, result = [], getFn; + + each(fields, function(f){ + getFn = self.jQ.getterFns[f]; + + each(records, function(r, i){ + (result[i] || (result[i] = {}))[f] = getFn(r); + }); + }); + + return result; + }; + + var execPluck = function(field, records){ + var getFn = this.jQ.getterFns[field], result = []; + + each(records, function(r){ + result.push(getFn(r)); + }); + + return result; + }; + + var execUniq = function(fields, records){ + var result = [], self = this; + + if(getDataType(records[0]) != 'Object'){ + each(records, function(r){ + if(result.indexOf(r) == -1){ + result.push(r); + } + }); + + return result; + } + + result.push(records[0]); + + each(records, function(r){ + var present = false; + + for(var i = 0, l = result.length; i < l; i++){ + if(compareObj.call(self.jQ, result[i], r, fields)){ + present = true; + } + } + + if(!present){ + result.push(r); + } + }); + + return result; + }; + + + + var Query = function(jQ, records){ + this.jQ = jQ; + this.records = records; + this.criteria = {}; + return this; + }; + + var Q = Query.prototype; + + Q.each = function(callback, context){ + each(this.exec() || [], callback, context) + }; + + Q.exec = Q.toArray = function(callback){ + var result, c; + + if(this.criteria['all']){ + result = this.records; + } + + if(this.criteria['where']){ + result = execWhere.call(this, this.criteria['where'], result || this.records); + } + + if(this.criteria['or']){ + result = result.concat(execWhere.call(this, this.criteria['or'], this.records)); + result = execUniq.call(this, [this.jQ.id], result); + } + + if(this.criteria['order']){ + result = execOrder.call(this, this.criteria['order'], result || this.records); + } + + if(this.criteria['near']){ + result = execNear.call(this, this.criteria['near'], result || this.records); + } + + if(this.criteria['uniq']){ + result = execUniq.call(this, this.criteria['uniq'], result || this.records); + } + + if(this.criteria['select']){ + result = execSelect.call(this, this.criteria['select'], result || this.records); + } + + if(this.criteria['pluck']){ + result = execPluck.call(this, this.criteria['pluck'], result || this.records); + } + + if(this.criteria['limit']){ + result = (result || this.records).slice(this.criteria['offset'] || 0, (this.criteria['offset'] || 0) + this.criteria['limit']); + } + + if(this.criteria['group_by']){ + result = execGroupBy.call(this, this.criteria['group_by'], result || this.records); + } + + if(!result){ + result = this.records; + } + + if(callback){ + each(result, callback); + } + + if(this.jQ.onResult){ + this.jQ.onResult(result, this.criteria); + } + + return result; + } + + var addToCriteria = function(type, query){ + var c; + + if(!this.criteria[type]){ + this.criteria[type] = {}; + } + + for(c in query){ + this.criteria[type][c] = query[c]; + } + + return this; + }; + + Q.where = function(query){ + return addToCriteria.call(this, 'where', query); + }; + + Q.or = function(query){ + return addToCriteria.call(this, 'or', query); + }; + + Q.groupBy = function(field){ + this.criteria['group_by'] = field; + return this; + }; + + Q.select = function(){ + this.criteria['select'] = arguments; + return this; + }; + + Q.pluck = function(field){ + this.criteria['pluck'] = field; + return this; + }; + + Q.limit = function(l){ + this.criteria['limit'] = l; + return this; + }; + + Q.offset = function(o){ + this.criteria['offset'] = o; + return this; + }; + + Q.order = function(criteria){ + var field; + this.criteria['order'] = this.criteria['order'] || []; + + for(field in criteria){ + this.criteria['order'].push({field: field, direction: criteria[field].toLowerCase()}); + } + + return this; + }; + + Q.uniq = function(){ + this.criteria['uniq'] = (arguments.length > 0 ? arguments : true); + return this; + }; + + Object.defineProperty(Q, 'count', { + get: function(){ + this.criteria['count'] = true; + var r = this.exec(); + + if(getDataType(r) == 'Array'){ + return this.exec().length; + }else{ + return Object.keys(r).length; + } + } + }); + + Object.defineProperty(Q, 'all', { + get: function(){ + this.criteria['all'] = true; + return this.exec(); + } + }); + + Object.defineProperty(Q, 'first', { + get: function(){ + this.criteria['first'] = true; + return this.exec()[0]; + } + }); + + Object.defineProperty(Q, 'last', { + get: function(){ + this.criteria['last'] = true; + var r = this.exec(); + return r[r.length - 1]; + } + }); + + //Geocoding + var GEO = { + redius: 6371, + toRad: function(v){ + return v * Math.PI / 180; + } + }; + + var calculateDistance = function(lat1, lat2, lng1, lng2){ + var dLat = GEO.toRad(lat2 - lat1), + dLon = GEO.toRad(lng2 - lng1), + lat1 = GEO.toRad(lat1), + lat2 = GEO.toRad(lat2); + + var a = Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.sin(dLon/2) * Math.sin(dLon/2) * Math.cos(lat1) * Math.cos(lat2); + + return (2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))) * GEO.redius; + }; + + var execNear = function(opts, records){ + var result = [], + self = this, + unit_c = opts.unit == 'mile' ? 0.6214 : 1, + latFn = self.jQ.getterFns[self.jQ.lat], + lngFn = self.jQ.getterFns[self.jQ.lng]; + + each(records, function(r){ + r._distance = calculateDistance(latFn(r), opts.lat, lngFn(r), opts.lng) * unit_c; + + if(r._distance <= opts.distance){ + result.push(r); + } + }); + + result.sort(function(a, b){ + return (a._distance < b._distance ? -1 : a._distance > b._distance ? 1 : 0); + }) + + return result; + }; + + Q.near = function(lat, lng, distance, unit){ + this.criteria['near'] = {lat: lat, lng: lng, distance: distance, unit: (unit || 'km')}; + return this; + }; + + //Helpers + Q.map = Q.collect = function(fn){ + var result = [], out; + + this.exec(function(r){ + if(out = fn(r)){ + result.push(out); + } + }) + return result; + }; + + Q.sum = function(field){ + var result = 0, + group, + getFn = this.jQ.getterFns[field]; + + if(this.criteria['group_by']){ + group = true; + result = {}; + } + + this.exec(function(r, i){ + if(group){ + result[i] = 0; + + each(r, function(e){ + result[i] = result[i] + (getFn(e) || 0); + }) + }else{ + result = result + (getFn(r) || 0); + } + }); + + return result; + }; + + Q.toJQ = function(){ + var q = JsonQuery(this.all, {schema: true}); + q.schema = this.jQ.schema; + q.getterFns = this.jQ.getterFns; + + return q; + }; + + Q.destroy_all = Q.destroy = function(){ + var marked_records = this.all; + + each(marked_records, function(r, i){ + r._destroy_ = true; + }); + + this.records = this.jQ.records = this.records.filter(function(r){ + return !r._destroy_; + }); + + return marked_records; + }; + + Q.update_all = Q.update = function(attrs){ + if(!attrs){ + return false; + } + + var updated_count = 0; + + each(this.all, function(r){ + each(attrs, function(value, key){ + r[key] = value; + }); + updated_count = updated_count + 1; + }); + + return updated_count; + }; + + + //In IE 8 indexOf method not define. + if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function(obj, start) { + for (var i = (start || 0), j = this.length; i < j; i++) { + if (this[i] === obj) { return i; } + } + return -1; + } + } + + if(!Object.defineProperty){ + Object.defineProperty = function(obj, name, opts){ + obj[name] = opts.get + } + } + + + + +})(this); + +;(function($, window, document) { + + "use strict"; + + + //View Template + // Ref: Underscopre.js + //JavaScript micro-templating, similar to John Resig's implementation. + var templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + var escapeStr = function(string) { + return (''+string).replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\//g, '/'); + }; + + function templateBuilder(str, data) { + var c = templateSettings; + var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' + + 'with(obj||{}){__p.push(\'' + + str.replace(/\\/g, '\\\\') + .replace(/'/g, "\\'") + .replace(c.escape, function(match, code) { + return "',escapeStr(" + code.replace(/\\'/g, "'") + "),'"; + }) + .replace(c.interpolate, function(match, code) { + return "'," + code.replace(/\\'/g, "'") + ",'"; + }) + .replace(c.evaluate || null, function(match, code) { + return "');" + code.replace(/\\'/g, "'") + .replace(/[\r\n\t]/g, ' ') + ";__p.push('"; + }) + .replace(/\r/g, '\\r') + .replace(/\n/g, '\\n') + .replace(/\t/g, '\\t') + + "');}return __p.join('');"; + + var func = new Function('obj', tmpl); + return data ? func(data) : function(data) { return func(data) }; + }; + + + function each (objs, callback, context){ + for (var i = 0, l = objs.length; i < l; i++) { + callback.call(context, objs[i], i); + } + } + + + var FJS = function(records, container, options) { + var self = this; + + this.opts = options || {}; + this.callbacks = this.opts.callbacks || {}; + this.$container = $(container); + this.view = this.opts.view || renderRecord; + this.criterias = []; + this._index = 1; + this.appendToContainer = this.opts.appendToContainer || appendToContainer; + this.has_pagination = !!this.opts.pagination; + this.search_text = ''; + this.anyFilterSelected = false; + this.setTemplate(this.opts.template); + + $.each(this.opts.criterias || [], function(){ + self.addCriteria(this); + }); + + this.Model = JsonQuery(); + this.Model.getterFns['_fid'] = function(r){ return r['_fid'];}; + this.addRecords(records, this.opts['filter_on_init'] || false); + + if(this.has_pagination){ + this.initPagination(); + } + }; + + var F = FJS.prototype; + + Object.defineProperty(F, 'records', { + get: function(){ return this.Model.records; } + }); + + Object.defineProperty(F, 'recordsCount', { + get: function(){ return this.Model.records.length; } + }); + + F.templateBuilder = templateBuilder; + + //Callback + F.execCallback = function(){ + var name = arguments[0]; + + if(this.callbacks[name]) { + this.callbacks[name].apply(this, Array.prototype.slice.call(arguments, 1)); + } + }; + + F.addCallback = function(name, fns){ + if(name && fns){ + this.callbacks[name] = fns; + } + }; + + //Add Data + F.addRecords = function(records, applyFilter){ + var has_scheme = !!this.Model.schema; + + this.execCallback('beforeAddRecords', records); + + if(this.Model.addRecords(records)){ + if(!this.has_scheme){ + this.initSearch(this.opts.search); + } + + this.render(records); + if(applyFilter !== false){ + this.filter(); + } + } + + this.execCallback('afterAddRecords', records); + }; + + F.removeRecords = function(criteria){ + var ids; + + if($.isPlainObject(criteria)){ + ids = this.Model.where(criteria).pluck('_fid').all; + }else if($.isArray(criteria)){ + ids = this.Model.where({'id.$in': criteria}).pluck('_fid').all; + } + + if(!ids){ + return false; + } + + var records = this.Model.records, + removedCount = 0, + idsLength = ids.length, + fid; + + for(var i = records.length - 1; i > -1; i--){ + fid = records[i]._fid + + if(ids.indexOf(fid) > -1){ + records.splice(i, 1); + removedCount ++; + + $('#fjs_' + fid).remove(); + } + + if(removedCount == idsLength){ + break; + } + } + + this.execCallback('afterRemove'); + + return true; + }; + + var renderRecord = function(record, index){ + return this.templateFn(record); + }; + + F.render = function(records){ + var self = this, + ele, + cbName; + + if(!records.length){return; } + + this.execCallback('beforeRender', records); + cbName = 'beforeRecordRender'; + + $.each(records, function(i){ + self.execCallback(cbName, this); + this._fid = (self._index++); + + if(!self.has_pagination){ + self.renderItem(this, i); + } + }); + }; + + F.renderItem = function(record, i){ + if(!record){ + return; + } + + var ele = this.view(record, i); + + if(typeof ele == 'string'){ + ele = $($.trim(ele)); + } + + ele.attr('id', 'fjs_' + record._fid).addClass('fjs_item'); + this.appendToContainer(ele, record); + }; + + var appendToContainer = function(html_ele, record){ + this.$container.append(html_ele); + }; + + var setDefaultCriteriaOpts = function(criteria){ + var ele = criteria.$ele, + eleType = criteria.$ele.attr('type'); + + if(!criteria.selector){ + if (ele.get(0).tagName == 'INPUT'){ + criteria.selector = (eleType == 'checkbox' || eleType == 'radio') ? ':checked' : ':input'; + }else if (ele.get(0).tagName == 'SELECT'){ + criteria.selector = 'select'; + } + } + + if (!criteria.event){ + criteria.event = (eleType == 'checkbox' || eleType == 'radio') ? 'click' : 'change'; + } + + return criteria; + }; + + F.addCriteria = function(criterias){ + var self = this; + + if(!criterias){ return false; } + + if($.isArray(criterias)){ + $.each(criterias, function(){ + addFilterCriteria.call(self, this); + }); + }else{ + addFilterCriteria.call(self, criterias); + } + + return true; + }; + + // Add Filter criteria + // criteria: { ele: '#name', event: 'check', field: 'name', type: 'range' } + var addFilterCriteria = function(criteria){ + if(!criteria || !criteria.field || !criteria.ele){ + return false; + } + + criteria.$ele = $(criteria.ele); + + if(!criteria.$ele.length){ + return false; + } + + criteria = setDefaultCriteriaOpts(criteria); + this.bindEvent(criteria.ele, criteria.event); + + criteria._q = criteria.field + (criteria.type == 'range' ? '.$bt' : '') + criteria.active = true; + + this.criterias.push(criteria); + + return true; + }; + + F.removeCriteria = function(field){ + var self = this, criteria, index; + + $.each(self.criterias, function(i){ + if(this.field == field){ + index = i; + } + }); + + if(index != null){ + criteria = this.criterias.splice(index, 1)[0]; + $('body').off(criteria.event, criteria.ele) + } + }; + + var changeCriteriaStatus = function(names, active){ + var self = this; + + if(!names){ return; } + + if(!$.isArray(names)){ + names = [names] + } + + $.each(names, function(){ + var name = this; + + $.each(self.criterias, function(){ + if(this.field == name){ + this.active = active; + } + }) + }); + }; + + F.deactivateCriteria = function(names){ + changeCriteriaStatus.call(self, names, false); + }; + + F.activateCriteria = function(names){ + changeCriteriaStatus.call(this, names, true); + }; + + F.getSelectedValues = function(criteria, context){ + var vals = []; + + criteria.$ele.filter(criteria.selector).each(function() { + vals.push($(this).val()); + }); + + if($.isArray(vals[0])){ + vals = [].concat.apply([], vals); + } + + if(criteria.all && vals.indexOf(criteria.all) > -1){ + return; + } + + if(criteria.type == 'range'){ + vals = vals[0].split(criteria.delimiter || '-'); + } + + vals = this.parseValues(criteria.field, vals); + + return context.execCallback('onFilterSelect', {criteria: criteria, values: vals}) || vals; + }; + + F.lastResult = function(){ + return (this.last_result || this.records); + }; + + F.filter = function(){ + var query = {}, + vals, _q, + count = 0, + self = this, + criteria; + + $.each(this.criterias, function(){ + if(this.active){ + vals = self.getSelectedValues(this, self); + + if(vals && vals.length){ + _q = ($.isArray(vals) && !this.type) ? (this._q + '.$in') : this._q; + query[_q] = vals ; + count = count + 1; + } + } + }); + + this.anyFilterSelected = count > 0; + criteria = this.Model.where(query); + this.execCallback('shortResult', criteria); + this.last_result = criteria.all; + + if(this.searchFilter(this.last_result)){ + return query; + } + + this.show(this.last_result); + this.renderPagination(this.last_result.length); + this.execCallback('afterFilter', this.last_result, JsonQuery.blankClone(this.Model, this.last_result)); + + return query; + }; + + F.show = function(result, type){ + var i = 0, l = result.length; + + if(this.has_pagination){ + + i = this.page.perPage *(this.page.currentPage - 1); + l = i + this.page.perPage; + + this.$container.html(""); + + for(i; i < l; i++){ + this.renderItem(result[i], i); + } + + return; + } + + $('.fjs_item').hide(); + + for(i; i < l; i++){ + $('#fjs_' + result[i]._fid).show(); + } + + }; + + F.filterTimer = function(timeout){ + var self = this; + + if (this.filterTimeoutId) { + clearTimeout(this.filterTimeoutId); + } + + this.filterTimeoutId = setTimeout(function() { + self.filter(); + }, timeout); + }; + + F.bindEvent = function(ele, eventName){ + var self = this; + + $(document).on(eventName, ele, function(e){ + self.filterTimer(self.opts.timeout || 35); + }); + + }; + + F.initSearch = function(opts){ + if(!opts || !opts.ele){ + return; + } + + if(!opts.start_length){ + this.opts.search.start_length = 2 + } + + this.$search_ele = $(this.opts.search.ele); + + if(this.$search_ele.length){ + this.has_search = true; + this.searchFn = this.buildSearchFn(opts.fields); + this.bindEvent(opts.ele, 'keyup'); + } + }; + + F.buildSearchFn = function(fields){ + var self = this, getterFns = []; + + if(fields){ + $.each(fields, function(){ + getterFns.push(self.Model.getterFns[this]); + }) + }else{ + $.each(self.Model.getterFns, function(i, fn){ + getterFns.push(fn); + }); + } + + return function(text, record){ + text = text.toLocaleUpperCase(); + + for(var i = 0, l = getterFns.length; i < l; i++){ + + if((getterFns[i](record) + '').toLocaleUpperCase().indexOf(text) > -1){ + return true; + } + + } + return false; + } + }; + + F.lastSearchResult = function(){ + if (this.search_text.length > this.opts.search.start_length){ + return this.search_result; + }else{ + return this.lastResult(); + } + } + + F.searchFilter = function(records) { + if(!this.has_search){ + return; + } + + var result; + this.search_text = $.trim(this.$search_ele.val()); + + if (this.search_text.length < this.opts.search.start_length){ + return false; + } + + result = this.search(this.search_text, records || this.lastResult()); + + this.search_result = result; + this.show(result); + this.renderPagination(result.length) + this.execCallback('afterFilter', result, JsonQuery.blankClone(this.Model, result)); + + return true; + }; + + F.search = function(text, records){ + text = text.toLocaleUpperCase(); + + var result = []; + + for(var i = 0, l = records.length; i < l; i++){ + if(this.searchFn(text, records[i])){ + result.push(records[i]); + } + } + + return result; + }; + + //Streaming + F.setStreaming = function(opts){ + if(!opts) {return;} + + this.opts.streaming = opts; + + if(opts.data_url){ + opts.stream_after = (opts.stream_after || 2)*1000; + opts.batch_size = opts.batch_size || false; + this.streamData(opts.stream_after); + } + + }; + + var fetchData = function(){ + var self = this, + params = this.opts.params || {}, + opts = this.opts.streaming; + + params.offset = this.recordsCount; + + if (opts.batch_size) { + params.limit = opts.batch_size; + } + + if (this.has_search){ + params['q'] = $.trim(this.$search_ele.val()); + } + + $.getJSON(opts.data_url, params).done(function(records){ + if (params.limit != null && (!records || !records.length)){ + self.stopStreaming(); + }else{ + self.setStreamInterval(); + self.addRecords(records); + } + + }).fail(function(e){ + self.stopStreaming(); + }); + }; + + F.setStreamInterval = function(){ + var self = this; + + if(self.opts.streaming.stop == true){ return; } + + self.streamingTimer = setTimeout(function(){ + fetchData.call(self); + }, self.opts.streaming.stream_after); + }; + + F.stopStreaming = function(){ + this.opts.streaming.stop = true; + + if (this.streamingTimer){ + clearTimeout(this.streamingTimer); + } + }; + + F.resumeStreaming = function(){ + this.opts.streaming.stop = false; + this.streamData(this.opts.streaming.stream_after); + }; + + F.streamData = function(time){ + this.setStreamInterval(); + + if(!this.opts.streaming.batch_size){ + this.stopStreaming(); + } + }; + + F.clear = function(){ + if(this.opts.streaming){ + this.stopStreaming(); + } + + $.each(this.criterias, function(){ + $(document).off(this.event, this.ele); + }) + + if(this.opts.search){ + $(document).off('keyup', this.opts.search.ele); + } + + if (this.filterTimeoutId) { + clearTimeout(this.filterTimeoutId); + } + } + + F.initPagination = function(){ + var self = this, + opts = this.opts.pagination; + + if(!opts.perPage){ + opts.perPage = {} + } + + if(!opts.perPage.values){ + opts.perPage.values = [10, 20, 30]; + } + + this.page = { currentPage: 1, perPage: opts.perPage.values }; + + this.paginator = new Paginator(this.lastResult().length, this.opts.pagination, function(currentPage, perPage){ + self.page = { currentPage: currentPage, perPage: perPage } + + if(self.has_search){ + self.show(self.lastSearchResult()) + }else{ + self.show(self.lastResult()) + } + }) + + this.filter(); + }; + + F.renderPagination = function(totalCount){ + if(this.has_pagination){ + this.paginator.setRecordCount(totalCount); + } + }; + + F.parseValues = function(field, values){ + var type = typeof this.Model.schema == 'undefined' ? 'String' : this.Model.schema[field]; + + if(type == 'Number'){ + return $.map(values, function(v){ return Number(v) }); + }else if(type == 'Boolean'){ + return $.map(values, function(v){ return (v == 'true' || v == true) }); + }else{ + return values; + } + }; + + F.setTemplate = function(template, rebuild) { + this.templateFn = templateBuilder($(template).html()); + if(rebuild === true) { + this.$container.empty(); + + this.render(this.records); + this.filter(); + } + }; + + + + var Paginator = function(recordsCount, opts, onPagination) { + var paginationView; + + this.recordsCount = recordsCount;; + this.opts = opts; + this.$container = $(this.opts.container); + + if(this.opts.paginationView){ + paginationView = $(this.opts.paginationView).html(); + } else { + paginationView = views.pagination; + } + + this.paginationTmpl = templateBuilder(paginationView); + + this.currentPage = 1; + this.onPagination = onPagination; + this.initPerPage(); + this.render(); + this.bindEvents(); + }; + + var P = Paginator.prototype; + + P.bindEvents = function(){ + var self = this; + + $(this.opts.container).on('click', '[data-page]', function(e){ + self.setCurrentPage($(this).data('page')); + e.preventDefault(); + }); + }; + + P.totalPages = function(){ + return Math.ceil(this.recordsCount/this.perPageCount); + }; + + P.setCurrentPage = function(page){ + page = this.toPage(page) + this.prevCurrentPage = this.currentPage; + this.currentPage = page; + this.paginate(page); + }; + + P.setRecordCount = function(total){ + this.recordsCount = total; + this.setCurrentPage(this.currentPage); + } + + P.toPage = function(page){ + if(page == 'first'){ + return 1; + } + + if(page == 'last'){ + return this.totalPages(); + } + + if(page == 'next'){ + var next_page = this.currentPage + 1; + return (next_page > this.totalPages() ? this.currentPage : next_page); + } + + if(page == 'prev'){ + var prev_page = this.currentPage - 1; + return (prev_page <= 0 ? this.currentPage : prev_page); + } + + return parseInt(page); + }; + + P.paginate = function(page){ + this.render(); + this.onPagination(this.currentPage, this.perPageCount); + }; + + P.render = function(){ + var pages = this.getPages(); + + if(this.currentPage > pages.totalPages){ + this.currentPage = pages.totalPages; + } + + if(this.currentPage == 0){ + this.currentPage = 1; + } + + pages.currentPage = this.currentPage; + this.$container.html(this.paginationTmpl(pages)) + }; + + function makePageArray(start, end){ + var i = start, pages = []; + + for(i; i <= end; i++){ + pages.push(i); + } + + return pages; + } + + P.getPages = function () { + var total = this.totalPages(); + + if(!this.opts.visiblePages){ + return { totalPages: total, pages: makePageArray(0, total), self: this }; + } + + var half = Math.floor(this.opts.visiblePages / 2); + var start = this.currentPage - half + 1 - this.opts.visiblePages % 2; + var end = this.currentPage + half; + + // handle boundary case + if (start <= 0) { + start = 1; + end = this.opts.visiblePages; + } + + if (end > total) { + start = total - this.opts.visiblePages; + + if(start <= 0){ + start = 1; + } + + end = total; + } + + return { currentPage: this.currentPage, totalPages: total, pages: makePageArray(start, end), self: this }; + }, + + P.initPerPage = function(){ + var opts = this.opts.perPage, + template, + html, + ele, + event_type, + self = this; + + this.perPageCount = opts.values[0]; + + template = opts.perPageView ? $(opts.perPageView).html() : views.per_page; + html = templateBuilder(template)({ values: opts.values }); + $(opts.container).html(html); + + ele = $(opts.container).find('[data-perpage]') + event_type = ele.get(0).tagName == 'SELECT' ? 'change' : 'click'; + + $(opts.container).on(event_type, '[data-perpage]', function(e){ + var value = parseInt($(this).val() || $(this).data('value')); + self.setPerPage(value) + e.preventDefault(); + }); + }; + + P.setPerPage = function(value){ + this.perPageCount = value; + this.setCurrentPage(this.currentPage); + } + + + $.fn.filterjs = function(records, options) { + var $this = $(this); + + if (!$this.data('fjs')){ + $this.data('fjs', FilterJS(records, $this, options)); + } + }; + + + + + var list = []; + var views = []; + var FilterJS = function(records, container, options) { + var fjs = new FJS(records, container, options); + list.push(fjs); + + return fjs; + }; + + FilterJS.list = list; + FilterJS.templateBuilder = templateBuilder; + + window.FilterJS = FilterJS; + + views['pagination'] = ''; + views['per_page'] = ''; + + /* + * Find html tag and parse options for filter + */ + function getElementWithOptions(name, hasMany){ + var attr = "fjs-"+ name; + var $eles = $("[" + attr + "]"); + var options = []; + + if(!$eles.length){ + return; + } + + $.each($eles, function(){ + var $ele = $(this); + var option = { ele: $ele }; + var optionStr = $ele.attr(attr); + + options.push(option); + + if(!optionStr){ + return options; + } + + $.each(optionStr.split(','), function(i, opt){ + var kv = opt.split("="); + option[kv[0]] = kv[1]; + }) + }) + + return hasMany ? options : options[0]; + }; + + FilterJS.auto = function(records, callbacks){ + var options = {}; + var container = getElementWithOptions("items"); + var fjs, + search, + template, + criterias; + + if(!container || !container.template){ + return; + } + + options.template = container.template + search = getElementWithOptions("search"); + + if(search){ + if(search.fields){ + search.fields = search.fields.split(','); + } + options.search = search; + } + + if(callbacks){ + options.callbacks = callbacks; + } + + fjs = FilterJS(records, container.ele, options) + + criterias = getElementWithOptions("criteria", true); + + if(criterias){ + fjs.addCriteria(criterias); + } + + return fjs + }; + + + + +})( jQuery, window , document ); diff --git a/app/assets/javascripts/light/users.js.coffee b/app/assets/javascripts/light/users.js.coffee index 3abac02..b43537d 100644 --- a/app/assets/javascripts/light/users.js.coffee +++ b/app/assets/javascripts/light/users.js.coffee @@ -1,33 +1,37 @@ # Place all the behaviors and hooks related to the matching controller here. # All this logic will automatically be available in application.js. # You can use CoffeeScript in this file: http://coffeescript.org/ + +window.initialize_filterjs_table = (div, data, template, data_url, search_fields, pagination_container = '#pagination', per_page_container = '#per_page', pagination_values = [ 10, 20, 25 ]) -> + batch_size = 500 -custom_function = -> - template = Mustache.compile($.trim($("#template").html())) - view_light = (record, index) -> - - if record.is_subscribed is true - record.str = "success" - else - record.str = "warning" - template - record: record - index: index - - if($("#stream_table_light").length) - console.log data - opts = { - view: view_light - data_url: '/newsletter/users.json' - stream_after: 1 - fetch_data_limit: 500 - auto_sorting: true - } - $("#stream_table_light").stream_table(opts, data) - - $(".st_search").css("height",27) - $(".st_search").css("margin-right",10) - -$(document).ready(custom_function) -$(document).on 'page:load', custom_function + mustache_template = $(template).html(); + view = (data) -> + Mustache.to_html(mustache_template, data) + fjs = FilterJS(data, div, + template: template + view: view + criterias: [{ + field: 'sidekiq_status', + ele: '#user_status', + event: 'change', + selector: 'select'}], + search: + ele: '#searchbox' + fields: ['username', 'email_id'] + start_length: 1 + pagination: + container: pagination_container + visiblePages: 5 + perPage: + values: pagination_values + container: per_page_container) + fjs.setStreaming + data_url: '/newsletter/users.json' + stream_after: 1 + batch_size: batch_size + fjs.addCallback 'beforeAddRecords', -> + if (@recordsCount + batch_size) >= total_count + @stopStreaming() + return diff --git a/app/models/light/user.rb b/app/models/light/user.rb index 98f06c4..e04cd02 100644 --- a/app/models/light/user.rb +++ b/app/models/light/user.rb @@ -37,8 +37,14 @@ class User end end - scope :subscribed_users, -> { where is_subscribed: true} - scope :new_users, -> { where(is_subscribed: false, sidekiq_status: NEW_USER)} + scope :subscribed_users, -> { where is_subscribed: true } + scope :unsubscribed_users, -> { where is_subscribed: false } + scope :new_users, -> { where(is_subscribed: false, sidekiq_status: NEW_USER) } + scope :blocked_users, -> { where(is_subscribed: false, sidekiq_status: 'Block') } + scope :bounced_users, -> { where(is_subscribed: false, sidekiq_status: 'Bounced') } + scope :spam_users, -> { where(is_subscribed: false, sidekiq_status: 'Spam') } + scope :invalid_users, -> { where(is_subscribed: false, sidekiq_status: 'Invalid') } + scope :opt_in_users, -> { where(is_subscribed: false, sidekiq_status: 'Opt in mail sent') } def self.add_users_from_worksheet(worksheet, column = 1) fails = [] diff --git a/app/views/light/newsletters/_newsletters.html.haml b/app/views/light/newsletters/_newsletters.html.haml index 41ae578..7ca5be3 100644 --- a/app/views/light/newsletters/_newsletters.html.haml +++ b/app/views/light/newsletters/_newsletters.html.haml @@ -10,7 +10,8 @@ %center %a.link{href: "#{newsletter_path(newsletter)}" } %div.tile.thumbnail - = image_tag newsletter.get_image + - if Rails.env.production? + = image_tag newsletter.get_image %div.cap %h4 = newsletter.sent_on.strftime("%B %Y") diff --git a/app/views/light/newsletters/_top_navbar.html.haml b/app/views/light/newsletters/_top_navbar.html.haml index 36fe45b..bcd3045 100644 --- a/app/views/light/newsletters/_top_navbar.html.haml +++ b/app/views/light/newsletters/_top_navbar.html.haml @@ -8,9 +8,6 @@ = menu_item "Google Spreadsheets", spreadsheets_path = menu_item "Show Users", users_path, ({:method => 'get'} if params[:controller] == 'light/users' && params[:action].in?(['new', 'create'])) - = menu_item raw ("Subscribers: #{badge(Light::User.subscribed_users.count)}") - = menu_item raw ("New users: #{badge(Light::User.new_users.count)}") - = menu_group :pull => :right do = content_tag :a, "Send Mail", :href => sendmailer_path, :class => 'btn btn-danger', :data => { :confirm => 'Are you sure ?'} = content_tag :a, "Test Mail", :href => testmail_path, :class => "btn btn-success" diff --git a/app/views/light/users/index.html.haml b/app/views/light/users/index.html.haml index edab817..fa847c2 100644 --- a/app/views/light/users/index.html.haml +++ b/app/views/light/users/index.html.haml @@ -1,37 +1,67 @@ = render 'light/newsletters/top_navbar' -%table#stream_table_light.table.table-striped - %thead - %tr - %th - %strong - = "#" - %th - %strong - User's Email Id - %th{data: {sort: "username:asc"}} - %strong - Username - %th - %strong - Edit - %th - %strong - Delete - %tbody +%nav.navbar.navbar-inverse + .container-fluid + .navbar-header + %ul.nav.navbar-nav + %li.nav-item + %label Per Page: + %span#users_per_page.content + %li.nav-item + %label.sr-only{:for => 'searchbox'} Search + %input#searchbox.form-control{:autocomplete => 'off', :placeholder => 'Search', :type => 'text'}/ + %span.glyphicon.glyphicon-search.search-icon - %script#template{:type => "text/html"} - {{#record}} - %tr{class: "{{str}}"} - %td {{index}} - %td - %a{ :href => "/newsletter/users/{{_id.$oid}}"} {{email_id}} - %td {{username}} - %td - %a{ :href => "/newsletter/users/{{_id.$oid}}/edit", :class => "btn btn-mini btn-success"} Edit - %td - %a{ :href => "/newsletter/users/{{_id.$oid}}", data:{ :confirm => 'Are you sure?', method: "delete"}, :class => "btn btn-mini btn-danger" } Delete - {{/record}} - + %li.nav-item + %label Status + %select.form-control#user_status + %option{:value => 'Subscribed'} Subscribed: #{Light::User.subscribed_users.count} + %option{:value => 'Unsubscribed'} Unsubscribed: #{Light::User.unsubscribed_users.count} + %option{:value => 'new user'} New User: #{Light::User.new_users.count} + %option{:value => 'Block'} Blocked: #{Light::User.blocked_users.count} + %option{:value => 'Invalid'} Invalid: #{Light::User.invalid_users.count} + %option{:value => 'Bounced'} Bounced: #{Light::User.bounced_users.count} + %option{:value => 'Spam'} Spam: #{Light::User.spam_users.count} + %option{:value => 'Opt in mail sent'} Opt in Mail Sent: #{Light::User.opt_in_users.count} + + %ul.nav.navbar-nav.pull-right + %li + %h5= raw ("Subscribers: #{badge(Light::User.subscribed_users.count)}") + + %li + %h5= raw ("New users: #{badge(Light::User.new_users.count)}") + +.row + .content.col-md-12 + %div + %table.table.table-striped.table-hover.table-new#users + %thead + %tr + %th.text-center + %strong + Email Id + %th.text-center + %strong + Name + %th.text-center + %strong + Actions + %tbody + %script#user-template{:type => "text/html"} + %tr + %td.text-center= '{{email_id}}' + %td.text-center= '{{username}}' + %td.text-center + %a{ :href => "/newsletter/users/{{_id.$oid}}/edit", :class => 'btn btn-sm btn-success'} Edit + + %a{ :href => "/newsletter/users/{{_id.$oid}}", data:{ :confirm => 'Are you sure?', method: 'delete'}, :class => 'btn btn-sm btn-danger' } Delete + + .col-lg-12.padding_none.text-center + #user_pagination.col-lg-12 + .col-lg-6.content :javascript - data = #{@users.to_json}; + var users_data = #{@users.to_json} + var total_count = #{@users.count} + $('#searchbox').css('height',30) + $('.per-page').css('width',90) + initialize_filterjs_table('#users tbody', users_data, '#user-template', '', [], '#user_pagination', '#users_per_page') From ad11600aa8c9d76d92f7b5dc78b675aac7418338 Mon Sep 17 00:00:00 2001 From: Ashish Date: Sat, 13 Jun 2020 09:53:27 +0530 Subject: [PATCH 2/2] Either replace field - is_subscribed with sidekiq_status or removed it totally wherever required --- app/controllers/light/users_controller.rb | 10 +++++----- app/models/light/user.rb | 20 +++++++++---------- app/views/light/users/_form.html.haml | 1 - app/views/light/users/edit.html.haml | 1 - app/views/light/users/show.html.haml | 2 +- app/workers/light/import_worker.rb | 2 +- app/workers/light/user_worker.rb | 4 ++-- lib/tasks/light_tasks.rake | 8 ++++---- .../light/users_controller_spec.rb | 11 ++++------ spec/models/light/user_spec.rb | 4 ---- 10 files changed, 27 insertions(+), 36 deletions(-) diff --git a/app/controllers/light/users_controller.rb b/app/controllers/light/users_controller.rb index 67096c2..5121b0e 100644 --- a/app/controllers/light/users_controller.rb +++ b/app/controllers/light/users_controller.rb @@ -43,16 +43,16 @@ def sendtest end def unsubscribe - unless(@user.is_subscribed) + unless(@user.sidekiq_status == 'Subscribed') @message = 'You have already unsubscribed!!' else - @user.update(is_subscribed: 'false') + @user.update(sidekiq_status: 'Unsubscribed') @message = 'Unsubscribed successfully!!' end end def subscribe - @user.update(is_subscribed: 'true', subscribed_at: DateTime.now, remote_ip: request.remote_ip, user_agent: request.env['HTTP_USER_AGENT']) + @user.update(sidekiq_status: 'Subscribed', subscribed_at: DateTime.now, remote_ip: request.remote_ip, user_agent: request.env['HTTP_USER_AGENT']) end def sendmailer @@ -105,7 +105,7 @@ def auto_opt_in def opt_in @user = Light::User.where(email_id: params[:email]).first if @user.present? - if @user.is_subscribed.eql?(false) + if @user.sidekiq_status.eql?('Unsubscribed') Light::UserMailer.auto_opt_in(@user.email_id, @user.slug, @user.token).deliver #send email end @@ -130,7 +130,7 @@ def thank_you private def users_params - params.require(:user).permit(:id, :email_id, :is_subscribed, :joined_on, :source, :username) + params.require(:user).permit(:id, :email_id, :sidekiq_status, :joined_on, :source, :username) end def user_with_token diff --git a/app/models/light/user.rb b/app/models/light/user.rb index e04cd02..e63bc08 100644 --- a/app/models/light/user.rb +++ b/app/models/light/user.rb @@ -27,7 +27,7 @@ class User slug :username - track_history on: [:opt_in_mail_sent_at, :subscribed_at, :remote_ip, :user_agent, :is_subscribed] + track_history on: [:opt_in_mail_sent_at, :subscribed_at, :remote_ip, :user_agent, :sidekiq_status] before_create do self.joined_on = Date.today self.sidekiq_status = NEW_USER if self.sidekiq_status.blank? @@ -37,14 +37,14 @@ class User end end - scope :subscribed_users, -> { where is_subscribed: true } - scope :unsubscribed_users, -> { where is_subscribed: false } - scope :new_users, -> { where(is_subscribed: false, sidekiq_status: NEW_USER) } - scope :blocked_users, -> { where(is_subscribed: false, sidekiq_status: 'Block') } - scope :bounced_users, -> { where(is_subscribed: false, sidekiq_status: 'Bounced') } - scope :spam_users, -> { where(is_subscribed: false, sidekiq_status: 'Spam') } - scope :invalid_users, -> { where(is_subscribed: false, sidekiq_status: 'Invalid') } - scope :opt_in_users, -> { where(is_subscribed: false, sidekiq_status: 'Opt in mail sent') } + scope :subscribed_users, -> { where sidekiq_status: 'Subscribed' } + scope :unsubscribed_users, -> { where sidekiq_status: 'Unsubscribed' } + scope :new_users, -> { where sidekiq_status: NEW_USER } + scope :blocked_users, -> { where sidekiq_status: 'Block' } + scope :bounced_users, -> { where sidekiq_status: 'Bounced' } + scope :spam_users, -> { where sidekiq_status: 'Spam' } + scope :invalid_users, -> { where sidekiq_status: 'Invalid' } + scope :opt_in_users, -> { where sidekiq_status: 'Opt in mail sent' } def self.add_users_from_worksheet(worksheet, column = 1) fails = [] @@ -53,7 +53,7 @@ def self.add_users_from_worksheet(worksheet, column = 1) user = new( email_id: worksheet[i + 1, column], username: worksheet[i + 1, column - 1], - is_subscribed: true, + sidekiq_status: NEW_USER, joined_on: Date.today, source: 'Google Spreadsheet') diff --git a/app/views/light/users/_form.html.haml b/app/views/light/users/_form.html.haml index 815dcd3..0ad8a08 100644 --- a/app/views/light/users/_form.html.haml +++ b/app/views/light/users/_form.html.haml @@ -5,7 +5,6 @@ = simple_form_for (@user), html: {role: 'form', class: 'form-horizontal'} do |f| = f.input :email_id, input_html:{class: 'span2'} = f.input :username - = f.input :is_subscribed, :as => :select = f.input :joined_on, input_html: { 'data-behaviour' => 'datepicker'} = f.input :source = f.button :submit, class: "btn btn-success" diff --git a/app/views/light/users/edit.html.haml b/app/views/light/users/edit.html.haml index 6e3693c..db5a8b2 100644 --- a/app/views/light/users/edit.html.haml +++ b/app/views/light/users/edit.html.haml @@ -6,7 +6,6 @@ %td = f.input :email_id, input_html:{class: 'span2'} = f.input :username - = f.input :is_subscribed, :as => :select = f.input :joined_on, input_html: { 'data-behaviour' => 'datepicker'} = f.input :source = f.button :submit, class: "btn btn-success" diff --git a/app/views/light/users/show.html.haml b/app/views/light/users/show.html.haml index 4cacdc5..5524732 100644 --- a/app/views/light/users/show.html.haml +++ b/app/views/light/users/show.html.haml @@ -3,4 +3,4 @@ =@user.username =@user.source =@user.joined_on -=@user.is_subscribed +=@user.sidekiq_status diff --git a/app/workers/light/import_worker.rb b/app/workers/light/import_worker.rb index 3b3a522..e3f693f 100644 --- a/app/workers/light/import_worker.rb +++ b/app/workers/light/import_worker.rb @@ -13,7 +13,7 @@ def perform(rows, email_id, source = "Business Card") email = "#{row[1]}" name = "#{row[0] || row[1]}" user = Light::User.create(username: name, email_id: email, source: source, - is_subscribed: false, sidekiq_status: Light::User::NEW_USER) if email.present? or name.present? + sidekiq_status: Light::User::NEW_USER) if email.present? or name.present? csv << [email, row[0], user.errors.messages] if user.present? and user.errors.present? end UserMailer.import_contacts_update(email_id, file_path).deliver diff --git a/app/workers/light/user_worker.rb b/app/workers/light/user_worker.rb index 992b28c..356ad99 100644 --- a/app/workers/light/user_worker.rb +++ b/app/workers/light/user_worker.rb @@ -5,7 +5,7 @@ class UserWorker def perform date = Date.today.strftime("%Y%m") - number_of_subscribed_users = Light::User.where(is_subscribed: true, :sent_on.nin => [date], is_blocked: {"$ne" => true}).count + number_of_subscribed_users = Light::User.where(sidekiq_status: 'Subscribed', :sent_on.nin => [date], is_blocked: {"$ne" => true}).count #number_of_subscribed_users = Light::User.users_for_opt_in_mail.count number_of_subscribed_users_count = number_of_subscribed_users current_batch = 0 @@ -17,7 +17,7 @@ def perform # order_by([:sent_on, :desc]).first if newsletter while number_of_subscribed_users > 0 - user_ids = Light::User.where(is_subscribed: true, :sent_on.nin => [date] , is_blocked: {"$ne" => true}).order_by([:email_id, :asc]).limit(users_in_batch).skip(users_in_batch*current_batch).collect { |user| user.id.to_s } + user_ids = Light::User.where(sidekiq_status: 'Subscribed', :sent_on.nin => [date] , is_blocked: {"$ne" => true}).order_by([:email_id, :asc]).limit(users_in_batch).skip(users_in_batch*current_batch).collect { |user| user.id.to_s } #user_ids = Light::User.users_for_opt_in_mail.order_by([:email_id, :asc]).limit(users_in_batch).skip(users_in_batch*current_batch).collect { |user| user.id.to_s } current_batch += 1 number_of_subscribed_users -= users_in_batch diff --git a/lib/tasks/light_tasks.rake b/lib/tasks/light_tasks.rake index 4c33ddb..7148d04 100644 --- a/lib/tasks/light_tasks.rake +++ b/lib/tasks/light_tasks.rake @@ -23,10 +23,10 @@ namespace :light do puts block_emails.count puts spam_emails.count - Light::User.where(:email_id.in => bounce_emails).update_all(is_subscribed: false, sidekiq_status: 'Bounced') - Light::User.where(:email_id.in => invalid_emails).update_all(is_subscribed: false, sidekiq_status: 'Invalid') - Light::User.where(:email_id.in => spam_emails).update_all(is_subscribed: false, sidekiq_status: 'Spam') - Light::User.where(:email_id.in => block_emails).update_all(is_subscribed: false, sidekiq_status: 'Block') + Light::User.where(:email_id.in => bounce_emails).update_all(sidekiq_status: 'Bounced') + Light::User.where(:email_id.in => invalid_emails).update_all(sidekiq_status: 'Invalid') + Light::User.where(:email_id.in => spam_emails).update_all(sidekiq_status: 'Spam') + Light::User.where(:email_id.in => block_emails).update_all(sidekiq_status: 'Block') #clean sendgrid... bounces.delete(delete_all: 1) invalid.delete(delete_all: 1) diff --git a/spec/controllers/light/users_controller_spec.rb b/spec/controllers/light/users_controller_spec.rb index 379bd2b..f84083a 100644 --- a/spec/controllers/light/users_controller_spec.rb +++ b/spec/controllers/light/users_controller_spec.rb @@ -40,16 +40,15 @@ module Light expect(response).to redirect_to(users_path) end - it "default status is new user and is_subscribed is false" do + it "default status is new user" do post :create, {user: @create_params} user = User.find_by(email_id: 'test@sub.com') expect(user.sidekiq_status).to eq 'new user' - expect(user.is_subscribed).to eq false end end it "not arise" do - post :create, {user: {email_id: "",username: "kanhaiya", is_subscribed: "false"}} + post :create, {user: {email_id: "",username: "kanhaiya"}} expect(response).to render_template("new") end end @@ -122,7 +121,7 @@ module Light context 'data from file import_users.csv' do let(:file) { Rack::Test::UploadedFile.new("#{Rails.root}/files/import_users.csv", 'text/csv') } - let!(:existing_user) {create :user, username: "Winona Bayer", email_id: "winona@gmail.com", is_subscribed: false } + let!(:existing_user) {create :user, username: "Winona Bayer", email_id: "winona@gmail.com", : false } it 'File to be imported should contain following data ' do users = [['Full Name', 'Email'], @@ -148,11 +147,9 @@ module Light existing_user.reload expect(existing_user).to be_present - expect(existing_user.is_subscribed).to eq(false) user = User.find_by(email_id: "claud@gmail.com") expect(user).to be_present - expect(user.is_subscribed).to eq(false) expect(user.sidekiq_status).to eq('new user') expect(user.source).to eq("Business Card") expect(user.username).to eq(user.email_id) # Since username is empty we are storing email id in username @@ -162,7 +159,7 @@ module Light context 'should return success if' do after do - create :user, username: "Winona Bayer", email_id: "winona@gmail.com", is_subscribed: false + create :user, username: "Winona Bayer", email_id: "winona@gmail.com" file = Rack::Test::UploadedFile.new(@file_path, 'text/csv') post :import, file: file diff --git a/spec/models/light/user_spec.rb b/spec/models/light/user_spec.rb index c837dde..0501bce 100644 --- a/spec/models/light/user_spec.rb +++ b/spec/models/light/user_spec.rb @@ -9,10 +9,6 @@ module Light expect(@user.email_id).to be_present end - it "validates presence of subcription" do - expect(@user.is_subscribed).to be_present - end - it "validates presence of joined date" do expect(@user.joined_on).to be_present end