This commit is contained in:
Nick Winter 2014-04-18 09:53:34 -07:00
commit 848ebf2ffe
39 changed files with 472 additions and 197 deletions

View file

@ -15,6 +15,9 @@
fork: "Fork" fork: "Fork"
play: "Play" play: "Play"
retry: "Retry" retry: "Retry"
watch: "Watch"
unwatch: "Unwatch"
submit_patch: "Submit Patch"
units: units:
second: "second" second: "second"

View file

@ -60,6 +60,7 @@ class CocoModel extends Backbone.Model
return result.errors unless result.valid return result.errors unless result.valid
save: (attrs, options) -> save: (attrs, options) ->
@set 'editPath', document.location.pathname
options ?= {} options ?= {}
success = options.success success = options.success
options.success = (resp) => options.success = (resp) =>
@ -68,7 +69,6 @@ class CocoModel extends Backbone.Model
@markToRevert() @markToRevert()
@clearBackup() @clearBackup()
@trigger "save", @ @trigger "save", @
patch.setStatus 'accepted' for patch in @acceptedPatches or []
return super attrs, options return super attrs, options
fetch: -> fetch: ->
@ -95,7 +95,6 @@ class CocoModel extends Backbone.Model
cloneNewMinorVersion: -> cloneNewMinorVersion: ->
newData = $.extend(null, {}, @attributes) newData = $.extend(null, {}, @attributes)
clone = new @constructor(newData) clone = new @constructor(newData)
clone.acceptedPatches = @acceptedPatches
clone clone
cloneNewMajorVersion: -> cloneNewMajorVersion: ->
@ -221,9 +220,11 @@ class CocoModel extends Backbone.Model
delta = @getDelta() delta = @getDelta()
deltasLib.expandDelta(delta, @_revertAttributes, @schema()) deltasLib.expandDelta(delta, @_revertAttributes, @schema())
addPatchToAcceptOnSave: (patch) -> watch: (doWatch=true) ->
@acceptedPatches ?= [] $.ajax("#{@urlRoot}/#{@id}/watch", {type:'PUT', data:{on:doWatch}})
@acceptedPatches.push patch @watching = -> doWatch
@acceptedPatches = _.uniq(@acceptedPatches, false, (p) -> p.id)
watching: ->
return me.id in (@get('watchers') or [])
module.exports = CocoModel module.exports = CocoModel

View file

@ -5,4 +5,7 @@ module.exports = class PatchModel extends CocoModel
urlRoot: "/db/patch" urlRoot: "/db/patch"
setStatus: (status) -> setStatus: (status) ->
$.ajax("/db/patch/#{@id}/status", {type:"PUT", data: {status:status}}) PatchModel.setStatus @id, status
@setStatus: (id, status) ->
$.ajax("/db/patch/#{id}/status", {type:"PUT", data: {status:status}})

View file

@ -63,7 +63,7 @@ patchableProps = ->
status: { enum: ['pending', 'accepted', 'rejected', 'cancelled']} status: { enum: ['pending', 'accepted', 'rejected', 'cancelled']}
}) })
allowPatches: { type: 'boolean' } allowPatches: { type: 'boolean' }
listeners: me.array({title:'Listeners'}, watchers: me.array({title:'Watchers'},
me.objectId(links: [{rel: 'extra', href: "/db/user/{($)}"}])) me.objectId(links: [{rel: 'extra', href: "/db/user/{($)}"}]))
me.extendPatchableProperties = (schema) -> me.extendPatchableProperties = (schema) ->

View file

@ -265,3 +265,8 @@ body[lang='ja']
font-family: 'Glyphicons Halflings' font-family: 'Glyphicons Halflings'
src: url("/fonts/glyphicons-halflings-regular.eot") src: url("/fonts/glyphicons-halflings-regular.eot")
src: url("/fonts/glyphicons-halflings-regular.eot?#iefix") format("embedded-opentype"), url("/fonts/glyphicons-halflings-regular.woff") format("woff"), url("/fonts/glyphicons-halflings-regular.ttf") format("truetype"), url("/fonts/glyphicons-halflings-regular.svg#glyphicons-halflingsregular") format("svg") src: url("/fonts/glyphicons-halflings-regular.eot?#iefix") format("embedded-opentype"), url("/fonts/glyphicons-halflings-regular.woff") format("woff"), url("/fonts/glyphicons-halflings-regular.ttf") format("truetype"), url("/fonts/glyphicons-halflings-regular.svg#glyphicons-halflingsregular") format("svg")
.spr:after
content: " "
.spl:before
content: " "

View file

@ -1,4 +1,11 @@
#editor-level-component-edit-view #editor-level-component-edit-view
nav
margin-bottom: 0
#component-patches
padding: 0 10px 10px
background: white
.navbar-text .navbar-text
float: left float: left
@ -7,9 +14,13 @@
left: 0 left: 0
right: 0 right: 0
bottom: 0 bottom: 0
top: 50px top: 35px
border: 2px solid black border: 2px solid black
border-top: none border-top: none
.active > a, .active > a:hover, .active > a:focus .inner-editor
background-color: white !important position: absolute
left: 0
right: 0
bottom: 0
top: 0px

View file

@ -1,4 +1,6 @@
#editor-level-components-tab-view #editor-level-components-tab-view
h3
margin-top: 0
.components-container .components-container
position: absolute position: absolute
@ -7,7 +9,7 @@
.treema-root .treema-root
position: absolute position: absolute
top: 50px top: 35px
bottom: 0 bottom: 0
width: 250px width: 250px
overflow: scroll overflow: scroll
@ -25,13 +27,13 @@
.treema-root .treema-root
position: absolute position: absolute
top: 50px top: 35px
right: 0 right: 0
left: 0px left: 0px
bottom: 0 bottom: 0
overflow: scroll overflow: scroll
#create-new-component-button #create-new-component-button-no-select
position: absolute position: absolute
top: 0 top: 0
right: 0 right: 0

View file

@ -1,10 +1,10 @@
#editor-level-view #editor-level-view
&, #level-editor-top-nav
min-width: 1024px
a a
font-family: helvetica, arial, sans serif font-family: helvetica, arial, sans serif
#top-nav
display: none
position: absolute position: absolute
top: 0px top: 0px
left: 0px left: 0px
@ -12,22 +12,54 @@
bottom: 0px bottom: 0px
$BG: rgba(228, 207, 140, 1.0) $BG: rgba(228, 207, 140, 1.0)
$NAVBG: #2f261d
li.navbar-btn li.navbar-btn
margin-right: 5px margin-right: 5px
#level-editor-top-nav // custom navbar height rules
.nav-tabs .navbar-nav > li > a
border-bottom: 0 !important padding: 7px 8px 8px
.active > a, .active > a:hover, .active > a:focus cursor: pointer
background-color: $BG !important &:hover
border-color: darken($BG, 50%) background-color: lighten($NAVBG, 10%)
border-bottom: 0 .navbar
min-height: 0px
border-radius: 0
.navbar-right // not sure why bootstrap puts a big negative margin in, but this overrides it
margin-right: 10px
// custom navbar styling
.navbar-brand
padding-top: 7px
padding-bottom: 7px
color: lighten(gold, 30%)
.navbar-header
border-left: 2px solid lighten($NAVBG, 20%)
border-right: 2px solid lighten($NAVBG, 20%)
background: lighten($NAVBG, 10%)
margin-left: 20px
.nav-tabs
margin-left: 5px
border-bottom: 0 !important
.active > a, .active > a:hover, .active > a:focus
background-color: $BG !important
border-color: darken($BG, 50%)
border-bottom: 0
a
padding: 7px 5px !important
.dropdown-menu a
cursor: pointer
&:hover
background-color: #d3d3d3
.badge
background-color: green
.outer-content .outer-content
background-color: $BG background-color: $BG
position: absolute position: absolute
top: 0 top: 35px
bottom: 0 bottom: 0
left: 0 left: 0
right: 0 right: 0
@ -45,12 +77,11 @@
#level-editor-tabs #level-editor-tabs
position: absolute position: absolute
left: 20px left: 15px
right: 20px right: 15px
top: 66px top: 15px
bottom: 20px bottom: 15px
.treema-root .treema-root
background-color: white background-color: white
border-radius: 4px border-radius: 4px

View file

@ -7,9 +7,13 @@
left: 0 left: 0
right: 0 right: 0
bottom: 0 bottom: 0
top: 50px top: 35px
border: 2px solid black border: 2px solid black
border-top: none border-top: none
.active > a, .active > a:hover, .active > a:focus .inner-editor
background-color: white !important position: absolute
left: 0
right: 0
bottom: 0
top: 0px

View file

@ -1,4 +1,6 @@
#editor-level-systems-tab-view #editor-level-systems-tab-view
h3
margin-top: 0
.systems-container .systems-container
position: absolute position: absolute
@ -7,7 +9,7 @@
.treema-root .treema-root
position: absolute position: absolute
top: 50px top: 35px
bottom: 0 bottom: 0
width: 250px width: 250px
overflow: scroll overflow: scroll
@ -30,7 +32,7 @@
.treema-root .treema-root
position: absolute position: absolute
top: 50px top: 35px
right: 0 right: 0
left: 0px left: 0px
bottom: 0 bottom: 0

View file

@ -1,3 +1,3 @@
.patches-view .patches-view
.status-buttons .status-buttons
margin: 10px 0 margin-bottom: 10px

View file

@ -1,23 +1,44 @@
nav.navbar.navbar-default(role='navigation') nav.navbar.navbar-default(role='navigation')
.container-fluid ul.nav.navbar-nav.nav-tabs
.navbar-header li.active
span.navbar-brand a(href="#component-code" data-toggle="tab" data-i18n="general.code")#component-code-tab Code
span(data-i18n="editor.level_component_edit_title") li
| Edit Component a(href="#component-config-schema" data-toggle="tab" data-i18n="editor.level_component_config_schema")#component-config-schema-tab Config Schema
span : li
| "#{editTitle}" a(href="#component-settings" data-toggle="tab" data-i18n="editor.level_component_settings")#component-settings-tab Settings
.collapse.navbar-collapse li
ul.nav.navbar-nav.nav-tabs a(href="#component-patches" data-toggle="tab" data-i18n="resources.patches")#component-patches-tab Patches
li.active
a(href="#component-code" data-toggle="tab" data-i18n="general.code") Code .navbar-header
span.navbar-brand= editTitle
ul.nav.navbar-nav.navbar-right
li.dropdown
a(data-toggle='dropdown')
span.glyphicon-chevron-down.glyphicon
ul.dropdown-menu
li.dropdown-header Actions
li li
a(href="#component-config-schema" data-toggle="tab" data-i18n="editor.level_component_config_schema") Config Schema a#component-watch-button
li span.watch
a(href="#component-settings" data-toggle="tab" data-i18n="editor.level_component_settings") Settings span.glyphicon.glyphicon-eye-open
ul.nav.navbar-nav.navbar-left span.spl Watch
li(data-i18n="general.version_history").btn.btn-primary.navbar-btn#history-button Version History span.unwatch.secret
ul.nav.navbar-nav.navbar-right span.glyphicon.glyphicon-eye-close
li(data-i18n="editor.level_component_btn_new").btn.btn-primary.navbar-btn#create-new-component-button Create New Component span.spl Unwatch
li#patch-component-button
a(data-i18n="common.submit_patch") Submit Patch
li#create-new-component-button
a(data-i18n="editor.level_component_btn_new") Create New Component
li.divider
li.dropdown-header Info
li#component-history-button
a(data-i18n="general.version_history") Version History
.tab-content .tab-content
.tab-pane.active#component-code .tab-pane.active#component-code
@ -26,3 +47,5 @@ nav.navbar.navbar-default(role='navigation')
#config-schema-treema #config-schema-treema
.tab-pane#component-settings .tab-pane#component-settings
#edit-component-treema #edit-component-treema
.tab-pane#component-patches
.patches-view

View file

@ -4,6 +4,6 @@
.edit-component-container .edit-component-container
if me.isAdmin() if me.isAdmin()
button(data-i18n="editor.level_component_btn_new").btn.btn-primary#create-new-component-button Create New Component button(data-i18n="editor.level_component_btn_new").btn.btn-primary#create-new-component-button-no-select Create New Component
#editor-level-component-edit-view #editor-level-component-edit-view

View file

@ -1,74 +1,90 @@
extends /templates/base extends /templates/base
block outer_content block header
.outer-content if level.loading
nav.navbar.navbar-default(role='navigation')#level-editor-top-nav nav.navbar.navbar-default(role='navigation')#level-editor-top-nav
.container-fluid .container-fluid
ul.nav.navbar-nav ul.nav.navbar-nav
li li
a(href="/editor/level", data-i18n="editor.back") Back a(href="/editor/level")
.navbar-header span.glyphicon-home.glyphicon
span.navbar-brand
span(data-i18n="editor.level_title") Level Editor
span :
span.level-title #{level.attributes.name}
.collapse.navbar-collapse
ul.nav.navbar-nav.nav-tabs
li.active
a(href="#editor-level-thangs-tab-view", data-toggle="tab", data-i18n="editor.level_tab_thangs") Thangs
li
a(href="#editor-level-scripts-tab-view", data-toggle="tab", data-i18n="editor.level_tab_scripts") Scripts
li
a(href="#editor-level-settings-tab-view", data-toggle="tab", data-i18n="editor.level_tab_settings") Settings
li
a(href="#editor-level-components-tab-view", data-toggle="tab", data-i18n="editor.level_tab_components") Components
li
a(href="#editor-level-systems-tab-view", data-toggle="tab", data-i18n="editor.level_tab_systems") Systems
li
a(href="#editor-level-patches", data-toggle="tab", data-i18n="resources.patches")#patches-tab Patches
ul.nav.navbar-nav.navbar-right
li(data-toggle="coco-modal", data-target="modal/revert", data-i18n="editor.revert", disabled=authorized === true ? undefined : "true").btn.btn-primary.navbar-btn#revert-button Revert
if authorized
li(data-i18n="common.save").btn.btn-primary.navbar-btn#commit-level-start-button Save
else
li(data-i18n="common.patch").btn.btn-primary.navbar-btn#commit-level-patch-button Patch
li(data-i18n="common.fork", disabled=anonymous ? "true": undefined).btn.btn-primary.navbar-btn#fork-level-start-button Fork
li(title="⌃↩ or ⌘↩: Play preview of current level", data-i18n="common.play")#play-button.btn.btn-inverse.banner.navbar-btn Play!
li.divider
li.dropdown
a.dropdown-toggle(href='#', data-toggle='dropdown', data-i18n="editor.more")
| More
b.caret
ul.dropdown-menu
li#history-button
a(href='#', data-i18n="general.version_history") Version History
li
a(href='https://github.com/codecombat/codecombat/wiki/Artisan-Home', data-i18n="editor.wiki") Wiki
li
a(href='http://www.hipchat.com/g3plnOKqa', data-i18n="editor.live_chat") Live Chat
li
a(href='http://discourse.codecombat.com/category/artisan', data-i18n="nav.forum") Forum
li
a(data-toggle="coco-modal", data-target="modal/contact", data-i18n="nav.contact") Email
ul.dropdown-menu
li
span(data-i18n="editor.level_some_options").dropdown-menu-header Some Options?
li.divider
li
a(data-delay="1000", href="#", data-i18n="common.delay_1_sec") 1 second
a(data-delay="3000", href="#", data-i18n="common.delay_3_sec") 3 seconds
a(data-delay="5000", href="#", data-i18n="common.delay_5_sec") 5 seconds
a(data-delay="90019001", href="#", data-i18n="common.manual") Manual
else
nav.navbar.navbar-default(role='navigation')#level-editor-top-nav
ul.nav.navbar-nav
li
a(href="/editor/level")
span.glyphicon-home.glyphicon
ul.nav.navbar-nav.nav-tabs
li.active
a(href="#editor-level-thangs-tab-view", data-toggle="tab", data-i18n="editor.level_tab_thangs") Thangs
li
a(href="#editor-level-scripts-tab-view", data-toggle="tab", data-i18n="editor.level_tab_scripts") Scripts
li
a(href="#editor-level-settings-tab-view", data-toggle="tab", data-i18n="editor.level_tab_settings") Settings
li
a(href="#editor-level-components-tab-view", data-toggle="tab", data-i18n="editor.level_tab_components") Components
li
a(href="#editor-level-systems-tab-view", data-toggle="tab", data-i18n="editor.level_tab_systems") Systems
li
a(href="#editor-level-patches", data-toggle="tab")#patches-tab
span(data-i18n="resources.patches").spr Patches
- var patches = level.get('patches')
if patches && patches.length
span.badge= patches.length
.navbar-header
span.navbar-brand #{level.attributes.name}
ul.nav.navbar-nav.navbar-right
if authorized
li#commit-level-start-button
a
span.glyphicon-floppy-disk.glyphicon
else
li#level-patch-button
a
span.glyphicon-floppy-disk.glyphicon
li(title="⌃↩ or ⌘↩: Play preview of current level")#play-button
a
span.glyphicon-play.glyphicon
li.dropdown
a(data-toggle='dropdown')
span.glyphicon-chevron-down.glyphicon
ul.dropdown-menu
li.dropdown-header Actions
li
a#level-watch-button
span.watch
span.glyphicon.glyphicon-eye-open
span.spl Watch
span.unwatch.secret
span.glyphicon.glyphicon-eye-close
span.spl Unwatch
li(class=anonymous ? "disabled": "")
a(data-i18n="common.fork")#fork-level-start-button Fork
li(class=anonymous ? "disabled": "")
a(data-toggle="coco-modal", data-target="modal/revert", data-i18n="editor.revert")#revert-button Revert
li.divider
li.dropdown-header Info
li#level-history-button
a(href='#', data-i18n="general.version_history") Version History
li.divider
li.dropdown-header Help
li
a(href='https://github.com/codecombat/codecombat/wiki/Artisan-Home', data-i18n="editor.wiki", target="_blank") Wiki
li
a(href='http://www.hipchat.com/g3plnOKqa', data-i18n="editor.live_chat", target="_blank") Live Chat
li
a(href='http://discourse.codecombat.com/category/artisan', data-i18n="nav.forum", target="_blank") Forum
li
a(data-toggle="coco-modal", data-target="modal/contact", data-i18n="nav.contact") Email
block outer_content
.outer-content
div.tab-content#level-editor-tabs div.tab-content#level-editor-tabs
div.tab-pane.active#editor-level-thangs-tab-view div.tab-pane.active#editor-level-thangs-tab-view

View file

@ -1,21 +1,40 @@
nav.navbar.navbar-default(role='navigation') nav.navbar.navbar-default(role='navigation')
.container-fluid
.navbar-header ul.nav.navbar-nav.nav-tabs
span.navbar-brand li.active
span(data-i18n="editor.level_system_edit_title") a(href="#system-code" data-toggle="tab")#system-code-tab Code
| Edit System li
span : a(href="#system-config-schema" data-toggle="tab")#system-config-schema-tab Config Schema
| "#{editTitle}" li
.collapse.navbar-collapse a(href="#system-settings" data-toggle="tab")#system-settings-tab Settings
ul.nav.navbar-nav.nav-tabs li
li.active a(href="#system-patches" data-toggle="tab" data-i18n="resources.patches")#system-patches-tab Patches
a(href="#system-code" data-toggle="tab") Code
ul.nav.navbar-nav.navbar-right
li.dropdown
a(data-toggle='dropdown')
span.glyphicon-chevron-down.glyphicon
ul.dropdown-menu
li.dropdown-header Actions
li li
a(href="#system-config-schema" data-toggle="tab") Config Schema a#system-watch-button
li span.watch
a(href="#system-settings" data-toggle="tab") Settings span.glyphicon.glyphicon-eye-open
ul.nav.navbar-nav.navbar-right span.spl Watch
li(data-i18n="editor.level_system_btn_new").btn.btn-primary.navbar-btn#create-new-system-button Create New System span.unwatch.secret
span.glyphicon.glyphicon-eye-close
span.spl Unwatch
li#patch-system-button
a(data-i18n="common.submit_patch") Submit Patch
li#create-new-system
a(data-i18n="editor.level_system_btn_new") Create New System
li.divider
li.dropdown-header Info
li#system-history-button
a(data-i18n="general.version_history") Version History
.navbar-header
span.navbar-brand= editTitle
.tab-content .tab-content
.tab-pane.active#system-code .tab-pane.active#system-code
@ -24,3 +43,5 @@ nav.navbar.navbar-default(role='navigation')
#config-schema-treema #config-schema-treema
.tab-pane#system-settings .tab-pane#system-settings
#edit-system-treema #edit-system-treema
.tab-pane#system-patches
.patches-view

View file

@ -6,6 +6,7 @@ block modal-header-content
block modal-body-content block modal-body-content
.modal-body .modal-body
p= patch.get('commitMessage')
.changes-stub .changes-stub

View file

@ -8,7 +8,7 @@ block modal-header-content
block modal-body-content block modal-body-content
if dataList if dataList
table.table table.table.table-condensed
tr tr
th(data-i18n="general.name") Name th(data-i18n="general.name") Name
th(data-i18n="general.version") Version th(data-i18n="general.version") Version

View file

@ -21,12 +21,12 @@ module.exports = class DeltaView extends CocoView
if @headModel if @headModel
@headDeltas = @headModel.getExpandedDelta() @headDeltas = @headModel.getExpandedDelta()
@conflicts = deltasLib.getConflicts(@headDeltas, @expandedDeltas) @conflicts = deltasLib.getConflicts(@headDeltas, @expandedDeltas)
DeltaView.deltaCounter += @expandedDeltas.length
getRenderData: -> getRenderData: ->
c = super() c = super()
c.deltas = @expandedDeltas c.deltas = @expandedDeltas
c.counter = DeltaView.deltaCounter c.counter = DeltaView.deltaCounter
DeltaView.deltaCounter += @expandedDeltas.length
c c
afterRender: -> afterRender: ->

View file

@ -1,7 +1,9 @@
View = require 'views/kinds/CocoView' View = require 'views/kinds/CocoView'
VersionHistoryView = require 'views/editor/component/versions_view'
template = require 'templates/editor/level/component/edit' template = require 'templates/editor/level/component/edit'
LevelComponent = require 'models/LevelComponent' LevelComponent = require 'models/LevelComponent'
VersionHistoryView = require 'views/editor/component/versions_view'
PatchesView = require 'views/editor/patches_view'
SaveVersionModal = require 'views/modal/save_version_modal'
module.exports = class LevelComponentEditView extends View module.exports = class LevelComponentEditView extends View
id: "editor-level-component-edit-view" id: "editor-level-component-edit-view"
@ -10,8 +12,14 @@ module.exports = class LevelComponentEditView extends View
events: events:
'click #done-editing-component-button': 'endEditing' 'click #done-editing-component-button': 'endEditing'
'click #history-button': 'showVersionHistory'
'click .nav a': (e) -> $(e.target).tab('show') 'click .nav a': (e) -> $(e.target).tab('show')
'click #component-patches-tab': -> @patchesView.load()
'click #component-code-tab': 'buildCodeEditor'
'click #component-config-schema-tab': 'buildConfigSchemaTreema'
'click #component-settings-tab': 'buildSettingsTreema'
'click #component-history-button': 'showVersionHistory'
'click #patch-component-button': 'startPatchingComponent'
'click #component-watch-button': 'toggleWatchComponent'
constructor: (options) -> constructor: (options) ->
super options super options
@ -21,6 +29,7 @@ module.exports = class LevelComponentEditView extends View
getRenderData: (context={}) -> getRenderData: (context={}) ->
context = super(context) context = super(context)
context.editTitle = "#{@levelComponent.get('system')}.#{@levelComponent.get('name')}" context.editTitle = "#{@levelComponent.get('system')}.#{@levelComponent.get('name')}"
context.component = @levelComponent
context context
afterRender: -> afterRender: ->
@ -28,6 +37,8 @@ module.exports = class LevelComponentEditView extends View
@buildSettingsTreema() @buildSettingsTreema()
@buildConfigSchemaTreema() @buildConfigSchemaTreema()
@buildCodeEditor() @buildCodeEditor()
@patchesView = @insertSubView(new PatchesView(@levelComponent), @$el.find('.patches-view'))
@$el.find('#component-watch-button').find('> span').toggleClass('secret') if @levelComponent.watching()
buildSettingsTreema: -> buildSettingsTreema: ->
data = _.pick @levelComponent.attributes, (value, key) => key in @editableSettings data = _.pick @levelComponent.attributes, (value, key) => key in @editableSettings
@ -40,7 +51,6 @@ module.exports = class LevelComponentEditView extends View
schema: schema schema: schema
data: data data: data
callbacks: {change: @onComponentSettingsEdited} callbacks: {change: @onComponentSettingsEdited}
treemaOptions.readOnly = true unless me.isAdmin()
@componentSettingsTreema = @$el.find('#edit-component-treema').treema treemaOptions @componentSettingsTreema = @$el.find('#edit-component-treema').treema treemaOptions
@componentSettingsTreema.build() @componentSettingsTreema.build()
@componentSettingsTreema.open() @componentSettingsTreema.open()
@ -58,7 +68,6 @@ module.exports = class LevelComponentEditView extends View
schema: LevelComponent.schema.properties.configSchema schema: LevelComponent.schema.properties.configSchema
data: @levelComponent.get 'configSchema' data: @levelComponent.get 'configSchema'
callbacks: {change: @onConfigSchemaEdited} callbacks: {change: @onConfigSchemaEdited}
treemaOptions.readOnly = true unless me.isAdmin()
@configSchemaTreema = @$el.find('#config-schema-treema').treema treemaOptions @configSchemaTreema = @$el.find('#config-schema-treema').treema treemaOptions
@configSchemaTreema.build() @configSchemaTreema.build()
@configSchemaTreema.open() @configSchemaTreema.open()
@ -70,10 +79,10 @@ module.exports = class LevelComponentEditView extends View
Backbone.Mediator.publish 'level-component-edited', levelComponent: @levelComponent Backbone.Mediator.publish 'level-component-edited', levelComponent: @levelComponent
buildCodeEditor: -> buildCodeEditor: ->
editorEl = @$el.find '#component-code-editor' @editor?.destroy()
editorEl.text @levelComponent.get('code') editorEl = $('<div></div>').text(@levelComponent.get('code')).addClass('inner-editor')
@$el.find('#component-code-editor').empty().append(editorEl)
@editor = ace.edit(editorEl[0]) @editor = ace.edit(editorEl[0])
@editor.setReadOnly(not me.isAdmin())
session = @editor.getSession() session = @editor.getSession()
session.setMode 'ace/mode/coffee' session.setMode 'ace/mode/coffee'
session.setTabSize 2 session.setTabSize 2
@ -90,11 +99,21 @@ module.exports = class LevelComponentEditView extends View
Backbone.Mediator.publish 'level-component-editing-ended', levelComponent: @levelComponent Backbone.Mediator.publish 'level-component-editing-ended', levelComponent: @levelComponent
null null
showVersionHistory: (e) ->
versionHistoryView = new VersionHistoryView {}, @levelComponent.id
@openModalView versionHistoryView
Backbone.Mediator.publish 'level:view-switched', e
startPatchingComponent: (e) ->
@openModalView new SaveVersionModal({model:@levelComponent})
Backbone.Mediator.publish 'level:view-switched', e
toggleWatchComponent: ->
button = @$el.find('#component-watch-button')
@levelComponent.watch(button.find('.watch').is(':visible'))
button.find('> span').toggleClass('secret')
destroy: -> destroy: ->
@editor?.destroy() @editor?.destroy()
super() super()
showVersionHistory: (e) ->
versionHistoryView = new VersionHistoryView component:@levelComponent, @levelComponent.id
@openModalView versionHistoryView
Backbone.Mediator.publish 'level:view-switched', e

View file

@ -21,6 +21,7 @@ module.exports = class ComponentsTabView extends View
events: events:
'click #create-new-component-button': 'createNewLevelComponent' 'click #create-new-component-button': 'createNewLevelComponent'
'click #create-new-component-button-no-select': 'createNewLevelComponent'
onLevelThangsChanged: (e) -> onLevelThangsChanged: (e) ->
thangsData = e.thangsData thangsData = e.thangsData

View file

@ -27,10 +27,18 @@ module.exports = class EditorLevelView extends View
'click #play-button': 'onPlayLevel' 'click #play-button': 'onPlayLevel'
'click #commit-level-start-button': 'startCommittingLevel' 'click #commit-level-start-button': 'startCommittingLevel'
'click #fork-level-start-button': 'startForkingLevel' 'click #fork-level-start-button': 'startForkingLevel'
'click #history-button': 'showVersionHistory' 'click #level-history-button': 'showVersionHistory'
'click #patches-tab': -> @patchesView.load() 'click #patches-tab': -> @patchesView.load()
'click #commit-level-patch-button': 'startPatchingLevel' 'click #level-patch-button': 'startPatchingLevel'
'click #level-watch-button': 'toggleWatchLevel'
subscriptions:
'refresh-level-editor': 'rerenderAllViews'
rerenderAllViews: ->
for view in [@thangsTab, @settingsTab, @scriptsTab, @componentsTab, @systemsTab, @patchesView]
view.render()
constructor: (options, @levelID) -> constructor: (options, @levelID) ->
super options super options
@listenToOnce(@supermodel, 'loaded-all', @onAllLoaded) @listenToOnce(@supermodel, 'loaded-all', @onAllLoaded)
@ -93,6 +101,8 @@ module.exports = class EditorLevelView extends View
Backbone.Mediator.publish 'level-loaded', level: @level Backbone.Mediator.publish 'level-loaded', level: @level
@showReadOnly() if me.get('anonymous') @showReadOnly() if me.get('anonymous')
@patchesView = @insertSubView(new PatchesView(@level), @$el.find('.patches-view')) @patchesView = @insertSubView(new PatchesView(@level), @$el.find('.patches-view'))
@listenTo @patchesView, 'accepted-patch', -> setTimeout "location.reload()", 400
@$el.find('#level-watch-button').find('> span').toggleClass('secret') if @level.watching()
onPlayLevel: (e) -> onPlayLevel: (e) ->
sendLevel = => sendLevel = =>
@ -124,3 +134,8 @@ module.exports = class EditorLevelView extends View
versionHistoryView = new VersionHistoryView level:@level, @levelID versionHistoryView = new VersionHistoryView level:@level, @levelID
@openModalView versionHistoryView @openModalView versionHistoryView
Backbone.Mediator.publish 'level:view-switched', e Backbone.Mediator.publish 'level:view-switched', e
toggleWatchLevel: ->
button = @$el.find('#level-watch-button')
@level.watch(button.find('.watch').is(':visible'))
button.find('> span').toggleClass('secret')

View file

@ -30,7 +30,7 @@ module.exports = class LevelSaveView extends SaveVersionModal
context context
afterRender: -> afterRender: ->
super() super(false)
changeEls = @$el.find('.changes-stub') changeEls = @$el.find('.changes-stub')
models = if @lastContext.levelNeedsSave then [@level] else [] models = if @lastContext.levelNeedsSave then [@level] else []
models = models.concat @lastContext.modifiedComponents models = models.concat @lastContext.modifiedComponents

View file

@ -1,6 +1,9 @@
View = require 'views/kinds/CocoView' View = require 'views/kinds/CocoView'
template = require 'templates/editor/level/system/edit' template = require 'templates/editor/level/system/edit'
LevelSystem = require 'models/LevelSystem' LevelSystem = require 'models/LevelSystem'
VersionHistoryView = require 'views/editor/system/versions_view'
PatchesView = require 'views/editor/patches_view'
SaveVersionModal = require 'views/modal/save_version_modal'
module.exports = class LevelSystemEditView extends View module.exports = class LevelSystemEditView extends View
id: "editor-level-system-edit-view" id: "editor-level-system-edit-view"
@ -10,6 +13,13 @@ module.exports = class LevelSystemEditView extends View
events: events:
'click #done-editing-system-button': 'endEditing' 'click #done-editing-system-button': 'endEditing'
'click .nav a': (e) -> $(e.target).tab('show') 'click .nav a': (e) -> $(e.target).tab('show')
'click #system-patches-tab': -> @patchesView.load()
'click #system-code-tab': 'buildCodeEditor'
'click #system-config-schema-tab': 'buildConfigSchemaTreema'
'click #system-settings-tab': 'buildSettingsTreema'
'click #system-history-button': 'showVersionHistory'
'click #patch-system-button': 'startPatchingSystem'
'click #system-watch-button': 'toggleWatchSystem'
constructor: (options) -> constructor: (options) ->
super options super options
@ -26,6 +36,7 @@ module.exports = class LevelSystemEditView extends View
@buildSettingsTreema() @buildSettingsTreema()
@buildConfigSchemaTreema() @buildConfigSchemaTreema()
@buildCodeEditor() @buildCodeEditor()
@patchesView = @insertSubView(new PatchesView(@levelSystem), @$el.find('.patches-view'))
buildSettingsTreema: -> buildSettingsTreema: ->
data = _.pick @levelSystem.attributes, (value, key) => key in @editableSettings data = _.pick @levelSystem.attributes, (value, key) => key in @editableSettings
@ -68,8 +79,9 @@ module.exports = class LevelSystemEditView extends View
Backbone.Mediator.publish 'level-system-edited', levelSystem: @levelSystem Backbone.Mediator.publish 'level-system-edited', levelSystem: @levelSystem
buildCodeEditor: -> buildCodeEditor: ->
editorEl = @$el.find '#system-code-editor' @editor?.destroy()
editorEl.text @levelSystem.get('code') editorEl = $('<div></div>').text(@levelSystem.get('code')).addClass('inner-editor')
@$el.find('#system-code-editor').empty().append(editorEl)
@editor = ace.edit(editorEl[0]) @editor = ace.edit(editorEl[0])
@editor.setReadOnly(not me.isAdmin()) @editor.setReadOnly(not me.isAdmin())
session = @editor.getSession() session = @editor.getSession()
@ -88,6 +100,21 @@ module.exports = class LevelSystemEditView extends View
Backbone.Mediator.publish 'level-system-editing-ended', levelSystem: @levelSystem Backbone.Mediator.publish 'level-system-editing-ended', levelSystem: @levelSystem
null null
showVersionHistory: (e) ->
versionHistoryView = new VersionHistoryView {}, @levelSystem.id
@openModalView versionHistoryView
Backbone.Mediator.publish 'level:view-switched', e
startPatchingSystem: (e) ->
@openModalView new SaveVersionModal({model:@levelSystem})
Backbone.Mediator.publish 'level:view-switched', e
toggleWatchSystem: ->
console.log 'toggle watch system?'
button = @$el.find('#system-watch-button')
@levelSystem.watch(button.find('.watch').is(':visible'))
button.find('> span').toggleClass('secret')
destroy: -> destroy: ->
@editor?.destroy() @editor?.destroy()
super() super()

View file

@ -23,6 +23,7 @@ module.exports = class SystemsTabView extends View
events: events:
'click #add-system-button': 'addLevelSystem' 'click #add-system-button': 'addLevelSystem'
'click #create-new-system-button': 'createNewLevelSystem' 'click #create-new-system-button': 'createNewLevelSystem'
'click #create-new-system': 'createNewLevelSystem'
constructor: (options) -> constructor: (options) ->
super options super options

View file

@ -255,7 +255,7 @@ module.exports = class ThangsTabView extends View
# @thangsTreema.deselectAll() # @thangsTreema.deselectAll()
selectAddThang: (e) => selectAddThang: (e) =>
return unless $(e.target).closest('.editor-level-thangs-tab-view').length return unless e? and $(e.target).closest('.editor-level-thangs-tab-view').length
if e then target = $(e.target) else target = @$el.find('.add-thangs-palette') # pretend to click on background if no event 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' return true if target.attr('id') is 'surface'
target = target.closest('.add-thang-palette-icon') target = target.closest('.add-thang-palette-icon')

View file

@ -7,6 +7,7 @@ module.exports = class PatchModal extends ModalView
id: "patch-modal" id: "patch-modal"
template: template template: template
plain: true plain: true
modalWidthPercent: 60
events: events:
'click #withdraw-button': 'withdrawPatch' 'click #withdraw-button': 'withdrawPatch'
@ -30,12 +31,15 @@ module.exports = class PatchModal extends ModalView
c.isPatchCreator = @patch.get('creator') is auth.me.id c.isPatchCreator = @patch.get('creator') is auth.me.id
c.isPatchRecipient = @targetModel.hasWriteAccess() c.isPatchRecipient = @targetModel.hasWriteAccess()
c.status = @patch.get 'status' c.status = @patch.get 'status'
c.patch = @patch
c c
afterRender: -> afterRender: ->
return if @originalSource.loading return if @originalSource.loading
headModel = @originalSource.clone(false) headModel = null
headModel.set(@targetModel.attributes) if @targetModel.hasWriteAccess()
headModel = @originalSource.clone(false)
headModel.set(@targetModel.attributes)
pendingModel = @originalSource.clone(false) pendingModel = @originalSource.clone(false)
pendingModel.applyDelta(@patch.get('delta')) pendingModel.applyDelta(@patch.get('delta'))
@ -48,7 +52,8 @@ module.exports = class PatchModal extends ModalView
acceptPatch: -> acceptPatch: ->
delta = @deltaView.getApplicableDelta() delta = @deltaView.getApplicableDelta()
@targetModel.applyDelta(delta) @targetModel.applyDelta(delta)
@targetModel.addPatchToAcceptOnSave(@patch) @patch.setStatus('accepted')
@trigger 'accepted-patch'
@hide() @hide()
rejectPatch: -> rejectPatch: ->

View file

@ -44,8 +44,11 @@ module.exports = class PatchesView extends CocoView
@$el.find(".#{@status}").addClass 'active' @$el.find(".#{@status}").addClass 'active'
onStatusButtonsChanged: (e) -> onStatusButtonsChanged: (e) ->
@loaded = false
@status = $(e.target).val() @status = $(e.target).val()
@reloadPatches()
reloadPatches: ->
@loaded = false
@initPatches() @initPatches()
@load() @load()
@render() @render()
@ -53,4 +56,9 @@ module.exports = class PatchesView extends CocoView
openPatchModal: (e) -> openPatchModal: (e) ->
patch = _.find @patches.models, {id:$(e.target).data('patch-id')} patch = _.find @patches.models, {id:$(e.target).data('patch-id')}
modal = new PatchModal(patch, @model) modal = new PatchModal(patch, @model)
@openModalView(modal) @openModalView(modal)
@listenTo modal, 'accepted-patch', -> @trigger 'accepted-patch'
@listenTo modal, 'hide', ->
f = => @reloadPatches()
setTimeout(f, 400)
@stopListening modal

View file

@ -0,0 +1,9 @@
VersionsModalView = require 'views/modal/versions_modal'
module.exports = class SystemVersionsView extends VersionsModalView
id: "editor-system-versions-view"
url: "/db/level.system/"
page: "system"
constructor: (options, @ID) ->
super options, ID, require 'models/LevelSystem'

View file

@ -207,6 +207,7 @@ class CocoView extends Backbone.View
# Modals # Modals
toggleModal: (e) -> toggleModal: (e) ->
return if visibleModal
if $(e.currentTarget).prop('target') is '_blank' if $(e.currentTarget).prop('target') is '_blank'
return true return true
# special handler for opening modals that are dynamically loaded, rather than static in the page. It works (or should work) like Bootstrap's modals, except use coco-modal for the data-toggle value. # special handler for opening modals that are dynamically loaded, rather than static in the page. It works (or should work) like Bootstrap's modals, except use coco-modal for the data-toggle value.

View file

@ -45,9 +45,11 @@ module.exports = class ModalView extends CocoView
super($el) super($el)
hide: -> hide: ->
@trigger 'hide'
@$el.removeClass('fade').modal "hide" @$el.removeClass('fade').modal "hide"
onHidden: -> onHidden: ->
@trigger 'hidden'
destroy: -> destroy: ->
@hide() unless @hidden @hide() unless @hidden

View file

@ -8,6 +8,7 @@ module.exports = class SaveVersionModal extends ModalView
id: 'save-version-modal' id: 'save-version-modal'
template: template template: template
plain: true plain: true
modalWidthPercent: 60
events: events:
'click #save-version-button': 'onClickSaveButton' 'click #save-version-button': 'onClickSaveButton'
@ -26,15 +27,16 @@ module.exports = class SaveVersionModal extends ModalView
c.hasChanges = @model.hasLocalChanges() c.hasChanges = @model.hasLocalChanges()
c c
afterRender: -> afterRender: (insertDeltaView=true) ->
super() super()
@$el.find(if me.get('signedCLA') then '#accept-cla-wrapper' else '#save-version-button').hide() @$el.find(if me.get('signedCLA') then '#accept-cla-wrapper' else '#save-version-button').hide()
changeEl = @$el.find('.changes-stub') changeEl = @$el.find('.changes-stub')
try if insertDeltaView
deltaView = new DeltaView({model:@model}) try
@insertSubView(deltaView, changeEl) deltaView = new DeltaView({model:@model})
catch e @insertSubView(deltaView, changeEl)
console.error "Couldn't create delta view:", e catch e
console.error "Couldn't create delta view:", e
@$el.find('.commit-message input').attr('placeholder', $.i18n.t('general.commit_msg')) @$el.find('.commit-message input').attr('placeholder', $.i18n.t('general.commit_msg'))
onClickSaveButton: -> onClickSaveButton: ->
@ -54,6 +56,7 @@ module.exports = class SaveVersionModal extends ModalView
} }
errors = patch.validate() errors = patch.validate()
forms.applyErrorsToForm(@$el, errors) if errors forms.applyErrorsToForm(@$el, errors) if errors
patch.set 'editPath', document.location.pathname
res = patch.save() res = patch.save()
return unless res return unless res
@enableModalInProgress(@$el) @enableModalInProgress(@$el)

View file

@ -13,6 +13,8 @@ class VersionsViewCollection extends Backbone.Collection
module.exports = class VersionsModalView extends ModalView module.exports = class VersionsModalView extends ModalView
template: template template: template
startsLoading: true startsLoading: true
plain: true
modalWidthPercent: 80
# needs to be overwritten by child # needs to be overwritten by child
id: "" id: ""

View file

@ -4,6 +4,8 @@ Grid = require 'gridfs-stream'
errors = require './errors' errors = require './errors'
log = require 'winston' log = require 'winston'
Patch = require '../patches/Patch' Patch = require '../patches/Patch'
User = require '../users/User'
sendwithus = require '../sendwithus'
PROJECT = {original:1, name:1, version:1, description: 1, slug:1, kind: 1} PROJECT = {original:1, name:1, version:1, description: 1, slug:1, kind: 1}
FETCH_LIMIT = 200 FETCH_LIMIT = 200
@ -95,7 +97,7 @@ module.exports = class Handler
# this handler should be overwritten by subclasses # this handler should be overwritten by subclasses
if @modelClass.schema.is_patchable if @modelClass.schema.is_patchable
return @getPatchesFor(req, res, args[0]) if req.route.method is 'get' and args[1] is 'patches' return @getPatchesFor(req, res, args[0]) if req.route.method is 'get' and args[1] is 'patches'
return @setListening(req, res, args[0]) if req.route.method is 'put' and args[1] is 'listen' return @setWatching(req, res, args[0]) if req.route.method is 'put' and args[1] is 'watch'
return @sendNotFoundError(res) return @sendNotFoundError(res)
getPatchesFor: (req, res, id) -> getPatchesFor: (req, res, id) ->
@ -105,16 +107,16 @@ module.exports = class Handler
patches = (patch.toObject() for patch in patches) patches = (patch.toObject() for patch in patches)
@sendSuccess(res, patches) @sendSuccess(res, patches)
setListening: (req, res, id) -> setWatching: (req, res, id) ->
@getDocumentForIdOrSlug id, (err, document) => @getDocumentForIdOrSlug id, (err, document) =>
return @sendUnauthorizedError(res) unless @hasAccessToDocument(req, document, 'get') return @sendUnauthorizedError(res) unless @hasAccessToDocument(req, document, 'get')
return @sendDatabaseError(res, err) if err return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) unless document? return @sendNotFoundError(res) unless document?
listeners = document.get('listeners') or [] watchers = document.get('watchers') or []
me = req.user.get('_id') me = req.user.get('_id')
listeners = (l for l in listeners when not l.equals(me)) watchers = (l for l in watchers when not l.equals(me))
listeners.push me if req.body.on watchers.push me if req.body.on and req.body.on isnt 'false'
document.set 'listeners', listeners document.set 'watchers', watchers
document.save (err, document) => document.save (err, document) =>
return @sendDatabaseError(res, err) if err return @sendDatabaseError(res, err) if err
@sendSuccess(res, @formatEntity(req, document)) @sendSuccess(res, @formatEntity(req, document))
@ -233,6 +235,9 @@ module.exports = class Handler
return @sendBadInputError(res, err.errors) if err?.valid is false return @sendBadInputError(res, err.errors) if err?.valid is false
return @sendDatabaseError(res, err) if err return @sendDatabaseError(res, err) if err
@sendSuccess(res, @formatEntity(req, document)) @sendSuccess(res, @formatEntity(req, document))
@onPostSuccess(req, document)
onPostSuccess: (req, doc) ->
### ###
TODO: think about pulling some common stuff out of postFirstVersion/postNewVersion TODO: think about pulling some common stuff out of postFirstVersion/postNewVersion
@ -248,7 +253,6 @@ module.exports = class Handler
document.set('original', document._id) document.set('original', document._id)
document.set('creator', req.user._id) document.set('creator', req.user._id)
@saveChangesToDocument req, document, (err) => @saveChangesToDocument req, document, (err) =>
console.log 'saved new version', document.toObject()
return @sendBadInputError(res, err.errors) if err?.valid is false return @sendBadInputError(res, err.errors) if err?.valid is false
return @sendDatabaseError(res, err) if err return @sendDatabaseError(res, err) if err
@sendSuccess(res, @formatEntity(req, document)) @sendSuccess(res, @formatEntity(req, document))
@ -291,6 +295,7 @@ module.exports = class Handler
newDocument.save (err) => newDocument.save (err) =>
return @sendDatabaseError(res, err) if err return @sendDatabaseError(res, err) if err
@sendSuccess(res, @formatEntity(req, newDocument)) @sendSuccess(res, @formatEntity(req, newDocument))
@notifyWatchersOfChange(req.user, newDocument, req.body.editPath) if @modelClass.schema.is_patchable
if major? if major?
parentDocument.makeNewMinorVersion(updatedObject, major, done) parentDocument.makeNewMinorVersion(updatedObject, major, done)
@ -298,8 +303,31 @@ module.exports = class Handler
else else
parentDocument.makeNewMajorVersion(updatedObject, done) parentDocument.makeNewMajorVersion(updatedObject, done)
notifyWatchersOfChange: (editor, changedDocument, editPath) ->
watchers = changedDocument.get('watchers') or []
watchers = (w for w in watchers when not w.equals(editor.get('_id')))
return unless watchers.length
User.find({_id:{$in:watchers}}).select({email:1, name:1}).exec (err, watchers) =>
for watcher in watchers
@notifyWatcherOfChange editor, watcher, changedDocument, editPath
notifyWatcherOfChange: (editor, watcher, changedDocument, editPath) ->
context =
email_id: sendwithus.templates.change_made_notify_watcher
recipient:
address: watcher.get('email')
name: watcher.get('name')
email_data:
doc_name: changedDocument.get('name') or '???'
submitter_name: editor.get('name') or '???'
doc_link: if editPath then "http://codecombat.com#{editPath}" else null
commit_message: changedDocument.get('commitMessage')
sendwithus.api.send context, (err, result) ->
makeNewInstance: (req) -> makeNewInstance: (req) ->
new @modelClass({}) model = new @modelClass({})
model.set 'watchers', [req.user.get('_id')] if @modelClass.schema.is_patchable
model
validateDocumentInput: (input) -> validateDocumentInput: (input) ->
tv4 = require('tv4').tv4 tv4 = require('tv4').tv4

View file

@ -39,8 +39,10 @@ PatchSchema.pre 'save', (next) ->
target.original = targetID target.original = targetID
patches = document.get('patches') or [] patches = document.get('patches') or []
patches = _.clone patches
patches.push @_id patches.push @_id
document.set 'patches', patches document.set 'patches', patches, {strict: false}
@targetLoaded = document
document.save (err) -> next(err) document.save (err) -> next(err)
module.exports = mongoose.model('patch', PatchSchema) module.exports = mongoose.model('patch', PatchSchema)

View file

@ -1,8 +1,11 @@
Patch = require('./Patch') Patch = require('./Patch')
User = require '../users/User'
Handler = require('../commons/Handler') Handler = require('../commons/Handler')
schema = require '../../app/schemas/models/patch' schema = require '../../app/schemas/models/patch'
{handlers} = require '../commons/mapping' {handlers} = require '../commons/mapping'
mongoose = require('mongoose') mongoose = require('mongoose')
log = require 'winston'
sendwithus = require '../sendwithus'
PatchHandler = class PatchHandler extends Handler PatchHandler = class PatchHandler extends Handler
modelClass: Patch modelClass: Patch
@ -50,6 +53,29 @@ PatchHandler = class PatchHandler extends Handler
patch.update {$set:{status:newStatus}}, {}, -> patch.update {$set:{status:newStatus}}, {}, ->
target.update {$pull:{patches:patch.get('_id')}}, {}, -> target.update {$pull:{patches:patch.get('_id')}}, {}, ->
@sendSuccess(res, null) @sendSuccess(res, null)
onPostSuccess: (req, doc) ->
log.error "Error sending patch created: could not find the loaded target on the patch object." unless doc.targetLoaded
return unless doc.targetLoaded
watchers = doc.targetLoaded.get('watchers') or []
watchers = (w for w in watchers when not w.equals(editor.get('_id')))
return unless watchers?.length
User.find({_id:{$in:watchers}}).select({email:1, name:1}).exec (err, watchers) =>
for watcher in watchers
@sendPatchCreatedEmail req.user, watcher, doc, doc.targetLoaded, req.body.editPath
sendPatchCreatedEmail: (patchCreator, watcher, patch, target, editPath) ->
# return if watcher._id is patchCreator._id
context =
email_id: sendwithus.templates.patch_created
recipient:
address: watcher.get('email')
name: watcher.get('name')
email_data:
doc_name: target.get('name') or '???'
submitter_name: patchCreator.get('name') or '???'
doc_link: "http://codecombat.com#{editPath}"
commit_message: patch.get('commitMessage')
sendwithus.api.send context, (err, result) ->
module.exports = new PatchHandler() module.exports = new PatchHandler()

View file

@ -14,3 +14,5 @@ if config.unittest
module.exports.templates = module.exports.templates =
welcome_email: 'utnGaBHuSU4Hmsi7qrAypU' welcome_email: 'utnGaBHuSU4Hmsi7qrAypU'
ladder_update_email: 'JzaZxf39A4cKMxpPZUfWy4' ladder_update_email: 'JzaZxf39A4cKMxpPZUfWy4'
patch_created: 'tem_xhxuNosLALsizTNojBjNcL'
change_made_notify_watcher: 'tem_7KVkfmv9SZETb25dtHbUtG'

View file

@ -64,13 +64,13 @@ GLOBAL.unittest = {}
unittest.users = unittest.users or {} unittest.users = unittest.users or {}
unittest.getNormalJoe = (done, force) -> unittest.getNormalJoe = (done, force) ->
unittest.getUser('normal@jo.com', 'food', done, force) unittest.getUser('Joe', 'normal@jo.com', 'food', done, force)
unittest.getOtherSam = (done, force) -> unittest.getOtherSam = (done, force) ->
unittest.getUser('other@sam.com', 'beer', done, force) unittest.getUser('Sam', 'other@sam.com', 'beer', done, force)
unittest.getAdmin = (done, force) -> unittest.getAdmin = (done, force) ->
unittest.getUser('admin@afc.com', '80yqxpb38j', done, force) unittest.getUser('Admin', 'admin@afc.com', '80yqxpb38j', done, force)
unittest.getUser = (email, password, done, force) -> unittest.getUser = (name, email, password, done, force) ->
# Creates the user if it doesn't already exist. # Creates the user if it doesn't already exist.
return done(unittest.users[email]) if unittest.users[email] and not force return done(unittest.users[email]) if unittest.users[email] and not force
@ -81,6 +81,7 @@ unittest.getUser = (email, password, done, force) ->
throw err if err throw err if err
User.findOne({email:email}).exec((err, user) -> User.findOne({email:email}).exec((err, user) ->
user.set('permissions', if password is '80yqxpb38j' then [ 'admin' ] else []) user.set('permissions', if password is '80yqxpb38j' then [ 'admin' ] else [])
user.set('name', name)
user.save (err) -> user.save (err) ->
wrapUpGetUser(email, user, done) wrapUpGetUser(email, user, done)
) )
@ -88,7 +89,6 @@ unittest.getUser = (email, password, done, force) ->
form = req.form() form = req.form()
form.append('email', email) form.append('email', email)
form.append('password', password) form.append('password', password)
wrapUpGetUser = (email, user, done) -> wrapUpGetUser = (email, user, done) ->
unittest.users[email] = user unittest.users[email] = user

View file

@ -16,13 +16,14 @@ describe '/db/patch', ->
patch = patch =
commitMessage: 'Accept this patch!' commitMessage: 'Accept this patch!'
delta: {name:['test']} delta: {name:['test']}
editPath: '/who/knows/yes'
target: target:
id:null id:null
collection: 'article' collection: 'article'
it 'creates an Article to patch', (done) -> it 'creates an Article to patch', (done) ->
loginAdmin -> loginAdmin ->
request.post {uri:articleURL, json:patch}, (err, res, body) -> request.post {uri:articleURL, json:article}, (err, res, body) ->
articles[0] = body articles[0] = body
patch.target.id = articles[0]._id patch.target.id = articles[0]._id
done() done()
@ -51,29 +52,29 @@ describe '/db/patch', ->
body = JSON.parse(body) body = JSON.parse(body)
expect(res.statusCode).toBe(200) expect(res.statusCode).toBe(200)
expect(body.length).toBe(1) expect(body.length).toBe(1)
done() done()
it 'allows you to set yourself as listening', (done) -> it 'allows you to set yourself as watching', (done) ->
listeningURL = getURL("/db/article/#{articles[0]._id}/listen") watchingURL = getURL("/db/article/#{articles[0]._id}/watch")
request.put {uri: listeningURL, json: {on:true}}, (err, res, body) -> request.put {uri: watchingURL, json: {on:true}}, (err, res, body) ->
expect(body.listeners[0]).toBeDefined() expect(body.watchers[1]).toBeDefined()
done() done()
it 'added the listener to the target document', (done) -> it 'added the watcher to the target document', (done) ->
Article.findOne({}).exec (err, article) -> Article.findOne({}).exec (err, article) ->
expect(article.toObject().listeners[0]).toBeDefined() expect(article.toObject().watchers[1]).toBeDefined()
done() done()
it 'does not add duplicate listeners', (done) -> it 'does not add duplicate watchers', (done) ->
listeningURL = getURL("/db/article/#{articles[0]._id}/listen") watchingURL = getURL("/db/article/#{articles[0]._id}/watch")
request.put {uri: listeningURL, json: {on:true}}, (err, res, body) -> request.put {uri: watchingURL, json: {on:true}}, (err, res, body) ->
expect(body.listeners.length).toBe(1) expect(body.watchers.length).toBe(2)
done() done()
it 'allows removing yourself', (done) -> it 'allows removing yourself', (done) ->
listeningURL = getURL("/db/article/#{articles[0]._id}/listen") watchingURL = getURL("/db/article/#{articles[0]._id}/watch")
request.put {uri: listeningURL, json: {on:false}}, (err, res, body) -> request.put {uri: watchingURL, json: {on:false}}, (err, res, body) ->
expect(body.listeners.length).toBe(0) expect(body.watchers.length).toBe(1)
done() done()
it 'allows the submitter to withdraw the pull request', (done) -> it 'allows the submitter to withdraw the pull request', (done) ->

View file

@ -112,7 +112,7 @@ ghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghl
unittest.getNormalJoe (joe) -> unittest.getNormalJoe (joe) ->
req = request.put getURL(urlUser), (err, res) -> req = request.put getURL(urlUser), (err, res) ->
expect(res.statusCode).toBe(200) expect(res.statusCode).toBe(200)
unittest.getUser('New@email.com', 'null', (joe) -> unittest.getUser('Wilhelm', 'New@email.com', 'null', (joe) ->
expect(joe.get('name')).toBe('Wilhelm') expect(joe.get('name')).toBe('Wilhelm')
expect(joe.get('emailLower')).toBe('new@email.com') expect(joe.get('emailLower')).toBe('new@email.com')
expect(joe.get('email')).toBe('New@email.com') expect(joe.get('email')).toBe('New@email.com')