togo 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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