lanes 0.0.5 → 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +0 -1
- data/Gemfile +0 -1
- data/README.md +2 -0
- data/client/lanes/data/Bootstrap.coffee +2 -2
- data/client/lanes/data/Collection.coffee +4 -0
- data/client/lanes/data/Config.coffee +0 -5
- data/client/lanes/data/Model.coffee +236 -150
- data/client/lanes/data/PubSub.coffee +6 -12
- data/client/lanes/data/Sync.coffee +1 -0
- data/client/lanes/extension/Extensions.coffee +4 -2
- data/client/lanes/lib/MakeBaseClass.coffee +1 -1
- data/client/lanes/minimal.js +11 -0
- data/client/lanes/minimal.scss.erb +12 -0
- data/client/lanes/screens/Base.coffee +1 -2
- data/client/lanes/screens/Instance.coffee +52 -0
- data/client/lanes/vendor/packaged.js +1 -2
- data/client/lanes/views/Base.coffee +12 -10
- data/client/lanes/workspace.scss.erb +3 -0
- data/client/lanes/workspace/index.js +2 -12
- data/docs/command.md +111 -0
- data/docs/model.md +188 -0
- data/docs/todo-example-part-1.md +71 -0
- data/docs/view.md +275 -0
- data/{spec/client/jasmine_examples/PlayerSpec.js → docs/welcome.md} +0 -0
- data/lanes.gemspec +3 -1
- data/lib/lanes/api/helper_methods.rb +8 -0
- data/lib/lanes/api/javascript_processor.rb +14 -10
- data/lib/lanes/api/pub_sub.rb +7 -7
- data/lib/lanes/api/request_wrapper.rb +1 -0
- data/lib/lanes/api/root.rb +2 -7
- data/lib/lanes/api/sprockets_compressor.rb +6 -2
- data/lib/lanes/api/sprockets_extension.rb +25 -9
- data/lib/lanes/api/test_specs.rb +13 -9
- data/lib/lanes/command.rb +16 -6
- data/lib/lanes/command/app.rb +11 -5
- data/lib/lanes/command/generate_model.rb +4 -3
- data/lib/lanes/command/generate_screen.rb +2 -1
- data/lib/lanes/command/generate_view.rb +1 -1
- data/lib/lanes/command/named_command.rb +5 -4
- data/lib/lanes/command/templates/Gemfile +1 -2
- data/lib/lanes/command/templates/client/data/Model.coffee +3 -3
- data/lib/lanes/command/templates/client/{namespace-extension.js → index.js} +0 -0
- data/lib/lanes/command/templates/client/screens/Screen.coffee +1 -3
- data/lib/lanes/command/templates/client/{styles/styles.scss → styles.scss} +0 -0
- data/lib/lanes/command/templates/client/views/View.coffee +1 -3
- data/lib/lanes/command/templates/config/lanes.rb +1 -1
- data/lib/lanes/command/templates/gitignore +1 -0
- data/lib/lanes/command/templates/lib/namespace/screen.rb +1 -1
- data/lib/lanes/command/templates/public/.gitkeep +0 -0
- data/lib/lanes/command/templates/spec/client/Screen.coffee +7 -0
- data/lib/lanes/command/templates/spec/client/views/ViewSpec.coffee +2 -2
- data/lib/lanes/concerns/all.rb +1 -1
- data/lib/lanes/concerns/sanitize_fields.rb +32 -0
- data/lib/lanes/concerns/set_attribute_data.rb +4 -4
- data/lib/lanes/db.rb +7 -8
- data/lib/lanes/extension.rb +37 -3
- data/lib/lanes/guard_tasks.rb +2 -2
- data/lib/lanes/model.rb +2 -2
- data/lib/lanes/screens.rb +1 -0
- data/lib/lanes/spec_helper.rb +17 -6
- data/{spec → lib/lanes}/testing_models.rb +1 -1
- data/lib/lanes/version.rb +1 -1
- data/npm-build/compile.coffee +1 -6
- data/public/javascripts/jasmine_examples/Player.js +22 -0
- data/public/javascripts/jasmine_examples/Song.js +7 -0
- data/spec/api/javascript_processor_spec.rb +6 -3
- data/spec/concerns/api_path_spec.rb +1 -1
- data/spec/concerns/association_extensions_spec.rb +7 -3
- data/spec/concerns/attr_accessor_with_default_spec.rb +1 -1
- data/spec/concerns/code_identifier_spec.rb +1 -1
- data/spec/concerns/export_associations_spec.rb +1 -1
- data/spec/concerns/export_methods_spec.rb +1 -14
- data/spec/concerns/export_scope_spec.rb +7 -9
- data/spec/concerns/exported_limits_spec.rb +1 -1
- data/spec/concerns/pub_sub_spec.rb +1 -1
- data/spec/concerns/set_attribute_data_spec.rb +16 -24
- data/spec/configuration_spec.rb +1 -1
- data/spec/helpers/lanes-helpers.coffee +61 -0
- data/spec/lanes/data/ModelSpec.coffee +152 -0
- data/spec/lanes/data/PubSubSpec.coffee +21 -0
- data/spec/{client/view → lanes/views}/BaseSpec.coffee +6 -26
- data/spec/numbers_spec.rb +1 -1
- data/spec/strings_spec.rb +1 -1
- data/views/index.erb +3 -10
- data/views/specs.erb +4 -1
- metadata +62 -16
- data/client/lanes/plugins/trigger.coffee +0 -15
- data/client/lanes/workspace/Instance.es6 +0 -64
- data/lib/lanes/concerns/sanitize_api_data.rb +0 -15
- data/spec/api/user_spec.rb +0 -52
- data/spec/fixtures/lanes/users.yml +0 -13
- data/spec/locked_fields_spec.rb +0 -27
- data/spec/role_collection_spec.rb +0 -19
- data/spec/user_role_spec.rb +0 -7
- data/spec/user_spec.rb +0 -53
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4e6cb1567e7230eb1c9320af1e79312501b03f6a
|
4
|
+
data.tar.gz: 075ddb6c5d12cb8006bb6b0a0f6b52350f9bd736
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 40c369f55a08e182b116d4a6baf22612a1385104ebb961967811c2639cea03ebb0255e222e7e820a53334f50e42f3271a49147e2866f09f15908adc9d206cf76
|
7
|
+
data.tar.gz: d33486e07c3b727ccf22160d161b87860eb6edccfb39e1b82d4fa23304c3e0c64da50d87995481bf2048c7054a4f1427a1614d82a6bce531e1c23f8a8c11c45b
|
data/.gitignore
CHANGED
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.
|
6
|
-
Lanes.Extensions.setBootstrapData(options
|
5
|
+
Lanes.Data.Config.api_path = options.api_path
|
6
|
+
Lanes.Extensions.setBootstrapData(options);
|
7
7
|
|
8
8
|
}
|
@@ -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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
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
|
-
|
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 + '/' +
|
69
|
-
|
70
|
-
|
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 =
|
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
|
-
|
221
|
+
# Fetch a single record using an ID and the query options
|
222
|
+
@fetch: (id, options={})->
|
98
223
|
record = new this()
|
99
|
-
if _.isNumber(
|
100
|
-
record.id =
|
101
|
-
|
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
|
-
|
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.
|
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
|
-
|
131
|
-
|
132
|
-
data = @attributes
|
249
|
+
method = if this.isNew()
|
250
|
+
'create'
|
133
251
|
else
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
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
|
-
|
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.
|
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
|
-
|
151
|
-
|
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
|
-
|
157
|
-
|
158
|
-
|
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
|
-
|
191
|
-
|
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
|
200
|
-
klass
|
201
|
-
klass.prototype.associations ||= {}
|
202
|
-
(klass.prototype.addStandardProperties || setupStandardProps).call(klass, klass.prototype)
|
326
|
+
klass::props ||= {}
|
327
|
+
klass::session ||= {}
|
203
328
|
|
204
|
-
|
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
|
-
|
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.
|
348
|
+
Lanes.Data.BasicModel = Lanes.lib.MakeBaseClass( Lanes.Vendor.Ampersand.State, BasicModel )
|
263
349
|
|
264
350
|
|
265
351
|
class State
|