<commit_before>// Project
// -------
// Model. A single TileMill map project. Describes an MML JSON map object that
// can be used by `carto` to render a map.
model = Backbone.Model.extend({
    schema: {
        'type': 'object',
        'properties': {
            // Mapnik-specific properties.
            'srs': {
                'type': 'string',
                'required': true
            },
            'Stylesheet': {
                'type': ['object', 'array'],
                'required': true
            },
            'Layer': {
                'type': ['object', 'array'],
                'required': true
            },

            // TileMill-specific properties. @TODO these need a home, see
            // https://github.com/mapbox/tilelive-mapnik/issues/4
            'format': {
                'type': 'string',
                'enum': ['png', 'png24', 'png8', 'jpeg80', 'jpeg85', 'jpeg90', 'jpeg95']
            },
            'interactivity': {
                'type': ['object', 'boolean']
            },

            // TileJSON properties.
            'name':        { 'type': 'string' },
            'description': { 'type': 'string' },
            'version':     { 'type': 'string' },
            'attribution': { 'type': 'string' },
            'legend':      { 'type': 'string' },
            'minzoom': {
                'minimum': 0,
                'maximum': 22,
                'type': 'integer'
            },
            'maxzoom': {
                'minimum': 0,
                'maximum': 22,
                'type': 'integer'
            },
            'bounds': {
                'type': 'array',
                'items': { 'type': 'number' }
            },
            'center': {
                'type': 'array',
                'items': { 'type': 'number' }
            },

            // Non-stored properties.
            // @TODO make this writable at some point
            'scheme': {
                'type': 'string',
                'ignore': true
            },
            // @TODO make this writable at some point
            'formatter': {
                'type': 'string',
                'ignore': true
            },
            'tilejson': {
                'type': 'string',
                'ignore': true
            },
            'tiles': {
                'type': 'array',
                'required': true,
                'items': { 'type': 'string' },
                'ignore': true
            },
            'grids': {
                'type': 'array',
                'items': { 'type': 'string' },
                'ignore': true
            },
            '_updated': {
                'type': 'integer',
                'description': 'Last update time of project',
                'ignore': true
            },
            'id': {
                'type': 'string',
                'required': true,
                'pattern': '^[A-Za-z0-9\-_]+$',
                'title': 'Name',
                'description': 'Name may include alphanumeric characters, dashes and underscores.',
                'ignore': true
            }
        }
    },
    STYLESHEET_DEFAULT: [{
        id: 'style.mss',
        data: 'Map {\n'
            + '  background-color: #fff;\n'
            + '}\n\n'
            + '#world {\n'
            + '  polygon-fill: #eee;\n'
            + '  line-color: #ccc;\n'
            + '  line-width: 0.5;\n'
            + '}'
    }],
    LAYER_DEFAULT: [{
        id: 'world',
        name: 'world',
        srs: '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 '
        + '+lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs +over',
        geometry: 'polygon',
        Datasource: {
            file: 'http://tilemill-data.s3.amazonaws.com/world_borders_merc.zip',
            type: 'shape'
        }
    }],
    defaults: {
        'bounds': [-180,-90,180,90],
        'center': [0,0,2],
        'format': 'png',
        'interactivity': false,
        'minzoom': 0,
        'maxzoom': 22,
        'srs': '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 '
            + '+lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs +over',
        'Stylesheet': [],
        'Layer': []
    },
    // Set model options to `this.options`.
    initialize: function(attributes, options) {
        this.options = options;
    },
    // Custom setDefaults() method for creating a project with default layers,
    // stylesheets, etc. Note that we do not use Backbone native initialize()
    // or defaults(), both of which make default values far pervasive than the
    // expected use here.
    setDefaults: function() {
        var template = {};
        !this.get('Stylesheet').length && (template.Stylesheet = this.STYLESHEET_DEFAULT);
        !this.get('Layer').length && (template.Layer = this.LAYER_DEFAULT);
        this.set(this.parse(template), { silent: true });
    },
    // Instantiate collections from arrays.
    parse: function(resp) {
        resp.Stylesheet && (resp.Stylesheet = new models.Stylesheets(
            resp.Stylesheet,
            {parent: this}
        ));
        resp.Layer && (resp.Layer = new models.Layers(
            resp.Layer,
            {parent: this}
        ));
        return resp;
    },
    url: function() {
        return 'api/Project/' + this.id;
    },
    // Adds id uniqueness checking to validate.
    validate: function(attr) {
        if (attr.id &&
            this.collection &&
            this.collection.get(attr.id) &&
            this.collection.get(attr.id) !== this)
                return new Error(_('Project "<%=id%>" already exists.').template(attr));
        return this.validateAttributes(attr);
    },
    // Custom validation method that allows for asynchronous processing.
    // Expects options.success and options.error callbacks to be consistent
    // with other Backbone methods.
    validateAsync: function(attributes, options) {
        // If client-side, pass-through.
        if (!Bones.server) return options.success(this, null);

        var carto = require('carto'),
        // var carto = require('tilelive-mapnik/node_modules/carto'),
            mapnik = require('tilelive-mapnik/node_modules/mapnik'),
            stylesheets = this.get('Stylesheet'),
            env = {
                returnErrors: true,
                errors: [],
                validation_data: {
                    fonts: mapnik.fonts()
                },
                deferred_externals: [],
                only_validate: true,
                effects: []
            };
<commit_msg>Add validation that interactivity layer exists.<commit_after>// Project
// -------
// Model. A single TileMill map project. Describes an MML JSON map object that
// can be used by `carto` to render a map.
model = Backbone.Model.extend({
    schema: {
        'type': 'object',
        'properties': {
            // Mapnik-specific properties.
            'srs': {
                'type': 'string',
                'required': true
            },
            'Stylesheet': {
                'type': ['object', 'array'],
                'required': true
            },
            'Layer': {
                'type': ['object', 'array'],
                'required': true
            },

            // TileMill-specific properties. @TODO these need a home, see
            // https://github.com/mapbox/tilelive-mapnik/issues/4
            'format': {
                'type': 'string',
                'enum': ['png', 'png24', 'png8', 'jpeg80', 'jpeg85', 'jpeg90', 'jpeg95']
            },
            'interactivity': {
                'type': ['object', 'boolean']
            },

            // TileJSON properties.
            'name':        { 'type': 'string' },
            'description': { 'type': 'string' },
            'version':     { 'type': 'string' },
            'attribution': { 'type': 'string' },
            'legend':      { 'type': 'string' },
            'minzoom': {
                'minimum': 0,
                'maximum': 22,
                'type': 'integer'
            },
            'maxzoom': {
                'minimum': 0,
                'maximum': 22,
                'type': 'integer'
            },
            'bounds': {
                'type': 'array',
                'items': { 'type': 'number' }
            },
            'center': {
                'type': 'array',
                'items': { 'type': 'number' }
            },

            // Non-stored properties.
            // @TODO make this writable at some point
            'scheme': {
                'type': 'string',
                'ignore': true
            },
            // @TODO make this writable at some point
            'formatter': {
                'type': 'string',
                'ignore': true
            },
            'tilejson': {
                'type': 'string',
                'ignore': true
            },
            'tiles': {
                'type': 'array',
                'required': true,
                'items': { 'type': 'string' },
                'ignore': true
            },
            'grids': {
                'type': 'array',
                'items': { 'type': 'string' },
                'ignore': true
            },
            '_updated': {
                'type': 'integer',
                'description': 'Last update time of project',
                'ignore': true
            },
            'id': {
                'type': 'string',
                'required': true,
                'pattern': '^[A-Za-z0-9\-_]+$',
                'title': 'Name',
                'description': 'Name may include alphanumeric characters, dashes and underscores.',
                'ignore': true
            }
        }
    },
    STYLESHEET_DEFAULT: [{
        id: 'style.mss',
        data: 'Map {\n'
            + '  background-color: #fff;\n'
            + '}\n\n'
            + '#world {\n'
            + '  polygon-fill: #eee;\n'
            + '  line-color: #ccc;\n'
            + '  line-width: 0.5;\n'
            + '}'
    }],
    LAYER_DEFAULT: [{
        id: 'world',
        name: 'world',
        srs: '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 '
        + '+lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs +over',
        geometry: 'polygon',
        Datasource: {
            file: 'http://tilemill-data.s3.amazonaws.com/world_borders_merc.zip',
            type: 'shape'
        }
    }],
    defaults: {
        'bounds': [-180,-90,180,90],
        'center': [0,0,2],
        'format': 'png',
        'interactivity': false,
        'minzoom': 0,
        'maxzoom': 22,
        'srs': '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 '
            + '+lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs +over',
        'Stylesheet': [],
        'Layer': []
    },
    // Set model options to `this.options`.
    initialize: function(attributes, options) {
        this.options = options;
    },
    // Custom setDefaults() method for creating a project with default layers,
    // stylesheets, etc. Note that we do not use Backbone native initialize()
    // or defaults(), both of which make default values far pervasive than the
    // expected use here.
    setDefaults: function() {
        var template = {};
        !this.get('Stylesheet').length && (template.Stylesheet = this.STYLESHEET_DEFAULT);
        !this.get('Layer').length && (template.Layer = this.LAYER_DEFAULT);
        this.set(this.parse(template), { silent: true });
    },
    // Instantiate collections from arrays.
    parse: function(resp) {
        resp.Stylesheet && (resp.Stylesheet = new models.Stylesheets(
            resp.Stylesheet,
            {parent: this}
        ));
        resp.Layer && (resp.Layer = new models.Layers(
            resp.Layer,
            {parent: this}
        ));
        return resp;
    },
    url: function() {
        return 'api/Project/' + this.id;
    },
    // Adds:
    // - id uniqueness checking.
    // - interactivity check to ensure the referenced layer exists.
    validate: function(attr) {
        if (attr.id &&
            this.collection &&
            this.collection.get(attr.id) &&
            this.collection.get(attr.id) !== this)
                return new Error(_('Project "<%=id%>" already exists.').template(attr));
        if (attr.interactivity &&
            attr.interactivity.layer &&
            attr.Layer &&
            !_(attr.Layer).chain().pluck('id').include(attr.interactivity.layer).value())
                return new Error(_('Interactivity layer "<%=obj%>" does not exist.').template(attr.interactivity.layer));
        return this.validateAttributes(attr);
    },
    // Custom validation method that allows for asynchronous processing.
    // Expects options.success and options.error callbacks to be consistent
    // with other Backbone methods.
    validateAsync: function(attributes, options) {
        // If client-side, pass-through.
        if (!Bones.server) return options.success(this, null);

        var carto = require('carto'),
        // var carto = require('tilelive-mapnik/node_modules/carto'),
            mapnik = require('tilelive-mapnik/node_modules/mapnik'),
            stylesheets = this.get('Stylesheet'),
            env = {
                returnErrors: true,
                errors: [],
                validation_data: {
                    fonts: mapnik.fonts()
                },
                deferred_externals: [],
                only_validate: true,
                effects: []
            };

{'fraction_non_alphanumeric': 0.1521931767833407, 'fraction_numerical': 0.013439669177374096, 'mean_word_length': 1.393179006891677, 'pattern_counts': {'":': 0, '<': 6, '<?xml version=': 0, '>': 6, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2444627', 'n_tokens_mistral': 4002, 'n_tokens_neox': 3768, 'n_words': 876}