zena 0.16.3 → 0.16.4

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.
Files changed (84) hide show
  1. data/.gitignore +1 -0
  2. data/History.txt +11 -0
  3. data/app/controllers/nodes_controller.rb +1 -1
  4. data/app/controllers/versions_controller.rb +1 -1
  5. data/app/controllers/virtual_classes_controller.rb +29 -2
  6. data/app/models/cached_page.rb +3 -3
  7. data/app/models/document.rb +1 -20
  8. data/app/models/iformat.rb +4 -4
  9. data/app/models/image.rb +1 -1
  10. data/app/models/image_content.rb +5 -5
  11. data/app/models/node.rb +25 -0
  12. data/app/models/text_document_version.rb +11 -3
  13. data/app/models/virtual_class.rb +50 -0
  14. data/app/views/{nodes → sites}/404.html +0 -0
  15. data/app/views/templates/edit_tabs/_image.rhtml +1 -1
  16. data/app/views/versions/_list.rhtml +3 -1
  17. data/app/views/virtual_classes/_li.erb +2 -2
  18. data/app/views/virtual_classes/index.erb +9 -1
  19. data/bricks/tags/patch/node.rb +1 -1
  20. data/config/bricks.yml +1 -1
  21. data/config/gems.yml +4 -2
  22. data/db/migrate/047_change_default_link_id_to_zero.rb +1 -1
  23. data/db/migrate/20090825201159_insert_zero_link.rb +1 -1
  24. data/lib/bricks.rb +2 -2
  25. data/lib/bricks/requirements_validation.rb +13 -8
  26. data/lib/gettext_strings.rb +1 -0
  27. data/lib/tasks/zena.rake +15 -6
  28. data/lib/zena.rb +9 -3
  29. data/lib/{base_additions.rb → zena/base_additions.rb} +0 -0
  30. data/lib/{core_ext → zena/core_ext}/date_time.rb +0 -0
  31. data/lib/{core_ext → zena/core_ext}/dir.rb +0 -0
  32. data/lib/zena/core_ext/file_utils.rb +13 -0
  33. data/lib/{core_ext → zena/core_ext}/fixnum.rb +0 -0
  34. data/lib/{core_ext → zena/core_ext}/string.rb +0 -0
  35. data/lib/zena/deploy.rb +1 -1
  36. data/lib/zena/deploy/start.html +1 -1
  37. data/lib/zena/deploy/template.rb +5 -1
  38. data/lib/{fix_rails_layouts.rb → zena/fix_rails_layouts.rb} +0 -0
  39. data/lib/zena/info.rb +2 -2
  40. data/lib/zena/routes.rb +1 -1
  41. data/lib/zena/use/fixtures.rb +1 -1
  42. data/lib/zena/use/html_tags.rb +3 -3
  43. data/lib/zena/use/image_builder.rb +329 -0
  44. data/lib/zena/use/query_comment.rb +96 -0
  45. data/lib/zena/use/query_node.rb +408 -0
  46. data/lib/zena/use/query_node_finders.rb +91 -0
  47. data/lib/zena/use/rendering.rb +2 -2
  48. data/lib/{webdav_adapter.rb → zena/webdav_adapter.rb} +0 -0
  49. data/locale/de/LC_MESSAGES/zena.mo +0 -0
  50. data/locale/de/zena.po +1456 -0
  51. data/locale/en/LC_MESSAGES/zena.mo +0 -0
  52. data/locale/en/zena.po +626 -1197
  53. data/locale/fr/LC_MESSAGES/zena.mo +0 -0
  54. data/locale/fr/zena.po +895 -1221
  55. data/locale/zena.pot +527 -1027
  56. data/public/calendar/lang/calendar-de-utf8.js +128 -0
  57. data/public/calendar/menuarrow.gif +0 -0
  58. data/public/stylesheets/admin.css +2 -1
  59. data/test/custom_queries/complex.host.yml +1 -1
  60. data/test/fixtures/files/vclasses.yml +12 -0
  61. data/test/functional/virtual_classes_controller_test.rb +9 -0
  62. data/test/helpers/{node_query → query_node}/basic.yml +0 -0
  63. data/test/helpers/{node_query → query_node}/comments.yml +0 -0
  64. data/test/helpers/{node_query → query_node}/complex.yml +0 -0
  65. data/test/helpers/{node_query → query_node}/filters.yml +0 -0
  66. data/test/helpers/{node_query → query_node}/relations.yml +0 -0
  67. data/test/helpers/{node_query_test.rb → query_node_test.rb} +1 -2
  68. data/test/integration/navigation_test.rb +2 -0
  69. data/test/test_helper.rb +1 -1
  70. data/test/test_zena.rb +1 -1
  71. data/test/unit/core_ext_test.rb +3 -3
  72. data/test/unit/iformat_test.rb +3 -3
  73. data/test/unit/image_builder_test.rb +34 -34
  74. data/test/unit/node_test.rb +15 -1
  75. data/test/unit/virtual_class_test.rb +106 -0
  76. data/test/unit/zena/use/upload_test.rb +3 -3
  77. data/vendor/plugins/gettext_i18n_rails/tasks/gettext_rails_i18n.rake +3 -3
  78. data/zena.gemspec +36 -29
  79. data/zena_console.rb +33 -0
  80. metadata +30 -23
  81. data/lib/comment_query.rb +0 -92
  82. data/lib/image_builder.rb +0 -325
  83. data/lib/zena/use/node_query_finders.rb +0 -494
  84. data/vendor/bricks/20070122-172926.txt +0 -282
@@ -0,0 +1,96 @@
1
+ require 'querybuilder'
2
+
3
+ module Zena
4
+ module Use
5
+ class QueryComment < QueryBuilder
6
+ attr_reader :uses_node_name, :node_name
7
+ set_main_table 'comments'
8
+ set_main_class 'Comment'
9
+
10
+ # Default sort order
11
+ def default_order_clause
12
+ "created_at ASC"
13
+ end
14
+
15
+ def default_context_filter
16
+ # should never be called alone
17
+ raise Exception.new("QueryComment should only be called from within QueryNode")
18
+ end
19
+
20
+ def parse_change_class(rel, is_last)
21
+ case rel
22
+ when 'author'
23
+ add_table('users')
24
+ @where << "#{table('users')}.id = #{field_or_attr('author_id')}"
25
+ # should we move on to Contact ?
26
+ when 'node', 'nodes'
27
+ add_table('discussions')
28
+ add_table('nodes')
29
+ @where << "#{table('discussions')}.id = #{table('comments')}.discussion_id"
30
+ @where << "#{table('nodes')}.id = #{table('discussions')}.node_id"
31
+ return Zena::Use::QueryNode # class change
32
+ else
33
+ return nil
34
+ end
35
+ end
36
+
37
+ # Same as QueryNode... DRY needed.
38
+ def map_literal(value)
39
+ if value =~ /(.*?)\[(node|visitor|param):(\w+)\](.*)/
40
+ val_start = $1 == '' ? '' : "#{$1.inspect} +"
41
+ val_end = $4 == '' ? '' : "+ #{$4.inspect}"
42
+ case $2
43
+ when 'visitor'
44
+ if $3 == 'user_id'
45
+ value = "visitor.id"
46
+ else
47
+ value = "Node.zafu_attribute(visitor.contact, #{$3.inspect})"
48
+ end
49
+ when 'node'
50
+ @uses_node_name = true
51
+ if $3 == 'user_id'
52
+ value = "#{@node_name}.user_id"
53
+ else
54
+ value = "Node.zafu_attribute(#{@node_name}, #{$3.inspect})"
55
+ end
56
+ when 'param'
57
+ return "\#{Node.connection.quote(#{val_start}params[:#{$3}].to_s#{val_end})}"
58
+ end
59
+
60
+ if !val_start.blank? || !val_end.blank?
61
+ "\#{Node.connection.quote(#{val_start}#{value}#{val_end})}"
62
+ else
63
+ "\#{#{value}}"
64
+ end
65
+ else
66
+ value = Node.connection.quote(value)
67
+ end
68
+ end
69
+
70
+ # Overwrite this and take car to check for valid fields.
71
+ def map_field(fld, table_name, context = nil)
72
+ if ['status', 'updated_at', 'author_name', 'created_at', 'title', 'text', 'author_id'].include?(fld)
73
+ "#{table_name}.#{fld}"
74
+ else
75
+ # TODO: error, raise / ignore ?
76
+ end
77
+ end
78
+
79
+ def map_attr(fld)
80
+ # error
81
+ nil
82
+ end
83
+
84
+ # Erb finder used by zafu
85
+ def finder(count)
86
+ return 'nil' unless valid?
87
+ case count
88
+ when :count
89
+ "#{node_name}.do_find(:count, #{to_s(:count)}, #{!uses_node_name}, #{main_class})"
90
+ else
91
+ "#{node_name}.do_find(#{count.inspect}, #{self.to_s}, #{!uses_node_name}, #{main_class})"
92
+ end
93
+ end
94
+ end # QueryComment
95
+ end # Use
96
+ end # Zena
@@ -0,0 +1,408 @@
1
+ require 'query_builder'
2
+ # Since QueryNode uses a special scope (secure_scope) that needs to be evaluated _in the query string_, you cannot
3
+ # use query.sql. You have to use: eval query.to_s. This is not an issue since normal useage for QueryNode is to be
4
+ # compilated into an ERB template.
5
+
6
+ module Zena
7
+ module Use
8
+ class QueryNode < QueryBuilder
9
+ attr_reader :context, :uses_node_name, :node_name
10
+ set_main_table 'nodes'
11
+ set_main_class 'Node'
12
+ @@filter_fields = {'id' => {:key => 'zip'}}
13
+
14
+ load_custom_queries ["#{RAILS_ROOT}/bricks/*/queries"]
15
+
16
+ def self.insert_zero_link(link_class)
17
+ return if link_class.find_by_id(0)
18
+ link_class.connection.execute "INSERT INTO #{link_class.table_name} (id,target_id,source_id,status,comment) VALUES (0,0,0,NULL,NULL)"
19
+ unless link_class.find_by_id(0)
20
+ # the zero id is replaced by auto-increment value
21
+ last_id = link_class.find(:first, :order => 'id DESC').id
22
+ link_class.connection.execute "UPDATE #{link_class.table_name} SET id = 0 WHERE id = #{last_id}"
23
+ end
24
+ end
25
+
26
+ def self.add_filter_field(key, fld_def)
27
+ @@filter_fields[key] = fld_def
28
+ end
29
+
30
+ def initialize(query, opts = {})
31
+ @uses_node_name = false
32
+ @table_name = 'nodes'
33
+ @node_name = opts[:node_name]
34
+ # list of dyna_attributes keys allready in the filter
35
+ @dyn_keys = {}
36
+ super(query, opts)
37
+ # Raw filters are statements prepared that should not be further processed except for table_name replacement.
38
+ parse_raw_filters(opts[:raw_filters])
39
+ end
40
+
41
+ # Build joins and filters from a relation.
42
+ def parse_relation(rel, context)
43
+ # join_relation first so we can overwrite 'class' finders (images) with a relation.
44
+ unless join_relation(rel, context) || context_relation(rel, context)
45
+ @errors << "unknown relation '#{rel}'"
46
+ end
47
+ end
48
+
49
+ # Default sort order
50
+ def default_order_clause
51
+ "position ASC, name ASC"
52
+ end
53
+
54
+ def default_context_filter
55
+ 'self'
56
+ end
57
+
58
+ def after_parse
59
+ @where.unshift "(\#{#{@node_name}.secure_scope('#{table}')})"
60
+ if @tables.include?('links')
61
+ link_table = table('links')
62
+ @select << "#{link_table}.id AS link_id,#{Zena::Use::Relations::LINK_ATTRIBUTES.map {|l| "#{link_table}.#{l} AS l_#{l}"}.join(',')}"
63
+ elsif @errors_unless_safe_links
64
+ @errors += @errors_unless_safe_links
65
+ end
66
+ @distinct = true if @tables.include?('versions')
67
+ end
68
+
69
+ # Erb finder used by zafu
70
+ def finder(count)
71
+ return 'nil' unless valid?
72
+ case count
73
+ when :count
74
+ "#{node_name}.do_find(:count, #{self.to_s(:count)}, #{!uses_node_name}, #{main_class})"
75
+ else
76
+ "#{node_name}.do_find(#{count.inspect}, #{self.to_s}, #{!uses_node_name}, #{main_class})"
77
+ end
78
+ end
79
+
80
+ private
81
+ # Make sure all alternate queries include "links.id = 0" (dummy link)
82
+ def fix_where_list(where_list)
83
+ return unless @tables.include?('links')
84
+ where_list.each do |f|
85
+ unless f =~ /links\./
86
+ f << " AND links.id = 0"
87
+ end
88
+ end
89
+ true
90
+ end
91
+
92
+ # Used to resolve 'in' clauses ('in project', 'in parent', etc)
93
+ def context_filter_fields(rel, is_last = false)
94
+ case rel
95
+ when 'self'
96
+ ['parent_id', 'id']
97
+ when 'parent'
98
+ is_last ? ['parent_id', 'parent_id'] : ['parent_id', 'id']
99
+ when 'project'
100
+ is_last ? ['project_id', 'project_id'] : ['project_id', 'id']
101
+ when 'section'
102
+ is_last ? ['section_id', 'section_id'] : ['section_id', 'id']
103
+ when 'site', main_table
104
+ :void
105
+ else
106
+ nil
107
+ end
108
+ end
109
+
110
+ # Relations that can be resolved without a join
111
+ def context_relation(rel, context)
112
+ case rel
113
+ when 'self'
114
+ fields = ['id', 'id']
115
+ when 'parent', 'project', 'section'
116
+ fields = ['id', "#{rel}_id"]
117
+ when 'parents', 'projects', 'sections'
118
+ if @table_counter[main_table] > 0 || @tables.include?('links')
119
+ fields = ['id', "#{rel[0..-2]}_id"]
120
+ end
121
+ when 'root'
122
+ @where << "#{table}.id = #{current_site.root_id}"
123
+ return true
124
+ when 'author', 'traductions', 'versions'
125
+ # TODO: not implemented yet...
126
+ return nil
127
+ when 'visitor'
128
+ @where << "#{table}.id = #{insert_bind("visitor.contact_id")}"
129
+ return true
130
+ end
131
+
132
+ unless fields
133
+ if klass = Node.get_class(rel)
134
+ parse_context(default_context_filter) unless context
135
+ @where << "#{table}.kpath LIKE '#{klass.kpath}%'"
136
+ return true
137
+ else
138
+ # unknown class
139
+ return nil
140
+ end
141
+ end
142
+
143
+ @where << "#{field_or_attr(fields[0])} = #{field_or_attr(fields[1], table(main_table,-1))}"
144
+ true
145
+ end
146
+
147
+ def parse_change_class(rel, is_last)
148
+ case rel
149
+ when 'comment', 'comments'
150
+ if is_last
151
+ # no need to load discussions, versions and all the mess
152
+ add_table('comments')
153
+ @where << "#{table('comments')}.discussion_id = #{map_attr('discussion_id')}"
154
+ return Zena::Use::QueryComment # class change
155
+ else
156
+ # parse_context(default_context_filter, true) if is_last
157
+ # after_parse
158
+ add_table('discussions')
159
+ add_table('comments')
160
+ @where << "#{table('discussions')}.node_id = #{table('nodes')}.id"
161
+ @where << "#{table('comments')}.discussion_id = #{table('discussions')}.id"
162
+ after_parse
163
+ return Zena::Use::QueryComment
164
+ end
165
+ else
166
+ return nil
167
+ end
168
+ end
169
+
170
+ # Filters that need a join
171
+ def join_relation(rel, context, new_table_alias = nil, previous_table_alias = nil)
172
+ new_table_alias ||= table(main_table)
173
+ previous_table_alias ||= table(main_table, -1)
174
+
175
+ if rel == main_table || rel == 'children'
176
+ # dummy clauses
177
+ parse_context(default_context_filter) unless context
178
+ return :void
179
+ end
180
+
181
+ if rel = RelationProxy.find_by_role(rel.singularize)
182
+ # We cannot use a LEFT JOIN here because it will totally mess up if we merge alternate queries
183
+ add_table('links')
184
+ # (= other_side = result) target <-- source (= link_side = caller)
185
+ if context && context != 'self'
186
+ # tagged in project (not equal to 'tagged from nodes in project')
187
+ # remove caller join
188
+ @distinct = true
189
+ @where << "#{field_or_attr('id', new_table_alias)} = #{table('links')}.#{rel.other_side} AND #{table('links')}.relation_id = #{rel[:id]}"
190
+ else
191
+ @where << "#{field_or_attr('id', new_table_alias)} = #{table('links')}.#{rel.other_side} AND #{table('links')}.relation_id = #{rel[:id]} AND #{table('links')}.#{rel.link_side} = #{field_or_attr('id', previous_table_alias)}"
192
+ end
193
+ else
194
+ nil
195
+ end
196
+ end
197
+
198
+ def map_literal(value, env = :sql)
199
+ if value =~ /(.*?)\[(visitor|param|node):(\w+)\](.*)/
200
+ val_start = $1 == '' ? '' : "#{$1.inspect} +"
201
+ val_end = $4 == '' ? '' : "+ #{$4.inspect}"
202
+ case $2
203
+ when 'visitor'
204
+ value = env == :sql ? insert_bind("#{val_start}Node.zafu_attribute(visitor.contact, #{$3.inspect})#{val_end}") : nil
205
+ when 'param'
206
+ value = env == :sql ? insert_bind("#{val_start}params[:#{$3}].to_s#{val_end}") : "params[:#{$3}]"
207
+ when 'node'
208
+ @uses_node_name = true
209
+ value = env == :sql ? insert_bind("#{val_start}#{@node_name}.safe_read(#{$3.inspect}).to_s#{val_end}") : "#{@node_name}.safe_read(#{$3.inspect})"
210
+ end
211
+ else
212
+ value = env == :sql ? quote(value) : nil
213
+ end
214
+ end
215
+
216
+ # Translate fields used for query/sort/grouping (context parameter) into something useable by SQL. Add the appropriate tables when needed.
217
+ def map_field(field, table_name = table, context = nil)
218
+ return map_literal("[#{field}]") if field =~ /\Aparam:/
219
+ case field[0..1]
220
+ when 'd_'
221
+ # DYNAMIC ATTRIBUTE
222
+ key = field[2..-1]
223
+ key, function = parse_sql_function_in_field(key)
224
+ key = Zena::Db.sql_function(function, dyn_value('versions', key, context))
225
+ when 'c_'
226
+ # CONTENT TABLE
227
+ field = field[2..-1]
228
+ # FIXME: implement #41
229
+ nil
230
+ when 'v_'
231
+ # VERSION
232
+ key = field[2..-1]
233
+ key, function = parse_sql_function_in_field(key)
234
+ if Version.safe_method_type([key]) && Version.column_names.include?(key)
235
+ vtable_name = needs_table('nodes', 'versions', "TABLE1.id = TABLE2.node_id")
236
+ Zena::Db.sql_function(function, "#{vtable_name}.#{key}")
237
+ else
238
+ # bad version attribute
239
+ nil
240
+ end
241
+ when 'l_'
242
+ key, function = parse_sql_function_in_field(field)
243
+ if key == 'l_status' || key == 'l_comment' || key == 'l_date' || (key == 'l_id' && [:order, :group].include?(context))
244
+ @errors_unless_safe_links ||= []
245
+ @errors_unless_safe_links << "cannot use link field '#{key}' in this query" unless (key == 'l_id' && context == :order)
246
+ # ok
247
+ Zena::Db.sql_function(function, "#{table('links')}.#{key[2..-1]}")
248
+ else
249
+ # bad attribute
250
+ nil
251
+ end
252
+ else
253
+ # NODE
254
+ key, function = parse_sql_function_in_field(field)
255
+ if context == :filter
256
+ if map_def = @@filter_fields[key]
257
+ if table_def = map_def[:table]
258
+ table_to_use = needs_table(*table_def)
259
+ else
260
+ table_to_use = table_name
261
+ end
262
+ Zena::Db.sql_function(function, "#{table_to_use}.#{map_def[:key]}")
263
+ elsif (Node.safe_method_type([key]) && Node.column_names.include?(key))
264
+ Zena::Db.sql_function(function, "#{table_name}.#{key}")
265
+ elsif key =~ /^(.*)_ids?$/
266
+ # tag_id = 33 ===> join links as lk, nodes as tt .......
267
+ rel = $1
268
+
269
+ if RelationProxy.find_by_role(rel.singularize)
270
+ add_table('jnode', 'nodes')
271
+ join_relation(rel, nil, table('jnode'), table('nodes'))
272
+ "#{table('jnode')}.zip"
273
+ else
274
+ nil
275
+ end
276
+ else
277
+ nil
278
+ end
279
+ else
280
+ if ['id', 'parent_id','project_id','section_id'].include?(key) || (Node.safe_method_type([key]) && Node.column_names.include?(key))
281
+ Zena::Db.sql_function(function, "#{table_name}.#{key}")
282
+ else
283
+ # bad attribute
284
+ nil
285
+ end
286
+ end
287
+ end
288
+ end
289
+
290
+ def valid_field?(table_name, fld)
291
+ # FIXME: security !
292
+ true
293
+ end
294
+
295
+ def map_attr(fld, env = :sql)
296
+ case fld
297
+ when 'project_id', 'section_id', 'discussion_id'
298
+ @uses_node_name = true
299
+ insert_bind("#{@node_name}.get_#{fld}")
300
+ when 'id', 'parent_id'
301
+ @uses_node_name = true
302
+ insert_bind("#{@node_name}.#{fld}")
303
+ else
304
+ # type = Node.safe_method_type([fld])
305
+ # type[:method] ?
306
+
307
+ # bad parameter
308
+ @errors << "invalid parameter '#{fld}'"
309
+ "0"
310
+ end
311
+ end
312
+
313
+ def parse_paginate_clause(paginate)
314
+ return @offset unless paginate
315
+ if !@limit
316
+ # TODO: raise error ?
317
+ @errors << "invalid paginate clause '#{paginate}' (used without limit)"
318
+ nil
319
+ elsif (fld = map_literal("[param:#{paginate}]", :ruby)) && (page_size = @limit[/ LIMIT (\d+)/,1])
320
+ @page_size = [2,page_size.to_i].max
321
+ " OFFSET #{insert_bind("((#{fld}.to_i > 0 ? #{fld}.to_i : 1)-1)*#{page_size.to_i}")}"
322
+ else
323
+ @errors << "invalid paginate clause '#{paginate}'"
324
+ nil
325
+ end
326
+ end
327
+
328
+ # When a field is defined as log_at:year, return [log_at, year].
329
+ def parse_sql_function_in_field(field)
330
+ if field =~ /\A(\w+):(\w+)\Z/
331
+ if ['year'].include?($2)
332
+ [$1,$2]
333
+ else
334
+ [$1]
335
+ end
336
+ else
337
+ [field]
338
+ end
339
+ end
340
+
341
+ def parse_raw_filters(filters)
342
+ return unless filters
343
+ filters.each do |f|
344
+ all_ok = true
345
+ filter = f.gsub(/TABLE_NAME(\[(\w+)\]|)/) do
346
+ if $2
347
+ if @table_counter[$2]
348
+ table($2)
349
+ else
350
+ all_ok = false
351
+ @errors << "invalid table_name '#{$2}' in raw filter"
352
+ @errors.uniq!
353
+ end
354
+ else
355
+ table
356
+ end
357
+ end
358
+ @where << filter if all_ok
359
+ end
360
+ end
361
+
362
+ def dyn_value(table_name, key, context)
363
+ @dyn_keys[table_name] ||= {}
364
+ @dyn_keys[table_name][key] ||= begin
365
+ needs_table('nodes', 'versions', "TABLE1.id = TABLE2.node_id")
366
+ dtable = needs_join_table('versions', 'LEFT', 'dyn_attributes', "TABLE1.id = TABLE2.owner_id AND TABLE2.key = #{quote(key)}", "versions=dyn_attributes=#{key}")
367
+ "#{dtable}.value"
368
+ end
369
+ end
370
+
371
+ def parse_custom_query_argument(key, value)
372
+ return nil unless value
373
+ super.gsub(/(RELATION_ID|NODE_ATTR)\(([^)]+)\)/) do
374
+ type, value = $1, $2
375
+ if type == 'RELATION_ID'
376
+ role = value
377
+ if rel = RelationProxy.find_by_role(role.singularize)
378
+ rel[:id]
379
+ else
380
+ @errors << "could not find Relation '#{role}' in custom query"
381
+ '-1'
382
+ end
383
+ elsif type == 'NODE_ATTR'
384
+ attribute = value
385
+ if Node.safe_method_type([attribute])
386
+ insert_bind("#{@node_name}.#{attribute}")
387
+ else
388
+ @errors << "cannot read attribute '#{attribute}' in custom query"
389
+ '-1'
390
+ end
391
+ end
392
+ end.gsub(/NODE_ID/) do
393
+ @uses_node_name = true
394
+ insert_bind("#{@node_name}.id")
395
+ end
396
+ end
397
+
398
+ def extract_custom_query(list)
399
+ super.singularize
400
+ end
401
+
402
+ def quote(literal)
403
+ Node.connection.quote(literal)
404
+ end
405
+ end # QueryNode
406
+ end # Use
407
+ end # Zena
408
+