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.
- 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
|