diff --git a/app/lib/auth.coffee b/app/lib/auth.coffee index acc899af5..c9d6fcefb 100644 --- a/app/lib/auth.coffee +++ b/app/lib/auth.coffee @@ -5,6 +5,7 @@ BEEN_HERE_BEFORE_KEY = 'beenHereBefore' init = -> module.exports.me = window.me = new User(window.userObject) # inserted into main.html + module.exports.me.onLoaded() trackFirstArrival() if me and not me.get('testGroupNumber')? # Assign testGroupNumber to returning visitors; new ones in server/routes/auth diff --git a/app/lib/deltas.coffee b/app/lib/deltas.coffee index e97d659e9..4ec639532 100644 --- a/app/lib/deltas.coffee +++ b/app/lib/deltas.coffee @@ -162,10 +162,14 @@ groupDeltasByAffectingPaths = (deltas) -> prunedMap module.exports.pruneConflictsFromDelta = (delta, conflicts) -> + expandedDeltas = (conflict.pendingDelta for conflict in conflicts) + module.exports.pruneExpandedDeltasFromDelta delta, expandedDeltas + +module.exports.pruneExpandedDeltasFromDelta = (delta, expandedDeltas) -> # 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 + for expandedDelta in expandedDeltas + prunePath delta, expandedDelta.deltaPath if _.isEmpty delta then undefined else delta prunePath = (delta, path) -> diff --git a/app/lib/forms.coffee b/app/lib/forms.coffee index fd1e795c2..73e419698 100644 --- a/app/lib/forms.coffee +++ b/app/lib/forms.coffee @@ -34,5 +34,5 @@ module.exports.applyErrorsToForm = (el, errors) -> module.exports.clearFormAlerts = (el) -> $('.has-error', el).removeClass('has-error') - $('.alert', el).remove() + $('.alert.alert-danger', el).remove() el.find('.help-block.error-help-block').remove() \ No newline at end of file diff --git a/app/locale/hu.coffee b/app/locale/hu.coffee index ddddf3b93..a93cc8a7e 100644 --- a/app/locale/hu.coffee +++ b/app/locale/hu.coffee @@ -78,7 +78,7 @@ module.exports = nativeDescription: "magyar", englishDescription: "Hungarian", t creating: "Fiók létrehozása" sign_up: "Regisztráció" log_in: "Belépés meglévő fiókkal" -# social_signup: "Or, you can sign up through Facebook or G+:" + social_signup: "De regisztrálhatsz a Facebook-on vagy a G+:-on keresztül is." # required: "You need to log in before you can go that way." home: @@ -166,10 +166,10 @@ module.exports = nativeDescription: "magyar", englishDescription: "Hungarian", t email_announcements_description: "Szeretnél levelet kapni a legújabb fejlesztéseinkről?" email_notifications: "Értesítések" # email_notifications_summary: "Controls for personalized, automatic email notifications related to your CodeCombat activity." -# email_any_notes: "Any Notifications" -# email_any_notes_description: "Disable to stop all activity notification emails." -# email_recruit_notes: "Job Opportunities" -# email_recruit_notes_description: "If you play really well, we may contact you about getting you a (better) job." + email_any_notes: "Bármely értesítés" + email_any_notes_description: "Minden tevékenységgel kapcsolatos e-mail értesítés letiltása." + email_recruit_notes: "Álláslehetőségek" + email_recruit_notes_description: "Ha igazán jól játszol lehet, hogy felveszzük veled a kapcsolatot és megbeszéljük, hogy szerezzünk-e neked egy (jobb) állást." contributor_emails: "Hozzájárulóknak szóló levelek" contribute_prefix: "Folyamatosan keresünk embereket, hogy csatlakozzanak hozzánk. Nézz rá a " contribute_page: "segítők oldalára" @@ -181,8 +181,8 @@ module.exports = nativeDescription: "magyar", englishDescription: "Hungarian", t job_profile: "Munkaköri leírás" job_profile_approved: "Munkaköri leírásodat a Codecombat jóváhagyta. Munkaadók mindaddig láthatják, amíg meg nem jelölöd inaktíként, vagy négy hétig,ha addig nem kerül megváltoztatásra." job_profile_explanation: "Szió! Töltsd ki ezt és majd kapcsolatba lépünk veled és keresünk neked egy szoftware fejlesztői állást." -# sample_profile: "See a sample profile" -# view_profile: "View Your Profile" + sample_profile: "Nézz meg egy mintaprofilt!" + view_profile: "Nézd meg a profilodat!" account_profile: edit_settings: "Beállítások szerkesztése" diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee index 3a8838b65..68a836c3d 100644 --- a/app/models/CocoModel.coffee +++ b/app/models/CocoModel.coffee @@ -15,10 +15,10 @@ class CocoModel extends Backbone.Model super() if not @constructor.className console.error("#{@} needs a className set.") - @markToRevert() @addSchemaDefaults() @on 'sync', @onLoaded, @ @on 'error', @onError, @ + @on 'add', @onLoaded, @ @saveBackup = _.debounce(@saveBackup, 500) type: -> @@ -38,14 +38,15 @@ class CocoModel extends Backbone.Model @loaded = true @loading = false @jqxhr = null - @markToRevert() @loadFromBackup() getNormalizedURL: -> "#{@urlRoot}/#{@id}" set: -> + inFlux = @loading or not @loaded + @markToRevert() unless inFlux or @_revertAttributes res = super(arguments...) - @saveBackup() if @saveBackups and @loaded and @hasLocalChanges() + @saveBackup() if @saveBackups and (not inFlux) and @hasLocalChanges() res loadFromBackup: -> @@ -61,22 +62,29 @@ class CocoModel extends Backbone.Model @backedUp = {} schema: -> return @constructor.schema + + getValidationErrors: -> + errors = tv4.validateMultiple(@attributes, @constructor.schema or {}).errors + return errors if errors?.length validate: -> - result = tv4.validateMultiple(@attributes, @constructor.schema? or {}) - if result.errors?.length - console.log @, "got validate result with errors:", result - return result.errors unless result.valid - + errors = @getValidationErrors() + if errors?.length + console.debug "Validation failed for #{@constructor.className}: '#{@get('name') or @}'." + for error in errors + console.debug "\t", error.dataPath, ":", error.message + return errors + save: (attrs, options) -> - @set 'editPath', document.location.pathname options ?= {} + options.headers ?= {} + options.headers['X-Current-Path'] = document.location.pathname success = options.success error = options.error options.success = (model, res) => @trigger "save:success", @ success(@, res) if success - @markToRevert() + @markToRevert() if @_revertAttributes @clearBackup() options.error = (model, res) => error(@, res) if error @@ -93,7 +101,7 @@ class CocoModel extends Backbone.Model @jqxhr markToRevert: -> - return unless @saveBackups + console.debug "Saving _revertAttributes for #{@constructor.className}: '#{@get('name')}'" if @type() is 'ThangType' @_revertAttributes = _.clone @attributes # No deep clones for these! else @@ -142,7 +150,6 @@ class CocoModel extends Backbone.Model #console.log "setting", prop, "to", sch.default, "from sch.default" if sch.default? @set prop, sch.default if sch.default? if @loaded - @markToRevert() @loadFromBackup() @isObjectID: (s) -> diff --git a/app/styles/modal/save_version.sass b/app/styles/modal/save_version.sass index 66de28a29..af350a53a 100644 --- a/app/styles/modal/save_version.sass +++ b/app/styles/modal/save_version.sass @@ -52,4 +52,8 @@ .checkbox margin: 10px 10px 0 input - margin-right: 5px \ No newline at end of file + margin-right: 5px + + #errors-wrapper + margin-top: 20px + margin-bottom: 0 \ No newline at end of file diff --git a/app/templates/editor/level/save.jade b/app/templates/editor/level/save.jade index eccef370a..ae119168a 100644 --- a/app/templates/editor/level/save.jade +++ b/app/templates/editor/level/save.jade @@ -53,3 +53,7 @@ block modal-body-content label input(id=id + "-version-is-major", name="version-is-major", type="checkbox") span(data-i18n="versions.new_major_version") New Major Version + + #errors-wrapper.alert.alert-danger.hide + strong Validation Error! Save failed. + p.errors \ No newline at end of file diff --git a/app/views/editor/delta.coffee b/app/views/editor/delta.coffee index 9036f5557..91a8f6dba 100644 --- a/app/views/editor/delta.coffee +++ b/app/views/editor/delta.coffee @@ -44,26 +44,27 @@ module.exports = class DeltaView extends CocoView @expandedDeltas = @model.getExpandedDeltaWith(@comparisonModel) else @expandedDeltas = @model.getExpandedDelta() - - @filterExpandedDeltas() + [@expandedDeltas, @skippedDeltas] = @filterDeltas(@expandedDeltas) if @headModel @headDeltas = @headModel.getExpandedDelta() + @headDeltas = @filterDeltas(@headDeltas)[0] @conflicts = deltasLib.getConflicts(@headDeltas, @expandedDeltas) - filterExpandedDeltas: -> - return unless @skipPaths + filterDeltas: (deltas) -> + return [deltas, []] unless @skipPaths for path, i in @skipPaths @skipPaths[i] = [path] if _.isString(path) newDeltas = [] - for delta in @expandedDeltas + skippedDeltas = [] + for delta in deltas skip = false for skipPath in @skipPaths if _.isEqual _.first(delta.dataPath, skipPath.length), skipPath skip = true break - newDeltas.push delta unless skip - @expandedDeltas = newDeltas + if skip then skippedDeltas.push delta else newDeltas.push delta + [newDeltas, skippedDeltas] getRenderData: -> c = super() @@ -87,7 +88,7 @@ module.exports = class DeltaView extends CocoView @expandDetails(deltaEl, deltaData) expandDetails: (deltaEl, deltaData) -> - treemaOptions = { schema: deltaData.schema, readOnly: true } + treemaOptions = { schema: deltaData.schema or {}, readOnly: true } if _.isObject(deltaData.left) and leftEl = deltaEl.find('.old-value') options = _.defaults {data: deltaData.left}, treemaOptions @@ -109,5 +110,6 @@ module.exports = class DeltaView extends CocoView getApplicableDelta: -> delta = @model.getDelta() - delta = deltasLib.pruneConflictsFromDelta delta, @conflicts if @conflicts + delta = deltasLib.pruneConflictsFromDelta delta, @conflicts if @conflicts + delta = deltasLib.pruneExpandedDeltasFromDelta delta, @skippedDeltas if @skippedDeltas delta \ No newline at end of file diff --git a/app/views/editor/level/save_view.coffee b/app/views/editor/level/save_view.coffee index 75226dd54..4516cc44d 100644 --- a/app/views/editor/level/save_view.coffee +++ b/app/views/editor/level/save_view.coffee @@ -53,7 +53,7 @@ module.exports = class LevelSaveView extends SaveVersionModal commitLevel: -> modelsToSave = [] - @showLoading() + formsToSave = [] for form in @$el.find('form') # Level form is first, then LevelComponents' forms, then LevelSystems' forms fields = {} @@ -75,7 +75,19 @@ module.exports = class LevelSaveView extends SaveVersionModal @level.publish() else if @level.isPublished() and not newModel.isPublished() newModel.publish() # Publish any LevelComponents that weren't published yet + formsToSave.push form + + for model in modelsToSave + if errors = model.getValidationErrors() + messages = ("\t #{error.dataPath}: #{error.message}" for error in errors) + messages = messages.join('
') + @$el.find('#errors-wrapper .errors').html(messages) + @$el.find('#errors-wrapper').removeClass('hide') + return + @showLoading() + tuples = _.zip(modelsToSave, formsToSave) + for [newModel, form] in tuples res = newModel.save() do (newModel, form) => res.error => diff --git a/app/views/editor/level/scripts_tab_view.coffee b/app/views/editor/level/scripts_tab_view.coffee index b2789948f..f9ad725a4 100644 --- a/app/views/editor/level/scripts_tab_view.coffee +++ b/app/views/editor/level/scripts_tab_view.coffee @@ -152,32 +152,6 @@ class EventPrereqNode extends TreemaNode.nodeMap.object class ChannelNode extends TreemaNode.nodeMap.string buildValueForEditing: (valEl) -> super(valEl) - valEl.find('input').autocomplete(source: channels, minLength: 0, delay: 0, autoFocus: true) + autocompleteValues = ({label: val?.title or key, value: key} for key, val of Backbone.Mediator.channelSchemas) + valEl.find('input').autocomplete(source: autocompleteValues, minLength: 0, delay: 0, autoFocus: true) valEl - -channels = [ - "tome:palette-hovered", - "tome:palette-clicked", - "tome:spell-shown", - "end-current-script", - "goal-manager:new-goal-states", - "god:new-world-created", - "help-multiplayer", - "help-next", - "help-overview", - "level-restart-ask", - "level-set-playing", - "level:docs-hidden", - "level:team-set", - "playback:manually-scrubbed", - "sprite:speech-updated", - "surface:coordinates-shown", - "surface:frame-changed", - "surface:sprite-selected", - "world:thang-attacked-when-out-of-range", - "world:thang-collected-item", - "world:thang-died", - "world:thang-left-map", - "world:thang-touched-goal", - "world:won" -] diff --git a/app/views/editor/level/settings_tab_view.coffee b/app/views/editor/level/settings_tab_view.coffee index f834e172c..8f68d0777 100644 --- a/app/views/editor/level/settings_tab_view.coffee +++ b/app/views/editor/level/settings_tab_view.coffee @@ -51,4 +51,5 @@ module.exports = class SettingsTabView extends View onSettingsChanged: (e) => $('.level-title').text @settingsTreema.data.name for key in @editableSettings + continue unless @settingsTreema.data[key] is undefined @level.set key, @settingsTreema.data[key] diff --git a/app/views/editor/level/thangs_tab_view.coffee b/app/views/editor/level/thangs_tab_view.coffee index b675fd3bc..22a504480 100644 --- a/app/views/editor/level/thangs_tab_view.coffee +++ b/app/views/editor/level/thangs_tab_view.coffee @@ -194,7 +194,7 @@ module.exports = class ThangsTabView extends View return unless @selectedExtantThang and e.thang?.id is @selectedExtantThang?.id @surface.camera.dragDisabled = true {stageX, stageY} = e.originalEvent - wop = @surface.camera.canvasToWorld x: stageX, y: stageY + wop = @surface.camera.screenToWorld x: stageX, y: stageY wop.z = @selectedExtantThang.depth / 2 @adjustThangPos @selectedExtantSprite, @selectedExtantThang, wop [w, h] = [@surface.camera.canvasWidth, @surface.camera.canvasHeight] @@ -250,6 +250,7 @@ module.exports = class ThangsTabView extends View # @thangsTreema.deselectAll() selectAddThang: (e) => + return if e? and $(e.target).closest('#thang-search').length # Ignore if you're trying to search thangs return unless e? and $(e.target).closest('#editor-level-thangs-tab-view').length or key.isPressed('esc') if e then target = $(e.target) else target = @$el.find('.add-thangs-palette') # pretend to click on background if no event return true if target.attr('id') is 'surface' @@ -258,7 +259,7 @@ module.exports = class ThangsTabView extends View @$el.find('.add-thangs-palette .add-thang-palette-icon.selected').removeClass('selected') @selectAddThangType(if wasSelected then null else target.attr 'data-thang-type') unless key.alt or key.meta target.addClass('selected') if @addThangType - false + #false # was causing #1099, any reason to keep? moveAddThangSelection: (direction) -> return unless @addThangType @@ -319,7 +320,7 @@ module.exports = class ThangsTabView extends View onSurfaceMouseMoved: (e) -> return unless @addThangSprite - wop = @surface.camera.canvasToWorld x: e.x, y: e.y + wop = @surface.camera.screenToWorld x: e.x, y: e.y wop.z = 0.5 @adjustThangPos @addThangSprite, @addThangSprite.thang, wop null diff --git a/app/views/editor/patch_modal.coffee b/app/views/editor/patch_modal.coffee index 4603cbc48..e3758c546 100644 --- a/app/views/editor/patch_modal.coffee +++ b/app/views/editor/patch_modal.coffee @@ -8,6 +8,7 @@ module.exports = class PatchModal extends ModalView template: template plain: true modalWidthPercent: 60 + @DOC_SKIP_PATHS = ['_id','version', 'commitMessage', 'parent', 'created', 'slug', 'index', '__v', 'patches'] events: 'click #withdraw-button': 'withdrawPatch' @@ -36,14 +37,16 @@ module.exports = class PatchModal extends ModalView headModel = null if @targetModel.hasWriteAccess() headModel = @originalSource.clone(false) + headModel.markToRevert true headModel.set(@targetModel.attributes) headModel.loaded = true pendingModel = @originalSource.clone(false) + pendingModel.markToRevert true pendingModel.applyDelta(@patch.get('delta')) pendingModel.loaded = true - @deltaView = new DeltaView({model:pendingModel, headModel:headModel}) + @deltaView = new DeltaView({model:pendingModel, headModel:headModel, skipPaths: PatchModal.DOC_SKIP_PATHS}) changeEl = @$el.find('.changes-stub') @insertSubView(@deltaView, changeEl) super() diff --git a/app/views/modal/versions_modal.coffee b/app/views/modal/versions_modal.coffee index 9a00f5d58..c690529be 100755 --- a/app/views/modal/versions_modal.coffee +++ b/app/views/modal/versions_modal.coffee @@ -2,6 +2,7 @@ ModalView = require 'views/kinds/ModalView' template = require 'templates/modal/versions' tableTemplate = require 'templates/kinds/table' DeltaView = require 'views/editor/delta' +PatchModal = require 'views/editor/patch_modal' class VersionsViewCollection extends Backbone.Collection url: "" @@ -49,7 +50,7 @@ module.exports = class VersionsModalView extends ModalView laterVersion = new @model(_id:$(rows[0]).val()) earlierVersion = new @model(_id:$(rows[1]).val()) - @deltaView = new DeltaView({model:earlierVersion, comparisonModel:laterVersion, skipPaths:['_id','version', 'commitMessage', 'parent', 'created', 'slug', 'index']}) + @deltaView = new DeltaView({model:earlierVersion, comparisonModel:laterVersion, skipPaths:PatchModal.DOC_SKIP_PATHS}) @insertSubView(@deltaView, deltaEl) getRenderData: (context={}) -> diff --git a/server/commons/Handler.coffee b/server/commons/Handler.coffee index 81293b4ad..82c5d45c3 100644 --- a/server/commons/Handler.coffee +++ b/server/commons/Handler.coffee @@ -325,7 +325,8 @@ module.exports = class Handler newDocument.save (err) => return @sendDatabaseError(res, err) if err @sendSuccess(res, @formatEntity(req, newDocument)) - @notifyWatchersOfChange(req.user, newDocument, req.body.editPath) if @modelClass.schema.is_patchable + if @modelClass.schema.is_patchable + @notifyWatchersOfChange(req.user, newDocument, req.headers['x-current-path']) if major? parentDocument.makeNewMinorVersion(updatedObject, major, done)