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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/Readme.md +2 -1
- data/VERSION +1 -1
- data/app/volt/controllers/notices_controller.rb +12 -2
- data/app/volt/models/user.rb +34 -1
- data/app/volt/tasks/store_tasks.rb +26 -23
- data/app/volt/tasks/user_tasks.rb +26 -2
- data/app/volt/views/notices/index.html +8 -6
- data/lib/volt.rb +47 -1
- data/lib/volt/cli/asset_compile.rb +24 -42
- data/lib/volt/config.rb +24 -14
- data/lib/volt/controllers/model_controller.rb +6 -1
- data/lib/volt/data_stores/mongo_driver.rb +7 -3
- data/lib/volt/models.rb +3 -0
- data/lib/volt/models/array_model.rb +21 -3
- data/lib/volt/models/model.rb +109 -48
- data/lib/volt/models/model_hash_behaviour.rb +25 -8
- data/lib/volt/models/persistors/array_store.rb +5 -2
- data/lib/volt/models/persistors/cookies.rb +91 -0
- data/lib/volt/models/persistors/model_store.rb +52 -14
- data/lib/volt/models/persistors/query/query_listener.rb +4 -1
- data/lib/volt/models/persistors/query/query_listener_pool.rb +1 -1
- data/lib/volt/models/persistors/store_state.rb +5 -5
- data/lib/volt/models/validations.rb +87 -18
- data/lib/volt/models/validators/length_validator.rb +1 -1
- data/lib/volt/models/validators/presence_validator.rb +1 -1
- data/lib/volt/models/validators/unique_validator.rb +28 -0
- data/lib/volt/page/bindings/template_binding.rb +2 -2
- data/lib/volt/page/page.rb +4 -0
- data/lib/volt/page/tasks.rb +2 -2
- data/lib/volt/reactive/computation.rb +2 -0
- data/lib/volt/reactive/hash_dependency.rb +1 -3
- data/lib/volt/reactive/reactive_array.rb +7 -0
- data/lib/volt/reactive/reactive_hash.rb +17 -0
- data/lib/volt/server.rb +1 -1
- data/lib/volt/server/component_templates.rb +3 -6
- data/lib/volt/server/html_parser/view_scope.rb +1 -1
- data/lib/volt/server/rack/component_code.rb +3 -2
- data/lib/volt/server/rack/component_paths.rb +15 -0
- data/lib/volt/server/rack/index_files.rb +1 -1
- data/lib/volt/tasks/dispatcher.rb +26 -12
- data/lib/volt/tasks/task_handler.rb +17 -15
- data/spec/apps/kitchen_sink/app/main/config/routes.rb +3 -0
- data/spec/apps/kitchen_sink/app/main/controllers/main_controller.rb +26 -0
- data/spec/apps/kitchen_sink/app/main/controllers/users_test_controller.rb +27 -0
- data/spec/apps/kitchen_sink/app/main/views/main/cookie_test.html +25 -0
- data/spec/apps/kitchen_sink/app/main/views/main/flash.html +13 -0
- data/spec/apps/kitchen_sink/app/main/views/main/main.html +3 -0
- data/spec/apps/kitchen_sink/app/main/views/users_test/index.html +22 -0
- data/spec/apps/kitchen_sink/config/app.rb +3 -0
- data/spec/integration/bindings_spec.rb +1 -1
- data/spec/integration/cookies_spec.rb +48 -0
- data/spec/integration/flash_spec.rb +32 -0
- data/spec/models/model_spec.rb +3 -4
- data/spec/models/validations_spec.rb +13 -13
- data/spec/tasks/dispatcher_spec.rb +1 -1
- data/templates/project/config/app.rb.tt +6 -1
- data/templates/project/{public → config/base_page}/index.html +0 -0
- data/volt.gemspec +4 -0
- metadata +47 -3
@@ -43,7 +43,8 @@ module Volt
|
|
43
43
|
|
44
44
|
inst.model = @default_model if @default_model
|
45
45
|
|
46
|
-
|
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
|
-
|
8
|
-
|
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
|
-
|
69
|
+
if @persistor
|
70
|
+
@persistor.added(model, @array.size - 1)
|
71
|
+
else
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
end
|
70
75
|
|
71
|
-
|
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
|
-
|
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)
|
data/lib/volt/models/model.rb
CHANGED
@@ -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
|
-
#
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
#
|
72
|
-
|
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
|
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
|
138
|
+
attribute_name = method_name.to_sym
|
131
139
|
|
132
140
|
value = args[0]
|
133
141
|
|
134
|
-
|
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
|
-
|
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
|
-
|
139
|
-
|
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(
|
171
|
+
def read_attribute(attr_name)
|
147
172
|
# Reading an attribute, we may get back a nil model.
|
148
|
-
|
173
|
+
attr_name = attr_name.to_sym
|
149
174
|
|
150
|
-
|
151
|
-
|
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
|
-
|
163
|
-
|
164
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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)
|
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
|