2014-01-26 17:46:25 -05:00
|
|
|
storage = require 'lib/storage'
|
2014-04-09 22:07:44 -04:00
|
|
|
deltasLib = require 'lib/deltas'
|
2014-01-26 17:46:25 -05:00
|
|
|
|
2014-01-03 13:32:13 -05:00
|
|
|
class CocoModel extends Backbone.Model
|
2014-06-30 22:16:26 -04:00
|
|
|
idAttribute: '_id'
|
2014-01-03 13:32:13 -05:00
|
|
|
loaded: false
|
|
|
|
loading: false
|
2014-01-26 17:46:25 -05:00
|
|
|
saveBackups: false
|
2014-05-22 14:22:52 -04:00
|
|
|
notyErrors: true
|
2014-01-03 13:32:13 -05:00
|
|
|
@schema: null
|
|
|
|
|
2014-07-17 19:22:06 -04:00
|
|
|
initialize: (attributes, options) ->
|
|
|
|
super(arguments...)
|
|
|
|
options ?= {}
|
|
|
|
@setProjection options.project
|
2014-01-03 13:32:13 -05:00
|
|
|
if not @constructor.className
|
|
|
|
console.error("#{@} needs a className set.")
|
2014-05-20 00:14:27 -04:00
|
|
|
@on 'sync', @onLoaded, @
|
2014-04-25 18:31:38 -04:00
|
|
|
@on 'error', @onError, @
|
2014-05-30 19:20:20 -04:00
|
|
|
@on 'add', @onLoaded, @
|
2014-01-26 17:46:25 -05:00
|
|
|
@saveBackup = _.debounce(@saveBackup, 500)
|
2014-08-25 18:39:47 -04: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 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-05-06 19:58:08 -04:00
|
|
|
|
2014-04-25 18:31:38 -04:00
|
|
|
onError: ->
|
|
|
|
@loading = false
|
2014-05-27 19:33:57 -04:00
|
|
|
@jqxhr = null
|
2014-01-03 13:32:13 -05:00
|
|
|
|
2014-01-26 17:46:25 -05:00
|
|
|
onLoaded: ->
|
2014-01-03 13:32:13 -05:00
|
|
|
@loaded = true
|
|
|
|
@loading = false
|
2014-05-27 19:33:57 -04:00
|
|
|
@jqxhr = null
|
2014-04-12 13:51:02 -04:00
|
|
|
@loadFromBackup()
|
2014-05-21 13:27:38 -04:00
|
|
|
|
2014-05-02 13:31:20 -04:00
|
|
|
getNormalizedURL: -> "#{@urlRoot}/#{@id}"
|
2014-09-01 12:11:10 -04:00
|
|
|
|
2014-08-23 14:07:52 -04:00
|
|
|
attributesWithDefaults: undefined
|
2014-09-01 12:11:10 -04:00
|
|
|
|
2014-08-23 14:07:52 -04:00
|
|
|
get: (attribute, withDefault=false) ->
|
|
|
|
if withDefault
|
|
|
|
if @attributesWithDefaults is undefined then @buildAttributesWithDefaults()
|
|
|
|
return @attributesWithDefaults[attribute]
|
|
|
|
else
|
|
|
|
super(attribute)
|
2014-09-01 12:11:10 -04:00
|
|
|
|
2014-09-02 21:28:02 -04:00
|
|
|
set: (attributes, options) ->
|
2014-08-23 14:07:52 -04:00
|
|
|
delete @attributesWithDefaults
|
2014-05-30 19:20:20 -04:00
|
|
|
inFlux = @loading or not @loaded
|
2014-09-03 13:58:43 -04:00
|
|
|
@markToRevert() unless inFlux or @_revertAttributes or @project or options?.fromMerge
|
2014-09-02 21:28:02 -04:00
|
|
|
res = super attributes, options
|
2014-05-30 19:20:20 -04:00
|
|
|
@saveBackup() if @saveBackups and (not inFlux) and @hasLocalChanges()
|
2014-01-26 17:46:25 -05:00
|
|
|
res
|
2014-02-12 15:41:41 -05:00
|
|
|
|
2014-08-23 14:07:52 -04:00
|
|
|
buildAttributesWithDefaults: ->
|
|
|
|
t0 = new Date()
|
|
|
|
clone = $.extend true, {}, @attributes
|
2014-08-28 18:54:05 -04:00
|
|
|
thisTV4 = tv4.freshApi()
|
|
|
|
thisTV4.addSchema('#', @schema())
|
|
|
|
thisTV4.addSchema('metaschema', require('schemas/metaschema'))
|
|
|
|
TreemaNode.utils.populateDefaults(clone, @schema(), thisTV4)
|
2014-08-23 14:07:52 -04:00
|
|
|
@attributesWithDefaults = clone
|
|
|
|
console.debug "Populated defaults for #{@attributes.name or @type()} in #{new Date() - t0}ms"
|
|
|
|
|
2014-03-23 13:29:08 -04:00
|
|
|
loadFromBackup: ->
|
|
|
|
return unless @saveBackups
|
|
|
|
existing = storage.load @id
|
|
|
|
if existing
|
2014-06-30 22:16:26 -04:00
|
|
|
@set(existing, {silent: true})
|
2014-03-23 13:29:08 -04:00
|
|
|
CocoModel.backedUp[@id] = @
|
|
|
|
|
2014-07-03 20:41:34 -04:00
|
|
|
saveBackup: -> @saveBackupNow()
|
2014-08-25 18:39:47 -04:00
|
|
|
|
2014-07-03 20:41:34 -04:00
|
|
|
saveBackupNow: ->
|
2014-01-26 17:46:25 -05:00
|
|
|
storage.save(@id, @attributes)
|
|
|
|
CocoModel.backedUp[@id] = @
|
2014-02-12 15:41:41 -05:00
|
|
|
|
2014-01-26 17:46:25 -05:00
|
|
|
@backedUp = {}
|
2014-01-03 13:32:13 -05:00
|
|
|
schema: -> return @constructor.schema
|
2014-06-24 12:43:14 -04:00
|
|
|
|
2014-05-30 20:24:53 -04:00
|
|
|
getValidationErrors: ->
|
|
|
|
errors = tv4.validateMultiple(@attributes, @constructor.schema or {}).errors
|
|
|
|
return errors if errors?.length
|
2014-01-03 13:32:13 -05:00
|
|
|
|
|
|
|
validate: ->
|
2014-05-30 20:24:53 -04:00
|
|
|
errors = @getValidationErrors()
|
|
|
|
if errors?.length
|
|
|
|
console.debug "Validation failed for #{@constructor.className}: '#{@get('name') or @}'."
|
|
|
|
for error in errors
|
2014-06-30 22:16:26 -04:00
|
|
|
console.debug "\t", error.dataPath, ':', error.message
|
2014-05-30 20:24:53 -04:00
|
|
|
return errors
|
2014-06-24 12:43:14 -04:00
|
|
|
|
2014-01-03 13:32:13 -05:00
|
|
|
save: (attrs, options) ->
|
|
|
|
options ?= {}
|
2014-05-30 17:41:41 -04:00
|
|
|
options.headers ?= {}
|
2014-09-08 17:31:52 -04:00
|
|
|
options.headers['X-Current-Path'] = document.location?.pathname ? 'unknown'
|
2014-01-03 13:32:13 -05:00
|
|
|
success = options.success
|
2014-05-21 13:27:38 -04:00
|
|
|
error = options.error
|
|
|
|
options.success = (model, res) =>
|
2014-06-30 22:16:26 -04:00
|
|
|
@trigger 'save:success', @
|
2014-05-21 13:27:38 -04:00
|
|
|
success(@, res) if success
|
2014-05-30 19:20:20 -04:00
|
|
|
@markToRevert() if @_revertAttributes
|
2014-01-26 17:46:25 -05:00
|
|
|
@clearBackup()
|
2014-05-24 14:45:53 -04:00
|
|
|
CocoModel.pollAchievements()
|
2014-05-21 13:27:38 -04:00
|
|
|
options.error = (model, res) =>
|
|
|
|
error(@, res) if error
|
2014-05-22 14:22:52 -04:00
|
|
|
return unless @notyErrors
|
2014-05-21 13:27:38 -04: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-06-30 22:16:26 -04:00
|
|
|
@trigger 'save', @
|
2014-01-03 13:32:13 -05:00
|
|
|
return super attrs, options
|
2014-06-24 12:43:14 -04:00
|
|
|
|
2014-06-10 23:43:25 -04:00
|
|
|
patch: (options) ->
|
|
|
|
return false unless @_revertAttributes
|
|
|
|
options ?= {}
|
|
|
|
options.patch = true
|
2014-06-24 12:43:14 -04:00
|
|
|
|
2014-06-10 23:43:25 -04:00
|
|
|
attrs = {_id: @id}
|
2014-06-11 17:17:31 -04:00
|
|
|
keys = []
|
2014-06-10 23:43:25 -04:00
|
|
|
for key in _.keys @attributes
|
|
|
|
unless _.isEqual @attributes[key], @_revertAttributes[key]
|
|
|
|
attrs[key] = @attributes[key]
|
2014-06-11 17:17:31 -04:00
|
|
|
keys.push key
|
2014-06-24 12:43:14 -04:00
|
|
|
|
2014-06-11 17:17:31 -04:00
|
|
|
return unless keys.length
|
|
|
|
console.debug 'Patching', @get('name') or @, keys
|
2014-06-10 23:43:25 -04:00
|
|
|
@save(attrs, options)
|
2014-01-03 13:32:13 -05:00
|
|
|
|
2014-07-17 19:22:06 -04:00
|
|
|
fetch: (options) ->
|
|
|
|
options ?= {}
|
|
|
|
options.data ?= {}
|
|
|
|
options.data.project = @project.join(',') if @project
|
|
|
|
@jqxhr = super(options)
|
2014-01-03 13:32:13 -05:00
|
|
|
@loading = true
|
2014-05-02 13:31:20 -04:00
|
|
|
@jqxhr
|
2014-01-03 13:32:13 -05:00
|
|
|
|
2014-05-30 19:20:20 -04:00
|
|
|
markToRevert: ->
|
2014-03-23 13:00:16 -04:00
|
|
|
if @type() is 'ThangType'
|
2014-09-02 20:46:52 -04: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 13:00:16 -04:00
|
|
|
else
|
2014-03-21 22:21:23 -04:00
|
|
|
@_revertAttributes = $.extend(true, {}, @attributes)
|
2014-01-03 13:32:13 -05:00
|
|
|
|
|
|
|
revert: ->
|
2014-08-26 13:14:36 -04:00
|
|
|
@clear({silent: true})
|
2014-01-03 13:32:13 -05:00
|
|
|
@set(@_revertAttributes, {silent: true}) if @_revertAttributes
|
2014-01-26 17:46:25 -05:00
|
|
|
@clearBackup()
|
2014-02-12 15:41:41 -05:00
|
|
|
|
2014-01-26 17:46:25 -05:00
|
|
|
clearBackup: ->
|
|
|
|
storage.remove @id
|
2014-01-03 13:32:13 -05:00
|
|
|
|
|
|
|
hasLocalChanges: ->
|
2014-05-29 13:48:48 -04:00
|
|
|
@_revertAttributes and not _.isEqual @attributes, @_revertAttributes
|
2014-01-03 13:32:13 -05:00
|
|
|
|
|
|
|
cloneNewMinorVersion: ->
|
2014-05-05 23:07:34 -04:00
|
|
|
newData = _.clone @attributes
|
2014-04-12 01:33:09 -04:00
|
|
|
clone = new @constructor(newData)
|
|
|
|
clone
|
2014-01-03 13:32:13 -05:00
|
|
|
|
|
|
|
cloneNewMajorVersion: ->
|
|
|
|
clone = @cloneNewMinorVersion()
|
|
|
|
clone.unset('version')
|
|
|
|
clone
|
|
|
|
|
|
|
|
isPublished: ->
|
2014-09-01 12:11:10 -04:00
|
|
|
for permission in (@get('permissions', true) ? [])
|
2014-01-03 13:32:13 -05:00
|
|
|
return true if permission.target is 'public' and permission.access is 'read'
|
|
|
|
false
|
|
|
|
|
|
|
|
publish: ->
|
2014-06-30 22:16:26 -04:00
|
|
|
if @isPublished() then throw new Error('Can\'t publish what\'s already-published. Can\'t kill what\'s already dead.')
|
2014-09-01 12:11:10 -04:00
|
|
|
@set 'permissions', @get('permissions', true).concat({access: 'read', target: 'public'})
|
2014-01-03 13:32:13 -05:00
|
|
|
|
|
|
|
@isObjectID: (s) ->
|
2014-04-11 23:38:34 -04:00
|
|
|
s.length is 24 and s.match(/[a-f0-9]/gi)?.length is 24
|
2014-01-03 13:32:13 -05:00
|
|
|
|
2014-03-03 14:41:35 -05:00
|
|
|
hasReadAccess: (actor) ->
|
|
|
|
# actor is a User object
|
2014-08-23 14:07:52 -04:00
|
|
|
actor ?= me
|
2014-04-10 16:09:44 -04:00
|
|
|
return true if actor.isAdmin()
|
2014-09-01 12:11:10 -04: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 14:41:35 -05:00
|
|
|
|
|
|
|
return false
|
|
|
|
|
2014-03-03 15:13:02 -05:00
|
|
|
hasWriteAccess: (actor) ->
|
|
|
|
# actor is a User object
|
2014-08-23 14:07:52 -04:00
|
|
|
actor ?= me
|
2014-04-10 16:09:44 -04:00
|
|
|
return true if actor.isAdmin()
|
2014-09-01 12:11:10 -04: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 14:41:35 -05:00
|
|
|
|
|
|
|
return false
|
2014-04-13 17:48:36 -04:00
|
|
|
|
2014-08-28 13:50:20 -04:00
|
|
|
getOwner: ->
|
2014-09-01 12:11:10 -04:00
|
|
|
ownerPermission = _.find @get('permissions', true), access: 'owner'
|
2014-08-28 13:50:20 -04:00
|
|
|
ownerPermission?.target
|
|
|
|
|
2014-04-09 19:09:35 -04:00
|
|
|
getDelta: ->
|
2014-04-12 00:11:52 -04:00
|
|
|
differ = deltasLib.makeJSONDiffer()
|
|
|
|
differ.diff @_revertAttributes, @attributes
|
2014-05-21 13:27:38 -04:00
|
|
|
|
2014-05-08 13:54:39 -04:00
|
|
|
getDeltaWith: (otherModel) ->
|
|
|
|
differ = deltasLib.makeJSONDiffer()
|
|
|
|
differ.diff @attributes, otherModel.attributes
|
2014-04-13 17:48:36 -04:00
|
|
|
|
2014-04-12 00:11:52 -04:00
|
|
|
applyDelta: (delta) ->
|
|
|
|
newAttributes = $.extend(true, {}, @attributes)
|
2014-06-24 12:43:14 -04:00
|
|
|
try
|
|
|
|
jsondiffpatch.patch newAttributes, delta
|
|
|
|
catch error
|
2014-07-04 23:45:42 -04:00
|
|
|
console.error 'Error applying delta\n', JSON.stringify(delta, null, '\t'), '\n\nto attributes\n\n', newAttributes
|
2014-06-24 17:25:01 -04:00
|
|
|
return false
|
2014-08-11 21:47:00 -04:00
|
|
|
for key, value of newAttributes
|
|
|
|
delete newAttributes[key] if _.isEqual value, @attributes[key]
|
2014-08-25 18:39:47 -04:00
|
|
|
|
2014-04-12 00:11:52 -04:00
|
|
|
@set newAttributes
|
2014-06-24 17:25:01 -04:00
|
|
|
return true
|
2014-04-13 17:48:36 -04:00
|
|
|
|
2014-04-09 22:07:44 -04:00
|
|
|
getExpandedDelta: ->
|
|
|
|
delta = @getDelta()
|
2014-04-12 05:46:42 -04:00
|
|
|
deltasLib.expandDelta(delta, @_revertAttributes, @schema())
|
2014-04-13 17:48:36 -04:00
|
|
|
|
2014-05-08 13:54:39 -04:00
|
|
|
getExpandedDeltaWith: (otherModel) ->
|
|
|
|
delta = @getDeltaWith(otherModel)
|
|
|
|
deltasLib.expandDelta(delta, @attributes, @schema())
|
|
|
|
|
2014-04-15 18:09:36 -04:00
|
|
|
watch: (doWatch=true) ->
|
2014-06-30 22:16:26 -04:00
|
|
|
$.ajax("#{@urlRoot}/#{@id}/watch", {type: 'PUT', data: {on: doWatch}})
|
2014-04-16 14:02:40 -04:00
|
|
|
@watching = -> doWatch
|
2014-04-22 14:11:08 -04:00
|
|
|
|
2014-04-15 18:09:36 -04:00
|
|
|
watching: ->
|
2014-04-16 14:02:40 -04:00
|
|
|
return me.id in (@get('watchers') or [])
|
2014-05-06 19:58:08 -04:00
|
|
|
|
2014-04-22 20:56:41 -04: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 12:42:50 -04:00
|
|
|
data.i18n = {'-':'-'} # mongoose doesn't work with empty objects
|
2014-04-22 20:56:41 -04:00
|
|
|
sum += 1
|
2014-05-06 19:58:08 -04:00
|
|
|
|
2014-04-22 20:56:41 -04: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 19:58:08 -04:00
|
|
|
|
2014-04-22 20:56:41 -04:00
|
|
|
if schema.items and _.isArray data
|
|
|
|
sum += @populateI18N(value, schema.items, path+'/'+index) for value, index in data
|
2014-05-06 19:58:08 -04:00
|
|
|
|
2014-04-22 20:56:41 -04:00
|
|
|
sum
|
2014-03-03 14:41:35 -05:00
|
|
|
|
2014-04-25 22:11:32 -04:00
|
|
|
@getReferencedModel: (data, schema) ->
|
|
|
|
return null unless schema.links?
|
2014-06-30 22:16:26 -04:00
|
|
|
linkObject = _.find schema.links, rel: 'db'
|
2014-04-25 22:11:32 -04:00
|
|
|
return null unless linkObject
|
2014-06-30 22:16:26 -04:00
|
|
|
return null if linkObject.href.match('thang.type') and not @isObjectID(data) # Skip loading hardcoded Thang Types for now (TODO)
|
2014-04-25 22:11:32 -04: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 19:58:08 -04:00
|
|
|
|
2014-04-26 15:54:03 -04:00
|
|
|
setURL: (url) ->
|
|
|
|
makeURLFunc = (u) -> -> u
|
|
|
|
@url = makeURLFunc(url)
|
|
|
|
@
|
2014-05-06 19:58:08 -04:00
|
|
|
|
2014-04-26 15:54:03 -04:00
|
|
|
getURL: ->
|
|
|
|
return if _.isString @url then @url else @url()
|
2014-05-06 19:58:08 -04:00
|
|
|
|
2014-05-24 14:45:53 -04:00
|
|
|
@pollAchievements: ->
|
2014-07-30 16:23:43 -04:00
|
|
|
NewAchievementCollection = require '../collections/NewAchievementCollection' # Nasty mutual inclusion if put on top
|
2014-05-24 14:45:53 -04:00
|
|
|
achievements = new NewAchievementCollection
|
2014-07-30 16:23:43 -04:00
|
|
|
achievements.fetch
|
2014-05-24 14:45:53 -04:00
|
|
|
success: (collection) ->
|
2014-08-27 15:24:03 -04:00
|
|
|
me.fetch (success: -> Backbone.Mediator.publish('achievements:new', earnedAchievements: collection)) unless _.isEmpty(collection.models)
|
2014-08-07 16:03:00 -04:00
|
|
|
error: ->
|
|
|
|
console.error 'Miserably failed to fetch unnotified achievements', arguments
|
2014-05-24 14:45:53 -04:00
|
|
|
|
|
|
|
CocoModel.pollAchievements = _.debounce CocoModel.pollAchievements, 500
|
|
|
|
|
2014-01-03 13:32:13 -05:00
|
|
|
module.exports = CocoModel
|