2014-01-26 14:46:25 -08:00
storage = require ' lib/storage '
2014-04-09 19:07:44 -07:00
deltasLib = require ' lib/deltas '
2014-01-26 14:46:25 -08:00
2014-01-03 10:32:13 -08:00
class CocoModel extends Backbone . Model
2014-07-01 10:16:26 +08:00
idAttribute: ' _id '
2014-01-03 10:32:13 -08:00
loaded: false
loading: false
2014-01-26 14:46:25 -08:00
saveBackups: false
2014-05-22 11:22:52 -07:00
notyErrors: true
2014-01-03 10:32:13 -08:00
@schema: null
2014-07-17 16:22:06 -07:00
initialize: (attributes, options) ->
super ( arguments . . . )
options ? = { }
@ setProjection options . project
2014-01-03 10:32:13 -08:00
if not @ constructor . className
console . error ( " #{ @ } needs a className set. " )
2014-05-19 21:14:27 -07:00
@ on ' sync ' , @ onLoaded , @
2014-04-25 15:31:38 -07:00
@ on ' error ' , @ onError , @
2014-05-30 16:20:20 -07:00
@ on ' add ' , @ onLoaded , @
2014-01-26 14:46:25 -08:00
@saveBackup = _ . debounce ( @ saveBackup , 500 )
2014-08-25 15:39:47 -07:00
setProjection: (project) ->
return if project is @ project
url = @ getURL ( )
url += ' &project= ' unless /project=/ . test url
url = url . replace ' & ' , ' ? ' unless /\?/ . test url
url = url . replace / project = [ ^ & ] * / , " project= #{ project ? . join ( ' , ' ) or ' ' } "
url = url . replace / [ & ? ] project = & / , ' & ' unless project ? . length
url = url . replace / [ & ? ] project = $ / , ' ' unless project ? . length
@ setURL url
@project = project
2014-02-12 12:41:41 -08:00
2014-01-03 10:32:13 -08:00
type: ->
@ constructor . className
2014-04-13 14:48:36 -07:00
2014-04-11 21:11:52 -07:00
clone: (withChanges=true) ->
# Backbone does not support nested documents
clone = super ( )
clone . set ( $ . extend ( true , { } , if withChanges then @ attributes else @ _revertAttributes ) )
clone
2014-05-06 16:58:08 -07:00
2014-04-25 15:31:38 -07:00
onError: ->
@loading = false
2014-05-27 16:33:57 -07:00
@jqxhr = null
2014-01-03 10:32:13 -08:00
2014-01-26 14:46:25 -08:00
onLoaded: ->
2014-01-03 10:32:13 -08:00
@loaded = true
@loading = false
2014-05-27 16:33:57 -07:00
@jqxhr = null
2014-04-12 10:51:02 -07:00
@ loadFromBackup ( )
2014-05-21 10:27:38 -07:00
2014-05-02 10:31:20 -07:00
getNormalizedURL: -> " #{ @ urlRoot } / #{ @ id } "
2014-09-01 09:11:10 -07:00
2014-08-23 11:07:52 -07:00
attributesWithDefaults: undefined
2014-09-01 09:11:10 -07:00
2014-08-23 11:07:52 -07:00
get: (attribute, withDefault=false) ->
if withDefault
if @ attributesWithDefaults is undefined then @ buildAttributesWithDefaults ( )
return @ attributesWithDefaults [ attribute ]
else
super ( attribute )
2014-09-01 09:11:10 -07:00
2014-09-02 18:28:02 -07:00
set: (attributes, options) ->
2014-08-23 11:07:52 -07:00
delete @ attributesWithDefaults
2014-05-30 16:20:20 -07:00
inFlux = @ loading or not @ loaded
2014-09-03 10:58:43 -07:00
@ markToRevert ( ) unless inFlux or @ _revertAttributes or @ project or options ? . fromMerge
2014-09-02 18:28:02 -07:00
res = super attributes , options
2014-05-30 16:20:20 -07:00
@ saveBackup ( ) if @ saveBackups and ( not inFlux ) and @ hasLocalChanges ( )
2014-01-26 14:46:25 -08:00
res
2014-02-12 12:41:41 -08:00
2014-08-23 11:07:52 -07:00
buildAttributesWithDefaults: ->
t0 = new Date ( )
clone = $ . extend true , { } , @ attributes
2014-08-28 15:54:05 -07:00
thisTV4 = tv4 . freshApi ( )
thisTV4 . addSchema ( ' # ' , @ schema ( ) )
thisTV4 . addSchema ( ' metaschema ' , require ( ' schemas/metaschema ' ) )
TreemaNode . utils . populateDefaults ( clone , @ schema ( ) , thisTV4 )
2014-08-23 11:07:52 -07:00
@attributesWithDefaults = clone
console . debug " Populated defaults for #{ @ attributes . name or @ type ( ) } in #{ new Date ( ) - t0 } ms "
2014-03-23 10:29:08 -07:00
loadFromBackup: ->
return unless @ saveBackups
existing = storage . load @ id
if existing
2014-07-01 10:16:26 +08:00
@ set ( existing , { silent: true } )
2014-03-23 10:29:08 -07:00
CocoModel . backedUp [ @ id ] = @
2014-07-03 17:41:34 -07:00
saveBackup: -> @ saveBackupNow ( )
2014-08-25 15:39:47 -07:00
2014-07-03 17:41:34 -07:00
saveBackupNow: ->
2014-01-26 14:46:25 -08:00
storage . save ( @ id , @ attributes )
CocoModel . backedUp [ @ id ] = @
2014-02-12 12:41:41 -08:00
2014-01-26 14:46:25 -08:00
@backedUp = { }
2014-01-03 10:32:13 -08:00
schema: -> return @ constructor . schema
2014-06-24 09:43:14 -07:00
2014-05-30 17:24:53 -07:00
getValidationErrors: ->
2014-09-08 16:03:29 -07:00
# Since Backbone unset only sets things to undefined instead of deleting them, we ignore undefined properties.
definedAttributes = _ . pick @ attributes , (v) -> v isnt undefined
errors = tv4 . validateMultiple ( definedAttributes , @ constructor . schema or { } ) . errors
2014-05-30 17:24:53 -07:00
return errors if errors ? . length
2014-01-03 10:32:13 -08:00
validate: ->
2014-05-30 17:24:53 -07:00
errors = @ getValidationErrors ( )
if errors ? . length
console . debug " Validation failed for #{ @ constructor . className } : ' #{ @ get ( ' name ' ) or @ } ' . "
for error in errors
2014-07-01 10:16:26 +08:00
console . debug " \t " , error . dataPath , ' : ' , error . message
2014-05-30 17:24:53 -07:00
return errors
2014-06-24 09:43:14 -07:00
2014-01-03 10:32:13 -08:00
save: (attrs, options) ->
options ? = { }
2014-05-30 14:41:41 -07:00
options . headers ? = { }
2014-09-08 14:31:52 -07:00
options . headers [ ' X-Current-Path ' ] = document . location ? . pathname ? ' unknown '
2014-01-03 10:32:13 -08:00
success = options . success
2014-05-21 10:27:38 -07:00
error = options . error
options.success = (model, res) =>
2014-07-01 10:16:26 +08:00
@ trigger ' save:success ' , @
2014-05-21 10:27:38 -07:00
success ( @ , res ) if success
2014-05-30 16:20:20 -07:00
@ markToRevert ( ) if @ _revertAttributes
2014-01-26 14:46:25 -08:00
@ clearBackup ( )
2014-05-24 20:45:53 +02:00
CocoModel . pollAchievements ( )
2014-05-21 10:27:38 -07:00
options.error = (model, res) =>
error ( @ , res ) if error
2014-05-22 11:22:52 -07:00
return unless @ notyErrors
2014-05-21 10:27:38 -07:00
errorMessage = " Error saving #{ @ get ( ' name ' ) ? @ type ( ) } "
console . error errorMessage , res . responseJSON
noty text: " #{ errorMessage } : #{ res . status } #{ res . statusText } " , layout: ' topCenter ' , type: ' error ' , killer: false , timeout: 10000
2014-07-01 10:16:26 +08:00
@ trigger ' save ' , @
2014-01-03 10:32:13 -08:00
return super attrs , options
2014-06-24 09:43:14 -07:00
2014-06-10 20:43:25 -07:00
patch: (options) ->
return false unless @ _revertAttributes
options ? = { }
options.patch = true
2014-06-24 09:43:14 -07:00
2014-06-10 20:43:25 -07:00
attrs = { _id: @ id }
2014-06-11 14:17:31 -07:00
keys = [ ]
2014-06-10 20:43:25 -07:00
for key in _ . keys @ attributes
unless _ . isEqual @ attributes [ key ] , @ _revertAttributes [ key ]
attrs [ key ] = @ attributes [ key ]
2014-06-11 14:17:31 -07:00
keys . push key
2014-06-24 09:43:14 -07:00
2014-06-11 14:17:31 -07:00
return unless keys . length
console . debug ' Patching ' , @ get ( ' name ' ) or @ , keys
2014-06-10 20:43:25 -07:00
@ save ( attrs , options )
2014-01-03 10:32:13 -08:00
2014-07-17 16:22:06 -07:00
fetch: (options) ->
options ? = { }
options . data ? = { }
options.data.project = @ project . join ( ' , ' ) if @ project
@jqxhr = super ( options )
2014-01-03 10:32:13 -08:00
@loading = true
2014-05-02 10:31:20 -07:00
@ jqxhr
2014-01-03 10:32:13 -08:00
2014-05-30 16:20:20 -07:00
markToRevert: ->
2014-03-23 10:00:16 -07:00
if @ type ( ) is ' ThangType '
2014-09-02 17:46:52 -07:00
# Don't deep clone the raw vector data, but do deep clone everything else.
@_revertAttributes = _ . clone @ attributes
for smallProp , value of @ attributes when value and smallProp isnt ' raw '
@ _revertAttributes [ smallProp ] = _ . cloneDeep value
2014-03-23 10:00:16 -07:00
else
2014-03-22 02:21:23 +00:00
@_revertAttributes = $ . extend ( true , { } , @ attributes )
2014-01-03 10:32:13 -08:00
revert: ->
2014-08-26 10:14:36 -07:00
@ clear ( { silent: true } )
2014-01-03 10:32:13 -08:00
@ set ( @ _revertAttributes , { silent: true } ) if @ _revertAttributes
2014-01-26 14:46:25 -08:00
@ clearBackup ( )
2014-02-12 12:41:41 -08:00
2014-01-26 14:46:25 -08:00
clearBackup: ->
storage . remove @ id
2014-01-03 10:32:13 -08:00
hasLocalChanges: ->
2014-05-29 10:48:48 -07:00
@ _revertAttributes and not _ . isEqual @ attributes , @ _revertAttributes
2014-01-03 10:32:13 -08:00
cloneNewMinorVersion: ->
2014-05-06 05:07:34 +02:00
newData = _ . clone @ attributes
2014-04-11 22:33:09 -07:00
clone = new @ constructor ( newData )
clone
2014-01-03 10:32:13 -08:00
cloneNewMajorVersion: ->
clone = @ cloneNewMinorVersion ( )
clone . unset ( ' version ' )
clone
isPublished: ->
2014-09-01 09:11:10 -07:00
for permission in ( @ get ( ' permissions ' , true ) ? [ ] )
2014-01-03 10:32:13 -08:00
return true if permission . target is ' public ' and permission . access is ' read '
false
publish: ->
2014-07-01 10:16:26 +08:00
if @ isPublished ( ) then throw new Error ( ' Can \' t publish what \' s already-published. Can \' t kill what \' s already dead. ' )
2014-09-01 09:11:10 -07:00
@ set ' permissions ' , @ get ( ' permissions ' , true ) . concat ( { access: ' read ' , target: ' public ' } )
2014-01-03 10:32:13 -08:00
@isObjectID: (s) ->
2014-04-11 20:38:34 -07:00
s . length is 24 and s . match ( /[a-f0-9]/gi ) ? . length is 24
2014-01-03 10:32:13 -08:00
2014-03-03 20:41:35 +01:00
hasReadAccess: (actor) ->
# actor is a User object
2014-08-23 11:07:52 -07:00
actor ? = me
2014-04-10 13:09:44 -07:00
return true if actor . isAdmin ( )
2014-09-01 09:11:10 -07:00
for permission in ( @ get ( ' permissions ' , true ) ? [ ] )
if permission . target is ' public ' or actor . get ( ' _id ' ) is permission . target
return true if permission . access in [ ' owner ' , ' read ' ]
2014-03-03 20:41:35 +01:00
return false
2014-03-03 21:13:02 +01:00
hasWriteAccess: (actor) ->
# actor is a User object
2014-08-23 11:07:52 -07:00
actor ? = me
2014-04-10 13:09:44 -07:00
return true if actor . isAdmin ( )
2014-09-01 09:11:10 -07:00
for permission in ( @ get ( ' permissions ' , true ) ? [ ] )
if permission . target is ' public ' or actor . get ( ' _id ' ) is permission . target
return true if permission . access in [ ' owner ' , ' write ' ]
2014-03-03 20:41:35 +01:00
return false
2014-04-13 14:48:36 -07:00
2014-08-28 10:50:20 -07:00
getOwner: ->
2014-09-01 09:11:10 -07:00
ownerPermission = _ . find @ get ( ' permissions ' , true ) , access: ' owner '
2014-08-28 10:50:20 -07:00
ownerPermission ? . target
2014-04-09 16:09:35 -07:00
getDelta: ->
2014-04-11 21:11:52 -07:00
differ = deltasLib . makeJSONDiffer ( )
differ . diff @ _revertAttributes , @ attributes
2014-05-21 10:27:38 -07:00
2014-05-08 10:54:39 -07:00
getDeltaWith: (otherModel) ->
differ = deltasLib . makeJSONDiffer ( )
differ . diff @ attributes , otherModel . attributes
2014-04-13 14:48:36 -07:00
2014-04-11 21:11:52 -07:00
applyDelta: (delta) ->
newAttributes = $ . extend ( true , { } , @ attributes )
2014-06-24 09:43:14 -07:00
try
jsondiffpatch . patch newAttributes , delta
catch error
2014-07-04 20:45:42 -07:00
console . error ' Error applying delta \n ' , JSON . stringify ( delta , null , ' \t ' ) , ' \n \n to attributes \n \n ' , newAttributes
2014-06-24 17:25:01 -04:00
return false
2014-08-11 18:47:00 -07:00
for key , value of newAttributes
delete newAttributes [ key ] if _ . isEqual value , @ attributes [ key ]
2014-08-25 15:39:47 -07:00
2014-04-11 21:11:52 -07:00
@ set newAttributes
2014-06-24 17:25:01 -04:00
return true
2014-04-13 14:48:36 -07:00
2014-04-09 19:07:44 -07:00
getExpandedDelta: ->
delta = @ getDelta ( )
2014-04-12 15:16:42 +05:30
deltasLib . expandDelta ( delta , @ _revertAttributes , @ schema ( ) )
2014-04-13 14:48:36 -07:00
2014-05-08 10:54:39 -07:00
getExpandedDeltaWith: (otherModel) ->
delta = @ getDeltaWith ( otherModel )
deltasLib . expandDelta ( delta , @ attributes , @ schema ( ) )
2014-04-15 15:09:36 -07:00
watch: (doWatch=true) ->
2014-07-01 10:16:26 +08:00
$ . ajax ( " #{ @ urlRoot } / #{ @ id } /watch " , { type: ' PUT ' , data: { on : doWatch } } )
2014-04-16 11:02:40 -07:00
@watching = -> doWatch
2014-04-22 11:11:08 -07:00
2014-04-15 15:09:36 -07:00
watching: ->
2014-04-16 11:02:40 -07:00
return me . id in ( @ get ( ' watchers ' ) or [ ] )
2014-05-06 16:58:08 -07:00
2014-04-22 17:56:41 -07:00
populateI18N: (data, schema, path='') ->
# TODO: Better schema/json walking
sum = 0
data ? = $ . extend true , { } , @ attributes
schema ? = @ schema ( ) or { }
if schema . properties ? . i18n and _ . isPlainObject ( data ) and not data . i18n ?
2014-07-12 09:42:50 -07:00
data.i18n = { ' - ' : ' - ' } # mongoose doesn't work with empty objects
2014-04-22 17:56:41 -07:00
sum += 1
2014-05-06 16:58:08 -07:00
2014-04-22 17:56:41 -07:00
if _ . isPlainObject data
for key , value of data
numChanged = 0
numChanged = @ populateI18N ( value , childSchema , path + ' / ' + key ) if childSchema = schema . properties ? [ key ]
if numChanged and not path # should only do this for the root object
@ set key , value
sum += numChanged
2014-05-06 16:58:08 -07:00
2014-04-22 17:56:41 -07:00
if schema . items and _ . isArray data
sum += @ populateI18N ( value , schema . items , path + ' / ' + index ) for value , index in data
2014-05-06 16:58:08 -07:00
2014-04-22 17:56:41 -07:00
sum
2014-03-03 20:41:35 +01:00
2014-04-25 19:11:32 -07:00
@getReferencedModel: (data, schema) ->
return null unless schema . links ?
2014-07-01 10:16:26 +08:00
linkObject = _ . find schema . links , rel: ' db '
2014-04-25 19:11:32 -07:00
return null unless linkObject
2014-07-01 10:16:26 +08:00
return null if linkObject . href . match ( ' thang.type ' ) and not @ isObjectID ( data ) # Skip loading hardcoded Thang Types for now (TODO)
2014-04-25 19:11:32 -07:00
# not fully extensible, but we can worry about that later
link = linkObject . href
link = link . replace ( ' {(original)} ' , data . original )
link = link . replace ( ' {(majorVersion)} ' , ' ' + ( data . majorVersion ? 0 ) )
link = link . replace ( ' {($)} ' , data )
@ getOrMakeModelFromLink ( link )
@getOrMakeModelFromLink: (link) ->
makeUrlFunc = (url) -> -> url
modelUrl = link . split ( ' / ' ) [ 2 ]
modelModule = _ . string . classify ( modelUrl )
modulePath = " models/ #{ modelModule } "
try
Model = require modulePath
catch e
console . error ' could not load model from link path ' , link , ' using path ' , modulePath
return
model = new Model ( )
model.url = makeUrlFunc ( link )
return model
2014-05-06 16:58:08 -07:00
2014-04-26 12:54:03 -07:00
setURL: (url) ->
makeURLFunc = (u) -> -> u
@url = makeURLFunc ( url )
@
2014-05-06 16:58:08 -07:00
2014-04-26 12:54:03 -07:00
getURL: ->
return if _ . isString @ url then @ url else @ url ( )
2014-05-06 16:58:08 -07:00
2014-05-24 20:45:53 +02:00
@pollAchievements: ->
2014-07-30 22:23:43 +02:00
NewAchievementCollection = require ' ../collections/NewAchievementCollection ' # Nasty mutual inclusion if put on top
2014-05-24 20:45:53 +02:00
achievements = new NewAchievementCollection
2014-07-30 22:23:43 +02:00
achievements . fetch
2014-05-24 20:45:53 +02:00
success: (collection) ->
2014-08-27 12:24:03 -07:00
me . fetch ( success: -> Backbone . Mediator . publish ( ' achievements:new ' , earnedAchievements: collection ) ) unless _ . isEmpty ( collection . models )
2014-08-07 22:03:00 +02:00
error: ->
console . error ' Miserably failed to fetch unnotified achievements ' , arguments
2014-05-24 20:45:53 +02:00
CocoModel.pollAchievements = _ . debounce CocoModel . pollAchievements , 500
2014-01-03 10:32:13 -08:00
module.exports = CocoModel