rack-push-notification 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/Gemfile.lock +40 -1
  2. data/README.md +22 -5
  3. data/lib/rack/push-notification.rb +30 -108
  4. data/lib/rack/push-notification/admin.rb +129 -0
  5. data/lib/rack/push-notification/assets/images/wallpaper-clown-fish.jpg +0 -0
  6. data/lib/rack/push-notification/assets/javascripts/application.coffee +28 -0
  7. data/lib/rack/push-notification/assets/javascripts/collections/devices.coffee +28 -0
  8. data/lib/rack/push-notification/assets/javascripts/models/device.coffee +2 -0
  9. data/lib/rack/push-notification/assets/javascripts/routers/root.coffee +30 -0
  10. data/lib/rack/push-notification/assets/javascripts/rpn.coffee +14 -0
  11. data/lib/rack/push-notification/assets/javascripts/templates/_devices.jst.eco +23 -0
  12. data/lib/rack/push-notification/assets/javascripts/templates/_preview.jst.eco +24 -0
  13. data/lib/rack/push-notification/assets/javascripts/templates/compose.jst.eco +46 -0
  14. data/lib/rack/push-notification/assets/javascripts/templates/devices.jst.eco +12 -0
  15. data/lib/rack/push-notification/assets/javascripts/templates/pagination.jst.eco +12 -0
  16. data/lib/rack/push-notification/assets/javascripts/vendor/backbone.js +1431 -0
  17. data/lib/rack/push-notification/assets/javascripts/vendor/backbone.paginator.js +833 -0
  18. data/lib/rack/push-notification/assets/javascripts/vendor/codemirror.javascript.js +411 -0
  19. data/lib/rack/push-notification/assets/javascripts/vendor/codemirror.js +3047 -0
  20. data/lib/rack/push-notification/assets/javascripts/vendor/date.js +104 -0
  21. data/lib/rack/push-notification/assets/javascripts/vendor/jquery.js +9404 -0
  22. data/lib/rack/push-notification/assets/javascripts/vendor/underscore.js +1059 -0
  23. data/lib/rack/push-notification/assets/javascripts/views/compose.coffee +119 -0
  24. data/lib/rack/push-notification/assets/javascripts/views/devices.coffee +23 -0
  25. data/lib/rack/push-notification/assets/javascripts/views/pagination.coffee +29 -0
  26. data/lib/rack/push-notification/assets/stylesheets/_codemirror.sass +219 -0
  27. data/lib/rack/push-notification/assets/stylesheets/_preview.sass +148 -0
  28. data/lib/rack/push-notification/assets/stylesheets/screen.sass +110 -0
  29. data/lib/rack/push-notification/assets/views/index.haml +26 -0
  30. data/lib/rack/push-notification/device.rb +33 -0
  31. data/lib/rack/push-notification/migrations/001_base_schema.rb +26 -0
  32. data/lib/rack/push-notification/migrations/002_add_full_text_search.rb +23 -0
  33. data/rack-push-notification.gemspec +9 -1
  34. metadata +164 -8
  35. data/lib/rack/push-notification/version.rb +0 -5
@@ -0,0 +1,833 @@
1
+ /*! backbone.paginator - v0.1.54 - 8/18/2012
2
+ * http://github.com/addyosmani/backbone.paginator
3
+ * Copyright (c) 2012 Addy Osmani; Licensed MIT */
4
+
5
+ Backbone.Paginator = (function ( Backbone, _, $ ) {
6
+ "use strict";
7
+
8
+ var Paginator = {};
9
+ Paginator.version = "0.15";
10
+
11
+ // @name: clientPager
12
+ //
13
+ // @tagline: Paginator for client-side data
14
+ //
15
+ // @description:
16
+ // This paginator is responsible for providing pagination
17
+ // and sort capabilities for a single payload of data
18
+ // we wish to paginate by the UI for easier browsering.
19
+ //
20
+ Paginator.clientPager = Backbone.Collection.extend({
21
+
22
+ // Default values used when sorting and/or filtering.
23
+ initialize: function(){
24
+ this.useDiacriticsPlugin = true; // use diacritics plugin if available
25
+ this.useLevenshteinPlugin = true; // use levenshtein plugin if available
26
+
27
+ this.sortColumn = "";
28
+ this.sortDirection = "desc";
29
+ this.lastSortColumn = "";
30
+
31
+ this.fieldFilterRules = [];
32
+ this.lastFieldFilterRules = [];
33
+
34
+ this.filterFields = "";
35
+ this.filterExpression = "";
36
+ this.lastFilterExpression = "";
37
+ },
38
+
39
+ sync: function ( method, model, options ) {
40
+
41
+ var self = this;
42
+
43
+ // Create default values if no others are specified
44
+ _.defaults(self.paginator_ui, {
45
+ firstPage: 0,
46
+ currentPage: 1,
47
+ perPage: 5,
48
+ totalPages: 10
49
+ });
50
+
51
+ // Change scope of 'paginator_ui' object values
52
+ _.each(self.paginator_ui, function(value, key) {
53
+ if( _.isUndefined(self[key]) ) {
54
+ self[key] = self.paginator_ui[key];
55
+ }
56
+ });
57
+
58
+ // Some values could be functions, let's make sure
59
+ // to change their scope too and run them
60
+ var queryAttributes = {};
61
+ _.each(self.server_api, function(value, key){
62
+ if( _.isFunction(value) ) {
63
+ value = _.bind(value, self);
64
+ value = value();
65
+ }
66
+ queryAttributes[key] = value;
67
+ });
68
+
69
+ var queryOptions = _.clone(self.paginator_core);
70
+ _.each(queryOptions, function(value, key){
71
+ if( _.isFunction(value) ) {
72
+ value = _.bind(value, self);
73
+ value = value();
74
+ }
75
+ queryOptions[key] = value;
76
+ });
77
+
78
+ // Create default values if no others are specified
79
+ queryOptions = _.defaults(queryOptions, {
80
+ timeout: 25000,
81
+ cache: false,
82
+ type: 'GET',
83
+ dataType: 'jsonp'
84
+ });
85
+
86
+ queryOptions = _.extend(queryOptions, {
87
+ jsonpCallback: 'callback',
88
+ data: decodeURIComponent($.param(queryAttributes)),
89
+ processData: false,
90
+ url: _.result(queryOptions, 'url')
91
+ }, options);
92
+
93
+ return $.ajax( queryOptions );
94
+
95
+ },
96
+
97
+ nextPage: function () {
98
+ this.currentPage = ++this.currentPage;
99
+ this.pager();
100
+ },
101
+
102
+ previousPage: function () {
103
+ this.currentPage = --this.currentPage || 1;
104
+ this.pager();
105
+ },
106
+
107
+ goTo: function ( page ) {
108
+ if(page !== undefined){
109
+ this.currentPage = parseInt(page, 10);
110
+ this.pager();
111
+ }
112
+ },
113
+
114
+ howManyPer: function ( perPage ) {
115
+ if(perPage !== undefined){
116
+ var lastPerPage = this.perPage;
117
+ this.perPage = parseInt(perPage, 10);
118
+ this.currentPage = Math.ceil( ( lastPerPage * ( this.currentPage - 1 ) + 1 ) / perPage);
119
+ this.pager();
120
+ }
121
+ },
122
+
123
+
124
+ // setSort is used to sort the current model. After
125
+ // passing 'column', which is the model's field you want
126
+ // to filter and 'direction', which is the direction
127
+ // desired for the ordering ('asc' or 'desc'), pager()
128
+ // and info() will be called automatically.
129
+ setSort: function ( column, direction ) {
130
+ if(column !== undefined && direction !== undefined){
131
+ this.lastSortColumn = this.sortColumn;
132
+ this.sortColumn = column;
133
+ this.sortDirection = direction;
134
+ this.pager();
135
+ this.info();
136
+ }
137
+ },
138
+
139
+ // setFieldFilter is used to filter each value of each model
140
+ // according to `rules` that you pass as argument.
141
+ // Example: You have a collection of books with 'release year' and 'author'.
142
+ // You can filter only the books that were released between 1999 and 2003
143
+ // And then you can add another `rule` that will filter those books only to
144
+ // authors who's name start with 'A'.
145
+ setFieldFilter: function ( fieldFilterRules ) {
146
+ if( !_.isEmpty( fieldFilterRules ) ) {
147
+ this.lastFieldFilterRules = this.fieldFilterRules;
148
+ this.fieldFilterRules = fieldFilterRules;
149
+ this.pager();
150
+ this.info();
151
+ }
152
+ },
153
+
154
+ // doFakeFieldFilter can be used to get the number of models that will remain
155
+ // after calling setFieldFilter with a filter rule(s)
156
+ doFakeFieldFilter: function ( fieldFilterRules ) {
157
+ if( !_.isEmpty( fieldFilterRules ) ) {
158
+ var bkp_lastFieldFilterRules = this.lastFieldFilterRules;
159
+ var bkp_fieldFilterRules = this.fieldFilterRules;
160
+
161
+ this.lastFieldFilterRules = this.fieldFilterRules;
162
+ this.fieldFilterRules = fieldFilterRules;
163
+ this.pager();
164
+ this.info();
165
+
166
+ var cmodels = this.models.length;
167
+
168
+ this.lastFieldFilterRules = bkp_lastFieldFilterRules;
169
+ this.fieldFilterRules = bkp_fieldFilterRules;
170
+ this.pager();
171
+ this.info();
172
+
173
+ // Return size
174
+ return cmodels;
175
+ }
176
+ },
177
+
178
+ // setFilter is used to filter the current model. After
179
+ // passing 'fields', which can be a string referring to
180
+ // the model's field, an array of strings representing
181
+ // each of the model's fields or an object with the name
182
+ // of the model's field(s) and comparing options (see docs)
183
+ // you wish to filter by and
184
+ // 'filter', which is the word or words you wish to
185
+ // filter by, pager() and info() will be called automatically.
186
+ setFilter: function ( fields, filter ) {
187
+ if( fields !== undefined && filter !== undefined ){
188
+ this.filterFields = fields;
189
+ this.lastFilterExpression = this.filterExpression;
190
+ this.filterExpression = filter;
191
+ this.pager();
192
+ this.info();
193
+ }
194
+ },
195
+
196
+ // doFakeFilter can be used to get the number of models that will
197
+ // remain after calling setFilter with a `fields` and `filter` args.
198
+ doFakeFilter: function ( fields, filter ) {
199
+ if( fields !== undefined && filter !== undefined ){
200
+ var bkp_filterFields = this.filterFields;
201
+ var bkp_lastFilterExpression = this.lastFilterExpression;
202
+ var bkp_filterExpression = this.filterExpression;
203
+
204
+ this.filterFields = fields;
205
+ this.lastFilterExpression = this.filterExpression;
206
+ this.filterExpression = filter;
207
+ this.pager();
208
+ this.info();
209
+
210
+ var cmodels = this.models.length;
211
+
212
+ this.filterFields = bkp_filterFields;
213
+ this.lastFilterExpression = bkp_lastFilterExpression;
214
+ this.filterExpression = bkp_filterExpression;
215
+ this.pager();
216
+ this.info();
217
+
218
+ // Return size
219
+ return cmodels;
220
+ }
221
+ },
222
+
223
+ // pager is used to sort, filter and show the data
224
+ // you expect the library to display.
225
+ pager: function () {
226
+ var self = this,
227
+ disp = this.perPage,
228
+ start = (self.currentPage - 1) * disp,
229
+ stop = start + disp;
230
+
231
+ // Saving the original models collection is important
232
+ // as we could need to sort or filter, and we don't want
233
+ // to loose the data we fetched from the server.
234
+ if (self.origModels === undefined) {
235
+ self.origModels = self.models;
236
+ }
237
+
238
+ self.models = self.origModels;
239
+
240
+ // Check if sorting was set using setSort.
241
+ if ( this.sortColumn !== "" ) {
242
+ self.models = self._sort(self.models, this.sortColumn, this.sortDirection);
243
+ }
244
+
245
+ // Check if field-filtering was set using setFieldFilter
246
+ if ( !_.isEmpty( this.fieldFilterRules ) ) {
247
+ self.models = self._fieldFilter(self.models, this.fieldFilterRules);
248
+ }
249
+
250
+ // Check if filtering was set using setFilter.
251
+ if ( this.filterExpression !== "" ) {
252
+ self.models = self._filter(self.models, this.filterFields, this.filterExpression);
253
+ }
254
+
255
+ // If the sorting or the filtering was changed go to the first page
256
+ if ( this.lastSortColumn !== this.sortColumn || this.lastFilterExpression !== this.filterExpression || !_.isEqual(this.fieldFilterRules, this.lastFieldFilterRules) ) {
257
+ start = 0;
258
+ stop = start + disp;
259
+ self.currentPage = 1;
260
+
261
+ this.lastSortColumn = this.sortColumn;
262
+ this.lastFieldFilterRules = this.fieldFilterRules;
263
+ this.lastFilterExpression = this.filterExpression;
264
+ }
265
+
266
+ // We need to save the sorted and filtered models collection
267
+ // because we'll use that sorted and filtered collection in info().
268
+ self.sortedAndFilteredModels = self.models;
269
+
270
+ self.reset(self.models.slice(start, stop));
271
+ },
272
+
273
+ // The actual place where the collection is sorted.
274
+ // Check setSort for arguments explicacion.
275
+ _sort: function ( models, sort, direction ) {
276
+ models = models.sort(function (a, b) {
277
+ var ac = a.get(sort),
278
+ bc = b.get(sort);
279
+
280
+ if ( !ac || !bc ) {
281
+ return 0;
282
+ } else {
283
+ /* Make sure that both ac and bc are lowercase strings.
284
+ * .toString() first so we don't have to worry if ac or bc
285
+ * have other String-only methods.
286
+ */
287
+ ac = ac.toString().toLowerCase();
288
+ bc = bc.toString().toLowerCase();
289
+ }
290
+
291
+ if (direction === 'desc') {
292
+
293
+ // We need to know if there aren't any non-number characters
294
+ // and that there are numbers-only characters and maybe a dot
295
+ // if we have a float.
296
+ if((!ac.match(/[^\d\.]/) && ac.match(/[\d\.]*/)) &&
297
+ (!bc.match(/[^\d\.]/) && bc.match(/[\d\.]*/))
298
+ ){
299
+
300
+ if( (ac - 0) < (bc - 0) ) {
301
+ return 1;
302
+ }
303
+ if( (ac - 0) > (bc - 0) ) {
304
+ return -1;
305
+ }
306
+ } else {
307
+ if (ac < bc) {
308
+ return 1;
309
+ }
310
+ if (ac > bc) {
311
+ return -1;
312
+ }
313
+ }
314
+
315
+ } else {
316
+
317
+ //Same as the regexp check in the 'if' part.
318
+ if((!ac.match(/[^\d\.]/) && ac.match(/[\d\.]*/)) &&
319
+ (!bc.match(/[^\d\.]/) && bc.match(/[\d\.]*/))
320
+ ){
321
+ if( (ac - 0) < (bc - 0) ) {
322
+ return -1;
323
+ }
324
+ if( (ac - 0) > (bc - 0) ) {
325
+ return 1;
326
+ }
327
+ } else {
328
+ if (ac < bc) {
329
+ return -1;
330
+ }
331
+ if (ac > bc) {
332
+ return 1;
333
+ }
334
+ }
335
+
336
+ }
337
+
338
+ return 0;
339
+ });
340
+
341
+ return models;
342
+ },
343
+
344
+ // The actual place where the collection is field-filtered.
345
+ // Check setFieldFilter for arguments explicacion.
346
+ _fieldFilter: function( models, rules ) {
347
+
348
+ // Check if there are any rules
349
+ if ( _.isEmpty(rules) ) {
350
+ return models;
351
+ }
352
+
353
+ var filteredModels = [];
354
+
355
+ // Iterate over each rule
356
+ _.each(models, function(model){
357
+
358
+ var should_push = true;
359
+
360
+ // Apply each rule to each model in the collection
361
+ _.each(rules, function(rule){
362
+
363
+ // Don't go inside the switch if we're already sure that the model won't be included in the results
364
+ if( !should_push ){
365
+ return false;
366
+ }
367
+
368
+ should_push = false;
369
+
370
+ // The field's value will be passed to a custom function, which should
371
+ // return true (if model should be included) or false (model should be ignored)
372
+ if(rule.type === "function"){
373
+ var f = _.wrap(rule.value, function(func){
374
+ return func( model.get(rule.field) );
375
+ });
376
+ if( f() ){
377
+ should_push = true;
378
+ }
379
+
380
+ // The field's value is required to be non-empty
381
+ }else if(rule.type === "required"){
382
+ if( !_.isEmpty( model.get(rule.field).toString() ) ) {
383
+ should_push = true;
384
+ }
385
+
386
+ // The field's value is required to be greater tan N (numbers only)
387
+ }else if(rule.type === "min"){
388
+ if( !_.isNaN( Number( model.get(rule.field) ) ) &&
389
+ !_.isNaN( Number( rule.value ) ) &&
390
+ Number( model.get(rule.field) ) >= Number( rule.value ) ) {
391
+ should_push = true;
392
+ }
393
+
394
+ // The field's value is required to be smaller tan N (numbers only)
395
+ }else if(rule.type === "max"){
396
+ if( !_.isNaN( Number( model.get(rule.field) ) ) &&
397
+ !_.isNaN( Number( rule.value ) ) &&
398
+ Number( model.get(rule.field) ) <= Number( rule.value ) ) {
399
+ should_push = true;
400
+ }
401
+
402
+ // The field's value is required to be between N and M (numbers only)
403
+ }else if(rule.type === "range"){
404
+ if( !_.isNaN( Number( model.get(rule.field) ) ) &&
405
+ _.isObject( rule.value ) &&
406
+ !_.isNaN( Number( rule.value.min ) ) &&
407
+ !_.isNaN( Number( rule.value.max ) ) &&
408
+ Number( model.get(rule.field) ) >= Number( rule.value.min ) &&
409
+ Number( model.get(rule.field) ) <= Number( rule.value.max ) ) {
410
+ should_push = true;
411
+ }
412
+
413
+ // The field's value is required to be more than N chars long
414
+ }else if(rule.type === "minLength"){
415
+ if( model.get(rule.field).toString().length >= rule.value ) {
416
+ should_push = true;
417
+ }
418
+
419
+ // The field's value is required to be no more than N chars long
420
+ }else if(rule.type === "maxLength"){
421
+ if( model.get(rule.field).toString().length <= rule.value ) {
422
+ should_push = true;
423
+ }
424
+
425
+ // The field's value is required to be more than N chars long and no more than M chars long
426
+ }else if(rule.type === "rangeLength"){
427
+ if( _.isObject( rule.value ) &&
428
+ !_.isNaN( Number( rule.value.min ) ) &&
429
+ !_.isNaN( Number( rule.value.max ) ) &&
430
+ model.get(rule.field).toString().length >= rule.value.min &&
431
+ model.get(rule.field).toString().length <= rule.value.max ) {
432
+ should_push = true;
433
+ }
434
+
435
+ // The field's value is required to be equal to one of the values in rules.value
436
+ }else if(rule.type === "oneOf"){
437
+ if( _.isArray( rule.value ) &&
438
+ _.include( rule.value, model.get(rule.field) ) ) {
439
+ should_push = true;
440
+ }
441
+
442
+ // The field's value is required to be equal to the value in rules.value
443
+ }else if(rule.type === "equalTo"){
444
+ if( rule.value === model.get(rule.field) ) {
445
+ should_push = true;
446
+ }
447
+
448
+ // The field's value is required to match the regular expression
449
+ }else if(rule.type === "pattern"){
450
+ if( model.get(rule.field).toString().match(rule.value) ) {
451
+ should_push = true;
452
+ }
453
+
454
+ //Unknown type
455
+ }else{
456
+ should_push = false;
457
+ }
458
+
459
+ });
460
+
461
+ if( should_push ){
462
+ filteredModels.push(model);
463
+ }
464
+
465
+ });
466
+
467
+ return filteredModels;
468
+ },
469
+
470
+ // The actual place where the collection is filtered.
471
+ // Check setFilter for arguments explicacion.
472
+ _filter: function ( models, fields, filter ) {
473
+
474
+ // For example, if you had a data model containing cars like { color: '', description: '', hp: '' },
475
+ // your fields was set to ['color', 'description', 'hp'] and your filter was set
476
+ // to "Black Mustang 300", the word "Black" will match all the cars that have black color, then
477
+ // "Mustang" in the description and then the HP in the 'hp' field.
478
+ // NOTE: "Black Musta 300" will return the same as "Black Mustang 300"
479
+
480
+ // We accept fields to be a string, an array or an object
481
+ // but if string or array is passed we need to convert it
482
+ // to an object.
483
+
484
+ var self = this;
485
+
486
+ var obj_fields = {};
487
+
488
+ if( _.isString( fields ) ) {
489
+ obj_fields[fields] = {cmp_method: 'regexp'};
490
+ }else if( _.isArray( fields ) ) {
491
+ _.each(fields, function(field){
492
+ obj_fields[field] = {cmp_method: 'regexp'};
493
+ });
494
+ }else{
495
+ _.each(fields, function( cmp_opts, field ) {
496
+ obj_fields[field] = _.defaults(cmp_opts, { cmp_method: 'regexp' });
497
+ });
498
+ }
499
+
500
+ fields = obj_fields;
501
+
502
+ //Remove diacritic characters if diacritic plugin is loaded
503
+ if( _.has(Backbone.Paginator, 'removeDiacritics') && self.useDiacriticsPlugin ){
504
+ filter = Backbone.Paginator.removeDiacritics(filter);
505
+ }
506
+
507
+ // 'filter' can be only a string.
508
+ // If 'filter' is string we need to convert it to
509
+ // a regular expression.
510
+ // For example, if 'filter' is 'black dog' we need
511
+ // to find every single word, remove duplicated ones (if any)
512
+ // and transform the result to '(black|dog)'
513
+ if( filter === '' || !_.isString(filter) ) {
514
+ return models;
515
+ } else {
516
+ var words = filter.match(/\w+/ig);
517
+ var pattern = "(" + _.uniq(words).join("|") + ")";
518
+ var regexp = new RegExp(pattern, "igm");
519
+ }
520
+
521
+ var filteredModels = [];
522
+
523
+ // We need to iterate over each model
524
+ _.each( models, function( model ) {
525
+
526
+ var matchesPerModel = [];
527
+
528
+ // and over each field of each model
529
+ _.each( fields, function( cmp_opts, field ) {
530
+
531
+ var value = model.get( field );
532
+
533
+ if( value ) {
534
+
535
+ // The regular expression we created earlier let's us detect if a
536
+ // given string contains each and all of the words in the regular expression
537
+ // or not, but in both cases match() will return an array containing all
538
+ // the words it matched.
539
+ var matchesPerField = [];
540
+
541
+ if( _.has(Backbone.Paginator, 'removeDiacritics') && self.useDiacriticsPlugin ){
542
+ value = Backbone.Paginator.removeDiacritics(value.toString());
543
+ }else{
544
+ value = value.toString();
545
+ }
546
+
547
+ // Levenshtein cmp
548
+ if( cmp_opts.cmp_method === 'levenshtein' && _.has(Backbone.Paginator, 'levenshtein') && self.useLevenshteinPlugin ) {
549
+ var distance = Backbone.Paginator.levenshtein(value, filter);
550
+
551
+ _.defaults(cmp_opts, { max_distance: 0 });
552
+
553
+ if( distance <= cmp_opts.max_distance ) {
554
+ matchesPerField = _.uniq(words);
555
+ }
556
+
557
+ // Default (RegExp) cmp
558
+ }else{
559
+ matchesPerField = value.match( regexp );
560
+ }
561
+
562
+ matchesPerField = _.map(matchesPerField, function(match) {
563
+ return match.toString().toLowerCase();
564
+ });
565
+
566
+ _.each(matchesPerField, function(match){
567
+ matchesPerModel.push(match);
568
+ });
569
+
570
+ }
571
+
572
+ });
573
+
574
+ // We just need to check if the returned array contains all the words in our
575
+ // regex, and if it does, it means that we have a match, so we should save it.
576
+ matchesPerModel = _.uniq( _.without(matchesPerModel, "") );
577
+
578
+ if( _.isEmpty( _.difference(words, matchesPerModel) ) ) {
579
+ filteredModels.push(model);
580
+ }
581
+
582
+ });
583
+
584
+ return filteredModels;
585
+ },
586
+
587
+ // You shouldn't need to call info() as this method is used to
588
+ // calculate internal data as first/prev/next/last page...
589
+ info: function () {
590
+ var self = this,
591
+ info = {},
592
+ totalRecords = (self.sortedAndFilteredModels) ? self.sortedAndFilteredModels.length : self.length,
593
+ totalPages = Math.ceil(totalRecords / self.perPage);
594
+
595
+ info = {
596
+ totalUnfilteredRecords: self.origModels.length,
597
+ totalRecords: totalRecords,
598
+ currentPage: self.currentPage,
599
+ perPage: this.perPage,
600
+ totalPages: totalPages,
601
+ lastPage: totalPages,
602
+ previous: false,
603
+ next: false,
604
+ startRecord: totalRecords === 0 ? 0 : (self.currentPage - 1) * this.perPage + 1,
605
+ endRecord: Math.min(totalRecords, self.currentPage * this.perPage)
606
+ };
607
+
608
+ if (self.currentPage > 1) {
609
+ info.previous = self.currentPage - 1;
610
+ }
611
+
612
+ if (self.currentPage < info.totalPages) {
613
+ info.next = self.currentPage + 1;
614
+ }
615
+
616
+ info.pageSet = self.setPagination(info);
617
+
618
+ self.information = info;
619
+ return info;
620
+ },
621
+
622
+
623
+ // setPagination also is an internal function that shouldn't be called directly.
624
+ // It will create an array containing the pages right before and right after the
625
+ // actual page.
626
+ setPagination: function ( info ) {
627
+ var pages = [], i = 0, l = 0;
628
+
629
+
630
+ // How many adjacent pages should be shown on each side?
631
+ var ADJACENT = 3,
632
+ ADJACENTx2 = ADJACENT * 2,
633
+ LASTPAGE = Math.ceil(info.totalRecords / info.perPage),
634
+ LPM1 = -1;
635
+
636
+ if (LASTPAGE > 1) {
637
+ // not enough pages to bother breaking it up
638
+ if (LASTPAGE < (7 + ADJACENTx2)) {
639
+ for (i = 1, l = LASTPAGE; i <= l; i++) {
640
+ pages.push(i);
641
+ }
642
+ }
643
+ // enough pages to hide some
644
+ else if (LASTPAGE > (5 + ADJACENTx2)) {
645
+
646
+ //close to beginning; only hide later pages
647
+ if (info.currentPage < (1 + ADJACENTx2)) {
648
+ for (i = 1, l = 4 + ADJACENTx2; i < l; i++) {
649
+ pages.push(i);
650
+ }
651
+ }
652
+
653
+ // in middle; hide some front and some back
654
+ else if (LASTPAGE - ADJACENTx2 > info.currentPage && info.currentPage > ADJACENTx2) {
655
+ for (i = info.currentPage - ADJACENT; i <= info.currentPage + ADJACENT; i++) {
656
+ pages.push(i);
657
+ }
658
+ }
659
+ // close to end; only hide early pages
660
+ else {
661
+ for (i = LASTPAGE - (2 + ADJACENTx2); i <= LASTPAGE; i++) {
662
+ pages.push(i);
663
+ }
664
+ }
665
+ }
666
+ }
667
+
668
+ return pages;
669
+ }
670
+
671
+ });
672
+
673
+
674
+ // @name: requestPager
675
+ //
676
+ // Paginator for server-side data being requested from a backend/API
677
+ //
678
+ // @description:
679
+ // This paginator is responsible for providing pagination
680
+ // and sort capabilities for requests to a server-side
681
+ // data service (e.g an API)
682
+ //
683
+ Paginator.requestPager = Backbone.Collection.extend({
684
+
685
+ sync: function ( method, model, options ) {
686
+
687
+ var self = this;
688
+
689
+ // Create default values if no others are specified
690
+ _.defaults(self.paginator_ui, {
691
+ firstPage: 0,
692
+ currentPage: 1,
693
+ perPage: 5,
694
+ totalPages: 10
695
+ });
696
+
697
+ // Change scope of 'paginator_ui' object values
698
+ _.each(self.paginator_ui, function(value, key) {
699
+ if( _.isUndefined(self[key]) ) {
700
+ self[key] = self.paginator_ui[key];
701
+ }
702
+ });
703
+
704
+ // Some values could be functions, let's make sure
705
+ // to change their scope too and run them
706
+ var queryAttributes = {};
707
+ _.each(self.server_api, function(value, key){
708
+ if( _.isFunction(value) ) {
709
+ value = _.bind(value, self);
710
+ value = value();
711
+ }
712
+ queryAttributes[key] = value;
713
+ });
714
+
715
+ var queryOptions = _.clone(self.paginator_core);
716
+ _.each(queryOptions, function(value, key){
717
+ if( _.isFunction(value) ) {
718
+ value = _.bind(value, self);
719
+ value = value();
720
+ }
721
+ queryOptions[key] = value;
722
+ });
723
+
724
+ // Create default values if no others are specified
725
+ queryOptions = _.defaults(queryOptions, {
726
+ timeout: 25000,
727
+ cache: false,
728
+ type: 'GET',
729
+ dataType: 'jsonp'
730
+ });
731
+
732
+ // Allows the passing in of {data: {foo: 'bar'}} at request time to overwrite server_api defaults
733
+ if( options.data ){
734
+ options.data = decodeURIComponent($.param(_.extend(queryAttributes,options.data)));
735
+ }else{
736
+ options.data = decodeURIComponent($.param(queryAttributes));
737
+ }
738
+
739
+ queryOptions = _.extend(queryOptions, {
740
+ jsonpCallback: 'callback',
741
+ processData: false,
742
+ url: _.result(queryOptions, 'url')
743
+ }, options);
744
+
745
+ return $.ajax( queryOptions );
746
+
747
+ },
748
+
749
+ requestNextPage: function ( options ) {
750
+ if ( this.currentPage !== undefined ) {
751
+ this.currentPage += 1;
752
+ return this.pager( options );
753
+ } else {
754
+ var response = new $.Deferred();
755
+ response.reject();
756
+ return response.promise();
757
+ }
758
+ },
759
+
760
+ requestPreviousPage: function ( options ) {
761
+ if ( this.currentPage !== undefined ) {
762
+ this.currentPage -= 1;
763
+ return this.pager( options );
764
+ } else {
765
+ var response = new $.Deferred();
766
+ response.reject();
767
+ return response.promise();
768
+ }
769
+ },
770
+
771
+ updateOrder: function ( column ) {
772
+ if (column !== undefined) {
773
+ this.sortField = column;
774
+ this.pager();
775
+ }
776
+
777
+ },
778
+
779
+ goTo: function ( page, options ) {
780
+ if ( page !== undefined ) {
781
+ this.currentPage = parseInt(page, 10);
782
+ return this.pager( options );
783
+ } else {
784
+ var response = new $.Deferred();
785
+ response.reject();
786
+ return response.promise();
787
+ }
788
+ },
789
+
790
+ howManyPer: function ( count ) {
791
+ if( count !== undefined ){
792
+ this.currentPage = this.firstPage;
793
+ this.perPage = count;
794
+ this.pager();
795
+ }
796
+ },
797
+
798
+ sort: function () {
799
+ //assign to as needed.
800
+ },
801
+
802
+ info: function () {
803
+
804
+ var info = {
805
+ // If parse() method is implemented and totalRecords is set to the length
806
+ // of the records returned, make it available. Else, default it to 0
807
+ totalRecords: this.totalRecords || 0,
808
+
809
+ currentPage: this.currentPage,
810
+ firstPage: this.firstPage,
811
+ totalPages: this.totalPages,
812
+ lastPage: this.totalPages,
813
+ perPage: this.perPage
814
+ };
815
+
816
+ this.information = info;
817
+ return info;
818
+ },
819
+
820
+ // fetches the latest results from the server
821
+ pager: function ( options ) {
822
+ if ( !_.isObject(options) ) {
823
+ options = {};
824
+ }
825
+ return this.fetch( options );
826
+ }
827
+
828
+
829
+ });
830
+
831
+ return Paginator;
832
+
833
+ }( Backbone, _, jQuery ));