lanes 0.0.5 → 0.0.8

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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -1
  3. data/Gemfile +0 -1
  4. data/README.md +2 -0
  5. data/client/lanes/data/Bootstrap.coffee +2 -2
  6. data/client/lanes/data/Collection.coffee +4 -0
  7. data/client/lanes/data/Config.coffee +0 -5
  8. data/client/lanes/data/Model.coffee +236 -150
  9. data/client/lanes/data/PubSub.coffee +6 -12
  10. data/client/lanes/data/Sync.coffee +1 -0
  11. data/client/lanes/extension/Extensions.coffee +4 -2
  12. data/client/lanes/lib/MakeBaseClass.coffee +1 -1
  13. data/client/lanes/minimal.js +11 -0
  14. data/client/lanes/minimal.scss.erb +12 -0
  15. data/client/lanes/screens/Base.coffee +1 -2
  16. data/client/lanes/screens/Instance.coffee +52 -0
  17. data/client/lanes/vendor/packaged.js +1 -2
  18. data/client/lanes/views/Base.coffee +12 -10
  19. data/client/lanes/workspace.scss.erb +3 -0
  20. data/client/lanes/workspace/index.js +2 -12
  21. data/docs/command.md +111 -0
  22. data/docs/model.md +188 -0
  23. data/docs/todo-example-part-1.md +71 -0
  24. data/docs/view.md +275 -0
  25. data/{spec/client/jasmine_examples/PlayerSpec.js → docs/welcome.md} +0 -0
  26. data/lanes.gemspec +3 -1
  27. data/lib/lanes/api/helper_methods.rb +8 -0
  28. data/lib/lanes/api/javascript_processor.rb +14 -10
  29. data/lib/lanes/api/pub_sub.rb +7 -7
  30. data/lib/lanes/api/request_wrapper.rb +1 -0
  31. data/lib/lanes/api/root.rb +2 -7
  32. data/lib/lanes/api/sprockets_compressor.rb +6 -2
  33. data/lib/lanes/api/sprockets_extension.rb +25 -9
  34. data/lib/lanes/api/test_specs.rb +13 -9
  35. data/lib/lanes/command.rb +16 -6
  36. data/lib/lanes/command/app.rb +11 -5
  37. data/lib/lanes/command/generate_model.rb +4 -3
  38. data/lib/lanes/command/generate_screen.rb +2 -1
  39. data/lib/lanes/command/generate_view.rb +1 -1
  40. data/lib/lanes/command/named_command.rb +5 -4
  41. data/lib/lanes/command/templates/Gemfile +1 -2
  42. data/lib/lanes/command/templates/client/data/Model.coffee +3 -3
  43. data/lib/lanes/command/templates/client/{namespace-extension.js → index.js} +0 -0
  44. data/lib/lanes/command/templates/client/screens/Screen.coffee +1 -3
  45. data/lib/lanes/command/templates/client/{styles/styles.scss → styles.scss} +0 -0
  46. data/lib/lanes/command/templates/client/views/View.coffee +1 -3
  47. data/lib/lanes/command/templates/config/lanes.rb +1 -1
  48. data/lib/lanes/command/templates/gitignore +1 -0
  49. data/lib/lanes/command/templates/lib/namespace/screen.rb +1 -1
  50. data/lib/lanes/command/templates/public/.gitkeep +0 -0
  51. data/lib/lanes/command/templates/spec/client/Screen.coffee +7 -0
  52. data/lib/lanes/command/templates/spec/client/views/ViewSpec.coffee +2 -2
  53. data/lib/lanes/concerns/all.rb +1 -1
  54. data/lib/lanes/concerns/sanitize_fields.rb +32 -0
  55. data/lib/lanes/concerns/set_attribute_data.rb +4 -4
  56. data/lib/lanes/db.rb +7 -8
  57. data/lib/lanes/extension.rb +37 -3
  58. data/lib/lanes/guard_tasks.rb +2 -2
  59. data/lib/lanes/model.rb +2 -2
  60. data/lib/lanes/screens.rb +1 -0
  61. data/lib/lanes/spec_helper.rb +17 -6
  62. data/{spec → lib/lanes}/testing_models.rb +1 -1
  63. data/lib/lanes/version.rb +1 -1
  64. data/npm-build/compile.coffee +1 -6
  65. data/public/javascripts/jasmine_examples/Player.js +22 -0
  66. data/public/javascripts/jasmine_examples/Song.js +7 -0
  67. data/spec/api/javascript_processor_spec.rb +6 -3
  68. data/spec/concerns/api_path_spec.rb +1 -1
  69. data/spec/concerns/association_extensions_spec.rb +7 -3
  70. data/spec/concerns/attr_accessor_with_default_spec.rb +1 -1
  71. data/spec/concerns/code_identifier_spec.rb +1 -1
  72. data/spec/concerns/export_associations_spec.rb +1 -1
  73. data/spec/concerns/export_methods_spec.rb +1 -14
  74. data/spec/concerns/export_scope_spec.rb +7 -9
  75. data/spec/concerns/exported_limits_spec.rb +1 -1
  76. data/spec/concerns/pub_sub_spec.rb +1 -1
  77. data/spec/concerns/set_attribute_data_spec.rb +16 -24
  78. data/spec/configuration_spec.rb +1 -1
  79. data/spec/helpers/lanes-helpers.coffee +61 -0
  80. data/spec/lanes/data/ModelSpec.coffee +152 -0
  81. data/spec/lanes/data/PubSubSpec.coffee +21 -0
  82. data/spec/{client/view → lanes/views}/BaseSpec.coffee +6 -26
  83. data/spec/numbers_spec.rb +1 -1
  84. data/spec/strings_spec.rb +1 -1
  85. data/views/index.erb +3 -10
  86. data/views/specs.erb +4 -1
  87. metadata +62 -16
  88. data/client/lanes/plugins/trigger.coffee +0 -15
  89. data/client/lanes/workspace/Instance.es6 +0 -64
  90. data/lib/lanes/concerns/sanitize_api_data.rb +0 -15
  91. data/spec/api/user_spec.rb +0 -52
  92. data/spec/fixtures/lanes/users.yml +0 -13
  93. data/spec/locked_fields_spec.rb +0 -27
  94. data/spec/role_collection_spec.rb +0 -19
  95. data/spec/user_role_spec.rb +0 -7
  96. data/spec/user_spec.rb +0 -53
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9103ba1713d2bda09e56196a703bf4f6641c4580
4
- data.tar.gz: d4126f48d2d374e4323c2dccfdba535894082956
3
+ metadata.gz: 4e6cb1567e7230eb1c9320af1e79312501b03f6a
4
+ data.tar.gz: 075ddb6c5d12cb8006bb6b0a0f6b52350f9bd736
5
5
  SHA512:
6
- metadata.gz: aad53b38768c367f155369608fdde18d4a34b6bd52da1e93a9d39a593885b08c8066a295ad69a3b908251c7c933c65cdd6a65d75e8a4440b3578b681139370c5
7
- data.tar.gz: ce087ff038a83b5ec978aaa7edc39f1c10c31ed153d43c9e6ecaff7d358985720c63446c634505f61d2c5a075471f0904729e3a6aa17221492aa0bcb3ade7bf2
6
+ metadata.gz: 40c369f55a08e182b116d4a6baf22612a1385104ebb961967811c2639cea03ebb0255e222e7e820a53334f50e42f3271a49147e2866f09f15908adc9d206cf76
7
+ data.tar.gz: d33486e07c3b727ccf22160d161b87860eb6edccfb39e1b82d4fa23304c3e0c64da50d87995481bf2048c7054a4f1427a1614d82a6bce531e1c23f8a8c11c45b
data/.gitignore CHANGED
@@ -8,7 +8,6 @@ InstalledFiles
8
8
  coverage
9
9
  doc/
10
10
  log/
11
- public
12
11
  lib/bundler/man
13
12
  pkg
14
13
  rdoc
data/Gemfile CHANGED
@@ -2,7 +2,6 @@ source 'https://rubygems.org'
2
2
 
3
3
  gem "yard-activerecord", github: 'nathanstitt/yard-activerecord', branch: 'develop'
4
4
  gem "active_record_mocks", github: 'envygeeks/active_record_mocks', branch: 'master'
5
- gem "guard-jasmine", github: "guard/guard-jasmine", branch: 'master'
6
5
 
7
6
  gem 'puma'
8
7
 
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Lanes is a web framework that aims to make writing single page apps as simple as traditional Rails apps.
4
4
 
5
+ Read more at [lanesframework.org](http://lanesframework.org/)
6
+
5
7
  It's extracted from the Stockor ERP application and is still very much a work in progress.
6
8
 
7
9
  ## Contributing
@@ -2,7 +2,7 @@ Lanes.Data.Bootstrap = {
2
2
 
3
3
  initialize: (options)->
4
4
  Lanes.Data.Config.csrf_token = options.csrf
5
- Lanes.Data.Config.api_path = options.root
6
- Lanes.Extensions.setBootstrapData(options.data);
5
+ Lanes.Data.Config.api_path = options.api_path
6
+ Lanes.Extensions.setBootstrapData(options);
7
7
 
8
8
  }
@@ -88,6 +88,10 @@ class DataCollection
88
88
  CommonMethods
89
89
  ]
90
90
 
91
+ @afterExtended: (klass)->
92
+ if klass::model
93
+ klass::model::Collection = klass
94
+
91
95
  copyServerMessages=(collection,msg)->
92
96
  return unless msg
93
97
  collection.errors = msg.errors || []
@@ -5,11 +5,6 @@ class Config
5
5
  csrf_token: { type: 'string', setOnce: false }
6
6
  api_path: { type: 'string', setOnce: false }
7
7
 
8
- # derived:
9
- # api_path:
10
- # deps: ['root_path']
11
- # fn: -> @root_path + "api"
12
-
13
8
  Lanes.Data.State.extend(Config)
14
9
 
15
10
  Lanes.Data.Config = new Config
@@ -1,24 +1,130 @@
1
- class DataModel
2
- isModel: true
3
1
 
4
- constructor: (attrs,options={})->
5
- this._unsaved = if attrs and !options.xhr then _.keys(attrs) else []
6
- super
7
- this.on('change', this._recordUnsaved )
2
+ # ------------------------------------------------------------------ #
3
+ # The ModelChangeMonitor watches for changes on the #
4
+ # Model and remembers which attributes have been changed #
5
+ # ------------------------------------------------------------------ #
6
+ class ModelChangeMonitor
7
+ constructor: (@model)->
8
+ @model.on('change', this.onChange, this)
9
+
10
+ onChange: (record,options)->
11
+ attrs = @model.changedAttributes()
12
+ this.recordChanged( _.keys(attrs) )
13
+
14
+ recordChanged: (names)->
15
+ @_unsaved ||= {}
16
+ for name in names
17
+ this.recordChangedAttribute(name)
18
+
19
+ changedAttributes: ->
20
+ _.keys(@_unsaved)
21
+
22
+ recordChangedAttribute:(name)->
23
+ if @model._definition[name] && !@model._definition[name].session
24
+ @_unsaved[ name ] = true
25
+
26
+ reset: ->
27
+ delete @_unsaved
28
+
29
+ isDirty: ->
30
+ !_.isEmpty(@_unsaved)
31
+
32
+ # ------------------------------------------------------------------ #
33
+ # Handles Association definitions. #
34
+ # It creates a derived definition for each one #
35
+ # and contains utility functions to operate on them #
36
+ # ------------------------------------------------------------------ #
37
+ class AssocationMap
38
+ constructor: (@klass)->
39
+ @klass::derived ||= {}
40
+ @definitions = @klass::associations
41
+ @definitions['created_by'] ||= { model: 'Lanes.Data.User', readOnly: true }
42
+ @definitions['updated_by'] ||= { model: 'Lanes.Data.User', readOnly: true }
43
+ for name, options of @definitions
44
+ @klass::derived[name] = this.derivedDefinition(name,options)
45
+
46
+ # returns the definition for the derived property
47
+ derivedDefinition: (name,definition)->
48
+ findAssocationClass = ->
49
+ object = definition.model || definition.collection
50
+ if _.isObject(object) then object else Lanes.getPath( object, "Lanes.Data")
51
+ target_klass = findAssocationClass()
52
+ # will be called in the scope of the model
53
+ createAssocation = ->
54
+ args = {}
55
+ args['id'] = this.get(definition.fk) if this.get(definition.fk)
56
+ if definition.defaultValue
57
+ _.defaults(args, _.evaluateFunction(definition.defaultValue))
58
+ target_klass ||= findAssociationClass() # it might not have been present previously
59
+ record = target_klass.findOrCreate(args)
60
+ record.parent = this
61
+ record
62
+ { deps: [definition.fk], fn: createAssocation }
63
+
64
+ # Sets the assocations for "model"
65
+ set: (model, data)->
66
+ for name, value of data
67
+ model[name].set(value) if _.has(@definitions, name)
68
+
69
+ # returns the data from all assocations for saving
70
+ dataForSave: (model,options)->
71
+ ret = {}
72
+ for name, options of @definitions
73
+ unless options.readOnly
74
+ ret[name] = model[name].dataForSave(options)
75
+ ret
76
+
77
+ # return a list of assocations from "name" that are not loaded
78
+ nonLoaded: (model, names)->
79
+ list = []
80
+ for name in names
81
+ if _.has(@definitions, name) && !model[name].isLoaded()
82
+ list.push(name)
83
+ list
84
+
85
+ urlError = ->
86
+ throw new Error('A "url" property or function must be specified for Model or Collection')
87
+
88
+ copyServerResp = (record,resp)->
89
+ record.errors = resp?.errors
90
+ record.lastServerMessage = resp?.message
91
+ { record: record, response: resp }
92
+
93
+ # Wraps a sync request's error and success functions
94
+ # Copies any errors onto the model and sets it's data on success
95
+ wrapRequest = (record, options)->
96
+ error = options.error
97
+ success = options.success
98
+ options.promise = new _.Promise( (resolve,reject)->
99
+ options.resolvePromise = resolve
100
+ options.rejectPromise = reject
101
+ )
102
+ options.error = (reply, resp, req)->
103
+ options.rejectPromise( copyServerResp(record,resp.responseJSON || {error: resp.responseText}) )
104
+ error?.apply(options.scope, arguments)
105
+
106
+ options.success = (reply,resp,req)->
107
+ record.setFromResponse( resp.data ) if resp?.data?
108
+ options.resolvePromise( copyServerResp(record,resp) )
109
+ success?.apply(options.scope, arguments)
110
+ options
111
+
8
112
 
113
+ # Da Model. Handles all things dataish
114
+ class DataModel
115
+ isModel: true
9
116
  session:
10
117
  errors: 'object'
11
118
  changes: { type: 'collection', setOnce: true }
12
119
  lastServerMessage: { type: 'string' }
13
-
14
120
  derived:
15
- error_message:
121
+ errorMessage:
16
122
  deps:['errors'], fn: ->
17
123
  if !@errors then ''
18
124
  else if @errors.exception then @errors.exception
19
125
  else _.toSentence( _.map(@errrors, (value,key)-> "#{key}: #{value}" ) )
20
-
21
126
  dataTypes:
127
+ # Big decimal for attributes that need precision math
22
128
  bigdec:
23
129
  set: (newVal)->
24
130
  val: new _.bigDecimal(newVal)
@@ -28,6 +134,7 @@ class DataModel
28
134
  set: (newVal)->
29
135
  val: parseInt(newVal)
30
136
  type: 'integer'
137
+ # Uses the "moment" lib to parse dates and coerce strings into the date type.
31
138
  date:
32
139
  get: (val)-> new Date(val)
33
140
  default: -> return new Date()
@@ -47,38 +154,51 @@ class DataModel
47
154
  type: newType
48
155
  }
49
156
 
157
+ constructor: (attrs,options={})->
158
+ super
159
+ @changeMonitor = new ModelChangeMonitor(this)
160
+ # The model was created with attributes and it did not originate from a XHR request
161
+ if attrs and !options.xhr
162
+ @changeMonitor.recordChanged(_.keys(attrs))
163
+
164
+ # In some cases a model's security should depend on the parent record, not on itself.
165
+ # For instance, a Customer's Address should have the same permissions as the Customer
166
+ # This allows a subclass to specify the model type that should be checked.
50
167
  modelForAccess: -> this
51
168
 
52
- isPersistent: ->
53
- !!( this.api_path && ! this.isNew() )
54
-
169
+ # used by PubSub to record a remote change to the model
55
170
  addChangeSet: (change)->
56
171
  this.changes ||= new Lanes.Data.ChangeSetCollection( parent: this )
57
172
  change.record = this
58
173
  change = this.changes.add(change)
59
174
  this.set( change.value() )
60
175
 
61
- rootRecord: ->
62
- record = this.parent
63
- while record
64
- if record.parent then record = record.parent else break
65
- record
66
-
67
176
  urlRoot: ->
68
- Lanes.Data.Config.api_path + '/' + @resultsFor('api_path')
69
-
70
- setupStandardProps: -> Lanes.emptyFn
71
-
177
+ Lanes.Data.Config.api_path + '/' + _.result(this,'api_path')
178
+
179
+ # Default URL for the model's representation on the server
180
+ url: ->
181
+ base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError();
182
+ if this.isNew() then return base;
183
+ if base.charAt(base.length - 1) != '/'
184
+ base += "/"
185
+ return base + encodeURIComponent(this.getId())
186
+
187
+
188
+ # A record is considered loaded if it has the id set and some attributes set
72
189
  isLoaded: ->
73
- !_.isEmpty( _.omit(this.attributes,this.idAttribute) )
190
+ !this.isNew() && !_.isEmpty( _.omit(this.attributes,this.idAttribute) )
191
+
192
+ # is the record saved
193
+ isPersistent: ->
194
+ !!( this.api_path && ! this.isNew() )
74
195
 
196
+ # Ensures the assocations given in "needed" are loaded
75
197
  withAssociations: (names...,options={})->
76
198
  scope = options.scope || this
77
199
  if _.isString(options)
78
200
  names.push(options); options={}
79
- needed = _.filter( names, (name)->
80
- this._associations[name] && ! this._associations[name].instance(this).isLoaded()
81
- ,this)
201
+ needed = this.associations.nonLoaded(this,names)
82
202
  if _.isEmpty( needed )
83
203
  options.success.call(scope, this ) if options.success
84
204
  options.complete.call(scope,this ) if options.complete
@@ -88,178 +208,144 @@ class DataModel
88
208
  this.fetch(options)
89
209
  .then (req)-> req.record
90
210
 
211
+ # Searches the PubSub idenity map for a record of the same type and matching id
212
+ # If one is found, it will update it with the given attributes and return it
213
+ # When not found, it will create a new record and return it.
214
+ # The newly created record will not be stored, in PubSub map, only records bound to a view are stored
91
215
  @findOrCreate: (attrs, options={})->
92
216
  if attrs.id && ( record = Lanes.Data.PubSub.instanceFor(this, attrs.id) )
93
217
  record.set(attrs)
94
218
  else
95
219
  new this(attrs,options)
96
220
 
97
- @fetch: (options)->
221
+ # Fetch a single record using an ID and the query options
222
+ @fetch: (id, options={})->
98
223
  record = new this()
99
- if _.isNumber(options)
100
- record.id = options
101
- options = {}
102
- ret = record.fetch(options)
103
- ret
104
-
105
- fetch: (options={})->
106
- handlers = wrapRequest(this,options)
107
- super(_.extend(options,{limit:1,ignoreUnsaved:true})).then( =>@ )
108
- handlers.promise
109
-
110
- parse:(resp)->
111
- if resp.data
112
- if _.isArray(resp['data']) then resp['data'][0] else resp['data']
113
- else
114
- resp
224
+ if _.isNumber(id)
225
+ record.id = id
226
+ record.fetch(options)
115
227
 
228
+ # Calls Ampersand State's set method, then sets any associations that are present as well
116
229
  set: (attrs,options)->
117
230
  super
118
- if _.isObject(attrs) && ! options?.saving
119
- for name, association of this._associations||{}
120
- association.instance(this).set(attrs[association.name], options) if attrs[association.name]
231
+ this.associations.set(this,attrs) if this.associations
121
232
  this
122
233
 
234
+ # Sets the attribute data from a server respose
235
+ setFromResponse: (data)->
236
+ return unless data
237
+ this.set( if _.isArray(data) then data[0] else data )
238
+ this.changeMonitor.reset()
239
+
240
+ # save the model's data to the server
241
+ # Only unsaved attributes will be sent unless
242
+ # the saveAll options is set to true
123
243
  save: (options={})->
124
- options.setFromResponse=true
244
+ options = _.clone(options)
245
+
125
246
  options.saving=true
126
247
  handlers = wrapRequest(this,options)
127
- super({}, options)
128
- handlers.promise
129
248
 
130
- dataForSave: (options={})->
131
- if options.saveAll
132
- data = @attributes
249
+ method = if this.isNew()
250
+ 'create'
133
251
  else
134
- data = this.unsavedData()
135
- for name, association of this._associations||{}
136
- if association.isCreated(this) && association.instance(this).isDirty()
137
- instance = association.instance(this)
138
- data[association.name] = if options.saveAll then instance.attributes else instance.unsavedData()
139
- data
252
+ if options.saveAll then 'update' else 'patch'
253
+
254
+ sync = this.sync(method, this, options);
255
+
256
+ handlers.promise
257
+
258
+ # Fetch the model from the server. If the server's representation of the
259
+ # model differs from its current attributes, they will be overridden,
260
+ # triggering a `"change"` event.
261
+ fetch: (options={}) ->
262
+ options = _.clone(options)
263
+ handlers = wrapRequest(this,options)
264
+ _.extend(options,{limit:1,ignoreUnsaved:true})
265
+
266
+ this.sync('read', this, options)
267
+ handlers.promise
140
268
 
269
+ # Removes the model's record from the server (if it is persistent)
270
+ # and then fires the "destroy" event
141
271
  destroy: (options={})->
142
272
  handlers = wrapRequest(this,options)
143
- super(options)
273
+ model = this
274
+ success = options.success
275
+ options.success = (reply, msg, options)->
276
+ model.trigger('destroy', model, model.collection, options)
277
+ if success then success(model, reply, options)
278
+
279
+ if this.isNew()
280
+ options.success()
281
+ return false
282
+ this.sync('delete', this, options)
144
283
  handlers.promise
145
284
 
285
+ # returns any attributes that have been set and not saved
146
286
  unsavedData: ->
147
287
  attrs = if this.isNew() then {} else { id: this.getId() }
148
- _.extend(attrs, _.pick( this.attributes, @_unsaved... ) )
288
+ _.extend(attrs, _.pick( this.getAttributes(props:true, true),
289
+ @changeMonitor.changedAttributes() ) )
290
+
291
+ # returns data to save to server. If options.saveAll is true,
292
+ # all data is returned. Otherwise only unsaved attributes (and associations)
293
+ # are returned.
294
+ dataForSave: (options={})->
295
+ if options.saveAll
296
+ data = this.getAttributes(props:true, true)
297
+ else
298
+ data = this.unsavedData()
299
+ _.extend(data, this.associations.dataForSave(this, options)) if this.associations
300
+ data
301
+
149
302
 
150
- isDirty:->
151
- !!@_unsaved.length
303
+ # returns true if any server-side attributes are unsaved
304
+ # Does not care about session-only properties
305
+ isDirty: ->
306
+ @changeMonitor.isDirty()
152
307
 
308
+ # True if the model has "name" as eitehr a prop or session attribute
153
309
  hasAttribute: (name)->
154
310
  !!this._definition[name]
155
311
 
156
- associationDefinition: (name,options)->
157
- _.extend(options, {
158
- fk: options.fk || name + "_id"
159
- name: name
160
- isCreated: (parent)->
161
- parent._cache.hasOwnProperty(@name)
162
- instance: (parent)-> parent[@name]
163
- })
164
-
165
-
166
- associationDerivedDefinition: (name,definition)->
167
- klass = associationLookupClass(definition)
168
- {
169
- deps: [definition.fk]
170
- fn: ->
171
- args = {}
172
- args['id'] = this.get(definition.fk) if this.get(definition.fk)
173
- if definition.defaultValue
174
- _.defaults(args, _.evaluateFunction(definition.defaultValue))
175
- klass ||= associationLookupClass(definition)
176
- record = klass.findOrCreate(args)
177
- record.parent = this
178
- record
179
- }
180
-
181
- validateFieldChange: (name, value)->
312
+ # Check if an attribute named "name" can be set to "value"
313
+ # Returns an empty string if value, and an appropriate error message if not
314
+ checkValid: (name, value)->
182
315
  return '' unless def = this._definition[name]
183
316
  if def.required && _.isEmpty(value)
184
317
  "Cannot be empty"
185
318
  else
186
319
  ''
187
-
320
+ # Use Sync directly
188
321
  sync: Lanes.Data.Sync
189
322
 
190
- _recordUnsaved: (record,options)->
191
- attrs = this.changedAttributes()
192
- unless options?.silent || options?.ignoreUnsaved
193
- for name,val of attrs
194
- @_unsaved.push( name ) if -1 == @_unsaved.indexOf(name)
195
- this
196
-
197
-
323
+ # When the model is extended it auto-creates the created_at and updated_at
324
+ # and sets up the AssociationMap
198
325
  @extended: (klass)->
199
- klass.prototype.props ||= {}
200
- klass.prototype.session ||= {}
201
- klass.prototype.associations ||= {}
202
- (klass.prototype.addStandardProperties || setupStandardProps).call(klass, klass.prototype)
326
+ klass::props ||= {}
327
+ klass::session ||= {}
203
328
 
204
- setupAssociations(klass.prototype) if klass.prototype.associations
329
+ klass::session['created_at'] ||= 'date'
330
+ klass::session['updated_at'] ||= 'date'
205
331
 
332
+ if klass::associations
333
+ klass::associations = new AssocationMap(klass)
206
334
 
207
- Lanes.lib.ModuleSupport.includeInto(@)
208
- @include Lanes.lib.results
209
-
210
-
211
- associationLookupClass = (definition)->
212
- object = definition.model || definition.collection
213
- if _.isObject(object) then object else Lanes.getPath( object, "Lanes.Data")
214
-
215
- setupAssociations=(klass)->
216
- klass.derived ||= {}
217
- klass._associations = {}
218
- derivedFn = ( klass.associationDerivedDefinition || DataModel.prototype.associationDerivedDefinition )
219
- createFn = ( klass.associationDefinition || DataModel.prototype.associationDefinition)
220
- for name, options of klass.associations
221
- definition = createFn.call(klass, name, options)
222
- klass.derived[ name ] = derivedFn.call(klass, name, definition)
223
- klass._associations[name] = definition
224
-
225
- setupStandardProps=(klass)->
226
- klass.session['created_at'] ||= 'date'
227
- klass.session['updated_at'] ||= 'date'
228
- klass.associations['created_by'] ||= { model: 'Lanes.Data.User' }
229
- klass.associations['updated_by'] ||= { model: 'Lanes.Data.User' }
230
-
231
- copyServerResp = (record,resp)->
232
- record.errors = resp?.errors
233
- record.lastServerMessage = resp?.message
234
- { record: record, response: resp }
235
-
236
- wrapRequest = (record, options)->
237
- error = options.error
238
- success = options.success
239
- options.promise = new _.Promise( (resolve,reject)->
240
- options.resolvePromise = resolve
241
- options.rejectPromise = reject
242
- )
243
- options.error = (record, resp, req)->
244
- options.rejectPromise( copyServerResp(record,resp.responseJSON || {error: resp.responseText}) )
245
- error?.apply(options.scope, arguments)
246
-
247
- options.success = (record,resp,req)->
248
- options.resolvePromise( copyServerResp(record,resp) )
249
- record._unsaved = []
250
- success?.apply(options.scope, arguments)
251
- options
252
335
 
336
+ Lanes.Data.Model = Lanes.lib.MakeBaseClass( Lanes.Vendor.Ampersand.State, DataModel )
253
337
 
254
- Lanes.Data.Model = Lanes.lib.MakeBaseClass( Lanes.Vendor.Ampersand.Model, DataModel )
255
338
 
256
339
 
257
- class BasicModel
340
+ # ------------------------------------------------------------------ #
341
+ # The BasicModel is just a very thin layer over State #
342
+ # ------------------------------------------------------------------ #
343
+ class BasicModel #
258
344
  constructor: -> super
259
345
  isPersistent: -> false
260
346
  isModel: true
261
347
 
262
- Lanes.Data.BasicModel = Lanes.lib.MakeBaseClass( Lanes.Vendor.Ampersand.Model, BasicModel )
348
+ Lanes.Data.BasicModel = Lanes.lib.MakeBaseClass( Lanes.Vendor.Ampersand.State, BasicModel )
263
349
 
264
350
 
265
351
  class State