lanes 0.0.5 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
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