volt 0.8.27.beta6 → 0.8.27.beta7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -3
  3. data/VERSION +1 -1
  4. data/app/volt/models/user.rb +1 -1
  5. data/app/volt/tasks/query_tasks.rb +2 -2
  6. data/app/volt/tasks/store_tasks.rb +14 -4
  7. data/app/volt/tasks/user_tasks.rb +1 -1
  8. data/lib/volt/controllers/http_controller.rb +60 -0
  9. data/lib/volt/controllers/model_controller.rb +5 -1
  10. data/lib/volt/extra_core/string.rb +6 -0
  11. data/lib/volt/models/array_model.rb +6 -2
  12. data/lib/volt/models/associations.rb +1 -1
  13. data/lib/volt/models/buffer.rb +14 -3
  14. data/lib/volt/models/model.rb +28 -60
  15. data/lib/volt/models/permissions.rb +4 -4
  16. data/lib/volt/reactive/computation.rb +15 -15
  17. data/lib/volt/reactive/reactive_array.rb +1 -0
  18. data/lib/volt/router/routes.rb +67 -27
  19. data/lib/volt/server.rb +37 -6
  20. data/lib/volt/server/component_templates.rb +2 -2
  21. data/lib/volt/server/rack/http_request.rb +50 -0
  22. data/lib/volt/server/rack/http_resource.rb +41 -0
  23. data/lib/volt/server/rack/http_response_header.rb +33 -0
  24. data/lib/volt/server/rack/http_response_renderer.rb +41 -0
  25. data/lib/volt/spec/setup.rb +4 -2
  26. data/lib/volt/tasks/dispatcher.rb +7 -7
  27. data/lib/volt/tasks/task_handler.rb +1 -1
  28. data/lib/volt/volt/users.rb +12 -6
  29. data/spec/apps/kitchen_sink/app/main/config/routes.rb +18 -10
  30. data/spec/apps/kitchen_sink/app/main/controllers/main_controller.rb +2 -2
  31. data/spec/apps/kitchen_sink/app/main/controllers/server/simple_http_controller.rb +15 -0
  32. data/spec/apps/kitchen_sink/app/main/controllers/upload_controller.rb +22 -0
  33. data/spec/apps/kitchen_sink/app/main/views/main/yield.html +2 -2
  34. data/spec/apps/kitchen_sink/app/main/views/upload/index.html +15 -0
  35. data/spec/controllers/http_controller_spec.rb +130 -0
  36. data/spec/extra_core/string_transformation_test_cases.rb +8 -0
  37. data/spec/extra_core/string_transformations_spec.rb +12 -0
  38. data/spec/integration/http_endpoints_spec.rb +29 -0
  39. data/spec/integration/user_spec.rb +42 -42
  40. data/spec/models/associations_spec.rb +4 -4
  41. data/spec/models/buffer_spec.rb +15 -0
  42. data/spec/models/model_spec.rb +70 -25
  43. data/spec/models/model_state_spec.rb +1 -1
  44. data/spec/models/permissions_spec.rb +64 -2
  45. data/spec/models/persistors/params_spec.rb +8 -8
  46. data/spec/models/persistors/store_spec.rb +1 -1
  47. data/spec/models/user_validation_spec.rb +1 -1
  48. data/spec/router/routes_spec.rb +111 -43
  49. data/spec/server/rack/http_request_spec.rb +50 -0
  50. data/spec/server/rack/http_resource_spec.rb +59 -0
  51. data/spec/server/rack/http_response_header_spec.rb +34 -0
  52. data/spec/server/rack/http_response_renderer_spec.rb +33 -0
  53. data/spec/tasks/dispatcher_spec.rb +2 -2
  54. data/templates/component/config/routes.rb +2 -2
  55. data/templates/project/Gemfile.tt +3 -5
  56. data/templates/project/app/main/config/routes.rb +4 -4
  57. data/volt.gemspec +2 -2
  58. metadata +33 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 664d87e842c6c03b483345d4771a71d72ad1567e
4
- data.tar.gz: 8e82d85deddbc176c6cda90317b5a005f63f918d
3
+ metadata.gz: a8f7ccdc9801cf342901b77cf83b2091af34975f
4
+ data.tar.gz: a4f0dd2b28f09f2423fa602eb45c9935d422840c
5
5
  SHA512:
6
- metadata.gz: a8e1ffac496bf1ead1564de04a4dac9a48ccbc68776d5db3fa0b9bb8ed2fcb7b10a31c9e5869f1f521f6b0d0796c37937a041a4efbe398d674e0884cbf93e3c2
7
- data.tar.gz: 5e54e036809c3d61f75de5a80f01658d46ef572aacc0193c45498a97b92c41dc665e2b761dcc848f7206a21240b18698557cc2df90be43cdf36b39b4594cce9c
6
+ metadata.gz: 94607eb5080e4809be8a505e2a6e0c2602a887346de86c335e7177b1230ed75048091a9d1751d1b3d30e1960bb242b385966c58eb1b13bd52f03a05b253f3069
7
+ data.tar.gz: bb4d64bc6781e697e646f53054c305499fcd4434e10c8bfde661c46fb0fef31a3a0bda791818c783bc5631aacfadd156486baddb0537a4868dd597056f91708c
data/CHANGELOG.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  ## 0.8.27 - WIP
4
4
  ### Added
5
- - _'s are no longer required for route constraints (so just use ```controller: 'main', action: 'index'``` now)
5
+ - added has_many and belongs_to on models. See docs.
6
+ - you can now serve http/rest from Volt. Thanks to @jfahrer for his great work. Docs coming soon.
6
7
  - fixed generated component code
7
8
  - added .order for sorting on the data store (since .sort is a ruby Enum method)
8
- - changed .find to .where to not conflict with ruby Enum's .find
9
- - added .fetch and .fetch_first for waiting on store model loads
9
+ - calling .then on ArrayModels has been changed to calling .fetch and .fetch_first. These both return a promise, and take an optional block
10
10
  - added .sync for synchronusly waiting on promises on the server only
11
11
  - added the ability to pass content into tags: (https://github.com/voltrb/docs/blob/master/en/docs/yield_binding.md)
12
12
  - the {action}_remove method had been changed to before_{action}_remove and after_{action}_remove to provide more hooks and a clearer understanding of when it is happening.
@@ -18,9 +18,22 @@
18
18
  - refactored TemplateBinding, moved code into ViewLookupForPath (SRP)
19
19
  - reserved fields now get a warning in models
20
20
  - bindings will now resolve any values that are promises. (currently only content and attribute, if, each, and template coming soon)
21
+ - ```store``` is now available inside of specs. If it is accessed in a spec, the database will be cleaned after the spec.
22
+ - ```the_page``` is a shortcut to the page collection inside of specs. (Unfortunately, ```page``` is used by capybara, so for now we're using ```the_page```, we'll find a better solution in the future.)
21
23
 
22
24
  ### Changed
25
+ - model attributes no longer return NilModels. Instead they just return nil. You can however add an ! to the end to "expand" the model to an empty model.
26
+
27
+ ```page._new_todo # => now returns nil```
28
+
29
+ ```page._new_todo! # => returns an empty model```
30
+
31
+ So if you wanted to use a property on ```_new_todo``` without initializing ```_new_todo```, you could add the ! to the lookup.
32
+ - Volt.user has been renamed to Volt.current_user so its clearer what it returns
33
+ - _'s are no longer required for route constraints (so just use ```controller: 'main', action: 'index'``` now)
23
34
  - the underlying way queries are normalized and passed to the server has changed (no external api changes)
35
+ - changed .find to .where to not conflict with ruby Enum's .find
36
+ - Volt::TaskHandler is now Volt::Task
24
37
 
25
38
  ## 0.8.24 - 2014-12-05
26
39
  ### Added
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.8.27.beta6
1
+ 0.8.27.beta7
@@ -21,7 +21,7 @@ module Volt
21
21
  deny :hashed_password
22
22
 
23
23
  # Deny all if this isn't the owner
24
- deny if !_id == Volt.user_id && !new?
24
+ deny if !_id == Volt.current_user_id && !new?
25
25
  end
26
26
 
27
27
  if RUBY_PLATFORM == 'opal'
@@ -1,7 +1,7 @@
1
1
  require_relative 'live_query/data_store'
2
2
  require_relative 'live_query/live_query_pool'
3
3
 
4
- class QueryTasks < Volt::TaskHandler
4
+ class QueryTasks < Volt::Task
5
5
  @@live_query_pool = LiveQueryPool.new(DataStore.new)
6
6
  @@channel_live_queries = {}
7
7
 
@@ -23,7 +23,7 @@ class QueryTasks < Volt::TaskHandler
23
23
  # For requests from the client (with @channel), we track the channel
24
24
  # so we can send the results back. Server side requests don't stay live,
25
25
  # they simply return to :dirty once the query is issued.
26
- @channel.user_id = Volt.user_id
26
+ @channel.user_id = Volt.current_user_id
27
27
 
28
28
  live_query.add_channel(@channel)
29
29
  end
@@ -1,7 +1,7 @@
1
1
  require 'mongo'
2
2
  require 'volt/models'
3
3
 
4
- class StoreTasks < Volt::TaskHandler
4
+ class StoreTasks < Volt::Task
5
5
  def initialize(channel = nil, dispatcher = nil)
6
6
  @channel = channel
7
7
  @dispatcher = dispatcher
@@ -35,8 +35,12 @@ class StoreTasks < Volt::TaskHandler
35
35
  def save(collection, path, data)
36
36
  data = data.symbolize_keys
37
37
  promise = nil
38
- Volt::Model.no_validate do
39
- promise = load_model(collection, path, data)
38
+
39
+ # Don't check the permissions when we load the model, since we want all fields
40
+ Volt.skip_permissions do
41
+ Volt::Model.no_validate do
42
+ promise = load_model(collection, path, data)
43
+ end
40
44
  end
41
45
 
42
46
  # On the backend, the promise is resolved before its returned, so we can
@@ -61,7 +65,13 @@ class StoreTasks < Volt::TaskHandler
61
65
 
62
66
  def delete(collection, id)
63
67
  # Load the model, then call .destroy on it
64
- store.send(:"_#{collection}").where(_id: id).fetch_first do |model|
68
+ query = nil
69
+
70
+ Volt.skip_permissions do
71
+ query = store.send(:"_#{collection}").where(_id: id)
72
+ end
73
+
74
+ query.fetch_first do |model|
65
75
  if model
66
76
  if model.can_delete?
67
77
  db[collection].remove('_id' => id)
@@ -1,4 +1,4 @@
1
- class UserTasks < Volt::TaskHandler
1
+ class UserTasks < Volt::Task
2
2
  # Login a user, takes a login and password. Login can be either a username
3
3
  # or an e-mail based on Volt.config.public.auth.use_username
4
4
  def login(login, password)
@@ -0,0 +1,60 @@
1
+ require 'volt/server/rack/http_response_header'
2
+ require 'volt/server/rack/http_response_renderer'
3
+
4
+ module Volt
5
+ # Allow you to create controllers that act as http endpoints
6
+ class HttpController
7
+ attr_accessor :response_body
8
+ attr_reader :params, :response_headers, :request
9
+
10
+ # Initialzed with the params parsed from the route and the HttpRequest
11
+ def initialize(params, request)
12
+ @response_headers = HttpResponseHeader.new
13
+ @response_body = []
14
+ @request = request
15
+ @params = params.symbolize_keys.merge(request.params)
16
+ end
17
+
18
+ def perform(action)
19
+ # TODO: before actions
20
+ send(action.to_sym)
21
+ # TODO: after actions / around actions
22
+ respond
23
+ end
24
+
25
+ private
26
+
27
+ def store
28
+ $page.store
29
+ end
30
+
31
+ def head(status, additional_headers = {})
32
+ @response_status = status
33
+ response_headers.merge!(additional_headers)
34
+ end
35
+
36
+ def redirect_to(target, status = :found)
37
+ head(status, location: target)
38
+ end
39
+
40
+ def render(content)
41
+ status = content.delete(:status) || :ok
42
+ body, additional_headers = HttpResponseRenderer.new.render(content)
43
+ head(status, additional_headers)
44
+ response_body << body
45
+ end
46
+
47
+ def respond
48
+ Rack::Response.new(response_body, response_status, response_headers)
49
+ end
50
+
51
+ # Get the http status code as integer
52
+ def response_status
53
+ if @response_status.is_a?(Symbol)
54
+ Rack::Utils::SYMBOL_TO_STATUS_CODE[@response_status]
55
+ else
56
+ @response_status.try(:to_i) || 200
57
+ end
58
+ end
59
+ end
60
+ end
@@ -108,7 +108,11 @@ module Volt
108
108
 
109
109
  # Change the url params, similar to redirecting to a new url
110
110
  def go(url)
111
- self.url.parse(url)
111
+ # We might be in the rendering loop, so wait until the next tick before
112
+ # we change the url
113
+ Timers.next_tick do
114
+ self.url.parse(url)
115
+ end
112
116
  end
113
117
 
114
118
  def page
@@ -35,6 +35,12 @@ class String
35
35
  gsub('_', ' ').split(' ').map(&:capitalize).join(' ')
36
36
  end
37
37
 
38
+ def headerize
39
+ split(/[_-]/)
40
+ .map { |new_str| new_str[0].capitalize + new_str[1..-1] }
41
+ .join('-')
42
+ end
43
+
38
44
  def plural?
39
45
  # TODO: Temp implementation
40
46
  pluralize == self
@@ -82,7 +82,10 @@ module Volt
82
82
  if @persistor
83
83
  promise = @persistor.added(model, @array.size - 1)
84
84
  if promise && promise.is_a?(Promise)
85
- return promise.fail do |err|
85
+ return promise.then do
86
+ # return the model
87
+ model
88
+ end.fail do |err|
86
89
  # remove from the collection because it failed to save on the server
87
90
  @array.delete(model)
88
91
 
@@ -95,7 +98,8 @@ module Volt
95
98
  end
96
99
  end
97
100
  else
98
- nil
101
+ # Return this model
102
+ model
99
103
  end
100
104
  end
101
105
 
@@ -21,7 +21,7 @@ module Volt
21
21
  # The key will be "{this class name}_id"
22
22
  remote_key_name ||= :"#{path[-2].singularize}_id"
23
23
 
24
- root.send(:"_#{method_name.pluralize}").where(remote_key_name => id)
24
+ root.send(:"_#{method_name.pluralize}!").where(remote_key_name => id)
25
25
  end
26
26
  end
27
27
  end
@@ -1,6 +1,10 @@
1
1
  module Volt
2
2
  module Buffer
3
- def save!
3
+ # Save saves the contents of a buffer to the save_to location. If the buffer is new, it will create a new
4
+ # model to save to. Otherwise it will be an existing model. Save returns a promise that may fail with
5
+ # validation errors from the server (or the client). You can also pass a block as a shortcut to calling
6
+ # ```.save!.then do```
7
+ def save!(&block)
4
8
  # TODO: this shouldn't need to be run, but if no attributes are assigned, then
5
9
  # if needs to be run. Maybe there's a better way to handle it.
6
10
  validate!
@@ -8,6 +12,8 @@ module Volt
8
12
  # Get errors from validate
9
13
  errors = self.errors.to_h
10
14
 
15
+ result = nil
16
+
11
17
  if errors.size == 0
12
18
  save_to = options[:save_to]
13
19
  if save_to
@@ -19,7 +25,7 @@ module Volt
19
25
  promise = save_to.assign_attributes(attributes)
20
26
  end
21
27
 
22
- return promise.then do |new_model|
28
+ result = promise.then do |new_model|
23
29
  # The main model saved, so mark the buffer as not new
24
30
  @new = false
25
31
 
@@ -42,8 +48,13 @@ module Volt
42
48
  end
43
49
  else
44
50
  # Some errors, mark all fields
45
- promise_for_errors(errors)
51
+ result = promise_for_errors(errors)
46
52
  end
53
+
54
+ # If passed a block, call then on it with the block.
55
+ result = result.then(&block) if block
56
+
57
+ return result
47
58
  end
48
59
 
49
60
  # When errors come in, we mark all fields and return a rejected promise.
@@ -188,7 +188,12 @@ module Volt
188
188
  # Assigning an attribute without the =
189
189
  set(method_name[0..-2], args[0], &block)
190
190
  else
191
- get(method_name)
191
+ # If the method has an ! on the end, then we assign an empty
192
+ # collection if no result exists already.
193
+ expand = (method_name[-1] == '!')
194
+ method_name = method_name[0..-2] if expand
195
+
196
+ get(method_name, expand)
192
197
  end
193
198
  else
194
199
  # Call on parent
@@ -198,7 +203,6 @@ module Volt
198
203
 
199
204
  # Do the assignment to a model and trigger a changed event
200
205
  def set(attribute_name, value, &block)
201
- self.expand!
202
206
  # Assign, without the =
203
207
  attribute_name = attribute_name.to_sym
204
208
 
@@ -227,13 +231,15 @@ module Volt
227
231
  # Save the changes
228
232
  run_changed(attribute_name) unless Volt.in_mode?(:no_change_tracking)
229
233
  end
234
+
235
+ new_value
230
236
  end
231
237
 
232
238
  # When reading an attribute, we need to handle reading on:
233
239
  # 1) a nil model, which returns a wrapped error
234
240
  # 2) reading directly from attributes
235
241
  # 3) trying to read a key that doesn't exist.
236
- def get(attr_name)
242
+ def get(attr_name, expand=false)
237
243
  # Reading an attribute, we may get back a nil model.
238
244
  attr_name = attr_name.to_sym
239
245
 
@@ -242,31 +248,29 @@ module Volt
242
248
  # Track that something is listening
243
249
  @root_dep.depend
244
250
 
251
+ # Track dependency
252
+ @deps.depend(attr_name)
253
+
245
254
  # See if the value is in attributes
246
255
  if @attributes && @attributes.key?(attr_name)
247
- # Track dependency
248
- @deps.depend(attr_name)
249
-
250
256
  return @attributes[attr_name]
251
257
  else
252
- new_model = read_new_model(attr_name)
253
- @attributes ||= {}
254
- @attributes[attr_name] = new_model
255
-
256
- # Trigger size change
257
- # TODO: We can probably improve Computations to just make this work
258
- # without the delay
259
- if Volt.in_browser?
260
- `setImmediate(function() {`
261
- @size_dep.changed!
262
- `});`
258
+ # If we're expanding, or the get is for a collection, in which
259
+ # case we always expand.
260
+ if expand || attr_name.plural?
261
+ new_value = read_new_model(attr_name)
262
+
263
+ # A value was generated, store it
264
+ if new_value
265
+ # Assign directly. Since this is the first time
266
+ # we're loading, we can just assign.
267
+ set(attr_name, new_value)
268
+ end
269
+
270
+ return new_value
263
271
  else
264
- @size_dep.changed!
272
+ return nil
265
273
  end
266
-
267
- # Depend on attribute
268
- @deps.depend(attr_name)
269
- return new_model
270
274
  end
271
275
  end
272
276
 
@@ -280,10 +284,11 @@ module Volt
280
284
  return @persistor.read_new_model(method_name)
281
285
  else
282
286
  opts = @options.merge(parent: self, path: path + [method_name])
287
+
283
288
  if method_name.plural?
284
289
  return new_array_model([], opts)
285
290
  else
286
- return new_model(nil, opts)
291
+ return new_model({}, opts)
287
292
  end
288
293
  end
289
294
  end
@@ -300,43 +305,6 @@ module Volt
300
305
  ArrayModel.new(attributes, options)
301
306
  end
302
307
 
303
- # If this model is nil, it makes it into a hash model, then
304
- # sets it up to track from the parent.
305
- def expand!
306
- if attributes.nil?
307
- @attributes = {}
308
- if @parent
309
- @parent.expand!
310
-
311
- @parent.send(:"_#{@path.last}=", self)
312
- end
313
- end
314
- end
315
-
316
- # Initialize an empty array and append to it
317
- def <<(value)
318
- if @parent
319
- @parent.expand!
320
- else
321
- fail 'Model data should be stored in sub collections.'
322
- end
323
-
324
- # Grab the last section of the path, so we can do the assign on the parent
325
- path = @path.last
326
- result = @parent.send(path)
327
-
328
- if result.nil?
329
- # If this isn't a model yet, instantiate it
330
- @parent.send(:"#{path}=", new_array_model([], @options))
331
- result = @parent.send(path)
332
- end
333
-
334
- # Add the new item
335
- result << value
336
-
337
- nil
338
- end
339
-
340
308
  def inspect
341
309
  Computation.run_without_tracking do
342
310
  str = "<#{self.class}:#{object_id}"
@@ -4,7 +4,7 @@ module Volt
4
4
  # The permissions module provides helpers for working with Volt permissions.
5
5
  module Permissions
6
6
  module ClassMethods
7
- # Own by user requires a logged in user (Volt.user) to save a model. If
7
+ # Own by user requires a logged in user (Volt.current_user) to save a model. If
8
8
  # the user is not logged in, an validation error will occur. Once created
9
9
  # the user can not be changed.
10
10
  #
@@ -13,7 +13,7 @@ module Volt
13
13
  # When the model is created, assign it the user_id (if the user is logged in)
14
14
  on(:new) do
15
15
  # Only assign the user_id if there isn't already one and the user is logged in.
16
- if _user_id.nil? && !(user_id = Volt.user_id).nil?
16
+ if _user_id.nil? && !(user_id = Volt.current_user_id).nil?
17
17
  send(:"_#{key}=", user_id)
18
18
  end
19
19
  end
@@ -102,13 +102,13 @@ module Volt
102
102
  end
103
103
 
104
104
  # owner? can be called on a model to check if the currently logged
105
- # in user (```Volt.user```) is the owner of this instance.
105
+ # in user (```Volt.current_user```) is the owner of this instance.
106
106
  #
107
107
  # @param key [Symbol] the name of the attribute where the user_id is stored
108
108
  def owner?(key=:user_id)
109
109
  # Lookup the original user_id
110
110
  owner_id = was(key) || send(:"_#{key}")
111
- owner_id != nil && owner_id == Volt.user_id
111
+ owner_id != nil && owner_id == Volt.current_user_id
112
112
  end
113
113
 
114
114
  # Returns boolean if the model can be deleted