Added data merge conflict handling.

This commit is contained in:
Scott Erickson 2014-04-11 21:11:52 -07:00
parent 177dd2c8cd
commit 34bf484bf2
14 changed files with 346 additions and 157 deletions

View file

@ -1,75 +1,158 @@
# path: an array of indexes to navigate into a JSON object
# left:
###
Good-to-knows:
dataPath: an array of keys that walks you up a JSON object that's being patched
ex: ['scripts', 0, 'description']
deltaPath: an array of keys that walks you up a JSON Diff Patch object.
ex: ['scripts', '_0', 'description']
###
module.exports.expandDelta = (delta, left, schema) ->
flattenedDeltas = flattenDelta(delta)
(expandFlattenedDelta(fd, left, schema) for fd in flattenedDeltas)
module.exports.interpretDelta = (delta, path, left, schema) ->
# takes a single delta and converts into an object that can be
flattenDelta = (delta, dataPath=null, deltaPath=null) ->
# takes a single jsondiffpatch delta and returns an array of objects with
return [] unless delta
dataPath ?= []
deltaPath ?= []
return [{dataPath:dataPath, deltaPath: deltaPath, o:delta}] if _.isArray delta
results = []
affectingArray = delta._t is 'a'
for deltaIndex, childDelta of delta
continue if deltaIndex is '_t'
dataIndex = if affectingArray then parseInt(deltaIndex.replace('_', '')) else deltaIndex
results = results.concat flattenDelta(
childDelta, dataPath.concat([dataIndex]), deltaPath.concat([deltaIndex]))
results
expandFlattenedDelta = (delta, left, schema) ->
# takes a single flattened delta and converts into an object that can be
# easily formatted into something human readable.
delta.action = '???'
o = delta.o # the raw jsondiffpatch delta
betterDelta = { action:'???', delta: delta }
if _.isArray(o) and o.length is 1
delta.action = 'added'
delta.newValue = o[0]
if _.isArray(delta) and delta.length is 1
betterDelta.action = 'added'
betterDelta.newValue = delta[0]
if _.isArray(o) and o.length is 2
delta.action = 'modified'
delta.oldValue = o[0]
delta.newValue = o[1]
if _.isArray(delta) and delta.length is 2
betterDelta.action = 'modified'
betterDelta.oldValue = delta[0]
betterDelta.newValue = delta[1]
if _.isArray(o) and o.length is 3 and o[1] is 0 and o[2] is 0
delta.action = 'deleted'
delta.oldValue = o[0]
if _.isArray(delta) and delta.length is 3 and delta[1] is 0 and delta[2] is 0
betterDelta.action = 'deleted'
betterDelta.oldValue = delta[0]
if _.isPlainObject(o) and o._t is 'a'
delta.action = 'modified-array'
if _.isPlainObject(delta) and delta._t is 'a'
betterDelta.action = 'modified-array'
if _.isPlainObject(o) and o._t isnt 'a'
delta.action = 'modified-object'
if _.isPlainObject(delta) and delta._t isnt 'a'
betterDelta.action = 'modified-object'
if _.isArray(o) and o.length is 3 and o[1] is 0 and o[2] is 3
delta.action = 'moved-index'
delta.destinationIndex = o[1]
delta.originalIndex = delta.dataPath[delta.dataPath.length-1]
if _.isArray(delta) and delta.length is 3 and delta[1] is 0 and delta[2] is 3
betterDelta.action = 'moved-index'
betterDelta.destinationIndex = delta[1]
if _.isArray(o) and o.length is 3 and o[1] is 0 and o[2] is 2
delta.action = 'text-diff'
delta.unidiff = o[0]
if _.isArray(delta) and delta.length is 3 and delta[1] is 0 and delta[2] is 2
betterDelta.action = 'text-diff'
betterDelta.unidiff = delta[0]
betterPath = []
humanPath = []
parentLeft = left
parentSchema = schema
for key, i in path
# TODO: A smarter way of getting child schemas
for key, i in delta.dataPath
# TODO: A more comprehensive way of getting child schemas
childSchema = parentSchema?.items or parentSchema?.properties?[key] or {}
childLeft = parentLeft?[key]
betterKey = null
childData = if i is path.length-1 and betterDelta.action is 'added' then delta[0] else childLeft
betterKey ?= childData.name or childData.id if childData
betterKey ?= "#{childSchema.title} ##{key+1}" if childSchema.title and _.isNumber(key)
betterKey ?= "#{childSchema.title}" if childSchema.title
betterKey ?= _.string.titleize key
betterPath.push betterKey
humanKey = null
childData = if i is delta.dataPath.length-1 and delta.action is 'added' then o[0] else childLeft
humanKey ?= childData.name or childData.id if childData
humanKey ?= "#{childSchema.title} ##{key+1}" if childSchema.title and _.isNumber(key)
humanKey ?= "#{childSchema.title}" if childSchema.title
humanKey ?= _.string.titleize key
humanPath.push humanKey
parentLeft = childLeft
parentSchema = childSchema
betterDelta.path = betterPath.join(' :: ')
betterDelta.schema = childSchema
betterDelta.left = childLeft
betterDelta.right = jsondiffpatch.patch childLeft, delta unless betterDelta.action is 'moved-index'
delta.humanPath = humanPath.join(' :: ')
delta.schema = childSchema
delta.left = childLeft
delta.right = jsondiffpatch.patch childLeft, delta.o unless delta.action is 'moved-index'
betterDelta
delta
module.exports.flattenDelta = flattenDelta = (delta, path=null) ->
# takes a single delta and returns an array of deltas
return [] unless delta
module.exports.makeJSONDiffer = ->
hasher = (obj) -> obj.name || obj.id || obj._id || JSON.stringify(_.keys(obj))
jsondiffpatch.create({objectHash:hasher})
module.exports.getConflicts = (headDeltas, pendingDeltas) ->
# headDeltas and pendingDeltas should be lists of deltas returned by interpretDelta
# Returns a list of conflict objects with properties:
# headDelta
# pendingDelta
# The deltas that have conflicts also have conflict properties pointing to one another.
path ?= []
headPathMap = groupDeltasByAffectingPaths(headDeltas)
pendingPathMap = groupDeltasByAffectingPaths(pendingDeltas)
paths = _.keys(headPathMap).concat(_.keys(pendingPathMap))
return [{path:path, delta:delta}] if _.isArray delta
# Here's my thinking:
# A) Conflicts happen when one delta path is a substring of another delta path
# B) A delta from one self-consistent group cannot conflict with another
# So, sort the paths, which will naturally make conflicts adjacent,
# and if one is identified, one path is from the headDeltas, the other is from pendingDeltas
# This is all to avoid an O(nm) brute force search.
results = []
affectingArray = delta._t is 'a'
for index, childDelta of delta
continue if index is '_t'
index = parseInt(index.replace('_', '')) if affectingArray
results = results.concat flattenDelta(childDelta, path.concat([index]))
results
conflicts = []
paths.sort()
for path, i in paths
continue if i + 1 is paths.length
nextPath = paths[i+1]
if nextPath.startsWith path
headDelta = (headPathMap[path] or headPathMap[nextPath])[0].delta
pendingDelta = (pendingPathMap[path] or pendingPathMap[nextPath])[0].delta
conflicts.push({headDelta:headDelta, pendingDelta:pendingDelta})
pendingDelta.conflict = headDelta
headDelta.conflict = pendingDelta
return conflicts if conflicts.length
groupDeltasByAffectingPaths = (deltas) ->
metaDeltas = []
for delta in deltas
conflictPaths = []
if delta.action is 'moved-index'
# every other action affects just the data path, but moved indexes affect a swath
indices = [delta.originalIndex, delta.destinationIndex]
indices.sort()
for index in _.range(indices[0], indices[1]+1)
conflictPaths.push delta.dataPath.slice(0, delta.dataPath.length-1).concat(index)
else
conflictPaths.push delta.dataPath
for path in conflictPaths
metaDeltas.push {
delta: delta
path: (item.toString() for item in path).join('/')
}
_.groupBy metaDeltas, 'path'
module.exports.pruneConflictsFromDelta = (delta, conflicts) ->
# the jsondiffpatch delta mustn't include any dangling nodes,
# or else things will get removed which shouldn't be, or errors will occur
for conflict in conflicts
prunePath delta, conflict.pendingDelta.deltaPath
if _.isEmpty delta then undefined else delta
prunePath = (delta, path) ->
if path.length is 1
delete delta[path]
else
prunePath delta[path[0]], path.slice(1)
keys = (k for k in _.keys(delta[path[0]]) when k isnt '_t')
delete delta[path[0]] if keys.length is 0

View file

@ -31,6 +31,12 @@ class CocoModel extends Backbone.Model
type: ->
@constructor.className
clone: (withChanges=true) ->
# Backbone does not support nested documents
clone = super()
clone.set($.extend(true, {}, if withChanges then @attributes else @_revertAttributes))
clone
onLoaded: ->
@loaded = true
@ -223,14 +229,16 @@ class CocoModel extends Backbone.Model
return false
getDelta: ->
jsd = jsondiffpatch.create({
objectHash: (obj) -> obj.name || obj.id || obj._id || JSON.stringify(_.keys(obj))
})
jsd.diff @_revertAttributes, @attributes
differ = deltasLib.makeJSONDiffer()
differ.diff @_revertAttributes, @attributes
applyDelta: (delta) ->
newAttributes = $.extend(true, {}, @attributes)
jsondiffpatch.patch newAttributes, delta
@set newAttributes
getExpandedDelta: ->
delta = @getDelta()
deltas = deltasLib.flattenDelta(delta)
(deltasLib.interpretDelta(d.delta, d.path, @_revertAttributes, @schema().attributes) for d in deltas)
deltasLib.expandDelta(delta, @_revertAttributes, @schema().attributes)
module.exports = CocoModel

View file

@ -1,4 +1,4 @@
.delta-list-view
.delta-view
.panel-heading
font-size: 13px
padding: 4px
@ -7,37 +7,37 @@
.delta-added
border-color: green
strong
color: green
.panel-heading
> .panel-heading
background-color: lighten(green, 70%)
strong
color: green
.delta-modified
border-color: darkgoldenrod
strong
color: darkgoldenrod
.panel-heading
> .panel-heading
background-color: lighten(darkgoldenrod, 40%)
strong
color: darkgoldenrod
.delta-text-diff
border-color: blue
strong
color: blue
.panel-heading
> .panel-heading
background-color: lighten(blue, 45%)
strong
color: blue
table
width: 100%
.delta-deleted
border-color: red
strong
color: red
.panel-heading
> .panel-heading
background-color: lighten(red, 42%)
strong
color: red
.delta-moved-index
border-color: darkslategray
strong
color: darkslategray
.panel-heading
background-color: lighten(darkslategray, 60%)
> .panel-heading
background-color: lighten(darkslategray, 60%)
strong
color: darkslategray

View file

@ -0,0 +1,3 @@
#patch-modal
.modal-body
padding: 10px

View file

@ -33,7 +33,7 @@
font-size: 0.9em
font-style: italic
.delta-list-view
.delta-view
overflow-y: auto
padding: 10px
border: 1px solid black

View file

@ -1,36 +1,46 @@
- var i = 0
mixin deltaPanel(delta, conflict)
- delta.index = i++
.delta.panel.panel-default(class='delta-'+delta.action, data-index=i)
.panel-heading
if delta.action === 'added'
strong(data-i18n="delta.added") Added
if delta.action === 'modified'
strong(data-i18n="delta.modified") Modified
if delta.action === 'deleted'
strong(data-i18n="delta.deleted") Deleted
if delta.action === 'moved-index'
strong(data-i18n="delta.modified_array") Moved Index
if delta.action === 'text-diff'
strong(data-i18n="delta.text_diff") Text Diff
span
a(data-toggle="collapse" data-parent="#delta-accordion"+(counter) href="#collapse-"+(i+counter))
span= delta.humanPath
.panel-collapse.collapse(id="collapse-"+(i+counter))
.panel-body.row(class=conflict ? "conflict-details" : "details")
if delta.action === 'added'
.new-value.col-md-12= delta.right
if delta.action === 'modified'
.old-value.col-md-6= delta.left
.new-value.col-md-6= delta.right
if delta.action === 'deleted'
.col-md-12
div.old-value= delta.left
if delta.action === 'text-diff'
.col-md-12
div.text-diff
if delta.action === 'moved-index'
.col-md-12
span Moved array value #{JSON.stringify(delta.left)} to index #{delta.destinationIndex}
if delta.conflict && !conflict
.panel-body
strong MERGE CONFLICT WITH
+deltaPanel(delta.conflict, true)
.panel-group(id='delta-accordion-'+(counter))
for delta in deltas
.delta.panel.panel-default(class='delta-'+delta.action)
.panel-heading
if delta.action === 'added'
strong(data-i18n="delta.added") Added
if delta.action === 'modified'
strong(data-i18n="delta.modified") Modified
if delta.action === 'deleted'
strong(data-i18n="delta.deleted") Deleted
if delta.action === 'moved-index'
strong(data-i18n="delta.modified_array") Moved Index
if delta.action === 'text-diff'
strong(data-i18n="delta.text_diff") Text Diff
span
a(data-toggle="collapse" data-parent="#delta-accordion"+(counter) href="#collapse-"+(i+counter))
span= delta.path
.panel-collapse.collapse(id="collapse-"+(i+counter))
.panel-body.row
if delta.action === 'added'
.new-value.col-md-12= delta.right
if delta.action === 'modified'
.old-value.col-md-6= delta.left
.new-value.col-md-6= delta.right
if delta.action === 'deleted'
.col-md-12
div.old-value= delta.left
if delta.action === 'text-diff'
.col-md-12
div.text-diff
if delta.action === 'moved-index'
.col-md-12
span Moved array value #{JSON.stringify(delta.left)} to index #{delta.destinationIndex}
- i += 1
+deltaPanel(delta)

View file

@ -0,0 +1,20 @@
extends /templates/modal/modal_base
block modal-header-content
.modal-header-content
h3 Patch
block modal-body-content
.modal-body
.changes-stub
block modal-footer
.modal-footer
button(data-dismiss="modal", data-i18n="common.cancel").btn Cancel
if canReject
button.btn.btn-danger Reject
if canWithdraw
button.btn.btn-danger Withdraw
if canAccept
button.btn.btn-primary Accept

View file

@ -20,8 +20,11 @@ else
th Submitter
th Submitted
th Commit Message
th Review
for patch in patches
tr
td= patch.userName
td= moment(patch.get('created')).format('llll')
td= patch.get('commitMessage')
td
span.glyphicon.glyphicon-wrench(data-patch-id=patch.id).patch-icon

View file

@ -19,7 +19,7 @@ block content
h3 Edit Thang Type: "#{thangType.attributes.name}"
ul.nav.nav-tabs
li.active
li
a(href="#editor-thang-main-tab-view", data-toggle="tab") Main
li
a(href="#editor-thang-components-tab-view", data-toggle="tab") Components
@ -27,13 +27,13 @@ block content
a(href="#editor-thang-spritesheets-view", data-toggle="tab") Spritesheets
li
a(href="#editor-thang-colors-tab-view", data-toggle="tab")#color-tab Colors
li
li.active
a(href="#editor-thang-patches-view", data-toggle="tab")#patches-tab Patches
div.tab-content
div.tab-pane#editor-thang-colors-tab-view
div.tab-pane.active#editor-thang-main-tab-view
div.tab-pane#editor-thang-main-tab-view
div.main-area.well
div.file-controls
@ -86,7 +86,7 @@ block content
div#spritesheets
div.tab-pane#editor-thang-patches-view
div.tab-pane#editor-thang-patches-view.active
div.patches-view

View file

@ -1,57 +1,70 @@
CocoView = require 'views/kinds/CocoView'
template = require 'templates/editor/delta'
deltaLib = require 'lib/deltas'
deltasLib = require 'lib/deltas'
module.exports = class DeltaListView extends CocoView
TEXTDIFF_OPTIONS =
baseTextName: "Old"
newTextName: "New"
contextSize: 5
viewType: 1
module.exports = class DeltaView extends CocoView
@deltaCounter: 0
className: "delta-list-view"
className: "delta-view"
template: template
constructor: (options) ->
super(options)
@model = options.model
@headModel = options.headModel
@expandedDeltas = @model.getExpandedDelta()
if @headModel
@headDeltas = @headModel.getExpandedDelta()
@conflicts = deltasLib.getConflicts(@headDeltas, @expandedDeltas)
DeltaView.deltaCounter += @expandedDeltas.length
getRenderData: ->
c = super()
c.deltas = @processedDeltas = @model.getExpandedDelta()
c.counter = DeltaListView.deltaCounter
DeltaListView.deltaCounter += c.deltas.length
c.deltas = @expandedDeltas
c.counter = DeltaView.deltaCounter
c
afterRender: ->
deltas = @$el.find('.delta')
deltas = @$el.find('.details')
for delta, i in deltas
deltaEl = $(delta)
deltaData = @processedDeltas[i]
if _.isObject(deltaData.left) and leftEl = deltaEl.find('.old-value')
options =
data: deltaData.left
schema: deltaData.schema
readOnly: true
treema = TreemaNode.make(leftEl, options)
treema.build()
deltaData = @expandedDeltas[i]
@expandDetails(deltaEl, deltaData)
conflictDeltas = @$el.find('.conflict-details')
conflicts = (delta.conflict for delta in @expandedDeltas when delta.conflict)
for delta, i in conflictDeltas
deltaEl = $(delta)
deltaData = conflicts[i]
@expandDetails(deltaEl, deltaData)
expandDetails: (deltaEl, deltaData) ->
treemaOptions = { schema: deltaData.schema, readOnly: true }
if _.isObject(deltaData.left) and leftEl = deltaEl.find('.old-value')
options = _.defaults {data: deltaData.left}, treemaOptions
TreemaNode.make(leftEl, options).build()
if _.isObject(deltaData.right) and rightEl = deltaEl.find('.new-value')
options = _.defaults {data: deltaData.right}, treemaOptions
TreemaNode.make(rightEl, options).build()
if deltaData.action is 'text-diff'
left = difflib.stringAsLines deltaData.left
right = difflib.stringAsLines deltaData.right
sm = new difflib.SequenceMatcher(left, right)
opcodes = sm.get_opcodes()
el = deltaEl.find('.text-diff')
options = {baseTextLines: left, newTextLines: right, opcodes: opcodes}
args = _.defaults options, TEXTDIFF_OPTIONS
el.append(diffview.buildView(args))
if _.isObject(deltaData.right) and rightEl = deltaEl.find('.new-value')
options =
data: deltaData.right
schema: deltaData.schema
readOnly: true
treema = TreemaNode.make(rightEl, options)
treema.build()
if deltaData.action is 'text-diff'
left = difflib.stringAsLines deltaData.left
right = difflib.stringAsLines deltaData.right
sm = new difflib.SequenceMatcher(left, right)
opcodes = sm.get_opcodes()
el = deltaEl.find('.text-diff')
args = {
baseTextLines: left
newTextLines: right
opcodes: opcodes
baseTextName: "Old"
newTextName: "New"
contextSize: 5
viewType: 1
}
el.append(diffview.buildView(args))
getApplicableDelta: ->
delta = @model.getDelta()
delta = deltasLib.pruneConflictsFromDelta delta, @conflicts if @conflicts
delta

View file

@ -0,0 +1,42 @@
ModalView = require 'views/kinds/ModalView'
template = require 'templates/editor/patch_modal'
DeltaView = require 'views/editor/delta'
module.exports = class PatchModal extends ModalView
id: "patch-modal"
template: template
plain: true
constructor: (@patch, @targetModel, options) ->
super(options)
targetID = @patch.get('target').id
if false
@originalSource = targetModel.clone(false)
@onOriginalLoaded()
else
@originalSource = new targetModel.constructor({_id:targetID})
@originalSource.fetch()
@listenToOnce @originalSource, 'sync', @onOriginalLoaded
@addResourceToLoad(@originalSource)
getRenderData: ->
c = super()
c
afterRender: ->
return if @originalSource.loading
headModel = @originalSource.clone(false)
headModel.set(@targetModel.attributes)
pendingModel = @originalSource.clone(false)
pendingModel.applyDelta(@patch.get('delta'))
@deltaView = new DeltaView({model:pendingModel, headModel:headModel})
changeEl = @$el.find('.changes-stub')
@insertSubView(@deltaView, changeEl)
super()
acceptPatch: ->
delta = @deltaView.getApplicableDelta()
pendingModel = @originalSource.clone(false)
pendingModel.applyDelta(delta)

View file

@ -2,6 +2,7 @@ CocoView = require 'views/kinds/CocoView'
template = require 'templates/editor/patches'
PatchesCollection = require 'collections/PatchesCollection'
nameLoader = require 'lib/NameLoader'
PatchModal = require './patch_modal'
module.exports = class PatchesView extends CocoView
template: template
@ -10,6 +11,7 @@ module.exports = class PatchesView extends CocoView
events:
'change .status-buttons': 'onStatusButtonsChanged'
'click .patch-icon': 'openPatchModal'
constructor: (@model, options) ->
super(options)
@ -47,3 +49,8 @@ module.exports = class PatchesView extends CocoView
@initPatches()
@load()
@render()
openPatchModal: (e) ->
patch = _.find @patches.models, {id:$(e.target).data('patch-id')}
modal = new PatchModal(patch, @model)
@openModalView(modal)

View file

@ -10,7 +10,7 @@ classCount = 0
makeScopeName = -> "view-scope-#{classCount++}"
doNothing = ->
module.exports = class CocoView extends Backbone.View
class CocoView extends Backbone.View
startsLoading: false
cache: false # signals to the router to keep this view around
template: -> ''
@ -348,6 +348,7 @@ module.exports = class CocoView extends Backbone.View
slider
mobileRELong = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i
mobileRELong = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i
mobileREShort = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i
module.exports = CocoView

View file

@ -40,7 +40,6 @@ PatchSchema.pre 'save', (next) ->
patches = document.get('patches') or []
patches.push @_id
console.log 'PATCH PUSHED', @_id
document.set 'patches', patches
document.save (err) -> next(err)