volt 0.8.18 → 0.8.19

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/Readme.md +2 -1
  4. data/VERSION +1 -1
  5. data/app/volt/controllers/notices_controller.rb +12 -2
  6. data/app/volt/models/user.rb +34 -1
  7. data/app/volt/tasks/store_tasks.rb +26 -23
  8. data/app/volt/tasks/user_tasks.rb +26 -2
  9. data/app/volt/views/notices/index.html +8 -6
  10. data/lib/volt.rb +47 -1
  11. data/lib/volt/cli/asset_compile.rb +24 -42
  12. data/lib/volt/config.rb +24 -14
  13. data/lib/volt/controllers/model_controller.rb +6 -1
  14. data/lib/volt/data_stores/mongo_driver.rb +7 -3
  15. data/lib/volt/models.rb +3 -0
  16. data/lib/volt/models/array_model.rb +21 -3
  17. data/lib/volt/models/model.rb +109 -48
  18. data/lib/volt/models/model_hash_behaviour.rb +25 -8
  19. data/lib/volt/models/persistors/array_store.rb +5 -2
  20. data/lib/volt/models/persistors/cookies.rb +91 -0
  21. data/lib/volt/models/persistors/model_store.rb +52 -14
  22. data/lib/volt/models/persistors/query/query_listener.rb +4 -1
  23. data/lib/volt/models/persistors/query/query_listener_pool.rb +1 -1
  24. data/lib/volt/models/persistors/store_state.rb +5 -5
  25. data/lib/volt/models/validations.rb +87 -18
  26. data/lib/volt/models/validators/length_validator.rb +1 -1
  27. data/lib/volt/models/validators/presence_validator.rb +1 -1
  28. data/lib/volt/models/validators/unique_validator.rb +28 -0
  29. data/lib/volt/page/bindings/template_binding.rb +2 -2
  30. data/lib/volt/page/page.rb +4 -0
  31. data/lib/volt/page/tasks.rb +2 -2
  32. data/lib/volt/reactive/computation.rb +2 -0
  33. data/lib/volt/reactive/hash_dependency.rb +1 -3
  34. data/lib/volt/reactive/reactive_array.rb +7 -0
  35. data/lib/volt/reactive/reactive_hash.rb +17 -0
  36. data/lib/volt/server.rb +1 -1
  37. data/lib/volt/server/component_templates.rb +3 -6
  38. data/lib/volt/server/html_parser/view_scope.rb +1 -1
  39. data/lib/volt/server/rack/component_code.rb +3 -2
  40. data/lib/volt/server/rack/component_paths.rb +15 -0
  41. data/lib/volt/server/rack/index_files.rb +1 -1
  42. data/lib/volt/tasks/dispatcher.rb +26 -12
  43. data/lib/volt/tasks/task_handler.rb +17 -15
  44. data/spec/apps/kitchen_sink/app/main/config/routes.rb +3 -0
  45. data/spec/apps/kitchen_sink/app/main/controllers/main_controller.rb +26 -0
  46. data/spec/apps/kitchen_sink/app/main/controllers/users_test_controller.rb +27 -0
  47. data/spec/apps/kitchen_sink/app/main/views/main/cookie_test.html +25 -0
  48. data/spec/apps/kitchen_sink/app/main/views/main/flash.html +13 -0
  49. data/spec/apps/kitchen_sink/app/main/views/main/main.html +3 -0
  50. data/spec/apps/kitchen_sink/app/main/views/users_test/index.html +22 -0
  51. data/spec/apps/kitchen_sink/config/app.rb +3 -0
  52. data/spec/integration/bindings_spec.rb +1 -1
  53. data/spec/integration/cookies_spec.rb +48 -0
  54. data/spec/integration/flash_spec.rb +32 -0
  55. data/spec/models/model_spec.rb +3 -4
  56. data/spec/models/validations_spec.rb +13 -13
  57. data/spec/tasks/dispatcher_spec.rb +1 -1
  58. data/templates/project/config/app.rb.tt +6 -1
  59. data/templates/project/{public → config/base_page}/index.html +0 -0
  60. data/volt.gemspec +4 -0
  61. metadata +47 -3
@@ -43,7 +43,8 @@ module Volt
43
43
 
44
44
  inst.model = @default_model if @default_model
45
45
 
46
- inst.initialize(*args, &block)
46
+ # In MRI initialize is private for some reason, so call it with send
47
+ inst.send(:initialize, *args, &block)
47
48
 
48
49
  inst
49
50
  end
@@ -87,6 +88,10 @@ module Volt
87
88
  $page.local_store
88
89
  end
89
90
 
91
+ def cookies
92
+ $page.cookies
93
+ end
94
+
90
95
  def url
91
96
  $page.url
92
97
  end
@@ -4,9 +4,13 @@ module Volt
4
4
  class DataStore
5
5
  class MongoDriver
6
6
  def self.fetch
7
- @@mongo_db ||= Mongo::MongoClient.new(Volt.config.db_host, Volt.config.db_path)
8
- @@db ||= @@mongo_db.db(Volt.config.db_name)
9
-
7
+ if Volt.config.db_uri.present?
8
+ @@mongo_db ||= Mongo::MongoClient.from_uri(Volt.config.db_uri)
9
+ @@db ||= @@mongo_db.db(Volt.config.db_uri.split('/').last || Volt.config.db_name)
10
+ else
11
+ @@mongo_db ||= Mongo::MongoClient.new(Volt.config.db_host, Volt.config.db_path)
12
+ @@db ||= @@mongo_db.db(Volt.config.db_name)
13
+ end
10
14
  @@db
11
15
  end
12
16
  end
data/lib/volt/models.rb CHANGED
@@ -5,6 +5,9 @@ require 'volt/models/persistors/store_factory'
5
5
  require 'volt/models/persistors/array_store'
6
6
  require 'volt/models/persistors/model_store'
7
7
  require 'volt/models/persistors/params'
8
+ if RUBY_PLATFORM == 'opal'
9
+ require 'volt/models/persistors/cookies'
10
+ end
8
11
  require 'volt/models/persistors/flash'
9
12
  require 'volt/models/persistors/local_store'
10
13
  if RUBY_PLATFORM == 'opal'
@@ -66,12 +66,30 @@ module Volt
66
66
 
67
67
  super(model)
68
68
 
69
- @persistor.added(model, @array.size - 1) if @persistor
69
+ if @persistor
70
+ @persistor.added(model, @array.size - 1)
71
+ else
72
+ nil
73
+ end
74
+ end
70
75
 
71
- model
76
+ # Works like << except it returns a promise
77
+ def append(model)
78
+ promise, model = self.send(:<<, model)
79
+
80
+ # Return a promise if one doesn't exist
81
+ promise ||= Promise.new.resolve(model)
82
+
83
+ promise
72
84
  end
73
85
 
74
- alias_method :append, :<<
86
+
87
+ # Find one does a query, but only returns the first item or
88
+ # nil if there is no match. Unlike #find, #find_one does not
89
+ # return another cursor that you can call .then on.
90
+ def find_one(*args, &block)
91
+ find(*args, &block).limit(1)[0]
92
+ end
75
93
 
76
94
  # Make sure it gets wrapped
77
95
  def inject(*args)
@@ -22,6 +22,7 @@ module Volt
22
22
 
23
23
  def initialize(attributes = {}, options = {}, initial_state = nil)
24
24
  @deps = HashDependency.new
25
+ @size_dep = Dependency.new
25
26
  self.options = options
26
27
 
27
28
  send(:attributes=, attributes, true)
@@ -60,16 +61,20 @@ module Volt
60
61
  if attrs
61
62
  # Assign id first
62
63
  id = attrs.delete(:_id)
63
- self._id = id if id
64
64
 
65
- # Assign each attribute using setters
66
- attrs.each_pair do |key, value|
67
- if self.respond_to?(:"#{key}=")
68
- # If a method without an underscore is defined, call that.
69
- send(:"#{key}=", value)
70
- else
71
- # Otherwise, use the _ version
72
- send(:"_#{key}=", value)
65
+ # When doing a mass-assign, we don't save until the end.
66
+ Model.nosave do
67
+ self._id = id if id
68
+
69
+ # Assign each attribute using setters
70
+ attrs.each_pair do |key, value|
71
+ if self.respond_to?(:"#{key}=")
72
+ # If a method without an underscore is defined, call that.
73
+ send(:"#{key}=", value)
74
+ else
75
+ # Otherwise, use the _ version
76
+ send(:"_#{key}=", value)
77
+ end
73
78
  end
74
79
  end
75
80
  else
@@ -111,9 +116,12 @@ module Volt
111
116
 
112
117
  def method_missing(method_name, *args, &block)
113
118
  if method_name[0] == '_'
119
+
120
+ # Remove underscore
121
+ method_name = method_name[1..-1]
114
122
  if method_name[-1] == '='
115
- # Assigning an attribute with =
116
- assign_attribute(method_name, *args, &block)
123
+ # Assigning an attribute without the =
124
+ assign_attribute(method_name[0..-2], *args, &block)
117
125
  else
118
126
  read_attribute(method_name)
119
127
  end
@@ -127,47 +135,71 @@ module Volt
127
135
  def assign_attribute(method_name, *args, &block)
128
136
  self.expand!
129
137
  # Assign, without the =
130
- attribute_name = method_name[1..-2].to_sym
138
+ attribute_name = method_name.to_sym
131
139
 
132
140
  value = args[0]
133
141
 
134
- @attributes[attribute_name] = wrap_value(value, [attribute_name])
142
+ old_value = @attributes[attribute_name]
143
+ new_value = wrap_value(value, [attribute_name])
144
+
145
+ if old_value != new_value
146
+ @attributes[attribute_name] = new_value
147
+
148
+ @deps.changed!(attribute_name)
135
149
 
136
- @deps.changed!(attribute_name)
150
+ if old_value == nil || new_value == nil
151
+ @size_dep.changed!
152
+ end
153
+
154
+ # TODO: Can we make this so it doesn't need to be handled for non store collections
155
+ # (maybe move it to persistor, though thats weird since buffers don't have a persistor)
156
+ clear_server_errors(attribute_name) if @server_errors
137
157
 
138
- # Let the persistor know something changed
139
- @persistor.changed(attribute_name) if @persistor
158
+
159
+ # Don't save right now if we're in a nosave block
160
+ if !defined?(Thread) || !Thread.current['nosave']
161
+ # Let the persistor know something changed
162
+ @persistor.changed(attribute_name) if @persistor
163
+ end
164
+ end
140
165
  end
141
166
 
142
167
  # When reading an attribute, we need to handle reading on:
143
168
  # 1) a nil model, which returns a wrapped error
144
169
  # 2) reading directly from attributes
145
170
  # 3) trying to read a key that doesn't exist.
146
- def read_attribute(method_name)
171
+ def read_attribute(attr_name)
147
172
  # Reading an attribute, we may get back a nil model.
148
- method_name = method_name.to_sym
173
+ attr_name = attr_name.to_sym
149
174
 
150
- if method_name[0] != '_' && @attributes.nil?
151
- # The method we are calling is on a nil model, return a wrapped
152
- # exception.
153
- return_undefined_method(method_name)
154
- else
155
- attr_name = method_name[1..-1].to_sym
156
- # See if the value is in attributes
157
- value = (@attributes && @attributes[attr_name])
175
+ # Track dependency
176
+ # @deps.depend(attr_name)
158
177
 
178
+ # See if the value is in attributes
179
+ if @attributes && @attributes.key?(attr_name)
159
180
  # Track dependency
160
181
  @deps.depend(attr_name)
161
182
 
162
- if value
163
- # key was in attributes or cache
164
- value
183
+ return @attributes[attr_name]
184
+ else
185
+ new_model = read_new_model(attr_name)
186
+ @attributes ||= {}
187
+ @attributes[attr_name] = new_model
188
+
189
+ # Trigger size change
190
+ # TODO: We can probably improve Computations to just make this work
191
+ # without the delay
192
+ if Volt.in_browser?
193
+ `setImmediate(function() {`
194
+ @size_dep.changed!
195
+ `});`
165
196
  else
166
- new_model = read_new_model(attr_name)
167
- @attributes ||= {}
168
- @attributes[attr_name] = new_model
169
- new_model
197
+ @size_dep.changed!
170
198
  end
199
+
200
+ # Depend on attribute
201
+ @deps.depend(attr_name)
202
+ return new_model
171
203
  end
172
204
  end
173
205
 
@@ -261,32 +293,43 @@ module Volt
261
293
  if save_to
262
294
  if save_to.is_a?(ArrayModel)
263
295
  # Add to the collection
264
- new_model = save_to << attributes
265
-
266
- # Set the buffer's id to track the main model's id
267
- attributes[:_id] = new_model._id
268
- options[:save_to] = new_model
269
-
270
- # TODO: return a promise that resolves if the append works
296
+ promise = save_to.append(attributes)
271
297
  else
272
298
  # We have a saved model
273
- return save_to.assign_attributes(attributes)
299
+ promise = save_to.assign_attributes(attributes)
300
+ end
301
+
302
+ return promise.then do |new_model|
303
+ if new_model
304
+ # Set the buffer's id to track the main model's id
305
+ attributes[:_id] = new_model._id
306
+ options[:save_to] = new_model
307
+ end
308
+
309
+ nil
310
+ end.fail do |errors|
311
+ if errors.is_a?(Hash)
312
+ server_errors.replace(errors)
313
+ end
314
+
315
+ promise_for_errors(errors)
274
316
  end
275
317
  else
276
318
  fail 'Model is not a buffer, can not be saved, modifications should be persisted as they are made.'
277
319
  end
278
-
279
- Promise.new.resolve({})
280
320
  else
281
321
  # Some errors, mark all fields
282
- self.class.validations.keys.each do |key|
283
- mark_field!(key.to_sym)
284
- end
285
-
286
- Promise.new.reject(errors)
322
+ promise_for_errors(errors)
287
323
  end
288
324
  end
289
325
 
326
+ # When errors come in, we mark all fields and return a rejected promise.
327
+ def promise_for_errors(errors)
328
+ mark_all_fields!
329
+
330
+ Promise.new.reject(errors)
331
+ end
332
+
290
333
  # Returns a buffered version of the model
291
334
  def buffer
292
335
  model_path = options[:path]
@@ -312,6 +355,24 @@ module Volt
312
355
  model
313
356
  end
314
357
 
358
+ # Takes a block that when run, changes to models will not save inside of
359
+ if RUBY_PLATFORM == 'opal'
360
+ # Temporary stub for no save on client
361
+ def self.nosave
362
+ yield
363
+ end
364
+ else
365
+ def self.nosave
366
+ previous = Thread.current['nosave']
367
+ Thread.current['nosave'] = true
368
+ begin
369
+ yield
370
+ ensure
371
+ Thread.current['nosave'] = previous
372
+ end
373
+ end
374
+ end
375
+
315
376
  private
316
377
 
317
378
  def setup_buffer(model)
@@ -6,7 +6,9 @@ module Volt
6
6
  def delete(name)
7
7
  name = name.to_sym
8
8
 
9
- value = attributes.delete(name)
9
+ value = @attributes.delete(name)
10
+
11
+ @size_dep.changed!
10
12
  @deps.delete(name)
11
13
 
12
14
  @persistor.removed(name) if @persistor
@@ -14,28 +16,40 @@ module Volt
14
16
  value
15
17
  end
16
18
 
19
+ def size
20
+ @size_dep.depend
21
+ @attributes.size
22
+ end
23
+
24
+ def keys
25
+ @size_dep.depend
26
+ @attributes.keys
27
+ end
28
+
17
29
  def nil?
18
- attributes.nil?
30
+ @attributes.nil?
19
31
  end
20
32
 
21
33
  def empty?
22
- !attributes || attributes.size == 0
34
+ @size_dep.depend
35
+ !@attributes || @attributes.size == 0
23
36
  end
24
37
 
25
38
  def false?
26
- attributes.false?
39
+ @attributes.false?
27
40
  end
28
41
 
29
42
  def true?
30
- attributes.true?
43
+ @attributes.true?
31
44
  end
32
45
 
33
46
  def clear
34
- attributes.each_pair do |key, value|
47
+ @attributes.each_pair do |key, value|
35
48
  @deps.changed!(key)
36
49
  end
37
50
 
38
- attributes.clear
51
+ @attributes.clear
52
+ @size_dep.changed!
39
53
 
40
54
  @persistor.removed(nil) if @persistor
41
55
  end
@@ -46,15 +60,18 @@ module Volt
46
60
 
47
61
  # Convert the model to a hash all of the way down.
48
62
  def to_h
63
+ @size_dep.depend
64
+
49
65
  if @attributes.nil?
50
66
  nil
51
67
  else
52
68
  hash = {}
53
- attributes.each_pair do |key, value|
69
+ @attributes.each_pair do |key, value|
54
70
  hash[key] = deep_unwrap(value)
55
71
  end
56
72
  hash
57
73
  end
58
74
  end
75
+
59
76
  end
60
77
  end
@@ -2,6 +2,7 @@ require 'volt/models/persistors/store'
2
2
  require 'volt/models/persistors/query/query_listener_pool'
3
3
  require 'volt/models/persistors/store_state'
4
4
 
5
+
5
6
  module Volt
6
7
  module Persistors
7
8
  class ArrayStore < Store
@@ -82,6 +83,7 @@ module Volt
82
83
 
83
84
  # Clear out the models data, since we're not listening anymore.
84
85
  def unload_data
86
+ puts "Unload Data"
85
87
  change_state_to :not_loaded
86
88
  @model.clear
87
89
  end
@@ -138,9 +140,10 @@ module Volt
138
140
  # Returns a promise that is resolved/rejected when the query is complete. Any
139
141
  # passed block will be passed to the promises then. Then will be passed the model.
140
142
  def then(&block)
143
+ raise "then must pass a block" unless block
141
144
  promise = Promise.new
142
145
 
143
- promise = promise.then(&block) if block
146
+ promise = promise.then(&block)
144
147
 
145
148
  if @state == :loaded
146
149
  promise.resolve(@model)
@@ -197,7 +200,7 @@ module Volt
197
200
  # method. This should trigger a save.
198
201
  def added(model, index)
199
202
  if model.persistor
200
- # Tell the persistor it was added
203
+ # Tell the persistor it was added, return the promise
201
204
  model.persistor.add_to_collection
202
205
  end
203
206
  end
@@ -0,0 +1,91 @@
1
+ # Use a volt model to persist to cookies.
2
+ # Some code borrowed from: https://github.com/opal/opal-browser/blob/master/opal/browser/cookies.rb
3
+
4
+ require 'volt/models/persistors/base'
5
+
6
+ module Volt
7
+ module Persistors
8
+ # Backs a collection in the local store
9
+ class Cookies < Base
10
+ def read_cookies
11
+ cookies = `document.cookie`
12
+ Hash[cookies.split(';').map do |v|
13
+ parts = v.split('=').map { |p| p = p.strip ; `decodeURIComponent(p)` }
14
+
15
+ # Default to empty if no value
16
+ parts << '' if parts.size == 1
17
+
18
+ parts
19
+ end]
20
+ end
21
+
22
+ def write_cookie(key, value, options={})
23
+ parts = []
24
+
25
+ parts << `encodeURIComponent(key)`
26
+ parts << '='
27
+ parts << `encodeURIComponent(value)`
28
+ parts << '; '
29
+
30
+ parts << 'max-age=' << options[:max_age] << '; ' if options[:max_age]
31
+ if options[:expires]
32
+ expires = options[:expires]
33
+ parts << 'expires=' << `expires.toGMTString()` << '; '
34
+ end
35
+ parts << 'path=' << options[:path] << '; ' if options[:path]
36
+ parts << 'domain=' << options[:domain] << '; ' if options[:domain]
37
+ parts << 'secure' if options[:secure]
38
+
39
+ cookie_val = parts.join
40
+
41
+ `document.cookie = cookie_val`
42
+ end
43
+
44
+ def initialize(model)
45
+ @model = model
46
+ end
47
+
48
+ # Called when a model is added to the collection
49
+ def added(model, index)
50
+ # Save an added cookie
51
+ end
52
+
53
+ def loaded(initial_state = nil)
54
+ # When the main model is first loaded, we pull in the data from the
55
+ # store if it exists
56
+ if !@loaded && @model.path == []
57
+ @loaded = true
58
+
59
+ writing_cookies do
60
+ read_cookies.each_pair do |key, value|
61
+ @model.assign_attribute(key, value)
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ # Callled when an cookies value is changed
68
+ def changed(attribute_name)
69
+ # TODO: Make sure we're only assigning directly, not sub models
70
+ unless $writing_cookies
71
+ value = @model.read_attribute(attribute_name)
72
+
73
+ # Temp, expire in 1 year, going to expand this api
74
+ write_cookie(attribute_name, value.to_s, expires: Time.now + (356 * 24 * 60 * 60))
75
+ end
76
+ end
77
+
78
+ def removed(attribute_name)
79
+ writing_cookies do
80
+ write_cookie(attribute_name, '', expires: Time.now)
81
+ end
82
+ end
83
+
84
+ def writing_cookies
85
+ $writing_cookies = true
86
+ yield
87
+ $writing_cookies = false
88
+ end
89
+ end
90
+ end
91
+ end