From c38288974823218ff94ad39b1038895e6ed55e53 Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Tue, 8 Apr 2014 14:10:50 -0700 Subject: [PATCH 01/46] Bunch of server changes, mainly adding all the JSON schema validation and fixing tests. --- package.json | 4 ++-- server/articles/article_handler.coffee | 1 + server/commons/Handler.coffee | 12 +++++------- server/commons/mapping.coffee | 2 ++ server/commons/schemas.coffee | 13 ++++++++++++- .../components/level_component_handler.coffee | 1 + .../levels/feedbacks/level_feedback_handler.coffee | 1 + server/levels/level_handler.coffee | 1 + server/levels/sessions/level_session_handler.coffee | 1 + server/levels/systems/level_system_handler.coffee | 1 + server/levels/thangs/thang_type_handler.coffee | 1 + server/routes/db.coffee | 3 ++- test/server/common.coffee | 7 ++----- test/server/functional/auth.spec.coffee | 3 +-- test/server/functional/level.spec.coffee | 3 +++ test/server/functional/level_component.spec.coffee | 9 ++++++--- test/server/functional/level_system.spec.coffee | 4 +++- 17 files changed, 45 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 665ac0131..3e2552f1c 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "mongoose": "3.8.x", "mongoose-text-search": "~0.0.2", "request": "2.12.x", - "tv4": "1.0.11", + "tv4": "1.0.x", "lodash": "~2.0.0", "underscore.string": "2.3.x", "async": "0.2.x", @@ -56,7 +56,7 @@ "graceful-fs": "~2.0.1", "node-force-domain": "~0.1.0", "mailchimp-api": "2.0.x", - "express-useragent": "~0.0.9", + "express-useragent": "~0.0.9", "gridfs-stream": "0.4.x", "stream-buffers": "0.2.x", "sendwithus": "2.0.x", diff --git a/server/articles/article_handler.coffee b/server/articles/article_handler.coffee index b519b8b9f..0e632539f 100644 --- a/server/articles/article_handler.coffee +++ b/server/articles/article_handler.coffee @@ -4,6 +4,7 @@ Handler = require('../commons/Handler') ArticleHandler = class ArticleHandler extends Handler modelClass: Article editableProperties: ['body', 'name', 'i18n'] + jsonSchema: require './article_schema' hasAccess: (req) -> req.method is 'GET' or req.user?.isAdmin() diff --git a/server/commons/Handler.coffee b/server/commons/Handler.coffee index f38885fd9..b7b8aad0f 100644 --- a/server/commons/Handler.coffee +++ b/server/commons/Handler.coffee @@ -48,6 +48,7 @@ module.exports = class Handler sendMethodNotAllowed: (res) -> errors.badMethod(res) sendBadInputError: (res, message) -> errors.badInput(res, message) sendDatabaseError: (res, err) -> + return @sendError(res, err.code, err.response) if err.response and err.code log.error "Database error, #{err}" errors.serverError(res, 'Database error, ' + err) @@ -203,10 +204,9 @@ module.exports = class Handler return @sendBadInputError(res, 'No input.') if _.isEmpty(req.body) return @sendBadInputError(res, 'id should not be included.') if req.body._id return @sendUnauthorizedError(res) unless @hasAccess(req) - validation = @validateDocumentInput(req.body) - return @sendBadInputError(res, validation.errors) unless validation.valid document = @makeNewInstance(req) @saveChangesToDocument req, document, (err) => + return @sendBadInputError(res, err.errors) if err?.valid is false return @sendDatabaseError(res, err) if err @sendSuccess(res, @formatEntity(req, document)) @@ -220,13 +220,11 @@ module.exports = class Handler return @sendBadInputError(res, 'No input.') if _.isEmpty(req.body) return @sendBadInputError(res, 'id should not be included.') if req.body._id return @sendUnauthorizedError(res) unless @hasAccess(req) - validation = @validateDocumentInput(req.body) - return @sendBadInputError(res, validation.errors) unless validation.valid document = @makeNewInstance(req) document.set('original', document._id) document.set('creator', req.user._id) @saveChangesToDocument req, document, (err) => - return @sendBadInputError(res, err.response) if err?.response + return @sendBadInputError(res, err.errors) if err?.valid is false return @sendDatabaseError(res, err) if err @sendSuccess(res, @formatEntity(req, document)) @@ -245,8 +243,6 @@ module.exports = class Handler return @sendBadInputError(res, 'This entity is not versioned') unless @modelClass.schema.uses_coco_versions return @sendBadInputError(res, 'No input.') if _.isEmpty(req.body) return @sendUnauthorizedError(res) unless @hasAccess(req) - validation = @validateDocumentInput(req.body) - return @sendBadInputError(res, validation.errors) unless validation.valid @getDocumentForIdOrSlug req.body._id, (err, parentDocument) => return @sendBadInputError(res, 'Bad id.') if err and err.name is 'CastError' return @sendDatabaseError(res, err) if err @@ -261,6 +257,8 @@ module.exports = class Handler delete updatedObject[prop] delete updatedObject._id major = req.body.version?.major + validation = @validateDocumentInput(updatedObject) + return @sendBadInputError(res, validation.errors) unless validation.valid done = (err, newDocument) => return @sendDatabaseError(res, err) if err diff --git a/server/commons/mapping.coffee b/server/commons/mapping.coffee index 2f659811b..d7400c951 100644 --- a/server/commons/mapping.coffee +++ b/server/commons/mapping.coffee @@ -6,6 +6,7 @@ module.exports.handlers = 'level_feedback': 'levels/feedbacks/level_feedback_handler' 'level_session': 'levels/sessions/level_session_handler' 'level_system': 'levels/systems/level_system_handler' + 'patch': 'patches/patch_handler' 'thang_type': 'levels/thangs/thang_type_handler' 'user': 'users/user_handler' @@ -19,6 +20,7 @@ module.exports.schemas = 'level_session': 'levels/sessions/level_session_schema' 'level_system': 'levels/systems/level_system_schema' 'metaschema': 'commons/metaschema' + 'patch': 'patches/patch_schema' 'thang_component': 'levels/thangs/thang_component_schema' 'thang_type': 'levels/thangs/thang_type_schema' 'user': 'users/user_schema' diff --git a/server/commons/schemas.coffee b/server/commons/schemas.coffee index 060ff8348..7d57a8c66 100644 --- a/server/commons/schemas.coffee +++ b/server/commons/schemas.coffee @@ -13,7 +13,7 @@ me.object = (ext, props) -> combine {type: 'object', additionalProperties: false me.array = (ext, items) -> combine {type: 'array', items: items or {}}, ext me.shortString = (ext) -> combine({type: 'string', maxLength: 100}, ext) me.pct = (ext) -> combine({type: 'number', maximum: 1.0, minimum: 0.0}, ext) -me.date = (ext) -> combine({type: 'string', format: 'date-time'}, ext) +me.date = (ext) -> combine({type: ['object', 'string'], format: 'date-time'}, ext) # should just be string (Mongo ID), but sometimes mongoose turns them into objects representing those, so we are lenient me.objectId = (ext) -> schema = combine({type: ['object', 'string'] }, ext) @@ -51,7 +51,18 @@ basicProps = (linkFragment) -> me.extendBasicProperties = (schema, linkFragment) -> schema.properties = {} unless schema.properties? _.extend(schema.properties, basicProps(linkFragment)) + +# PATCHABLE +patchableProps = -> + patches: me.array({title:'Patches'}, { + _id: me.objectId(links: [{rel: "db", href: "/db/patch/{($)}"}], title: "Patch ID", description: "A reference to the patch.") + status: { enum: ['pending', 'accepted', 'rejected', 'cancelled']} + }) + +me.extendPatchableProperties = (schema) -> + schema.properties = {} unless schema.properties? + _.extend(schema.properties, patchableProps()) # NAMED diff --git a/server/levels/components/level_component_handler.coffee b/server/levels/components/level_component_handler.coffee index 89a3ea21c..576bad3c8 100644 --- a/server/levels/components/level_component_handler.coffee +++ b/server/levels/components/level_component_handler.coffee @@ -3,6 +3,7 @@ Handler = require('../../commons/Handler') LevelComponentHandler = class LevelComponentHandler extends Handler modelClass: LevelComponent + jsonSchema: require './level_component_schema' editableProperties: [ 'system' 'description' diff --git a/server/levels/feedbacks/level_feedback_handler.coffee b/server/levels/feedbacks/level_feedback_handler.coffee index 5cb8be50b..21f581ea7 100644 --- a/server/levels/feedbacks/level_feedback_handler.coffee +++ b/server/levels/feedbacks/level_feedback_handler.coffee @@ -4,6 +4,7 @@ Handler = require('../../commons/Handler') class LevelFeedbackHandler extends Handler modelClass: LevelFeedback editableProperties: ['rating', 'review', 'level', 'levelID', 'levelName'] + jsonSchema: require './level_feedback_schema' makeNewInstance: (req) -> feedback = super(req) diff --git a/server/levels/level_handler.coffee b/server/levels/level_handler.coffee index ad26fe0e1..ccd76700a 100644 --- a/server/levels/level_handler.coffee +++ b/server/levels/level_handler.coffee @@ -8,6 +8,7 @@ mongoose = require('mongoose') LevelHandler = class LevelHandler extends Handler modelClass: Level + jsonSchema: require './level_schema' editableProperties: [ 'description' 'documentation' diff --git a/server/levels/sessions/level_session_handler.coffee b/server/levels/sessions/level_session_handler.coffee index ca8680a17..9017f99c4 100644 --- a/server/levels/sessions/level_session_handler.coffee +++ b/server/levels/sessions/level_session_handler.coffee @@ -9,6 +9,7 @@ class LevelSessionHandler extends Handler editableProperties: ['multiplayer', 'players', 'code', 'completed', 'state', 'levelName', 'creatorName', 'levelID', 'screenshot', 'chat', 'teamSpells', 'submitted', 'unsubscribed'] + jsonSchema: require './level_session_schema' getByRelationship: (req, res, args...) -> return @getActiveSessions req, res if args.length is 2 and args[1] is 'active' diff --git a/server/levels/systems/level_system_handler.coffee b/server/levels/systems/level_system_handler.coffee index 1b1e511c1..a76fed659 100644 --- a/server/levels/systems/level_system_handler.coffee +++ b/server/levels/systems/level_system_handler.coffee @@ -13,6 +13,7 @@ LevelSystemHandler = class LevelSystemHandler extends Handler 'configSchema' ] postEditableProperties: ['name'] + jsonSchema: require './level_system_schema' getEditableProperties: (req, document) -> props = super(req, document) diff --git a/server/levels/thangs/thang_type_handler.coffee b/server/levels/thangs/thang_type_handler.coffee index a446b56be..0627fc5f7 100644 --- a/server/levels/thangs/thang_type_handler.coffee +++ b/server/levels/thangs/thang_type_handler.coffee @@ -3,6 +3,7 @@ Handler = require('../../commons/Handler') ThangTypeHandler = class ThangTypeHandler extends Handler modelClass: ThangType + jsonSchema: require './thang_type_schema' editableProperties: [ 'name', 'raw', diff --git a/server/routes/db.coffee b/server/routes/db.coffee index 723e15b90..2cbbc7df9 100644 --- a/server/routes/db.coffee +++ b/server/routes/db.coffee @@ -42,6 +42,7 @@ module.exports.setup = (app) -> catch error log.error("Error trying db method #{req.route.method} route #{parts} from #{name}: #{error}") log.error(error) + log.error(error.stack) errors.notFound(res, "Route #{req.path} not found.") getSchema = (req, res, moduleName) -> @@ -49,7 +50,7 @@ getSchema = (req, res, moduleName) -> name = schemas[moduleName.replace '.', '_'] schema = require('../' + name) - res.send(schema) + res.send(JSON.stringify(schema, null, '\t')) res.end() catch error diff --git a/test/server/common.coffee b/test/server/common.coffee index d88fa21e8..1af742db6 100644 --- a/test/server/common.coffee +++ b/test/server/common.coffee @@ -78,11 +78,8 @@ unittest.getUser = (email, password, done, force) -> req = request.post(getURL('/db/user'), (err, response, body) -> throw err if err User.findOne({email:email}).exec((err, user) -> - if password is '80yqxpb38j' - user.set('permissions', [ 'admin' ]) - user.save (err) -> - wrapUpGetUser(email, user, done) - else + user.set('permissions', if password is '80yqxpb38j' then [ 'admin' ] else []) + user.save (err) -> wrapUpGetUser(email, user, done) ) ) diff --git a/test/server/functional/auth.spec.coffee b/test/server/functional/auth.spec.coffee index 18c3c7fc8..750f4997e 100644 --- a/test/server/functional/auth.spec.coffee +++ b/test/server/functional/auth.spec.coffee @@ -55,7 +55,7 @@ describe '/auth/login', -> it 'rejects wrong passwords', (done) -> req = request.post(urlLogin, (error, response) -> expect(response.statusCode).toBe(401) - expect(response.body.indexOf("wrong, wrong")).toBeGreaterThan(-1) + expect(response.body.indexOf("wrong")).toBeGreaterThan(-1) done() ) form = req.form() @@ -96,7 +96,6 @@ describe '/auth/reset', -> it 'resets user password', (done) -> req = request.post(urlReset, (error, response) -> expect(response).toBeDefined() - console.log 'status code is', response.statusCode expect(response.statusCode).toBe(200) expect(response.body).toBeDefined() passwordReset = response.body diff --git a/test/server/functional/level.spec.coffee b/test/server/functional/level.spec.coffee index 13dc6425a..edd163d0d 100644 --- a/test/server/functional/level.spec.coffee +++ b/test/server/functional/level.spec.coffee @@ -6,6 +6,9 @@ describe 'Level', -> name: "King's Peak 3" description: 'Climb a mountain.' permissions: simplePermissions + scripts: [] + thangs: [] + documentation: {specificArticles:[], generalArticles:[]} urlLevel = '/db/level' diff --git a/test/server/functional/level_component.spec.coffee b/test/server/functional/level_component.spec.coffee index 4850d834c..9127ccefd 100644 --- a/test/server/functional/level_component.spec.coffee +++ b/test/server/functional/level_component.spec.coffee @@ -3,11 +3,14 @@ require '../common' describe 'LevelComponent', -> component = - name:'Bashes Everything' + name:'BashesEverything' description:'Makes the unit uncontrollably bash anything bashable, using the bash system.' code: 'bash();' - language: 'javascript' + language: 'coffeescript' permissions:simplePermissions + propertyDocumentation: [] + system: 'ai' + dependencies: [] components = {} @@ -45,7 +48,7 @@ describe 'LevelComponent', -> it 'have a unique name.', (done) -> loginAdmin -> request.post {uri:url, json:component}, (err, res, body) -> - expect(res.statusCode).toBe(422) + expect(res.statusCode).toBe(409) done() it 'can be read by an admin.', (done) -> diff --git a/test/server/functional/level_system.spec.coffee b/test/server/functional/level_system.spec.coffee index 32ca61df1..229c3a39d 100644 --- a/test/server/functional/level_system.spec.coffee +++ b/test/server/functional/level_system.spec.coffee @@ -11,6 +11,8 @@ describe 'LevelSystem', -> """ language: 'coffeescript' permissions:simplePermissions + dependencies: [] + propertyDocumentation: [] systems = {} @@ -48,7 +50,7 @@ describe 'LevelSystem', -> it 'have a unique name.', (done) -> loginAdmin -> request.post {uri:url, json:system}, (err, res, body) -> - expect(res.statusCode).toBe(422) + expect(res.statusCode).toBe(409) done() it 'can be read by an admin.', (done) -> From ff73aecf08b45a82239d9542e5bfe626d660aa0e Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Tue, 8 Apr 2014 19:08:33 -0700 Subject: [PATCH 02/46] Turned off sendwithus API during testing. --- server/sendwithus.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/sendwithus.coffee b/server/sendwithus.coffee index ad7a07500..bda58d896 100644 --- a/server/sendwithus.coffee +++ b/server/sendwithus.coffee @@ -9,6 +9,8 @@ module.exports.setupRoutes = (app) -> debug = not config.isProduction module.exports.api = new sendwithusAPI swuAPIKey, debug +if config.unittest + module.exports.api.send = -> module.exports.templates = welcome_email: 'utnGaBHuSU4Hmsi7qrAypU' ladder_update_email: 'JzaZxf39A4cKMxpPZUfWy4' From 2f988ba4859e84a6c5c23bd9dfdbcefffa1e5cba Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Tue, 8 Apr 2014 19:26:19 -0700 Subject: [PATCH 03/46] Made the server side of the patch system. --- server/articles/Article.coffee | 5 +- server/articles/article_handler.coffee | 4 + server/articles/article_schema.coffee | 7 +- server/commons/Handler.coffee | 33 ++++- server/commons/schemas.coffee | 3 + server/levels/Level.coffee | 1 + .../levels/components/LevelComponent.coffee | 9 +- .../components/level_component_schema.coffee | 1 + server/levels/level_handler.coffee | 3 +- server/levels/level_schema.coffee | 1 + .../sessions/level_session_handler.coffee | 2 +- server/levels/systems/LevelSystem.coffee | 1 + .../levels/systems/level_system_schema.coffee | 3 +- server/levels/thangs/ThangType.coffee | 7 +- server/levels/thangs/thang_type_schema.coffee | 7 +- server/patches/Patch.coffee | 47 +++++++ server/patches/patch_handler.coffee | 55 ++++++++ server/patches/patch_schema.coffee | 28 +++++ server/plugins/plugins.coffee | 4 + server/users/user_handler.coffee | 2 +- test/server/common.coffee | 2 + test/server/functional/patch.spec.coffee | 117 ++++++++++++++++++ 22 files changed, 317 insertions(+), 25 deletions(-) create mode 100644 server/patches/Patch.coffee create mode 100644 server/patches/patch_handler.coffee create mode 100644 server/patches/patch_schema.coffee create mode 100644 test/server/functional/patch.spec.coffee diff --git a/server/articles/Article.coffee b/server/articles/Article.coffee index 19a1e3253..626fc779c 100644 --- a/server/articles/Article.coffee +++ b/server/articles/Article.coffee @@ -1,12 +1,11 @@ mongoose = require('mongoose') plugins = require('../plugins/plugins') -ArticleSchema = new mongoose.Schema( - body: String, -) +ArticleSchema = new mongoose.Schema(body: String, {strict:false}) ArticleSchema.plugin(plugins.NamedPlugin) ArticleSchema.plugin(plugins.VersionedPlugin) ArticleSchema.plugin(plugins.SearchablePlugin, {searchable: ['body', 'name']}) +ArticleSchema.plugin(plugins.PatchablePlugin) module.exports = mongoose.model('article', ArticleSchema) diff --git a/server/articles/article_handler.coffee b/server/articles/article_handler.coffee index 0e632539f..1d9e90436 100644 --- a/server/articles/article_handler.coffee +++ b/server/articles/article_handler.coffee @@ -9,4 +9,8 @@ ArticleHandler = class ArticleHandler extends Handler hasAccess: (req) -> req.method is 'GET' or req.user?.isAdmin() + hasAccessToDocument: (req, document, method=null) -> + return true if req.method is 'GET' or method is 'get' or req.user?.isAdmin() + return false + module.exports = new ArticleHandler() diff --git a/server/articles/article_schema.coffee b/server/articles/article_schema.coffee index 1fd4769f7..54b9847ea 100644 --- a/server/articles/article_schema.coffee +++ b/server/articles/article_schema.coffee @@ -6,8 +6,9 @@ c.extendNamedProperties ArticleSchema # name first ArticleSchema.properties.body = { type: 'string', title: 'Content', format: 'markdown' } ArticleSchema.properties.i18n = { type: 'object', title: 'i18n', format: 'i18n', props: ['body'] } -c.extendBasicProperties(ArticleSchema, 'article') -c.extendSearchableProperties(ArticleSchema) -c.extendVersionedProperties(ArticleSchema, 'article') +c.extendBasicProperties ArticleSchema, 'article' +c.extendSearchableProperties ArticleSchema +c.extendVersionedProperties ArticleSchema, 'article' +c.extendPatchableProperties ArticleSchema module.exports = ArticleSchema diff --git a/server/commons/Handler.coffee b/server/commons/Handler.coffee index b7b8aad0f..23909b6a2 100644 --- a/server/commons/Handler.coffee +++ b/server/commons/Handler.coffee @@ -3,6 +3,7 @@ mongoose = require('mongoose') Grid = require 'gridfs-stream' errors = require './errors' log = require 'winston' +Patch = require '../patches/Patch' PROJECT = {original:1, name:1, version:1, description: 1, slug:1, kind: 1} FETCH_LIMIT = 200 @@ -27,8 +28,7 @@ module.exports = class Handler getEditableProperties: (req, document) -> props = @editableProperties.slice() isBrandNew = req.method is 'POST' and not req.body.original - if isBrandNew - props = props.concat @postEditableProperties + props = props.concat @postEditableProperties if isBrandNew if @modelClass.schema.uses_coco_permissions # can only edit permissions if this is a brand new property, @@ -37,8 +37,8 @@ module.exports = class Handler if isBrandNew or isOwner or req.user?.isAdmin() props.push 'permissions' - if @modelClass.schema.uses_coco_versions - props.push 'commitMessage' + props.push 'commitMessage' if @modelClass.schema.uses_coco_versions + props.push 'allowPatches' if @modelClass.schema.is_patchable props @@ -93,8 +93,32 @@ module.exports = class Handler getByRelationship: (req, res, args...) -> # this handler should be overwritten by subclasses + if @modelClass.schema.is_patchable + 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 @sendNotFoundError(res) + getPatchesFor: (req, res, id) -> + query = { 'target.original': mongoose.Types.ObjectId(id), status: req.query.status or 'pending' } + Patch.find(query).sort('-created').exec (err, patches) => + return @sendDatabaseError(res, err) if err + patches = (patch.toObject() for patch in patches) + @sendSuccess(res, patches) + + setListening: (req, res, id) -> + @getDocumentForIdOrSlug id, (err, document) => + return @sendUnauthorizedError(res) unless @hasAccessToDocument(req, document, 'get') + return @sendDatabaseError(res, err) if err + return @sendNotFoundError(res) unless document? + listeners = document.get('listeners') or [] + me = req.user.get('_id') + listeners = (l for l in listeners when not l.equals(me)) + listeners.push me if req.body.on + document.set 'listeners', listeners + document.save (err, document) => + return @sendDatabaseError(res, err) if err + @sendSuccess(res, @formatEntity(req, document)) + search: (req, res) -> unless @modelClass.schema.uses_coco_search return @sendNotFoundError(res) @@ -224,6 +248,7 @@ module.exports = class Handler document.set('original', document._id) document.set('creator', req.user._id) @saveChangesToDocument req, document, (err) => + console.log 'saved new version', document.toObject() return @sendBadInputError(res, err.errors) if err?.valid is false return @sendDatabaseError(res, err) if err @sendSuccess(res, @formatEntity(req, document)) diff --git a/server/commons/schemas.coffee b/server/commons/schemas.coffee index 7d57a8c66..e81f49587 100644 --- a/server/commons/schemas.coffee +++ b/server/commons/schemas.coffee @@ -59,6 +59,9 @@ patchableProps = -> _id: me.objectId(links: [{rel: "db", href: "/db/patch/{($)}"}], title: "Patch ID", description: "A reference to the patch.") status: { enum: ['pending', 'accepted', 'rejected', 'cancelled']} }) + allowPatches: { type: 'boolean' } + listeners: me.array({title:'Listeners'}, + me.objectId(links: [{rel: 'extra', href: "/db/user/{($)}"}])) me.extendPatchableProperties = (schema) -> schema.properties = {} unless schema.properties? diff --git a/server/levels/Level.coffee b/server/levels/Level.coffee index c61245ed5..bb4d10065 100644 --- a/server/levels/Level.coffee +++ b/server/levels/Level.coffee @@ -10,6 +10,7 @@ LevelSchema.plugin(plugins.NamedPlugin) LevelSchema.plugin(plugins.PermissionsPlugin) LevelSchema.plugin(plugins.VersionedPlugin) LevelSchema.plugin(plugins.SearchablePlugin, {searchable: ['name', 'description']}) +LevelSchema.plugin(plugins.PatchablePlugin) LevelSchema.pre 'init', (next) -> return next() unless jsonschema.properties? diff --git a/server/levels/components/LevelComponent.coffee b/server/levels/components/LevelComponent.coffee index 3dc373be1..515e7d80a 100644 --- a/server/levels/components/LevelComponent.coffee +++ b/server/levels/components/LevelComponent.coffee @@ -7,10 +7,11 @@ LevelComponentSchema = new mongoose.Schema { system: String }, {strict: false} -LevelComponentSchema.plugin(plugins.NamedPlugin) -LevelComponentSchema.plugin(plugins.PermissionsPlugin) -LevelComponentSchema.plugin(plugins.VersionedPlugin) -LevelComponentSchema.plugin(plugins.SearchablePlugin, {searchable: ['name', 'description', 'system']}) +LevelComponentSchema.plugin plugins.NamedPlugin +LevelComponentSchema.plugin plugins.PermissionsPlugin +LevelComponentSchema.plugin plugins.VersionedPlugin +LevelComponentSchema.plugin plugins.SearchablePlugin, {searchable: ['name', 'description', 'system']} +LevelComponentSchema.plugin plugins.PatchablePlugin LevelComponentSchema.pre 'init', (next) -> return next() unless jsonschema.properties? diff --git a/server/levels/components/level_component_schema.coffee b/server/levels/components/level_component_schema.coffee index ac399da2c..45135a774 100644 --- a/server/levels/components/level_component_schema.coffee +++ b/server/levels/components/level_component_schema.coffee @@ -115,5 +115,6 @@ c.extendBasicProperties LevelComponentSchema, 'level.component' c.extendSearchableProperties LevelComponentSchema c.extendVersionedProperties LevelComponentSchema, 'level.component' c.extendPermissionsProperties LevelComponentSchema, 'level.component' +c.extendPatchableProperties LevelComponentSchema module.exports = LevelComponentSchema diff --git a/server/levels/level_handler.coffee b/server/levels/level_handler.coffee index ccd76700a..af9392bf2 100644 --- a/server/levels/level_handler.coffee +++ b/server/levels/level_handler.coffee @@ -38,8 +38,7 @@ LevelHandler = class LevelHandler extends Handler return @getLeaderboardFacebookFriends(req, res, args[0]) if args[1] is 'leaderboard_facebook_friends' return @getLeaderboardGPlusFriends(req, res, args[0]) if args[1] is 'leaderboard_gplus_friends' return @getHistogramData(req, res, args[0]) if args[1] is 'histogram_data' - - return @sendNotFoundError(res) + super(arguments...) fetchLevelByIDAndHandleErrors: (id, req, res, callback) -> @getDocumentForIdOrSlug id, (err, level) => diff --git a/server/levels/level_schema.coffee b/server/levels/level_schema.coffee index 8d2d60cd3..90e199f23 100644 --- a/server/levels/level_schema.coffee +++ b/server/levels/level_schema.coffee @@ -243,6 +243,7 @@ c.extendBasicProperties LevelSchema, 'level' c.extendSearchableProperties LevelSchema c.extendVersionedProperties LevelSchema, 'level' c.extendPermissionsProperties LevelSchema, 'level' +c.extendPatchableProperties LevelSchema module.exports = LevelSchema diff --git a/server/levels/sessions/level_session_handler.coffee b/server/levels/sessions/level_session_handler.coffee index 9017f99c4..eaa5e4ed6 100644 --- a/server/levels/sessions/level_session_handler.coffee +++ b/server/levels/sessions/level_session_handler.coffee @@ -13,7 +13,7 @@ class LevelSessionHandler extends Handler getByRelationship: (req, res, args...) -> return @getActiveSessions req, res if args.length is 2 and args[1] is 'active' - return @sendNotFoundError(res) + super(arguments...) getActiveSessions: (req, res) -> return @sendUnauthorizedError(res) unless req.user.isAdmin() diff --git a/server/levels/systems/LevelSystem.coffee b/server/levels/systems/LevelSystem.coffee index cf21f7355..a02a3aab0 100644 --- a/server/levels/systems/LevelSystem.coffee +++ b/server/levels/systems/LevelSystem.coffee @@ -10,6 +10,7 @@ LevelSystemSchema.plugin(plugins.NamedPlugin) LevelSystemSchema.plugin(plugins.PermissionsPlugin) LevelSystemSchema.plugin(plugins.VersionedPlugin) LevelSystemSchema.plugin(plugins.SearchablePlugin, {searchable: ['name', 'description']}) +LevelSystemSchema.plugin(plugins.PatchablePlugin) LevelSystemSchema.pre 'init', (next) -> return next() unless jsonschema.properties? diff --git a/server/levels/systems/level_system_schema.coffee b/server/levels/systems/level_system_schema.coffee index cc4bc7891..7adcb969e 100644 --- a/server/levels/systems/level_system_schema.coffee +++ b/server/levels/systems/level_system_schema.coffee @@ -101,6 +101,7 @@ _.extend LevelSystemSchema.properties, c.extendBasicProperties LevelSystemSchema, 'level.system' c.extendSearchableProperties LevelSystemSchema c.extendVersionedProperties LevelSystemSchema, 'level.system' -c.extendPermissionsProperties LevelSystemSchema, 'level.system' +c.extendPermissionsProperties LevelSystemSchema +c.extendPatchableProperties LevelSystemSchema module.exports = LevelSystemSchema diff --git a/server/levels/thangs/ThangType.coffee b/server/levels/thangs/ThangType.coffee index 92915e8d0..292597719 100644 --- a/server/levels/thangs/ThangType.coffee +++ b/server/levels/thangs/ThangType.coffee @@ -5,8 +5,9 @@ ThangTypeSchema = new mongoose.Schema({ body: String, }, {strict: false}) -ThangTypeSchema.plugin(plugins.NamedPlugin) -ThangTypeSchema.plugin(plugins.VersionedPlugin) -ThangTypeSchema.plugin(plugins.SearchablePlugin, {searchable: ['name']}) +ThangTypeSchema.plugin plugins.NamedPlugin +ThangTypeSchema.plugin plugins.VersionedPlugin +ThangTypeSchema.plugin plugins.SearchablePlugin, {searchable: ['name']} +ThangTypeSchema.plugin plugins.PatchablePlugin module.exports = mongoose.model('thang.type', ThangTypeSchema) diff --git a/server/levels/thangs/thang_type_schema.coffee b/server/levels/thangs/thang_type_schema.coffee index 8b70ccbaf..68eb6d084 100644 --- a/server/levels/thangs/thang_type_schema.coffee +++ b/server/levels/thangs/thang_type_schema.coffee @@ -146,8 +146,9 @@ ThangTypeSchema.definitions = action: ActionSchema sound: SoundSchema -c.extendBasicProperties(ThangTypeSchema, 'thang.type') -c.extendSearchableProperties(ThangTypeSchema) -c.extendVersionedProperties(ThangTypeSchema, 'thang.type') +c.extendBasicProperties ThangTypeSchema, 'thang.type' +c.extendSearchableProperties ThangTypeSchema +c.extendVersionedProperties ThangTypeSchema, 'thang.type' +c.extendPatchableProperties ThangTypeSchema module.exports = ThangTypeSchema diff --git a/server/patches/Patch.coffee b/server/patches/Patch.coffee new file mode 100644 index 000000000..a6c5da41f --- /dev/null +++ b/server/patches/Patch.coffee @@ -0,0 +1,47 @@ +mongoose = require('mongoose') +{handlers} = require '../commons/mapping' + +PatchSchema = new mongoose.Schema({}, {strict: false}) + +PatchSchema.pre 'save', (next) -> + return next() unless @isNew # patch can't be altered after creation, so only need to check data once + target = @get('target') + targetID = target.id + Handler = require '../commons/Handler' + if not Handler.isID(targetID) + err = new Error('Invalid input.') + err.response = {message:"isn't a MongoDB id.", property:'target.id'} + err.code = 422 + return next(err) + + collection = target.collection + handler = require('../' + handlers[collection]) + handler.getDocumentForIdOrSlug targetID, (err, document) => + if err + err = new Error('Server error.') + err.response = {message:'', property:'target.id'} + err.code = 500 + return next(err) + + if not document + err = new Error('Target of patch not found.') + err.response = {message:'was not found.', property:'target.id'} + err.code = 404 + return next(err) + + target.id = document.get('_id') + if handler.modelClass.schema.uses_coco_versions + target.original = document.get('original') + version = document.get('version') + target.version = _.pick document.get('version'), 'major', 'minor' + @set('target', target) + else + target.original = targetID + + patches = document.get('patches') or [] + patches.push @_id + console.log 'PATCH PUSHED', @_id + document.set 'patches', patches + document.save (err) -> next(err) + +module.exports = mongoose.model('patch', PatchSchema) diff --git a/server/patches/patch_handler.coffee b/server/patches/patch_handler.coffee new file mode 100644 index 000000000..a9a26e05b --- /dev/null +++ b/server/patches/patch_handler.coffee @@ -0,0 +1,55 @@ +Patch = require('./Patch') +Handler = require('../commons/Handler') +schema = require './patch_schema' +{handlers} = require '../commons/mapping' +mongoose = require('mongoose') + +PatchHandler = class PatchHandler extends Handler + modelClass: Patch + editableProperties: [] + postEditableProperties: ['delta', 'target'] + jsonSchema: require './patch_schema' + + makeNewInstance: (req) -> + patch = super(req) + patch.set 'creator', req.user._id + patch.set 'created', new Date().toISOString() + patch.set 'status', 'pending' + patch + + getByRelationship: (req, res, args...) -> + return @setStatus(req, res, args[0]) if req.route.method is 'put' and args[1] is 'status' + super(arguments...) + + setStatus: (req, res, id) -> + newStatus = req.body.status + unless newStatus in ['rejected', 'accepted', 'withdrawn'] + return @sendBadInputError(res, "Status must be 'rejected', 'accepted', or 'withdrawn'") + + @getDocumentForIdOrSlug id, (err, patch) => + return @sendDatabaseError(res, err) if err + return @sendNotFoundError(res) unless patch? + targetInfo = patch.get('target') + targetHandler = require('../' + handlers[targetInfo.collection]) + targetModel = targetHandler.modelClass + + query = { 'original': targetInfo.original } + sort = { 'version.major': -1, 'version.minor': -1 } + targetModel.findOne(query).sort(sort).exec (err, target) => + return @sendDatabaseError(res, err) if err + return @sendNotFoundError(res) unless target? + return @sendUnauthorizedError(res) unless targetHandler.hasAccessToDocument(req, target, 'get') + + if newStatus in ['rejected', 'accepted'] + return @sendUnauthorizedError(res) unless targetHandler.hasAccessToDocument(req, target, 'put') + + if newStatus is 'withdrawn' + return @sendUnauthorizedError(res) unless req.user.get('_id').equals patch.get('creator') + + # these require callbacks + patch.update {$set:{status:newStatus}}, {}, -> + target.update {$pull:{patches:patch.get('_id')}}, {}, -> + @sendSuccess(res, null) + + +module.exports = new PatchHandler() diff --git a/server/patches/patch_schema.coffee b/server/patches/patch_schema.coffee new file mode 100644 index 000000000..7e02f4b8a --- /dev/null +++ b/server/patches/patch_schema.coffee @@ -0,0 +1,28 @@ +c = require '../commons/schemas' + +patchables = ['level', 'thang_type', 'level_system', 'level_component', 'article'] + +PatchSchema = c.object({title:'Patch', required:['target', 'delta']}, { + delta: { title: 'Delta', type:['array', 'object'] } + title: c.shortString() + description: c.shortString({maxLength: 500}) + creator: c.objectId(links: [{rel: 'extra', href: "/db/user/{($)}"}]) + created: c.date( { title: 'Created', readOnly: true }) + status: { enum: ['pending', 'accepted', 'rejected', 'withdrawn']} + + target: c.object({title: 'Target', required:['collection', 'id']}, { + collection: { enum: patchables } + id: c.objectId(title: 'Target ID') # search by this if not versioned + + # if target is versioned, want to know that info too + original: c.objectId(title: 'Target Original') # search by this if versioned + version: + properties: + major: { type: 'number', minimum: 0 } + minor: { type: 'number', minimum: 0 } + }) +}) + +c.extendBasicProperties(PatchSchema, 'patch') + +module.exports = PatchSchema diff --git a/server/plugins/plugins.coffee b/server/plugins/plugins.coffee index f1f224b82..a6dba1238 100644 --- a/server/plugins/plugins.coffee +++ b/server/plugins/plugins.coffee @@ -2,6 +2,10 @@ mongoose = require('mongoose') User = require('../users/User') textSearch = require('mongoose-text-search') +module.exports.PatchablePlugin = (schema) -> + schema.is_patchable = true + schema.index({'target.original':1, 'status':'1', 'created':-1}) + module.exports.NamedPlugin = (schema) -> schema.add({name: String, slug: String}) schema.index({'slug': 1}, {unique: true, sparse: true, name: 'slug index'}) diff --git a/server/users/user_handler.coffee b/server/users/user_handler.coffee index 168f10d91..b43f3ed6d 100644 --- a/server/users/user_handler.coffee +++ b/server/users/user_handler.coffee @@ -171,7 +171,7 @@ UserHandler = class UserHandler extends Handler return @getNamesByIds(req, res) if args[1] is 'names' return @nameToID(req, res, args[0]) if args[1] is 'nameToID' return @getLevelSessions(req, res, args[0]) if args[1] is 'level.sessions' - return @sendNotFoundError(res) + super(arguments...) agreeToCLA: (req, res) -> return @sendUnauthorizedError(res) unless req.user diff --git a/test/server/common.coffee b/test/server/common.coffee index 1af742db6..e68c27b72 100644 --- a/test/server/common.coffee +++ b/test/server/common.coffee @@ -10,6 +10,7 @@ _.mixin(_.str.exports()) GLOBAL.mongoose = require 'mongoose' mongoose.connect('mongodb://localhost/coco_unittest') path = require('path') +GLOBAL.testing = true models_path = [ '../../server/articles/Article' @@ -19,6 +20,7 @@ models_path = [ '../../server/levels/sessions/LevelSession' '../../server/levels/thangs/LevelThangType' '../../server/users/User' + '../../server/patches/Patch' ] for m in models_path diff --git a/test/server/functional/patch.spec.coffee b/test/server/functional/patch.spec.coffee new file mode 100644 index 000000000..b9875a814 --- /dev/null +++ b/test/server/functional/patch.spec.coffee @@ -0,0 +1,117 @@ +require '../common' + +describe '/db/patch', -> + request = require 'request' + it 'clears the db first', (done) -> + clearModels [User, Article, Patch], (err) -> + throw err if err + done() + + article = {name: 'Yo', body:'yo ma'} + articleURL = getURL('/db/article') + articles = {} + + patchURL = getURL('/db/patch') + patches = {} + patch = + delta: {name:['test']} + target: + id:null + collection: 'article' + + it 'creates an Article to patch', (done) -> + loginAdmin -> + request.post {uri:articleURL, json:patch}, (err, res, body) -> + articles[0] = body + patch.target.id = articles[0]._id + done() + + it "allows someone to submit a patch to something they don't control", (done) -> + loginJoe (joe) -> + request.post {uri: patchURL, json: patch}, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.target.original).toBeDefined() + expect(body.target.version.major).toBeDefined() + expect(body.target.version.minor).toBeDefined() + expect(body.status).toBe('pending') + expect(body.created).toBeDefined() + expect(body.creator).toBe(joe.id) + patches[0] = body + done() + + it 'adds a patch to the target document', (done) -> + Article.findOne({}).exec (err, article) -> + expect(article.toObject().patches[0]).toBeDefined() + done() + + it 'shows up in patch requests', (done) -> + patchesURL = getURL("/db/article/#{articles[0]._id}/patches") + request.get {uri: patchesURL}, (err, res, body) -> + body = JSON.parse(body) + expect(res.statusCode).toBe(200) + expect(body.length).toBe(1) + done() + + it 'allows you to set yourself as listening', (done) -> + listeningURL = getURL("/db/article/#{articles[0]._id}/listen") + request.put {uri: listeningURL, json: {on:true}}, (err, res, body) -> + expect(body.listeners[0]).toBeDefined() + done() + + it 'added the listener to the target document', (done) -> + Article.findOne({}).exec (err, article) -> + expect(article.toObject().listeners[0]).toBeDefined() + done() + + it 'does not add duplicate listeners', (done) -> + listeningURL = getURL("/db/article/#{articles[0]._id}/listen") + request.put {uri: listeningURL, json: {on:true}}, (err, res, body) -> + expect(body.listeners.length).toBe(1) + done() + + it 'allows removing yourself', (done) -> + listeningURL = getURL("/db/article/#{articles[0]._id}/listen") + request.put {uri: listeningURL, json: {on:false}}, (err, res, body) -> + expect(body.listeners.length).toBe(0) + done() + + it 'allows the submitter to withdraw the pull request', (done) -> + statusURL = getURL("/db/patch/#{patches[0]._id}/status") + request.put {uri: statusURL, json: {status:'withdrawn'}}, (err, res, body) -> + expect(res.statusCode).toBe(200) + Patch.findOne({}).exec (err, article) -> + expect(article.get('status')).toBe 'withdrawn' + Article.findOne({}).exec (err, article) -> + expect(article.toObject().patches.length).toBe(0) + done() + + it 'does not allow the submitter to reject or accept the pull request', (done) -> + statusURL = getURL("/db/patch/#{patches[0]._id}/status") + request.put {uri: statusURL, json: {status:'rejected'}}, (err, res, body) -> + expect(res.statusCode).toBe(403) + request.put {uri: statusURL, json: {status:'accepted'}}, (err, res, body) -> + expect(res.statusCode).toBe(403) + Patch.findOne({}).exec (err, article) -> + expect(article.get('status')).toBe 'withdrawn' + done() + + it 'allows the recipient to accept or reject the pull request', (done) -> + statusURL = getURL("/db/patch/#{patches[0]._id}/status") + loginAdmin -> + request.put {uri: statusURL, json: {status:'rejected'}}, (err, res, body) -> + expect(res.statusCode).toBe(200) + Patch.findOne({}).exec (err, article) -> + expect(article.get('status')).toBe 'rejected' + request.put {uri: statusURL, json: {status:'accepted'}}, (err, res, body) -> + expect(res.statusCode).toBe(200) + Patch.findOne({}).exec (err, article) -> + expect(article.get('status')).toBe 'accepted' + done() + + it 'does not allow the recipient to withdraw the pull request', (done) -> + statusURL = getURL("/db/patch/#{patches[0]._id}/status") + request.put {uri: statusURL, json: {status:'withdrawn'}}, (err, res, body) -> + expect(res.statusCode).toBe(403) + Patch.findOne({}).exec (err, article) -> + expect(article.get('status')).toBe 'accepted' + done() \ No newline at end of file From 315ef7f7b8b7a45dabf6e0a8f23a827e54357b09 Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Tue, 8 Apr 2014 19:26:34 -0700 Subject: [PATCH 04/46] Added jsondiffpatch to the client. Bower update everyone! --- bower.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bower.json b/bower.json index e73834bc5..6fd3878b7 100644 --- a/bower.json +++ b/bower.json @@ -36,7 +36,8 @@ "underscore.string": "~2.3.3", "firebase": "~1.0.2", "catiline": "~2.9.3", - "d3": "~3.4.4" + "d3": "~3.4.4", + "jsondiffpatch": "~0.1.5" }, "overrides": { "backbone": { From f2d21b960f42db915462f56ee8eede1da41da0cf Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Wed, 9 Apr 2014 16:09:35 -0700 Subject: [PATCH 05/46] Added a delta subview for displaying deltas of schema'd data. --- app/lib/deltas.coffee | 89 +++++++++++++++++++++++++++++++++ app/models/CocoModel.coffee | 7 ++- app/styles/editor/delta.sass | 35 +++++++++++++ app/templates/editor/delta.jade | 35 +++++++++++++ app/views/editor/delta.coffee | 43 ++++++++++++++++ bower.json | 3 ++ 6 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 app/lib/deltas.coffee create mode 100644 app/styles/editor/delta.sass create mode 100644 app/templates/editor/delta.jade create mode 100644 app/views/editor/delta.coffee diff --git a/app/lib/deltas.coffee b/app/lib/deltas.coffee new file mode 100644 index 000000000..abf3dddf5 --- /dev/null +++ b/app/lib/deltas.coffee @@ -0,0 +1,89 @@ +# path: an array of indexes to navigate into a JSON object +# left: + +module.exports.interpretDelta = (delta, path, left, schema) -> + # takes a single delta and converts into an object that can be + # easily formatted into something human readable. + + betterDelta = { action:'???', delta: delta } + + betterPath = [] + parentLeft = left + parentSchema = schema + for key in path + # TODO: A smarter way of getting child schemas + childSchema = parentSchema?.items or parentSchema?.properties?[key] or {} + childLeft = parentLeft?[key] + betterKey = null + betterKey ?= childLeft.name or childLeft.id if childLeft + betterKey ?= "#{childSchema.title} ##{key+1}" if childSchema.title and _.isNumber(key) + betterKey ?= "#{childSchema.title}" if childSchema.title + betterKey ?= _.string.titleize key + betterPath.push betterKey + parentLeft = childLeft + parentSchema = childSchema + + betterDelta.path = betterPath.join(' :: ') + betterDelta.schema = childSchema + betterDelta.left = childLeft + betterDelta.right = jsondiffpatch.patch childLeft, delta + + if _.isArray(delta) and delta.length is 1 + betterDelta.action = 'added' + betterDelta.newValue = delta[0] + + if _.isArray(delta) and delta.length is 2 + betterDelta.action = 'modified' + betterDelta.oldValue = delta[0] + betterDelta.newValue = delta[1] + + if _.isArray(delta) and delta.length is 3 and delta[1] is 0 and delta[2] is 0 + betterDelta.action = 'deleted' + betterDelta.oldValue = delta[0] + + if _.isPlainObject(delta) and delta._t is 'a' + betterDelta.action = 'modified-array' + + if _.isPlainObject(delta) and delta._t isnt 'a' + betterDelta.action = 'modified-object' + + if _.isArray(delta) and delta.length is 3 and delta[1] is 0 and delta[2] is 3 + betterDelta.action = 'moved-index' + betterDelta.destinationIndex = delta[1] + + if _.isArray(delta) and delta.length is 3 and delta[1] is 0 and delta[2] is 2 + betterDelta.action = 'text-diff' + betterDelta.unidiff = delta[0] + left = betterDelta.left.trim().split('\n') + right = betterDelta.right.trim().split('\n') + shifted = popped = false + while left.length > 5 and right.length > 5 and left[0] is right[0] and left[1] is right[1] + left.shift() + right.shift() + shifted = true + while left.length > 5 and right.length > 5 and left[left.length-1] is right[right.length-1] and left[left.length-2] is right[right.length-2] + left.pop() + right.pop() + popped = true + left.push('...') and right.push('...') if popped + left.unshift('...') and right.unshift('...') if shifted + betterDelta.trimmedLeft = left.join('\n') + betterDelta.trimmedRight = right.join('\n') + + + betterDelta + +module.exports.flattenDelta = flattenDelta = (delta, path=null) -> + # takes a single delta and returns an array of deltas + + path ?= [] + + return [{path:path, delta:delta}] if _.isArray delta + + results = [] + affectingArray = delta._t is 'a' + for index, childDelta of delta + continue if index is '_t' + index = parseInt(index.replace('_', '')) if affectingArray + results = results.concat flattenDelta(childDelta, path.concat([index])) + results \ No newline at end of file diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee index 862ba72fd..6edd0d634 100644 --- a/app/models/CocoModel.coffee +++ b/app/models/CocoModel.coffee @@ -215,6 +215,11 @@ class CocoModel extends Backbone.Model return true if permission.access in ['owner', 'write'] return false - + + getDelta: -> + jsd = jsondiffpatch.create({ + objectHash: (obj) -> obj.name || obj.id || obj._id || JSON.stringify(_.keys(obj)) + }) + jsd.diff @_revertAttributes, @attributes module.exports = CocoModel diff --git a/app/styles/editor/delta.sass b/app/styles/editor/delta.sass new file mode 100644 index 000000000..b7c4cdd56 --- /dev/null +++ b/app/styles/editor/delta.sass @@ -0,0 +1,35 @@ +#delta-list-view + width: 600px + .panel-heading + font-size: 13px + padding: 4px + .row + padding: 5px 10px + + .delta-added + border-color: green + strong + color: green + .panel-heading + background-color: lighten(green, 70%) + + .delta-modified + border-color: darkgoldenrod + strong + color: darkgoldenrod + .panel-heading + background-color: lighten(darkgoldenrod, 40%) + + .delta-text-diff + border-color: blue + strong + color: blue + .panel-heading + background-color: lighten(blue, 45%) + + .delta-deleted + border-color: red + strong + color: red + .panel-heading + background-color: lighten(red, 42%) diff --git a/app/templates/editor/delta.jade b/app/templates/editor/delta.jade new file mode 100644 index 000000000..33b85ba15 --- /dev/null +++ b/app/templates/editor/delta.jade @@ -0,0 +1,35 @@ +- var i = 0 +.panel-group#accordion + for delta in deltas + .delta.panel.panel-default(class='delta-'+delta.action) + .panel-heading + if delta.action === 'added' + strong(data-i18n="delta.added") Added + if delta.action === 'modified' + strong(data-i18n="delta.modified") Modified + if delta.action === 'deleted' + strong(data-i18n="delta.deleted") Deleted + if delta.action === 'moved-index' + strong(data-i18n="delta.modified_array") Moved Index + if delta.action === 'text-diff' + strong(data-i18n="delta.text_diff") Text Diff + span + a(data-toggle="collapse" data-parent="#accordion" href="#collapse-"+i) + span= delta.path + + .panel-collapse.collapse(id="collapse-"+i) + .panel-body.row + if delta.action === 'added' + .new-value.col-md-12= delta.right + if delta.action === 'modified' + .old-value.col-md-6= delta.left + .new-value.col-md-6= delta.right + if delta.action === 'deleted' + .col-md-12 + div.old-value= delta.left + if delta.action === 'text-diff' + .col-md-6 + pre= delta.trimmedLeft + .col-md-6 + pre= delta.trimmedRight + - i += 1 \ No newline at end of file diff --git a/app/views/editor/delta.coffee b/app/views/editor/delta.coffee new file mode 100644 index 000000000..46c637f36 --- /dev/null +++ b/app/views/editor/delta.coffee @@ -0,0 +1,43 @@ +CocoView = require 'views/kinds/CocoView' +template = require 'templates/editor/delta' +deltaLib = require 'lib/deltas' + +module.exports = class DeltaListView extends CocoView + id: "delta-list-view" + template: template + + constructor: (options) -> + super(options) + @delta = options.delta + @schema = options.schema or {} + @left = options.left + + getRenderData: -> + c = super() + deltas = deltaLib.flattenDelta @delta + deltas = (deltaLib.interpretDelta(d.delta, d.path, @left, @schema) for d in deltas) + c.deltas = deltas + @processedDeltas = deltas + c + + afterRender: -> + deltas = @$el.find('.delta') + for delta, i in deltas + deltaEl = $(delta) + deltaData = @processedDeltas[i] + console.log 'delta', deltaEl, deltaData + if _.isObject(deltaData.left) and leftEl = deltaEl.find('.old-value') + options = + data: deltaData.left + schema: deltaData.schema + readOnly: true + treema = TreemaNode.make(leftEl, options) + treema.build() + + if _.isObject(deltaData.right) and rightEl = deltaEl.find('.old-value') + options = + data: deltaData.right + schema: deltaData.schema + readOnly: true + treema = TreemaNode.make(rightEl, options) + treema.build() diff --git a/bower.json b/bower.json index 0a6b42f4a..84e10d876 100644 --- a/bower.json +++ b/bower.json @@ -52,6 +52,9 @@ }, "underscore.string": { "main": "lib/underscore.string.js" + }, + "jsondiffpatch": { + "main": ["build/bundle-full.js", "build/formatters.js", "src/formatters/html.css"] } } } From b15380047c49044368a49ca3a311bd4fe31ad3a0 Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Wed, 9 Apr 2014 16:09:53 -0700 Subject: [PATCH 06/46] Tweak to view. --- app/views/admin/base_view.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/admin/base_view.coffee b/app/views/admin/base_view.coffee index 7fe8c09c1..5db653086 100644 --- a/app/views/admin/base_view.coffee +++ b/app/views/admin/base_view.coffee @@ -1,6 +1,6 @@ -View = require 'views/kinds/RootView' +RootView = require 'views/kinds/RootView' template = require 'templates/base' -module.exports = class BaseView extends View +module.exports = class BaseView extends RootView id: "base-view" template: template From 66d455285e389e2b715417e95b6802e2b22598da Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Wed, 9 Apr 2014 19:07:44 -0700 Subject: [PATCH 07/46] Integrated the delta view into the save view for levels. --- app/lib/deltas.coffee | 60 ++++++++++--------------- app/models/CocoModel.coffee | 6 +++ app/styles/editor/delta.sass | 3 +- app/styles/modal/save_version.sass | 23 ++++++++++ app/templates/editor/delta.jade | 10 ++--- app/templates/editor/level/save.jade | 55 ++++++++++++----------- app/views/editor/delta.coffee | 16 +++---- app/views/editor/level/save_view.coffee | 13 ++++++ app/views/kinds/CocoView.coffee | 5 ++- 9 files changed, 109 insertions(+), 82 deletions(-) diff --git a/app/lib/deltas.coffee b/app/lib/deltas.coffee index abf3dddf5..e6d8c936d 100644 --- a/app/lib/deltas.coffee +++ b/app/lib/deltas.coffee @@ -7,27 +7,6 @@ module.exports.interpretDelta = (delta, path, left, schema) -> betterDelta = { action:'???', delta: delta } - betterPath = [] - parentLeft = left - parentSchema = schema - for key in path - # TODO: A smarter way of getting child schemas - childSchema = parentSchema?.items or parentSchema?.properties?[key] or {} - childLeft = parentLeft?[key] - betterKey = null - betterKey ?= childLeft.name or childLeft.id if childLeft - betterKey ?= "#{childSchema.title} ##{key+1}" if childSchema.title and _.isNumber(key) - betterKey ?= "#{childSchema.title}" if childSchema.title - betterKey ?= _.string.titleize key - betterPath.push betterKey - parentLeft = childLeft - parentSchema = childSchema - - betterDelta.path = betterPath.join(' :: ') - betterDelta.schema = childSchema - betterDelta.left = childLeft - betterDelta.right = jsondiffpatch.patch childLeft, delta - if _.isArray(delta) and delta.length is 1 betterDelta.action = 'added' betterDelta.newValue = delta[0] @@ -54,27 +33,34 @@ module.exports.interpretDelta = (delta, path, left, schema) -> if _.isArray(delta) and delta.length is 3 and delta[1] is 0 and delta[2] is 2 betterDelta.action = 'text-diff' betterDelta.unidiff = delta[0] - left = betterDelta.left.trim().split('\n') - right = betterDelta.right.trim().split('\n') - shifted = popped = false - while left.length > 5 and right.length > 5 and left[0] is right[0] and left[1] is right[1] - left.shift() - right.shift() - shifted = true - while left.length > 5 and right.length > 5 and left[left.length-1] is right[right.length-1] and left[left.length-2] is right[right.length-2] - left.pop() - right.pop() - popped = true - left.push('...') and right.push('...') if popped - left.unshift('...') and right.unshift('...') if shifted - betterDelta.trimmedLeft = left.join('\n') - betterDelta.trimmedRight = right.join('\n') - + betterPath = [] + parentLeft = left + parentSchema = schema + for key, i in path + # TODO: A smarter way of getting child schemas + childSchema = parentSchema?.items or parentSchema?.properties?[key] or {} + childLeft = parentLeft?[key] + betterKey = null + childData = if i is path.length-1 and betterDelta.action is 'added' then delta[0] else childLeft + betterKey ?= childData.name or childData.id if childData + betterKey ?= "#{childSchema.title} ##{key+1}" if childSchema.title and _.isNumber(key) + betterKey ?= "#{childSchema.title}" if childSchema.title + betterKey ?= _.string.titleize key + betterPath.push betterKey + parentLeft = childLeft + parentSchema = childSchema + + betterDelta.path = betterPath.join(' :: ') + betterDelta.schema = childSchema + betterDelta.left = childLeft + betterDelta.right = jsondiffpatch.patch childLeft, delta + betterDelta module.exports.flattenDelta = flattenDelta = (delta, path=null) -> # takes a single delta and returns an array of deltas + return [] unless delta path ?= [] diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee index 6edd0d634..669899e6c 100644 --- a/app/models/CocoModel.coffee +++ b/app/models/CocoModel.coffee @@ -1,4 +1,5 @@ storage = require 'lib/storage' +deltasLib = require 'lib/deltas' class CocoSchema extends Backbone.Model constructor: (path, args...) -> @@ -221,5 +222,10 @@ class CocoModel extends Backbone.Model objectHash: (obj) -> obj.name || obj.id || obj._id || JSON.stringify(_.keys(obj)) }) jsd.diff @_revertAttributes, @attributes + + getExpandedDelta: -> + delta = @getDelta() + deltas = deltasLib.flattenDelta(delta) + (deltasLib.interpretDelta(d.delta, d.path, @_revertAttributes, @schema().attributes) for d in deltas) module.exports = CocoModel diff --git a/app/styles/editor/delta.sass b/app/styles/editor/delta.sass index b7c4cdd56..b754dea9a 100644 --- a/app/styles/editor/delta.sass +++ b/app/styles/editor/delta.sass @@ -1,5 +1,4 @@ -#delta-list-view - width: 600px +.delta-list-view .panel-heading font-size: 13px padding: 4px diff --git a/app/styles/modal/save_version.sass b/app/styles/modal/save_version.sass index 9af4225dc..b64014ace 100644 --- a/app/styles/modal/save_version.sass +++ b/app/styles/modal/save_version.sass @@ -1,4 +1,7 @@ #save-version-modal + .modal-body + padding: 0px 50px 30px 20px + #cla-link cursor: pointer text-decoration: underline @@ -25,3 +28,23 @@ font-size: 0.9em font-style: italic + .delta-list-view + overflow-y: auto + padding: 10px + border: 1px solid black + background: lighten(#add8e6, 17%) + margin-bottom: 10px + ul + padding-left: 20px + + form + width: 100% + + .commit-message + display: block + width: 100% + + .checkbox + margin: 10px 10px 0 + input + margin-right: 5px \ No newline at end of file diff --git a/app/templates/editor/delta.jade b/app/templates/editor/delta.jade index 33b85ba15..765839537 100644 --- a/app/templates/editor/delta.jade +++ b/app/templates/editor/delta.jade @@ -1,5 +1,5 @@ - var i = 0 -.panel-group#accordion +.panel-group(id='delta-accordion-'+(counter)) for delta in deltas .delta.panel.panel-default(class='delta-'+delta.action) .panel-heading @@ -14,10 +14,10 @@ if delta.action === 'text-diff' strong(data-i18n="delta.text_diff") Text Diff span - a(data-toggle="collapse" data-parent="#accordion" href="#collapse-"+i) + a(data-toggle="collapse" data-parent="#delta-accordion"+(counter) href="#collapse-"+(i+counter)) span= delta.path - .panel-collapse.collapse(id="collapse-"+i) + .panel-collapse.collapse(id="collapse-"+(i+counter)) .panel-body.row if delta.action === 'added' .new-value.col-md-12= delta.right @@ -29,7 +29,7 @@ div.old-value= delta.left if delta.action === 'text-diff' .col-md-6 - pre= delta.trimmedLeft + pre= delta.left .col-md-6 - pre= delta.trimmedRight + pre= delta.right - i += 1 \ No newline at end of file diff --git a/app/templates/editor/level/save.jade b/app/templates/editor/level/save.jade index 8ada52b23..498d32f13 100644 --- a/app/templates/editor/level/save.jade +++ b/app/templates/editor/level/save.jade @@ -3,19 +3,20 @@ extends /templates/modal/save_version block modal-body-content h3= "Level: " + level.get('name') + " - " + (levelNeedsSave ? "Modified" : "Not Modified") if levelNeedsSave - form#save-level-form.form - .form-group - label.control-label(for="level-commit-message") Commit Message - textarea.form-control#level-commit-message(name="commit-message", type="text") + .changes-stub + form#save-level-form.form-inline + .form-group.commit-message + input.form-control#level-commit-message(name="commit-message", type="text", placeholder="Commit Message") if level.isPublished() - .form-group.checkbox - label.control-label(for="level-version-is-major") Major Changes? - input#level-version-is-major(name="version-is-major", type="checkbox") - span.help-block (Could this update break old solutions of the level?) + .checkbox + label + input#level-version-is-major(name="version-is-major", type="checkbox") + | Major Changes if !level.isPublished() - .form-group.checkbox - label.control-label(for="level-publish") Publish This Level (irreversible)? - input#level-publish(name="publish", type="checkbox") + .checkbox + label + input#level-publish(name="publish", type="checkbox") + | Publish if modifiedComponents.length hr @@ -23,17 +24,17 @@ block modal-body-content each component in modifiedComponents - var id = component.get('_id') h4= "Component: " + component.get('system') + '.' + component.get('name') - form.component-form(id="save-component-" + id + "-form") + .changes-stub + form.form-inline.component-form(id="save-component-" + id + "-form") input(name="component-original", type="hidden", value=component.get('original')) input(name="component-parent-major-version", type="hidden", value=component.get('version').major) - .form-group - label.control-label(for=id + "-commit-message") Commit Message - textarea.form-control(id=id + "-commit-message", name="commit-message", type="text") + .form-group.commit-message + input.form-control(id=id + "-commit-message", name="commit-message", type="text", placeholder="Commit Message") if component.isPublished() - .form-group.checkbox - label.control-label(for=id + "-version-is-major") Major Changes? - input(id=id + "-version-is-major", name="version-is-major", type="checkbox") - span.help-block (Could this update break anything depending on this Component?) + .checkbox + label + input(id=id + "-version-is-major", name="version-is-major", type="checkbox") + | Major Changes if modifiedSystems.length hr @@ -41,14 +42,14 @@ block modal-body-content each system in modifiedSystems - var id = system.get('_id') h4= "System: " + system.get('name') - form.system-form(id="save-system-" + id + "-form") + .changes-stub + form.form-inline.system-form(id="save-system-" + id + "-form") input(name="system-original", type="hidden", value=system.get('original')) input(name="system-parent-major-version", type="hidden", value=system.get('version').major) - .form-group - label.control-label(for=id + "-commit-message") Commit Message - textarea.form-control(id=id + "-commit-message", name="commit-message", type="text") + .form-group.commit-message + input.form-control(id=id + "-commit-message", name="commit-message", type="text", placeholder="Commit Message") if system.isPublished() - .form-group.checkbox - label.control-label(for=id + "-version-is-major") Major Changes? - input(id=id + "-version-is-major", name="version-is-major", type="checkbox") - span.help-block (Could this update break anything depending on this System?) + .checkbox + label + input(id=id + "-version-is-major", name="version-is-major", type="checkbox") + | Major changes diff --git a/app/views/editor/delta.coffee b/app/views/editor/delta.coffee index 46c637f36..0f72512e4 100644 --- a/app/views/editor/delta.coffee +++ b/app/views/editor/delta.coffee @@ -3,21 +3,19 @@ template = require 'templates/editor/delta' deltaLib = require 'lib/deltas' module.exports = class DeltaListView extends CocoView - id: "delta-list-view" + @deltaCounter: 0 + className: "delta-list-view" template: template constructor: (options) -> super(options) - @delta = options.delta - @schema = options.schema or {} - @left = options.left + @model = options.model getRenderData: -> c = super() - deltas = deltaLib.flattenDelta @delta - deltas = (deltaLib.interpretDelta(d.delta, d.path, @left, @schema) for d in deltas) - c.deltas = deltas - @processedDeltas = deltas + c.deltas = @processedDeltas = @model.getExpandedDelta() + c.counter = DeltaListView.deltaCounter + DeltaListView.deltaCounter += c.deltas.length c afterRender: -> @@ -34,7 +32,7 @@ module.exports = class DeltaListView extends CocoView treema = TreemaNode.make(leftEl, options) treema.build() - if _.isObject(deltaData.right) and rightEl = deltaEl.find('.old-value') + if _.isObject(deltaData.right) and rightEl = deltaEl.find('.new-value') options = data: deltaData.right schema: deltaData.schema diff --git a/app/views/editor/level/save_view.coffee b/app/views/editor/level/save_view.coffee index e3e5ad25c..c5d9e718d 100644 --- a/app/views/editor/level/save_view.coffee +++ b/app/views/editor/level/save_view.coffee @@ -3,6 +3,7 @@ template = require 'templates/editor/level/save' forms = require 'lib/forms' LevelComponent = require 'models/LevelComponent' LevelSystem = require 'models/LevelSystem' +DeltaView = require 'views/editor/delta' module.exports = class LevelSaveView extends SaveVersionModal template: template @@ -24,7 +25,19 @@ module.exports = class LevelSaveView extends SaveVersionModal context.modifiedComponents = _.filter @supermodel.getModels(LevelComponent), @shouldSaveEntity context.modifiedSystems = _.filter @supermodel.getModels(LevelSystem), @shouldSaveEntity context.noSaveButton = not (context.levelNeedsSave or context.modifiedComponents.length or context.modifiedSystems.length) + @lastContext = context context + + afterRender: -> + super() + changeEls = @$el.find('.changes-stub') + models = if @lastContext.levelNeedsSave then [@level] else [] + models = models.concat @lastContext.modifiedComponents + models = models.concat @lastContext.modifiedSystems + for changeEl, i in changeEls + model = models[i] + deltaView = new DeltaView({model:model}) + @insertSubView(deltaView, $(changeEl)) shouldSaveEntity: (m) -> return true if m.hasLocalChanges() diff --git a/app/views/kinds/CocoView.coffee b/app/views/kinds/CocoView.coffee index d8272d1b3..3ea8204a3 100644 --- a/app/views/kinds/CocoView.coffee +++ b/app/views/kinds/CocoView.coffee @@ -299,9 +299,10 @@ module.exports = class CocoView extends Backbone.View # Subviews - insertSubView: (view) -> + insertSubView: (view, elToReplace=null) -> @subviews[view.id].destroy() if view.id of @subviews - @$el.find('#'+view.id).after(view.el).remove() + elToReplace ?= @$el.find('#'+view.id) + elToReplace.after(view.el).remove() view.parent = @ view.render() view.afterInsert() From 5629284c76e25d4c13aa1bc046e5fa3788b5dfb5 Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Thu, 10 Apr 2014 10:24:14 -0700 Subject: [PATCH 08/46] Added text diff library to delta view. --- app/styles/editor/delta.sass | 2 + app/templates/editor/delta.jade | 6 +- app/views/editor/delta.coffee | 18 +- vendor/scripts/difflib.js | 413 ++++++++++++++++++++++++++++++++ vendor/scripts/diffview.js | 198 +++++++++++++++ vendor/styles/diffview.css | 83 +++++++ 6 files changed, 715 insertions(+), 5 deletions(-) create mode 100644 vendor/scripts/difflib.js create mode 100644 vendor/scripts/diffview.js create mode 100644 vendor/styles/diffview.css diff --git a/app/styles/editor/delta.sass b/app/styles/editor/delta.sass index b754dea9a..ec70d3135 100644 --- a/app/styles/editor/delta.sass +++ b/app/styles/editor/delta.sass @@ -25,6 +25,8 @@ color: blue .panel-heading background-color: lighten(blue, 45%) + table + width: 100% .delta-deleted border-color: red diff --git a/app/templates/editor/delta.jade b/app/templates/editor/delta.jade index 765839537..35c3c9a7e 100644 --- a/app/templates/editor/delta.jade +++ b/app/templates/editor/delta.jade @@ -28,8 +28,6 @@ .col-md-12 div.old-value= delta.left if delta.action === 'text-diff' - .col-md-6 - pre= delta.left - .col-md-6 - pre= delta.right + .col-md-12 + div.text-diff - i += 1 \ No newline at end of file diff --git a/app/views/editor/delta.coffee b/app/views/editor/delta.coffee index 0f72512e4..4d4635ebf 100644 --- a/app/views/editor/delta.coffee +++ b/app/views/editor/delta.coffee @@ -23,7 +23,6 @@ module.exports = class DeltaListView extends CocoView for delta, i in deltas deltaEl = $(delta) deltaData = @processedDeltas[i] - console.log 'delta', deltaEl, deltaData if _.isObject(deltaData.left) and leftEl = deltaEl.find('.old-value') options = data: deltaData.left @@ -39,3 +38,20 @@ module.exports = class DeltaListView extends CocoView readOnly: true treema = TreemaNode.make(rightEl, options) treema.build() + + if deltaData.action is 'text-diff' + left = difflib.stringAsLines deltaData.left + right = difflib.stringAsLines deltaData.right + sm = new difflib.SequenceMatcher(left, right) + opcodes = sm.get_opcodes() + el = deltaEl.find('.text-diff') + args = { + baseTextLines: left + newTextLines: right + opcodes: opcodes + baseTextName: "Old" + newTextName: "New" + contextSize: 5 + viewType: 1 + } + el.append(diffview.buildView(args)) diff --git a/vendor/scripts/difflib.js b/vendor/scripts/difflib.js new file mode 100644 index 000000000..191fe4563 --- /dev/null +++ b/vendor/scripts/difflib.js @@ -0,0 +1,413 @@ +/*** +This is part of jsdifflib v1.0. <http://snowtide.com/jsdifflib> + +Copyright (c) 2007, Snowtide Informatics Systems, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the Snowtide Informatics Systems nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. +***/ +/* Author: Chas Emerick <cemerick@snowtide.com> */ +var __whitespace = {" ":true, "\t":true, "\n":true, "\f":true, "\r":true}; + +var difflib = { + defaultJunkFunction: function (c) { + return __whitespace.hasOwnProperty(c); + }, + + stripLinebreaks: function (str) { return str.replace(/^[\n\r]*|[\n\r]*$/g, ""); }, + + stringAsLines: function (str) { + var lfpos = str.indexOf("\n"); + var crpos = str.indexOf("\r"); + var linebreak = ((lfpos > -1 && crpos > -1) || crpos < 0) ? "\n" : "\r"; + + var lines = str.split(linebreak); + for (var i = 0; i < lines.length; i++) { + lines[i] = difflib.stripLinebreaks(lines[i]); + } + + return lines; + }, + + // iteration-based reduce implementation + __reduce: function (func, list, initial) { + if (initial != null) { + var value = initial; + var idx = 0; + } else if (list) { + var value = list[0]; + var idx = 1; + } else { + return null; + } + + for (; idx < list.length; idx++) { + value = func(value, list[idx]); + } + + return value; + }, + + // comparison function for sorting lists of numeric tuples + __ntuplecomp: function (a, b) { + var mlen = Math.max(a.length, b.length); + for (var i = 0; i < mlen; i++) { + if (a[i] < b[i]) return -1; + if (a[i] > b[i]) return 1; + } + + return a.length == b.length ? 0 : (a.length < b.length ? -1 : 1); + }, + + __calculate_ratio: function (matches, length) { + return length ? 2.0 * matches / length : 1.0; + }, + + // returns a function that returns true if a key passed to the returned function + // is in the dict (js object) provided to this function; replaces being able to + // carry around dict.has_key in python... + __isindict: function (dict) { + return function (key) { return dict.hasOwnProperty(key); }; + }, + + // replacement for python's dict.get function -- need easy default values + __dictget: function (dict, key, defaultValue) { + return dict.hasOwnProperty(key) ? dict[key] : defaultValue; + }, + + SequenceMatcher: function (a, b, isjunk) { + this.set_seqs = function (a, b) { + this.set_seq1(a); + this.set_seq2(b); + } + + this.set_seq1 = function (a) { + if (a == this.a) return; + this.a = a; + this.matching_blocks = this.opcodes = null; + } + + this.set_seq2 = function (b) { + if (b == this.b) return; + this.b = b; + this.matching_blocks = this.opcodes = this.fullbcount = null; + this.__chain_b(); + } + + this.__chain_b = function () { + var b = this.b; + var n = b.length; + var b2j = this.b2j = {}; + var populardict = {}; + for (var i = 0; i < b.length; i++) { + var elt = b[i]; + if (b2j.hasOwnProperty(elt)) { + var indices = b2j[elt]; + if (n >= 200 && indices.length * 100 > n) { + populardict[elt] = 1; + delete b2j[elt]; + } else { + indices.push(i); + } + } else { + b2j[elt] = [i]; + } + } + + for (var elt in populardict) { + if (populardict.hasOwnProperty(elt)) { + delete b2j[elt]; + } + } + + var isjunk = this.isjunk; + var junkdict = {}; + if (isjunk) { + for (var elt in populardict) { + if (populardict.hasOwnProperty(elt) && isjunk(elt)) { + junkdict[elt] = 1; + delete populardict[elt]; + } + } + for (var elt in b2j) { + if (b2j.hasOwnProperty(elt) && isjunk(elt)) { + junkdict[elt] = 1; + delete b2j[elt]; + } + } + } + + this.isbjunk = difflib.__isindict(junkdict); + this.isbpopular = difflib.__isindict(populardict); + } + + this.find_longest_match = function (alo, ahi, blo, bhi) { + var a = this.a; + var b = this.b; + var b2j = this.b2j; + var isbjunk = this.isbjunk; + var besti = alo; + var bestj = blo; + var bestsize = 0; + var j = null; + var k; + + var j2len = {}; + var nothing = []; + for (var i = alo; i < ahi; i++) { + var newj2len = {}; + var jdict = difflib.__dictget(b2j, a[i], nothing); + for (var jkey in jdict) { + if (jdict.hasOwnProperty(jkey)) { + j = jdict[jkey]; + if (j < blo) continue; + if (j >= bhi) break; + newj2len[j] = k = difflib.__dictget(j2len, j - 1, 0) + 1; + if (k > bestsize) { + besti = i - k + 1; + bestj = j - k + 1; + bestsize = k; + } + } + } + j2len = newj2len; + } + + while (besti > alo && bestj > blo && !isbjunk(b[bestj - 1]) && a[besti - 1] == b[bestj - 1]) { + besti--; + bestj--; + bestsize++; + } + + while (besti + bestsize < ahi && bestj + bestsize < bhi && + !isbjunk(b[bestj + bestsize]) && + a[besti + bestsize] == b[bestj + bestsize]) { + bestsize++; + } + + while (besti > alo && bestj > blo && isbjunk(b[bestj - 1]) && a[besti - 1] == b[bestj - 1]) { + besti--; + bestj--; + bestsize++; + } + + while (besti + bestsize < ahi && bestj + bestsize < bhi && isbjunk(b[bestj + bestsize]) && + a[besti + bestsize] == b[bestj + bestsize]) { + bestsize++; + } + + return [besti, bestj, bestsize]; + } + + this.get_matching_blocks = function () { + if (this.matching_blocks != null) return this.matching_blocks; + var la = this.a.length; + var lb = this.b.length; + + var queue = [[0, la, 0, lb]]; + var matching_blocks = []; + var alo, ahi, blo, bhi, qi, i, j, k, x; + while (queue.length) { + qi = queue.pop(); + alo = qi[0]; + ahi = qi[1]; + blo = qi[2]; + bhi = qi[3]; + x = this.find_longest_match(alo, ahi, blo, bhi); + i = x[0]; + j = x[1]; + k = x[2]; + + if (k) { + matching_blocks.push(x); + if (alo < i && blo < j) + queue.push([alo, i, blo, j]); + if (i+k < ahi && j+k < bhi) + queue.push([i + k, ahi, j + k, bhi]); + } + } + + matching_blocks.sort(difflib.__ntuplecomp); + + var i1 = 0, j1 = 0, k1 = 0, block = 0; + var i2, j2, k2; + var non_adjacent = []; + for (var idx in matching_blocks) { + if (matching_blocks.hasOwnProperty(idx)) { + block = matching_blocks[idx]; + i2 = block[0]; + j2 = block[1]; + k2 = block[2]; + if (i1 + k1 == i2 && j1 + k1 == j2) { + k1 += k2; + } else { + if (k1) non_adjacent.push([i1, j1, k1]); + i1 = i2; + j1 = j2; + k1 = k2; + } + } + } + + if (k1) non_adjacent.push([i1, j1, k1]); + + non_adjacent.push([la, lb, 0]); + this.matching_blocks = non_adjacent; + return this.matching_blocks; + } + + this.get_opcodes = function () { + if (this.opcodes != null) return this.opcodes; + var i = 0; + var j = 0; + var answer = []; + this.opcodes = answer; + var block, ai, bj, size, tag; + var blocks = this.get_matching_blocks(); + for (var idx in blocks) { + if (blocks.hasOwnProperty(idx)) { + block = blocks[idx]; + ai = block[0]; + bj = block[1]; + size = block[2]; + tag = ''; + if (i < ai && j < bj) { + tag = 'replace'; + } else if (i < ai) { + tag = 'delete'; + } else if (j < bj) { + tag = 'insert'; + } + if (tag) answer.push([tag, i, ai, j, bj]); + i = ai + size; + j = bj + size; + + if (size) answer.push(['equal', ai, i, bj, j]); + } + } + + return answer; + } + + // this is a generator function in the python lib, which of course is not supported in javascript + // the reimplementation builds up the grouped opcodes into a list in their entirety and returns that. + this.get_grouped_opcodes = function (n) { + if (!n) n = 3; + var codes = this.get_opcodes(); + if (!codes) codes = [["equal", 0, 1, 0, 1]]; + var code, tag, i1, i2, j1, j2; + if (codes[0][0] == 'equal') { + code = codes[0]; + tag = code[0]; + i1 = code[1]; + i2 = code[2]; + j1 = code[3]; + j2 = code[4]; + codes[0] = [tag, Math.max(i1, i2 - n), i2, Math.max(j1, j2 - n), j2]; + } + if (codes[codes.length - 1][0] == 'equal') { + code = codes[codes.length - 1]; + tag = code[0]; + i1 = code[1]; + i2 = code[2]; + j1 = code[3]; + j2 = code[4]; + codes[codes.length - 1] = [tag, i1, Math.min(i2, i1 + n), j1, Math.min(j2, j1 + n)]; + } + + var nn = n + n; + var group = []; + var groups = []; + for (var idx in codes) { + if (codes.hasOwnProperty(idx)) { + code = codes[idx]; + tag = code[0]; + i1 = code[1]; + i2 = code[2]; + j1 = code[3]; + j2 = code[4]; + if (tag == 'equal' && i2 - i1 > nn) { + group.push([tag, i1, Math.min(i2, i1 + n), j1, Math.min(j2, j1 + n)]); + groups.push(group); + group = []; + i1 = Math.max(i1, i2-n); + j1 = Math.max(j1, j2-n); + } + + group.push([tag, i1, i2, j1, j2]); + } + } + + if (group && !(group.length == 1 && group[0][0] == 'equal')) groups.push(group) + + return groups; + } + + this.ratio = function () { + matches = difflib.__reduce( + function (sum, triple) { return sum + triple[triple.length - 1]; }, + this.get_matching_blocks(), 0); + return difflib.__calculate_ratio(matches, this.a.length + this.b.length); + } + + this.quick_ratio = function () { + var fullbcount, elt; + if (this.fullbcount == null) { + this.fullbcount = fullbcount = {}; + for (var i = 0; i < this.b.length; i++) { + elt = this.b[i]; + fullbcount[elt] = difflib.__dictget(fullbcount, elt, 0) + 1; + } + } + fullbcount = this.fullbcount; + + var avail = {}; + var availhas = difflib.__isindict(avail); + var matches = numb = 0; + for (var i = 0; i < this.a.length; i++) { + elt = this.a[i]; + if (availhas(elt)) { + numb = avail[elt]; + } else { + numb = difflib.__dictget(fullbcount, elt, 0); + } + avail[elt] = numb - 1; + if (numb > 0) matches++; + } + + return difflib.__calculate_ratio(matches, this.a.length + this.b.length); + } + + this.real_quick_ratio = function () { + var la = this.a.length; + var lb = this.b.length; + return _calculate_ratio(Math.min(la, lb), la + lb); + } + + this.isjunk = isjunk ? isjunk : difflib.defaultJunkFunction; + this.a = this.b = null; + this.set_seqs(a, b); + } +}; + diff --git a/vendor/scripts/diffview.js b/vendor/scripts/diffview.js new file mode 100644 index 000000000..372753d84 --- /dev/null +++ b/vendor/scripts/diffview.js @@ -0,0 +1,198 @@ +/* +This is part of jsdifflib v1.0. <http://github.com/cemerick/jsdifflib> + +Copyright 2007 - 2011 Chas Emerick <cemerick@snowtide.com>. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list + of conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY Chas Emerick ``AS IS'' AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Chas Emerick OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those of the +authors and should not be interpreted as representing official policies, either expressed +or implied, of Chas Emerick. +*/ +diffview = { + /** + * Builds and returns a visual diff view. The single parameter, `params', should contain + * the following values: + * + * - baseTextLines: the array of strings that was used as the base text input to SequenceMatcher + * - newTextLines: the array of strings that was used as the new text input to SequenceMatcher + * - opcodes: the array of arrays returned by SequenceMatcher.get_opcodes() + * - baseTextName: the title to be displayed above the base text listing in the diff view; defaults + * to "Base Text" + * - newTextName: the title to be displayed above the new text listing in the diff view; defaults + * to "New Text" + * - contextSize: the number of lines of context to show around differences; by default, all lines + * are shown + * - viewType: if 0, a side-by-side diff view is generated (default); if 1, an inline diff view is + * generated + */ + buildView: function (params) { + var baseTextLines = params.baseTextLines; + var newTextLines = params.newTextLines; + var opcodes = params.opcodes; + var baseTextName = params.baseTextName ? params.baseTextName : "Base Text"; + var newTextName = params.newTextName ? params.newTextName : "New Text"; + var contextSize = params.contextSize; + var inline = (params.viewType == 0 || params.viewType == 1) ? params.viewType : 0; + + if (baseTextLines == null) + throw "Cannot build diff view; baseTextLines is not defined."; + if (newTextLines == null) + throw "Cannot build diff view; newTextLines is not defined."; + if (!opcodes) + throw "Canno build diff view; opcodes is not defined."; + + function celt (name, clazz) { + var e = document.createElement(name); + e.className = clazz; + return e; + } + + function telt (name, text) { + var e = document.createElement(name); + e.appendChild(document.createTextNode(text)); + return e; + } + + function ctelt (name, clazz, text) { + var e = document.createElement(name); + e.className = clazz; + e.appendChild(document.createTextNode(text)); + return e; + } + + var tdata = document.createElement("thead"); + var node = document.createElement("tr"); + tdata.appendChild(node); + if (inline) { + node.appendChild(document.createElement("th")); + node.appendChild(document.createElement("th")); + node.appendChild(ctelt("th", "texttitle", baseTextName + " vs. " + newTextName)); + } else { + node.appendChild(document.createElement("th")); + node.appendChild(ctelt("th", "texttitle", baseTextName)); + node.appendChild(document.createElement("th")); + node.appendChild(ctelt("th", "texttitle", newTextName)); + } + tdata = [tdata]; + + var rows = []; + var node2; + + /** + * Adds two cells to the given row; if the given row corresponds to a real + * line number (based on the line index tidx and the endpoint of the + * range in question tend), then the cells will contain the line number + * and the line of text from textLines at position tidx (with the class of + * the second cell set to the name of the change represented), and tidx + 1 will + * be returned. Otherwise, tidx is returned, and two empty cells are added + * to the given row. + */ + function addCells (row, tidx, tend, textLines, change) { + if (tidx < tend) { + row.appendChild(telt("th", (tidx + 1).toString())); + row.appendChild(ctelt("td", change, textLines[tidx].replace(/\t/g, "\u00a0\u00a0\u00a0\u00a0"))); + return tidx + 1; + } else { + row.appendChild(document.createElement("th")); + row.appendChild(celt("td", "empty")); + return tidx; + } + } + + function addCellsInline (row, tidx, tidx2, textLines, change) { + row.appendChild(telt("th", tidx == null ? "" : (tidx + 1).toString())); + row.appendChild(telt("th", tidx2 == null ? "" : (tidx2 + 1).toString())); + row.appendChild(ctelt("td", change, textLines[tidx != null ? tidx : tidx2].replace(/\t/g, "\u00a0\u00a0\u00a0\u00a0"))); + } + + for (var idx = 0; idx < opcodes.length; idx++) { + code = opcodes[idx]; + change = code[0]; + var b = code[1]; + var be = code[2]; + var n = code[3]; + var ne = code[4]; + var rowcnt = Math.max(be - b, ne - n); + var toprows = []; + var botrows = []; + for (var i = 0; i < rowcnt; i++) { + // jump ahead if we've alredy provided leading context or if this is the first range + if (contextSize && opcodes.length > 1 && ((idx > 0 && i == contextSize) || (idx == 0 && i == 0)) && change=="equal") { + var jump = rowcnt - ((idx == 0 ? 1 : 2) * contextSize); + if (jump > 1) { + toprows.push(node = document.createElement("tr")); + + b += jump; + n += jump; + i += jump - 1; + node.appendChild(telt("th", "...")); + if (!inline) node.appendChild(ctelt("td", "skip", "")); + node.appendChild(telt("th", "...")); + node.appendChild(ctelt("td", "skip", "")); + + // skip last lines if they're all equal + if (idx + 1 == opcodes.length) { + break; + } else { + continue; + } + } + } + + toprows.push(node = document.createElement("tr")); + if (inline) { + if (change == "insert") { + addCellsInline(node, null, n++, newTextLines, change); + } else if (change == "replace") { + botrows.push(node2 = document.createElement("tr")); + if (b < be) addCellsInline(node, b++, null, baseTextLines, "delete"); + if (n < ne) addCellsInline(node2, null, n++, newTextLines, "insert"); + } else if (change == "delete") { + addCellsInline(node, b++, null, baseTextLines, change); + } else { + // equal + addCellsInline(node, b++, n++, baseTextLines, change); + } + } else { + b = addCells(node, b, be, baseTextLines, change); + n = addCells(node, n, ne, newTextLines, change); + } + } + + for (var i = 0; i < toprows.length; i++) rows.push(toprows[i]); + for (var i = 0; i < botrows.length; i++) rows.push(botrows[i]); + } + + rows.push(node = ctelt("th", "author", "diff view generated by ")); + node.setAttribute("colspan", inline ? 3 : 4); + node.appendChild(node2 = telt("a", "jsdifflib")); + node2.setAttribute("href", "http://github.com/cemerick/jsdifflib"); + + tdata.push(node = document.createElement("tbody")); + for (var idx in rows) rows.hasOwnProperty(idx) && node.appendChild(rows[idx]); + + node = celt("table", "diff" + (inline ? " inlinediff" : "")); + for (var idx in tdata) tdata.hasOwnProperty(idx) && node.appendChild(tdata[idx]); + return node; + } +}; + diff --git a/vendor/styles/diffview.css b/vendor/styles/diffview.css new file mode 100644 index 000000000..811a593b7 --- /dev/null +++ b/vendor/styles/diffview.css @@ -0,0 +1,83 @@ +/* +This is part of jsdifflib v1.0. <http://github.com/cemerick/jsdifflib> + +Copyright 2007 - 2011 Chas Emerick <cemerick@snowtide.com>. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list + of conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY Chas Emerick ``AS IS'' AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Chas Emerick OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those of the +authors and should not be interpreted as representing official policies, either expressed +or implied, of Chas Emerick. +*/ +table.diff { + border-collapse:collapse; + border:1px solid darkgray; + white-space:pre-wrap +} +table.diff tbody { + font-family:Courier, monospace +} +table.diff tbody th { + font-family:verdana,arial,'Bitstream Vera Sans',helvetica,sans-serif; + background:#EED; + font-size:11px; + font-weight:normal; + border:1px solid #BBC; + color:#886; + padding:.3em .5em .1em 2em; + text-align:right; + vertical-align:top +} +table.diff thead { + border-bottom:1px solid #BBC; + background:#EFEFEF; + font-family:Verdana +} +table.diff thead th.texttitle { + text-align:left +} +table.diff tbody td { + padding:0px .4em; + padding-top:.4em; + vertical-align:top; +} +table.diff .empty { + background-color:#DDD; +} +table.diff .replace { + background-color:#FD8 +} +table.diff .delete { + background-color:#E99; +} +table.diff .skip { + background-color:#EFEFEF; + border:1px solid #AAA; + border-right:1px solid #BBC; +} +table.diff .insert { + background-color:#9E9 +} +table.diff th.author { + text-align:right; + border-top:1px solid #BBC; + background:#EFEFEF +} \ No newline at end of file From 3c832d3707843ea5a6be8e1247f4a8b40851041a Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Thu, 10 Apr 2014 11:13:33 -0700 Subject: [PATCH 09/46] Added delta views to the other save modals. Fixed modified deltas in delta views. A few style tweaks. --- app/lib/deltas.coffee | 2 +- app/styles/base.sass | 3 +++ app/styles/editor/delta.sass | 7 +++++++ app/styles/modal/save_version.sass | 2 +- app/templates/editor/article/edit.jade | 2 +- app/templates/editor/delta.jade | 3 +++ app/templates/editor/thang/edit.jade | 2 +- app/templates/modal/save_version.jade | 16 ++++++++-------- app/views/editor/article/edit.coffee | 7 ++++++- app/views/editor/level/save_view.coffee | 1 + app/views/editor/thang/edit.coffee | 13 +++++++++---- app/views/kinds/ModalView.coffee | 2 ++ app/views/modal/save_version_modal.coffee | 12 +++++++++++- 13 files changed, 54 insertions(+), 18 deletions(-) diff --git a/app/lib/deltas.coffee b/app/lib/deltas.coffee index e6d8c936d..954af00ba 100644 --- a/app/lib/deltas.coffee +++ b/app/lib/deltas.coffee @@ -54,7 +54,7 @@ module.exports.interpretDelta = (delta, path, left, schema) -> betterDelta.path = betterPath.join(' :: ') betterDelta.schema = childSchema betterDelta.left = childLeft - betterDelta.right = jsondiffpatch.patch childLeft, delta + betterDelta.right = jsondiffpatch.patch childLeft, delta unless betterDelta.action is 'moved-index' betterDelta diff --git a/app/styles/base.sass b/app/styles/base.sass index 72ff4fe2f..2c1ee9309 100644 --- a/app/styles/base.sass +++ b/app/styles/base.sass @@ -111,6 +111,9 @@ a[data-toggle="modal"] @include box-shadow(0 0 0 #000) //position: absolute width: 99% + + .background-wrapper.plain + background: white .modal-content @include box-shadow(none) diff --git a/app/styles/editor/delta.sass b/app/styles/editor/delta.sass index ec70d3135..f41da3667 100644 --- a/app/styles/editor/delta.sass +++ b/app/styles/editor/delta.sass @@ -34,3 +34,10 @@ color: red .panel-heading background-color: lighten(red, 42%) + + .delta-moved-index + border-color: darkslategray + strong + color: darkslategray + .panel-heading + background-color: lighten(darkslategray, 60%) \ No newline at end of file diff --git a/app/styles/modal/save_version.sass b/app/styles/modal/save_version.sass index b64014ace..256a59a5e 100644 --- a/app/styles/modal/save_version.sass +++ b/app/styles/modal/save_version.sass @@ -1,6 +1,6 @@ #save-version-modal .modal-body - padding: 0px 50px 30px 20px + padding: 10px 50px 30px 20px #cla-link cursor: pointer diff --git a/app/templates/editor/article/edit.jade b/app/templates/editor/article/edit.jade index 4969e30e9..89cbc8bdd 100644 --- a/app/templates/editor/article/edit.jade +++ b/app/templates/editor/article/edit.jade @@ -13,7 +13,7 @@ block content button(data-i18n="general.history").btn.btn-primary#history-button History button(data-toggle="coco-modal", data-target="modal/revert", data-i18n="editor.revert", disabled=authorized === true ? undefined : "true").btn.btn-primary#revert-button Revert button(data-i18n="article.edit_btn_preview", disabled=authorized === true ? undefined : "true").btn.btn-primary#preview-button Preview - button(data-toggle="coco-modal", data-target="modal/save_version", data-i18n="common.save", disabled=authorized === true ? undefined : "true").btn.btn-primary#save-button Save + button(data-i18n="common.save", disabled=authorized === true ? undefined : "true").btn.btn-primary#save-button Save h3(data-i18n="article.edit_article_title") Edit Article span diff --git a/app/templates/editor/delta.jade b/app/templates/editor/delta.jade index 35c3c9a7e..961483324 100644 --- a/app/templates/editor/delta.jade +++ b/app/templates/editor/delta.jade @@ -30,4 +30,7 @@ if delta.action === 'text-diff' .col-md-12 div.text-diff + if delta.action === 'moved-index' + .col-md-12 + span Moved array value #{JSON.stringify(delta.left)} to index #{delta.destinationIndex} - i += 1 \ No newline at end of file diff --git a/app/templates/editor/thang/edit.jade b/app/templates/editor/thang/edit.jade index 1e8ce462d..556385873 100644 --- a/app/templates/editor/thang/edit.jade +++ b/app/templates/editor/thang/edit.jade @@ -13,7 +13,7 @@ block content img#portrait.img-thumbnail button.btn.btn-secondary#history-button(data-i18n="general.history") History - button.btn.btn-primary#save-button(data-toggle="coco-modal", data-target="modal/save_version", data-i18n="common.save", disabled=authorized === true ? undefined : "true") Save + button.btn.btn-primary#save-button(data-i18n="common.save", disabled=authorized === true ? undefined : "true") Save button.btn.btn-primary#revert-button(data-toggle="coco-modal", data-target="modal/revert", data-i18n="editor.revert", disabled=authorized === true ? undefined : "true") Revert h3 Edit Thang Type: "#{thangType.attributes.name}" diff --git a/app/templates/modal/save_version.jade b/app/templates/modal/save_version.jade index d1f8fc219..ebeb7d352 100644 --- a/app/templates/modal/save_version.jade +++ b/app/templates/modal/save_version.jade @@ -4,14 +4,14 @@ block modal-header-content h3(data-i18n="versions.save_version_title") Save New Version block modal-body-content - form.form - .form-group - label.control-label(for="commitMessage", data-i18n="general.commit_msg") Commit Message - textarea#commit-message.input-large.form-control(name="commitMessage", type="text") - .form-group - label.control-label(for="level-version-is-major", data-i18n="versions.new_major_version") New Major Version - input#major-version.input-large.form-control(name="version-is-major", type="checkbox") - span.help-block + .changes-stub + form.form-inline + .form-group.commit-message + input.form-control#commit-message(name="commitMessage", type="text", placeholder="Commit Message") + .checkbox + label + input#major-version(name="version-is-major", type="checkbox") + | Major Changes block modal-body-wait-content h3(data-i18n="common.saving") Saving... diff --git a/app/views/editor/article/edit.coffee b/app/views/editor/article/edit.coffee index 875dc6113..1d91558f1 100644 --- a/app/views/editor/article/edit.coffee +++ b/app/views/editor/article/edit.coffee @@ -3,6 +3,7 @@ VersionHistoryView = require './versions_view' ErrorView = require '../../error_view' template = require 'templates/editor/article/edit' Article = require 'models/Article' +SaveVersionModal = require 'views/modal/save_version_modal' module.exports = class ArticleEditView extends View id: "editor-article-edit-view" @@ -12,6 +13,7 @@ module.exports = class ArticleEditView extends View events: 'click #preview-button': 'openPreview' 'click #history-button': 'showVersionHistory' + 'click #save-button': 'openSaveModal' subscriptions: 'save-new-version': 'saveNewArticle' @@ -80,11 +82,14 @@ module.exports = class ArticleEditView extends View return if @startsLoading @showReadOnly() unless me.isAdmin() or @article.hasWriteAccess(me) - openPreview: => + openPreview: -> @preview = window.open('/editor/article/x/preview', 'preview', 'height=800,width=600') @preview.focus() if window.focus @preview.onload = => @pushChangesToPreview() return false + + openSaveModal: -> + @openModalView(new SaveVersionModal({model: @article})) saveNewArticle: (e) -> @treema.endExistingEdits() diff --git a/app/views/editor/level/save_view.coffee b/app/views/editor/level/save_view.coffee index c5d9e718d..b2a6874db 100644 --- a/app/views/editor/level/save_view.coffee +++ b/app/views/editor/level/save_view.coffee @@ -9,6 +9,7 @@ module.exports = class LevelSaveView extends SaveVersionModal template: template instant: false modalWidthPercent: 60 + plain: true events: 'click #save-version-button': 'commitLevel' diff --git a/app/views/editor/thang/edit.coffee b/app/views/editor/thang/edit.coffee index 660280e54..af88bf030 100644 --- a/app/views/editor/thang/edit.coffee +++ b/app/views/editor/thang/edit.coffee @@ -9,6 +9,7 @@ View = require 'views/kinds/RootView' ThangComponentEditView = require 'views/editor/components/main' VersionHistoryView = require './versions_view' ColorsTabView = require './colors_tab_view' +SaveVersionModal = require 'views/modal/save_version_modal' ErrorView = require '../../error_view' template = require 'templates/editor/thang/edit' @@ -33,6 +34,7 @@ module.exports = class ThangTypeEditView extends View 'click #marker-button': 'toggleDots' 'click #end-button': 'endAnimation' 'click #history-button': 'showVersionHistory' + 'click #save-button': 'openSaveModal' subscriptions: 'save-new-version': 'saveNewThangType' @@ -396,11 +398,14 @@ module.exports = class ThangTypeEditView extends View @showAnimation() @showingSelectedNode = false - destroy: -> - @camera?.destroy() - super() - showVersionHistory: (e) -> versionHistoryView = new VersionHistoryView thangType:@thangType, @thangTypeID @openModalView versionHistoryView Backbone.Mediator.publish 'level:view-switched', e + + openSaveModal: -> + @openModalView(new SaveVersionModal({model: @thangType})) + + destroy: -> + @camera?.destroy() + super() diff --git a/app/views/kinds/ModalView.coffee b/app/views/kinds/ModalView.coffee index 5222df067..2bf6ee8db 100644 --- a/app/views/kinds/ModalView.coffee +++ b/app/views/kinds/ModalView.coffee @@ -5,6 +5,7 @@ module.exports = class ModalView extends CocoView closeButton: true closesOnClickOutside: true modalWidthPercent: null + plain: false shortcuts: 'esc': 'hide' @@ -31,6 +32,7 @@ module.exports = class ModalView extends CocoView @$el.on 'hide.bs.modal', => @onHidden() unless @hidden @hidden = true + @$el.find('.background-wrapper').addClass('plain') if @plain afterInsert: -> super() diff --git a/app/views/modal/save_version_modal.coffee b/app/views/modal/save_version_modal.coffee index 86e1ea96b..826ac06ed 100644 --- a/app/views/modal/save_version_modal.coffee +++ b/app/views/modal/save_version_modal.coffee @@ -1,18 +1,28 @@ ModalView = require 'views/kinds/ModalView' template = require 'templates/modal/save_version' +DeltaView = require 'views/editor/delta' module.exports = class SaveVersionModal extends ModalView id: 'save-version-modal' template: template + plain: true events: 'click #save-version-button': 'onClickSaveButton' 'click #cla-link': 'onClickCLALink' 'click #agreement-button': 'onAgreedToCLA' - + + constructor: (options) -> + super options + @model = options.model + afterRender: -> super() @$el.find(if me.get('signedCLA') then '#accept-cla-wrapper' else '#save-version-button').hide() + return unless @model + changeEl = @$el.find('.changes-stub') + deltaView = new DeltaView({model:@model}) + @insertSubView(deltaView, changeEl) onClickSaveButton: -> Backbone.Mediator.publish 'save-new-version', { From a3951b0fa7cab7830fb5875e68f2c879d29b138b Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Thu, 10 Apr 2014 13:09:44 -0700 Subject: [PATCH 10/46] Extended the save version modal to allow submitting patches. --- app/models/CocoModel.coffee | 5 +++ app/models/Patch.coffee | 5 +++ app/styles/modal/save_version.sass | 5 +++ app/templates/modal/save_version.jade | 46 +++++++++++++++-------- app/views/admin/users_view.coffee | 3 +- app/views/editor/level/save_view.coffee | 2 +- app/views/modal/save_version_modal.coffee | 33 +++++++++++++++- server/patches/patch_handler.coffee | 2 +- server/patches/patch_schema.coffee | 5 +-- test/server/functional/patch.spec.coffee | 1 + 10 files changed, 84 insertions(+), 23 deletions(-) create mode 100644 app/models/Patch.coffee diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee index 669899e6c..de1695490 100644 --- a/app/models/CocoModel.coffee +++ b/app/models/CocoModel.coffee @@ -1,5 +1,6 @@ storage = require 'lib/storage' deltasLib = require 'lib/deltas' +auth = require 'lib/auth' class CocoSchema extends Backbone.Model constructor: (path, args...) -> @@ -200,6 +201,8 @@ class CocoModel extends Backbone.Model hasReadAccess: (actor) -> # actor is a User object + actor ?= auth.me + return true if actor.isAdmin() if @get('permissions')? for permission in @get('permissions') if permission.target is 'public' or actor.get('_id') is permission.target @@ -210,6 +213,8 @@ class CocoModel extends Backbone.Model hasWriteAccess: (actor) -> # actor is a User object + actor ?= auth.me + return true if actor.isAdmin() if @get('permissions')? for permission in @get('permissions') if permission.target is 'public' or actor.get('_id') is permission.target diff --git a/app/models/Patch.coffee b/app/models/Patch.coffee new file mode 100644 index 000000000..a88c30941 --- /dev/null +++ b/app/models/Patch.coffee @@ -0,0 +1,5 @@ +CocoModel = require('./CocoModel') + +module.exports = class PatchModel extends CocoModel + @className: "Patch" + urlRoot: "/db/patch" \ No newline at end of file diff --git a/app/styles/modal/save_version.sass b/app/styles/modal/save_version.sass index 256a59a5e..e7ab79751 100644 --- a/app/styles/modal/save_version.sass +++ b/app/styles/modal/save_version.sass @@ -1,6 +1,11 @@ #save-version-modal .modal-body padding: 10px 50px 30px 20px + + .modal-footer + text-align: left + .buttons + text-align: right #cla-link cursor: pointer diff --git a/app/templates/modal/save_version.jade b/app/templates/modal/save_version.jade index ebeb7d352..7fcfd871d 100644 --- a/app/templates/modal/save_version.jade +++ b/app/templates/modal/save_version.jade @@ -1,30 +1,46 @@ extends /templates/modal/modal_base block modal-header-content - h3(data-i18n="versions.save_version_title") Save New Version + if isPatch + h3(data-i18n="versions.submit_patch_title") Submit Patch + else + h3(data-i18n="versions.save_version_title") Save New Version block modal-body-content - .changes-stub - form.form-inline - .form-group.commit-message - input.form-control#commit-message(name="commitMessage", type="text", placeholder="Commit Message") - .checkbox - label - input#major-version(name="version-is-major", type="checkbox") - | Major Changes + if hasChanges + .changes-stub + form.form-inline + .form-group.commit-message + input.form-control#commit-message(name="commitMessage", type="text", placeholder="Commit Message") + if !isPatch + .checkbox + label + input#major-version(name="version-is-major", type="checkbox") + | Major Changes + else + .alert.alert-danger No changes block modal-body-wait-content - h3(data-i18n="common.saving") Saving... + if hasChanges + if isPatch + h3(data-i18n="versions.submitting_patch") Submitting Patch... + else + h3(data-i18n="common.saving") Saving... block modal-footer-content - if !noSaveButton + if hasChanges #accept-cla-wrapper.alert.alert-info span(data-i18n="versions.cla_prefix") To save changes, first you must agree to our | strong#cla-link(data-i18n="versions.cla_url") CLA span(data-i18n="versions.cla_suffix") . - button.btn#agreement-button(data-i18n="versions.cla_agree") I AGREE + button.btn.btn-sm#agreement-button(data-i18n="versions.cla_agree") I AGREE + if isPatch + .alert.alert-info An owner will need to approve it before your changes will become visible. - button.btn(data-dismiss="modal", data-i18n="common.cancel") Cancel - if !noSaveButton - button.btn.btn-primary#save-version-button(data-i18n="common.save") Save + .buttons + button.btn(data-dismiss="modal", data-i18n="common.cancel") Cancel + if hasChanges && !isPatch + button.btn.btn-primary#save-version-button(data-i18n="common.save") Save + if hasChanges && isPatch + button.btn.btn-primary#submit-patch-button(data-i18n="versions.submit_patch") Submit Patch \ No newline at end of file diff --git a/app/views/admin/users_view.coffee b/app/views/admin/users_view.coffee index c19c7bd37..acc9a8152 100644 --- a/app/views/admin/users_view.coffee +++ b/app/views/admin/users_view.coffee @@ -38,8 +38,7 @@ module.exports = class UsersView extends View @users.fetch() @listenTo(@users, 'all', @render) - getRenderData: => + getRenderData: -> c = super() c.users = (user.attributes for user in @users.models) - console.log('our render data', c) c \ 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 b2a6874db..33d94370b 100644 --- a/app/views/editor/level/save_view.coffee +++ b/app/views/editor/level/save_view.coffee @@ -25,7 +25,7 @@ module.exports = class LevelSaveView extends SaveVersionModal context.levelNeedsSave = @level.hasLocalChanges() context.modifiedComponents = _.filter @supermodel.getModels(LevelComponent), @shouldSaveEntity context.modifiedSystems = _.filter @supermodel.getModels(LevelSystem), @shouldSaveEntity - context.noSaveButton = not (context.levelNeedsSave or context.modifiedComponents.length or context.modifiedSystems.length) + context.hasChanges = (context.levelNeedsSave or context.modifiedComponents.length or context.modifiedSystems.length) @lastContext = context context diff --git a/app/views/modal/save_version_modal.coffee b/app/views/modal/save_version_modal.coffee index 826ac06ed..db4c45de3 100644 --- a/app/views/modal/save_version_modal.coffee +++ b/app/views/modal/save_version_modal.coffee @@ -1,6 +1,8 @@ ModalView = require 'views/kinds/ModalView' template = require 'templates/modal/save_version' DeltaView = require 'views/editor/delta' +Patch = require 'models/Patch' +forms = require 'lib/forms' module.exports = class SaveVersionModal extends ModalView id: 'save-version-modal' @@ -11,15 +13,23 @@ module.exports = class SaveVersionModal extends ModalView 'click #save-version-button': 'onClickSaveButton' 'click #cla-link': 'onClickCLALink' 'click #agreement-button': 'onAgreedToCLA' + 'click #submit-patch-button': 'onClickPatchButton' constructor: (options) -> super options @model = options.model + new Patch() + @isPatch = not @model.hasWriteAccess() + + getRenderData: -> + c = super() + c.isPatch = @isPatch + c.hasChanges = @model.hasLocalChanges() + c afterRender: -> super() @$el.find(if me.get('signedCLA') then '#accept-cla-wrapper' else '#save-version-button').hide() - return unless @model changeEl = @$el.find('.changes-stub') deltaView = new DeltaView({model:@model}) @insertSubView(deltaView, changeEl) @@ -30,6 +40,27 @@ module.exports = class SaveVersionModal extends ModalView commitMessage: @$el.find('#commit-message').val() } + onClickPatchButton: -> + forms.clearFormAlerts @$el + patch = new Patch() + patch.set 'delta', @model.getDelta() + patch.set 'commitMessage', @$el.find('#commit-message').val() + patch.set 'target', { + 'collection': _.string.underscored @model.constructor.className + 'id': @model.id + } + errors = patch.validate() + forms.applyErrorsToForm(@$el, errors) if errors + res = patch.save() + return unless res + @enableModalInProgress(@$el) + + res.error => + @disableModalInProgress(@$el) + + res.success => + @hide() + onClickCLALink: -> window.open('/cla', 'cla', 'height=800,width=900') diff --git a/server/patches/patch_handler.coffee b/server/patches/patch_handler.coffee index a9a26e05b..c38068ae5 100644 --- a/server/patches/patch_handler.coffee +++ b/server/patches/patch_handler.coffee @@ -7,7 +7,7 @@ mongoose = require('mongoose') PatchHandler = class PatchHandler extends Handler modelClass: Patch editableProperties: [] - postEditableProperties: ['delta', 'target'] + postEditableProperties: ['delta', 'target', 'commitMessage'] jsonSchema: require './patch_schema' makeNewInstance: (req) -> diff --git a/server/patches/patch_schema.coffee b/server/patches/patch_schema.coffee index 7e02f4b8a..eae980d4e 100644 --- a/server/patches/patch_schema.coffee +++ b/server/patches/patch_schema.coffee @@ -2,10 +2,9 @@ c = require '../commons/schemas' patchables = ['level', 'thang_type', 'level_system', 'level_component', 'article'] -PatchSchema = c.object({title:'Patch', required:['target', 'delta']}, { +PatchSchema = c.object({title:'Patch', required:['target', 'delta', 'commitMessage']}, { delta: { title: 'Delta', type:['array', 'object'] } - title: c.shortString() - description: c.shortString({maxLength: 500}) + commitMessage: c.shortString({maxLength: 500, minLength: 1}) creator: c.objectId(links: [{rel: 'extra', href: "/db/user/{($)}"}]) created: c.date( { title: 'Created', readOnly: true }) status: { enum: ['pending', 'accepted', 'rejected', 'withdrawn']} diff --git a/test/server/functional/patch.spec.coffee b/test/server/functional/patch.spec.coffee index b9875a814..d8694baf0 100644 --- a/test/server/functional/patch.spec.coffee +++ b/test/server/functional/patch.spec.coffee @@ -14,6 +14,7 @@ describe '/db/patch', -> patchURL = getURL('/db/patch') patches = {} patch = + commitMessage: 'Accept this patch!' delta: {name:['test']} target: id:null From 2c67df355c033dc41422aab968f1ebfcd9cb10ce Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Fri, 11 Apr 2014 10:33:22 -0700 Subject: [PATCH 11/46] Merge branch 'backbone_mediator' of https://github.com/rubenvereecken/codecombat into feature/jsondiffpatch Conflicts: app/initialize.coffee bower.json --- bower.json | 8 +------- server/levels/level_handler.coffee | 5 +---- server/users/user_handler.coffee | 5 +---- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/bower.json b/bower.json index 5995b14c3..8f143900b 100644 --- a/bower.json +++ b/bower.json @@ -37,15 +37,11 @@ "firebase": "~1.0.2", "catiline": "~2.9.3", "d3": "~3.4.4", -<<<<<<< HEAD "jsondiffpatch": "~0.1.5", - "nanoscroller": "~0.8.0" -======= "nanoscroller": "~0.8.0", "jquery.tablesorter": "~2.15.13", "treema": "~0.0.1", "bootstrap": "~3.1.1" ->>>>>>> master }, "overrides": { "backbone": { @@ -60,10 +56,9 @@ "underscore.string": { "main": "lib/underscore.string.js" }, -<<<<<<< HEAD "jsondiffpatch": { "main": ["build/bundle-full.js", "build/formatters.js", "src/formatters/html.css"] -======= + }, "jquery.tablesorter": { "main": [ "js/jquery.tablesorter.js", @@ -79,7 +74,6 @@ "./dist/fonts/glyphicons-halflings-regular.ttf", "./dist/fonts/glyphicons-halflings-regular.woff" ] ->>>>>>> master } } } diff --git a/server/levels/level_handler.coffee b/server/levels/level_handler.coffee index 15c75b521..a19487191 100644 --- a/server/levels/level_handler.coffee +++ b/server/levels/level_handler.coffee @@ -38,12 +38,9 @@ LevelHandler = class LevelHandler extends Handler return @getLeaderboardFacebookFriends(req, res, args[0]) if args[1] is 'leaderboard_facebook_friends' return @getLeaderboardGPlusFriends(req, res, args[0]) if args[1] is 'leaderboard_gplus_friends' return @getHistogramData(req, res, args[0]) if args[1] is 'histogram_data' -<<<<<<< HEAD - super(arguments...) -======= return @checkExistence(req, res, args[0]) if args[1] is 'exists' return @sendNotFoundError(res) ->>>>>>> master + super(arguments...) fetchLevelByIDAndHandleErrors: (id, req, res, callback) -> @getDocumentForIdOrSlug id, (err, level) => diff --git a/server/users/user_handler.coffee b/server/users/user_handler.coffee index 0ef407690..258051db9 100644 --- a/server/users/user_handler.coffee +++ b/server/users/user_handler.coffee @@ -173,12 +173,9 @@ UserHandler = class UserHandler extends Handler return @getNamesByIds(req, res) if args[1] is 'names' return @nameToID(req, res, args[0]) if args[1] is 'nameToID' return @getLevelSessions(req, res, args[0]) if args[1] is 'level.sessions' -<<<<<<< HEAD - super(arguments...) -======= return @getCandidates(req, res) if args[1] is 'candidates' return @sendNotFoundError(res) ->>>>>>> master + super(arguments...) agreeToCLA: (req, res) -> return @sendUnauthorizedError(res) unless req.user From bbb9fb7a648525db6410121bbd052517085712f4 Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Fri, 11 Apr 2014 14:19:17 -0700 Subject: [PATCH 12/46] Added a patches view. --- app/collections/PatchesCollection.coffee | 10 +++++ app/lib/NameLoader.coffee | 17 ++++++++ app/styles/editor/patches.sass | 3 ++ app/templates/editor/patches.jade | 27 +++++++++++++ app/templates/editor/thang/edit.jade | 6 +++ app/views/editor/patches_view.coffee | 49 ++++++++++++++++++++++++ app/views/editor/thang/edit.coffee | 3 ++ app/views/kinds/CocoView.coffee | 11 ++++-- 8 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 app/collections/PatchesCollection.coffee create mode 100644 app/lib/NameLoader.coffee create mode 100644 app/styles/editor/patches.sass create mode 100644 app/templates/editor/patches.jade create mode 100644 app/views/editor/patches_view.coffee diff --git a/app/collections/PatchesCollection.coffee b/app/collections/PatchesCollection.coffee new file mode 100644 index 000000000..456db1e42 --- /dev/null +++ b/app/collections/PatchesCollection.coffee @@ -0,0 +1,10 @@ +PatchModel = require 'models/Patch' +CocoCollection = require 'models/CocoCollection' + +module.exports = class PatchesCollection extends CocoCollection + model: PatchModel + + initialize: (models, options, forModel, @status='pending') -> + super(arguments...) + @url = "#{forModel.urlRoot}/#{forModel.get('original')}/patches?status=#{@status}" + diff --git a/app/lib/NameLoader.coffee b/app/lib/NameLoader.coffee new file mode 100644 index 000000000..0dd798d80 --- /dev/null +++ b/app/lib/NameLoader.coffee @@ -0,0 +1,17 @@ +CocoClass = require 'lib/CocoClass' + +namesCache = {} + +class NameLoader extends CocoClass + loadNames: (ids) -> + toLoad = (id for id in ids when not namesCache[id]) + return false unless toLoad.length + jqxhr = $.ajax('/db/user/x/names', {type:'POST', data:{ids:toLoad}}) + jqxhr.done @loadedNames + + loadedNames: (newNames) => + _.extend namesCache, newNames + + getName: (id) -> namesCache[id] + +module.exports = new NameLoader() diff --git a/app/styles/editor/patches.sass b/app/styles/editor/patches.sass new file mode 100644 index 000000000..87c22728e --- /dev/null +++ b/app/styles/editor/patches.sass @@ -0,0 +1,3 @@ +.patches-view + .status-buttons + margin: 10px 0 diff --git a/app/templates/editor/patches.jade b/app/templates/editor/patches.jade new file mode 100644 index 000000000..ce3b1af84 --- /dev/null +++ b/app/templates/editor/patches.jade @@ -0,0 +1,27 @@ +.btn-group(data-toggle="buttons").status-buttons + label.btn.btn-default.pending + input(type="radio", name="status", value="pending") + | Pending + label.btn.btn-default.accepted + input(type="radio", name="status", value="accepted") + | Accepted + label.btn.btn-default.rejected + input(type="radio", name="status", value="rejected") + | Rejected + label.btn.btn-default.withdrawn + input(type="radio", name="status", value="withdrawn") + | Withdrawn + +if patches.loading + p Loading +else + table.table.table-condensed.table-bordered + tr + th Submitter + th Submitted + th Commit Message + for patch in patches + tr + td= patch.userName + td= moment(patch.get('created')).format('llll') + td= patch.get('commitMessage') diff --git a/app/templates/editor/thang/edit.jade b/app/templates/editor/thang/edit.jade index 556385873..04486aba1 100644 --- a/app/templates/editor/thang/edit.jade +++ b/app/templates/editor/thang/edit.jade @@ -27,6 +27,8 @@ block content a(href="#editor-thang-spritesheets-view", data-toggle="tab") Spritesheets li a(href="#editor-thang-colors-tab-view", data-toggle="tab")#color-tab Colors + li + a(href="#editor-thang-patches-view", data-toggle="tab")#patches-tab Patches div.tab-content div.tab-pane#editor-thang-colors-tab-view @@ -83,6 +85,10 @@ block content div.tab-pane#editor-thang-spritesheets-view div#spritesheets + + div.tab-pane#editor-thang-patches-view + + div.patches-view div#error-view diff --git a/app/views/editor/patches_view.coffee b/app/views/editor/patches_view.coffee new file mode 100644 index 000000000..abba96997 --- /dev/null +++ b/app/views/editor/patches_view.coffee @@ -0,0 +1,49 @@ +CocoView = require 'views/kinds/CocoView' +template = require 'templates/editor/patches' +PatchesCollection = require 'collections/PatchesCollection' +nameLoader = require 'lib/NameLoader' + +module.exports = class PatchesView extends CocoView + template: template + className: 'patches-view' + status: 'pending' + + events: + 'change .status-buttons': 'onStatusButtonsChanged' + + constructor: (@model, options) -> + super(options) + @initPatches() + + initPatches: -> + @startedLoading = false + @patches = new PatchesCollection([], {}, @model, @status) + @listenToOnce @patches, 'sync', @gotPatches + @addResourceToLoad @patches, 'patches' + + gotPatches: -> + ids = (p.get('creator') for p in @patches.models) + jqxhr = nameLoader.loadNames ids + if jqxhr then @addRequestToLoad(jqxhr, 'user_names', 'gotPatches') else @render() + + load: -> + return if @startedLoading + @patches.fetch() + @startedLoading = true + + getRenderData: -> + c = super() + patch.userName = nameLoader.getName(patch.get('creator')) for patch in @patches.models + c.patches = @patches.models + c.status + c + + afterRender: -> + @$el.find(".#{@status}").addClass 'active' + + onStatusButtonsChanged: (e) -> + @loaded = false + @status = $(e.target).val() + @initPatches() + @load() + @render() diff --git a/app/views/editor/thang/edit.coffee b/app/views/editor/thang/edit.coffee index af88bf030..67edc6978 100644 --- a/app/views/editor/thang/edit.coffee +++ b/app/views/editor/thang/edit.coffee @@ -9,6 +9,7 @@ View = require 'views/kinds/RootView' ThangComponentEditView = require 'views/editor/components/main' VersionHistoryView = require './versions_view' ColorsTabView = require './colors_tab_view' +PatchesView = require 'views/editor/patches_view' SaveVersionModal = require 'views/modal/save_version_modal' ErrorView = require '../../error_view' template = require 'templates/editor/thang/edit' @@ -35,6 +36,7 @@ module.exports = class ThangTypeEditView extends View 'click #end-button': 'endAnimation' 'click #history-button': 'showVersionHistory' 'click #save-button': 'openSaveModal' + 'click #patches-tab': -> @patchesView.load() subscriptions: 'save-new-version': 'saveNewThangType' @@ -92,6 +94,7 @@ module.exports = class ThangTypeEditView extends View @initSliders() @initComponents() @insertSubView(new ColorsTabView(@thangType)) + @patchesView = @insertSubView(new PatchesView(@thangType), @$el.find('.patches-view')) @showReadOnly() unless me.isAdmin() or @thangType.hasWriteAccess(me) initComponents: => diff --git a/app/views/kinds/CocoView.coffee b/app/views/kinds/CocoView.coffee index 360913182..b73685eb1 100644 --- a/app/views/kinds/CocoView.coffee +++ b/app/views/kinds/CocoView.coffee @@ -101,6 +101,7 @@ module.exports = class CocoView extends Backbone.View context.fbRef = context.pathname.replace(/[^a-zA-Z0-9+/=\-.:_]/g, '').slice(0, 40) or 'home' context.isMobile = @isMobile() context.isIE = @isIE() + context.moment = moment context afterRender: -> @@ -300,18 +301,22 @@ module.exports = class CocoView extends Backbone.View # Subviews insertSubView: (view, elToReplace=null) -> - @subviews[view.id].destroy() if view.id of @subviews + key = view.id or (view.constructor.name+classCount++) + key = _.string.underscored(key) + @subviews[key].destroy() if key of @subviews elToReplace ?= @$el.find('#'+view.id) elToReplace.after(view.el).remove() view.parent = @ view.render() view.afterInsert() - @subviews[view.id] = view + view.parentKey = key + @subviews[key] = view + view removeSubView: (view) -> view.$el.empty() + delete @subviews[view.parentKey] view.destroy() - delete @subviews[view.id] # Utilities From 177dd2c8cd814c48590add83ec224a56f0cb22b0 Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Fri, 11 Apr 2014 15:37:04 -0700 Subject: [PATCH 13/46] Set up Bootstrap 3 glyphicons. --- .../fonts/glyphicons-halflings-regular.eot | Bin 0 -> 20335 bytes .../fonts/glyphicons-halflings-regular.svg | 229 ++++++++++++++++++ .../fonts/glyphicons-halflings-regular.ttf | Bin 0 -> 41280 bytes .../fonts/glyphicons-halflings-regular.woff | Bin 0 -> 23320 bytes app/styles/base.sass | 7 + 5 files changed, 236 insertions(+) create mode 100644 app/assets/fonts/glyphicons-halflings-regular.eot create mode 100644 app/assets/fonts/glyphicons-halflings-regular.svg create mode 100644 app/assets/fonts/glyphicons-halflings-regular.ttf create mode 100644 app/assets/fonts/glyphicons-halflings-regular.woff diff --git a/app/assets/fonts/glyphicons-halflings-regular.eot b/app/assets/fonts/glyphicons-halflings-regular.eot new file mode 100644 index 0000000000000000000000000000000000000000..4a4ca865d67e86f961bc6e2ef00bffa4e34bb9ed GIT binary patch literal 20335 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 literal 0 HcmV?d00001 diff --git a/app/assets/fonts/glyphicons-halflings-regular.svg b/app/assets/fonts/glyphicons-halflings-regular.svg new file mode 100644 index 000000000..e3e2dc739 --- /dev/null +++ b/app/assets/fonts/glyphicons-halflings-regular.svg @@ -0,0 +1,229 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" > +<svg xmlns="http://www.w3.org/2000/svg"> +<metadata></metadata> +<defs> +<font id="glyphicons_halflingsregular" horiz-adv-x="1200" > +<font-face units-per-em="1200" ascent="960" descent="-240" /> +<missing-glyph horiz-adv-x="500" /> +<glyph /> +<glyph /> +<glyph unicode="
" /> +<glyph unicode=" " /> +<glyph unicode="*" d="M100 500v200h259l-183 183l141 141l183 -183v259h200v-259l183 183l141 -141l-183 -183h259v-200h-259l183 -183l-141 -141l-183 183v-259h-200v259l-183 -183l-141 141l183 183h-259z" /> +<glyph unicode="+" d="M0 400v300h400v400h300v-400h400v-300h-400v-400h-300v400h-400z" /> +<glyph unicode=" " /> +<glyph unicode=" " horiz-adv-x="652" /> +<glyph unicode=" " horiz-adv-x="1304" /> +<glyph unicode=" " horiz-adv-x="652" /> +<glyph unicode=" " horiz-adv-x="1304" /> +<glyph unicode=" " horiz-adv-x="434" /> +<glyph unicode=" " horiz-adv-x="326" /> +<glyph unicode=" " horiz-adv-x="217" /> +<glyph unicode=" " horiz-adv-x="217" /> +<glyph unicode=" " horiz-adv-x="163" /> +<glyph unicode=" " horiz-adv-x="260" /> +<glyph unicode=" " horiz-adv-x="72" /> +<glyph unicode=" " horiz-adv-x="260" /> +<glyph unicode=" " horiz-adv-x="326" /> +<glyph unicode="€" d="M100 500l100 100h113q0 47 5 100h-218l100 100h135q37 167 112 257q117 141 297 141q242 0 354 -189q60 -103 66 -209h-181q0 55 -25.5 99t-63.5 68t-75 36.5t-67 12.5q-24 0 -52.5 -10t-62.5 -32t-65.5 -67t-50.5 -107h379l-100 -100h-300q-6 -46 -6 -100h406l-100 -100 h-300q9 -74 33 -132t52.5 -91t62 -54.5t59 -29t46.5 -7.5q29 0 66 13t75 37t63.5 67.5t25.5 96.5h174q-31 -172 -128 -278q-107 -117 -274 -117q-205 0 -324 158q-36 46 -69 131.5t-45 205.5h-217z" /> +<glyph unicode="−" d="M200 400h900v300h-900v-300z" /> +<glyph unicode="◼" horiz-adv-x="500" d="M0 0z" /> +<glyph unicode="☁" d="M-14 494q0 -80 56.5 -137t135.5 -57h750q120 0 205 86.5t85 207.5t-85 207t-205 86q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5z" /> +<glyph unicode="✉" d="M0 100l400 400l200 -200l200 200l400 -400h-1200zM0 300v600l300 -300zM0 1100l600 -603l600 603h-1200zM900 600l300 300v-600z" /> +<glyph unicode="✏" d="M-13 -13l333 112l-223 223zM187 403l214 -214l614 614l-214 214zM887 1103l214 -214l99 92q13 13 13 32.5t-13 33.5l-153 153q-15 13 -33 13t-33 -13z" /> +<glyph unicode="" d="M0 1200h1200l-500 -550v-550h300v-100h-800v100h300v550z" /> +<glyph unicode="" d="M14 84q18 -55 86 -75.5t147 5.5q65 21 109 69t44 90v606l600 155v-521q-64 16 -138 -7q-79 -26 -122.5 -83t-25.5 -111q18 -55 86 -75.5t147 4.5q70 23 111.5 63.5t41.5 95.5v881q0 10 -7 15.5t-17 2.5l-752 -193q-10 -3 -17 -12.5t-7 -19.5v-689q-64 17 -138 -7 q-79 -25 -122.5 -82t-25.5 -112z" /> +<glyph unicode="" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233z" /> +<glyph unicode="" d="M100 784q0 64 28 123t73 100.5t104.5 64t119 20.5t120 -38.5t104.5 -104.5q48 69 109.5 105t121.5 38t118.5 -20.5t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-149.5 152.5t-126.5 127.5 t-94 124.5t-33.5 117.5z" /> +<glyph unicode="" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1z" /> +<glyph unicode="" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1zM237 700l196 -142l-73 -226l192 140l195 -141l-74 229l193 140h-235l-77 211l-78 -211h-239z" /> +<glyph unicode="" d="M0 0v143l400 257v100q-37 0 -68.5 74.5t-31.5 125.5v200q0 124 88 212t212 88t212 -88t88 -212v-200q0 -51 -31.5 -125.5t-68.5 -74.5v-100l400 -257v-143h-1200z" /> +<glyph unicode="" d="M0 0v1100h1200v-1100h-1200zM100 100h100v100h-100v-100zM100 300h100v100h-100v-100zM100 500h100v100h-100v-100zM100 700h100v100h-100v-100zM100 900h100v100h-100v-100zM300 100h600v400h-600v-400zM300 600h600v400h-600v-400zM1000 100h100v100h-100v-100z M1000 300h100v100h-100v-100zM1000 500h100v100h-100v-100zM1000 700h100v100h-100v-100zM1000 900h100v100h-100v-100z" /> +<glyph unicode="" d="M0 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM0 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5zM600 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM600 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400 q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5z" /> +<glyph unicode="" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200 q-21 0 -35.5 14.5t-14.5 35.5zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 450v200q0 21 14.5 35.5t35.5 14.5h200 q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5z" /> +<glyph unicode="" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v200q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5 t-14.5 -35.5v-200zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5z" /> +<glyph unicode="" d="M29 454l419 -420l818 820l-212 212l-607 -607l-206 207z" /> +<glyph unicode="" d="M106 318l282 282l-282 282l212 212l282 -282l282 282l212 -212l-282 -282l282 -282l-212 -212l-282 282l-282 -282z" /> +<glyph unicode="" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233zM300 600v200h100v100h200v-100h100v-200h-100v-100h-200v100h-100z" /> +<glyph unicode="" d="M23 694q0 200 142 342t342 142t342 -142t142 -342q0 -141 -78 -262l300 -299q7 -7 7 -18t-7 -18l-109 -109q-8 -8 -18 -8t-18 8l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 694q0 -136 97 -233t234 -97t233.5 97t96.5 233t-96.5 233t-233.5 97t-234 -97 t-97 -233zM300 601h400v200h-400v-200z" /> +<glyph unicode="" d="M23 600q0 183 105 331t272 210v-166q-103 -55 -165 -155t-62 -220q0 -177 125 -302t302 -125t302 125t125 302q0 120 -62 220t-165 155v166q167 -62 272 -210t105 -331q0 -118 -45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5 zM500 750q0 -21 14.5 -35.5t35.5 -14.5h100q21 0 35.5 14.5t14.5 35.5v400q0 21 -14.5 35.5t-35.5 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-400z" /> +<glyph unicode="" d="M100 1h200v300h-200v-300zM400 1v500h200v-500h-200zM700 1v800h200v-800h-200zM1000 1v1200h200v-1200h-200z" /> +<glyph unicode="" d="M26 601q0 -33 6 -74l151 -38l2 -6q14 -49 38 -93l3 -5l-80 -134q45 -59 105 -105l133 81l5 -3q45 -26 94 -39l5 -2l38 -151q40 -5 74 -5q27 0 74 5l38 151l6 2q46 13 93 39l5 3l134 -81q56 44 104 105l-80 134l3 5q24 44 39 93l1 6l152 38q5 40 5 74q0 28 -5 73l-152 38 l-1 6q-16 51 -39 93l-3 5l80 134q-44 58 -104 105l-134 -81l-5 3q-45 25 -93 39l-6 1l-38 152q-40 5 -74 5q-27 0 -74 -5l-38 -152l-5 -1q-50 -14 -94 -39l-5 -3l-133 81q-59 -47 -105 -105l80 -134l-3 -5q-25 -47 -38 -93l-2 -6l-151 -38q-6 -48 -6 -73zM385 601 q0 88 63 151t152 63t152 -63t63 -151q0 -89 -63 -152t-152 -63t-152 63t-63 152z" /> +<glyph unicode="" d="M100 1025v50q0 10 7.5 17.5t17.5 7.5h275v100q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5v-100h275q10 0 17.5 -7.5t7.5 -17.5v-50q0 -11 -7 -18t-18 -7h-1050q-11 0 -18 7t-7 18zM200 100v800h900v-800q0 -41 -29.5 -71t-70.5 -30h-700q-41 0 -70.5 30 t-29.5 71zM300 100h100v700h-100v-700zM500 100h100v700h-100v-700zM500 1100h300v100h-300v-100zM700 100h100v700h-100v-700zM900 100h100v700h-100v-700z" /> +<glyph unicode="" d="M1 601l656 644l644 -644h-200v-600h-300v400h-300v-400h-300v600h-200z" /> +<glyph unicode="" d="M100 25v1150q0 11 7 18t18 7h475v-500h400v-675q0 -11 -7 -18t-18 -7h-850q-11 0 -18 7t-7 18zM700 800v300l300 -300h-300z" /> +<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 500v400h100 v-300h200v-100h-300z" /> +<glyph unicode="" d="M-100 0l431 1200h209l-21 -300h162l-20 300h208l431 -1200h-538l-41 400h-242l-40 -400h-539zM488 500h224l-27 300h-170z" /> +<glyph unicode="" d="M0 0v400h490l-290 300h200v500h300v-500h200l-290 -300h490v-400h-1100zM813 200h175v100h-175v-100z" /> +<glyph unicode="" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM188 600q0 -170 121 -291t291 -121t291 121t121 291t-121 291t-291 121 t-291 -121t-121 -291zM350 600h150v300h200v-300h150l-250 -300z" /> +<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM350 600l250 300 l250 -300h-150v-300h-200v300h-150z" /> +<glyph unicode="" d="M0 25v475l200 700h800l199 -700l1 -475q0 -11 -7 -18t-18 -7h-1150q-11 0 -18 7t-7 18zM200 500h200l50 -200h300l50 200h200l-97 500h-606z" /> +<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 397v401 l297 -200z" /> +<glyph unicode="" d="M23 600q0 -118 45.5 -224.5t123 -184t184 -123t224.5 -45.5t224.5 45.5t184 123t123 184t45.5 224.5h-150q0 -177 -125 -302t-302 -125t-302 125t-125 302t125 302t302 125q136 0 246 -81l-146 -146h400v400l-145 -145q-157 122 -355 122q-118 0 -224.5 -45.5t-184 -123 t-123 -184t-45.5 -224.5z" /> +<glyph unicode="" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5q198 0 355 -122l145 145v-400h-400l147 147q-112 80 -247 80q-177 0 -302 -125t-125 -302h-150zM100 0v400h400l-147 -147q112 -80 247 -80q177 0 302 125t125 302h150q0 -118 -45.5 -224.5t-123 -184t-184 -123 t-224.5 -45.5q-198 0 -355 122z" /> +<glyph unicode="" d="M100 0h1100v1200h-1100v-1200zM200 100v900h900v-900h-900zM300 200v100h100v-100h-100zM300 400v100h100v-100h-100zM300 600v100h100v-100h-100zM300 800v100h100v-100h-100zM500 200h500v100h-500v-100zM500 400v100h500v-100h-500zM500 600v100h500v-100h-500z M500 800v100h500v-100h-500z" /> +<glyph unicode="" d="M0 100v600q0 41 29.5 70.5t70.5 29.5h100v200q0 82 59 141t141 59h300q82 0 141 -59t59 -141v-200h100q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-900q-41 0 -70.5 29.5t-29.5 70.5zM400 800h300v150q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-150z" /> +<glyph unicode="" d="M100 0v1100h100v-1100h-100zM300 400q60 60 127.5 84t127.5 17.5t122 -23t119 -30t110 -11t103 42t91 120.5v500q-40 -81 -101.5 -115.5t-127.5 -29.5t-138 25t-139.5 40t-125.5 25t-103 -29.5t-65 -115.5v-500z" /> +<glyph unicode="" d="M0 275q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 127 70.5 231.5t184.5 161.5t245 57t245 -57t184.5 -161.5t70.5 -231.5v-300q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 116 -49.5 227t-131 192.5t-192.5 131t-227 49.5t-227 -49.5t-192.5 -131t-131 -192.5 t-49.5 -227v-300zM200 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14zM800 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14z" /> +<glyph unicode="" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM688 459l141 141l-141 141l71 71l141 -141l141 141l71 -71l-141 -141l141 -141l-71 -71l-141 141l-141 -141z" /> +<glyph unicode="" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM700 857l69 53q111 -135 111 -310q0 -169 -106 -302l-67 54q86 110 86 248q0 146 -93 257z" /> +<glyph unicode="" d="M0 401v400h300l300 200v-800l-300 200h-300zM702 858l69 53q111 -135 111 -310q0 -170 -106 -303l-67 55q86 110 86 248q0 145 -93 257zM889 951l7 -8q123 -151 123 -344q0 -189 -119 -339l-7 -8l81 -66l6 8q142 178 142 405q0 230 -144 408l-6 8z" /> +<glyph unicode="" d="M0 0h500v500h-200v100h-100v-100h-200v-500zM0 600h100v100h400v100h100v100h-100v300h-500v-600zM100 100v300h300v-300h-300zM100 800v300h300v-300h-300zM200 200v100h100v-100h-100zM200 900h100v100h-100v-100zM500 500v100h300v-300h200v-100h-100v-100h-200v100 h-100v100h100v200h-200zM600 0v100h100v-100h-100zM600 1000h100v-300h200v-300h300v200h-200v100h200v500h-600v-200zM800 800v300h300v-300h-300zM900 0v100h300v-100h-300zM900 900v100h100v-100h-100zM1100 200v100h100v-100h-100z" /> +<glyph unicode="" d="M0 200h100v1000h-100v-1000zM100 0v100h300v-100h-300zM200 200v1000h100v-1000h-100zM500 0v91h100v-91h-100zM500 200v1000h200v-1000h-200zM700 0v91h100v-91h-100zM800 200v1000h100v-1000h-100zM900 0v91h200v-91h-200zM1000 200v1000h200v-1000h-200z" /> +<glyph unicode="" d="M0 700l1 475q0 10 7.5 17.5t17.5 7.5h474l700 -700l-500 -500zM148 953q0 -42 29 -71q30 -30 71.5 -30t71.5 30q29 29 29 71t-29 71q-30 30 -71.5 30t-71.5 -30q-29 -29 -29 -71z" /> +<glyph unicode="" d="M1 700l1 475q0 11 7 18t18 7h474l700 -700l-500 -500zM148 953q0 -42 30 -71q29 -30 71 -30t71 30q30 29 30 71t-30 71q-29 30 -71 30t-71 -30q-30 -29 -30 -71zM701 1200h100l700 -700l-500 -500l-50 50l450 450z" /> +<glyph unicode="" d="M100 0v1025l175 175h925v-1000l-100 -100v1000h-750l-100 -100h750v-1000h-900z" /> +<glyph unicode="" d="M200 0l450 444l450 -443v1150q0 20 -14.5 35t-35.5 15h-800q-21 0 -35.5 -15t-14.5 -35v-1151z" /> +<glyph unicode="" d="M0 100v700h200l100 -200h600l100 200h200v-700h-200v200h-800v-200h-200zM253 829l40 -124h592l62 124l-94 346q-2 11 -10 18t-18 7h-450q-10 0 -18 -7t-10 -18zM281 24l38 152q2 10 11.5 17t19.5 7h500q10 0 19.5 -7t11.5 -17l38 -152q2 -10 -3.5 -17t-15.5 -7h-600 q-10 0 -15.5 7t-3.5 17z" /> +<glyph unicode="" d="M0 200q0 -41 29.5 -70.5t70.5 -29.5h1000q41 0 70.5 29.5t29.5 70.5v600q0 41 -29.5 70.5t-70.5 29.5h-150q-4 8 -11.5 21.5t-33 48t-53 61t-69 48t-83.5 21.5h-200q-41 0 -82 -20.5t-70 -50t-52 -59t-34 -50.5l-12 -20h-150q-41 0 -70.5 -29.5t-29.5 -70.5v-600z M356 500q0 100 72 172t172 72t172 -72t72 -172t-72 -172t-172 -72t-172 72t-72 172zM494 500q0 -44 31 -75t75 -31t75 31t31 75t-31 75t-75 31t-75 -31t-31 -75zM900 700v100h100v-100h-100z" /> +<glyph unicode="" d="M53 0h365v66q-41 0 -72 11t-49 38t1 71l92 234h391l82 -222q16 -45 -5.5 -88.5t-74.5 -43.5v-66h417v66q-34 1 -74 43q-18 19 -33 42t-21 37l-6 13l-385 998h-93l-399 -1006q-24 -48 -52 -75q-12 -12 -33 -25t-36 -20l-15 -7v-66zM416 521l178 457l46 -140l116 -317h-340 z" /> +<glyph unicode="" d="M100 0v89q41 7 70.5 32.5t29.5 65.5v827q0 28 -1 39.5t-5.5 26t-15.5 21t-29 14t-49 14.5v71l471 -1q120 0 213 -88t93 -228q0 -55 -11.5 -101.5t-28 -74t-33.5 -47.5t-28 -28l-12 -7q8 -3 21.5 -9t48 -31.5t60.5 -58t47.5 -91.5t21.5 -129q0 -84 -59 -156.5t-142 -111 t-162 -38.5h-500zM400 200h161q89 0 153 48.5t64 132.5q0 90 -62.5 154.5t-156.5 64.5h-159v-400zM400 700h139q76 0 130 61.5t54 138.5q0 82 -84 130.5t-239 48.5v-379z" /> +<glyph unicode="" d="M200 0v57q77 7 134.5 40.5t65.5 80.5l173 849q10 56 -10 74t-91 37q-6 1 -10.5 2.5t-9.5 2.5v57h425l2 -57q-33 -8 -62 -25.5t-46 -37t-29.5 -38t-17.5 -30.5l-5 -12l-128 -825q-10 -52 14 -82t95 -36v-57h-500z" /> +<glyph unicode="" d="M-75 200h75v800h-75l125 167l125 -167h-75v-800h75l-125 -167zM300 900v300h150h700h150v-300h-50q0 29 -8 48.5t-18.5 30t-33.5 15t-39.5 5.5t-50.5 1h-200v-850l100 -50v-100h-400v100l100 50v850h-200q-34 0 -50.5 -1t-40 -5.5t-33.5 -15t-18.5 -30t-8.5 -48.5h-49z " /> +<glyph unicode="" d="M33 51l167 125v-75h800v75l167 -125l-167 -125v75h-800v-75zM100 901v300h150h700h150v-300h-50q0 29 -8 48.5t-18 30t-33.5 15t-40 5.5t-50.5 1h-200v-650l100 -50v-100h-400v100l100 50v650h-200q-34 0 -50.5 -1t-39.5 -5.5t-33.5 -15t-18.5 -30t-8 -48.5h-50z" /> +<glyph unicode="" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 350q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM0 650q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1000q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 950q0 -20 14.5 -35t35.5 -15h600q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-600q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" /> +<glyph unicode="" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 650q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM200 350q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM200 950q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" /> +<glyph unicode="" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM100 650v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1000q-21 0 -35.5 15 t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM500 950v100q0 21 14.5 35.5t35.5 14.5h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-600 q-21 0 -35.5 15t-14.5 35z" /> +<glyph unicode="" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100 q-21 0 -35.5 15t-14.5 35z" /> +<glyph unicode="" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM300 50v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800 q-21 0 -35.5 15t-14.5 35zM300 650v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 950v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15 h-800q-21 0 -35.5 15t-14.5 35z" /> +<glyph unicode="" d="M-101 500v100h201v75l166 -125l-166 -125v75h-201zM300 0h100v1100h-100v-1100zM500 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35 v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 650q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100 q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100z" /> +<glyph unicode="" d="M1 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 650 q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM801 0v1100h100v-1100 h-100zM934 550l167 -125v75h200v100h-200v75z" /> +<glyph unicode="" d="M0 275v650q0 31 22 53t53 22h750q31 0 53 -22t22 -53v-650q0 -31 -22 -53t-53 -22h-750q-31 0 -53 22t-22 53zM900 600l300 300v-600z" /> +<glyph unicode="" d="M0 44v1012q0 18 13 31t31 13h1112q19 0 31.5 -13t12.5 -31v-1012q0 -18 -12.5 -31t-31.5 -13h-1112q-18 0 -31 13t-13 31zM100 263l247 182l298 -131l-74 156l293 318l236 -288v500h-1000v-737zM208 750q0 56 39 95t95 39t95 -39t39 -95t-39 -95t-95 -39t-95 39t-39 95z " /> +<glyph unicode="" d="M148 745q0 124 60.5 231.5t165 172t226.5 64.5q123 0 227 -63t164.5 -169.5t60.5 -229.5t-73 -272q-73 -114 -166.5 -237t-150.5 -189l-57 -66q-10 9 -27 26t-66.5 70.5t-96 109t-104 135.5t-100.5 155q-63 139 -63 262zM342 772q0 -107 75.5 -182.5t181.5 -75.5 q107 0 182.5 75.5t75.5 182.5t-75.5 182t-182.5 75t-182 -75.5t-75 -181.5z" /> +<glyph unicode="" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM173 600q0 -177 125.5 -302t301.5 -125v854q-176 0 -301.5 -125 t-125.5 -302z" /> +<glyph unicode="" d="M117 406q0 94 34 186t88.5 172.5t112 159t115 177t87.5 194.5q21 -71 57.5 -142.5t76 -130.5t83 -118.5t82 -117t70 -116t50 -125.5t18.5 -136q0 -89 -39 -165.5t-102 -126.5t-140 -79.5t-156 -33.5q-114 6 -211.5 53t-161.5 139t-64 210zM243 414q14 -82 59.5 -136 t136.5 -80l16 98q-7 6 -18 17t-34 48t-33 77q-15 73 -14 143.5t10 122.5l9 51q-92 -110 -119.5 -185t-12.5 -156z" /> +<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5q366 -6 397 -14l-186 -186h-311q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v125l200 200v-225q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM436 341l161 50l412 412l-114 113l-405 -405zM995 1015l113 -113l113 113l-21 85l-92 28z" /> +<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h261l2 -80q-133 -32 -218 -120h-145q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5l200 153v-53q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5 zM423 524q30 38 81.5 64t103 35.5t99 14t77.5 3.5l29 -1v-209l360 324l-359 318v-216q-7 0 -19 -1t-48 -8t-69.5 -18.5t-76.5 -37t-76.5 -59t-62 -88t-39.5 -121.5z" /> +<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q61 0 127 -23l-178 -177h-349q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v69l200 200v-169q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM342 632l283 -284l567 567l-137 137l-430 -431l-146 147z" /> +<glyph unicode="" d="M0 603l300 296v-198h200v200h-200l300 300l295 -300h-195v-200h200v198l300 -296l-300 -300v198h-200v-200h195l-295 -300l-300 300h200v200h-200v-198z" /> +<glyph unicode="" d="M200 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-1100l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" /> +<glyph unicode="" d="M0 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-487l500 487v-1100l-500 488v-488l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" /> +<glyph unicode="" d="M136 550l564 550v-487l500 487v-1100l-500 488v-488z" /> +<glyph unicode="" d="M200 0l900 550l-900 550v-1100z" /> +<glyph unicode="" d="M200 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5t-14.5 -35.5v-800zM600 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" /> +<glyph unicode="" d="M200 150q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v800q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" /> +<glyph unicode="" d="M0 0v1100l500 -487v487l564 -550l-564 -550v488z" /> +<glyph unicode="" d="M0 0v1100l500 -487v487l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438l-500 -488v488z" /> +<glyph unicode="" d="M300 0v1100l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438z" /> +<glyph unicode="" d="M100 250v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5zM100 500h1100l-550 564z" /> +<glyph unicode="" d="M185 599l592 -592l240 240l-353 353l353 353l-240 240z" /> +<glyph unicode="" d="M272 194l353 353l-353 353l241 240l572 -571l21 -22l-1 -1v-1l-592 -591z" /> +<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM300 500h200v-200h200v200h200v200h-200v200h-200v-200h-200v-200z" /> +<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM300 500h600v200h-600v-200z" /> +<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM246 459l213 -213l141 142l141 -142l213 213l-142 141l142 141l-213 212l-141 -141l-141 142l-212 -213l141 -141 z" /> +<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM270 551l276 -277l411 411l-175 174l-236 -236l-102 102z" /> +<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM364 700h143q4 0 11.5 -1t11 -1t6.5 3t3 9t1 11t3.5 8.5t3.5 6t5.5 4t6.5 2.5t9 1.5t9 0.5h11.5h12.5 q19 0 30 -10t11 -26q0 -22 -4 -28t-27 -22q-5 -1 -12.5 -3t-27 -13.5t-34 -27t-26.5 -46t-11 -68.5h200q5 3 14 8t31.5 25.5t39.5 45.5t31 69t14 94q0 51 -17.5 89t-42 58t-58.5 32t-58.5 15t-51.5 3q-50 0 -90.5 -12t-75 -38.5t-53.5 -74.5t-19 -114zM500 300h200v100h-200 v-100z" /> +<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM400 300h400v100h-100v300h-300v-100h100v-200h-100v-100zM500 800h200v100h-200v-100z" /> +<glyph unicode="" d="M0 500v200h195q31 125 98.5 199.5t206.5 100.5v200h200v-200q54 -20 113 -60t112.5 -105.5t71.5 -134.5h203v-200h-203q-25 -102 -116.5 -186t-180.5 -117v-197h-200v197q-140 27 -208 102.5t-98 200.5h-194zM290 500q24 -73 79.5 -127.5t130.5 -78.5v206h200v-206 q149 48 201 206h-201v200h200q-25 74 -75.5 127t-124.5 77v-204h-200v203q-75 -23 -130 -77t-79 -126h209v-200h-210z" /> +<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM356 465l135 135 l-135 135l109 109l135 -135l135 135l109 -109l-135 -135l135 -135l-109 -109l-135 135l-135 -135z" /> +<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM322 537l141 141 l87 -87l204 205l142 -142l-346 -345z" /> +<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -115 62 -215l568 567q-100 62 -216 62q-171 0 -292.5 -121.5t-121.5 -292.5zM391 245q97 -59 209 -59q171 0 292.5 121.5t121.5 292.5 q0 112 -59 209z" /> +<glyph unicode="" d="M0 547l600 453v-300h600v-300h-600v-301z" /> +<glyph unicode="" d="M0 400v300h600v300l600 -453l-600 -448v301h-600z" /> +<glyph unicode="" d="M204 600l450 600l444 -600h-298v-600h-300v600h-296z" /> +<glyph unicode="" d="M104 600h296v600h300v-600h298l-449 -600z" /> +<glyph unicode="" d="M0 200q6 132 41 238.5t103.5 193t184 138t271.5 59.5v271l600 -453l-600 -448v301q-95 -2 -183 -20t-170 -52t-147 -92.5t-100 -135.5z" /> +<glyph unicode="" d="M0 0v400l129 -129l294 294l142 -142l-294 -294l129 -129h-400zM635 777l142 -142l294 294l129 -129v400h-400l129 -129z" /> +<glyph unicode="" d="M34 176l295 295l-129 129h400v-400l-129 130l-295 -295zM600 600v400l129 -129l295 295l142 -141l-295 -295l129 -130h-400z" /> +<glyph unicode="" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM456 851l58 -302q4 -20 21.5 -34.5t37.5 -14.5h54q20 0 37.5 14.5 t21.5 34.5l58 302q4 20 -8 34.5t-32 14.5h-207q-21 0 -33 -14.5t-8 -34.5zM500 300h200v100h-200v-100z" /> +<glyph unicode="" d="M0 800h100v-200h400v300h200v-300h400v200h100v100h-111q1 1 1 6.5t-1.5 15t-3.5 17.5l-34 172q-11 39 -41.5 63t-69.5 24q-32 0 -61 -17l-239 -144q-22 -13 -40 -35q-19 24 -40 36l-238 144q-33 18 -62 18q-39 0 -69.5 -23t-40.5 -61l-35 -177q-2 -8 -3 -18t-1 -15v-6 h-111v-100zM100 0h400v400h-400v-400zM200 900q-3 0 14 48t36 96l18 47l213 -191h-281zM700 0v400h400v-400h-400zM731 900l202 197q5 -12 12 -32.5t23 -64t25 -72t7 -28.5h-269z" /> +<glyph unicode="" d="M0 -22v143l216 193q-9 53 -13 83t-5.5 94t9 113t38.5 114t74 124q47 60 99.5 102.5t103 68t127.5 48t145.5 37.5t184.5 43.5t220 58.5q0 -189 -22 -343t-59 -258t-89 -181.5t-108.5 -120t-122 -68t-125.5 -30t-121.5 -1.5t-107.5 12.5t-87.5 17t-56.5 7.5l-99 -55z M238.5 300.5q19.5 -6.5 86.5 76.5q55 66 367 234q70 38 118.5 69.5t102 79t99 111.5t86.5 148q22 50 24 60t-6 19q-7 5 -17 5t-26.5 -14.5t-33.5 -39.5q-35 -51 -113.5 -108.5t-139.5 -89.5l-61 -32q-369 -197 -458 -401q-48 -111 -28.5 -117.5z" /> +<glyph unicode="" d="M111 408q0 -33 5 -63q9 -56 44 -119.5t105 -108.5q31 -21 64 -16t62 23.5t57 49.5t48 61.5t35 60.5q32 66 39 184.5t-13 157.5q79 -80 122 -164t26 -184q-5 -33 -20.5 -69.5t-37.5 -80.5q-10 -19 -14.5 -29t-12 -26t-9 -23.5t-3 -19t2.5 -15.5t11 -9.5t19.5 -5t30.5 2.5 t42 8q57 20 91 34t87.5 44.5t87 64t65.5 88.5t47 122q38 172 -44.5 341.5t-246.5 278.5q22 -44 43 -129q39 -159 -32 -154q-15 2 -33 9q-79 33 -120.5 100t-44 175.5t48.5 257.5q-13 -8 -34 -23.5t-72.5 -66.5t-88.5 -105.5t-60 -138t-8 -166.5q2 -12 8 -41.5t8 -43t6 -39.5 t3.5 -39.5t-1 -33.5t-6 -31.5t-13.5 -24t-21 -20.5t-31 -12q-38 -10 -67 13t-40.5 61.5t-15 81.5t10.5 75q-52 -46 -83.5 -101t-39 -107t-7.5 -85z" /> +<glyph unicode="" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5t145.5 -23.5t132.5 -59t116.5 -83.5t97 -90t74.5 -85.5t49 -63.5t20 -30l26 -40l-26 -40q-6 -10 -20 -30t-49 -63.5t-74.5 -85.5t-97 -90t-116.5 -83.5t-132.5 -59t-145.5 -23.5 t-145.5 23.5t-132.5 59t-116.5 83.5t-97 90t-74.5 85.5t-49 63.5t-20 30zM120 600q7 -10 40.5 -58t56 -78.5t68 -77.5t87.5 -75t103 -49.5t125 -21.5t123.5 20t100.5 45.5t85.5 71.5t66.5 75.5t58 81.5t47 66q-1 1 -28.5 37.5t-42 55t-43.5 53t-57.5 63.5t-58.5 54 q49 -74 49 -163q0 -124 -88 -212t-212 -88t-212 88t-88 212q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l105 105q-37 24 -75 72t-57 84l-20 36z" /> +<glyph unicode="" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5q61 0 121 -17l37 142h148l-314 -1200h-148l37 143q-82 21 -165 71.5t-140 102t-109.5 112t-72 88.5t-29.5 43zM120 600q210 -282 393 -336l37 141q-107 18 -178.5 101.5t-71.5 193.5 q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l47 47l23 87q-30 28 -59 69t-44 68l-14 26zM780 161l38 145q22 15 44.5 34t46 44t40.5 44t41 50.5t33.5 43.5t33 44t24.5 34q-97 127 -140 175l39 146q67 -54 131.5 -125.5t87.5 -103.5t36 -52l26 -40l-26 -40 q-7 -12 -25.5 -38t-63.5 -79.5t-95.5 -102.5t-124 -100t-146.5 -79z" /> +<glyph unicode="" d="M-97.5 34q13.5 -34 50.5 -34h1294q37 0 50.5 35.5t-7.5 67.5l-642 1056q-20 34 -48 36.5t-48 -29.5l-642 -1066q-21 -32 -7.5 -66zM155 200l445 723l445 -723h-345v100h-200v-100h-345zM500 600l100 -300l100 300v100h-200v-100z" /> +<glyph unicode="" d="M100 262v41q0 20 11 44.5t26 38.5l363 325v339q0 62 44 106t106 44t106 -44t44 -106v-339l363 -325q15 -14 26 -38.5t11 -44.5v-41q0 -20 -12 -26.5t-29 5.5l-359 249v-263q100 -91 100 -113v-64q0 -20 -13 -28.5t-32 0.5l-94 78h-222l-94 -78q-19 -9 -32 -0.5t-13 28.5 v64q0 22 100 113v263l-359 -249q-17 -12 -29 -5.5t-12 26.5z" /> +<glyph unicode="" d="M0 50q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v750h-1100v-750zM0 900h1100v150q0 21 -14.5 35.5t-35.5 14.5h-150v100h-100v-100h-500v100h-100v-100h-150q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 100v100h100v-100h-100zM100 300v100h100v-100h-100z M100 500v100h100v-100h-100zM300 100v100h100v-100h-100zM300 300v100h100v-100h-100zM300 500v100h100v-100h-100zM500 100v100h100v-100h-100zM500 300v100h100v-100h-100zM500 500v100h100v-100h-100zM700 100v100h100v-100h-100zM700 300v100h100v-100h-100zM700 500 v100h100v-100h-100zM900 100v100h100v-100h-100zM900 300v100h100v-100h-100zM900 500v100h100v-100h-100z" /> +<glyph unicode="" d="M0 200v200h259l600 600h241v198l300 -295l-300 -300v197h-159l-600 -600h-341zM0 800h259l122 -122l141 142l-181 180h-341v-200zM678 381l141 142l122 -123h159v198l300 -295l-300 -300v197h-241z" /> +<glyph unicode="" d="M0 400v600q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-596l-304 -300v300h-100q-41 0 -70.5 29.5t-29.5 70.5z" /> +<glyph unicode="" d="M100 600v200h300v-250q0 -113 6 -145q17 -92 102 -117q39 -11 92 -11q37 0 66.5 5.5t50 15.5t36 24t24 31.5t14 37.5t7 42t2.5 45t0 47v25v250h300v-200q0 -42 -3 -83t-15 -104t-31.5 -116t-58 -109.5t-89 -96.5t-129 -65.5t-174.5 -25.5t-174.5 25.5t-129 65.5t-89 96.5 t-58 109.5t-31.5 116t-15 104t-3 83zM100 900v300h300v-300h-300zM800 900v300h300v-300h-300z" /> +<glyph unicode="" d="M-30 411l227 -227l352 353l353 -353l226 227l-578 579z" /> +<glyph unicode="" d="M70 797l580 -579l578 579l-226 227l-353 -353l-352 353z" /> +<glyph unicode="" d="M-198 700l299 283l300 -283h-203v-400h385l215 -200h-800v600h-196zM402 1000l215 -200h381v-400h-198l299 -283l299 283h-200v600h-796z" /> +<glyph unicode="" d="M18 939q-5 24 10 42q14 19 39 19h896l38 162q5 17 18.5 27.5t30.5 10.5h94q20 0 35 -14.5t15 -35.5t-15 -35.5t-35 -14.5h-54l-201 -961q-2 -4 -6 -10.5t-19 -17.5t-33 -11h-31v-50q0 -20 -14.5 -35t-35.5 -15t-35.5 15t-14.5 35v50h-300v-50q0 -20 -14.5 -35t-35.5 -15 t-35.5 15t-14.5 35v50h-50q-21 0 -35.5 15t-14.5 35q0 21 14.5 35.5t35.5 14.5h535l48 200h-633q-32 0 -54.5 21t-27.5 43z" /> +<glyph unicode="" d="M0 0v800h1200v-800h-1200zM0 900v100h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-100h-1200z" /> +<glyph unicode="" d="M1 0l300 700h1200l-300 -700h-1200zM1 400v600h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-200h-1000z" /> +<glyph unicode="" d="M302 300h198v600h-198l298 300l298 -300h-198v-600h198l-298 -300z" /> +<glyph unicode="" d="M0 600l300 298v-198h600v198l300 -298l-300 -297v197h-600v-197z" /> +<glyph unicode="" d="M0 100v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM31 400l172 739q5 22 23 41.5t38 19.5h672q19 0 37.5 -22.5t23.5 -45.5l172 -732h-1138zM800 100h100v100h-100v-100z M1000 100h100v100h-100v-100z" /> +<glyph unicode="" d="M-101 600v50q0 24 25 49t50 38l25 13v-250l-11 5.5t-24 14t-30 21.5t-24 27.5t-11 31.5zM100 500v250v8v8v7t0.5 7t1.5 5.5t2 5t3 4t4.5 3.5t6 1.5t7.5 0.5h200l675 250v-850l-675 200h-38l47 -276q2 -12 -3 -17.5t-11 -6t-21 -0.5h-8h-83q-20 0 -34.5 14t-18.5 35 q-55 337 -55 351zM1100 200v850q0 21 14.5 35.5t35.5 14.5q20 0 35 -14.5t15 -35.5v-850q0 -20 -15 -35t-35 -15q-21 0 -35.5 15t-14.5 35z" /> +<glyph unicode="" d="M74 350q0 21 13.5 35.5t33.5 14.5h18l117 173l63 327q15 77 76 140t144 83l-18 32q-6 19 3 32t29 13h94q20 0 29 -10.5t3 -29.5q-18 -36 -18 -37q83 -19 144 -82.5t76 -140.5l63 -327l118 -173h17q20 0 33.5 -14.5t13.5 -35.5q0 -20 -13 -40t-31 -27q-8 -3 -23 -8.5 t-65 -20t-103 -25t-132.5 -19.5t-158.5 -9q-125 0 -245.5 20.5t-178.5 40.5l-58 20q-18 7 -31 27.5t-13 40.5zM497 110q12 -49 40 -79.5t63 -30.5t63 30.5t39 79.5q-48 -6 -102 -6t-103 6z" /> +<glyph unicode="" d="M21 445l233 -45l-78 -224l224 78l45 -233l155 179l155 -179l45 233l224 -78l-78 224l234 45l-180 155l180 156l-234 44l78 225l-224 -78l-45 233l-155 -180l-155 180l-45 -233l-224 78l78 -225l-233 -44l179 -156z" /> +<glyph unicode="" d="M0 200h200v600h-200v-600zM300 275q0 -75 100 -75h61q124 -100 139 -100h250q46 0 83 57l238 344q29 31 29 74v100q0 44 -30.5 84.5t-69.5 40.5h-328q28 118 28 125v150q0 44 -30.5 84.5t-69.5 40.5h-50q-27 0 -51 -20t-38 -48l-96 -198l-145 -196q-20 -26 -20 -63v-400z M400 300v375l150 213l100 212h50v-175l-50 -225h450v-125l-250 -375h-214l-136 100h-100z" /> +<glyph unicode="" d="M0 400v600h200v-600h-200zM300 525v400q0 75 100 75h61q124 100 139 100h250q46 0 83 -57l238 -344q29 -31 29 -74v-100q0 -44 -30.5 -84.5t-69.5 -40.5h-328q28 -118 28 -125v-150q0 -44 -30.5 -84.5t-69.5 -40.5h-50q-27 0 -51 20t-38 48l-96 198l-145 196 q-20 26 -20 63zM400 525l150 -212l100 -213h50v175l-50 225h450v125l-250 375h-214l-136 -100h-100v-375z" /> +<glyph unicode="" d="M8 200v600h200v-600h-200zM308 275v525q0 17 14 35.5t28 28.5l14 9l362 230q14 6 25 6q17 0 29 -12l109 -112q14 -14 14 -34q0 -18 -11 -32l-85 -121h302q85 0 138.5 -38t53.5 -110t-54.5 -111t-138.5 -39h-107l-130 -339q-7 -22 -20.5 -41.5t-28.5 -19.5h-341 q-7 0 -90 81t-83 94zM408 289l100 -89h293l131 339q6 21 19.5 41t28.5 20h203q16 0 25 15t9 36q0 20 -9 34.5t-25 14.5h-457h-6.5h-7.5t-6.5 0.5t-6 1t-5 1.5t-5.5 2.5t-4 4t-4 5.5q-5 12 -5 20q0 14 10 27l147 183l-86 83l-339 -236v-503z" /> +<glyph unicode="" d="M-101 651q0 72 54 110t139 38l302 -1l-85 121q-11 16 -11 32q0 21 14 34l109 113q13 12 29 12q11 0 25 -6l365 -230q7 -4 17 -10.5t26.5 -26t16.5 -36.5v-526q0 -13 -86 -93.5t-94 -80.5h-341q-16 0 -29.5 20t-19.5 41l-130 339h-107q-84 0 -139 39t-55 111zM-1 601h222 q15 0 28.5 -20.5t19.5 -40.5l131 -339h293l107 89v502l-343 237l-87 -83l145 -184q10 -11 10 -26q0 -11 -5 -20q-1 -3 -3.5 -5.5l-4 -4t-5 -2.5t-5.5 -1.5t-6.5 -1t-6.5 -0.5h-7.5h-6.5h-476v-100zM1000 201v600h200v-600h-200z" /> +<glyph unicode="" d="M97 719l230 -363q4 -6 10.5 -15.5t26 -25t36.5 -15.5h525q13 0 94 83t81 90v342q0 15 -20 28.5t-41 19.5l-339 131v106q0 84 -39 139t-111 55t-110 -53.5t-38 -138.5v-302l-121 84q-15 12 -33.5 11.5t-32.5 -13.5l-112 -110q-22 -22 -6 -53zM172 739l83 86l183 -146 q22 -18 47 -5q3 1 5.5 3.5l4 4t2.5 5t1.5 5.5t1 6.5t0.5 6.5v7.5v6.5v456q0 22 25 31t50 -0.5t25 -30.5v-202q0 -16 20 -29.5t41 -19.5l339 -130v-294l-89 -100h-503zM400 0v200h600v-200h-600z" /> +<glyph unicode="" d="M2 585q-16 -31 6 -53l112 -110q13 -13 32 -13.5t34 10.5l121 85q0 -51 -0.5 -153.5t-0.5 -148.5q0 -84 38.5 -138t110.5 -54t111 55t39 139v106l339 131q20 6 40.5 19.5t20.5 28.5v342q0 7 -81 90t-94 83h-525q-17 0 -35.5 -14t-28.5 -28l-10 -15zM77 565l236 339h503 l89 -100v-294l-340 -130q-20 -6 -40 -20t-20 -29v-202q0 -22 -25 -31t-50 0t-25 31v456v14.5t-1.5 11.5t-5 12t-9.5 7q-24 13 -46 -5l-184 -146zM305 1104v200h600v-200h-600z" /> +<glyph unicode="" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM298 701l2 -201h300l-2 -194l402 294l-402 298v-197h-300z" /> +<glyph unicode="" d="M0 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t231.5 47.5q122 0 232.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-218 -217.5t-300 -80t-299.5 80t-217.5 217.5t-80 299.5zM200 600l402 -294l-2 194h300l2 201h-300v197z" /> +<glyph unicode="" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600h200v-300h200v300h200l-300 400z" /> +<glyph unicode="" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600l300 -400l300 400h-200v300h-200v-300h-200z" /> +<glyph unicode="" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM254 780q-8 -33 5.5 -92.5t7.5 -87.5q0 -9 17 -44t16 -60 q12 0 23 -5.5t23 -15t20 -13.5q24 -12 108 -42q22 -8 53 -31.5t59.5 -38.5t57.5 -11q8 -18 -15 -55t-20 -57q42 -71 87 -80q0 -6 -3 -15.5t-3.5 -14.5t4.5 -17q104 -3 221 112q30 29 47 47t34.5 49t20.5 62q-14 9 -37 9.5t-36 7.5q-14 7 -49 15t-52 19q-9 0 -39.5 -0.5 t-46.5 -1.5t-39 -6.5t-39 -16.5q-50 -35 -66 -12q-4 2 -3.5 25.5t0.5 25.5q-6 13 -26.5 17t-24.5 7q2 22 -2 41t-16.5 28t-38.5 -20q-23 -25 -42 4q-19 28 -8 58q6 16 22 22q6 -1 26 -1.5t33.5 -4t19.5 -13.5q12 -19 32 -37.5t34 -27.5l14 -8q0 3 9.5 39.5t5.5 57.5 q-4 23 14.5 44.5t22.5 31.5q5 14 10 35t8.5 31t15.5 22.5t34 21.5q-6 18 10 37q8 0 23.5 -1.5t24.5 -1.5t20.5 4.5t20.5 15.5q-10 23 -30.5 42.5t-38 30t-49 26.5t-43.5 23q11 39 2 44q31 -13 58 -14.5t39 3.5l11 4q7 36 -16.5 53.5t-64.5 28.5t-56 23q-19 -3 -37 0 q-15 -12 -36.5 -21t-34.5 -12t-44 -8t-39 -6q-15 -3 -45.5 0.5t-45.5 -2.5q-21 -7 -52 -26.5t-34 -34.5q-3 -11 6.5 -22.5t8.5 -18.5q-3 -34 -27.5 -90.5t-29.5 -79.5zM518 916q3 12 16 30t16 25q10 -10 18.5 -10t14 6t14.5 14.5t16 12.5q0 -24 17 -66.5t17 -43.5 q-9 2 -31 5t-36 5t-32 8t-30 14zM692 1003h1h-1z" /> +<glyph unicode="" d="M0 164.5q0 21.5 15 37.5l600 599q-33 101 6 201.5t135 154.5q164 92 306 -9l-259 -138l145 -232l251 126q13 -175 -151 -267q-123 -70 -253 -23l-596 -596q-15 -16 -36.5 -16t-36.5 16l-111 110q-15 15 -15 36.5z" /> +<glyph unicode="" horiz-adv-x="1220" d="M0 196v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 596v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5zM0 996v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM600 596h500v100h-500v-100zM800 196h300v100h-300v-100zM900 996h200v100h-200v-100z" /> +<glyph unicode="" d="M100 1100v100h1000v-100h-1000zM150 1000h900l-350 -500v-300l-200 -200v500z" /> +<glyph unicode="" d="M0 200v200h1200v-200q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 500v400q0 41 29.5 70.5t70.5 29.5h300v100q0 41 29.5 70.5t70.5 29.5h200q41 0 70.5 -29.5t29.5 -70.5v-100h300q41 0 70.5 -29.5t29.5 -70.5v-400h-500v100h-200v-100h-500z M500 1000h200v100h-200v-100z" /> +<glyph unicode="" d="M0 0v400l129 -129l200 200l142 -142l-200 -200l129 -129h-400zM0 800l129 129l200 -200l142 142l-200 200l129 129h-400v-400zM729 329l142 142l200 -200l129 129v-400h-400l129 129zM729 871l200 200l-129 129h400v-400l-129 129l-200 -200z" /> +<glyph unicode="" d="M0 596q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 596q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM291 655 q0 23 15.5 38.5t38.5 15.5t39 -16t16 -38q0 -23 -16 -39t-39 -16q-22 0 -38 16t-16 39zM400 850q0 22 16 38.5t39 16.5q22 0 38 -16t16 -39t-16 -39t-38 -16q-23 0 -39 16.5t-16 38.5zM514 609q0 32 20.5 56.5t51.5 29.5l122 126l1 1q-9 14 -9 28q0 22 16 38.5t39 16.5 q22 0 38 -16t16 -39t-16 -39t-38 -16q-14 0 -29 10l-55 -145q17 -22 17 -51q0 -36 -25.5 -61.5t-61.5 -25.5t-61.5 25.5t-25.5 61.5zM800 655q0 22 16 38t39 16t38.5 -15.5t15.5 -38.5t-16 -39t-38 -16q-23 0 -39 16t-16 39z" /> +<glyph unicode="" d="M-40 375q-13 -95 35 -173q35 -57 94 -89t129 -32q63 0 119 28q33 16 65 40.5t52.5 45.5t59.5 64q40 44 57 61l394 394q35 35 47 84t-3 96q-27 87 -117 104q-20 2 -29 2q-46 0 -78.5 -16.5t-67.5 -51.5l-389 -396l-7 -7l69 -67l377 373q20 22 39 38q23 23 50 23 q38 0 53 -36q16 -39 -20 -75l-547 -547q-52 -52 -125 -52q-55 0 -100 33t-54 96q-5 35 2.5 66t31.5 63t42 50t56 54q24 21 44 41l348 348q52 52 82.5 79.5t84 54t107.5 26.5q25 0 48 -4q95 -17 154 -94.5t51 -175.5q-7 -101 -98 -192l-252 -249l-253 -256l7 -7l69 -60 l517 511q67 67 95 157t11 183q-16 87 -67 154t-130 103q-69 33 -152 33q-107 0 -197 -55q-40 -24 -111 -95l-512 -512q-68 -68 -81 -163z" /> +<glyph unicode="" d="M80 784q0 131 98.5 229.5t230.5 98.5q143 0 241 -129q103 129 246 129q129 0 226 -98.5t97 -229.5q0 -46 -17.5 -91t-61 -99t-77 -89.5t-104.5 -105.5q-197 -191 -293 -322l-17 -23l-16 23q-43 58 -100 122.5t-92 99.5t-101 100q-71 70 -104.5 105.5t-77 89.5t-61 99 t-17.5 91zM250 784q0 -27 30.5 -70t61.5 -75.5t95 -94.5l22 -22q93 -90 190 -201q82 92 195 203l12 12q64 62 97.5 97t64.5 79t31 72q0 71 -48 119.5t-105 48.5q-74 0 -132 -83l-118 -171l-114 174q-51 80 -123 80q-60 0 -109.5 -49.5t-49.5 -118.5z" /> +<glyph unicode="" d="M57 353q0 -95 66 -159l141 -142q68 -66 159 -66q93 0 159 66l283 283q66 66 66 159t-66 159l-141 141q-8 9 -19 17l-105 -105l212 -212l-389 -389l-247 248l95 95l-18 18q-46 45 -75 101l-55 -55q-66 -66 -66 -159zM269 706q0 -93 66 -159l141 -141q7 -7 19 -17l105 105 l-212 212l389 389l247 -247l-95 -96l18 -17q47 -49 77 -100l29 29q35 35 62.5 88t27.5 96q0 93 -66 159l-141 141q-66 66 -159 66q-95 0 -159 -66l-283 -283q-66 -64 -66 -159z" /> +<glyph unicode="" d="M200 100v953q0 21 30 46t81 48t129 38t163 15t162 -15t127 -38t79 -48t29 -46v-953q0 -41 -29.5 -70.5t-70.5 -29.5h-600q-41 0 -70.5 29.5t-29.5 70.5zM300 300h600v700h-600v-700zM496 150q0 -43 30.5 -73.5t73.5 -30.5t73.5 30.5t30.5 73.5t-30.5 73.5t-73.5 30.5 t-73.5 -30.5t-30.5 -73.5z" /> +<glyph unicode="" d="M0 0l303 380l207 208l-210 212h300l267 279l-35 36q-15 14 -15 35t15 35q14 15 35 15t35 -15l283 -282q15 -15 15 -36t-15 -35q-14 -15 -35 -15t-35 15l-36 35l-279 -267v-300l-212 210l-208 -207z" /> +<glyph unicode="" d="M295 433h139q5 -77 48.5 -126.5t117.5 -64.5v335q-6 1 -15.5 4t-11.5 3q-46 14 -79 26.5t-72 36t-62.5 52t-40 72.5t-16.5 99q0 92 44 159.5t109 101t144 40.5v78h100v-79q38 -4 72.5 -13.5t75.5 -31.5t71 -53.5t51.5 -84t24.5 -118.5h-159q-8 72 -35 109.5t-101 50.5 v-307l64 -14q34 -7 64 -16.5t70 -31.5t67.5 -52t47.5 -80.5t20 -112.5q0 -139 -89 -224t-244 -96v-77h-100v78q-152 17 -237 104q-40 40 -52.5 93.5t-15.5 139.5zM466 889q0 -29 8 -51t16.5 -34t29.5 -22.5t31 -13.5t38 -10q7 -2 11 -3v274q-61 -8 -97.5 -37.5t-36.5 -102.5 zM700 237q170 18 170 151q0 64 -44 99.5t-126 60.5v-311z" /> +<glyph unicode="" d="M100 600v100h166q-24 49 -44 104q-10 26 -14.5 55.5t-3 72.5t25 90t68.5 87q97 88 263 88q129 0 230 -89t101 -208h-153q0 52 -34 89.5t-74 51.5t-76 14q-37 0 -79 -14.5t-62 -35.5q-41 -44 -41 -101q0 -28 16.5 -69.5t28 -62.5t41.5 -72h241v-100h-197q8 -50 -2.5 -115 t-31.5 -94q-41 -59 -99 -113q35 11 84 18t70 7q33 1 103 -16t103 -17q76 0 136 30l50 -147q-41 -25 -80.5 -36.5t-59 -13t-61.5 -1.5q-23 0 -128 33t-155 29q-39 -4 -82 -17t-66 -25l-24 -11l-55 145l16.5 11t15.5 10t13.5 9.5t14.5 12t14.5 14t17.5 18.5q48 55 54 126.5 t-30 142.5h-221z" /> +<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM602 900l298 300l298 -300h-198v-900h-200v900h-198z" /> +<glyph unicode="" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v200h100v-100h200v-100h-300zM700 400v100h300v-200h-99v-100h-100v100h99v100h-200zM700 700v500h300v-500h-100v100h-100v-100h-100zM801 900h100v200h-100v-200z" /> +<glyph unicode="" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v500h300v-500h-100v100h-100v-100h-100zM700 700v200h100v-100h200v-100h-300zM700 1100v100h300v-200h-99v-100h-100v100h99v100h-200zM801 200h100v200h-100v-200z" /> +<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 100v400h300v-500h-100v100h-200zM800 1100v100h200v-500h-100v400h-100zM901 200h100v200h-100v-200z" /> +<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 400v100h200v-500h-100v400h-100zM800 800v400h300v-500h-100v100h-200zM901 900h100v200h-100v-200z" /> +<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h500v-200h-500zM700 400v200h400v-200h-400zM700 700v200h300v-200h-300zM700 1000v200h200v-200h-200z" /> +<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h200v-200h-200zM700 400v200h300v-200h-300zM700 700v200h400v-200h-400zM700 1000v200h500v-200h-500z" /> +<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q162 0 281 -118.5t119 -281.5v-300q0 -165 -118.5 -282.5t-281.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500z" /> +<glyph unicode="" d="M0 400v300q0 163 119 281.5t281 118.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-163 0 -281.5 117.5t-118.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM400 300l333 250l-333 250v-500z" /> +<glyph unicode="" d="M0 400v300q0 163 117.5 281.5t282.5 118.5h300q163 0 281.5 -119t118.5 -281v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 700l250 -333l250 333h-500z" /> +<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -162 -118.5 -281t-281.5 -119h-300q-165 0 -282.5 118.5t-117.5 281.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 400h500l-250 333z" /> +<glyph unicode="" d="M0 400v300h300v200l400 -350l-400 -350v200h-300zM500 0v200h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-500v200h400q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-400z" /> +<glyph unicode="" d="M217 519q8 -19 31 -19h302q-155 -438 -160 -458q-5 -21 4 -32l9 -8h9q14 0 26 15q11 13 274.5 321.5t264.5 308.5q14 19 5 36q-8 17 -31 17l-301 -1q1 4 78 219.5t79 227.5q2 15 -5 27l-9 9h-9q-15 0 -25 -16q-4 -6 -98 -111.5t-228.5 -257t-209.5 -237.5q-16 -19 -6 -41 z" /> +<glyph unicode="" d="M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q47 0 100 15v185h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h500v185q-14 4 -114 7.5t-193 5.5l-93 2q-165 0 -282.5 -117.5t-117.5 -282.5v-300zM600 400v300h300v200l400 -350l-400 -350v200h-300z " /> +<glyph unicode="" d="M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q163 0 281.5 117.5t118.5 282.5v98l-78 73l-122 -123v-148q0 -41 -29.5 -70.5t-70.5 -29.5h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h156l118 122l-74 78h-100q-165 0 -282.5 -117.5t-117.5 -282.5 v-300zM496 709l353 342l-149 149h500v-500l-149 149l-342 -353z" /> +<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM406 600 q0 80 57 137t137 57t137 -57t57 -137t-57 -137t-137 -57t-137 57t-57 137z" /> +<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 800l445 -500l450 500h-295v400h-300v-400h-300zM900 150h100v50h-100v-50z" /> +<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 700h300v-300h300v300h295l-445 500zM900 150h100v50h-100v-50z" /> +<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 705l305 -305l596 596l-154 155l-442 -442l-150 151zM900 150h100v50h-100v-50z" /> +<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 988l97 -98l212 213l-97 97zM200 400l697 1l3 699l-250 -239l-149 149l-212 -212l149 -149zM900 150h100v50h-100v-50z" /> +<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM200 612l212 -212l98 97l-213 212zM300 1200l239 -250l-149 -149l212 -212l149 148l249 -237l-1 697zM900 150h100v50h-100v-50z" /> +<glyph unicode="" d="M23 415l1177 784v-1079l-475 272l-310 -393v416h-392zM494 210l672 938l-672 -712v-226z" /> +<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-850q0 -21 -15 -35.5t-35 -14.5h-150v400h-700v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200z" /> +<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-218l-276 -275l-120 120l-126 -127h-378v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM581 306l123 123l120 -120l353 352l123 -123l-475 -476zM600 1000h100v200h-100v-200z" /> +<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-269l-103 -103l-170 170l-298 -298h-329v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200zM700 133l170 170l-170 170l127 127l170 -170l170 170l127 -128l-170 -169l170 -170 l-127 -127l-170 170l-170 -170z" /> +<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-300h-400v-200h-500v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300l300 -300l300 300h-200v300h-200v-300h-200zM600 1000v200h100v-200h-100z" /> +<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-402l-200 200l-298 -298h-402v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300h200v-300h200v300h200l-300 300zM600 1000v200h100v-200h-100z" /> +<glyph unicode="" d="M0 250q0 -21 14.5 -35.5t35.5 -14.5h1100q21 0 35.5 14.5t14.5 35.5v550h-1200v-550zM0 900h1200v150q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 300v200h400v-200h-400z" /> +<glyph unicode="" d="M0 400l300 298v-198h400v-200h-400v-198zM100 800v200h100v-200h-100zM300 800v200h100v-200h-100zM500 800v200h400v198l300 -298l-300 -298v198h-400zM800 300v200h100v-200h-100zM1000 300h100v200h-100v-200z" /> +<glyph unicode="" d="M100 700v400l50 100l50 -100v-300h100v300l50 100l50 -100v-300h100v300l50 100l50 -100v-400l-100 -203v-447q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v447zM800 597q0 -29 10.5 -55.5t25 -43t29 -28.5t25.5 -18l10 -5v-397q0 -21 14.5 -35.5 t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v1106q0 31 -18 40.5t-44 -7.5l-276 -116q-25 -17 -43.5 -51.5t-18.5 -65.5v-359z" /> +<glyph unicode="" d="M100 0h400v56q-75 0 -87.5 6t-12.5 44v394h500v-394q0 -38 -12.5 -44t-87.5 -6v-56h400v56q-4 0 -11 0.5t-24 3t-30 7t-24 15t-11 24.5v888q0 22 25 34.5t50 13.5l25 2v56h-400v-56q75 0 87.5 -6t12.5 -44v-394h-500v394q0 38 12.5 44t87.5 6v56h-400v-56q4 0 11 -0.5 t24 -3t30 -7t24 -15t11 -24.5v-888q0 -22 -25 -34.5t-50 -13.5l-25 -2v-56z" /> +<glyph unicode="" d="M0 300q0 -41 29.5 -70.5t70.5 -29.5h300q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-300q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM100 100h400l200 200h105l295 98v-298h-425l-100 -100h-375zM100 300v200h300v-200h-300zM100 600v200h300v-200h-300z M100 1000h400l200 -200v-98l295 98h105v200h-425l-100 100h-375zM700 402v163l400 133v-163z" /> +<glyph unicode="" d="M16.5 974.5q0.5 -21.5 16 -90t46.5 -140t104 -177.5t175 -208q103 -103 207.5 -176t180 -103.5t137 -47t92.5 -16.5l31 1l163 162q17 18 13.5 41t-22.5 37l-192 136q-19 14 -45 12t-42 -19l-118 -118q-142 101 -268 227t-227 268l118 118q17 17 20 41.5t-11 44.5 l-139 194q-14 19 -36.5 22t-40.5 -14l-162 -162q-1 -11 -0.5 -32.5z" /> +<glyph unicode="" d="M0 50v212q0 20 10.5 45.5t24.5 39.5l365 303v50q0 4 1 10.5t12 22.5t30 28.5t60 23t97 10.5t97 -10t60 -23.5t30 -27.5t12 -24l1 -10v-50l365 -303q14 -14 24.5 -39.5t10.5 -45.5v-212q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-20 0 -35 14.5t-15 35.5zM0 712 q0 -21 14.5 -33.5t34.5 -8.5l202 33q20 4 34.5 21t14.5 38v146q141 24 300 24t300 -24v-146q0 -21 14.5 -38t34.5 -21l202 -33q20 -4 34.5 8.5t14.5 33.5v200q-6 8 -19 20.5t-63 45t-112 57t-171 45t-235 20.5q-92 0 -175 -10.5t-141.5 -27t-108.5 -36.5t-81.5 -40 t-53.5 -36.5t-31 -27.5l-9 -10v-200z" /> +<glyph unicode="" d="M100 0v100h1100v-100h-1100zM175 200h950l-125 150v250l100 100v400h-100v-200h-100v200h-200v-200h-100v200h-200v-200h-100v200h-100v-400l100 -100v-250z" /> +<glyph unicode="" d="M100 0h300v400q0 41 -29.5 70.5t-70.5 29.5h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-400zM500 0v1000q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-1000h-300zM900 0v700q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-700h-300z" /> +<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z" /> +<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h100v200h100v-200h100v500h-100v-200h-100v200h-100v-500zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z" /> +<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v100h-200v300h200v100h-300v-500zM600 300h300v100h-200v300h200v100h-300v-500z" /> +<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 550l300 -150v300zM600 400l300 150l-300 150v-300z" /> +<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300v500h700v-500h-700zM300 400h130q41 0 68 42t27 107t-28.5 108t-66.5 43h-130v-300zM575 549 q0 -65 27 -107t68 -42h130v300h-130q-38 0 -66.5 -43t-28.5 -108z" /> +<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z" /> +<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v400h-200v100h-100v-500zM301 400v200h100v-200h-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z" /> +<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 700v100h300v-300h-99v-100h-100v100h99v200h-200zM201 300v100h100v-100h-100zM601 300v100h100v-100h-100z M700 700v100h200v-500h-100v400h-100z" /> +<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 500v200 l100 100h300v-100h-300v-200h300v-100h-300z" /> +<glyph unicode="" d="M0 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 400v400h300 l100 -100v-100h-100v100h-200v-100h200v-100h-200v-100h-100zM700 400v100h100v-100h-100z" /> +<glyph unicode="" d="M-14 494q0 -80 56.5 -137t135.5 -57h222v300h400v-300h128q120 0 205 86.5t85 207.5t-85 207t-205 86q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200h200v300h200v-300h200 l-300 -300z" /> +<glyph unicode="" d="M-14 494q0 -80 56.5 -137t135.5 -57h8l414 414l403 -403q94 26 154.5 104.5t60.5 178.5q0 120 -85 206.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200l300 300 l300 -300h-200v-300h-200v300h-200z" /> +<glyph unicode="" d="M100 200h400v-155l-75 -45h350l-75 45v155h400l-270 300h170l-270 300h170l-300 333l-300 -333h170l-270 -300h170z" /> +<glyph unicode="" d="M121 700q0 -53 28.5 -97t75.5 -65q-4 -16 -4 -38q0 -74 52.5 -126.5t126.5 -52.5q56 0 100 30v-306l-75 -45h350l-75 45v306q46 -30 100 -30q74 0 126.5 52.5t52.5 126.5q0 24 -9 55q50 32 79.5 83t29.5 112q0 90 -61.5 155.5t-150.5 71.5q-26 89 -99.5 145.5 t-167.5 56.5q-116 0 -197.5 -81.5t-81.5 -197.5q0 -4 1 -11.5t1 -11.5q-14 2 -23 2q-74 0 -126.5 -52.5t-52.5 -126.5z" /> +</font> +</defs></svg> \ No newline at end of file diff --git a/app/assets/fonts/glyphicons-halflings-regular.ttf b/app/assets/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..67fa00bf83801d2fa568546b982c80d27f6ef74e GIT binary patch literal 41280 zcmc${2b>$#wLd<0X4JKkMs=IoY9(#guC%-Ix~!LV@5XgawLzwtVoFRi&4B<;Yzzq| z1QHw)z@da0*@PsIyqA!`6G@b6oWOe_b_$P#@)GbXG2Zd-d+unfZAkvV-{<pRThcUX z?mhS1bI<vnG6E9>LBX3Wc;?Pswd9i3FaAXkSUrx`&zn7GF0<l{z@}h7wrpi4d*Ks{ zZpZ!*_Sc_(*@n#v|6=<B_P;3z;#KEwxh5twNq6GM+Hl@_!R8AuTe&~;1opdee&dB3 zuHKB}9zifaOy@7W^!f`Tkz5@6UloMAr(d*jLqR-mNEd{A4&i+7ML1y3%5Fi}RgL|| zi!Qt7y6=5Y*@^w>_`M^SUUB}0?t9iO6@<@rQX4MYaNTB6W_twTb8q4L*yS58+j!vF z2j3Nh`>lc?ZQXpu)z^G$?&B8=!spQk>+PGb+PGPLztt}YU&eW%<JyhqT`=_QiZ`)O z{War7)DeC><I$he=TDu%_l-|m2S4M&9<>aO!9EjS$4lmWxSf0(+a;I;S#pX$!?81r zPxe(ID}q`APM!R3^`f;)g#n@JcY^fY<DiwPTG)y!c}ptai19rMd-SR|&aq3eM_1Jg zGFy(%<@gm7QCp}IVnVC1CuWajV&}%5Zty0LrC8dQb{~=s*$&klYChC=Xu+X%ht?mu z_|Wx-?mqOuVcX%F!_9~L4$nQj;PAS`7azX<@ZE>+Km6eDgyYBYd&V!e;1`7xevutA z9r7HC9qK$ZaA-Mx@w`Ku58Zlb*I{<pb2xUm^YFmo`G=PuUVnJ=;ad;yI{X0tMRxqZ zj{pAn>&GuRWclsyf4l#;7ri09Ui*6RHTP@wSWT=t=8ZXH=9myY8a)#IAo_0fKca`D z*F~?2UK+h1x;}btbX|01bV+nx^t9+egvQ|i`5yx>jQlJU@$>W=|A&(_6vm%?s-YdZ z;Q!}OV(bZjm;rz1-#tQ;_`j;qrV74A>f+@?>cTDSR3S05S~a&0%~;2e-Lx)tKxMv; z>UNd2#a>sPt?jDVwrIuBoW#0#yDGI^Tpd#fmJh|%fpzVw+(uuGC*n5@{id$Gt`64? z4cEQ9t}YQ*O|3)f+%4<)iFNDnd#1Lkv(9K&&23r(y9;-Z-F4Pkb*g}$v9xK8{LsMY zA#0mgiS=dLRa;x^Cc4QF@cS`UN-jvmR5`U!6_yWe-?)84j5em!#pCPhw)4Fe#va|! zZnVx*=ZWJcj<(n@cz2v_v5abIJ!>cyo0pio;gZ-;tZ<(36Leh_-5IxzZI8{{K6gW6 zdu)4x-!7pFD~8koT#5eCZPkH|w1e-s_?>1Ptd7U)Vh6W_4EWLlv~6{zZD=1ZbGId8 z2P-#E#D*5Ftc$B`-OzS)XhC9oBDQ_O_QVEi33Z3wsXZPV1}}y|p$^c7cTxw?(8S!t zhD+9u?+Ja?*M?4Pzmv$eu#nhpQDe)8rq_KJXZ&sZgaI}%ILH=#(<7WO@OQd+HCi6q zzG5hG9$KFmtiuOO41)3lD~5_fOqg~4V3EZbKGfLxYR$%a-ctNxpiRY5&;@Vp#E_7w zkT-73wkGUcB*ievEJBCIgv|7!MHb)9YG%{FPcKR$HU&+h!zMahw3wx1(~FFb=ajgT z%qfW`HlV-tm%m7{V~3g`k(p2s3i4uku@Dj(1y#tXRXLTFRY#Vo)fv@yP&H*$Z&|fu zwHnqcbawfA;^}-y$tn4eB_4=}ENLa7Skn0dlb+x4d<icm>BA$NM<yN6hxujHC;ajI zI)sHn(JlzBbaM;8xhY#@g1vR$0F!L3Om${d`$1LAs<yH!XNxvFow8YMtwp<yHtm9~ zRjcByt6_2U)A+}Bu{O3#->e@P+tN3)UA)gG`7`p@g}ksuP_r4esa$Nz(oZ#Y*myhQ zydBZ3YRahfIn`WNYqM$~qdLmPfP*d!c&KGlGHRZ;tf8!hquH$5;L+MytLn+B9c9&> z)%sYg){s}cs-;hDSBj2Uwy&>`sF=@n=M(u{Z@xE|4FyAq?hY~0;1VryOWYj5TSU%f z`^BD|*<wyKq℞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 literal 0 HcmV?d00001 diff --git a/app/assets/fonts/glyphicons-halflings-regular.woff b/app/assets/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..8c54182aa5d4d1ab3c9171976b615c1dcb1dc187 GIT binary patch literal 23320 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 literal 0 HcmV?d00001 diff --git a/app/styles/base.sass b/app/styles/base.sass index 2c1ee9309..8c17369f5 100644 --- a/app/styles/base.sass +++ b/app/styles/base.sass @@ -241,3 +241,10 @@ body[lang='ru'], body[lang|='zh'], body[lang='ja'], body[lang='pl'], body[lang=' margin-bottom: 20px .partner-badges display: none + +// point the new glyphicons to the fonts in public + +@font-face + font-family: 'Glyphicons Halflings' + 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") From 91c2f0fa33fa6aaaa919cadf81cdfc108d64e60f Mon Sep 17 00:00:00 2001 From: Nick Winter <livelily@gmail.com> Date: Fri, 11 Apr 2014 17:36:15 -0700 Subject: [PATCH 14/46] Fixed #715, and some misc cleanup. --- app/templates/modal/revert.jade | 3 --- app/views/editor/level/systems_tab_view.coffee | 7 ++++--- app/views/kinds/CocoView.coffee | 1 - app/views/modal/revert_modal.coffee | 7 +++---- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/app/templates/modal/revert.jade b/app/templates/modal/revert.jade index 7226e25d4..28b111337 100644 --- a/app/templates/modal/revert.jade +++ b/app/templates/modal/revert.jade @@ -11,6 +11,3 @@ block modal-body-content | #{model.type()}: #{model.get('name')} td button(value=model.id, data-i18n="editor.revert") Revert - -block modal-footer-content - button.btn(data-dismiss="modal", data-i18n="common.cancel") Cancel \ No newline at end of file diff --git a/app/views/editor/level/systems_tab_view.coffee b/app/views/editor/level/systems_tab_view.coffee index 52d92b969..d04a463ff 100644 --- a/app/views/editor/level/systems_tab_view.coffee +++ b/app/views/editor/level/systems_tab_view.coffee @@ -159,11 +159,12 @@ class LevelSystemNode extends TreemaObjectNode name = "#{@system.get('name')} v#{@system.get('version').major}" @buildValueForDisplaySimply valEl, "#{name}" - onEnterPressed: -> + onEnterPressed: (e) -> + super e Backbone.Mediator.publish 'edit-level-system', original: @data.original, majorVersion: @data.majorVersion - open: -> - super() + open: (depth) -> + super depth cTreema = @childrenTreemas.config if cTreema? and (cTreema.getChildren().length or cTreema.canAddChild()) cTreema.open() diff --git a/app/views/kinds/CocoView.coffee b/app/views/kinds/CocoView.coffee index ed9ad844a..62985ae0f 100644 --- a/app/views/kinds/CocoView.coffee +++ b/app/views/kinds/CocoView.coffee @@ -210,7 +210,6 @@ module.exports = class CocoView extends Backbone.View return unless elem.data('toggle') is 'coco-modal' target = elem.data('target') view = application.router.getView(target, '_modal') # could set up a system for loading cached modals, if told to - console.log "got target", target, "which gave view", view @openModalView(view) openModalView: (modalView, softly=false) -> diff --git a/app/views/modal/revert_modal.coffee b/app/views/modal/revert_modal.coffee index 91358988c..68094c371 100644 --- a/app/views/modal/revert_modal.coffee +++ b/app/views/modal/revert_modal.coffee @@ -5,16 +5,16 @@ CocoModel = require 'models/CocoModel' module.exports = class RevertModal extends ModalView id: 'revert-modal' template: template - + events: 'click #changed-models button': 'onRevertModel' - + onRevertModel: (e) -> id = $(e.target).val() CocoModel.backedUp[id].revert() $(e.target).closest('tr').remove() @reloadOnClose = true - + getRenderData: -> c = super() models = _.values CocoModel.backedUp @@ -23,5 +23,4 @@ module.exports = class RevertModal extends ModalView c onHidden: -> - console.log 'reload?', @reloadOnClose location.reload() if @reloadOnClose From 99b430c1b462732d0439592c0836a308a0b29138 Mon Sep 17 00:00:00 2001 From: Nick Winter <livelily@gmail.com> Date: Fri, 11 Apr 2014 17:55:47 -0700 Subject: [PATCH 15/46] Fixed #714 with a hack. --- app/views/play/level_view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/play/level_view.coffee b/app/views/play/level_view.coffee index ebb380146..63d4507eb 100644 --- a/app/views/play/level_view.coffee +++ b/app/views/play/level_view.coffee @@ -421,7 +421,7 @@ module.exports = class PlayLevelView extends View return if @alreadyLoadedState @alreadyLoadedState = true state = @originalSessionState - if state.frame + if state.frame and @level.get('type') isnt 'ladder' # https://github.com/codecombat/codecombat/issues/714 Backbone.Mediator.publish 'level-set-time', { time: 0, frameOffset: state.frame } if state.selected # TODO: Should also restore selected spell here by saving spellName From f0aa5e1d5e2e31bfedd45a77e40f74c6fc78e988 Mon Sep 17 00:00:00 2001 From: Nick Winter <livelily@gmail.com> Date: Fri, 11 Apr 2014 18:19:52 -0700 Subject: [PATCH 16/46] Fixed #681 by removing full-screen button in Safari. --- app/views/play/level/playback_view.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/views/play/level/playback_view.coffee b/app/views/play/level/playback_view.coffee index a79594429..191a00c22 100644 --- a/app/views/play/level/playback_view.coffee +++ b/app/views/play/level/playback_view.coffee @@ -107,6 +107,9 @@ module.exports = class PlaybackView extends View @hookUpScrubber() @updateMusicButton() $(window).on('resize', @onWindowResize) + ua = navigator.userAgent.toLowerCase() + if /safari/.test(ua) and not /chrome/.test(ua) + @$el.find('.toggle-fullscreen').hide() updatePopupContent: -> @timePopup.updateContent "<h2>#{@timeToString @newTime}</h2>#{@formatTime(@current, @currentTime)}<br/>#{@formatTime(@total, @totalTime)}" From dea1c7607a808c30668bcdf9b383e0422a255c44 Mon Sep 17 00:00:00 2001 From: Nick Winter <livelily@gmail.com> Date: Fri, 11 Apr 2014 19:08:55 -0700 Subject: [PATCH 17/46] Horribly untested improvement for #555 which I will test momentarily. --- scripts/devSetup/directoryController.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/scripts/devSetup/directoryController.py b/scripts/devSetup/directoryController.py index 6585ff5f8..5087db888 100644 --- a/scripts/devSetup/directoryController.py +++ b/scripts/devSetup/directoryController.py @@ -20,23 +20,30 @@ class DirectoryController(object): def bin_directory(self): return self.root_install_directory + def mkdir(self, path): + if os.path.exists(path): + print(u"Skipping creation of " + path + " because it exists.") + else: + os.mkdir(path) + def create_directory_in_tmp(self,subdirectory): - os.mkdir(self.generate_path_for_directory_in_tmp(subdirectory)) + path = self.generate_path_for_directory_in_tmp(subdirectory) + self.mkdir(path) def generate_path_for_directory_in_tmp(self,subdirectory): return self.tmp_directory + os.sep + subdirectory def create_directory_in_bin(self,subdirectory): full_path = self.bin_directory + os.sep + subdirectory - os.mkdir(full_path) + self.mkdir(full_path) def create_base_directories(self): shutil.rmtree(self.root_dir + os.sep + "coco" + os.sep + "node_modules",ignore_errors=True) #just in case try: - if os.path.exists(self.tmp_directory): - self.remove_tmp_directory() - os.mkdir(self.tmp_directory) + if os.path.exists(self.tmp_directory): + self.remove_tmp_directory() + os.mkdir(self.tmp_directory) except: - raise errors.CoCoError(u"There was an error creating the directory structure, do you have correct permissions? Please remove all and start over.") + raise errors.CoCoError(u"There was an error creating the directory structure, do you have correct permissions? Please remove all and start over.") def remove_directories(self): shutil.rmtree(self.bin_directory + os.sep + "node",ignore_errors=True) From 99ead5d193760a62837ea138f785220cf6416850 Mon Sep 17 00:00:00 2001 From: Nick Winter <livelily@gmail.com> Date: Fri, 11 Apr 2014 19:51:20 -0700 Subject: [PATCH 18/46] #223 is done, since goals aren't in scripts, and I just removed some goals-of-scripts stuff. --- app/lib/scripts/defaultScripts.coffee | 36 ------------------- .../editor/level/scripts_tab_view.coffee | 2 -- server/levels/level_schema.coffee | 6 ++-- 3 files changed, 3 insertions(+), 41 deletions(-) delete mode 100644 app/lib/scripts/defaultScripts.coffee diff --git a/app/lib/scripts/defaultScripts.coffee b/app/lib/scripts/defaultScripts.coffee deleted file mode 100644 index 25c851e7b..000000000 --- a/app/lib/scripts/defaultScripts.coffee +++ /dev/null @@ -1,36 +0,0 @@ -module.exports = [ - { - "id": "Add Default Goals", - "channel": "god:new-world-created", - "noteChain": [ - { - "goals": { - "add": [ - { - "name": "Humans Survive", - "id": "humans-survive", - "saveThangs": [ - "humans" - ], - "worldEndsAfter": 3, - "howMany": 1, - "hiddenGoal": true - }, - { - "name": "Ogres Die", - "id": "ogres-die", - "killThangs": [ - "ogres" - ], - "worldEndsAfter": 3, - "hiddenGoal": true - } - ] - } - } - ] - } -] - - -# Could add other default scripts, like not having to redo Victory Playback sequence from scratch every time. diff --git a/app/views/editor/level/scripts_tab_view.coffee b/app/views/editor/level/scripts_tab_view.coffee index 8e06b8a58..380902d27 100644 --- a/app/views/editor/level/scripts_tab_view.coffee +++ b/app/views/editor/level/scripts_tab_view.coffee @@ -3,7 +3,6 @@ template = require 'templates/editor/level/scripts_tab' Level = require 'models/Level' Surface = require 'lib/surface/Surface' nodes = require './treema_nodes' -defaultScripts = require 'lib/scripts/defaultScripts' module.exports = class ScriptsTabView extends View id: "editor-level-scripts-tab-view" @@ -22,7 +21,6 @@ module.exports = class ScriptsTabView extends View @level = e.level @dimensions = @level.dimensions() scripts = $.extend(true, [], @level.get('scripts') ? []) - scripts = _.cloneDeep defaultScripts unless scripts.length treemaOptions = schema: Level.schema.get('properties').scripts data: scripts diff --git a/server/levels/level_schema.coffee b/server/levels/level_schema.coffee index 8d2d60cd3..0c4147730 100644 --- a/server/levels/level_schema.coffee +++ b/server/levels/level_schema.coffee @@ -108,9 +108,9 @@ NoteGroupSchema = c.object {title: "Note Group", description: "A group of notes lock: {title: "Lock", description: "Whether the interface should be locked so that the player's focus is on the script, or specific areas to lock.", type: ['boolean', 'array'], items: {type: 'string', enum: ['surface', 'editor', 'palette', 'hud', 'playback', 'playback-hover', 'level', ]}} letterbox: {type: 'boolean', title: 'Letterbox', description:'Turn letterbox mode on or off. Disables surface and playback controls.'} - goals: c.object {title: "Goals", description: "Add or remove goals for the player to complete in the level."}, - add: c.array {title: "Add", description: "Add these goals."}, GoalSchema - remove: c.array {title: "Remove", description: "Remove these goals."}, GoalSchema + goals: c.object {title: "Goals (Old)", description: "Deprecated. Goals added here have no effect. Add goals in the level settings instead."}, + add: c.array {title: "Add", description: "Deprecated. Goals added here have no effect. Add goals in the level settings instead."}, GoalSchema + remove: c.array {title: "Remove", description: "Deprecated. Goals removed here have no effect. Adjust goals in the level settings instead."}, GoalSchema playback: c.object {title: "Playback", description: "Control the playback of the level."}, playing: {type: 'boolean', title: "Set Playing", description: "Set whether playback is playing or paused."} From 8ea2a800882209621a4999c7d823670ed1c8bd1e Mon Sep 17 00:00:00 2001 From: Nick Winter <livelily@gmail.com> Date: Fri, 11 Apr 2014 19:58:52 -0700 Subject: [PATCH 19/46] Fixed #555. Fixed #350. --- scripts/devSetup/mongo.py | 5 ++++- scripts/devSetup/node.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/devSetup/mongo.py b/scripts/devSetup/mongo.py index 88de736af..e653219e1 100644 --- a/scripts/devSetup/mongo.py +++ b/scripts/devSetup/mongo.py @@ -38,7 +38,10 @@ class MongoDB(Dependency): def install_dependencies(self): install_directory = self.config.directory.bin_directory + os.sep + u"mongo" import shutil - shutil.copytree(self.findUnzippedMongoBinPath(),install_directory) + if os.path.exists(install_directory): + print(u"Skipping creation of " + install_directory + " because it exists.") + else: + shutil.copytree(self.findUnzippedMongoBinPath(),install_directory) def findUnzippedMongoBinPath(self): return self.downloader.download_directory + os.sep + \ diff --git a/scripts/devSetup/node.py b/scripts/devSetup/node.py index 065634aad..35d1daf30 100644 --- a/scripts/devSetup/node.py +++ b/scripts/devSetup/node.py @@ -49,7 +49,10 @@ class Node(Dependency): print("Copying node into /usr/local/bin/...") shutil.copy(unzipped_node_path + os.sep + "bin" + os.sep + "node","/usr/local/bin/") os.chmod("/usr/local/bin/node",S_IRWXG|S_IRWXO|S_IRWXU) - shutil.copytree(self.findUnzippedNodePath(),install_directory) + if os.path.exists(install_directory): + print(u"Skipping creation of " + install_directory + " because it exists.") + else: + shutil.copytree(self.findUnzippedNodePath(),install_directory) wants_to_upgrade = True if self.check_if_executable_installed(u"npm"): warning_string = u"A previous version of npm has been found. \nYou may experience problems if you have a version of npm that's too old.Would you like to upgrade?(y/n) " From 137d2b0fd4b50f3e206b0bc46b6ffe11c112dcc1 Mon Sep 17 00:00:00 2001 From: Nick Winter <livelily@gmail.com> Date: Fri, 11 Apr 2014 20:37:06 -0700 Subject: [PATCH 20/46] Fixed #419 to not redo node/mongo downloads when rerunning the script. --- scripts/devSetup/factories.py | 8 +++++--- scripts/devSetup/mongo.py | 12 ++++++++---- scripts/devSetup/node.py | 11 ++++++++--- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/scripts/devSetup/factories.py b/scripts/devSetup/factories.py index f14f922ec..fa82e1abe 100644 --- a/scripts/devSetup/factories.py +++ b/scripts/devSetup/factories.py @@ -37,10 +37,12 @@ class SetupFactory(object): try: mongo_version_string = subprocess.check_output("mongod --version",shell=True) mongo_version_string = mongo_version_string.decode(encoding='UTF-8') - except: - print("Mongod not found.") + except Exception, e: + print("Mongod not found: %s"%e) if "v2.6." not in mongo_version_string: - print("MongoDB not found, so installing...") + if mongo_version_string: + print("Had MongoDB version: %s"%mongo_version_string) + print("MongoDB not found, so installing a local copy...") self.mongo.download_dependencies() self.mongo.install_dependencies() self.node.download_dependencies() diff --git a/scripts/devSetup/mongo.py b/scripts/devSetup/mongo.py index e653219e1..eb57ea452 100644 --- a/scripts/devSetup/mongo.py +++ b/scripts/devSetup/mongo.py @@ -8,7 +8,7 @@ import os from configuration import Configuration from dependency import Dependency import sys - +import shutil class MongoDB(Dependency): def __init__(self,configuration): @@ -32,12 +32,16 @@ class MongoDB(Dependency): def bashrc_string(self): return "COCO_MONGOD_PATH=" + self.config.directory.bin_directory + os.sep + u"mongo" + os.sep +"bin" + os.sep + "mongod" + def download_dependencies(self): - self.downloader.download() - self.downloader.decompress() + install_directory = self.config.directory.bin_directory + os.sep + u"mongo" + if os.path.exists(install_directory): + print(u"Skipping MongoDB download because " + install_directory + " exists.") + else: + self.downloader.download() + self.downloader.decompress() def install_dependencies(self): install_directory = self.config.directory.bin_directory + os.sep + u"mongo" - import shutil if os.path.exists(install_directory): print(u"Skipping creation of " + install_directory + " because it exists.") else: diff --git a/scripts/devSetup/node.py b/scripts/devSetup/node.py index 35d1daf30..8fb1265d8 100644 --- a/scripts/devSetup/node.py +++ b/scripts/devSetup/node.py @@ -37,21 +37,26 @@ class Node(Dependency): return self.config.directory.bin_directory def download_dependencies(self): - self.downloader.download() - self.downloader.decompress() + install_directory = self.config.directory.bin_directory + os.sep + u"node" + if os.path.exists(install_directory): + print(u"Skipping Node download because " + install_directory + " exists.") + else: + self.downloader.download() + self.downloader.decompress() def bashrc_string(self): return "COCO_NODE_PATH=" + self.config.directory.bin_directory + os.sep + u"node" + os.sep + "bin" + os.sep +"node" def install_dependencies(self): install_directory = self.config.directory.bin_directory + os.sep + u"node" #check for node here - unzipped_node_path = self.findUnzippedNodePath() if self.config.system.operating_system in ["mac","linux"] and not which("node"): + unzipped_node_path = self.findUnzippedNodePath() print("Copying node into /usr/local/bin/...") shutil.copy(unzipped_node_path + os.sep + "bin" + os.sep + "node","/usr/local/bin/") os.chmod("/usr/local/bin/node",S_IRWXG|S_IRWXO|S_IRWXU) if os.path.exists(install_directory): print(u"Skipping creation of " + install_directory + " because it exists.") else: + unzipped_node_path = self.findUnzippedNodePath() shutil.copytree(self.findUnzippedNodePath(),install_directory) wants_to_upgrade = True if self.check_if_executable_installed(u"npm"): From fc9d30ccaea24c5587e18e62859336afb0cb3095 Mon Sep 17 00:00:00 2001 From: Nick Winter <livelily@gmail.com> Date: Fri, 11 Apr 2014 20:38:34 -0700 Subject: [PATCH 21/46] Smarter fix for #810, since not all 24-character strings are like MongoDB ids. --- app/models/CocoModel.coffee | 2 +- server/plugins/plugins.coffee | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee index 862ba72fd..f9652fd3a 100644 --- a/app/models/CocoModel.coffee +++ b/app/models/CocoModel.coffee @@ -194,7 +194,7 @@ class CocoModel extends Backbone.Model return model @isObjectID: (s) -> - s.length is 24 and s.match(/[a-z0-9]/gi)?.length is 24 + s.length is 24 and s.match(/[a-f0-9]/gi)?.length is 24 hasReadAccess: (actor) -> # actor is a User object diff --git a/server/plugins/plugins.coffee b/server/plugins/plugins.coffee index f1f224b82..4ca4667d2 100644 --- a/server/plugins/plugins.coffee +++ b/server/plugins/plugins.coffee @@ -22,13 +22,11 @@ module.exports.NamedPlugin = (schema) -> schema.methods.checkSlugConflicts = (done) -> slug = @get('slug') - try - id = mongoose.Types.ObjectId.createFromHexString(slug) + if slug.length is 24 and slug.match(/[a-f0-9]/gi)?.length is 24 err = new Error('Bad name.') - err.response = {message:'cannot be like a MondoDB id, Mr Hacker.', property:'name'} + err.response = {message: 'cannot be like a MongoDB ID, Mr. Hacker.', property: 'name'} err.code = 422 done(err) - catch e query = { slug:slug } From b7f3d5310e2f695b12c8bb2177cacbc891d6c5c5 Mon Sep 17 00:00:00 2001 From: Nick Winter <livelily@gmail.com> Date: Fri, 11 Apr 2014 20:48:42 -0700 Subject: [PATCH 22/46] #353 is fixed as far as I can tell; python3 and python2 both run setup. --- scripts/devSetup/factories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/devSetup/factories.py b/scripts/devSetup/factories.py index fa82e1abe..1eab847bb 100644 --- a/scripts/devSetup/factories.py +++ b/scripts/devSetup/factories.py @@ -37,7 +37,7 @@ class SetupFactory(object): try: mongo_version_string = subprocess.check_output("mongod --version",shell=True) mongo_version_string = mongo_version_string.decode(encoding='UTF-8') - except Exception, e: + except Exception as e: print("Mongod not found: %s"%e) if "v2.6." not in mongo_version_string: if mongo_version_string: From 34bf484bf21cd7c8b8157a41ed26bc160dd760e9 Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Fri, 11 Apr 2014 21:11:52 -0700 Subject: [PATCH 23/46] Added data merge conflict handling. --- app/lib/deltas.coffee | 189 ++++++++++++++++++-------- app/models/CocoModel.coffee | 20 ++- app/styles/editor/delta.sass | 34 ++--- app/styles/editor/patch.sass | 3 + app/styles/modal/save_version.sass | 2 +- app/templates/editor/delta.jade | 76 ++++++----- app/templates/editor/patch_modal.jade | 20 +++ app/templates/editor/patches.jade | 3 + app/templates/editor/thang/edit.jade | 8 +- app/views/editor/delta.coffee | 91 +++++++------ app/views/editor/patch_modal.coffee | 42 ++++++ app/views/editor/patches_view.coffee | 7 + app/views/kinds/CocoView.coffee | 7 +- server/patches/Patch.coffee | 1 - 14 files changed, 346 insertions(+), 157 deletions(-) create mode 100644 app/styles/editor/patch.sass create mode 100644 app/templates/editor/patch_modal.jade create mode 100644 app/views/editor/patch_modal.coffee diff --git a/app/lib/deltas.coffee b/app/lib/deltas.coffee index 954af00ba..0782231eb 100644 --- a/app/lib/deltas.coffee +++ b/app/lib/deltas.coffee @@ -1,75 +1,158 @@ -# path: an array of indexes to navigate into a JSON object -# left: +### + Good-to-knows: + dataPath: an array of keys that walks you up a JSON object that's being patched + ex: ['scripts', 0, 'description'] + deltaPath: an array of keys that walks you up a JSON Diff Patch object. + ex: ['scripts', '_0', 'description'] +### + +module.exports.expandDelta = (delta, left, schema) -> + flattenedDeltas = flattenDelta(delta) + (expandFlattenedDelta(fd, left, schema) for fd in flattenedDeltas) + -module.exports.interpretDelta = (delta, path, left, schema) -> - # takes a single delta and converts into an object that can be +flattenDelta = (delta, dataPath=null, deltaPath=null) -> + # takes a single jsondiffpatch delta and returns an array of objects with + return [] unless delta + dataPath ?= [] + deltaPath ?= [] + return [{dataPath:dataPath, deltaPath: deltaPath, o:delta}] if _.isArray delta + + results = [] + affectingArray = delta._t is 'a' + for deltaIndex, childDelta of delta + continue if deltaIndex is '_t' + dataIndex = if affectingArray then parseInt(deltaIndex.replace('_', '')) else deltaIndex + results = results.concat flattenDelta( + childDelta, dataPath.concat([dataIndex]), deltaPath.concat([deltaIndex])) + results + + +expandFlattenedDelta = (delta, left, schema) -> + # takes a single flattened delta and converts into an object that can be # easily formatted into something human readable. + + delta.action = '???' + o = delta.o # the raw jsondiffpatch delta - betterDelta = { action:'???', delta: delta } + if _.isArray(o) and o.length is 1 + delta.action = 'added' + delta.newValue = o[0] - if _.isArray(delta) and delta.length is 1 - betterDelta.action = 'added' - betterDelta.newValue = delta[0] + if _.isArray(o) and o.length is 2 + delta.action = 'modified' + delta.oldValue = o[0] + delta.newValue = o[1] - if _.isArray(delta) and delta.length is 2 - betterDelta.action = 'modified' - betterDelta.oldValue = delta[0] - betterDelta.newValue = delta[1] + if _.isArray(o) and o.length is 3 and o[1] is 0 and o[2] is 0 + delta.action = 'deleted' + delta.oldValue = o[0] - if _.isArray(delta) and delta.length is 3 and delta[1] is 0 and delta[2] is 0 - betterDelta.action = 'deleted' - betterDelta.oldValue = delta[0] + if _.isPlainObject(o) and o._t is 'a' + delta.action = 'modified-array' - if _.isPlainObject(delta) and delta._t is 'a' - betterDelta.action = 'modified-array' + if _.isPlainObject(o) and o._t isnt 'a' + delta.action = 'modified-object' - if _.isPlainObject(delta) and delta._t isnt 'a' - betterDelta.action = 'modified-object' + if _.isArray(o) and o.length is 3 and o[1] is 0 and o[2] is 3 + delta.action = 'moved-index' + delta.destinationIndex = o[1] + delta.originalIndex = delta.dataPath[delta.dataPath.length-1] - if _.isArray(delta) and delta.length is 3 and delta[1] is 0 and delta[2] is 3 - betterDelta.action = 'moved-index' - betterDelta.destinationIndex = delta[1] + if _.isArray(o) and o.length is 3 and o[1] is 0 and o[2] is 2 + delta.action = 'text-diff' + delta.unidiff = o[0] - if _.isArray(delta) and delta.length is 3 and delta[1] is 0 and delta[2] is 2 - betterDelta.action = 'text-diff' - betterDelta.unidiff = delta[0] - - betterPath = [] + humanPath = [] parentLeft = left parentSchema = schema - for key, i in path - # TODO: A smarter way of getting child schemas + for key, i in delta.dataPath + # TODO: A more comprehensive way of getting child schemas childSchema = parentSchema?.items or parentSchema?.properties?[key] or {} childLeft = parentLeft?[key] - betterKey = null - childData = if i is path.length-1 and betterDelta.action is 'added' then delta[0] else childLeft - betterKey ?= childData.name or childData.id if childData - betterKey ?= "#{childSchema.title} ##{key+1}" if childSchema.title and _.isNumber(key) - betterKey ?= "#{childSchema.title}" if childSchema.title - betterKey ?= _.string.titleize key - betterPath.push betterKey + humanKey = null + childData = if i is delta.dataPath.length-1 and delta.action is 'added' then o[0] else childLeft + humanKey ?= childData.name or childData.id if childData + humanKey ?= "#{childSchema.title} ##{key+1}" if childSchema.title and _.isNumber(key) + humanKey ?= "#{childSchema.title}" if childSchema.title + humanKey ?= _.string.titleize key + humanPath.push humanKey parentLeft = childLeft parentSchema = childSchema - betterDelta.path = betterPath.join(' :: ') - betterDelta.schema = childSchema - betterDelta.left = childLeft - betterDelta.right = jsondiffpatch.patch childLeft, delta unless betterDelta.action is 'moved-index' + delta.humanPath = humanPath.join(' :: ') + delta.schema = childSchema + delta.left = childLeft + delta.right = jsondiffpatch.patch childLeft, delta.o unless delta.action is 'moved-index' - betterDelta + delta -module.exports.flattenDelta = flattenDelta = (delta, path=null) -> - # takes a single delta and returns an array of deltas - return [] unless delta +module.exports.makeJSONDiffer = -> + hasher = (obj) -> obj.name || obj.id || obj._id || JSON.stringify(_.keys(obj)) + jsondiffpatch.create({objectHash:hasher}) + +module.exports.getConflicts = (headDeltas, pendingDeltas) -> + # headDeltas and pendingDeltas should be lists of deltas returned by interpretDelta + # Returns a list of conflict objects with properties: + # headDelta + # pendingDelta + # The deltas that have conflicts also have conflict properties pointing to one another. - path ?= [] + headPathMap = groupDeltasByAffectingPaths(headDeltas) + pendingPathMap = groupDeltasByAffectingPaths(pendingDeltas) + paths = _.keys(headPathMap).concat(_.keys(pendingPathMap)) - return [{path:path, delta:delta}] if _.isArray delta + # Here's my thinking: + # A) Conflicts happen when one delta path is a substring of another delta path + # B) A delta from one self-consistent group cannot conflict with another + # So, sort the paths, which will naturally make conflicts adjacent, + # and if one is identified, one path is from the headDeltas, the other is from pendingDeltas + # This is all to avoid an O(nm) brute force search. - results = [] - affectingArray = delta._t is 'a' - for index, childDelta of delta - continue if index is '_t' - index = parseInt(index.replace('_', '')) if affectingArray - results = results.concat flattenDelta(childDelta, path.concat([index])) - results \ No newline at end of file + conflicts = [] + paths.sort() + for path, i in paths + continue if i + 1 is paths.length + nextPath = paths[i+1] + if nextPath.startsWith path + headDelta = (headPathMap[path] or headPathMap[nextPath])[0].delta + pendingDelta = (pendingPathMap[path] or pendingPathMap[nextPath])[0].delta + conflicts.push({headDelta:headDelta, pendingDelta:pendingDelta}) + pendingDelta.conflict = headDelta + headDelta.conflict = pendingDelta + + return conflicts if conflicts.length + +groupDeltasByAffectingPaths = (deltas) -> + metaDeltas = [] + for delta in deltas + conflictPaths = [] + if delta.action is 'moved-index' + # every other action affects just the data path, but moved indexes affect a swath + indices = [delta.originalIndex, delta.destinationIndex] + indices.sort() + for index in _.range(indices[0], indices[1]+1) + conflictPaths.push delta.dataPath.slice(0, delta.dataPath.length-1).concat(index) + else + conflictPaths.push delta.dataPath + for path in conflictPaths + metaDeltas.push { + delta: delta + path: (item.toString() for item in path).join('/') + } + _.groupBy metaDeltas, 'path' + +module.exports.pruneConflictsFromDelta = (delta, conflicts) -> + # the jsondiffpatch delta mustn't include any dangling nodes, + # or else things will get removed which shouldn't be, or errors will occur + for conflict in conflicts + prunePath delta, conflict.pendingDelta.deltaPath + if _.isEmpty delta then undefined else delta + +prunePath = (delta, path) -> + if path.length is 1 + delete delta[path] + else + prunePath delta[path[0]], path.slice(1) + keys = (k for k in _.keys(delta[path[0]]) when k isnt '_t') + delete delta[path[0]] if keys.length is 0 \ No newline at end of file diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee index de1695490..59820f703 100644 --- a/app/models/CocoModel.coffee +++ b/app/models/CocoModel.coffee @@ -31,6 +31,12 @@ class CocoModel extends Backbone.Model type: -> @constructor.className + + clone: (withChanges=true) -> + # Backbone does not support nested documents + clone = super() + clone.set($.extend(true, {}, if withChanges then @attributes else @_revertAttributes)) + clone onLoaded: -> @loaded = true @@ -223,14 +229,16 @@ class CocoModel extends Backbone.Model return false getDelta: -> - jsd = jsondiffpatch.create({ - objectHash: (obj) -> obj.name || obj.id || obj._id || JSON.stringify(_.keys(obj)) - }) - jsd.diff @_revertAttributes, @attributes + differ = deltasLib.makeJSONDiffer() + differ.diff @_revertAttributes, @attributes + + applyDelta: (delta) -> + newAttributes = $.extend(true, {}, @attributes) + jsondiffpatch.patch newAttributes, delta + @set newAttributes getExpandedDelta: -> delta = @getDelta() - deltas = deltasLib.flattenDelta(delta) - (deltasLib.interpretDelta(d.delta, d.path, @_revertAttributes, @schema().attributes) for d in deltas) + deltasLib.expandDelta(delta, @_revertAttributes, @schema().attributes) module.exports = CocoModel diff --git a/app/styles/editor/delta.sass b/app/styles/editor/delta.sass index f41da3667..013478efb 100644 --- a/app/styles/editor/delta.sass +++ b/app/styles/editor/delta.sass @@ -1,4 +1,4 @@ -.delta-list-view +.delta-view .panel-heading font-size: 13px padding: 4px @@ -7,37 +7,37 @@ .delta-added border-color: green - strong - color: green - .panel-heading + > .panel-heading background-color: lighten(green, 70%) + strong + color: green .delta-modified border-color: darkgoldenrod - strong - color: darkgoldenrod - .panel-heading + > .panel-heading background-color: lighten(darkgoldenrod, 40%) + strong + color: darkgoldenrod .delta-text-diff border-color: blue - strong - color: blue - .panel-heading + > .panel-heading background-color: lighten(blue, 45%) + strong + color: blue table width: 100% .delta-deleted border-color: red - strong - color: red - .panel-heading + > .panel-heading background-color: lighten(red, 42%) + strong + color: red .delta-moved-index border-color: darkslategray - strong - color: darkslategray - .panel-heading - background-color: lighten(darkslategray, 60%) \ No newline at end of file + > .panel-heading + background-color: lighten(darkslategray, 60%) + strong + color: darkslategray diff --git a/app/styles/editor/patch.sass b/app/styles/editor/patch.sass new file mode 100644 index 000000000..3296d946c --- /dev/null +++ b/app/styles/editor/patch.sass @@ -0,0 +1,3 @@ +#patch-modal + .modal-body + padding: 10px \ No newline at end of file diff --git a/app/styles/modal/save_version.sass b/app/styles/modal/save_version.sass index e7ab79751..66de28a29 100644 --- a/app/styles/modal/save_version.sass +++ b/app/styles/modal/save_version.sass @@ -33,7 +33,7 @@ font-size: 0.9em font-style: italic - .delta-list-view + .delta-view overflow-y: auto padding: 10px border: 1px solid black diff --git a/app/templates/editor/delta.jade b/app/templates/editor/delta.jade index 961483324..480e4ef01 100644 --- a/app/templates/editor/delta.jade +++ b/app/templates/editor/delta.jade @@ -1,36 +1,46 @@ - var i = 0 + +mixin deltaPanel(delta, conflict) + - delta.index = i++ + .delta.panel.panel-default(class='delta-'+delta.action, data-index=i) + .panel-heading + if delta.action === 'added' + strong(data-i18n="delta.added") Added + if delta.action === 'modified' + strong(data-i18n="delta.modified") Modified + if delta.action === 'deleted' + strong(data-i18n="delta.deleted") Deleted + if delta.action === 'moved-index' + strong(data-i18n="delta.modified_array") Moved Index + if delta.action === 'text-diff' + strong(data-i18n="delta.text_diff") Text Diff + span + a(data-toggle="collapse" data-parent="#delta-accordion"+(counter) href="#collapse-"+(i+counter)) + span= delta.humanPath + + .panel-collapse.collapse(id="collapse-"+(i+counter)) + .panel-body.row(class=conflict ? "conflict-details" : "details") + if delta.action === 'added' + .new-value.col-md-12= delta.right + if delta.action === 'modified' + .old-value.col-md-6= delta.left + .new-value.col-md-6= delta.right + if delta.action === 'deleted' + .col-md-12 + div.old-value= delta.left + if delta.action === 'text-diff' + .col-md-12 + div.text-diff + if delta.action === 'moved-index' + .col-md-12 + span Moved array value #{JSON.stringify(delta.left)} to index #{delta.destinationIndex} + + if delta.conflict && !conflict + .panel-body + strong MERGE CONFLICT WITH + +deltaPanel(delta.conflict, true) + .panel-group(id='delta-accordion-'+(counter)) for delta in deltas - .delta.panel.panel-default(class='delta-'+delta.action) - .panel-heading - if delta.action === 'added' - strong(data-i18n="delta.added") Added - if delta.action === 'modified' - strong(data-i18n="delta.modified") Modified - if delta.action === 'deleted' - strong(data-i18n="delta.deleted") Deleted - if delta.action === 'moved-index' - strong(data-i18n="delta.modified_array") Moved Index - if delta.action === 'text-diff' - strong(data-i18n="delta.text_diff") Text Diff - span - a(data-toggle="collapse" data-parent="#delta-accordion"+(counter) href="#collapse-"+(i+counter)) - span= delta.path - - .panel-collapse.collapse(id="collapse-"+(i+counter)) - .panel-body.row - if delta.action === 'added' - .new-value.col-md-12= delta.right - if delta.action === 'modified' - .old-value.col-md-6= delta.left - .new-value.col-md-6= delta.right - if delta.action === 'deleted' - .col-md-12 - div.old-value= delta.left - if delta.action === 'text-diff' - .col-md-12 - div.text-diff - if delta.action === 'moved-index' - .col-md-12 - span Moved array value #{JSON.stringify(delta.left)} to index #{delta.destinationIndex} - - i += 1 \ No newline at end of file + +deltaPanel(delta) + \ No newline at end of file diff --git a/app/templates/editor/patch_modal.jade b/app/templates/editor/patch_modal.jade new file mode 100644 index 000000000..4b094cd81 --- /dev/null +++ b/app/templates/editor/patch_modal.jade @@ -0,0 +1,20 @@ +extends /templates/modal/modal_base + +block modal-header-content + .modal-header-content + h3 Patch + +block modal-body-content + .modal-body + .changes-stub + + +block modal-footer + .modal-footer + button(data-dismiss="modal", data-i18n="common.cancel").btn Cancel + if canReject + button.btn.btn-danger Reject + if canWithdraw + button.btn.btn-danger Withdraw + if canAccept + button.btn.btn-primary Accept \ No newline at end of file diff --git a/app/templates/editor/patches.jade b/app/templates/editor/patches.jade index ce3b1af84..872788e7d 100644 --- a/app/templates/editor/patches.jade +++ b/app/templates/editor/patches.jade @@ -20,8 +20,11 @@ else th Submitter th Submitted th Commit Message + th Review for patch in patches tr td= patch.userName td= moment(patch.get('created')).format('llll') td= patch.get('commitMessage') + td + span.glyphicon.glyphicon-wrench(data-patch-id=patch.id).patch-icon diff --git a/app/templates/editor/thang/edit.jade b/app/templates/editor/thang/edit.jade index 04486aba1..b751d6de8 100644 --- a/app/templates/editor/thang/edit.jade +++ b/app/templates/editor/thang/edit.jade @@ -19,7 +19,7 @@ block content h3 Edit Thang Type: "#{thangType.attributes.name}" ul.nav.nav-tabs - li.active + li a(href="#editor-thang-main-tab-view", data-toggle="tab") Main li a(href="#editor-thang-components-tab-view", data-toggle="tab") Components @@ -27,13 +27,13 @@ block content a(href="#editor-thang-spritesheets-view", data-toggle="tab") Spritesheets li a(href="#editor-thang-colors-tab-view", data-toggle="tab")#color-tab Colors - li + li.active a(href="#editor-thang-patches-view", data-toggle="tab")#patches-tab Patches div.tab-content div.tab-pane#editor-thang-colors-tab-view - div.tab-pane.active#editor-thang-main-tab-view + div.tab-pane#editor-thang-main-tab-view div.main-area.well div.file-controls @@ -86,7 +86,7 @@ block content div#spritesheets - div.tab-pane#editor-thang-patches-view + div.tab-pane#editor-thang-patches-view.active div.patches-view diff --git a/app/views/editor/delta.coffee b/app/views/editor/delta.coffee index 4d4635ebf..09c0981a6 100644 --- a/app/views/editor/delta.coffee +++ b/app/views/editor/delta.coffee @@ -1,57 +1,70 @@ CocoView = require 'views/kinds/CocoView' template = require 'templates/editor/delta' -deltaLib = require 'lib/deltas' +deltasLib = require 'lib/deltas' -module.exports = class DeltaListView extends CocoView +TEXTDIFF_OPTIONS = + baseTextName: "Old" + newTextName: "New" + contextSize: 5 + viewType: 1 + +module.exports = class DeltaView extends CocoView @deltaCounter: 0 - className: "delta-list-view" + className: "delta-view" template: template constructor: (options) -> super(options) @model = options.model + @headModel = options.headModel + @expandedDeltas = @model.getExpandedDelta() + if @headModel + @headDeltas = @headModel.getExpandedDelta() + @conflicts = deltasLib.getConflicts(@headDeltas, @expandedDeltas) + DeltaView.deltaCounter += @expandedDeltas.length getRenderData: -> c = super() - c.deltas = @processedDeltas = @model.getExpandedDelta() - c.counter = DeltaListView.deltaCounter - DeltaListView.deltaCounter += c.deltas.length + c.deltas = @expandedDeltas + c.counter = DeltaView.deltaCounter c afterRender: -> - deltas = @$el.find('.delta') + deltas = @$el.find('.details') for delta, i in deltas deltaEl = $(delta) - deltaData = @processedDeltas[i] - if _.isObject(deltaData.left) and leftEl = deltaEl.find('.old-value') - options = - data: deltaData.left - schema: deltaData.schema - readOnly: true - treema = TreemaNode.make(leftEl, options) - treema.build() + deltaData = @expandedDeltas[i] + @expandDetails(deltaEl, deltaData) + + conflictDeltas = @$el.find('.conflict-details') + conflicts = (delta.conflict for delta in @expandedDeltas when delta.conflict) + for delta, i in conflictDeltas + deltaEl = $(delta) + deltaData = conflicts[i] + @expandDetails(deltaEl, deltaData) + + expandDetails: (deltaEl, deltaData) -> + treemaOptions = { schema: deltaData.schema, readOnly: true } + + if _.isObject(deltaData.left) and leftEl = deltaEl.find('.old-value') + options = _.defaults {data: deltaData.left}, treemaOptions + TreemaNode.make(leftEl, options).build() + + if _.isObject(deltaData.right) and rightEl = deltaEl.find('.new-value') + options = _.defaults {data: deltaData.right}, treemaOptions + TreemaNode.make(rightEl, options).build() + + if deltaData.action is 'text-diff' + left = difflib.stringAsLines deltaData.left + right = difflib.stringAsLines deltaData.right + sm = new difflib.SequenceMatcher(left, right) + opcodes = sm.get_opcodes() + el = deltaEl.find('.text-diff') + options = {baseTextLines: left, newTextLines: right, opcodes: opcodes} + args = _.defaults options, TEXTDIFF_OPTIONS + el.append(diffview.buildView(args)) - if _.isObject(deltaData.right) and rightEl = deltaEl.find('.new-value') - options = - data: deltaData.right - schema: deltaData.schema - readOnly: true - treema = TreemaNode.make(rightEl, options) - treema.build() - - if deltaData.action is 'text-diff' - left = difflib.stringAsLines deltaData.left - right = difflib.stringAsLines deltaData.right - sm = new difflib.SequenceMatcher(left, right) - opcodes = sm.get_opcodes() - el = deltaEl.find('.text-diff') - args = { - baseTextLines: left - newTextLines: right - opcodes: opcodes - baseTextName: "Old" - newTextName: "New" - contextSize: 5 - viewType: 1 - } - el.append(diffview.buildView(args)) + getApplicableDelta: -> + delta = @model.getDelta() + delta = deltasLib.pruneConflictsFromDelta delta, @conflicts if @conflicts + delta \ No newline at end of file diff --git a/app/views/editor/patch_modal.coffee b/app/views/editor/patch_modal.coffee new file mode 100644 index 000000000..2eecab361 --- /dev/null +++ b/app/views/editor/patch_modal.coffee @@ -0,0 +1,42 @@ +ModalView = require 'views/kinds/ModalView' +template = require 'templates/editor/patch_modal' +DeltaView = require 'views/editor/delta' + +module.exports = class PatchModal extends ModalView + id: "patch-modal" + template: template + plain: true + + constructor: (@patch, @targetModel, options) -> + super(options) + targetID = @patch.get('target').id + if false + @originalSource = targetModel.clone(false) + @onOriginalLoaded() + else + @originalSource = new targetModel.constructor({_id:targetID}) + @originalSource.fetch() + @listenToOnce @originalSource, 'sync', @onOriginalLoaded + @addResourceToLoad(@originalSource) + + getRenderData: -> + c = super() + c + + afterRender: -> + return if @originalSource.loading + headModel = @originalSource.clone(false) + headModel.set(@targetModel.attributes) + + pendingModel = @originalSource.clone(false) + pendingModel.applyDelta(@patch.get('delta')) + + @deltaView = new DeltaView({model:pendingModel, headModel:headModel}) + changeEl = @$el.find('.changes-stub') + @insertSubView(@deltaView, changeEl) + super() + + acceptPatch: -> + delta = @deltaView.getApplicableDelta() + pendingModel = @originalSource.clone(false) + pendingModel.applyDelta(delta) \ No newline at end of file diff --git a/app/views/editor/patches_view.coffee b/app/views/editor/patches_view.coffee index abba96997..f8dd4fa15 100644 --- a/app/views/editor/patches_view.coffee +++ b/app/views/editor/patches_view.coffee @@ -2,6 +2,7 @@ CocoView = require 'views/kinds/CocoView' template = require 'templates/editor/patches' PatchesCollection = require 'collections/PatchesCollection' nameLoader = require 'lib/NameLoader' +PatchModal = require './patch_modal' module.exports = class PatchesView extends CocoView template: template @@ -10,6 +11,7 @@ module.exports = class PatchesView extends CocoView events: 'change .status-buttons': 'onStatusButtonsChanged' + 'click .patch-icon': 'openPatchModal' constructor: (@model, options) -> super(options) @@ -47,3 +49,8 @@ module.exports = class PatchesView extends CocoView @initPatches() @load() @render() + + openPatchModal: (e) -> + patch = _.find @patches.models, {id:$(e.target).data('patch-id')} + modal = new PatchModal(patch, @model) + @openModalView(modal) \ No newline at end of file diff --git a/app/views/kinds/CocoView.coffee b/app/views/kinds/CocoView.coffee index b73685eb1..c3fd12228 100644 --- a/app/views/kinds/CocoView.coffee +++ b/app/views/kinds/CocoView.coffee @@ -10,7 +10,7 @@ classCount = 0 makeScopeName = -> "view-scope-#{classCount++}" doNothing = -> -module.exports = class CocoView extends Backbone.View +class CocoView extends Backbone.View startsLoading: false cache: false # signals to the router to keep this view around template: -> '' @@ -348,6 +348,7 @@ module.exports = class CocoView extends Backbone.View slider - -mobileRELong = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i + mobileRELong = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i mobileREShort = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i + +module.exports = CocoView diff --git a/server/patches/Patch.coffee b/server/patches/Patch.coffee index a6c5da41f..df621f2a4 100644 --- a/server/patches/Patch.coffee +++ b/server/patches/Patch.coffee @@ -40,7 +40,6 @@ PatchSchema.pre 'save', (next) -> patches = document.get('patches') or [] patches.push @_id - console.log 'PATCH PUSHED', @_id document.set 'patches', patches document.save (err) -> next(err) From a7922861618ca5f369a57a2c4687422856c25d0a Mon Sep 17 00:00:00 2001 From: Nick Winter <livelily@gmail.com> Date: Fri, 11 Apr 2014 22:30:28 -0700 Subject: [PATCH 24/46] Ameliorated #468, maybe. At least, it doesn't erroneously show the name errors any more. --- app/views/modal/wizard_settings_modal.coffee | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/views/modal/wizard_settings_modal.coffee b/app/views/modal/wizard_settings_modal.coffee index 0223187bd..5715a4c1f 100644 --- a/app/views/modal/wizard_settings_modal.coffee +++ b/app/views/modal/wizard_settings_modal.coffee @@ -22,6 +22,7 @@ module.exports = class WizardSettingsModal extends View WizardSettingsView = require 'views/account/wizard_settings_view' view = new WizardSettingsView() @insertSubView view + super() checkNameExists: => forms.clearFormAlerts(@$el) @@ -31,7 +32,7 @@ module.exports = class WizardSettingsModal extends View forms.applyErrorsToForm(@$el, {property:'name', message:'is already taken'}) if id and id isnt me.id $.ajax("/db/user/#{name}/nameToID", {success: success}) - onWizardSettingsDone: => + onWizardSettingsDone: -> me.set('name', $('#wizard-settings-name').val()) forms.clearFormAlerts(@$el) res = me.validate() @@ -42,10 +43,11 @@ module.exports = class WizardSettingsModal extends View res = me.save() return unless res save = $('#save-button', @$el).text($.i18n.t('common.saving', defaultValue: 'Saving...')) - .addClass('btn-info').show().removeClass('btn-danger') + .addClass('btn-info').show().removeClass('btn-danger') res.error => errors = JSON.parse(res.responseText) + console.warn "Got errors saving user:", errors forms.applyErrorsToForm(@$el, errors) @disableModalInProgress(@$el) @@ -53,4 +55,3 @@ module.exports = class WizardSettingsModal extends View @hide() @enableModalInProgress(@$el) - me.save() From 0eb74ab2877ec973bd8e5c4d2a20f03f1a200dcf Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Fri, 11 Apr 2014 22:33:09 -0700 Subject: [PATCH 25/46] Added buttons for performing actions on patches. --- app/models/CocoModel.coffee | 10 +++++++++- app/models/Patch.coffee | 5 ++++- app/templates/editor/patch_modal.jade | 15 +++++++++------ app/templates/editor/thang/edit.jade | 8 ++++---- app/views/editor/patch_modal.coffee | 22 ++++++++++++++++++++-- 5 files changed, 46 insertions(+), 14 deletions(-) diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee index 59820f703..cdef843d3 100644 --- a/app/models/CocoModel.coffee +++ b/app/models/CocoModel.coffee @@ -91,6 +91,7 @@ class CocoModel extends Backbone.Model @markToRevert() @clearBackup() @trigger "save", @ + patch.setStatus 'accepted' for patch in @acceptedPatches or [] return super attrs, options fetch: -> @@ -116,7 +117,9 @@ class CocoModel extends Backbone.Model cloneNewMinorVersion: -> newData = $.extend(null, {}, @attributes) - new @constructor(newData) + clone = new @constructor(newData) + clone.acceptedPatches = @acceptedPatches + clone cloneNewMajorVersion: -> clone = @cloneNewMinorVersion() @@ -240,5 +243,10 @@ class CocoModel extends Backbone.Model getExpandedDelta: -> delta = @getDelta() deltasLib.expandDelta(delta, @_revertAttributes, @schema().attributes) + + addPatchToAcceptOnSave: (patch) -> + @acceptedPatches ?= [] + @acceptedPatches.push patch + @acceptedPatches = _.uniq(@acceptedPatches, false, (p) -> p.id) module.exports = CocoModel diff --git a/app/models/Patch.coffee b/app/models/Patch.coffee index a88c30941..68b62eca9 100644 --- a/app/models/Patch.coffee +++ b/app/models/Patch.coffee @@ -2,4 +2,7 @@ CocoModel = require('./CocoModel') module.exports = class PatchModel extends CocoModel @className: "Patch" - urlRoot: "/db/patch" \ No newline at end of file + urlRoot: "/db/patch" + + setStatus: (status) -> + $.ajax("/db/patch/#{@id}/status", {type:"PUT", data: {status:status}}) \ No newline at end of file diff --git a/app/templates/editor/patch_modal.jade b/app/templates/editor/patch_modal.jade index 4b094cd81..68fed43f7 100644 --- a/app/templates/editor/patch_modal.jade +++ b/app/templates/editor/patch_modal.jade @@ -12,9 +12,12 @@ block modal-body-content block modal-footer .modal-footer button(data-dismiss="modal", data-i18n="common.cancel").btn Cancel - if canReject - button.btn.btn-danger Reject - if canWithdraw - button.btn.btn-danger Withdraw - if canAccept - button.btn.btn-primary Accept \ No newline at end of file + if isPatchCreator + if status != 'withdrawn' + button.btn.btn-danger#withdraw-button Withdraw + if isPatchRecipient + if status != 'accepted' + button.btn.btn-primary#accept-button Accept + if status != 'rejected' + button.btn.btn-danger#reject-button Reject + \ No newline at end of file diff --git a/app/templates/editor/thang/edit.jade b/app/templates/editor/thang/edit.jade index b751d6de8..37c9054ed 100644 --- a/app/templates/editor/thang/edit.jade +++ b/app/templates/editor/thang/edit.jade @@ -19,7 +19,7 @@ block content h3 Edit Thang Type: "#{thangType.attributes.name}" ul.nav.nav-tabs - li + li.active a(href="#editor-thang-main-tab-view", data-toggle="tab") Main li a(href="#editor-thang-components-tab-view", data-toggle="tab") Components @@ -27,13 +27,13 @@ block content a(href="#editor-thang-spritesheets-view", data-toggle="tab") Spritesheets li a(href="#editor-thang-colors-tab-view", data-toggle="tab")#color-tab Colors - li.active + li a(href="#editor-thang-patches-view", data-toggle="tab")#patches-tab Patches div.tab-content div.tab-pane#editor-thang-colors-tab-view - div.tab-pane#editor-thang-main-tab-view + div.tab-pane#editor-thang-main-tab-view.active div.main-area.well div.file-controls @@ -86,7 +86,7 @@ block content div#spritesheets - div.tab-pane#editor-thang-patches-view.active + div.tab-pane#editor-thang-patches-view div.patches-view diff --git a/app/views/editor/patch_modal.coffee b/app/views/editor/patch_modal.coffee index 2eecab361..19f3ebfe6 100644 --- a/app/views/editor/patch_modal.coffee +++ b/app/views/editor/patch_modal.coffee @@ -1,11 +1,17 @@ ModalView = require 'views/kinds/ModalView' template = require 'templates/editor/patch_modal' DeltaView = require 'views/editor/delta' +auth = require 'lib/auth' module.exports = class PatchModal extends ModalView id: "patch-modal" template: template plain: true + + events: + 'click #withdraw-button': 'withdrawPatch' + 'click #reject-button': 'rejectPatch' + 'click #accept-button': 'acceptPatch' constructor: (@patch, @targetModel, options) -> super(options) @@ -21,6 +27,9 @@ module.exports = class PatchModal extends ModalView getRenderData: -> c = super() + c.isPatchCreator = @patch.get('creator') is auth.me.id + c.isPatchRecipient = @targetModel.hasWriteAccess() + c.status = @patch.get 'status' c afterRender: -> @@ -38,5 +47,14 @@ module.exports = class PatchModal extends ModalView acceptPatch: -> delta = @deltaView.getApplicableDelta() - pendingModel = @originalSource.clone(false) - pendingModel.applyDelta(delta) \ No newline at end of file + @targetModel.applyDelta(delta) + @targetModel.addPatchToAcceptOnSave(@patch) + @hide() + + rejectPatch: -> + @patch.setStatus('rejected') + @hide() + + withdrawPatch: -> + @patch.setStatus('withdrawn') + @hide() \ No newline at end of file From 53579b2632c960b28426bc0afda5e3f299ded04d Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Fri, 11 Apr 2014 22:52:05 -0700 Subject: [PATCH 26/46] Merge branch 'master' into feature/jsondiffpatch Conflicts: app/templates/editor/level/save.jade app/templates/editor/thang/edit.jade --- app/locale/en.coffee | 5 +---- app/templates/editor/thang/edit.jade | 7 +------ app/templates/modal/save_version.jade | 4 ++-- app/views/modal/save_version_modal.coffee | 1 + 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 8e65e4212..b65b93340 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -6,6 +6,7 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr send: "Send" cancel: "Cancel" save: "Save" + publish: "Publish" create: "Create" delay_1_sec: "1 second" delay_3_sec: "3 seconds" @@ -47,9 +48,6 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr versions: save_version_title: "Save New Version" new_major_version: "New Major Version" - update_break_level: "(Could this update break old solutions of the level?)" - update_break_component: "(Could this update break anything depending on this Component?)" - update_break_system: "(Could this update break anything depending on this System?)" cla_prefix: "To save changes, first you must agree to our" cla_url: "CLA" cla_suffix: "." @@ -327,7 +325,6 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr more: "More" wiki: "Wiki" live_chat: "Live Chat" - level_publish: "Publish This Level (irreversible)?" level_some_options: "Some Options?" level_tab_thangs: "Thangs" level_tab_scripts: "Scripts" diff --git a/app/templates/editor/thang/edit.jade b/app/templates/editor/thang/edit.jade index 062a97575..b1924f439 100644 --- a/app/templates/editor/thang/edit.jade +++ b/app/templates/editor/thang/edit.jade @@ -12,13 +12,8 @@ block content img#portrait.img-thumbnail -<<<<<<< HEAD - button.btn.btn-secondary#history-button(data-i18n="general.history") History - button.btn.btn-primary#save-button(data-i18n="common.save", disabled=authorized === true ? undefined : "true") Save -======= button.btn.btn-secondary#history-button(data-i18n="general.version_history") Version History - button.btn.btn-primary#save-button(data-toggle="coco-modal", data-target="modal/save_version", data-i18n="common.save", disabled=authorized === true ? undefined : "true") Save ->>>>>>> master + button.btn.btn-primary#save-button(data-i18n="common.save", disabled=authorized === true ? undefined : "true") Save button.btn.btn-primary#revert-button(data-toggle="coco-modal", data-target="modal/revert", data-i18n="editor.revert", disabled=authorized === true ? undefined : "true") Revert h3 Edit Thang Type: "#{thangType.attributes.name}" diff --git a/app/templates/modal/save_version.jade b/app/templates/modal/save_version.jade index 7fcfd871d..748c541a9 100644 --- a/app/templates/modal/save_version.jade +++ b/app/templates/modal/save_version.jade @@ -11,12 +11,12 @@ block modal-body-content .changes-stub form.form-inline .form-group.commit-message - input.form-control#commit-message(name="commitMessage", type="text", placeholder="Commit Message") + input.form-control#commit-message(name="commitMessage", type="text") if !isPatch .checkbox label input#major-version(name="version-is-major", type="checkbox") - | Major Changes + span(data-i18n="versions.new_major_version") New Major Version else .alert.alert-danger No changes diff --git a/app/views/modal/save_version_modal.coffee b/app/views/modal/save_version_modal.coffee index db4c45de3..1ed4a4d54 100644 --- a/app/views/modal/save_version_modal.coffee +++ b/app/views/modal/save_version_modal.coffee @@ -33,6 +33,7 @@ module.exports = class SaveVersionModal extends ModalView changeEl = @$el.find('.changes-stub') deltaView = new DeltaView({model:@model}) @insertSubView(deltaView, changeEl) + $('.commit-message input').attr('placeholder', $.i18n.t('general.commit_msg')) onClickSaveButton: -> Backbone.Mediator.publish 'save-new-version', { From a4284e62096430aabc68c9d63cf15a1e83cf110f Mon Sep 17 00:00:00 2001 From: Nick Winter <livelily@gmail.com> Date: Fri, 11 Apr 2014 23:07:48 -0700 Subject: [PATCH 27/46] Fixed #776 with some CSS/font fixes and \@therealbond's admin footerization. --- app/styles/base.sass | 19 ++++++++++++++++++- app/styles/common/top_nav.sass | 2 +- app/templates/base.jade | 8 ++------ 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/app/styles/base.sass b/app/styles/base.sass index 72ff4fe2f..b5a9a0752 100644 --- a/app/styles/base.sass +++ b/app/styles/base.sass @@ -212,7 +212,7 @@ table.table .header-font font-family: $headings-font-family -body[lang='ru'], body[lang|='zh'], body[lang='ja'], body[lang='pl'], body[lang='tr'], body[lang='cs'], body[lang='el'], body[lang='ro'], body[lang='vi'], body[lang='th'], body[lang='ko'], body[lang='sk'], body[lang='sl'], body[lang='bg'], body[lang='he'], body[lang='lt'], body[lang='sr'], body[lang='uk'], body[lang='hi'], body[lang='ur'], +body[lang='ru'], body[lang|='zh'], body[lang='pl'], body[lang='tr'], body[lang='cs'], body[lang='el'], body[lang='ro'], body[lang='vi'], body[lang='th'], body[lang='ko'], body[lang='sk'], body[lang='sl'], body[lang='bg'], body[lang='he'], body[lang='lt'], body[lang='sr'], body[lang='uk'], body[lang='hi'], body[lang='ur'], body[lang='hu'] h1, h2, h3, h4, h5, h6 font-family: 'Open Sans Condensed', Impact, "Arial Narrow", "Arial", sans-serif text-transform: uppercase @@ -222,6 +222,23 @@ body[lang='ru'], body[lang|='zh'], body[lang='ja'], body[lang='pl'], body[lang=' font-family: 'Open Sans Condensed', Impact, "Arial Narrow", "Arial", sans-serif !important text-transform: uppercase letter-spacing: -1px !important + +body[lang='ja'] + h1, h2, h3, h4, h5, h6 + font-family: "ヒラギノ角ゴ Pro W3", "Hiragino Kaku Gothic Pro", Osaka, "メイリオ", Meiryo, "MS Pゴシック", "MS PGothic", 'Open Sans Condensed', sans-serif + text-transform: uppercase + letter-spacing: -1px !important + + .header-font + font-family: "ヒラギノ角ゴ Pro W3", "Hiragino Kaku Gothic Pro", Osaka, "メイリオ", Meiryo, "MS Pゴシック", "MS PGothic", 'Open Sans Condensed', sans-serif + text-transform: uppercase + letter-spacing: -1px !important + + #top-nav + .navbar-nav + li + a.header-font + font-size: 16px @media only screen and (max-width: 800px) .main-content-area diff --git a/app/styles/common/top_nav.sass b/app/styles/common/top_nav.sass index 9a41771fe..f5c61685e 100644 --- a/app/styles/common/top_nav.sass +++ b/app/styles/common/top_nav.sass @@ -11,7 +11,7 @@ letter-spacing: 1px .navbuttontext-user-name - max-width: 125px + max-width: 110px overflow: hidden text-overflow: ellipsis white-space: nowrap diff --git a/app/templates/base.jade b/app/templates/base.jade index f31bc2db3..96d184876 100644 --- a/app/templates/base.jade +++ b/app/templates/base.jade @@ -51,12 +51,6 @@ body a.header-font(href='http://blog.codecombat.com/', data-i18n="nav.blog") Blog li.forum a.header-font(href='http://discourse.codecombat.com/', data-i18n="nav.forum") Forum - if me.isAdmin() - li.admin - a.header-font(href='/admin', data-i18n="nav.admin") Admin - - - block outer_content @@ -79,6 +73,8 @@ body a(href='/legal', title='Legal', tabindex=-1, data-i18n="nav.legal") Legal a(href='/about', title='About', tabindex=-1, data-i18n="nav.about") About a(title='Contact', tabindex=-1, data-toggle="coco-modal", data-target="modal/contact", data-i18n="nav.contact") Contact + if me.isAdmin() + a(href='/admin', data-i18n="nav.admin") Admin .share-buttons .g-plusone(data-href="http://codecombat.com", data-size="medium") From 6fb5b59a012389cc607c3e6e2256adfb31f42130 Mon Sep 17 00:00:00 2001 From: Aditya Raisinghani <aditya.ajeet@gmail.com> Date: Sat, 12 Apr 2014 14:05:56 +0530 Subject: [PATCH 28/46] Refactored schemas to be in /app --- app/models/CocoModel.coffee | 21 +- app/models/SuperModel.coffee | 4 +- app/schemas/article_schema.coffee | 13 + app/schemas/i18n_schema.coffee | 48 ++++ app/schemas/languages.coffee | 49 ++++ app/schemas/level_component_schema.coffee | 119 ++++++++ app/schemas/level_feedback_schema.coffee | 27 ++ app/schemas/level_schema.coffee | 254 ++++++++++++++++++ app/schemas/level_session_schema.coffee | 213 +++++++++++++++ app/schemas/level_system_schema.coffee | 106 ++++++++ app/schemas/metaschema.coffee | 132 +++++++++ app/schemas/schemas.coffee | 158 +++++++++++ app/schemas/thang_component_schema.coffee | 21 ++ app/schemas/thang_type_schema.coffee | 153 +++++++++++ app/schemas/user_schema.coffee | 98 +++++++ app/views/account/settings_view.coffee | 4 +- app/views/editor/article/edit.coffee | 2 +- app/views/editor/components/main.coffee | 4 +- app/views/editor/level/component/edit.coffee | 6 +- .../editor/level/scripts_tab_view.coffee | 4 +- .../editor/level/settings_tab_view.coffee | 2 +- app/views/editor/level/system/edit.coffee | 6 +- .../editor/level/systems_tab_view.coffee | 2 +- app/views/editor/level/thangs_tab_view.coffee | 2 +- app/views/editor/thang/colors_tab_view.coffee | 4 +- app/views/editor/thang/edit.coffee | 4 +- app/views/modal/login_modal.coffee | 2 +- app/views/modal/signup_modal.coffee | 2 +- server/routes/db.coffee | 4 +- 29 files changed, 1429 insertions(+), 35 deletions(-) create mode 100644 app/schemas/article_schema.coffee create mode 100644 app/schemas/i18n_schema.coffee create mode 100644 app/schemas/languages.coffee create mode 100644 app/schemas/level_component_schema.coffee create mode 100644 app/schemas/level_feedback_schema.coffee create mode 100644 app/schemas/level_schema.coffee create mode 100644 app/schemas/level_session_schema.coffee create mode 100644 app/schemas/level_system_schema.coffee create mode 100644 app/schemas/metaschema.coffee create mode 100644 app/schemas/schemas.coffee create mode 100644 app/schemas/thang_component_schema.coffee create mode 100644 app/schemas/thang_type_schema.coffee create mode 100644 app/schemas/user_schema.coffee diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee index 8dc8e03a1..974cd4826 100644 --- a/app/models/CocoModel.coffee +++ b/app/models/CocoModel.coffee @@ -5,9 +5,11 @@ auth = require 'lib/auth' class CocoSchema extends Backbone.Model constructor: (path, args...) -> super(args...) - @urlRoot = path + '/schema' + # @urlRoot = path + '/schema' + @schemaName = path[4..].replace '.', '_' + @schema = require 'schemas/' + @schemaName + '_schema' -window.CocoSchema = CocoSchema +# window.CocoSchema = CocoSchema.schema class CocoModel extends Backbone.Model idAttribute: "_id" @@ -18,7 +20,7 @@ class CocoModel extends Backbone.Model initialize: -> super() - @constructor.schema ?= new CocoSchema(@urlRoot) + @constructor.schema ?= @urlRoot[4..].replace '.', '_' if not @constructor.className console.error("#{@} needs a className set.") @markToRevert() @@ -65,8 +67,9 @@ class CocoModel extends Backbone.Model loadSchema: -> return if @constructor.schema.loading - @constructor.schema.fetch() - @listenToOnce(@constructor.schema, 'sync', @onConstructorSync) + @constructor.schema = require 'schemas/' + @constructor.schema + '_schema' unless @constructor.schema.loaded + @onConstructorSync() + # @listenToOnce(@constructor.schema, 'sync', @onConstructorSync) onConstructorSync: -> @constructor.schema.loaded = true @@ -77,7 +80,7 @@ class CocoModel extends Backbone.Model schema: -> return @constructor.schema validate: -> - result = tv4.validateMultiple(@attributes, @constructor.schema?.attributes or {}) + 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 @@ -138,11 +141,11 @@ class CocoModel extends Backbone.Model addSchemaDefaults: -> return if @addedSchemaDefaults or not @constructor.hasSchema() @addedSchemaDefaults = true - for prop, defaultValue of @constructor.schema.attributes.default or {} + for prop, defaultValue of @constructor.schema.default or {} continue if @get(prop)? #console.log "setting", prop, "to", defaultValue, "from attributes.default" @set prop, defaultValue - for prop, sch of @constructor.schema.attributes.properties or {} + for prop, sch of @constructor.schema.properties or {} continue if @get(prop)? #console.log "setting", prop, "to", sch.default, "from sch.default" if sch.default? @set prop, sch.default if sch.default? @@ -154,7 +157,7 @@ class CocoModel extends Backbone.Model # returns unfetched model shells for every referenced doc in this model # OPTIMIZE so that when loading models, it doesn't cause the site to stutter data ?= @attributes - schema ?= @schema().attributes + schema ?= @schema() models = [] if $.isArray(data) and schema.items? diff --git a/app/models/SuperModel.coffee b/app/models/SuperModel.coffee index 9e2bcd347..adcac62eb 100644 --- a/app/models/SuperModel.coffee +++ b/app/models/SuperModel.coffee @@ -29,9 +29,9 @@ class SuperModel model.loadSchema() schema = model.schema() unless schema.loaded - @schemas[schema.urlRoot] = schema + @schemas[model.urlRoot] = schema return schema.once('sync', => @modelLoaded(model)) - refs = model.getReferencedModels(model.attributes, schema.attributes, '/', @shouldLoadProjection) + refs = model.getReferencedModels(model.attributes, schema, '/', @shouldLoadProjection) refs = [] unless @mustPopulate is model or @shouldPopulate(model) # console.log 'Loaded', model.get('name') for ref, i in refs when @shouldLoadReference ref diff --git a/app/schemas/article_schema.coffee b/app/schemas/article_schema.coffee new file mode 100644 index 000000000..012d46ec3 --- /dev/null +++ b/app/schemas/article_schema.coffee @@ -0,0 +1,13 @@ +c = require './schemas' + +ArticleSchema = c.object() +c.extendNamedProperties ArticleSchema # name first + +ArticleSchema.properties.body = { type: 'string', title: 'Content', format: 'markdown' } +ArticleSchema.properties.i18n = { type: 'object', title: 'i18n', format: 'i18n', props: ['name', 'body'] } + +c.extendBasicProperties(ArticleSchema, 'article') +c.extendSearchableProperties(ArticleSchema) +c.extendVersionedProperties(ArticleSchema, 'article') + +module.exports = ArticleSchema diff --git a/app/schemas/i18n_schema.coffee b/app/schemas/i18n_schema.coffee new file mode 100644 index 000000000..2a2aaf816 --- /dev/null +++ b/app/schemas/i18n_schema.coffee @@ -0,0 +1,48 @@ +#this file will hold the experimental JSON schema for i18n +c = require './schemas' + +languageCodeArrayRegex = c.generateLanguageCodeArrayRegex() + + +ExampleSchema = { + title: "Example Schema", + description:"An example schema", + type: "object", + properties: { + text: { + title: "Text", + description: "A short message to display in the dialogue area. Markdown okay.", + type: "string", + maxLength: 400 + }, + i18n: {"$ref": "#/definitions/i18n"} + }, + + definitions: { + i18n: { + title: "i18n", + description: "The internationalization object", + type: "object", + patternProperties: { + languageCodeArrayRegex: { + additionalProperties: false, + properties: { + #put the translatable properties here + #if it is possible to not include i18n with a reference + # to #/properties, you could just do + properties: {"$ref":"#/properties"} + # text: {"$ref": "#/properties/text"} + } + default: { + title: "LanguageCode", + description: "LanguageDescription" + } + } + } + } + }, + +} + +#define a i18n object type for each schema, then have the i18n have it's oneOf check against +#translatable schemas of that object \ No newline at end of file diff --git a/app/schemas/languages.coffee b/app/schemas/languages.coffee new file mode 100644 index 000000000..e9c8e0f33 --- /dev/null +++ b/app/schemas/languages.coffee @@ -0,0 +1,49 @@ +# errors = require '../commons/errors' +# log = require 'winston' +locale = require '../locale/locale' # requiring from app; will break if we stop serving from where app lives + +# module.exports.setup = (app) -> +# app.all '/languages/add/:lang/:namespace', (req, res) -> +# # Should probably store these somewhere +# log.info "#{req.params.lang}.#{req.params.namespace} missing an i18n key:", req.body +# res.send('') +# res.end() + +# app.all '/languages', (req, res) -> +# # Now that these are in the client, not sure when we would use this, but hey +# return errors.badMethod(res) if req.route.method isnt 'get' +# res.send(languages) +# return res.end() + +languages = [] +for code, localeInfo of locale + languages.push code: code, nativeDescription: localeInfo.nativeDescription, englishDescription: localeInfo.englishDescription + +module.exports.languages = languages +module.exports.languageCodes = languageCodes = (language.code for language in languages) +module.exports.languageCodesLower = languageCodesLower = (code.toLowerCase() for code in languageCodes) + +# Keep keys lower-case for matching and values with second subtag uppercase like i18next expects +languageAliases = + 'en': 'en-US' + + 'zh-cn': 'zh-HANS' + 'zh-hans-cn': 'zh-HANS' + 'zh-sg': 'zh-HANS' + 'zh-hans-sg': 'zh-HANS' + + 'zh-tw': 'zh-HANT' + 'zh-hant-tw': 'zh-HANT' + 'zh-hk': 'zh-HANT' + 'zh-hant-hk': 'zh-HANT' + 'zh-mo': 'zh-HANT' + 'zh-hant-mo': 'zh-HANT' + +module.exports.languageCodeFromAcceptedLanguages = languageCodeFromAcceptedLanguages = (acceptedLanguages) -> + for lang in acceptedLanguages ? [] + code = languageAliases[lang.toLowerCase()] + return code if code + codeIndex = _.indexOf languageCodesLower, lang + if codeIndex isnt -1 + return languageCodes[codeIndex] + return 'en-US' diff --git a/app/schemas/level_component_schema.coffee b/app/schemas/level_component_schema.coffee new file mode 100644 index 000000000..d67c69376 --- /dev/null +++ b/app/schemas/level_component_schema.coffee @@ -0,0 +1,119 @@ +c = require './schemas' +metaschema = require './metaschema' + +attackSelfCode = """ +class AttacksSelf extends Component + @className: "AttacksSelf" + chooseAction: -> + @attack @ +""" +systems = [ + 'action', 'ai', 'alliance', 'collision', 'combat', 'display', 'event', 'existence', 'hearing' + 'inventory', 'movement', 'programming', 'targeting', 'ui', 'vision', 'misc', 'physics', 'effect', + 'magic' +] + +PropertyDocumentationSchema = c.object { + title: "Property Documentation" + description: "Documentation entry for a property this Component will add to its Thang which other Components might + want to also use." + "default": + name: "foo" + type: "object" + description: 'The `foo` property can satisfy all the #{spriteName}\'s foobar needs. Use it wisely.' + required: ['name', 'type', 'description'] +}, + name: {type: 'string', title: "Name", description: "Name of the property."} + # not actual JS types, just whatever they describe... + type: c.shortString(title: "Type", description: "Intended type of the property.") + description: {title: "Description", type: 'string', description: "Description of the property.", format: 'markdown', maxLength: 1000} + args: c.array {title: "Arguments", description: "If this property has type 'function', then provide documentation for any function arguments."}, c.FunctionArgumentSchema + owner: {title: "Owner", type: 'string', description: 'Owner of the property, like "this" or "Math".'} + example: {title: "Example", type: 'string', description: 'An optional example code block.', format: 'javascript'} + returns: c.object { + title: "Return Value" + description: 'Optional documentation of any return value.' + required: ['type'] + default: {type: 'null'} + }, + type: c.shortString(title: "Type", description: "Type of the return value") + example: c.shortString(title: "Example", description: "Example return value") + description: {title: "Description", type: 'string', description: "Description of the return value.", maxLength: 1000} + +DependencySchema = c.object { + title: "Component Dependency" + description: "A Component upon which this Component depends." + "default": + #original: ? + majorVersion: 0 + required: ["original", "majorVersion"] + format: 'latest-version-reference' + links: [{rel: "db", href: "/db/level.component/{(original)}/version/{(majorVersion)}"}] +}, + original: c.objectId(title: "Original", description: "A reference to another Component upon which this Component depends.") + majorVersion: + title: "Major Version" + description: "Which major version of the Component this Component needs." + type: 'integer' + minimum: 0 + +LevelComponentSchema = c.object { + title: "Component" + description: "A Component which can affect Thang behavior." + required: ["system", "name", "description", "code", "dependencies", "propertyDocumentation", "language"] + "default": + system: "ai" + name: "AttacksSelf" + description: "This Component makes the Thang attack itself." + code: attackSelfCode + language: "coffeescript" + dependencies: [] # TODO: should depend on something by default + propertyDocumentation: [] +} +c.extendNamedProperties LevelComponentSchema # let's have the name be the first property +LevelComponentSchema.properties.name.pattern = c.classNamePattern +_.extend LevelComponentSchema.properties, + system: + title: "System" + description: "The short name of the System this Component belongs to, like \"ai\"." + type: "string" + "enum": systems + "default": "ai" + description: + title: "Description" + description: "A short explanation of what this Component does." + type: "string" + maxLength: 2000 + "default": "This Component makes the Thang attack itself." + language: + type: "string" + title: "Language" + description: "Which programming language this Component is written in." + "enum": ["coffeescript"] + code: + title: "Code" + description: "The code for this Component, as a CoffeeScript class. TODO: add link to documentation for + how to write these." + "default": attackSelfCode + type: "string" + format: "coffee" + js: + title: "JavaScript" + description: "The transpiled JavaScript code for this Component" + type: "string" + format: "hidden" + dependencies: c.array {title: "Dependencies", description: "An array of Components upon which this Component depends.", "default": [], uniqueItems: true}, DependencySchema + propertyDocumentation: c.array {title: "Property Documentation", description: "An array of documentation entries for each notable property this Component will add to its Thang which other Components might want to also use.", "default": []}, PropertyDocumentationSchema + configSchema: _.extend metaschema, {title: "Configuration Schema", description: "A schema for validating the arguments that can be passed to this Component as configuration.", default: {type: 'object', additionalProperties: false}} + official: + type: "boolean" + title: "Official" + description: "Whether this is an official CodeCombat Component." + "default": false + +c.extendBasicProperties LevelComponentSchema, 'level.component' +c.extendSearchableProperties LevelComponentSchema +c.extendVersionedProperties LevelComponentSchema, 'level.component' +c.extendPermissionsProperties LevelComponentSchema, 'level.component' + +module.exports = LevelComponentSchema diff --git a/app/schemas/level_feedback_schema.coffee b/app/schemas/level_feedback_schema.coffee new file mode 100644 index 000000000..201beb468 --- /dev/null +++ b/app/schemas/level_feedback_schema.coffee @@ -0,0 +1,27 @@ +c = require './schemas' + +LevelFeedbackLevelSchema = c.object {required: ['original', 'majorVersion']}, { + original: c.objectId({}) + majorVersion: {type: 'integer', minimum: 0, default: 0}} + +LevelFeedbackSchema = c.object { + title: "Feedback" + description: "Feedback on a level." +} + +_.extend LevelFeedbackSchema.properties, + # denormalization + creatorName: { type: 'string' } + levelName: { type: 'string' } + levelID: { type: 'string' } + + creator: c.objectId(links: [{rel: 'extra', href: "/db/user/{($)}"}]) + created: c.date( { title: 'Created', readOnly: true }) + + level: LevelFeedbackLevelSchema + rating: { type: 'number', minimum: 1, maximum: 5 } + review: { type: 'string' } + +c.extendBasicProperties LevelFeedbackSchema, 'level.feedback' + +module.exports = LevelFeedbackSchema diff --git a/app/schemas/level_schema.coffee b/app/schemas/level_schema.coffee new file mode 100644 index 000000000..e372bdd52 --- /dev/null +++ b/app/schemas/level_schema.coffee @@ -0,0 +1,254 @@ +c = require './schemas' +ThangComponentSchema = require './thang_component_schema' + +SpecificArticleSchema = c.object() +c.extendNamedProperties SpecificArticleSchema # name first +SpecificArticleSchema.properties.body = { type: 'string', title: 'Content', description: "The body content of the article, in Markdown.", format: 'markdown' } +SpecificArticleSchema.displayProperty = 'name' + +side = {title: "Side", description: "A side.", type: 'string', 'enum': ['left', 'right', 'top', 'bottom']} +thang = {title: "Thang", description: "The name of a Thang.", type: 'string', maxLength: 30, format:'thang'} + +eventPrereqValueTypes = ["boolean", "integer", "number", "null", "string"] # not "object" or "array" +EventPrereqSchema = c.object {title: "Event Prerequisite", format: 'event-prereq', description: "Script requires that the value of some property on the event triggering it to meet some prerequisite.", "default": {eventProps: []}, required: ["eventProps"]}, + eventProps: c.array {'default': ["thang"], format:'event-value-chain', maxItems: 10, title: "Event Property", description: 'A chain of keys in the event, like "thang.pos.x" to access event.thang.pos.x.'}, c.shortString(title: "Property", description: "A key in the event property key chain.") + equalTo: c.object {type: eventPrereqValueTypes, title: "==", description: "Script requires the event's property chain value to be equal to this value."} + notEqualTo: c.object {type: eventPrereqValueTypes, title: "!=", description: "Script requires the event's property chain value to *not* be equal to this value."} + greaterThan: {type: 'number', title: ">", description: "Script requires the event's property chain value to be greater than this value."} + greaterThanOrEqualTo: {type: 'number', title: ">=", description: "Script requires the event's property chain value to be greater or equal to this value."} + lessThan: {type: 'number', title: "<", description: "Script requires the event's property chain value to be less than this value."} + lessThanOrEqualTo: {type: 'number', title: "<=", description: "Script requires the event's property chain value to be less than or equal to this value."} + containingString: c.shortString(title: "Contains", description: "Script requires the event's property chain value to be a string containing this string.") + notContainingString: c.shortString(title: "Does not contain", description: "Script requires the event's property chain value to *not* be a string containing this string.") + containingRegexp: c.shortString(title: "Contains Regexp", description: "Script requires the event's property chain value to be a string containing this regular expression.") + notContainingRegexp: c.shortString(title: "Does not contain regexp", description: "Script requires the event's property chain value to *not* be a string containing this regular expression.") + +GoalSchema = c.object {title: "Goal", description: "A goal that the player can accomplish.", required: ["name", "id"]}, + name: c.shortString(title: "Name", description: "Name of the goal that the player will see, like \"Defeat eighteen dragons\".") + i18n: {type: "object", format: 'i18n', props: ['name'], description: "Help translate this goal"} + id: c.shortString(title: "ID", description: "Unique identifier for this goal, like \"defeat-dragons\".") # unique somehow? + worldEndsAfter: {title: 'World Ends After', description: "When included, ends the world this many seconds after this goal succeeds or fails.", type: 'number', minimum: 0, exclusiveMinimum: true, maximum: 300, default: 3} + howMany: {title: "How Many", description: "When included, require only this many of the listed goal targets instead of all of them.", type: 'integer', minimum: 1} + hiddenGoal: {title: "Hidden", description: "Hidden goals don't show up in the goals area for the player until they're failed. (Usually they're obvious, like 'don't die'.)", 'type': 'boolean', default: false} + team: c.shortString(title: 'Team', description: 'Name of the team this goal is for, if it is not for all of the playable teams.') + killThangs: c.array {title: "Kill Thangs", description: "A list of Thang IDs the player should kill, or team names.", uniqueItems: true, minItems: 1, "default": ["ogres"]}, thang + saveThangs: c.array {title: "Save Thangs", description: "A list of Thang IDs the player should save, or team names", uniqueItems: true, minItems: 1, "default": ["humans"]}, thang + getToLocations: c.object {title: "Get To Locations", description: "Will be set off when any of the \"who\" touch any of the \"targets\" ", required: ["who", "targets"]}, + who: c.array {title: "Who", description: "The Thangs who must get to the target locations.", minItems: 1}, thang + targets: c.array {title: "Targets", description: "The target locations to which the Thangs must get.", minItems: 1}, thang + getAllToLocations: c.array {title: "Get all to locations", description: "Similar to getToLocations but now a specific \"who\" can have a specific \"target\", also must be used with the HowMany property for desired effect",required: ["getToLocation"]}, + c.object {title: "", description: ""}, + getToLocation: c.object {title: "Get To Locations", description: "TODO: explain", required: ["who", "targets"]}, + who: c.array {title: "Who", description: "The Thangs who must get to the target locations.", minItems: 1}, thang + targets: c.array {title: "Targets", description: "The target locations to which the Thangs must get.", minItems: 1}, thang + keepFromLocations: c.object {title: "Keep From Locations", description: "TODO: explain", required: ["who", "targets"]}, + who: c.array {title: "Who", description: "The Thangs who must not get to the target locations.", minItems: 1}, thang + targets: c.array {title: "Targets", description: "The target locations to which the Thangs must not get.", minItems: 1}, thang + keepAllFromLocations: c.array {title: "Keep ALL From Locations", description: "Similar to keepFromLocations but now a specific \"who\" can have a specific \"target\", also must be used with the HowMany property for desired effect", required: ["keepFromLocation"]}, + c.object {title: "", description: ""}, + keepFromLocation: c.object {title: "Keep From Locations", description: "TODO: explain", required: ["who", "targets"]}, + who: c.array {title: "Who", description: "The Thangs who must not get to the target locations.", minItems: 1}, thang + targets: c.array {title: "Targets", description: "The target locations to which the Thangs must not get.", minItems: 1}, thang + leaveOffSides: c.object {title: "Leave Off Sides", description: "Sides of the level to get some Thangs to leave across.", required: ["who", "sides"]}, + who: c.array {title: "Who", description: "The Thangs which must leave off the sides of the level.", minItems: 1}, thang + sides: c.array {title: "Sides", description: "The sides off which the Thangs must leave.", minItems: 1}, side + keepFromLeavingOffSides: c.object {title: "Keep From Leaving Off Sides", description: "Sides of the level to keep some Thangs from leaving across.", required: ["who", "sides"]}, + who: c.array {title: "Who", description: "The Thangs which must not leave off the sides of the level.", minItems: 1}, thang + sides: side, {title: "Sides", description: "The sides off which the Thangs must not leave.", minItems: 1}, side + collectThangs: c.object {title: "Collect", description: "Thangs that other Thangs must collect.", required: ["who", "targets"]}, + who: c.array {title: "Who", description: "The Thangs which must collect the target items.", minItems: 1}, thang + targets: c.array {title: "Targets", description: "The target items which the Thangs must collect.", minItems: 1}, thang + keepFromCollectingThangs: c.object {title: "Keep From Collecting", description: "Thangs that the player must prevent other Thangs from collecting.", required: ["who", "targets"]}, + who: c.array {title: "Who", description: "The Thangs which must not collect the target items.", minItems: 1}, thang + targets: c.array {title: "Targets", description: "The target items which the Thangs must not collect.", minItems: 1}, thang + +ResponseSchema = c.object {title: "Dialogue Button", description: "A button to be shown to the user with the dialogue.", required: ["text"]}, + text: {title: "Title", description: "The text that will be on the button", "default": "Okay", type: 'string', maxLength: 30} + channel: c.shortString(title: "Channel", format: 'event-channel', description: 'Channel that this event will be broadcast over, like "level-set-playing".') + event: {type: 'object', title: "Event", description: "Event that will be broadcast when this button is pressed, like {playing: true}."} + buttonClass: c.shortString(title: "Button Class", description: 'CSS class that will be added to the button, like "btn-primary".') + i18n: {type: "object", format: 'i18n', props: ['text'], description: "Help translate this button"} + +PointSchema = c.object {title: "Point", description: "An {x, y} coordinate point.", format: "point2d", required: ["x", "y"]}, + x: {title: "x", description: "The x coordinate.", type: "number", "default": 15} + y: {title: "y", description: "The y coordinate.", type: "number", "default": 20} + +SpriteCommandSchema = c.object {title: "Thang Command", description: "Make a target Thang move or say something, or select/deselect it.", required: ["id"], default: {id: "Captain Anya"}}, + id: thang + select: {title: "Select", description: "Select or deselect this Thang.", type: 'boolean'} + say: c.object {title: "Say", description: "Make this Thang say a message.", required: ["text"]}, + blurb: c.shortString(title: "Blurb", description: "A very short message to display above this Thang's head. Plain text.", maxLength: 50) + mood: c.shortString(title: "Mood", description: "The mood with which the Thang speaks.", "enum": ["explain", "debrief", "congrats", "attack", "joke", "tip", "alarm"], "default": "explain") + text: {title: "Text", description: "A short message to display in the dialogue area. Markdown okay.", type: "string", maxLength: 400} + sound: c.object {title: "Sound", description: "A dialogue sound file to accompany the message.", required: ["mp3", "ogg"]}, + mp3: c.shortString(title: "MP3", format: 'sound-file') + ogg: c.shortString(title: "OGG", format: 'sound-file') + preload: {title: "Preload", description: "Whether to load this sound file before the level can begin (typically for the first dialogue of a level).", type: 'boolean', "default": false} + responses: c.array {title: "Buttons", description: "An array of buttons to include with the dialogue, with which the user can respond."}, ResponseSchema + i18n: {type: "object", format: 'i18n', props: ['blurb', 'text'], description: "Help translate this message"} + move: c.object {title: "Move", description: "Tell the Thang to move.", required: ['target'], default: {target: {x: 20, y: 20}, duration: 500}}, + target: _.extend _.cloneDeep(PointSchema), {title: 'Target', description: 'Target point to which the Thang will move.'} + duration: {title: "Duration", description: "Number of milliseconds over which to move, or 0 for an instant move.", type: 'integer', minimum: 0, default: 500, format: 'milliseconds'} + +NoteGroupSchema = c.object {title: "Note Group", description: "A group of notes that should be sent out as a result of this script triggering.", displayProperty: "name"}, + name: {title: "Name", description: "Short name describing the script, like \"Anya greets the player\", for your convenience.", type: "string"} + dom: c.object {title: "DOM", description: "Manipulate things in the play area DOM, outside of the level area canvas."}, + focus: c.shortString(title: "Focus", description: "Set the window focus to this DOM selector string.") + showVictory: { + title: "Show Victory", + description: "Show the done button and maybe also the victory modal.", + enum: [true, 'Done Button', 'Done Button And Modal'] # deprecate true, same as 'done_button_and_modal' + } + highlight: c.object {title: "Highlight", description: "Highlight the target DOM selector string with a big arrow."}, + target: c.shortString(title: "Target", description: "Target highlight element DOM selector string.") + delay: {type: 'integer', minimum: 0, title: "Delay", description: "Show the highlight after this many milliseconds. Doesn't affect the dim shade cutout highlight method."} + offset: _.extend _.cloneDeep(PointSchema), {title: 'Offset', description: 'Pointing arrow tip offset in pixels from the default target.', format: null} + rotation: {type: 'number', minimum: 0, title: "Rotation", description: "Rotation of the pointing arrow, in radians. PI / 2 points left, PI points up, etc."} + sides: c.array {title: "Sides", description: "Which sides of the target element to point at."}, {type: 'string', 'enum': ['left', 'right', 'top', 'bottom'], title: "Side", description: "A side of the target element to point at."} + lock: {title: "Lock", description: "Whether the interface should be locked so that the player's focus is on the script, or specific areas to lock.", type: ['boolean', 'array'], items: {type: 'string', enum: ['surface', 'editor', 'palette', 'hud', 'playback', 'playback-hover', 'level', ]}} + letterbox: {type: 'boolean', title: 'Letterbox', description:'Turn letterbox mode on or off. Disables surface and playback controls.'} + + goals: c.object {title: "Goals", description: "Add or remove goals for the player to complete in the level."}, + add: c.array {title: "Add", description: "Add these goals."}, GoalSchema + remove: c.array {title: "Remove", description: "Remove these goals."}, GoalSchema + + playback: c.object {title: "Playback", description: "Control the playback of the level."}, + playing: {type: 'boolean', title: "Set Playing", description: "Set whether playback is playing or paused."} + scrub: c.object {title: "Scrub", description: "Scrub the level playback time to a certain point.", default: {offset: 2, duration: 1000, toRatio: 0.5}}, + offset: {type: 'integer', title: "Offset", description: "Number of frames by which to adjust the scrub target time.", default: 2} + duration: {type: 'integer', title: "Duration", description: "Number of milliseconds over which to scrub time.", minimum: 0, format: 'milliseconds'} + toRatio: {type: 'number', title: "To Progress Ratio", description: "Set playback time to a target playback progress ratio.", minimum: 0, maximum: 1} + toTime: {type: 'number', title: "To Time", description: "Set playback time to a target playback point, in seconds.", minimum: 0} + toGoal: c.shortString(title: "To Goal", description: "Set playback time to when this goal was achieved. (TODO: not implemented.)") + + script: c.object {title: "Script", description: "Extra configuration for this action group."}, + duration: {type: 'integer', minimum: 0, title: "Duration", description: "How long this script should last in milliseconds. 0 for indefinite.", format: 'milliseconds'} + skippable: {type: 'boolean', title: "Skippable", description: "Whether this script shouldn't bother firing when the player skips past all current scripts."} + beforeLoad: {type: 'boolean', title: "Before Load", description: "Whether this script should fire before the level is finished loading."} + + sprites: c.array {title: "Sprites", description: "Commands to issue to Sprites on the Surface."}, SpriteCommandSchema + + surface: c.object {title: "Surface", description: "Commands to issue to the Surface itself."}, + focus: c.object {title: "Camera", description: "Focus the camera on a specific point on the Surface.", format:'viewport'}, + target: {anyOf: [PointSchema, thang, {type: 'null'}], title: "Target", description: "Where to center the camera view."} + zoom: {type: 'number', minimum: 0, exclusiveMinimum: true, maximum: 64, title: "Zoom", description: "What zoom level to use."} + duration: {type:'number', minimum: 0, title: "Duration", description: "in ms"} + bounds: c.array {title:'Boundary', maxItems: 2, minItems: 2, default:[{x:0,y:0}, {x:46, y:39}], format: 'bounds'}, PointSchema + isNewDefault: {type:'boolean', format: 'hidden', title: "New Default", description: 'Set this as new default zoom once scripts end.'} # deprecated + highlight: c.object {title: "Highlight", description: "Highlight specific Sprites on the Surface."}, + targets: c.array {title: "Targets", description: "Thang IDs of target Sprites to highlight."}, thang + delay: {type: 'integer', minimum: 0, title: "Delay", description: "Delay in milliseconds before the highlight appears."} + lockSelect: {type: 'boolean', title: "Lock Select", description: "Whether to lock Sprite selection so that the player can't select/deselect anything."} + + sound: c.object {title: "Sound", description: "Commands to control sound playback."}, + suppressSelectionSounds: {type: "boolean", title: "Suppress Selection Sounds", description: "Whether to suppress selection sounds made from clicking on Thangs."} + music: c.object { title: "Music", description: "Control music playing"}, + play: { title: "Play", type: "boolean" } + file: c.shortString(title: "File", enum:['/music/music_level_1','/music/music_level_2','/music/music_level_3','/music/music_level_4','/music/music_level_5']) + +ScriptSchema = c.object { + title: "Script" + description: 'A script fires off a chain of notes to interact with the game when a certain event triggers it.' + required: ["channel"] + 'default': {channel: "world:won", noteChain: []} +}, + id: c.shortString(title: "ID", description: "A unique ID that other scripts can rely on in their Happens After prereqs, for sequencing.") # uniqueness? + channel: c.shortString(title: "Event", format: 'event-channel', description: 'Event channel this script might trigger for, like "world:won".') + eventPrereqs: c.array {title: "Event Checks", description: "Logical checks on the event for this script to trigger.", format:'event-prereqs'}, EventPrereqSchema + repeats: {title: "Repeats", description: "Whether this script can trigger more than once during a level.", enum: [true, false, 'session'], "default": false} + scriptPrereqs: c.array {title: "Happens After", description: "Scripts that need to fire first."}, + c.shortString(title: "ID", description: "A unique ID of a script.") + notAfter: c.array {title: "Not After", description: "Do not run this script if any of these scripts have run."}, + c.shortString(title: "ID", description: "A unique ID of a script.") + noteChain: c.array {title: "Actions", description: "A list of things that happen when this script triggers."}, NoteGroupSchema + +LevelThangSchema = c.object { + title: "Thang", + description: "Thangs are any units, doodads, or abstract things that you use to build the level. (\"Thing\" was too confusing to say.)", + format: "thang" + required: ["id", "thangType", "components"] + 'default': + id: "Boris" + thangType: "Soldier" + components: [] +}, + id: thang # TODO: figure out if we can make this unique and how to set dynamic defaults + # TODO: split thangType into "original" and "majorVersion" like the rest for consistency + thangType: c.objectId(links: [{rel: "db", href: "/db/thang.type/{($)}/version"}], title: "Thang Type", description: "A reference to the original Thang template being configured.", format: 'thang-type') + 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 + +LevelSystemSchema = c.object { + title: "System" + description: "Configuration for a System that this Level uses." + format: 'level-system' + required: ['original', 'majorVersion'] + 'default': + majorVersion: 0 + config: {} + links: [{rel: "db", href: "/db/level.system/{(original)}/version/{(majorVersion)}"}] +}, + original: c.objectId(title: "Original", description: "A reference to the original System being configured.", format: "hidden") + config: c.object {title: "Configuration", description: "System-specific configuration properties.", additionalProperties: true, format: 'level-system-configuration'} + majorVersion: {title: "Major Version", description: "Which major version of the System is being used.", type: 'integer', minimum: 0, default: 0, format: "hidden"} + +GeneralArticleSchema = c.object { + title: "Article" + description: "Reference to a general documentation article." + required: ['original'] + format: 'latest-version-reference' + 'default': + original: null + majorVersion: 0 + links: [{rel: "db", href: "/db/article/{(original)}/version/{(majorVersion)}"}] +}, + original: c.objectId(title: "Original", description: "A reference to the original Article.")#, format: "hidden") # hidden? + majorVersion: {title: "Major Version", description: "Which major version of the Article is being used.", type: 'integer', minimum: 0}#, format: "hidden"} # hidden? + +LevelSchema = c.object { + title: "Level" + description: "A spectacular level which will delight and educate its stalwart players with the sorcery of coding." + required: ["name", "description", "scripts", "thangs", "documentation"] + 'default': + name: "Ineffable Wizardry" + description: "This level is indescribably flarmy." + documentation: {specificArticles: [], generalArticles: []} + scripts: [] + thangs: [] +} +c.extendNamedProperties LevelSchema # let's have the name be the first property +_.extend LevelSchema.properties, + description: {title: "Description", description: "A short explanation of what this level is about.", type: "string", maxLength: 65536, "default": "This level is indescribably flarmy!", format: 'markdown'} + documentation: c.object {title: "Documentation", description: "Documentation articles relating to this level.", required: ["specificArticles", "generalArticles"], 'default': {specificArticles: [], generalArticles: []}}, + specificArticles: c.array {title: "Specific Articles", description: "Specific documentation articles that live only in this level.", uniqueItems: true, "default": []}, SpecificArticleSchema + generalArticles: c.array {title: "General Articles", description: "General documentation articles that can be linked from multiple levels.", uniqueItems: true, "default": []}, GeneralArticleSchema + background: c.objectId({format: 'hidden'}) + nextLevel: { + type:'object', + links: [{rel: "extra", href: "/db/level/{($)}"}, {rel:'db', href: "/db/level/{(original)}/version/{(majorVersion)}"}], + format: 'latest-version-reference', + title: "Next Level", + description: "Reference to the next level players will player after beating this one." + } + scripts: c.array {title: "Scripts", description: "An array of scripts that trigger based on what the player does and affect things outside of the core level simulation.", "default": []}, ScriptSchema + thangs: c.array {title: "Thangs", description: "An array of Thangs that make up the level.", "default": []}, LevelThangSchema + systems: c.array {title: "Systems", description: "Levels are configured by changing the Systems attached to them.", uniqueItems: true, default: []}, LevelSystemSchema # TODO: uniqueness should be based on "original", not whole thing + victory: c.object {title: "Victory Screen", default: {}, properties: {'body': {type: 'string', format: 'markdown', title: 'Body Text', description: 'Inserted into the Victory Modal once this level is complete. Tell the player they did a good job and what they accomplished!'}, i18n: {type: "object", format: 'i18n', props: ['body'], description: "Help translate this victory message"}}} + i18n: {type: "object", format: 'i18n', props: ['name', 'description'], description: "Help translate this level"} + icon: { type: 'string', format: 'image-file', title: 'Icon' } + goals: c.array {title: 'Goals', description: 'An array of goals which are visible to the player and can trigger scripts.'}, GoalSchema + type: c.shortString(title: "Type", description: "What kind of level this is.", "enum": ['campaign', 'ladder', 'ladder-tutorial']) + showsGuide: c.shortString(title: "Shows Guide", description: "If the guide is shown at the beginning of the level.", "enum": ['first-time', 'always']) + +c.extendBasicProperties LevelSchema, 'level' +c.extendSearchableProperties LevelSchema +c.extendVersionedProperties LevelSchema, 'level' +c.extendPermissionsProperties LevelSchema, 'level' + +module.exports = LevelSchema + +# To test: +# 1: Copy the schema from http://localhost:3000/db/level/schema +# 2. Open up the Treema demo page http://localhost:9090/demo.html +# 3. tv4.addSchema(metaschema.id, metaschema) +# 4. S = <paste big schema here> +# 5. tv4.validateMultiple(S, metaschema) and look for errors diff --git a/app/schemas/level_session_schema.coffee b/app/schemas/level_session_schema.coffee new file mode 100644 index 000000000..4244c4771 --- /dev/null +++ b/app/schemas/level_session_schema.coffee @@ -0,0 +1,213 @@ +c = require './schemas' + +LevelSessionPlayerSchema = c.object + id: c.objectId + links: [ + { + rel: 'extra' + href: "/db/user/{($)}" + } + ] + time: + type: 'Number' + changes: + type: 'Number' + + +LevelSessionLevelSchema = c.object {required: ['original', 'majorVersion']}, + original: c.objectId({}) + majorVersion: + type: 'integer' + minimum: 0 + default: 0 + + +LevelSessionSchema = c.object + title: "Session" + description: "A single session for a given level." + + +_.extend LevelSessionSchema.properties, + # denormalization + creatorName: + type: 'string' + levelName: + type: 'string' + levelID: + type: 'string' + multiplayer: + type: 'boolean' + creator: c.objectId + links: + [ + { + rel: 'extra' + href: "/db/user/{($)}" + } + ] + created: c.date + title: 'Created' + readOnly: true + + changed: c.date + title: 'Changed' + readOnly: true + + team: c.shortString() + level: LevelSessionLevelSchema + + screenshot: + type: 'string' + + state: c.object {}, + complete: + type: 'boolean' + scripts: c.object {}, + ended: + type: 'object' + additionalProperties: + type: 'number' + currentScript: + type: [ + 'null' + 'string' + ] + currentScriptOffset: + type: 'number' + + selected: + type: [ + 'null' + 'string' + ] + playing: + type: 'boolean' + frame: + type: 'number' + thangs: + type: 'object' + additionalProperties: + title: 'Thang' + type: 'object' + properties: + methods: + type: 'object' + additionalProperties: + title: 'Thang Method' + type: 'object' + properties: + metrics: + type: 'object' + source: + type: 'string' + +# TODO: specify this more + code: + type: 'object' + + teamSpells: + type: 'object' + additionalProperties: + type: 'array' + + players: + type: 'object' + + chat: + type: 'array' + + meanStrength: + type: 'number' + + standardDeviation: + type:'number' + minimum: 0 + + totalScore: + type: 'number' + + submitted: + type: 'boolean' + + submitDate: c.date + title: 'Submitted' + + submittedCode: + type: 'object' + + isRanking: + type: 'boolean' + description: 'Whether this session is still in the first ranking chain after being submitted.' + + unsubscribed: + type: 'boolean' + description: 'Whether the player has opted out of receiving email updates about ladder rankings for this session.' + + numberOfWinsAndTies: + type: 'number' + + numberOfLosses: + type: 'number' + + scoreHistory: + type: 'array' + title: 'Score History' + description: 'A list of objects representing the score history of a session' + items: + title: 'Score History Point' + description: 'An array with the format [unix timestamp, totalScore]' + type: 'array' + items: + type: 'number' + + matches: + type: 'array' + title: 'Matches' + description: 'All of the matches a submitted session has played in its current state.' + items: + type: 'object' + properties: + date: c.date + title: 'Date computed' + description: 'The date a match was computed.' + metrics: + type: 'object' + title: 'Metrics' + description: 'Various information about the outcome of a match.' + properties: + rank: + title: 'Rank' + description: 'A 0-indexed ranking representing the player\'s standing in the outcome of a match' + type: 'number' + opponents: + type: 'array' + title: 'Opponents' + description: 'An array containing information about the opponents\' sessions in a given match.' + items: + type: 'object' + properties: + sessionID: + title: 'Opponent Session ID' + description: 'The session ID of an opponent.' + type: ['object', 'string'] + userID: + title: 'Opponent User ID' + description: 'The user ID of an opponent' + type: ['object','string'] + metrics: + type: 'object' + properties: + rank: + title: 'Opponent Rank' + description: 'The opponent\'s ranking in a given match' + type: 'number' + + + + + + +c.extendBasicProperties LevelSessionSchema, 'level.session' +c.extendPermissionsProperties LevelSessionSchema, 'level.session' + +module.exports = LevelSessionSchema diff --git a/app/schemas/level_system_schema.coffee b/app/schemas/level_system_schema.coffee new file mode 100644 index 000000000..0d7cad2c0 --- /dev/null +++ b/app/schemas/level_system_schema.coffee @@ -0,0 +1,106 @@ +c = require './schemas' +metaschema = require './metaschema' + +jitterSystemCode = """ +class Jitter extends System + constructor: (world, config) -> + super world, config + @idlers = @addRegistry (thang) -> thang.exists and thang.acts and thang.moves and thang.action is 'idle' + + update: -> + # We return a simple numeric hash that will combine to a frame hash + # help us determine whether this frame has changed in resimulations. + hash = 0 + for thang in @idlers + hash += thang.pos.x += 0.5 - Math.random() + hash += thang.pos.y += 0.5 - Math.random() + thang.hasMoved = true + return hash +""" + +PropertyDocumentationSchema = c.object { + title: "Property Documentation" + description: "Documentation entry for a property this System will add to its Thang which other Systems + might want to also use." + "default": + name: "foo" + type: "object" + description: "This System provides a 'foo' property to satisfy all one's foobar needs. Use it wisely." + required: ['name', 'type', 'description'] +}, + name: {type: 'string', pattern: c.identifierPattern, title: "Name", description: "Name of the property."} + # not actual JS types, just whatever they describe... + type: c.shortString(title: "Type", description: "Intended type of the property.") + description: {type: 'string', description: "Description of the property.", maxLength: 1000} + args: c.array {title: "Arguments", description: "If this property has type 'function', then provide documentation for any function arguments."}, c.FunctionArgumentSchema + +DependencySchema = c.object { + title: "System Dependency" + description: "A System upon which this System depends." + "default": + #original: ? + majorVersion: 0 + required: ["original", "majorVersion"] + format: 'latest-version-reference' + links: [{rel: "db", href: "/db/level.system/{(original)}/version/{(majorVersion)}"}] +}, + original: c.objectId(title: "Original", description: "A reference to another System upon which this System depends.") + majorVersion: + title: "Major Version" + description: "Which major version of the System this System needs." + type: 'integer' + minimum: 0 + +LevelSystemSchema = c.object { + title: "System" + description: "A System which can affect Level behavior." + required: ["name", "description", "code", "dependencies", "propertyDocumentation", "language"] + "default": + name: "JitterSystem" + description: "This System makes all idle, movable Thangs jitter around." + code: jitterSystemCode + language: "coffeescript" + dependencies: [] # TODO: should depend on something by default + propertyDocumentation: [] +} +c.extendNamedProperties LevelSystemSchema # let's have the name be the first property +LevelSystemSchema.properties.name.pattern = c.classNamePattern +_.extend LevelSystemSchema.properties, + description: + title: "Description" + description: "A short explanation of what this System does." + type: "string" + maxLength: 2000 + "default": "This System doesn't do anything yet." + language: + type: "string" + title: "Language" + description: "Which programming language this System is written in." + "enum": ["coffeescript"] + code: + title: "Code" + description: "The code for this System, as a CoffeeScript class. TODO: add link to documentation + for how to write these." + "default": jitterSystemCode + type: "string" + format: "coffee" + js: + title: "JavaScript" + description: "The transpiled JavaScript code for this System" + type: "string" + format: "hidden" + dependencies: c.array {title: "Dependencies", description: "An array of Systems upon which this System depends.", "default": [], uniqueItems: true}, DependencySchema + propertyDocumentation: c.array {title: "Property Documentation", description: "An array of documentation entries for each notable property this System will add to its Level which other Systems might want to also use.", "default": []}, PropertyDocumentationSchema + configSchema: _.extend metaschema, {title: "Configuration Schema", description: "A schema for validating the arguments that can be passed to this System as configuration.", default: {type: 'object', additionalProperties: false}} + official: + type: "boolean" + title: "Official" + description: "Whether this is an official CodeCombat System." + "default": false + +c.extendBasicProperties LevelSystemSchema, 'level.system' +c.extendSearchableProperties LevelSystemSchema +c.extendVersionedProperties LevelSystemSchema, 'level.system' +c.extendPermissionsProperties LevelSystemSchema, 'level.system' + +module.exports = LevelSystemSchema diff --git a/app/schemas/metaschema.coffee b/app/schemas/metaschema.coffee new file mode 100644 index 000000000..4d9d7c0d8 --- /dev/null +++ b/app/schemas/metaschema.coffee @@ -0,0 +1,132 @@ +# The JSON Schema Core/Validation Meta-Schema, but with titles and descriptions added to make it easier to edit in Treema, and in CoffeeScript + +module.exports = + id: "metaschema" + displayProperty: "title" + $schema: "http://json-schema.org/draft-04/schema#" + title: "Schema" + description: "Core schema meta-schema" + definitions: + schemaArray: + type: "array" + minItems: 1 + items: { $ref: "#" } + title: "Array of Schemas" + "default": [{}] + positiveInteger: + type: "integer" + minimum: 0 + title: "Positive Integer" + positiveIntegerDefault0: + allOf: [ { $ref: "#/definitions/positiveInteger" }, { "default": 0 } ] + simpleTypes: + title: "Single Type" + "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] + stringArray: + type: "array" + items: { type: "string" } + minItems: 1 + uniqueItems: true + title: "String Array" + "default": [''] + type: "object" + properties: + id: + type: "string" + format: "uri" + $schema: + type: "string" + format: "uri" + "default": "http://json-schema.org/draft-04/schema#" + title: + type: "string" + description: + type: "string" + "default": {} + multipleOf: + type: "number" + minimum: 0 + exclusiveMinimum: true + maximum: + type: "number" + exclusiveMaximum: + type: "boolean" + "default": false + minimum: + type: "number" + exclusiveMinimum: + type: "boolean" + "default": false + maxLength: { $ref: "#/definitions/positiveInteger" } + minLength: { $ref: "#/definitions/positiveIntegerDefault0" } + pattern: + type: "string" + format: "regex" + additionalItems: + anyOf: [ + { type: "boolean", "default": false } + { $ref: "#" } + ] + items: + anyOf: [ + { $ref: "#" } + { $ref: "#/definitions/schemaArray" } + ] + "default": {} + maxItems: { $ref: "#/definitions/positiveInteger" } + minItems: { $ref: "#/definitions/positiveIntegerDefault0" } + uniqueItems: + type: "boolean" + "default": false + maxProperties: { $ref: "#/definitions/positiveInteger" } + minProperties: { $ref: "#/definitions/positiveIntegerDefault0" } + required: { $ref: "#/definitions/stringArray" } + additionalProperties: + anyOf: [ + { type: "boolean", "default": true } + { $ref: "#" } + ] + "default": {} + definitions: + type: "object" + additionalProperties: { $ref: "#" } + "default": {} + properties: + type: "object" + additionalProperties: { $ref: "#" } + "default": {} + patternProperties: + type: "object" + additionalProperties: { $ref: "#" } + "default": {} + dependencies: + type: "object" + additionalProperties: + anyOf: [ + { $ref: "#" } + { $ref: "#/definitions/stringArray" } + ] + "enum": + type: "array" + minItems: 1 + uniqueItems: true + "default": [''] + type: + anyOf: [ + { $ref: "#/definitions/simpleTypes" } + { + type: "array" + items: { $ref: "#/definitions/simpleTypes" } + minItems: 1 + uniqueItems: true + title: "Array of Types" + "default": ['string'] + }] + allOf: { $ref: "#/definitions/schemaArray" } + anyOf: { $ref: "#/definitions/schemaArray" } + oneOf: { $ref: "#/definitions/schemaArray" } + not: { $ref: "#" } + dependencies: + exclusiveMaximum: [ "maximum" ] + exclusiveMinimum: [ "minimum" ] + "default": {} diff --git a/app/schemas/schemas.coffee b/app/schemas/schemas.coffee new file mode 100644 index 000000000..eebe6f954 --- /dev/null +++ b/app/schemas/schemas.coffee @@ -0,0 +1,158 @@ +#language imports +Language = require './languages' +# schema helper methods + +me = module.exports + +combine = (base, ext) -> + return base unless ext? + return _.extend(base, ext) + +urlPattern = '^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&%\$#_=]*)?$' + +# Common schema properties +me.object = (ext, props) -> combine {type: 'object', additionalProperties: false, properties: props or {}}, ext +me.array = (ext, items) -> combine {type: 'array', items: items or {}}, ext +me.shortString = (ext) -> combine({type: 'string', maxLength: 100}, ext) +me.pct = (ext) -> combine({type: 'number', maximum: 1.0, minimum: 0.0}, ext) +me.date = (ext) -> combine({type: 'string', format: 'date-time'}, ext) +# should just be string (Mongo ID), but sometimes mongoose turns them into objects representing those, so we are lenient +me.objectId = (ext) -> schema = combine({type: ['object', 'string'] }, ext) +me.url = (ext) -> combine({type: 'string', format: 'url', pattern: urlPattern}, ext) + +PointSchema = me.object {title: "Point", description: "An {x, y} coordinate point.", format: "point2d", required: ["x", "y"]}, + x: {title: "x", description: "The x coordinate.", type: "number", "default": 15} + y: {title: "y", description: "The y coordinate.", type: "number", "default": 20} + +me.point2d = (ext) -> combine(_.cloneDeep(PointSchema), ext) + +SoundSchema = me.object { format: 'sound' }, + mp3: { type: 'string', format: 'sound-file' } + ogg: { type: 'string', format: 'sound-file' } + +me.sound = (props) -> + obj = _.cloneDeep(SoundSchema) + obj.properties[prop] = props[prop] for prop of props + obj + +ColorConfigSchema = me.object { format: 'color-sound' }, + hue: { format: 'range', type: 'number', minimum: 0, maximum: 1 } + saturation: { format: 'range', type: 'number', minimum: 0, maximum: 1 } + lightness: { format: 'range', type: 'number', minimum: 0, maximum: 1 } + +me.colorConfig = (props) -> + obj = _.cloneDeep(ColorConfigSchema) + obj.properties[prop] = props[prop] for prop of props + obj + +# BASICS + +basicProps = (linkFragment) -> + _id: me.objectId(links: [{rel: 'self', href: "/db/#{linkFragment}/{($)}"}], format: 'hidden') + __v: { title: 'Mongoose Version', format: 'hidden' } + +me.extendBasicProperties = (schema, linkFragment) -> + schema.properties = {} unless schema.properties? + _.extend(schema.properties, basicProps(linkFragment)) + + +# NAMED + +namedProps = -> + name: me.shortString({title: 'Name'}) + slug: me.shortString({title: 'Slug', format: 'hidden'}) + +me.extendNamedProperties = (schema) -> + schema.properties = {} unless schema.properties? + _.extend(schema.properties, namedProps()) + + +# VERSIONED + +versionedProps = (linkFragment) -> + version: + 'default': { minor: 0, major: 0, isLatestMajor: true, isLatestMinor: true } + format: 'version' + title: 'Version' + type: 'object' + readOnly: true + additionalProperties: false + properties: + major: { type: 'number', minimum: 0 } + minor: { type: 'number', minimum: 0 } + isLatestMajor: { type: 'boolean' } + isLatestMinor: { type: 'boolean' } + # TODO: figure out useful 'rel' values here + original: me.objectId(links: [{rel: 'extra', href: "/db/#{linkFragment}/{($)}"}], format: 'hidden') + parent: me.objectId(links: [{rel: 'extra', href: "/db/#{linkFragment}/{($)}"}], format: 'hidden') + creator: me.objectId(links: [{rel: 'extra', href: "/db/user/{($)}"}], format: 'hidden') + created: me.date( { title: 'Created', readOnly: true }) + commitMessage: { type: 'string', maxLength: 500, title: 'Commit Message', readOnly: true } + +me.extendVersionedProperties = (schema, linkFragment) -> + schema.properties = {} unless schema.properties? + _.extend(schema.properties, versionedProps(linkFragment)) + + +# SEARCHABLE + +searchableProps = -> + index: { format: 'hidden' } + +me.extendSearchableProperties = (schema) -> + schema.properties = {} unless schema.properties? + _.extend(schema.properties, searchableProps()) + + +# PERMISSIONED + +permissionsProps = -> + permissions: + type: 'array' + items: + type: 'object' + additionalProperties: false + properties: + target: {} + access: {type: 'string', 'enum': ['read', 'write', 'owner']} + format: "hidden" + +me.extendPermissionsProperties = (schema) -> + schema.properties = {} unless schema.properties? + _.extend(schema.properties, permissionsProps()) + +# TRANSLATABLE + +me.generateLanguageCodeArrayRegex = -> "^(" + Language.languageCodes.join("|") + ")$" + +me.getLanguageCodeArray = -> + return Language.languageCodes + +me.getLanguagesObject = -> return Language + +# OTHER + +me.classNamePattern = "^[A-Z][A-Za-z0-9]*$" # starts with capital letter; just letters and numbers +me.identifierPattern = "^[a-z][A-Za-z0-9]*$" # starts with lowercase letter; just letters and numbers +me.constantPattern = "^[A-Z0-9_]+$" # just uppercase letters, underscores, and numbers +me.identifierOrConstantPattern = "^([a-z][A-Za-z0-9]*|[A-Z0-9_]+)$" + +me.FunctionArgumentSchema = me.object { + title: "Function Argument", + description: "Documentation entry for a function argument." + "default": + name: "target" + type: "object" + example: "this.getNearestEnemy()" + description: "The target of this function." + required: ['name', 'type', 'example', 'description'] +}, + name: {type: 'string', pattern: me.identifierPattern, title: "Name", description: "Name of the function argument."} + # not actual JS types, just whatever they describe... + type: me.shortString(title: "Type", description: "Intended type of the argument.") + example: me.shortString(title: "Example", description: "Example value for the argument.") + description: {title: "Description", type: 'string', description: "Description of the argument.", maxLength: 1000} + "default": + title: "Default" + description: "Default value of the argument. (Your code should set this.)" + "default": null diff --git a/app/schemas/thang_component_schema.coffee b/app/schemas/thang_component_schema.coffee new file mode 100644 index 000000000..b6d574fdc --- /dev/null +++ b/app/schemas/thang_component_schema.coffee @@ -0,0 +1,21 @@ +c = require './schemas' + +module.exports = ThangComponentSchema = c.object { + title: "Component" + description: "Configuration for a Component that this Thang uses." + format: 'thang-component' + required: ['original', 'majorVersion'] + 'default': + majorVersion: 0 + config: {} + links: [{rel: "db", href: "/db/level.component/{(original)}/version/{(majorVersion)}"}] +}, + original: c.objectId(title: "Original", description: "A reference to the original Component being configured.", format: "hidden") + config: c.object {title: "Configuration", description: "Component-specific configuration properties.", additionalProperties: true, format: 'thang-component-configuration'} + majorVersion: + title: "Major Version" + description: "Which major version of the Component is being used." + type: 'integer' + minimum: 0 + default: 0 + format: "hidden" diff --git a/app/schemas/thang_type_schema.coffee b/app/schemas/thang_type_schema.coffee new file mode 100644 index 000000000..fb459811d --- /dev/null +++ b/app/schemas/thang_type_schema.coffee @@ -0,0 +1,153 @@ +c = require './schemas' +ThangComponentSchema = require './thang_component_schema' + +ThangTypeSchema = c.object() +c.extendNamedProperties ThangTypeSchema # name first + +ShapeObjectSchema = c.object { title: 'Shape' }, + fc: { type: 'string', title: 'Fill Color' } + lf: { type: 'array', title: 'Linear Gradient Fill' } + ls: { type: 'array', title: 'Linear Gradient Stroke' } + p: { type: 'string', title: 'Path' } + de: { type: 'array', title: 'Draw Ellipse' } + sc: { type: 'string', title: 'Stroke Color' } + ss: { type: 'array', title: 'Stroke Style' } + t: c.array {}, { type: 'number', title: 'Transform' } + m: { type: 'string', title: 'Mask' } + +ContainerObjectSchema = c.object { format: 'container' }, + b: c.array { title: 'Bounds' }, { type: 'number' } + c: c.array { title: 'Children' }, { anyOf: [ + { type: 'string', title: 'Shape Child' }, + c.object { title: 'Container Child' } + gn: { type: 'string', title: 'Global Name' } + t: c.array {}, { type: 'number' } + ]} + +RawAnimationObjectSchema = c.object {}, + bounds: c.array { title: 'Bounds' }, { type: 'number' } + frameBounds: c.array { title: 'Frame Bounds' }, c.array { title: 'Bounds' }, { type: 'number' } + shapes: c.array {}, + bn: { type: 'string', title: 'Block Name' } + gn: { type: 'string', title: 'Global Name' } + im : { type: 'boolean', title: 'Is Mask' } + m: { type: 'string', title: 'Uses Mask' } + containers: c.array {}, + bn: { type: 'string', title: 'Block Name' } + gn: { type: 'string', title: 'Global Name' } + t: c.array {}, { type: 'number' } + o: { type: 'boolean', title: 'Starts Hidden (_off)'} + al: { type: 'number', title: 'Alpha'} + animations: c.array {}, + bn: { type: 'string', title: 'Block Name' } + gn: { type: 'string', title: 'Global Name' } + t: c.array {}, { type: 'number', title: 'Transform' } + a: c.array { title: 'Arguments' } + tweens: c.array {}, + c.array { title: 'Function Chain', }, + c.object { title: 'Function Call' }, + n: { type: 'string', title: 'Name' } + a: c.array { title: 'Arguments' } + graphics: c.array {}, + bn: { type: 'string', title: 'Block Name' } + p: { type: 'string', title: 'Path' } + +PositionsSchema = c.object { title: 'Positions', description: 'Customize position offsets.' }, + registration: c.point2d { title: 'Registration Point', description: "Action-specific registration point override." } + torso: c.point2d { title: 'Torso Offset', description: "Action-specific torso offset override." } + mouth: c.point2d { title: 'Mouth Offset', description: "Action-specific mouth offset override." } + aboveHead: c.point2d { title: 'Above Head Offset', description: "Action-specific above-head offset override." } + +ActionSchema = c.object {}, + animation: { type: 'string', description: 'Raw animation being sourced', format: 'raw-animation' } + container: { type: 'string', description: 'Name of the container to show' } + relatedActions: c.object { }, + begin: { $ref: '#/definitions/action' } + end: { $ref: '#/definitions/action' } + main: { $ref: '#/definitions/action' } + fore: { $ref: '#/definitions/action' } + back: { $ref: '#/definitions/action' } + side: { $ref: '#/definitions/action' } + + "?0?011?11?11": { $ref: '#/definitions/action', title: "NW corner" } + "?0?11011?11?": { $ref: '#/definitions/action', title: "NE corner, flipped" } + "?0?111111111": { $ref: '#/definitions/action', title: "N face" } + "?11011011?0?": { $ref: '#/definitions/action', title: "SW corner, top" } + "11?11?110?0?": { $ref: '#/definitions/action', title: "SE corner, top, flipped" } + "?11011?0????": { $ref: '#/definitions/action', title: "SW corner, bottom" } + "11?110?0????": { $ref: '#/definitions/action', title: "SE corner, bottom, flipped" } + "?11011?11?11": { $ref: '#/definitions/action', title: "W face" } + "11?11011?11?": { $ref: '#/definitions/action', title: "E face, flipped" } + "011111111111": { $ref: '#/definitions/action', title: "NW elbow" } + "110111111111": { $ref: '#/definitions/action', title: "NE elbow, flipped" } + "111111111?0?": { $ref: '#/definitions/action', title: "S face, top" } + "111111?0????": { $ref: '#/definitions/action', title: "S face, bottom" } + "111111111011": { $ref: '#/definitions/action', title: "SW elbow, top" } + "111111111110": { $ref: '#/definitions/action', title: "SE elbow, top, flipped" } + "111111011?11": { $ref: '#/definitions/action', title: "SW elbow, bottom" } + "11111111011?": { $ref: '#/definitions/action', title: "SE elbow, bottom, flipped" } + "111111111111": { $ref: '#/definitions/action', title: "Middle" } + + loops: { type: 'boolean' } + speed: { type: 'number' } + goesTo: { type: 'string', description: 'Action (animation?) to which we switch after this animation.' } + frames: { type: 'string', pattern:'^[0-9,]+$', description: 'Manually way to specify frames.' } + framerate: { type: 'number', description: 'Get this from the HTML output.' } + positions: PositionsSchema + scale: { title: 'Scale', type: 'number' } + flipX: { title: "Flip X", type: 'boolean', description: "Flip this animation horizontally?" } + flipY: { title: "Flip Y", type: 'boolean', description: "Flip this animation vertically?" } + +SoundSchema = c.sound({delay: { type: 'number' }}) + +_.extend ThangTypeSchema.properties, + raw: c.object {title: 'Raw Vector Data'}, + shapes: c.object {title: 'Shapes', additionalProperties: ShapeObjectSchema} + containers: c.object {title: 'Containers', additionalProperties: ContainerObjectSchema} + animations: c.object {title: 'Animations', additionalProperties: RawAnimationObjectSchema} + kind: c.shortString { enum: ['Unit', 'Floor', 'Wall', 'Doodad', 'Misc', 'Mark'], default: 'Misc', title: 'Kind' } + + actions: c.object { title: 'Actions', additionalProperties: { $ref: '#/definitions/action' } } + soundTriggers: c.object { title: "Sound Triggers", additionalProperties: c.array({}, { $ref: '#/definitions/sound' }) }, + say: c.object { format: 'slug-props', additionalProperties: { $ref: '#/definitions/sound' } }, + defaultSimlish: c.array({}, { $ref: '#/definitions/sound' }) + swearingSimlish: c.array({}, { $ref: '#/definitions/sound' }) + rotationType: { title: 'Rotation', type: 'string', enum: ['isometric', 'fixed']} + matchWorldDimensions: { title: 'Match World Dimensions', type: 'boolean' } + shadow: { title: 'Shadow Diameter', type: 'number', format: 'meters', description: "Shadow diameter in meters" } + layerPriority: + title: 'Layer Priority' + type: 'integer' + description: "Within its layer, sprites are sorted by layer priority, then y, then z." + scale: + title: 'Scale' + type: 'number' + positions: PositionsSchema + colorGroups: c.object + title: 'Color Groups' + additionalProperties: + type:'array' + format: 'thang-color-group' + items: {type:'string'} + snap: c.object { title: "Snap", description: "In the level editor, snap positioning to these intervals.", required: ['x', 'y'] }, + x: + title: "Snap X" + type: 'number' + description: "Snap to this many meters in the x-direction." + default: 4 + y: + title: "Snap Y" + type: 'number' + description: "Snap to this many meters in the y-direction." + default: 4 + 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 + +ThangTypeSchema.definitions = + action: ActionSchema + sound: SoundSchema + +c.extendBasicProperties(ThangTypeSchema, 'thang.type') +c.extendSearchableProperties(ThangTypeSchema) +c.extendVersionedProperties(ThangTypeSchema, 'thang.type') + +module.exports = ThangTypeSchema diff --git a/app/schemas/user_schema.coffee b/app/schemas/user_schema.coffee new file mode 100644 index 000000000..a386051f3 --- /dev/null +++ b/app/schemas/user_schema.coffee @@ -0,0 +1,98 @@ +c = require './schemas' +emailSubscriptions = ['announcement', 'tester', 'level_creator', 'developer', 'article_editor', 'translator', 'support', 'notification'] + +UserSchema = c.object {}, + name: c.shortString({title: 'Display Name', default:''}) + email: c.shortString({title: 'Email', format: 'email'}) + firstName: c.shortString({title: 'First Name'}) + lastName: c.shortString({title: 'Last Name'}) + gender: {type: 'string', 'enum': ['male', 'female']} + password: {type: 'string', maxLength: 256, minLength: 2, title:'Password'} + passwordReset: {type: 'string'} + photoURL: {type: 'string', format: 'image-file', title: 'Profile Picture', description: 'Upload a 256x256px or larger image to serve as your profile picture.'} + + facebookID: c.shortString({title: 'Facebook ID'}) + gplusID: c.shortString({title: 'G+ ID'}) + + wizardColor1: c.pct({title: 'Wizard Clothes Color'}) + volume: c.pct({title: 'Volume'}) + music: {type: 'boolean', default: true} + autocastDelay: {type: 'integer', 'default': 5000 } + lastLevel: { type: 'string' } + + emailSubscriptions: c.array {uniqueItems: true, 'default': ['announcement', 'notification']}, {'enum': emailSubscriptions} + + # server controlled + permissions: c.array {'default': []}, c.shortString() + dateCreated: c.date({title: 'Date Joined'}) + anonymous: {type: 'boolean', 'default': true} + testGroupNumber: {type: 'integer', minimum: 0, maximum: 256, exclusiveMaximum: true} + mailChimp: {type: 'object'} + hourOfCode: {type: 'boolean'} + hourOfCodeComplete: {type: 'boolean'} + + emailLower: c.shortString() + nameLower: c.shortString() + passwordHash: {type: 'string', maxLength: 256} + + # client side + emailHash: {type: 'string'} + + #Internationalization stuff + preferredLanguage: {type: 'string', default: 'en', 'enum': c.getLanguageCodeArray()} + + signedCLA: c.date({title: 'Date Signed the CLA'}) + wizard: c.object {}, + colorConfig: c.object {additionalProperties: c.colorConfig()} + + aceConfig: c.object {}, + language: {type: 'string', 'default': 'javascript', 'enum': ['javascript', 'coffeescript']} + keyBindings: {type: 'string', 'default': 'default', 'enum': ['default', 'vim', 'emacs']} + invisibles: {type: 'boolean', 'default': false} + indentGuides: {type: 'boolean', 'default': false} + behaviors: {type: 'boolean', 'default': false} + + simulatedBy: {type: 'integer', minimum: 0, default: 0} + simulatedFor: {type: 'integer', minimum: 0, default: 0} + + jobProfile: c.object {title: 'Job Profile', required: ['lookingFor', 'jobTitle', 'active', 'name', 'city', 'country', 'skills', 'experience', 'shortDescription', 'longDescription', 'visa', 'work', 'education', 'projects', 'links']}, + lookingFor: {title: 'Looking For', type: 'string', enum: ['Full-time', 'Part-time', 'Remote', 'Contracting', 'Internship'], default: 'Full-time', description: 'What kind of developer position do you want?'} + jobTitle: {type: 'string', maxLength: 50, title: 'Desired Job Title', description: 'What role are you looking for? Ex.: "Full Stack Engineer", "Front-End Developer", "iOS Developer"', default: 'Software Developer'} + active: {title: 'Active', type: 'boolean', description: 'Want interview offers right now?'} + updated: c.date {title: 'Last Updated', description: 'How fresh your profile appears to employers. The fresher, the better. Profiles go inactive after 30 days.'} + name: c.shortString {title: 'Name', description: 'Name you want employers to see, like "Nick Winter".'} + city: c.shortString {title: 'City', description: 'City you want to work in (or live in now), like "San Francisco" or "Lubbock, TX".', default: 'Defaultsville, CA', format: 'city'} + country: c.shortString {title: 'Country', description: 'Country you want to work in (or live in now), like "USA" or "France".', default: 'USA', format: 'country'} + skills: c.array {title: 'Skills', description: 'Tag relevant developer skills in order of proficiency. Employers will see the first five at a glance.', default: ['javascript'], minItems: 1, maxItems: 30, uniqueItems: true}, + {type: 'string', minLength: 1, maxLength: 20, description: 'Ex.: "objective-c", "mongodb", "rails", "android", "javascript"', format: 'skill'} + experience: {type: 'integer', title: 'Years of Experience', minimum: 0, description: 'How many years of professional experience (getting paid) developing software do you have?'} + shortDescription: {type: 'string', maxLength: 140, title: 'Short Description', description: 'Who are you, and what are you looking for? 140 characters max.', default: 'Programmer seeking to build great software.'} + longDescription: {type: 'string', maxLength: 600, title: 'Description', description: 'Describe yourself to potential employers. Keep it short and to the point. We recommend outlining the position that would most interest you. Tasteful markdown okay; 600 characters max.', format: 'markdown', default: '* I write great code.\n* You need great code?\n* Great!'} + visa: c.shortString {title: 'US Work Status', description: 'Are you authorized to work in the US, or do you need visa sponsorship?', enum: ['Authorized to work in the US', 'Need visa sponsorship'], default: 'Authorized to work in the US'} + work: c.array {title: 'Work Experience', description: 'List your relevant work experience, most recent first.'}, + c.object {title: 'Job', description: 'Some work experience you had.', required: ['employer', 'role', 'duration']}, + employer: c.shortString {title: 'Employer', description: 'Name of your employer.'} + role: c.shortString {title: 'Job Title', description: 'What was your job title or role?'} + duration: c.shortString {title: 'Duration', description: 'When did you hold this gig? Ex.: "Feb 2013 - present".'} + education: c.array {title: 'Education', description: 'List your academic ordeals.'}, + c.object {title: 'Ordeal', description: 'Some education that befell you.', required: ['school', 'degree', 'duration']}, + school: c.shortString {title: 'School', description: 'Name of your school.'} + degree: c.shortString {title: 'Degree', description: 'What was your degree and field of study? Ex. Ph.D. Human-Computer Interaction (incomplete)'} + duration: c.shortString {title: 'Dates', description: 'When? Ex.: "Aug 2004 - May 2008".'} + projects: c.array {title: 'Projects', description: 'Highlight your projects to amaze employers.'}, + c.object {title: 'Project', description: 'A project you created.', required: ['name', 'description', 'picture'], default: {name: 'My Project', description: 'A project I worked on.', link: 'http://example.com', picture: ''}}, + name: c.shortString {title: 'Project Name', description: 'What was the project called?', default: 'My Project'} + description: {type: 'string', title: 'Description', description: 'Briefly describe the project.', maxLength: 400, default: 'A project I worked on.', format: 'markdown'} + picture: {type: 'string', title: 'Picture', format: 'image-file', description: 'Upload a 230x115px or larger image showing off the project.'} + link: c.url {title: 'Link', description: 'Link to the project.', default: 'http://example.com'} + links: c.array {title: 'Personal and Social Links', description: 'Link any other sites or profiles you want to highlight, like your GitHub, your LinkedIn, or your blog.'}, + c.object {title: 'Link', description: 'A link to another site you want to highlight, like your GitHub, your LinkedIn, or your blog.', required: ['name', 'link']}, + name: {type: 'string', maxLength: 30, title: 'Link Name', description: 'What are you linking to? Ex: "Personal Website", "Twitter"', format: 'link-name'} + link: c.url {title: 'Link', description: 'The URL.', default: 'http://example.com'} + photoURL: {type: 'string', format: 'image-file', title: 'Profile Picture', description: 'Upload a 256x256px or larger image if you want to show a different profile picture to employers than your normal avatar.'} + + jobProfileApproved: {title: 'Job Profile Approved', type: 'boolean', description: 'Whether your profile has been approved by CodeCombat.'} + jobProfileNotes: {type: 'string', maxLength: 1000, title: 'Our Notes', description: "CodeCombat's notes on the candidate.", format: 'markdown', default: ''} +c.extendBasicProperties UserSchema, 'user' + +module.exports = UserSchema diff --git a/app/views/account/settings_view.coffee b/app/views/account/settings_view.coffee index c75225d69..e74db5f65 100644 --- a/app/views/account/settings_view.coffee +++ b/app/views/account/settings_view.coffee @@ -82,8 +82,8 @@ module.exports = class SettingsView extends View buildPictureTreema: -> data = photoURL: me.get('photoURL') data.photoURL = null if data.photoURL?.search('gravatar') isnt -1 # Old style - schema = _.cloneDeep me.schema().attributes - schema.properties = _.pick me.schema().get('properties'), 'photoURL' + schema = _.cloneDeep me.schema() + schema.properties = _.pick me.schema().properties, 'photoURL' schema.required = ['photoURL'] treemaOptions = filePath: "db/user/#{me.id}" diff --git a/app/views/editor/article/edit.coffee b/app/views/editor/article/edit.coffee index 1d91558f1..b7a06bd36 100644 --- a/app/views/editor/article/edit.coffee +++ b/app/views/editor/article/edit.coffee @@ -54,7 +54,7 @@ module.exports = class ArticleEditView extends View options = data: data filePath: "db/thang.type/#{@article.get('original')}" - schema: Article.schema.attributes + schema: Article.schema readOnly: true unless me.isAdmin() or @article.hasWriteAccess(me) callbacks: change: @pushChangesToPreview diff --git a/app/views/editor/components/main.coffee b/app/views/editor/components/main.coffee index 7b813595b..2c39b6086 100644 --- a/app/views/editor/components/main.coffee +++ b/app/views/editor/components/main.coffee @@ -45,7 +45,7 @@ module.exports = class ThangComponentEditView extends CocoView buildExtantComponentTreema: -> treemaOptions = supermodel: @supermodel - schema: Level.schema.get('properties').thangs.items.properties.components + schema: Level.schema.properties.thangs.items.properties.components data: _.cloneDeep @components callbacks: {select: @onSelectExtantComponent, change:@onChangeExtantComponents} noSortable: true @@ -69,7 +69,7 @@ module.exports = class ThangComponentEditView extends CocoView treemaOptions = supermodel: @supermodel - schema: { type: 'array', items: LevelComponent.schema.attributes } + schema: { type: 'array', items: LevelComponent.schema } data: ($.extend(true, {}, c) for c in components) callbacks: {select: @onSelectAddableComponent, enter: @onAddComponentEnterPressed } readOnly: true diff --git a/app/views/editor/level/component/edit.coffee b/app/views/editor/level/component/edit.coffee index 7571e9a80..3f91f1467 100644 --- a/app/views/editor/level/component/edit.coffee +++ b/app/views/editor/level/component/edit.coffee @@ -31,7 +31,7 @@ module.exports = class LevelComponentEditView extends View buildSettingsTreema: -> data = _.pick @levelComponent.attributes, (value, key) => key in @editableSettings - schema = _.cloneDeep LevelComponent.schema.attributes + schema = _.cloneDeep LevelComponent.schema schema.properties = _.pick schema.properties, (value, key) => key in @editableSettings schema.required = _.intersection schema.required, @editableSettings @@ -55,7 +55,7 @@ module.exports = class LevelComponentEditView extends View buildConfigSchemaTreema: -> treemaOptions = supermodel: @supermodel - schema: LevelComponent.schema.get('properties').configSchema + schema: LevelComponent.schema.properties.configSchema data: @levelComponent.get 'configSchema' callbacks: {change: @onConfigSchemaEdited} treemaOptions.readOnly = true unless me.isAdmin() @@ -63,7 +63,7 @@ module.exports = class LevelComponentEditView extends View @configSchemaTreema.build() @configSchemaTreema.open() # TODO: schema is not loaded for the first one here? - @configSchemaTreema.tv4.addSchema('metaschema', LevelComponent.schema.get('properties').configSchema) + @configSchemaTreema.tv4.addSchema('metaschema', LevelComponent.schema.properties.configSchema) onConfigSchemaEdited: => @levelComponent.set 'configSchema', @configSchemaTreema.data diff --git a/app/views/editor/level/scripts_tab_view.coffee b/app/views/editor/level/scripts_tab_view.coffee index 380902d27..03855662d 100644 --- a/app/views/editor/level/scripts_tab_view.coffee +++ b/app/views/editor/level/scripts_tab_view.coffee @@ -22,7 +22,7 @@ module.exports = class ScriptsTabView extends View @dimensions = @level.dimensions() scripts = $.extend(true, [], @level.get('scripts') ? []) treemaOptions = - schema: Level.schema.get('properties').scripts + schema: Level.schema.properties.scripts data: scripts callbacks: change: @onScriptsChanged @@ -52,7 +52,7 @@ module.exports = class ScriptsTabView extends View filePath: "db/level/#{@level.get('original')}" files: @files view: @ - schema: Level.schema.get('properties').scripts.items + schema: Level.schema.properties.scripts.items data: selected.data thangIDs: thangIDs dimensions: @dimensions diff --git a/app/views/editor/level/settings_tab_view.coffee b/app/views/editor/level/settings_tab_view.coffee index 7556c4a4f..6f6822885 100644 --- a/app/views/editor/level/settings_tab_view.coffee +++ b/app/views/editor/level/settings_tab_view.coffee @@ -25,7 +25,7 @@ module.exports = class SettingsTabView extends View onLevelLoaded: (e) -> @level = e.level data = _.pick @level.attributes, (value, key) => key in @editableSettings - schema = _.cloneDeep Level.schema.attributes + schema = _.cloneDeep Level.schema schema.properties = _.pick schema.properties, (value, key) => key in @editableSettings schema.required = _.intersection schema.required, @editableSettings thangIDs = @getThangIDs() diff --git a/app/views/editor/level/system/edit.coffee b/app/views/editor/level/system/edit.coffee index c92894cdd..338ede1e5 100644 --- a/app/views/editor/level/system/edit.coffee +++ b/app/views/editor/level/system/edit.coffee @@ -29,7 +29,7 @@ module.exports = class LevelSystemEditView extends View buildSettingsTreema: -> data = _.pick @levelSystem.attributes, (value, key) => key in @editableSettings - schema = _.cloneDeep LevelSystem.schema.attributes + schema = _.cloneDeep LevelSystem.schema schema.properties = _.pick schema.properties, (value, key) => key in @editableSettings schema.required = _.intersection schema.required, @editableSettings @@ -53,7 +53,7 @@ module.exports = class LevelSystemEditView extends View buildConfigSchemaTreema: -> treemaOptions = supermodel: @supermodel - schema: LevelSystem.schema.get('properties').configSchema + schema: LevelSystem.schema.properties.configSchema data: @levelSystem.get 'configSchema' callbacks: {change: @onConfigSchemaEdited} treemaOptions.readOnly = true unless me.isAdmin() @@ -61,7 +61,7 @@ module.exports = class LevelSystemEditView extends View @configSchemaTreema.build() @configSchemaTreema.open() # TODO: schema is not loaded for the first one here? - @configSchemaTreema.tv4.addSchema('metaschema', LevelSystem.schema.get('properties').configSchema) + @configSchemaTreema.tv4.addSchema('metaschema', LevelSystem.schema.properties.configSchema) onConfigSchemaEdited: => @levelSystem.set 'configSchema', @configSchemaTreema.data diff --git a/app/views/editor/level/systems_tab_view.coffee b/app/views/editor/level/systems_tab_view.coffee index d04a463ff..eb4747b0e 100644 --- a/app/views/editor/level/systems_tab_view.coffee +++ b/app/views/editor/level/systems_tab_view.coffee @@ -67,7 +67,7 @@ module.exports = class SystemsTabView extends View treemaOptions = # TODO: somehow get rid of the + button, or repurpose it to open the LevelSystemAddView instead supermodel: @supermodel - schema: Level.schema.get('properties').systems + schema: Level.schema.properties.systems data: systems readOnly: true unless me.isAdmin() or @level.hasWriteAccess(me) callbacks: diff --git a/app/views/editor/level/thangs_tab_view.coffee b/app/views/editor/level/thangs_tab_view.coffee index 243b1e540..8a6c4cfe5 100644 --- a/app/views/editor/level/thangs_tab_view.coffee +++ b/app/views/editor/level/thangs_tab_view.coffee @@ -140,7 +140,7 @@ module.exports = class ThangsTabView extends View return if @startsLoading data = $.extend(true, {}, @level.attributes) treemaOptions = - schema: Level.schema.get('properties').thangs + schema: Level.schema.properties.thangs data: data.thangs supermodel: @supermodel callbacks: diff --git a/app/views/editor/thang/colors_tab_view.coffee b/app/views/editor/thang/colors_tab_view.coffee index a858f4385..87c887522 100644 --- a/app/views/editor/thang/colors_tab_view.coffee +++ b/app/views/editor/thang/colors_tab_view.coffee @@ -12,7 +12,7 @@ module.exports = class ColorsTabView extends CocoView constructor: (@thangType, options) -> @listenToOnce(@thangType, 'sync', @tryToBuild) - @listenToOnce(@thangType.schema(), 'sync', @tryToBuild) + # @listenToOnce(@thangType.schema(), 'sync', @tryToBuild) @colorConfig = { hue: 0, saturation: 0.5, lightness: 0.5 } @spriteBuilder = new SpriteBuilder(@thangType) f = => @@ -115,7 +115,7 @@ module.exports = class ColorsTabView extends CocoView return unless @thangType.loaded and @thangType.schema().loaded data = @thangType.get('colorGroups') data ?= {} - schema = @thangType.schema().attributes.properties?.colorGroups + schema = @thangType.schema().properties?.colorGroups treemaOptions = data: data schema: schema diff --git a/app/views/editor/thang/edit.coffee b/app/views/editor/thang/edit.coffee index 67edc6978..adc7c3a63 100644 --- a/app/views/editor/thang/edit.coffee +++ b/app/views/editor/thang/edit.coffee @@ -62,7 +62,7 @@ module.exports = class ThangTypeEditView extends View @thangType.fetch() @thangType.loadSchema() - @listenToOnce(@thangType.schema(), 'sync', @onThangTypeSync) + # @listenToOnce(@thangType.schema(), 'sync', @onThangTypeSync) @listenToOnce(@thangType, 'sync', @onThangTypeSync) @refreshAnimation = _.debounce @refreshAnimation, 500 @@ -344,7 +344,7 @@ module.exports = class ThangTypeEditView extends View buildTreema: -> data = @getThangData() - schema = _.cloneDeep ThangType.schema.attributes + schema = _.cloneDeep ThangType.schema schema.properties = _.pick schema.properties, (value, key) => not (key in ['components']) options = data: data diff --git a/app/views/modal/login_modal.coffee b/app/views/modal/login_modal.coffee index 8a433dd28..68306a03e 100644 --- a/app/views/modal/login_modal.coffee +++ b/app/views/modal/login_modal.coffee @@ -36,7 +36,7 @@ module.exports = class LoginModalView extends View loginAccount: (e) => forms.clearFormAlerts(@$el) userObject = forms.formToObject @$el - res = tv4.validateMultiple userObject, User.schema.attributes + res = tv4.validateMultiple userObject, User.schema return forms.applyErrorsToForm(@$el, res.errors) unless res.valid @enableModalInProgress(@$el) # TODO: part of forms loginUser(userObject) diff --git a/app/views/modal/signup_modal.coffee b/app/views/modal/signup_modal.coffee index f9224cb3f..63174261f 100644 --- a/app/views/modal/signup_modal.coffee +++ b/app/views/modal/signup_modal.coffee @@ -57,7 +57,7 @@ module.exports = class SignupModalView extends View userObject.emailSubscriptions.push 'notification' unless 'notification' in userObject.emailSubscriptions else userObject.emailSubscriptions = _.without (userObject.emailSubscriptions ? []), 'announcement', 'notification' - res = tv4.validateMultiple userObject, User.schema.attributes + res = tv4.validateMultiple userObject, User.schema return forms.applyErrorsToForm(@$el, res.errors) unless res.valid window.tracker?.trackEvent 'Finished Signup' @enableModalInProgress(@$el) diff --git a/server/routes/db.coffee b/server/routes/db.coffee index 2cbbc7df9..072b0ea8d 100644 --- a/server/routes/db.coffee +++ b/server/routes/db.coffee @@ -47,8 +47,8 @@ module.exports.setup = (app) -> getSchema = (req, res, moduleName) -> try - name = schemas[moduleName.replace '.', '_'] - schema = require('../' + name) + name = moduleName.replace '.', '_' + schema = require('../../app/schemas/' + name + '_schema') res.send(JSON.stringify(schema, null, '\t')) res.end() From b932bf1e7c80fdf465a6351463ed64d63802300c Mon Sep 17 00:00:00 2001 From: Aditya Raisinghani <aditya.ajeet@gmail.com> Date: Sat, 12 Apr 2014 14:16:41 +0530 Subject: [PATCH 29/46] Deleted schemas from /server and modified files to point to /app/schemas --- app/models/CocoModel.coffee | 10 - app/schemas/article_schema.coffee | 7 +- app/schemas/languages.coffee | 15 -- app/schemas/level_component_schema.coffee | 1 + app/schemas/level_schema.coffee | 9 +- app/schemas/level_system_schema.coffee | 3 +- app/schemas/schemas.coffee | 16 +- app/schemas/thang_type_schema.coffee | 7 +- app/views/editor/thang/edit.coffee | 1 - server/commons/i18n_schema.coffee | 48 ---- server/commons/metaschema.coffee | 132 ----------- server/levels/Level.coffee | 2 +- .../levels/components/LevelComponent.coffee | 2 +- server/levels/feedbacks/LevelFeedback.coffee | 2 +- .../feedbacks/level_feedback_schema.coffee | 27 --- server/levels/sessions/LevelSession.coffee | 2 +- .../sessions/level_session_schema.coffee | 213 ------------------ server/levels/systems/LevelSystem.coffee | 2 +- .../thangs/thang_component_schema.coffee | 21 -- server/users/User.coffee | 2 +- server/users/user_handler.coffee | 2 +- server/users/user_schema.coffee | 98 -------- 22 files changed, 38 insertions(+), 584 deletions(-) delete mode 100644 server/commons/i18n_schema.coffee delete mode 100644 server/commons/metaschema.coffee delete mode 100644 server/levels/feedbacks/level_feedback_schema.coffee delete mode 100644 server/levels/sessions/level_session_schema.coffee delete mode 100644 server/levels/thangs/thang_component_schema.coffee delete mode 100644 server/users/user_schema.coffee diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee index 974cd4826..6e5c0249b 100644 --- a/app/models/CocoModel.coffee +++ b/app/models/CocoModel.coffee @@ -2,15 +2,6 @@ storage = require 'lib/storage' deltasLib = require 'lib/deltas' auth = require 'lib/auth' -class CocoSchema extends Backbone.Model - constructor: (path, args...) -> - super(args...) - # @urlRoot = path + '/schema' - @schemaName = path[4..].replace '.', '_' - @schema = require 'schemas/' + @schemaName + '_schema' - -# window.CocoSchema = CocoSchema.schema - class CocoModel extends Backbone.Model idAttribute: "_id" loaded: false @@ -69,7 +60,6 @@ class CocoModel extends Backbone.Model return if @constructor.schema.loading @constructor.schema = require 'schemas/' + @constructor.schema + '_schema' unless @constructor.schema.loaded @onConstructorSync() - # @listenToOnce(@constructor.schema, 'sync', @onConstructorSync) onConstructorSync: -> @constructor.schema.loaded = true diff --git a/app/schemas/article_schema.coffee b/app/schemas/article_schema.coffee index 012d46ec3..0274f92a6 100644 --- a/app/schemas/article_schema.coffee +++ b/app/schemas/article_schema.coffee @@ -6,8 +6,9 @@ c.extendNamedProperties ArticleSchema # name first ArticleSchema.properties.body = { type: 'string', title: 'Content', format: 'markdown' } ArticleSchema.properties.i18n = { type: 'object', title: 'i18n', format: 'i18n', props: ['name', 'body'] } -c.extendBasicProperties(ArticleSchema, 'article') -c.extendSearchableProperties(ArticleSchema) -c.extendVersionedProperties(ArticleSchema, 'article') +c.extendBasicProperties ArticleSchema, 'article' +c.extendSearchableProperties ArticleSchema +c.extendVersionedProperties ArticleSchema, 'article' +c.extendPatchableProperties ArticleSchema module.exports = ArticleSchema diff --git a/app/schemas/languages.coffee b/app/schemas/languages.coffee index e9c8e0f33..053a89f2a 100644 --- a/app/schemas/languages.coffee +++ b/app/schemas/languages.coffee @@ -1,20 +1,5 @@ -# errors = require '../commons/errors' -# log = require 'winston' locale = require '../locale/locale' # requiring from app; will break if we stop serving from where app lives -# module.exports.setup = (app) -> -# app.all '/languages/add/:lang/:namespace', (req, res) -> -# # Should probably store these somewhere -# log.info "#{req.params.lang}.#{req.params.namespace} missing an i18n key:", req.body -# res.send('') -# res.end() - -# app.all '/languages', (req, res) -> -# # Now that these are in the client, not sure when we would use this, but hey -# return errors.badMethod(res) if req.route.method isnt 'get' -# res.send(languages) -# return res.end() - languages = [] for code, localeInfo of locale languages.push code: code, nativeDescription: localeInfo.nativeDescription, englishDescription: localeInfo.englishDescription diff --git a/app/schemas/level_component_schema.coffee b/app/schemas/level_component_schema.coffee index d67c69376..3178eb916 100644 --- a/app/schemas/level_component_schema.coffee +++ b/app/schemas/level_component_schema.coffee @@ -115,5 +115,6 @@ c.extendBasicProperties LevelComponentSchema, 'level.component' c.extendSearchableProperties LevelComponentSchema c.extendVersionedProperties LevelComponentSchema, 'level.component' c.extendPermissionsProperties LevelComponentSchema, 'level.component' +c.extendPatchableProperties LevelComponentSchema module.exports = LevelComponentSchema diff --git a/app/schemas/level_schema.coffee b/app/schemas/level_schema.coffee index e372bdd52..4ce229ca6 100644 --- a/app/schemas/level_schema.coffee +++ b/app/schemas/level_schema.coffee @@ -1,5 +1,5 @@ c = require './schemas' -ThangComponentSchema = require './thang_component_schema' +ThangComponentSchema = require './thangs/thang_component_schema' SpecificArticleSchema = c.object() c.extendNamedProperties SpecificArticleSchema # name first @@ -108,9 +108,9 @@ NoteGroupSchema = c.object {title: "Note Group", description: "A group of notes lock: {title: "Lock", description: "Whether the interface should be locked so that the player's focus is on the script, or specific areas to lock.", type: ['boolean', 'array'], items: {type: 'string', enum: ['surface', 'editor', 'palette', 'hud', 'playback', 'playback-hover', 'level', ]}} letterbox: {type: 'boolean', title: 'Letterbox', description:'Turn letterbox mode on or off. Disables surface and playback controls.'} - goals: c.object {title: "Goals", description: "Add or remove goals for the player to complete in the level."}, - add: c.array {title: "Add", description: "Add these goals."}, GoalSchema - remove: c.array {title: "Remove", description: "Remove these goals."}, GoalSchema + goals: c.object {title: "Goals (Old)", description: "Deprecated. Goals added here have no effect. Add goals in the level settings instead."}, + add: c.array {title: "Add", description: "Deprecated. Goals added here have no effect. Add goals in the level settings instead."}, GoalSchema + remove: c.array {title: "Remove", description: "Deprecated. Goals removed here have no effect. Adjust goals in the level settings instead."}, GoalSchema playback: c.object {title: "Playback", description: "Control the playback of the level."}, playing: {type: 'boolean', title: "Set Playing", description: "Set whether playback is playing or paused."} @@ -243,6 +243,7 @@ c.extendBasicProperties LevelSchema, 'level' c.extendSearchableProperties LevelSchema c.extendVersionedProperties LevelSchema, 'level' c.extendPermissionsProperties LevelSchema, 'level' +c.extendPatchableProperties LevelSchema module.exports = LevelSchema diff --git a/app/schemas/level_system_schema.coffee b/app/schemas/level_system_schema.coffee index 0d7cad2c0..9b186aaac 100644 --- a/app/schemas/level_system_schema.coffee +++ b/app/schemas/level_system_schema.coffee @@ -101,6 +101,7 @@ _.extend LevelSystemSchema.properties, c.extendBasicProperties LevelSystemSchema, 'level.system' c.extendSearchableProperties LevelSystemSchema c.extendVersionedProperties LevelSystemSchema, 'level.system' -c.extendPermissionsProperties LevelSystemSchema, 'level.system' +c.extendPermissionsProperties LevelSystemSchema +c.extendPatchableProperties LevelSystemSchema module.exports = LevelSystemSchema diff --git a/app/schemas/schemas.coffee b/app/schemas/schemas.coffee index eebe6f954..2d7ae0603 100644 --- a/app/schemas/schemas.coffee +++ b/app/schemas/schemas.coffee @@ -15,7 +15,7 @@ me.object = (ext, props) -> combine {type: 'object', additionalProperties: false me.array = (ext, items) -> combine {type: 'array', items: items or {}}, ext me.shortString = (ext) -> combine({type: 'string', maxLength: 100}, ext) me.pct = (ext) -> combine({type: 'number', maximum: 1.0, minimum: 0.0}, ext) -me.date = (ext) -> combine({type: 'string', format: 'date-time'}, ext) +me.date = (ext) -> combine({type: ['object', 'string'], format: 'date-time'}, ext) # should just be string (Mongo ID), but sometimes mongoose turns them into objects representing those, so we are lenient me.objectId = (ext) -> schema = combine({type: ['object', 'string'] }, ext) me.url = (ext) -> combine({type: 'string', format: 'url', pattern: urlPattern}, ext) @@ -54,7 +54,21 @@ basicProps = (linkFragment) -> me.extendBasicProperties = (schema, linkFragment) -> schema.properties = {} unless schema.properties? _.extend(schema.properties, basicProps(linkFragment)) + +# PATCHABLE +patchableProps = -> + patches: me.array({title:'Patches'}, { + _id: me.objectId(links: [{rel: "db", href: "/db/patch/{($)}"}], title: "Patch ID", description: "A reference to the patch.") + status: { enum: ['pending', 'accepted', 'rejected', 'cancelled']} + }) + allowPatches: { type: 'boolean' } + listeners: me.array({title:'Listeners'}, + me.objectId(links: [{rel: 'extra', href: "/db/user/{($)}"}])) + +me.extendPatchableProperties = (schema) -> + schema.properties = {} unless schema.properties? + _.extend(schema.properties, patchableProps()) # NAMED diff --git a/app/schemas/thang_type_schema.coffee b/app/schemas/thang_type_schema.coffee index fb459811d..1e6bc2ee5 100644 --- a/app/schemas/thang_type_schema.coffee +++ b/app/schemas/thang_type_schema.coffee @@ -146,8 +146,9 @@ ThangTypeSchema.definitions = action: ActionSchema sound: SoundSchema -c.extendBasicProperties(ThangTypeSchema, 'thang.type') -c.extendSearchableProperties(ThangTypeSchema) -c.extendVersionedProperties(ThangTypeSchema, 'thang.type') +c.extendBasicProperties ThangTypeSchema, 'thang.type' +c.extendSearchableProperties ThangTypeSchema +c.extendVersionedProperties ThangTypeSchema, 'thang.type' +c.extendPatchableProperties ThangTypeSchema module.exports = ThangTypeSchema diff --git a/app/views/editor/thang/edit.coffee b/app/views/editor/thang/edit.coffee index adc7c3a63..5fea84696 100644 --- a/app/views/editor/thang/edit.coffee +++ b/app/views/editor/thang/edit.coffee @@ -62,7 +62,6 @@ module.exports = class ThangTypeEditView extends View @thangType.fetch() @thangType.loadSchema() - # @listenToOnce(@thangType.schema(), 'sync', @onThangTypeSync) @listenToOnce(@thangType, 'sync', @onThangTypeSync) @refreshAnimation = _.debounce @refreshAnimation, 500 diff --git a/server/commons/i18n_schema.coffee b/server/commons/i18n_schema.coffee deleted file mode 100644 index 2a2aaf816..000000000 --- a/server/commons/i18n_schema.coffee +++ /dev/null @@ -1,48 +0,0 @@ -#this file will hold the experimental JSON schema for i18n -c = require './schemas' - -languageCodeArrayRegex = c.generateLanguageCodeArrayRegex() - - -ExampleSchema = { - title: "Example Schema", - description:"An example schema", - type: "object", - properties: { - text: { - title: "Text", - description: "A short message to display in the dialogue area. Markdown okay.", - type: "string", - maxLength: 400 - }, - i18n: {"$ref": "#/definitions/i18n"} - }, - - definitions: { - i18n: { - title: "i18n", - description: "The internationalization object", - type: "object", - patternProperties: { - languageCodeArrayRegex: { - additionalProperties: false, - properties: { - #put the translatable properties here - #if it is possible to not include i18n with a reference - # to #/properties, you could just do - properties: {"$ref":"#/properties"} - # text: {"$ref": "#/properties/text"} - } - default: { - title: "LanguageCode", - description: "LanguageDescription" - } - } - } - } - }, - -} - -#define a i18n object type for each schema, then have the i18n have it's oneOf check against -#translatable schemas of that object \ No newline at end of file diff --git a/server/commons/metaschema.coffee b/server/commons/metaschema.coffee deleted file mode 100644 index 4d9d7c0d8..000000000 --- a/server/commons/metaschema.coffee +++ /dev/null @@ -1,132 +0,0 @@ -# The JSON Schema Core/Validation Meta-Schema, but with titles and descriptions added to make it easier to edit in Treema, and in CoffeeScript - -module.exports = - id: "metaschema" - displayProperty: "title" - $schema: "http://json-schema.org/draft-04/schema#" - title: "Schema" - description: "Core schema meta-schema" - definitions: - schemaArray: - type: "array" - minItems: 1 - items: { $ref: "#" } - title: "Array of Schemas" - "default": [{}] - positiveInteger: - type: "integer" - minimum: 0 - title: "Positive Integer" - positiveIntegerDefault0: - allOf: [ { $ref: "#/definitions/positiveInteger" }, { "default": 0 } ] - simpleTypes: - title: "Single Type" - "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] - stringArray: - type: "array" - items: { type: "string" } - minItems: 1 - uniqueItems: true - title: "String Array" - "default": [''] - type: "object" - properties: - id: - type: "string" - format: "uri" - $schema: - type: "string" - format: "uri" - "default": "http://json-schema.org/draft-04/schema#" - title: - type: "string" - description: - type: "string" - "default": {} - multipleOf: - type: "number" - minimum: 0 - exclusiveMinimum: true - maximum: - type: "number" - exclusiveMaximum: - type: "boolean" - "default": false - minimum: - type: "number" - exclusiveMinimum: - type: "boolean" - "default": false - maxLength: { $ref: "#/definitions/positiveInteger" } - minLength: { $ref: "#/definitions/positiveIntegerDefault0" } - pattern: - type: "string" - format: "regex" - additionalItems: - anyOf: [ - { type: "boolean", "default": false } - { $ref: "#" } - ] - items: - anyOf: [ - { $ref: "#" } - { $ref: "#/definitions/schemaArray" } - ] - "default": {} - maxItems: { $ref: "#/definitions/positiveInteger" } - minItems: { $ref: "#/definitions/positiveIntegerDefault0" } - uniqueItems: - type: "boolean" - "default": false - maxProperties: { $ref: "#/definitions/positiveInteger" } - minProperties: { $ref: "#/definitions/positiveIntegerDefault0" } - required: { $ref: "#/definitions/stringArray" } - additionalProperties: - anyOf: [ - { type: "boolean", "default": true } - { $ref: "#" } - ] - "default": {} - definitions: - type: "object" - additionalProperties: { $ref: "#" } - "default": {} - properties: - type: "object" - additionalProperties: { $ref: "#" } - "default": {} - patternProperties: - type: "object" - additionalProperties: { $ref: "#" } - "default": {} - dependencies: - type: "object" - additionalProperties: - anyOf: [ - { $ref: "#" } - { $ref: "#/definitions/stringArray" } - ] - "enum": - type: "array" - minItems: 1 - uniqueItems: true - "default": [''] - type: - anyOf: [ - { $ref: "#/definitions/simpleTypes" } - { - type: "array" - items: { $ref: "#/definitions/simpleTypes" } - minItems: 1 - uniqueItems: true - title: "Array of Types" - "default": ['string'] - }] - allOf: { $ref: "#/definitions/schemaArray" } - anyOf: { $ref: "#/definitions/schemaArray" } - oneOf: { $ref: "#/definitions/schemaArray" } - not: { $ref: "#" } - dependencies: - exclusiveMaximum: [ "maximum" ] - exclusiveMinimum: [ "minimum" ] - "default": {} diff --git a/server/levels/Level.coffee b/server/levels/Level.coffee index bb4d10065..83e8d678b 100644 --- a/server/levels/Level.coffee +++ b/server/levels/Level.coffee @@ -1,6 +1,6 @@ mongoose = require('mongoose') plugins = require('../plugins/plugins') -jsonschema = require('./level_schema') +jsonschema = require('../../app/schemas/level_schema') LevelSchema = new mongoose.Schema({ description: String diff --git a/server/levels/components/LevelComponent.coffee b/server/levels/components/LevelComponent.coffee index 515e7d80a..5f00f261c 100644 --- a/server/levels/components/LevelComponent.coffee +++ b/server/levels/components/LevelComponent.coffee @@ -1,6 +1,6 @@ mongoose = require('mongoose') plugins = require('../../plugins/plugins') -jsonschema = require('./level_component_schema') +jsonschema = require('../../../app/schemas/level_component_schema') LevelComponentSchema = new mongoose.Schema { description: String diff --git a/server/levels/feedbacks/LevelFeedback.coffee b/server/levels/feedbacks/LevelFeedback.coffee index 0eecdec32..234caf367 100644 --- a/server/levels/feedbacks/LevelFeedback.coffee +++ b/server/levels/feedbacks/LevelFeedback.coffee @@ -2,7 +2,7 @@ mongoose = require('mongoose') plugins = require('../../plugins/plugins') -jsonschema = require('./level_feedback_schema') +jsonschema = require('../../../app/schemas/level_feedback_schema') LevelFeedbackSchema = new mongoose.Schema({ created: diff --git a/server/levels/feedbacks/level_feedback_schema.coffee b/server/levels/feedbacks/level_feedback_schema.coffee deleted file mode 100644 index 54d9e84e1..000000000 --- a/server/levels/feedbacks/level_feedback_schema.coffee +++ /dev/null @@ -1,27 +0,0 @@ -c = require '../../commons/schemas' - -LevelFeedbackLevelSchema = c.object {required: ['original', 'majorVersion']}, { - original: c.objectId({}) - majorVersion: {type: 'integer', minimum: 0, default: 0}} - -LevelFeedbackSchema = c.object { - title: "Feedback" - description: "Feedback on a level." -} - -_.extend LevelFeedbackSchema.properties, - # denormalization - creatorName: { type: 'string' } - levelName: { type: 'string' } - levelID: { type: 'string' } - - creator: c.objectId(links: [{rel: 'extra', href: "/db/user/{($)}"}]) - created: c.date( { title: 'Created', readOnly: true }) - - level: LevelFeedbackLevelSchema - rating: { type: 'number', minimum: 1, maximum: 5 } - review: { type: 'string' } - -c.extendBasicProperties LevelFeedbackSchema, 'level.feedback' - -module.exports = LevelFeedbackSchema diff --git a/server/levels/sessions/LevelSession.coffee b/server/levels/sessions/LevelSession.coffee index 952782f1b..d91b7241c 100644 --- a/server/levels/sessions/LevelSession.coffee +++ b/server/levels/sessions/LevelSession.coffee @@ -2,7 +2,7 @@ mongoose = require('mongoose') plugins = require('../../plugins/plugins') -jsonschema = require('./level_session_schema') +jsonschema = require('../../../app/schemas/level_session_schema') LevelSessionSchema = new mongoose.Schema({ created: diff --git a/server/levels/sessions/level_session_schema.coffee b/server/levels/sessions/level_session_schema.coffee deleted file mode 100644 index d798a9d88..000000000 --- a/server/levels/sessions/level_session_schema.coffee +++ /dev/null @@ -1,213 +0,0 @@ -c = require '../../commons/schemas' - -LevelSessionPlayerSchema = c.object - id: c.objectId - links: [ - { - rel: 'extra' - href: "/db/user/{($)}" - } - ] - time: - type: 'Number' - changes: - type: 'Number' - - -LevelSessionLevelSchema = c.object {required: ['original', 'majorVersion']}, - original: c.objectId({}) - majorVersion: - type: 'integer' - minimum: 0 - default: 0 - - -LevelSessionSchema = c.object - title: "Session" - description: "A single session for a given level." - - -_.extend LevelSessionSchema.properties, - # denormalization - creatorName: - type: 'string' - levelName: - type: 'string' - levelID: - type: 'string' - multiplayer: - type: 'boolean' - creator: c.objectId - links: - [ - { - rel: 'extra' - href: "/db/user/{($)}" - } - ] - created: c.date - title: 'Created' - readOnly: true - - changed: c.date - title: 'Changed' - readOnly: true - - team: c.shortString() - level: LevelSessionLevelSchema - - screenshot: - type: 'string' - - state: c.object {}, - complete: - type: 'boolean' - scripts: c.object {}, - ended: - type: 'object' - additionalProperties: - type: 'number' - currentScript: - type: [ - 'null' - 'string' - ] - currentScriptOffset: - type: 'number' - - selected: - type: [ - 'null' - 'string' - ] - playing: - type: 'boolean' - frame: - type: 'number' - thangs: - type: 'object' - additionalProperties: - title: 'Thang' - type: 'object' - properties: - methods: - type: 'object' - additionalProperties: - title: 'Thang Method' - type: 'object' - properties: - metrics: - type: 'object' - source: - type: 'string' - -# TODO: specify this more - code: - type: 'object' - - teamSpells: - type: 'object' - additionalProperties: - type: 'array' - - players: - type: 'object' - - chat: - type: 'array' - - meanStrength: - type: 'number' - - standardDeviation: - type:'number' - minimum: 0 - - totalScore: - type: 'number' - - submitted: - type: 'boolean' - - submitDate: c.date - title: 'Submitted' - - submittedCode: - type: 'object' - - isRanking: - type: 'boolean' - description: 'Whether this session is still in the first ranking chain after being submitted.' - - unsubscribed: - type: 'boolean' - description: 'Whether the player has opted out of receiving email updates about ladder rankings for this session.' - - numberOfWinsAndTies: - type: 'number' - - numberOfLosses: - type: 'number' - - scoreHistory: - type: 'array' - title: 'Score History' - description: 'A list of objects representing the score history of a session' - items: - title: 'Score History Point' - description: 'An array with the format [unix timestamp, totalScore]' - type: 'array' - items: - type: 'number' - - matches: - type: 'array' - title: 'Matches' - description: 'All of the matches a submitted session has played in its current state.' - items: - type: 'object' - properties: - date: c.date - title: 'Date computed' - description: 'The date a match was computed.' - metrics: - type: 'object' - title: 'Metrics' - description: 'Various information about the outcome of a match.' - properties: - rank: - title: 'Rank' - description: 'A 0-indexed ranking representing the player\'s standing in the outcome of a match' - type: 'number' - opponents: - type: 'array' - title: 'Opponents' - description: 'An array containing information about the opponents\' sessions in a given match.' - items: - type: 'object' - properties: - sessionID: - title: 'Opponent Session ID' - description: 'The session ID of an opponent.' - type: ['object', 'string'] - userID: - title: 'Opponent User ID' - description: 'The user ID of an opponent' - type: ['object','string'] - metrics: - type: 'object' - properties: - rank: - title: 'Opponent Rank' - description: 'The opponent\'s ranking in a given match' - type: 'number' - - - - - - -c.extendBasicProperties LevelSessionSchema, 'level.session' -c.extendPermissionsProperties LevelSessionSchema, 'level.session' - -module.exports = LevelSessionSchema diff --git a/server/levels/systems/LevelSystem.coffee b/server/levels/systems/LevelSystem.coffee index a02a3aab0..730b338ad 100644 --- a/server/levels/systems/LevelSystem.coffee +++ b/server/levels/systems/LevelSystem.coffee @@ -1,6 +1,6 @@ mongoose = require('mongoose') plugins = require('../../plugins/plugins') -jsonschema = require('./level_system_schema') +jsonschema = require('../../../app/schemas/level_system_schema') LevelSystemSchema = new mongoose.Schema { description: String diff --git a/server/levels/thangs/thang_component_schema.coffee b/server/levels/thangs/thang_component_schema.coffee deleted file mode 100644 index 0118d3a4c..000000000 --- a/server/levels/thangs/thang_component_schema.coffee +++ /dev/null @@ -1,21 +0,0 @@ -c = require '../../commons/schemas' - -module.exports = ThangComponentSchema = c.object { - title: "Component" - description: "Configuration for a Component that this Thang uses." - format: 'thang-component' - required: ['original', 'majorVersion'] - 'default': - majorVersion: 0 - config: {} - links: [{rel: "db", href: "/db/level.component/{(original)}/version/{(majorVersion)}"}] -}, - original: c.objectId(title: "Original", description: "A reference to the original Component being configured.", format: "hidden") - config: c.object {title: "Configuration", description: "Component-specific configuration properties.", additionalProperties: true, format: 'thang-component-configuration'} - majorVersion: - title: "Major Version" - description: "Which major version of the Component is being used." - type: 'integer' - minimum: 0 - default: 0 - format: "hidden" diff --git a/server/users/User.coffee b/server/users/User.coffee index 28009e610..fd9b81969 100644 --- a/server/users/User.coffee +++ b/server/users/User.coffee @@ -1,5 +1,5 @@ mongoose = require('mongoose') -jsonschema = require('./user_schema') +jsonschema = require('../../app/schemas/user_schema') crypto = require('crypto') {salt, isProduction} = require('../../server_config') mail = require '../commons/mail' diff --git a/server/users/user_handler.coffee b/server/users/user_handler.coffee index c9446afda..eb26ff7e5 100644 --- a/server/users/user_handler.coffee +++ b/server/users/user_handler.coffee @@ -1,4 +1,4 @@ -schema = require './user_schema' +schema = require '../../app/schemas/user_schema' crypto = require 'crypto' request = require 'request' User = require './User' diff --git a/server/users/user_schema.coffee b/server/users/user_schema.coffee deleted file mode 100644 index c7de194e3..000000000 --- a/server/users/user_schema.coffee +++ /dev/null @@ -1,98 +0,0 @@ -c = require '../commons/schemas' -emailSubscriptions = ['announcement', 'tester', 'level_creator', 'developer', 'article_editor', 'translator', 'support', 'notification'] - -UserSchema = c.object {}, - name: c.shortString({title: 'Display Name', default:''}) - email: c.shortString({title: 'Email', format: 'email'}) - firstName: c.shortString({title: 'First Name'}) - lastName: c.shortString({title: 'Last Name'}) - gender: {type: 'string', 'enum': ['male', 'female']} - password: {type: 'string', maxLength: 256, minLength: 2, title:'Password'} - passwordReset: {type: 'string'} - photoURL: {type: 'string', format: 'image-file', title: 'Profile Picture', description: 'Upload a 256x256px or larger image to serve as your profile picture.'} - - facebookID: c.shortString({title: 'Facebook ID'}) - gplusID: c.shortString({title: 'G+ ID'}) - - wizardColor1: c.pct({title: 'Wizard Clothes Color'}) - volume: c.pct({title: 'Volume'}) - music: {type: 'boolean', default: true} - autocastDelay: {type: 'integer', 'default': 5000 } - lastLevel: { type: 'string' } - - emailSubscriptions: c.array {uniqueItems: true, 'default': ['announcement', 'notification']}, {'enum': emailSubscriptions} - - # server controlled - permissions: c.array {'default': []}, c.shortString() - dateCreated: c.date({title: 'Date Joined'}) - anonymous: {type: 'boolean', 'default': true} - testGroupNumber: {type: 'integer', minimum: 0, maximum: 256, exclusiveMaximum: true} - mailChimp: {type: 'object'} - hourOfCode: {type: 'boolean'} - hourOfCodeComplete: {type: 'boolean'} - - emailLower: c.shortString() - nameLower: c.shortString() - passwordHash: {type: 'string', maxLength: 256} - - # client side - emailHash: {type: 'string'} - - #Internationalization stuff - preferredLanguage: {type: 'string', default: 'en', 'enum': c.getLanguageCodeArray()} - - signedCLA: c.date({title: 'Date Signed the CLA'}) - wizard: c.object {}, - colorConfig: c.object {additionalProperties: c.colorConfig()} - - aceConfig: c.object {}, - language: {type: 'string', 'default': 'javascript', 'enum': ['javascript', 'coffeescript']} - keyBindings: {type: 'string', 'default': 'default', 'enum': ['default', 'vim', 'emacs']} - invisibles: {type: 'boolean', 'default': false} - indentGuides: {type: 'boolean', 'default': false} - behaviors: {type: 'boolean', 'default': false} - - simulatedBy: {type: 'integer', minimum: 0, default: 0} - simulatedFor: {type: 'integer', minimum: 0, default: 0} - - jobProfile: c.object {title: 'Job Profile', required: ['lookingFor', 'jobTitle', 'active', 'name', 'city', 'country', 'skills', 'experience', 'shortDescription', 'longDescription', 'visa', 'work', 'education', 'projects', 'links']}, - lookingFor: {title: 'Looking For', type: 'string', enum: ['Full-time', 'Part-time', 'Remote', 'Contracting', 'Internship'], default: 'Full-time', description: 'What kind of developer position do you want?'} - jobTitle: {type: 'string', maxLength: 50, title: 'Desired Job Title', description: 'What role are you looking for? Ex.: "Full Stack Engineer", "Front-End Developer", "iOS Developer"', default: 'Software Developer'} - active: {title: 'Active', type: 'boolean', description: 'Want interview offers right now?'} - updated: c.date {title: 'Last Updated', description: 'How fresh your profile appears to employers. The fresher, the better. Profiles go inactive after 30 days.'} - name: c.shortString {title: 'Name', description: 'Name you want employers to see, like "Nick Winter".'} - city: c.shortString {title: 'City', description: 'City you want to work in (or live in now), like "San Francisco" or "Lubbock, TX".', default: 'Defaultsville, CA', format: 'city'} - country: c.shortString {title: 'Country', description: 'Country you want to work in (or live in now), like "USA" or "France".', default: 'USA', format: 'country'} - skills: c.array {title: 'Skills', description: 'Tag relevant developer skills in order of proficiency. Employers will see the first five at a glance.', default: ['javascript'], minItems: 1, maxItems: 30, uniqueItems: true}, - {type: 'string', minLength: 1, maxLength: 20, description: 'Ex.: "objective-c", "mongodb", "rails", "android", "javascript"', format: 'skill'} - experience: {type: 'integer', title: 'Years of Experience', minimum: 0, description: 'How many years of professional experience (getting paid) developing software do you have?'} - shortDescription: {type: 'string', maxLength: 140, title: 'Short Description', description: 'Who are you, and what are you looking for? 140 characters max.', default: 'Programmer seeking to build great software.'} - longDescription: {type: 'string', maxLength: 600, title: 'Description', description: 'Describe yourself to potential employers. Keep it short and to the point. We recommend outlining the position that would most interest you. Tasteful markdown okay; 600 characters max.', format: 'markdown', default: '* I write great code.\n* You need great code?\n* Great!'} - visa: c.shortString {title: 'US Work Status', description: 'Are you authorized to work in the US, or do you need visa sponsorship?', enum: ['Authorized to work in the US', 'Need visa sponsorship'], default: 'Authorized to work in the US'} - work: c.array {title: 'Work Experience', description: 'List your relevant work experience, most recent first.'}, - c.object {title: 'Job', description: 'Some work experience you had.', required: ['employer', 'role', 'duration']}, - employer: c.shortString {title: 'Employer', description: 'Name of your employer.'} - role: c.shortString {title: 'Job Title', description: 'What was your job title or role?'} - duration: c.shortString {title: 'Duration', description: 'When did you hold this gig? Ex.: "Feb 2013 - present".'} - education: c.array {title: 'Education', description: 'List your academic ordeals.'}, - c.object {title: 'Ordeal', description: 'Some education that befell you.', required: ['school', 'degree', 'duration']}, - school: c.shortString {title: 'School', description: 'Name of your school.'} - degree: c.shortString {title: 'Degree', description: 'What was your degree and field of study? Ex. Ph.D. Human-Computer Interaction (incomplete)'} - duration: c.shortString {title: 'Dates', description: 'When? Ex.: "Aug 2004 - May 2008".'} - projects: c.array {title: 'Projects', description: 'Highlight your projects to amaze employers.'}, - c.object {title: 'Project', description: 'A project you created.', required: ['name', 'description', 'picture'], default: {name: 'My Project', description: 'A project I worked on.', link: 'http://example.com', picture: ''}}, - name: c.shortString {title: 'Project Name', description: 'What was the project called?', default: 'My Project'} - description: {type: 'string', title: 'Description', description: 'Briefly describe the project.', maxLength: 400, default: 'A project I worked on.', format: 'markdown'} - picture: {type: 'string', title: 'Picture', format: 'image-file', description: 'Upload a 230x115px or larger image showing off the project.'} - link: c.url {title: 'Link', description: 'Link to the project.', default: 'http://example.com'} - links: c.array {title: 'Personal and Social Links', description: 'Link any other sites or profiles you want to highlight, like your GitHub, your LinkedIn, or your blog.'}, - c.object {title: 'Link', description: 'A link to another site you want to highlight, like your GitHub, your LinkedIn, or your blog.', required: ['name', 'link']}, - name: {type: 'string', maxLength: 30, title: 'Link Name', description: 'What are you linking to? Ex: "Personal Website", "Twitter"', format: 'link-name'} - link: c.url {title: 'Link', description: 'The URL.', default: 'http://example.com'} - photoURL: {type: 'string', format: 'image-file', title: 'Profile Picture', description: 'Upload a 256x256px or larger image if you want to show a different profile picture to employers than your normal avatar.'} - - jobProfileApproved: {title: 'Job Profile Approved', type: 'boolean', description: 'Whether your profile has been approved by CodeCombat.'} - jobProfileNotes: {type: 'string', maxLength: 1000, title: 'Our Notes', description: "CodeCombat's notes on the candidate.", format: 'markdown', default: ''} -c.extendBasicProperties UserSchema, 'user' - -module.exports = UserSchema From e04787475cb7007ce4a02ff980f9729655312071 Mon Sep 17 00:00:00 2001 From: Aditya Raisinghani <aditya.ajeet@gmail.com> Date: Sat, 12 Apr 2014 15:16:42 +0530 Subject: [PATCH 30/46] Rebased master to get updated schemas and added patch schema --- app/models/CocoModel.coffee | 2 +- app/schemas/level_schema.coffee | 2 +- .../schemas}/patch_schema.coffee | 2 +- app/views/account/job_profile_view.coffee | 2 +- app/views/kinds/SearchView.coffee | 2 +- .../components/level_component_handler.coffee | 2 +- .../components/level_component_schema.coffee | 120 --------- .../feedbacks/level_feedback_handler.coffee | 2 +- server/levels/level_handler.coffee | 2 +- server/levels/level_schema.coffee | 255 ------------------ .../sessions/level_session_handler.coffee | 2 +- .../systems/level_system_handler.coffee | 2 +- .../levels/systems/level_system_schema.coffee | 107 -------- .../levels/thangs/thang_type_handler.coffee | 2 +- server/levels/thangs/thang_type_schema.coffee | 154 ----------- server/patches/patch_handler.coffee | 4 +- 16 files changed, 13 insertions(+), 649 deletions(-) rename {server/patches => app/schemas}/patch_schema.coffee (96%) delete mode 100644 server/levels/components/level_component_schema.coffee delete mode 100644 server/levels/level_schema.coffee delete mode 100644 server/levels/systems/level_system_schema.coffee delete mode 100644 server/levels/thangs/thang_type_schema.coffee diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee index 6e5c0249b..bb2fc0547 100644 --- a/app/models/CocoModel.coffee +++ b/app/models/CocoModel.coffee @@ -235,7 +235,7 @@ class CocoModel extends Backbone.Model getExpandedDelta: -> delta = @getDelta() - deltasLib.expandDelta(delta, @_revertAttributes, @schema().attributes) + deltasLib.expandDelta(delta, @_revertAttributes, @schema()) addPatchToAcceptOnSave: (patch) -> @acceptedPatches ?= [] diff --git a/app/schemas/level_schema.coffee b/app/schemas/level_schema.coffee index 4ce229ca6..919b44c44 100644 --- a/app/schemas/level_schema.coffee +++ b/app/schemas/level_schema.coffee @@ -1,5 +1,5 @@ c = require './schemas' -ThangComponentSchema = require './thangs/thang_component_schema' +ThangComponentSchema = require './thang_component_schema' SpecificArticleSchema = c.object() c.extendNamedProperties SpecificArticleSchema # name first diff --git a/server/patches/patch_schema.coffee b/app/schemas/patch_schema.coffee similarity index 96% rename from server/patches/patch_schema.coffee rename to app/schemas/patch_schema.coffee index eae980d4e..5c2ce122e 100644 --- a/server/patches/patch_schema.coffee +++ b/app/schemas/patch_schema.coffee @@ -1,4 +1,4 @@ -c = require '../commons/schemas' +c = require './schemas' patchables = ['level', 'thang_type', 'level_system', 'level_component', 'article'] diff --git a/app/views/account/job_profile_view.coffee b/app/views/account/job_profile_view.coffee index a39fb6b16..d14fc2f79 100644 --- a/app/views/account/job_profile_view.coffee +++ b/app/views/account/job_profile_view.coffee @@ -29,7 +29,7 @@ module.exports = class JobProfileView extends CocoView visibleSettings = @editableSettings.concat @readOnlySettings data = _.pick (me.get('jobProfile') ? {}), (value, key) => key in visibleSettings data.name ?= (me.get('firstName') + ' ' + me.get('lastName')).trim() if me.get('firstName') - schema = _.cloneDeep me.schema().get('properties').jobProfile + schema = _.cloneDeep me.schema().properties.jobProfile schema.properties = _.pick schema.properties, (value, key) => key in visibleSettings schema.required = _.intersection schema.required, visibleSettings for prop in @readOnlySettings diff --git a/app/views/kinds/SearchView.coffee b/app/views/kinds/SearchView.coffee index 5f93924c3..9ce303b7c 100644 --- a/app/views/kinds/SearchView.coffee +++ b/app/views/kinds/SearchView.coffee @@ -96,7 +96,7 @@ module.exports = class SearchView extends View name = @$el.find('#name').val() model = new @model() model.set('name', name) - if @model.schema.get('properties').permissions + if @model.schema.properties.permissions model.set 'permissions', [{access: 'owner', target: me.id}] res = model.save() return unless res diff --git a/server/levels/components/level_component_handler.coffee b/server/levels/components/level_component_handler.coffee index 576bad3c8..e2b7a2ba8 100644 --- a/server/levels/components/level_component_handler.coffee +++ b/server/levels/components/level_component_handler.coffee @@ -3,7 +3,7 @@ Handler = require('../../commons/Handler') LevelComponentHandler = class LevelComponentHandler extends Handler modelClass: LevelComponent - jsonSchema: require './level_component_schema' + jsonSchema: require '../../../app/schemas/level_component_schema' editableProperties: [ 'system' 'description' diff --git a/server/levels/components/level_component_schema.coffee b/server/levels/components/level_component_schema.coffee deleted file mode 100644 index 45135a774..000000000 --- a/server/levels/components/level_component_schema.coffee +++ /dev/null @@ -1,120 +0,0 @@ -c = require '../../commons/schemas' -metaschema = require '../../commons/metaschema' - -attackSelfCode = """ -class AttacksSelf extends Component - @className: "AttacksSelf" - chooseAction: -> - @attack @ -""" -systems = [ - 'action', 'ai', 'alliance', 'collision', 'combat', 'display', 'event', 'existence', 'hearing' - 'inventory', 'movement', 'programming', 'targeting', 'ui', 'vision', 'misc', 'physics', 'effect', - 'magic' -] - -PropertyDocumentationSchema = c.object { - title: "Property Documentation" - description: "Documentation entry for a property this Component will add to its Thang which other Components might - want to also use." - "default": - name: "foo" - type: "object" - description: 'The `foo` property can satisfy all the #{spriteName}\'s foobar needs. Use it wisely.' - required: ['name', 'type', 'description'] -}, - name: {type: 'string', title: "Name", description: "Name of the property."} - # not actual JS types, just whatever they describe... - type: c.shortString(title: "Type", description: "Intended type of the property.") - description: {title: "Description", type: 'string', description: "Description of the property.", format: 'markdown', maxLength: 1000} - args: c.array {title: "Arguments", description: "If this property has type 'function', then provide documentation for any function arguments."}, c.FunctionArgumentSchema - owner: {title: "Owner", type: 'string', description: 'Owner of the property, like "this" or "Math".'} - example: {title: "Example", type: 'string', description: 'An optional example code block.', format: 'javascript'} - returns: c.object { - title: "Return Value" - description: 'Optional documentation of any return value.' - required: ['type'] - default: {type: 'null'} - }, - type: c.shortString(title: "Type", description: "Type of the return value") - example: c.shortString(title: "Example", description: "Example return value") - description: {title: "Description", type: 'string', description: "Description of the return value.", maxLength: 1000} - -DependencySchema = c.object { - title: "Component Dependency" - description: "A Component upon which this Component depends." - "default": - #original: ? - majorVersion: 0 - required: ["original", "majorVersion"] - format: 'latest-version-reference' - links: [{rel: "db", href: "/db/level.component/{(original)}/version/{(majorVersion)}"}] -}, - original: c.objectId(title: "Original", description: "A reference to another Component upon which this Component depends.") - majorVersion: - title: "Major Version" - description: "Which major version of the Component this Component needs." - type: 'integer' - minimum: 0 - -LevelComponentSchema = c.object { - title: "Component" - description: "A Component which can affect Thang behavior." - required: ["system", "name", "description", "code", "dependencies", "propertyDocumentation", "language"] - "default": - system: "ai" - name: "AttacksSelf" - description: "This Component makes the Thang attack itself." - code: attackSelfCode - language: "coffeescript" - dependencies: [] # TODO: should depend on something by default - propertyDocumentation: [] -} -c.extendNamedProperties LevelComponentSchema # let's have the name be the first property -LevelComponentSchema.properties.name.pattern = c.classNamePattern -_.extend LevelComponentSchema.properties, - system: - title: "System" - description: "The short name of the System this Component belongs to, like \"ai\"." - type: "string" - "enum": systems - "default": "ai" - description: - title: "Description" - description: "A short explanation of what this Component does." - type: "string" - maxLength: 2000 - "default": "This Component makes the Thang attack itself." - language: - type: "string" - title: "Language" - description: "Which programming language this Component is written in." - "enum": ["coffeescript"] - code: - title: "Code" - description: "The code for this Component, as a CoffeeScript class. TODO: add link to documentation for - how to write these." - "default": attackSelfCode - type: "string" - format: "coffee" - js: - title: "JavaScript" - description: "The transpiled JavaScript code for this Component" - type: "string" - format: "hidden" - dependencies: c.array {title: "Dependencies", description: "An array of Components upon which this Component depends.", "default": [], uniqueItems: true}, DependencySchema - propertyDocumentation: c.array {title: "Property Documentation", description: "An array of documentation entries for each notable property this Component will add to its Thang which other Components might want to also use.", "default": []}, PropertyDocumentationSchema - configSchema: _.extend metaschema, {title: "Configuration Schema", description: "A schema for validating the arguments that can be passed to this Component as configuration.", default: {type: 'object', additionalProperties: false}} - official: - type: "boolean" - title: "Official" - description: "Whether this is an official CodeCombat Component." - "default": false - -c.extendBasicProperties LevelComponentSchema, 'level.component' -c.extendSearchableProperties LevelComponentSchema -c.extendVersionedProperties LevelComponentSchema, 'level.component' -c.extendPermissionsProperties LevelComponentSchema, 'level.component' -c.extendPatchableProperties LevelComponentSchema - -module.exports = LevelComponentSchema diff --git a/server/levels/feedbacks/level_feedback_handler.coffee b/server/levels/feedbacks/level_feedback_handler.coffee index 21f581ea7..cd4ffda26 100644 --- a/server/levels/feedbacks/level_feedback_handler.coffee +++ b/server/levels/feedbacks/level_feedback_handler.coffee @@ -4,7 +4,7 @@ Handler = require('../../commons/Handler') class LevelFeedbackHandler extends Handler modelClass: LevelFeedback editableProperties: ['rating', 'review', 'level', 'levelID', 'levelName'] - jsonSchema: require './level_feedback_schema' + jsonSchema: require '../../../app/schemas/level_feedback_schema' makeNewInstance: (req) -> feedback = super(req) diff --git a/server/levels/level_handler.coffee b/server/levels/level_handler.coffee index f2e9f0228..741c56eba 100644 --- a/server/levels/level_handler.coffee +++ b/server/levels/level_handler.coffee @@ -8,7 +8,7 @@ mongoose = require('mongoose') LevelHandler = class LevelHandler extends Handler modelClass: Level - jsonSchema: require './level_schema' + jsonSchema: require '../../app/schemas/level_schema' editableProperties: [ 'description' 'documentation' diff --git a/server/levels/level_schema.coffee b/server/levels/level_schema.coffee deleted file mode 100644 index 984236cfd..000000000 --- a/server/levels/level_schema.coffee +++ /dev/null @@ -1,255 +0,0 @@ -c = require '../commons/schemas' -ThangComponentSchema = require './thangs/thang_component_schema' - -SpecificArticleSchema = c.object() -c.extendNamedProperties SpecificArticleSchema # name first -SpecificArticleSchema.properties.body = { type: 'string', title: 'Content', description: "The body content of the article, in Markdown.", format: 'markdown' } -SpecificArticleSchema.displayProperty = 'name' - -side = {title: "Side", description: "A side.", type: 'string', 'enum': ['left', 'right', 'top', 'bottom']} -thang = {title: "Thang", description: "The name of a Thang.", type: 'string', maxLength: 30, format:'thang'} - -eventPrereqValueTypes = ["boolean", "integer", "number", "null", "string"] # not "object" or "array" -EventPrereqSchema = c.object {title: "Event Prerequisite", format: 'event-prereq', description: "Script requires that the value of some property on the event triggering it to meet some prerequisite.", "default": {eventProps: []}, required: ["eventProps"]}, - eventProps: c.array {'default': ["thang"], format:'event-value-chain', maxItems: 10, title: "Event Property", description: 'A chain of keys in the event, like "thang.pos.x" to access event.thang.pos.x.'}, c.shortString(title: "Property", description: "A key in the event property key chain.") - equalTo: c.object {type: eventPrereqValueTypes, title: "==", description: "Script requires the event's property chain value to be equal to this value."} - notEqualTo: c.object {type: eventPrereqValueTypes, title: "!=", description: "Script requires the event's property chain value to *not* be equal to this value."} - greaterThan: {type: 'number', title: ">", description: "Script requires the event's property chain value to be greater than this value."} - greaterThanOrEqualTo: {type: 'number', title: ">=", description: "Script requires the event's property chain value to be greater or equal to this value."} - lessThan: {type: 'number', title: "<", description: "Script requires the event's property chain value to be less than this value."} - lessThanOrEqualTo: {type: 'number', title: "<=", description: "Script requires the event's property chain value to be less than or equal to this value."} - containingString: c.shortString(title: "Contains", description: "Script requires the event's property chain value to be a string containing this string.") - notContainingString: c.shortString(title: "Does not contain", description: "Script requires the event's property chain value to *not* be a string containing this string.") - containingRegexp: c.shortString(title: "Contains Regexp", description: "Script requires the event's property chain value to be a string containing this regular expression.") - notContainingRegexp: c.shortString(title: "Does not contain regexp", description: "Script requires the event's property chain value to *not* be a string containing this regular expression.") - -GoalSchema = c.object {title: "Goal", description: "A goal that the player can accomplish.", required: ["name", "id"]}, - name: c.shortString(title: "Name", description: "Name of the goal that the player will see, like \"Defeat eighteen dragons\".") - i18n: {type: "object", format: 'i18n', props: ['name'], description: "Help translate this goal"} - id: c.shortString(title: "ID", description: "Unique identifier for this goal, like \"defeat-dragons\".") # unique somehow? - worldEndsAfter: {title: 'World Ends After', description: "When included, ends the world this many seconds after this goal succeeds or fails.", type: 'number', minimum: 0, exclusiveMinimum: true, maximum: 300, default: 3} - howMany: {title: "How Many", description: "When included, require only this many of the listed goal targets instead of all of them.", type: 'integer', minimum: 1} - hiddenGoal: {title: "Hidden", description: "Hidden goals don't show up in the goals area for the player until they're failed. (Usually they're obvious, like 'don't die'.)", 'type': 'boolean', default: false} - team: c.shortString(title: 'Team', description: 'Name of the team this goal is for, if it is not for all of the playable teams.') - killThangs: c.array {title: "Kill Thangs", description: "A list of Thang IDs the player should kill, or team names.", uniqueItems: true, minItems: 1, "default": ["ogres"]}, thang - saveThangs: c.array {title: "Save Thangs", description: "A list of Thang IDs the player should save, or team names", uniqueItems: true, minItems: 1, "default": ["humans"]}, thang - getToLocations: c.object {title: "Get To Locations", description: "Will be set off when any of the \"who\" touch any of the \"targets\" ", required: ["who", "targets"]}, - who: c.array {title: "Who", description: "The Thangs who must get to the target locations.", minItems: 1}, thang - targets: c.array {title: "Targets", description: "The target locations to which the Thangs must get.", minItems: 1}, thang - getAllToLocations: c.array {title: "Get all to locations", description: "Similar to getToLocations but now a specific \"who\" can have a specific \"target\", also must be used with the HowMany property for desired effect",required: ["getToLocation"]}, - c.object {title: "", description: ""}, - getToLocation: c.object {title: "Get To Locations", description: "TODO: explain", required: ["who", "targets"]}, - who: c.array {title: "Who", description: "The Thangs who must get to the target locations.", minItems: 1}, thang - targets: c.array {title: "Targets", description: "The target locations to which the Thangs must get.", minItems: 1}, thang - keepFromLocations: c.object {title: "Keep From Locations", description: "TODO: explain", required: ["who", "targets"]}, - who: c.array {title: "Who", description: "The Thangs who must not get to the target locations.", minItems: 1}, thang - targets: c.array {title: "Targets", description: "The target locations to which the Thangs must not get.", minItems: 1}, thang - keepAllFromLocations: c.array {title: "Keep ALL From Locations", description: "Similar to keepFromLocations but now a specific \"who\" can have a specific \"target\", also must be used with the HowMany property for desired effect", required: ["keepFromLocation"]}, - c.object {title: "", description: ""}, - keepFromLocation: c.object {title: "Keep From Locations", description: "TODO: explain", required: ["who", "targets"]}, - who: c.array {title: "Who", description: "The Thangs who must not get to the target locations.", minItems: 1}, thang - targets: c.array {title: "Targets", description: "The target locations to which the Thangs must not get.", minItems: 1}, thang - leaveOffSides: c.object {title: "Leave Off Sides", description: "Sides of the level to get some Thangs to leave across.", required: ["who", "sides"]}, - who: c.array {title: "Who", description: "The Thangs which must leave off the sides of the level.", minItems: 1}, thang - sides: c.array {title: "Sides", description: "The sides off which the Thangs must leave.", minItems: 1}, side - keepFromLeavingOffSides: c.object {title: "Keep From Leaving Off Sides", description: "Sides of the level to keep some Thangs from leaving across.", required: ["who", "sides"]}, - who: c.array {title: "Who", description: "The Thangs which must not leave off the sides of the level.", minItems: 1}, thang - sides: side, {title: "Sides", description: "The sides off which the Thangs must not leave.", minItems: 1}, side - collectThangs: c.object {title: "Collect", description: "Thangs that other Thangs must collect.", required: ["who", "targets"]}, - who: c.array {title: "Who", description: "The Thangs which must collect the target items.", minItems: 1}, thang - targets: c.array {title: "Targets", description: "The target items which the Thangs must collect.", minItems: 1}, thang - keepFromCollectingThangs: c.object {title: "Keep From Collecting", description: "Thangs that the player must prevent other Thangs from collecting.", required: ["who", "targets"]}, - who: c.array {title: "Who", description: "The Thangs which must not collect the target items.", minItems: 1}, thang - targets: c.array {title: "Targets", description: "The target items which the Thangs must not collect.", minItems: 1}, thang - -ResponseSchema = c.object {title: "Dialogue Button", description: "A button to be shown to the user with the dialogue.", required: ["text"]}, - text: {title: "Title", description: "The text that will be on the button", "default": "Okay", type: 'string', maxLength: 30} - channel: c.shortString(title: "Channel", format: 'event-channel', description: 'Channel that this event will be broadcast over, like "level-set-playing".') - event: {type: 'object', title: "Event", description: "Event that will be broadcast when this button is pressed, like {playing: true}."} - buttonClass: c.shortString(title: "Button Class", description: 'CSS class that will be added to the button, like "btn-primary".') - i18n: {type: "object", format: 'i18n', props: ['text'], description: "Help translate this button"} - -PointSchema = c.object {title: "Point", description: "An {x, y} coordinate point.", format: "point2d", required: ["x", "y"]}, - x: {title: "x", description: "The x coordinate.", type: "number", "default": 15} - y: {title: "y", description: "The y coordinate.", type: "number", "default": 20} - -SpriteCommandSchema = c.object {title: "Thang Command", description: "Make a target Thang move or say something, or select/deselect it.", required: ["id"], default: {id: "Captain Anya"}}, - id: thang - select: {title: "Select", description: "Select or deselect this Thang.", type: 'boolean'} - say: c.object {title: "Say", description: "Make this Thang say a message.", required: ["text"]}, - blurb: c.shortString(title: "Blurb", description: "A very short message to display above this Thang's head. Plain text.", maxLength: 50) - mood: c.shortString(title: "Mood", description: "The mood with which the Thang speaks.", "enum": ["explain", "debrief", "congrats", "attack", "joke", "tip", "alarm"], "default": "explain") - text: {title: "Text", description: "A short message to display in the dialogue area. Markdown okay.", type: "string", maxLength: 400} - sound: c.object {title: "Sound", description: "A dialogue sound file to accompany the message.", required: ["mp3", "ogg"]}, - mp3: c.shortString(title: "MP3", format: 'sound-file') - ogg: c.shortString(title: "OGG", format: 'sound-file') - preload: {title: "Preload", description: "Whether to load this sound file before the level can begin (typically for the first dialogue of a level).", type: 'boolean', "default": false} - responses: c.array {title: "Buttons", description: "An array of buttons to include with the dialogue, with which the user can respond."}, ResponseSchema - i18n: {type: "object", format: 'i18n', props: ['blurb', 'text'], description: "Help translate this message"} - move: c.object {title: "Move", description: "Tell the Thang to move.", required: ['target'], default: {target: {x: 20, y: 20}, duration: 500}}, - target: _.extend _.cloneDeep(PointSchema), {title: 'Target', description: 'Target point to which the Thang will move.'} - duration: {title: "Duration", description: "Number of milliseconds over which to move, or 0 for an instant move.", type: 'integer', minimum: 0, default: 500, format: 'milliseconds'} - -NoteGroupSchema = c.object {title: "Note Group", description: "A group of notes that should be sent out as a result of this script triggering.", displayProperty: "name"}, - name: {title: "Name", description: "Short name describing the script, like \"Anya greets the player\", for your convenience.", type: "string"} - dom: c.object {title: "DOM", description: "Manipulate things in the play area DOM, outside of the level area canvas."}, - focus: c.shortString(title: "Focus", description: "Set the window focus to this DOM selector string.") - showVictory: { - title: "Show Victory", - description: "Show the done button and maybe also the victory modal.", - enum: [true, 'Done Button', 'Done Button And Modal'] # deprecate true, same as 'done_button_and_modal' - } - highlight: c.object {title: "Highlight", description: "Highlight the target DOM selector string with a big arrow."}, - target: c.shortString(title: "Target", description: "Target highlight element DOM selector string.") - delay: {type: 'integer', minimum: 0, title: "Delay", description: "Show the highlight after this many milliseconds. Doesn't affect the dim shade cutout highlight method."} - offset: _.extend _.cloneDeep(PointSchema), {title: 'Offset', description: 'Pointing arrow tip offset in pixels from the default target.', format: null} - rotation: {type: 'number', minimum: 0, title: "Rotation", description: "Rotation of the pointing arrow, in radians. PI / 2 points left, PI points up, etc."} - sides: c.array {title: "Sides", description: "Which sides of the target element to point at."}, {type: 'string', 'enum': ['left', 'right', 'top', 'bottom'], title: "Side", description: "A side of the target element to point at."} - lock: {title: "Lock", description: "Whether the interface should be locked so that the player's focus is on the script, or specific areas to lock.", type: ['boolean', 'array'], items: {type: 'string', enum: ['surface', 'editor', 'palette', 'hud', 'playback', 'playback-hover', 'level', ]}} - letterbox: {type: 'boolean', title: 'Letterbox', description:'Turn letterbox mode on or off. Disables surface and playback controls.'} - - goals: c.object {title: "Goals (Old)", description: "Deprecated. Goals added here have no effect. Add goals in the level settings instead."}, - add: c.array {title: "Add", description: "Deprecated. Goals added here have no effect. Add goals in the level settings instead."}, GoalSchema - remove: c.array {title: "Remove", description: "Deprecated. Goals removed here have no effect. Adjust goals in the level settings instead."}, GoalSchema - - playback: c.object {title: "Playback", description: "Control the playback of the level."}, - playing: {type: 'boolean', title: "Set Playing", description: "Set whether playback is playing or paused."} - scrub: c.object {title: "Scrub", description: "Scrub the level playback time to a certain point.", default: {offset: 2, duration: 1000, toRatio: 0.5}}, - offset: {type: 'integer', title: "Offset", description: "Number of frames by which to adjust the scrub target time.", default: 2} - duration: {type: 'integer', title: "Duration", description: "Number of milliseconds over which to scrub time.", minimum: 0, format: 'milliseconds'} - toRatio: {type: 'number', title: "To Progress Ratio", description: "Set playback time to a target playback progress ratio.", minimum: 0, maximum: 1} - toTime: {type: 'number', title: "To Time", description: "Set playback time to a target playback point, in seconds.", minimum: 0} - toGoal: c.shortString(title: "To Goal", description: "Set playback time to when this goal was achieved. (TODO: not implemented.)") - - script: c.object {title: "Script", description: "Extra configuration for this action group."}, - duration: {type: 'integer', minimum: 0, title: "Duration", description: "How long this script should last in milliseconds. 0 for indefinite.", format: 'milliseconds'} - skippable: {type: 'boolean', title: "Skippable", description: "Whether this script shouldn't bother firing when the player skips past all current scripts."} - beforeLoad: {type: 'boolean', title: "Before Load", description: "Whether this script should fire before the level is finished loading."} - - sprites: c.array {title: "Sprites", description: "Commands to issue to Sprites on the Surface."}, SpriteCommandSchema - - surface: c.object {title: "Surface", description: "Commands to issue to the Surface itself."}, - focus: c.object {title: "Camera", description: "Focus the camera on a specific point on the Surface.", format:'viewport'}, - target: {anyOf: [PointSchema, thang, {type: 'null'}], title: "Target", description: "Where to center the camera view."} - zoom: {type: 'number', minimum: 0, exclusiveMinimum: true, maximum: 64, title: "Zoom", description: "What zoom level to use."} - duration: {type:'number', minimum: 0, title: "Duration", description: "in ms"} - bounds: c.array {title:'Boundary', maxItems: 2, minItems: 2, default:[{x:0,y:0}, {x:46, y:39}], format: 'bounds'}, PointSchema - isNewDefault: {type:'boolean', format: 'hidden', title: "New Default", description: 'Set this as new default zoom once scripts end.'} # deprecated - highlight: c.object {title: "Highlight", description: "Highlight specific Sprites on the Surface."}, - targets: c.array {title: "Targets", description: "Thang IDs of target Sprites to highlight."}, thang - delay: {type: 'integer', minimum: 0, title: "Delay", description: "Delay in milliseconds before the highlight appears."} - lockSelect: {type: 'boolean', title: "Lock Select", description: "Whether to lock Sprite selection so that the player can't select/deselect anything."} - - sound: c.object {title: "Sound", description: "Commands to control sound playback."}, - suppressSelectionSounds: {type: "boolean", title: "Suppress Selection Sounds", description: "Whether to suppress selection sounds made from clicking on Thangs."} - music: c.object { title: "Music", description: "Control music playing"}, - play: { title: "Play", type: "boolean" } - file: c.shortString(title: "File", enum:['/music/music_level_1','/music/music_level_2','/music/music_level_3','/music/music_level_4','/music/music_level_5']) - -ScriptSchema = c.object { - title: "Script" - description: 'A script fires off a chain of notes to interact with the game when a certain event triggers it.' - required: ["channel"] - 'default': {channel: "world:won", noteChain: []} -}, - id: c.shortString(title: "ID", description: "A unique ID that other scripts can rely on in their Happens After prereqs, for sequencing.") # uniqueness? - channel: c.shortString(title: "Event", format: 'event-channel', description: 'Event channel this script might trigger for, like "world:won".') - eventPrereqs: c.array {title: "Event Checks", description: "Logical checks on the event for this script to trigger.", format:'event-prereqs'}, EventPrereqSchema - repeats: {title: "Repeats", description: "Whether this script can trigger more than once during a level.", enum: [true, false, 'session'], "default": false} - scriptPrereqs: c.array {title: "Happens After", description: "Scripts that need to fire first."}, - c.shortString(title: "ID", description: "A unique ID of a script.") - notAfter: c.array {title: "Not After", description: "Do not run this script if any of these scripts have run."}, - c.shortString(title: "ID", description: "A unique ID of a script.") - noteChain: c.array {title: "Actions", description: "A list of things that happen when this script triggers."}, NoteGroupSchema - -LevelThangSchema = c.object { - title: "Thang", - description: "Thangs are any units, doodads, or abstract things that you use to build the level. (\"Thing\" was too confusing to say.)", - format: "thang" - required: ["id", "thangType", "components"] - 'default': - id: "Boris" - thangType: "Soldier" - components: [] -}, - id: thang # TODO: figure out if we can make this unique and how to set dynamic defaults - # TODO: split thangType into "original" and "majorVersion" like the rest for consistency - thangType: c.objectId(links: [{rel: "db", href: "/db/thang.type/{($)}/version"}], title: "Thang Type", description: "A reference to the original Thang template being configured.", format: 'thang-type') - 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 - -LevelSystemSchema = c.object { - title: "System" - description: "Configuration for a System that this Level uses." - format: 'level-system' - required: ['original', 'majorVersion'] - 'default': - majorVersion: 0 - config: {} - links: [{rel: "db", href: "/db/level.system/{(original)}/version/{(majorVersion)}"}] -}, - original: c.objectId(title: "Original", description: "A reference to the original System being configured.", format: "hidden") - config: c.object {title: "Configuration", description: "System-specific configuration properties.", additionalProperties: true, format: 'level-system-configuration'} - majorVersion: {title: "Major Version", description: "Which major version of the System is being used.", type: 'integer', minimum: 0, default: 0, format: "hidden"} - -GeneralArticleSchema = c.object { - title: "Article" - description: "Reference to a general documentation article." - required: ['original'] - format: 'latest-version-reference' - 'default': - original: null - majorVersion: 0 - links: [{rel: "db", href: "/db/article/{(original)}/version/{(majorVersion)}"}] -}, - original: c.objectId(title: "Original", description: "A reference to the original Article.")#, format: "hidden") # hidden? - majorVersion: {title: "Major Version", description: "Which major version of the Article is being used.", type: 'integer', minimum: 0}#, format: "hidden"} # hidden? - -LevelSchema = c.object { - title: "Level" - description: "A spectacular level which will delight and educate its stalwart players with the sorcery of coding." - required: ["name", "description", "scripts", "thangs", "documentation"] - 'default': - name: "Ineffable Wizardry" - description: "This level is indescribably flarmy." - documentation: {specificArticles: [], generalArticles: []} - scripts: [] - thangs: [] -} -c.extendNamedProperties LevelSchema # let's have the name be the first property -_.extend LevelSchema.properties, - description: {title: "Description", description: "A short explanation of what this level is about.", type: "string", maxLength: 65536, "default": "This level is indescribably flarmy!", format: 'markdown'} - documentation: c.object {title: "Documentation", description: "Documentation articles relating to this level.", required: ["specificArticles", "generalArticles"], 'default': {specificArticles: [], generalArticles: []}}, - specificArticles: c.array {title: "Specific Articles", description: "Specific documentation articles that live only in this level.", uniqueItems: true, "default": []}, SpecificArticleSchema - generalArticles: c.array {title: "General Articles", description: "General documentation articles that can be linked from multiple levels.", uniqueItems: true, "default": []}, GeneralArticleSchema - background: c.objectId({format: 'hidden'}) - nextLevel: { - type:'object', - links: [{rel: "extra", href: "/db/level/{($)}"}, {rel:'db', href: "/db/level/{(original)}/version/{(majorVersion)}"}], - format: 'latest-version-reference', - title: "Next Level", - description: "Reference to the next level players will player after beating this one." - } - scripts: c.array {title: "Scripts", description: "An array of scripts that trigger based on what the player does and affect things outside of the core level simulation.", "default": []}, ScriptSchema - thangs: c.array {title: "Thangs", description: "An array of Thangs that make up the level.", "default": []}, LevelThangSchema - systems: c.array {title: "Systems", description: "Levels are configured by changing the Systems attached to them.", uniqueItems: true, default: []}, LevelSystemSchema # TODO: uniqueness should be based on "original", not whole thing - victory: c.object {title: "Victory Screen", default: {}, properties: {'body': {type: 'string', format: 'markdown', title: 'Body Text', description: 'Inserted into the Victory Modal once this level is complete. Tell the player they did a good job and what they accomplished!'}, i18n: {type: "object", format: 'i18n', props: ['body'], description: "Help translate this victory message"}}} - i18n: {type: "object", format: 'i18n', props: ['name', 'description'], description: "Help translate this level"} - icon: { type: 'string', format: 'image-file', title: 'Icon' } - goals: c.array {title: 'Goals', description: 'An array of goals which are visible to the player and can trigger scripts.'}, GoalSchema - type: c.shortString(title: "Type", description: "What kind of level this is.", "enum": ['campaign', 'ladder', 'ladder-tutorial']) - showsGuide: c.shortString(title: "Shows Guide", description: "If the guide is shown at the beginning of the level.", "enum": ['first-time', 'always']) - -c.extendBasicProperties LevelSchema, 'level' -c.extendSearchableProperties LevelSchema -c.extendVersionedProperties LevelSchema, 'level' -c.extendPermissionsProperties LevelSchema, 'level' -c.extendPatchableProperties LevelSchema - -module.exports = LevelSchema - -# To test: -# 1: Copy the schema from http://localhost:3000/db/level/schema -# 2. Open up the Treema demo page http://localhost:9090/demo.html -# 3. tv4.addSchema(metaschema.id, metaschema) -# 4. S = <paste big schema here> -# 5. tv4.validateMultiple(S, metaschema) and look for errors diff --git a/server/levels/sessions/level_session_handler.coffee b/server/levels/sessions/level_session_handler.coffee index eaa5e4ed6..25131833d 100644 --- a/server/levels/sessions/level_session_handler.coffee +++ b/server/levels/sessions/level_session_handler.coffee @@ -9,7 +9,7 @@ class LevelSessionHandler extends Handler editableProperties: ['multiplayer', 'players', 'code', 'completed', 'state', 'levelName', 'creatorName', 'levelID', 'screenshot', 'chat', 'teamSpells', 'submitted', 'unsubscribed'] - jsonSchema: require './level_session_schema' + jsonSchema: require '../../../app/schemas/level_session_schema' getByRelationship: (req, res, args...) -> return @getActiveSessions req, res if args.length is 2 and args[1] is 'active' diff --git a/server/levels/systems/level_system_handler.coffee b/server/levels/systems/level_system_handler.coffee index a76fed659..c3fd0a366 100644 --- a/server/levels/systems/level_system_handler.coffee +++ b/server/levels/systems/level_system_handler.coffee @@ -13,7 +13,7 @@ LevelSystemHandler = class LevelSystemHandler extends Handler 'configSchema' ] postEditableProperties: ['name'] - jsonSchema: require './level_system_schema' + jsonSchema: require '../../../app/schemas/level_system_schema' getEditableProperties: (req, document) -> props = super(req, document) diff --git a/server/levels/systems/level_system_schema.coffee b/server/levels/systems/level_system_schema.coffee deleted file mode 100644 index 7adcb969e..000000000 --- a/server/levels/systems/level_system_schema.coffee +++ /dev/null @@ -1,107 +0,0 @@ -c = require '../../commons/schemas' -metaschema = require '../../commons/metaschema' - -jitterSystemCode = """ -class Jitter extends System - constructor: (world, config) -> - super world, config - @idlers = @addRegistry (thang) -> thang.exists and thang.acts and thang.moves and thang.action is 'idle' - - update: -> - # We return a simple numeric hash that will combine to a frame hash - # help us determine whether this frame has changed in resimulations. - hash = 0 - for thang in @idlers - hash += thang.pos.x += 0.5 - Math.random() - hash += thang.pos.y += 0.5 - Math.random() - thang.hasMoved = true - return hash -""" - -PropertyDocumentationSchema = c.object { - title: "Property Documentation" - description: "Documentation entry for a property this System will add to its Thang which other Systems - might want to also use." - "default": - name: "foo" - type: "object" - description: "This System provides a 'foo' property to satisfy all one's foobar needs. Use it wisely." - required: ['name', 'type', 'description'] -}, - name: {type: 'string', pattern: c.identifierPattern, title: "Name", description: "Name of the property."} - # not actual JS types, just whatever they describe... - type: c.shortString(title: "Type", description: "Intended type of the property.") - description: {type: 'string', description: "Description of the property.", maxLength: 1000} - args: c.array {title: "Arguments", description: "If this property has type 'function', then provide documentation for any function arguments."}, c.FunctionArgumentSchema - -DependencySchema = c.object { - title: "System Dependency" - description: "A System upon which this System depends." - "default": - #original: ? - majorVersion: 0 - required: ["original", "majorVersion"] - format: 'latest-version-reference' - links: [{rel: "db", href: "/db/level.system/{(original)}/version/{(majorVersion)}"}] -}, - original: c.objectId(title: "Original", description: "A reference to another System upon which this System depends.") - majorVersion: - title: "Major Version" - description: "Which major version of the System this System needs." - type: 'integer' - minimum: 0 - -LevelSystemSchema = c.object { - title: "System" - description: "A System which can affect Level behavior." - required: ["name", "description", "code", "dependencies", "propertyDocumentation", "language"] - "default": - name: "JitterSystem" - description: "This System makes all idle, movable Thangs jitter around." - code: jitterSystemCode - language: "coffeescript" - dependencies: [] # TODO: should depend on something by default - propertyDocumentation: [] -} -c.extendNamedProperties LevelSystemSchema # let's have the name be the first property -LevelSystemSchema.properties.name.pattern = c.classNamePattern -_.extend LevelSystemSchema.properties, - description: - title: "Description" - description: "A short explanation of what this System does." - type: "string" - maxLength: 2000 - "default": "This System doesn't do anything yet." - language: - type: "string" - title: "Language" - description: "Which programming language this System is written in." - "enum": ["coffeescript"] - code: - title: "Code" - description: "The code for this System, as a CoffeeScript class. TODO: add link to documentation - for how to write these." - "default": jitterSystemCode - type: "string" - format: "coffee" - js: - title: "JavaScript" - description: "The transpiled JavaScript code for this System" - type: "string" - format: "hidden" - dependencies: c.array {title: "Dependencies", description: "An array of Systems upon which this System depends.", "default": [], uniqueItems: true}, DependencySchema - propertyDocumentation: c.array {title: "Property Documentation", description: "An array of documentation entries for each notable property this System will add to its Level which other Systems might want to also use.", "default": []}, PropertyDocumentationSchema - configSchema: _.extend metaschema, {title: "Configuration Schema", description: "A schema for validating the arguments that can be passed to this System as configuration.", default: {type: 'object', additionalProperties: false}} - official: - type: "boolean" - title: "Official" - description: "Whether this is an official CodeCombat System." - "default": false - -c.extendBasicProperties LevelSystemSchema, 'level.system' -c.extendSearchableProperties LevelSystemSchema -c.extendVersionedProperties LevelSystemSchema, 'level.system' -c.extendPermissionsProperties LevelSystemSchema -c.extendPatchableProperties LevelSystemSchema - -module.exports = LevelSystemSchema diff --git a/server/levels/thangs/thang_type_handler.coffee b/server/levels/thangs/thang_type_handler.coffee index 0627fc5f7..851d2ccf6 100644 --- a/server/levels/thangs/thang_type_handler.coffee +++ b/server/levels/thangs/thang_type_handler.coffee @@ -3,7 +3,7 @@ Handler = require('../../commons/Handler') ThangTypeHandler = class ThangTypeHandler extends Handler modelClass: ThangType - jsonSchema: require './thang_type_schema' + jsonSchema: require '../../../app/schemas/thang_type_schema' editableProperties: [ 'name', 'raw', diff --git a/server/levels/thangs/thang_type_schema.coffee b/server/levels/thangs/thang_type_schema.coffee deleted file mode 100644 index 68eb6d084..000000000 --- a/server/levels/thangs/thang_type_schema.coffee +++ /dev/null @@ -1,154 +0,0 @@ -c = require '../../commons/schemas' -ThangComponentSchema = require './thang_component_schema' - -ThangTypeSchema = c.object() -c.extendNamedProperties ThangTypeSchema # name first - -ShapeObjectSchema = c.object { title: 'Shape' }, - fc: { type: 'string', title: 'Fill Color' } - lf: { type: 'array', title: 'Linear Gradient Fill' } - ls: { type: 'array', title: 'Linear Gradient Stroke' } - p: { type: 'string', title: 'Path' } - de: { type: 'array', title: 'Draw Ellipse' } - sc: { type: 'string', title: 'Stroke Color' } - ss: { type: 'array', title: 'Stroke Style' } - t: c.array {}, { type: 'number', title: 'Transform' } - m: { type: 'string', title: 'Mask' } - -ContainerObjectSchema = c.object { format: 'container' }, - b: c.array { title: 'Bounds' }, { type: 'number' } - c: c.array { title: 'Children' }, { anyOf: [ - { type: 'string', title: 'Shape Child' }, - c.object { title: 'Container Child' } - gn: { type: 'string', title: 'Global Name' } - t: c.array {}, { type: 'number' } - ]} - -RawAnimationObjectSchema = c.object {}, - bounds: c.array { title: 'Bounds' }, { type: 'number' } - frameBounds: c.array { title: 'Frame Bounds' }, c.array { title: 'Bounds' }, { type: 'number' } - shapes: c.array {}, - bn: { type: 'string', title: 'Block Name' } - gn: { type: 'string', title: 'Global Name' } - im : { type: 'boolean', title: 'Is Mask' } - m: { type: 'string', title: 'Uses Mask' } - containers: c.array {}, - bn: { type: 'string', title: 'Block Name' } - gn: { type: 'string', title: 'Global Name' } - t: c.array {}, { type: 'number' } - o: { type: 'boolean', title: 'Starts Hidden (_off)'} - al: { type: 'number', title: 'Alpha'} - animations: c.array {}, - bn: { type: 'string', title: 'Block Name' } - gn: { type: 'string', title: 'Global Name' } - t: c.array {}, { type: 'number', title: 'Transform' } - a: c.array { title: 'Arguments' } - tweens: c.array {}, - c.array { title: 'Function Chain', }, - c.object { title: 'Function Call' }, - n: { type: 'string', title: 'Name' } - a: c.array { title: 'Arguments' } - graphics: c.array {}, - bn: { type: 'string', title: 'Block Name' } - p: { type: 'string', title: 'Path' } - -PositionsSchema = c.object { title: 'Positions', description: 'Customize position offsets.' }, - registration: c.point2d { title: 'Registration Point', description: "Action-specific registration point override." } - torso: c.point2d { title: 'Torso Offset', description: "Action-specific torso offset override." } - mouth: c.point2d { title: 'Mouth Offset', description: "Action-specific mouth offset override." } - aboveHead: c.point2d { title: 'Above Head Offset', description: "Action-specific above-head offset override." } - -ActionSchema = c.object {}, - animation: { type: 'string', description: 'Raw animation being sourced', format: 'raw-animation' } - container: { type: 'string', description: 'Name of the container to show' } - relatedActions: c.object { }, - begin: { $ref: '#/definitions/action' } - end: { $ref: '#/definitions/action' } - main: { $ref: '#/definitions/action' } - fore: { $ref: '#/definitions/action' } - back: { $ref: '#/definitions/action' } - side: { $ref: '#/definitions/action' } - - "?0?011?11?11": { $ref: '#/definitions/action', title: "NW corner" } - "?0?11011?11?": { $ref: '#/definitions/action', title: "NE corner, flipped" } - "?0?111111111": { $ref: '#/definitions/action', title: "N face" } - "?11011011?0?": { $ref: '#/definitions/action', title: "SW corner, top" } - "11?11?110?0?": { $ref: '#/definitions/action', title: "SE corner, top, flipped" } - "?11011?0????": { $ref: '#/definitions/action', title: "SW corner, bottom" } - "11?110?0????": { $ref: '#/definitions/action', title: "SE corner, bottom, flipped" } - "?11011?11?11": { $ref: '#/definitions/action', title: "W face" } - "11?11011?11?": { $ref: '#/definitions/action', title: "E face, flipped" } - "011111111111": { $ref: '#/definitions/action', title: "NW elbow" } - "110111111111": { $ref: '#/definitions/action', title: "NE elbow, flipped" } - "111111111?0?": { $ref: '#/definitions/action', title: "S face, top" } - "111111?0????": { $ref: '#/definitions/action', title: "S face, bottom" } - "111111111011": { $ref: '#/definitions/action', title: "SW elbow, top" } - "111111111110": { $ref: '#/definitions/action', title: "SE elbow, top, flipped" } - "111111011?11": { $ref: '#/definitions/action', title: "SW elbow, bottom" } - "11111111011?": { $ref: '#/definitions/action', title: "SE elbow, bottom, flipped" } - "111111111111": { $ref: '#/definitions/action', title: "Middle" } - - loops: { type: 'boolean' } - speed: { type: 'number' } - goesTo: { type: 'string', description: 'Action (animation?) to which we switch after this animation.' } - frames: { type: 'string', pattern:'^[0-9,]+$', description: 'Manually way to specify frames.' } - framerate: { type: 'number', description: 'Get this from the HTML output.' } - positions: PositionsSchema - scale: { title: 'Scale', type: 'number' } - flipX: { title: "Flip X", type: 'boolean', description: "Flip this animation horizontally?" } - flipY: { title: "Flip Y", type: 'boolean', description: "Flip this animation vertically?" } - -SoundSchema = c.sound({delay: { type: 'number' }}) - -_.extend ThangTypeSchema.properties, - raw: c.object {title: 'Raw Vector Data'}, - shapes: c.object {title: 'Shapes', additionalProperties: ShapeObjectSchema} - containers: c.object {title: 'Containers', additionalProperties: ContainerObjectSchema} - animations: c.object {title: 'Animations', additionalProperties: RawAnimationObjectSchema} - kind: c.shortString { enum: ['Unit', 'Floor', 'Wall', 'Doodad', 'Misc', 'Mark'], default: 'Misc', title: 'Kind' } - - actions: c.object { title: 'Actions', additionalProperties: { $ref: '#/definitions/action' } } - soundTriggers: c.object { title: "Sound Triggers", additionalProperties: c.array({}, { $ref: '#/definitions/sound' }) }, - say: c.object { format: 'slug-props', additionalProperties: { $ref: '#/definitions/sound' } }, - defaultSimlish: c.array({}, { $ref: '#/definitions/sound' }) - swearingSimlish: c.array({}, { $ref: '#/definitions/sound' }) - rotationType: { title: 'Rotation', type: 'string', enum: ['isometric', 'fixed']} - matchWorldDimensions: { title: 'Match World Dimensions', type: 'boolean' } - shadow: { title: 'Shadow Diameter', type: 'number', format: 'meters', description: "Shadow diameter in meters" } - layerPriority: - title: 'Layer Priority' - type: 'integer' - description: "Within its layer, sprites are sorted by layer priority, then y, then z." - scale: - title: 'Scale' - type: 'number' - positions: PositionsSchema - colorGroups: c.object - title: 'Color Groups' - additionalProperties: - type:'array' - format: 'thang-color-group' - items: {type:'string'} - snap: c.object { title: "Snap", description: "In the level editor, snap positioning to these intervals.", required: ['x', 'y'] }, - x: - title: "Snap X" - type: 'number' - description: "Snap to this many meters in the x-direction." - default: 4 - y: - title: "Snap Y" - type: 'number' - description: "Snap to this many meters in the y-direction." - default: 4 - 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 - -ThangTypeSchema.definitions = - action: ActionSchema - sound: SoundSchema - -c.extendBasicProperties ThangTypeSchema, 'thang.type' -c.extendSearchableProperties ThangTypeSchema -c.extendVersionedProperties ThangTypeSchema, 'thang.type' -c.extendPatchableProperties ThangTypeSchema - -module.exports = ThangTypeSchema diff --git a/server/patches/patch_handler.coffee b/server/patches/patch_handler.coffee index c38068ae5..33b729e22 100644 --- a/server/patches/patch_handler.coffee +++ b/server/patches/patch_handler.coffee @@ -1,6 +1,6 @@ Patch = require('./Patch') Handler = require('../commons/Handler') -schema = require './patch_schema' +schema = require '../../app/schemas/patch_schema' {handlers} = require '../commons/mapping' mongoose = require('mongoose') @@ -8,7 +8,7 @@ PatchHandler = class PatchHandler extends Handler modelClass: Patch editableProperties: [] postEditableProperties: ['delta', 'target', 'commitMessage'] - jsonSchema: require './patch_schema' + jsonSchema: require '../../app/schemas/patch_schema' makeNewInstance: (req) -> patch = super(req) From 3078036da1fed4ab743deb2bb57f9d16b77c5e08 Mon Sep 17 00:00:00 2001 From: Aditya Raisinghani <aditya.ajeet@gmail.com> Date: Sat, 12 Apr 2014 15:33:57 +0530 Subject: [PATCH 31/46] Deleted updated schemas. --- server/articles/article_schema.coffee | 14 --- server/commons/schemas.coffee | 172 -------------------------- 2 files changed, 186 deletions(-) delete mode 100644 server/articles/article_schema.coffee delete mode 100644 server/commons/schemas.coffee diff --git a/server/articles/article_schema.coffee b/server/articles/article_schema.coffee deleted file mode 100644 index 8c1764258..000000000 --- a/server/articles/article_schema.coffee +++ /dev/null @@ -1,14 +0,0 @@ -c = require '../commons/schemas' - -ArticleSchema = c.object() -c.extendNamedProperties ArticleSchema # name first - -ArticleSchema.properties.body = { type: 'string', title: 'Content', format: 'markdown' } -ArticleSchema.properties.i18n = { type: 'object', title: 'i18n', format: 'i18n', props: ['name', 'body'] } - -c.extendBasicProperties ArticleSchema, 'article' -c.extendSearchableProperties ArticleSchema -c.extendVersionedProperties ArticleSchema, 'article' -c.extendPatchableProperties ArticleSchema - -module.exports = ArticleSchema diff --git a/server/commons/schemas.coffee b/server/commons/schemas.coffee deleted file mode 100644 index a98c4a9b5..000000000 --- a/server/commons/schemas.coffee +++ /dev/null @@ -1,172 +0,0 @@ -#language imports -Language = require '../routes/languages' -# schema helper methods - -me = module.exports - -combine = (base, ext) -> - return base unless ext? - return _.extend(base, ext) - -urlPattern = '^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&%\$#_=]*)?$' - -# Common schema properties -me.object = (ext, props) -> combine {type: 'object', additionalProperties: false, properties: props or {}}, ext -me.array = (ext, items) -> combine {type: 'array', items: items or {}}, ext -me.shortString = (ext) -> combine({type: 'string', maxLength: 100}, ext) -me.pct = (ext) -> combine({type: 'number', maximum: 1.0, minimum: 0.0}, ext) -me.date = (ext) -> combine({type: ['object', 'string'], format: 'date-time'}, ext) -# should just be string (Mongo ID), but sometimes mongoose turns them into objects representing those, so we are lenient -me.objectId = (ext) -> schema = combine({type: ['object', 'string'] }, ext) -me.url = (ext) -> combine({type: 'string', format: 'url', pattern: urlPattern}, ext) - -PointSchema = me.object {title: "Point", description: "An {x, y} coordinate point.", format: "point2d", required: ["x", "y"]}, - x: {title: "x", description: "The x coordinate.", type: "number", "default": 15} - y: {title: "y", description: "The y coordinate.", type: "number", "default": 20} - -me.point2d = (ext) -> combine(_.cloneDeep(PointSchema), ext) - -SoundSchema = me.object { format: 'sound' }, - mp3: { type: 'string', format: 'sound-file' } - ogg: { type: 'string', format: 'sound-file' } - -me.sound = (props) -> - obj = _.cloneDeep(SoundSchema) - obj.properties[prop] = props[prop] for prop of props - obj - -ColorConfigSchema = me.object { format: 'color-sound' }, - hue: { format: 'range', type: 'number', minimum: 0, maximum: 1 } - saturation: { format: 'range', type: 'number', minimum: 0, maximum: 1 } - lightness: { format: 'range', type: 'number', minimum: 0, maximum: 1 } - -me.colorConfig = (props) -> - obj = _.cloneDeep(ColorConfigSchema) - obj.properties[prop] = props[prop] for prop of props - obj - -# BASICS - -basicProps = (linkFragment) -> - _id: me.objectId(links: [{rel: 'self', href: "/db/#{linkFragment}/{($)}"}], format: 'hidden') - __v: { title: 'Mongoose Version', format: 'hidden' } - -me.extendBasicProperties = (schema, linkFragment) -> - schema.properties = {} unless schema.properties? - _.extend(schema.properties, basicProps(linkFragment)) - -# PATCHABLE - -patchableProps = -> - patches: me.array({title:'Patches'}, { - _id: me.objectId(links: [{rel: "db", href: "/db/patch/{($)}"}], title: "Patch ID", description: "A reference to the patch.") - status: { enum: ['pending', 'accepted', 'rejected', 'cancelled']} - }) - allowPatches: { type: 'boolean' } - listeners: me.array({title:'Listeners'}, - me.objectId(links: [{rel: 'extra', href: "/db/user/{($)}"}])) - -me.extendPatchableProperties = (schema) -> - schema.properties = {} unless schema.properties? - _.extend(schema.properties, patchableProps()) - -# NAMED - -namedProps = -> - name: me.shortString({title: 'Name'}) - slug: me.shortString({title: 'Slug', format: 'hidden'}) - -me.extendNamedProperties = (schema) -> - schema.properties = {} unless schema.properties? - _.extend(schema.properties, namedProps()) - - -# VERSIONED - -versionedProps = (linkFragment) -> - version: - 'default': { minor: 0, major: 0, isLatestMajor: true, isLatestMinor: true } - format: 'version' - title: 'Version' - type: 'object' - readOnly: true - additionalProperties: false - properties: - major: { type: 'number', minimum: 0 } - minor: { type: 'number', minimum: 0 } - isLatestMajor: { type: 'boolean' } - isLatestMinor: { type: 'boolean' } - # TODO: figure out useful 'rel' values here - original: me.objectId(links: [{rel: 'extra', href: "/db/#{linkFragment}/{($)}"}], format: 'hidden') - parent: me.objectId(links: [{rel: 'extra', href: "/db/#{linkFragment}/{($)}"}], format: 'hidden') - creator: me.objectId(links: [{rel: 'extra', href: "/db/user/{($)}"}], format: 'hidden') - created: me.date( { title: 'Created', readOnly: true }) - commitMessage: { type: 'string', maxLength: 500, title: 'Commit Message', readOnly: true } - -me.extendVersionedProperties = (schema, linkFragment) -> - schema.properties = {} unless schema.properties? - _.extend(schema.properties, versionedProps(linkFragment)) - - -# SEARCHABLE - -searchableProps = -> - index: { format: 'hidden' } - -me.extendSearchableProperties = (schema) -> - schema.properties = {} unless schema.properties? - _.extend(schema.properties, searchableProps()) - - -# PERMISSIONED - -permissionsProps = -> - permissions: - type: 'array' - items: - type: 'object' - additionalProperties: false - properties: - target: {} - access: {type: 'string', 'enum': ['read', 'write', 'owner']} - format: "hidden" - -me.extendPermissionsProperties = (schema) -> - schema.properties = {} unless schema.properties? - _.extend(schema.properties, permissionsProps()) - -# TRANSLATABLE - -me.generateLanguageCodeArrayRegex = -> "^(" + Language.languageCodes.join("|") + ")$" - -me.getLanguageCodeArray = -> - return Language.languageCodes - -me.getLanguagesObject = -> return Language - -# OTHER - -me.classNamePattern = "^[A-Z][A-Za-z0-9]*$" # starts with capital letter; just letters and numbers -me.identifierPattern = "^[a-z][A-Za-z0-9]*$" # starts with lowercase letter; just letters and numbers -me.constantPattern = "^[A-Z0-9_]+$" # just uppercase letters, underscores, and numbers -me.identifierOrConstantPattern = "^([a-z][A-Za-z0-9]*|[A-Z0-9_]+)$" - -me.FunctionArgumentSchema = me.object { - title: "Function Argument", - description: "Documentation entry for a function argument." - "default": - name: "target" - type: "object" - example: "this.getNearestEnemy()" - description: "The target of this function." - required: ['name', 'type', 'example', 'description'] -}, - name: {type: 'string', pattern: me.identifierPattern, title: "Name", description: "Name of the function argument."} - # not actual JS types, just whatever they describe... - type: me.shortString(title: "Type", description: "Intended type of the argument.") - example: me.shortString(title: "Example", description: "Example value for the argument.") - description: {title: "Description", type: 'string', description: "Description of the argument.", maxLength: 1000} - "default": - title: "Default" - description: "Default value of the argument. (Your code should set this.)" - "default": null From 34c883b7df85e7baf5f8f4e90a9303bcda479aa8 Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker <decauwsemaecker.glen@gmail.com> Date: Sat, 12 Apr 2014 16:55:59 +0200 Subject: [PATCH 32/46] Moved language selection to the top and starting localized the hardcoded stuff. --- .../windows/coco-dev-setup/batch/config/config.coco | 2 +- .../batch/config/{ => localized}/license.coco | 0 .../batch/config/{ => localized}/readme.coco | 0 .../batch/config/localized/tips-nl.coco | 8 ++++++++ .../batch/config/{ => localized}/tips.coco | 0 .../coco-dev-setup/batch/localisation/de.coco | 13 ++++++++++++- .../coco-dev-setup/batch/localisation/en.coco | 6 +++++- .../coco-dev-setup/batch/localisation/nl.coco | 6 +++++- .../coco-dev-setup/batch/localisation/ru.coco | 6 +++++- .../coco-dev-setup/batch/localisation/zh-HANS.coco | 13 ++++++++++++- .../coco-dev-setup/batch/localisation/zh-HANT.coco | 13 ++++++++++++- .../coco-dev-setup/batch/scripts/get_language.bat | 11 +++++++---- .../batch/scripts/print_localized_file.bat | 8 ++++++++ .../coco-dev-setup/batch/scripts/print_tips.bat | 2 +- .../windows/coco-dev-setup/batch/scripts/setup.bat | 7 ++++--- 15 files changed, 80 insertions(+), 15 deletions(-) rename scripts/windows/coco-dev-setup/batch/config/{ => localized}/license.coco (100%) rename scripts/windows/coco-dev-setup/batch/config/{ => localized}/readme.coco (100%) create mode 100755 scripts/windows/coco-dev-setup/batch/config/localized/tips-nl.coco rename scripts/windows/coco-dev-setup/batch/config/{ => localized}/tips.coco (100%) create mode 100755 scripts/windows/coco-dev-setup/batch/scripts/print_localized_file.bat diff --git a/scripts/windows/coco-dev-setup/batch/config/config.coco b/scripts/windows/coco-dev-setup/batch/config/config.coco index eab3e07a0..da381689f 100755 --- a/scripts/windows/coco-dev-setup/batch/config/config.coco +++ b/scripts/windows/coco-dev-setup/batch/config/config.coco @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="ISO-8859-1" ?> <variables> - <version>3.4</version> + <version>3.5</version> <author>GlenDC</author> <copyright>CodeCombat.com � 2013-2014</copyright> <github_url>https://github.com/codecombat/codecombat.git</github_url> diff --git a/scripts/windows/coco-dev-setup/batch/config/license.coco b/scripts/windows/coco-dev-setup/batch/config/localized/license.coco similarity index 100% rename from scripts/windows/coco-dev-setup/batch/config/license.coco rename to scripts/windows/coco-dev-setup/batch/config/localized/license.coco diff --git a/scripts/windows/coco-dev-setup/batch/config/readme.coco b/scripts/windows/coco-dev-setup/batch/config/localized/readme.coco similarity index 100% rename from scripts/windows/coco-dev-setup/batch/config/readme.coco rename to scripts/windows/coco-dev-setup/batch/config/localized/readme.coco diff --git a/scripts/windows/coco-dev-setup/batch/config/localized/tips-nl.coco b/scripts/windows/coco-dev-setup/batch/config/localized/tips-nl.coco new file mode 100755 index 000000000..bc12d3bf5 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/config/localized/tips-nl.coco @@ -0,0 +1,8 @@ + 1) Antwoord voorzichtig en juist, indien er een vraag gesteld wordt. + 2) Deze installatie is nog steeds in beta en kan bugs bevatten. + 3) Rapporteer bugs op 'https://github.com/codecombat/codecombat/issues' + 4) Heb je vragen of suggesties? Praat met ons op HipChat via CodeCombat.com + + Je kan een Engelstalige stappengids + voor deze installatie vinden op onze wiki: + github.com/codecombat/codecombat/wiki/Setup-on-Windows:-a-step-by-step-guide \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/config/tips.coco b/scripts/windows/coco-dev-setup/batch/config/localized/tips.coco similarity index 100% rename from scripts/windows/coco-dev-setup/batch/config/tips.coco rename to scripts/windows/coco-dev-setup/batch/config/localized/tips.coco diff --git a/scripts/windows/coco-dev-setup/batch/localisation/de.coco b/scripts/windows/coco-dev-setup/batch/localisation/de.coco index e8af90621..fb8f1fce6 100755 --- a/scripts/windows/coco-dev-setup/batch/localisation/de.coco +++ b/scripts/windows/coco-dev-setup/batch/localisation/de.coco @@ -3,8 +3,12 @@ <global> <native>Deutsch</native> <description>German</description> - <intro>Ab jetzt senden wir unser Feedback in Englisch!</intro> + <tips>Before we start the installation, here are some tips:</tips> </global> + <language> + <choosen>You have choosen Deutsch as your language.</choosen> + <feedback>Ab jetzt senden wir unser Feedback in Deutsch.</feedback> + </language> <install> <system> <bit>-Bit System erkannt.</bit> @@ -52,6 +56,13 @@ <bashq>Bitte gebe den kompletten Pfad zur Git Bash ein, oder drücke Enter, um den Standardpfad zu verwenden</bashq> <ssh>Willst du das Repository via SSH auschecken?</ssh> </process> + <config> + <intro>You should have forked CodeCombat to your own GitHub Account by now...</intro> + <info>Please enter your github information, to configure your local repository.</info> + <username>Username: </username> + <password>Password: </password> + <process>Thank you... Configuring your local repistory right now...</process> + </config> </github> <switch> <install>The installation of your local environment was succesfull!</install> diff --git a/scripts/windows/coco-dev-setup/batch/localisation/en.coco b/scripts/windows/coco-dev-setup/batch/localisation/en.coco index 46464bb55..d763e34a5 100755 --- a/scripts/windows/coco-dev-setup/batch/localisation/en.coco +++ b/scripts/windows/coco-dev-setup/batch/localisation/en.coco @@ -3,8 +3,12 @@ <global> <native>English</native> <description>English</description> - <intro>From now on we'll send our feedback in English!</intro> + <tips>Before we start the installation, here are some tips:</tips> </global> + <language> + <choosen>You have choosen English as your language.</choosen> + <feedback>From now on we'll send our feedback in English.</feedback> + </language> <install> <system> <bit>-bit computer detected.</bit> diff --git a/scripts/windows/coco-dev-setup/batch/localisation/nl.coco b/scripts/windows/coco-dev-setup/batch/localisation/nl.coco index 501a4e339..971caf048 100755 --- a/scripts/windows/coco-dev-setup/batch/localisation/nl.coco +++ b/scripts/windows/coco-dev-setup/batch/localisation/nl.coco @@ -3,8 +3,12 @@ <global> <native>Nederlands</native> <description>Dutch</description> - <intro>Vanaf nu geven we onze feedback in het Nederlands!</intro> + <tips>Voor we verder gaan met de installatie hier volgen enkele tips:</tips> </global> + <language> + <choosen>Je hebt Nederlands gekozen als jouw taal naar keuze.</choosen> + <feedback>Vanaf nu geven we onze feedback in het Nederlands.</feedback> + </language> <install> <system> <bit>-bit computer gedetecteerd.</bit> diff --git a/scripts/windows/coco-dev-setup/batch/localisation/ru.coco b/scripts/windows/coco-dev-setup/batch/localisation/ru.coco index 37e9d0b2f..e7fc59f3e 100644 --- a/scripts/windows/coco-dev-setup/batch/localisation/ru.coco +++ b/scripts/windows/coco-dev-setup/batch/localisation/ru.coco @@ -3,8 +3,12 @@ <global> <native>�������</native> <description>Russian</description> - <intro>C ������� ������� �� ����� �������� �� �������!</intro> + <tips>Before we start the installation, here are some tips:</tips> </global> + <language> + <choosen>You have choosen ������� as your language.</choosen> + <feedback>C ������� ������� �� ����� �������� �� �������.</feedback> + </language> <install> <system> <bit>-������ ��������� ���������.</bit> diff --git a/scripts/windows/coco-dev-setup/batch/localisation/zh-HANS.coco b/scripts/windows/coco-dev-setup/batch/localisation/zh-HANS.coco index 410d032f7..29f8977e7 100755 --- a/scripts/windows/coco-dev-setup/batch/localisation/zh-HANS.coco +++ b/scripts/windows/coco-dev-setup/batch/localisation/zh-HANS.coco @@ -3,8 +3,12 @@ <global> <native>简体中文</native> <description>Traditional Chinese</description> - <intro>目前我们只能用英文给你反馈!</intro> + <tips>Before we start the installation, here are some tips:</tips> </global> + <language> + <choosen>You have choosen 简体中文 as your language.</choosen> + <feedback>目前我们只能用英文给你反馈</feedback> + </language> <install> <system> <bit>-位系统.</bit> @@ -52,6 +56,13 @@ <bashq>请输入 git bash 的安装全路径, 如果你安装的是默认路径, 那么直接输入回车即可</bashq> <ssh>你是否想使用 ssh 来检出(checkout)库(repository)?</ssh> </process> + <config> + <intro>You should have forked CodeCombat to your own GitHub Account by now...</intro> + <info>Please enter your github information, to configure your local repository.</info> + <username>Username: </username> + <password>Password: </password> + <process>Thank you... Configuring your local repistory right now...</process> + </config> </github> <switch> <install>The installation of your local environment was succesfull!</install> diff --git a/scripts/windows/coco-dev-setup/batch/localisation/zh-HANT.coco b/scripts/windows/coco-dev-setup/batch/localisation/zh-HANT.coco index 8c242effa..efb024647 100755 --- a/scripts/windows/coco-dev-setup/batch/localisation/zh-HANT.coco +++ b/scripts/windows/coco-dev-setup/batch/localisation/zh-HANT.coco @@ -3,8 +3,12 @@ <global> <native>繁体中文</native> <description>Simplified Chinese</description> - <intro>From now on we'll send our feedback in English!</intro> + <tips>Before we start the installation, here are some tips:</tips> </global> + <language> + <choosen>You have choosen 繁体中文 as your language.</choosen> + <feedback>From now on we'll send our feedback in 繁体中文.</feedback> + </language> <install> <system> <bit>-bit computer detected.</bit> @@ -52,6 +56,13 @@ <bashq>Please enter the full path where git bash is installed or just press enter if it's in the default location</bashq> <ssh>Do you want to checkout the repository via ssh?</ssh> </process> + <config> + <intro>You should have forked CodeCombat to your own GitHub Account by now...</intro> + <info>Please enter your github information, to configure your local repository.</info> + <username>Username: </username> + <password>Password: </password> + <process>Thank you... Configuring your local repistory right now...</process> + </config> </github> <switch> <install>The installation of your local environment was succesfull!</install> diff --git a/scripts/windows/coco-dev-setup/batch/scripts/get_language.bat b/scripts/windows/coco-dev-setup/batch/scripts/get_language.bat index fa66aafc8..96da622e9 100755 --- a/scripts/windows/coco-dev-setup/batch/scripts/get_language.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/get_language.bat @@ -27,10 +27,13 @@ goto:get_localisation_id goto:get_localisation_id ) else ( set language_id=!languages[%local_id%]! - call get_text !language_id! global_native global native call print_dashed_seperator - echo You have choosen !global_native! as your language. - call get_text !language_id! global_intro global intro - echo !global_intro! + + call get_local_text language_choosen language choosen + echo !language_choosen! + + call get_local_text language_feedback language feedback + echo !language_feedback! + call print_seperator ) \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/print_localized_file.bat b/scripts/windows/coco-dev-setup/batch/scripts/print_localized_file.bat new file mode 100755 index 000000000..14c74e7a7 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/scripts/print_localized_file.bat @@ -0,0 +1,8 @@ +set "LFTP=%1-%language_id%.coco" +if not exist "%LFTP%" ( + echo printing %1.coco... + call print_file %1.coco +) else ( + echo printing %LFTP%... + call print_file %LFTP% +) \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/print_tips.bat b/scripts/windows/coco-dev-setup/batch/scripts/print_tips.bat index c00833574..0a2e3033a 100755 --- a/scripts/windows/coco-dev-setup/batch/scripts/print_tips.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/print_tips.bat @@ -1 +1 @@ -print_file ..\\config\\tips.coco \ No newline at end of file +call print_localized_file ..\\config\\localized\\tips \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/setup.bat b/scripts/windows/coco-dev-setup/batch/scripts/setup.bat index 9ac1c55df..b8ad678bc 100755 --- a/scripts/windows/coco-dev-setup/batch/scripts/setup.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/setup.bat @@ -13,14 +13,15 @@ echo Welcome to the automated Installation of the CodeCombat Dev. Environment! echo v%version% authored by %author% and published by %copyright%. call print_seperator -echo Before we start the installation, here are some tips: +call get_language + +call get_local_text global_tips global tips +echo !global_tips! call print_tips call print_seperator call sign_license -call get_language - call download_and_install_applications start cmd /c "setup_p2.bat" From 10b166041960f6ce7aa2d9fd5e371c18049b52da Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Sat, 12 Apr 2014 08:48:49 -0700 Subject: [PATCH 33/46] Fixed the level editor so you can save again. --- app/locale/en.coffee | 5 +++-- app/templates/editor/level/edit.jade | 14 +++++++++++--- app/views/editor/article/edit.coffee | 2 +- app/views/editor/level/edit.coffee | 14 +++++++++++--- app/views/editor/thang/edit.coffee | 2 +- app/views/kinds/CocoView.coffee | 2 +- app/views/modal/save_version_modal.coffee | 6 +++--- server/levels/level_handler.coffee | 1 - 8 files changed, 31 insertions(+), 15 deletions(-) diff --git a/app/locale/en.coffee b/app/locale/en.coffee index b65b93340..087e95976 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -1,4 +1,4 @@ -module.exports = nativeDescription: "English", englishDescription: "English", translation: +module.exports = nativeDescription: "English", englishDescription: "English", translation: common: loading: "Loading..." saving: "Saving..." @@ -359,7 +359,7 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr thang_search_title: "Search Thang Types Here" level_search_title: "Search Levels Here" signup_to_create: "Sign Up to Create a New Content" - read_only_warning: "Note: you can't save any edits here, because you're not logged in as an admin." + read_only_warning2: "Note: you can't save any edits here, because you're not logged in." article: edit_btn_preview: "Preview" @@ -661,3 +661,4 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr leaderboard: "Leaderboard" user_schema: "User Schema" user_profile: "User Profile" + patches: "Patches" diff --git a/app/templates/editor/level/edit.jade b/app/templates/editor/level/edit.jade index c029c4a73..ba69b27fd 100644 --- a/app/templates/editor/level/edit.jade +++ b/app/templates/editor/level/edit.jade @@ -26,11 +26,16 @@ block outer_content 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 - li(data-i18n="common.save", disabled=authorized === true ? undefined : "true").btn.btn-primary.navbar-btn#commit-level-start-button Save + 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! @@ -74,6 +79,9 @@ block outer_content div.tab-pane#editor-level-components-tab-view div.tab-pane#editor-level-systems-tab-view + + div.tab-pane#editor-level-patches + .patches-view div#error-view diff --git a/app/views/editor/article/edit.coffee b/app/views/editor/article/edit.coffee index 1d91558f1..b6f279089 100644 --- a/app/views/editor/article/edit.coffee +++ b/app/views/editor/article/edit.coffee @@ -80,7 +80,7 @@ module.exports = class ArticleEditView extends View afterRender: -> super() return if @startsLoading - @showReadOnly() unless me.isAdmin() or @article.hasWriteAccess(me) + @showReadOnly() if me.get('anonymous') openPreview: -> @preview = window.open('/editor/article/x/preview', 'preview', 'height=800,width=600') diff --git a/app/views/editor/level/edit.coffee b/app/views/editor/level/edit.coffee index b685d457f..e28f87247 100644 --- a/app/views/editor/level/edit.coffee +++ b/app/views/editor/level/edit.coffee @@ -12,6 +12,8 @@ ComponentsTabView = require './components_tab_view' SystemsTabView = require './systems_tab_view' LevelSaveView = require './save_view' LevelForkView = require './fork_view' +SaveVersionModal = require 'views/modal/save_version_modal' +PatchesView = require 'views/editor/patches_view' VersionHistoryView = require './versions_view' ErrorView = require '../../error_view' @@ -26,6 +28,8 @@ module.exports = class EditorLevelView extends View 'click #commit-level-start-button': 'startCommittingLevel' 'click #fork-level-start-button': 'startForkingLevel' 'click #history-button': 'showVersionHistory' + 'click #patches-tab': -> @patchesView.load() + 'click #commit-level-patch-button': 'startPatchingLevel' constructor: (options, @levelID) -> super options @@ -88,7 +92,8 @@ module.exports = class EditorLevelView extends View @componentsTab = @insertSubView new ComponentsTabView supermodel: @supermodel @systemsTab = @insertSubView new SystemsTabView supermodel: @supermodel Backbone.Mediator.publish 'level-loaded', level: @level - @showReadOnly() unless me.isAdmin() or @level.hasWriteAccess(me) + @showReadOnly() if me.get('anonymous') + @patchesView = @insertSubView(new PatchesView(@level), @$el.find('.patches-view')) onPlayLevel: (e) -> sendLevel = => @@ -103,9 +108,12 @@ module.exports = class EditorLevelView extends View @childWindow.onPlayLevelViewLoaded = (e) => sendLevel() # still a hack @childWindow.focus() + startPatchingLevel: (e) -> + @openModalView new SaveVersionModal({model:@level}) + Backbone.Mediator.publish 'level:view-switched', e + startCommittingLevel: (e) -> - levelSaveView = new LevelSaveView level: @level, supermodel: @supermodel - @openModalView levelSaveView + @openModalView new LevelSaveView level: @level, supermodel: @supermodel Backbone.Mediator.publish 'level:view-switched', e startForkingLevel: (e) -> diff --git a/app/views/editor/thang/edit.coffee b/app/views/editor/thang/edit.coffee index 67edc6978..bd8a34804 100644 --- a/app/views/editor/thang/edit.coffee +++ b/app/views/editor/thang/edit.coffee @@ -95,7 +95,7 @@ module.exports = class ThangTypeEditView extends View @initComponents() @insertSubView(new ColorsTabView(@thangType)) @patchesView = @insertSubView(new PatchesView(@thangType), @$el.find('.patches-view')) - @showReadOnly() unless me.isAdmin() or @thangType.hasWriteAccess(me) + @showReadOnly() if me.get('anonymous') initComponents: => options = diff --git a/app/views/kinds/CocoView.coffee b/app/views/kinds/CocoView.coffee index 0287dc959..338a2cc3b 100644 --- a/app/views/kinds/CocoView.coffee +++ b/app/views/kinds/CocoView.coffee @@ -262,7 +262,7 @@ class CocoView extends Backbone.View showReadOnly: -> return if me.isAdmin() - warning = $.i18n.t 'editor.read_only_warning', defaultValue: "Note: you can't save any edits here, because you're not logged in as an admin." + warning = $.i18n.t 'editor.read_only_warning2', defaultValue: "Note: you can't save any edits here, because you're not logged in." noty text: warning, layout: 'center', type: 'information', killer: true, timeout: 5000 # Loading ModalViews diff --git a/app/views/modal/save_version_modal.coffee b/app/views/modal/save_version_modal.coffee index 1ed4a4d54..8c49327f5 100644 --- a/app/views/modal/save_version_modal.coffee +++ b/app/views/modal/save_version_modal.coffee @@ -17,8 +17,8 @@ module.exports = class SaveVersionModal extends ModalView constructor: (options) -> super options - @model = options.model - new Patch() + @model = options.model or options.level + new Patch() # hack to get the schema to load, delete this later @isPatch = not @model.hasWriteAccess() getRenderData: -> @@ -33,7 +33,7 @@ module.exports = class SaveVersionModal extends ModalView changeEl = @$el.find('.changes-stub') deltaView = new DeltaView({model:@model}) @insertSubView(deltaView, changeEl) - $('.commit-message input').attr('placeholder', $.i18n.t('general.commit_msg')) + @$el.find('.commit-message input').attr('placeholder', $.i18n.t('general.commit_msg')) onClickSaveButton: -> Backbone.Mediator.publish 'save-new-version', { diff --git a/server/levels/level_handler.coffee b/server/levels/level_handler.coffee index a19487191..1caa693f1 100644 --- a/server/levels/level_handler.coffee +++ b/server/levels/level_handler.coffee @@ -39,7 +39,6 @@ LevelHandler = class LevelHandler extends Handler return @getLeaderboardGPlusFriends(req, res, args[0]) if args[1] is 'leaderboard_gplus_friends' return @getHistogramData(req, res, args[0]) if args[1] is 'histogram_data' return @checkExistence(req, res, args[0]) if args[1] is 'exists' - return @sendNotFoundError(res) super(arguments...) fetchLevelByIDAndHandleErrors: (id, req, res, callback) -> From 30f785f7cfaa93f7b4a64480f8f61f77d3ba5932 Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Sat, 12 Apr 2014 10:51:02 -0700 Subject: [PATCH 34/46] Moved the model schemas into a models folder. --- app/models/CocoModel.coffee | 26 ++++--------------- app/models/SuperModel.coffee | 8 ------ .../article.coffee} | 2 +- .../level.coffee} | 4 +-- .../level_component.coffee} | 4 +-- .../level_feedback.coffee} | 2 +- .../level_session.coffee} | 2 +- .../level_system.coffee} | 4 +-- .../patch.coffee} | 2 +- .../thang_component.coffee} | 2 +- .../thang_type.coffee} | 4 +-- .../user.coffee} | 2 +- app/views/account/job_profile_view.coffee | 7 +---- app/views/account/settings_view.coffee | 6 +---- app/views/editor/article/edit.coffee | 10 ++----- app/views/editor/components/main.coffee | 5 +--- app/views/editor/thang/edit.coffee | 3 +-- server/articles/article_handler.coffee | 2 +- server/commons/mapping.coffee | 15 ----------- server/levels/Level.coffee | 2 +- .../levels/components/LevelComponent.coffee | 2 +- .../components/level_component_handler.coffee | 2 +- server/levels/feedbacks/LevelFeedback.coffee | 2 +- .../feedbacks/level_feedback_handler.coffee | 2 +- server/levels/level_handler.coffee | 2 +- server/levels/sessions/LevelSession.coffee | 2 +- .../sessions/level_session_handler.coffee | 2 +- server/levels/systems/LevelSystem.coffee | 2 +- .../systems/level_system_handler.coffee | 2 +- .../levels/thangs/thang_type_handler.coffee | 2 +- server/patches/patch_handler.coffee | 4 +-- server/routes/db.coffee | 3 +-- server/users/User.coffee | 2 +- server/users/user_handler.coffee | 2 +- test/server/functional/article.spec.coffee | 1 + 35 files changed, 43 insertions(+), 101 deletions(-) rename app/schemas/{article_schema.coffee => models/article.coffee} (94%) rename app/schemas/{level_schema.coffee => models/level.coffee} (99%) rename app/schemas/{level_component_schema.coffee => models/level_component.coffee} (98%) rename app/schemas/{level_feedback_schema.coffee => models/level_feedback.coffee} (96%) rename app/schemas/{level_session_schema.coffee => models/level_session.coffee} (99%) rename app/schemas/{level_system_schema.coffee => models/level_system.coffee} (98%) rename app/schemas/{patch_schema.coffee => models/patch.coffee} (97%) rename app/schemas/{thang_component_schema.coffee => models/thang_component.coffee} (96%) rename app/schemas/{thang_type_schema.coffee => models/thang_type.coffee} (98%) rename app/schemas/{user_schema.coffee => models/user.coffee} (99%) diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee index bb2fc0547..e63e1cd0a 100644 --- a/app/models/CocoModel.coffee +++ b/app/models/CocoModel.coffee @@ -11,14 +11,11 @@ class CocoModel extends Backbone.Model initialize: -> super() - @constructor.schema ?= @urlRoot[4..].replace '.', '_' + @constructor.schema ?= require "schemas/models/#{@urlRoot[4..].replace '.', '_'}" if not @constructor.className console.error("#{@} needs a className set.") @markToRevert() - if @constructor.schema?.loaded - @addSchemaDefaults() - else - @loadSchema() + @addSchemaDefaults() @once 'sync', @onLoaded, @ @saveBackup = _.debounce(@saveBackup, 500) @@ -34,9 +31,8 @@ class CocoModel extends Backbone.Model onLoaded: -> @loaded = true @loading = false - if @constructor.schema?.loaded - @markToRevert() - @loadFromBackup() + @markToRevert() + @loadFromBackup() set: -> res = super(arguments...) @@ -55,18 +51,6 @@ class CocoModel extends Backbone.Model CocoModel.backedUp[@id] = @ @backedUp = {} - - loadSchema: -> - return if @constructor.schema.loading - @constructor.schema = require 'schemas/' + @constructor.schema + '_schema' unless @constructor.schema.loaded - @onConstructorSync() - - onConstructorSync: -> - @constructor.schema.loaded = true - @addSchemaDefaults() - @trigger 'schema-loaded' - - @hasSchema: -> return @schema?.loaded schema: -> return @constructor.schema validate: -> @@ -129,7 +113,7 @@ class CocoModel extends Backbone.Model @set "permissions", (@get("permissions") or []).concat({access: 'read', target: 'public'}) addSchemaDefaults: -> - return if @addedSchemaDefaults or not @constructor.hasSchema() + return if @addedSchemaDefaults @addedSchemaDefaults = true for prop, defaultValue of @constructor.schema.default or {} continue if @get(prop)? diff --git a/app/models/SuperModel.coffee b/app/models/SuperModel.coffee index adcac62eb..6963392a2 100644 --- a/app/models/SuperModel.coffee +++ b/app/models/SuperModel.coffee @@ -2,7 +2,6 @@ class SuperModel constructor: -> @models = {} @collections = {} - @schemas = {} _.extend(@, Backbone.Events) populateModel: (model) -> @@ -26,11 +25,7 @@ class SuperModel @removeEventsFromModel(model) modelLoaded: (model) -> - model.loadSchema() schema = model.schema() - unless schema.loaded - @schemas[model.urlRoot] = schema - return schema.once('sync', => @modelLoaded(model)) refs = model.getReferencedModels(model.attributes, schema, '/', @shouldLoadProjection) refs = [] unless @mustPopulate is model or @shouldPopulate(model) # console.log 'Loaded', model.get('name') @@ -103,9 +98,6 @@ class SuperModel for model in _.values @models total += 1 loaded += 1 if model.loaded - for schema in _.values @schemas - total += 1 - loaded += 1 if schema.loaded return 1.0 unless total return loaded / total diff --git a/app/schemas/article_schema.coffee b/app/schemas/models/article.coffee similarity index 94% rename from app/schemas/article_schema.coffee rename to app/schemas/models/article.coffee index 0274f92a6..60f65640f 100644 --- a/app/schemas/article_schema.coffee +++ b/app/schemas/models/article.coffee @@ -1,4 +1,4 @@ -c = require './schemas' +c = require './../schemas' ArticleSchema = c.object() c.extendNamedProperties ArticleSchema # name first diff --git a/app/schemas/level_schema.coffee b/app/schemas/models/level.coffee similarity index 99% rename from app/schemas/level_schema.coffee rename to app/schemas/models/level.coffee index 919b44c44..7180c6a67 100644 --- a/app/schemas/level_schema.coffee +++ b/app/schemas/models/level.coffee @@ -1,5 +1,5 @@ -c = require './schemas' -ThangComponentSchema = require './thang_component_schema' +c = require './../schemas' +ThangComponentSchema = require './../models/thang_component' SpecificArticleSchema = c.object() c.extendNamedProperties SpecificArticleSchema # name first diff --git a/app/schemas/level_component_schema.coffee b/app/schemas/models/level_component.coffee similarity index 98% rename from app/schemas/level_component_schema.coffee rename to app/schemas/models/level_component.coffee index 3178eb916..8552979ee 100644 --- a/app/schemas/level_component_schema.coffee +++ b/app/schemas/models/level_component.coffee @@ -1,5 +1,5 @@ -c = require './schemas' -metaschema = require './metaschema' +c = require './../schemas' +metaschema = require './../metaschema' attackSelfCode = """ class AttacksSelf extends Component diff --git a/app/schemas/level_feedback_schema.coffee b/app/schemas/models/level_feedback.coffee similarity index 96% rename from app/schemas/level_feedback_schema.coffee rename to app/schemas/models/level_feedback.coffee index 201beb468..f8bb6a73c 100644 --- a/app/schemas/level_feedback_schema.coffee +++ b/app/schemas/models/level_feedback.coffee @@ -1,4 +1,4 @@ -c = require './schemas' +c = require './../schemas' LevelFeedbackLevelSchema = c.object {required: ['original', 'majorVersion']}, { original: c.objectId({}) diff --git a/app/schemas/level_session_schema.coffee b/app/schemas/models/level_session.coffee similarity index 99% rename from app/schemas/level_session_schema.coffee rename to app/schemas/models/level_session.coffee index 4244c4771..670dc9ad4 100644 --- a/app/schemas/level_session_schema.coffee +++ b/app/schemas/models/level_session.coffee @@ -1,4 +1,4 @@ -c = require './schemas' +c = require './../schemas' LevelSessionPlayerSchema = c.object id: c.objectId diff --git a/app/schemas/level_system_schema.coffee b/app/schemas/models/level_system.coffee similarity index 98% rename from app/schemas/level_system_schema.coffee rename to app/schemas/models/level_system.coffee index 9b186aaac..1804de363 100644 --- a/app/schemas/level_system_schema.coffee +++ b/app/schemas/models/level_system.coffee @@ -1,5 +1,5 @@ -c = require './schemas' -metaschema = require './metaschema' +c = require './../schemas' +metaschema = require './../metaschema' jitterSystemCode = """ class Jitter extends System diff --git a/app/schemas/patch_schema.coffee b/app/schemas/models/patch.coffee similarity index 97% rename from app/schemas/patch_schema.coffee rename to app/schemas/models/patch.coffee index 5c2ce122e..e14423371 100644 --- a/app/schemas/patch_schema.coffee +++ b/app/schemas/models/patch.coffee @@ -1,4 +1,4 @@ -c = require './schemas' +c = require './../schemas' patchables = ['level', 'thang_type', 'level_system', 'level_component', 'article'] diff --git a/app/schemas/thang_component_schema.coffee b/app/schemas/models/thang_component.coffee similarity index 96% rename from app/schemas/thang_component_schema.coffee rename to app/schemas/models/thang_component.coffee index b6d574fdc..eebcf155b 100644 --- a/app/schemas/thang_component_schema.coffee +++ b/app/schemas/models/thang_component.coffee @@ -1,4 +1,4 @@ -c = require './schemas' +c = require './../schemas' module.exports = ThangComponentSchema = c.object { title: "Component" diff --git a/app/schemas/thang_type_schema.coffee b/app/schemas/models/thang_type.coffee similarity index 98% rename from app/schemas/thang_type_schema.coffee rename to app/schemas/models/thang_type.coffee index 1e6bc2ee5..eb78c1c11 100644 --- a/app/schemas/thang_type_schema.coffee +++ b/app/schemas/models/thang_type.coffee @@ -1,5 +1,5 @@ -c = require './schemas' -ThangComponentSchema = require './thang_component_schema' +c = require './../schemas' +ThangComponentSchema = require './thang_component' ThangTypeSchema = c.object() c.extendNamedProperties ThangTypeSchema # name first diff --git a/app/schemas/user_schema.coffee b/app/schemas/models/user.coffee similarity index 99% rename from app/schemas/user_schema.coffee rename to app/schemas/models/user.coffee index a386051f3..6bb3939e6 100644 --- a/app/schemas/user_schema.coffee +++ b/app/schemas/models/user.coffee @@ -1,4 +1,4 @@ -c = require './schemas' +c = require './../schemas' emailSubscriptions = ['announcement', 'tester', 'level_creator', 'developer', 'article_editor', 'translator', 'support', 'notification'] UserSchema = c.object {}, diff --git a/app/views/account/job_profile_view.coffee b/app/views/account/job_profile_view.coffee index d14fc2f79..940d52ffe 100644 --- a/app/views/account/job_profile_view.coffee +++ b/app/views/account/job_profile_view.coffee @@ -14,12 +14,6 @@ module.exports = class JobProfileView extends CocoView 'updated' ] - constructor: (options) -> - super options - unless me.schema().loaded - @addSomethingToLoad("user_schema") - @listenToOnce me, 'schema-loaded', => @somethingLoaded 'user_schema' - afterRender: -> super() return if @loading() @@ -29,6 +23,7 @@ module.exports = class JobProfileView extends CocoView visibleSettings = @editableSettings.concat @readOnlySettings data = _.pick (me.get('jobProfile') ? {}), (value, key) => key in visibleSettings data.name ?= (me.get('firstName') + ' ' + me.get('lastName')).trim() if me.get('firstName') + console.log 'schema?', me.schema() schema = _.cloneDeep me.schema().properties.jobProfile schema.properties = _.pick schema.properties, (value, key) => key in visibleSettings schema.required = _.intersection schema.required, visibleSettings diff --git a/app/views/account/settings_view.coffee b/app/views/account/settings_view.coffee index e74db5f65..0665a7c9c 100644 --- a/app/views/account/settings_view.coffee +++ b/app/views/account/settings_view.coffee @@ -43,11 +43,7 @@ module.exports = class SettingsView extends View @jobProfileView = new JobProfileView() @listenTo @jobProfileView, 'change', @save @insertSubView @jobProfileView - - if me.schema().loaded - @buildPictureTreema() - else - @listenToOnce me, 'schema-loaded', @buildPictureTreema + @buildPictureTreema() chooseTab: (category) -> id = "##{category}-pane" diff --git a/app/views/editor/article/edit.coffee b/app/views/editor/article/edit.coffee index 31acafbca..0123546e0 100644 --- a/app/views/editor/article/edit.coffee +++ b/app/views/editor/article/edit.coffee @@ -35,17 +35,11 @@ module.exports = class ArticleEditView extends View ) @article.fetch() - @article.loadSchema() - @listenToOnce(@article, 'sync', @onArticleSync) - @listenToOnce(@article, 'schema-loaded', @buildTreema) + @listenToOnce(@article, 'sync', @buildTreema) @pushChangesToPreview = _.throttle(@pushChangesToPreview, 500) - onArticleSync: -> - @article.loaded = true - @buildTreema() - buildTreema: -> - return if @treema? or (not @article.loaded) or (not Article.hasSchema()) + return if @treema? or (not @article.loaded) unless @article.attributes.body @article.set('body', '') @startsLoading = false diff --git a/app/views/editor/components/main.coffee b/app/views/editor/components/main.coffee index 2c39b6086..0104aa5d9 100644 --- a/app/views/editor/components/main.coffee +++ b/app/views/editor/components/main.coffee @@ -20,9 +20,6 @@ module.exports = class ThangComponentEditView extends CocoView render: => return if @destroyed - for model in [Level, LevelComponent] - temp = new model() - @listenToOnce temp, 'schema-loaded', @render unless model.schema?.loaded if not @componentCollection @componentCollection = @supermodel.getCollection new ComponentsCollection() unless @componentCollection.loaded @@ -32,7 +29,7 @@ module.exports = class ThangComponentEditView extends CocoView afterRender: -> super() - return @showLoading() unless @componentCollection?.loaded and Level.schema.loaded and LevelComponent.schema.loaded + return @showLoading() unless @componentCollection?.loaded @hideLoading() @buildExtantComponentTreema() @buildAddComponentTreema() diff --git a/app/views/editor/thang/edit.coffee b/app/views/editor/thang/edit.coffee index 7c3500fad..c5b08d7c0 100644 --- a/app/views/editor/thang/edit.coffee +++ b/app/views/editor/thang/edit.coffee @@ -61,12 +61,11 @@ module.exports = class ThangTypeEditView extends View ) @thangType.fetch() - @thangType.loadSchema() @listenToOnce(@thangType, 'sync', @onThangTypeSync) @refreshAnimation = _.debounce @refreshAnimation, 500 onThangTypeSync: -> - return unless @thangType.loaded and ThangType.hasSchema() + return unless @thangType.loaded @startsLoading = false @files = new DocumentFiles(@thangType) @files.fetch() diff --git a/server/articles/article_handler.coffee b/server/articles/article_handler.coffee index 1d9e90436..8aa2d26dc 100644 --- a/server/articles/article_handler.coffee +++ b/server/articles/article_handler.coffee @@ -4,7 +4,7 @@ Handler = require('../commons/Handler') ArticleHandler = class ArticleHandler extends Handler modelClass: Article editableProperties: ['body', 'name', 'i18n'] - jsonSchema: require './article_schema' + jsonSchema: require '../../app/schemas/models/article' hasAccess: (req) -> req.method is 'GET' or req.user?.isAdmin() diff --git a/server/commons/mapping.coffee b/server/commons/mapping.coffee index d7400c951..3cfcc2164 100644 --- a/server/commons/mapping.coffee +++ b/server/commons/mapping.coffee @@ -10,21 +10,6 @@ module.exports.handlers = 'thang_type': 'levels/thangs/thang_type_handler' 'user': 'users/user_handler' -module.exports.schemas = - 'article': 'articles/article_schema' - 'common': 'commons/schemas' - 'i18n': 'commons/i18n_schema' - 'level': 'levels/level_schema' - 'level_component': 'levels/components/level_component_schema' - 'level_feedback': 'levels/feedbacks/level_feedback_schema' - 'level_session': 'levels/sessions/level_session_schema' - 'level_system': 'levels/systems/level_system_schema' - 'metaschema': 'commons/metaschema' - 'patch': 'patches/patch_schema' - 'thang_component': 'levels/thangs/thang_component_schema' - 'thang_type': 'levels/thangs/thang_type_schema' - 'user': 'users/user_schema' - module.exports.routes = [ 'routes/auth' diff --git a/server/levels/Level.coffee b/server/levels/Level.coffee index 83e8d678b..9cadeac7b 100644 --- a/server/levels/Level.coffee +++ b/server/levels/Level.coffee @@ -1,6 +1,6 @@ mongoose = require('mongoose') plugins = require('../plugins/plugins') -jsonschema = require('../../app/schemas/level_schema') +jsonschema = require('../../app/schemas/models/level') LevelSchema = new mongoose.Schema({ description: String diff --git a/server/levels/components/LevelComponent.coffee b/server/levels/components/LevelComponent.coffee index 5f00f261c..6c1a58370 100644 --- a/server/levels/components/LevelComponent.coffee +++ b/server/levels/components/LevelComponent.coffee @@ -1,6 +1,6 @@ mongoose = require('mongoose') plugins = require('../../plugins/plugins') -jsonschema = require('../../../app/schemas/level_component_schema') +jsonschema = require('../../../app/schemas/models/level_component') LevelComponentSchema = new mongoose.Schema { description: String diff --git a/server/levels/components/level_component_handler.coffee b/server/levels/components/level_component_handler.coffee index e2b7a2ba8..3bcc572d0 100644 --- a/server/levels/components/level_component_handler.coffee +++ b/server/levels/components/level_component_handler.coffee @@ -3,7 +3,7 @@ Handler = require('../../commons/Handler') LevelComponentHandler = class LevelComponentHandler extends Handler modelClass: LevelComponent - jsonSchema: require '../../../app/schemas/level_component_schema' + jsonSchema: require '../../../app/schemas/models/level_component' editableProperties: [ 'system' 'description' diff --git a/server/levels/feedbacks/LevelFeedback.coffee b/server/levels/feedbacks/LevelFeedback.coffee index 234caf367..5fef6a567 100644 --- a/server/levels/feedbacks/LevelFeedback.coffee +++ b/server/levels/feedbacks/LevelFeedback.coffee @@ -2,7 +2,7 @@ mongoose = require('mongoose') plugins = require('../../plugins/plugins') -jsonschema = require('../../../app/schemas/level_feedback_schema') +jsonschema = require('../../../app/schemas/models/level_feedback') LevelFeedbackSchema = new mongoose.Schema({ created: diff --git a/server/levels/feedbacks/level_feedback_handler.coffee b/server/levels/feedbacks/level_feedback_handler.coffee index cd4ffda26..58d268db1 100644 --- a/server/levels/feedbacks/level_feedback_handler.coffee +++ b/server/levels/feedbacks/level_feedback_handler.coffee @@ -4,7 +4,7 @@ Handler = require('../../commons/Handler') class LevelFeedbackHandler extends Handler modelClass: LevelFeedback editableProperties: ['rating', 'review', 'level', 'levelID', 'levelName'] - jsonSchema: require '../../../app/schemas/level_feedback_schema' + jsonSchema: require '../../../app/schemas/models/level_feedback' makeNewInstance: (req) -> feedback = super(req) diff --git a/server/levels/level_handler.coffee b/server/levels/level_handler.coffee index 9a9a8aafd..f0c6d225b 100644 --- a/server/levels/level_handler.coffee +++ b/server/levels/level_handler.coffee @@ -8,7 +8,7 @@ mongoose = require('mongoose') LevelHandler = class LevelHandler extends Handler modelClass: Level - jsonSchema: require '../../app/schemas/level_schema' + jsonSchema: require '../../app/schemas/models/level' editableProperties: [ 'description' 'documentation' diff --git a/server/levels/sessions/LevelSession.coffee b/server/levels/sessions/LevelSession.coffee index d91b7241c..c30519ba0 100644 --- a/server/levels/sessions/LevelSession.coffee +++ b/server/levels/sessions/LevelSession.coffee @@ -2,7 +2,7 @@ mongoose = require('mongoose') plugins = require('../../plugins/plugins') -jsonschema = require('../../../app/schemas/level_session_schema') +jsonschema = require('../../../app/schemas/models/level_session') LevelSessionSchema = new mongoose.Schema({ created: diff --git a/server/levels/sessions/level_session_handler.coffee b/server/levels/sessions/level_session_handler.coffee index 25131833d..5771711f2 100644 --- a/server/levels/sessions/level_session_handler.coffee +++ b/server/levels/sessions/level_session_handler.coffee @@ -9,7 +9,7 @@ class LevelSessionHandler extends Handler editableProperties: ['multiplayer', 'players', 'code', 'completed', 'state', 'levelName', 'creatorName', 'levelID', 'screenshot', 'chat', 'teamSpells', 'submitted', 'unsubscribed'] - jsonSchema: require '../../../app/schemas/level_session_schema' + jsonSchema: require '../../../app/schemas/models/level_session' getByRelationship: (req, res, args...) -> return @getActiveSessions req, res if args.length is 2 and args[1] is 'active' diff --git a/server/levels/systems/LevelSystem.coffee b/server/levels/systems/LevelSystem.coffee index 730b338ad..f945aaa95 100644 --- a/server/levels/systems/LevelSystem.coffee +++ b/server/levels/systems/LevelSystem.coffee @@ -1,6 +1,6 @@ mongoose = require('mongoose') plugins = require('../../plugins/plugins') -jsonschema = require('../../../app/schemas/level_system_schema') +jsonschema = require('../../../app/schemas/models/level_system') LevelSystemSchema = new mongoose.Schema { description: String diff --git a/server/levels/systems/level_system_handler.coffee b/server/levels/systems/level_system_handler.coffee index c3fd0a366..bf1bb39d5 100644 --- a/server/levels/systems/level_system_handler.coffee +++ b/server/levels/systems/level_system_handler.coffee @@ -13,7 +13,7 @@ LevelSystemHandler = class LevelSystemHandler extends Handler 'configSchema' ] postEditableProperties: ['name'] - jsonSchema: require '../../../app/schemas/level_system_schema' + jsonSchema: require '../../../app/schemas/models/level_system' getEditableProperties: (req, document) -> props = super(req, document) diff --git a/server/levels/thangs/thang_type_handler.coffee b/server/levels/thangs/thang_type_handler.coffee index 851d2ccf6..abdecd529 100644 --- a/server/levels/thangs/thang_type_handler.coffee +++ b/server/levels/thangs/thang_type_handler.coffee @@ -3,7 +3,7 @@ Handler = require('../../commons/Handler') ThangTypeHandler = class ThangTypeHandler extends Handler modelClass: ThangType - jsonSchema: require '../../../app/schemas/thang_type_schema' + jsonSchema: require '../../../app/schemas/models/thang_type' editableProperties: [ 'name', 'raw', diff --git a/server/patches/patch_handler.coffee b/server/patches/patch_handler.coffee index 33b729e22..12a68ed9a 100644 --- a/server/patches/patch_handler.coffee +++ b/server/patches/patch_handler.coffee @@ -1,6 +1,6 @@ Patch = require('./Patch') Handler = require('../commons/Handler') -schema = require '../../app/schemas/patch_schema' +schema = require '../../app/schemas/models/patch' {handlers} = require '../commons/mapping' mongoose = require('mongoose') @@ -8,7 +8,7 @@ PatchHandler = class PatchHandler extends Handler modelClass: Patch editableProperties: [] postEditableProperties: ['delta', 'target', 'commitMessage'] - jsonSchema: require '../../app/schemas/patch_schema' + jsonSchema: require '../../app/schemas/models/patch' makeNewInstance: (req) -> patch = super(req) diff --git a/server/routes/db.coffee b/server/routes/db.coffee index 072b0ea8d..beb120573 100644 --- a/server/routes/db.coffee +++ b/server/routes/db.coffee @@ -1,7 +1,6 @@ log = require 'winston' errors = require '../commons/errors' handlers = require('../commons/mapping').handlers -schemas = require('../commons/mapping').schemas mongoose = require 'mongoose' module.exports.setup = (app) -> @@ -48,7 +47,7 @@ module.exports.setup = (app) -> getSchema = (req, res, moduleName) -> try name = moduleName.replace '.', '_' - schema = require('../../app/schemas/' + name + '_schema') + schema = require('../../app/schemas/models/' + name) res.send(JSON.stringify(schema, null, '\t')) res.end() diff --git a/server/users/User.coffee b/server/users/User.coffee index fd9b81969..0d3c42a92 100644 --- a/server/users/User.coffee +++ b/server/users/User.coffee @@ -1,5 +1,5 @@ mongoose = require('mongoose') -jsonschema = require('../../app/schemas/user_schema') +jsonschema = require('../../app/schemas/models/user') crypto = require('crypto') {salt, isProduction} = require('../../server_config') mail = require '../commons/mail' diff --git a/server/users/user_handler.coffee b/server/users/user_handler.coffee index eb26ff7e5..9023e3d94 100644 --- a/server/users/user_handler.coffee +++ b/server/users/user_handler.coffee @@ -1,4 +1,4 @@ -schema = require '../../app/schemas/user_schema' +schema = require '../../app/schemas/models/user' crypto = require 'crypto' request = require 'request' User = require './User' diff --git a/test/server/functional/article.spec.coffee b/test/server/functional/article.spec.coffee index 377907180..c48b6783b 100644 --- a/test/server/functional/article.spec.coffee +++ b/test/server/functional/article.spec.coffee @@ -33,6 +33,7 @@ describe '/db/article', -> new_article = _.clone(articles[0]) new_article.body = '...' request.post {uri:url, json:new_article}, (err, res, body) -> + console.log 'new article?', body expect(res.statusCode).toBe(200) expect(body.version.major).toBe(0) expect(body.version.minor).toBe(1) From 94e75b852c5603ffdad98b6910f12b548d10d96d Mon Sep 17 00:00:00 2001 From: Scott Erickson <sderickson@gmail.com> Date: Sat, 12 Apr 2014 10:51:31 -0700 Subject: [PATCH 35/46] Removed a log. --- test/server/functional/article.spec.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/test/server/functional/article.spec.coffee b/test/server/functional/article.spec.coffee index c48b6783b..377907180 100644 --- a/test/server/functional/article.spec.coffee +++ b/test/server/functional/article.spec.coffee @@ -33,7 +33,6 @@ describe '/db/article', -> new_article = _.clone(articles[0]) new_article.body = '...' request.post {uri:url, json:new_article}, (err, res, body) -> - console.log 'new article?', body expect(res.statusCode).toBe(200) expect(body.version.major).toBe(0) expect(body.version.minor).toBe(1) From b95d7d4cb44df45e58440d76471f0b8ffdd2cc00 Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker <decauwsemaecker.glen@gmail.com> Date: Sat, 12 Apr 2014 20:18:55 +0200 Subject: [PATCH 36/46] Fixed grammar error in files --- .../{localisation => localization}/de.coco | 0 .../{localisation => localization}/en.coco | 0 .../languages.coco | 0 .../{localisation => localization}/nl.coco | 0 .../{localisation => localization}/ru.coco | 0 .../zh-HANS.coco | 0 .../zh-HANT.coco | 0 .../batch/scripts/get_language.bat | 22 +++++++++---------- .../coco-dev-setup/batch/scripts/get_text.bat | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) rename scripts/windows/coco-dev-setup/batch/{localisation => localization}/de.coco (100%) rename scripts/windows/coco-dev-setup/batch/{localisation => localization}/en.coco (100%) rename scripts/windows/coco-dev-setup/batch/{localisation => localization}/languages.coco (100%) rename scripts/windows/coco-dev-setup/batch/{localisation => localization}/nl.coco (100%) rename scripts/windows/coco-dev-setup/batch/{localisation => localization}/ru.coco (100%) rename scripts/windows/coco-dev-setup/batch/{localisation => localization}/zh-HANS.coco (100%) rename scripts/windows/coco-dev-setup/batch/{localisation => localization}/zh-HANT.coco (100%) diff --git a/scripts/windows/coco-dev-setup/batch/localisation/de.coco b/scripts/windows/coco-dev-setup/batch/localization/de.coco similarity index 100% rename from scripts/windows/coco-dev-setup/batch/localisation/de.coco rename to scripts/windows/coco-dev-setup/batch/localization/de.coco diff --git a/scripts/windows/coco-dev-setup/batch/localisation/en.coco b/scripts/windows/coco-dev-setup/batch/localization/en.coco similarity index 100% rename from scripts/windows/coco-dev-setup/batch/localisation/en.coco rename to scripts/windows/coco-dev-setup/batch/localization/en.coco diff --git a/scripts/windows/coco-dev-setup/batch/localisation/languages.coco b/scripts/windows/coco-dev-setup/batch/localization/languages.coco similarity index 100% rename from scripts/windows/coco-dev-setup/batch/localisation/languages.coco rename to scripts/windows/coco-dev-setup/batch/localization/languages.coco diff --git a/scripts/windows/coco-dev-setup/batch/localisation/nl.coco b/scripts/windows/coco-dev-setup/batch/localization/nl.coco similarity index 100% rename from scripts/windows/coco-dev-setup/batch/localisation/nl.coco rename to scripts/windows/coco-dev-setup/batch/localization/nl.coco diff --git a/scripts/windows/coco-dev-setup/batch/localisation/ru.coco b/scripts/windows/coco-dev-setup/batch/localization/ru.coco similarity index 100% rename from scripts/windows/coco-dev-setup/batch/localisation/ru.coco rename to scripts/windows/coco-dev-setup/batch/localization/ru.coco diff --git a/scripts/windows/coco-dev-setup/batch/localisation/zh-HANS.coco b/scripts/windows/coco-dev-setup/batch/localization/zh-HANS.coco similarity index 100% rename from scripts/windows/coco-dev-setup/batch/localisation/zh-HANS.coco rename to scripts/windows/coco-dev-setup/batch/localization/zh-HANS.coco diff --git a/scripts/windows/coco-dev-setup/batch/localisation/zh-HANT.coco b/scripts/windows/coco-dev-setup/batch/localization/zh-HANT.coco similarity index 100% rename from scripts/windows/coco-dev-setup/batch/localisation/zh-HANT.coco rename to scripts/windows/coco-dev-setup/batch/localization/zh-HANT.coco diff --git a/scripts/windows/coco-dev-setup/batch/scripts/get_language.bat b/scripts/windows/coco-dev-setup/batch/scripts/get_language.bat index 96da622e9..70c36f5fe 100755 --- a/scripts/windows/coco-dev-setup/batch/scripts/get_language.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/get_language.bat @@ -3,28 +3,28 @@ echo but most feedback is sent and localised by us. echo Here is a list of languages: call print_dashed_seperator -call get_array ..\\localisation\\languages.coco languages language_count +call get_array ..\\localization\\languages.coco languages language_count for /l %%i in (1,1,%language_count%) do ( call get_text !languages[%%i]! global_description global description echo [%%i] !global_description! ) -goto:get_localisation_id +goto:get_localization_id -:get_localisation_id +:get_localization_id call print_dashed_seperator - set /p "localisation_id=Enter the language ID of your preference and press <ENTER>: " + set /p "localization_id=Enter the language ID of your preference and press <ENTER>: " goto:validation_check :validation_check - set "localisation_is_false=" - set /a local_id = %localisation_id% - if !local_id! EQU 0 set localisation_is_false=1 - if !local_id! LSS 1 set localisation_is_false=1 - if !local_id! GTR !language_count! set localisation_is_false=1 - if defined localisation_is_false ( + set "localization_is_false=" + set /a local_id = %localization_id% + if !local_id! EQU 0 set localization_is_false=1 + if !local_id! LSS 1 set localization_is_false=1 + if !local_id! GTR !language_count! set localization_is_false=1 + if defined localization_is_false ( echo The id you entered is invalid, please try again... - goto:get_localisation_id + goto:get_localization_id ) else ( set language_id=!languages[%local_id%]! call print_dashed_seperator diff --git a/scripts/windows/coco-dev-setup/batch/scripts/get_text.bat b/scripts/windows/coco-dev-setup/batch/scripts/get_text.bat index aacdf94f2..178f81a50 100755 --- a/scripts/windows/coco-dev-setup/batch/scripts/get_text.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/get_text.bat @@ -1,3 +1,3 @@ -for /F "delims=" %%F in ('call run_script .\\get_var.ps1 ..\\localisation\\%1.coco %3 %4 %5 %6') do ( +for /F "delims=" %%F in ('call run_script .\\get_var.ps1 ..\\localization\\%1.coco %3 %4 %5 %6') do ( set "%2=%%F" ) \ No newline at end of file From 5c7a255e98739f5dc8a38a42f0afb9f8bdd66fbc Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker <decauwsemaecker.glen@gmail.com> Date: Sat, 12 Apr 2014 20:34:06 +0200 Subject: [PATCH 37/46] License agreement and feedback for that is now also localized --- .../coco-dev-setup/batch/localization/de.coco | 7 ++++++ .../coco-dev-setup/batch/localization/en.coco | 7 ++++++ .../coco-dev-setup/batch/localization/nl.coco | 7 ++++++ .../coco-dev-setup/batch/localization/ru.coco | 7 ++++++ .../batch/localization/zh-HANS.coco | 7 ++++++ .../batch/localization/zh-HANT.coco | 7 ++++++ .../batch/scripts/print_license.bat | 2 +- .../batch/scripts/print_localized_file.bat | 2 -- .../batch/scripts/sign_license.bat | 22 ++++++++++++++----- 9 files changed, 60 insertions(+), 8 deletions(-) diff --git a/scripts/windows/coco-dev-setup/batch/localization/de.coco b/scripts/windows/coco-dev-setup/batch/localization/de.coco index fb8f1fce6..1630f8d53 100755 --- a/scripts/windows/coco-dev-setup/batch/localization/de.coco +++ b/scripts/windows/coco-dev-setup/batch/localization/de.coco @@ -9,6 +9,13 @@ <choosen>You have choosen Deutsch as your language.</choosen> <feedback>Ab jetzt senden wir unser Feedback in Deutsch.</feedback> </language> + <license> + <s1>In order to continue the installation of the developers environment</s1> + <s2>you will have to read and agree with the following license:</s2> + <q1>Have you read the license and do you agree with it?</q1> + <a1>This setup can't happen without an agreement.</a1> + <a2>Installation and Setup of the CodeCombat environment is cancelled.</a2> + </license> <install> <system> <bit>-Bit System erkannt.</bit> diff --git a/scripts/windows/coco-dev-setup/batch/localization/en.coco b/scripts/windows/coco-dev-setup/batch/localization/en.coco index d763e34a5..fdfcefc99 100755 --- a/scripts/windows/coco-dev-setup/batch/localization/en.coco +++ b/scripts/windows/coco-dev-setup/batch/localization/en.coco @@ -9,6 +9,13 @@ <choosen>You have choosen English as your language.</choosen> <feedback>From now on we'll send our feedback in English.</feedback> </language> + <license> + <s1>In order to continue the installation of the developers environment</s1> + <s2>you will have to read and agree with the following license:</s2> + <q1>Have you read the license and do you agree with it?</q1> + <a1>This setup can't happen without an agreement.</a1> + <a2>Installation and Setup of the CodeCombat environment is cancelled.</a2> + </license> <install> <system> <bit>-bit computer detected.</bit> diff --git a/scripts/windows/coco-dev-setup/batch/localization/nl.coco b/scripts/windows/coco-dev-setup/batch/localization/nl.coco index 971caf048..70af88eb0 100755 --- a/scripts/windows/coco-dev-setup/batch/localization/nl.coco +++ b/scripts/windows/coco-dev-setup/batch/localization/nl.coco @@ -9,6 +9,13 @@ <choosen>Je hebt Nederlands gekozen als jouw taal naar keuze.</choosen> <feedback>Vanaf nu geven we onze feedback in het Nederlands.</feedback> </language> + <license> + <s1>Om verder te gaan met de installatie van jouw CodeCombat omgeving</s1> + <s2>moet je de licentieovereenkomst lezen en ermee akkoord gaan.</s2> + <q1>Heb je de licentieovereenkomst gelezen en ga je ermee akkoord?</q1> + <a1>Deze installatie kan niet doorgaan zonder jouw akkoord.</a1> + <a2>De installatie van jouw Developers omgeving is nu geannulleerd.</a2> + </license> <install> <system> <bit>-bit computer gedetecteerd.</bit> diff --git a/scripts/windows/coco-dev-setup/batch/localization/ru.coco b/scripts/windows/coco-dev-setup/batch/localization/ru.coco index e7fc59f3e..cf3ae6823 100644 --- a/scripts/windows/coco-dev-setup/batch/localization/ru.coco +++ b/scripts/windows/coco-dev-setup/batch/localization/ru.coco @@ -9,6 +9,13 @@ <choosen>You have choosen ������� as your language.</choosen> <feedback>C ������� ������� �� ����� �������� �� �������.</feedback> </language> + <license> + <s1>In order to continue the installation of the developers environment</s1> + <s2>you will have to read and agree with the following license:</s2> + <q1>Have you read the license and do you agree with it?</q1> + <a1>This setup can't happen without an agreement.</a1> + <a2>Installation and Setup of the CodeCombat environment is cancelled.</a2> + </license> <install> <system> <bit>-������ ��������� ���������.</bit> diff --git a/scripts/windows/coco-dev-setup/batch/localization/zh-HANS.coco b/scripts/windows/coco-dev-setup/batch/localization/zh-HANS.coco index 29f8977e7..5932e75c2 100755 --- a/scripts/windows/coco-dev-setup/batch/localization/zh-HANS.coco +++ b/scripts/windows/coco-dev-setup/batch/localization/zh-HANS.coco @@ -9,6 +9,13 @@ <choosen>You have choosen 简体中文 as your language.</choosen> <feedback>目前我们只能用英文给你反馈</feedback> </language> + <license> + <s1>In order to continue the installation of the developers environment</s1> + <s2>you will have to read and agree with the following license:</s2> + <q1>Have you read the license and do you agree with it?</q1> + <a1>This setup can't happen without an agreement.</a1> + <a2>Installation and Setup of the CodeCombat environment is cancelled.</a2> + </license> <install> <system> <bit>-位系统.</bit> diff --git a/scripts/windows/coco-dev-setup/batch/localization/zh-HANT.coco b/scripts/windows/coco-dev-setup/batch/localization/zh-HANT.coco index efb024647..03768f3ce 100755 --- a/scripts/windows/coco-dev-setup/batch/localization/zh-HANT.coco +++ b/scripts/windows/coco-dev-setup/batch/localization/zh-HANT.coco @@ -9,6 +9,13 @@ <choosen>You have choosen 繁体中文 as your language.</choosen> <feedback>From now on we'll send our feedback in 繁体中文.</feedback> </language> + <license> + <s1>In order to continue the installation of the developers environment</s1> + <s2>you will have to read and agree with the following license:</s2> + <q1>Have you read the license and do you agree with it?</q1> + <a1>This setup can't happen without an agreement.</a1> + <a2>Installation and Setup of the CodeCombat environment is cancelled.</a2> + </license> <install> <system> <bit>-bit computer detected.</bit> diff --git a/scripts/windows/coco-dev-setup/batch/scripts/print_license.bat b/scripts/windows/coco-dev-setup/batch/scripts/print_license.bat index a208ca559..3acee4bcc 100755 --- a/scripts/windows/coco-dev-setup/batch/scripts/print_license.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/print_license.bat @@ -1 +1 @@ -print_file ..\\config\\license.coco \ No newline at end of file +call print_localized_file ..\\config\\localized\\license \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/print_localized_file.bat b/scripts/windows/coco-dev-setup/batch/scripts/print_localized_file.bat index 14c74e7a7..e71fe3364 100755 --- a/scripts/windows/coco-dev-setup/batch/scripts/print_localized_file.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/print_localized_file.bat @@ -1,8 +1,6 @@ set "LFTP=%1-%language_id%.coco" if not exist "%LFTP%" ( - echo printing %1.coco... call print_file %1.coco ) else ( - echo printing %LFTP%... call print_file %LFTP% ) \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/sign_license.bat b/scripts/windows/coco-dev-setup/batch/scripts/sign_license.bat index 139ddfd80..4b0ceecfc 100755 --- a/scripts/windows/coco-dev-setup/batch/scripts/sign_license.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/sign_license.bat @@ -1,15 +1,27 @@ -echo In order to continue the installation of the developers environment -echo you will have to read and agree with the following license: +call get_local_text license_s1 license s1 +echo !license_s1! + +call get_local_text license_s2 license s2 +echo !license_s2! + call print_dashed_seperator call print_license call print_dashed_seperator -call ask_question "Have you read the license and do you agree with it?" +call get_local_text license_q1 license q1 +call ask_question "%license_q1%" + call print_dashed_seperator if "%result%"=="false" ( - echo This setup can't happen without an agreement. - echo Installation and Setup of the CodeCombat environment is cancelled. + call get_local_text license_a1 license a1 + echo !license_a1! + + call get_local_text license_a2 license a2 + echo !license_a2! + + echo. + call print_exit ) \ No newline at end of file From 8e8b49708bfbf03524ba85acfc11dfaf555bbe1e Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker <decauwsemaecker.glen@gmail.com> Date: Sat, 12 Apr 2014 20:38:26 +0200 Subject: [PATCH 38/46] Readme at end of installation is now also localized --- .../batch/config/localized/license-nl.coco | 10 +++++++ .../batch/config/localized/readme-nl.coco | 29 +++++++++++++++++++ .../scripts/open_localized_text_file.bat | 6 ++++ .../batch/scripts/open_readme.bat | 2 +- 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100755 scripts/windows/coco-dev-setup/batch/config/localized/license-nl.coco create mode 100755 scripts/windows/coco-dev-setup/batch/config/localized/readme-nl.coco create mode 100755 scripts/windows/coco-dev-setup/batch/scripts/open_localized_text_file.bat diff --git a/scripts/windows/coco-dev-setup/batch/config/localized/license-nl.coco b/scripts/windows/coco-dev-setup/batch/config/localized/license-nl.coco new file mode 100755 index 000000000..9b753bf10 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/config/localized/license-nl.coco @@ -0,0 +1,10 @@ + +The MIT License (MIT) + +Copyright (c) 2014 CodeCombat Inc. and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in allcopies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN sCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THESOFTWARE. diff --git a/scripts/windows/coco-dev-setup/batch/config/localized/readme-nl.coco b/scripts/windows/coco-dev-setup/batch/config/localized/readme-nl.coco new file mode 100755 index 000000000..40665c28c --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/config/localized/readme-nl.coco @@ -0,0 +1,29 @@ + _____ _ _____ _ _ + / __ \ | | / __ \ | | | | + | / \/ ___ __| | ___ | / \/ ___ _ __ ___ | |__ __ _| |_ + | | / _ \ / _` |/ _ \ | | / _ \| '_ ` _ \| '_ \ / _` | __| + | \__/\ (_) | (_| | __/ | \__/\ (_) | | | | | | |_) | (_| | |_ + \____/\___/ \__,_|\___| \____/\___/|_| |_| |_|_.__/ \__,_|\__| + +============================================================================= + +Congratulations, you are now part of the CodeCombat community. +Now that your Develop Environment has been setup, you are ready to start +contributing and help us make this world a better place. + +Do you have questions or would you like to meet us? +Talk with us on hipchat @ https://www.hipchat.com/g3plnOKqa + +Another way to reach is, is by visiting our forum. +You can find it @ http://discourse.codecombat.com/ + +You can read about the latest developments on our blog site. +This one can be found @ http://blog.codecombat.com/ + +Last but not least, you can find most of our documentation +and information on our wiki @ https://github.com/codecombat/codecombat/wiki + +We hope you'll enjoy yourself within our community, just as much as us. + + + - Nick, George, Scott, Michael, Jeremy and Glen diff --git a/scripts/windows/coco-dev-setup/batch/scripts/open_localized_text_file.bat b/scripts/windows/coco-dev-setup/batch/scripts/open_localized_text_file.bat new file mode 100755 index 000000000..bee9f5dd6 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/scripts/open_localized_text_file.bat @@ -0,0 +1,6 @@ +set "LFTP=%1-%language_id%.coco" +if not exist "%LFTP%" ( + call open_text_file %1.coco +) else ( + call open_text_file %LFTP% +) \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/open_readme.bat b/scripts/windows/coco-dev-setup/batch/scripts/open_readme.bat index 484f3dd75..730a3f577 100755 --- a/scripts/windows/coco-dev-setup/batch/scripts/open_readme.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/open_readme.bat @@ -1 +1 @@ -call open_text_file ..\\config\\readme.coco \ No newline at end of file +call open_localized_text_file ..\\config\\localized\\readme \ No newline at end of file From 74f8a469a59f286442011f643bcc8e37abef5197 Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker <decauwsemaecker.glen@gmail.com> Date: Sat, 12 Apr 2014 20:49:08 +0200 Subject: [PATCH 39/46] Exit script is now also localized --- scripts/windows/coco-dev-setup/batch/localization/de.coco | 1 + scripts/windows/coco-dev-setup/batch/localization/en.coco | 1 + scripts/windows/coco-dev-setup/batch/localization/nl.coco | 1 + scripts/windows/coco-dev-setup/batch/localization/ru.coco | 1 + scripts/windows/coco-dev-setup/batch/localization/zh-HANS.coco | 1 + scripts/windows/coco-dev-setup/batch/localization/zh-HANT.coco | 1 + scripts/windows/coco-dev-setup/batch/scripts/print_exit.bat | 3 ++- 7 files changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/windows/coco-dev-setup/batch/localization/de.coco b/scripts/windows/coco-dev-setup/batch/localization/de.coco index 1630f8d53..cb24108fe 100755 --- a/scripts/windows/coco-dev-setup/batch/localization/de.coco +++ b/scripts/windows/coco-dev-setup/batch/localization/de.coco @@ -4,6 +4,7 @@ <native>Deutsch</native> <description>German</description> <tips>Before we start the installation, here are some tips:</tips> + <exit>Press any key to exit...</exit> </global> <language> <choosen>You have choosen Deutsch as your language.</choosen> diff --git a/scripts/windows/coco-dev-setup/batch/localization/en.coco b/scripts/windows/coco-dev-setup/batch/localization/en.coco index fdfcefc99..bef7c9ae6 100755 --- a/scripts/windows/coco-dev-setup/batch/localization/en.coco +++ b/scripts/windows/coco-dev-setup/batch/localization/en.coco @@ -4,6 +4,7 @@ <native>English</native> <description>English</description> <tips>Before we start the installation, here are some tips:</tips> + <exit>Press any key to exit...</exit> </global> <language> <choosen>You have choosen English as your language.</choosen> diff --git a/scripts/windows/coco-dev-setup/batch/localization/nl.coco b/scripts/windows/coco-dev-setup/batch/localization/nl.coco index 70af88eb0..a969efb31 100755 --- a/scripts/windows/coco-dev-setup/batch/localization/nl.coco +++ b/scripts/windows/coco-dev-setup/batch/localization/nl.coco @@ -4,6 +4,7 @@ <native>Nederlands</native> <description>Dutch</description> <tips>Voor we verder gaan met de installatie hier volgen enkele tips:</tips> + <exit>Druk een willekeurige toets in om af te sluiten...</exit> </global> <language> <choosen>Je hebt Nederlands gekozen als jouw taal naar keuze.</choosen> diff --git a/scripts/windows/coco-dev-setup/batch/localization/ru.coco b/scripts/windows/coco-dev-setup/batch/localization/ru.coco index cf3ae6823..150391711 100644 --- a/scripts/windows/coco-dev-setup/batch/localization/ru.coco +++ b/scripts/windows/coco-dev-setup/batch/localization/ru.coco @@ -4,6 +4,7 @@ <native>�������</native> <description>Russian</description> <tips>Before we start the installation, here are some tips:</tips> + <exit>Press any key to exit...</exit> </global> <language> <choosen>You have choosen ������� as your language.</choosen> diff --git a/scripts/windows/coco-dev-setup/batch/localization/zh-HANS.coco b/scripts/windows/coco-dev-setup/batch/localization/zh-HANS.coco index 5932e75c2..2d598306d 100755 --- a/scripts/windows/coco-dev-setup/batch/localization/zh-HANS.coco +++ b/scripts/windows/coco-dev-setup/batch/localization/zh-HANS.coco @@ -4,6 +4,7 @@ <native>简体中文</native> <description>Traditional Chinese</description> <tips>Before we start the installation, here are some tips:</tips> + <exit>Press any key to exit...</exit> </global> <language> <choosen>You have choosen 简体中文 as your language.</choosen> diff --git a/scripts/windows/coco-dev-setup/batch/localization/zh-HANT.coco b/scripts/windows/coco-dev-setup/batch/localization/zh-HANT.coco index 03768f3ce..d5510b57d 100755 --- a/scripts/windows/coco-dev-setup/batch/localization/zh-HANT.coco +++ b/scripts/windows/coco-dev-setup/batch/localization/zh-HANT.coco @@ -4,6 +4,7 @@ <native>繁体中文</native> <description>Simplified Chinese</description> <tips>Before we start the installation, here are some tips:</tips> + <exit>Press any key to exit...</exit> </global> <language> <choosen>You have choosen 繁体中文 as your language.</choosen> diff --git a/scripts/windows/coco-dev-setup/batch/scripts/print_exit.bat b/scripts/windows/coco-dev-setup/batch/scripts/print_exit.bat index 6f1051cc6..afcd9643b 100755 --- a/scripts/windows/coco-dev-setup/batch/scripts/print_exit.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/print_exit.bat @@ -1,2 +1,3 @@ -set /p res="Press any key to exit..." +call get_local_text global_exit global exit +set /p res="%global_exit%" exit \ No newline at end of file From 9157a5d3ba909daf5d7548d464fd56fa99bcc819 Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker <decauwsemaecker.glen@gmail.com> Date: Sat, 12 Apr 2014 21:11:12 +0200 Subject: [PATCH 40/46] Seperation of s3 and s4 tag in the installation process. --- .../batch/scripts/download_and_install_applications.bat | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/windows/coco-dev-setup/batch/scripts/download_and_install_applications.bat b/scripts/windows/coco-dev-setup/batch/scripts/download_and_install_applications.bat index 3c5f798fd..437fc6afa 100755 --- a/scripts/windows/coco-dev-setup/batch/scripts/download_and_install_applications.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/download_and_install_applications.bat @@ -38,6 +38,7 @@ call get_local_text install_process_winpath install process winpath echo !install_process_s1! echo !install_process_s2! echo !install_process_s3! +echo. echo !install_process_s4! echo. echo !install_process_winpath! From 1efa1a54e12f4bebbadf37380c2e8c654d9ebb89 Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker <decauwsemaecker.glen@gmail.com> Date: Sat, 12 Apr 2014 21:15:36 +0200 Subject: [PATCH 41/46] Seperated win7 and win8 to prevent any future awkwardness --- .../windows/coco-dev-setup/batch/config/downloads.coco | 8 ++++++++ .../batch/scripts/get_system_information.bat | 6 ++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/scripts/windows/coco-dev-setup/batch/config/downloads.coco b/scripts/windows/coco-dev-setup/batch/config/downloads.coco index 1d57fbb71..f8906cbb4 100755 --- a/scripts/windows/coco-dev-setup/batch/config/downloads.coco +++ b/scripts/windows/coco-dev-setup/batch/config/downloads.coco @@ -18,6 +18,14 @@ <vs10redist>http://download.microsoft.com/download/C/6/D/C6D0FD4E-9E53-4897-9B91-836EBA2AACD3/vcredist_x86.exe</vs10redist> </general> </general> + <Win8> + <b32> + <mongodb>https://fastdl.mongodb.org/win32/mongodb-win32-i386-2.6.0.zip</mongodb> + </b32> + <b64> + <mongodb>https://fastdl.mongodb.org/win32/mongodb-win32-x86_64-2008plus-2.6.0.zip</mongodb> + </b64> + </Win8> <Win7> <b32> <mongodb>https://fastdl.mongodb.org/win32/mongodb-win32-i386-2.6.0.zip</mongodb> diff --git a/scripts/windows/coco-dev-setup/batch/scripts/get_system_information.bat b/scripts/windows/coco-dev-setup/batch/scripts/get_system_information.bat index 908399932..2cf69dfda 100755 --- a/scripts/windows/coco-dev-setup/batch/scripts/get_system_information.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/get_system_information.bat @@ -8,10 +8,8 @@ for /f "tokens=4-5 delims=. " %%i in ('ver') do set VERSION=%%i.%%j if "%version%" == "5.2" ( call:set_os XP ) if "%version%" == "6.0" ( call:set_os Vista ) if "%version%" == "6.1" ( call:set_os Win7 ) -:: we handle win8.0 as win7 -if "%version%" == "6.2" ( call:set_os Win7 ) -:: we handle win8.1 as win7 -if "%version%" == "6.3" ( call:set_os Win7 ) +if "%version%" == "6.2" ( call:set_os Win8 ) +if "%version%" == "6.3" ( call:set_os Win8 ) goto:end From 1f3bf9e8aba25f4a202a9590537a46fcfdd69727 Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker <decauwsemaecker.glen@gmail.com> Date: Sat, 12 Apr 2014 21:16:49 +0200 Subject: [PATCH 42/46] Modified seperation scripts to have the correct width --- .../coco-dev-setup/batch/scripts/print_dashed_seperator.bat | 2 +- .../windows/coco-dev-setup/batch/scripts/print_seperator.bat | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/windows/coco-dev-setup/batch/scripts/print_dashed_seperator.bat b/scripts/windows/coco-dev-setup/batch/scripts/print_dashed_seperator.bat index 727d7e61c..5022e09a7 100755 --- a/scripts/windows/coco-dev-setup/batch/scripts/print_dashed_seperator.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/print_dashed_seperator.bat @@ -1,3 +1,3 @@ echo. -echo - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +echo - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - echo. \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/print_seperator.bat b/scripts/windows/coco-dev-setup/batch/scripts/print_seperator.bat index c68792d46..b77582db3 100755 --- a/scripts/windows/coco-dev-setup/batch/scripts/print_seperator.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/print_seperator.bat @@ -1,3 +1,3 @@ echo. -echo ----------------------------------------------------------------------------- +echo ------------------------------------------------------------------------------- echo. \ No newline at end of file From 8349578057a1bbc5d8a63eaaa69cde0d20bb9ef9 Mon Sep 17 00:00:00 2001 From: Nick Winter <livelily@gmail.com> Date: Sat, 12 Apr 2014 12:35:45 -0700 Subject: [PATCH 43/46] Fixed #786. --- app/views/kinds/RootView.coffee | 6 ++---- app/views/play_view.coffee | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/app/views/kinds/RootView.coffee b/app/views/kinds/RootView.coffee index fd824cd47..7ef3e7221 100644 --- a/app/views/kinds/RootView.coffee +++ b/app/views/kinds/RootView.coffee @@ -41,13 +41,13 @@ module.exports = class RootView extends CocoView hash = location.hash location.hash = '' location.hash = hash - @buildLanguages() @renderScrollbar() #@$('.antiscroll-wrap').antiscroll() # not yet, buggy afterRender: -> super(arguments...) @chooseTab(location.hash.replace('#','')) if location.hash + @buildLanguages() $('body').removeClass('is-playing') chooseTab: (category) -> @@ -58,7 +58,7 @@ module.exports = class RootView extends CocoView buildLanguages: -> $select = @$el.find(".language-dropdown").empty() if $select.hasClass("fancified") - $select.parent().find('.options,.trigger').remove() + $select.parent().find('.options, .trigger').remove() $select.unwrap().removeClass("fancified") preferred = me.lang() codes = _.keys(locale) @@ -76,10 +76,8 @@ module.exports = class RootView extends CocoView $.i18n.setLng(newLang, {}) @saveLanguage(newLang) @render() - @buildLanguages() unless newLang.split('-')[0] is "en" @openModalView(application.router.getView("modal/diplomat_suggestion", "_modal")) - $('body').attr('lang', newLang) saveLanguage: (newLang) -> me.set('preferredLanguage', newLang) diff --git a/app/views/play_view.coffee b/app/views/play_view.coffee index d505cb5e6..349f900c7 100644 --- a/app/views/play_view.coffee +++ b/app/views/play_view.coffee @@ -227,4 +227,3 @@ module.exports = class PlayView extends View super() @$el.find('.modal').on 'shown.bs.modal', -> $('input:visible:first', @).focus() - From 072729acc34123c42250d361955438cfd8c210d7 Mon Sep 17 00:00:00 2001 From: Nick Winter <livelily@gmail.com> Date: Sat, 12 Apr 2014 13:03:46 -0700 Subject: [PATCH 44/46] Non-admins can save (but not overwrite) file uploads. --- server/routes/file.coffee | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/server/routes/file.coffee b/server/routes/file.coffee index 7a16c3709..f01f635e3 100644 --- a/server/routes/file.coffee +++ b/server/routes/file.coffee @@ -19,7 +19,7 @@ fileGet = (req, res) -> objectId = mongoose.Types.ObjectId(path) query = objectId catch e - path = path.split('/') + path = path.split('/') filename = path[path.length-1] path = path[...path.length-1].join('/') query = @@ -34,7 +34,7 @@ fileGet = (req, res) -> res.setHeader('Content-Type', 'text/json') res.send(results) res.end() - + else Grid.gfs.collection('media').findOne query, (err, filedata) => return errors.notFound(res) if not filedata @@ -42,7 +42,7 @@ fileGet = (req, res) -> if req.headers['if-modified-since'] is filedata.uploadDate res.status(304) return res.end() - + res.setHeader('Content-Type', filedata.contentType) res.setHeader('Last-Modified', filedata.uploadDate) res.setHeader('Cache-Control', 'public') @@ -70,7 +70,7 @@ postFileSchema = required: ['filename', 'mimetype', 'path'] filePost = (req, res) -> - return errors.forbidden(res) unless req.user?.isAdmin() + return errors.forbidden(res) unless req.user options = req.body tv4 = require('tv4').tv4 valid = tv4.validate(options, postFileSchema) @@ -83,7 +83,8 @@ filePost = (req, res) -> saveURL = (req, res) -> options = createPostOptions(req) - checkExistence options, res, req.body.force, (err) -> + force = req.user.isAdmin() and req.body.force + checkExistence options, res, force, (err) -> return errors.serverError(res) if err writestream = Grid.gfs.createWriteStream(options) request(req.body.url).pipe(writestream) @@ -91,7 +92,8 @@ saveURL = (req, res) -> saveFile = (req, res) -> options = createPostOptions(req) - checkExistence options, res, req.body.force, (err) -> + force = req.user.isAdmin() and req.body.force + checkExistence options, res, force, (err) -> return if err writestream = Grid.gfs.createWriteStream(options) f = req.files[req.body.postName] @@ -101,7 +103,8 @@ saveFile = (req, res) -> savePNG = (req, res) -> options = createPostOptions(req) - checkExistence options, res, req.body.force, (err) -> + force = req.user.isAdmin() and req.body.force + checkExistence options, res, force, (err) -> return errors.serverError(res) if err writestream = Grid.gfs.createWriteStream(options) img = new Buffer(req.body.b64png, 'base64') @@ -143,11 +146,11 @@ createPostOptions = (req) -> unless req.body.name name = req.body.filename.split('.')[0] req.body.name = _.str.humanize(name) - + path = req.body.path or '' path = path[1...] if path and path[0] is '/' path = path[...path.length-2] if path and path[path.length-1] is '/' - + options = mode: 'w' filename: req.body.filename @@ -158,6 +161,6 @@ createPostOptions = (req) -> name: req.body.name path: path creator: ''+req.user._id - options.metadata.description = req.body.description if req.body.description? + options.metadata.description = req.body.description if req.body.description? options From 9879cffdbc70ccc94f5219defcfae1670d1d6c73 Mon Sep 17 00:00:00 2001 From: Alexei Nikitin <mr-a1@yandex.ru> Date: Sun, 13 Apr 2014 01:06:42 +0400 Subject: [PATCH 45/46] Update RU.coffee --- app/locale/ru.coffee | 64 ++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/app/locale/ru.coffee b/app/locale/ru.coffee index 8b7a05e89..d629c1270 100644 --- a/app/locale/ru.coffee +++ b/app/locale/ru.coffee @@ -3,7 +3,7 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi loading: "Загрузка..." saving: "Сохранение..." sending: "Отправка..." -# send: "Send" + send: "Отправить" cancel: "Отмена" save: "Сохранить" create: "Создать" @@ -115,8 +115,8 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi forum_page: "наш форум" forum_suffix: "." send: "Отправить отзыв" -# contact_candidate: "Contact Candidate" -# recruitment_reminder: "Use this form to reach out to candidates you are interested in interviewing. Remember that CodeCombat charges 18% of first-year salary. The fee is due upon hiring the employee and is refundable for 90 days if the employee does not remain employed. Part time, remote, and contract employees are free, as are interns." + contact_candidate: "Связаться с кандидатом" + recruitment_reminder: "Используйте эту форму, чтобы обратиться к кандидатам, если вы заинтересованы в интервью. Помните, что CodeCombat взимает 18% от первого года зарплаты. Плата производится по найму сотрудника и подлежит возмещению в течение 90 дней, если работник не остаётся на рабочем месте. Работники с частичной занятостью, удалённые и работающие по контракту свободны, как стажёры." diplomat_suggestion: title: "Помогите перевести CodeCombat!" @@ -151,7 +151,7 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi wizard_tab: "Волшебник" password_tab: "Пароль" emails_tab: "Email-адреса" -# job_profile_tab: "Job Profile" + job_profile_tab: "Профиль соискателя" admin: "Админ" wizard_color: "Цвет одежды волшебника" new_password: "Новый пароль" @@ -169,37 +169,37 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi error_saving: "Ошибка сохранения" saved: "Изменения сохранены" password_mismatch: "Пароли не совпадают." -# job_profile: "Job Profile" -# job_profile_approved: "Your job profile has been approved by CodeCombat. Employers will be able to see it until you either mark it inactive or it has not been changed for four weeks." -# job_profile_explanation: "Hi! Fill this out, and we will get in touch about finding you a software developer job." + job_profile: "Профиль соискателя" + job_profile_approved: "Ваш профиль соискателя был одобрен CodeCombat. Работодатели смогут видеть его, пока вы не отметите его неактивным или он не будет изменен в течение четырёх недель." + job_profile_explanation: "Привет! Заполните это, и мы свяжемся с вами при нахождении работы разработчика программного обеспечения для вас." account_profile: edit_settings: "Изменить настройки" profile_for_prefix: "Профиль для " profile_for_suffix: "" -# approved: "Approved" -# not_approved: "Not Approved" -# looking_for: "Looking for:" -# last_updated: "Last updated:" -# contact: "Contact" -# work_experience: "Work Experience" -# education: "Education" -# our_notes: "Our Notes" -# projects: "Projects" + approved: "Одобрено" + not_approved: "Не одобрено" + looking_for: "Ищет:" + last_updated: "Последнее обновление:" + contact: "Контакты" + work_experience: "Опыт работы" + education: "Образование" + our_notes: "Наши заметки" + projects: "Проекты" -# employers: -# want_to_hire_our_players: "Want to hire expert CodeCombat players?" -# contact_george: "Contact George to see our candidates" -# candidates_count_prefix: "We currently have " -# candidates_count_many: "many" -# candidates_count_suffix: "highly skilled and vetted developers looking for work." -# candidate_name: "Name" -# candidate_location: "Location" -# candidate_looking_for: "Looking For" -# candidate_role: "Role" -# candidate_top_skills: "Top Skills" -# candidate_years_experience: "Yrs Exp" -# candidate_last_updated: "Last Updated" + employers: + want_to_hire_our_players: "Хотите нанимать игроков-экспертов CodeCombat?" + contact_george: "Свяжитесь с Джорджем, чтобы посмотреть наших кандидатов" + candidates_count_prefix: "Сейчас у нас есть " + candidates_count_many: "много" + candidates_count_suffix: "высококвалифицированных и проверенных разработчиков, ищущих работу." + candidate_name: "Имя" + candidate_location: "Местонахождение" + candidate_looking_for: "Ищет" + candidate_role: "Роль" + candidate_top_skills: "Лучшие навыки" + candidate_years_experience: "Лет опыта" + candidate_last_updated: "Последнее обновление" play_level: level_load_error: "Уровень не может быть загружен: " @@ -379,7 +379,7 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi results: "Результаты" description: "Описание" or: "или" -# subject: "Subject" + subject: "Тема" email: "Email" password: "Пароль" message: "Сообщение" @@ -661,5 +661,5 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi gplus_friends: "Друзья G+" gplus_friend_sessions: "Сессии друзей G+" leaderboard: "таблица лидеров" -# user_schema: "User Schema" -# user_profile: "User Profile" + user_schema: "Пользовательская Schema" + user_profile: "Пользовательский профиль" From 08616def105ea3bba4dd4526d02f7c995dca900c Mon Sep 17 00:00:00 2001 From: Nick Winter <livelily@gmail.com> Date: Sat, 12 Apr 2014 14:53:09 -0700 Subject: [PATCH 46/46] Refactored contribute views to share some templates and link to users' profile photos and GitHub accounts when available. --- app/styles/contribute_classes.sass | 10 ++- app/templates/contribute/adventurer.jade | 30 ++------ app/templates/contribute/ambassador.jade | 29 ++------ app/templates/contribute/archmage.jade | 35 ++------- app/templates/contribute/artisan.jade | 35 ++------- app/templates/contribute/contribute.jade | 63 ++-------------- .../contribute/contributor_list.jade | 13 ++++ .../contribute/contributor_signup.jade | 5 ++ .../contributor_signup_anonymous.jade | 12 +++ app/templates/contribute/diplomat.jade | 74 ++++++++----------- app/templates/contribute/scribe.jade | 37 ++-------- app/views/account/settings_view.coffee | 5 +- app/views/contribute/adventurer_view.coffee | 3 +- app/views/contribute/ambassador_view.coffee | 1 + app/views/contribute/archmage_view.coffee | 34 +++++---- app/views/contribute/artisan_view.coffee | 7 +- .../contribute/contribute_class_view.coffee | 9 +++ app/views/contribute/counselor_view.coffee | 1 + app/views/contribute/diplomat_view.coffee | 1 + app/views/contribute/scribe_view.coffee | 11 +++ server/users/user_handler.coffee | 12 ++- 21 files changed, 162 insertions(+), 265 deletions(-) create mode 100644 app/templates/contribute/contributor_list.jade create mode 100644 app/templates/contribute/contributor_signup.jade create mode 100644 app/templates/contribute/contributor_signup_anonymous.jade diff --git a/app/styles/contribute_classes.sass b/app/styles/contribute_classes.sass index 9244aca55..e4b8ff7fb 100644 --- a/app/styles/contribute_classes.sass +++ b/app/styles/contribute_classes.sass @@ -49,9 +49,15 @@ &:hover background-color: rgba(200, 244, 255, 0.2) - h4 - text-align: center + a:not(.has-github) + cursor: default + text-decoration: none + img max-width: 100px max-height: 100px + .caption + background-color: transparent + h4 + text-align: center diff --git a/app/templates/contribute/adventurer.jade b/app/templates/contribute/adventurer.jade index 0cbe1e866..3b9367620 100644 --- a/app/templates/contribute/adventurer.jade +++ b/app/templates/contribute/adventurer.jade @@ -53,30 +53,12 @@ block content span(data-i18n="contribute.adventurer_join_suf") | so if you prefer to be notified those ways, sign up there! - if me.attributes.anonymous - div#sign-up.alert.alert-info - strong(data-i18n="contribute.alert_account_message_intro") - | Hey there! - span - span(data-i18n="contribute.alert_account_message_pref") - | To subscribe for class emails, you'll need to - a(data-toggle="coco-modal", data-target="modal/signup", data-i18n="contribute.alert_account_message_create_url") - | create an account - span - span(data-i18n="contribute.alert_account_message_suf") - | first. + .contributor-signup-anonymous + .contributor-signup(data-contributor-class-id="tester", data-contributor-class-name="adventurer") - label.checkbox(for="tester").well - input(type='checkbox', name="tester", id="tester") - span(data-i18n="contribute.adventurer_subscribe_desc") - | Get emails when there are new levels to test. - .saved-notification ✓ Saved - - //#Contributors - // h3(data-i18n="contribute.brave_adventurers") - // | Our Brave Adventurers: - // ul.adventurers - // li Kieizroe - // li ... many, many more + //h3(data-i18n="contribute.brave_adventurers") + // | Our Brave Adventurers: + // + //#contributor-list div.clearfix diff --git a/app/templates/contribute/ambassador.jade b/app/templates/contribute/ambassador.jade index dc1048ac6..2d1f65564 100644 --- a/app/templates/contribute/ambassador.jade +++ b/app/templates/contribute/ambassador.jade @@ -47,29 +47,12 @@ block content | solving levels can summon higher level wizards to help them. | This will be a great way for ambassadors to do their thing. We'll keep you posted! - if me.attributes.anonymous - div#sign-up.alert.alert-info - strong(data-i18n="contribute.alert_account_message_intro") - | Hey there! - span - span(data-i18n="contribute.alert_account_message_pref") - | To subscribe for class emails, you'll need to - a(data-toggle="coco-modal", data-target="modal/signup", data-i18n="contribute.alert_account_message_create_url") - | create an account - span - span(data-i18n="contribute.alert_account_message_suf") - | first. + .contributor-signup-anonymous + .contributor-signup(data-contributor-class-id="support", data-contributor-class-name="ambassador") - label.checkbox(for="support").well - input(type='checkbox', name="support", id="support") - span(data-i18n="contribute.ambassador_subscribe_desc") - | Get emails on support updates and multiplayer developments. - .saved-notification ✓ Saved - - //#Contributors - // h3(data-i18n="contribute.helpful_ambassadors") - // | Our Helpful Ambassadorsd: - // ul.ambassadors - // li + //h3(data-i18n="contribute.helpful_ambassadors") + // | Our Helpful Ambassadorsd: + // + //#contributor-list div.clearfix diff --git a/app/templates/contribute/archmage.jade b/app/templates/contribute/archmage.jade index db7dad7db..ae982472e 100644 --- a/app/templates/contribute/archmage.jade +++ b/app/templates/contribute/archmage.jade @@ -57,37 +57,12 @@ block content span(data-i18n="contribute.join_desc_4") | and we'll go from there! - if me.attributes.anonymous - div#sign-up.alert.alert-info - strong(data-i18n="contribute.alert_account_message_intro") - | Hey there! - span - span(data-i18n="contribute.alert_account_message_pref") - | To subscribe for class emails, you'll need to - a(data-toggle="coco-modal", data-target="modal/signup", data-i18n="contribute.alert_account_message_create_url") - | create an account - span - span(data-i18n="contribute.alert_account_message_suf") - | first. + .contributor-signup-anonymous + .contributor-signup(data-contributor-class-id="developer", data-contributor-class-name="archmage") - label.checkbox(for="developer").well - input(type='checkbox', name="developer", id="developer") - span(data-i18n="contribute.archmage_subscribe_desc") - | Get emails on new coding opportunities and announcements. - .saved-notification ✓ Saved + h3(data-i18n="contribute.powerful_archmages") + | Our Powerful Archmages: - #Contributors - h3(data-i18n="contribute.powerful_archmages") - | Our Powerful Archmages: - .row - for contributor in contributors - .col-xs-6.col-md-3 - .thumbnail - if contributor.avatar - img.img-responsive(src="/images/pages/contribute/archmage/" + contributor.avatar + "_small.png", alt="") - else - img.img-responsive(src="/images/pages/contribute/archmage.png", alt="") - .caption - h4= contributor.name + #contributor-list div.clearfix diff --git a/app/templates/contribute/artisan.jade b/app/templates/contribute/artisan.jade index 9e6d1320d..54a27327b 100644 --- a/app/templates/contribute/artisan.jade +++ b/app/templates/contribute/artisan.jade @@ -54,38 +54,13 @@ block content li a(href="http://discourse.codecombat.com", data-i18n="contribute.artisan_join_step4") Post your levels on the forum for feedback. - if me.attributes.anonymous - div#sign-up.alert.alert-info - strong(data-i18n="contribute.alert_account_message_intro") - | Hey there! - span - span(data-i18n="contribute.alert_account_message_pref") - | To subscribe for class emails, you'll need to - a(data-toggle="coco-modal", data-target="modal/signup", data-i18n="contribute.alert_account_message_create_url") - | create an account - span - span(data-i18n="contribute.alert_account_message_suf") - | first. + .contributor-signup-anonymous + .contributor-signup(data-contributor-class-id="level_creator", data-contributor-class-name="artisan") - label.checkbox(for="level_creator").well - input(type='checkbox', name="level_creator", id="level_creator") - span(data-i18n="contribute.artisan_subscribe_desc") - | Get emails on level editor updates and announcements. - .saved-notification ✓ Saved + h3(data-i18n="contribute.creative_artisans") + | Our Creative Artisans: - #Contributors - h3(data-i18n="contribute.creative_artisans") - | Our Creative Artisans: - .row - for contributor in contributors - .col-xs-6.col-md-3 - .thumbnail - if contributor.avatar - img.img-responsive(src="/images/pages/contribute/artisan/" + contributor.avatar + "_small.png", alt="") - else - img.img-responsive(src="/images/pages/contribute/artisan.png", alt="") - .caption - h4= contributor.name + #contributor-list div.clearfix diff --git a/app/templates/contribute/contribute.jade b/app/templates/contribute/contribute.jade index ffb7045a6..800b383e7 100644 --- a/app/templates/contribute/contribute.jade +++ b/app/templates/contribute/contribute.jade @@ -37,18 +37,7 @@ block content | - Nick, George, Scott, Michael, Jeremy and Glen hr - if me.attributes.anonymous - div#sign-up.alert.alert-info - strong(data-i18n="contribute.alert_account_message_intro") - | Hey there! - span - span(data-i18n="contribute.alert_account_message_pref") - | To subscribe for class emails, you'll need to - a(data-toggle="coco-modal", data-target="modal/signup", data-i18n="contribute.alert_account_message_create_url") - | create an account - span - span(data-i18n="contribute.alert_account_message_suf") - | first. + .contributor-signup-anonymous #archmage.header-scrolling-fix .class_image @@ -69,13 +58,7 @@ block content p.lead(data-i18n="contribute.more_about_archmage") | Learn More About Becoming an Archmage - label.checkbox(for="developer").well - input(type='checkbox', name="developer", id="developer") - span(data-i18n="contribute.archmage_subscribe_desc") - | Get emails on new coding opportunities and announcements. - .saved-notification - | ✓ - span(data-i18n="contribute.saved") Saved + .contributor-signup(data-contributor-class-id="developer", data-contributor-class-name="archmage") #artisan.header-scrolling-fix @@ -102,13 +85,7 @@ block content p.lead(data-i18n="contribute.more_about_artisan") | Learn More About Becoming An Artisan - label.checkbox(for="level_creator").well - input(type='checkbox', name="level_creator", id="level_creator") - span(data-i18n="contribute.artisan_subscribe_desc") - | Get emails on level editor updates and announcements. - .saved-notification - | ✓ - span(data-i18n="contribute.saved") Saved + .contributor-signup(data-contributor-class-id="level_creator", data-contributor-class-name="artisan") #adventurer.header-scrolling-fix @@ -130,13 +107,7 @@ block content p.lead(data-i18n="contribute.more_about_adventurer") | Learn More About Becoming an Adventurer - label.checkbox(for="tester").well - input(type='checkbox', name="tester", id="tester") - span(data-i18n="contribute.adventurer_subscribe_desc") - | Get emails when there are new levels to test. - .saved-notification - | ✓ - span(data-i18n="contribute.saved") Saved + .contributor-signup(data-contributor-class-id="tester", data-contributor-class-name="adventurer") #scribe.header-scrolling-fix @@ -162,13 +133,7 @@ block content p.lead(data-i18n="contribute.more_about_scribe") | Learn More About Becoming a Scribe - label.checkbox(for="article_editor").well - input(type='checkbox', name="article_editor", id="article_editor") - span(data-i18n="contribute.scribe_subscribe_desc") - | Get emails about article writing announcements. - .saved-notification - | ✓ - span(data-i18n="contribute.saved") Saved + .contributor-signup(data-contributor-class-id="article_editor", data-contributor-class-name="scribe") #diplomat.header-scrolling-fix @@ -191,14 +156,8 @@ block content p.lead(data-i18n="contribute.more_about_diplomat") | Learn More About Becoming a Diplomat - label.checkbox(for="translator").well - input(type='checkbox', name="translator", id="translator") - span(data-i18n="contribute.diplomat_subscribe_desc") - | Get emails about i18n developments and levels to translate. - .saved-notification - | ✓ - span(data-i18n="contribute.saved") Saved - + .contributor-signup(data-contributor-class-id="translator", data-contributor-class-name="diplomat") + #ambassador.header-scrolling-fix .class_image @@ -218,13 +177,7 @@ block content p.lead(data-i18n="contribute.more_about_ambassador") | Learn More About Becoming an Ambassador - label.checkbox(for="support").well - input(type='checkbox', name="support", id="support") - span(data-i18n="contribute.ambassador_subscribe_desc") - | Get emails on support updates and multiplayer developments. - .saved-notification - | ✓ - span(data-i18n="contribute.saved") Saved + .contributor-signup(data-contributor-class-id="support", data-contributor-class-name="ambassador") #counselor.header-scrolling-fix diff --git a/app/templates/contribute/contributor_list.jade b/app/templates/contribute/contributor_list.jade new file mode 100644 index 000000000..32e5ac9e3 --- /dev/null +++ b/app/templates/contribute/contributor_list.jade @@ -0,0 +1,13 @@ +.row + for contributor in contributors + .col-xs-6.col-md-3 + .thumbnail + - var src = "/images/pages/contribute/" + contributorClassName + ".png"; + - if(contributor.avatar) + - src = src.replace(contributorClassName, contributorClassName + "/" + contributor.avatar + "_small"); + - if(contributor.id) + - src = "/db/user/" + contributor.id + "/avatar?s=100&fallback=" + src; + a(href=contributor.github ? "https://github.com/codecombat/codecombat/commits?author=" + contributor.github : null, class=contributor.github ? 'has-github' : '') + img.img-responsive(src=src, alt=contributor.name) + .caption + h4= contributor.name diff --git a/app/templates/contribute/contributor_signup.jade b/app/templates/contribute/contributor_signup.jade new file mode 100644 index 000000000..2b9a8f188 --- /dev/null +++ b/app/templates/contribute/contributor_signup.jade @@ -0,0 +1,5 @@ +label.checkbox(for=contributorClassID).well + input(type='checkbox', name=contributorClassID, id=contributorClassID) + span(data-i18n="contribute.#{contributorClassName}_subscribe_desc") + .saved-notification ✓ Saved + diff --git a/app/templates/contribute/contributor_signup_anonymous.jade b/app/templates/contribute/contributor_signup_anonymous.jade new file mode 100644 index 000000000..5ee89a23a --- /dev/null +++ b/app/templates/contribute/contributor_signup_anonymous.jade @@ -0,0 +1,12 @@ +if me.attributes.anonymous + div#sign-up.alert.alert-info + strong(data-i18n="contribute.alert_account_message_intro") + | Hey there! + span + span(data-i18n="contribute.alert_account_message_pref") + | To subscribe for class emails, you'll need to + a(data-toggle="coco-modal", data-target="modal/signup", data-i18n="contribute.alert_account_message_create_url") + | create an account + span + span(data-i18n="contribute.alert_account_message_suf") + | first. diff --git a/app/templates/contribute/diplomat.jade b/app/templates/contribute/diplomat.jade index 3c161e778..1150bdda7 100644 --- a/app/templates/contribute/diplomat.jade +++ b/app/templates/contribute/diplomat.jade @@ -44,51 +44,37 @@ block content | , edit it online, and submit a pull request. Also, check this box below to | keep up-to-date on new internationalization developments! - if me.attributes.anonymous - div#sign-up.alert.alert-info - strong(data-i18n="contribute.alert_account_message_intro") - | Hey there! - span - span(data-i18n="contribute.alert_account_message_pref") - | To subscribe for class emails, you'll need to - a(data-toggle="coco-modal", data-target="modal/signup", data-i18n="contribute.alert_account_message_create_url") - | create an account - span - span(data-i18n="contribute.alert_account_message_suf") - | first. + .contributor-signup-anonymous + .contributor-signup(data-contributor-class-id="translator", data-contributor-class-name="diplomat") - label.checkbox(for="translator").well - input(type='checkbox', name="translator", id="translator") - span(data-i18n="contribute.diplomat_subscribe_desc") - | Get emails about i18n developments and levels to translate. - .saved-notification ✓ Saved + h3(data-i18n="contribute.translating_diplomats") + | Our Translating Diplomats: - #Contributors - h3(data-i18n="contribute.translating_diplomats") - | Our Translating Diplomats: - ul.diplomats - li Turkish - Nazım Gediz Aydındoğmuş, cobaimelan, wakeup - li Brazilian Portuguese - Gutenberg Barros, Kieizroe, Matthew Burt, brunoporto, cassiocardoso - li Portugal Portuguese - Matthew Burt, ReiDuKuduro - li German - Dirk, faabsen, HiroP0, Anon, bkimminich - li Thai - Kamolchanok Jittrepit - li Vietnamese - An Nguyen Hoang Thien - li Dutch - Glen De Cauwsemaecker, Guido Zuidhof, Ruben Vereecken, Jasper D'haene - li Greek - Stergios - li Latin American Spanish - Jesús Ruppel, Matthew Burt, Mariano Luzza - li Spain Spanish - Matthew Burt, DanielRodriguezRivero, Anon, Pouyio - li French - Xeonarno, Elfisen, Armaldio, MartinDelille, pstweb, veritable, jaybi, xavismeh, Anon, Feugy - li Hungarian - ferpeter, csuvsaregal, atlantisguru, Anon - li Japanese - g1itch, kengos - li Chinese - Adam23, spacepope, yangxuan8282, Cheng Zheng - li Polish - Anon, Kacper Ciepielewski - li Danish - Einar Rasmussen, sorsjen, Randi Hillerøe, Anon - li Slovak - Anon - li Persian - Reza Habibi (Rehb) - li Czech - vanous - li Russian - fess89, ser-storchak, Mr A - li Ukrainian - fess89 - li Italian - flauta - li Norwegian - bardeh + //#contributor-list + // TODO: collect CodeCombat userids for these guys so we can include a tiled list + ul.diplomats + li Turkish - Nazım Gediz Aydındoğmuş, cobaimelan, wakeup + li Brazilian Portuguese - Gutenberg Barros, Kieizroe, Matthew Burt, brunoporto, cassiocardoso + li Portugal Portuguese - Matthew Burt, ReiDuKuduro + li German - Dirk, faabsen, HiroP0, Anon, bkimminich + li Thai - Kamolchanok Jittrepit + li Vietnamese - An Nguyen Hoang Thien + li Dutch - Glen De Cauwsemaecker, Guido Zuidhof, Ruben Vereecken, Jasper D'haene + li Greek - Stergios + li Latin American Spanish - Jesús Ruppel, Matthew Burt, Mariano Luzza + li Spain Spanish - Matthew Burt, DanielRodriguezRivero, Anon, Pouyio + li French - Xeonarno, Elfisen, Armaldio, MartinDelille, pstweb, veritable, jaybi, xavismeh, Anon, Feugy + li Hungarian - ferpeter, csuvsaregal, atlantisguru, Anon + li Japanese - g1itch, kengos + li Chinese - Adam23, spacepope, yangxuan8282, Cheng Zheng + li Polish - Anon, Kacper Ciepielewski + li Danish - Einar Rasmussen, sorsjen, Randi Hillerøe, Anon + li Slovak - Anon + li Persian - Reza Habibi (Rehb) + li Czech - vanous + li Russian - fess89, ser-storchak, Mr A + li Ukrainian - fess89 + li Italian - flauta + li Norwegian - bardeh div.clearfix diff --git a/app/templates/contribute/scribe.jade b/app/templates/contribute/scribe.jade index eafab49b6..92155143c 100644 --- a/app/templates/contribute/scribe.jade +++ b/app/templates/contribute/scribe.jade @@ -44,37 +44,12 @@ block content | tell us a little about yourself, your experience with programming and | what sort of things you'd like to write about. We'll go from there! - if me.attributes.anonymous - div#sign-up.alert.alert-info - strong(data-i18n="contribute.alert_account_message_intro") - | Hey there! - span - span(data-i18n="contribute.alert_account_message_pref") - | To subscribe for class emails, you'll need to - a(data-toggle="coco-modal", data-target="modal/signup", data-i18n="contribute.alert_account_message_create_url") - | create an account - span - span(data-i18n="contribute.alert_account_message_suf") - | first. + .contributor-signup-anonymous + .contributor-signup(data-contributor-class-id="article_editor", data-contributor-class-name="scribe") - label.checkbox(for="article_editor").well - input(type='checkbox', name="article_editor", id="article_editor") - span(data-i18n="contribute.scribe_subscribe_desc") - | Get emails about article writing announcements. - .saved-notification ✓ Saved - - #Contributors - h3(data-i18n="contribute.diligent_scribes") - | Our Diligent Scribes: - ul.scribes - li Ryan Faidley - li Glen De Cauwsemaecker - li Mischa Lewis-Norelle - li Tavio - li Ronnie Cheng - li engstrom - li Dman19993 - li mattinsler - + h3(data-i18n="contribute.diligent_scribes") + | Our Diligent Scribes: + + #contributor-list(data-contributor-class-name="scribe") div.clearfix diff --git a/app/views/account/settings_view.coffee b/app/views/account/settings_view.coffee index 0665a7c9c..5e1d606da 100644 --- a/app/views/account/settings_view.coffee +++ b/app/views/account/settings_view.coffee @@ -81,6 +81,7 @@ module.exports = class SettingsView extends View schema = _.cloneDeep me.schema() schema.properties = _.pick me.schema().properties, 'photoURL' schema.required = ['photoURL'] + console.log 'have data', data, 'schema', schema treemaOptions = filePath: "db/user/#{me.id}" schema: schema @@ -88,8 +89,8 @@ module.exports = class SettingsView extends View callbacks: {change: @onPictureChanged} @pictureTreema = @$el.find('#picture-treema').treema treemaOptions - @pictureTreema.build() - @pictureTreema.open() + @pictureTreema?.build() + @pictureTreema?.open() @$el.find('.gravatar-fallback').toggle not me.get 'photoURL' onPictureChanged: (e) => diff --git a/app/views/contribute/adventurer_view.coffee b/app/views/contribute/adventurer_view.coffee index cdb711e98..9428a8624 100644 --- a/app/views/contribute/adventurer_view.coffee +++ b/app/views/contribute/adventurer_view.coffee @@ -4,4 +4,5 @@ template = require 'templates/contribute/adventurer' module.exports = class AdventurerView extends ContributeClassView id: "adventurer-view" - template: template \ No newline at end of file + template: template + contributorClassName: 'adventurer' diff --git a/app/views/contribute/ambassador_view.coffee b/app/views/contribute/ambassador_view.coffee index 669951176..6ea7a68cd 100644 --- a/app/views/contribute/ambassador_view.coffee +++ b/app/views/contribute/ambassador_view.coffee @@ -5,3 +5,4 @@ template = require 'templates/contribute/ambassador' module.exports = class AmbassadorView extends ContributeClassView id: "ambassador-view" template: template + contributorClassName: 'ambassador' diff --git a/app/views/contribute/archmage_view.coffee b/app/views/contribute/archmage_view.coffee index ec18303f8..7ac7508ca 100644 --- a/app/views/contribute/archmage_view.coffee +++ b/app/views/contribute/archmage_view.coffee @@ -4,28 +4,30 @@ template = require 'templates/contribute/archmage' module.exports = class ArchmageView extends ContributeClassView id: "archmage-view" template: template + contributorClassName: 'archmage' contributors: [ - {name: "Tom Steinbrecher", avatar: "tom"} - {name: "Sébastien Moratinos", avatar: "sebastien"} - {name: "deepak1556", avatar: "deepak"} - {name: "Ronnie Cheng", avatar: "ronald"} - {name: "Chloe Fan", avatar: "chloe"} - {name: "Rachel Xiang", avatar: "rachel"} - {name: "Dan Ristic", avatar: "dan"} - {name: "Brad Dickason", avatar: "brad"} + {id: "52bfc3ecb7ec628868001297", name: "Tom Steinbrecher", github: "TomSteinbrecher"} + {id: "5272806093680c5817033f73", name: "Sébastien Moratinos", github: "smoratinos"} + {name: "deepak1556", avatar: "deepak", github: "deepak1556"} + {name: "Ronnie Cheng", avatar: "ronald", github: "rhc2104"} + {name: "Chloe Fan", avatar: "chloe", github: "chloester"} + {name: "Rachel Xiang", avatar: "rachel", github: "rdxiang"} + {name: "Dan Ristic", avatar: "dan", github: "dristic"} + {name: "Brad Dickason", avatar: "brad", github: "bdickason"} {name: "Rebecca Saines", avatar: "becca"} - {name: "Laura Watiker", avatar: "laura"} - {name: "Shiying Zheng", avatar: "shiying"} - {name: "Mischa Lewis-Norelle", avatar: "mischa"} + {name: "Laura Watiker", avatar: "laura", github: "lwatiker"} + {name: "Shiying Zheng", avatar: "shiying", github: "shiyingzheng"} + {name: "Mischa Lewis-Norelle", avatar: "mischa", github: "mlewisno"} {name: "Paul Buser", avatar: "paul"} {name: "Benjamin Stern", avatar: "ben"} {name: "Alex Cotsarelis", avatar: "alex"} {name: "Ken Stanley", avatar: "ken"} - {name: "devast8a", avatar: ""} - {name: "phansch", avatar: ""} - {name: "Zach Martin", avatar: ""} + {name: "devast8a", avatar: "", github: "devast8a"} + {name: "phansch", avatar: "", github: "phansch"} + {name: "Zach Martin", avatar: "", github: "zachster01"} {name: "David Golds", avatar: ""} - {name: "gabceb", avatar: ""} - {name: "MDP66", avatar: ""} + {name: "gabceb", avatar: "", github: "gabceb"} + {name: "MDP66", avatar: "", github: "MDP66"} + {name: "Alexandru Caciulescu", avatar: "", github: "Darredevil"} ] diff --git a/app/views/contribute/artisan_view.coffee b/app/views/contribute/artisan_view.coffee index b7d184d57..dbad64902 100644 --- a/app/views/contribute/artisan_view.coffee +++ b/app/views/contribute/artisan_view.coffee @@ -5,10 +5,11 @@ template = require 'templates/contribute/artisan' module.exports = class ArtisanView extends ContributeClassView id: "artisan-view" template: template + contributorClassName: 'artisan' contributors: [ {name: "Sootn", avatar: ""} - {name: "Zach Martin", avatar: ""} + {name: "Zach Martin", avatar: "", github: "zachster01"} {name: "Aftermath", avatar: ""} {name: "mcdavid1991", avatar: ""} {name: "dwhittaker", avatar: ""} @@ -19,6 +20,6 @@ module.exports = class ArtisanView extends ContributeClassView {name: "Axandre Oge", avatar: "axandre"} {name: "Katharine Chan", avatar: "katharine"} {name: "Derek Wong", avatar: "derek"} - {name: "Alexandru Caciulescu", avatar: ""} - {name: "Prabh Simran Singh Baweja", avatar: ""} + {name: "Alexandru Caciulescu", avatar: "", github: "Darredevil"} + {name: "Prabh Simran Singh Baweja", avatar: "", github: "prabh27"} ] diff --git a/app/views/contribute/contribute_class_view.coffee b/app/views/contribute/contribute_class_view.coffee index 5442ff719..246499694 100644 --- a/app/views/contribute/contribute_class_view.coffee +++ b/app/views/contribute/contribute_class_view.coffee @@ -1,6 +1,9 @@ SignupModalView = require 'views/modal/signup_modal' View = require 'views/kinds/RootView' {me} = require('lib/auth') +contributorSignupAnonymousTemplate = require 'templates/contribute/contributor_signup_anonymous' +contributorSignupTemplate = require 'templates/contribute/contributor_signup' +contributorListTemplate = require 'templates/contribute/contributor_list' module.exports = class ContributeClassView extends View navPrefix: '/contribute' @@ -16,6 +19,12 @@ module.exports = class ContributeClassView extends View afterRender: -> super() + @$el.find('.contributor-signup-anonymous').replaceWith(contributorSignupAnonymousTemplate(me: me)) + @$el.find('.contributor-signup').each -> + context = me: me, contributorClassID: $(@).data('contributor-class-id'), contributorClassName: $(@).data('contributor-class-name') + $(@).replaceWith(contributorSignupTemplate(context)) + @$el.find('#contributor-list').replaceWith(contributorListTemplate(contributors: @contributors, contributorClassName: @contributorClassName)) + checkboxes = @$el.find('input[type="checkbox"]').toArray() _.forEach checkboxes, (el) -> el = $(el) diff --git a/app/views/contribute/counselor_view.coffee b/app/views/contribute/counselor_view.coffee index b589c4ed9..6e5a6be2c 100644 --- a/app/views/contribute/counselor_view.coffee +++ b/app/views/contribute/counselor_view.coffee @@ -5,3 +5,4 @@ template = require 'templates/contribute/counselor' module.exports = class CounselorView extends ContributeClassView id: "counselor-view" template: template + contributorClassName: 'counselor' diff --git a/app/views/contribute/diplomat_view.coffee b/app/views/contribute/diplomat_view.coffee index 6b159bfed..0769af517 100644 --- a/app/views/contribute/diplomat_view.coffee +++ b/app/views/contribute/diplomat_view.coffee @@ -5,3 +5,4 @@ template = require 'templates/contribute/diplomat' module.exports = class DiplomatView extends ContributeClassView id: "diplomat-view" template: template + contributorClassName: 'diplomat' diff --git a/app/views/contribute/scribe_view.coffee b/app/views/contribute/scribe_view.coffee index 2aedbbb98..7f87a3275 100644 --- a/app/views/contribute/scribe_view.coffee +++ b/app/views/contribute/scribe_view.coffee @@ -5,3 +5,14 @@ template = require 'templates/contribute/scribe' module.exports = class ScribeView extends ContributeClassView id: "scribe-view" template: template + contributorClassName: 'scribe' + + contributors: [ + {name: "Ryan Faidley"} + {name: "Mischa Lewis-Norelle", github: "mlewisno"} + {name: "Tavio"} + {name: "Ronnie Cheng", github: "rhc2104"} + {name: "engstrom"} + {name: "Dman19993"} + {name: "mattinsler"} + ] diff --git a/server/users/user_handler.coffee b/server/users/user_handler.coffee index 9023e3d94..cbdb44af8 100644 --- a/server/users/user_handler.coffee +++ b/server/users/user_handler.coffee @@ -198,7 +198,10 @@ UserHandler = class UserHandler extends Handler @modelClass.findById(id).exec (err, document) => return @sendDatabaseError(res, err) if err photoURL = document?.get('photoURL') - photoURL ||= @buildGravatarURL document + if photoURL + photoURL = "/file/#{photoURL}" + else + photoURL = @buildGravatarURL document, req.query.s, req.query.fallback res.redirect photoURL res.end() @@ -239,10 +242,11 @@ UserHandler = class UserHandler extends Handler obj.jobProfile = _.pick obj.jobProfile, subfields obj - buildGravatarURL: (user) -> + buildGravatarURL: (user, size, fallback) -> emailHash = @buildEmailHash user - defaultAvatar = "http://codecombat.com/file/db/thang.type/52a00d55cf1818f2be00000b/portrait.png" - "https://www.gravatar.com/avatar/#{emailHash}?default=#{defaultAvatar}" + fallback ?= "http://codecombat.com/file/db/thang.type/52a00d55cf1818f2be00000b/portrait.png" + fallback = "http://codecombat.com#{fallback}" unless /^http/.test fallback + "https://www.gravatar.com/avatar/#{emailHash}?s=#{size}&default=#{fallback}" buildEmailHash: (user) -> # emailHash is used by gravatar