codecombat/app/models/CocoModel.coffee

365 lines
12 KiB
CoffeeScript
Raw Normal View History

storage = require 'lib/storage'
deltasLib = require 'lib/deltas'
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
saveBackups: false
notyErrors: true
2014-01-03 13:32:13 -05:00
@schema: null
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.")
@on 'sync', @onLoaded, @
@on 'error', @onError, @
@on 'add', @onLoaded, @
@saveBackup = _.debounce(@saveBackup, 500)
console.debug = console.log unless console.debug # Needed for IE10 and earlier
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
onError: ->
@loading = false
@jqxhr = null
2014-01-03 13:32:13 -05:00
onLoaded: ->
2014-01-03 13:32:13 -05:00
@loaded = true
@loading = false
@jqxhr = null
@loadFromBackup()
getNormalizedURL: -> "#{@urlRoot}/#{@id}"
attributesWithDefaults: undefined
get: (attribute, withDefault=false) ->
if withDefault
if @attributesWithDefaults is undefined then @buildAttributesWithDefaults()
return @attributesWithDefaults[attribute]
else
super(attribute)
set: (attributes, options) ->
delete @attributesWithDefaults unless attributes is 'thangs' # unless attributes is 'thangs': performance optimization for Levels keeping their cache.
inFlux = @loading or not @loaded
@markToRevert() unless inFlux or @_revertAttributes or @project or options?.fromMerge
res = super attributes, options
@saveBackup() if @saveBackups and (not inFlux)
res
2014-02-12 15:41:41 -05: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)
@attributesWithDefaults = clone
duration = new Date() - t0
console.debug "Populated defaults for #{@type()}#{if @attributes.name then ' ' + @attributes.name else ''} in #{duration}ms" if duration > 10
loadFromBackup: ->
return unless @saveBackups
existing = storage.load @id
if existing
2014-06-30 22:16:26 -04:00
@set(existing, {silent: true})
CocoModel.backedUp[@id] = @
saveBackup: -> @saveBackupNow()
saveBackupNow: ->
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
2014-06-24 12:43:14 -04:00
getValidationErrors: ->
2014-09-08 19:03:29 -04: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
return errors if errors?.length
2014-01-03 13:32:13 -05:00
validate: ->
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
console.trace?()
return errors
2014-06-24 12:43:14 -04:00
2014-01-03 13:32:13 -05:00
save: (attrs, options) ->
options ?= {}
options.headers ?= {}
options.headers['X-Current-Path'] = document.location?.pathname ? 'unknown'
2014-01-03 13:32:13 -05:00
success = options.success
error = options.error
options.success = (model, res) =>
2014-06-30 22:16:26 -04:00
@trigger 'save:success', @
success(@, res) if success
@markToRevert() if @_revertAttributes
@clearBackup()
CocoModel.pollAchievements()
options.success = options.error = null # So the callbacks can be garbage-collected.
options.error = (model, res) =>
error(@, res) if error
return unless @notyErrors
errorMessage = "Error saving #{@get('name') ? @type()}"
console.log 'going to log an error message'
console.warn errorMessage, res.responseJSON
2014-10-02 01:02:52 -04:00
unless webkit?.messageHandlers # Don't show these notys on iPad
try
noty text: "#{errorMessage}: #{res.status} #{res.statusText}", layout: 'topCenter', type: 'error', killer: false, timeout: 10000
catch notyError
console.warn "Couldn't even show noty error for", error, "because", notyError
options.success = options.error = null # So the callbacks can be garbage-collected.
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
patch: (options) ->
return false unless @_revertAttributes
options ?= {}
options.patch = true
2014-06-24 12:43:14 -04:00
attrs = {_id: @id}
keys = []
for key in _.keys @attributes
unless _.isEqual @attributes[key], @_revertAttributes[key]
attrs[key] = @attributes[key]
keys.push key
2014-06-24 12:43:14 -04:00
return unless keys.length
console.debug 'Patching', @get('name') or @, keys
@save(attrs, options)
2014-01-03 13:32:13 -05: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
@jqxhr
2014-01-03 13:32:13 -05:00
markToRevert: ->
if @type() is 'ThangType'
# 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
else
@_revertAttributes = $.extend(true, {}, @attributes)
2014-01-03 13:32:13 -05:00
revert: ->
@clear({silent: true})
2014-01-03 13:32:13 -05:00
@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: ->
@_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
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', 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.')
@set 'permissions', @get('permissions', true).concat({access: 'read', target: 'public'})
2014-01-03 13:32:13 -05:00
@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 ?= me
return true if actor.isAdmin()
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']
return false
hasWriteAccess: (actor) ->
# actor is a User object
actor ?= me
return true if actor.isAdmin()
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']
return false
2014-04-13 17:48:36 -04:00
2014-08-28 13:50:20 -04:00
getOwner: ->
ownerPermission = _.find @get('permissions', true), access: 'owner'
2014-08-28 13:50:20 -04:00
ownerPermission?.target
getDelta: ->
2014-04-12 00:11:52 -04:00
differ = deltasLib.makeJSONDiffer()
differ.diff @_revertAttributes, @attributes
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
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-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
getExpandedDelta: ->
delta = @getDelta()
deltasLib.expandDelta(delta, @_revertAttributes, @schema())
2014-04-13 17:48:36 -04:00
getExpandedDeltaWith: (otherModel) ->
delta = @getDeltaWith(otherModel)
deltasLib.expandDelta(delta, @attributes, @schema())
watch: (doWatch=true) ->
2014-06-30 22:16:26 -04:00
$.ajax("#{@urlRoot}/#{@id}/watch", {type: 'PUT', data: {on: doWatch}})
@watching = -> doWatch
watching: ->
return me.id in (@get('watchers') or [])
2014-05-06 19:58:08 -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?
data.i18n = {'-':'-'} # mongoose doesn't work with empty objects
sum += 1
2014-05-06 19:58:08 -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
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-10-17 12:12:06 -04:00
@updateI18NCoverage()
sum
@getReferencedModel: (data, schema) ->
return null unless schema.links?
2014-06-30 22:16:26 -04:00
linkObject = _.find schema.links, rel: 'db'
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)
# 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
setURL: (url) ->
makeURLFunc = (u) -> -> u
@url = makeURLFunc(url)
@
2014-05-06 19:58:08 -04:00
getURL: ->
return if _.isString @url then @url else @url()
2014-05-06 19:58:08 -04:00
@pollAchievements: ->
NewAchievementCollection = require '../collections/NewAchievementCollection' # Nasty mutual inclusion if put on top
achievements = new NewAchievementCollection
achievements.fetch
success: (collection) ->
me.fetch (success: -> Backbone.Mediator.publish('achievements:new', earnedAchievements: collection)) unless _.isEmpty(collection.models)
error: ->
console.error 'Miserably failed to fetch unnotified achievements', arguments
CocoModel.pollAchievements = _.debounce CocoModel.pollAchievements, 500
#- Internationalization
updateI18NCoverage: ->
i18nObjects = @findI18NObjects()
console.log 'i18n objects', i18nObjects
langCodeArrays = (_.keys(i18n) for i18n in i18nObjects)
console.log 'lang code arrays', langCodeArrays
window.codes = langCodeArrays
@set('i18nCoverage', _.intersection(langCodeArrays...))
findI18NObjects: (data, results) ->
data ?= @attributes
results ?= []
if _.isPlainObject(data) or _.isArray(data)
for [key, value] in _.pairs data
if key is 'i18n'
results.push value
else if _.isPlainObject(value) or _.isArray(value)
@findI18NObjects(value, results)
return results
2014-01-03 13:32:13 -05:00
module.exports = CocoModel