zena 1.2.2 → 1.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (143) hide show
  1. data/History.txt +25 -0
  2. data/app/controllers/documents_controller.rb +3 -25
  3. data/app/controllers/nodes_controller.rb +34 -24
  4. data/app/controllers/user_sessions_controller.rb +5 -4
  5. data/app/controllers/versions_controller.rb +44 -17
  6. data/app/models/acl.rb +2 -7
  7. data/app/models/group.rb +6 -2
  8. data/app/models/link.rb +14 -0
  9. data/app/models/node.rb +2 -2
  10. data/app/models/site.rb +13 -4
  11. data/app/models/text_document.rb +1 -1
  12. data/app/models/user.rb +11 -2
  13. data/app/models/virtual_class.rb +1 -1
  14. data/app/views/groups/_form.rhtml +6 -6
  15. data/app/views/nodes/render_error.rhtml +15 -0
  16. data/app/views/templates/document_create_tabs/_file.rhtml +1 -1
  17. data/app/views/templates/document_create_tabs/_import.rhtml +1 -1
  18. data/app/views/templates/document_create_tabs/_template.rhtml +1 -1
  19. data/app/views/templates/document_create_tabs/_text_document.rhtml +1 -1
  20. data/app/views/templates/edit_tabs/_title.rhtml +1 -1
  21. data/app/views/zafu/default/Node-admin.zafu +1 -1
  22. data/bricks/acls/zena/test/integration/acl_integration_test.rb +2 -2
  23. data/bricks/acls/zena/test/unit/acl_test.rb +2 -1
  24. data/bricks/fs_skin/zena/migrate/20110702010330_add_fs_skin_to_idx_templates.rb +1 -0
  25. data/bricks/fs_skin/zena/skins/blog/img/style.css +4 -4
  26. data/bricks/grid/lib/bricks/grid.rb +9 -3
  27. data/bricks/passenger/zena/deploy.rb +2 -1
  28. data/bricks/pdf/lib/bricks/pdf.rb +1 -1
  29. data/bricks/tags/zena/test/zafu/tags.yml +5 -1
  30. data/bricks/zena/zena/migrate/20120904071601_change_link_status_to_float.rb +13 -0
  31. data/config/bricks.yml +10 -10
  32. data/config/deploy.rb +1 -5
  33. data/config/gems.yml +2 -2
  34. data/db/init/base/skins/default/Node.zafu +7 -3
  35. data/db/init/base/skins/default/notes.zafu +3 -1
  36. data/lib/zafu/all.rb +0 -9
  37. data/lib/zafu/compiler.rb +0 -4
  38. data/lib/zafu/controller_methods.rb +0 -2
  39. data/lib/zafu/handler.rb +0 -5
  40. data/lib/zafu/markup.rb +4 -6
  41. data/lib/zafu/ordered_hash.rb +3 -2
  42. data/lib/zafu/parsing_rules.rb +1 -3
  43. data/lib/zafu/process/ajax.rb +4 -2
  44. data/lib/zafu/process/context.rb +34 -4
  45. data/lib/zafu/process/forms.rb +2 -2
  46. data/lib/zafu/process/ruby_less_processing.rb +5 -10
  47. data/lib/zafu/template.rb +0 -2
  48. data/lib/zafu/test_helper.rb +0 -2
  49. data/lib/zafu/view_methods.rb +0 -1
  50. data/lib/zafu.rb +1 -1
  51. data/lib/zena/acts/secure_node.rb +5 -4
  52. data/lib/zena/console.rb +19 -17
  53. data/lib/zena/core_ext/string.rb +3 -2
  54. data/lib/zena/deploy/app_init.rhtml +6 -1
  55. data/lib/zena/deploy/httpd.rhtml +16 -13
  56. data/lib/zena/deploy/stats.vhost.rhtml +1 -1
  57. data/lib/zena/deploy/vhost.rhtml +31 -11
  58. data/lib/zena/deploy/vhost_ssl_redir.rhtml +12 -0
  59. data/lib/zena/deploy/vhost_www.rhtml +1 -1
  60. data/lib/zena/deploy.rb +55 -11
  61. data/lib/zena/info.rb +1 -1
  62. data/lib/zena/parser/zazen_rules.rb +18 -9
  63. data/lib/zena/routes.rb +1 -3
  64. data/lib/zena/site_worker.rb +8 -1
  65. data/lib/zena/use/ajax.rb +29 -3
  66. data/lib/zena/use/ancestry.rb +2 -1
  67. data/lib/zena/use/authlogic.rb +12 -18
  68. data/lib/zena/use/context.rb +1 -1
  69. data/lib/zena/use/dates.rb +28 -18
  70. data/lib/zena/use/display.rb +49 -7
  71. data/lib/zena/use/forms.rb +51 -18
  72. data/lib/zena/use/html_tags.rb +6 -6
  73. data/lib/zena/use/i18n.rb +13 -4
  74. data/lib/zena/use/image_builder.rb +2 -0
  75. data/lib/zena/use/query_builder.rb +39 -14
  76. data/lib/zena/use/query_link.rb +57 -0
  77. data/lib/zena/use/query_node.rb +68 -32
  78. data/lib/zena/use/relations.rb +25 -15
  79. data/lib/zena/use/rendering.rb +66 -15
  80. data/lib/zena/use/upload.rb +34 -5
  81. data/lib/zena/use/urls.rb +28 -25
  82. data/lib/zena/use/version_hash.rb +14 -2
  83. data/lib/zena/use/zafu_safe_definitions.rb +72 -3
  84. data/lib/zena/use/zazen.rb +16 -4
  85. data/lib/zena.rb +1 -0
  86. data/public/javascripts/grid.js +213 -64
  87. data/public/javascripts/raphael.js +10 -0
  88. data/public/javascripts/zena.js +146 -22
  89. data/public/stylesheets/reset.css +12 -12
  90. data/public/stylesheets/zena.css +1 -1
  91. data/test/custom_queries/complex.host.yml +19 -0
  92. data/test/fixtures/files/TestNode.zafu +40 -4
  93. data/test/functional/nodes_controller_test.rb +84 -39
  94. data/test/functional/versions_controller_test.rb +2 -2
  95. data/test/integration/navigation_test.rb +61 -35
  96. data/test/integration/query_node/basic.yml +7 -7
  97. data/test/integration/query_node/comments.yml +1 -1
  98. data/test/integration/query_node/complex.yml +3 -3
  99. data/test/integration/query_node/filters.yml +32 -8
  100. data/test/integration/query_node/idx_key_value.yml +10 -10
  101. data/test/integration/query_node/idx_scope.yml +7 -7
  102. data/test/integration/query_node/relations.yml +4 -4
  103. data/test/integration/zafu_compiler/ajax.yml +19 -11
  104. data/test/integration/zafu_compiler/apphelper.yml +1 -1
  105. data/test/integration/zafu_compiler/asset.yml +2 -2
  106. data/test/integration/zafu_compiler/comments.yml +1 -1
  107. data/test/integration/zafu_compiler/dates.yml +1 -1
  108. data/test/integration/zafu_compiler/display.yml +49 -21
  109. data/test/integration/zafu_compiler/eval.yml +4 -4
  110. data/test/integration/zafu_compiler/forms.yml +25 -11
  111. data/test/integration/zafu_compiler/i18n.yml +5 -0
  112. data/test/integration/zafu_compiler/meta.yml +3 -3
  113. data/test/integration/zafu_compiler/query.yml +27 -9
  114. data/test/integration/zafu_compiler/relations.yml +9 -9
  115. data/test/integration/zafu_compiler/roles.yml +6 -6
  116. data/test/integration/zafu_compiler/rubyless.yml +7 -2
  117. data/test/integration/zafu_compiler/safe_definitions.yml +33 -4
  118. data/test/integration/zafu_compiler/security.yml +46 -1
  119. data/test/integration/zafu_compiler/urls.yml +28 -13
  120. data/test/integration/zafu_compiler/user.yml +12 -7
  121. data/test/integration/zafu_compiler/zafu_attributes.yml +1 -1
  122. data/test/integration/zafu_compiler/zazen.yml +5 -5
  123. data/test/integration/zafu_compiler_test.rb +18 -0
  124. data/test/selenium/Filter/filter3.rsel +20 -0
  125. data/test/selenium/Filter/filter4.rsel +20 -0
  126. data/test/sites/zena/versions.yml +2 -0
  127. data/test/unit/exif_data_test.rb +6 -1
  128. data/test/unit/group_test.rb +18 -3
  129. data/test/unit/node_test.rb +0 -7
  130. data/test/unit/project_test.rb +4 -0
  131. data/test/unit/relation_proxy_test.rb +2 -2
  132. data/test/unit/remote_test.rb +0 -9
  133. data/test/unit/role_test.rb +1 -1
  134. data/test/unit/string_hash_test.rb +1 -1
  135. data/test/unit/text_document_test.rb +13 -13
  136. data/test/unit/zena/use/html_tags_test.rb +6 -6
  137. data/test/unit/zena/use/rendering_test.rb +20 -10
  138. data/test/unit/zena/use/urls_test.rb +21 -18
  139. data/test/unit/zena/use/zafu_template_test.rb +0 -5
  140. data/test/unit/zena/use/zazen_test.rb +25 -25
  141. data/zena.gemspec +63 -57
  142. metadata +136 -130
  143. data/test/functional/nodes_controller_commit_test.rb +0 -67
@@ -1,10 +1,66 @@
1
1
  module Zena
2
2
  module Use
3
3
  module ZafuSafeDefinitions
4
+ # This is a dummy class to declare safe parameters on params.
4
5
  class ParamsDictionary
5
6
  include RubyLess
6
7
  safe_method ['[]', Symbol] => {:class => String, :nil => true}
7
8
  end
9
+
10
+ # This class is used to return Array params ['foo', 'bar'] as ['foo', 'bar'], not 'foobar'.
11
+ # It also splits String params 'foo, bar' as ['foo', 'bar'].
12
+ class AParamsDictionary
13
+ def initialize(params)
14
+ @aparams = {}
15
+ params.each do |k, v|
16
+ @aparams[k.to_sym] = transform(v)
17
+ end
18
+
19
+ def [](key)
20
+ @aparams[key.to_sym] || []
21
+ end
22
+ end
23
+
24
+ include RubyLess
25
+ safe_method ['[]', Symbol] => {:class => [String], :nil => false}
26
+
27
+ private
28
+ def transform(v)
29
+ if v.kind_of?(Array)
30
+ v.map {|k| k.to_s}.select{|e| !e.blank?}
31
+ elsif v.kind_of?(String)
32
+ v.split(',').map(&:strip).select{|e| !e.blank?}
33
+ else
34
+ []
35
+ end
36
+ end
37
+ end
38
+
39
+ # This class is used to access nested Hash params {:key => 'foo'}.
40
+ class HParamsDictionary
41
+ def initialize(params)
42
+ @params = {}
43
+ params.each do |k, v|
44
+ @params[k.to_sym] = transform(v)
45
+ end
46
+ end
47
+
48
+ def [](key)
49
+ @params[key.to_sym] || {}
50
+ end
51
+
52
+ include RubyLess
53
+ safe_method ['[]', Symbol] => {:class => ParamsDictionary, :nil => false}
54
+
55
+ private
56
+ def transform(v)
57
+ if v.kind_of?(Hash)
58
+ v
59
+ else
60
+ {}
61
+ end
62
+ end
63
+ end
8
64
 
9
65
  module ViewMethods
10
66
  include RubyLess
@@ -96,10 +152,13 @@ module Zena
96
152
  end
97
153
 
98
154
  safe_method :params => ParamsDictionary
155
+ safe_method :aparams => AParamsDictionary
156
+ safe_method :hparams => HParamsDictionary
157
+
99
158
  safe_method :now => {:method => 'Time.now', :class => Time}
100
159
  safe_method :string_hash => {:method => 'StringHash.new', :class => StringHash}
101
160
  safe_method [:string_hash, Hash] => {:method => 'StringHash.from_hash', :class => StringHash}
102
- safe_method [:h, String] => {:class => String, :nil => true}
161
+ safe_method [:h, String] => {:class => String, :accept_nil => true}
103
162
  safe_method_for String, [:gsub, Regexp, String] => {:class => String, :pre_processor => true}
104
163
  safe_method_for String, :upcase => {:class => String, :pre_processor => true}
105
164
  safe_method_for String, :strip => {:class => String, :pre_processor => true}
@@ -107,8 +166,8 @@ module Zena
107
166
  safe_method_for String, :url_name => {:class => String, :pre_processor => true, :method => :url_name}
108
167
  safe_method_for String, :to_i => {:class => Number, :pre_processor => true}
109
168
  safe_method_for String, :to_s => {:class => String, :pre_processor => true}
110
- safe_method_for String, [:limit, Number] => {:class => String, :pre_processor => true}
111
- safe_method_for String, [:limit, Number, String] => {:class => String, :pre_processor => true}
169
+ safe_method_for String, [:limit, Number] => {:class => String, :pre_processor => true, :html_safe => true}
170
+ safe_method_for String, [:limit, Number, String] => {:class => String, :pre_processor => true, :html_safe => true}
112
171
  safe_method_for String, :to_f => {:class => Number, :pre_processor => true}
113
172
  safe_method_for String, :to_json => {:class => String, :pre_processor => true}
114
173
  safe_method_for String, [:split, String] => {:class => [String], :pre_processor => true}
@@ -120,6 +179,8 @@ module Zena
120
179
  safe_method_for Number, :fmt => {:class => String, :pre_processor => true}
121
180
  safe_method_for Number, [:fmt, Number] => {:class => String, :pre_processor => true}
122
181
 
182
+ safe_method_for Range, :to_a => {:class => [Number], :pre_processor => true}
183
+
123
184
  safe_method_for NilClass, :to_f => {:class => Number, :pre_processor => true}
124
185
  safe_method_for NilClass, :to_i => {:class => Number, :pre_processor => true}
125
186
  safe_method_for NilClass, :to_json => {:class => String, :pre_processor => true}
@@ -168,6 +229,14 @@ module Zena
168
229
  def zafu_max(a, b)
169
230
  [a, b].max
170
231
  end
232
+
233
+ def aparams
234
+ @aparams ||= AParamsDictionary.new(params)
235
+ end
236
+
237
+ def hparams
238
+ @hparams ||= HParamsDictionary.new(params)
239
+ end
171
240
  end # ViewMethods
172
241
 
173
242
 
@@ -65,7 +65,9 @@ module Zena
65
65
  if text[0..0] == ' '
66
66
  text = "\n\n#{text}"
67
67
  end
68
- res = ZazenParser.new(text,:helper=>self).render(opt)
68
+ @zazen = ZazenParser.new(text, :helper=>self)
69
+ res = @zazen.render(opt)
70
+ @zazen = nil
69
71
  if no_p && !text.include?("\n")
70
72
  res.gsub(%r{\A<p>|</p>\Z},'')
71
73
  else
@@ -75,6 +77,14 @@ module Zena
75
77
  rescue Timeout::Error
76
78
  return %Q{<span class='parser_error'>#{_('Could not render text (Timeout error)')}</span>}
77
79
  end
80
+
81
+ def raw_content(txt)
82
+ if @zazen
83
+ @zazen.raw_content(txt)
84
+ else
85
+ txt
86
+ end
87
+ end
78
88
 
79
89
  # TODO: test
80
90
  def zazen_diff(text1, text2, opt={})
@@ -172,7 +182,7 @@ module Zena
172
182
  def make_image(opts)
173
183
  id, style, link, mode, title = opts[:id], opts[:style], opts[:link], opts[:mode], opts[:title]
174
184
  mode ||= 'std' # default mode
175
- img = opts[:node] || secure(Document) { Document.find_by_zip(id) }
185
+ img = opts[:node] || secure(Node) { Node.find_by_zip(id) }
176
186
 
177
187
  return "<span class='unknownLink'>#{_('unknown document')}</span>" unless img
178
188
 
@@ -182,11 +192,13 @@ module Zena
182
192
  title = img.summary if title == ""
183
193
 
184
194
  image = img_tag(img, :mode=>mode, :host=>opts[:host])
195
+
196
+ return "<span class='unknownLink'>#{_('invalid document')}</span>" unless image
185
197
 
186
198
  unless link
187
- if id[0..0] == "0" || !img.kind_of?(Image)
199
+ if id[0..0] == "0" || (!img.kind_of?(Image) && !(image =~ /ZAZENBLOCKCODE/))
188
200
  # if the id starts with '0' or it is not an Image, link to data
189
- link = zen_path(img, :format => img.ext)
201
+ link = zen_path(img, :format => img.prop['ext'])
190
202
  link_tag = "<a class='popup' href='#{link}' target='_blank'>"
191
203
  end
192
204
  end
data/lib/zena.rb CHANGED
@@ -315,6 +315,7 @@ EXT_TYPE = [
315
315
  [ "jpe" , "image/jpeg" ],
316
316
  [ "jpeg" , "image/jpeg" ],
317
317
  [ "js" , "application/x-javascript" ],
318
+ [ "js" , "text/javascript" ],
318
319
  [ "kar" , "audio/midi" ],
319
320
  [ "latex" , "application/x-latex" ],
320
321
  [ "lha" , "application/octet-stream" ],
@@ -18,27 +18,10 @@ Grid.log = function(what, msg) {
18
18
  }
19
19
 
20
20
  Grid.changed = function(cell, val, prev, skip_html) {
21
- if (!skip_html) {
22
- if (val.value == val.show) {
23
- cell.innerHTML = val.value
24
- } else {
25
- cell.innerHTML = val.show
26
- cell.setAttribute('data-v', val.value)
27
- }
28
- }
29
21
  var row = cell.up('tr')
30
22
  var table = row.up('table')
31
23
  var grid = table.grid
32
- if (prev.value == val.value) return;
33
- if (cell.orig_value == val.value) {
34
- cell.removeClassName('changed')
35
- if (row.select('.changed').length == 0) {
36
- row.removeClassName('changed')
37
- }
38
- } else {
39
- cell.addClassName('changed')
40
- row.addClassName('changed')
41
- }
24
+
42
25
  var pos = Grid.pos(cell)
43
26
 
44
27
  var attr, id
@@ -53,6 +36,34 @@ Grid.changed = function(cell, val, prev, skip_html) {
53
36
  id = Grid.buildObj(grid, row)
54
37
  }
55
38
  }
39
+
40
+ if (!skip_html) {
41
+ var show_h = grid.show[attr] || {}
42
+ if (cell.hasAttribute('data-v')) cell.setAttribute('data-v', val.value)
43
+ cell.innerHTML = show_h[val.value] || val.show
44
+ }
45
+
46
+ if (grid.onChange) {
47
+ // onChange can be used to recompute total (so cell value should be set first)
48
+ var v = val.value
49
+ val = grid.onChange(cell, val, attr)
50
+ if (!val) return
51
+ if (v != val.value) {
52
+ cell.innerHTML = val.show
53
+ if (cell.hasAttribute('data-v')) cell.setAttribute('data-v', val.value)
54
+ }
55
+ }
56
+
57
+ if (prev.value == val.value) return;
58
+ if (cell.orig_value == val.value) {
59
+ cell.removeClassName('changed')
60
+ if (row.select('.changed').length == 0) {
61
+ row.removeClassName('changed')
62
+ }
63
+ } else {
64
+ cell.addClassName('changed')
65
+ row.addClassName('changed')
66
+ }
56
67
 
57
68
  var change = {
58
69
  id: id,
@@ -102,27 +113,28 @@ Grid.buildObj = function(grid, row) {
102
113
  }
103
114
 
104
115
  Grid.closeCell = function(e) {
105
- if (Grid.in_paste) return
116
+ if (Grid.no_blur) return
106
117
  var cell = e.tagName ? e : e.element().up()
107
118
  var table = cell.up('table')
108
119
  var prev = cell.prev_value
109
120
  var val = Grid.getValue(cell)
110
121
 
111
- if (table.grid.list_name) {
122
+ if (table.grid.is_list) {
112
123
  if (val.value == 'on') {
113
124
  cell.addClassName('on')
114
125
  } else {
115
126
  cell.removeClassName('on')
116
127
  }
117
- } else {
118
- cell.removeClassName('input')
119
128
  }
129
+ cell.removeClassName('input')
120
130
 
121
131
  Grid.changed(cell, val, prev)
122
132
 
123
133
  if (table.grid.input) {
124
134
  // single attribute table, serialize in input field
125
135
  table.grid.input.value = Grid.serialize(table)
136
+ } else if (table.grid.autoSave) {
137
+ Grid.save(table.id)
126
138
  }
127
139
  }
128
140
 
@@ -150,9 +162,9 @@ Grid.paste = function(event) {
150
162
  bottom: "<textarea style='position:fixed; top:0; left:10100px;' id='grid_p_" + table.grid.id + "'></textarea>"
151
163
  });
152
164
  paster = $("grid_p_" + table.grid.id);
153
- Grid.in_paste = true // prevent original input blur
165
+ Grid.no_blur = true // prevent original input blur
154
166
  paster.focus();
155
- Grid.in_paste = false
167
+ Grid.no_blur = false
156
168
  }
157
169
  setTimeout(function() {
158
170
  var text
@@ -264,6 +276,11 @@ Grid.keydown = function(event) {
264
276
  return false
265
277
  } else if (cell.childElements().first().tagName == 'SELECT' && event.shiftKey) {
266
278
  return
279
+ } else if (grid.autoSave) {
280
+ // this will close cell
281
+ var i = cell.childElements().first()
282
+ if (i) i.blur()
283
+ return
267
284
  }
268
285
  var pos = Grid.pos(cell);
269
286
  // go to next row
@@ -306,18 +323,39 @@ Grid.keydown = function(event) {
306
323
  }
307
324
 
308
325
  Grid.isReadOnly = function(cell) {
309
- return cell.select('a').length > 0 || cell.getAttribute('data-m') == 'r'
326
+ return cell.select('a').length > 0 || cell.getAttribute('data-m') == 'r' || cell.hasClassName('action')
310
327
  }
311
328
 
312
- Grid.openCell = function(cell) {
313
- if (cell.hasClassName('input') || Grid.isReadOnly(cell)) return;
329
+ Grid.closeCheckbox = function() {
330
+ if (Grid.need_close) {
331
+ Grid.closeCell(Grid.need_close)
332
+ Grid.need_close = false
333
+ }
334
+ }
335
+ Grid.openCell = function(cell, get_next) {
336
+ var get_next = get_next == undefined ? true : get_next
337
+ Grid.closeCheckbox()
338
+
339
+ if (cell.hasClassName('input')) return;
340
+ if (Grid.isReadOnly(cell)) {
341
+ if (get_next) {
342
+ var n = Element.next(cell) || cell.up().nextSiblings().first().childElements()[0]
343
+ if (n) {
344
+ return Grid.openCell(n)
345
+ } else {
346
+ return false
347
+ }
348
+ } else {
349
+ return false
350
+ }
351
+ }
314
352
  var val = Grid.getValue(cell)
315
353
  cell.prev_value = val;
316
354
 
317
355
  var value = val.value
318
356
  var table = cell.up('table')
319
357
 
320
- if (table.grid.list_name) {
358
+ if (table.grid.is_list) {
321
359
  if (value == 'on') {
322
360
  cell.setAttribute('data-v', 'off')
323
361
  } else {
@@ -353,16 +391,23 @@ Grid.openCell = function(cell) {
353
391
  input = cell.childElements()[0]
354
392
  input.value = value
355
393
  }
394
+
356
395
  input.setStyle({
357
396
  width: w + 'px',
358
397
  height: h + 'px'
359
398
  })
360
- input.observe('blur', Grid.closeCell)
399
+
400
+ if (input.type == 'checkbox') {
401
+ Grid.need_close = cell
402
+ } else {
403
+ input.observe('blur', Grid.closeCell)
404
+ }
361
405
  input.observe('keydown', Grid.keydown)
362
406
  input.observe('paste', Grid.paste)
363
407
  input.focus()
364
408
  input.select()
365
409
  }
410
+ return true
366
411
  }
367
412
 
368
413
  Grid.click = function(event) {
@@ -371,13 +416,19 @@ Grid.click = function(event) {
371
416
  var table = cell.up('table')
372
417
  if (row.hasClassName('action')) {
373
418
  Grid.action(event, cell, row, true)
419
+ Event.stop(event)
374
420
  } else if (cell.hasClassName('action')) {
375
421
  Grid.action(event, cell, row, false)
422
+ Event.stop(event)
376
423
  } else if (cell.tagName == 'TH' && !table.grid.attr_name) {
377
424
  // sort
378
425
  Grid.sort(cell)
426
+ Event.stop(event)
379
427
  } else {
380
- Grid.openCell(cell)
428
+ if (event.element().tagName == 'INPUT') return;
429
+ if (Grid.openCell(cell, false)) {
430
+ Event.stop(event)
431
+ }
381
432
  }
382
433
  }
383
434
 
@@ -438,10 +489,15 @@ Grid.copy = function(cell) {
438
489
 
439
490
  Grid.addRow = function(table, row) {
440
491
  // insert row below
441
- var row_str = '<tr>';
442
- var cells = row.childElements();
443
- for (var i = 0; i < cells.length -1; i++) {
444
- row_str = row_str + '<td></td>';
492
+ var row_str = '<tr>'
493
+ var grid = table.grid
494
+ if (grid.newRow) {
495
+ row_str = row_str + $(grid.newRow).innerHTML
496
+ } else {
497
+ var cells = row.childElements()
498
+ for (var i = 0; i < cells.length -1; i++) {
499
+ row_str = row_str + '<td></td>'
500
+ }
445
501
  }
446
502
  row_str = row_str + Grid.Buttons(table.grid) + '</tr>';
447
503
  row.insert({
@@ -723,26 +779,37 @@ Grid.make = function(table, opts) {
723
779
  if (table.grid) return;
724
780
  Grid.grid_c++;
725
781
  Grid.grids[Grid.grid_c] = table;
726
- table.grid = {
782
+ var grid = {
727
783
  changes: [],
728
784
  id: Grid.grid_c,
729
- helper_id: table.getAttribute('data-helper'),
730
- fdate: table.getAttribute('data-fdate'),
785
+ helper_id: opts.helper || table.getAttribute('data-helper'),
786
+ fdate: opts.fdate || table.getAttribute('data-fdate'),
787
+ newRow: opts.newRow,
731
788
  counter: 0, // Used to create dom_ids for new objects
732
789
  onSuccess: opts.onSuccess,
733
790
  onFailure: opts.onFailure || Grid.onFailure,
734
791
  onStart: opts.onStart || Grid.onStart,
792
+ onChange: opts.onChange,
793
+ // Save on cell close
794
+ autoSave: opts.autoSave || false,
795
+ sort: opts.sort == undefined ? true : opts.sort,
735
796
  add: opts.add || opts.add == undefined,
736
797
  remove: opts.remove || opts.remove == undefined,
737
798
  keydown: opts.keydown,
799
+ show: opts.show || {},
738
800
  };
801
+ table.grid = grid
802
+ grid.table = table
739
803
 
740
804
  // Detect type.
741
- table.grid.attr_name = table.getAttribute('data-a');
742
- table.grid.list_name = table.getAttribute('data-l');
805
+ grid.attr_name = table.getAttribute('data-a')
806
+ grid.list_name = table.getAttribute('data-l')
807
+ // Reverse list name (update columns instead of rows)
808
+ grid.rlist_name = table.getAttribute('data-r')
809
+ grid.is_list = grid.list_name || grid.rlist_name
743
810
 
744
811
  var empty = false;
745
- if (table.grid.attr_name && table.select('th').length == 0) {
812
+ if (grid.attr_name && table.select('th').length == 0) {
746
813
  empty = true;
747
814
  var msg = table.getAttribute('data-msg') || "type to edit";
748
815
  table.innerHTML = "<tr><th>" + msg + "</th></tr><tr><td></td></tr>";
@@ -752,27 +819,34 @@ Grid.make = function(table, opts) {
752
819
  Grid.addButtons(table)
753
820
 
754
821
 
755
- if (table.grid.attr_name) {
822
+ if (grid.attr_name) {
823
+ table.insert({
824
+ after: "<p class='grid_btn'><a class='undo' href='javascript:' onclick='Grid.undo(" + Grid.grid_c + ")'>undo</a></p>"
825
+ });
756
826
  // If we have an attr_name, rows and columns are
757
827
  // serialized as json in a single field.
758
828
  table.insert({
759
- after: "<input type='hidden' id='grid_a_" + Grid.grid_c + "' name='" + table.grid.attr_name + "'/>"
829
+ after: "<input type='hidden' id='grid_a_" + Grid.grid_c + "' name='" + grid.attr_name + "'/>"
760
830
  });
761
- table.grid.input = $("grid_a_" + Grid.grid_c);
762
- if (!empty) table.grid.input.value = Grid.serialize(table);
831
+ grid.input = $("grid_a_" + Grid.grid_c);
832
+ if (!empty) grid.input.value = Grid.serialize(table);
763
833
  } else {
834
+ // Otherwise each row is a new object and each column
835
+ // corresponds to a different attribute (defined in the
836
+ // 'th' of the table).
764
837
  var rows = table.select('tr')
765
838
  for (var i = 1; i < rows.length; i++) {
766
- if (!rows[i].id) {
839
+ if (rows[i].id || rows[i].getAttribute('data-m') == 'r') {
840
+ // do not create object
841
+ } else {
767
842
  Grid.buildObj(table.grid, rows[i])
768
843
  }
769
844
  }
770
- // Otherwise each row is a new object and each column
771
- // corresponds to a different attribute (defined in the
772
- // 'th' of the table).
773
- table.insert({
774
- after: "<p class='grid_btn'><a class='save' href='javascript:' onclick='Grid.save(" + Grid.grid_c + ")'>save</a> <a class='undo' href='javascript:' onclick='Grid.undo(" + Grid.grid_c + ")'>undo</a></p>"
775
- });
845
+ if (!grid.autoSave) {
846
+ table.insert({
847
+ after: "<p class='grid_btn'><a class='save' href='javascript:' onclick='Grid.save(" + Grid.grid_c + ")'>save</a> <a class='undo' href='javascript:' onclick='Grid.undo(" + Grid.grid_c + ")'>undo</a></p>"
848
+ });
849
+ }
776
850
  }
777
851
 
778
852
  table.observe('click', Grid.click);
@@ -795,22 +869,22 @@ Grid.clearChanges = function(list, id) {
795
869
  }
796
870
 
797
871
  Grid.isChanged = function(elem) {
798
- var table = $(elem)
799
- var grid = table.grid
800
- // buildObj adds a row per new object on load
801
- return grid.changes.length > (table.attr_name ? 0 : table.select('tr').length - 1)
872
+ return $$('#'+$(elem).id+' .changed').length > 0
802
873
  }
803
874
 
804
-
805
875
  Grid.save = function(grid_id) {
806
876
  // do not run on GUI thread
807
877
  setTimeout(function() {
808
- var table = Grid.grids[grid_id]
878
+ Grid.closeCheckbox()
879
+ var table = Grid.grids[grid_id] || $(grid_id)
809
880
  var grid = table.grid
810
881
  var data = Grid.compact(grid.changes)
811
882
  if (grid.list_name) {
812
883
  data = Grid.dataForList(grid, data)
884
+ } else if (grid.rlist_name) {
885
+ data = Grid.dataForReverseList(grid, data)
813
886
  }
887
+
814
888
  var todo_count = data.keys().length
815
889
  var done_count = 0
816
890
  if (grid.onStart) {
@@ -822,8 +896,9 @@ Grid.save = function(grid_id) {
822
896
  operations.put = (operations.put || 0) + 1
823
897
  }
824
898
  })
825
- if (!grid.onStart(operations)) return
899
+ if (!grid.onStart(operations, data)) return
826
900
  }
901
+
827
902
  data.each(function(pair) {
828
903
  var id = pair.key
829
904
  var changes = pair.value
@@ -833,6 +908,7 @@ Grid.save = function(grid_id) {
833
908
  attrs['node['+pair.key+']'] = pair.value
834
909
  }
835
910
  })
911
+
836
912
  if (changes._new) {
837
913
  new Ajax.Request('/nodes', {
838
914
  parameters: attrs,
@@ -864,7 +940,7 @@ Grid.save = function(grid_id) {
864
940
  method: 'post'
865
941
  });
866
942
  } else {
867
- new Ajax.Request('/nodes/' + id.replace('id_',''), {
943
+ new Ajax.Request('/nodes/' + id.replace(/^[^0-9]+/,''), {
868
944
  parameters: attrs,
869
945
  onSuccess: function(transport) {
870
946
  done_count++
@@ -884,7 +960,9 @@ Grid.save = function(grid_id) {
884
960
  }
885
961
 
886
962
  Grid.undo = function(grid_id, skip_undone) {
887
- var table = Grid.grids[grid_id]
963
+ Grid.closeCheckbox()
964
+
965
+ var table = Grid.grids[grid_id] || $(grid_id)
888
966
  var grid = table.grid
889
967
  var changes = grid.changes
890
968
  var last = changes.last()
@@ -911,7 +989,7 @@ Grid.undo = function(grid_id, skip_undone) {
911
989
  [cell, cell.up()].invoke('addClassName', 'changed')
912
990
  }
913
991
  cell.addClassName('undone')
914
- if (grid.list_name) {
992
+ if (grid.is_list) {
915
993
  cell.setAttribute('data-v', value)
916
994
  if (value == 'on') {
917
995
  cell.addClassName('on')
@@ -927,6 +1005,11 @@ Grid.undo = function(grid_id, skip_undone) {
927
1005
  changes.pop()
928
1006
  }
929
1007
 
1008
+ if (grid.input) {
1009
+ // single attribute table, serialize in input field
1010
+ grid.input.value = Grid.serialize(table)
1011
+ }
1012
+
930
1013
  if (!skip_undone) {
931
1014
  setTimeout(function() {
932
1015
  table.select('.undone').invoke('removeClassName', 'undone')
@@ -965,6 +1048,9 @@ Grid.test = function() {
965
1048
  // detects which rows have changes and builds the full list of
966
1049
  // ids in the format some_relation_ids:"123,345,888,432". Other fields
967
1050
  // are kept as is.
1051
+ // If update_relation_for is 'column', the list of ids is reversed (they
1052
+ // contain the row ids) and we build operation for setting the relation
1053
+ // with the column id).
968
1054
  Grid.dataForList = function(grid, data) {
969
1055
  var res = {}
970
1056
  var list_name = grid.list_name
@@ -999,6 +1085,43 @@ Grid.dataForList = function(grid, data) {
999
1085
  return $H(res)
1000
1086
  }
1001
1087
 
1088
+ Grid.dataForReverseList = function(grid, data) {
1089
+ var res = {}
1090
+ var rlist_name = grid.rlist_name
1091
+ // {"id_184": {"147": "on"}, "id_193": {"147": "on", "146": "off"}}
1092
+ data.each(function(pair) {
1093
+ var def = pair.value
1094
+ for (var key in def) {
1095
+ if (parseInt(key) + '' == key) {
1096
+ // number key = there is a change in the list
1097
+ var col = res[key]
1098
+ if (!col) {
1099
+ // build full column update operation
1100
+ col = {id:key}
1101
+ res[key] = col
1102
+ var list = []
1103
+ var pos = grid.pos[key]
1104
+ var rows = grid.table.childElements()[0].select('tr')
1105
+ for(var i = rows.length - 1; i >= 0; i--) {
1106
+ var row = rows[i]
1107
+ if (row.getAttribute('data-m') != 'r') {
1108
+ var cell = rows[i].childElements()[pos]
1109
+ if (cell && cell.hasClassName('on')) {
1110
+ list.push(row.id.sub(/^[^\d]+/,''))
1111
+ }
1112
+ }
1113
+ }
1114
+ col[rlist_name] = list.join(',')
1115
+ }
1116
+ } else {
1117
+ // ignore other attributes
1118
+ }
1119
+ }
1120
+ })
1121
+
1122
+ return $H(res)
1123
+ }
1124
+
1002
1125
  Grid.notify = function(table, changes) {
1003
1126
  var rows = table.childElements()[0].select('tr')
1004
1127
  var grid = table.grid
@@ -1011,10 +1134,11 @@ Grid.notify = function(table, changes) {
1011
1134
  } else {
1012
1135
  row = $(obj_id)
1013
1136
  }
1014
- var cells = row.childElements()
1137
+
1015
1138
  var change = changes[obj_id]
1016
1139
  for (var attr in change) {
1017
1140
  if (attr == 'id') continue
1141
+
1018
1142
  if (attr == grid.list_name) {
1019
1143
  var list_on = change[attr].split(/,/)
1020
1144
  var cells = row.childElements()
@@ -1031,11 +1155,36 @@ Grid.notify = function(table, changes) {
1031
1155
  }
1032
1156
  cell.prev_value = undefined
1033
1157
  cell.removeClassName('error')
1158
+ if (cell.hasClassName('changed')) cell.addClassName('saved')
1034
1159
  cell.removeClassName('changed')
1035
- cell.addClassName('saved')
1160
+ }
1161
+ }
1162
+ } else if (attr == grid.rlist_name) {
1163
+ var list_on = change[attr].split(/,/)
1164
+ var pos = grid.pos[obj_id.sub(/^[^\d]+/,'')]
1165
+ var rows = grid.table.childElements()[0].select('tr')
1166
+ for(var i = rows.length - 1; i >= 0; i--) {
1167
+ var row = rows[i]
1168
+ if (row.getAttribute('data-m') != 'r') {
1169
+ var cell = row.childElements()[pos]
1170
+ if (list_on.indexOf(row.id.sub(/^[^\d]+/,'')) >= 0) {
1171
+ cell.orig_value = 'on'
1172
+ cell.setAttribute('data-v', 'on')
1173
+ } else {
1174
+ cell.orig_value = 'off'
1175
+ cell.setAttribute('data-v', 'off')
1176
+ }
1177
+ cell.prev_value = undefined
1178
+ cell.removeClassName('error')
1179
+ if (cell.hasClassName('changed')) cell.addClassName('saved')
1180
+ cell.removeClassName('changed')
1181
+ if (row.select('.changed').length == 0) {
1182
+ row.removeClassName('changed')
1183
+ }
1036
1184
  }
1037
1185
  }
1038
1186
  } else {
1187
+ var cells = row.childElements()
1039
1188
  var cell
1040
1189
  var i = pos[attr]
1041
1190
  if (i == undefined) continue
@@ -1059,7 +1208,7 @@ Grid.notify = function(table, changes) {
1059
1208
  // later
1060
1209
  setTimeout(function() {
1061
1210
  table.select('.saved').invoke('removeClassName', 'saved')
1062
- }, 1000)
1211
+ }, 600)
1063
1212
  }
1064
1213
 
1065
1214
  Grid.simulateClick = function(l) {