Built diplomat-specific views for editing levels, components, achievements and thang types.

This commit is contained in:
Scott Erickson 2014-10-27 17:11:48 -07:00
parent 2dbdacd958
commit cea04d27ad
37 changed files with 627 additions and 28 deletions

View file

@ -72,6 +72,12 @@ module.exports = class CocoRouter extends Backbone.Router
'github/*path': 'routeToServer'
'i18n': go('i18n/I18NHomeView')
'i18n/thang/:handle': go('i18n/I18NEditThangTypeView')
'i18n/component/:handle': go('i18n/I18NEditComponentView')
'i18n/level/:handle': go('i18n/I18NEditLevelView')
'i18n/achievement/:handle': go('i18n/I18NEditAchievementView')
'legal': go('LegalView')
'multiplayer': go('MultiplayerView')

View file

@ -175,3 +175,7 @@ prunePath = (delta, path) ->
prunePath delta[path[0]], path.slice(1) unless delta[path[0]] is undefined
keys = (k for k in _.keys(delta[path[0]]) when k isnt '_t')
delete delta[path[0]] if keys.length is 0
module.exports.DOC_SKIP_PATHS = [
'_id','version', 'commitMessage', 'parent', 'created',
'slug', 'index', '__v', 'patches', 'creator', 'js', 'watchers']

View file

@ -536,6 +536,7 @@
achievement_query_misc: "Key achievement off of miscellanea"
achievement_query_goals: "Key achievement off of level goals"
level_completion: "Level Completion"
pop_i18n: "Populate I18N"
article:
edit_btn_preview: "Preview"

View file

@ -233,7 +233,7 @@ class CocoModel extends Backbone.Model
getDelta: ->
differ = deltasLib.makeJSONDiffer()
differ.diff @_revertAttributes, @attributes
differ.diff(_.omit(@_revertAttributes, deltasLib.DOC_SKIP_PATHS), _.omit(@attributes, deltasLib.DOC_SKIP_PATHS))
getDeltaWith: (otherModel) ->
differ = deltasLib.makeJSONDiffer()
@ -272,9 +272,11 @@ class CocoModel extends Backbone.Model
sum = 0
data ?= $.extend true, {}, @attributes
schema ?= @schema() or {}
addedI18N = false
if schema.properties?.i18n and _.isPlainObject(data) and not data.i18n?
data.i18n = {'-':'-'} # mongoose doesn't work with empty objects
sum += 1
addedI18N = true
if _.isPlainObject data
for key, value of data
@ -287,6 +289,7 @@ class CocoModel extends Backbone.Model
if schema.items and _.isArray data
sum += @populateI18N(value, schema.items, path+'/'+index) for value, index in data
@set('i18n', data.i18n) if addedI18N and not path # need special case for root i18n
@updateI18NCoverage()
sum
@ -343,10 +346,8 @@ class CocoModel extends Backbone.Model
updateI18NCoverage: ->
i18nObjects = @findI18NObjects()
console.log 'i18n objects', i18nObjects
return unless i18nObjects.length
langCodeArrays = (_.keys(i18n) for i18n in i18nObjects)
console.log 'lang code arrays', langCodeArrays
window.codes = langCodeArrays
@set('i18nCoverage', _.intersection(langCodeArrays...))
findI18NObjects: (data, results) ->

View file

@ -78,10 +78,7 @@ _.extend AchievementSchema.properties,
default: {kind: 'linear', parameters: {}}
required: ['kind', 'parameters']
additionalProperties: false
i18n: c.object
format: 'i18n'
props: ['name', 'description']
description: 'Help translate this achievement'
i18n: {type: 'object', format: 'i18n', props: ['name', 'description'], description: 'Help translate this achievement'}
rewards: c.RewardSchema 'awarded by this achievement'
@ -93,5 +90,6 @@ _.extend AchievementSchema, # Let's have these on the bottom
AchievementSchema.definitions = {}
AchievementSchema.definitions['mongoQueryOperator'] = MongoQueryOperatorSchema
AchievementSchema.definitions['mongoFindQuery'] = MongoFindQuerySchema
c.extendTranslationCoverageProperties AchievementSchema
module.exports = AchievementSchema

View file

@ -23,6 +23,12 @@ PropertyDocumentationSchema = c.object {
required: ['name', 'type', 'description']
},
name: {type: 'string', title: 'Name', description: 'Name of the property.'}
i18n: { type: 'object', format: 'i18n', props: ['description', 'context'], description: 'Help translate this property'}
context: {
type: 'object'
title: 'Example template context'
additionalProperties: { type: 'string' }
}
codeLanguages: c.array {title: 'Specific Code Languages', description: 'If present, then only the languages specified will show this documentation. Leave unset for language-independent documentation.', format: 'code-languages-array'}, c.shortString(title: 'Code Language', description: 'A specific code language to show this documentation for.', format: 'code-language')
# not actual JS types, just whatever they describe...
type: c.shortString(title: 'Type', description: 'Intended type of the property.')
@ -84,6 +90,7 @@ PropertyDocumentationSchema = c.object {
}
{title: 'Description', type: 'string', description: 'Description of the return value.', maxLength: 1000}
]
i18n: { type: 'object', format: 'i18n', props: ['description'], description: 'Help translate this return value'}
DependencySchema = c.object {
title: 'Component Dependency'
@ -155,5 +162,6 @@ c.extendSearchableProperties LevelComponentSchema
c.extendVersionedProperties LevelComponentSchema, 'level.component'
c.extendPermissionsProperties LevelComponentSchema, 'level.component'
c.extendPatchableProperties LevelComponentSchema
c.extendTranslationCoverageProperties LevelComponentSchema
module.exports = LevelComponentSchema

View file

@ -116,6 +116,7 @@ _.extend ThangTypeSchema.properties,
rotationType: {title: 'Rotation', type: 'string', enum: ['isometric', 'fixed', 'free']}
matchWorldDimensions: {title: 'Match World Dimensions', type: 'boolean'}
shadow: {title: 'Shadow Diameter', type: 'number', format: 'meters', description: 'Shadow diameter in meters'}
description: { type:'string', format: 'markdown', title: 'Description' }
layerPriority:
title: 'Layer Priority'
type: 'integer'
@ -144,6 +145,7 @@ _.extend ThangTypeSchema.properties,
type: 'number'
description: 'Snap to this many meters in the y-direction.'
components: c.array {title: 'Components', description: 'Thangs are configured by changing the Components attached to them.', uniqueItems: true, format: 'thang-components-array'}, ThangComponentSchema # TODO: uniqueness should be based on 'original', not whole thing
i18n: {type: 'object', format: 'i18n', props: ['name', 'description'], description: 'Help translate this ThangType\'s name and description.'}
ThangTypeSchema.required = []
@ -158,5 +160,6 @@ c.extendBasicProperties ThangTypeSchema, 'thang.type'
c.extendSearchableProperties ThangTypeSchema
c.extendVersionedProperties ThangTypeSchema, 'thang.type'
c.extendPatchableProperties ThangTypeSchema
c.extendTranslationCoverageProperties ThangTypeSchema
module.exports = ThangTypeSchema

View file

@ -166,6 +166,7 @@ me.FunctionArgumentSchema = me.object {
required: ['name', 'type', 'example', 'description']
},
name: {type: 'string', pattern: me.identifierPattern, title: 'Name', description: 'Name of the function argument.'}
i18n: { type: 'object', format: 'i18n', props: ['description'], description: 'Help translate this argument'}
# not actual JS types, just whatever they describe...
type: me.shortString(title: 'Type', description: 'Intended type of the argument.')
example:

View file

@ -0,0 +1,12 @@
.i18n-edit-model-view
#patch-submit
margin-top: 5px
td
width: 40%
.outer-content
padding: 10px
select
margin-bottom: 10px

View file

@ -14,6 +14,7 @@ block content
button.achievement-tool-button(data-i18n="", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#recalculate-button Recalculate
button.achievement-tool-button(data-i18n="common.delete", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#delete-button Delete
button.achievement-tool-button(data-i18n="common.save", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#save-button Save
button.achievement-tool-button(data-i18n="editor.pop_i18n").btn.btn-primary#populate-i18n-button Populate I18N
h3(data-i18n="achievement.edit_achievement_title") Edit Achievement
span

View file

@ -34,7 +34,9 @@ nav.navbar.navbar-default(role='navigation')
if !me.get('anonymous')
li#create-new-component-button
a(data-i18n="editor.level_component_b_new") Create New Component
li
a(data-i18n="editor.pop_i18n")#pop-component-i18n-button Populate i18n
li.divider
li.dropdown-header Info

View file

@ -56,6 +56,8 @@ block header
a(data-i18n="common.fork")#fork-start-button Fork
li(class=anonymous ? "disabled": "")
a(data-toggle="coco-modal", data-target="modal/RevertModal", data-i18n="editor.revert")#revert-button Revert
li(class=anonymous ? "disabled": "")
a(data-i18n="editor.pop_i18n")#pop-level-i18n-button Populate i18n
li.divider
li.dropdown-header Info
li#history-button

View file

@ -0,0 +1,71 @@
extends /templates/base
block header
if model.loading
nav.navbar.navbar-default(role='navigation')
.container-fluid
ul.nav.navbar-nav
li
a(href="/i18n")
span.glyphicon-home.glyphicon
else
nav.navbar.navbar-default(role='navigation')
ul.nav.navbar-nav
li
a(href="/i18n")
span.glyphicon-home.glyphicon
.navbar-header
span.navbar-brand #{model.get('name')}
ul.nav.navbar-nav.navbar-right
li
button.btn.btn-info.btn-sm.pull-right#patch-submit(disabled=model.hasLocalChanges() ? null : 'disabled', value=model.id) Submit Changes
li.dropdown
a(data-toggle='dropdown')
span.glyphicon-chevron-down.glyphicon
ul.dropdown-menu
li.dropdown-header Actions
li(class=anonymous ? "disabled": "")
a(data-toggle="coco-modal", data-target="modal/RevertModal", data-i18n="editor.revert")#revert-button Revert
li.divider
li.dropdown-header Info
li#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', 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/diplomat', data-i18n="nav.forum", target="_blank") Forum
li
a(data-toggle="coco-modal", data-target="modal/ContactModal", data-i18n="nav.contact") Email
block outer_content
.outer-content
select.form-control#language-select
table.table
for row in translationList
tr(data-format=row.format || '')
th= row.title
td.english-value-row
div= row.enValue
td.to-value-row
if row.format === 'markdown'
div(data-index=row.index.toString())= row.toValue
else
input.input-sm.form-control.translation-input(data-index=row.index.toString(), value=row.toValue)
div#error-view
.clearfix
block footer

View file

@ -0,0 +1,20 @@
extends /templates/base
block content
table.table.table-condensed
tr
th
select#language-select.form-control.input-sm
th Type
th Specifically Covered
th Generally Covered
for model in collection.models
tr
td
a(href=model.i18nURLBase+model.get('slug'))= model.get('name')
td= model.constructor.className
td(class=model.specificallyCovered ? 'success' : 'danger')= model.specificallyCovered ? 'Yes' : 'No'
td(class=model.generallyCovered ? 'success' : 'danger')= model.generallyCovered ? 'Yes' : 'No'

View file

@ -93,14 +93,14 @@ module.exports = class DeltaView extends CocoView
treemaOptions = { schema: deltaData.schema or {}, readOnly: true }
if _.isObject(deltaData.left) and leftEl = deltaEl.find('.old-value')
options = _.defaults {data: deltaData.left}, treemaOptions
options = _.defaults {data: _.merge({}, deltaData.left)}, treemaOptions
try
TreemaNode.make(leftEl, options).build()
catch error
console.error "Couldn't show left details Treema for", deltaData.left, treemaOptions
if _.isObject(deltaData.right) and rightEl = deltaEl.find('.new-value')
options = _.defaults {data: deltaData.right}, treemaOptions
options = _.defaults {data: _.merge({}, deltaData.right)}, treemaOptions
try
TreemaNode.make(rightEl, options).build()
catch error

View file

@ -2,15 +2,13 @@ ModalView = require 'views/kinds/ModalView'
template = require 'templates/editor/patch_modal'
DeltaView = require 'views/editor/DeltaView'
auth = require 'lib/auth'
deltasLib = require 'lib/deltas'
module.exports = class PatchModal extends ModalView
id: 'patch-modal'
template: template
plain: true
modalWidthPercent: 60
@DOC_SKIP_PATHS = [
'_id','version', 'commitMessage', 'parent', 'created',
'slug', 'index', '__v', 'patches', 'creator', 'js', 'watchers']
events:
'click #withdraw-button': 'withdrawPatch'
@ -54,7 +52,7 @@ module.exports = class PatchModal extends ModalView
afterRender: ->
return super() unless @supermodel.finished() and @deltaWorked
@deltaView = new DeltaView({model:@pendingModel, headModel:@headModel, skipPaths: PatchModal.DOC_SKIP_PATHS})
@deltaView = new DeltaView({model:@pendingModel, headModel:@headModel, skipPaths: deltasLib.DOC_SKIP_PATHS})
changeEl = @$el.find('.changes-stub')
@insertSubView(@deltaView, changeEl)
super()

View file

@ -16,7 +16,9 @@ module.exports = class AchievementEditView extends RootView
'click #recalculate-button': 'confirmRecalculation'
'click #recalculate-all-button': 'confirmAllRecalculation'
'click #delete-button': 'confirmDeletion'
'click #populate-i18n-button': -> @achievement.populateI18N()
constructor: (options, @achievementID) ->
super options
@achievement = new Achievement(_id: @achievementID)

View file

@ -20,6 +20,7 @@ module.exports = class LevelComponentEditView extends CocoView
'click #component-history-button': 'showVersionHistory'
'click #patch-component-button': 'startPatchingComponent'
'click #component-watch-button': 'toggleWatchComponent'
'click #pop-component-i18n-button': -> @levelComponent.populateI18N()
constructor: (options) ->
super options

View file

@ -5,6 +5,7 @@ LevelComponent = require 'models/LevelComponent'
LevelSystem = require 'models/LevelSystem'
DeltaView = require 'views/editor/DeltaView'
PatchModal = require 'views/editor/PatchModal'
deltasLib = require 'lib/deltas'
module.exports = class SaveLevelModal extends SaveVersionModal
template: template
@ -40,7 +41,7 @@ module.exports = class SaveLevelModal extends SaveVersionModal
for changeEl, i in changeEls
model = models[i]
try
deltaView = new DeltaView({model: model, skipPaths: PatchModal.DOC_SKIP_PATHS})
deltaView = new DeltaView({model: model, skipPaths: deltasLib.DOC_SKIP_PATHS})
@insertSubView(deltaView, $(changeEl))
catch e
console.error 'Couldn\'t create delta view:', e

View file

@ -45,6 +45,7 @@ module.exports = class ThangTypeEditView extends RootView
'click .play-with-level-button': 'onPlayLevel'
'click .play-with-level-parent': 'onPlayLevelSelect'
'keyup .play-with-level-input': 'onPlayLevelKeyUp'
'click #pop-level-i18n-button': 'onPopulateLevelI18N'
subscriptions:
'editor:thang-type-color-groups-changed': 'onColorGroupsChanged'
@ -352,7 +353,8 @@ module.exports = class ThangTypeEditView extends RootView
saveNewThangType: (e) ->
newThangType = if e.major then @thangType.cloneNewMajorVersion() else @thangType.cloneNewMinorVersion()
newThangType.set('commitMessage', e.commitMessage)
newThangType.updateI18NCoverage() if newThangType.get('i18nCoverage')
res = newThangType.save()
return unless res
modal = $('#save-version-modal')
@ -455,6 +457,10 @@ module.exports = class ThangTypeEditView extends RootView
showVersionHistory: (e) ->
@openModalView new ThangTypeVersionsModal thangType: @thangType, @thangTypeID
onPopulateLevelI18N: ->
@thangType.populateI18N()
_.delay((-> document.location.reload()), 500)
openSaveModal: ->
@openModalView new SaveVersionModal model: @thangType

View file

@ -0,0 +1,16 @@
I18NEditModelView = require './I18NEditModelView'
Achievement = require 'models/Achievement'
module.exports = class I18NEditAchievementView extends I18NEditModelView
id: "i18n-edit-component-view"
modelClass: Achievement
buildTranslationList: ->
lang = @selectedLanguage
# name, description
if i18n = @model.get('i18n')
if name = @model.get('name')
@wrapRow "Achievement name", ['name'], name, i18n[lang]?.name, []
if description = @model.get('description')
@wrapRow "Achievement description", ['description'], description, i18n[lang]?.description, []

View file

@ -0,0 +1,44 @@
I18NEditModelView = require './I18NEditModelView'
LevelComponent = require 'models/LevelComponent'
module.exports = class I18NEditComponentView extends I18NEditModelView
id: "i18n-edit-component-view"
modelClass: LevelComponent
buildTranslationList: ->
lang = @selectedLanguage
propDocs = @model.get('propertyDocumentation')
for propDoc, propDocIndex in propDocs
#- Component property descriptions
if i18n = propDoc.i18n
path = ["propertyDocumentation", propDocIndex]
if _.isObject propDoc.description
for progLang, description of propDoc
@wrapRow "#{propDoc.name} description (#{progLang})", [progLang, 'description'], description, i18n[lang]?[progLang]?.description, path, 'markdown'
else if _.isString propDoc.description
@wrapRow "#{propDoc.name} description", ['description'], propDoc.description, i18n[lang]?.description, path, 'markdown'
#- Component return value descriptions
if i18n = propDoc.returns?.i18n
path = ["propertyDocumentation", propDocIndex, "returns"]
d = propDoc.returns.description
if _.isObject d
for progLang, description of d
@wrapRow "#{propDoc.name} return val (#{progLang})", [progLang, 'description'], description, i18n[lang]?[progLang]?.description, path, 'markdown'
else if _.isString d
@wrapRow "#{propDoc.name} return val", ['description'], d, i18n[lang]?.description, path, 'markdown'
#- Component argument descriptions
if propDoc.args
for argDoc, argIndex in propDoc.args
if i18n = argDoc.i18n
path = ["propertyDocumentation", propDocIndex, 'args', argIndex]
if _.isObject argDoc.description
for progLang, description of argDoc
@wrapRow "#{propDoc.name} arg description #{argDoc.name} (#{progLang})", [progLang, 'description'], description, i18n[lang]?[progLang]?.description, path, 'markdown'
else if _.isString argDoc.description
@wrapRow "#{propDoc.name} arg description #{argDoc.name}", ['description'], argDoc.description, i18n[lang]?.description, path, 'markdown'

View file

@ -0,0 +1,48 @@
I18NEditModelView = require './I18NEditModelView'
Level = require 'models/Level'
module.exports = class I18NEditLevelView extends I18NEditModelView
id: "i18n-edit-level-view"
modelClass: Level
buildTranslationList: ->
lang = @selectedLanguage
# name, description
if i18n = @model.get('i18n')
if name = @model.get('name')
@wrapRow "Level name", ['name'], name, i18n[lang]?.name, []
if description = @model.get('description')
@wrapRow "Level description", ['description'], description, i18n[lang]?.description, []
# goals
for goal, index in @model.get('goals') ? []
if i18n = goal.i18n
@wrapRow "Goal name", ['name'], goal.name, i18n[lang]?.name, ['goals', index]
# documentation
for doc, index in @model.get('documentation')?.specificArticles ? []
if i18n = doc.i18n
@wrapRow "Guide article name", ['name'], doc.name, i18n[lang]?.name, ['documentation', 'specificArticles', index]
@wrapRow "'#{doc.name}' description", ['description'], doc.description, i18n[lang]?.description, ['documentation', 'specificArticles', index], 'markdown'
# sprite dialogues
for script, scriptIndex in @model.get('scripts') ? []
for noteGroup, noteGroupIndex in script.noteChain ? []
for spriteCommand, spriteCommandIndex in noteGroup.sprites ? []
pathPrefix = ['scripts', scriptIndex, 'noteChain', noteGroupIndex, 'sprites', spriteCommandIndex, 'say']
if i18n = spriteCommand.say?.i18n
if spriteCommand.say.text
@wrapRow "Sprite text", ['text'], spriteCommand.say.text, i18n[lang]?.text, pathPrefix, 'markdown'
if spriteCommand.say.blurb
@wrapRow "Sprite blurb", ['blurb'], spriteCommand.say.blurb, i18n[lang]?.blurb, pathPrefix
for response, responseIndex in spriteCommand.say?.responses ? []
if i18n = response.i18n
@wrapRow "Response button", ['text'], response.text, i18n[lang]?.text, pathPrefix.concat(['responses', responseIndex])
# victory modal
if i18n = @model.get('victory')?.i18n
@wrapRow "Victory text", ['body'], @model.get('victory').body, i18n[lang]?.body, ['victory'], 'markdown'

View file

@ -0,0 +1,159 @@
RootView = require 'views/kinds/RootView'
locale = require 'locale/locale'
Patch = require 'models/Patch'
template = require 'templates/i18n/i18n-edit-model-view'
deltasLib = require 'lib/deltas'
module.exports = class I18NEditModelView extends RootView
className: 'editor i18n-edit-model-view'
template: template
events:
'change .translation-input': 'onInputChanged'
'change #language-select': 'onLanguageSelectChanged'
'click #patch-submit': 'onSubmitPatch'
constructor: (options, @modelHandle) ->
super(options)
@model = new @modelClass(_id: @modelHandle)
@model = @supermodel.loadModel(@model, 'model').model
@model.saveBackups = true
@selectedLanguage = me.get('preferredLanguage', true)
showLoading: ($el) ->
$el ?= @$el.find('.outer-content')
super($el)
onLoaded: ->
super()
@model.markToRevert() unless @model.hasLocalChanges()
getRenderData: ->
c = super()
c.model = @model
c.selectedLanguage = @selectedLanguage
@translationList = []
if @supermodel.finished() then @buildTranslationList() else []
result.index = index for result, index in @translationList
c.translationList = @translationList
c
afterRender: ->
super()
@hush = true
$select = @$el.find('#language-select').empty()
@addLanguagesToSelect($select, @selectedLanguage)
@hush = false
editors = []
@$el.find('tr[data-format="markdown"]').each((index, el) =>
englishEditor = ace.edit(enEl=$(el).find('.english-value-row div')[0])
englishEditor.el = enEl
englishEditor.setReadOnly(true)
toEditor = ace.edit(toEl=$(el).find('.to-value-row div')[0])
toEditor.el = toEl
toEditor.on 'change', @onEditorChange
editors = editors.concat([englishEditor, toEditor])
)
for editor in editors
session = editor.getSession()
session.setTabSize 2
session.setMode 'ace/mode/markdown'
session.setNewLineMode = 'unix'
session.setUseSoftTabs true
editor.setOptions({ maxLines: Infinity })
onEditorChange: (event, editor) =>
return if @destroyed
index = $(editor.el).data('index')
rowInfo = @translationList[index]
value = editor.getValue()
@onTranslationChanged(rowInfo, value)
wrapRow: (title, key, enValue, toValue, path, format) ->
@translationList.push {
title: title,
key: key,
enValue: enValue,
toValue: toValue or '',
path: path
format: format
}
buildTranslationList: -> [] # overwrite
onInputChanged: (e) ->
index = $(e.target).data('index')
rowInfo = @translationList[index]
value = $(e.target).val()
@onTranslationChanged(rowInfo, value)
onTranslationChanged: (rowInfo, value) ->
#- Navigate down to where the translation will live
base = @model.attributes
for seg in rowInfo.path
base = base[seg]
base = base.i18n
base[@selectedLanguage] ?= {}
base = base[@selectedLanguage]
if rowInfo.key.length > 1
for seg in rowInfo.key[..-2]
base[seg] ?= {}
base = base[seg]
#- Set the data in a non-kosher way
base[rowInfo.key[rowInfo.key.length-1]] = value
@model.saveBackup()
#- Enable patch submit button
@$el.find('#patch-submit').attr('disabled', null)
onLanguageSelectChanged: (e) ->
return if @hush
@selectedLanguage = $(e.target).val()
@render()
onSubmitPatch: (e) ->
delta = @model.getDelta()
flattened = deltasLib.flattenDelta(delta)
save = _.all(flattened, (delta) ->
return _.isArray(delta.o) and delta.o.length is 1 and 'i18n' in delta.dataPath
)
if save
modelToSave = @model.cloneNewMinorVersion()
modelToSave.updateI18NCoverage() if modelToSave.get('i18nCoverage')
else
modelToSave = new Patch()
modelToSave.set 'delta', @model.getDelta()
modelToSave.set 'target', {
'collection': _.string.underscored @model.constructor.className
'id': @model.id
}
if @modelClass.schema.properties.commitMessage
commitMessage = "Diplomat submission for lang #{@selectedLanguage}: #{flattened.length} change(s)."
modelToSave.set 'commitMessage', commitMessage
errors = modelToSave.validate()
button = $(e.target)
button.attr('disabled', 'disabled')
return button.text('Failed to Submit Changes') if errors
res = modelToSave.save()
return button.text('Failed to Submit Changes') unless res
res.error => button.text('Error Submitting Changes')
res.success => button.text('Submit Changes')

View file

@ -0,0 +1,15 @@
I18NEditModelView = require './I18NEditModelView'
ThangType = require 'models/ThangType'
module.exports = class ThangTypeI18NView extends I18NEditModelView
id: "thang-type-i18n-view"
modelClass: ThangType
buildTranslationList: ->
lang = @selectedLanguage
@model.markToRevert() unless @model.hasLocalChanges()
i18n = @model.get('i18n')
if i18n
name = @model.get('name')
@wrapRow('Name', ['name'], name, i18n[lang]?.name, [])
@wrapRow('Description', ['description'], @model.get('description'), i18n[lang]?.description, [], 'markdown')

View file

@ -0,0 +1,87 @@
RootView = require 'views/kinds/RootView'
template = require 'templates/i18n/i18n-home-view'
CocoCollection = require 'collections/CocoCollection'
LevelComponent = require 'models/LevelComponent'
ThangType = require 'models/ThangType'
Level = require 'models/Level'
Achievement = require 'models/Achievement'
languages = _.keys(require 'locale/locale').sort()
PAGE_SIZE = 100
module.exports = class I18NHomeView extends RootView
id: "i18n-home-view"
template: template
events:
'change #language-select': 'onLanguageSelectChanged'
constructor: (options) ->
super(options)
@selectedLanguage = me.get('preferredLanguage', true)
#-
@aggregateModels = new Backbone.Collection()
project = ['name', 'components.original', 'i18nCoverage', 'slug']
@thangTypes = new CocoCollection([], { url: '/db/thang.type?view=i18n-coverage', project: project, model: ThangType })
@components = new CocoCollection([], { url: '/db/level.component?view=i18n-coverage', project: project, model: LevelComponent })
@levels = new CocoCollection([], { url: '/db/level?view=i18n-coverage', project: project, model: Level })
@achievements = new CocoCollection([], { url: '/db/achievement?view=i18n-coverage', project: project, model: Achievement })
for c in [@thangTypes, @components, @levels, @achievements]
c.skip = 0
c.fetch({data: {skip: 0, limit: PAGE_SIZE}, cache:false})
@supermodel.loadCollection(c, 'documents')
@listenTo c, 'sync', @onCollectionSynced
onCollectionSynced: (collection) ->
for model in collection.models
model.i18nURLBase = switch model.constructor.className
when "ThangType" then "/i18n/thang/"
when "LevelComponent" then "/i18n/component/"
when "Achievement" then "/i18n/achievement/"
when "Level" then "/i18n/level/"
getMore = collection.models.length is PAGE_SIZE
@aggregateModels.add(collection.models)
@render()
if getMore
collection.skip += PAGE_SIZE
collection.fetch({data: {skip: collection.skip, limit: PAGE_SIZE}})
getRenderData: ->
c = super()
@updateCoverage()
c.languages = languages
c.selectedLanguage = @selectedLanguage
c.collection = @aggregateModels
c
updateCoverage: ->
selectedBase = @selectedLanguage[..2]
relatedLanguages = (l for l in languages when l.startsWith(selectedBase) and l isnt @selectedLanguage)
for model in @aggregateModels.models
@updateCoverageForModel(model, relatedLanguages)
model.generallyCovered = true if @selectedLanguage.startsWith 'en'
updateCoverageForModel: (model, relatedLanguages) ->
model.specificallyCovered = true
model.generallyCovered = true
coverage = model.get('i18nCoverage')
if @selectedLanguage not in coverage
model.specificallyCovered = false
if not _.any((l in coverage for l in relatedLanguages))
model.generallyCovered = false
return
afterRender: ->
super()
@addLanguagesToSelect(@$el.find('#language-select'), @selectedLanguage)
onLanguageSelectChanged: (e) ->
@selectedLanguage = $(e.target).val()
@render()

View file

@ -106,15 +106,20 @@ module.exports = class RootView extends CocoView
$select.parent().find('.options, .trigger').remove()
$select.unwrap().removeClass('fancified')
preferred = me.get('preferredLanguage', true)
@addLanguagesToSelect($select, preferred)
$select.fancySelect().parent().find('.trigger').addClass('header-font')
$('body').attr('lang', preferred)
addLanguagesToSelect: ($select, initialVal) ->
initialVal ?= me.get('preferredLanguage', true)
codes = _.keys(locale)
genericCodes = _.filter codes, (code) ->
_.find(codes, (code2) ->
code2 isnt code and code2.split('-')[0] is code)
for code, localeInfo of locale when not (code in genericCodes) or code is preferred
for code, localeInfo of locale when not (code in genericCodes) or code is initialVal
$select.append(
$('<option></option>').val(code).text(localeInfo.nativeDescription))
$select.val(preferred).fancySelect().parent().find('.trigger').addClass('header-font')
$('body').attr('lang', preferred)
$select.val(initialVal)
onLanguageChanged: ->
newLang = $('.language-dropdown').val()

View file

@ -4,6 +4,7 @@ DeltaView = require 'views/editor/DeltaView'
PatchModal = require 'views/editor/PatchModal'
nameLoader = require 'lib/NameLoader'
CocoCollection = require 'collections/CocoCollection'
deltasLib = require 'lib/deltas'
class VersionsViewCollection extends CocoCollection
url: ''
@ -55,7 +56,7 @@ module.exports = class VersionsModal extends ModalView
@deltaView = new DeltaView({
model: earlierVersion
comparisonModel: laterVersion
skipPaths: PatchModal.DOC_SKIP_PATHS
skipPaths: deltasLib.DOC_SKIP_PATHS
loadModels: true
})
@insertSubView(@deltaView, deltaEl)

View file

@ -68,7 +68,8 @@
"node-gyp": "~0.13.0",
"aether": "~0.2.39",
"JASON": "~0.1.3",
"JQDeferred": "~2.1.0"
"JQDeferred": "~2.1.0",
"jsondiffpatch": "0.1.17"
},
"devDependencies": {
"jade": "0.33.x",

View file

@ -64,6 +64,7 @@ AchievementSchema.post 'save', -> @constructor.loadAchievements()
AchievementSchema.plugin(plugins.NamedPlugin)
AchievementSchema.plugin(plugins.SearchablePlugin, {searchable: ['name']})
AchievementSchema.plugin plugins.TranslationCoveragePlugin
module.exports = Achievement = mongoose.model('Achievement', AchievementSchema, 'achievements')

View file

@ -5,13 +5,37 @@ class AchievementHandler extends Handler
modelClass: Achievement
# Used to determine which properties requests may edit
editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon', 'function', 'related', 'difficulty', 'category', 'rewards']
editableProperties: [
'name'
'query'
'worth'
'collection'
'description'
'userField'
'proportionalTo'
'icon'
'function'
'related'
'difficulty'
'category'
'rewards'
'i18n'
'i18nCoverage'
]
allowedMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
jsonSchema = require '../../app/schemas/models/achievement.coffee'
hasAccess: (req) ->
req.method is 'GET' or req.user?.isAdmin()
req.method in ['GET', 'PUT'] or req.user?.isAdmin()
hasAccessToDocument: (req, document, method=null) ->
method = (method or req.method).toLowerCase()
return true if method is 'get'
return true if req.user?.isAdmin()
return true if method is 'put' and @isJustFillingTranslations(req, document)
return
get: (req, res) ->
# /db/achievement?related=<ID>

View file

@ -7,6 +7,7 @@ Patch = require '../patches/Patch'
User = require '../users/User'
sendwithus = require '../sendwithus'
hipchat = require '../hipchat'
deltasLib = require '../../app/lib/deltas'
PROJECT = {original: 1, name: 1, version: 1, description: 1, slug: 1, kind: 1, created: 1, permissions: 1}
FETCH_LIMIT = 300
@ -32,9 +33,22 @@ module.exports = class Handler
hasAccess: (req) -> true
hasAccessToDocument: (req, document, method=null) ->
return true if req.user?.isAdmin()
if @modelClass.schema.uses_coco_translation_coverage and (method or req.method).toLowerCase() is 'put'
return true if @isJustFillingTranslations(req, document)
if @modelClass.schema.uses_coco_permissions
return document.hasPermissionsForMethod?(req.user, method or req.method)
return true
isJustFillingTranslations: (req, document) ->
differ = deltasLib.makeJSONDiffer()
omissions = ['original'].concat(deltasLib.DOC_SKIP_PATHS)
delta = differ.diff(_.omit(document.toObject(), omissions), _.omit(req.body, omissions))
flattened = deltasLib.flattenDelta(delta)
_.all(flattened, (delta) ->
# sometimes coverage gets moved around... allow other changes to happen to i18nCoverage
return _.isArray(delta.o) and (('i18n' in delta.dataPath and delta.o.length is 1) or 'i18nCoverage' in delta.dataPath))
formatEntity: (req, document) -> document?.toObject()
getEditableProperties: (req, document) ->
@ -87,6 +101,27 @@ module.exports = class Handler
get: (req, res) ->
@sendForbiddenError(res) if not @hasAccess(req)
if @modelClass.schema.uses_coco_translation_coverage and req.query.view is 'i18n-coverage'
# TODO: generalize view, project, limit and skip query parameters
projection = {}
if req.query.project
projection[field] = 1 for field in req.query.project.split(',')
query = {slug: {$exists: true}, i18nCoverage: {$exists: true}}
q = @modelClass.find(query, projection)
skip = parseInt(req.query.skip)
if skip? and skip < 1000000
q.skip(skip)
limit = parseInt(req.query.limit)
if limit? and limit < 1000
q.limit(limit)
q.exec (err, documents) =>
return @sendDatabaseError(res, err) if err
documents = (@formatEntity(req, doc) for doc in documents)
@sendSuccess(res, documents)
specialParameters = ['term', 'project', 'conditions']
# If the model uses coco search it's probably a text search

View file

@ -12,6 +12,7 @@ LevelComponentSchema.plugin plugins.PermissionsPlugin
LevelComponentSchema.plugin plugins.VersionedPlugin
LevelComponentSchema.plugin plugins.SearchablePlugin, {searchable: ['name', 'searchStrings', 'description']}
LevelComponentSchema.plugin plugins.PatchablePlugin
LevelComponentSchema.plugin plugins.TranslationCoveragePlugin
LevelComponentSchema.pre('save', (next) ->
name = @get('name')
strings = _.str.humanize(name).toLowerCase().split(' ')

View file

@ -14,6 +14,7 @@ LevelComponentHandler = class LevelComponentHandler extends Handler
'propertyDocumentation'
'configSchema'
'name'
'i18nCoverage'
]
getEditableProperties: (req, document) ->

View file

@ -28,6 +28,7 @@ LevelHandler = class LevelHandler extends Handler
'banner'
'employerDescription'
'terrain'
'i18nCoverage'
]
postEditableProperties: ['name']

View file

@ -36,13 +36,22 @@ ThangTypeHandler = class ThangTypeHandler extends Handler
'featureImage'
'spriteType'
'i18nCoverage'
'i18n'
'description'
]
hasAccess: (req) ->
req.method is 'GET' or req.user?.isAdmin()
req.method in ['GET', 'PUT'] or req.user?.isAdmin()
hasAccessToDocument: (req, document, method=null) ->
method = (method or req.method).toLowerCase()
return true if method is 'get'
return true if req.user?.isAdmin()
return true if method is 'put' and @isJustFillingTranslations(req, document)
return
get: (req, res) ->
if req.query.view in ['items', 'heroes']
if req.query.view in ['items', 'heroes', 'i18n-coverage']
projection = {}
if req.query.project
projection[field] = 1 for field in req.query.project.split(',')
@ -52,7 +61,19 @@ ThangTypeHandler = class ThangTypeHandler extends Handler
else if req.query.view is 'heroes'
query.kind = 'Unit'
query.original = {$in: _.values heroes} # TODO: replace with some sort of ThangType property later
ThangType.find(query, projection).exec (err, documents) =>
else if req.query.view is 'i18n-coverage'
query.i18nCoverage = {$exists: true}
q = ThangType.find(query, projection)
skip = parseInt(req.query.skip)
if skip? and skip < 1000000
q.skip(skip)
limit = parseInt(req.query.limit)
if limit? and limit < 1000
q.limit(limit)
q.exec (err, documents) =>
return @sendDatabaseError(res, err) if err
documents = (@formatEntity(req, doc) for doc in documents)
@sendSuccess(res, documents)

View file

@ -14,6 +14,7 @@ config = require './server_config'
auth = require './server/routes/auth'
UserHandler = require './server/users/user_handler'
global.tv4 = require 'tv4' # required for TreemaUtils to work
global.jsondiffpatch = require 'jsondiffpatch'
productionLogging = (tokens, req, res) ->
status = res.statusCode