togo 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/togo-admin +5 -5
- data/lib/togo/admin/admin.rb +45 -3
- data/lib/togo/admin/helpers.rb +0 -0
- data/lib/togo/admin/public/css/screen.css +43 -3
- data/lib/togo/admin/public/js/edit.js +35 -0
- data/lib/togo/admin/public/js/index.js +17 -0
- data/lib/togo/admin/public/js/togo.js +372 -1
- data/lib/togo/admin/views/edit.erb +3 -2
- data/lib/togo/admin/views/index.erb +11 -2
- data/lib/togo/dispatch/dispatch.rb +17 -7
- data/lib/togo/model/model.rb +50 -3
- data/lib/togo/model/types/belongs_to.erb +48 -12
- data/lib/togo/model/types/datetime.erb +1 -1
- data/lib/togo/model/types/has_n.erb +41 -10
- metadata +7 -5
data/bin/togo-admin
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require 'optparse'
|
4
|
-
files_to_require =
|
4
|
+
files_to_require = %w( dm-core dm-serializer )
|
5
5
|
|
6
6
|
config = {
|
7
7
|
:environment => :development,
|
@@ -18,9 +18,10 @@ OptionParser.new{|opt|
|
|
18
18
|
puts "Usage: togo-admin [options]"
|
19
19
|
puts "Options: "
|
20
20
|
puts "\t-p [port]: which port to run on"
|
21
|
-
puts "\t-
|
21
|
+
puts "\t-h [host]: which host to run on"
|
22
22
|
puts "\t-e [environment]: which environment to boot in"
|
23
|
-
puts "\t-r [file]:
|
23
|
+
puts "\t-r [file]: specify a file to require before running"
|
24
|
+
puts "\t-a [handler]: which handler to run the server with (e.g. Thin, Webrick, etc)"
|
24
25
|
puts "\t--help: what you're seeing"
|
25
26
|
puts "TIP: To load code automatically before running togo-admin, create a file called togo-admin-config.rb."
|
26
27
|
exit
|
@@ -42,5 +43,4 @@ config.merge!({
|
|
42
43
|
:reloader => Togo::TogoReloader
|
43
44
|
})
|
44
45
|
|
45
|
-
Togo::Admin.
|
46
|
-
Togo::Admin.run!
|
46
|
+
Togo::Admin.run!(config)
|
data/lib/togo/admin/admin.rb
CHANGED
@@ -1,9 +1,30 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module Helpers
|
2
|
+
|
3
|
+
def paging_links(page, count, qs = {})
|
4
|
+
prev_link, next_link = 'Previous', 'Next'
|
5
|
+
if not qs.blank?
|
6
|
+
qs = qs.keys.collect{|k|
|
7
|
+
[k,escape(qs[k])].join('=') if not qs[k].blank?
|
8
|
+
}.compact.join('&')
|
9
|
+
qs = nil if qs.blank?
|
10
|
+
end
|
11
|
+
|
12
|
+
if not page == 1
|
13
|
+
prev_link = "<a href=\"?p=#{[page-1, qs].compact.join('&')}\" rel=\"previous\">#{prev_link}</a>"
|
14
|
+
end
|
15
|
+
if not page == count and count > 1
|
16
|
+
next_link = "<a href=\"?p=#{[page+1, qs].compact.join('&')}\" rel=\"next\">#{next_link}</a>"
|
17
|
+
end
|
18
|
+
[prev_link, next_link]
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
3
22
|
|
4
23
|
module Togo
|
5
24
|
class Admin < Dispatch
|
6
25
|
|
26
|
+
include Helpers
|
27
|
+
|
7
28
|
before do
|
8
29
|
@model = Togo.const_get(params[:model]) if params[:model]
|
9
30
|
end
|
@@ -13,7 +34,14 @@ module Togo
|
|
13
34
|
end
|
14
35
|
|
15
36
|
get '/:model' do
|
16
|
-
@
|
37
|
+
@q = params[:q] || ''
|
38
|
+
@p = (params[:p] || 1).to_i
|
39
|
+
@limit = 50
|
40
|
+
@offset = @limit*(@p-1)
|
41
|
+
@count = (@q.blank? ? @model.all : @model.search(:q => @q)).size
|
42
|
+
@page_count = @count == 0 ? 1 : (@count.to_f/@limit.to_f).ceil
|
43
|
+
@criteria = {:limit => @limit, :offset => @offset}
|
44
|
+
@content = @q.blank? ? @model.all(@criteria) : @model.search(@criteria.merge(:q => @q))
|
17
45
|
erb :index
|
18
46
|
end
|
19
47
|
|
@@ -63,6 +91,20 @@ module Togo
|
|
63
91
|
end
|
64
92
|
end
|
65
93
|
|
94
|
+
get '/search/:model' do
|
95
|
+
@limit = params[:limit] || 10
|
96
|
+
@offset = params[:offset] || 0
|
97
|
+
@q = params[:q] || ''
|
98
|
+
@count = (@q.blank? ? @model.all : @model.search(:q => @q)).size
|
99
|
+
@criteria = {:offset => @offset, :limit => @limit}
|
100
|
+
if params[:ids]
|
101
|
+
@items = @model.all(@criteria.merge(:id => params[:ids].split(',').map(&:to_i)))
|
102
|
+
else
|
103
|
+
@items = @model.search(@criteria.merge(:q => @q))
|
104
|
+
end
|
105
|
+
{:count => @count, :results => @items}.to_json
|
106
|
+
end
|
107
|
+
|
66
108
|
end
|
67
109
|
|
68
110
|
# Subclass Rack Reloader to call DataMapper.auto_upgrade! on file reload
|
File without changes
|
@@ -71,7 +71,7 @@ body {
|
|
71
71
|
position: absolute;
|
72
72
|
right: 0;
|
73
73
|
z-index: 3;
|
74
|
-
width:
|
74
|
+
width: 220px;
|
75
75
|
top: 0;
|
76
76
|
}
|
77
77
|
#main #search-form fieldset {
|
@@ -80,6 +80,12 @@ body {
|
|
80
80
|
#main #search-form input[type=text] {
|
81
81
|
width: 190px;
|
82
82
|
}
|
83
|
+
#main #paging {
|
84
|
+
position: absolute;
|
85
|
+
right: 240px;
|
86
|
+
z-index: 3;
|
87
|
+
top: 22px;
|
88
|
+
}
|
83
89
|
#main h1 a {
|
84
90
|
text-decoration: none;
|
85
91
|
color: #4076c7;
|
@@ -105,7 +111,8 @@ body {
|
|
105
111
|
border-bottom: 1px solid #CECECE;
|
106
112
|
background: #FFF;
|
107
113
|
}
|
108
|
-
#main table td.checkbox
|
114
|
+
#main table td.checkbox,
|
115
|
+
#main table th.checkbox {
|
109
116
|
width: 10px;
|
110
117
|
text-align: center;
|
111
118
|
padding-right: 0;
|
@@ -154,11 +161,44 @@ body {
|
|
154
161
|
#main form fieldset.has_n {
|
155
162
|
padding-right: 20px;
|
156
163
|
}
|
164
|
+
#main form fieldset.belongs_to input[type=text],
|
165
|
+
#main form fieldset.has_n input[type=text] {
|
166
|
+
width: 190px;
|
167
|
+
}
|
168
|
+
#main form fieldset.belongs_to label,
|
169
|
+
#main form fieldset.has_n label {
|
170
|
+
float: left;
|
171
|
+
}
|
172
|
+
#main form fieldset.belongs_to div.search,
|
173
|
+
#main form fieldset.has_n div.search {
|
174
|
+
display: none;
|
175
|
+
width: 100%;
|
176
|
+
text-align: right;
|
177
|
+
}
|
178
|
+
#main form fieldset.belongs_to div.paging,
|
179
|
+
#main form fieldset.has_n div.paging {
|
180
|
+
float: right;
|
181
|
+
margin-left: 10px;
|
182
|
+
line-height: 30px;
|
183
|
+
}
|
184
|
+
#main form fieldset.belongs_to .checkbox,
|
185
|
+
#main form fieldset.has_n .checkbox {
|
186
|
+
display: none;
|
187
|
+
}
|
188
|
+
#main form fieldset.belongs_to .active,
|
189
|
+
#main form fieldset.has_n .active {
|
190
|
+
display: table-cell;
|
191
|
+
}
|
157
192
|
#main form fieldset table th,
|
158
193
|
#main form fieldset table td {
|
159
194
|
padding: 5px;
|
160
195
|
}
|
161
|
-
|
196
|
+
#main form fieldset table tfoot td {
|
197
|
+
background: #DDD;
|
198
|
+
}
|
199
|
+
#main form fieldset table tfoot td button {
|
200
|
+
float: right;
|
201
|
+
}
|
162
202
|
div.actions {
|
163
203
|
position: absolute;
|
164
204
|
bottom: 0;
|
@@ -0,0 +1,35 @@
|
|
1
|
+
var hasModified = false;
|
2
|
+
function saveBeforeLeaving(e) {
|
3
|
+
return 'You have made changes to this item. Are you sure you want to leave before saving?';
|
4
|
+
}
|
5
|
+
function checkForModifiedText(e) {
|
6
|
+
if ((e.target.nodeName == 'INPUT' && e.target.getAttribute('type') == 'text') ||
|
7
|
+
e.target.nodeName == 'TEXTAREA') {
|
8
|
+
console.log('modified');
|
9
|
+
hasModified = (e.target.value != e.target.defaultValue);
|
10
|
+
setBeforeLeaving();
|
11
|
+
}
|
12
|
+
}
|
13
|
+
function checkForModifiedInputs(e) {
|
14
|
+
if (e.target.nodeName == 'SELECT' ||
|
15
|
+
(e.target.nodeName == 'INPUT' && e.target.getAttribute('type') == 'checkbox') ||
|
16
|
+
(e.target.nodeName == 'INPUT' && e.target.getAttribute('type') == 'radio')) {
|
17
|
+
console.log('modified');
|
18
|
+
hasModified = true;
|
19
|
+
setBeforeLeaving();
|
20
|
+
}
|
21
|
+
}
|
22
|
+
function setBeforeLeaving() {
|
23
|
+
if (hasModified) {
|
24
|
+
listen('beforeunload', window, saveBeforeLeaving);
|
25
|
+
} else {
|
26
|
+
unlisten('beforeunload', window, saveBeforeLeaving);
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
listen('keyup', window, checkForModifiedText);
|
31
|
+
listen('click', window, checkForModifiedInputs);
|
32
|
+
listen('submit', el('edit-form'), function() {
|
33
|
+
hasModified = false;
|
34
|
+
setBeforeLeaving();
|
35
|
+
});
|
@@ -1,6 +1,20 @@
|
|
1
1
|
var app = (function() {
|
2
2
|
|
3
3
|
var selectedItems = [];
|
4
|
+
var searchInput = el('search-form').getElementsByTagName('input')[0];
|
5
|
+
var searchInputDefaultValue = 'Search...';
|
6
|
+
|
7
|
+
var searchInputHandler = function(action) {
|
8
|
+
return function(e) {
|
9
|
+
console.log(e);
|
10
|
+
if (this.value == '') {
|
11
|
+
this.value = searchInputDefaultValue;
|
12
|
+
}
|
13
|
+
if (this.value == searchInputDefaultValue && action == 'focus') {
|
14
|
+
this.value = '';
|
15
|
+
}
|
16
|
+
};
|
17
|
+
};
|
4
18
|
|
5
19
|
var handleMultiSelect = function(e) {
|
6
20
|
var id = e.target.getAttribute('id').split('_')[1];
|
@@ -22,6 +36,9 @@ var app = (function() {
|
|
22
36
|
el('delete-list').value = selectedItems.join(',');
|
23
37
|
};
|
24
38
|
|
39
|
+
listen('focus', searchInput, searchInputHandler('focus'));
|
40
|
+
listen('blur', searchInput, searchInputHandler('blur'));
|
41
|
+
|
25
42
|
var c = el('list-table').getElementsByTagName('input');
|
26
43
|
for (var i = 0; i < c.length; i++) {
|
27
44
|
if (c[i].getAttribute('type') == 'checkbox') {
|
@@ -1,3 +1,374 @@
|
|
1
1
|
function el(node) {
|
2
2
|
return document.getElementById(node);
|
3
|
-
}
|
3
|
+
}
|
4
|
+
|
5
|
+
function hasClass(node, name) {
|
6
|
+
if (!node || !node.className) { return false; }
|
7
|
+
return node.className.split(' ').some(function (n) { return n === name; });
|
8
|
+
}
|
9
|
+
|
10
|
+
function addClass(node, name) {
|
11
|
+
if (!node) { return; }
|
12
|
+
if (typeof(node.length) == 'undefined') { node = [node]; }
|
13
|
+
for (var i = 0; i < node.length; i++) {
|
14
|
+
if (!hasClass(node[i], name)) {
|
15
|
+
var curClasses = node[i].className.split(' ');
|
16
|
+
curClasses.push(name);
|
17
|
+
node[i].className = curClasses.join(' ');
|
18
|
+
}
|
19
|
+
}
|
20
|
+
};
|
21
|
+
|
22
|
+
function removeClass(node, name) {
|
23
|
+
if (!node) { return; }
|
24
|
+
if (typeof(node.length) == 'undefined') { node = [node]; }
|
25
|
+
for (var i = 0; i < node.length; i++) {
|
26
|
+
node[i].className = node[i].className.replace(new RegExp("(\\s" + name + ")|(" + name + "\\s)|" + name), "");
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
function toggleClass(node,name) {
|
31
|
+
if (hasClass(node,name)) {
|
32
|
+
removeClass(node,name);
|
33
|
+
} else {
|
34
|
+
addClass(node,name);
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
var parseJSON = function(json) {
|
39
|
+
if (window.JSON) {
|
40
|
+
parseJSON = function(json) {
|
41
|
+
return JSON.parse(json);
|
42
|
+
};
|
43
|
+
} else {
|
44
|
+
parseJSON = function(json) {
|
45
|
+
return eval("(" + json + ")");
|
46
|
+
};
|
47
|
+
}
|
48
|
+
return parseJSON(json);
|
49
|
+
};
|
50
|
+
|
51
|
+
var ajaxObject = function() {
|
52
|
+
if (window.ActiveXObject) {
|
53
|
+
ajaxObject = function() {
|
54
|
+
return new ActiveXObject('Microsoft.XMLHTTP');
|
55
|
+
};
|
56
|
+
} else {
|
57
|
+
ajaxObject = function() {
|
58
|
+
return new XMLHttpRequest();
|
59
|
+
};
|
60
|
+
}
|
61
|
+
return ajaxObject();
|
62
|
+
}
|
63
|
+
|
64
|
+
var ajax = function(opts) {
|
65
|
+
var req = ajaxObject();
|
66
|
+
req.onreadystatechange = function() {
|
67
|
+
switch (req.readyState) {
|
68
|
+
case 1:
|
69
|
+
if (typeof opts.loading == 'function') { opts.loading(); }
|
70
|
+
break;
|
71
|
+
case 2:
|
72
|
+
if (typeof opts.loaded == 'function') { opts.loaded(); }
|
73
|
+
break;
|
74
|
+
case 4:
|
75
|
+
var res = req.responseText;
|
76
|
+
switch (req.status) {
|
77
|
+
case 200: if (typeof opts.success == 'function') { opts.success(res); } break;
|
78
|
+
case 404: if (typeof opts.error == 'function') { opts.error(); } break;
|
79
|
+
case 500: if (typeof opts.error == 'function') { opts.error(); } break;
|
80
|
+
};
|
81
|
+
if (typeof opts.complete == 'function') { opts.complete(); }
|
82
|
+
break;
|
83
|
+
};
|
84
|
+
};
|
85
|
+
req.open((opts.type || 'get').toUpperCase(),opts.url,true);
|
86
|
+
req.send(null);
|
87
|
+
};
|
88
|
+
|
89
|
+
var listen = function(evnt, elem, func) {
|
90
|
+
if (elem.addEventListener) {
|
91
|
+
listen = function(evnt, elem, func) {
|
92
|
+
elem.addEventListener(evnt,func,false);
|
93
|
+
};
|
94
|
+
} else if (elem.attachEvent) {
|
95
|
+
listen = function(evnt, elem, func) {
|
96
|
+
elem.attachEvent('on' + evnt, func);
|
97
|
+
};
|
98
|
+
}
|
99
|
+
listen(evnt,elem,func);
|
100
|
+
};
|
101
|
+
|
102
|
+
var unlisten = function(evnt, elem, func) {
|
103
|
+
if (elem.removeEventListener) {
|
104
|
+
unlisten = function(evnt, elem, func) {
|
105
|
+
elem.removeEventListener(evnt,func,false);
|
106
|
+
};
|
107
|
+
} else if (elem.attachEvent) {
|
108
|
+
unlisten = function(evnt, elem, func) {
|
109
|
+
elem.detachEvent('on' + evnt, func);
|
110
|
+
};
|
111
|
+
}
|
112
|
+
unlisten(evnt,elem,func);
|
113
|
+
};
|
114
|
+
|
115
|
+
var getElementsByClassName = function(classname, el) {
|
116
|
+
if (document.getElementsByClassName) {
|
117
|
+
getElementsByClassName = function(classname, el) {
|
118
|
+
if (!el) { el = document; }
|
119
|
+
return el.getElementsByClassName(classname);
|
120
|
+
};
|
121
|
+
} else {
|
122
|
+
getElementsByClassName = function(classname, el) {
|
123
|
+
var classElements = new Array();
|
124
|
+
if (!el) { el = document; }
|
125
|
+
var els = el.getElementsByTagName('*');
|
126
|
+
var elsLen = els.length;
|
127
|
+
var pattern = new RegExp("(^|\\s)"+classname+"(\\s|$)");
|
128
|
+
for (var i = 0, j = 0; i < elsLen; i++) {
|
129
|
+
if ( pattern.test(els[i].className) ) {
|
130
|
+
classElements[j] = els[i];
|
131
|
+
j++;
|
132
|
+
}
|
133
|
+
}
|
134
|
+
return classElements;
|
135
|
+
};
|
136
|
+
}
|
137
|
+
return getElementsByClassName(classname, el);
|
138
|
+
};
|
139
|
+
|
140
|
+
var AssociationManager = function(opts) {
|
141
|
+
this.relationship = opts.relationship;
|
142
|
+
this.name = opts.name;
|
143
|
+
this.model = opts.model;
|
144
|
+
this.fields = opts.fields;
|
145
|
+
this.count = opts.count;
|
146
|
+
this.pageCount = Math.ceil(this.count/10);
|
147
|
+
this.pageNumber = 1;
|
148
|
+
this.relatedIds = {};
|
149
|
+
|
150
|
+
for (var i = 0, len = opts.related_ids.length; i < len; i++) {
|
151
|
+
this.relatedIds[opts.related_ids[i]] = true;
|
152
|
+
}
|
153
|
+
|
154
|
+
this.fieldset = el('property-' + this.name);
|
155
|
+
|
156
|
+
this.pageNumberField = getElementsByClassName('page-number',this.fieldset)[0];
|
157
|
+
this.pageCountField = getElementsByClassName('page-count',this.fieldset)[0];
|
158
|
+
|
159
|
+
this.search = getElementsByClassName('search', this.fieldset)[0];
|
160
|
+
this.searchInput = this.search.getElementsByTagName('input')[0];
|
161
|
+
this.defaultSearchValue = this.searchInput.value;
|
162
|
+
|
163
|
+
listen('keydown',this.search,this.searchHandler(this));
|
164
|
+
listen('focus', this.searchInput,this.searchInputHandler(this, 'focus'));
|
165
|
+
listen('blur', this.searchInput,this.searchInputHandler(this, 'blur'));
|
166
|
+
|
167
|
+
this.table = this.fieldset.getElementsByTagName('table')[0];
|
168
|
+
this.tbody = this.table.getElementsByTagName('tbody')[0];
|
169
|
+
this.associatedCountDisplay = getElementsByClassName('associated-count-display',this.table)[0];
|
170
|
+
this.associatedCount = getElementsByClassName('associated-count',this.table)[0];
|
171
|
+
|
172
|
+
listen('click',this.table,this.tableHandler(this));
|
173
|
+
this.modifyButton = getElementsByClassName('association-modify', this.table)[0];
|
174
|
+
listen('click',this.modifyButton,this.modifyHandler(this));
|
175
|
+
|
176
|
+
this.paging = getElementsByClassName('paging',this.fieldset)[0];
|
177
|
+
listen('click',this.paging,this.pagingHandler(this));
|
178
|
+
|
179
|
+
this.selectorType = this.relationship == 'belongs_to' ? 'radio' : 'checkbox';
|
180
|
+
|
181
|
+
this.states = {
|
182
|
+
MODIFY: 'modify',
|
183
|
+
SEARCH: 'search',
|
184
|
+
PAGING: 'paging'
|
185
|
+
};
|
186
|
+
};
|
187
|
+
|
188
|
+
// Shows/hides all checkboxes table cells, called when 'modify' is clicked
|
189
|
+
AssociationManager.prototype.toggleCheckboxes = function() {
|
190
|
+
var checkboxes = getElementsByClassName('checkbox', this.table),
|
191
|
+
func = (this.currentState == this.states.MODIFY) ? addClass : removeClass;
|
192
|
+
for (var i = 0, len = checkboxes.length; i < len; i++) {
|
193
|
+
func(checkboxes[i],'active');
|
194
|
+
}
|
195
|
+
};
|
196
|
+
|
197
|
+
// Adjust the list of related content IDs, called after clicking 'done'
|
198
|
+
AssociationManager.prototype.adjustRelatedList = function() {
|
199
|
+
var selected = [];
|
200
|
+
for (k in this.relatedIds) {
|
201
|
+
if (this.relatedIds[k] == true) {
|
202
|
+
selected.push(k);
|
203
|
+
}
|
204
|
+
}
|
205
|
+
getElementsByClassName('related_ids', this.fieldset)[0].value = selected.join(',');
|
206
|
+
var handler = this;
|
207
|
+
ajax({
|
208
|
+
url: "/search/" + this.model + "?ids=" + selected.slice(0,10).join(','),
|
209
|
+
success: function(data) {
|
210
|
+
handler.searchResultsHandler(handler)(data);
|
211
|
+
handler.count = selected.length;
|
212
|
+
if (selected.length < 10) {
|
213
|
+
handler.associatedCount.innerHTML = 0;
|
214
|
+
handler.associatedCountDisplay.style.display = 'none';
|
215
|
+
} else {
|
216
|
+
handler.associatedCount.innerHTML = selected.length-10;
|
217
|
+
handler.associatedCountDisplay.style.display = 'block';
|
218
|
+
}
|
219
|
+
}
|
220
|
+
});
|
221
|
+
};
|
222
|
+
|
223
|
+
AssociationManager.prototype.setToModifyState = function() {
|
224
|
+
console.log('setToModifyState');
|
225
|
+
ajax({
|
226
|
+
url: "/search/" + this.model,
|
227
|
+
success: this.searchResultsHandler(this)
|
228
|
+
});
|
229
|
+
};
|
230
|
+
|
231
|
+
AssociationManager.prototype.addLinkToValue = function(id, value, linkIt) {
|
232
|
+
if (!linkIt) { return value; }
|
233
|
+
return '<a href="/edit/' + this.model + '/' + id + '">' + value + '</a>';
|
234
|
+
};
|
235
|
+
|
236
|
+
AssociationManager.prototype.generateRow = function(data) {
|
237
|
+
var out = '<tr><td class="checkbox active">' +
|
238
|
+
'<input type="' + this.selectorType + '" value="' + data.id + '" id="selection_' + this.model + '_' + data.id + '"';
|
239
|
+
if (this.relatedIds[data.id]) {
|
240
|
+
out += ' checked="checked"';
|
241
|
+
}
|
242
|
+
if (this.selectorType == 'radio') {
|
243
|
+
out += ' name=' + this.model + '_' + this.name + '_selection';
|
244
|
+
}
|
245
|
+
out += ' /></td>';
|
246
|
+
var linkIt = true;
|
247
|
+
for (var i = 0, len = this.fields.length; i < len; i++) {
|
248
|
+
if (i == 1) { linkIt = false; }
|
249
|
+
out += '<td>' + this.addLinkToValue(data.id, (data[this.fields[i]] == null ? '-' : data[this.fields[i]]), linkIt) + '</td>';
|
250
|
+
}
|
251
|
+
out += '</tr>';
|
252
|
+
return out;
|
253
|
+
};
|
254
|
+
|
255
|
+
AssociationManager.prototype.clearTable = function() {
|
256
|
+
while (this.tbody.hasChildNodes()) {
|
257
|
+
this.tbody.removeChild(this.tbody.lastChild);
|
258
|
+
}
|
259
|
+
};
|
260
|
+
|
261
|
+
// Handles clicking next/previous on paging
|
262
|
+
AssociationManager.prototype.pagingHandler = function(handler) {
|
263
|
+
return function(e) {
|
264
|
+
e.preventDefault();
|
265
|
+
if (e.target.nodeName == 'A') {
|
266
|
+
var q = '';
|
267
|
+
if (handler.searchInput.value != handler.defaultSearchValue) {
|
268
|
+
q = handler.searchInput.value;
|
269
|
+
}
|
270
|
+
var direction = e.target.getAttribute('rel') == 'next' ? 1 : -1;
|
271
|
+
handler.pageNumber = handler.pageNumber-(direction*-1);
|
272
|
+
if (handler.pageNumber <= 0) {
|
273
|
+
handler.pageNumber = 1;
|
274
|
+
} else if (handler.pageNumber > handler.pageCount) {
|
275
|
+
handler.pageNumber = handler.pageCount;
|
276
|
+
}
|
277
|
+
handler.pageNumberField.innerHTML = handler.pageNumber;
|
278
|
+
var offset = (handler.pageNumber-1)*10;
|
279
|
+
ajax({
|
280
|
+
url: "/search/" + handler.model + "?q=" + q + "&limit=10&offset=" + offset,
|
281
|
+
success: handler.searchResultsHandler(handler)
|
282
|
+
});
|
283
|
+
}
|
284
|
+
};
|
285
|
+
};
|
286
|
+
|
287
|
+
// Handles clicks on the table, catching checkbox clicks
|
288
|
+
AssociationManager.prototype.tableHandler = function(handler) {
|
289
|
+
return function(e) {
|
290
|
+
if (e.target.getAttribute('type') == 'checkbox') {
|
291
|
+
handler.relatedIds[e.target.value] = e.target.checked;
|
292
|
+
}
|
293
|
+
if (e.target.getAttribute('type') == 'radio') {
|
294
|
+
handler.relatedIds = {};
|
295
|
+
handler.relatedIds[e.target.value] = e.target.checked;
|
296
|
+
console.log(handler.relatedIds);
|
297
|
+
}
|
298
|
+
};
|
299
|
+
};
|
300
|
+
|
301
|
+
// Handlers clicking on the "modify" button on association table
|
302
|
+
AssociationManager.prototype.modifyHandler = function(handler) {
|
303
|
+
return function(e) {
|
304
|
+
e.preventDefault();
|
305
|
+
switch (handler.currentState) {
|
306
|
+
case handler.states.MODIFY:
|
307
|
+
handler.modifyButton.innerHTML = 'Modify';
|
308
|
+
handler.search.style.display = 'none';
|
309
|
+
handler.associatedCountDisplay.style.display = 'table-cell';
|
310
|
+
handler.currentState = null;
|
311
|
+
handler.pageNumber = 1;
|
312
|
+
handler.searchInput.value = handler.defaultSearchValue;
|
313
|
+
handler.adjustRelatedList();
|
314
|
+
break;
|
315
|
+
default:
|
316
|
+
handler.modifyButton.innerHTML = 'Done';
|
317
|
+
handler.search.style.display = 'block';
|
318
|
+
handler.associatedCountDisplay.style.display = 'none';
|
319
|
+
handler.currentState = handler.states.MODIFY;
|
320
|
+
handler.setToModifyState();
|
321
|
+
}
|
322
|
+
};
|
323
|
+
};
|
324
|
+
|
325
|
+
// Handles search results coming back from paging and searching
|
326
|
+
AssociationManager.prototype.searchResultsHandler = function(handler) {
|
327
|
+
return function(data) {
|
328
|
+
data = parseJSON(data);
|
329
|
+
handler.clearTable();
|
330
|
+
handler.count = data.count;
|
331
|
+
handler.pageCount = Math.ceil(handler.count/10);
|
332
|
+
handler.pageCountField.innerHTML = handler.pageCount;
|
333
|
+
handler.pageNumberField.innerHTML = handler.pageNumber;
|
334
|
+
console.log(data);
|
335
|
+
if (data.results.length > 0) {
|
336
|
+
var out = [];
|
337
|
+
for (var i = 0, len = data.results.length; i < len; i++) {
|
338
|
+
out.push(handler.generateRow(data.results[i]));
|
339
|
+
}
|
340
|
+
handler.tbody.innerHTML = out.join('');
|
341
|
+
} else {
|
342
|
+
}
|
343
|
+
handler.toggleCheckboxes();
|
344
|
+
};
|
345
|
+
};
|
346
|
+
|
347
|
+
// Handlers search box input
|
348
|
+
AssociationManager.prototype.searchHandler = function(handler) {
|
349
|
+
return function(e) {
|
350
|
+
if (e.keyCode == 13) {
|
351
|
+
e.preventDefault();
|
352
|
+
var q = handler.searchInput.value;
|
353
|
+
handler.pageNumber = 1;
|
354
|
+
var offset = (handler.pageNumber-1)*10;
|
355
|
+
ajax({
|
356
|
+
url: "/search/" + handler.model + "?q=" + q + "&limit=10&offset=" + offset,
|
357
|
+
success: handler.searchResultsHandler(handler)
|
358
|
+
});
|
359
|
+
return false;
|
360
|
+
}
|
361
|
+
return true;
|
362
|
+
};
|
363
|
+
};
|
364
|
+
|
365
|
+
AssociationManager.prototype.searchInputHandler = function(handler, action) {
|
366
|
+
return function(e) {
|
367
|
+
if (this.value == '') {
|
368
|
+
this.value = handler.defaultSearchValue;
|
369
|
+
}
|
370
|
+
if (this.value == handler.defaultSearchValue && action == 'focus') {
|
371
|
+
this.value = '';
|
372
|
+
}
|
373
|
+
};
|
374
|
+
};
|
@@ -1,9 +1,9 @@
|
|
1
1
|
<h1><a href="/<%= @model.name %>"><%= @model.display_name %></a> / Edit</h1>
|
2
|
-
<form action="/update/<%= @model.name %>/<%= @content.id %>" method="post">
|
2
|
+
<form action="/update/<%= @model.name %>/<%= @content.id %>" method="post" id="edit-form">
|
3
3
|
<div class="wrapper">
|
4
4
|
<% if @errors %><div id="save_errors" class="errors"><%= @errors %></div><% end %>
|
5
5
|
<% @model.form_properties.each do |p| %>
|
6
|
-
<fieldset id="
|
6
|
+
<fieldset id="property-<%= p.name %>" class="<%= @model.field_class_for(p) %>">
|
7
7
|
<%= @model.form_for(p,@content) %>
|
8
8
|
</fieldset>
|
9
9
|
<% end %>
|
@@ -18,3 +18,4 @@
|
|
18
18
|
<button type="submit" id="delete-button">Delete</button>
|
19
19
|
</form>
|
20
20
|
</div>
|
21
|
+
<script type="text/javascript" src="/js/edit.js"></script>
|
@@ -1,7 +1,16 @@
|
|
1
1
|
<h1><%= @model.display_name %></h1>
|
2
|
+
<div id="paging">
|
3
|
+
<%= @count %> items
|
4
|
+
<% if @page_count > 1 %>
|
5
|
+
Page <span class="page-number"><%= @p %></span> of <span class="page-count"><%= @page_count %></span>
|
6
|
+
<% prev_link, next_link = paging_links(@p, @page_count, :q => params[:q]) %>
|
7
|
+
<%= prev_link %>
|
8
|
+
<%= next_link %>
|
9
|
+
<% end %>
|
10
|
+
</div>
|
2
11
|
<form method="get" id="search-form">
|
3
12
|
<fieldset>
|
4
|
-
<input type="text" name="q" value="<%= params[:q]
|
13
|
+
<input type="text" name="q" value="<%= params[:q].blank? ? 'Search...' : params[:q] %>" size="40" />
|
5
14
|
</fieldset>
|
6
15
|
</form>
|
7
16
|
<div class="wrapper">
|
@@ -38,7 +47,7 @@
|
|
38
47
|
<a href="/new/<%= @model.name %>" class="action new" id="new-button">New</a>
|
39
48
|
<form action="/delete/<%= @model.name %>" method="post" id="delete-form">
|
40
49
|
<input type="hidden" id="delete-list" name="id" value="" />
|
41
|
-
<button type="submit" id="delete-button" disabled="disabled">Delete</button>
|
50
|
+
<button type="submit" id="delete-button" disabled="disabled" onclick="return confirm('Are you sure you want to delete these items?');">Delete</button>
|
42
51
|
</form>
|
43
52
|
</div>
|
44
53
|
<script type="text/javascript" src="/js/index.js"></script>
|
@@ -43,13 +43,13 @@ module Togo
|
|
43
43
|
@response.status = 404
|
44
44
|
@response.write("404 Not Found")
|
45
45
|
else
|
46
|
-
|
46
|
+
begin
|
47
47
|
__before if defined? __before
|
48
48
|
@response.write(send(method))
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
49
|
+
rescue => detail
|
50
|
+
@response.status = 500
|
51
|
+
@response.write(["Error: #{detail}",$!.backtrace.join("<br />\n")].join("<br />\n"))
|
52
|
+
end
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
@@ -112,8 +112,11 @@ module Togo
|
|
112
112
|
method_name = "__#{type.downcase}#{clean_path(route)}"
|
113
113
|
k = []
|
114
114
|
p = route.gsub(/(:\w+)/){|m| k << m[1..-1]; "([^?/#&]+)"}
|
115
|
-
|
116
|
-
|
115
|
+
r = [/^#{p}$/,k,method_name]
|
116
|
+
if not routes[type].rindex{|t| t[0] == r[0]}
|
117
|
+
routes[type].push(r)
|
118
|
+
define_method(method_name, &block)
|
119
|
+
end
|
117
120
|
end
|
118
121
|
|
119
122
|
def before(&block)
|
@@ -136,6 +139,13 @@ module Togo
|
|
136
139
|
builder.use Rack::ShowExceptions
|
137
140
|
builder.use opts[:reloader] if opts[:reloader]
|
138
141
|
end
|
142
|
+
if opts[:routes] and opts[:routes].is_a?(Array)
|
143
|
+
opts[:routes].each do |r|
|
144
|
+
send(r[:type].to_sym, r[:path]) do
|
145
|
+
erb r[:template].to_sym
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
139
149
|
builder.use Rack::Static, :urls => opts[:static_urls], :root => opts[:public_path]
|
140
150
|
builder.run new(opts)
|
141
151
|
if opts[:standalone]
|
data/lib/togo/model/model.rb
CHANGED
@@ -51,6 +51,11 @@ module Togo
|
|
51
51
|
|
52
52
|
def stage_content(content,attrs)
|
53
53
|
content.attributes = properties.inject({}){|m,p| attrs[p.name.to_sym] ? m.merge!(p.name.to_sym => attrs[p.name.to_sym]) : m}
|
54
|
+
relationships.each do |r|
|
55
|
+
key = "related_#{r[0]}".to_sym
|
56
|
+
next if not attrs[key] or attrs[key] == 'unset'
|
57
|
+
content = RelationshipManager.new(content, r, :ids => attrs[key]).relate
|
58
|
+
end
|
54
59
|
content
|
55
60
|
end
|
56
61
|
|
@@ -68,12 +73,16 @@ module Togo
|
|
68
73
|
|
69
74
|
def search(opts)
|
70
75
|
q = "%#{opts[:q].gsub(/\s+/,'%')}%"
|
76
|
+
limit = opts[:limit]
|
77
|
+
offset = opts[:offset]
|
71
78
|
conditions, values = [], []
|
72
79
|
search_properties.each{|l|
|
73
80
|
conditions << "#{l.name} like ?"
|
74
81
|
values << q
|
75
82
|
}
|
76
|
-
|
83
|
+
params = {:conditions => [conditions.join(' OR ')] + values}
|
84
|
+
params.merge!(:limit => limit.to_i, :offset => offset.to_i) if limit and offset
|
85
|
+
all(params)
|
77
86
|
end
|
78
87
|
|
79
88
|
def field_class_for(property)
|
@@ -89,7 +98,7 @@ module Togo
|
|
89
98
|
def type_from_property(property)
|
90
99
|
case property
|
91
100
|
when ::DataMapper::Property
|
92
|
-
Extlib::Inflection.demodulize(property.type).downcase
|
101
|
+
Extlib::Inflection.demodulize(property.type || property.class).downcase # type seems to be deprecated in 1.0
|
93
102
|
when ::DataMapper::Associations::ManyToOne::Relationship
|
94
103
|
'belongs_to'
|
95
104
|
when ::DataMapper::Associations::OneToMany::Relationship
|
@@ -112,12 +121,50 @@ module Togo
|
|
112
121
|
end
|
113
122
|
|
114
123
|
def search_properties
|
124
|
+
# Support dm 0.10.x and 1.x by checking for deprecated(?) types
|
115
125
|
only_properties = [String, ::DataMapper::Types::Text]
|
116
|
-
|
126
|
+
begin # rescue exception when not using dm-core 1.0, these don't exist in the 0.10.x series
|
127
|
+
only_properties.concat([::DataMapper::Property::String, ::DataMapper::Property::Text])
|
128
|
+
rescue
|
129
|
+
end
|
130
|
+
properties.select{|p| only_properties.include?(p.type || p.class)} # type seems to be depracated in 1.0
|
117
131
|
end
|
118
132
|
|
119
133
|
end
|
120
134
|
|
121
135
|
end # Model
|
136
|
+
|
137
|
+
class RelationshipManager
|
138
|
+
def initialize(content, relationship, opts = {})
|
139
|
+
@content = content
|
140
|
+
@relationship = relationship[1]
|
141
|
+
@relationship_name = relationship[0]
|
142
|
+
@ids = (opts[:ids] || '').split(',').map(&:to_i)
|
143
|
+
define_values
|
144
|
+
end
|
145
|
+
|
146
|
+
def relate
|
147
|
+
@content.send("#{@relationship_name}=", find_for_assignment)
|
148
|
+
@content
|
149
|
+
end
|
150
|
+
|
151
|
+
def define_values
|
152
|
+
case @relationship
|
153
|
+
when ::DataMapper::Associations::ManyToOne::Relationship
|
154
|
+
@unset_value = nil
|
155
|
+
@related_model = @relationship.parent_model
|
156
|
+
@find_op = Proc.new{|p| p.get(@ids.first)}
|
157
|
+
when ::DataMapper::Associations::OneToMany::Relationship
|
158
|
+
@unset_value = []
|
159
|
+
@related_model = @relationship.child_model
|
160
|
+
@find_op = Proc.new{|p| p.all(:id => @ids)}
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def find_for_assignment
|
165
|
+
return @unset_value if @ids.blank?
|
166
|
+
@find_op.call(@related_model)
|
167
|
+
end
|
168
|
+
end # RelationshipManager
|
122
169
|
end # DataMapper
|
123
170
|
end # Togo
|
@@ -1,29 +1,65 @@
|
|
1
|
-
<%
|
2
|
-
<%
|
3
|
-
|
4
|
-
|
1
|
+
<% content_count = content.send(property.name.to_sym).blank? ? 0 : 1 %>
|
2
|
+
<% related_content = [content.send(property.name.to_sym)].compact %>
|
3
|
+
<% related_ids = related_content.map(&:id) %>
|
4
|
+
<% related_model = property.parent_model %>
|
5
|
+
<input type="hidden" name="related_<%= property.name %>" class="related_ids" value="unset" />
|
6
|
+
<label for="<%= property.name %>"><%= related_model.display_name %></label>
|
7
|
+
<div class="search" id="search-<%= related_model.name %>">
|
8
|
+
<input type="text" name="search-<%= property.name %>" value="Search..." />
|
9
|
+
<div class="paging">
|
10
|
+
Page <span class="page-number">1</span> of <span class="page-count"><%= (content_count/10).ceil %></span>
|
11
|
+
<a href="#" rel="prev">« Previous</a> <a href="#" rel="next">Next »</a>
|
12
|
+
</div>
|
13
|
+
</div>
|
14
|
+
<table>
|
5
15
|
<thead>
|
6
16
|
<tr>
|
7
|
-
|
8
|
-
<th></th>
|
9
|
-
<% end %>
|
17
|
+
<th class="checkbox"></th>
|
10
18
|
<% related_model.list_properties.each do |p| %>
|
11
19
|
<th><%= p.name.to_s.humanize.titleize %></th>
|
12
20
|
<% end %>
|
21
|
+
<% if related_model.list_properties.size < 2 %>
|
22
|
+
<th></th>
|
23
|
+
<% end %>
|
13
24
|
</tr>
|
14
25
|
</thead>
|
26
|
+
<tfoot>
|
27
|
+
<tr>
|
28
|
+
<td class="checkbox"></td>
|
29
|
+
<td>
|
30
|
+
<span class="associated-count-display" style="<%= (content_count == 0 or content_count < 10) ? 'display: none;' : '' %>">
|
31
|
+
...and <span class="associated-count"><%= content_count-related_content.size %></span> more associated items
|
32
|
+
</span>
|
33
|
+
</td>
|
34
|
+
<td colspan="<%= related_model.form_properties.size-1 %>">
|
35
|
+
<button class="association-modify">Modify</button>
|
36
|
+
</td>
|
37
|
+
</tr>
|
38
|
+
</tfoot>
|
15
39
|
<tbody>
|
16
|
-
<% if
|
40
|
+
<% if related_content.empty? %>
|
17
41
|
<tr>
|
18
|
-
<td colspan="<%= related_model.
|
42
|
+
<td colspan="<%= related_model.form_properties.size %>">No Content Associated.</td>
|
19
43
|
</tr>
|
20
44
|
<% else %>
|
45
|
+
<% related_content.each do |c| %>
|
21
46
|
<tr>
|
22
|
-
<td class="checkbox"><input type="
|
47
|
+
<td class="checkbox"><input type="radio" name="selection[<%= c.id %>]" value="<%= c.id %>" id="selection_<%= related_model.name %>_<%= c.id %>" checked="checked" /></td>
|
23
48
|
<% related_model.list_properties.each_with_index do |p,i| %>
|
24
|
-
<td><% if i == 0 %><a href="/edit/<%= related_model.name %>/<%=
|
49
|
+
<td><% if i == 0 %><a href="/edit/<%= related_model.name %>/<%= c.id %>"><%= c.send(p.name.to_sym) || '-' %></a><% else %><%= c.send(p.name.to_sym) %><% end %></td>
|
25
50
|
<% end %>
|
26
|
-
</tr>
|
51
|
+
</tr>
|
52
|
+
<% end %>
|
27
53
|
<% end %>
|
28
54
|
</tbody>
|
29
55
|
</table>
|
56
|
+
<script type="text/javascript">
|
57
|
+
new AssociationManager({
|
58
|
+
relationship: 'belongs_to',
|
59
|
+
name: '<%= property.name %>',
|
60
|
+
model: '<%= related_model.name %>',
|
61
|
+
fields: <%= related_model.list_properties.map(&:name).to_json %>,
|
62
|
+
count: <%= content_count %>,
|
63
|
+
related_ids: <%= related_ids.to_json %>
|
64
|
+
});
|
65
|
+
</script>
|
@@ -1,2 +1,2 @@
|
|
1
1
|
<label for="<%= property.name %>"><%= property.model.property_options[property.name][:label] rescue property.name.to_s.humanize.titleize %></label>
|
2
|
-
<input type="text" name="<%= property.name %>" id="<%= property.name %>" value="<%= content.send(property.name.to_sym) %>" />
|
2
|
+
<input type="text" name="<%= property.name %>" id="<%= property.name %>" value="<%= content.send(property.name.to_sym).strftime("%B %d, %Y %I:%M%p") rescue '' %>" />
|
@@ -1,31 +1,62 @@
|
|
1
|
-
<%
|
2
|
-
<%
|
1
|
+
<% content_count = content.send(property.name.to_sym).size %>
|
2
|
+
<% related_ids = content.send(property.name.to_sym).map(&:id) %>
|
3
|
+
<% related_content = content.send(property.name.to_sym)[0...10] %>
|
4
|
+
<% related_model = property.child_model %>
|
5
|
+
<input type="hidden" name="related_<%= property.name %>" class="related_ids" value="unset" />
|
3
6
|
<label for="<%= property.name %>"><%= related_model.display_name %></label>
|
4
|
-
<
|
7
|
+
<div class="search" id="search-<%= related_model.name %>">
|
8
|
+
<input type="text" name="search-<%= property.name %>" value="Search..." />
|
9
|
+
<div class="paging">
|
10
|
+
Page <span class="page-number">1</span> of <span class="page-count"><%= (content_count/10).ceil %></span>
|
11
|
+
<a href="#" rel="prev">« Previous</a> <a href="#" rel="next">Next »</a>
|
12
|
+
</div>
|
13
|
+
</div>
|
14
|
+
<table>
|
5
15
|
<thead>
|
6
16
|
<tr>
|
7
|
-
|
8
|
-
<th></th>
|
9
|
-
<% end %>
|
17
|
+
<th class="checkbox"></th>
|
10
18
|
<% related_model.list_properties.each do |p| %>
|
11
19
|
<th><%= p.name.to_s.humanize.titleize %></th>
|
12
20
|
<% end %>
|
13
21
|
</tr>
|
14
22
|
</thead>
|
23
|
+
<tfoot>
|
24
|
+
<tr>
|
25
|
+
<td class="checkbox"></td>
|
26
|
+
<td>
|
27
|
+
<span class="associated-count-display" style="<%= (content_count == 0 or content_count < 10) ? 'display: none;' : '' %>">
|
28
|
+
...and <span class="associated-count"><%= content_count-related_content.size %></span> more associated items
|
29
|
+
</span>
|
30
|
+
</td>
|
31
|
+
<td colspan="<%= related_model.form_properties.size-1 %>">
|
32
|
+
<button class="association-modify">Modify</button>
|
33
|
+
</td>
|
34
|
+
</tr>
|
35
|
+
</tfoot>
|
15
36
|
<tbody>
|
16
|
-
<% if
|
37
|
+
<% if related_content.empty? %>
|
17
38
|
<tr>
|
18
|
-
<td colspan="<%= related_model.
|
39
|
+
<td colspan="<%= related_model.form_properties.size %>">No Content Associated.</td>
|
19
40
|
</tr>
|
20
41
|
<% else %>
|
21
42
|
<% related_content.each do |c| %>
|
22
43
|
<tr>
|
23
|
-
<td class="checkbox"><input type="checkbox" name="selection[<%= c.id %>]" value="
|
44
|
+
<td class="checkbox"><input type="checkbox" name="selection[<%= c.id %>]" value="<%= c.id %>" id="selection_<%= related_model.name %>_<%= c.id %>" checked="checked" /></td>
|
24
45
|
<% related_model.list_properties.each_with_index do |p,i| %>
|
25
46
|
<td><% if i == 0 %><a href="/edit/<%= related_model.name %>/<%= c.id %>"><%= c.send(p.name.to_sym) || '-' %></a><% else %><%= c.send(p.name.to_sym) %><% end %></td>
|
26
47
|
<% end %>
|
27
48
|
</tr>
|
28
|
-
<% end
|
49
|
+
<% end %>
|
29
50
|
<% end %>
|
30
51
|
</tbody>
|
31
52
|
</table>
|
53
|
+
<script type="text/javascript">
|
54
|
+
new AssociationManager({
|
55
|
+
relationship: 'has_n',
|
56
|
+
name: '<%= property.name %>',
|
57
|
+
model: '<%= related_model.name %>',
|
58
|
+
fields: <%= related_model.list_properties.map(&:name).to_json %>,
|
59
|
+
count: <%= content_count %>,
|
60
|
+
related_ids: <%= related_ids.to_json %>
|
61
|
+
});
|
62
|
+
</script>
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: togo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 15
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
8
|
+
- 4
|
9
|
+
- 0
|
10
|
+
version: 0.4.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Matt King
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-
|
18
|
+
date: 2010-08-10 00:00:00 -07:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -63,6 +63,7 @@ files:
|
|
63
63
|
- Changelog
|
64
64
|
- LICENSE
|
65
65
|
- lib/togo/admin/admin.rb
|
66
|
+
- lib/togo/admin/helpers.rb
|
66
67
|
- lib/togo/admin/public/css/screen.css
|
67
68
|
- lib/togo/admin/public/img/bg-header.png
|
68
69
|
- lib/togo/admin/public/img/bg-headline.png
|
@@ -70,6 +71,7 @@ files:
|
|
70
71
|
- lib/togo/admin/public/img/bg-side.png
|
71
72
|
- lib/togo/admin/public/img/btn-bg.gif
|
72
73
|
- lib/togo/admin/public/img/btn-bg.png
|
74
|
+
- lib/togo/admin/public/js/edit.js
|
73
75
|
- lib/togo/admin/public/js/index.js
|
74
76
|
- lib/togo/admin/public/js/togo.js
|
75
77
|
- lib/togo/admin/views/custom_title.erb
|