ajax_table_rails 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/README.md +44 -29
- data/app/assets/javascripts/ajaxtable.js +128 -128
- data/lib/ajax_table_rails/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7daf0536ef80c2d76d7eeadd42c5e4b3dbd1b5a4
|
4
|
+
data.tar.gz: 3dc3a84175edbdb15d2d139d391edf9098032763
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3778d000593bdfe61d1ae038dfba7de8a79151938444e48501544e8cb134e49e3428de682ccdc364dec2c91d6f985c03a1914be5fb481efc6a048da57eb8e897
|
7
|
+
data.tar.gz: bfa11c01eccf58b264937061988fbe6ef2f15781bda807aa416c307a1fa7b2c6883f434beb00f0f6ad8d9695832db6be0df83cf2cfff10a09285fee2594d7908
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# AjaxTableRails
|
2
2
|
|
3
|
-
AjaxTableRails is a super simple, super lightweight library if all you want is fast, JSON-loaded tables with ajax sorting and
|
3
|
+
AjaxTableRails is a super simple, super lightweight library if all you want is fast, JSON-loaded tables with ajax sorting, pagination, and filtering. It provides just enough to get you going and doesn't do anything fancy. It is also still very much in early development, so there are probably bugs.
|
4
4
|
|
5
5
|
## Usage
|
6
6
|
|
@@ -26,12 +26,12 @@ Add the following to your `app/assets/javascripts/application.js`:
|
|
26
26
|
|
27
27
|
### Basic usage
|
28
28
|
|
29
|
-
####
|
29
|
+
#### Table structure
|
30
30
|
|
31
|
-
|
31
|
+
Your table should look like this:
|
32
32
|
|
33
33
|
````
|
34
|
-
<table
|
34
|
+
<table data-source="<%= users_path(format: :json)%>" id="users-table">
|
35
35
|
<thead>
|
36
36
|
<tr>
|
37
37
|
<th data-sort-column="name">Name</th>
|
@@ -47,26 +47,42 @@ In your view (using Users as an example):
|
|
47
47
|
</table>
|
48
48
|
````
|
49
49
|
|
50
|
-
|
50
|
+
Records are inserted into the `tbody` and record count and pagination are inserted into the `tfoot`.
|
51
51
|
|
52
|
-
**
|
52
|
+
**Data Attributes**
|
53
53
|
|
54
54
|
| Attribute | Description |
|
55
55
|
| --------- | ----------- |
|
56
56
|
| table data-source | JSON path for your table data |
|
57
|
+
| th data-sort-column | Matches database column you'd like to sort against. |
|
57
58
|
|
58
|
-
|
59
|
+
#### Filtering results
|
59
60
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
61
|
+
You can optionally specify a form that will be bound to your table for simple table searching and reseting.
|
62
|
+
|
63
|
+
````
|
64
|
+
<form id="users-search">
|
65
|
+
<input class="ajax-table-search-input">
|
66
|
+
<a href="#" class="ajax-table-reset">x</a>
|
67
|
+
<input type="submit" value="Search">
|
68
|
+
</form>
|
69
|
+
````
|
70
|
+
|
71
|
+
The `ajax-table-search-input` and `ajax-table-reset` class names are required. Alternatively, you can call search and reset manually (see Advanced Usage, below).
|
72
|
+
|
73
|
+
#### Init that!
|
74
|
+
|
75
|
+
````
|
76
|
+
$(function() {
|
77
|
+
$('#users-table').ajaxTable({ searchForm: '#users-search' });
|
78
|
+
});
|
79
|
+
````
|
64
80
|
|
65
81
|
#### Build your controller
|
66
82
|
|
67
83
|
Call `set_ajax_table` in a `before_action` to set your sorting criteria, then setup the query in a JSON response block.
|
68
84
|
|
69
|
-
`set_ajax_table` populates `@order
|
85
|
+
`set_ajax_table` populates `@order`, `@page`, and `@search`, which you can use directly in your query. I use [kaminari](https://github.com/amatsuda/kaminari) for pagination and either a custom scope or [pg_search](https://github.com/Casecommons/pg_search) for searching, but you can use whatever you like.
|
70
86
|
|
71
87
|
````
|
72
88
|
before_action -> {
|
@@ -78,12 +94,14 @@ def index
|
|
78
94
|
format.html {}
|
79
95
|
format.json {
|
80
96
|
@users = User.order(@order).page(@page)
|
97
|
+
@users = @users.search(@search) if @search.present?
|
98
|
+
@total_count = @users.except(:order, :limit, :offset).count
|
81
99
|
}
|
82
100
|
end
|
83
101
|
end
|
84
102
|
````
|
85
103
|
|
86
|
-
**
|
104
|
+
**Data attributes**
|
87
105
|
|
88
106
|
| Attribute | Description |
|
89
107
|
| --------- | ----------- |
|
@@ -103,31 +121,19 @@ end
|
|
103
121
|
json.pagination do
|
104
122
|
json.per_page User.default_per_page
|
105
123
|
json.count @users.size
|
106
|
-
json.total_count
|
124
|
+
json.total_count @total_count
|
107
125
|
end
|
108
126
|
````
|
109
127
|
|
110
128
|
### Advanced usage
|
111
129
|
|
112
|
-
####
|
113
|
-
|
114
|
-
**Under construction.** Adding the following anywhere on your page will currently work.
|
115
|
-
|
116
|
-
````
|
117
|
-
<form class="ajax-table-search" data-ajax-table-id="users-table">
|
118
|
-
<input class="ajax-table-search-input">
|
119
|
-
<a href="#" class="ajax-table-reset">x</a>
|
120
|
-
<input type="submit" value="Search">
|
121
|
-
</form>
|
122
|
-
````
|
123
|
-
|
124
|
-
#### Customize your table
|
130
|
+
#### Custom settings
|
125
131
|
|
126
132
|
AjaxTableRails is built with Bootstrap and FontAwesome in mind, as well as some other defaults that may make you unhappy. You may want to override the classes used for pagination and sorting, as well as some other bits and bops. Here's what a full customization looks like (default values shown):
|
127
133
|
|
128
134
|
````
|
129
135
|
$(function() {
|
130
|
-
|
136
|
+
$('#users-table').ajaxTable({
|
131
137
|
cssClasses: {
|
132
138
|
count: 'at-count', // "Showing xx records out of xx" span
|
133
139
|
pagination: 'pagination', // Pagination ul, defaults to match Bootstrap
|
@@ -135,6 +141,7 @@ $(function() {
|
|
135
141
|
sortAsc: 'fa fa-sort-up', // Sort icon ascending indicator, defaults to use FontAwesome
|
136
142
|
sortDesc: 'fa fa-sort-down' // Sort icon descending indicator, defaults to use FontAwesome
|
137
143
|
},
|
144
|
+
searchForm: null, // Form selector to be automatically bound for searching this table
|
138
145
|
text: {
|
139
146
|
count: 'Showing {count} records out of {total_count}', // Pass null to skip rendering of this element
|
140
147
|
nextPage: '»',
|
@@ -144,6 +151,15 @@ $(function() {
|
|
144
151
|
});
|
145
152
|
````
|
146
153
|
|
154
|
+
#### Custom search and reset
|
155
|
+
|
156
|
+
The `search()` and `reset()` methods are public, so you're free to forego the simple automagic implementation and realize your wildest interface fantasies.
|
157
|
+
|
158
|
+
````
|
159
|
+
$('#users-table').ajaxTable('search', 'baby sloths');
|
160
|
+
$('#users-table').ajaxTable('reset');
|
161
|
+
````
|
162
|
+
|
147
163
|
#### Make it shiny
|
148
164
|
|
149
165
|
Use whatever CSS you like. Here's a rudimentary example of some things you may want to do.
|
@@ -176,7 +192,6 @@ Copyright © 2014 Yuval Kordov. See MIT-LICENSE for further details.
|
|
176
192
|
|
177
193
|
## TODO
|
178
194
|
|
179
|
-
* Result filtering
|
180
195
|
* Windowed pagination
|
181
196
|
* Show default sort
|
182
197
|
* Allow customization via data attributes
|
@@ -1,98 +1,12 @@
|
|
1
|
-
|
1
|
+
;(function ($, window, document, undefined) {
|
2
2
|
|
3
|
-
//
|
4
|
-
// ajaxify all tables with the `ajax-table` class
|
5
|
-
// bind search/reset via forms with the `ajax-table-search` class
|
6
|
-
//
|
7
|
-
// If you want to use custom settings for your tables, forego the `ajax-table` class and call init manually:
|
8
|
-
// $(function() {
|
9
|
-
// ajaxTable.init($('#some-table'), {
|
10
|
-
// cssClasses: { pagination: 'some-class' },
|
11
|
-
// text: { count: null },
|
12
|
-
// ...
|
13
|
-
// });
|
14
|
-
// });
|
15
|
-
//
|
16
|
-
// @see config Available settings
|
17
|
-
$(function() {
|
18
|
-
$('table.ajax-table').each(function() {
|
19
|
-
init($(this));
|
20
|
-
});
|
21
|
-
|
22
|
-
$('body').on('submit', 'form.ajax-table-search', function(e) {
|
23
|
-
var $form = $(this);
|
24
|
-
search($('#'+$form.data('ajax-table-id')), $form.find('input.ajax-table-search-input').val());
|
25
|
-
e.preventDefault();
|
26
|
-
});
|
27
|
-
|
28
|
-
$('body').on('click', '.ajax-table-reset', function(e) {
|
29
|
-
var $form = $(this).closest('form.ajax-table-search');
|
30
|
-
$form.find('input.ajax-table-search-input').val('');
|
31
|
-
resetTable($('#'+$form.data('ajax-table-id')));
|
32
|
-
e.preventDefault();
|
33
|
-
});
|
34
|
-
});
|
35
|
-
|
36
|
-
/* public */
|
37
|
-
|
38
|
-
// Load and build initial table, and bind static events
|
39
|
-
var init = function($table, options) {
|
40
|
-
$.extend(true, config, options);
|
41
|
-
$table.data('page', 1);
|
42
|
-
loadTable($table);
|
43
|
-
initSorting($table);
|
44
|
-
};
|
45
|
-
|
46
|
-
// Filter table records against submitted keywords
|
47
|
-
var search = function($table, val) {
|
48
|
-
$table.data('page', 1);
|
49
|
-
$table.data('search', val);
|
50
|
-
loadTable($table);
|
51
|
-
};
|
52
|
-
|
53
|
-
// Reset table to an unpaginated and unfiltered state
|
54
|
-
var resetTable = function($table) {
|
55
|
-
$table.data('page', 1);
|
56
|
-
$table.removeData('search');
|
57
|
-
loadTable($table);
|
58
|
-
};
|
59
|
-
|
60
|
-
/* private */
|
61
|
-
|
62
|
-
// Default config
|
63
|
-
// All of these settings can be overriden by manually calling `init`
|
64
|
-
var config = {
|
65
|
-
// You can safely use multiple classes here
|
66
|
-
// @example `pagination: 'pagination foo-pagination'`
|
67
|
-
cssClasses: {
|
68
|
-
count: 'at-count', // "Showing xx records out of xx" span
|
69
|
-
pagination: 'pagination', // Pagination ul, defaults to match Bootstrap
|
70
|
-
sort: 'at-sort', // Sort icon base class
|
71
|
-
sortAsc: 'fa fa-sort-up', // Sort icon ascending indicator, defaults to use FontAwesome
|
72
|
-
sortDesc: 'fa fa-sort-down' // Sort icon descending indicator, defaults to use FontAwesome
|
73
|
-
},
|
74
|
-
// @note Querystring param keys match up with those used by ajax_table.rb
|
75
|
-
params: {
|
76
|
-
page: 'page',
|
77
|
-
search: 'search',
|
78
|
-
sortColumn: 'sort',
|
79
|
-
sortDirection: 'direction'
|
80
|
-
},
|
81
|
-
// To not display count, pass `text.count: null`
|
82
|
-
text: {
|
83
|
-
count: 'Showing {count} records out of {total_count}',
|
84
|
-
nextPage: '»',
|
85
|
-
previousPage: '«'
|
86
|
-
}
|
87
|
-
};
|
88
|
-
|
89
|
-
// Load and render table, based on current page, sort, and search filter
|
3
|
+
// Load and build table and pagination based on current data state
|
90
4
|
var loadTable = function($table) {
|
91
5
|
params = {};
|
92
|
-
params[
|
93
|
-
params[
|
94
|
-
params[
|
95
|
-
params[
|
6
|
+
params[$table.data('ajaxTable').params.page] = $table.data('page');
|
7
|
+
params[$table.data('ajaxTable').params.sortColumn] = $table.data('sort-column');
|
8
|
+
params[$table.data('ajaxTable').params.sortDirection] = $table.data('sort-direction');
|
9
|
+
params[$table.data('ajaxTable').params.search] = $table.data('search');
|
96
10
|
|
97
11
|
var request = $.ajax({
|
98
12
|
url: $table.data('source'),
|
@@ -106,28 +20,6 @@ var ajaxTable = (function($) {
|
|
106
20
|
});
|
107
21
|
};
|
108
22
|
|
109
|
-
// Bind table headers to sort with direction
|
110
|
-
// @note To enable sorting on a header, add a `data-sort-column` attribute with the desired column name.
|
111
|
-
// @example `<th data-sort-column="email">Email</th>`
|
112
|
-
var initSorting = function($table) {
|
113
|
-
$table.find('th[data-sort-column]').on('click', function() {
|
114
|
-
// Reset pagination
|
115
|
-
$table.data('page', 1);
|
116
|
-
// Set direction based on prior and just-clicked sort column
|
117
|
-
var sortColumn = $(this).data('sort-column');
|
118
|
-
var direction = ($table.data('sort-column') == sortColumn && $table.data('sort-direction') == 'asc') ? 'desc' : 'asc';
|
119
|
-
$table.data('sort-direction', direction);
|
120
|
-
// Set new sort column
|
121
|
-
$table.data('sort-column', sortColumn);
|
122
|
-
// Remove and re-insert sort icon
|
123
|
-
$table.find('th i.' + config.cssClasses.sort).remove();
|
124
|
-
var $i = $('<i/>', { class: config.cssClasses.sort + ' ' + (direction == 'asc' ? config.cssClasses.sortAsc : config.cssClasses.sortDesc) });
|
125
|
-
$(this).append($i);
|
126
|
-
// Reload table
|
127
|
-
loadTable($table);
|
128
|
-
});
|
129
|
-
};
|
130
|
-
|
131
23
|
// Build table rows
|
132
24
|
var buildRows = function($table, rows) {
|
133
25
|
var $tbody = $table.find('tbody');
|
@@ -147,32 +39,32 @@ var ajaxTable = (function($) {
|
|
147
39
|
$td.children().remove();
|
148
40
|
if (pagination.total_count > pagination.count) {
|
149
41
|
// Display current out of total record count
|
150
|
-
if (
|
151
|
-
var $count = $('<span/>', { class:
|
42
|
+
if ($table.data('ajaxTable').text.count) {
|
43
|
+
var $count = $('<span/>', { class: $table.data('ajaxTable').cssClasses.count });
|
152
44
|
$td.append($count);
|
153
|
-
$count.html(
|
45
|
+
$count.html($table.data('ajaxTable').text.count.replace('{count}', pagination.count).replace('{total_count}', pagination.total_count));
|
154
46
|
}
|
155
47
|
// Build pagination controls
|
156
48
|
var pageCount = Math.ceil(pagination.total_count / pagination.per_page);
|
157
49
|
var currentPage = $table.data('page');
|
158
|
-
var $ul = $('<ul/>', { class:
|
50
|
+
var $ul = $('<ul/>', { class: $table.data('ajaxTable').cssClasses.pagination });
|
159
51
|
$td.append($ul);
|
160
52
|
if (currentPage > 1) {
|
161
53
|
var previousPage = currentPage-1;
|
162
|
-
$ul.append('<li><a href="#" data-page=' + previousPage + '>' +
|
54
|
+
$ul.append('<li><a href="#" data-page=' + previousPage + '>' + $table.data('ajaxTable').text.previousPage + '</a></li>');
|
163
55
|
}
|
164
56
|
for (var pageNumber = 1; pageNumber <= pageCount; pageNumber++) {
|
165
57
|
var li = '<li';
|
166
|
-
if (pageNumber == currentPage) li += ' class="active"';
|
58
|
+
if (pageNumber == currentPage || (pageNumber == 1 && !currentPage)) li += ' class="active"';
|
167
59
|
li += '>';
|
168
60
|
$ul.append(li + '<a href="#" data-page=' + pageNumber + '>' + pageNumber + '</a></li>');
|
169
61
|
}
|
170
62
|
if (currentPage < pageCount) {
|
171
63
|
var nextPage = currentPage+1;
|
172
|
-
$ul.append('<li><a href="#" data-page=' + nextPage + '>' +
|
64
|
+
$ul.append('<li><a href="#" data-page=' + nextPage + '>' + $table.data('ajaxTable').text.nextPage + '</a></li>');
|
173
65
|
}
|
174
66
|
// Bind pagination controls
|
175
|
-
$table.find('ul.' +
|
67
|
+
$table.find('ul.' + $table.data('ajaxTable').cssClasses.pagination.split(' ').join('.') + ' a').on('click', function(e) {
|
176
68
|
$table.data('page', $(this).data('page'));
|
177
69
|
loadTable($table);
|
178
70
|
e.preventDefault();
|
@@ -180,10 +72,118 @@ var ajaxTable = (function($) {
|
|
180
72
|
}
|
181
73
|
};
|
182
74
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
75
|
+
// Bind table headers to sort with direction
|
76
|
+
// @note To enable sorting on a header, add a `data-sort-column` attribute with the desired column name.
|
77
|
+
// @example `<th data-sort-column="email">Email</th>`
|
78
|
+
var initSorting = function($table) {
|
79
|
+
$table.find('th[data-sort-column]').on('click', function() {
|
80
|
+
// Reset pagination
|
81
|
+
$table.data('page', 1);
|
82
|
+
// Set direction based on prior and just-clicked sort column
|
83
|
+
var sortColumn = $(this).data('sort-column');
|
84
|
+
var direction = ($table.data('sort-column') == sortColumn && $table.data('sort-direction') == 'asc') ? 'desc' : 'asc';
|
85
|
+
$table.data('sort-direction', direction);
|
86
|
+
// Set new sort column
|
87
|
+
$table.data('sort-column', sortColumn);
|
88
|
+
// Remove and re-insert sort icon
|
89
|
+
$table.find('th i.' + $table.data('ajaxTable').cssClasses.sort).remove();
|
90
|
+
var $i = $('<i/>', { class: $table.data('ajaxTable').cssClasses.sort + ' ' + (direction == 'asc' ? $table.data('ajaxTable').cssClasses.sortAsc : $table.data('ajaxTable').cssClasses.sortDesc) });
|
91
|
+
$(this).append($i);
|
92
|
+
// Reload table
|
93
|
+
loadTable($table);
|
94
|
+
});
|
95
|
+
};
|
96
|
+
|
97
|
+
// Initialize search and reset based off of searchForm specified on init
|
98
|
+
var initSearch = function($table, searchForm) {
|
99
|
+
$('body').on('submit', searchForm, function(e) {
|
100
|
+
searchTable($table, $(this).find('input.ajax-table-search-input').val());
|
101
|
+
e.preventDefault();
|
102
|
+
});
|
103
|
+
|
104
|
+
$('body').on('click', '.ajax-table-reset', function(e) {
|
105
|
+
$(this).closest('form').find('input.ajax-table-search-input').val('');
|
106
|
+
resetTable($table);
|
107
|
+
e.preventDefault();
|
108
|
+
});
|
109
|
+
};
|
110
|
+
|
111
|
+
// Filter table records against submitted keywords
|
112
|
+
var searchTable = function($table, val) {
|
113
|
+
$table.data('page', 1);
|
114
|
+
$table.data('search', val);
|
115
|
+
loadTable($table);
|
116
|
+
};
|
117
|
+
|
118
|
+
// Reset table to an unpaginated and unfiltered state
|
119
|
+
var resetTable = function($table) {
|
120
|
+
$table.data('page', 1);
|
121
|
+
$table.removeData('search');
|
122
|
+
loadTable($table);
|
123
|
+
};
|
124
|
+
|
125
|
+
var publicMethods = {
|
126
|
+
|
127
|
+
init: function(options) {
|
128
|
+
return this.each(function() {
|
129
|
+
var $this = $(this);
|
130
|
+
var defaults = {
|
131
|
+
// You can safely use multiple classes here
|
132
|
+
// @example `pagination: 'pagination foo-pagination'`
|
133
|
+
cssClasses: {
|
134
|
+
count: 'at-count', // "Showing xx records out of xx" span
|
135
|
+
pagination: 'pagination', // Pagination ul, defaults to match Bootstrap
|
136
|
+
sort: 'at-sort', // Sort icon base class
|
137
|
+
sortAsc: 'fa fa-sort-up', // Sort icon ascending indicator, defaults to use FontAwesome
|
138
|
+
sortDesc: 'fa fa-sort-down' // Sort icon descending indicator, defaults to use FontAwesome
|
139
|
+
},
|
140
|
+
// @note Querystring param keys match up with those used by ajax_table.rb
|
141
|
+
params: {
|
142
|
+
page: 'page',
|
143
|
+
search: 'search',
|
144
|
+
sortColumn: 'sort',
|
145
|
+
sortDirection: 'direction'
|
146
|
+
},
|
147
|
+
searchForm: null, // Form selector to be automatically bound for searching this table
|
148
|
+
text: {
|
149
|
+
count: 'Showing {count} records out of {total_count}', // To not display count, pass `text.count: null`
|
150
|
+
nextPage: '»',
|
151
|
+
previousPage: '«'
|
152
|
+
}
|
153
|
+
}
|
154
|
+
|
155
|
+
var settings = $.extend(true, defaults, options);
|
156
|
+
$this.data('ajaxTable', settings);
|
157
|
+
loadTable($this);
|
158
|
+
initSorting($this);
|
159
|
+
if (settings.searchForm) {
|
160
|
+
initSearch($this, settings.searchForm);
|
161
|
+
}
|
162
|
+
});
|
163
|
+
},
|
164
|
+
|
165
|
+
search: function(val) {
|
166
|
+
var $this = $(this);
|
167
|
+
searchTable($this, val);
|
168
|
+
return $this;
|
169
|
+
},
|
170
|
+
|
171
|
+
reset: function() {
|
172
|
+
var $this = $(this);
|
173
|
+
resetTable($this);
|
174
|
+
return $this;
|
175
|
+
}
|
176
|
+
|
177
|
+
};
|
178
|
+
|
179
|
+
$.fn.ajaxTable = function(method) {
|
180
|
+
if (publicMethods[method]) {
|
181
|
+
return publicMethods[method].apply(this, Array.prototype.slice.call(arguments, 1));
|
182
|
+
} else if (typeof(method) === 'object' || !method) {
|
183
|
+
return publicMethods.init.apply(this, arguments);
|
184
|
+
} else {
|
185
|
+
$.error('Method ' + method + ' does not exist on jQuery.ajaxTable.');
|
186
|
+
}
|
187
|
+
};
|
188
188
|
|
189
|
-
})(jQuery);
|
189
|
+
})(jQuery, window, document);
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ajax_table_rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yuval Kordov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-01-
|
11
|
+
date: 2014-01-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|