volt 0.8.18 → 0.8.19

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 (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