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
@@ -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!