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
data/lib/comment_query.rb DELETED
@@ -1,92 +0,0 @@
1
- require 'querybuilder'
2
-
3
- class CommentQuery < QueryBuilder
4
- attr_reader :uses_node_name, :node_name
5
- set_main_table 'comments'
6
- set_main_class 'Comment'
7
-
8
- # Default sort order
9
- def default_order_clause
10
- "created_at ASC"
11
- end
12
-
13
- def default_context_filter
14
- # should never be called alone
15
- raise Exception.new("CommentQuery should only be called from within NodeQuery")
16
- end
17
-
18
- def parse_change_class(rel, is_last)
19
- case rel
20
- when 'author'
21
- add_table('users')
22
- @where << "#{table('users')}.id = #{field_or_attr('author_id')}"
23
- # should we move on to Contact ?
24
- when 'node', 'nodes'
25
- add_table('discussions')
26
- add_table('nodes')
27
- @where << "#{table('discussions')}.id = #{table('comments')}.discussion_id"
28
- @where << "#{table('nodes')}.id = #{table('discussions')}.node_id"
29
- return NodeQuery # class change
30
- else
31
- return nil
32
- end
33
- end
34
-
35
- # Same as NodeQuery... DRY needed.
36
- def map_literal(value)
37
- if value =~ /(.*?)\[(node|visitor|param):(\w+)\](.*)/
38
- val_start = $1 == '' ? '' : "#{$1.inspect} +"
39
- val_end = $4 == '' ? '' : "+ #{$4.inspect}"
40
- case $2
41
- when 'visitor'
42
- if $3 == 'user_id'
43
- value = "visitor.id"
44
- else
45
- value = "Node.zafu_attribute(visitor.contact, #{$3.inspect})"
46
- end
47
- when 'node'
48
- @uses_node_name = true
49
- if $3 == 'user_id'
50
- value = "#{@node_name}.user_id"
51
- else
52
- value = "Node.zafu_attribute(#{@node_name}, #{$3.inspect})"
53
- end
54
- when 'param'
55
- return "\#{Node.connection.quote(#{val_start}params[:#{$3}].to_s#{val_end})}"
56
- end
57
-
58
- if !val_start.blank? || !val_end.blank?
59
- "\#{Node.connection.quote(#{val_start}#{value}#{val_end})}"
60
- else
61
- "\#{#{value}}"
62
- end
63
- else
64
- value = Node.connection.quote(value)
65
- end
66
- end
67
-
68
- # Overwrite this and take car to check for valid fields.
69
- def map_field(fld, table_name, context = nil)
70
- if ['status', 'updated_at', 'author_name', 'created_at', 'title', 'text', 'author_id'].include?(fld)
71
- "#{table_name}.#{fld}"
72
- else
73
- # TODO: error, raise / ignore ?
74
- end
75
- end
76
-
77
- def map_attr(fld)
78
- # error
79
- nil
80
- end
81
-
82
- # Erb finder used by zafu
83
- def finder(count)
84
- return 'nil' unless valid?
85
- case count
86
- when :count
87
- "#{node_name}.do_find(:count, #{to_s(:count)}, #{!uses_node_name}, #{main_class})"
88
- else
89
- "#{node_name}.do_find(#{count.inspect}, #{self.to_s}, #{!uses_node_name}, #{main_class})"
90
- end
91
- end
92
- end
data/lib/image_builder.rb DELETED
@@ -1,325 +0,0 @@
1
- require 'tempfile'
2
- require 'digest/sha1'
3
- unless defined?(Magick)
4
- begin
5
- # this works on the deb box
6
- require 'RMagick'
7
- rescue LoadError
8
- begin
9
- # this works on my Mac
10
- require 'rmagick'
11
- rescue LoadError
12
- puts "ImageMagick not found. Using dummy."
13
- require 'ftools'
14
- # Create a dummy magick module
15
- module Magick
16
- CenterGravity = OverCompositeOp = MaxRGB = NorthGravity = SouthGravity = nil
17
- class << self
18
- end
19
- class ZenaDummy
20
- def initialize(*a)
21
- end
22
- def method_missing(meth, *args)
23
- # do nothing
24
- end
25
- end
26
- class Image < ZenaDummy
27
- end
28
- class ImageList < ZenaDummy
29
- end
30
- end
31
- end
32
- end
33
- end
34
-
35
- class ImageBuilder
36
- DEFAULT_FORMATS = {
37
- 'tiny' => { :name=>'tiny', :size=>:force, :width=>16, :height=>16 , :gravity=>Magick::CenterGravity },
38
- 'mini' => { :name=>'mini', :size=>:force, :width=>32, :height=>32 , :gravity=>Magick::CenterGravity },
39
- 'square' => { :name=>'square', :size=>:limit, :width=>180, :height=>180, :gravity=>Magick::CenterGravity },
40
- 'med' => { :name=>'med', :size=>:limit, :width=>280, :height=>186, :gravity=>Magick::CenterGravity },
41
- 'top' => { :name=>'top', :size=>:force, :width=>280, :height=>186, :gravity => Magick::NorthGravity },
42
- 'low' => { :name=>'low', :size=>:force, :width=>280, :height=>186, :gravity => Magick::SouthGravity },
43
- 'side' => { :name=>'side', :size=>:force, :width=>220, :height=>500, :gravity=>Magick::CenterGravity },
44
- 'std' => { :name=>'std', :size=>:limit, :width=>600, :height=>400, :gravity=>Magick::CenterGravity },
45
- 'pv' => { :name=>'pv', :size=>:force, :width=>70, :height=>70 , :gravity=>Magick::CenterGravity },
46
- 'edit' => { :name=>'edit', :size=>:limit, :width=>400, :height=>400, :gravity=>Magick::CenterGravity },
47
- 'full' => { :name=>'full', :size=>:keep , :gravity=>Magick::CenterGravity },
48
- nil => { :name=>'full', :size=>:keep , :gravity=>Magick::CenterGravity },
49
- }.freeze
50
- # 'sepia'=> { :size=>:limit, :width=>280, :ratio=>2/3.0, :post=>Proc.new {|img| img.sepiatone(Magick::MaxRGB * 0.8)}},
51
-
52
- class << self
53
- def image_content_type?(content_type)
54
- content_type =~ /image/
55
- end
56
-
57
- def dummy?
58
- Magick.const_defined?(:ZenaDummy)
59
- end
60
-
61
- def hash_id(format)
62
- Digest::SHA1.hexdigest("#{format[:name]}#{format[:size]}#{format[:width]}#{format[:height]}#{format[:gravity]}")[0..9].to_i(16)
63
- end
64
-
65
- DEFAULT_FORMATS.each do |k, v|
66
- v[:hash_id] = ImageBuilder.hash_id(v)
67
- v.freeze
68
- end
69
- end
70
-
71
- def initialize(h)
72
- params = {:height=>nil, :width=>nil, :path=>nil, :file=>nil, :actions=>[]}.merge(h)
73
-
74
- params.each do |k,v|
75
- case k
76
- when :height
77
- @height = v if v
78
- when :width
79
- @width = v if v
80
- when :path
81
- @path = v if v
82
- when :file
83
- @file = v if v
84
- when :actions
85
- if v.kind_of?(Array)
86
- @actions = v
87
- else
88
- raise StandardError, "Bad actions format"
89
- end
90
- else
91
- raise StandardError, "Bad parameter (#{k})"
92
- end
93
- end
94
- unless @width && @height || dummy?
95
- if @img = build_image_from_file_or_path
96
- @width = @img.columns
97
- @height = @img.rows
98
- end
99
- end
100
- end
101
-
102
- def dummy?
103
- ImageBuilder.dummy? || (!@path && !@img && !@file)
104
- end
105
-
106
- def read
107
- return nil if dummy?
108
- render_img
109
- @img.to_blob
110
- end
111
-
112
- def write(path)
113
- return false if dummy?
114
- render_img
115
- @img.write(path)
116
- end
117
-
118
- def rows
119
- return nil unless @height || !dummy?
120
- (@height ||= render_img.rows).round
121
- end
122
-
123
- def columns
124
- return nil unless @width || !dummy?
125
- (@width ||= render_img.columns).round
126
- end
127
-
128
- alias height rows
129
- alias width columns
130
-
131
- def resize!(s)
132
- # we do not zoom pixels
133
- return unless s < 1.0
134
- @img = nil # reset current rendered image
135
- @width *= s
136
- @height *= s
137
- @actions << Proc.new {|img| img.resize!(s) }
138
- end
139
-
140
- def crop!(x,y,w,h)
141
- @img = nil # reset current rendered image
142
- @width = [@width -x, w].min
143
- @height = [@height-y, h].min
144
- @actions << Proc.new {|img| img.crop!(x,y,[@img.columns-x, w].min,[@img.rows-y, h].min, true) }
145
- end
146
-
147
- def format=(fmt)
148
- return unless !dummy? && Magick.formats[fmt.upcase] =~ /w/
149
- @actions << Proc.new {|img| img.format = fmt.upcase; img }
150
- end
151
-
152
- def format
153
- render_img.format
154
- end
155
-
156
- def exif
157
- @exif ||= ExifData.new(render_img.get_exif_by_entry)
158
- end
159
-
160
- def max_filesize=(size)
161
- @actions << Proc.new {|img| do_limit!(size) }
162
- end
163
-
164
- def do_limit!(size)
165
- return @img if @filesize <= size
166
-
167
- # Check real size
168
- tmp_path = Tempfile.new('tmp_img').path
169
- @img.write('jpeg:' + tmp_path)
170
-
171
- return @img if File.stat(tmp_path).size <= size
172
-
173
- # Change type to JPG and quality to 80
174
- if (@img.format == 'JPG' || @img.format == 'JPEG') && @img.quality > 80
175
- @img.write('jpeg:' + tmp_path) { self.quality = 80 }
176
- else
177
- @img.format = 'JPG'
178
- @img.write('jpeg:' + tmp_path) { self.quality = 80 }
179
- end
180
- ratio = File.stat(tmp_path).size.to_f / size
181
-
182
- return @img = Magick::ImageList.new(tmp_path) if ratio <= 1.0
183
-
184
- # Not enough ? Resize.
185
- ratio = 1.0 / Math.sqrt(ratio)
186
- @width *= ratio
187
- @height *= ratio
188
- @img.resize!(ratio)
189
- @img
190
- end
191
-
192
- def crop_min!(w,h,gravity=Magick::CenterGravity)
193
- @img = nil # reset current rendered image
194
- @width = [@width ,w].min
195
- @height = [@height,h].min
196
- @actions << Proc.new {|img| img.crop!(gravity,[@img.columns,w].min,[@img.rows,h].min, true) }
197
- end
198
-
199
- def set_background!(opacity,w,h)
200
- @img = nil # reset current rendered image
201
- @width = [@width ,w].max
202
- @height = [@height,h].max
203
- @actions << Proc.new do |img|
204
- bg = Magick::Image.new(w,h)
205
- bg.opacity = opacity
206
- bg.format = img.format
207
- img = bg.composite(img, Magick::CenterGravity, Magick::OverCompositeOp)
208
- end
209
- end
210
-
211
- # Transform into another format. If nil : do nothing.
212
- def transform!(tformat=nil)
213
- return self unless tformat
214
- @img = nil
215
- format = { :size=>:limit, :gravity=>Magick::CenterGravity }.merge(tformat)
216
- @pre, @post = format[:pre], format[:post]
217
-
218
- if format[:size] == :keep
219
- h,w = @height, @width
220
- else
221
- h,w = format[:height], format[:width]
222
- end
223
- if format[:scale]
224
- if h || w
225
- # scale is a pre-zoom before crop
226
- scale = format[:scale]
227
- else
228
- # we resize to scale
229
- h,w = @height*format[:scale], @width*format[:scale]
230
- # but we do not zoom
231
- scale = 1.0
232
- # ignore ':size' format if not height nor width was given
233
- format[:size] = :force
234
- end
235
- else
236
- scale = 1.0
237
- end
238
- if format[:ratio] && h && !w
239
- w = h / format[:ratio]
240
- elsif format[:ratio] && w && !h
241
- h = w * format[:ratio]
242
- end
243
-
244
- pw,ph = @width, @height
245
- if [w,h,pw,ph].include?(nil) || [w,h,pw,ph].min <= 0
246
- # image size or thumb size is null (no image processing tool used, no idea on image size)
247
- if format[:size] == :keep
248
- @height, @width = nil, nil
249
- else
250
- @height, @width = h, w
251
- end
252
- return self
253
- end
254
-
255
- case format[:size]
256
- when :force
257
- crop_scale = [w.to_f/pw, h.to_f/ph].max
258
- if crop_scale > 1.0
259
- # we do not zoom. Fill with transparent background.
260
- crop_min!(w,h,format[:gravity])
261
- set_background!(Magick::MaxRGB, w, h)
262
- else
263
- resize!(crop_scale * scale)
264
- crop_min!(w, h,format[:gravity])
265
- end
266
- when :force_no_crop
267
- crop_scale = [w.to_f/pw, h.to_f/ph].min
268
- resize!(crop_scale * scale)
269
- crop_min!(w, h,format[:gravity])
270
- set_background!(Magick::MaxRGB, w, h)
271
- when :limit
272
- crop_scale = [w.to_f/pw, h.to_f/ph].min
273
- resize!(crop_scale * scale)
274
- crop_min!(w, h,format[:gravity])
275
- when :keep
276
- end
277
- self
278
- end
279
-
280
- def render_img
281
- raise IOError, 'MagickDummy cannot render image' if ImageBuilder.dummy?
282
- unless @img
283
- unless @img = build_image_from_file_or_path
284
- raise IOError, 'Cannot render image without path or file'
285
- end
286
- if @pre
287
- @pre = [@pre].flatten
288
- @pre.each do |a|
289
- @img = a.call(@img)
290
- end
291
- end
292
-
293
- if @actions
294
- @actions.each do |a|
295
- @img = a.call(@img)
296
- end
297
- end
298
-
299
- if @post
300
- @post = [@post].flatten
301
- @post.each do |a|
302
- @img = a.call(@img)
303
- end
304
- end
305
- end
306
- @img
307
- end
308
-
309
- def build_image_from_file_or_path
310
- if @file || @path
311
- if @file.kind_of?(StringIO)
312
- img = Magick::ImageList.new
313
- @file.rewind
314
- img.from_blob(@file.read)
315
- @file.rewind
316
- @filesize = @file.size
317
- img
318
- else
319
- img = Magick::ImageList.new(@file ? @file.path : @path)
320
- @filesize = img.filesize
321
- img
322
- end
323
- end
324
- end
325
- end
@@ -1,494 +0,0 @@
1
- require 'query_builder'
2
- # Since NodeQuery 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 NodeQuery is to be
4
- # compilated into an ERB template.
5
- class NodeQuery < QueryBuilder
6
- attr_reader :context, :uses_node_name, :node_name
7
- set_main_table 'nodes'
8
- set_main_class 'Node'
9
- @@filter_fields = {'id' => {:key => 'zip'}}
10
-
11
- load_custom_queries ["#{RAILS_ROOT}/bricks/*/queries"]
12
-
13
- def self.insert_zero_link(link_class)
14
- return if link_class.find_by_id(0)
15
- link_class.connection.execute "INSERT INTO #{link_class.table_name} (id,target_id,source_id,status,comment) VALUES (0,0,0,NULL,NULL)"
16
- unless link_class.find_by_id(0)
17
- # the zero id is replaced by auto-increment value
18
- last_id = link_class.find(:first, :order => 'id DESC').id
19
- link_class.connection.execute "UPDATE #{link_class.table_name} SET id = 0 WHERE id = #{last_id}"
20
- end
21
- end
22
-
23
- def self.add_filter_field(key, fld_def)
24
- @@filter_fields[key] = fld_def
25
- end
26
-
27
- def initialize(query, opts = {})
28
- @uses_node_name = false
29
- @table_name = 'nodes'
30
- @node_name = opts[:node_name]
31
- # list of dyna_attributes keys allready in the filter
32
- @dyn_keys = {}
33
- super(query, opts)
34
- # Raw filters are statements prepared that should not be further processed except for table_name replacement.
35
- parse_raw_filters(opts[:raw_filters])
36
- end
37
-
38
- # Build joins and filters from a relation.
39
- def parse_relation(rel, context)
40
- # join_relation first so we can overwrite 'class' finders (images) with a relation.
41
- unless join_relation(rel, context) || context_relation(rel, context)
42
- @errors << "unknown relation '#{rel}'"
43
- end
44
- end
45
-
46
- # Default sort order
47
- def default_order_clause
48
- "position ASC, name ASC"
49
- end
50
-
51
- def default_context_filter
52
- 'self'
53
- end
54
-
55
- def after_parse
56
- @where.unshift "(\#{#{@node_name}.secure_scope('#{table}')})"
57
- if @tables.include?('links')
58
- link_table = table('links')
59
- @select << "#{link_table}.id AS link_id,#{Zena::Use::Relations::LINK_ATTRIBUTES.map {|l| "#{link_table}.#{l} AS l_#{l}"}.join(',')}"
60
- elsif @errors_unless_safe_links
61
- @errors += @errors_unless_safe_links
62
- end
63
- @distinct = true if @tables.include?('versions')
64
- end
65
-
66
- # Erb finder used by zafu
67
- def finder(count)
68
- return 'nil' unless valid?
69
- case count
70
- when :count
71
- "#{node_name}.do_find(:count, #{self.to_s(:count)}, #{!uses_node_name}, #{main_class})"
72
- else
73
- "#{node_name}.do_find(#{count.inspect}, #{self.to_s}, #{!uses_node_name}, #{main_class})"
74
- end
75
- end
76
-
77
- private
78
- # Make sure all alternate queries include "links.id = 0" (dummy link)
79
- def fix_where_list(where_list)
80
- return unless @tables.include?('links')
81
- where_list.each do |f|
82
- unless f =~ /links\./
83
- f << " AND links.id = 0"
84
- end
85
- end
86
- true
87
- end
88
-
89
- # Used to resolve 'in' clauses ('in project', 'in parent', etc)
90
- def context_filter_fields(rel, is_last = false)
91
- case rel
92
- when 'self'
93
- ['parent_id', 'id']
94
- when 'parent'
95
- is_last ? ['parent_id', 'parent_id'] : ['parent_id', 'id']
96
- when 'project'
97
- is_last ? ['project_id', 'project_id'] : ['project_id', 'id']
98
- when 'section'
99
- is_last ? ['section_id', 'section_id'] : ['section_id', 'id']
100
- when 'site', main_table
101
- :void
102
- else
103
- nil
104
- end
105
- end
106
-
107
- # Relations that can be resolved without a join
108
- def context_relation(rel, context)
109
- case rel
110
- when 'self'
111
- fields = ['id', 'id']
112
- when 'parent', 'project', 'section'
113
- fields = ['id', "#{rel}_id"]
114
- when 'parents', 'projects', 'sections'
115
- if @table_counter[main_table] > 0 || @tables.include?('links')
116
- fields = ['id', "#{rel[0..-2]}_id"]
117
- end
118
- when 'root'
119
- @where << "#{table}.id = #{current_site.root_id}"
120
- return true
121
- when 'author', 'traductions', 'versions'
122
- # TODO: not implemented yet...
123
- return nil
124
- when 'visitor'
125
- @where << "#{table}.id = #{insert_bind("visitor.contact_id")}"
126
- return true
127
- end
128
-
129
- unless fields
130
- if klass = Node.get_class(rel)
131
- parse_context(default_context_filter) unless context
132
- @where << "#{table}.kpath LIKE '#{klass.kpath}%'"
133
- return true
134
- else
135
- # unknown class
136
- return nil
137
- end
138
- end
139
-
140
- @where << "#{field_or_attr(fields[0])} = #{field_or_attr(fields[1], table(main_table,-1))}"
141
- true
142
- end
143
-
144
- def parse_change_class(rel, is_last)
145
- case rel
146
- when 'comment', 'comments'
147
- if is_last
148
- # no need to load discussions, versions and all the mess
149
- add_table('comments')
150
- @where << "#{table('comments')}.discussion_id = #{map_attr('discussion_id')}"
151
- return CommentQuery # class change
152
- else
153
- # parse_context(default_context_filter, true) if is_last
154
- # after_parse
155
- add_table('discussions')
156
- add_table('comments')
157
- @where << "#{table('discussions')}.node_id = #{table('nodes')}.id"
158
- @where << "#{table('comments')}.discussion_id = #{table('discussions')}.id"
159
- after_parse
160
- return CommentQuery
161
- end
162
- else
163
- return nil
164
- end
165
- end
166
-
167
- # Filters that need a join
168
- def join_relation(rel, context, new_table_alias = nil, previous_table_alias = nil)
169
- new_table_alias ||= table(main_table)
170
- previous_table_alias ||= table(main_table, -1)
171
-
172
- if rel == main_table || rel == 'children'
173
- # dummy clauses
174
- parse_context(default_context_filter) unless context
175
- return :void
176
- end
177
-
178
- if rel = RelationProxy.find_by_role(rel.singularize)
179
- # We cannot use a LEFT JOIN here because it will totally mess up if we merge alternate queries
180
- add_table('links')
181
- # (= other_side = result) target <-- source (= link_side = caller)
182
- if context && context != 'self'
183
- # tagged in project (not equal to 'tagged from nodes in project')
184
- # remove caller join
185
- @distinct = true
186
- @where << "#{field_or_attr('id', new_table_alias)} = #{table('links')}.#{rel.other_side} AND #{table('links')}.relation_id = #{rel[:id]}"
187
- else
188
- @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)}"
189
- end
190
- else
191
- nil
192
- end
193
- end
194
-
195
- def map_literal(value, env = :sql)
196
- if value =~ /(.*?)\[(visitor|param|node):(\w+)\](.*)/
197
- val_start = $1 == '' ? '' : "#{$1.inspect} +"
198
- val_end = $4 == '' ? '' : "+ #{$4.inspect}"
199
- case $2
200
- when 'visitor'
201
- value = env == :sql ? insert_bind("#{val_start}Node.zafu_attribute(visitor.contact, #{$3.inspect})#{val_end}") : nil
202
- when 'param'
203
- value = env == :sql ? insert_bind("#{val_start}params[:#{$3}].to_s#{val_end}") : "params[:#{$3}]"
204
- when 'node'
205
- @uses_node_name = true
206
- value = env == :sql ? insert_bind("#{val_start}#{@node_name}.safe_read(#{$3.inspect}).to_s#{val_end}") : "#{@node_name}.safe_read(#{$3.inspect})"
207
- end
208
- else
209
- value = env == :sql ? quote(value) : nil
210
- end
211
- end
212
-
213
- # Translate fields used for query/sort/grouping (context parameter) into something useable by SQL. Add the appropriate tables when needed.
214
- def map_field(field, table_name = table, context = nil)
215
- return map_literal("[#{field}]") if field =~ /\Aparam:/
216
- case field[0..1]
217
- when 'd_'
218
- # DYNAMIC ATTRIBUTE
219
- key = field[2..-1]
220
- key, function = parse_sql_function_in_field(key)
221
- key = Zena::Db.sql_function(function, dyn_value('versions', key, context))
222
- when 'c_'
223
- # CONTENT TABLE
224
- field = field[2..-1]
225
- # FIXME: implement #41
226
- nil
227
- when 'v_'
228
- # VERSION
229
- key = field[2..-1]
230
- key, function = parse_sql_function_in_field(key)
231
- if Version.safe_method_type([key]) && Version.column_names.include?(key)
232
- vtable_name = needs_table('nodes', 'versions', "TABLE1.id = TABLE2.node_id")
233
- Zena::Db.sql_function(function, "#{vtable_name}.#{key}")
234
- else
235
- # bad version attribute
236
- nil
237
- end
238
- when 'l_'
239
- key, function = parse_sql_function_in_field(field)
240
- if key == 'l_status' || key == 'l_comment' || key == 'l_date' || (key == 'l_id' && [:order, :group].include?(context))
241
- @errors_unless_safe_links ||= []
242
- @errors_unless_safe_links << "cannot use link field '#{key}' in this query" unless (key == 'l_id' && context == :order)
243
- # ok
244
- Zena::Db.sql_function(function, "#{table('links')}.#{key[2..-1]}")
245
- else
246
- # bad attribute
247
- nil
248
- end
249
- else
250
- # NODE
251
- key, function = parse_sql_function_in_field(field)
252
- if context == :filter
253
- if map_def = @@filter_fields[key]
254
- if table_def = map_def[:table]
255
- table_to_use = needs_table(*table_def)
256
- else
257
- table_to_use = table_name
258
- end
259
- Zena::Db.sql_function(function, "#{table_to_use}.#{map_def[:key]}")
260
- elsif (Node.safe_method_type([key]) && Node.column_names.include?(key))
261
- Zena::Db.sql_function(function, "#{table_name}.#{key}")
262
- elsif key =~ /^(.*)_ids?$/
263
- # tag_id = 33 ===> join links as lk, nodes as tt .......
264
- rel = $1
265
-
266
- if RelationProxy.find_by_role(rel.singularize)
267
- add_table('jnode', 'nodes')
268
- join_relation(rel, nil, table('jnode'), table('nodes'))
269
- "#{table('jnode')}.zip"
270
- else
271
- nil
272
- end
273
- else
274
- nil
275
- end
276
- else
277
- if ['id', 'parent_id','project_id','section_id'].include?(key) || (Node.safe_method_type([key]) && Node.column_names.include?(key))
278
- Zena::Db.sql_function(function, "#{table_name}.#{key}")
279
- else
280
- # bad attribute
281
- nil
282
- end
283
- end
284
- end
285
- end
286
-
287
- def valid_field?(table_name, fld)
288
- # FIXME: security !
289
- true
290
- end
291
-
292
- def map_attr(fld, env = :sql)
293
- case fld
294
- when 'project_id', 'section_id', 'discussion_id'
295
- @uses_node_name = true
296
- insert_bind("#{@node_name}.get_#{fld}")
297
- when 'id', 'parent_id'
298
- @uses_node_name = true
299
- insert_bind("#{@node_name}.#{fld}")
300
- else
301
- # type = Node.safe_method_type([fld])
302
- # type[:method] ?
303
-
304
- # bad parameter
305
- @errors << "invalid parameter '#{fld}'"
306
- "0"
307
- end
308
- end
309
-
310
- def parse_paginate_clause(paginate)
311
- return @offset unless paginate
312
- if !@limit
313
- # TODO: raise error ?
314
- @errors << "invalid paginate clause '#{paginate}' (used without limit)"
315
- nil
316
- elsif (fld = map_literal("[param:#{paginate}]", :ruby)) && (page_size = @limit[/ LIMIT (\d+)/,1])
317
- @page_size = [2,page_size.to_i].max
318
- " OFFSET #{insert_bind("((#{fld}.to_i > 0 ? #{fld}.to_i : 1)-1)*#{page_size.to_i}")}"
319
- else
320
- @errors << "invalid paginate clause '#{paginate}'"
321
- nil
322
- end
323
- end
324
-
325
- # When a field is defined as log_at:year, return [log_at, year].
326
- def parse_sql_function_in_field(field)
327
- if field =~ /\A(\w+):(\w+)\Z/
328
- if ['year'].include?($2)
329
- [$1,$2]
330
- else
331
- [$1]
332
- end
333
- else
334
- [field]
335
- end
336
- end
337
-
338
- def parse_raw_filters(filters)
339
- return unless filters
340
- filters.each do |f|
341
- all_ok = true
342
- filter = f.gsub(/TABLE_NAME(\[(\w+)\]|)/) do
343
- if $2
344
- if @table_counter[$2]
345
- table($2)
346
- else
347
- all_ok = false
348
- @errors << "invalid table_name '#{$2}' in raw filter"
349
- @errors.uniq!
350
- end
351
- else
352
- table
353
- end
354
- end
355
- @where << filter if all_ok
356
- end
357
- end
358
-
359
- def dyn_value(table_name, key, context)
360
- @dyn_keys[table_name] ||= {}
361
- @dyn_keys[table_name][key] ||= begin
362
- needs_table('nodes', 'versions', "TABLE1.id = TABLE2.node_id")
363
- dtable = needs_join_table('versions', 'LEFT', 'dyn_attributes', "TABLE1.id = TABLE2.owner_id AND TABLE2.key = #{quote(key)}", "versions=dyn_attributes=#{key}")
364
- "#{dtable}.value"
365
- end
366
- end
367
-
368
- def parse_custom_query_argument(key, value)
369
- return nil unless value
370
- super.gsub(/(RELATION_ID|NODE_ATTR)\(([^)]+)\)/) do
371
- type, value = $1, $2
372
- if type == 'RELATION_ID'
373
- role = value
374
- if rel = RelationProxy.find_by_role(role.singularize)
375
- rel[:id]
376
- else
377
- @errors << "could not find Relation '#{role}' in custom query"
378
- '-1'
379
- end
380
- elsif type == 'NODE_ATTR'
381
- attribute = value
382
- if Node.safe_method_type([attribute])
383
- insert_bind("#{@node_name}.#{attribute}")
384
- else
385
- @errors << "cannot read attribute '#{attribute}' in custom query"
386
- '-1'
387
- end
388
- end
389
- end.gsub(/NODE_ID/) do
390
- @uses_node_name = true
391
- insert_bind("#{@node_name}.id")
392
- end
393
- end
394
-
395
- def extract_custom_query(list)
396
- super.singularize
397
- end
398
-
399
- def quote(literal)
400
- Node.connection.quote(literal)
401
- end
402
- end
403
-
404
- module Zena
405
- module Use
406
- module NodeQueryFinders
407
- module AddUseNodeQueryMethod
408
- def self.included(base)
409
- # create class methods
410
- base.extend AddUseNodeQueryMethodImpl
411
- end
412
- end
413
-
414
- module AddUseNodeQueryMethodImpl
415
- def use_node_query
416
- class_eval do
417
- include Zena::Use::NodeQueryFinders::InstanceMethods
418
- class << self
419
- include Zena::Use::NodeQueryFinders::ClassMethods
420
- end
421
- end
422
- end
423
- end
424
-
425
- module ClassMethods
426
-
427
- # Return an sql query string that will be used by 'do_find':
428
- # build_find(:all, PSEUDO_SQL, node_name) => "SELECT * FROM nodes WHERE nodes.parent_id = #{@node[:id]} AND ..."
429
- # PSEUDO_SQL: what to find in pseudo sql (See NodeQuery for details).
430
- # node_name: contextual variable name
431
- #
432
- # Pseudo sql syntax:
433
- #
434
- # '[CLASS|VCLASS|RELATION] [in [site|section|project|parent]|] [where CLAUSE|] [from SUB_QUERY|] or [PSEUDO_SQL]'
435
- #
436
- # with :
437
- # * CLASS: a native class ('images', 'documents', 'pages', 'projects', ...)
438
- # * VCLASS: a virtual class created by the user ('posts', 'houses', ...)
439
- # * RELATION: a relation defined by the user ('icon_for', 'news', 'calendar', ...)
440
- # * CLAUSE: field = value ('log_at:year = 2005'). You can use parameters, visitor data in clause: 'log_at:year = [param:year]', 'd_assigned = [visitor:name]'. You can only use 'and' in clauses. 'or' is not supported. You can use version and/or dynamic attributes : 'v_comment = super', 'd_priority = low'.
441
- #
442
- # Examples: 'todos in section where d_priority = high and d_assigned = [visitor:name]'
443
- #def build_find(count, pseudo_sql, node_name, raw_filters = nil, ignore_warnings = false, ref_date = nil)
444
- def build_find(count, pseudo_sql, opts = {})
445
- if count == :first
446
- opts[:limit] = 1
447
- end
448
- NodeQuery.new(pseudo_sql, opts.merge(:custom_query_group => visitor.site.host))
449
- end
450
- end # ClassMethods
451
-
452
-
453
- module InstanceMethods
454
-
455
- # Find a node and propagate visitor
456
- def do_find(count, query, ignore_source = false, klass = Node)
457
- return nil if query.empty?
458
- return nil if (new_record? && !ignore_source) # do not run query (might contain nil id)
459
-
460
- case count
461
- when :all
462
- res = klass.find_by_sql(query)
463
- secure_result(Node,res)
464
- when :first
465
- res = klass.find_by_sql(query).first
466
- secure_result(Node,res)
467
- when :count
468
- klass.count_by_sql(query)
469
- else
470
- nil
471
- end
472
- end
473
-
474
- # Find related nodes.
475
- # See Node#build_find for details on the options available.
476
- # TODO: replace with rubyless translate ? Is this thing really used anyway ?
477
- def find(count, rel, opts = {})
478
- rel = [rel] if rel.kind_of?(String)
479
-
480
- if !opts[:skip_rubyless] && rel.size == 1 && type = RubyLess::SafeClass.safe_method_type_for(self.class, [rel.first])
481
- self.send(type[:method])
482
- else
483
- query = Node.build_find(count, rel, :node_name => 'self')
484
- if query.valid?
485
- do_find(count, eval(query.to_s))
486
- else
487
- nil
488
- end
489
- end
490
- end
491
- end # InstanceMethods
492
- end # NodeQueryFinders
493
- end # Use
494
- end # Zena