codecombat/app/models/CocoModel.coffee

231 lines
6.9 KiB
CoffeeScript
Raw Normal View History

storage = require 'lib/storage'
deltasLib = require 'lib/deltas'
auth = require 'lib/auth'
2014-01-03 13:32:13 -05:00
class CocoModel extends Backbone.Model
idAttribute: "_id"
loaded: false
loading: false
saveBackups: false
2014-01-03 13:32:13 -05:00
@schema: null
initialize: ->
super()
if not @constructor.className
console.error("#{@} needs a className set.")
@markToRevert()
@addSchemaDefaults()
@once 'sync', @onLoaded, @
@saveBackup = _.debounce(@saveBackup, 500)
2014-02-12 15:41:41 -05:00
2014-01-03 13:32:13 -05:00
type: ->
@constructor.className
2014-04-13 17:48:36 -04:00
2014-04-12 00:11:52 -04:00
clone: (withChanges=true) ->
# Backbone does not support nested documents
clone = super()
clone.set($.extend(true, {}, if withChanges then @attributes else @_revertAttributes))
clone
2014-01-03 13:32:13 -05:00
onLoaded: ->
2014-01-03 13:32:13 -05:00
@loaded = true
@loading = false
@markToRevert()
@loadFromBackup()
2014-02-12 15:41:41 -05:00
set: ->
res = super(arguments...)
2014-02-05 14:03:32 -05:00
@saveBackup() if @saveBackups and @loaded and @hasLocalChanges()
res
2014-02-12 15:41:41 -05:00
loadFromBackup: ->
return unless @saveBackups
existing = storage.load @id
if existing
@set(existing, {silent:true})
CocoModel.backedUp[@id] = @
saveBackup: ->
storage.save(@id, @attributes)
CocoModel.backedUp[@id] = @
2014-02-12 15:41:41 -05:00
@backedUp = {}
2014-01-03 13:32:13 -05:00
schema: -> return @constructor.schema
validate: ->
2014-04-12 04:35:56 -04:00
result = tv4.validateMultiple(@attributes, @constructor.schema? or {})
2014-01-03 13:32:13 -05:00
if result.errors?.length
console.log @, "got validate result with errors:", result
return result.errors unless result.valid
save: (attrs, options) ->
@set 'editPath', document.location.pathname
2014-01-03 13:32:13 -05:00
options ?= {}
success = options.success
options.success = (resp) =>
@trigger "save:success", @
success(@, resp) if success
@markToRevert()
@clearBackup()
2014-01-03 13:32:13 -05:00
@trigger "save", @
return super attrs, options
fetch: ->
res = super(arguments...)
2014-01-03 13:32:13 -05:00
@loading = true
res
2014-01-03 13:32:13 -05:00
markToRevert: ->
if @type() is 'ThangType'
@_revertAttributes = _.clone @attributes # No deep clones for these!
else
@_revertAttributes = $.extend(true, {}, @attributes)
2014-01-03 13:32:13 -05:00
revert: ->
@set(@_revertAttributes, {silent: true}) if @_revertAttributes
@clearBackup()
2014-02-12 15:41:41 -05:00
clearBackup: ->
storage.remove @id
2014-01-03 13:32:13 -05:00
hasLocalChanges: ->
not _.isEqual @attributes, @_revertAttributes
cloneNewMinorVersion: ->
newData = $.extend(null, {}, @attributes)
clone = new @constructor(newData)
clone
2014-01-03 13:32:13 -05:00
cloneNewMajorVersion: ->
clone = @cloneNewMinorVersion()
clone.unset('version')
clone
isPublished: ->
for permission in @get('permissions') or []
return true if permission.target is 'public' and permission.access is 'read'
false
publish: ->
if @isPublished() then throw new Error("Can't publish what's already-published. Can't kill what's already dead.")
@set "permissions", (@get("permissions") or []).concat({access: 'read', target: 'public'})
addSchemaDefaults: ->
return if @addedSchemaDefaults
2014-01-03 13:32:13 -05:00
@addedSchemaDefaults = true
2014-04-12 04:35:56 -04:00
for prop, defaultValue of @constructor.schema.default or {}
2014-01-03 13:32:13 -05:00
continue if @get(prop)?
#console.log "setting", prop, "to", defaultValue, "from attributes.default"
@set prop, defaultValue
2014-04-12 04:35:56 -04:00
for prop, sch of @constructor.schema.properties or {}
2014-01-03 13:32:13 -05:00
continue if @get(prop)?
continue if prop is 'emails' # hack, defaults are handled through User.coffee's email-specific methods.
2014-01-03 13:32:13 -05:00
#console.log "setting", prop, "to", sch.default, "from sch.default" if sch.default?
@set prop, sch.default if sch.default?
if @loaded
@markToRevert()
@loadFromBackup()
2014-01-03 13:32:13 -05:00
getReferencedModels: (data, schema, path='/', shouldLoadProjection=null) ->
2014-01-03 13:32:13 -05:00
# returns unfetched model shells for every referenced doc in this model
# OPTIMIZE so that when loading models, it doesn't cause the site to stutter
data ?= @attributes
2014-04-12 04:35:56 -04:00
schema ?= @schema()
2014-01-03 13:32:13 -05:00
models = []
if $.isArray(data) and schema.items?
for subData, i in data
models = models.concat(@getReferencedModels(subData, schema.items, path+i+'/', shouldLoadProjection))
2014-01-03 13:32:13 -05:00
if $.isPlainObject(data) and schema.properties?
for key, subData of data
continue unless schema.properties[key]
models = models.concat(@getReferencedModels(subData, schema.properties[key], path+key+'/', shouldLoadProjection))
2014-01-03 13:32:13 -05:00
model = CocoModel.getReferencedModel data, schema, shouldLoadProjection
2014-01-03 13:32:13 -05:00
models.push model if model
return models
@getReferencedModel: (data, schema, shouldLoadProjection=null) ->
2014-01-03 13:32:13 -05:00
return null unless schema.links?
linkObject = _.find schema.links, rel: "db"
return null unless linkObject
return null if linkObject.href.match("thang.type") and not @isObjectID(data) # Skip loading hardcoded Thang Types for now (TODO)
2014-01-03 13:32:13 -05: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, shouldLoadProjection)
2014-01-03 13:32:13 -05:00
@getOrMakeModelFromLink: (link, shouldLoadProjection=null) ->
2014-01-03 13:32:13 -05:00
makeUrlFunc = (url) -> -> url
modelUrl = link.split('/')[2]
modelModule = _.string.classify(modelUrl)
modulePath = "models/#{modelModule}"
window.loadedModels ?= {}
try
Model = require modulePath
window.loadedModels[modulePath] = Model
catch e
console.error 'could not load model from link path', link, 'using path', modulePath
return
model = new Model()
if shouldLoadProjection? model
sep = if link.search(/\?/) is -1 then "?" else "&"
link += sep + "project=true"
2014-01-03 13:32:13 -05:00
model.url = makeUrlFunc(link)
return model
@isObjectID: (s) ->
s.length is 24 and s.match(/[a-f0-9]/gi)?.length is 24
2014-01-03 13:32:13 -05:00
hasReadAccess: (actor) ->
# actor is a User object
actor ?= auth.me
return true if actor.isAdmin()
if @get('permissions')?
for permission in @get('permissions')
if permission.target is 'public' or actor.get('_id') is permission.target
return true if permission.access in ['owner', 'read']
return false
hasWriteAccess: (actor) ->
# actor is a User object
actor ?= auth.me
return true if actor.isAdmin()
if @get('permissions')?
for permission in @get('permissions')
if permission.target is 'public' or actor.get('_id') is permission.target
return true if permission.access in ['owner', 'write']
return false
2014-04-13 17:48:36 -04:00
getDelta: ->
2014-04-12 00:11:52 -04:00
differ = deltasLib.makeJSONDiffer()
differ.diff @_revertAttributes, @attributes
2014-04-13 17:48:36 -04:00
2014-04-12 00:11:52 -04:00
applyDelta: (delta) ->
newAttributes = $.extend(true, {}, @attributes)
jsondiffpatch.patch newAttributes, delta
@set newAttributes
2014-04-13 17:48:36 -04:00
getExpandedDelta: ->
delta = @getDelta()
deltasLib.expandDelta(delta, @_revertAttributes, @schema())
2014-04-13 17:48:36 -04:00
watch: (doWatch=true) ->
$.ajax("#{@urlRoot}/#{@id}/watch", {type:'PUT', data:{on:doWatch}})
@watching = -> doWatch
watching: ->
return me.id in (@get('watchers') or [])
2014-01-03 13:32:13 -05:00
module.exports = CocoModel