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 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-p [host]: which host to run on"
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]: which file to require before running."
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.configure(config)
46
- Togo::Admin.run!
46
+ Togo::Admin.run!(config)
@@ -1,9 +1,30 @@
1
- %w(dm-core rack).each{|l| require l}
2
- Dir.glob(File.join('models','*.rb')).each{|f| require f}
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
- @content = params[:q] ? @model.search(:q => params[:q]) : @model.all
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: 270px;
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="property_<%= p.name %>" class="<%= @model.field_class_for(p) %>">
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] rescue '' %>" size="40" /> <button type="submit">Search</button>
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
- # begin
46
+ begin
47
47
  __before if defined? __before
48
48
  @response.write(send(method))
49
- # rescue => detail
50
- # @response.status = 500
51
- # @response.write(["Error: #{detail}",$!.backtrace.join("<br />\n")].join("<br />\n"))
52
- # end
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
- routes[type].push([/^#{p}$/,k,method_name])
116
- define_method(method_name, &block)
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]
@@ -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
- all(:conditions => [conditions.join(' OR ')] + values)
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
- properties.select{|p| only_properties.include?(p.type)}
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
- <% related_content = content.send(property.name.to_sym) %>
2
- <% related_model = property.parent_key.first.model %>
3
- <label for="<%= property.name %>"><%= property.name.to_s.humanize.titleize %> (<a href="/new/<%= related_model.name %>">New...</a>)</label>
4
- <table id="association-table">
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">&laquo; Previous</a> <a href="#" rel="next">Next &raquo;</a>
12
+ </div>
13
+ </div>
14
+ <table>
5
15
  <thead>
6
16
  <tr>
7
- <% if related_content %>
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 not related_content %>
40
+ <% if related_content.empty? %>
17
41
  <tr>
18
- <td colspan="<%= related_model.list_properties.size %>">No Content Associated.</td>
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="checkbox" name="selection[<%= related_content.id %>]" value="1" id="selection_<%= related_content.id %>" /></td>
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 %>/<%= related_content.id %>"><%= related_content.send(p.name.to_sym) || '-' %></a><% else %><%= related_content.send(p.name.to_sym) %><% end %></td>
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
- <% related_content = content.send(property.name.to_sym) %>
2
- <% related_model = property.child_key.first.model %>
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
- <table id="association-table">
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">&laquo; Previous</a> <a href="#" rel="next">Next &raquo;</a>
12
+ </div>
13
+ </div>
14
+ <table>
5
15
  <thead>
6
16
  <tr>
7
- <% if related_content.empty? %>
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 not related_content.empty? %>
37
+ <% if related_content.empty? %>
17
38
  <tr>
18
- <td colspan="<%= related_model.list_properties.size %>">No Content Associated.</td>
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="1" id="selection_<%= c.id %>" /></td>
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: 17
4
+ hash: 15
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 3
9
- - 1
10
- version: 0.3.1
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-06-30 00:00:00 -07:00
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