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
@@ -25,6 +25,8 @@ module Volt
25
25
  def add_to_collection
26
26
  @in_collection = true
27
27
  ensure_setup
28
+
29
+ # Call changed, return the promise
28
30
  changed
29
31
  end
30
32
 
@@ -84,14 +86,12 @@ module Volt
84
86
  fail 'Attempting to save model directly on store.'
85
87
  else
86
88
  if RUBY_PLATFORM == 'opal'
87
- StoreTasks.save(collection, path, self_attributes).then do |errors|
88
- if errors.size == 0
89
- promise.resolve(nil)
90
- else
91
- promise.reject(errors)
92
- end
93
- end
89
+ @save_promises ||= []
90
+ @save_promises << promise
91
+
92
+ queue_client_save
94
93
  else
94
+ puts "Save to DB"
95
95
  errors = save_to_db!(self_attributes)
96
96
  if errors.size == 0
97
97
  promise.resolve(nil)
@@ -101,9 +101,40 @@ module Volt
101
101
  end
102
102
  end
103
103
  end
104
+
104
105
  promise
105
106
  end
106
107
 
108
+ def queue_client_save
109
+ `
110
+ if (!self.saveTimer) {
111
+ self.saveTimer = setImmediate(self.$run_save.bind(self));
112
+ }
113
+ `
114
+ end
115
+
116
+ # Run save is called on the client side after a queued setImmediate. It does the
117
+ # saving on the front-end. Adding a setImmediate allows multiple changes to be
118
+ # batched together.
119
+ def run_save
120
+ # Clear the save timer
121
+ `
122
+ clearImmediate(self.saveTimer);
123
+ delete self.saveTimer;
124
+ `
125
+
126
+ StoreTasks.save(collection, @model.path, self_attributes).then do
127
+ save_promises = @save_promises
128
+ @save_promises = nil
129
+ save_promises.each {|promise| promise.resolve(nil) }
130
+ end.fail do |errors|
131
+ save_promises = @save_promises
132
+ @save_promises = nil
133
+ save_promises.each {|promise| promise.reject(errors) }
134
+ end
135
+
136
+ end
137
+
107
138
  def event_added(event, first, first_for_event)
108
139
  if first_for_event && event == :changed
109
140
  ensure_setup
@@ -113,12 +144,14 @@ module Volt
113
144
  # Update the models based on the id/identity map. Usually these requests
114
145
  # will come from the backend.
115
146
  def self.changed(model_id, data)
116
- model = @@identity_map.lookup(model_id)
147
+ Model.nosave do
148
+ model = @@identity_map.lookup(model_id)
117
149
 
118
- if model
119
- data.each_pair do |key, value|
120
- if key != :_id
121
- model.send(:"_#{key}=", value)
150
+ if model
151
+ data.each_pair do |key, value|
152
+ if key != :_id
153
+ model.send(:"_#{key}=", value)
154
+ end
122
155
  end
123
156
  end
124
157
  end
@@ -147,6 +180,11 @@ module Volt
147
180
 
148
181
  # Do the actual writing of data to the database, only runs on the backend.
149
182
  def save_to_db!(values)
183
+ # Check to make sure the model has no validation errors.
184
+ errors = @model.errors
185
+ return errors if errors.present?
186
+
187
+ # Passed, save it
150
188
  id = values[:_id]
151
189
 
152
190
  # Try to create
@@ -166,8 +204,8 @@ module Volt
166
204
  end
167
205
  end
168
206
 
169
- # puts "Update Collection: #{collection.inspect} - #{values.inspect}"
170
- QueryTasks.live_query_pool.updated_collection(collection.to_s, Thread.current['from_channel'])
207
+ puts "Update Collection: #{collection.inspect} - #{values.inspect} -- #{Thread.current['in_channel'].inspect}"
208
+ QueryTasks.live_query_pool.updated_collection(collection.to_s, Thread.current['in_channel'])
171
209
  return {}
172
210
  end
173
211
  end
@@ -33,7 +33,7 @@ module Volt
33
33
  store.change_state_to(:loaded)
34
34
  end
35
35
  end.fail do |err|
36
- puts "Err: #{err.inspect}"
36
+ puts "Error adding listener: #{err.inspect}"
37
37
  end
38
38
  end
39
39
 
@@ -47,6 +47,8 @@ module Volt
47
47
  @stores.first.model.each_with_index do |item, index|
48
48
  store.add(index, item.to_h)
49
49
  end
50
+
51
+ store.change_state_to(:loaded)
50
52
  else
51
53
  # First time we've added a store, setup the listener and get
52
54
  # the initial data.
@@ -84,6 +86,7 @@ module Volt
84
86
 
85
87
  def changed(model_id, data)
86
88
  $loading_models = true
89
+ puts "new data: #{data.inspect}"
87
90
  Persistors::ModelStore.changed(model_id, data)
88
91
  $loading_models = false
89
92
  end
@@ -11,7 +11,7 @@ module Volt
11
11
  puts '--- Running Queries ---'
12
12
 
13
13
  @pool.each_pair do |table, query_hash|
14
- query_hash.keys.each do |query|
14
+ query_hash.each_key do |query|
15
15
  puts "#{table}: #{query.inspect}"
16
16
  end
17
17
  end
@@ -10,19 +10,18 @@ module Volt
10
10
  def state
11
11
  @state_dep ||= Dependency.new
12
12
  @state_dep.depend
13
+
13
14
  @state
14
15
  end
15
16
 
16
17
  # Called from the QueryListener when the data is loaded
17
- def change_state_to(new_state, skip_trigger = false)
18
+ def change_state_to(new_state)
18
19
  old_state = @state
19
20
  @state = new_state
20
21
 
21
22
  # Trigger changed on the 'state' method
22
- unless skip_trigger
23
- if old_state != @state
24
- @state_dep.changed! if @state_dep
25
- end
23
+ if old_state != @state
24
+ @state_dep.changed! if @state_dep
26
25
  end
27
26
 
28
27
  if @state == :loaded && @fetch_promises
@@ -30,6 +29,7 @@ module Volt
30
29
  @fetch_promises.compact.each { |fp| fp.resolve(@model) }
31
30
  @fetch_promises = nil
32
31
 
32
+ # puts "STOP LIST---------"
33
33
  stop_listening
34
34
  end
35
35
  end
@@ -1,17 +1,33 @@
1
1
  # require 'volt/models/validations/errors'
2
2
  require 'volt/models/validators/length_validator'
3
3
  require 'volt/models/validators/presence_validator'
4
+ require 'volt/models/validators/unique_validator'
4
5
 
5
6
  module Volt
6
7
  # Include in any class to get validation logic
7
8
  module Validations
8
9
  module ClassMethods
9
- def validate(field_name, options)
10
- @validations ||= {}
11
- @validations[field_name] = options
10
+ def validate(field_name=nil, options=nil, &block)
11
+ if block
12
+ if field_name || options
13
+ raise "validate should be passed a field name and options or a block, not both."
14
+ end
15
+ @custom_validations ||= []
16
+ @custom_validations << block
17
+ else
18
+ @validations ||= {}
19
+ @validations[field_name] = options
20
+ end
21
+ end
22
+
23
+ # TODO: For some reason attr_reader on a class doesn't work in Opal
24
+ def validations
25
+ @validations
12
26
  end
13
27
 
14
- attr_reader :validations
28
+ def custom_validations
29
+ @custom_validations
30
+ end
15
31
  end
16
32
 
17
33
  def self.included(base)
@@ -20,7 +36,7 @@ module Volt
20
36
 
21
37
  # Once a field is ready, we can use include_in_errors! to start
22
38
  # showing its errors.
23
- def mark_field!(field_name, trigger_changed = true)
39
+ def mark_field!(field_name)
24
40
  marked_fields[field_name] = true
25
41
  end
26
42
 
@@ -28,31 +44,69 @@ module Volt
28
44
  @marked_fields ||= ReactiveHash.new
29
45
  end
30
46
 
47
+
48
+ # Marks all fields, useful for when a model saves.
49
+ def mark_all_fields!
50
+ validations = self.class.validations
51
+ if validations
52
+ validations.each_key do |key|
53
+ mark_field!(key.to_sym)
54
+ end
55
+ end
56
+ end
57
+
58
+
31
59
  def marked_errors
32
60
  errors(true)
33
61
  end
34
62
 
63
+ # server errors are errors that come back from the server when we save!
64
+ # Any changes to the associated fields will clear the error until another
65
+ # save!
66
+ def server_errors
67
+ @server_errors ||= ReactiveHash.new
68
+ end
69
+
70
+ # When a field is changed, we want to clear any errors from the server
71
+ def clear_server_errors(key)
72
+ @server_errors.delete(key)
73
+ end
74
+
35
75
  # TODO: Errors is being called for any validation change. We should have errors return a
36
76
  # hash like object that only calls the validation for each one.
37
77
  def errors(marked_only = false)
38
78
  errors = {}
39
79
 
40
- validations = self.class.validations
80
+ # Merge into errors, combining any error arrays
81
+ merge = proc do |new_errors|
82
+ errors.merge!(new_errors) do |key, new_val, old_val|
83
+ new_val + old_val
84
+ end
85
+ end
86
+
87
+ errors = run_validations(errors, merge, marked_only)
41
88
 
89
+ # See if any server errors are in place and merge them in if they are
90
+ if Volt.client?
91
+ errors = merge.call(server_errors.to_h)
92
+ end
93
+
94
+ errors = run_custom_validations(errors, merge)
95
+
96
+ errors
97
+ end
98
+
99
+ private
100
+
101
+ # Runs through each of the normal validations.
102
+ def run_validations(errors, merge, marked_only)
103
+ validations = self.class.validations
42
104
  if validations
43
- # Merge into errors, combining any error arrays
44
- merge = proc do |new_errors|
45
- errors.merge!(new_errors) do |key, new_val, old_val|
46
- new_val + old_val
47
- end
48
- end
49
105
 
50
106
  # Run through each validation
51
107
  validations.each_pair do |field_name, options|
52
- if marked_only
53
- # When marked only, skip any validations on non-marked fields
54
- next unless marked_fields[field_name]
55
- end
108
+ # When marked only, skip any validations on non-marked fields
109
+ next if marked_only && !marked_fields[field_name]
56
110
 
57
111
  options.each_pair do |validation, args|
58
112
  # Call the specific validator, then merge the results back
@@ -68,10 +122,25 @@ module Volt
68
122
  end
69
123
  end
70
124
 
71
- errors
125
+ return errors
126
+ end
127
+
128
+ def run_custom_validations(errors, merge)
129
+ # Call all of the custom validations
130
+ custom_validations = self.class.custom_validations
131
+ if custom_validations
132
+ custom_validations.each do |custom_validation|
133
+ # Run the validator in the context of the model
134
+ result = instance_eval(&custom_validation)
135
+ if result
136
+ errors = merge.call(result)
137
+ end
138
+ end
139
+ end
140
+
141
+ return errors
72
142
  end
73
143
 
74
- private
75
144
 
76
145
  # calls the validate method on the class, passing the right arguments.
77
146
  def validate_with(merge, klass, field_name, args)
@@ -2,7 +2,7 @@ module Volt
2
2
  class LengthValidator
3
3
  def self.validate(model, field_name, args)
4
4
  errors = {}
5
- value = model.send(field_name)
5
+ value = model.read_attribute(field_name)
6
6
 
7
7
  if args.is_a?(Fixnum)
8
8
  min = args
@@ -2,7 +2,7 @@ module Volt
2
2
  class PresenceValidator
3
3
  def self.validate(model, field_name, args)
4
4
  errors = {}
5
- value = model.send(field_name)
5
+ value = model.read_attribute(field_name)
6
6
  if !value || value.blank?
7
7
  if args.is_a?(Hash) && args[:message]
8
8
  message = args[:message]
@@ -0,0 +1,28 @@
1
+ module Volt
2
+ class UniqueValidator
3
+ def self.validate(model, field_name, args)
4
+ errors = {}
5
+
6
+ if RUBY_PLATFORM != 'opal'
7
+ if args
8
+ value = model.read_attribute(field_name)
9
+
10
+ query = {}
11
+ # Check to see if any other documents have this value.
12
+ query[field_name.to_s] = value
13
+ query['_id'] = {'$ne' => model._id}
14
+
15
+ # Check if the value is taken
16
+ # TODO: need a way to handle scope for unique
17
+ if $page.store.send(:"_#{model.path[-2]}").find(query).size > 0
18
+ message = (args.is_a?(Hash) && args[:message]) || "is already taken"
19
+
20
+ errors[field_name] = [message]
21
+ end
22
+ end
23
+ end
24
+
25
+ errors
26
+ end
27
+ end
28
+ end
@@ -53,7 +53,7 @@ module Volt
53
53
  # The defaults are as follows:
54
54
  # 1. component - main
55
55
  # 2. controller - main
56
- # 3. view - main
56
+ # 3. view - index
57
57
  # 4. section - body
58
58
  def path_for_template(lookup_path, force_section = nil)
59
59
  parts = lookup_path.split('/')
@@ -243,7 +243,7 @@ module Volt
243
243
  action = controller_path[-1]
244
244
 
245
245
  # Get the constant parts
246
- parts = controller_path[0..-2].map { |v| v.gsub('-', '_').camelize }
246
+ parts = controller_path[0..-2].map { |v| v.tr('-', '_').camelize }
247
247
 
248
248
  # Home doesn't get namespaced
249
249
  if parts.first == 'Main'
@@ -86,6 +86,10 @@ module Volt
86
86
  @local_store ||= Model.new({}, persistor: Persistors::LocalStore)
87
87
  end
88
88
 
89
+ def cookies
90
+ @cookies ||= Model.new({}, persistor: Persistors::Cookies)
91
+ end
92
+
89
93
  def tasks
90
94
  @tasks ||= Tasks.new(self)
91
95
  end
@@ -12,7 +12,7 @@ module Volt
12
12
  end
13
13
  end
14
14
 
15
- def call(class_name, method_name, *args)
15
+ def call(class_name, method_name, meta_data, *args)
16
16
  promise_id = @promise_id
17
17
  @promise_id += 1
18
18
 
@@ -22,7 +22,7 @@ module Volt
22
22
 
23
23
  # TODO: Timeout on these callbacks
24
24
 
25
- @page.channel.send_message([promise_id, class_name, method_name, *args])
25
+ @page.channel.send_message([promise_id, class_name, method_name, meta_data, *args])
26
26
 
27
27
  promise
28
28
  end
@@ -56,6 +56,8 @@ module Volt
56
56
  if Volt.in_browser?
57
57
  self.class.queue_flush!
58
58
  end
59
+
60
+ # If we are not in the browser, the user must manually flush
59
61
  end
60
62
 
61
63
  invalidations = @invalidations
@@ -13,14 +13,12 @@ module Volt
13
13
  end
14
14
 
15
15
  def delete(key)
16
- dep = @hash_depedencies[key]
16
+ dep = @hash_depedencies.delete(key)
17
17
 
18
18
  if dep
19
19
  dep.changed!
20
20
  dep.remove
21
21
  end
22
-
23
- @hash_depedencies.delete(key)
24
22
  end
25
23
 
26
24
  def changed_all!